OpenCV (Python的cv2) 删除后未释放相机的VideoCapture

7 投票
3 回答
39508 浏览
提问于 2025-04-17 19:23

我刚学会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 个回答

0

你能试试这段代码,看看你能得到多少帧每秒(FPS)吗?我加了一个FPS计算,这样我们可以对比一下结果。(补充:还有错误信息。我在原始代码中没有遇到你说的错误,而下面的代码我没有任何错误。)

我从头开始写这段代码,看看能不能做出不同的东西。这里有几个不同之处:

  1. 有一个(小的?)bug:opencv默认的颜色通道是BGR,而不是RGB。所以你需要把灰度转换的代码从 cv2.COLOR_RGB2GRAY 改成 cv2.COLOR_BGR2GRAY。你可以在这个VideoCapture示例中看到类似的做法。
  2. 我用一个简单的标签来显示图像,而不是用画布。我之前没用过画布,所以不太清楚需要怎么处理。用简单的标签时,你需要保持对你显示的图像的引用,这样它才不会被垃圾回收。你可以在update_image()函数中看到这一点。
  3. 关于回调,我使用了带参数的lambda表达式(就像你在评论中提到的)。否则,当你用参数调用一个函数时,回调会立即执行,而不是注册它。看起来好像在正常工作,但其实并不是你想的那样。或者,如果你更喜欢打包参数并发送一个未调用的函数,可以使用functools.partial
  4. 另外,对于回调,我添加了一个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()
1

在你初始化摄像头对象之前,先设置环境变量。

os.environ['OPENCV_VIDEOIO_PRIORITY_MSMF'] = '0'

这样即使在我的代码中关闭了摄像头对象,摄像头也会被释放。

9

解决了!在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

撰写回答