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