目标检测视频流处理性能优化

本文探讨了视频分析中的目标检测性能优化,包括通过控制处理帧的数量、使用多线程并行处理来解决卡顿和花屏问题。介绍了如何计算和控制视频流的fps,以及如何利用帧的相似性降低算法调用频率。通过多线程和队列管理确保输出帧顺序正确,以实现流畅的视频流处理。
部署运行你感兴趣的模型镜像

视频分析的处理过程是从摄像头读取帧,逐帧处理(做目标检测)后,放入流媒体服务器,发布出新的视频流。
rtmp流媒体服务器可以采用nginx_rtmp_module、srs。这篇文章里有关于nginx_rtmp_module的说明,https://zhuanlan.zhihu.com/p/28009037,手动安装比较复杂,可以使用直接使用 alfg/nginx-rtmp 镜像 https://github.com/alfg/docker-nginx-rtmp。
最初的代码参考了这篇文章,https://zhuanlan.zhihu.com/p/74260950。

import subprocess as sp

rtmpUrl = ""
camera_path = ""
cap = cv.VideoCapture(camera_path)

# Get video information
fps = int(cap.get(cv.CAP_PROP_FPS))
width = int(cap.get(cv.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv.CAP_PROP_FRAME_HEIGHT))

# ffmpeg command
command = ['ffmpeg',
        '-y',
        '-f', 'rawvideo',
        '-vcodec','rawvideo',
        '-pix_fmt', 'bgr24',
        '-s', "{}x{}".format(width, height),
        '-r', str(fps),
        '-i', '-',
        '-c:v', 'libx264',
        '-pix_fmt', 'yuv420p',
        '-preset', 'ultrafast',
        '-f', 'flv', 
        rtmpUrl]

# 管道配置
p = sp.Popen(command, stdin=sp.PIPE)

# read webcamera
while(cap.isOpened()):
    ret, frame = cap.read()
    if not ret:
        print("Opening camera is failed")
        break

    # process frame
    # your code
    # process frame

    # write to pipe
    p.stdin.write(frame.tostring())

基本思路是使用opencv读取视频流,把视频流里帧处理后,放入管道,ffmpeg把管道里的帧推送到rtmp服务器,客户端就可以播放rtmp服务器里的处理后的视频。
在实际运行中,会出现卡顿、花屏问题。原因是这一部分处理帧的代码耗时比较久。

    # process frame
    # your code
    # process frame

解决的方法是控制处理帧的数量和使用多线程并行处理。
对视频文件的fps(帧率)可以用fps = int(cap.get(cv.CAP_PROP_FPS))查看,对于摄像头视频流可以采用https://blog.youkuaiyun.com/zziahgf/article/details/90706839 里的方法计算。我们需要控制输出的视频流。一般电影的fps是24、电视是25或30。输出的视频流的fps是10的话,也可以满足一些监控的需求。

用下面的方法可以控制输出视频流的fps

        frame_rate = 25
        last_process_frame_time = 0 # 处理上一个帧的时间,用于按frame_rate处理帧
        while(self.cap.isOpened()): 
            time_elapsed = time.time() - last_process_frame_time
            ret, frame = self.cap.read()
            if not ret:
                print("Opening camera is failed")
                break
            if time_elapsed > 1./frame_rate:
                last_process_frame_time = time.time()
                process_frame(frame)

按指定的fps取到帧后,也不需要每帧都调用目标检测算法,因为临近的几帧是基本相同的,所以可以隔几帧调用一次算法,对于其它帧使用前一次检测的结果做画图即可,不用再执行算法。

i = 0
count = 5 #每隔5帧提取一次图片
while True:
    ret, frame = cap.read()
    i = i + 1
    if(i == 0):
        detect(frame)
    else:
        drawbox(frame)
    i = i + 1
    i = i % count

