Memory leak with PySide QGraphicsPixmapItem in Python 3.4 - python

In my PySide app I repeatedly update a QGraphicsPixmapItem with a new pixmap that I create from a (always differently scaled) numpy array:
# Once:
from PySide import QtGui
self._image_pixmap_item = self._scene.addPixmap(QtGui.QPixmap(width, height))
# Repeated:
image = QtGui.QImage(numpy_array, width, height)
pixmap = QtGui.QPixmap.fromImage(image)
self._image_pixmap_item.setPixmap(pixmap)
This code works fine with Python 2.7 but memory usage is constantly increasing under Python 3.4. I could fix this by manually invoking the garbage collector in each loop:
import gc
gc.collect()
but performance is (of cause) quite bad. I use Python 3.4.3 with PySide 1.2.4 and numpy 1.11.2.
Is this a bug in the (relatively new) Python 3.x support of PySide or am I missing something? Also, is there a way to directly fill the the pixmap buffer without creating a new QImage every time?
Thanks
Alex
UPDATE:
As workaround, using qimage2ndarray (https://github.com/hmeine/qimage2ndarray) to convert the numpy array to a QImage works perfectly well.

Related

Buffer function for python 3+

I'm trying to open a vtk window using vtk_show, but my Ipython console crashes every time i do this, apparently this is because Ipython can't display an external window, which is exactly what vtk_show does. I searched on google for a solution, but it's written for python2 (i'm using python 3.6.3). Here's the solution i found:
import vtk
from IPython.display import Image
def vtk_show(renderer, width=400, height=300):
"""
Takes vtkRenderer instance and returns an IPython Image with the
rendering.
"""
renderWindow = vtk.vtkRenderWindow()
renderWindow.SetOffScreenRendering(1)
renderWindow.AddRenderer(renderer)
renderWindow.SetSize(width, height)
renderWindow.Render()
windowToImageFilter = vtk.vtkWindowToImageFilter()
windowToImageFilter.SetInput(renderWindow)
windowToImageFilter.Update()
writer = vtk.vtkPNGWriter()
writer.SetWriteToMemory(1)
writer.SetInputConnection(windowToImageFilter.GetOutputPort())
writer.Write()
data = str(buffer(writer.GetResult()))
return Image(data)
I'm getting an error while trying to use the buffer built-in function of python2, but as this function doesn't exist on python3+ i'm stuck.. If anyone could help me with this i would be very appreciated. Thanks in advance!
At least these two points must be modified on your code to have the same behavior with Python 3:
The buffer(...) built-in function in Python 2 has been replaced by memoryview(...) in Python 3: What is Python buffer type for?. Replace the buffer call by memoryview
the str(...) built-in function has to replaced by a bytes(...) call to get a bytes object: https://docs.python.org/2/howto/pyporting.html#text-versus-binary-data
So the data = ... line should read:
data = bytes(memoryview(writer.GetResult()))
To clarify, I believe this example was an adaptation of a very informative blog example showing how to extract surfaces from medical images using VTK's marching cubes algorithm. The accompanying Jupyter notebook was intended for Python 2.7, and as mentioned for it to be used in Python 3.6+, the data=... portion needs to be changed.
import vtk
from IPython.display import Image
def vtk_show(renderer, width=400, height=300):
"""
Takes vtkRenderer instance and returns an IPython Image with the
rendering.
"""
renderWindow = vtk.vtkRenderWindow()
renderWindow.SetOffScreenRendering(1)
renderWindow.AddRenderer(renderer)
renderWindow.SetSize(width, height)
renderWindow.Render()
windowToImageFilter = vtk.vtkWindowToImageFilter()
windowToImageFilter.SetInput(renderWindow)
windowToImageFilter.Update()
writer = vtk.vtkPNGWriter()
writer.SetWriteToMemory(1)
writer.SetInputConnection(windowToImageFilter.GetOutputPort())
writer.Write()
data = memoryview(writer.GetResults()).tobytes()
return Image(data)
Credit for the solution definitely goes to #MafiaSkafia and #jcgiret, but I wanted to post a full and final solution.

PySide crash when displaying pixmaps

