在使用opencv处理视频的过程中,通常我们会读取视频帧,读取出来的视频帧就相当于一幅图像,我们只要读取到了图像就可以对图像进行各种各样的操作。
例如,行人检测,汽车检测这些算法,目前非常火的目标识别算法就是yolo3,无论是在速度上还是精度上,yolo3都要比他的前辈,RCNN,FASTRCNN,YOLOv1要好很多。
但是就算是这样的背景下,我们在实际运用yolo的过程中,也是会遇到这样那样的问题,例如尽管网上对于yolo的速度大加赞赏,但是我在使用过程中,用cpu进行一次80类别数据集的检测却要耗费0.6秒,普通的视频fps大约25-30,对于每帧0.6秒的处理速度来说是远远不够的,那么性能好的gpu呢,在使用gpu的情况下,一次检测也需要将近0.08秒,接近0.1秒,在将算法应用到视频中时,会发现明显的卡顿,但是卡顿都是可以接受的,令人无法接受的是,opencv自带的帧缓冲区,这个就是cap函数读取视频时存放帧的地方,在实测过程中发现,cap函数无法获取最新帧,而是按照cap缓冲区中顺序一帧一帧的读取,这导致了我算法的延时,会导致延时越来越严重,我们期望cap能够跳过算法的处理时间,直接读取当前帧而抛掉算法运行过程中的帧,但是事实显示,opencv并不是这样做的,在和同学的讨论中,一位同学告诉我,cap能够读取当前帧,但是我在实验过程中发现,事实并不是这样,在研究中发现,opencv的确会在一定时间清空帧缓冲区,但这个清空的时机并不是我们能够控制的,也就是说我没有发现一个api可以让我们手动清空帧缓冲区。
于是在这种情况下,编写我们自己的帧缓冲区就很重要。
大致思路就是,创建一个自定义的帧缓冲区,开启一个线程使用cap函数读取视频帧,将读取到的视频帧存入我们自定义的缓冲区,这个缓冲区可以设计成固定大小,每次新增新的帧进入缓冲区,将挤掉旧的帧。代码可以参考:
class Stack:
def __init__(self, stack_size):
self.items = []
self.stack_size = stack_size
def is_empty(self):
return len(self.items) == 0
def pop(self):
return self.items.pop()
def peek(self):
if not self.isEmpty():
return self.items[len(self.items) - 1]
def size(self):
return len(self.items)
def push(self, item):
if self.size() >= self.stack_size:
for i in range(self.size() - self.stack_size + 1):
self.items.remove(self.items[0])
self.items.append(item)
另外,开启一个线程,这里用主线程也可以,读取视频缓冲区中的帧并进行算法,然后显示。
def capture_thread(video_path, frame_buffer, lock):
print("capture_thread start")
vid = cv2.VideoCapture(video_path)
if not vid.isOpened():
raise IOError("Couldn't open webcam or video")
while True:
return_value, frame = vid.read()
if return_value is not True:
break
lock.acquire()
frame_buffer.push(frame)
lock.release()
cv2.waitKey(25)
def play_thread(frame_buffer, lock):
print("detect_thread start")
print("detect_thread frame_buffer size is", frame_buffer.size())
while True:
if frame_buffer.size() > 0:
lock.acquire()
frame = frame_buffer.pop()
lock.release()
# TODO 算法
cv2.imshow("result", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
主函数可以参考这里:
from thread import capture_thread, play_thread
from stack import Stack
import threading
path = ''
rtsp_url = ''
frame_buffer = Stack(3)
lock = threading.RLock()
t1 = threading.Thread(target=capture_thread, args=(path, frame_buffer, lock))
t1.start()
t2 = threading.Thread(target=play_thread, args=(frame_buffer, lock))
t2.start()
这样,我们就可以控制自己的帧缓冲区,并且每次处理耗时算法以后,能够获取到最新的一帧进行处理,虽然画面可以能会掉帧的样子,但是不会再有延时的情况。