已阅读:43,902 次
Android视频渲染: YUV转RGB
ian | Android,其他 | 2012/04/25


Android SDK为Camera预览提供了一个Demo,这个Demo的大致流程是初始化一个Camera和一个SurfaceView,SurfaceView被创建之后可以获取到一个SurfaceHolder的实例,将这个SurfaceHolder传递给Camera,这样Camera就会自动的将捕获到的视频数据渲染到SurfaceView上面,这也就是Camera预览的效果。当然更多的时候我们需要获取到Camera的实时视频数据来自己进行预处理并渲染,Camera也提供了这个接口,用法如下:

1
2
3
4
5
mCamera.setPreviewCallback(new PreviewCallback(){
        @Override
        public void onPreviewFrame(byte[] data, Camera camera) 
       {
       });

在这个回调里我们就能够获取到当前帧的数据,我们可以对其进行预处理,比如压缩、加密、特效处理等,不过byte[]这个buffer里面的数据是YUV格式的,一般是YUV420SP,而Android提供的SurfaceView、GLSurfaceView、TextureView等控件只支持RGB格式的渲染,因此我们需要一个算法来解码。

先介绍一个YUV转RGB的算法,转换的公式一般如下,也是线性的关系:
R=Y+1.4075*(V-128)
G=Y-0.3455*(U-128) – 0.7169*(V-128)
B=Y+1.779*(U-128)

下面是一段将YUV转成ARGB_8888的jni代码,类似的代码网上很多,将这个代码简单修改一下也能直接用在C中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
jintArray Java_com_spore_jni_ImageUtilEngine_decodeYUV420SP(JNIEnv * env,
		jobject thiz, jbyteArray buf, jint width, jint height) 
{
	jbyte * yuv420sp = (*env)->GetByteArrayElements(env, buf, 0);
 
	int frameSize = width * height;
	jint rgb[frameSize]; // 新图像像素值
 
	int i = 0, j = 0,yp = 0;
	int uvp = 0, u = 0, v = 0;
	for (j = 0, yp = 0; j < height; j++)
	{
		uvp = frameSize + (j >> 1) * width;
		u = 0;
		v = 0;
		for (i = 0; i < width; i++, yp++)
		{
			int y = (0xff & ((int) yuv420sp[yp])) - 16;
			if (y < 0)
				y = 0;
			if ((i & 1) == 0)
			{
				v = (0xff & yuv420sp[uvp++]) - 128;
				u = (0xff & yuv420sp[uvp++]) - 128;
			}
 
			int y1192 = 1192 * y;
			int r = (y1192 + 1634 * v);
			int g = (y1192 - 833 * v - 400 * u);
			int b = (y1192 + 2066 * u);
 
			if (r < 0) r = 0; else if (r > 262143) r = 262143;
			if (g < 0) g = 0; else if (g > 262143) g = 262143;
			if (b < 0) b = 0; else if (b > 262143) b = 262143;
 
			rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
		}
	}
 
	jintArray result = (*env)->NewIntArray(env, frameSize);
	(*env)->SetIntArrayRegion(env, result, 0, frameSize, rgb);
	(*env)->ReleaseByteArrayElements(env, buf, yuv420sp, 0);
	return result;
}

JNI代码对应的Java接口如下:

1
public native int[] decodeYUV420SP(byte[] buf, int width, int height);

从这个接口就很容易理解了,参数buf就是从Camera的onPreviewFrame回调用获取到的YUV格式的视频帧数据,width和height分别是对应的Bitmap的宽高。返回的结果是一个ARGB_8888格式的颜色数组,将这个数组组装成Bitmap也是十分容易的,代码如下:

1
mBitmap = Bitmap.createBitmap(data, width, height, Config.ARGB_8888);

基本上这样就能实现YUV2RGB了,但是这样的实现有一个问题:由于是软解码,所以性能并不理想。如果考虑到一般的视频通话的场景,例如320*240左右的分辨率的话,那基本能满足实时性的需求,但是对于720P的高清视频则基本无望。当然,对于上面的实现,我们也可以尽我们所能的做一些优化。

上面的算法实现中,已经没有浮点运算了,并且大多数操作已经使用了移位运算,剩下的可优化部分只有中间的乘法了,我们可以使用查表法来替代。上面的代码我们简单分析就可以发现,Y、U、V的取值都只有256种情况,而对应的r、g、b跟YUV是线性的关系,其中r跟Y和V相关,g跟Y、V、U相关,b跟Y和U相关,于是我们可以预先计算出所有可能的情况,比如所有的1634 * v的值保存在一个长度为256的数组中,这样我们只需要根据v值查找相乘的结果即可,可以节省这次的乘法运算。

考虑到RGB和YUV的相关性,我们可以把R和B的所有可能值预先计算并缓存,其长度均是256 * 256的int数组,也就是256KB,为什么不针对G值建表呢?因为G值跟YUV三个分量都有关,需要建256 * 256 *256长的表才行,也就是64M,这在手机设备上是不可行的。

下面是查表优化的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
int g_v_table[256],g_u_table[256],y_table[256];
int r_yv_table[256][256],b_yu_table[256][256];
int inited = 0;
 
void initTable()
{
	if (inited == 0)
	{
		inited = 1;
		int m = 0,n=0;
		for (; m < 256; m++)
		{
			g_v_table[m] = 833 * (m - 128);
			g_u_table[m] = 400 * (m - 128);
			y_table[m] = 1192 * (m - 16);
		}
		int temp = 0;
		for (m = 0; m < 256; m++)
			for (n = 0; n < 256; n++)
			{
				temp = 1192 * (m - 16) + 1634 * (n - 128);
				if (temp < 0) temp = 0; else if (temp > 262143) temp = 262143;
				r_yv_table[m][n] = temp;
 
				temp = 1192 * (m - 16) + 2066 * (n - 128);
				if (temp < 0) temp = 0; else if (temp > 262143) temp = 262143;
				b_yu_table[m][n] = temp;
			}
	}
}
 
jintArray Java_com_spore_jni_ImageUtilEngine_decodeYUV420SP(JNIEnv * env,
		jobject thiz, jbyteArray buf, jint width, jint height) 
{
	jbyte * yuv420sp = (*env)->GetByteArrayElements(env, buf, 0);
 
	int frameSize = width * height;
	jint rgb[frameSize]; // 新图像像素值
 
	initTable();
 
	int i = 0, j = 0,yp = 0;
	int uvp = 0, u = 0, v = 0;
	for (j = 0, yp = 0; j < height; j++)
	{
		uvp = frameSize + (j >> 1) * width;
		u = 0;
		v = 0;
		for (i = 0; i < width; i++, yp++)
		{
			int y = (0xff & ((int) yuv420sp[yp]));
			if (y < 0)
				y = 0;
			if ((i & 1) == 0)
			{
				v = (0xff & yuv420sp[uvp++]);
				u = (0xff & yuv420sp[uvp++]);
			}
 
			int y1192 = y_table[y];
			int r = r_yv_table[y][v];
			int g = (y1192 - g_v_table[v] - g_u_table[u]);
			int b = b_yu_table[y][u];
 
			if (g < 0) g = 0; else if (g > 262143) g = 262143;
 
			rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
		}
	}
 
	jintArray result = (*env)->NewIntArray(env, frameSize);
	(*env)->SetIntArrayRegion(env, result, 0, frameSize, rgb);
	(*env)->ReleaseByteArrayElements(env, buf, yuv420sp, 0);
	return result;
}

当然,还有其他的一些细节可以优化一下,比如转化结果的数组,可以预先在Java层分配,将数组的指针传递给JNI,这样可以省去数组在Java和C之间的传递时间,因为720P的图片是很大的,所以这个成本值得去优化。

下面是效果结果:

左边是一个SurfaceView用于Camera的预览,右侧是GLSurfaceView,将转码后的Bitmap渲染出来,由于截屏软件的问题,左侧Camera预览区域变成黑的了。

这样转码的效率如何呢?根据我在Nexus One上的测试结果,720P的图像,也就是1280 * 720的分辨率,转码并渲染的速度大概是8帧。

另外介绍一个看起来速度应该更快的查表转码的算法:传送门。不过这里没有对参数进行说明,所以我调了好久发现转码之后的Bitmap始终很奇怪,大家可以去研究一下,如果调通了请告知一下多谢。

完整的代码下载,请点击此处

原创文章,转载请注明:转载自ian的个人博客[http://www.icodelogic.com]
本文链接地址: http://www.icodelogic.com/?p=605

tags:

29条评论

  1. leafisme 说:

    哥们,能给个能跑的源码吗?请发我邮箱,谢谢!

  2. 蒲路萍 说:

    我也在写两个Android设备之间的视频传输,需要转YUV420Sp为RGB,
    哥们给我也发一份源码设。感激不尽!!!

    • ian 说:

      源码发给你了,但是据我验证,这样的转码效率是很低的,如果追求比较高的分辨率的实时视频传输,这样的方法无法满足需求,可能需要硬件解码才行。

  3. 白白 说:

    你好,麻烦你给我也发个源码,我现在也在看这方面。谢啦。发我邮箱。

  4. piner 说:

    你好,麻烦你给我也发个源码,我最近也在研究这方面。谢些

  5. 白白 说:

    请问,为什么我用你这个YUV转ARGB的算法来显示640*480的视频,出来的图像的颜色跑偏了?比如绿色的树叶的绿色就跑到数外边去了?是不是这个转化只适合720p以上的视频呢?谢谢!

  6. 白白 说:

    是YUV420SP格式的视频,但是显示后没有颜色,或是颜色跑到一边了。我用的是
    gl.glTexImage2D(GL10.GL_TEXTURE_2D, 0, GL10.GL_RGB, w,h, 0, GL10.GL_RGB, GL10.GL_UNSIGNED_BYTE, ByteBuffer.wrap(pixelComponents));其中pixelComponents存放的是ARGB数据。谢谢你的回复。

  7. 白白 说:

    呵呵呵,错了,我将ARGB中的A去掉了,

  8. 白白 说:

    我不管是用ARGB还是RGB,不管是用BitMap还是用opengl,显示都是颜色不对。呜呜,我实在不知道问题再哪?

  9. 白白 说:

    问题我找到了,谢谢你的帮助!

  10. neil 说:

    你好,请问
    int y1192 = 1192 * y;
    int r = (y1192 + 1634 * v);
    int g = (y1192 – 833 * v – 400 * u);
    int b = (y1192 + 2066 * u);
    1192 、 1634 、 833 、 400 、 2066 、 262143都代表什么意思?

    • ian 说:

      这几条语句其实就是YUV转RGB的算法,算法一般如下:
      Y = 0.299R + 0.587G + 0.114B
      Cb = 0.564(B - Y )
      Cr = 0.713(R - Y )
      R = Y + 1.402Cr
      G = Y – 0.344Cb – 0.714Cr
      B = Y + 1.772Cb
      至于此算法详细的理论介绍,可以请教Google大神

      • neil 说:

        这些我都查过了 比如y1192 = y*1192; 1192是怎么设定的没有查到。
        还有 “这样转码的效率如何呢?根据我在Nexus One上的测试结果,720P的图像,也就是1280 * 720的分辨率,转码并渲染的速度大概是8帧。”看你这个博文里说的,Preview能拿的数据最大不是屏幕的宽×高嘛 如何能取得720p 1280×720的图像呢? 求请教 呵呵

        • ian 说:

          Camera可以设置PreviewView的一些参数,其中就包括分辨率,你可以看一下Camera.Parameters相关的接口

          • neil 说:

            你是指setPreviewSize嘛? 我用的I9100 设置最大是800×480 如果设置1280×720后,会变为640×480

          • ian 说:

            先使用getSupportedPictureSizes看看Camera能否支持720P的分辨率,如果设置的分辨率Camera本身不支持,那么一般设备中Camera预览是无法出现正常图像的

        • shaoying 说:

          解出来的视频不连续,有点卡的感觉,352*288的,还有更好的方法吗

  11. andliu 说:

    ian,刚跑了下你的代码,我的屏幕上左边Camera的preview可以正常显示,但是右边是一片黑,什么原因呢,

    • ian 说:

      这份代码被我改了很多个版本,后来没有管过了。刚才看了一下,右边确实不能显示,因为setPreviewCallback没有设置
      我已经将代码修改好重新上传了,你可以再下载看一下。

  12. chengcl 说:

    我用你的那个不转化为bitmap进行渲染,发现Frame Rate 能达到9,但是显示不正常,马赛克
    这是什么原因??

  13. hu 说:

    int r_yv_table[256][256],b_yu_table[256][256];
    可以直接优化成
    unsigned char r_yv_table[256][256];
    unsigned char b_yu_table[256][256];
    减少空间占用

  14. chenfuwei 说:

    直接显示回调来的YUV数据,怎么是黑白色的呢?颜色空间怎么加上去啊?求教

  15. chenfuwei 说:

    GLSurface可以直接显示YUV数据的吧,直接将回调的YUV数据去显示,好像只显示Y信号,所以是黑白的,U跟V信号怎样加上去?

发表评论

你需要先 登录 才能回复