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 來處理每一幀,然后將結果返回給應用層。
審核編輯:劉清
-
圖像處理
+關注
關注
29文章
1342瀏覽量
59542 -
OpenCV
+關注
關注
33文章
652瀏覽量
44800 -
USB攝像頭
+關注
關注
0文章
25瀏覽量
11686
原文標題:OpenCV + Kotlin 實現 USB 攝像頭(相機)實時畫面、拍照
文章出處:【微信號:CVSCHOOL,微信公眾號:OpenCV學堂】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
解決USB攝像頭所導致的花屏故障
攝像頭用色溫鏡擋住拍照很暗
android多攝像頭同時預覽
怎樣通過串口傳輸攝像頭畫面
labview調用USB攝像頭無法選擇其它攝像頭
【賽昉科技昉·星光RISC-V單板計算機試用體驗】使用海康威視USB攝像頭拍照和錄制視頻
如何在deepstream-app里調用USB與CSI攝像頭
使用JavaCV調用USB攝像頭進行實時畫面的展示和拍照
評論