您是否曾经通过 OpenCV
的 cv2.VideoCapture
函数处理视频文件并发现读取帧感觉缓慢?您的整个视频处理管道一直在运行,每秒处理的帧数不能超过一到两帧——即使您没有进行任何类型的计算成本高昂的图像处理操作。
根据您的视频文件类型、安装的编解码器,机器的物理硬件,大部分视频处理管道的消耗是来自于读取和解码视频文件中的下一帧。
这只是计算上的浪费——还有更好的方法。
在今天博文的其余部分,我将演示如何使用线程和队列数据结构将视频文件的 FPS 速率提高 52% 以上!
在使用OpenCV处理视频文件时,您可能会使用 cv2.VideoCapture 函数。 首先,通过传入输入视频文件的路径来实例化 cv2.VideoCapture
对象。 然后你开始一个循环,调用 cv2.VideoCapture
的 .read
方法来轮询视频文件中的下一帧,这样你就可以在你的管道中处理它。
问题(以及这种方法感觉缓慢和迟缓的原因)是您在主处理线程中读取和解码帧!
.read
方法是一个阻塞操作——你的 Python + OpenCV 应用程序的主线程完全阻塞(即停止),直到从视频文件中读取帧、解码并返回到调用函数。
通过将这些阻塞 I/O 操作移至单独的线程并维护解码帧队列,我们实际上可以将 FPS 处理速率提高 52% 以上!
帧处理速度的提高来自于显著减少延迟——我们不必等待 .read
方法完成读取和解码帧;相反,总是有一个预解码的帧可供我们处理。
为了减少延迟,我们的目标是将视频文件帧的读取和解码移动到程序的一个完全独立的线程中,释放我们的主线程来处理实际的图像处理。
但是,在我们欣赏更快的、线程化的视频帧处理方法之前,我们首先需要为较慢的、非线程化的版本设置一个基准/基线。
本文要使用fps.py
来计算帧率,代码如下:
import datetime
class FPS:
def __init__(self):
# 存储开始时间、结束时间和在开始和结束间隔之间检查的帧总数
self._start = None
self._end = None
self._numFrames = 0
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()
本节的目标是使用OpenCV和Python获得视频帧处理吞吐量的基线。
首先,打开一个新文件,将其命名为 read_frames_slow.py
,然后插入以下代码:
# import the necessary packages
from imutils.video import FPS
import numpy as np
import argparse
import imutils
import cv2
# 构造参数解析器并解析参数
ap = argparse.ArgumentParser()
# 解析我们的命令行参数。对于这个脚本,我们只需要一个开关 --video ,它是我们输入视频文件的路径。
ap.add_argument("-v", "--video", required=True,
help="path to input video file")
args = vars(ap.parse_args())
# 打开一个指向视频流的指针,并启动FPS定时器
stream = cv2.VideoCapture(args["video"])
fps = FPS().start() # 启动一个我们可以用来测量 FPS 的计时器
# 循环视频文件流中的帧
while True:
# 从线程视频文件流中抓取帧
(grabbed, frame) = stream.read()
# 如果帧没有被抓取,那么我们已经到达了流的末尾
if not grabbed:
break
# 调整帧大小并将其转换为灰度(同时仍保留 3 个通道)
frame = imutils.resize(frame, width=450)
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame = np.dstack([frame, frame, frame])
# 在图像中显示一段文本(这样我们就可以公平地对快速方法进行基准测试)
cv2.putText(frame, "Slow Method", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
# 显示帧并更新 FPS 计数器
cv2.imshow("Frame", frame)
cv2.waitKey(1)
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()
使用示例视频获取帧率的基线:python read_frames_slow.py --video videos/jurassic_park_intro.mp4
如您所见,处理 31 秒视频剪辑的每个单独帧大约需要 47 秒,FPS 处理率为 20.21。
这些结果表明,读取和解码单个帧实际上需要比视频的实际长度更长的时间!
为了提高使用 OpenCV 从视频文件中读取帧的 FPS 处理速率,我们将利用线程和队列数据结构:
由于 cv2.VideoCapture
的 .read
方法是一个阻塞 I/O 操作,我们可以通过创建一个单独的线程来获得显著的加速,该线程仅负责从视频文件中读取帧并维护一个队列。
由于 Python 的 Queue
数据结构是线程安全的,因此我们已经完成了大部分艰苦的工作——我们只需要将所有部分放在一起。
# import the necessary packages
from threading import Thread
import sys
import cv2
# 从 Python 3 导入 Queue 类
if sys.version_info >= (3, 0):
from queue import Queue
# 否则,为 Python 2.7 导入 Queue 类
else:
from Queue import Queue
class FileVideoStream:
def __init__(self, path, queueSize=128):
# 初始化文件视频流以及用于指示线程是否应该停止的布尔值
self.stream = cv2.VideoCapture(path)
self.stopped = False
# 初始化存储视频文件帧的队列
self.Q = Queue(maxsize=queueSize)
def start(self):
# 启动一个线程从文件视频流中读取帧
t = Thread(target=self.update, args=())
t.daemon = True
t.start()
return self
def update(self):
# 循环
while True:
# 如果设置了线程指示器变量,则停止线程
if self.stopped:
return
# 否则,请确保队列中有空间
if not self.Q.full():
# 从文件中读取下一帧
(grabbed, frame) = self.stream.read()
# 如果 grabbed 布尔值为 False,那么我们已经到了视频文件的末尾
if not grabbed:
self.stop()
return
# 将帧添加到队列中
self.Q.put(frame)
def read(self):
# 返回队列中的下一帧
return self.Q.get()
def more(self):
# 如果队列中还有帧,则返回 True
return self.Q.qsize() > 0
def stop(self):
# 指示应该停止线程
self.stopped = True
现在我们已经定义了 FileVideoStream 类,我们可以将所有部分放在一起,享受使用 OpenCV 读取的更快、线程化的视频文件。
打开一个新文件,将其命名为 read_frames_fast.py
,然后插入以下代码:
# 导入必要的包
from imutils.video import FileVideoStream
from imutils.video import FPS
import numpy as np
import argparse
import imutils
import time
import cv2
# 构造参数解析并解析参数
ap = argparse.ArgumentParser()
ap.add_argument("-v", "--video", required=True,
help="path to input video file")
args = vars(ap.parse_args())
# 启动文件视频流线程并允许缓冲区开始填充
print("[INFO] starting video file thread...")
fvs = FileVideoStream(args["video"]).start()
time.sleep(1.0)
# 启动 FPS 计时器
fps = FPS().start()
# 循环播放视频文件流中的帧
while fvs.more():
# 从线程视频文件流中抓取帧,调整大小,并将其转换为灰度(同时仍保留 3 个通道)
frame = fvs.read()
frame = imutils.resize(frame, width=450)
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame = np.dstack([frame, frame, frame])
# 在frame上显示队列的大小
cv2.putText(frame, "Queue Size: {}".format(fvs.Q.qsize()),
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
# 显示帧并更新 FPS 计数器
cv2.imshow("Frame", frame)
cv2.waitKey(1)
fps.update()
# 停止计时器并显示 FPS 信息
fps.stop()
print("[INFO] elasped time: {:.2f}".format(fps.elapsed()))
print("[INFO] approx. FPS: {:.2f}".format(fps.fps()))
# 做一些清理工作
cv2.destroyAllWindows()
fvs.stop()
执行以下命令:python read_frames_fast.py --video videos/jurassic_park_intro.mp4
正如我们从结果中看到的那样,我们能够在 31.09 秒内处理整个 31 秒的视频剪辑——这比缓慢、原始的方法提高了 34%!
实际帧率要快得多,达到每秒 30.75 帧,提高了 52.15%。
线程可以显着提高视频处理管道的速度——尽可能使用它。
创建一个camera.py
文件,写入以下代码:
from threading import Thread, Lock
from datetime import datetime
import time
import cv2
time_cycle = 80
class CameraThread(Thread):
def __init__(self, kill_event, src = 0, width = 320, height = 240):
self.kill_event = kill_event
self.stream = cv2.VideoCapture(src)
self.stream.set(cv2.CAP_PROP_FRAME_WIDTH, width)
self.stream.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
(self.grabbed, self.frame) = self.stream.read()
self.read_lock = Lock()
Thread.__init__(self, args = kill_event)
def update(self):
(grabbed, frame) = self.stream.read()
self.read_lock.acquire()
self.grabbed, self.frame = grabbed, frame
self.read_lock.release()
def read(self):
self.read_lock.acquire()
frame = self.frame.copy()
self.read_lock.release()
return frame
def run(self):
while not self.kill_event.is_set():
start_time = datetime.now()
self.update()
finish_time = datetime.now()
dt = finish_time - start_time
ms = (dt.days * 24 * 60 * 60 + dt.seconds) * 1000 + dt.microseconds / 1000.0
if ms < time_cycle:
time.sleep((time_cycle - ms) / 1000.0)
然后实现简单用例:
from camera import CameraThread
from threading import Event
import cv2
width = 640
height = 480
if __name__ == "__main__":
kill_event = Event()
cam = CameraThread(kill_event, height = height, width = width)
cam.start()
while True:
frame = cam.read()
cv2.imshow('webcam', frame)
if cv2.waitKey(1) == 27:
break
kill_event.set()
cv2.destroyAllWindows()
https://github.com/SakshayMahna/Computer-Vision-on-Humans/blob/main/PoseEstimation/camera.py
https://www.pyimagesearch.com/2017/02/06/faster-video-file-fps-with-cv2-videocapture-and-opencv/