YUV_420_888 интерпретация на Samsung Galaxy S7 (Camera2)

Я написал преобразование из YUV_420_888 в Bitmap, учитывая следующую логику (насколько я понимаю):

Чтобы подвести итог подхода: координаты ядра x и y совпадают как с x, так и с y не дополненной части Y-плоскости (2d-выделение) и с x и y выходного растрового изображения. Тем не менее, U- и V-плоскости имеют структуру, отличную от Y-плоскости, поскольку они используют 1 байт для покрытия 4 пикселей, и, кроме того, могут иметь PixelStride больше одного, кроме того, они могут также есть отступы, которые могут отличаться от Y-плоскости. Поэтому для эффективного доступа к U и V ядром я поместил их в 1-d распределения и создал индекс "uvIndex", который дает позицию соответствующих U- и V в этом 1-d распределении, для данного (x,y) координаты в (не дополненной) плоскости Y (и, следовательно, выходной битовый массив).

Чтобы поддерживать rs-Kernel, я исключил область заполнения в yPlane, ограничив x-диапазон с помощью LaunchOptions (это отражает RowStride y-плоскости, который, таким образом, можно игнорировать в ядре). Поэтому нам просто нужно рассмотреть uvPixelStride и uvRowStride внутри uvIndex, то есть индекс, используемый для доступа к значениям u и v.

Это мой код:

Renderscript Kernel с именем yuv420888.rs

  #pragma version(1)
  #pragma rs java_package_name(com.xxxyyy.testcamera2);
  #pragma rs_fp_relaxed

  int32_t width;
  int32_t height;

  uint picWidth, uvPixelStride, uvRowStride ;
  rs_allocation ypsIn,uIn,vIn;

 // The LaunchOptions ensure that the Kernel does not enter the padding  zone of Y, so yRowStride can be ignored WITHIN the Kernel.
 uchar4 __attribute__((kernel)) doConvert(uint32_t x, uint32_t y) {

 // index for accessing the uIn's and vIn's
uint uvIndex=  uvPixelStride * (x/2) + uvRowStride*(y/2);

// get the y,u,v values
uchar yps= rsGetElementAt_uchar(ypsIn, x, y);
uchar u= rsGetElementAt_uchar(uIn, uvIndex);
uchar v= rsGetElementAt_uchar(vIn, uvIndex);

// calc argb
int4 argb;
    argb.r = yps + v * 1436 / 1024 - 179;
    argb.g =  yps -u * 46549 / 131072 + 44 -v * 93604 / 131072 + 91;
    argb.b = yps +u * 1814 / 1024 - 227;
    argb.a = 255;

uchar4 out = convert_uchar4(clamp(argb, 0, 255));
return out;
}

Java сторона:

    private Bitmap YUV_420_888_toRGB(Image image, int width, int height){
    // Get the three image planes
    Image.Plane[] planes = image.getPlanes();
    ByteBuffer buffer = planes[0].getBuffer();
    byte[] y = new byte[buffer.remaining()];
    buffer.get(y);

    buffer = planes[1].getBuffer();
    byte[] u = new byte[buffer.remaining()];
    buffer.get(u);

    buffer = planes[2].getBuffer();
    byte[] v = new byte[buffer.remaining()];
    buffer.get(v);

    // get the relevant RowStrides and PixelStrides
    // (we know from documentation that PixelStride is 1 for y)
    int yRowStride= planes[0].getRowStride();
    int uvRowStride= planes[1].getRowStride();  // we know from   documentation that RowStride is the same for u and v.
    int uvPixelStride= planes[1].getPixelStride();  // we know from   documentation that PixelStride is the same for u and v.


    // rs creation just for demo. Create rs just once in onCreate and use it again.
    RenderScript rs = RenderScript.create(this);
    //RenderScript rs = MainActivity.rs;
    ScriptC_yuv420888 mYuv420=new ScriptC_yuv420888 (rs);

    // Y,U,V are defined as global allocations, the out-Allocation is the Bitmap.
    // Note also that uAlloc and vAlloc are 1-dimensional while yAlloc is 2-dimensional.
    Type.Builder typeUcharY = new Type.Builder(rs, Element.U8(rs));
    typeUcharY.setX(yRowStride).setY(height);
    Allocation yAlloc = Allocation.createTyped(rs, typeUcharY.create());
    yAlloc.copyFrom(y);
    mYuv420.set_ypsIn(yAlloc);

    Type.Builder typeUcharUV = new Type.Builder(rs, Element.U8(rs));
    // note that the size of the u's and v's are as follows:
    //      (  (width/2)*PixelStride + padding  ) * (height/2)
    // =    (RowStride                          ) * (height/2)
    // but I noted that on the S7 it is 1 less...
    typeUcharUV.setX(u.length);
    Allocation uAlloc = Allocation.createTyped(rs, typeUcharUV.create());
    uAlloc.copyFrom(u);
    mYuv420.set_uIn(uAlloc);

    Allocation vAlloc = Allocation.createTyped(rs, typeUcharUV.create());
    vAlloc.copyFrom(v);
    mYuv420.set_vIn(vAlloc);

    // handover parameters
    mYuv420.set_picWidth(width);
    mYuv420.set_uvRowStride (uvRowStride);
    mYuv420.set_uvPixelStride (uvPixelStride);

    Bitmap outBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Allocation outAlloc = Allocation.createFromBitmap(rs, outBitmap, Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_SCRIPT);

    Script.LaunchOptions lo = new Script.LaunchOptions();
    lo.setX(0, width);  // by this we ignore the y’s padding zone, i.e. the right side of x between width and yRowStride
    lo.setY(0, height);

    mYuv420.forEach_doConvert(outAlloc,lo);
    outAlloc.copyTo(outBitmap);

    return outBitmap;
}

