我们在开发安卓相机相关应用的时候,最基本的操作是在屏幕上实时预览相机捕获的图像。但更多时候需要对捕获到的图像或者视频进行处理,例如人脸检测,图像增强等等。在处理图像时,不可避免地会遇到图像格式的问题,常见的格式有:RGB,YUV和灰度图等。
安卓开发中,我们处理的实时图像数据来自相机的回调函数 onPreviewFrame(final byte[] bytes, final Camera camera)。其中 bytes
存的就是当前帧的数据,但是不能直接对其进行处理,需要先进行一些格式转换。 bytes
的默认格式是 YUV(NV21) ,接下来就从该格式入手,介绍安卓相机相关的图像格式问题。
1 YUV格式
YUV,分为三个分量,Y
表示明亮度(Luminance或Luma),也就是灰度值;而U
和V
表示的则是色度(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采样方式得到数据的存储格式分为YUV420P
和YUV420SP
两种,其含义分别是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