Merge pull request #1936 from pijyoi/empty_qpolygonf
Handle empty QPolygonF
This commit is contained in:
commit
2d90e54441
@ -17,12 +17,10 @@ import pyqtgraph.functions as fn
|
||||
import itertools
|
||||
import argparse
|
||||
|
||||
if QT_LIB == 'PySide2':
|
||||
wrapinstance = pg.Qt.shiboken2.wrapInstance
|
||||
elif QT_LIB == 'PySide6':
|
||||
wrapinstance = pg.Qt.shiboken6.wrapInstance
|
||||
elif QT_LIB in ['PyQt5', 'PyQt6']:
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
wrapinstance = pg.Qt.sip.wrapinstance
|
||||
else:
|
||||
wrapinstance = pg.Qt.shiboken.wrapInstance
|
||||
|
||||
# defaults here result in the same configuration as the original PlotSpeedTest
|
||||
parser = argparse.ArgumentParser()
|
||||
|
@ -152,7 +152,12 @@ if QT_LIB == PYQT5:
|
||||
_copy_attrs(PyQt5.QtGui, QtGui)
|
||||
_copy_attrs(PyQt5.QtWidgets, QtWidgets)
|
||||
|
||||
from PyQt5 import sip, uic
|
||||
try:
|
||||
from PyQt5 import sip
|
||||
except ImportError:
|
||||
# some Linux distros package it this way (e.g. Ubuntu)
|
||||
import sip
|
||||
from PyQt5 import uic
|
||||
|
||||
try:
|
||||
from PyQt5 import QtSvg
|
||||
@ -203,8 +208,7 @@ elif QT_LIB == PYSIDE2:
|
||||
except ImportError as err:
|
||||
QtTest = FailedImport(err)
|
||||
|
||||
import shiboken2
|
||||
isQObjectAlive = shiboken2.isValid
|
||||
import shiboken2 as shiboken
|
||||
import PySide2
|
||||
VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__
|
||||
elif QT_LIB == PYSIDE6:
|
||||
@ -226,8 +230,7 @@ elif QT_LIB == PYSIDE6:
|
||||
except ImportError as err:
|
||||
QtTest = FailedImport(err)
|
||||
|
||||
import shiboken6
|
||||
isQObjectAlive = shiboken6.isValid
|
||||
import shiboken6 as shiboken
|
||||
import PySide6
|
||||
VERSION_INFO = 'PySide6 ' + PySide6.__version__ + ' Qt ' + QtCore.__version__
|
||||
|
||||
@ -313,6 +316,7 @@ if QT_LIB in [PYQT6, PYSIDE6]:
|
||||
if QT_LIB in [PYSIDE2, PYSIDE6]:
|
||||
QtVersion = QtCore.__version__
|
||||
loadUiType = _loadUiType
|
||||
isQObjectAlive = shiboken.isValid
|
||||
|
||||
# PySide does not implement qWait
|
||||
if not isinstance(QtTest, FailedImport):
|
||||
|
@ -98,7 +98,7 @@ class ImageExporter(Exporter):
|
||||
painter.end()
|
||||
|
||||
if self.params['invertValue']:
|
||||
bg = fn.qimage_to_ndarray(self.png)
|
||||
bg = fn.ndarray_from_qimage(self.png)
|
||||
if sys.byteorder == 'little':
|
||||
cv = slice(0, 3)
|
||||
else:
|
||||
|
@ -42,7 +42,7 @@ __all__ = [
|
||||
'makeRGBA', 'makeARGB',
|
||||
# 'try_fastpath_argb', 'ndarray_to_qimage',
|
||||
'makeQImage',
|
||||
# 'qimage_to_ndarray',
|
||||
# 'ndarray_from_qimage',
|
||||
'imageToArray', 'colorToAlpha',
|
||||
'gaussianFilter', 'downsample', 'arrayToQPath',
|
||||
# 'ndarray_from_qpolygonf', 'create_qpolygonf', 'arrayToQPolygonF',
|
||||
@ -1682,24 +1682,28 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
||||
return ndarray_to_qimage(imgData, imgFormat)
|
||||
|
||||
|
||||
def qimage_to_ndarray(qimg):
|
||||
def ndarray_from_qimage(qimg):
|
||||
img_ptr = qimg.bits()
|
||||
|
||||
if hasattr(img_ptr, 'setsize'): # PyQt sip.voidptr
|
||||
if img_ptr is None:
|
||||
raise ValueError("Null QImage not supported")
|
||||
|
||||
h, w = qimg.height(), qimg.width()
|
||||
bpl = qimg.bytesPerLine()
|
||||
depth = qimg.depth()
|
||||
logical_bpl = w * depth // 8
|
||||
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
# sizeInBytes() was introduced in Qt 5.10
|
||||
# however PyQt5 5.12 will fail with:
|
||||
# "TypeError: QImage.sizeInBytes() is a private method"
|
||||
# note that sizeInBytes() works fine with:
|
||||
# PyQt5 5.15, PySide2 5.12, PySide2 5.15
|
||||
try:
|
||||
# 64-bits size
|
||||
nbytes = qimg.sizeInBytes()
|
||||
except (TypeError, AttributeError):
|
||||
# 32-bits size
|
||||
nbytes = qimg.byteCount()
|
||||
img_ptr.setsize(nbytes)
|
||||
img_ptr.setsize(h * bpl)
|
||||
|
||||
memory = np.frombuffer(img_ptr, dtype=np.ubyte).reshape((h, bpl))
|
||||
memory = memory[:, :logical_bpl]
|
||||
|
||||
depth = qimg.depth()
|
||||
if depth in (8, 24, 32):
|
||||
dtype = np.uint8
|
||||
nchan = depth // 8
|
||||
@ -1708,10 +1712,12 @@ def qimage_to_ndarray(qimg):
|
||||
nchan = depth // 16
|
||||
else:
|
||||
raise ValueError("Unsupported Image Type")
|
||||
shape = qimg.height(), qimg.width()
|
||||
|
||||
shape = h, w
|
||||
if nchan != 1:
|
||||
shape = shape + (nchan,)
|
||||
return np.frombuffer(img_ptr, dtype=dtype).reshape(shape)
|
||||
arr = memory.view(dtype).reshape(shape)
|
||||
return arr
|
||||
|
||||
|
||||
def imageToArray(img, copy=False, transpose=True):
|
||||
@ -1721,7 +1727,7 @@ def imageToArray(img, copy=False, transpose=True):
|
||||
the QImage is collected before the array, there may be trouble).
|
||||
The array will have shape (width, height, (b,g,r,a)).
|
||||
"""
|
||||
arr = qimage_to_ndarray(img)
|
||||
arr = ndarray_from_qimage(img)
|
||||
|
||||
fmt = img.format()
|
||||
if fmt == img.Format.Format_RGB32:
|
||||
@ -2060,25 +2066,25 @@ def arrayToQPath(x, y, connect='all', finiteCheck=True):
|
||||
|
||||
def ndarray_from_qpolygonf(polyline):
|
||||
nbytes = 2 * len(polyline) * 8
|
||||
if QT_LIB == "PySide2":
|
||||
buffer = Qt.shiboken2.VoidPtr(polyline.data(), nbytes, True)
|
||||
elif QT_LIB == "PySide6":
|
||||
buffer = Qt.shiboken6.VoidPtr(polyline.data(), nbytes, True)
|
||||
else:
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
buffer = polyline.data()
|
||||
if buffer is None:
|
||||
buffer = Qt.sip.voidptr(0)
|
||||
buffer.setsize(nbytes)
|
||||
else:
|
||||
ptr = polyline.data()
|
||||
if ptr is None:
|
||||
ptr = 0
|
||||
buffer = Qt.shiboken.VoidPtr(ptr, nbytes, True)
|
||||
memory = np.frombuffer(buffer, np.double).reshape((-1, 2))
|
||||
return memory
|
||||
|
||||
def create_qpolygonf(size):
|
||||
if QtVersion.startswith("5"):
|
||||
polyline = QtGui.QPolygonF(size)
|
||||
polyline = QtGui.QPolygonF()
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
polyline.fill(QtCore.QPointF(), size)
|
||||
else:
|
||||
polyline = QtGui.QPolygonF()
|
||||
if QT_LIB == "PySide6":
|
||||
polyline.resize(size)
|
||||
else:
|
||||
polyline.fill(QtCore.QPointF(), size)
|
||||
polyline.resize(size)
|
||||
return polyline
|
||||
|
||||
def arrayToQPolygonF(x, y):
|
||||
|
@ -1299,7 +1299,7 @@ class ROI(GraphicsObject):
|
||||
p.drawPath(shape)
|
||||
p.end()
|
||||
cidx = 0 if sys.byteorder == 'little' else 3
|
||||
mask = fn.qimage_to_ndarray(im)[...,cidx].T
|
||||
mask = fn.ndarray_from_qimage(im)[...,cidx].T
|
||||
return mask.astype(float) / 255
|
||||
|
||||
def getGlobalTransform(self, relativeTo=None):
|
||||
|
@ -4,6 +4,7 @@ import itertools
|
||||
import math
|
||||
import numpy as np
|
||||
import weakref
|
||||
from .. import Qt
|
||||
from ..Qt import QtGui, QtCore, QT_LIB
|
||||
from ..Point import Point
|
||||
from .. import functions as fn
|
||||
@ -13,13 +14,6 @@ from .. import getConfigOption
|
||||
from collections import OrderedDict
|
||||
from .. import debug
|
||||
|
||||
if QT_LIB == 'PySide2':
|
||||
from shiboken2 import wrapInstance
|
||||
elif QT_LIB == 'PySide6':
|
||||
from shiboken6 import wrapInstance
|
||||
elif QT_LIB in ['PyQt5', 'PyQt6']:
|
||||
from ..Qt import sip
|
||||
|
||||
__all__ = ['ScatterPlotItem', 'SpotItem']
|
||||
|
||||
|
||||
@ -167,11 +161,11 @@ class PixmapFragments:
|
||||
# instances into a contiguous array, in order to call the underlying C++ native API.
|
||||
self.arr = np.empty((size, 10), dtype=np.float64)
|
||||
if QT_LIB.startswith('PyQt'):
|
||||
self.ptrs = list(map(sip.wrapinstance,
|
||||
self.ptrs = list(map(Qt.sip.wrapinstance,
|
||||
itertools.count(self.arr.ctypes.data, self.arr.strides[0]),
|
||||
itertools.repeat(QtGui.QPainter.PixmapFragment, self.arr.shape[0])))
|
||||
else:
|
||||
self.ptrs = wrapInstance(self.arr.ctypes.data, QtGui.QPainter.PixmapFragment)
|
||||
self.ptrs = Qt.shiboken.wrapInstance(self.arr.ctypes.data, QtGui.QPainter.PixmapFragment)
|
||||
|
||||
def array(self, size):
|
||||
if size > self.arr.shape[0]:
|
||||
|
@ -23,6 +23,6 @@ def test_ImageExporter_toBytes():
|
||||
exp = ImageExporter(p.getPlotItem())
|
||||
qimg = exp.export(toBytes=True)
|
||||
qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
|
||||
data = fn.qimage_to_ndarray(qimg)
|
||||
data = fn.ndarray_from_qimage(qimg)
|
||||
black = (0, 0, 0, 255)
|
||||
assert np.all(data == black), "Exported image should be entirely black."
|
||||
|
@ -77,7 +77,7 @@ def getImageFromWidget(widget):
|
||||
painter.end()
|
||||
|
||||
qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
|
||||
return fn.qimage_to_ndarray(qimg).copy()
|
||||
return fn.ndarray_from_qimage(qimg).copy()
|
||||
|
||||
|
||||
def assertImageApproved(image, standardFile, message=None, **kwargs):
|
||||
@ -126,7 +126,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
|
||||
else:
|
||||
qimg = QtGui.QImage(stdFileName)
|
||||
qimg = qimg.convertToFormat(QtGui.QImage.Format.Format_RGBA8888)
|
||||
stdImage = fn.qimage_to_ndarray(qimg).copy()
|
||||
stdImage = fn.ndarray_from_qimage(qimg).copy()
|
||||
del qimg
|
||||
|
||||
# If the test image does not match, then we go to audit if requested.
|
||||
|
@ -9,6 +9,7 @@ import pytest
|
||||
from numpy.testing import assert_array_almost_equal
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtGui
|
||||
|
||||
np.random.seed(12345)
|
||||
|
||||
@ -333,3 +334,31 @@ def test_arrayToQPath(xs, ys, connect, expected):
|
||||
continue
|
||||
element = path.elementAt(i)
|
||||
assert eq(expected[i], (element.type, element.x, element.y))
|
||||
|
||||
|
||||
def test_ndarray_from_qpolygonf():
|
||||
# test that we get an empty ndarray from an empty QPolygonF
|
||||
poly = pg.functions.create_qpolygonf(0)
|
||||
arr = pg.functions.ndarray_from_qpolygonf(poly)
|
||||
assert isinstance(arr, np.ndarray)
|
||||
|
||||
|
||||
def test_ndarray_from_qimage():
|
||||
# for QImages created w/o specifying bytesPerLine, Qt will pad
|
||||
# each line to a multiple of 4-bytes.
|
||||
# test that we can handle such QImages.
|
||||
h = 10
|
||||
|
||||
fmt = QtGui.QImage.Format.Format_RGB888
|
||||
for w in [5, 6, 7, 8]:
|
||||
qimg = QtGui.QImage(w, h, fmt)
|
||||
qimg.fill(0)
|
||||
arr = pg.functions.ndarray_from_qimage(qimg)
|
||||
assert arr.shape == (h, w, 3)
|
||||
|
||||
fmt = QtGui.QImage.Format.Format_Grayscale8
|
||||
for w in [5, 6, 7, 8]:
|
||||
qimg = QtGui.QImage(w, h, fmt)
|
||||
qimg.fill(0)
|
||||
arr = pg.functions.ndarray_from_qimage(qimg)
|
||||
assert arr.shape == (h, w)
|
||||
|
28
tests/test_qimage_writethru.py
Normal file
28
tests/test_qimage_writethru.py
Normal file
@ -0,0 +1,28 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
def test_qimage_writethrough():
|
||||
w, h = 256, 256
|
||||
backstore = np.ones((h, w), dtype=np.uint8)
|
||||
ptr0 = backstore.ctypes.data
|
||||
fmt = pg.Qt.QtGui.QImage.Format.Format_Grayscale8
|
||||
qimg = pg.functions.ndarray_to_qimage(backstore, fmt)
|
||||
|
||||
def get_pointer(obj):
|
||||
if hasattr(obj, 'setsize'):
|
||||
return int(obj)
|
||||
else:
|
||||
return np.frombuffer(obj, dtype=np.uint8).ctypes.data
|
||||
|
||||
# test that QImage is using the provided buffer (i.e. zero-copy)
|
||||
ptr1 = get_pointer(qimg.constBits())
|
||||
assert ptr0 == ptr1
|
||||
|
||||
# test that QImage is not const (i.e. no COW)
|
||||
# if QImage is const, then bits() returns a copy
|
||||
ptr2 = get_pointer(qimg.bits())
|
||||
assert ptr1 == ptr2
|
||||
|
||||
# test that data gets written through to provided buffer
|
||||
qimg.fill(0)
|
||||
assert np.all(backstore == 0)
|
Loading…
Reference in New Issue
Block a user