使用线程处理 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/