移动端视频进阶(一):摄像头视频回调数据格式浅析

前言

最近一段时间,接触到移动端音视频通话相关的内容,主要是结合OpenCV,TensorFlow等做一些视频数据的分析,检测工作。中间碰到大量的问题,入坑了算是,这里总结一下!

摄像头数据回调

关于移动端调用摄像头的相关内容,这里就不多说了,我们直接来看回调得到的数据!

iOS

我们设置了AVCaptureVideoDataOutputSampleBufferDelegate代理后,就在下边方法中拿到的摄像头中的一帧数据CMSampleBufferRef

- (void)captureOutput:(AVCaptureOutput *)captureOutput
    didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
           fromConnection:(AVCaptureConnection *)connection

那么这一帧数据是什么格式呢?取决于我们在采集时候的设置:

NSDictionary *rgbOutputSettings = @{
      (__bridge NSString*)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)
  };
[self.videoDataOutput setVideoSettings:rgbOutputSettings];

这里设置的kCVPixelFormatType_32BGRA就是得到的数据的格式(32位BGRA)!
还有其他常用的格式,比如:kCVPixelFormatType_420YpCbCr8BiPlanarFullRangekCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,这两种都是NV12格式!

根据源码中的注释,可以知道kCVPixelFormatType_420YpCbCr8BiPlanarFullRange8位双平面组件Y'CbCr比例为4:2:0,全范围(亮度=[0,255] 色度=[1,255])
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange8位双平面组件Y'CbCr比例为4:2:0,视频范围(亮度=[16,235] 色度=[16,240])

Android

Camera

在5.0之前,使用CameraAPI,需要实现Camera.PreviewCallback接口,在回调方法onPreviewFrame中拿到数据:

@Override
public void onPreviewFrame(final byte[] bytes, final Camera camera) {
	....
}

这里的bytes数组就是得到yuv420sp格式的数据,也称为NV21格式。这是CameraAPI的默认预览格式。当然我们也可以指定预览的输出格式:

android.hardware.Camera.Parameters#setPreviewFormat(ImageFormat.NV21)}

具体格式都在ImageFormat类里边!
我们也可以通过调用android.hardware.Camera.Parameters#getSupportedPreviewFormats()来查看CameraAPI支持的预览格式!

Camera2

在5.0之后,Camera被废弃,使用Camera2API,需要实现OnImageAvailableListener接口,在回调方法onImageAvailable中拿到数据

@Override
public void onImageAvailable(final ImageReader reader) {
	final Image image = reader.acquireLatestImage();
	....
}

调用reader.acquireLatestImage()方法,就得到一个Image对象。
这里就要注意了,这个Image中存放的数据格式默认是谷歌自家的YUV_420_888,根据文档描述这是一种新的YUV格式YUV420Flexible。而且不再支持CameraAPI中预览回调默认的NV21格式

这块也是折腾好久啊,我们这里简单来了解一下:

final Plane[] planes = image.getPlanes();
yRowStride = planes[0].getRowStride();
final int uvRowStride = planes[1].getRowStride();
final int uvPixelStride = planes[1].getPixelStride();

YUV_420_888类型,其表示YUV420格式的集合,888表示Y、U、V分量中每个颜色占8bit。Image将三个分量存储在三个Plane类中,通过getPlanes()方法得到一个Plane数组,plane[0]存放Y分量,plane[1]存放U分量,plane[2]存放V分量。还有那么既然是YUV格式,Y分量就一定是连续存储的,那么重点就在U、V分量在数组中是如何的?!

下面了解一下Plane类型的两个属性rowStridepixelStride
rowStride:则是行跨度,就是每行存放的数据量。
pixelStride:指像素跨度,即在一个平面中,U/V分量的取值间隔,这里我们可以知道即使UV分量分别存储在不同平面中,他们也不一定是连续存储的(即PixelStride不总是为1)

举个例子,比如一个4x4的图片:
我们知道一个NV21格式的数据,如果在Camera的回调中,byte[]数组中分量以

YYYYYYYYYYYYYYYYVUVUVUVU

的形式存储,而在Camera2得到的Image中,由于YUV_420_888格式强制分离了UV分量,这时Plane的属性:

分量rowStridepixelStridedata
Y分量41YYYYYYYYYYYYYYYY
U分量42U_U_U_U_
V分量42V_V_V_V_

