patternpythonMinor
Multi-layer PyQt4 image viewer
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
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
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
both versions, e.g.:
Also, in Python 3 I get a lot of errors because
I'd address that with a method like this:
Or so; exit early, use declarative syntax (which looks a bit cleaner
than a lot of
The
filename, so check the return value of
destructuring it:
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
Especially whitespace and abbrevations bug me:
readable than
iterate over the contents, instead of using the indexes from
images/widgets/... than to keep the number of things as a separate
variable; e.g. both
sync with each other. If you want a shortcut, maybe use a property
for
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
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
takes so much time (
then set zoom to 50% and wiggle the mouse around a lot):
And so forth. I've sorted by
in functions.
So what stands out?
Let's start with
where a problem, because
for that gives me
this Stackoverflow answer,
which adapte
For Python 2 and 3 compatibility
xrange would need to be defined inboth versions, e.g.:
try:
xrange
except NameError:
xrange = rangeAlso, 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 assignmentI'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 afilename, so check the return value of
getSaveFileName beforedestructuring 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 morereadable than
green.- Some variables are unused, e.g.
smbytefuncandtobytefunc.
- Instead of
clampyou 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 justiterate 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 ofsync 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 isgood 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 whattakes so much time (
python -mcProfile -oqt.profile -stottime qt.pythen 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 timein functions.
So what stands out?
downsamp and bytescale I'd say.Let's start with
downsamp. I guessed that the mean calls at the endwhere a problem, because
mean comes up in the stats as well; searchingfor that gives me
this Stackoverflow answer,
which adapte
Code Snippets
try:
xrange
except NameError:
xrange = rangeTraceback (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 assignmentdef 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.