本人在读研期间接到的项目,需要用一个工业内窥镜(支持USB和Type-C接口)外接到 Android 设备上,获取其中图像进行目标检测等后续需求。本文主要讲Android设备如何显示、获取USB摄像头采集到的每一帧图像,后续可以通过深度学习或者Android版本的OpenCV进行目标检测。
查阅目前的技术博客,得到了几位大牛的文章指点,最终成功完成了项目。所以本文期望在前辈的基础上进行总结,给后来者提供一些帮助。
因为行文时作者还是在校学生,接触Android时间也不算长,可能用方法比较繁琐笨拙,如有不对处,望指正。
荣耀V10手机一台(鸿蒙OS)、Redmi K30 PRO(Android 11)、工业内窥镜(支持USB和Type-C接口)
Android studio 4.2.1
UVC全称为USB Video Class,直接翻译过来的意思就是:USB视频类,它是一种专门为USB视频捕获设备定义的协议标准。
这个标准是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准,已经成为USB org标准之一。
现在的主流操作系统,都已提供UVC设备驱动,因此符合UVC规格的硬件设备在不需要安装任何的驱动程序下即可在主机中正常使用。是的,目前Android系统已经支持uvc设备。(摘自小驰笔记:https://www.jianshu.com/p/972e05fa76a3)
本文实现的功能使参考三位大牛的文章或源码。
UVCCamera 开源项目
里面有8个例程(从易到难),需要引用作者自己的 libuvccamera 库,有参考价值。(https://github.com/saki4510t/UVCCamera)
博主 小驰笔记 的开源项目
简书博主“小驰嘻嘻”的项目,小驰博主的文章要好好读一下,里面也是用到上面的 libuvccamera 库,但是里面用到了AIDL,像我这样的新手看不懂。(捂脸)(https://github.com/yorkZJC/UvcCameraDemo)
博主 jiangdongguo 的开源项目
这里面需要引入博主自己的 libuvccamera 库,不能用 UVCCamera 原作者的库,照着他的 demo 编写程序,是可以在自己的项目中实现USB摄像头预览的。(GitHub源码地址:https://github.com/jiangdongguo/AndroidUSBCamera)
最终本文参考了博主 jiangdongguo 的开源项目,并在博主无名之辈FTER文章的指导下完成。(https://blog.csdn.net/andrexpert/article/details/78324181)
下载博主 jiangdongguo 的开源项目,在自己的项目中引入该项目的 libuvccamera 模块。
接下来会遇到一个问题
参考博客 https://blog.csdn.net/shenggaofei/article/details/98055433 在project目录下的build.gradle对应位置添加以下代码解决(版本号比较低,所以为黄色,不影响运行)
classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5'
下一个问题
原因是 libuvccamera 模块中的各个版本已经定义为变量,而我们引入的时候没有给这些变量赋值,于是在开源项目中,找到声明版本号的地方,复制到自己项目的对应位置(也可以自己定义),之后重新同步一下,所有报错就消失了。
最后一步
在自己项目的 build.gradle 中添加依赖,否则 UVCCameraTextureView 图像预览控件将无法使用。
implementation project(':libusbcamera')
布局根据需求来吧,本文项目既要实时预览外接 USB相机拍摄的图像,又需要对图像进行分析,故首先把预览图像的控件摆上。 libuvccamera 模块中有一个定义好的控件 UVCCameraTextureView 直接拿来用就行。本文demo只有一个MainActivity,布局如下:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.serenegiant.usb.widget.UVCCameraTextureView
android:id="@+id/camera_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
FrameLayout>
androidx.constraintlayout.widget.ConstraintLayout>
权限方面,也按照开源项目配置好即可,在 AndroidManifest.xml 中加入以下权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="30"
tools:ignore="ScopedStorage"
/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.usb.host" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
如果报红,就按 alt+Enter 解决
初始化方面,在之前下载的 jiangdongguo 的开源项目 里面找到一个叫 CrashHandler.java 的文件,拷贝到自己的项目中,会出现两处报错。
第12行报错,源于原作者引用自己编写的类所在文件夹的名字,由于类没有拷贝过来而报错,删除这行即可。
136行就是该类不存在而报错,可以改为自己的类的名字,也可以干脆删除MyApplication.DIRECTORY_NAME,让日志文件直接存放在根目录下,日志文件会存放调试时候出现的bug。
还有一个地方需要注意,由于 Android 10 以上系统不支持静态的写权限,所以需要把 build.gradle 中 compileSdkVersion 和 targetSdkVersion 均改到 28 及以下。(这个问题不会在编译期报错,只会在插入USB摄像头的时候弹出错误提示,并且无法正常退出程序。困扰了我好久,看来还是对Android了解得不够多…)
package com.example.myapplication;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.PixelFormat;
import android.hardware.usb.UsbDevice;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.os.StrictMode;
import android.view.Surface;
import android.view.View;
import android.widget.Toast;
import com.jiangdg.usbcamera.UVCCameraHelper;
import com.jiangdg.usbcamera.utils.FileUtils;
import com.serenegiant.usb.CameraDialog;
import com.serenegiant.usb.USBMonitor;
import com.serenegiant.usb.UVCCamera;
import com.serenegiant.usb.common.AbstractUVCCameraHandler;
import com.serenegiant.usb.widget.CameraViewInterface;
public class MainActivity extends AppCompatActivity implements CameraDialog.CameraDialogParent, CameraViewInterface.Callback {
private CrashHandler mCrashHandler;
public View mTextureView;
private UVCCameraHelper mCameraHelper;
private CameraViewInterface mUVCCameraView;
private boolean isRequest;
private boolean isPreview;
private UVCCameraHelper.OnMyDevConnectListener listener = new UVCCameraHelper.OnMyDevConnectListener() {
@Override
public void onAttachDev(UsbDevice device) {
// request open permission
if (!isRequest) {
isRequest = true;
if (mCameraHelper != null) {
mCameraHelper.requestPermission(0);
}
}
}
@Override
public void onDettachDev(UsbDevice device) {
// close camera
if (isRequest) {
isRequest = false;
mCameraHelper.closeCamera();
showShortMsg(device.getDeviceName() + " is out");
}
}
@Override
public void onConnectDev(UsbDevice device, boolean isConnected) {
if (!isConnected) {
showShortMsg("fail to connect,please check resolution params");
isPreview = false;
} else {
isPreview = true;
showShortMsg("connecting");
// initialize seekbar
// need to wait UVCCamera initialize over
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
Looper.prepare();
Looper.loop();
}
}).start();
}
}
@Override
public void onDisConnectDev(UsbDevice device) {
showShortMsg("disconnecting");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AccessRequest();
/**------- USBCamera引入 ---------*/
mCrashHandler = CrashHandler.getInstance();
mCrashHandler.init(getApplicationContext(), getClass());
mTextureView = findViewById(R.id.camera_view);
// step.1 initialize UVCCameraHelper
mUVCCameraView = (CameraViewInterface) mTextureView;
mUVCCameraView.setCallback(this);
mCameraHelper = UVCCameraHelper.getInstance();
mCameraHelper.setDefaultFrameFormat(UVCCameraHelper.FRAME_FORMAT_MJPEG);
mCameraHelper.initUSBMonitor(this, mUVCCameraView, listener);
mCameraHelper.setOnPreviewFrameListener(new AbstractUVCCameraHandler.OnPreViewResultListener() {
@Override
public void onPreviewResult(byte[] data) {
// 获取单帧图像回调
}
});
}
public boolean isCameraOpened() {
return mCameraHelper.isCameraOpened();
}
private void showShortMsg(String msg) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
@Override
public void onSurfaceCreated(CameraViewInterface view, Surface surface) {
if (!isPreview && mCameraHelper.isCameraOpened()) {
mCameraHelper.startPreview(mUVCCameraView);
isPreview = true;
}
}
@Override
public void onSurfaceChanged(CameraViewInterface view, Surface surface, int width, int height) {
}
@Override
public void onSurfaceDestroy(CameraViewInterface view, Surface surface) {
if (isPreview && mCameraHelper.isCameraOpened()) {
mCameraHelper.stopPreview();
isPreview = false;
}
}
@Override
public USBMonitor getUSBMonitor() {
return mCameraHelper.getUSBMonitor();
}
@Override
public void onDialogResult(boolean canceled) {
if (canceled) {
showShortMsg("取消操作");
}
}
@Override
protected void onStart() {
super.onStart();
// step.2 register USB event broadcast
if (mCameraHelper != null) {
mCameraHelper.registerUSB();
}
}
@Override
protected void onResume() {
super.onResume();
}
@Override
protected void onStop() {
super.onStop();
// step.3 unregister USB event broadcast
if (mCameraHelper != null) {
mCameraHelper.unregisterUSB();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
FileUtils.releaseFile();
// step.4 release uvc camera resources
if (mCameraHelper != null) {
mCameraHelper.release();
}
}
/**
* 检查权限 方法
*/
private boolean checkPermission() {
//是否有权限
boolean haveCameraPermission = ContextCompat.checkSelfPermission(this,
Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
boolean haveWritePermission = ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
boolean haverReadPermission = ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
return haveCameraPermission && haveWritePermission && haverReadPermission;
}
/**
* 请求权限 方法
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private void requestPermissions() {
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, Manifest.permission.SYSTEM_ALERT_WINDOW,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}
private void AccessRequest() {
//动态权限检测和申请
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//大于Android 6.0
if (!checkPermission()) { //没有或没有全部授权
requestPermissions(); //请求权限
}
}
//加 StrictMode, Android 7.0以后,获取文件Uri需要加上这么一段
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
}
}
}
编译时又出现了问题
貌似找不到一个名叫 libusbcommon_v4.1.1 的文件。
这个文件在引入项目的时候其实已经就放在libuvccamera 模块中,把整个libs文件夹拷贝到自己的项目文件中,如下图所示。
然后在 build.gradle 中引入这个文件夹