OpenCV (Python的cv2) 删除后未释放相机的VideoCapture
我刚学会Python,差不多一个月前开始接触的,最近根据网上找到的例子和别人的代码拼凑了一个程序。
我用Tkinter做了一个图形界面,可以在画布上显示摄像头的实时画面,画面会不断更新。但是每次关闭图形界面再重新运行脚本时,偶尔会出现一个错误:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python27\lib\lib-tk\Tkinter.py", line 1410, in __call__
return self.func(*args)
File "C:\Python27\lib\lib-tk\Tkinter.py", line 495, in callit
func(*args)
File "C:\...\cv2_cam_v8.py", line 20, in update_video
(self.readsuccessful,self.f) = self.cam.read()
SystemError: NULL object passed to Py_BuildValue
出现错误时,程序无法读取任何图像,视频流也无法更新画布。第一次运行和每隔一次运行时,脚本都能正常工作,没有错误。根据我之前对cv2模块中VideoCapture函数的测试,我发现必须删除摄像头对象才能释放它,这样后续的运行才能正常捕捉到摄像头的画面。我在控制台输入who
检查命名空间时,没有看到cam
,所以我知道在关闭图形界面后它确实被删除了。我不明白为什么cv2的读取函数会出错。我觉得这个错误每隔一次出现,是因为在错误发生时,某些垃圾回收或错误处理机制删除或释放了与摄像头相关的东西,但我不知道具体是什么...
这是我的代码:
import cv2
import Tkinter as tk
from PIL import Image, ImageTk
class vid():
def __init__(self,cam,root,canvas):
self.cam = cam
self.root = root
self.canvas = canvas
def update_video(self):
(self.readsuccessful,self.f) = self.cam.read()
self.gray_im = cv2.cvtColor(self.f, cv2.COLOR_RGB2GRAY)
self.a = Image.fromarray(self.gray_im)
self.b = ImageTk.PhotoImage(image=self.a)
self.canvas.create_image(0,0,image=self.b,anchor=tk.NW)
self.root.update()
self.root.after(33,self.update_video)
if __name__ == '__main__':
root = tk.Tk()
videoframe = tk.LabelFrame(root,text='Captured video')
videoframe.grid(column=0,row=0,columnspan=1,rowspan=1,padx=5, pady=5, ipadx=5, ipady=5)
canvas = tk.Canvas(videoframe, width=640,height=480)
canvas.grid(column=0,row=0)
cam = cv2.VideoCapture(2)
x = vid(cam,root,canvas)
root.after(0,x.update_video)
button = tk.Button(text='Quit',master=videoframe,command=root.destroy)
button.grid(column=0,row=1)
root.mainloop()
del cam
像这样重构代码:
def update_video(cam,root,canvas):
(readsuccessful,f) = cam.read()
gray_im = cv2.cvtColor(f, cv2.COLOR_RGB2GRAY)
a = Image.fromarray(gray_im)
b = ImageTk.PhotoImage(image=a)
canvas.create_image(0,0,image=b,anchor=tk.NW)
root.update()
root.after(33,update_video(cam,root,canvas))
if __name__ == '__main__':
root = tk.Tk()
videoframe = tk.LabelFrame(root,text='Captured video')
videoframe.grid(column=0,row=0,columnspan=1,rowspan=1,padx=5, pady=5, ipadx=5, ipady=5)
canvas = tk.Canvas(videoframe, width=640,height=480)
canvas.grid(column=0,row=0)
cam = cv2.VideoCapture(2)
root.after(0,update_video(cam,root,canvas))
button = tk.Button(text='Quit',master=videoframe,command=root.destroy)
button.grid(column=0,row=1)
root.mainloop()
del cam
在图形界面中没有显示按钮,并且在关闭窗口后出现了这个错误:
RuntimeError: Too early to create image
我有三个问题
1 - 我该如何避免这些异常? 更新:把“root.after(0,update_video(cam,root,canvas))”改成“root.after(0,lambda: update_video(cam,root,canvas))”以及把“update_video(cam,root,canvas)”改成“update_video(cam,root,canvas,event=None)”或者用这种格式传递参数:“root.after(time_to_wait, callback, arguments, master)”解决了第二个错误(还有其他一些我没有提到的错误)。另外,正如kobejohn指出的,添加一个try: except块也能解决第二个错误。更多细节请查看他的回答。
2 - 在cv2中有没有比.read()更快、更高效的函数?编辑:有没有办法重构我的代码以获得更高的帧率?文档中只列出了读取函数,我刚看到有说如果不在文档中,那就不可用。这个方法只能给我大约5帧每秒,而10-20帧每秒会更可接受。 更新:根据kobejohn和我在不同摄像头上的测试差异,低帧率是由于摄像头质量差造成的。更好的摄像头能提供更高的帧率。
3 - 我看到说尽量避免使用update(),那我该如何让画布重新绘制图像呢(或者如何在这段代码中实现update_idletasks())?我需要实现某种线程吗,还是可以避免? 更新:我已经让代码在不使用update()方法的情况下工作,但我还是得考虑实现线程,因为当我从主图形界面点击按钮开始录制视频流时,程序会冻结/变得无响应。
完成的程序将用于Ubuntu和Windows(可能也会在Mac上使用)。我现在在使用Windows 7,IDE是Spyder 2.1.11(Python 2.7.3)。
谢谢大家,任何建议和解决方案都非常感谢!
祝好,
S. Chia
3 个回答
你能试试这段代码,看看你能得到多少帧每秒(FPS)吗?我加了一个FPS计算,这样我们可以对比一下结果。(补充:还有错误信息。我在原始代码中没有遇到你说的错误,而下面的代码我没有任何错误。)
我从头开始写这段代码,看看能不能做出不同的东西。这里有几个不同之处:
- 有一个(小的?)bug:opencv默认的颜色通道是BGR,而不是RGB。所以你需要把灰度转换的代码从
cv2.COLOR_RGB2GRAY
改成cv2.COLOR_BGR2GRAY
。你可以在这个VideoCapture示例中看到类似的做法。 - 我用一个简单的标签来显示图像,而不是用画布。我之前没用过画布,所以不太清楚需要怎么处理。用简单的标签时,你需要保持对你显示的图像的引用,这样它才不会被垃圾回收。你可以在update_image()函数中看到这一点。
- 关于回调,我使用了带参数的lambda表达式(就像你在评论中提到的)。否则,当你用参数调用一个函数时,回调会立即执行,而不是注册它。看起来好像在正常工作,但其实并不是你想的那样。或者,如果你更喜欢打包参数并发送一个未调用的函数,可以使用functools.partial。
- 另外,对于回调,我添加了一个try: except块,以防回调在root被销毁后开始运行。我不知道这是否是“正确”的做法,但就我所知,这样是有效的。
用这段代码,我在Windows 7上得到了15 FPS,没有任何错误:
from collections import deque
import cv2
import Image, ImageTk
import time
import Tkinter as tk
def quit_(root):
root.destroy()
def update_image(image_label, cam):
(readsuccessful, f) = cam.read()
gray_im = cv2.cvtColor(f, cv2.COLOR_BGR2GRAY)
a = Image.fromarray(gray_im)
b = ImageTk.PhotoImage(image=a)
image_label.configure(image=b)
image_label._image_cache = b # avoid garbage collection
root.update()
def update_fps(fps_label):
frame_times = fps_label._frame_times
frame_times.rotate()
frame_times[0] = time.time()
sum_of_deltas = frame_times[0] - frame_times[-1]
count_of_deltas = len(frame_times) - 1
try:
fps = int(float(count_of_deltas) / sum_of_deltas)
except ZeroDivisionError:
fps = 0
fps_label.configure(text='FPS: {}'.format(fps))
def update_all(root, image_label, cam, fps_label):
update_image(image_label, cam)
update_fps(fps_label)
root.after(20, func=lambda: update_all(root, image_label, cam, fps_label))
if __name__ == '__main__':
root = tk.Tk()
# label for the video frame
image_label = tk.Label(master=root)
image_label.pack()
# camera
cam = cv2.VideoCapture(0)
# label for fps
fps_label = tk.Label(master=root)
fps_label._frame_times = deque([0]*5) # arbitrary 5 frame average FPS
fps_label.pack()
# quit button
quit_button = tk.Button(master=root, text='Quit',
command=lambda: quit_(root))
quit_button.pack()
# setup the update callback
root.after(0, func=lambda: update_all(root, image_label, cam, fps_label))
root.mainloop()
在你初始化摄像头对象之前,先设置环境变量。
os.environ['OPENCV_VIDEOIO_PRIORITY_MSMF'] = '0'
这样即使在我的代码中关闭了摄像头对象,摄像头也会被释放。
解决了!在Python中使用OpenCV 2.4.2/cv2
出于某种奇怪的原因,我之前找不到'release'这个方法,其他论坛和页面特别提到Python对OpenCV的绑定不包括这个'release'方法。也许这只适用于使用'import cv'的情况。我最开始是用后者进行原型设计的,结果在寻找ReleaseCapture方法时,错过了cv2中的'release'方法。
刚在文档中找到了它:http://docs.opencv.org/modules/highgui/doc/reading_and_writing_images_and_video.html
import cv2
cam=cv2.VideoCapture(0)
cam.release