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 格式?的主要内容,如果未能解决你的问题,请参考以下文章