关于Camera2得到的YUV_420_888格式更多内容,可以参考这篇文章:
Android: Image类浅析(结合YUV_420_888)

转换

在使用TensorFlow时,一般要求输入的图片格式为RGB,下边说一下如何将上述获得的数据转换为RGB格式!
此转换来自于TensorFlow Lite的object_detection例子
首先对于YUV格式的数据,在得到每一个分量上的byte数组后,转换有一个公式:

private static int YUV2RGB(int y, int u, int v) {
    // This value is 2 ^ 18 - 1, and is used to clamp the RGB values before their ranges
    // are normalized to eight bits.
    static final int kMaxChannelValue = 262143;


    // Adjust and check YUV values
    y = (y - 16) < 0 ? 0 : (y - 16);
    u -= 128;
    v -= 128;

    // This is the floating point equivalent. We do the conversion in integer
    // because some Android devices do not have floating point in hardware.
    // nR = (int)(1.164 * nY + 2.018 * nU);
    // nG = (int)(1.164 * nY - 0.813 * nV - 0.391 * nU);
    // nB = (int)(1.164 * nY + 1.596 * nV);
    int y1192 = 1192 * y;
    int r = (y1192 + 1634 * v);
    int g = (y1192 - 833 * v - 400 * u);
    int b = (y1192 + 2066 * u);

    // Clipping RGB values to be inside boundaries [ 0 , kMaxChannelValue ]
    r = r > kMaxChannelValue ? kMaxChannelValue : (r < 0 ? 0 : r);
    g = g > kMaxChannelValue ? kMaxChannelValue : (g < 0 ? 0 : g);
    b = b > kMaxChannelValue ? kMaxChannelValue : (b < 0 ? 0 : b);

    return 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
  }

然后,对于NV21格式的Camera回调:

    //input即为摄像头回调byte[]数组,output为int[] 转换后的rgb输出
    //width 为图片宽度    height为图片高度
	final int frameSize = width * height;
    for (int j = 0, yp = 0; j < height; j++) {
      int uvp = frameSize + (j >> 1) * width;
      int u = 0;
      int v = 0;

      for (int i = 0; i < width; i++, yp++) {
        int y = 0xff & input[yp];
        if ((i & 1) == 0) {
          v = 0xff & input[uvp++];
          u = 0xff & input[uvp++];
        }

        output[yp] = YUV2RGB(y, u, v);
      }
    }

对于Camera2Image
首先得到yuv各个分量的byte[]:

protected void fillBytes(final Plane[] planes, final byte[][] yuvBytes) {
    // Because of the variable row stride it's not possible to know in
    // advance the actual necessary dimensions of the yuv planes.
    for (int i = 0; i < planes.length; ++i) {
      final ByteBuffer buffer = planes[i].getBuffer();
      if (yuvBytes[i] == null) {
        LOGGER.d("Initializing buffer %d at size %d", i, buffer.capacity());
        yuvBytes[i] = new byte[buffer.capacity()];
      }
      buffer.get(yuvBytes[i]);
    }
}

private byte[][] yuvBytes = new byte[3][];

final Plane[] planes = image.getPlanes();
fillBytes(planes, yuvBytes);

然后得到stride:

yRowStride = planes[0].getRowStride();
final int uvRowStride = planes[1].getRowStride();
final int uvPixelStride = planes[1].getPixelStride();

最后转换:

    /**
     * width 为图片宽度
     * height 为图片高度
     * yRowStride 为y分量的行跨度
     * uvRowStride 为uv分量的行跨度
     * uvPixelStride 为uv分量的像素跨度
     * yData,uData,vData 分别对应yuvBytes[0],yuvBytes[1],yuvBytes[2]
     * out[] 为输出的rgb int[]数组
     */
    int yp = 0;
    for (int j = 0; j < height; j++) {
      int pY = yRowStride * j;
      int pUV = uvRowStride * (j >> 1);

      for (int i = 0; i < width; i++) {
        int uv_offset = pUV + (i >> 1) * uvPixelStride;

        out[yp++] = YUV2RGB(0xff & yData[pY + i], 0xff & uData[uv_offset], 0xff & vData[uv_offset]);
      }
    }

结语

好了,以上就是关于移动端获取摄像头回调数据的相关内容,在下一篇我们再去了解关于和OpenCV中mat的转换和相关操作,以及竖屏时,图片逆时针旋转90度的问题!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值