Android使用渲染脚本将ImageReader图像转换为YCbCr_420_SP(NV21)字节数组?

17

我目前正在使用Javacv,它使用public void onPreviewFrame(byte[] data, Camera camera)相机函数。

由于相机已被弃用,我一直在研究camera2MediaProjection。这两个库都使用了ImageReader类

目前,我使用以下代码实例化ImageReader

ImageReader.newInstance(DISPLAY_WIDTH, DISPLAY_HEIGHT, PixelFormat.RGBA_8888, 2);

并像这样附加一个OnImageAvailableListener

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

        @Override
        public void onImageAvailable(ImageReader reader) {

            mBackgroundHandler.post(new processImage(reader.acquireNextImage()));
        }

我尝试使用Javacv中的RGBA_8888格式,如此线程所述:https://github.com/bytedeco/javacv/issues/298,但对我无效。 因此,我考虑使用RenderScript将这些Image转换为默认输出格式为onPreviewFrame函数中的NV21(YUV_420_SP)格式(摄像机的默认输出格式),因为我在camera库中使用了这种方法。
我还阅读了一些帖子,例如这个这个网站来进行转换,但这些对我无效,而且我担心它们太慢。 此外,我的C语言知识非常有限。 基本上看起来我需要反向操作https://developer.android.com/reference/android/renderscript/ScriptIntrinsicYuvToRGB.html 那么,如何将Image转换为与onPreviewFrame函数输出匹配的字节数组,即NV21(YUV_420_SP)格式?最好使用Renderscript,因为它更快。

编辑 1:

我尝试使用ImageFormat.YUV_420_888,但无济于事。我一直收到像“生成器输出缓冲区格式0x1与ImageReader配置的缓冲区格式不匹配”这样的错误。我切换回PixelFormat.RGBA_8888并发现ImageObject中只有一个平面。该平面的字节缓冲区大小为width*height*4(分别为R、G、B、A一字节)。因此,我尝试将其转换为NV21格式。
我修改了这个答案的代码,以产生以下函数:
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++];

            // 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++;
        }
    }
}

并使用以下方式调用它:

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

byte[] rgbaBytes = new byte[mWidth * mHeight * 4];
mImage.getPlanes()[0].getBuffer().get(rgbaBytes);
mImage.close();

byte[] yuv = new byte[mWidth * mHeight * 3 / 2];
RGBtoNV21(yuv, rgbaBytes, mWidth, mHeight);

这里的mImage是由我的ImageReader创建的Image对象。

然而,这会产生类似于以下图像的结果:

错误格式图像

显然格式错误。看起来我的转换有问题,但我无法确定具体问题出在哪里。


为了更好地理解问题,您是在尝试从ImageReader处理一些RGBA图像,然后将其转换回YUV吗? - Miao Wang
@MiaoWang 是的,我试图将ImageReader中的图像转换回使用旧相机API获得的格式。我希望使用RenderScript来完成这个任务,因为这可能会对CPU造成较大压力(每秒转换30张图片)。 - Gooey
虽然你的问题是合法的,但我认为这不是将camera2接口连接到javacv的最佳方法。我认为首选的方法是请求ImageFormat.YUV_420_888,并对处理代码进行最小的调整以采用此格式,该格式仅通过不同的排序与NV21不同。如果你真的需要,你可以有一个简单的循环来重新排列ImageFormat.YUV_420_888和NV21之间的字节。 - Alex Cohn
我强烈建议在仅返回LEGACY或LIMITED到INFO_SUPPORTED_HARDWARE_LEVEL查询的设备上不要使用camera2。使用新的API没有任何优势,只会遇到OEM厂商兼容层中的错误实现。特别是如果您已经有可以与旧相机API一起使用的代码。 - Alex Cohn
fadden在那里谈到了屏幕截图,这与相机捕获非常不同。请注意,内部相机产生NV21,因此通过强制使用camera2 API,您会造成每帧两个不太便宜的转换。作为一种练习,将RGBA_8888转换为NV21是合法的。但作为已部署应用程序的一部分则不适合。 - Alex Cohn
显示剩余10条评论
1个回答

6
@TargetApi(19)
public static byte[] yuvImageToByteArray(Image image) {

    assert(image.getFormat() == ImageFormat.YUV_420_888);

    int width = image.getWidth();
    int height = image.getHeight();

    Image.Plane[] planes = image.getPlanes();
    byte[] result = new byte[width * height * 3 / 2];

    int stride = planes[0].getRowStride();
    assert (1 == planes[0].getPixelStride());
    if (stride == width) {
        planes[0].getBuffer().get(result, 0, width*height);
    }
    else {
        for (int row = 0; row < height; row++) {
            planes[0].getBuffer().position(row*stride);
            planes[0].getBuffer().get(result, row*width, width);
        }
    }

    stride = planes[1].getRowStride();
    assert (stride == planes[2].getRowStride());
    int pixelStride = planes[1].getPixelStride();
    assert (pixelStride == planes[2].getPixelStride());
    byte[] rowBytesCb = new byte[stride];
    byte[] rowBytesCr = new byte[stride];

    for (int row = 0; row < height/2; row++) {
        int rowOffset = width*height + width/2 * row;
        planes[1].getBuffer().position(row*stride);
        planes[1].getBuffer().get(rowBytesCb);
        planes[2].getBuffer().position(row*stride);
        planes[2].getBuffer().get(rowBytesCr);

        for (int col = 0; col < width/2; col++) {
            result[rowOffset + col*2] = rowBytesCr[col*pixelStride];
            result[rowOffset + col*2 + 1] = rowBytesCb[col*pixelStride];
        }
    }
    return result;
}

我已经发布了另一个函数,它有类似的要求。这个新实现试图利用一个事实,那就是很多时候,YUV_420_888只是假扮成NV21。


我无法使用任何一个__批量获取方法__。尝试给出错误java.lang.Exception: Failed resolving method get on class java.nio.DirectByteBuffer。这是通过camera2ImageReader捕获的Image返回的getBuffer - Mudlabs
@Mudlabs:抱歉,这段代码有错误。现在已经修复了。感谢您的反馈! - Alex Cohn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接