再初次接触机器视觉的小伙伴应该都会遇到这种情况,明明摄像头输出的画面是 30 fps 的,但对每一帧图像进行畸变矫正或者是一些图像处理后,视频帧数降低到了不到 10 fps!还伴随出现丢帧或者是播放缓慢,内存占用越来越多。
最近想到了一种利用线程池去并行处理视频流的方法,该方法在处理耗时高于视频帧产生间隔时,能有效提高最终视频输出的帧率。本文会用伪代码来描述整个方法的框架,不提供实际的代码。
基本思路就是将读取、计算和消费三部分分离到不同的线程中,读取视频流的图像为单独一条线程,图像处理的计算在线程池中进行,最后使用结果的部分也是单独一条线程。
下面开始介绍完整的流程细节,先定义一些基本的接口和类型:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | type Image
 {
 
 size_t id
 bytes data
 }
 
 
 Image NextFrame()
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 
 | type PriorityQueue<Image>
 {
 
 void Push(Image image)
 
 
 bool Empty()
 
 
 Image& Top()
 
 
 Image Pop()
 }
 
 
 type ThreadPool
 {
 
 void AddTask(Functor task)
 }
 
 
 type Mutex
 {
 void Lock()
 void Unlock()
 }
 
 
 type ConditionVariable
 {
 void Wait(Mutex &m, Predicate pred)
 void Notify()
 }
 
 | 
首先就是读取图像线程的部分,该线程负责读取一帧视频图像,然后创建一个图像处理任务提交到线程池中,图像处理完成后,把结果塞到优先队列里。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 
 | 
 ThreadPool pool
 
 PriorityQueue<Image> result_buffer
 
 Mutex result_buffer_mutex
 
 ConditionVariable notifier
 
 
 void Producer()
 {
 while
 {
 input_image = NextFrame()
 pool.AddTask(func() {
 Image output_image
 
 SomeOperation(input_image, output_image)
 
 result_buffer_mutex.Lock()
 result_buffer.Push(output_image)
 result_buffer_mutex.Unlock()
 
 notifier.Notify()
 })
 }
 }
 
 | 
消费结果的线程逻辑会稍微复杂一点,因为线程池中执行任务的顺序是不确定,我们需要用条件变量来控制输出顺序:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | size_t need_output_id = 0;
 
 
 void Consumer()
 {
 while
 {
 
 result_buffer_mutex.Lock()
 notifier.Wait(result_buffer_mutex, func() {
 
 return !result_buffer.Empty() &&
 (result_buffer.Top().id == need_output_id);
 })
 output_image = result_buffer.Pop()
 result_buffer_mutex.Unlock()
 
 
 DoSomething(output_image);
 }
 }
 
 | 
总结
整个框架的思路其实挺简单的,但实际实现的时候需要注意的细节会比较多,这里举例两个:
- 多线程的安全问题,用 C/C++ 实现的时候,要尽可能避免图像数据的拷贝,又要保证线程池任务执行和最后读取结果的时候图像数据是有效的;
- 最终使用结果图像的部分,由于是并行处理多帧图像,最终得到图像的频率是不稳定的,有可能一瞬间就读读到 N 幅图像,这时候就需要根据需求来修改代码了,例如我最终的图像是要实时显示出来,那就要加上合适的延迟再播放下一帧,不如画面就会出现卡顿一下下,然后瞬间播放好几帧的情况;
(●’◡’●)