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;i< topFrame.total();i++) { r = topFrame.data[3 * i + 2]; g = topFrame.data[3 * i + 1]; b = topFrame.data[3 * i + 0]; data[i] = (((jint)r << 16) & 0x00FF0000) + (((jint)g << 8) & 0x0000FF00) + ((jint)b & 0x000000FF); } env->SetIntArrayRegion(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;i< topFrame.total();i++) { r = topFrame.data[3 * i + 2]; g = topFrame.data[3 * i + 1]; b = topFrame.data[3 * i + 0]; data[i] = (((jint)r << 16) & 0x00FF0000) + (((jint)g << 8) & 0x0000FF00) + ((jint)b & 0x000000FF); } env->SetIntArrayRegion(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;i< mat.total();i++) { r = mat.data[3 * i + 2]; g = mat.data[3 * i + 1]; b = mat.data[3 * i + 0]; data[i] = (((jint)r << 16) & 0x00FF0000) + (((jint)g << 8) & 0x0000FF00) + ((jint)b & 0x000000FF); } jint* _data = (jint*)data; int size = mat.rows * mat.cols; jintArray jarray = env->NewIntArray(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文章
1292浏览量
56744 -
OpenCV
+关注
关注
31文章
635浏览量
41347 -
USB摄像头
+关注
关注
0文章
22浏览量
11266
原文标题:OpenCV + Kotlin 实现 USB 摄像头(相机)实时画面、拍照
文章出处:【微信号:CVSCHOOL,微信公众号:OpenCV学堂】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论