安卓相机YUV格式解析

Posted by lsq on February 14, 2019

我们在开发安卓相机相关应用的时候,最基本的操作是在屏幕上实时预览相机捕获的图像。但更多时候需要对捕获到的图像或者视频进行处理,例如人脸检测,图像增强等等。在处理图像时,不可避免地会遇到图像格式的问题,常见的格式有:RGB,YUV和灰度图等。

安卓开发中,我们处理的实时图像数据来自相机的回调函数 onPreviewFrame(final byte[] bytes, final Camera camera)。其中 bytes 存的就是当前帧的数据,但是不能直接对其进行处理,需要先进行一些格式转换。 bytes 的默认格式是 YUV(NV21) ,接下来就从该格式入手,介绍安卓相机相关的图像格式问题。

1 YUV格式

YUV,分为三个分量,Y表示明亮度(Luminance或Luma),也就是灰度值;而UV 表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。它将亮度信息(Y)与色彩信息(UV)分离,没有UV信息一样可以显示完整的图像,只不过是黑白的,这样的设计很好地解决了彩色电视机与黑白电视的兼容问题。并且,YUV不像RGB那样要求三个独立的视频信号同时传输,所以用YUV方式传送占用极少的频宽。

YUV的存储格式其实与其采样的方式密切相关,主流的采样方式有三种,YUV4:4:4,YUV4:2:2,YUV4:2:0。下面用三个图来直观地表示采样方式,以黑点表示采样该像素点的Y分量,以空心圆圈表示采用该像素点的UV分量。YUV 4:4:4采样:每一个Y对应一组UV分量;YUV 4:2:2采样:每两个Y共用一组UV分量;YUV 4:2:0采样:每四个Y共用一组UV分量。

YUV420采样方式得到数据的存储格式分为YUV420PYUV420SP两种,其含义分别是planar和packed。对于YUV420P(YU12和YV12),先连续存储所有像素点的Y,紧接着存储所有像素点的U和所有像素点的V。对于YUV420SP(NV21和NV12),先连续存储所有像素点的Y,接着连续交叉存储每个像素点的U,V。举一个包含8个像素点的例子:

YU12: YYYYYYYY UU VV =>YUV420P
YV12: YYYYYYYY VV UU =>YUV420P
NV12: YYYYYYYY UVUV =>YUV420SP
NV21: YYYYYYYY VUVU =>YUV420SP

从这个例子中,我们还可以得到的结论是,一个 WxH 大小的图像,其对应的YUV420存储格式所需的空间大小为:WxHx1.5,这个结论能在我们处理相机缓存数据时提供帮助。在上面四种存储格式中,NV21是安卓相机的默认格式,因此以下内容均针对NV21.

2 YUV420(NV21)分量提取

在上一节提到YUV420格式图像数据是由Y,U,V三部分组成的,一般我们不会对YUV数据直接处理。大多数情况下,会先将YUV数据转换成RGB或者灰度图,而在转换之前需要先对原YUV数据进行解析。YUV数据来自相机回调函数 onPreviewFrame(final byte[] bytes, final Camera camera), 其中参数 bytes 就是我们的原始 YUV 数据。安卓相机默认的 YUV 数据存储格式为YUV420(NV21),各个通道的排列方式如下图。

YUV420(NV21)和图像像素位置的对应方式如上图,其在视频流数据中的排列方式是行优先的,下图是一个NV12(UV交替,U在前)的例子,NV21是VU交替。到了这里,YUV420(NV21)数据格式已经解释的很清楚了,下面给出了分解YUV三通道的程序。如果出于性能考虑不能转换RGB或者其他格式,可以考虑直接使用分解出来的Y分量,即原图的黑白图像。

private static int[] Y = null;
private static int[] U = null;
private static int[] V = null;

public static int[] devideYUV420_NV21(byte [] data, int width, int height) {
    int size = width*height;
    Y = new int[size];
    //The UV plane works on 2x2 blocks, so dimensions with odd size must be rounded up.
    U = new int[(width + 1) * (height + 1) / 4];
    V = new int[(width + 1) * (height + 1) / 4];
    int u, v, y;
    
    for(int i=0; i < size; i++) {
        y = data[i] & 0xFF;
        Y[i] = 0xff000000 | y<<16 | y<<8 | y;
    }

    //U在V前面且互相交替
    for(int j = 0 , k = 0; i < (width + 1) * (height + 1) / 2; j+=2, k++) {   
        v = data[size + j]&0xff;
        u = data[size + j + 1]&0xff;
        V[k] = v-128;
        U[k] = u-128;
    }
}

3 YUV420转换RGB

YUV420转RGB的方案貌似并不统一,下面的实现来自于 tensorflow/examples/android/src/org/tensorflow/demo/env/ImageUtils.java。


public static void convertYUV420SPToARGB8888(
      byte[] input,
      int width,
      int height,
      int[] output) {
    // Java implementation of YUV420SP to ARGB8888 converting
    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);
      }
    }
  }

  private static int YUV2RGB(int y, int u, int v) {
    // 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);
  }

Reference

图文详解YUV420数据格式
图解YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P、YUV444P的区别
Java: When to Use (n » 8) & 0xff and When to Use (byte)(n »> 8)
YUV pixel formats
YUV格式剖析以及与RGB的转换实现 – 视频和图像编程基础之二
从Android相机的NV21格式提取黑白图像
Create Native Method on Android App