Use OpenCV with Gstreamer
date_range 23/08/2017 visibility Hits: 319info
最近在Rockchip Linux的平臺嘗試了一下OpenCV + Gstreamer的組合, 發(fā)現效果還蠻不錯的.:)
過(guò)程中有些心得, 在這里記錄一下…. 我想這些也不只適用RockChip平臺,因為涉及的都是標準的概念, 比如DMABUF, DRM,OpenCL,G2D…放到像Intel, Nvdia這些平臺也是成立的.
下面的內容會(huì )涉及一些Linux概念, 如果你不懂的話(huà)建議先查閱下相關(guān)文章, 當然最好是接觸下對應的開(kāi)發(fā):
DRMDMABufV4l2OpenCLCode
一個(gè)簡(jiǎn)單的人臉識別應用:
使用了2D加速, 視頻硬解加速
gstreamer-opencvBackground
Gstreamer
首先,要先討論下為什么需要在OpenCV上用上Gstreamer. 比如我直接一個(gè)攝像頭 v4l2 圖像傳給 OpenCV不行嗎?
Gstreamer是嵌入式平臺處理Media的首選組件, 像Nvdia/TI/NXP/Rockchip平臺,都是使用Gstreamer來(lái)整合Media應用. 在Rockchip平臺上,我們已經(jīng)有為Gstreamer開(kāi)發(fā)了像Decode/Encode/ISP-Camera/2D加速器/DRM-Display-sink這些的Plugin.
所以OpenCV如果鏈接上Gstreamer, 輸入源就不僅僅是攝像頭, 還可以是RTSP/本地視頻;輸出顯示的代碼可以不用寫(xiě),讓Gstreamer來(lái)顯示; 轉換格式讓Gstreamer來(lái)轉, 利用硬件加速; 處理的圖像送回Gstreamer編碼.
ARM
在A(yíng)RM系統上做Media的開(kāi)發(fā), 有一個(gè)原則要很重要, 就是 : 避免拷貝.
如果你手邊正好有一塊ARM板子和Linux PC, 可以嘗試在上面跑一些memcpy的Test. 一般來(lái)說(shuō),測試的性能會(huì )相差5,6倍. 即時(shí)是DDR同頻的兩個(gè)系統, 性能也會(huì )差到3-4倍(不過(guò)也可能是DDR其他參數有影響?).內存操作速度的劣勢是RISC天生的, ARM也不列外. (雖然也沒(méi)有研究過(guò)對應微處理器結構,道聽(tīng)途說(shuō) :-P)
還有一個(gè)更影響速度的就是, 這些Buffer一般都是uncached的DMA Buffer, 為了保證cpu和其他ip的內存一致性,所以CPU讀寫(xiě)速度就更慢了..
在開(kāi)發(fā)OpenCV + Gstreamer的過(guò)程中, 一定要盡量避免拷貝的發(fā)生, 如果一定要有, 也不能是由CPU來(lái)做.(替代可以是2D加速器, GPU)(當然這里用2D加速拷出來(lái)后buffer,默認還是uncached的,還是不適合CPU直接在上面處理,就算改成cache的,cache刷新的時(shí)間也要10ms+。。不過(guò)如果你的算法需要CPU去實(shí)時(shí)處理每幀的話(huà),我想一般的ARMCPU都做不到吧)
OpenCV
我之前只在X86上使用過(guò)OpenCV, 其實(shí)不太了解OpenCV在A(yíng)RM Device需要怎么開(kāi)發(fā).(懷疑其他ARM平臺上到底能不能用OpenCV, 因為像TI/NXP這種, CPU/GPU太弱, 估計只能內部的DSP跑算法;像全志, 基本沒(méi)有Linux平臺的組件支持; 唯一能搞的估計也就是Nvdia的terga了, cuda還是厲害. ;) )
根據上面ARM的原則, 開(kāi)發(fā)的時(shí)候要避免調用到OpenCv的cvtcolor和clone這些函數,因為每次拷貝都會(huì )消耗大量的CPU資源.
OpenCV也支持
OpenCL加速,當然..其實(shí)沒(méi)什么卵用, 尤其你是在處理實(shí)時(shí)的圖像的時(shí)候, 因為GPU處理數據的時(shí)候, 需要加載Texture到GPU內存上,放OpenCL上, 就是你要處理的幀, 全部要拷一份到新的內存地址上….雖然在嵌入式設備上, GPU并沒(méi)有和CPU使用分離的內存,完全沒(méi)必要這么做; 在圖形應用的框架上, GPU處理dmabuf都是zero-copy的, 也就是要處理的幀, 只要讓GPUMMAP一下就可以了, 而OpenCV, OpenCL,我是沒(méi)找到方法…(所以GPU通用計算還是要靠Vulkan了..)
當然在算法的處理耗時(shí)有好幾秒的時(shí)候, 加載紋理消耗10毫秒也是可以忽視的 : 這種場(chǎng)合才建議使用OpenCL.
才發(fā)現這個(gè)好像是ARM上特有的問(wèn)題, opencv已經(jīng)是用了CL_MEM_USE_HOST_PTR,理論上不應該有拷貝.
但是ARM上這個(gè)flag卻會(huì )導致拷貝,ARM上需要使用
特殊的api來(lái)做zero-copy.
嗯…這樣你得去修改OpenCV才能用起來(lái)…
這幾天嘗試添加了一下異步處理, 這樣來(lái)看拷貝的耗時(shí)反而不重要了, 比如一秒里可能就處理了2,3張圖片,拷貝這一幀的30ms,opencl減少耗時(shí)500ms。而且拷貝后的buffer是cached的normal內存,cpu處理起來(lái)速度會(huì )更快. 所以拷貝是不是個(gè)問(wèn)題, 得看相應的應用場(chǎng)景和算法需求.
Desgin
Pipeline
Pipeline Prototype 1:
video/rtsp/camera -> decoder -> opencv
這是我最先想到的, 通過(guò)gstreamer拿到decoder的buffer, 然后全部由opencv來(lái)處理. 但是前面說(shuō)過(guò),要避免拷貝, 而opencv的顯示 imshow , 是存在大量拷貝的, 所以不能這么做.
Pipeline Prototype 2:
video/rtsp/camera -> decoder -> opencv -> display sink
為了優(yōu)化顯示, 需要把buffer送回給gstreamer, 這樣就得到了Prototype 2. 但是是要注意,OpenCV的默認格式是bgr的, 所有的畫(huà)圖函數都是基于bgr; CV的大部分算法都是都需要預處理成灰度圖,而某些圖像格式排列不適合轉換灰度圖.
在Rockchip平臺上Decoder出來(lái)的顏色格式是NV12的, 必須要想辦法轉換成BGR格式.
所以decoder到opencv之間還需要有處理顏色格式的單元, 這個(gè)工作不可能由CPU來(lái)做, 一般可以使用
專(zhuān)有硬件,如果相應的平臺沒(méi)有這樣的硬件, 也可以使用GPU用特定的Shader來(lái)轉(OpenGL的設計目的里, 加速2D就是很重要的一塊,我們有時(shí)候看到QT/Wayland這些地方說(shuō)使用到GPU加速, 就是用GPU做這樣的事).
Pipeline Prototype 3:
video/rtsp/camera -> decoder -> 2d convert -> opencv -> display sink
Implement
首先opencv在gstreamer是有plugin的, 但是從應用開(kāi)發(fā)的角度, 這樣不夠flexible :plugin里的東西和外界是封閉的. 在實(shí)現上, 更建議使用Appsink和AppSrc, 這些模塊, 在你的應用里,是以Thread的形式存在的, 開(kāi)發(fā)起來(lái)要更方便.
另外還有一點(diǎn)很重要, 就是什么gstreamer, gobject, 其實(shí)挺難用, 用C++會(huì )舒服很多。
代碼結構上很簡(jiǎn)單: Gstreamer AppSink不停的送Buffer, 應用MMap出來(lái)給OpenCV處理,完后AppSrc送會(huì )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";
代碼解釋
這段是核心的代碼,是buffer處理過(guò)程,見(jiàn)其中的中文注釋。
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); mem = gst_buffer_peek_memory(buffer, 0); # 注意這里拿到dmabuf的fd啦?。。。?!很重要 fd = gst_dmabuf_memory_get_fd(mem); # 為什么不直接用gstreamer里已經(jīng)mmap過(guò)的地址?因為gstreamer有權限問(wèn)題,有可能mmap成只讀的了 # 這里拿到buffer可讀的地址了?。。。。。?!fd就是這么轉vaddr的 map_data = mmap64(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); std::list::iterator itor = effect_lists.begin(); while (itor != effect_lists.end()) { # 因為用了RGA,視頻解碼后的nv12已經(jīng)變成rgb的了,你也可以不用rga,那opencv里就要當nv12處理 (*itor)->Process((void*)map_data, width, height); itor++; } munmap(map_data, size); # refcount加一,appsource pipeline的過(guò)程處理完了,他會(huì )減一 gst_buffer_ref(buffer); src_pipeline__->SendBUF(buffer); sink_pipeline__->ReleaseFrameBuffer(); } }}