本文共 6128 字,大约阅读时间需要 20 分钟。
为了统一和兼容各个平台图像数据格式的差异,和提供更丰富的相机参数设置,Android5.0之后推出了camera2 API,一般的我们会使用相机有几种需求
这些需求在官方给的一系列demo中都有示例,我也对Camera2Basic写过一篇笔记android-camera2basic源码逻辑流程解析 ,今天还是一个笔记,记录下第四个需求的问题。 首先,YUV420是一系列格式,从这个名字只能确定Y:U:V是4:1:1,具体的有YUV420P、YUV420SP、NV21、YV12等等。
新API提供了ImageReader这样一个类,来帮助开发者获取每一帧的原始数据,同时提供了实例化方法
ImageReader reader = ImageReader.newInstance(int width, int height, int format, int maxImages); 1 这里第三个参数是指定从ImageReader中获取的原始数据的格式。提个醒,不要太相信这个参数,这里指示不具体是一,就算指定了,摄像头不一定支持这个参数,可能会给出相似的格式的数据也说不准。
先说一个Android里面很有趣的玩笑。跳到newInstance()源码里面可以看到这句
if (format == ImageFormat.NV21) { throw new IllegalArgumentException( "NV21 format is not supported"); } 1 2 3 4 Android提供另一个YUV数据的帮助类YuvImage,构造函数
public YuvImage(byte[] yuv, int format, int width, int height, int[] strides) { if (format != ImageFormat.NV21 && format != ImageFormat.YUY2) { throw new IllegalArgumentException( "only support ImageFormat.NV21 " + "and ImageFormat.YUY2 for now"); } //ellipsis code mData = yuv; mFormat = format; mWidth = width; mHeight = height; }
mageReader不支持NV21 YuvIamge只支持NV21和YUY2 [捂脸]What?这是什么意思?有了解这块为什么这样的同学,请不吝赐教。
说回正点上。
在ImageReader实例化时传入ImageFormat.YUV_420_888,得到的数据到底是什么样的?网上有很多经典的讲YUV数据的各种格式,简单来讲就是三个通道数据比例是4:1:1的,有效数据量大小为width*height×2/3,但是从Android中拿到的Plane中的byte怎么提取,却很少有人提及。 也就是说,一般我们调用其他的api,比如人脸识别、物体识别等API的时候,要求传入的yuvData都是byte[]类型的。我们需要从Image->Plane->Buffer->byte[],这样拿到最终的byte,这个过程看着简单,网上的其他解析文章也都是,简单的直接从三个Plane的Buffer中直接执行如下代码,然后将三个byte直接拼接就行了。[捂脸]
byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes); 1 2 但是我从Iamge的Plane中拿数据的时候确遇到了很多问题。直接按正确的取数据的过程说吧。 1. Image中拿到width,height,和Planes[] 2. 每一个plane中拿到pixelsStride和rowStride 3. pixelsStride:像素步长,有可能是1、有可能是2,如果是1也就是说U、V的数据是紧密排列的,如果是2,就是每隔一位是有效的,可以理解在U和V的buffer中数据是类似u0u0u0u0u0u0u0u…和v0v0v0v0v0v0v0v0…0表示那一位byte无效。理解是可以这么理解,但实际上数据的排列是uvuvuvuvuvuvu…vuvuvuvuvuvuv…,也就是说API从sensor那边取过来的时候的数据就是uv交错的,因为这个有效位置的作用,U的数据把第一位去掉,最后补上一位之后就成了V的数据。所以像YUV420sp格式本身就要求图像是UV交错,可以直接取U的数据,补上最后一位,就OK了。但官方没有指出可以这么取,所以正确性并不能保证。实际中我试着这么取过,只有图像右下角最后一个像素是有问题的。如果不介意这个,可以尝试直接取U的数据。这一点在另外一片文章里面看到的,说的比较清楚。链接在这Image类浅析(结合YUV_420_888) 这里其实也好理解,sensor在处理的时候是逐行扫描的,YUV都是连续生成的,为了节省存储,可能将uv的数据交错合并输出给Android的framework层。 4. rowStride:“每行数据”的“宽度”,注意这里也有个坑,这个rowStride不一定是和width一样,有的相机输出的比图片本身的width要大,需要“逐行截取”。 5. 还有一种情况,就是上面那个链接中提到的CropRect的问题,应该会是显示部分正确图像,鉴于我没遇见过,就不瞎猜了。
结合上面的理解,我自己画了一些图。以6*4的图片为例,bytebuffer的排列可以理解如下: 在这里width=6,height=4,rowStride=6或者8,等于8时,最后两列会由于某些原因空一些byte,如果你转成rgb图像预览发现有规律的绿色栅格,那么考虑rowStride>width这种情况。 当然这张图只是说可以这么理解,实际上拿到的一维的byte数组,是每行数据接出来的如下:
且有:
yBytes.length==w*h; uBytes.length==w*h/4; vBytes.length==w*h/4; plane[0]==rowStride*h; if(pixelsStride==2) rowStride==w/2+temp; plane[1].length==plane[2].length==rowStride*h/2-1 else if(pixelsStride=1) rowStride==w/2+temp; plane[1].length==plane[2].length==rowStride*h/2 1 2 3 4 5 6 7 8 9 10 最后附上我写的工具类:(这里为了节省内存,Y的数据直接copy到了最终的bytes里面,也可以)
/** * yuv420p: yyyyyyyyuuvv * yuv420sp: yyyyyyyyuvuv * nv21: yyyyyyyyvuvu */
public class ImageUtil { public static final int YUV420P = 0; public static final int YUV420SP = 1; public static final int NV21 = 2; private static final String TAG = "ImageUtil";
/*** * 此方法内注释以640*480为例 * 未考虑CropRect的 */ public static byte[] getBytesFromImageAsType(Image image, int type) { try { //获取源数据,如果是YUV格式的数据planes.length = 3 //plane[i]里面的实际数据可能存在byte[].length <= capacity (缓冲区总大小) final Image.Plane[] planes = image.getPlanes();
//数据有效宽度,一般的,图片width <= rowStride,这也是导致byte[].length <= capacity的原因 // 所以我们只取width部分 int width = image.getWidth(); int height = image.getHeight();
//此处用来装填最终的YUV数据,需要1.5倍的图片大小,因为Y U V 比例为 4:1:1 byte[] yuvBytes = new byte[width * height * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8]; //目标数组的装填到的位置 int dstIndex = 0;
//临时存储uv数据的 byte uBytes[] = new byte[width * height / 4]; byte vBytes[] = new byte[width * height / 4]; int uIndex = 0; int vIndex = 0;
int pixelsStride, rowStride; for (int i = 0; i < planes.length; i++) { pixelsStride = planes[i].getPixelStride(); rowStride = planes[i].getRowStride();
ByteBuffer buffer = planes[i].getBuffer();
//如果pixelsStride==2,一般的Y的buffer长度=640*480,UV的长度=640*480/2-1 //源数据的索引,y的数据是byte中连续的,u的数据是v向左移以为生成的,两者都是偶数位为有效数据 byte[] bytes = new byte[buffer.capacity()]; buffer.get(bytes);
int srcIndex = 0; if (i == 0) { //直接取出来所有Y的有效区域,也可以存储成一个临时的bytes,到下一步再copy for (int j = 0; j < height; j++) { System.arraycopy(bytes, srcIndex, yuvBytes, dstIndex, width); srcIndex += rowStride; dstIndex += width; } } else if (i == 1) { //根据pixelsStride取相应的数据 for (int j = 0; j < height / 2; j++) { for (int k = 0; k < width / 2; k++) { uBytes[uIndex++] = bytes[srcIndex]; srcIndex += pixelsStride; } if (pixelsStride == 2) { srcIndex += rowStride - width; } else if (pixelsStride == 1) { srcIndex += rowStride - width / 2; } } } else if (i == 2) { //根据pixelsStride取相应的数据 for (int j = 0; j < height / 2; j++) { for (int k = 0; k < width / 2; k++) { vBytes[vIndex++] = bytes[srcIndex]; srcIndex += pixelsStride; } if (pixelsStride == 2) { srcIndex += rowStride - width; } else if (pixelsStride == 1) { srcIndex += rowStride - width / 2; } } } }
image.close();
//根据要求的结果类型进行填充 switch (type) { case YUV420P: System.arraycopy(uBytes, 0, yuvBytes, dstIndex, uBytes.length); System.arraycopy(vBytes, 0, yuvBytes, dstIndex + uBytes.length, vBytes.length); break; case YUV420SP: for (int i = 0; i < vBytes.length; i++) { yuvBytes[dstIndex++] = uBytes[i]; yuvBytes[dstIndex++] = vBytes[i]; } break; case NV21: for (int i = 0; i < vBytes.length; i++) { yuvBytes[dstIndex++] = vBytes[i]; yuvBytes[dstIndex++] = uBytes[i]; } break; } return yuvBytes; } catch (final Exception e) { if (image != null) { image.close(); } Log.i(TAG, e.toString()); } return null; } } 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 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 结论:基于一些特殊的sensor,Android API给出的YUV数据,需要根据rowStride,pixelsStride重新筛选拼接,才能得到正确的数据。
参考: Image类浅析(结合YUV_420_888)
转载地址:http://ykdab.baihongyu.com/