Part1一. 業(yè)務(wù)背景
我們團(tuán)隊前段時間做了一款小型的智能硬件,它能夠自動拍攝一些商品的圖片,這些圖片將會出現(xiàn)在電商 App 的詳情頁并進(jìn)行展示。
基于以上的背景,我們需要一個業(yè)務(wù)后臺用于發(fā)送相應(yīng)的拍照指令,還需要開發(fā)一款軟件(上位機)用于接收拍照指令和操作硬件設(shè)備。
Part2二. 原先的實現(xiàn)方式以及痛點
早期為了快速實現(xiàn)功能,我們團(tuán)隊使用 JavaCV 調(diào)用 USB 攝像頭(相機)進(jìn)行實時畫面的展示和拍照。這樣的好處在于,能夠快速實現(xiàn)產(chǎn)品經(jīng)理提出的功能,并快速上線。當(dāng)然,也會遇到一些問題。
我列舉幾個遇到的問題:
軟件體積過大
編譯速度慢
軟件運行時占用大量的內(nèi)存
對于獲取的實時畫面,不利于在軟件側(cè)(客戶端側(cè))調(diào)用機器學(xué)習(xí)或者深度學(xué)習(xí)的庫,因為整個軟件采用 Java/Kotlin 編寫的。
Part3三. 使用 OpenCV 進(jìn)行重構(gòu)
基于上述的原因,我嘗試用 OpenCV 替代 JavaCV 看看能否解決這些問題。
13.1JNI 調(diào)用的設(shè)計
由于我使用 OpenCV C++ 版本來進(jìn)行開發(fā),因此在開發(fā)之前需要先設(shè)計好應(yīng)用層(我們的軟件主要是采用 Java/Kotlin 編寫的)如何跟 Native 層進(jìn)行交互的一些的方法。比如:USB 攝像頭(相機)的開啟和關(guān)閉、拍照、相機相關(guān)參數(shù)的設(shè)置等等。
為此,設(shè)計了一個專門用于圖像處理的類 WImagesProcess(W 是項目的代號),它包含了上述的方法。
objectWImagesProcess{ init{ System.load("${FileUtil.loadPath}WImagesProcess.dll") } /** *算法的版本號 */ externalfungetVersion():String /** *獲取OpenCV對應(yīng)相機的indexid *@parampidvid相機的pid、vid */ externalfungetCameraIndexIdFromPidVid(pidvid:String):Int /** *開啟俯拍相機 *@paramindex相機的indexid *@paramcameraParaMap相機相關(guān)的參數(shù) *@paramlistenerjni層給Java層的回調(diào) */ externalfunstartTopVideoCapture(index:Int,cameraParaMap:Map,listener:VideoCaptureListener) /** *開啟側(cè)拍相機 *@paramindex相機的indexid *@paramcameraParaMap相機相關(guān)的參數(shù) *@paramlistenerjni層給Java層的回調(diào) */ externalfunstartRightVideoCapture(index:Int,cameraParaMap:Map ,listener:VideoCaptureListener) /** *調(diào)用對應(yīng)的相機拍攝照片,使用時需要將IntArray轉(zhuǎn)換成BufferedImage *@paramcameraId1:俯拍相機;2:側(cè)拍相機 */ externalfuntakePhoto(cameraId:Int):IntArray /** *設(shè)置相機的曝光 *@paramcameraId1:俯拍相機;2:側(cè)拍相機 */ externalfunexposure(cameraId:Int,value:Double):Double /** *設(shè)置相機的亮度 *@paramcameraId1:俯拍相機;2:側(cè)拍相機 */ externalfunbrightness(cameraId:Int,value:Double):Double /** *設(shè)置相機的焦距 *@paramcameraId1:俯拍相機;2:側(cè)拍相機 */ externalfunfocus(cameraId:Int,value:Double):Double /** *關(guān)閉相機,釋放相機的資源 *@paramcameraId1:俯拍相機;2:側(cè)拍相機 */ externalfuncloseVideoCapture(cameraId:Int) }
其中,VideoCaptureListener 是監(jiān)聽 USB 攝像頭(相機)行為的 Listener。
interfaceVideoCaptureListener{ /** *Native層調(diào)用相機成功 */ funonSuccess() /** *jni將Native層調(diào)用相機獲取每一幀的Mat轉(zhuǎn)換成IntArray,回調(diào)給Java層 *@paramarray回調(diào)給Java層的IntArray,Java層可以將其轉(zhuǎn)化成BufferedImage */ funonRead(array:IntArray) /** *Native層調(diào)用相機失敗 */ funonFailed() }
VideoCaptureListener#onRead() 方法是在攝像頭(相機)打開后,會實時將每一幀的數(shù)據(jù)通過回調(diào)的形式返回給應(yīng)用層。
23.2 JNI && Native 層的實現(xiàn)
定義一個 xxx_WImagesProcess.h,它與應(yīng)用層的 WImagesProcess 類對應(yīng)。
#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 的規(guī)范。
接下來,需要定義一個 xxx_WImagesProcess.cpp 用于實現(xiàn)上述的方法。
3.2.1 USB 攝像頭(相機)的開啟
僅以 startTopVideoCapture() 為例,它的作用是開啟智能硬件的俯拍相機,該硬件有 2 款相機介紹其中一種實現(xiàn)方式,另一種也很類似。
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 相關(guān)的內(nèi)容,接下來會簡單說明。
首先,JavaHashMapToStlMap() 方法用于將 Java 的 HashMap 轉(zhuǎn)換成 C++ STL 的 Map。開啟相機時,需要傳遞相機相關(guān)的參數(shù)。由于相機需要設(shè)置參數(shù)很多,因此在應(yīng)用層使用 HashMap,傳遞到 JNI 層需要將他們進(jìn)行轉(zhuǎn)化成 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); } }
接下來幾行,表示將應(yīng)用層傳遞的 VideoCaptureListener 在 JNI 層需要獲取其類型。然后,查找 VideoCaptureListener 中的幾個方法,便于后面調(diào)用。這樣 JNI 層就可以跟應(yīng)用層的 Java/Kotlin 進(jìn)行交互了。
jclasslistenerClass=env->GetObjectClass(topListener); jmethodIDsuccessId=env->GetMethodID(listenerClass,"onSuccess","()V"); jmethodIDreadId=env->GetMethodID(listenerClass,"onRead","([I)V"); jmethodIDfailedId=env->GetMethodID(listenerClass,"onFailed","()V");
接下來,開始打開攝像頭(相機),并回調(diào)給應(yīng)用層,這樣 VideoCaptureListener#onSuccess() 方法就能收到回調(diào)。
topVideoCapture=wImageProcess.getVideoCapture(index,mapOut); env->CallVoidMethod(listenerObject,successId);
打開攝像頭(相機)后,就可以實時把獲取的每一幀返回給應(yīng)用層。同樣,VideoCaptureListener#onRead() 方法就能收到回調(diào)。
while(topFlag){ topVideoCapture>>topFrame; for(inti=0;iSetIntArrayRegion(jarray,0,size,(jint*)data); env->CallVoidMethod(listenerObject,readId,jarray); waitKey(100); }
后面的代碼是關(guān)閉相機,釋放資源。
3.2.2 打開相機,設(shè)置相機參數(shù)
在 3.2.1 中,有以下這樣一段代碼:
topVideoCapture=wImageProcess.getVideoCapture(index,mapOut);
它的用途是通過 index id 打開對應(yīng)的相機,并設(shè)置相機需要的參數(shù),最后返回 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; }
對于存在同時調(diào)用多個相機的情況,OpenCV 需要基于 index id 來獲取對應(yīng)的相機。那如何獲取 index id 呢?以后有機會再寫一篇文章吧。
WImagesProcess 類還額外提供了多個方法用于設(shè)置相機的曝光、亮度、焦距等。我們在啟動相機的時候不是可以通過 HashMap 來傳遞相機需要的參數(shù)嘛,為何還提供這些方法呢?這樣做的目的是因為針對不同商品拍照時,可能會調(diào)節(jié)相機相關(guān)的參數(shù),因此 WImagesProcess 類提供了這些方法。
3.2.3 拍照
基于 cameraId 來找到對應(yīng)的相機進(jìn)行拍照,并將結(jié)果返回給應(yīng)用層,唯一需要注意的是 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 相關(guān)的代碼最終編譯成一個 dll 文件,供軟件(上位機)調(diào)用,實現(xiàn)最終的需求。
33.3 應(yīng)用層的調(diào)用
上述代碼寫好后,攝像頭(相機)在應(yīng)用層的打開就非常簡單了,大致的代碼如下:
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(){ ...... } })
應(yīng)用層的拍照也很簡單:
valbufferedImage=WImagesProcess.takePhoto(cameraId).toBufferedImage()
其中,toBufferedImage() 是 Kotlin 的擴展函數(shù)。因為 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 }
這樣,對于應(yīng)用層的調(diào)用是非常簡單的。
Part4四. 總結(jié)
通過 OpenCV 替換 JavaCV 之后,軟件遇到的痛點問題基本可以解決。例如軟件體積明顯變小了。
另外,軟件在運行時占用大量內(nèi)存的情況也得到明顯改善。如果需要在展示實時畫面時,對圖像做一些處理,也可以在 Native 層使用 OpenCV 來處理每一幀,然后將結(jié)果返回給應(yīng)用層。
審核編輯:劉清
-
圖像處理
+關(guān)注
關(guān)注
27文章
1300瀏覽量
56854 -
OpenCV
+關(guān)注
關(guān)注
31文章
635瀏覽量
41488 -
USB攝像頭
+關(guān)注
關(guān)注
0文章
22瀏覽量
11321
原文標(biāo)題:OpenCV + Kotlin 實現(xiàn) USB 攝像頭(相機)實時畫面、拍照
文章出處:【微信號:CVSCHOOL,微信公眾號:OpenCV學(xué)堂】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論