Part1一. 業務背景
我們團隊前段時間做了一款小型的智能硬件,它能夠自動拍攝一些商品的圖片,這些圖片將會出現在電商 App 的詳情頁并進行展示。
基于以上的背景,我們需要一個業務后臺用于發送相應的拍照指令,還需要開發一款軟件(上位機)用于接收拍照指令和操作硬件設備。
Part2二. 原先的實現方式以及痛點
早期為了快速實現功能,我們團隊使用 JavaCV 調用 USB 攝像頭(相機)進行實時畫面的展示和拍照。這樣的好處在于,能夠快速實現產品經理提出的功能,并快速上線。當然,也會遇到一些問題。
我列舉幾個遇到的問題:
軟件體積過大
編譯速度慢
軟件運行時占用大量的內存
對于獲取的實時畫面,不利于在軟件側(客戶端側)調用機器學習或者深度學習的庫,因為整個軟件采用 Java/Kotlin 編寫的。
Part3三. 使用 OpenCV 進行重構
基于上述的原因,我嘗試用 OpenCV 替代 JavaCV 看看能否解決這些問題。
13.1JNI 調用的設計
由于我使用 OpenCV C++ 版本來進行開發,因此在開發之前需要先設計好應用層(我們的軟件主要是采用 Java/Kotlin 編寫的)如何跟 Native 層進行交互的一些的方法。比如:USB 攝像頭(相機)的開啟和關閉、拍照、相機相關參數的設置等等。
為此,設計了一個專門用于圖像處理的類 WImagesProcess(W 是項目的代號),它包含了上述的方法。
objectWImagesProcess{ init{ System.load("${FileUtil.loadPath}WImagesProcess.dll") } /** *算法的版本號 */ externalfungetVersion():String /** *獲取OpenCV對應相機的indexid *@parampidvid相機的pid、vid */ externalfungetCameraIndexIdFromPidVid(pidvid:String):Int /** *開啟俯拍相機 *@paramindex相機的indexid *@paramcameraParaMap相機相關的參數 *@paramlistenerjni層給Java層的回調 */ externalfunstartTopVideoCapture(index:Int,cameraParaMap:Map,listener:VideoCaptureListener) /** *開啟側拍相機 *@paramindex相機的indexid *@paramcameraParaMap相機相關的參數 *@paramlistenerjni層給Java層的回調 */ externalfunstartRightVideoCapture(index:Int,cameraParaMap:Map ,listener:VideoCaptureListener) /** *調用對應的相機拍攝照片,使用時需要將IntArray轉換成BufferedImage *@paramcameraId1:俯拍相機;2:側拍相機 */ externalfuntakePhoto(cameraId:Int):IntArray /** *設置相機的曝光 *@paramcameraId1:俯拍相機;2:側拍相機 */ externalfunexposure(cameraId:Int,value:Double):Double /** *設置相機的亮度 *@paramcameraId1:俯拍相機;2:側拍相機 */ externalfunbrightness(cameraId:Int,value:Double):Double /** *設置相機的焦距 *@paramcameraId1:俯拍相機;2:側拍相機 */ externalfunfocus(cameraId:Int,value:Double):Double /** *關閉相機,釋放相機的資源 *@paramcameraId1:俯拍相機;2:側拍相機 */ externalfuncloseVideoCapture(cameraId:Int) }
其中,VideoCaptureListener 是監聽 USB 攝像頭(相機)行為的 Listener。
interfaceVideoCaptureListener{ /** *Native層調用相機成功 */ funonSuccess() /** *jni將Native層調用相機獲取每一幀的Mat轉換成IntArray,回調給Java層 *@paramarray回調給Java層的IntArray,Java層可以將其轉化成BufferedImage */ funonRead(array:IntArray) /** *Native層調用相機失敗 */ funonFailed() }
VideoCaptureListener#onRead() 方法是在攝像頭(相機)打開后,會實時將每一幀的數據通過回調的形式返回給應用層。
23.2 JNI && Native 層的實現
定義一個 xxx_WImagesProcess.h,它與應用層的 WImagesProcess 類對應。
#include#ifndef_Include_xxx_WImagesProcess #define_Include_xxx_WImagesProcess #ifdef__cplusplus extern"C"{ #endif JNIEXPORTjstringJNICALLJava_xxx_WImagesProcess_getVersion (JNIEnv*env,jobject); JNIEXPORTvoidJNICALLJava_xxx_WImagesProcess_startTopVideoCapture (JNIEnv*env,jobject,intindex,jobjectcameraParaMap,jobjectlistener); JNIEXPORTvoidJNICALLJava_xxx_WImagesProcess_startRightVideoCapture (JNIEnv*env,jobject,intindex,jobjectcameraParaMap,jobjectlistener); JNIEXPORTjintArrayJNICALLJava_xxx_WImagesProcess_takePhoto (JNIEnv*env,jobject,intcameraId); JNIEXPORTdoubleJNICALLJava_xxx_WImagesProcess_exposure (JNIEnv*env,jobject,intcameraId,doublevalue); JNIEXPORTdoubleJNICALLJava_xxx_WImagesProcess_brightness (JNIEnv*env,jobject,intcameraId,doublevalue); JNIEXPORTdoubleJNICALLJava_xxx_WImagesProcess_focus (JNIEnv*env,jobject,intcameraId,doublevalue); JNIEXPORTvoidJNICALLJava_xxx_WImagesProcess_closeVideoCapture (JNIEnv*env,jobject,intcameraId); JNIEXPORTintJNICALLJava_xxx_WImagesProcess_getCameraIndexIdFromPidVid (JNIEnv*env,jobject,jstringpidvid); #ifdef__cplusplus } #endif #endif #pragmaonce
xxx 代表的是 Java 項目中 WImagesProcess 類所在的 package 名稱。畢竟是公司項目,我不便貼出完整的 package 名稱。不熟悉這種寫法的,可以參考 JNI 的規范。
接下來,需要定義一個 xxx_WImagesProcess.cpp 用于實現上述的方法。
3.2.1 USB 攝像頭(相機)的開啟
僅以 startTopVideoCapture() 為例,它的作用是開啟智能硬件的俯拍相機,該硬件有 2 款相機介紹其中一種實現方式,另一種也很類似。
JNIEXPORTvoidJNICALLJava_xxx_WImagesProcess_startTopVideoCapture (JNIEnv*env,jobject,intindex,jobjectcameraParaMap,jobjectlistener){ jobjecttopListener=env->NewLocalRef(listener); std::mapmapOut; JavaHashMapToStlMap(env,cameraParaMap,mapOut); jclasslistenerClass=env->GetObjectClass(topListener); jmethodIDsuccessId=env->GetMethodID(listenerClass,"onSuccess","()V"); jmethodIDreadId=env->GetMethodID(listenerClass,"onRead","([I)V"); jmethodIDfailedId=env->GetMethodID(listenerClass,"onFailed","()V"); jobjectlistenerObject=env->NewLocalRef(listenerClass); try{ topVideoCapture=wImageProcess.getVideoCapture(index,mapOut); env->CallVoidMethod(listenerObject,successId); jintArrayjarray; topVideoCapture>>topFrame; int*data=newint[topFrame.total()]; intsize=topFrame.rows*topFrame.cols; jarray=env->NewIntArray(size); charr,g,b; while(topFlag){ topVideoCapture>>topFrame; for(inti=0;iSetIntArrayRegion(jarray,0,size,(jint*)data); env->CallVoidMethod(listenerObject,readId,jarray); waitKey(100); } topVideoCapture.release(); env->ReleaseIntArrayElements(jarray,env->GetIntArrayElements(jarray,JNI_FALSE),0); delete[]data; } catch(...){ env->CallVoidMethod(listenerObject,failedId); } env->DeleteLocalRef(listenerObject); env->DeleteLocalRef(topListener); }
這個方法用了很多 JNI 相關的內容,接下來會簡單說明。
首先,JavaHashMapToStlMap() 方法用于將 Java 的 HashMap 轉換成 C++ STL 的 Map。開啟相機時,需要傳遞相機相關的參數。由于相機需要設置參數很多,因此在應用層使用 HashMap,傳遞到 JNI 層需要將他們進行轉化成 C++ 能用的 Map。
voidJavaHashMapToStlMap(JNIEnv*env,jobjecthashMap,std::map&mapOut){ //GettheMap'sentrySet. jclassmapClass=env->FindClass("java/util/Map"); if(mapClass==NULL){ return; } jmethodIDentrySet= env->GetMethodID(mapClass,"entrySet","()Ljava/util/Set;"); if(entrySet==NULL){ return; } jobjectset=env->CallObjectMethod(hashMap,entrySet); if(set==NULL){ return; } //ObtainaniteratorovertheSet jclasssetClass=env->FindClass("java/util/Set"); if(setClass==NULL){ return; } jmethodIDiterator= env->GetMethodID(setClass,"iterator","()Ljava/util/Iterator;"); if(iterator==NULL){ return; } jobjectiter=env->CallObjectMethod(set,iterator); if(iter==NULL){ return; } //GettheIteratormethodIDs jclassiteratorClass=env->FindClass("java/util/Iterator"); if(iteratorClass==NULL){ return; } jmethodIDhasNext=env->GetMethodID(iteratorClass,"hasNext","()Z"); if(hasNext==NULL){ return; } jmethodIDnext= env->GetMethodID(iteratorClass,"next","()Ljava/lang/Object;"); if(next==NULL){ return; } //GettheEntryclassmethodIDs jclassentryClass=env->FindClass("java/util/Map$Entry"); if(entryClass==NULL){ return; } jmethodIDgetKey= env->GetMethodID(entryClass,"getKey","()Ljava/lang/Object;"); if(getKey==NULL){ return; } jmethodIDgetValue= env->GetMethodID(entryClass,"getValue","()Ljava/lang/Object;"); if(getValue==NULL){ return; } //IterateovertheentrySet while(env->CallBooleanMethod(iter,hasNext)){ jobjectentry=env->CallObjectMethod(iter,next); jstringkey=(jstring)env->CallObjectMethod(entry,getKey); jstringvalue=(jstring)env->CallObjectMethod(entry,getValue); constchar*keyStr=env->GetStringUTFChars(key,NULL); if(!keyStr){ return; } constchar*valueStr=env->GetStringUTFChars(value,NULL); if(!valueStr){ env->ReleaseStringUTFChars(key,keyStr); return; } mapOut.insert(std::make_pair(string(keyStr),string(valueStr))); env->DeleteLocalRef(entry); env->ReleaseStringUTFChars(key,keyStr); env->DeleteLocalRef(key); env->ReleaseStringUTFChars(value,valueStr); env->DeleteLocalRef(value); } }
接下來幾行,表示將應用層傳遞的 VideoCaptureListener 在 JNI 層需要獲取其類型。然后,查找 VideoCaptureListener 中的幾個方法,便于后面調用。這樣 JNI 層就可以跟應用層的 Java/Kotlin 進行交互了。
jclasslistenerClass=env->GetObjectClass(topListener); jmethodIDsuccessId=env->GetMethodID(listenerClass,"onSuccess","()V"); jmethodIDreadId=env->GetMethodID(listenerClass,"onRead","([I)V"); jmethodIDfailedId=env->GetMethodID(listenerClass,"onFailed","()V");
接下來,開始打開攝像頭(相機),并回調給應用層,這樣 VideoCaptureListener#onSuccess() 方法就能收到回調。
topVideoCapture=wImageProcess.getVideoCapture(index,mapOut); env->CallVoidMethod(listenerObject,successId);
打開攝像頭(相機)后,就可以實時把獲取的每一幀返回給應用層。同樣,VideoCaptureListener#onRead() 方法就能收到回調。
while(topFlag){ topVideoCapture>>topFrame; for(inti=0;iSetIntArrayRegion(jarray,0,size,(jint*)data); env->CallVoidMethod(listenerObject,readId,jarray); waitKey(100); }
后面的代碼是關閉相機,釋放資源。
3.2.2 打開相機,設置相機參數
在 3.2.1 中,有以下這樣一段代碼:
topVideoCapture=wImageProcess.getVideoCapture(index,mapOut);
它的用途是通過 index id 打開對應的相機,并設置相機需要的參數,最后返回 VideoCapture 對象。
VideoCaptureWImageProcess::getVideoCapture(intindex,std::mapcameraParaMap){ VideoCapturecapture(index); for(auto&t:cameraParaMap){ intkey=stoi(t.first); doublevalue=stod(t.second); capture.set(key,value); } returncapture; }
對于存在同時調用多個相機的情況,OpenCV 需要基于 index id 來獲取對應的相機。那如何獲取 index id 呢?以后有機會再寫一篇文章吧。
WImagesProcess 類還額外提供了多個方法用于設置相機的曝光、亮度、焦距等。我們在啟動相機的時候不是可以通過 HashMap 來傳遞相機需要的參數嘛,為何還提供這些方法呢?這樣做的目的是因為針對不同商品拍照時,可能會調節相機相關的參數,因此 WImagesProcess 類提供了這些方法。
3.2.3 拍照
基于 cameraId 來找到對應的相機進行拍照,并將結果返回給應用層,唯一需要注意的是 C++ 得手動釋放資源。
JNIEXPORTjintArrayJNICALLJava_xxx_WImagesProcess_takePhoto (JNIEnv*env,jobject,intcameraId){ Matmat; if(cameraId==1){ mat=topFrame; } elseif(cameraId==2){ mat=rightFrame; } int*data=newint[mat.total()]; charr,g,b; for(inti=0;iNewIntArray(size); env->SetIntArrayRegion(jarray,0,size,_data); delete[]data; returnjarray; }
最后,將 CV 程序和 JNI 相關的代碼最終編譯成一個 dll 文件,供軟件(上位機)調用,實現最終的需求。
33.3 應用層的調用
上述代碼寫好后,攝像頭(相機)在應用層的打開就非常簡單了,大致的代碼如下:
valmap=HashMap() map[CAP_PROP_FRAME_WIDTH]=4208.toString() map[CAP_PROP_FRAME_HEIGHT]=3120.toString() map[CAP_PROP_AUTO_EXPOSURE]=0.25.toString() map[CAP_PROP_EXPOSURE]=getTopExposure() map[CAP_PROP_GAIN]=getTopFocus() map[CAP_PROP_BRIGHTNESS]=getTopBrightness() WImagesProcess.startTopVideoCapture(index+CAP_DSHOW,map,object:VideoCaptureListener{ overridefunonSuccess(){ ...... } overridefunonRead(array:IntArray){ ...... } overridefunonFailed(){ ...... } })
應用層的拍照也很簡單:
valbufferedImage=WImagesProcess.takePhoto(cameraId).toBufferedImage()
其中,toBufferedImage() 是 Kotlin 的擴展函數。因為 takePhoto() 方法返回 IntArray 對象。
funIntArray.toBufferedImage():BufferedImage{ valdestImage=BufferedImage(FRAME_WIDTH,FRAME_HEIGHT,BufferedImage.TYPE_INT_RGB) destImage.setRGB(0,0,FRAME_WIDTH,FRAME_HEIGHT,this,0,FRAME_WIDTH) returndestImage }
這樣,對于應用層的調用是非常簡單的。
Part4四. 總結
通過 OpenCV 替換 JavaCV 之后,軟件遇到的痛點問題基本可以解決。例如軟件體積明顯變小了。
另外,軟件在運行時占用大量內存的情況也得到明顯改善。如果需要在展示實時畫面時,對圖像做一些處理,也可以在 Native 層使用 OpenCV 來處理每一幀,然后將結果返回給應用層。
審核編輯:劉清
-
圖像處理
+關注
關注
27文章
1299瀏覽量
56837 -
OpenCV
+關注
關注
31文章
635瀏覽量
41453 -
USB攝像頭
+關注
關注
0文章
22瀏覽量
11317
原文標題:OpenCV + Kotlin 實現 USB 攝像頭(相機)實時畫面、拍照
文章出處:【微信號:CVSCHOOL,微信公眾號:OpenCV學堂】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論