Best approach to draw and update thousands of QGraphicsRectItem(s) efficiently? - python

I'm new to Qt (PySide), and I'm trying to draw a 'grid map' efficiently. However my solution slows down to a halt with 10k+ QGraphicsRectItem.
Currently it works like so:
class GridMapView(QObject, QGraphicsItemGroup):
def __init__(self, mapWidth, mapHeight, cellSize):
QObject.__init__(self)
QGraphicsItemGroup.__init__(self)
self.mapWidth = mapWidth
self.mapHeight = mapHeight
self.cellSize = cellSize
self.graphicCells = []
#Create cells.
for x in range(self.mapWidth / self.cellSize):
self.graphicCells.append([])
for y in range(self.mapHeight / self.cellSize):
self.graphicCells[x].append(QGraphicsRectItem(x * self.cellSize, y * self.cellSize, self.cellSize, self.cellSize))
self.graphicCells[x][-1].setBrush(QBrush(QColor('grey')))
self.addToGroup(self.graphicCells[x][-1])
self.setPos(-mapWidth/2, -mapHeight/2)
#Slot(Point, int)
def onCellUpdated(self, index, state):
cell = self.graphicCells[index.x][index.y]
if state == CellStates.UNKNOWN:
cell.setBrush(QBrush(QColor('grey')))
cell.setVisible(True)
elif state == CellStates.FREE:
cell.setVisible(False)
elif state == CellStates.OCCUPIED:
cell.setBrush(QBrush(QColor('black')))
cell.setVisible(True)
The initial grid is populated during creation. When the appropriate signal is fired, a specific cell will be updated. This updating is fairly infrequent, and my assumption was that Qt only draws what changes.
The entire 'map' is visible in my viewport, and disabling the rendering makes my application run perfectly fine.
I've tried setting QGraphicsView.NoViewportUpdate, yet it still updates the entire view. I hoped it would require me to call '.update()'.
Is this approach flawed from the start? Thanks in advance.

Related

Scene not updated without user interaction

I am trying to implement a simple visualization example using pyqt5 and vtk, while embedding the vtk objects (vtkSphereSource) in a QFrame- (myFrame). My first problem is that the rendering is not updated, if I don't interact on the frame. The second is, if I do not set a fixed width and height for widget, the window is represented very small on the top left corner of my frame, although it has a Expanding size policy in the ui design. I would be thankful for any advice.
# called first after the program start
def initialize():
global sphere
sphere = list()
global mapper
mapper = list()
global actor
actor = list()
global renderer
renderer= vtk.vtkRenderer()
self.widget = QVTKRenderWindowInteractor(self.myFrame)
self.interactor = self.widget.GetRenderWindow().GetInteractor()
self.interactor.SetInteractorStyle(vtkInteractorStyleTrackballCamera())
renderer.ResetCamera()
self.interactor.Initialize()
self.widget.setFixedWidth(1000)
self.widget.setFixedHeight(800)
self.widget.GetRenderWindow().AddRenderer(renderer)
# called later if new pos are avaialble
def update_sphere_pos():
array = np.array([[-3.40993, -145.858, 1062.88],
[2.9301, -240.421, 1037.68],
[3.36048, -252.239, 1014.06],
[-5.43725, -89.469, 989.953]])
for idx in range(4):
sphere.append(vtk.vtkSphereSource())
sphere[idx].SetCenter(array[idx][0], array[idx][1], array[idx][2])
sphere[idx].SetRadius(4)
sphere[idx].Update()
mapper.append(vtk.vtkPolyDataMapper())
mapper[idx].SetInputConnection(sphere[idx].GetOutputPort())
actor.append(vtk.vtkActor())
actor[idx].SetMapper(mapper[idx])
actor[idx].GetProperty().SetColor(0.0, 1.0, 0.0)
renderer.AddActor(actor[idx])
renderer.Modified()
if __name__ == "__main__":
initialize()
update_sphere_pos()

QPixmaps Performance using Python

I am currently trying to create a game object that changes pixmap whenever it moves via .setPos() in a QGraphicsScene. Since I'm new to all this, I'm not sure what the most performance-efficient methods are to cache pixmaps or to change images.
I've already looked at QPixmapCache and re-implementing the paint() function, but I'm still unsure what the best method is. This is the idea I've got at the moment:
class Object(QGraphicsPixmapItem):
def __init__(self):
super(Object, self).__init__()
self.state = 0
self.img = {
"pix1": QPixmap("pix1.png"),
"pix2": QPixmap("pix2.png"),
"pix3": QPixmap("pix3.png")}
def changePix(self):
if self.state == 0:
self.setPixmap(self.img["pix1"])
elif self.state == 1:
self.setPixmap(self.img["pix2"])
elif self.state == 2:
self.setPixmap(self.img["pix3"])
I would appreciate any advice or feedback I can get.
I've never had to use a QPixmapCache object to avoid any performance issue previously, but that's going to depend on what exactly you're doing. If you're just switching between 5 or so relatively small static/generated images (.png < 20kB), I would say it's not necessary. But if you're going to be doing something like a 2k paint buffer with an undo function, or some graph that would need to be regenerated after some paint event, you'll want some sort of caching in place. I refactored your code a bit as well to avoid hard-coding anything.
class Object(QGraphicsPixmapItem):
def __init__(self, *args):
super(Object, self).__init__()
self.img = [a for a in args if os.path.exists(a)]
def load_image(img_path, set_now=False):
if img_path not in self.img:
self.img.append(img_path)
if set_now:
self.change_state(img_path)
def change_state(img_path):
if img_name in self.img:
self.setPixmap(QPixmap(self.img[self.img.index(img_path)]))

Tkinter - memory leak with canvas

I have a Python script that handles Modbus communications. One feature I added was a "graph" that shows the response times along with a color coded line that indicates if the response was successful, had an exception, or an error. The graph is just a scrollable canvas widget from Tkinter.
After graphing a certain number of lines old lines will be deleted and then a new one will be added to the end. For this example I have it set to 10, which means there will never be more than 10 lines on the canvas at once.
The code works correctly but there is a memory leak somewhere in this function. I let it run for about 24 hours and it took about 6x more memory after 24 hours. The function is part of a larger class.
My current guess is that my code causes the canvas size to constantly "expand," which slowly eats up the memory.
self.lineList = []
self.xPos = 0
def UpdateResponseTimeGraph(self):
if not self.graphQueue.empty():
temp = self.graphQueue.get() #pull from queue. A separate thread handles calculating the length and color of the line.
self.graphQueue.task_done()
lineName = temp[0] #assign queue values to variables
lineLength = temp[1]
lineColor = temp[2]
if len(self.lineList) >= 10: #if more than 10 lines are on the graph, delete the first one.
self.responseTimeCanvas.delete(self.lineList[0])
del self.lineList[0]
#Add line to canvas and a list so it can be referenced.
self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-lineLength,
fill=lineColor, outline=''))
self.xPos += 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?
self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))
self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.
self.graphFrame.after(10, self.UpdateResponseTimeGraph)
One solution could be loop back to the start of the graph once a limit is reached but I would rather not do this since it may be confusing where the graph starts. Usually I have far more responses than 10.
EDIT:
I'm still doing to trail and error stuff but it looks like the memory leak can be eliminated with Bryan's suggestion as long as the line attributes are not changed via itemconfig. The code below should be able to run as is, if you're on python 2.7 change the import statement from tkinter to Tkinter (lower case vs uppercase t). This code will have the memory leak in it. Comment out the itemconfig line and it will be eliminated.
import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random
def RGB(r, g, b):
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
class MainUI:
def __init__(self, master):
self.master = master
self.lineList = []
self.xPos = 0
self.maxLine = 122
self.responseIndex = 0
self.responseWidth = 100
self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
self.responseTimeCanvas.pack()
self.UpdateResponseTimeGraph()
def UpdateResponseTimeGraph(self):
self.lineLength = random.randint(10,99)
if len(self.lineList) >= self.maxLine:
self.lineLength = random.randint(5,95)
self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength)
#if i comment out the line below the memory leak goes away.
self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)))
else:
self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength,
fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)), outline=''))
self.xPos += 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?
self.responseIndex += 1
self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))
self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.
self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)
mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()
The underlying tk canvas doesn't reuse or recycle object identifiers. Whenever you create a new object, a new identifier is generated. The memory of these objects is never reclaimed.
Note: this is memory inside the embedded tcl interpreter, rather than memory managed by python.
The solution is to reconfigure old, no longer used elements rather than deleting them and creating new ones.
Here's the code with no memory leak. The original source of the leak was me deleting the old line then creating a new one. This solution moves the first the line to the end then change's its attributes as necessary. I had a second 'leak' in my example code where I was picking a random color each time which lead to the number of colors used eating up a lot of memory. This code just prints green lines but the length will be random.
import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random
def RGB(r, g, b):
return '#{:02x}{:02x}{:02x}'.format(r, g, b)
class MainUI:
def __init__(self, master):
self.master = master
self.lineList = []
self.xPos = 0
self.maxLine = 122
self.responseIndex = 0
self.responseWidth = 100
self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
self.responseTimeCanvas.pack()
self.UpdateResponseTimeGraph()
def UpdateResponseTimeGraph(self):
self.lineLength = random.randint(10,99)
if len(self.lineList) >= self.maxLine:
self.lineLength = random.randint(5,95)
self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength)
self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(100, 255, 100))
else:
self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos + 4, self.responseWidth-self.lineLength,
fill=RGB(100, 255, 100), outline=''))
self.xPos += 5 #will cause the next line to start 5 pixels later.
self.responseIndex += 1
self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))
self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.
self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)
mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()

