Android视频渲染: YUV转RGB

举报
ShaderJoy 发表于 2021/12/30 00:46:48 2021/12/30
【摘要】 原创文章,转载请注明: 转载自ian的个人博客 [ http://www.icodelogic.com ] 本文链接地址: http://www.icodelogic.com/?p=6 Android SDK为Camera预览提供了一个Demo,这个Demo的大致流程是初始化一个Camera和一个SurfaceView,...
原创文章,转载请注明: 转载自ian的个人博客 [ http://www.icodelogic.com ]

本文链接地址: http://www.icodelogic.com/?p=6

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


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

在这个回调里我们就能够获取到当前帧的数据,我们可以对其进行预处理,比如压缩、加密、特效处理等,不过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. jintArray Java_com_spore_jni_ImageUtilEngine_decodeYUV420SP(JNIEnv * env,
  2. jobject thiz, jbyteArray buf, jint width, jint height)
  3. {
  4. jbyte * yuv420sp = (*env)->GetByteArrayElements(env, buf, 0);
  5. int frameSize = width * height;
  6. jint rgb[frameSize]; // 新图像像素值
  7. int i = 0, j = 0,yp = 0;
  8. int uvp = 0, u = 0, v = 0;
  9. for (j = 0, yp = 0; j < height; j++)
  10. {
  11. uvp = frameSize + (j >> 1) * width;
  12. u = 0;
  13. v = 0;
  14. for (i = 0; i < width; i++, yp++)
  15. {
  16. int y = (0xff & ((int) yuv420sp[yp])) - 16;
  17. if (y < 0)
  18. y = 0;
  19. if ((i & 1) == 0)
  20. {
  21. v = (0xff & yuv420sp[uvp++]) - 128;
  22. u = (0xff & yuv420sp[uvp++]) - 128;
  23. }
  24. int y1192 = 1192 * y;
  25. int r = (y1192 + 1634 * v);
  26. int g = (y1192 - 833 * v - 400 * u);
  27. int b = (y1192 + 2066 * u);
  28. if (r < 0) r = 0; else if (r > 262143) r = 262143;
  29. if (g < 0) g = 0; else if (g > 262143) g = 262143;
  30. if (b < 0) b = 0; else if (b > 262143) b = 262143;
  31. rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
  32. }
  33. }
  34. jintArray result = (*env)->NewIntArray(env, frameSize);
  35. (*env)->SetIntArrayRegion(env, result, 0, frameSize, rgb);
  36. (*env)->ReleaseByteArrayElements(env, buf, yuv420sp, 0);
  37. return result;
  38. }

JNI代码对应的Java接口如下:
public native int[] decodeYUV420SP(byte[] buf, int width, int height);

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

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

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

下面是效果结果:

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

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

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

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

其他YUV2RGB的代码:

方法1


  
  1. // YUV2RGB方法1
  2. public void YUV2RGB(int data[])
  3. {
  4. synchronized (data)
  5. {
  6. int frameSize = getFrameWidth() * getFrameHeight();
  7. int[] rgba = new int[frameSize];
  8. for (int i = 0; i < getFrameHeight(); i++)
  9. {
  10. for (int j = 0; j < getFrameWidth(); j++)
  11. {
  12. int y = (0xff & data[i * getFrameWidth() + j]);
  13. int u = (0xff & data[frameSize + (i >> 1) * getFrameWidth()
  14. + (j & ~1) + 0]);
  15. int v = (0xff & data[frameSize + (i >> 1) * getFrameWidth()
  16. + (j & ~1) + 1]);
  17. y = y < 16 ? 16 : y;
  18. int r = Math.round(1.164f * (y - 16) + 1.596f * (v - 128));
  19. int g = Math.round(1.164f * (y - 16) - 0.813f * (v - 128)
  20. - 0.391f * (u - 128));
  21. int b = Math.round(1.164f * (y - 16) + 2.018f * (u - 128));
  22. r = r < 0 ? 0 : (r > 255 ? 255 : r);
  23. g = g < 0 ? 0 : (g > 255 ? 255 : g);
  24. b = b < 0 ? 0 : (b > 255 ? 255 : b);
  25. rgba[i * getFrameWidth() + j] = 0xff000000 + (b << 16)
  26. + (g << 8) + r;
  27. }
  28. }
  29. }
  30. }


  
  1. //YUV2RGB方法2
  2. public Bitmap renderCroppedGreyscaleBitmap(byte[] yuvData)
  3. {
  4. int width = 240;
  5. int height = 240;
  6. int[] pixels = new int[width * height];
  7. byte[] yuv = yuvData;
  8. int inputOffset = 0 * 320 + 40;
  9. for (int y = 0; y < height; y++)
  10. {
  11. int outputOffset = y * width;
  12. for (int x = 0; x < width; x++)
  13. {
  14. int grey = yuv[inputOffset + x] & 0xff;
  15. pixels[outputOffset + x] = 0xFF000000 | (grey * 0x00010101);
  16. }
  17. inputOffset += 320;
  18. }
  19. Bitmap bitmap = Bitmap.createBitmap(width, height,
  20. Bitmap.Config.ARGB_8888);
  21. bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
  22. return bitmap;
  23. }





文章来源: panda1234lee.blog.csdn.net,作者:panda1234lee,版权归原作者所有,如需转载,请联系作者。

原文链接:panda1234lee.blog.csdn.net/article/details/8538248

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。