Uploaded image for project: 'Qt for Python'
  1. Qt for Python
  2. PYSIDE-1924

Allow QPainter draw methods to accept pointers to structured numpy arrays

    XMLWordPrintable

Details

    • Suggestion
    • Resolution: Fixed
    • Not Evaluated
    • 6.5.0, 6.4.3
    • 6.2.4
    • PySide, Shiboken
    • None
    • All
    • eb9f91af7 (dev), bc6db6827 (6.4)

    Description

      Greeting PySide developers,

      I am opening this issue as a feature request on behalf of pyqtgraph to expose more of the QPainter draw methods that accept pointers, so we can pass NumPy arrays to them in a more direct fashion. The overloaded method signature we are most interested in is QPainter::drawLines(const QLineF *lines int lineCount)

      https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines

      QPainter::draw methods are highly performance-sensitive operations in pyqtgraph. Given that we are working from structured NumPy arrays the overwhelming majority of the time, being able to leverage that to minimize iteration or casting would be something that we would find highly desirable.

      Hopefully, the following example will highlight our intent.

      import numpy as np
      from PySide6 import QtCore, QtGui, QtWidgets, shiboken6
      import itertools
      import sys
      
      app = QtWidgets.QApplication([])
      # array makeup [[x1, y1, x2, y2]]
      lines = np.array([
      [0, 0, 0, 10],
      [0, 10, 10, 0],
      [10, 0, 20, 10]], dtype=np.float64
      )
      
      qimg = QtGui.QImage(20, 20, QtGui.QImage.Format.Format_RGB32)
      qimg.fill(0)
      painter = QtGui.QPainter(qimg)
      painter.setPen(QtCore.Qt.GlobalColor.cyan)
      
      # # desired implementation [https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-4]
      # ptr = shiboken6.wrapInstance(lines, lines.ctypes.data, QtCore.QLineF)
      # painter.drawLines(ptr, lines.shape[0])
      
      # alternative desired implementation
      # ptr = shiboken6.VoidPtr(lines)
      # painter.drawLines(ptr, lines.shape[0])
      
      # current implementation [https://doc-snapshots.qt.io/qt6-dev/qpainter.html#drawLines-5]
      ptr = list(map(shiboken6.wrapinstance,
      itertools.count(lines.ctypes.data, lines.strides[0]),
      itertools.repeat(QtCore.QLineF, lines.shape[0])))
      painter.drawLines(ptr)
      
      painter.end()
      qimg.save('drawLines.png')
      

      Oddly enough, PySide bindings do offer this functionality with QPainter::drawPixmapFragments, this was discovered somewhat by accident. I am unsure if this was intentional or not, but we had some significant performance improvements by being able to pass NumPy arrays more directly by knowing the layout is of the PixmapFragment object, see this example

      import numpy as np
      from PySide6 import QtCore, QtGui, QtWidgets, shiboken6
      import itertools
      
      app = QtWidgets.QApplication([])
      # make the pixmap
      pix = QtGui.QPixmap(51, 51)
      pix.fill(0)
      painter = QtGui.QPainter(pix)
      painter.setPen(QtCore.Qt.GlobalColor.cyan)
      painter.drawEllipse(0, 0, 50, 50)
      painter.end()
      
      # create numpy array representing fragments
      fieldnames = ['x', 'y', 'sourceLeft', 'sourceTop', 'width', 'height', 'scaleX', 'scaleY', 'rotation', 'opacity']
      frags_array = np.zeros(3, dtype=[(name, 'f8') for name in fieldnames])
      frags_array['sourceLeft'] = 0
      frags_array['sourceTop'] = 0
      frags_array['width'] = 51
      frags_array['height'] = 51
      frags_array['scaleX'] = 1.0
      frags_array['scaleY'] = 1.0
      frags_array['rotation'] = 0.0
      frags_array['opacity'] = 1.0
      
      frags_array['x'] = [50, 100, 150]
      frags_array['y'] = [50, 100, 150]
      # current implementation
      frag_ptr = shiboken6.wrapInstance(frags_array.ctypes.data, QtGui.QPainter.PixmapFragment)
      painter.drawPixmapFragments(frag_ptr, frags_array.size, pix)
      painter.end()
      

      If this is something PySide developers would be interested in supporting, there are other draw methods that we would benefit from having this kind of ability to pass pointers to structured NumPy arrays.

      QPainter::drawConvexPolygon(const QPointF *points, int pointCount)
      QPainter::drawPoints(const QPointF *points, int pointCount)
      QPainter::drawPolygon(const QPointF *points, int pointCount, Qt::FillRule fillRule = Qt::OddEvenFill)
      QPainter::drawRects(const QRectF *rectangles, int rectCount)
      QPainter::drawPolyline(const QPointF *points, int pointCount)
      

      We do these kinds of operations in our construction of QPolygonF objects from NumPy arrays, which we found to be a much faster method of constructing QPainterPath than trying to go via the QDataStream >> QPainterPath method.

      x = np.arange(100)
      y = np.ones(100)
      size = x.size
      polyline = QtGui.QPolygonF()
      polyline.resize(size)
      nbytes = 2 * len(polyline) * 8
      ptr = polyline.data()
      buffer = shiboken6.VoidPtr(ptr, nbytes, True)
      memory = np.frombuffer(buffer, np.double).reshape((-1, 2))
      memory[:, 0] = x
      memory[:, 1] = y
      

      Please let me know if I can provide any other information of relevance and thanks for your work maintaining these bindings!

      Attachments

        Issue Links

          Activity

            People

              crmaurei Cristian Maureira-Fredes
              j9ac9k Ognyan Moore
              Votes:
              0 Vote for this issue
              Watchers:
              4 Start watching this issue

              Dates

                Created:
                Updated:
                Resolved: