碰见一种特殊情况,Android 设备没有默认集成Camera摄像头。只好选择了 usb 摄像头。
一开始临时拿了个比较老的usb摄像头(ps:标注1080p,但是清晰度不太好)。插入设备的USB口之后,通过Android相机可以正确唤起设备。
也就是系统本身自动加载了该相机。之后在开发过程中直接通过CameraX 可以加载这个USB摄像头。
但是有两种问题:
考虑了之后,打算换个高清点的摄像头。就买了一个2K的高清USB摄像头。
结果发现CameraX发现不了设备了。
没办法,系统改不了的情况下。选择了UVC协议加载USB摄像头。
还好在Android平台上有大佬提供了UVC 加载USB摄像头的开源库。https://github.com/jiangdongguo/AndroidUSBCamera
依赖该库之后,可以正常加载和显示USB摄像头的画面了。
以下基于AndroidUSBCamera 3.2.10版本 因为不想用多路相机,同时3.3.x之后部分api进行了比较大的改动。
同时,根据项目的readme介绍文档,找到了获取Camera的实时回调的 yuv 数据
//获取 相机原始数据 yuv
cameraClient.addPreviewDataCallBack(new IPreviewDataCallBack() {
@Override
public void onPreviewData(@Nullable byte[] bytes, @NonNull DataFormat dataFormat) {
// Log.e("TAG", "请求得到 YUV数据");
assert bytes != null;
// Log.e("TAG", "YUV数据长度" + bytes.length);
}
});
得到 byte[]
的yuv数据之后。(PS:该yuv是 NV21格式的)根据我的业务需求。
我需要将yuv数组转为Mat用于OpenCV的计算。
然后中间出现了各种异常和问题。本篇内容就是记录一下,我碰见的各种情况和最后的解决方法。
笨办法可以先将yuv转Bitmap,然后再使用OpenCV提供的Utils.btimapToMat
转换成Mat。
但是很明显,中间的转换过程可以进行优化。或者我们直接使用AndroidUSBCamera
库中的cameraClient.captureImage
直接得到图片算了。(ps:这个方法会将相机数据输出为本地文件存储。)然后再转换。
将yuv byte[] 转Bitmap 的步骤如下:
byte[] imageInBuffer ;// 这个是我们的byte数组
FrameMetadata frameMetadata = new FrameMetadata.Builder().setHeight(previewHeight).setWidth(previewWidth).setRotation(0).build(); //是我们的图片对的宽高信息和旋转角度。
try {
YuvImage image =
new YuvImage(
imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream);
Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());
stream.close();
return rotateBitmap(bmp, metadata.getRotation(), false, false);
} catch (Exception e) {
Log.e("VisionProcessorBase", "Error: " + e.getMessage());
}
然后我们就能得到Bitmap bmp了。只需要将该bmp转换为Mat就可以了。
import org.opencv.android.Utils;
Mat yuv = null;
Utils.bitmapToMat(bitmap, yuv);
除了上诉的的方法以外,我们还可以使用Android提供的ScriptIntrinsicYuvToRGB
进行转换。
这种方式转换Bitmap的效率要比上面通过YuvImage
进行转换的速度快。
public class FastYUVtoRGB {
private RenderScript rs;
private ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic;
private Type.Builder yuvType, rgbaType;
private Allocation in, out;
public FastYUVtoRGB(Context context) {
rs = RenderScript.create(context);
yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs));
}
public Bitmap convertYUVtoRGB(byte[] yuvData, int width, int height) {
if (yuvType == null) {
yuvType = new Type.Builder(rs, Element.U8(rs)).setX(yuvData.length);
in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);
rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);
}
in.copyFrom(yuvData);
yuvToRgbIntrinsic.setInput(in);
yuvToRgbIntrinsic.forEach(out);
Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
out.copyTo(bmpout);
return bmpout;
}
}
PS:AndroidUSBCamera库中返回的byte数据是NV21格式的。同时该接口是异步线程。所以我们转成Bitmap之后进行显示时需要注意线程切换。
上面的转换过程都先进行了Bitmap转换,但是OpenCV现在可以直接将yuv数据填充到Mat中。
如果是处理好的yuv数组,我们应该是可以直接使用:
Mat yuv_mat = new Mat(height + (height / 2), width, CvType.CV_8UC1);
yuv_mat.put(0, 0, bytes);
Mat bgr_i420 = new Mat();
Imgproc.cvtColor(yuv_mat, bgr_i420, Imgproc.COLOR_YUV2RGBA_I420, 4);
Bitmap bitmap1 = Bitmap.createBitmap(bgr_i420.width(), bgr_i420.height(), Bitmap.Config.ARGB_8888);
Utils.matToBitmap(bgr_i420, bitmap1); //将 mat转bitmap
viewBinding.imSitArea.setImageBitmap(bitmap1); //使用ImageView 显示该bitmap
按照上面的代码直接使用后,我们显示的图片是灰度图。原因在哪里,原因在于我格式配置错误了。
因为我们的数据是YUV NV21也就是YUV420sp。图像数据比值关系是4:2:0
所以,我们如果想将相机得到的yuv数据,转换为Mat只需要写为:
Mat yuv_mat = new Mat(480 + (480 / 2), 640, CvType.CV_8UC1);
yuv_mat.put(0, 0, bytes);
Mat rgb_mat = new Mat();
Imgproc.cvtColor(yuv_mat, rgb_mat, Imgproc.COLOR_YUV420sp2RGB);
就能得到一个彩色的Mat对象了。之后再根据我们的需求进行处理Mat就可以了。
在得到的yuv的 byte[]
的数据的数组长度应该是:width*height*3/2
验证一下:
cameraClient.addPreviewDataCallBack(new IPreviewDataCallBack() {
@Override
public void onPreviewData(@Nullable byte[] bytes, @NonNull DataFormat dataFormat) {
//Log.e("TAG", "请求得到 YUV数据");
assert bytes != null;
Log.e("TAG", "YUV数据长度" + bytes.length);
}
//输出: YUV数据长度460800
已知我的宽高为640*480 。那么代入到上面的计算方法中: 640*480*3/2
= 460800。完全符合输出的数组长度。
yuv 中,Y代表的亮度值,而UV是颜色值。NV21属于YUV420格式 。也就是4:2:0的关系。
每四个Y值对应一个点U和一个点V。如果不太能理解的话:
还是用上面计算的进行拆分介绍:
Y = 640*480 = 307200
// 每四个Y 对应一个U和V那么可算出U和V的数量:
U =76800
V =76800
Y+U+V =307200+76800+76800 = 460800
到这里我们就能理解了吧,byte[]数组中,每个值其实就代表了Y,U,V 中某个信息值。那么我们如何去区分数组中哪些值是Y,哪些值是U哪些值是V。
就需要知道YUV格式了,也就是上面介绍的NV21了。
在YUV420格式中width*height 代表的是Y的值,而后面的就是UV的值了。
NV12类型的YUV420格式的数据效果如下:
[
Y Y Y Y
Y Y Y Y
U V U V
]
NV21的数据格式,就刚好和NV12相反了:
[
Y Y Y Y
Y Y Y Y
V U V U
]
通过分析,我们如果直接从byte中提取到指定长度应该是能够正常显示灰度图的。所以,我们验证一下:
byte[] s ;// 这个是相机返回的 yuv420数据
Mat yuv_mat = new Mat(480, 640, CvType.CV_8UC1);
yuv_mat.put(0, 0, s);
我们如果直接显示该yuv_mat 就会得到一个灰度图的效果。说明逻辑和方法是正确的。
那么,Mat是怎么识别byte数组中的nv的顺序的呢?很简单,通过我们后面
Imgproc.cvtColor(yuv_mat, rgb_mat, Imgproc.COLOR_YUV420sp2RGB);
方法中的 Imgproc.COLOR_YUV420sp2RGB
判断的。
上面这个代码的作用是,将yuv_mat中的数据采用YUV420sp格式转换为RGB格式,并赋值给rgb_mat
。
因为YUV NV21或者 NV12格式数据,在Mat中识别为了YUV420sp,我们可以统一使用YUV420sp将NV21或NV12格式的yuv数据组成的Mat转换为其他的Mat数据。
到这里,转换就算结束了。希望对于转换过程中出现问题的小伙伴们,有一点点参考价值。