Merge pull request #1936 from pijyoi/empty_qpolygonf

Handle empty QPolygonF
This commit is contained in:
Ogi Moore 2021-07-31 07:30:20 -07:00 committed by GitHub
commit 2d90e54441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 109 additions and 50 deletions

View File

@ -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()

View File

@ -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):

View File

@ -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:

View File

@ -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)
else:
polyline = QtGui.QPolygonF()
if QT_LIB == "PySide6":
polyline.resize(size)
else:
if QT_LIB.startswith('PyQt'):
polyline.fill(QtCore.QPointF(), size)
else:
polyline.resize(size)
return polyline
def arrayToQPolygonF(x, y):

View File

@ -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):

View File

@ -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]:

View File

@ -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."

View File

@ -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.

View File

@ -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)

View 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)