Bootstrap

我与计算机视觉 - [Today is OpenCV] - [OpenCV与多线程]

在使用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()

这样,我们就可以控制自己的帧缓冲区,并且每次处理耗时算法以后,能够获取到最新的一帧进行处理,虽然画面可以能会掉帧的样子,但是不会再有延时的情况。

;