使用新的 Android camera2 api 从 YUV_420_888 进行 JPEG 编码时的绿色图像

Posted

技术标签:

【中文标题】使用新的 Android camera2 api 从 YUV_420_888 进行 JPEG 编码时的绿色图像【英文标题】:Green images when doing a JPEG encoding from YUV_420_888 using the new Android camera2 api 【发布时间】:2015-06-21 15:10:36 【问题描述】:

我正在尝试使用新的相机 API。突发捕获太慢了,所以我在 ImageReader 中使用 YUV_420_888 格式并稍后进行 JPEG 编码,如下面的帖子所述:

android camera2 capture burst is too slow

问题是当我尝试使用 RenderScript 从 YUV_420_888 编码 JPEG 时,我得到绿色图像,如下所示:

RenderScript rs = RenderScript.create(mContext);
ScriptIntrinsicYuvToRGB yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.RGBA_8888(rs));
Type.Builder yuvType = new Type.Builder(rs, Element.YUV(rs)).setX(width).setY(height).setYuvFormat(ImageFormat.YUV_420_888);
Allocation in = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT);

Type.Builder rgbaType = new Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height);
Allocation out = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT);

in.copyFrom(data);

yuvToRgbIntrinsic.setInput(in);
yuvToRgbIntrinsic.forEach(out);

Bitmap bmpout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
out.copyTo(bmpout);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
bmpout.compress(Bitmap.CompressFormat.JPEG, 100, baos);
byte[] jpegBytes = baos.toByteArray();

数据变量(YUV_420_888数据)来自:

ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);

我在 JPEG 编码中做错了什么以使图像仅显示为绿色?

提前致谢

已编辑:这是我获得的绿色图像示例:

https://drive.google.com/file/d/0B1yCC7QDeEjdaXF2dVp6NWV6eWs/view?usp=sharing

【问题讨论】:

FWIW,YUV 值为 0,0,0 是中绿色。因此,如果您的图像完全是绿色的,我猜您正在转换一个充满零的缓冲区,而不是一个充满 YUV 像素数据的缓冲区。 我已经用我获得的图像示例编辑了这个问题。它们并不完全是绿色的,它似乎是绿色的。我认为这是因为我只从 YUV 格式的三个平面中的第一个平面获取数据。我已经搜索了一种从三个平面获取信息并将其传递给 RenderScript 的方法,但我无法使我找到的小代码工作。 嗨,你解决了这个问题吗? 我试过你的代码,保存的 png 图像是绿色的。似乎 ScriptIntrinsicYuvToRGB 无法将 YUV_420_888 转换为位图。您是否找到其他方法来实现它? 【参考方案1】:

所以这个问题有好几层的答案。

首先,我认为没有直接的方法可以将 YUV_420_888 数据的图像复制到 RS 分配中,即使分配的格式为 YUV_420_888。

因此,如果您没有将图像用于此 JPEG 编码步骤之外的任何其他操作,那么您可以直接使用分配作为相机的输出,方法是使用 Allocation#getSurface 和 Allocation#ioReceive。然后你可以执行你的 YUV->RGB 转换并读出位图。

但是,请注意,JPEG 文件实际上存储的是 YUV 数据,因此当您压缩 JPEG 时,Bitmap 将在保存文件时进行另一次 RGB->YUV 转换。因此,为了获得最大效率,您希望将 YUV 数据直接提供给可以接受它的 JPEG 编码器,并完全避免额外的转换步骤。不幸的是,这无法通过公共 API 实现,因此您必须使用 JNI 代码并包含您自己的 libjpeg 副本或等效的 JPEG 编码库。

如果您不需要非常快速地保存 JPEG 文件,您可以将 YUV_420_888 数据转换为 NV21 字节[],然后使用 YuvImage,但您需要注意 YUV_420_888 数据的像素和行步长以及将其正确映射到 NV21 - YUV_420_888 很灵活,可以表示几种不同类型的内存布局(包括 NV21),并且在不同的设备上可能会有所不同。因此,在将布局修改为 NV21 时,确保正确进行映射至关重要。

【讨论】:

