Use OpenCV with Gstreamer
-
date_range 23/08/2017 info
最近在Rockchip Linux的平台尝试了一下OpenCV + Gstreamer的组合, 发现效果还蛮不错的. :)
过程中有些心得, 在这里记录一下…. 我想这些也不只适用RockChip平台,因为涉及的都是标准的概念,
比如DMABUF, DRM, OpenCL,G2D…放到像Intel, Nvdia这些平台也是成立的.
下面的内容会涉及一些Linux概念, 如果你不懂的话建议先查阅下相关文章, 当然最好是接触下对应的开发:
1. Code
一个简单的人脸识别应用:
使用了2D加速, 视频硬解加速
gstreamer-opencv
2. Background
2.1. Gstreamer
首先,要先讨论下为什么需要在OpenCV上用上Gstreamer. 比如我直接一个摄像头 v4l2 图像传给 OpenCV 不行吗?
Gstreamer是嵌入式平台处理Media的首选组件, 像Nvdia/TI/NXP/Rockchip平台, 都是使用Gstreamer来整合Media应用. 在Rockchip平台上, 我们已经有为Gstreamer开发了像Decode/Encode/ISP-Camera/2D加速器/DRM-Display-sink这些的Plugin.
所以OpenCV如果链接上Gstreamer, 输入源就不仅仅是摄像头, 还可以是RTSP/本地视频;输出显示的代码可以不用写, 让Gstreamer来显示; 转换格式让Gstreamer来转, 利用硬件加速; 处理的图像送回Gstreamer编码.
2.2. ARM
在ARM系统上做Media的开发, 有一个原则要很重要, 就是 : 避免拷贝.
如果你手边正好有一块ARM板子和Linux PC, 可以尝试在上面跑一些memcpy的Test. 一般来说, 测试的性能会相差5,6倍.
即时是DDR同频的两个系统, 性能也会差到3-4倍(不过也可能是DDR其他参数有影响?).
内存操作速度的劣势是RISC天生的, ARM也不列外. (虽然也没有研究过对应微处理器结构,道听途说 :-P)
还有一个更影响速度的就是, 这些Buffer一般都是uncached的DMA Buffer, 为了保证cpu和其他ip的内存一致性, 所以CPU读写速度就更慢了..
在开发OpenCV + Gstreamer的过程中, 一定要尽量避免拷贝的发生, 如果一定要有, 也不能是由CPU来做. (替代可以是2D加速器, GPU) (当然这里用2D加速拷出来后buffer,默认还是uncached的,还是不适合CPU直接在上面处理,就算改成cache的,cache刷新的时间也要10ms+。。不过如果你的算法需要CPU去实时处理每帧的话,我想一般的ARM CPU都做不到吧)
2.3. OpenCV
我之前只在X86上使用过OpenCV, 其实不太了解OpenCV在ARM Device需要怎么开发. (怀疑其他ARM平台上到底能不能用OpenCV, 因为像TI/NXP这种, CPU/GPU太弱, 估计只能内部的DSP跑算法; 像全志, 基本没有Linux平台的组件支持; 唯一能搞的估计也就是Nvdia的terga了, cuda还是厉害. ;) )
根据上面ARM的原则, 开发的时候要避免调用到OpenCv的cvtcolor和clone这些函数, 因为每次拷贝都会消耗大量的CPU资源.
OpenCV也支持OpenCL加速, 当然..其实没什么卵用, 尤其你是在处理实时的图像的时候,
因为GPU处理数据的时候, 需要加载Texture到GPU内存上, 放OpenCL上, 就是你要处理的帧, 全部要拷一份到新的内存地址上….虽然在嵌入式设备上, GPU并没有和CPU使用分离的内存, 完全没必要这么做; 在图形应用的框架上, GPU处理dmabuf都是zero-copy的, 也就是要处理的帧, 只要让GPU MMAP一下就可以了, 而OpenCV, OpenCL, 我是没找到方法…(所以GPU通用计算还是要靠Vulkan了..)
当然在算法的处理耗时有好几秒的时候, 加载纹理消耗10毫秒也是可以忽视的 : 这种场合才建议使用OpenCL.
才发现这个好像是ARM上特有的问题, opencv已经是用了CL_MEM_USE_HOST_PTR, 理论上不应该有拷贝. 但是ARM上这个flag却会导致拷贝, ARM上需要使用特殊的api来做zero-copy.
嗯…这样你得去修改OpenCV才能用起来…
这几天尝试添加了一下异步处理, 这样来看拷贝的耗时反而不重要了, 比如一秒里可能就处理了2,3张图片, 拷贝这一帧的30ms,opencl减少耗时500ms。而且拷贝后的buffer是cached的normal内存, cpu处理起来速度会更快. 所以拷贝是不是个问题, 得看相应的应用场景和算法需求.
3. Desgin
3.1. Pipeline
Pipeline Prototype 1:
video/rtsp/camera -> decoder -> opencv
这是我最先想到的, 通过gstreamer拿到decoder的buffer, 然后全部由opencv来处理. 但是前面说过, 要避免拷贝, 而opencv的显示 imshow , 是存在大量拷贝的, 所以不能这么做.
Pipeline Prototype 2:
video/rtsp/camera -> decoder -> opencv -> display sink
为了优化显示, 需要把buffer送回给gstreamer, 这样就得到了Prototype 2. 但是是要注意, OpenCV的默认格式是bgr的, 所有的画图函数都是基于bgr; CV的大部分算法都是都需要预处理成灰度图, 而某些图像格式排列不适合转换灰度图.
在Rockchip平台上Decoder出来的颜色格式是NV12的, 必须要想办法转换成BGR格式.
所以decoder到opencv之间还需要有处理颜色格式的单元, 这个工作不可能由CPU来做, 一般可以使用专有硬件, 如果相应的平台没有这样的硬件,
也可以使用GPU用特定的Shader来转(OpenGL的设计目的里, 加速2D就是很重要的一块, 我们有时候看到QT/Wayland这些地方说使用到GPU加速, 就是用GPU做这样的事).
Pipeline Prototype 3:
video/rtsp/camera -> decoder -> 2d convert -> opencv -> display sink
3.2. Implement
首先opencv在gstreamer是有plugin的, 但是从应用开发的角度, 这样不够flexible : plugin里的东西和外界是封闭的.
在实现上, 更建议使用Appsink和AppSrc, 这些模块, 在你的应用里, 是以Thread的形式存在的, 开发起来要更方便.
另外还有一点很重要, 就是什么gstreamer, gobject, 其实挺难用, 用C++会舒服很多。
代码结构上很简单: Gstreamer AppSink不停的送Buffer, 应用MMap出来给OpenCV处理, 完后AppSrc送会Gstreamer显示.
Gstreamer Pipeline:
video/rtsp/camera ! decoder ! v4l2videoconvert ! appsink
appsrc ! display
Rockchip Gstreamer Pipeline:
"filesrc location=/usr/local/test.mp4 ! qtdemux ! h264parse ! mppvideodec \
! v4l2video0convert output-io-mode=dmabuf capture-io-mode=dmabuf ! \
video/x-raw,format=BGR,width=(int)1920,height=(int)1080 ! \
appsink caps=video/x-raw,format=BGR name=sink"
"appsrc caps=video/x-raw,format=(string)BGR,width=(int)1920,height=(int)1080,framerate=(fraction)30/1 \
block=true name=src ! rkximagesink sync=false";
3.3. 代码解释
这段是核心的代码,是buffer处理过程,见其中的中文注释。
void OpenCVStream::Process()
{
GstSample* sample;
GstMapInfo map;
GstStructure* s;
GstBuffer* buffer;
GstCaps* caps;
GstMemory* mem;
int height, width, size;
int fd;
void* map_data;
while (is_streaming__) {
if (sink_pipeline__->GetIsNewFrameAvailable()) {
sink_pipeline__->GetLatestSample(&sample);
# 这里都是为了拿到buffer
caps = gst_sample_get_caps(sample);
buffer = gst_sample_get_buffer(sample);
s = gst_caps_get_structure(caps, 0);
gst_structure_get_int(s, "height", &height);
gst_structure_get_int(s, "width", &width);
size = gst_buffer_get_size(buffer);
/* Since gstreamer don't support map buffer to write,
* we have to use mmap directly
*/
mem = gst_buffer_peek_memory(buffer, 0);
# 注意这里拿到dmabuf的fd啦!!!!!很重要
fd = gst_dmabuf_memory_get_fd(mem);
# 为什么不直接用gstreamer里已经mmap过的地址?因为gstreamer有权限问题,有可能mmap成只读的了
# 这里拿到buffer可读的地址了!!!!!!!fd就是这么转vaddr的
map_data = mmap64(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
std::list<OpenCVEffect*>::iterator itor = effect_lists.begin();
while (itor != effect_lists.end()) {
/* assume BGR */
# 因为用了RGA,视频解码后的nv12已经变成rgb的了,你也可以不用rga,那opencv里就要当nv12处理
(*itor)->Process((void*)map_data, width, height);
itor++;
}
munmap(map_data, size);
/* will auto released */
# refcount加一,appsource pipeline的过程处理完了,他会减一
gst_buffer_ref(buffer);
src_pipeline__->SendBUF(buffer);
sink_pipeline__->ReleaseFrameBuffer();
/* g_print("%s\n", gst_caps_to_string(caps)); */
}
}
}