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

7

我对Python还比较陌生,只是在过去的一个月左右学习了一些,并根据在网上找到的示例和其他人的代码拼凑出了这个程序。

我已经用Tkinter GUI显示了来自网络摄像头的视频流,作为画布上连续更新图像的循环。每隔一段时间退出GUI并重新运行脚本会导致出现以下错误:

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,因此我知道关闭GUI后它已被正确删除。我不明白为什么cv2的read函数会产生错误。我认为这只是每隔一次才会发生,因为当错误发生时,某些垃圾回收或错误处理会删除或释放与相机有关的某些内容,但我不知道这是什么……

下面是我的代码:

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

在 GUI 中不显示按钮,并在关闭窗口后给出此错误:

RuntimeError: Too early to create image

我有3个问题
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()更快,更有效的函数?编辑:是否有一种重构我的代码以获得更高帧率的方法?读取功能是唯一列在文档中的功能,并且我刚刚在某个地方读到,如果它不在文档中,则不可用。该方法仅给我提供约5fps,而10-20fps将更加可接受。 更新:根据kobejohn对不同摄像头进行测试的差异,低帧速率是质量较差的网络摄像头的结果。 更好质量的网络摄像头具有更高的帧速率。
3-我一直在阅读应尽可能避免update(),但我如何使画布重新绘制图像(或在此代码中实现update_idletasks())? 我必须实施某种线程,还是可以避免这种情况? 更新:我已成功使代码工作而不使用update()方法,但仍需考虑实施线程,因为当我从按钮启动视频源的录制时,主GUI会冻结/变得无响应。
成品程序将用于Ubuntu和Windows(可能也适用于Mac)。 我正在运行Windows 7,IDE是Spyder 2.1.11(Python 2.7.3)。
提前感谢您的建议和/或解决方案!
问候,
S. Chia

我已经多次运行了您的原始代码,对我来说它没有出现任何错误就停止/重新启动了。您可以尝试另一种摄像头吗?也许这是摄像头驱动程序的问题? - KobeJohn
为什么我们不专注于第一段代码呢?我认为第二段代码至少有一个错误:root.after(0,update_video(cam,root,canvas)) 注册的是 update_video(...) 的返回值,而不是函数 update_video 本身。 - KobeJohn
我也得到了高帧率(可能在20-30左右?)的信息。 - KobeJohn
我也试过第一个,但是在我退出后它无法重新启动。 - b_m
很抱歉让大家等了这么久才更新。我一直在努力寻找第三个网络摄像头,但不想购买一个新的。我先是测试了我的摄像头和一个便宜的摄像头,但它们都出现了同样的问题。我又测试了第三个摄像头,但问题仍然存在。也许这只是我的机器和/或我的Windows Python安装出了问题。我会找一台不同的机器来测试代码并再次更新。 - S. Chia
我仍然没有能够获得另一台机器,但已经修复了第二个代码,以便不再出现特定的错误。感谢您的第二条评论,kobejohn =D。您是如何测试返回值的?我将 "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)"。这之前已经解决了我在其他Tkinter回调中遇到的问题,所以我想试一试。 - S. Chia
3个回答

9
解决了!OpenCV 2.4.2/ cv2在python中的使用
由于某些原因,在其他论坛和页面上,我找不到'release'方法,这些页面明确提到opencv的python绑定不包括'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

1
在初始化opencv相机对象之前设置环境变量。
os.environ['OPENCV_VIDEOIO_PRIORITY_MSMF'] = '0'

这使得即使在我的代码中关闭相机对象后也可以释放相机。

0

你能试试这段代码并查看你得到的FPS吗?我加入了一个FPS计算,这样我们可以比较一下。 (编辑:还有错误。我在原始代码中没有得到你得到的错误,并且使用下面的代码时我没有得到任何错误)

我从头开始编写代码,只是想看看是否会得到不同的结果。有几个区别:

  1. 有一个(小?)bug:opencv默认的颜色通道是BGR而不是RGB。所以将你的灰度转换从cv2.COLOR_RGB2GRAY改为cv2.COLOR_BGR2GRAY。你可以在VideoCapture example中看到他们做了类似的事情。
  2. 我使用了一个简单的标签来显示图像,而不是画布。我以前没有使用过画布,所以我不确定你需要做什么。使用简单的标签,你必须保留对你正在显示的图像的引用,这样它就不会被垃圾回收。你可以在update_image()中看到这一点。
  3. 对于回调,我使用带参数的lambda函数(正如你在评论中提到的)。否则,当你使用带参数的函数调用时,你会立即运行回调函数,而不是注册它。最终看起来好像它正在工作,但它并没有做你想要的事情。或者,如果你喜欢打包你的参数并将其作为未调用的函数发送,你也可以使用functools.partial
  4. 同样对于回调,我添加了一个try: except块,以防回调在root被销毁后开始运行。我不知道这是否是“正确”的方法,但据我所知,它是有效的。
使用这段代码,在Windows 7上我可以获得15帧每秒的流畅体验,且没有任何错误:
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()

嗨Kobejohn,感谢您花费时间和精力!不幸的是,我仍然遇到相同的错误,并且使用您的代码获得的帧速率与使用我的原始代码获得的帧速率相同... =( 我认为您是正确的,这可能是相机驱动程序(和质量)问题。try:except块是一个好主意,感谢您指出了那个错误!我还发现了另一种将参数传递给回调函数的方法:“root.after(time_to_wait,callback,arguments,master)”,它可以完成工作并看起来更清洁。 - S. Chia

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接