更糟糕的是,虽然 YUV_420_888 被声明为 RS 的格式之一,但截至今天,Renderscript does not support this format。 这在 Java 级别是不正确的,至少在快速测试中是这样。使用 YUV_420_888 的 google 演示 github.com/googlesamples/android-HdrViewfinder:github.com/googlesamples/android-HdrViewfinder/blob/master/… 在运行 Android 8.1 的 Google Pixel 2 上对我来说很好。 看起来对于USAGE_IO_INPUT 这可以工作。此外,如果在后台忽略这种格式,我不会感到惊讶,而 HAL 提供了实际的 NV21 或 YV12,请参见例如rsallocation.c【参考方案2】:

你必须使用 RenderScript 吗? 如果没有,您可以将图像从 YUV 转换为 N21,然后从 N21 转换为 JPEG,而无需任何花哨的结构。 首先你坐0号和2号飞机去N21:

private byte[] convertYUV420ToN21(Image imgYUV420) 
    byte[] rez = new byte[0];

    ByteBuffer buffer0 = imgYUV420.getPlanes()[0].getBuffer();
    ByteBuffer buffer2 = imgYUV420.getPlanes()[2].getBuffer();
    int buffer0_size = buffer0.remaining();
    int buffer2_size = buffer2.remaining();
    rez = new byte[buffer0_size + buffer2_size];

    buffer0.get(rez, 0, buffer0_size);
    buffer2.get(rez, buffer0_size, buffer2_size);

    return rez;

然后你可以使用YuvImage的内置方法来压缩成JPEG。 wh 参数是图像文件的宽度和高度。

private byte[] convertN21ToJpeg(byte[] bytesN21, int w, int h) 
    byte[] rez = new byte[0];

    YuvImage yuv_image = new YuvImage(bytesN21, ImageFormat.NV21, w, h, null);
    Rect rect = new Rect(0, 0, w, h);
    ByteArrayOutputStream output_stream = new ByteArrayOutputStream();
    yuv_image.compressToJpeg(rect, 100, output_stream);
    rez = output_stream.toByteArray();

    return rez;

【讨论】:

我不需要使用 RenderScript。我试过你的代码,但图像不正确。你可以在这里看到结果:drive.google.com/file/d/0B1yCC7QDeEjdcjlCRGRPaVR1RVk/… 非常感谢! 图像的一部分现在看起来没问题,你不觉得吗?在我看来,您似乎需要检查如何将参数传递给这些方法(特别是宽度和高度)。对它们进行试验,看看结果是否不同。 YUV_420_888 缓冲区与 NV21 不同,因此您不能将 3 个平面连接在一起并将其视为 NV21。您需要查看 UV 缓冲区的行和像素步长,并可能逐个像素地读取; NV21 是半平面的(交错的 V/U 平面),而 YUV_420_888 可以是半平面的或完全平面的。您还需要注意行步长 - YuvImage 的输入假定没有行步长,而 Image 的 Y、U 和 V 平面可能有行步长 > 宽度。【参考方案3】:

我设法让它工作,panonski 提供的答案不太正确,一个大问题是这种YUV_420_888 格式涵盖了许多不同的内存布局,其中NV21 格式非常具体(我不知道)不知道为什么默认格式会这样改,对我来说毫无意义)

请注意,由于几个原因,此方法可能会非常慢。

    因为NV21 交织色度通道,而YUV_420_888 包括具有非隔行色度通道的格式,唯一可靠的选择(据我所知)是逐字节复制。我很想知道是否有加快这个过程的技巧,我怀疑有一个。我提供了一个仅灰度选项,因为该部分是非常快速的逐行复制。

    当从相机抓取帧时,它们的字节将被标记为受保护,这意味着无法直接访问,必须复制它们才能直接操作。

    图像似乎是以反向字节顺序存储的,因此转换后最终的数组需要反转。这可能只是我的相机,我怀疑这里还有另一个技巧可以大大加快速度。

这里是代码:

private byte[] getRawCopy(ByteBuffer in) 
    ByteBuffer rawCopy = ByteBuffer.allocate(in.capacity());
    rawCopy.put(in);
    return rawCopy.array();


private void fastReverse(byte[] array, int offset, int length) 
    int end = offset + length;
    for (int i = offset; i < offset + (length / 2); i++) 
        array[i] = (byte)(array[i] ^ array[end - i  - 1]);
        array[end - i  - 1] = (byte)(array[i] ^ array[end - i  - 1]);
        array[i] = (byte)(array[i] ^ array[end - i  - 1]);
    


