您的位置:首页 > 移动开发 > Android开发

Android视频渲染: YUV转RGB

2017-12-07 20:36 501 查看
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
56
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
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
56
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
43
44
45
46
47
48
49
50
5152
53
54
55
56
57
58
59
60
6162
63
64
65
66
67
68
69
70
7172
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始终很奇怪,大家可以去研究一下,如果调通了请告知一下多谢。

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
56
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
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
56
7
8
9
10
1112
13
14
15
16
17
18
19
20
2122
23
24
25
26
27
28
29
30
3132
33
34
35
36
37
38
39
40
4142
43
44
45
46
47
48
49
50
5152
53
54
55
56
57
58
59
60
6162
63
64
65
66
67
68
69
70
7172
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始终很奇怪,大家可以去研究一下,如果调通了请告知一下多谢。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: