HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Multi-layer PyQt4 image viewer

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
layerpyqt4imagemultiviewer

Problem

I have written a functional GUI program using PyQt4, and I'm looking for some feedback because it's not as fast as I would like. It takes in some number of large, same-sized 2D arrays and displays a sub-image of each one in its own sub-window - at full resolution or only zoomed by a factor of 2 or 4. The central GUI contains one of the complete images down-sampled so it fits on the screen, on which is a movable selection box that can be moved by mouse or by the arrow keys.

The response when I move the box is slow when I set the sub-window zoom level to 25% or 50%, is there a way to speed it up? I also could not get around creating a sub-class of QtGraphicsView for the central widget, is that the most proper way to do this?

The primary code - it's decently long, but I've removed some other functional settings (such as the color of the box) to bring the line count down:

```
import sys
import numpy as np
from PyQt4 import QtCore, QtGui
from ajsutil import get_screen_size, bytescale, clamp, upsamp, downsamp

# This is used as the central widget in the main GUI
class SubView(QtGui.QGraphicsView):
"""A sub-class of QGraphicsView that allows specific mouse and keyboard
handling.
"""

# Custom signals - one for keyboard update and one for mouse update
updateEvent = QtCore.pyqtSignal(list)
modEvent = QtCore.pyqtSignal(list)

def __init__(self, img, boxsize):
"""Initialize the class with an image and a box size."""

super(SubView,self).__init__()

wdims = (img.size().width(), img.size().height())
self.bs = boxsize

# Construct a scene with a pixmap and a rectangle
scene = QtGui.QGraphicsScene(0, 0, wdims[0], wdims[1])
self.px = scene.addPixmap(QtGui.QPixmap.fromImage(img))

self.rpen = QtGui.QPen(QtCore.Qt.green)
self.rect = scene.addRect(0, 0, boxsize,boxsize, pen=self.rpen)

self.setScene(scene)

# Set size policies and settings
self.setSizePol

Solution

Problems

For Python 2 and 3 compatibility xrange would need to be defined in
both versions, e.g.:

try:
    xrange
except NameError:
    xrange = range


Also, in Python 3 I get a lot of errors because mod isn't set:

Traceback (most recent call last):
  File "qt.py", line 64, in keyPressEvent
    self.modEvent.emit([x*self.bs/2 for x in mod])
UnboundLocalError: local variable 'mod' referenced before assignment


I'd address that with a method like this:

def keyPressEvent(self, event):
    """Move the box with arrow keys."""
    if self.mouseOn:
        return

    mod = {
        QtCore.Qt.Key_Up: [0, -1],
        QtCore.Qt.Key_Down: [0, 1],
        QtCore.Qt.Key_Left: [-1, 0],
        QtCore.Qt.Key_Right: [1, 0]
    }.get(event.key())

    if not mod:
        return

    self.modEvent.emit([x * self.bs / 2 for x in mod])


Or so; exit early, use declarative syntax (which looks a bit cleaner
than a lot of ifs and only use mod when it actually has a value.

The save method will raise an error if the user didn't select a
filename, so check the return value of getSaveFileName before
destructuring it:

def save(self):
    """Save the big image to an image file."""
    selection = QtGui.QFileDialog.getSaveFileName(self)
    if selection:
        self.view.px.pixmap().save(selection[0])


Zoom level 25% is showing artifacts when the picture isn't big enough.
I guess that's somewhat expected, but I'd still rather catch that
instead.

Style

  • Obviously PEP8.


Especially whitespace and abbrevations bug me: grn is so not more
readable than green.

  • Some variables are unused, e.g. smbytefunc and tobytefunc.



  • Instead of clamp you can use


numpy.clip.

  • Loops with range(len(...)) are probably better written with


enumerate instead. If you have a list of images you can also just
iterate over the contents, instead of using the indexes from
range(number_of_images).

  • It is less error-prone to keep just one list of


images/widgets/... than to keep the number of things as a separate
variable; e.g. both nimgs and smid is more likely to get out of
sync with each other. If you want a shortcut, maybe use a property
for nimgs instead.

  • Using both Tk and Qt is not nice; (if you're already using Qt, than)


try to minimise the number of (additional) dependencies in your
application. I found
this question
for the screen size and it seems to work fine for this setup.

The separation between the SubView and the main application window is
good enough I think. The main view though does quite a lot; it might
make sense to move part of the update logic into the separate windows
and create a new class for them instead.

Performance

For the slowness I'd always use a profiler to figure out what's
happening there. So with handy dandy cProfile in hand, let's see what
takes so much time (python -mcProfile -oqt.profile -stottime qt.py
then set zoom to 50% and wiggle the mouse around a lot):

qt.profile% stats
Tue Aug  4 23:41:39 2015    qt.profile

         96226 function calls (93379 primitive calls) in 10.468 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    4.468    4.468   10.158   10.158 {built-in method exec_}
     1731    3.882    0.002    3.882    0.002 {method 'reduce' of 'numpy.ufunc' objects}
     1724    0.548    0.000    4.451    0.003 /usr/lib64/python3.3/site-packages/numpy/core/_methods.py:53(_mean)
      865    0.425    0.000    0.540    0.001 ./ajsutil.py:39(bytescale)
     4353    0.357    0.000    0.370    0.000 {built-in method array}
      287    0.193    0.001    5.678    0.020 qt.py:292(updateView)
      288    0.064    0.000    0.064    0.000 {built-in method showMessage}
      866    0.058    0.000    0.058    0.000 {method 'astype' of 'numpy.ndarray' objects}
    26/23    0.053    0.002    0.057    0.002 {built-in method load_dynamic}
      862    0.045    0.000    4.810    0.006 ./ajsutil.py:53(downsamp)
        1    0.035    0.035    0.035    0.035 {built-in method create}
      120    0.030    0.000    0.030    0.000 {built-in method loads}
        4    0.027    0.007    0.027    0.007 {built-in method show}
      865    0.025    0.000    0.025    0.000 {built-in method setColorTable}
     2586    0.022    0.000    0.022    0.000 {method 'reshape' of 'numpy.ndarray' objects}
      862    0.018    0.000    0.031    0.000 /usr/lib64/python3.3/site-packages/numpy/lib/shape_base.py:792(tile)
      862    0.012    0.000    0.012    0.000 ./ajsutil.py:61()
        1    0.010    0.010   10.277   10.277 qt.py:87(__init__)


And so forth. I've sorted by tottime to see the actual running time
in functions.

So what stands out? downsamp and bytescale I'd say.

Let's start with downsamp. I guessed that the mean calls at the end
where a problem, because mean comes up in the stats as well; searching
for that gives me
this Stackoverflow answer,
which adapte

Code Snippets

try:
    xrange
except NameError:
    xrange = range
Traceback (most recent call last):
  File "qt.py", line 64, in keyPressEvent
    self.modEvent.emit([x*self.bs/2 for x in mod])
UnboundLocalError: local variable 'mod' referenced before assignment
def keyPressEvent(self, event):
    """Move the box with arrow keys."""
    if self.mouseOn:
        return

    mod = {
        QtCore.Qt.Key_Up: [0, -1],
        QtCore.Qt.Key_Down: [0, 1],
        QtCore.Qt.Key_Left: [-1, 0],
        QtCore.Qt.Key_Right: [1, 0]
    }.get(event.key())

    if not mod:
        return

    self.modEvent.emit([x * self.bs / 2 for x in mod])
def save(self):
    """Save the big image to an image file."""
    selection = QtGui.QFileDialog.getSaveFileName(self)
    if selection:
        self.view.px.pixmap().save(selection[0])
qt.profile% stats
Tue Aug  4 23:41:39 2015    qt.profile

         96226 function calls (93379 primitive calls) in 10.468 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    4.468    4.468   10.158   10.158 {built-in method exec_}
     1731    3.882    0.002    3.882    0.002 {method 'reduce' of 'numpy.ufunc' objects}
     1724    0.548    0.000    4.451    0.003 /usr/lib64/python3.3/site-packages/numpy/core/_methods.py:53(_mean)
      865    0.425    0.000    0.540    0.001 ./ajsutil.py:39(bytescale)
     4353    0.357    0.000    0.370    0.000 {built-in method array}
      287    0.193    0.001    5.678    0.020 qt.py:292(updateView)
      288    0.064    0.000    0.064    0.000 {built-in method showMessage}
      866    0.058    0.000    0.058    0.000 {method 'astype' of 'numpy.ndarray' objects}
    26/23    0.053    0.002    0.057    0.002 {built-in method load_dynamic}
      862    0.045    0.000    4.810    0.006 ./ajsutil.py:53(downsamp)
        1    0.035    0.035    0.035    0.035 {built-in method create}
      120    0.030    0.000    0.030    0.000 {built-in method loads}
        4    0.027    0.007    0.027    0.007 {built-in method show}
      865    0.025    0.000    0.025    0.000 {built-in method setColorTable}
     2586    0.022    0.000    0.022    0.000 {method 'reshape' of 'numpy.ndarray' objects}
      862    0.018    0.000    0.031    0.000 /usr/lib64/python3.3/site-packages/numpy/lib/shape_base.py:792(tile)
      862    0.012    0.000    0.012    0.000 ./ajsutil.py:61(<listcomp>)
        1    0.010    0.010   10.277   10.277 qt.py:87(__init__)

Context

StackExchange Code Review Q#74711, answer score: 5

Revisions (0)

No revisions yet.