private ByteBuffer convertYUV420ToN21(Image imgYUV420, boolean grayscale) 

    Image.Plane yPlane = imgYUV420.getPlanes()[0];
    byte[] yData = getRawCopy(yPlane.getBuffer());

    Image.Plane uPlane = imgYUV420.getPlanes()[1];
    byte[] uData = getRawCopy(uPlane.getBuffer());

    Image.Plane vPlane = imgYUV420.getPlanes()[2];
    byte[] vData = getRawCopy(vPlane.getBuffer());

    // NV21 stores a full frame luma (y) and half frame chroma (u,v), so total size is
    // size(y) + size(y) / 2 + size(y) / 2 = size(y) + size(y) / 2 * 2 = size(y) + size(y) = 2 * size(y)
    int npix = imgYUV420.getWidth() * imgYUV420.getHeight();
    byte[] nv21Image = new byte[npix * 2];
    Arrays.fill(nv21Image, (byte)127); // 127 -> 0 chroma (luma will be overwritten in either case)

    // Copy the Y-plane
    ByteBuffer nv21Buffer = ByteBuffer.wrap(nv21Image);
    for(int i = 0; i < imgYUV420.getHeight(); i++) 
        nv21Buffer.put(yData, i * yPlane.getRowStride(), imgYUV420.getWidth());
    

    // Copy the u and v planes interlaced
    if(!grayscale) 
        for (int row = 0; row < imgYUV420.getHeight() / 2; row++) 
            for (int cnt = 0, upix = 0, vpix = 0; cnt < imgYUV420.getWidth() / 2; upix += uPlane.getPixelStride(), vpix += vPlane.getPixelStride(), cnt++) 
                nv21Buffer.put(uData[row * uPlane.getRowStride() + upix]);
                nv21Buffer.put(vData[row * vPlane.getRowStride() + vpix]);
            
        

        fastReverse(nv21Image, npix, npix);
    

    fastReverse(nv21Image, 0, npix);

    return nv21Buffer;

【讨论】:

除非您对自己的答案不满意,否则请接受。关于像素的倒序,您的相机可能只是具有非标准方向,例如Nexus 5X。是的,对于这种倒置摄像头,您转换为 N21 的性能可能会有所改善。 @AlexCohn 我很乐意接受我的回答,但由于我没有问这个问题,所以我不能 对不起,我的错误。当同时打开太多 Chrome 标签页时会发生这种情况。【参考方案4】:

如果我正确理解了您的描述,我可以在您的代码中看到至少两个问题:

    您似乎只是将图像的 Y 部分传递给 YUV->RGB 转换代码,因为看起来您只使用ByteBuffer buffer = mImage.getPlanes()[0].getBuffer(); 中的第一个平面,而忽略了 U 和 V 平面。

    我还不熟悉这些 Renderscript 类型,但看起来 Element.RGBA_8888 和 Bitmap.Config.ARGB_8888 指的是略有不同的字节顺序,因此您可能需要进行一些重新排序工作。

这两个问题都可能是结果图片呈绿色的原因。

【讨论】:

我认为这是两件事的原因。我尝试重新排序 alpha 字节,但我只得到红色图像,或者粉红色或黄色而不是绿色图像。我怀疑另一个原因是我只使用了第一架飞机,但我不知道如何将飞机的所有信息放在一个格式良好的字节数组中(如果我必须在上一个,或按什么顺序等)【参考方案5】:

这是一个答案/问题。 在几个类似的帖子中,建议使用此脚本: https://github.com/pinguo-yuyidong/Camera2/blob/master/camera2/src/main/rs/yuv2rgb.rs

但我不知道如何使用它。欢迎提供建议。

【讨论】:

【参考方案6】:

上述转换是否有效?因为我通过复制第一个和最后一个平面来尝试使用渲染脚本,但我仍然收到了一个绿色的过滤图像,就像上面的那个。

【讨论】:

不,我已经在评论中添加了指向我获得的图像的链接。它不完全是绿色,但颜色不是很好。

以上是关于使用新的 Android camera2 api 从 YUV_420_888 进行 JPEG 编码时的绿色图像的主要内容,如果未能解决你的问题,请参考以下文章

支持 Android Camera Api 和 Camera2 Api 的问题

关于使用Android新版Camera即Camera2的使用介绍 暨解决Camera.PreviewCallback和MediaRecorder无法同时进行

Android Camera2前置摄像头

Android Camera2拍照——使用SurfaceView

将 Android camera2 api YUV_420_888 转换为 RGB

使用 Camera2(Android 版本 21)API 录制 60fps 视频