I'm writing a GUI for a video camera that can basically run in two modes that I call liveview and recordview. The only difference being that I'm recording in the latter and only viewing in the former.
In liveview mode the image gets updated properly. I've set a button that triggers recordview but during this acquisition the GUI gets unresponsive and the image doesn't get updated. Let me show you the relevant parts of the code:
import numpy as np
from PyQt4 import QtGui, QtCore
import pyqtgraph as pg
from lantz.drivers.andor.ccd import CCD
app = QtGui.QApplication([])
def updateview(): # <-- this works OK
global img, andor
img.setImage(andor.most_recent_image16(andor.detector_shape),
autoLevels=False)
def liveview():
""" Image live view when not recording
"""
global andor, img, viewtimer
andor.acquisition_mode = 'Run till abort'
andor.start_acquisition()
viewtimer.start(0)
def UpdateWhileRec():
global stack, andor, img, n, ishape
j = 0
while j < n:
if andor.n_images_acquired > j:
# Data saving <-- this part (and the whole while-loop) works OK
i, j = andor.new_images_index
stack[i - 1:j] = andor.images16(i, j, ishape, 1, n)
# Image updating <-- this doesn't work
img.setImage(stack[j - 1], autoLevels=False)
liveview() # After recording, it goes back to liveview mode
def record(n):
""" Record an n-frames acquisition
"""
global andor, ishape, viewtimer, img, stack, rectimer
andor.acquisition_mode = 'Kinetics'
andor.set_n_kinetics(n)
andor.start_acquisition()
# Stop the QTimer that updates the image with incoming data from the
# 'Run till abort' acquisition mode.
viewtimer.stop()
QtCore.QTimer.singleShot(1, UpdateWhileRec)
if __name__ == '__main__':
with CCD() as andor:
win = QtGui.QWidget()
rec = QtGui.QPushButton('REC')
imagewidget = pg.GraphicsLayoutWidget()
p1 = imagewidget.addPlot()
img = pg.ImageItem()
p1.addItem(img)
layout = QtGui.QGridLayout()
win.setLayout(layout)
layout.addWidget(rec, 2, 0)
layout.addWidget(imagewidget, 1, 2, 3, 1)
win.show()
viewtimer = QtCore.QTimer()
viewtimer.timeout.connect(updateview)
# Record routine
n = 100
newimage = np.zeros(ishape)
stack = np.zeros((n, ishape[0], ishape[1]))
rec.pressed.connect(lambda: record(n))
liveview()
app.exec_()
viewtimer.stop()
As you see UpdateWhileRec runs only once per acquisition while updateview runs until viewtimer.stop() is called.
I'm new to PyQt and PyQtGraph so regardless of the particular way of solving my present issue, there's probably a better way to do everything else. If that's the case please tell me!
thanks in advanced
Your problem stems from the fact that you need to return control to the Qt event loop for it to redraw the picture. Since you remain in the UpdateWhileRec callback while waiting for the next image to be acquired, Qt never gets a chance to draw the image. It only gets the chance once you exit the function UpdateWhileRec.
I would suggest the following changes.
Then instead of your while loop in UpdateWhileRec, have a QTimer that periodically calls the contents of your current while loop (i would probably suggest a singleshot timer). This ensures control will be returned to Qt so it can draw the image before checking for a new one.
So something like:
def UpdateWhileRec():
# Note, j should be initialised to 0 in the record function now
global stack, andor, img, n, j, ishape
if andor.n_images_acquired > j:
# Data saving <-- this part (and the whole while-loop) works OK
i, j = andor.new_images_index
stack[i - 1:j] = andor.images16(i, j, ishape, 1, n)
# Image updating <-- this should now work
img.setImage(stack[j - 1], autoLevels=False)
if j < n:
QTimer.singleShot(0, UpdateWhileRec)
else:
liveview() # After recording, it goes back to liveview mode
Note, you should probably put functions and variables in a class, and create an instance of the class (an object). That way you don't have to call global everywhere and things are more encapsulated.
Ultimately, you may want to look into whether your andor library supports registering a function to be called when a new image is available (a callback) which would save you doing this constant polling and/or acquiring the images in a thread and posting them back to the GUI thread to be drawn. But one step at a time!
Related
What I am trying to do is:
Create a main frame (pc_gui) and two sub frames (video_frame) and (side_frame) respectively.
Allow the user to be able to resize the application window to suit and have all frames, widgets, images, etc. resize accordingly.
Reference frames elsewhere in the gui.
In attempt to keep things tidy, possibly in error, I have an Application class, as well as functions: init, app_size, create_frames, create_widgets, and an updater function. There is quite a bit more, but if I can get this core part to work right I should be able to keep making progress.
This begins with the relatively standard tkinter UI initialization which includes the starting size of the application window.
# Initialize Application Window
pc_gui = tk.Tk()
pc_gui.geometry("1280x720")
# Initialize Updater Variables
UPDATE_RATE = 1000
# Run application
app = Application(pc_gui)
pc_gui.mainloop()
Then I create the class, main frame (pc_gui), and I call the various sub frame, application size, and images functions.
class Application(tk.Frame):
""" GUI """
def __init__(self, pc_gui):
""" Initialize the frame """
tk.Frame.__init__(self, pc_gui)
self.app_size()
self.grid()
self.create_frames()
self.create_images()
self.updater()
This is the app_size function. It's purpose is to parse the current size of the application window and the display monitor assuming the user will change these to suit while the program is running.
def app_size(self):
pc_gui.update_idletasks() # Get Current Application Values
app_width = pc_gui.winfo_width() # Current Application Width
app_height = pc_gui.winfo_height() # Current Application Height
disp_width = pc_gui.winfo_screenwidth() # Monitor (Screen) Width
disp_height = pc_gui.winfo_screenheight() # Monitor (Screen) Height
return app_width, app_height, disp_width, disp_height
Then I create the sub frames using the app_size values. The colors are simply there to help me keep track of things during development.
def create_frames(self):
""" Create Frames """
app_size = self.app_size() # Get size wxh of application window and display
app_width = app_size[0]
app_height = app_size[1]
disp_width = app_size[2]
disp_height = app_size[3]
geometry = "%dx%d" % (app_width, app_height) # Create text value for geometry
pc_gui.geometry(geometry) # Set Application Window Size
# Section of Application window dedicated to source video
video_frame = tk.Frame(
master = pc_gui,
width = app_width*.75,
height = app_height,
bg = "blue"
)
video_frame.place(
x = 0,
y = 0
)
# Section of Application window dedicated to calculations
side_frame = tk.Frame(
master = pc_gui,
width = app_width*.25,
height = app_height,
bg = "red"
)
side_frame.place(
x = app_width*.75,
y = 0
)
pc_gui.update_idletasks()
sf_x = side_frame.winfo_x()
sf_y = side_frame.winfo_y()
sf_w = side_frame.winfo_width()
sf_h = side_frame.winfo_height()
return sf_x, sf_y, sf_w, sf_h
def updater(self):
#self.create_frames() # Update Application Window
self.create_images() # Update Images Widget
self.after(UPDATE_RATE, self.updater) # Call "updater" function after 1 second
Then I start creating widgets. For the image (Label) widgets I would like to use grid, but I am having the hardest time figuring out how to reference the sub frame (side_frame) in the create_images function. I have truncated the important bits to make this question more to the point.
def create_images(self):
sf_dims = self.create_frames()
sf_x = sf_dims[0]
sf_y = sf_dims[1]
sf_w = sf_dims[2]
sf_h = sf_dims[3]
...
fp1_lbl = tk.Label(
master = pc_gui,
image = fp1
)
fp1_lbl.image = fp1
fp1_lbl.place(
x=sf_x + img_padding,
y=sf_y + 20,
anchor='nw'
)
Everything up to this point "works" admittedly rather inefficiently. What I would like to do is not have those sf_x, _y, _w, and _h hanging out there in the wind and it follows that I would also not have to call the frame function from widget function in order to get them.
The reason is that I feel it's not in the right spirit of python frames as I would like to use Grid on the side frame only but create (or use) Grid from the widget function (the point of creating the side_frame in the first place) and I would prefer to only refresh the sub parts of the application window that need to be refreshed, and not the whole smash every 1 second.
What ends up happening is the images flicker every 1 second, even when nothing needs to be updated and while the images do resize according to the application window I am doing this with the Place functionality and effectively ignoring the existence of side_frame.
My current attempts have been around the following
fp1_lbl = tk.Label(
master = self.side_frame,
image = fp1
)
fp1_lbl.image = fp1
fp1_lbl.place(
x=sf_x + img_padding,
y=sf_y + 20,
anchor='nw'
)
I get versions of the following error:
AttributeError: 'Application' object has no attribute 'side_frame'
You need to name things with the "self" prefix in order to use them in other methods. So when you create the subframes:
# Section of Application window dedicated to calculations
self.side_frame = tk.Frame(
master = pc_gui,
width = app_width*.25,
height = app_height,
bg = "red"
)
self.side_frame.place(
x = app_width*.75,
y = 0
)
Now you can use them anywhere in your class as you have tried:
fp1_lbl = tk.Label(
master = self.side_frame,
image = fp1
)
Remember there is no implied relationship between a variable named foo and self.foo. They are 2 completely unrelated names.
As for your resizing, tkinter will do that for you. You can use the relwidth and relheight arguments to set the width and height to a fraction between 0 and 1. Here's a demo:
import tkinter as tk
class Application(tk.Frame):
""" GUI """
def __init__(self, pc_gui):
""" Initialize the frame """
tk.Frame.__init__(self, pc_gui)
self.create_frames()
def create_frames(self):
# insert the subframes in *this* Frame (self), not in the master Frame
self.video_frame = tk.Frame(self, bg = "blue")
self.video_frame.place(relheight=1.0, relwidth=.25)
self.side_frame = tk.Frame(self, bg = "red")
self.side_frame.place(relheight=1.0, relwidth=.75, relx=1.0, anchor='ne')
pc_gui = tk.Tk()
pc_gui.geometry("1280x720")
win = Application(pc_gui)
win.pack(fill=tk.BOTH, expand=True)
tk.Button(pc_gui, text = "Don't click me!").pack()
pc_gui.mainloop()
I'm getting a message box: "Python has stopped working" when I load an image into a QLabel in a window that is already visible. Selecting debug shows: an unhandled Win32 exception occurred in Python.exe.
If I load the image into the label before showing the window, it displays correctly.
Here's the stripped down code:
#!/usr/bin/etc python
import sys
import os
import stat
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PIL import *
from PIL.ImageQt import *
def update(label):
filename = r"C:\Users\me\Pictures\images\00000229.jpg"
im1 = Image.open(filename)
print ("Read ({},{})".format(im1.width, im1.height))
im2 = im1.rotate(90, expand=True)
print("Rotate ({},{})".format(im2.width, im2.height))
im2.thumbnail((1200,1200))
print("Thumbnail({},{})".format(im2.width, im2.height))
qimage = ImageQt(im2)
pixmap = QPixmap.fromImage(qimage)
label.setPixmap(pixmap)
app = QApplication(sys.argv)
desktop = QDesktopWidget()
deskGeometry = desktop.availableGeometry()
print("desktop ({},{})".format(deskGeometry.width(), deskGeometry.height()))
window = QFrame()
# If you let QT pick the sizes itself, it picks dumb ones, then complains
# 'cause they are dumb
window.setMinimumSize(300, 200)
window.setMaximumSize(deskGeometry.width(), deskGeometry.height())
label = QLabel()
#call update here: no crash
caption = QLabel()
caption.setText("Hello world")
box = QVBoxLayout()
box.addWidget(label)
box.addWidget(caption)
#call update here, no crash
window.setLayout(box)
#call update here, no crash
window.show()
#this call to update results in a crash
update(label)
#window.updateGeometry()
print("App: exec")
app.exec_()
Output:
desktop (3623,2160)
Read (1515,1051)
Rotate (1051,1515)
Thumbnail(832,1200)
App: exec
Do I need to do anything special to tell QT that the window size will be changing? Any suggestions for diagnosing the problem from here...
Update:
If I copy the body of the update function and paste it in place of the call to update, it no long crashes -- it works as expected.
From this I conclude that there is an object-lifetime issue. Somewhere behind the scenes QT and/or Pillow is keeping a pointer to an internal buffer rather than making a copy or "stealing" the buffer. When the object containing the buffer is deleted the pointer becomes invalid and "Bad Things Happen[TM]"
Now to determine who's being lazy...
I found a solution based on the observation mentioned in the Update that this appeared to be an object lifetime issue.
Changing the line in the update function from
pixmap = QPixmap.fromImage(qimage)
to
pixmap = QPixmap.fromImage(qimage).copy()
forces a copy of the pixmap. This copy apparently has its own data buffer rather than borrowing the buffer from the Image.
The Label then keeps a reference to the pixmap -- ensuring the lifetime of the buffer. The 'bug' seems to be that QPixmap.fromImage captures a pointer to the data in the Image, but does not keep a reference to the Image so if the Image gets garbage collected (which is likely 'cause it's a big object, the Label (and the pixmap) have a pointer to unallocated memory.
[This 'pointer to the buffer' stuff is sheer speculation on my part, but the bottom line is the program no longer crashes.]
In case anybody else stumbles upon this:
I struggled with a very similar issue and was able to solve it using slots and signals (based on this great article) since the .copy() solution did not work for me. My solution was to separate the creation of the QPixmap and the QLabel.setPixmap into different functions. When the function that creates the QPixmap finishes, it emits a signal that triggers the function that sets the pixmap to the label.
For instance, if you wanted to use threading:
class WorkerSignals(QObject):
ready = pyqtSignal()
class Worker(QRunnable):
def __init__(self, some_function, *args, **kwargs):
super(Worker, self).__init__()
self.some_function = some_function
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
self.kwargs['ready'] = self.signals.ready
#pyqtSlot()
def run(self):
self.some_function(*self.args, **self.kwargs)
def make_pixmap(qimage, ready, **kwargs):
pixmap = QPixmap.fromImage(qimage) # qimage is some QImage
ready.emit()
def set_pixmap(pixmap):
label.setPixmap(pixmap) # 'label' is some QLabel
threadpool = QThreadPool
worker = Worker(make_pixmap, image)
worker.signals.ready.connect(set_pixmap)
threadpool.start(worker)
Obviously using threading is not necessary in this scenario, but this is just to show that it's possible in case you want to process and show images without hanging the rest of the GUI.
Edit: The crashes came back, disregard the above. Working on another fix.
I am trying to improve the user experience by showing a load mask above the active QMainWindow/QDialog when performing tasks that takes some time. I have managed to get it working as I want it, except for a moving GIF when performing the task. If I leave the load mask on after the task is complete, the GIF starts moving as it should.
My class for the load mask:
from PyQt4 import QtGui, QtCore
from dlgLoading_view import Ui_dlgLoading
class dlgLoading(QtGui.QDialog, Ui_dlgLoading):
def __init__(self,parent):
QtGui.QDialog.__init__(self,parent)
self.setupUi(self)
self.setWindowFlags(QtCore.Qt.WindowFlags(QtCore.Qt.FramelessWindowHint))
self.setGeometry(0, 0, parent.frameGeometry().width(), parent.frameGeometry().height())
self.setStyleSheet("background-color: rgba(255, 255, 255, 100);")
movie = QtGui.QMovie("loader.gif")
self.lblLoader.setMovie(movie)
movie.start()
def showEvent(self, event):
QtGui.qApp.processEvents()
super(dlgLoading, self).showEvent(event)
def setMessage(self,message):
self.lblMessage.setText(message)
The Ui_dlgLoading contains two labels and some vertical spacers: lblLoader (will contain the gif) and lblMessage (will contain a message if needed)
I create the load mask with this code:
loadmask = dlgLoading(self)
loadmask.setMessage('Reading data... Please wait')
loadmask.show()
I figured I needed some multithreading/multiprocessing, but I can't for the life of me figure out how to do it. I read somewhere that you can't tamper with the GUIs threading, so I would need to move the heavy task there instead, but I'm still blank.
As a simple example, let's say I am trying to load a huge file into memory:
file = open(dataFilename, 'r')
self.dataRaw = file.read()
file.close()
Around that I would create and close my load mask dialog. How do I start the file read without halting the GIF animation?
The GUI is for running some heavy external exe files, so it should work with that too.
I ended up doing this:
class runthread(threading.Thread):
def __init__(self, commandline, cwd):
self.stdout = None
self.stderr = None
self.commandline = commandline
self.cwd = cwd
self.finished = False
threading.Thread.__init__(self)
def run(self):
subprocess.call(self.commandline, cwd=self.cwd)
self.finished = True
class command()
def __init__(self):
...
def run():
...
thread = runthread("\"%s\" \"%s\"" % (os.path.join(self.__caller.exefolder, "%s.exe" % self.__cmdtype), self.__name), self.__caller.exeWorkdir)
thread.start()
count = 0
sleeptime = 0.5
maxcount = 60.0/sleeptime
while True:
time.sleep(sleeptime)
QtWidgets.qApp.processEvents()
count += 1
if thread.finished:
break
if count >= maxcount:
results = QtWidgets.QMessageBox.question(self.__caller, "Continue?", "The process is taking longer than expected. Do you want to continue?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if results == QtWidgets.QMessageBox.Yes:
count == 0
else:
QtWidgets.QMessageBox.warning(self.__caller, "Process stopped", "The process was stopped")
return False
It actually doesn't directly answer my question, but it worked for me, so I'm posting the answer if others want to do something similar.
I call a process (in this case Pythons subprocess.call) through a thread and track when the process is actually finished. A continuous loop checks periodically if the process is done and updates the GUI (processEvents - this is what triggers the GIF to update). To avoid an infinite loop I offer the user an option to exit after some time.
What I want to do is colour in a single pixel in the centre of the screen, then at random choose an adjacent pixel and colour it in, and then repeat until some condition is met - anything such as time, or the screen is full, or after a certain number of pixels are full. This ending isn't too important, I haven't got that far yet, and I think I could manage to work that out myself.
I have no experience with tkinter, but I decided it was the best way to display this, since I don't really no any other way. Some of this code (mainly the tkinter functions like Canvas, PhotoImage etc) is therefore copy-pasted (and slightly edited) from examples I found here.
What my code does when run is hard to tell - it uses the CPU as much as it can seemingly indefinitely, and slowly increases its memory usage, but doesn't appear to do anything. No window opens, and the IDLE interpreter goes to a blank line, as usual when calculating something. When killed, the window opens, and displays a white page with a little black blob in the bottom right corner - as if the program had done what it was meant to, but without showing it happening, and starting in the wrong place.
So:
Why does it do this?
What should I do to make my program work?
What would be a better way of coding this, changing as many things as you like (ie. no tkinter, a different algorithm etc)?
from tkinter import Tk, Canvas, PhotoImage, mainloop
from random import randrange
from time import sleep
def choose_pixel(pixel_list):
possible_pixels = []
for x in pixel_list:
#adjacent pixels to existing ones
a = [x[0] + 1, x[1]]
b = [x[0] - 1, x[1]]
c = [x[0], x[1] + 1]
d = [x[0], x[1] - 1]
#if a not in pixel_list:
possible_pixels.append(a)
#if b not in pixel_list:
possible_pixels.append(b)
#if c not in pixel_list:
possible_pixels.append(c)
#if d not in pixel_list:
possible_pixels.append(d)
pixel_choosing = randrange(len(possible_pixels))
final_choice = possible_pixels[pixel_choosing]
return final_choice
def update_image(img_name, pixel):
img.put("#000000", (pixel[0], pixel[1]))
WIDTH, HEIGHT = 320, 240
window = Tk()
#create white background image
canvas = Canvas(window, width=WIDTH, height=HEIGHT, bg="#ffffff")
canvas.pack()
img = PhotoImage(width=WIDTH, height=HEIGHT)
canvas.create_image((WIDTH, HEIGHT), image=img, state="normal")
first_pixel = [int(WIDTH/2), int(HEIGHT/2)]
pixel_list = [first_pixel]
img.put("#000000", (first_pixel[0], first_pixel[1]))
canvas.pack()
runs = 0
while True:
next_pixel = choose_pixel(pixel_list)
pixel_list.append(next_pixel)
window.after(0, update_image, img, next_pixel)
canvas.pack()
runs+=1
window.mainloop()
The pattern for running something periodically in tkinter is to write a function that does whatever you want it to do, and then the last thing it does is use after to call itself again in the future. It looks something like this:
import tkinter as tk
...
class Example(...):
def __init__(self, ...):
...
self.canvas = tk.Canvas(...)
self.delay = 100 # 100ms equals ten times a second
...
# draw the first thing
self.draw_something()
def draw_something(self):
<put your code to draw one thing here>
self.canvas.after(self.delay, self.draw_something)
After the function draws something, it schedules itself to run again in the future. The delay defines approximately how long to wait before the next call. The smaller the number, the faster it runs but the more CPU it uses. This works, because between the time after is called and the time elapses, the event loop (mainloop) is free to handle other events such as screen redraws.
While you may think this looks like recursion, it isn't since it's not making a recursive call. It's merely adding a job to a queue that the mainloop periodically checks.
I am attempting to create some simple asynchronously-executing animations based on ipythonblocks and I am trying to update the cell output area using clear_output() followed by a grid.show().
For text output the basis of the technique is discussed in Per-cell output for threaded IPython Notebooks so my simplistic assumption was to use the same method to isolate HTML output. Since I want to repeatedly replace a grid with its updated HTML version I try to use clear_output() to ensure that only one copy of the grid is displayed.
I verified that this proposed technique works for textual output with the following cells. First the context manager.
import sys
from contextlib import contextmanager
import threading
stdout_lock = threading.Lock()
n = 0
#contextmanager
def set_stdout_parent(parent):
"""a context manager for setting a particular parent for sys.stdout
(i.e. redirecting output to a specific cell). The parent determines
the destination cell of output
"""
global n
save_parent = sys.stdout.parent_header
# we need a lock, so that other threads don't snatch control
# while we have set a temporary parent
with stdout_lock:
sys.stdout.parent_header = parent
try:
yield
finally:
# the flush is important, because that's when the parent_header actually has its effect
n += 1; print("Flushing", n)
sys.stdout.flush()
sys.stdout.parent_header = save_parent
Then the test code
import threading
import time
class timedThread(threading.Thread):
def run(self):
# record the parent (uncluding the stdout cell) when the thread starts
thread_parent = sys.stdout.parent_header
for i in range(3):
time.sleep(2)
# then ensure that the parent is the same as when the thread started
# every time we print
with set_stdout_parent(thread_parent):
print(i)
timedThread().start()
This provided the output
0
Flushing 1
1
Flushing 2
2
Flushing 3
So I modified the code to clear the cell between cycles.
import IPython.core.display
class clearingTimedThread(threading.Thread):
def run(self):
# record the parent (uncluding the stdout cell) when the thread starts
thread_parent = sys.stdout.parent_header
for i in range(3):
time.sleep(2)
# then ensure that the parent is the same as when the thread started
# every time we print
with set_stdout_parent(thread_parent):
IPython.core.display.clear_output()
print(i)
clearingTimedThread().start()
As expected the output area of the cell was repeatedly cleared, and ended up reading
2
Flushing 6
I therefore thought I was on safe ground in using the same technique to clear a cell's output area when using ipythonblocks. Alas no. This code
from ipythonblocks import BlockGrid
w = 10
h = 10
class clearingBlockThread(threading.Thread):
def run(self):
grid = BlockGrid(w, h)
# record the parent (uncluding the stdout cell) when the thread starts
thread_parent = sys.stdout.parent_header
for i in range(10):
# then ensure that the parent is the same as when the thread started
# every time we print
with set_stdout_parent(thread_parent):
block = grid[i, i]
block.green = 255
IPython.core.display.clear_output(other=True)
grid.show()
time.sleep(0.2)
clearingBlockThread().start()
does indeed produce the desired end state (a black matrix with a green diagonal) but the intermediate steps don't appear in the cell's output area. To complicate things slightly (?) this example is running on Python 3. In checking before posting here I discover that the expected behavior (a simple animation) does in fact occur under Python 2.7. Hence I though to ask whether this is an issue I need to report.