Heatmap with pyqtgraph

3,507 views
Skip to first unread message

jure.b...@gmail.com

unread,
Jan 22, 2014, 12:34:03 PM1/22/14
to pyqt...@googlegroups.com
Hi,

I am developing a heatmap and using pyqtgraph for drawing. Heatmap is an image, which first gets drawn in small detail (image with big squares) and then gets updated with more detail (making squares as small as one pixel). There is only one big image (size 500x500) which gets updated with smaller images (size 50x50, at position x,y).

So far I have written:

I create an ImageView and add an HeatmapItem (which subclasses ImageItem):
self.hmi = HeatmapItem(image_rect=QtCore.QRectF(0, 0, self.default_image_width, self.default_image_height))
self.imv = pg.ImageView(self.mainArea, view=pg.PlotItem(), imageItem=self.hmi)


HeatmapItem looks like this:
class HeatmapItem(pg.ImageItem):
    def __init__(self, image=None, image_rect=None):
        pg.ImageItem.__init__(self)

        if image is not None:
            self.image = image
        else:
            self.image = np.zeros((100, 100))

        if image_rect:
            self.image_rect = image_rect
        else:
            self.image_rect = QtCore.QRectF(0, 0, 500, 500)

    def setRect(self, rect):
        self.image_rect = rect

    def setImage(self, image=None, autoLevels=None, **kargs):
        self.image = image
        self.update()

    def updateImage_(self, image, image_rect):
        # self.image[image_rect.x() : image_rect.x()+image_rect.width() - 1, image_rect.y() : image_rect.y()+image_rect.height() - 1] = image
        self.image[image_rect.x() : image_rect.x()+image.shape[1], image_rect.y() : image_rect.y()+image.shape[0]] = image
        self.render()
        self.update()

    def boundingRect(self):
        return self.image_rect


The first time I display an image, it is called like this:
item = self.imv.getImageItem()
item.setRect(rect)
item.setImage(colors)
self.imv.view.getAxis('bottom').setRange(self.X_min, self.X_max)
self.imv.view.getAxis('left').setRange(self.Y_min, self.Y_max)

This has the effect of drawing a large image onto the ImageView.

Every time I update the image, I call this:
item = self.imv.getImageItem()
item.updateImage_(colors, rect)
self.imv.view.getAxis('bottom').setRange(self.X_min, self.X_max)
self.imv.view.getAxis('left').setRange(self.Y_min, self.Y_max)

This has the effect of drawing a smaller image inside the large image. (see method HeatmapItem.updateImage_()). It just puts the data into the right place inside numpy array, which holds the big image.



Well, it works, but - I have some issues:
1. I have to manually set axis range each time after updating image, otherwise it changes to the big image width and height.
2. The axes values also change when I zoom in or move the image
3. I am hiding the histogram widget. If I hide it before the first (the big one) image is drawn, it crashes. If I hide it after the first image is drawn, but before it is updated, then it works OK.

What would be the best approach to show a heatmap I described, with these demands:
4. bottom and left axes with custom min and max values (not the image width and height)
5. hide histogram
6. user can select a part of the image (she draws a rect over the image with her mouse), which then gets updated
7. the zooming still works


Am I heading in the right direction? Is there a better approach? Have I provided enough information?


Thanks.
Jure

Luke Campagnola

unread,
Jan 22, 2014, 8:59:18 PM1/22/14
to pyqt...@googlegroups.com
I may not fully understand what you are trying to do, but the way you have overridden the ImageItem.setImage  and .setRect methods will prevent them working correctly. You should not need to set the axis ranges at all; this should done automatically when setRect is called.

 
3. I am hiding the histogram widget. If I hide it before the first (the big one) image is drawn, it crashes. If I hide it after the first image is drawn, but before it is updated, then it works OK.

What kind of crash? Is there an error message?
 

What would be the best approach to show a heatmap I described, with these demands:
4. bottom and left axes with custom min and max values (not the image width and height)
5. hide histogram
6. user can select a part of the image (she draws a rect over the image with her mouse), which then gets updated
7. the zooming still works

Here is a simple example that I think covers all of the things you are looking for. Key things are:
* I have overridden ImageItem.mouseDragEvent to detect rectangle-drawing with the left mouse button.
* I have NOT overridden ImageItem.setRect or .setImage; calling hm.setRect() causes the image to be displayed with the correct size relative to the axes.
* I use a PlotWidget instead of ImageView since it seems you do not need any of ImageView's features.
* Zooming and pan with the middle mouse button are unaffected.

    import pyqtgraph as pg
    import numpy as np
    pg.mkQApp()

    class Heatmap(pg.ImageItem):
        def __init__(self):
            self.heatmap = np.zeros((500, 500))
            pg.ImageItem.__init__(self, self.heatmap)
        def mouseHoverEvent(self, ev):
            ev.acceptDrags(pg.Qt.QtCore.Qt.LeftButton)
        def mouseDragEvent(self, ev):
            if ev.button() != pg.Qt.QtCore.Qt.LeftButton:
                ev.ignore()
                return
            else:
                ev.accept()
            heatmap = self.heatmap.copy()
            p1 = ev.buttonDownPos()
            p2 = ev.pos()
            x1, x2 = sorted([p1.x(), p2.x()])
            y1, y2 = sorted([p1.y(), p2.y()])
            heatmap[x1:x2, y1:y2] += 1
            self.setImage(heatmap, levels=[0,20])
            if ev.isFinish():
                self.heatmap = heatmap

    hm = Heatmap()
    v = pg.PlotWidget()
    v.addItem(hm)
    v.show()
    hm.setRect(pg.QtCore.QRectF(1, 2, 10, 10))


I hope this gets you started in the right direction..

Luke

jure.b...@gmail.com

unread,
Jan 23, 2014, 9:43:47 AM1/23/14
to pyqt...@googlegroups.com
Hi Luke,

This is how the heatmap is implemented now, based on your suggestions:
A plot widget is created:
self.plot = pg.PlotWidget()

The heatmap class:
class Heatmap(pg.ImageItem):
    def __init__(self, image=None):

        if image is not None:
            self.image = image
        else:
            self.image = np.zeros((500, 500))

        pg.ImageItem.__init__(self, self.image)

    def updateImage_(self, image, image_rect):
        self.image[image_rect.y() : image_rect.y()+image.shape[0], image_rect.x() : image_rect.x()+image.shape[1]] = image
        self.render()
        self.update()


The first time the image is drawn like this:
self.hmi = Heatmap(colors)
self.plot.addItem(self.hmi)
self.hmi.setRect(QtCore.QRectF(self.X_min, self.Y_min, self.X_max-self.X_min, self.Y_max-self.Y_min))

And each time it gets updated:
self.hmi.updateImage_(colors, rect)


Now it works as I wanted:
- axes ranges are correct
- there is no historgram widget
- zoom and pan works as it should


And now, the questions:
1.) I have a question about this line of code you included:
pg.mkQApp()
What does it do?

2.) Also, I get a warning (was the same with previous implementation):
QApplication was created before pyqtgraph was imported; there may be problems (to avoid bugs, call QApplication.setGraphicsSystem("raster") before the QApplication is created).
It seems that everything is working correctly, but still: can I call setGraphicsSystem("raster") somewhere in my module - the whole application is pretty big, and I don't really know where the QApplication is created.


3.) Regarding point 6 from my first post (the user can select a region that should get updated):
I changed the design - I think this would be the most simple solution: (this is why I did not use mouseHoverEvent() and mouseDragEvent() in your solution)
Now, the User has two options:
1. she can zoom the image with mouse wheel and move it with left button drag, so that the region she wants to be updated, is displayed
2. she can zoom the image with right button drag: a box is drawn over a region of the scene and the scene is scaled and panned to fit the box
When the scene is zoomed, I would call
self.plot.viewRect()
to obtain the rect that is to be updated, calculate the updates, and then use

self.hmi.updateImage_(colors, rect)
to update the heatmap.
Would this be the correct approach?

While doing this, I am trying to set an option, so the mac user can also do the equivalent of right button drag:
pg.setConfigOption('leftButtonPan', False)
And this line has no effect - it is called, no errors, but the mouse behaviour doesn't change. It works if i right click on the scene and select 1button mouse mode. What am I missing?


Thank you for a fast reply yesterday, I was working on this for a couple of days now, and after reading your suggestions, I had it done in minutes... Thanks, your help is really appreciated :)

Jure


Dne četrtek, 23. januar 2014 02:59:18 UTC+1 je oseba Luke Campagnola napisala:

Luke Campagnola

unread,
Jan 23, 2014, 12:07:50 PM1/23/14
to pyqt...@googlegroups.com
This just creates a QApplication() if one is not already present. For your application, you can omit this.
 

2.) Also, I get a warning (was the same with previous implementation):
QApplication was created before pyqtgraph was imported; there may be problems (to avoid bugs, call QApplication.setGraphicsSystem("raster") before the QApplication is created).
It seems that everything is working correctly, but still: can I call setGraphicsSystem("raster") somewhere in my module - the whole application is pretty big, and I don't really know where the QApplication is created.

No; QApplication.setGraphicsSystem only works if it is called before the QApplication has been created. You can probably just ignore the warning for now, but if you run into weird problems later (on OSX I have seen incorrect line widths and inexplicable flashing in the UI) then it might be necessary to track down the QApplication instantiation.

3.) Regarding point 6 from my first post (the user can select a region that should get updated):
I changed the design - I think this would be the most simple solution: (this is why I did not use mouseHoverEvent() and mouseDragEvent() in your solution)
Now, the User has two options:
1. she can zoom the image with mouse wheel and move it with left button drag, so that the region she wants to be updated, is displayed
2. she can zoom the image with right button drag: a box is drawn over a region of the scene and the scene is scaled and panned to fit the box
When the scene is zoomed, I would call
self.plot.viewRect()
to obtain the rect that is to be updated, calculate the updates, and then use

self.hmi.updateImage_(colors, rect)
to update the heatmap.
Would this be the correct approach?

That looks right to me. Just remember that you would need to map from the view coordinates to the pixel coordinates of the image. You can do that manually, or you can ask the scenegraph to do the mapping for you:

vr = self.plot.viewRect()
ir = self.hmi.mapRectFromView(vr)
 
While doing this, I am trying to set an option, so the mac user can also do the equivalent of right button drag:
pg.setConfigOption('leftButtonPan', False)
And this line has no effect - it is called, no errors, but the mouse behaviour doesn't change. It works if i right click on the scene and select 1button mouse mode. What am I missing?

setConfigOption must be called before the plot is created. Otherwise, you can call plot.vb.setMouseMode:
http://www.pyqtgraph.org/documentation/graphicsItems/viewbox.html#pyqtgraph.ViewBox.setMouseMode
 

Cheers,
Luke
Reply all
Reply to author
Forward
0 new messages