Тестирование на Nexus 7 (API 22) возвращает хорошие цветные растровые изображения. Это устройство, однако, имеет тривиальные пиксельные строки (=1) и не имеет отступов (т.е. rowstride=width). Тестируя на новом Samsung S7 (API 23), я получаю картинки, цвета которых не правильные, кроме зеленых. Но картинка не показывает общего смещения в сторону зеленого, просто кажется, что не зеленые цвета воспроизводятся неправильно. Обратите внимание, что S7 применяет u/v пиксельный шаг 2, а не заполнение.

Поскольку наиболее важная строка кода находится внутри rs-кода, доступ к плоскостям u/v uint uvIndex= (...) Я думаю, здесь может быть проблема, возможно, с неправильным рассмотрением пиксельных последовательностей здесь. Кто-нибудь видит решение? Благодарю.

ОБНОВЛЕНИЕ: Я проверил все, и я почти уверен, что код доступа к y, u, v правильный. Так что проблема должна быть с самими значениями u и v. Небеленые цвета имеют фиолетовый оттенок, и, глядя на значения u, v, они кажутся в довольно узком диапазоне около 110-150. Действительно ли возможно, что нам нужно справиться с конкретными преобразованиями YUV -> RBG для конкретного устройства...?! Я что-то пропустил?

ОБНОВЛЕНИЕ 2: исправили код, теперь он работает, благодаря обратной связи Эдди.

5 ответов

Решение

Смотреть на

floor((float) uvPixelStride*(x)/2)

который вычисляет смещение строки U,V (uv_row_offset) из координаты Y

если uvPixelStride = 2, то при увеличении x:

x = 0, uv_row_offset = 0
x = 1, uv_row_offset = 1
x = 2, uv_row_offset = 2
x = 3, uv_row_offset = 3

и это неверно. При uv_row_offset = 1 или 3 нет действительного значения U/V-пикселя, поскольку uvPixelStride = 2.

Ты хочешь

uvPixelStride * floor(x/2)

(при условии, что вы не доверяете себе, чтобы вспомнить критическое поведение округления до целого, если вы это сделаете):

uvPixelStride * (x/2)

должно быть достаточно

С этим ваше отображение становится:

x = 0, uv_row_offset = 0
x = 1, uv_row_offset = 0
x = 2, uv_row_offset = 2
x = 3, uv_row_offset = 2

Посмотрите, исправит ли это цветовые ошибки. На практике неправильная адресация здесь будет означать, что каждый другой образец цвета будет из неправильной цветовой плоскости, поскольку вполне вероятно, что лежащие в основе YUV-данные являются полупланарными (поэтому плоскость U начинается в плоскости V + 1 байт с чередованием двух плоскостей)

Для людей, которые сталкиваются с ошибкой

android.support.v8.renderscript.RSIllegalArgumentException: Array too small for allocation type

использование buffer.capacity() вместо buffer.remaining()

и если вы уже сделали некоторые операции с изображением, вам нужно будет позвонить rewind() метод в буфере.

Кроме того, для всех остальных

android.support.v8.renderscript.RSIllegalArgumentException: массив слишком мал для типа размещения

Я исправил это, изменив yAlloc.copyFrom(y); в yAlloc.copy1DRangeFrom(0, y.length, y);

Публикация полного решения для преобразования YUV->BGR (может быть адаптировано и для других форматов), а также для поворота изображения в вертикальное положение с помощью скрипта визуализации. Выделение используется как ввод, а байтовый массив - как вывод. Он был протестирован на Android 8+, включая устройства Samsung.

Джава

/**
 * Renderscript-based process to convert YUV_420_888 to BGR_888 and rotation to upright.
 */
public class ImageProcessor {

    protected final String TAG = this.getClass().getSimpleName();

