Python Tkinter:如何安全用update()替代mainloop实现CV2与GUI共存
我希望用update()和update_idletask()而非mainloop()的原因:
- 需要显示Basler Dart USB相机在独立线程采集的全高清50FPS流,同时保持Tkinter GUI窗口置顶
- 将cv2 Mat转tk PhotoImage并更新画布/标签的操作最优耗时超30ms,远慢于采集帧+位运算叠加透明图像的数毫秒速度,这种显示方式无法满足需求
为了复用现有代码、避免学习新GUI框架,我尝试同时显示cv2.imshow全屏窗口和tk.Tk置顶窗口的方案,但看到cpython _tkinter.c中关于线程锁的警告,不确定该方案是否合理,以及正确实现方式。
我查过一些线程同时显示tk和cv2窗口的方案但都没生效,可能是因为我的CV2Window内的图像采集已经在独立线程中。直接在cv2循环里调用update()很容易实现,但不确定是否安全:如果用线程安全队列实现两窗口通信,且Tkinter事件无过长阻塞,忽略tcl锁直接用update()是否可行?
当前简化代码
主程序代码
# Standard library imports from cv2 import waitKey from sys import exit # Local application imports from CV2Window import CV2Window from tkWindows import MenuWindow, MenuFrame # child class of tk.Tk # -topmost and overrideredirect True, geometry "+0+0" class MenuApp(MenuWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # frame withdraws and deiconifies buttons when the menu toggle button is clicked, # to create a dropdown-menu. Row 0 is used by the toggle button self.menu_frame = MenuFrame(self) self.menu_frame.add_button("Example Button", command=None, icon="file-plus", row=1) self.menu_frame.add_button("Close", command=self.close, icon="power", row=2) self.menu_frame.grid() def close(self): self.destroy() def main(): cv2window = CV2Window() gui = MenuApp() while True: ## Update cv2 window cv2window.update() ## Update GUI manually gui.update() ## Check if GUI is still open, otherwise close cv2 window try: gui.winfo_exists() except: print("GUI closed, closing cv2 window..") cv2window.close() break
CV2Window代码
# Standard library imports from cv2 import namedWindow, setWindowProperty, imshow, WINDOW_FULLSCREEN, WND_PROP_FULLSCREEN, waitKey, destroyAllWindows from pypylon.genicam import GenericException # Local application imports from CameraClass import MyCameraHandler class CV2Window(): def __init__(self): try: self.cam_handler = MyCameraHandler() self.cam_handler.start_grabbing() except GenericException as e: print("Exception in CV2Window: ", e) try: self.cam_handler.stop() except: pass exit(1) self.img = self.cam_handler.get_image() namedWindow("cam", WND_PROP_FULLSCREEN) setWindowProperty("cam", WND_PROP_FULLSCREEN, WINDOW_FULLSCREEN) imshow("cam", self.img) def update(self): self.img = self.cam_handler.get_image() waitKey(0) imshow("cam", self.img) def close(self): destroyAllWindows() self.cam_handler.stop()
潜在问题与疑问
从cpython _tkinter.c中看到的线程相关说明:
The threading situation is complicated. Tcl is not thread-safe, except
when configured with --enable-threads.
So we need to use a lock around all uses of Tcl. Previously, the
Python interpreter lock was used for this. However, this causes
problems when other Python threads need to run while Tcl is blocked
waiting for events.
使用mainloop()时Tkinter会正确管理锁,但无法同时显示和更新CV2Window。我理解update()只是单次tcl调用,没有锁管理。
目前代码能运行,但两窗口无通信,还有以下疑问:
- 需要从Tkinter GUI调用CV2Window的方法,未来可能还要从CV2Window向Tkinter传递少量数据,打算用
queue.Queue实现通信(只需传递信息/操作指令,无需共享图像,且queue.Queue是线程安全的),这是否可行? - 只要
update()触发的事件耗时小于约15ms,加上帧采集和imshow操作,就能满足<20ms的帧时间要求,对吗? - 我有没有遗漏什么关键点?刚接触Python和Tkinter,对Tkinter调用的tcl相关内容理解有难度,希望得到帮助。
内容的提问来源于stack exchange,提问作者Alex-Schaefer

