基于cv2.VideoCapture 和 OpenCV 得到更快的 FPS之Webcam篇
时间:2022-08-28 07:30:00
使用线程处理 I/O 繁重的任务(如从相机传感器读取帧)是一种已经存在了几十年的编程模型。
例如,如果我们想建立一个网络爬虫来捕捉一系列网页(根据定义,这个任务是 I/O 绑定),我们的主程序将生成多个线程来处理并行下载这组页面,而不仅仅是依靠单个线程(我们的主线程)。这可以让我们更快地掌握网页。
同样的概念也适用于计算机视觉中从相机读取帧——我们可以简单地创建一个新的线程来改进我们 FPS,该线程轮询相机获取新帧,我们的主线程处理当前帧。
这是一个简单的概念,但它在 OpenCV 例子中很少见,因为它确实为项目添加了几行额外的代码(或者有时许多行取决于您的线程库)。多线程也可以使您的程序更难调试,但一旦您做对了,您可以显著改进 FPS。
我们将编写一个线程 Python 类来开始这一系列文章,以使用 OpenCV 访问您的网络摄像头或 USB 摄像头。
1.使用线程获得更高的线程 FPS
使用 OpenCV 处理视频流时获得更高的收获 FPS 秘密是将军 I/O(即从相机传感器读取帧)移动到单独的线程。
您可以看到,使用 cv2.VideoCapture 函数和 .read() 访问您的网络摄像头/USB 相机是一塞操作。 Python 在从相机设备读取帧并返回我们的脚本之前,脚本的主线程被完全堵塞(即停滞)。
I/O 任务,与 CPU 相反,绑定操作往往非常缓慢。虽然计算机视觉和视频处理应用程序肯定会占用很多 CPU(特别是如果他们打算实时运行),但事实证明,相机 I/O 也许是一个巨大的瓶颈。
正如我们将在本文后面看到的,只调整相机 I/O 这个过程,我们 FPS 提高多达 379%!
当然,这不是真的 FPS 增加,因为延迟明显减少(也就是说,总有一帧可以处理;我们不需要轮询相机设备,等待 I/O 完成)。在本文的其他部分,为了简洁起见,我称我们的指标为FPS 但要记住,这是延迟减少和增加 FPS 结合增加。
为了实现FPS我们的目标是从网络摄像头或USB设备读取帧移动到一个完全不同的线程,完全独立于我们的Python主脚本。
这将允许从 I/O 线程连续读取帧,我们的主线程处理当前帧。一旦主线程处理了帧,它只需要从 I/O 在线程中获得当前帧。不需要等待阻塞 I/O 操作即可完成。
实现线程视频流功能的第一步是定义一个 FPS 我们可以用它来测量我们的每秒帧数。
创建一件名称fps.py
写入以下代码的文件:
# 导入必要的包 import datetime class FPS: def __init__(self): # 在开始和结束间隔之间检查存储开始时间、结束时间和帧总数 self._start = None # 当我们开始读取帧时,开始时间戳。 self._end = None # 当我们停止读取帧时,结束时间戳。 self._numFrames = 0 # 在 _start 和 _end 间隔期间读取的帧总数。 def start(self): # 启动计时器 self._start = datetime.datetime.now() return self def stop(self): # 停止计时器 self._end = datetime.datetime.now() def update(self): # 增加开始和结束间隔检查的帧总数 self._numFrames = 1 def elapsed(self): # 返回开始和结束间隔之间的总秒 return (self._end - self._start).total_seconds() def fps(self):
# 计算(近似)每秒帧数
return self._numFrames / self.elapsed()
2.使用 Python 和 OpenCV 提高网络摄像头 FPS
现在我们已经定义了 FPS
类(因此我们可以根据经验比较结果),创建WebcamVideoStream.py
文件,让我们在文件中定义包含实际线程摄像头读取的 WebcamVideoStream
类:
# import the necessary packages
from threading import Thread
import cv2
class WebcamVideoStream:
def __init__(self, src=0):
# 初始化摄像机流并从流中读取第一帧
self.stream = cv2.VideoCapture(src)
(self.grabbed, self.frame) = self.stream.read()
# 初始化用于指示线程是否应该停止的变量
self.stopped = False
def start(self):
# 启动线程从视频流中读取帧
Thread(target=self.update, args=()).start()
return self
def update(self):
# 继续无限循环,直到线程停止
while True:
# 如果设置了线程指示器变量,则停止线程
if self.stopped:
return
# 否则,从流中读取下一帧
(self.grabbed, self.frame) = self.stream.read()
def read(self):
# 返回最近读取的帧
return self.frame
def stop(self):
# 表示应该停止线程
self.stopped = True
3.案例测试
现在我们已经定义了 FPS
和 WebcamVideoStream
类,我们可以将所有部分放在 fps_demo.py
中:
# import the necessary packages
from __future__ import print_function
from imutils.video import WebcamVideoStream
from imutils.video import FPS
import argparse
import imutils
import cv2
# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add_argument("-n", "--num-frames", type=int, default=100,
help="# of frames to loop over for FPS test")
ap.add_argument("-d", "--display", type=int, default=-1,
help="Whether or not frames should be displayed")
args = vars(ap.parse_args())
# 此代码块将帮助我们获得 FPS 的基线,没有线程并且在从相机流中读取帧时使用阻塞 I/O。
# 获取指向视频流的指针并初始化 FPS 计数器
print("[INFO] sampling frames from webcam...")
stream = cv2.VideoCapture(0)
fps = FPS().start()
# 循环一些帧
while fps._numFrames < args["num_frames"]:
# 从流中抓取帧并将其调整为最大宽度为 400 像素
(grabbed, frame) = stream.read()
frame = imutils.resize(frame, width=400)
# 检查帧是否应该显示到我们的屏幕上
if args["display"] > 0:
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
# 更新 FPS 计数器
fps.update()
# 停止计时器并显示 FPS 信息
fps.stop()
print("[INFO] elasped time: {:.2f}".format(fps.elapsed()))
print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))
# 做一些清理工作
stream.release()
cv2.destroyAllWindows()
# 让我们看看我们从视频流中读取帧的线程代码:
# 创建了一个*线程*视频流,允许相机传感器预热,并启动 FPS 计数器
print("[INFO] sampling THREADED frames from webcam...")
vs = WebcamVideoStream(src=0).start()
fps = FPS().start()
# 循环某些帧…这一次使用线程流
while fps._numFrames < args["num_frames"]:
# 从线程视频流中抓取帧并将其调整为最大宽度为 400 像素
frame = vs.read()
frame = imutils.resize(frame, width=400)
# 检查帧是否应该显示到我们的屏幕上
if args["display"] > 0:
cv2.imshow("Frame", frame)
key = cv2.waitKey(1) & 0xFF
# 更新 FPS 计数器
fps.update()
# 停止计时器并显示 FPS 信息
fps.stop()
print("[INFO] elasped time: {:.2f}".format(fps.elapsed()))
print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))
# 做一些清理工作
cv2.destroyAllWindows()
vs.stop()
4.结果分析
要查看网络摄像头 I/O 线程在运行中的影响,只需执行以下命令:python fps_demo.py
正如我们所见,通过在 Python 脚本的主线程中不使用线程并从视频流中顺序读取帧,我们能够获得可观的 29.97 FPS。然而,一旦我们切换到使用线程相机 I/O,我们就达到了 143.71 FPS – 增加了 379% 以上!
这显然大大降低了我们的延迟,并显著提高了我们的FPS,这都是通过使用线程而获得的。
然而,正如我们即将发现的那样,使用 cv2.imshow
可以大大降低我们的 FPS。如果您考虑一下,这种行为是有道理的–cv2.show
函数只是另一种形式的 I/O,只是这次不是从视频流中读取帧,而是将帧发送到显示器上的输出。
注意:这里我们还使用了cv2.waitKey(1)
函数,它确实为我们的主循环添加了1ms
的延迟。也就是说,这个函数对于键盘交互和在屏幕上显示图像帧是必要的。
要演示 cv2.imshow
I/O 如何降低 FPS,只需发出以下命令:python fps_demo.py --display 1
不使用线程,我们达到了 28.90 FPS。通过线程,我们达到了 39.93 FPS。 FPS 仍然增加了 38%,但远不及我们之前示例的 379%。
总的来说,我建议使用 cv2.imshow
函数来帮助调试您的程序——但如果您的最终生产代码不需要它,则没有理由包含它,因为您会损害您的 FPS。
这类程序的一个很好的例子就是开发一种家庭监控运动探测器,它会向你发送一条文本信息,其中包含一个刚刚走进你家前门的人的照片。实际上,您并不需要cv2.imshow
函数。通过删除它,您可以提高运动检测器的性能,并允许它更快地处理更多帧。
参考目录
https://www.pyimagesearch.com/2015/12/21/increasing-webcam-fps-with-python-and-opencv/