    private Allocation mInputAllocation;
    private Allocation mOutAllocLand;
    private Allocation mOutAllocPort;

    private Handler mProcessingHandler;
    private ScriptC_yuv_bgr mConvertScript;
    private byte[] frameBGR;

    public ProcessingTask mTask;
    private ImageListener listener;
    private Supplier<Integer> rotation;

    public ImageProcessor(RenderScript rs, Size dimensions, ImageListener listener, Supplier<Integer> rotation) {

        this.listener = listener;
        this.rotation = rotation;
        int w = dimensions.getWidth();
        int h = dimensions.getHeight();

        Type.Builder yuvTypeBuilder = new Type.Builder(rs, Element.YUV(rs));
        yuvTypeBuilder.setX(w);
        yuvTypeBuilder.setY(h);
        yuvTypeBuilder.setYuvFormat(ImageFormat.YUV_420_888);
        mInputAllocation = Allocation.createTyped(rs, yuvTypeBuilder.create(),
                Allocation.USAGE_IO_INPUT | Allocation.USAGE_SCRIPT);

        //keep 2 allocations to handle different image rotations
        mOutAllocLand = createOutBGRAlloc(rs, w, h);
        mOutAllocPort = createOutBGRAlloc(rs, h, w);

        frameBGR = new byte[w*h*3];

        HandlerThread processingThread = new HandlerThread(this.getClass().getSimpleName());
        processingThread.start();
        mProcessingHandler = new Handler(processingThread.getLooper());

        mConvertScript = new ScriptC_yuv_bgr(rs);
        mConvertScript.set_inWidth(w);
        mConvertScript.set_inHeight(h);

        mTask = new ProcessingTask(mInputAllocation);
    }

    private Allocation createOutBGRAlloc(RenderScript rs, int width, int height) {
        //Stored as Vec4, it's impossible to store as Vec3, buffer size will be for Vec4 anyway
        //using RGB_888 as alternative for BGR_888, can be just U8_3 type
        Type.Builder rgbTypeBuilderPort = new Type.Builder(rs, Element.RGB_888(rs));
        rgbTypeBuilderPort.setX(width);
        rgbTypeBuilderPort.setY(height);
        Allocation allocation = Allocation.createTyped(
            rs, rgbTypeBuilderPort.create(), Allocation.USAGE_SCRIPT
        );
        //Use auto-padding to be able to copy to x*h*3 bytes array
        allocation.setAutoPadding(true);

        return allocation;
    }

    public Surface getInputSurface() {
        return mInputAllocation.getSurface();
    }

    /**
     * Simple class to keep track of incoming frame count,
     * and to process the newest one in the processing thread
     */
    class ProcessingTask implements Runnable, Allocation.OnBufferAvailableListener {

        private int mPendingFrames = 0;

        private Allocation mInputAllocation;

        public ProcessingTask(Allocation input) {
            mInputAllocation = input;
            mInputAllocation.setOnBufferAvailableListener(this);
        }

        @Override
        public void onBufferAvailable(Allocation a) {
            synchronized(this) {
                mPendingFrames++;
                mProcessingHandler.post(this);
            }
        }

        @Override
        public void run() {
            // Find out how many frames have arrived
            int pendingFrames;
            synchronized(this) {
                pendingFrames = mPendingFrames;
                mPendingFrames = 0;

                // Discard extra messages in case processing is slower than frame rate
                mProcessingHandler.removeCallbacks(this);
            }

            // Get to newest input
            for (int i = 0; i < pendingFrames; i++) {
                mInputAllocation.ioReceive();
            }

            int rot = rotation.get();

            mConvertScript.set_currentYUVFrame(mInputAllocation);
            mConvertScript.set_rotation(rot);

            Allocation allocOut = rot==90 || rot== 270 ? mOutAllocPort : mOutAllocLand;

            // Run processing
            // ain allocation isn't really used, global frame param is used to get data from
            mConvertScript.forEach_yuv_bgr(allocOut);

            //Save to byte array, BGR 24bit
            allocOut.copyTo(frameBGR);

            int w = allocOut.getType().getX();
            int h = allocOut.getType().getY();

            if (listener != null) {
                listener.onImageAvailable(frameBGR, w, h);
            }
        }
    }

    public interface ImageListener {

        /**
         * Called when there is available image, image is in upright position.
         *
         * @param bgr BGR 24bit bytes
         * @param width image width
         * @param height image height
         */
        void onImageAvailable(byte[] bgr, int width, int height);
    }
}

RS

#pragma version(1)
#pragma rs java_package_name(com.affectiva.camera)
#pragma rs_fp_relaxed

//Script convers YUV to BGR(uchar3)

//current YUV frame to read pixels from
rs_allocation currentYUVFrame;

