Android преобразует ImageReader Image в массив байтов YCbCr_420_SP (NV21), используя render script?

В настоящее время я работаю с Javacv, который использует функцию камеры public void onPreviewFrame(byte[] data, Camera camera).

Поскольку камера устарела, я искал camera2 и MediaProjection. Обе эти библиотеки используют класс 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()));
        }

Я пробовал использовать формат RGBA_8888 с Javacv в соответствии с этим потоком: https://github.com/bytedeco/javacv/issues/298, но это не работает для меня.

Поэтому вместо этого я думал об использовании Render script для преобразования этих Image до NV21(YUV_420_SP) (который является выходным сигналом камеры по умолчанию в функции onPreviewFrame), так как это работало для меня с библиотекой camera.

Я также читал сообщения, такие как этот и этот веб-сайт сделайте преобразование, но это не сработало для меня, и я боюсь, что они будут слишком медленными. Кроме того, мои знания о C строго ограничены. В принципе, похоже, что мне нужна обратная операция https://developer.android.com/reference/android/renderscript/ScriptIntrinsicYuvToRGB.html

Итак, как вы можете перейти от массива Image к байту, который будет соответствовать выходу функции onPreviewFrame i.e. NV21(YUV_420_SP)? Предпочтительно использовать Renderscript как можно быстрее.


Изменить 1:

Я пробовал использовать ImageFormat.YUV_420_888, но безрезультатно. Я продолжал получать ошибки, такие как The producer output buffer format 0x1 doesn't match the ImageReader configured buffer format. Я переключился на PixelFormat.RGBA_8888 и обнаружил, что в ImageObject есть только одна плоскость. Байт-буфер этой плоскости имеет размер ширины * высота * 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 - объект Image, созданный моим ImageReader.

Но это приводит к результату, подобному этому изображению:

malformed image

который явно искажен. Кажется, что мое преобразование выключено, но я не могу понять, что именно.

Ответы

Ответ 1

@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.