I am programming a GUI application for Data visualization using Python and Qt via PySide.
I experience occasional crashes ('python.exe has stopped working') which I think I narrowed down to the following problem:
When creating a pixmap from a numpy array, somehow the memory is freed by python (?) even when the pixmap already exists. This does not happen if the image format used is QImage.Format_ARGB32. (Why not?). Check out the code example below, I hope you can reproduce the problem.
EDIT: To clarify - If the numpy array is not deleted by python, everything works just as expected. However, in my application, new data is generated constantly and I would have to find a good way to track which dataset is currently displayed as a pixmap, and delete it as soon as it is not displayed anymore. I would like to find the correct way for Qt to take care of the (image-) data and store it in memory until not required anymore.
As far as I understood the documentation of Qt and PySide, the pixmap should hold all the data of the image, thus Qt should be responsible for the memory management.
Is this a bug in Qt, Pyside, or did I not understand something? I could not find any details on the memory management in the regular documentation.
Background: I need to regularly update the data to display, thus it may happen that between creating the pixmap and displaying it, the numpy data array is already overwritten by python (as there are some CPU intensive threads involved that sometimes slow the GUI). Thus, storing the numpy array forever is not an option.
Here is a code example, the interesting bits happen in the display_image method:
import numpy as np
from PySide import QtCore, QtGui
import sys
class displaywidget(QtGui.QWidget):
def __init__(self,parent = None):
super(displaywidget, self).__init__(parent)
## set up the GUI elements
self.setLayout(QtGui.QGridLayout())
self.view = QtGui.QGraphicsView()
self.layout().addWidget(self.view)
self.scene = QtGui.QGraphicsScene()
self.view.setScene(self.scene)
# create a pixmap and display it on the graphicsview
self.display_image()
def display_image(self):
# create image data in numpy array
size = 1024
r = np.linspace(0,255, num = size**2, dtype = np.uint32)
argb = (r<<16) +(255<<24)
# image should display a black to red shading
image = QtGui.QImage(argb, size,size, size*4, QtGui.QImage.Format_RGB32)
### using ARGB format option does not cause the problem
# image = QtGui.QImage(argb, size,size, size*4, QtGui.QImage.Format_RGB32)
pixmap = QtGui.QPixmap.fromImage(image)
self.scene.addPixmap(pixmap)
### when the image data is stored, everything works fine, too
# self.cache = argb
### if only the pixmap and image is stored, the problem still exists
# self.cache = [pixmap, image]
def main(argv):
## create application and main window
try:
app = QtGui.QApplication(argv)
new_qtapp = True
except:
new_qtapp = False
mainwindow = QtGui.QMainWindow()
mainwindow.setCentralWidget(displaywidget())
mainwindow.show()
if new_qtapp:
sys.exit(app.exec_())
return mainwindow
if __name__=="__main__":
w = main(sys.argv)
I am using 32 bit Python 2.7.6 and PySide 1.2.2 on a generic Windows7 Office PC.
Thanks for your help!
This simple change keeps the image from being garbage collected when the function is done. Which seems to be what caused the problem
self.argb = (r<<16) +(255<<24)
# image should display a black to red shading
image = QtGui.QImage(self.argb, size,size, size*4, QtGui.QImage.Format_RGB32)

pyqtgraph exporting from API within PyQt4 widget fails / crashes python