//input image rotation: 0,90,180,270 clockwise
uint32_t rotation;
uint32_t inWidth;
uint32_t inHeight;

//method returns uchar3  BGR which will be set to x,y in output allocation
uchar3 __attribute__((kernel)) yuv_bgr(uint32_t x, uint32_t y) {

    // Read in pixel values from latest frame - YUV color space

    uchar3 inPixel;
    uint32_t xRot = x;
    uint32_t yRot = y;

    //Do not rotate if 0
    if (rotation==90) {
      //rotate 270 clockwise
      xRot = y;
      yRot = inHeight - 1 - x;
    } else if (rotation==180) {
      xRot = inWidth - 1 - x;
      yRot = inHeight - 1 - y;
    } else if (rotation==270) {
      //rotate 90 clockwise
      xRot = inWidth - 1 - y;
      yRot = x;
    }

    inPixel.r = rsGetElementAtYuv_uchar_Y(currentYUVFrame, xRot, yRot);
    inPixel.g = rsGetElementAtYuv_uchar_U(currentYUVFrame, xRot, yRot);
    inPixel.b = rsGetElementAtYuv_uchar_V(currentYUVFrame, xRot, yRot);

    // Convert YUV to RGB, JFIF transform with fixed-point math
    // R = Y + 1.402 * (V - 128)
    // G = Y - 0.34414 * (U - 128) - 0.71414 * (V - 128)
    // B = Y + 1.772 * (U - 128)

    int3 bgr;
    //get red pixel and assing to b
    bgr.b = inPixel.r +
            inPixel.b * 1436 / 1024 - 179;
    bgr.g = inPixel.r -
            inPixel.g * 46549 / 131072 + 44 -
            inPixel.b * 93604 / 131072 + 91;
    //get blue pixel and assign to red
    bgr.r = inPixel.r +
            inPixel.g * 1814 / 1024 - 227;

    // Write out
    return convert_uchar3(clamp(bgr, 0, 255));
}

Этот код требует использования библиотеки совместимости RenderScript (android.support.v8.renderscript.*).

Чтобы заставить библиотеку совместимости работать с Android API 23, я обновился до gradle-plugin 2.1.0 и Build-Tools 23.0.3 в соответствии с ответом Мяо Вана на Как создать сценарии Renderscript в Android Studio и заставить их работать?

Если вы следите за его ответом и получаете сообщение об ошибке "Требуется Gradle версии 2.10", НЕ меняйте

classpath 'com.android.tools.build:gradle:2.1.0'

Вместо этого обновите поле distributionUrl файла Project\gradle\wrapper\gradle-wrapper.properties до

distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip

и измените File > Settings > Builds,Execution,Deployment > Build Tools > Gradle >Gradle, чтобы использовать упаковщик по умолчанию для Gradle согласно "Требуется Gradle версии 2.10". Ошибка

На Samsung Galaxy Tab 5 (Tablet), Android версии 5.1.1 (22), с предполагаемым форматом YUV_420_888, следующая математика визуализации хорошо работает и дает правильные цвета:

uchar yValue    = rsGetElementAt_uchar(gCurrentFrame, x + y * yRowStride);
uchar vValue    = rsGetElementAt_uchar(gCurrentFrame, ( (x/2) + (y/4) * yRowStride ) + (xSize * ySize) );
uchar uValue    = rsGetElementAt_uchar(gCurrentFrame, ( (x/2) + (y/4) * yRowStride ) + (xSize * ySize) + (xSize * ySize) / 4);

Я не понимаю, почему горизонтальное значение (т. Е. У) масштабируется в четыре раза вместо двух, но это работает хорошо. Мне также нужно было избегать использования rsGetElementAtYuv_uchar_Y|U|V. Я считаю, что соответствующее значение шага распределения установлено равным нулю, а не чем-то правильным. Использование rsGetElementAt_uchar() - разумный обходной путь.

На Samsung Galaxy S5 (смартфон) Android версии 5.0 (21) с предполагаемым форматом YUV_420_888 я не могу восстановить значения u и v, они отображаются как все нули. Это приводит к зеленому изображению. Светится нормально, но изображение перевернуто вертикально.

Re: RSIllegalArgumentException

В моем случае это было так, что buffer.remaining() не был кратным шагу: длина последней строки была меньше шага (т.е. только до того места, где были фактические данные).

FYI на случай, если кто-то еще получит это, поскольку я также получал "android.support.v8.renderscript.RSIllegalArgumentException: массив слишком мал для типа распределения" при тестировании кода. В моем случае оказалось, что при выделении буфера для Y мне пришлось перемотать буфер, потому что он оставался не на том конце и не копировал данные. Выполняя buffer.rewind(); перед распределением новый массив байтов заставляет его работать нормально.

Другие вопросы по тегам