下面的代码里把detect和drawbox放在了process_frame方法中,把i作为frame_index传入了process_frame。
如果采用25的fps,意味着检测每帧的时间要在40毫秒以内。要执行的算法很多,难以要求每个都能达到这样的要求。尤其是当算法部署在其它机器,通过网络调用时,受网络延时影响,更容易超出这一限制。这时,需要用多线程来并行执行算法。

executor = ThreadPoolExecutor(max_workers=10)
future= executor.submit(detect,frame)

多线程会打乱帧的顺序,所以我们使用了队列deque来在主线程里记录每一帧的输入时间,并使用一个dict来记录这一帧的输出。在process_frame时,先把输入时间加入到队列,然后使用多线程执行检测,检测完成后,把结果加入dict,使用输入时间作为key。对于不需要检测,只需要画框的帧,为检测任务future设置回调,检测完成后,回调函数为这些帧画框,然后把画完框的帧加入到dict中。
输出时,先检查deque中最早的帧在dict中有没有结果,如果有结果,就把结果帧取出,放入管道中。同时,删除队列和dict中这一帧的数据。这样就保证了输出顺序。
这时,如果忽略画图的时间,单次调用算法执行时间除以线程数 小于 40毫秒X5(抽帧频率)=200毫秒 就能保证流畅的输出。

    # 处理帧,检测或画框
    def process_frame(self,stub, frame,frame_index):
        try:
            input_time = datetime.datetime.now()
            input_time_queue.append(input_time)
            need_detect = frame_index == 0 # 是否需要调用算法检测
            if(need_detect):
                self.last_detect_future = self.executor.submit(self.detect_and_save,stub, frame,input_time)
            else:
                drowbox_after_finish_detect(self.last_detect_future,frame,input_time)
        except Exception as e:
            logger.exception(repr(e))
            logger.exception('process_frame  fail')

    # 检测帧,并把结果记录到dic中
    def detect_and_save(self,stub,frame,input_time):
        output = detect(stub, frame)
        result_frame_dic[input_time] = output 
        return output
# 检测完成后,对后续帧画图
def drowbox_after_finish_detect(task, frame,input_time):
    try:
        logger.debug('into drowbox_after_finish_detect')
        for future in concurrent.futures.as_completed([task]):
            detect_result = future.result()
            out_frame = drawbox(frame, detect_result)
            result_frame_dic[input_time] = {"out_frame":out_frame} 
    except Exception as e:
        logger.exception(repr(e))
        logger.exception('drowbox_after_finish_detect  fail')    
    # 取出最早的结果帧,放入管道
    def write_to_pipe(self):
        try:
            if(len(input_time_queue)>0):
                first_time = input_time_queue[0]
                if(first_time in result_frame_dic):
                    logger.debug('find first_time' )
                    out_frame = result_frame_dic[first_time]['out_frame']
                    self.p.stdin.write(out_frame.tostring())
                    remove_from_queue(first_time)
        except Exception as e:
            logger.exception(repr(e))
            logger.exception('write_to_pipe fail')       
# 把帧信息从队列删除
def remove_from_queue(input_time):
    result_frame_dic.pop(input_time)
    input_time_queue.popleft() 

以上就是优化后的主要代码。
回顾下基本思路:
1.控制要处理的帧的数量:
(1)控制fps
(2)抽帧检测,其它帧仅使用检测结果画图
2.多线程处理
(1)多个线程同时调用算法,调用时可以减小分辨率来加速检测
(2)使用回调为后续帧画图
(3)使用队列保证输出帧的顺序和输入顺序一致,保证每一帧都被处理

https://zhuanlan.zhihu.com/p/129329018

您可能感兴趣的与本文相关的镜像

Yolo-v5

Yolo-v5

Yolo

YOLO(You Only Look Once)是一种流行的物体检测和图像分割模型,由华盛顿大学的Joseph Redmon 和Ali Farhadi 开发。 YOLO 于2015 年推出,因其高速和高精度而广受欢迎

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值