Android ImageReader 获取 NV21 格式?

Posted

技术标签:

【中文标题】Android ImageReader 获取 NV21 格式?【英文标题】:Android ImageReader get NV21 format? 【发布时间】:2016-06-13 01:32:17 【问题描述】:

我没有图像或图形方面的背景,所以请多多包涵:)

我在我的一个项目中使用JavaCV。在examples 中,构造了一个Frame,它有一个certain size 的缓冲区。

android中使用public void onPreviewFrame(byte[] data, Camera camera)函数时,如果将Frame声明为new Frame(frameWidth, frameHeight, Frame.DEPTH_UBYTE, 2);,其中frameWidthframeHeight声明为

,则复制此data字节数组是没有问题的
Camera.Size previewSize = cameraParam.getPreviewSize();
int frameWidth = previewSize.width;
int frameHeight = previewSize.height;

最近,Android 添加了一种捕获屏幕的方法。自然,我想抓取这些图像并将它们转换为Frames。我修改了the example code from Google 以使用ImageReader。

这个ImageReader 构造为ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, PixelFormat.RGBA_8888, 2);。所以目前它使用 RGBA_8888 像素格式。我使用以下代码将字节复制到Frame,实例化为new Frame(DISPLAY_WIDTH, DISPLAY_HEIGHT, Frame.DEPTH_UBYTE, 2);

ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
mImage.close();
((ByteBuffer) frame.image[0].position(0)).put(bytes);

但这给了我一个java.nio.BufferOverflowException。我打印了两个缓冲区的大小,Frame 的缓冲区大小为 691200,而上面的 bytes 数组的大小为 1413056。由于I ran into this native call,弄清楚后一个数字是如何构造的失败了。很明显,这是行不通的。

经过相当多的挖掘,我发现NV21 image format 是“相机预览图像的默认格式,当没有使用 setPreviewFormat(int) 设置时”,而是ImageReader class does not support the NV21 format(请参阅格式参数)。所以运气不好。在文档中它还写道 "对于 android.hardware.camera2 API,YUV 输出推荐使用 YUV_420_888 格式。"

所以我尝试创建一个像 ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, ImageFormat.YUV_420_888, 2); 这样的 ImageReader,但这给了我 java.lang.UnsupportedOperationException: The producer output buffer format 0x1 doesn't match the ImageReader's configured buffer format 0x23.,所以这也行不通。

作为最后的手段,我尝试使用例如将 RGBA_8888 转换为 YUV。 this post,但我不明白如何根据答案获得int[] rgba

那么,TL;DR 我怎样才能像你在 Android 的 public void onPreviewFrame(byte[] data, Camera camera) 相机功能中获得 NV21 图像数据来实例化我的 Frame 并使用 Android 的 ImageReader(和媒体投影)使用它?

编辑(2016 年 10 月 25 日)

我创建了以下可运行的从 RGBA 到 NV21 格式的转换:

private class updateImage implements Runnable 

    private final Image mImage;

    public updateImage(Image image) 
        mImage = image;
    

    @Override
    public void run() 

        int mWidth = mImage.getWidth();
        int mHeight = mImage.getHeight();

        // Four bytes per pixel: width * height * 4.
        byte[] rgbaBytes = new byte[mWidth * mHeight * 4];
        // put the data into the rgbaBytes array.
        mImage.getPlanes()[0].getBuffer().get(rgbaBytes);

        mImage.close(); // Access to the image is no longer needed, release it.

        // Create a yuv byte array: width * height * 1.5 ().
        byte[] yuv = new byte[mWidth * mHeight * 3 / 2];
        RGBtoNV21(yuv, rgbaBytes, mWidth, mHeight);
        ((ByteBuffer) yuvImage.image[0].position(0)).put(yuv);
    

    void RGBtoNV21(byte[] yuv420sp, byte[] argb, int width, int height) 
        final int frameSize = width * height;

        int yIndex = 0;
        int uvIndex = frameSize;

        int A, R, G, B, Y, U, V;
        int index = 0;
        int rgbIndex = 0;

        for (int i = 0; i < height; i++) 
            for (int j = 0; j < width; j++) 

                R = argb[rgbIndex++];
                G = argb[rgbIndex++];
                B = argb[rgbIndex++];
                A = argb[rgbIndex++]; // Ignored right now.

                // RGB to YUV conversion according to
                // https://en.wikipedia.org/wiki/YUV#Y.E2.80.B2UV444_to_RGB888_conversion
                Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
                U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
                V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

                // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor
                // of 2 meaning for every 4 Y pixels there are 1 V and 1 U.
                // Note the sampling is every other pixel AND every other scanline.
                yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
                if (i % 2 == 0 && index % 2 == 0) 
                    yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
                    yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
                
                index++;
            
        
    

yuvImage 对象初始化为yuvImage = new Frame(DISPLAY_WIDTH, DISPLAY_HEIGHT, Frame.DEPTH_UBYTE, 2);DISPLAY_WIDTHDISPLAY_HEIGHT 只是指定显示大小的两个整数。 这是后台处理程序处理 onImageReady 的代码:

private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() 

        @Override
        public void onImageAvailable(ImageReader reader) 
            mBackgroundHandler.post(new updateImage(reader.acquireNextImage()));
        

    ;

...

mImageReader = ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, PixelFormat.RGBA_8888, 2);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);

这些方法有效,我至少没有收到任何错误,但输出图像格式错误。我的转换出了什么问题?正在创建的示例图像:

编辑 (15-11-2016)

我已将RGBtoNV21 函数修改为如下:

void RGBtoNV21(byte[] yuv420sp, int width, int height) 
    try 
        final int frameSize = width * height;

        int yIndex = 0;
        int uvIndex = frameSize;
        int pixelStride = mImage.getPlanes()[0].getPixelStride();
        int rowStride = mImage.getPlanes()[0].getRowStride();
        int rowPadding = rowStride - pixelStride * width;
        ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();

        Bitmap bitmap = Bitmap.createBitmap(getResources().getDisplayMetrics(), width, height, Bitmap.Config.ARGB_8888);

        int A, R, G, B, Y, U, V;
        int offset = 0;

        for (int i = 0; i < height; i++) 
            for (int j = 0; j < width; j++) 

                // Useful link: https://***.com/questions/26673127/android-imagereader-acquirelatestimage-returns-invalid-jpg

                R = (buffer.get(offset) & 0xff) << 16;     // R
                G = (buffer.get(offset + 1) & 0xff) << 8;  // G
                B = (buffer.get(offset + 2) & 0xff);       // B
                A = (buffer.get(offset + 3) & 0xff) << 24; // A
                offset += pixelStride;

                int pixel = 0;
                pixel |= R;     // R
                pixel |= G;  // G
                pixel |= B;       // B
                pixel |= A; // A
                bitmap.setPixel(j, i, pixel);

                // RGB to YUV conversion according to
                // https://en.wikipedia.org/wiki/YUV#Y.E2.80.B2UV444_to_RGB888_conversion
//                        Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
//                        U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
//                        V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;

                Y = (int) Math.round(R *  .299000 + G *  .587000 + B *  .114000);
                U = (int) Math.round(R * -.168736 + G * -.331264 + B *  .500000 + 128);
                V = (int) Math.round(R *  .500000 + G * -.418688 + B * -.081312 + 128);

                // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor
                // of 2 meaning for every 4 Y pixels there are 1 V and 1 U.
                // Note the sampling is every other pixel AND every other scanline.
                yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
                if (i % 2 == 0 && j % 2 == 0) 
                    yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
                    yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
                
            
            offset += rowPadding;
        

        File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath(), "/Awesomebitmap.png");
        FileOutputStream fos = new FileOutputStream(file);
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos);
     catch (Exception e) 
        Timber.e(e, "Converting image to NV21 went wrong.");
    

现在图像不再畸形,但色度已关闭。

右侧是在该循环中创建的位图,左侧是保存到图像的 NV21。所以RGB像素被正确处理。显然色度已关闭,但 RGB 到 YUV 的转换应该与***描述的相同。这里有什么问题?

【问题讨论】:

您在使用 Camera 1 API 吗? 不,我正在使用 ImageReader 并希望获得类似于 Camera 1 API (NV21) 的输出。 我希望您再次尝试使用ImageFormat.YUV_420_888,但使用支持的相机预览尺寸(不要使用显示尺寸)。 Get list of supported sizes,然后选择一个。 该格式在我的设备上崩溃,因为我运行的是不支持该格式的 Android 5。 (问题中也有注明) 根据您在接受的答案下的 cmets,您似乎设法解决了色度关闭问题,您可以发布解决方案吗?这种有问题的转换让我头疼。 【参考方案1】:

一般来说,ImageReader 的目的是让您以最小的开销对发送到 Surface 的像素进行原始访问,因此尝试让它执行颜色转换是没有意义的。

对于相机,您可以选择两种输出格式(NV21 或 YV12)之一,所以选择 YV12。那是你的原始 YUV 数据。对于屏幕捕获,输出将始终为 RGB,因此您需要为 ImageReader 选择 RGBA_8888 (format 0x1),而不是 YUV_420_888 (format 0x23)。如果为此需要 YUV,则必须自己进行转换。 ImageReader 为您提供了一系列 Plane 对象,而不是 byte[],因此您需要适应它。

【讨论】:

感谢您的回答,您对如何自己进行转换有任何指导吗? 您可以在网上找到一些代码,尤其是在 *** 上的问题答案中。我认为它使用的是BT.601,但我不记得了。 (见en.wikipedia.org/wiki/YUV#Converting_between_Y.27UV_and_RGB) 嗨@fadden 很抱歉用一个半年前的问题打扰你,但我继续致力于我的项目(学到了很多关于图像和格式的知识)并设法取得了一些进展。它现在可以工作,但是图像格式不正确,因为您对这个主题有很多了解,您介意看看我提供的代码吗?非常感谢您的帮助。 像这样滑落的像素通常意味着您的宽度或步幅值不正确。检查平面中的行和像素步幅(developer.android.com/reference/android/media/Image.Plane.html);我的猜测是行步幅大于宽度。你有正确的图像副本吗?它有助于查看正确与错误。 如果我正确理解了您的答案,我们无法真正直接从相机获取 NV21,而是必须通过转换 NV21(来自相机设备)-> RGBA(相机在发送像素之前为我们完成该操作)到表面)-> NV21(我们自己转换回NV21)?还是我没有抓住重点?

以上是关于Android ImageReader 获取 NV21 格式?的主要内容,如果未能解决你的问题,请参考以下文章

如何区分 imageReader 相机 API 2 中的 NV21 和 YV12 编码?

Android Camera2 ImageReader 图像格式 YUV

Android ImageReader YUV 420 888 重复数据

使用Android NDK Camera2获取预览帧的正确方法是啥

Android Camera2 ImageReader 大小在 Android 5.0 Galaxy S5 上不正确

Android camera2 输出到 ImageReader 格式 YUV_420_888 仍然很慢