how to use wx.Slider with SELRANGE?

In the docs for wx.Slider (wxPython for py2, wxPython for py3, wxWidgets), there is listed a widget control named wx.SL_SELRANGE, defined to allow "the user to select a range on the slider (MSW only)". To me, this speaks of a twin-control, two sliders on the same axis in order to define a low/high range. I can't get it to show two controls.
Basic code to get it started. I'm not even worried yet about methods, events, or whatnot at this point, just to show something.
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
# ... sizers and other stuff
self.myslider = wx.Slider(self.notebook_1_pane_2, wx.ID_ANY, 0, -100, 100, style=wx.SL_SELRANGE)
# ...
self.myslider.SetSelection(10, 90)
With all of that, the most I've been able to get it to show is a blue line spanning about where I would expect things to be.
The wxPython docs all talk about it but how is the user supposed to be able to "select a range on the slider", like shown here (taken from shiny)?
What am I missing? Are there any reasonable public examples of a wxPython wx.Slider in the wild with this functionality?
PS:
One page I found speaks of WinXP only, but since that page hasn't been updated in seven years, I don't consider it authoritative on the version restriction.
I've been using wxGlade for gui layout, but I'm certainly willing/able to go into the code after export and muck around.
System: win81_64, python-2.7.10, wxPython-3.0.2.0
I have made a custom implementation for this, partly using a code from this question. Left click on the slider area sets the left border of the range, right click sets the right border. Dragging the slider moves the selection. left_gap and right_gap indicates what is the empty space between edges of the widget and actual start of the drawn slider. As in the source, these must be found out by experimentation.
class RangeSlider(wx.Slider):
def __init__(self, left_gap, right_gap, *args, **kwargs):
wx.Slider.__init__(self, *args, **kwargs)
self.left_gap = left_gap
self.right_gap = right_gap
self.Bind(wx.EVT_LEFT_UP, self.on_left_click)
self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
self.Bind(wx.EVT_SCROLL_PAGEUP, self.on_pageup)
self.Bind(wx.EVT_SCROLL_PAGEDOWN, self.on_pagedown)
self.Bind(wx.EVT_SCROLL_THUMBTRACK, self.on_slide)
self.slider_value=self.Value
self.is_dragging=False
def linapp(self, x1, x2, y1, y2, x):
proportion=float(x - x1) / (x2 - x1)
length = y2 - y1
return round(proportion*length + y1)
# if left click set the start of selection
def on_left_click(self, e):
if not self.is_dragging: #if this wasn't a dragging operation
position = self.get_position(e)
if position <= self.SelEnd:
self.SetSelection(position, self.SelEnd)
else:
self.SetSelection(self.SelEnd, position)
else:
self.is_dragging = False
e.Skip()
# if right click set the end of selection
def on_right_click(self, e):
position = self.get_position(e)
if position >= self.SelStart:
self.SetSelection(self.SelStart, position)
else:
self.SetSelection(position, self.SelStart)
e.Skip()
# drag the selection along when sliding
def on_slide(self, e):
self.is_dragging=True
delta_distance=self.Value-self.slider_value
self.SetSelection(self.SelStart+delta_distance, self.SelEnd+delta_distance)
self.slider_value=self.Value
# disable pageup and pagedown using following functions
def on_pageup(self, e):
self.SetValue(self.Value+self.PageSize)
def on_pagedown(self, e):
self.SetValue(self.Value-self.PageSize)
# get click position on the slider scale
def get_position(self, e):
click_min = self.left_gap #standard size 9
click_max = self.GetSize()[0] - self.right_gap #standard size 55
click_position = e.GetX()
result_min = self.GetMin()
result_max = self.GetMax()
if click_position > click_min and click_position < click_max:
result = self.linapp(click_min, click_max,
result_min, result_max,
click_position)
elif click_position <= click_min:
result = result_min
else:
result = result_max
return result
I had this same problem before and couldn't find a good solution. What I ended up doing was creating my own custom RangeSlider widget with two actual thumbs.
Code is available in this answer, or in this GitHub gist.
Better late than never: to answer the original question, wxSL_SELRANGE does work but it only results in the expected appearance if it's combined with wxSL_LABELS. With both of these styles (and the selection set to 20..80 for the total range 0..100) the control appears like this:

how to force matplotlib to update a plot

I am trying to construct a little GUI that has a plot which updates every time a new data sample is read. I would prefer not to run it with a timer, since the data will be arriving at differing intervals. Instead, I'm trying to make an implementation using signals, where the data collection function will emit a signal when data is read, and then the painting function will emit a signal when the painting is completed.
The problem, as it appears right now, is that the canvas is not updating as soon as I call canvas.draw(). When this program runs, data_collect() and paint() alternate sending signals, but the figure is not updated until after I stop the process. How can I force matplotlib to update the figure whenever paint() is called?
What follows is a relatively simple piece of example code which is not optimal, but hopefully will convey the flavor of what I'm trying to do...
N_length = 150;
count = [0];
def sinval(delay):
k = 0;
x = [];
# set up data vector with sinusoidal data in it.
while k < N_length:
x.append(math.sin(2*math.pi*k/N_length));
k += 1;
def next():
time.sleep(delay);
outstring = "%0.3e" % (x[count[0]]);
if (count[0] == (N_length-1)):
count[0] = 0;
else:
count[0] += 1;
return outstring;
return next;
class DesignerMainWindow(QtGui.QMainWindow, Ui_mplMainWindow):
def __init__(self, parent = None):
super(DesignerMainWindow, self).__init__(parent)
self.setupUi(self)
QtCore.QObject.connect(self.mplStartButton, QtCore.SIGNAL("clicked()"), self.start_graph);
QtCore.QObject.connect(self.mplStopButton, QtCore.SIGNAL("clicked()"), self.stop_graph);
QtCore.QObject.connect(self.mplQuitButton, QtCore.SIGNAL("clicked()"), QtGui.qApp, QtCore.SLOT("quit()"));
QtCore.QObject.connect(self, QtCore.SIGNAL("data_collect()"), self.data_collect);
QtCore.QObject.connect(self, QtCore.SIGNAL("paint()"), self.paint);
def start_graph(self):
# generates first "empty" plots
self.user = [];
self.l_user, = self.mpl.canvas.ax.plot([], self.user, label='sine wave');
# set up the axes.
self.mpl.canvas.ax.set_xlim(0, 300);
self.mpl.canvas.ax.set_ylim(-1.1, 1.1);
self.mpl.canvas.draw();
# start the data collection process.
self.delay = 0.05;
self.next = sinval(self.delay);
self.emit(QtCore.SIGNAL('data_collect()'));
def data_collect(self):
outstring = self.next();
self.user.append(float(outstring.split()[0]));
self.l_user.set_data(range(len(self.user)), self.user);
self.emit(QtCore.SIGNAL('paint()'));
def paint(self):
self.mpl.canvas.draw();
self.emit(QtCore.SIGNAL('data_collect()'));
I'd guess that calling QCoreApplication::processEvents after paint() will help. More elegant would be to have a separate QThread for the reading. Take a look at this thread.

Categories