OpenCV Python如何从同一摄像头取流同时实现Tkinter展示与QR码解码
问题描述
我开发的程序需要通过tkinter展示树莓派摄像头的画面,同时对进入画面的QR码进行解码,运行时持续报错:TypeError: cannot unpack non-iterable NoneType object,经排查是解码函数无法访问摄像头流导致。我已尝试将frame设为全局变量,但问题未得到解决。
原始代码
# import all the necessary modules from tkinter import * from multiprocessing import Process, Queue from queue import Empty # for excepting a specific error import cv2 import pyzbar.pyzbar as pyzbar import sys import os import requests from PIL import Image, ImageTk import time def resetGUI(): root.configure(bg='white') nameFrame.configure(bg='white') instructionsFrame.configure(bg='white') nameLabel.config(text="", bg='white') instructionsLabel.config(text="Place your QR code under the scanner below.", bg='white') def processScan(code): if code != "": print (code) #some post request return ["hellllllo"] def showVideo(): _, frame = vid.read() frame = cv2.flip(frame, 1) cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) img = Image.fromarray(cv2image) imgtk = ImageTk.PhotoImage(image=img) lmain.imgtk = imgtk lmain.configure(image=imgtk) cv2.waitKey(5) cv2.destroyAllWindows lmain.after(10, showVideo) # this is the function that will be run in a child process def startScanning(queue_): i = 0 while i<2: # _,f = vid.read() _,frame = vid.read() decoded = pyzbar.decode(frame) for obj in decoded: queue_.put(obj.data) print('decoded') #cv2.imshow('QRCode',f) #cv2.waitKey(5) #cv2.destroyAllWindows #time.sleep(1) # just a function to not write a `lambda`, just easier # to read code def start_process(): Process(target=startScanning, args=(queue, ), daemon=True).start() def updateGUI(): try: data = queue.get(block=False) while not queue.empty(): queue.get() except Empty: pass else: #queue.clear() code = processScan(data) if code: if len(code) > 1: #Pass ACCEPTED root.configure(bg='green'): #Pass DENIED root.after(3000, resetGUI) finally: root.after(100, updateGUI) # crucial part is to use this if statement because # child processes run this whole script again if __name__ == '__main__': root = Tk() logo = "logo.png" settings = "settings.png" if os.environ.get('DISPLAY','') == '': print('no display found. Using :0.0') os.environ.__setitem__('DISPLAY', ':0.0') #create main window #root.attributes("-fullscreen", True) root.title("Title") root.geometry("480x800") root.configure(bg='white') ttelogo = PhotoImage(file = logo) settingslogo = PhotoImage(file = settings) #settings button settingsFrame = Frame(root,width=50,height=50,bg="white") settingsFrame.pack_propagate(0) # Stops child widgets of label_frame from resizing it settingsBtn = Button(settingsFrame, image=settingslogo).pack() settingsFrame.place(x=430,y=0) #logo img = Label(root, image=ttelogo, bg='white') img.image = ttelogo img.place(x=176.5,y=10) #Name Label nameFrame = Frame(root,width=400,height=100,bg="white") nameFrame.pack_propagate(0) # Stops child widgets of label_frame from resizing it nameLabel = Label(nameFrame,bg="white",fg="black",text="",font=("Calibri",22)) nameLabel.pack() nameFrame.place(x=40,y=140) #Instructions Label instructionsFrame = Frame(root,width=440,height=100,bg="white") instructionsFrame.pack_propagate(0) # Stops child widgets of label_frame from resizing it instructionsLabel = Label(instructionsFrame,bg="white",fg="black",text="Hello",font=("Calibri",15)) instructionsLabel.pack() instructionsFrame.place(x=20,y=210) #Camera Window cameraFrame = Frame(root, width=440, height=480) cameraFrame.place(x=20, y=260) #Camera Feed lmain = Label(cameraFrame) lmain.place(x=0, y=0) vid = cv2.VideoCapture(0) # define queue (since it is a global variable now # it can be easily used in the functions) queue = Queue() #label = Label(root) #label.pack() # initially start the update function updateGUI() # just a button for starting the process, but you can also simply # call the function start_process() showVideo() root.mainloop()
迭代版本1:改用Threading并移除冗余代码
# import all the necessary modules from tkinter import * from multiprocessing import Process, Queue from queue import Empty # for excepting a specific error import cv2 import pyzbar.pyzbar as pyzbar import sys import os import requests from PIL import Image, ImageTk import time import threading def resetGUI(): root.configure(bg='white') nameFrame.configure(bg='white') instructionsFrame.configure(bg='white') nameLabel.config(text="", bg='white') instructionsLabel.config(text="Place your QR code under the scanner below.", bg='white') def processScan(code): if code != "": print (code) #some post request return ["hellllllo"] def showVideo(): vid = cv2.VideoCapture(0) _, frame = vid.read() frame = cv2.flip(frame, 1) cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) img = Image.fromarray(cv2image) imgtk = ImageTk.PhotoImage(image=img) lmain.imgtk = imgtk lmain.configure(image=imgtk) cv2.waitKey(5) cv2.destroyAllWindows lmain.after(10, showVideo) # this is the function that will be run in a child process def startScanning(queue_): i = 0 while i<2: vid = cv2.VideoCapture(0) # _,f = vid.read() _,frame = vid.read() decoded = pyzbar.decode(frame) for obj in decoded: queue_.put(obj.data) print('decoded') def start_process(): threading.Thread(target=startScanning, args=(queue, ), daemon=True).start() def updateGUI(): try: data = queue.get(block=False) while not queue.empty(): queue.get() except Empty: pass else: #queue.clear() code = processScan(data) if code: if len(code) > 1: #Pass ACCEPTED root.configure(bg='green'): #Pass DENIED root.after(3000, resetGUI) finally: root.after(100, updateGUI) if __name__ == '__main__': root = Tk() logo = "logo.png" settings = "settings.png" if os.environ.get('DISPLAY','') == '': print('no display found. Using :0.0') os.environ.__setitem__('DISPLAY', ':0.0') #create main window #root.attributes("-fullscreen", True) root.title("Title") root.geometry("480x800") root.configure(bg='white') #Camera Window cameraFrame = Frame(root, width=440, height=480) cameraFrame.place(x=20, y=260) #Camera Feed lmain = Label(cameraFrame) lmain.place(x=0, y=0) queue = Queue() # initially start the update function updateGUI() start_process() showVideo() root.mainloop()
迭代版本2:尝试将vid设为全局变量
from tkinter import * from multiprocessing import Process, Queue from queue import Empty # for excepting a specific error import cv2 import pyzbar.pyzbar as pyzbar import sys import os import requests from PIL import Image, ImageTk import time import threading def resetGUI(): root.configure(bg='white') nameFrame.configure(bg='white') instructionsFrame.configure(bg='white') nameLabel.config(text="", bg='white') instructionsLabel.config(text="Place your QR code under the scanner below.", bg='white') def processScan(code): if code != "": print (code) #some post request return ["hellllllo"] def showVideo(): global vid vid = cv2.VideoCapture(0) _, frame = vid.read() frame = cv2.flip(frame, 1) cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) img = Image.fromarray(cv2image) imgtk = ImageTk.PhotoImage(image=img) lmain.imgtk = imgtk lmain.configure(image=imgtk) cv2.waitKey(5) cv2.destroyAllWindows lmain.after(10, showVideo) # this is the function that will be run in a child process def startScanning(queue_): i = 0 while i<2: # _,f = vid.read() _,frame = vid.read() decoded = pyzbar.decode(frame) for obj in decoded: queue_.put(obj.data) print('decoded') def start_process(): threading.Thread(target=startScanning, args=(queue, ), daemon=True).start() def updateGUI(): try: data = queue.get(block=False) while not queue.empty(): queue.get() except Empty: pass else: #queue.clear() code = processScan(data) if code: if len(code) > 1: #Pass ACCEPTED root.configure(bg='green'): #Pass DENIED root.after(3000, resetGUI) finally: root.after(100, updateGUI) if __name__ == '__main__': root = Tk() logo = "logo.png" settings = "settings.png" if os.environ.get('DISPLAY','') == '': print('no display found. Using :0.0') os.environ.__setitem__('DISPLAY', ':0.0') #create main window #root.attributes("-fullscreen", True) root.title("Title") root.geometry("480x800") root.configure(bg='white') #Camera Window cameraFrame = Frame(root, width=440, height=480) cameraFrame.place(x=20, y=260) #Camera Feed lmain = Label(cameraFrame) lmain.place(x=0, y=0) queue = Queue() # initially start the update function updateGUI() showVideo() start_process() root.mainloop()
最终报错信息
[ WARN:0] global /root/opencv/modules/videoio/src/cap_v4l.cpp (890) open VIDEOIO(V4L2:/dev/video0): can't open camera by index Exception in Tkinter callback Exception in thread Thread-1: Traceback (most recent call last): File "/usr/lib/python3.7/threading.py", line 917, in _bootstrap_inner self.run() File "/usr/lib/python3.7/threading.py", line 865, in run self._target(*self._args, **self._kwargs) File "stackoverflow2.py", line 49, in startScanning decoded = pyzbar.decode(frame) File "/usr/local/lib/python3.7/dist-packages/pyzbar/pyzbar.py", line 181, in decode pixels, width, height = _pixel_data(image) File "/usr/local/lib/python3.7/dist-packages/pyzbar/pyzbar.py", line 147, in _pixel_data pixels, width, height = image TypeError: cannot unpack non-iterable NoneType object Traceback (most recent call last): File "/usr/lib/python3.7/tkinter/__init__.py", line 1705, in __call__ return self.func(*args) File "/usr/lib/python3.7/tkinter/__init__.py", line 749, in callit func(*args) File "stackoverflow2.py", line 34, in showVideo cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) cv2.error: OpenCV(4.5.3) /root/opencv/modules/imgproc/src/color.cpp:182: error: (-215:Assertion failed) !_src.empty() in function 'cvtColor'
解决方案
核心问题原因
- Linux下V4L2协议的摄像头同一时间只能被一个进程/线程独占,你在多个位置重复调用
cv2.VideoCapture(0)会导致后续打开请求直接失败,vid.read()会返回(False, None),空的frame传入后续处理逻辑就会触发你遇到的两类报错。 - 你在
showVideo函数的循环里每次都重新初始化摄像头,会导致摄像头持续被抢占,根本无法正常读取帧。 - 缺少帧读取成功校验,拿到空frame后直接做处理,没有做异常拦截。
修复步骤
- 全局仅初始化一次摄像头实例,放在主程序入口处,不要在循环或者子线程里重复打开。
- 所有逻辑共用同一个摄像头实例,扫码线程不需要单独读帧,直接用画面展示逻辑已经读取到的全局frame即可,避免冲突。
- 每次调用
vid.read()后先判断返回的第一个值是否为True,确认帧读取成功后再执行后续操作。 - 不要在子进程里操作摄像头,改用线程的情况下可以正常共享同一个摄像头实例。
修复后核心代码示例
from tkinter import * from multiprocessing import Queue from queue import Empty import cv2 import pyzbar.pyzbar as pyzbar import os from PIL import Image, ImageTk import threading # 全局变量存储当前帧 current_frame = None def resetGUI(): root.configure(bg='white') nameFrame.configure(bg='white') instructionsFrame.configure(bg='white') nameLabel.config(text="", bg='white') instructionsLabel.config(text="Place your QR code under the scanner below.", bg='white') def processScan(code): if code != "": print(code) # 你的post请求逻辑 return ["hellllllo"] def showVideo(): global current_frame ret, frame = vid.read() if not ret: lmain.after(10, showVideo) return current_frame = frame.copy() frame = cv2.flip(frame, 1) cv2image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) img = Image.fromarray(cv2image) imgtk = ImageTk.PhotoImage(image=img) lmain.imgtk = imgtk lmain.configure(image=imgtk) lmain.after(10, showVideo) def startScanning(queue_): while True: if current_frame is None: continue decoded = pyzbar.decode(current_frame) for obj in decoded: queue_.put(obj.data) print('decoded') def start_process(): threading.Thread(target=startScanning, args=(queue, ), daemon=True).start() def updateGUI(): try: data = queue.get(block=False) while not queue.empty(): queue.get() except Empty: pass else: code = processScan(data)
相关产品推荐
相关产品推荐

