0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

使用JavaCV调用USB摄像头进行实时画面的展示和拍照

OpenCV学堂 来源:Java与Android技术栈 作者:fengzhizi715 2022-10-09 10:26 次阅读

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 之后,软件遇到的痛点问题基本可以解决。例如软件体积明显变小了。

2416f652-4197-11ed-96c9-dac502259ad0.png

另外,软件在运行时占用大量内存的情况也得到明显改善。如果需要在展示实时画面时,对图像做一些处理,也可以在 Native 层使用 OpenCV 来处理每一帧,然后将结果返回给应用层。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 图像处理
    +关注

    关注

    26

    文章

    1228

    浏览量

    55851
  • OpenCV
    +关注

    关注

    29

    文章

    611

    浏览量

    40795
  • USB摄像头
    +关注

    关注

    0

    文章

    22

    浏览量

    11191

原文标题:OpenCV + Kotlin 实现 USB 摄像头(相机)实时画面、拍照

文章出处:【微信号:CVSCHOOL,微信公众号:OpenCV学堂】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    解决USB摄像头所导致的花屏故障

    成像画面立即恢复正常了。看来摄像头花屏故障是由于计算机原先的USB端口损坏引起的,于是笔者又对原先的USB端口进行了检查,最后发现原先的
    发表于 03-16 09:53

    求一个用labview编写的控制摄像头拍照及保存的例子,谢谢!

    求一个用labview编写的控制摄像头拍照及保存的例子,谢谢!调用摄像头应该是怎么调用,然后调用
    发表于 03-15 13:05

    摄像头用色温镜挡住拍照很暗

    `大家好,最近在弄一个串口摄像头摄像头sensor是OV7725,DSP芯片是CL8529。现在有个问题,用蓝色的色温镜挡住摄像头的时候,拍照很暗,同时晚上红外
    发表于 06-25 11:27

    【转载分享】USB摄像头采集图像

    `如果你有USB摄像头,就是随便的那种。平时QQ视频的就可以了(笔记本上自带的摄像头,也可以),那你就可以用LabVIEW进行图像采集了。注意:由于LabVIEW8.6是个神奇的版本,
    发表于 03-02 11:36

    android多摄像头同时预览

    程序可以打开,预览画面还算流畅,大概25帧左右。调试USB摄像头通过UVC生成了video节点,接着移植了camera的HAL层库文件,可以顺利打开。当要调用第二个
    发表于 01-18 14:55

    自制USB摄像头程序,可拍照,可以录制视频

    `利用NI Vision做的USB摄像头程序如果有多个USB摄像头,可以从其中选择想要用的摄像头可以拍照
    发表于 03-14 12:17

    怎样通过串口传输摄像头画面

    各位大神们,帮帮忙啊~~~~~怎样通过串口显示摄像头画面呢让画面实时显示在labview前面板上面。。当然 最好有个做好的小例子参考一下就最好啦
    发表于 03-08 10:25

    labview调用USB摄像头无法选择其它摄像头

    labview调用USB摄像头无法选择其它摄像头,选cam1但是程序采集的图像是cam0的图像 补充内容 (2017-9-12 15:48): 把session in改变为常量也不管
    发表于 09-12 10:05

    请问USB摄像头怎么在树莓派中实现拍照功能?

    求助各位大神USB摄像头怎么在树莓派中实现拍照功能
    发表于 05-28 05:56

    请问树莓派官方的摄像头进行实时监控吗?

    请问树莓派官方的摄像头不能进行实时监控吗!是不是实现实时监控只能用USB摄像头啊!
    发表于 11-11 05:55

    NI vision调用笔记本摄像头拍照做的子程序

    用NI Vision控件做的一个调用笔记本摄像头拍照的程序
    发表于 09-30 14:37

    OV7670摄像头模块是如何实现摄像头画面上传到onenet的

    OV7670摄像头模块是如何实现摄像头画面上传到onenet的?
    发表于 09-30 08:53

    Labview 实时分析USB摄像头拍摄照片直方图

    本帖最后由 laowu2021 于 2021-11-16 22:06 编辑 Labview 打开USB摄像头,并实时分析红绿蓝的直方图。感谢两位朋友的指导,尝试用双循环实现定时拍照
    发表于 10-16 21:04

    为什么哪吒板跑USB摄像头拍照Demo时jpeg文件打不开呢

    哪吒板跑USB摄像头拍照Demo时jpeg文件打不开,图像不显示,已确认摄像头功能正常,支持jpeg格式的图片。用哪个软件抓的数据包呢? 具体步骤是什么?是哪一个文档呢?软件打开输出的
    发表于 03-29 19:16

    【赛昉科技昉·星光RISC-V单板计算机试用体验】使用海康威视USB摄像头拍照和录制视频

    拍照部分,我使用了camorama,可以实时预览摄像头画面,并且在界面上点击拍照。录制视频部分,可以试用的软件很多,我选择了使用命令行的f
    发表于 07-17 11:42