Android UVC Camera获取的yuv转Mat

2023-07-14 11:06:48 浏览数 (2)

1. 前言

碰见一种特殊情况,Android 设备没有默认集成Camera摄像头。只好选择了 usb 摄像头。

一开始临时拿了个比较老的usb摄像头(ps:标注1080p,但是清晰度不太好)。插入设备的USB口之后,通过Android相机可以正确唤起设备。

也就是系统本身自动加载了该相机。之后在开发过程中直接通过CameraX 可以加载这个USB摄像头。

但是有两种问题:

  1. 设备经常在使用和关闭切换过程中,出现相机错误,无法使用的问题。
  2. 摄像头发现速度比较慢。

考虑了之后,打算换个高清点的摄像头。就买了一个2K的高清USB摄像头。

结果发现CameraX发现不了设备了。

没办法,系统改不了的情况下。选择了UVC协议加载USB摄像头。

1.1 UVC Camera

还好在Android平台上有大佬提供了UVC 加载USB摄像头的开源库。https://github.com/jiangdongguo/AndroidUSBCamera

依赖该库之后,可以正常加载和显示USB摄像头的画面了。

以下基于AndroidUSBCamera 3.2.10版本 因为不想用多路相机,同时3.3.x之后部分api进行了比较大的改动。

同时,根据项目的readme介绍文档,找到了获取Camera的实时回调的 yuv 数据

代码语言:javascript复制
        //获取 相机原始数据 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的计算。

然后中间出现了各种异常和问题。本篇内容就是记录一下,我碰见的各种情况和最后的解决方法。

2. 转换yuv byte 转 Bitmap

笨办法可以先将yuv转Bitmap,然后再使用OpenCV提供的Utils.btimapToMat转换成Mat。

但是很明显,中间的转换过程可以进行优化。或者我们直接使用AndroidUSBCamera 库中的cameraClient.captureImage直接得到图片算了。(ps:这个方法会将相机数据输出为本地文件存储。)然后再转换。

2.1 方法一

将yuv byte[] 转Bitmap 的步骤如下:

代码语言:javascript复制
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就可以了。

代码语言:javascript复制
import org.opencv.android.Utils;

Mat yuv = null;
Utils.bitmapToMat(bitmap, yuv);

2.2 方法二

除了上诉的的方法以外,我们还可以使用Android提供的ScriptIntrinsicYuvToRGB进行转换。

这种方式转换Bitmap的效率要比上面通过YuvImage进行转换的速度快。

代码语言:javascript复制
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之后进行显示时需要注意线程切换。

3. yuv byte [] 转 Mat

上面的转换过程都先进行了Bitmap转换,但是OpenCV现在可以直接将yuv数据填充到Mat中。

如果是处理好的yuv数组,我们应该是可以直接使用:

代码语言:javascript复制
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只需要写为:

代码语言:javascript复制
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就可以了。

3.1 yuv 数据格式

在得到的yuv的 byte[]的数据的数组长度应该是:width*height*3/2

验证一下:

代码语言:javascript复制
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。如果不太能理解的话:

还是用上面计算的进行拆分介绍:

代码语言:javascript复制
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格式的数据效果如下:

代码语言:javascript复制
[               
	Y Y Y Y      
	Y Y Y Y    
	U V U V 
] 

NV21的数据格式,就刚好和NV12相反了:

代码语言:javascript复制
[
    Y Y Y Y
    Y Y Y Y
    V U V U
]               

通过分析,我们如果直接从byte中提取到指定长度应该是能够正常显示灰度图的。所以,我们验证一下:

代码语言:javascript复制
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数据。

4.小结

到这里,转换就算结束了。希望对于转换过程中出现问题的小伙伴们,有一点点参考价值。

0 人点赞