I have a small application that I have built using PyQt4 and pyqtgraph. I want to put a few buttons in that call the exporters available with pyqtgraph (rather than, or really in addition to, using the context menu that pops up when a user right clicks on a plot).
So far, however, I have not been able to get this to work.
Here is a simplified version of the application:
from PyQt4 import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.exporters
import numpy as np
import sys
class SimpleUI(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.resize(1500, 1000)
self.plot_widget = pg.GraphicsLayoutWidget(self)
self.layout = QtGui.QVBoxLayout(self)
data = np.arange(10)
self.plt = self.plot_widget.addPlot()
self.plt.plot(data)
self.export_btn = QtGui.QPushButton("Export")
self.export_btn.clicked.connect(self.export)
self.layout.addWidget(self.plot_widget)
self.layout.addWidget(self.export_btn)
def export(self):
img = pg.exporters.ImageExporter(self.plt)
img.export()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = SimpleUi()
ex.show()
sys.exit(app.exec_())
Clicking on the "Export" button in this case causes a dialog to quickly pop up and then disappear.
If instead I put
img.export(copy=True)
And try to paste what's on the clipboard into something (Paint, whatever), python.exe crashes.
Oddly, exporting through the context menu that is available by default with pyqtgraph works just fine. Also, just working in the terminal I can copy / save plotItems just fine using the same exact lines of code as above. I.e.:
import numpy as np
import pyqtgraph as pg
import pyqtgraph.exporters
plt = pg.plot(np.arange(10))
img = pg.exporters.ImageExporter(plt.plotItem)
img.export()
Which implies to me that that exporters are working fine, but there is some weird interaction that is going on when they are called from within a pyqt4 widget in the manner that I am calling them.
I have tried both pyqtgraph 0.9.8 as well as the main branch on github. Very much at a loss as to what is the issue here
Thanks
It looks like you are not storing img anywhere, so it is collected as soon as the call to export() returns.
Explanation:
Objects in Python are kept in memory only as long as they are needed. When Python determines that an object is no longer needed, it deletes the object.
How does Python know when an object is no longer needed? By counting references. When you execute img = ImageExporter(...), a new object is created with one reference: the local variable img.
Variables that are created inside a function are considered local to the scope of that function. When the function exits, the variable img disappears, which causes the reference count of the ImageExporter object to drop to 0, which causes Python to delete the object.
By setting self.img = ImageExporter(...), you are assigning a reference to the object that is not local to the scope of the function (because the SimpleUI object referred to as self continues to exist after the function returns). This allows the object to persist as long as the SimpleUI still holds the reference.

How can I efficiently transfer data from a NumPy array to a QPolygonF when using PySide?

I want do draw polylines with many control points in a PyQt4 / PySide application. The point coordinates come from a NumPy array and must be put into a QPolygonF in order to be drawn with QPainter.drawPolyline(...).
With PyQt4, this can be done efficiently e.g. with something like this:
import numpy as np
from PyQt4.QtGui import *
n = 3
qpoints = QPolygonF(n)
vptr = qpoints.data()
vptr.setsize(8*2*n)
aa = np.ndarray( shape=(n,2), dtype=np.float64, buffer=buffer(vptr))
aa.setflags(write=True)
aa[:,0] = np.arange(n)
aa[:,1] = np.arange(n)
for i in range(n):
print qpoints.at(i)
This works, because, when using PyQt4, QPolygonF.data() returns something (a sip.voidptr object) which speaks the Python buffer protocol.
The problem now is that if I try to run the above code using PySide instead of PyQt4, QPolygonF.data() just returns a QPointF object (with the coordinates of the first point in the QPolygonF) and is thus useless.
So my question is: is there any known workaround to this? How can I, with PySide, put data into a QPolygonF without inserting QPointF objects, element-wise?
Here is an efficient way of writing a Numpy array into the memory block pointed by a QPolygonF object using PySide2:
https://github.com/PierreRaybaut/PythonQwt/blob/master/qwt/plot_curve.py#L63
(See function "array2d_to_qpolygonf")
This is as efficient as with PyQt4 or PyQt5.
This should work
from pylab import *
from PySide.QtGui import QPolygonF
from PySide.QtCore import QPointF
xy = resize(arange(10),(2,10)).T
qPlg = QPolygonF()
for p in xy:
qPlg.append(QPointF(*p))
Hope it helps!
In PyQt6, you can accelerate the code given by Pierre Raybaut (up to a factor three on my computer) by replacing
polyline = QPolygonF([QPointF(0, 0)] * size)
by
polyline = QPolygonF([QPointF(0, 0)])
polyline.fill(QPointF(0, 0),size)
This indeed removes the need of creating a long Python list.
However, if you want to add this polygon to a series with QChart.addSeries, the time of this operation is pretty long, but it still allows working fluently up to 1 million points with my Core i7 10th gen, on Ubuntu 22.04.

PyQT4 and QPixmap: load image with size zero?

I am confused by PyQt4. I have tried the following steps on python2.6:
In [1]: from PyQt4 import QtGui
In [2]: import sys
In [3]: app = QtGui.QApplication(sys.argv)
In [4]: pix = QtGui.QPixmap("P1010001.JPG")
In [5]: pix.width(), pix.height()
Out[5]: (0, 0)
Why does width and height show zero? The image exists and is fine. This is completely counterintuitive, which I do not expect from python.
PyQt adds a little syntactic sugar here and there to make things more Pythonic. But it is mostly a fairly thin wrapper around Qt (which is a C++ library) - and so it would be a mistake to expect PyQt to always behave in a way that is intuitive to Python programmers.
I suppose most Python programmers might expect QPixmap to raise an error when setting a path that doesn't exist. But Qt doesn't do this, and, in this case, neither does PyQt. Instead, you can check that you have a valid pixmap by using:
pix.isNull()
To actually fix the code in your example, you will obviously have to change to the appropriate directory first (or use an absolute path).

Categories

Resources