由于工业用处理器的处理速度较慢,处理速度时常在,而摄像头的帧率又必须在固定帧率以上,导致两者无法达到同步,无法单线程运行。
而解决方案就是开辟一个线程来单独控制摄像头进行录制视频、存储视频等操作,在主线程中进行视频的读取、视频帧的处理操作。
假设主线程中视频帧的运行帧率为30fps,摄像头帧率为60fps,如果使用单线程进行摄像头的数据采集和视频帧的处理,会导致摄像头出现停等现象而被动降帧,摄像头的帧率最终会降到和视频帧的运行帧率保持一致。
我们确定的是:摄像头不能停下来等视频帧的处理(也就是不能使摄像头帧率被动降低),摄像头要一直拍摄,而视频帧的处理是可以有等待的(比如要等摄像头拍摄完一段之后才能进行处理,摄像头没有录下来视频的时候没有视频帧,自然也就无法处理视频帧)。所以可以将摄像头路下来的视频分为不同的小段,通过画甘特图可以发现,摄像头录制每个视频小段的时长为2^n(n=1,2,3,4,5,6,7……)时二者可以保持最大成度的并行,而最后等待的时间也最短,尽量缩短由于低性能处理器导致的时间消耗。但是存储不同的小段会频繁使用I/O操作,对于很长时间的录制分割(n>=6)来说没什么影响。但对于短时间的录制(n<6)分割会有一部分时间消耗和性能损耗,这种性能损耗在整体时间消耗占比还是相当大的。
最好的解决办法:保持cpu的视频帧处理速率大于摄像头的帧率。
"""
-*- coding: utf-8 -*-
@Time : 2022/5/20 16:26
@Author : wcc
@FileName: testMultithreading.py
@Software: PyCharm
@Blog :https://blog.youkuaiyun.com/qq_41575517?spm=1000.2115.3001.5343
"""
import cv2
import threading
import time as t
class fpsTimer:
# 初始化构造函数
def __init__(self):
self.begin = 0
self.now = 0
# 时间间隔
self.interval = 0
# 开始计时
def start(self):
self.begin = t.perf_counter()
# 计算从开始到现在的时间间隔
def calc(self):
self.now = t.perf_counter()
self.interval = self.now - self.begin
return self.interval
# 视频段存储标记(生产者消费者标记),已经被存储的段置为True,最后一位为摄像头线程停止标记(表示在第几段退出了,作用是让视频帧处理程序退出循环)
segmentFlag = [False, False, False, False, False, False, 100]
def captureVideoFromCamera():
cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
# WIDTH/HEIGHT必须和摄像头逐帧捕获的分辨率一致,否则会生成1kb视频文件并且无法播放
# 通过frame.shape获取摄像头逐帧分辨率
WIDTH = 640
HEIGHT = 480
fps = 10
cap.set(cv2.CAP_PROP_FPS, 10)
# 如下fourcc参数必须是小写,用大写会有OpenCV报错
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
frameCount = 0
# 计时器
segmentTimer = fpsTimer()
# 计段器,记录一共分割成了多少段,并且也是每一段的文件名
segmentCount = 0
# 存储上一段的时长
beforeSegment = 2
# 存储地址,初始存储命名为0
FILENAME = r'C:\Users\A\Desktop\test\{}.mp4'.format(segmentCount)
out = cv2.VideoWriter(FILENAME, fourcc=fourcc, fps=fps, frameSize=(WIDTH, HEIGHT))
print("第{}段开始存储,预计持续时间为{}s".format(segmentCount+1, beforeSegment))
segmentTimer.start()
if not cap.isOpened():
print("无法打开摄像头")
exit()
while True:
if segmentTimer.calc() > beforeSegment:
print("第{}段存储完成".format(segmentCount+1))
# 存储完毕之后才能给主程序使用
segmentFlag[segmentCount] = True
# 存储段数+1
segmentCount += 1
# 线程自然停止,只拍摄6段视频
if segmentCount == 3:
print("线程中止,在第{}段退出了", segmentCount + 1)
segmentFlag[-1] = segmentCount
return
# 存储地址
FILENAME = r'C:/Users/A/Desktop/test/{}.mp4'.format(segmentCount)
out = cv2.VideoWriter(FILENAME, fourcc=fourcc, fps=fps, frameSize=(WIDTH, HEIGHT))
# 下一段存储的长度是本段长度的2倍
beforeSegment *= 2
# 重新计时
segmentTimer.start()
print("第{}段开始存储,持续时间约为{}s".format(segmentCount+1, beforeSegment))
# 逐帧捕获
ret, frame = cap.read()
# 如果正确读取帧,ret为True
if not ret:
print("无法正确读帧,程序退出")
return
frameCount += 1
out.write(frame)
# 显示结果帧, 支持全屏
# cv2.namedWindow('frame', cv2.WND_PROP_FULLSCREEN)
# cv2.imshow('frame', frame)
# 线程结束条件,人工干预
if cv2.waitKey(1) == ord('q'):
print("最后一段为第{}段,段长应为{}s,实为{}s".format(segmentCount, beforeSegment, segmentTimer.calc()))
break
# 完成所有操作后,释放捕获器
out.release()
cap.release()
# 主线程
def main():
cameraThread = threading.Thread(target=captureVideoFromCamera)
cameraThread.start()
# 按顺序轮询读取已经存储的视频段,最大值为5(无间隙轮询可能比较消耗cpu,可以改成根据生产者上次消耗的时长来按照固定时间间隔轮询)
i = 0
while True:
if segmentFlag[i]:
cap = cv2.VideoCapture('C:/Users/A/Desktop/test/{}.mp4'.format(i))
print("读取到了第{}段".format(i+1))
while cap.isOpened():
# ret的返回值是bool类型
ret, frame = cap.read()
if not ret:
break
# 显示结果帧,支持全屏
cv2.imshow('image', frame)
# 键盘等待(这条语句不能删掉,删掉之后会报一个很神奇的错误,找不到所读取的资源)
cv2.waitKey(10)
i += 1
# 释放资源
cap.release()
# 关闭窗口
cv2.destroyAllWindows()
if i == 6 or i == segmentFlag[-1]:
print("读的最后一段是第{}段,读取结束".format(segmentFlag[-1]+1))
return
if __name__ == "__main__":
main()
对于这个问题我有些地方不是很懂,希望有经验的大佬能给我一些解答疑惑和批评指正,如下:
- 当我的处理器处理视频帧的速率低于摄像头的录制帧率,是不是不能将处理视频帧的代码和控制摄像头录制的代码写在一个线程里?
- 如果我单线程运行 处理视频帧的代码和控制摄像头录制的代码 的话会不会导致摄像头帧率被迫下降至和视频处理帧率相同?
- 摄像头会不会出现停等现象来等待视频帧的处理?