Camera2 захваченное изображение - преобразование из YUV_420_888 в NV21

Через API камеры2 мы получаем объект Image в формате YUV_420_888. Затем мы используем следующую функцию для преобразования в NV21:

private static byte[] YUV_420_888toNV21(Image image) {
    byte[] nv21;
    ByteBuffer yBuffer = image.getPlanes()[0].getBuffer();
    ByteBuffer uBuffer = image.getPlanes()[1].getBuffer();
    ByteBuffer vBuffer = image.getPlanes()[2].getBuffer();

    int ySize = yBuffer.remaining();
    int uSize = uBuffer.remaining();
    int vSize = vBuffer.remaining();

    nv21 = new byte[ySize + uSize + vSize];

    //U and V are swapped
    yBuffer.get(nv21, 0, ySize);
    vBuffer.get(nv21, ySize, vSize);
    uBuffer.get(nv21, ySize + vSize, uSize);

    return nv21;
}

Хотя эта функция отлично работает с cameraCaptureSessions.setRepeatingRequest, мы получаем ошибку сегментации при дальнейшей обработке (на стороне JNI) при вызове cameraCaptureSessions.capture. Оба запрашивают формат YUV_420_888 через ImageReader.

Почему результат для обоих вызовов функций различается при одинаковом запрошенном типе?

Обновлять: Как упоминалось в комментариях, я получаю такое поведение из-за разных размеров изображения (гораздо больший размер для запроса захвата). Но наши дальнейшие операции обработки на стороне JNI одинаковы для обоих запросов и не зависят от размеров изображения (только от соотношения сторон, которое в обоих случаях одинаково).

Я не знаю, почему эти две ситуации дают разные результаты. Но узнать можно. Сравните параметры Изображение, полученные в двух случаях. Обратите внимание на необработанные и пиксельные шаги, особенно для плоскостей U и V.

Alex Cohn 09.10.2018 21:46

Одним из важных отличий может быть то, что вы запрашиваете разные размеры. Не то чтобы capture намного больше, но из-за этого изменения он может иметь другое заполнение.

Alex Cohn 09.10.2018 21:50

@AlexCohn Вы совершенно правы, разное поведение является результатом разных размеров изображений. Как мне настроить функцию, чтобы она учитывала другое заполнение?

Entertain 10.10.2018 11:47
2
3
7 335
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Ваш код вернет правильный NV21, только если нет заполнения вообще, а равнины U и V перекрываются и фактически представляют чересстрочные значения VU. Это происходит довольно часто для предварительного просмотра, но в этом случае вы выделяете дополнительные байты w*h/4 для вашего массива (что, по-видимому, не является проблемой). Возможно, для захваченного изображения вам понадобится более надежная реализация, например

private static byte[] YUV_420_888toNV21(Image image) {

    int width = image.getWidth();
    int height = image.getHeight(); 
    int ySize = width*height;
    int uvSize = width*height/4;

    byte[] nv21 = new byte[ySize + uvSize*2];

    ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); // Y
    ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); // U
    ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); // V

    int rowStride = image.getPlanes()[0].getRowStride();
    assert(image.getPlanes()[0].getPixelStride() == 1);

    int pos = 0;

    if (rowStride == width) { // likely
        yBuffer.get(nv21, 0, ySize);
        pos += ySize;
    }
    else {
        long yBufferPos = -rowStride; // not an actual position
        for (; pos<ySize; pos+=width) {
            yBufferPos += rowStride;
            yBuffer.position(yBufferPos);
            yBuffer.get(nv21, pos, width);
        }
    }

    rowStride = image.getPlanes()[2].getRowStride();
    int pixelStride = image.getPlanes()[2].getPixelStride();

    assert(rowStride == image.getPlanes()[1].getRowStride());
    assert(pixelStride == image.getPlanes()[1].getPixelStride());
    
    if (pixelStride == 2 && rowStride == width && uBuffer.get(0) == vBuffer.get(1)) {
        // maybe V an U planes overlap as per NV21, which means vBuffer[1] is alias of uBuffer[0]
        byte savePixel = vBuffer.get(1);
        try {
            vBuffer.put(1, (byte)~savePixel);
            if (uBuffer.get(0) == (byte)~savePixel) {
                vBuffer.put(1, savePixel);
                vBuffer.position(0);
                uBuffer.position(0);
                vBuffer.get(nv21, ySize, 1);
                uBuffer.get(nv21, ySize + 1, uBuffer.remaining());

                return nv21; // shortcut
            }
        }
        catch (ReadOnlyBufferException ex) {
            // unfortunately, we cannot check if vBuffer and uBuffer overlap
        }

        // unfortunately, the check failed. We must save U and V pixel by pixel
        vBuffer.put(1, savePixel);
    }

    // other optimizations could check if (pixelStride == 1) or (pixelStride == 2), 
    // but performance gain would be less significant

    for (int row=0; row<height/2; row++) {
        for (int col=0; col<width/2; col++) {
            int vuPos = col*pixelStride + row*rowStride;
            nv21[pos++] = vBuffer.get(vuPos);
            nv21[pos++] = uBuffer.get(vuPos);
        }
    }

    return nv21;
}

Если вы все равно собираетесь передать полученный массив в C++, вы можете воспользоваться факт, который

the buffer returned will always have isDirect return true, so the underlying data could be mapped as a pointer in JNI without doing any copies with GetDirectBufferAddress.

Это означает, что такое же преобразование может быть выполнено на C++ с минимальными накладными расходами. В C++ вы даже можете обнаружить, что фактическое расположение пикселей уже соответствует NV21!

PS На самом деле это можно сделать на Java с незначительными накладными расходами, см. Строку if (pixelStride == 2 && … выше. Итак, мы можем массово скопировать все байты цветности в результирующий массив байтов, что намного быстрее, чем выполнение циклов, но все же медленнее, чем то, что может быть достигнуто для такого случая в C++. Для полной реализации см. Image.toByteArray ().

у меня он разбился, когда yBuffer.position был за пределами поля в последнем прогоне. Чтобы исправить это, измените цикл на int bufferPos = yBuffer.position(); for (; pos<ySize; pos+=width) { yBuffer.position(bufferPos); yBuffer.get(nv21, pos, width); bufferPos+= rowStride; }

uwe 21.06.2019 12:56

@uwe спасибо, что поделились, я обновил свой ответ, чтобы адресовать этот крайний кадд

Alex Cohn 21.06.2019 19:10

Я не понимаю так называемого «ярлыка» в коде. Очевидно, что вторая ветвь никогда не встретится: if (uBuffer.get (0) == 0) {vBuffer.put (1, (byte) 255); если (uBuffer.get (0) == 255) {}}

Casper Bang 03.02.2020 16:44

@CasperBang вторая ветвь выбирается довольно часто, потому что базовая библиотека использует буферы перекрытие для U и V. Итак, uBuffer[0] и vBuffer[1] просматривают один и тот же байт в памяти. В C мы можем просто сравнить два указателя. Не в Java ? Что мы можем сделать, так это проверить, что значения uBuffer[0] и vBuffer[1] совпадают даже после некоторых манипуляций.

Alex Cohn 03.02.2020 22:59

Привет! Только что видел ваши обновления, и у меня есть вопрос. Кажется, что каждый раз, когда вы вызываете vBuffer.put(), вы вызываете его по первому индексу со значением savePixel, которое в первую очередь является vBuffer.get(1). Это означает, что вы устанавливаете его на значение, которое уже существует в этом индексе. Так нельзя ли удалить все эти вызовы пут? Спрашивает, потому что я получаю исключение ReadOnlyBufferException при попытке put()

kjanderson2 03.02.2020 23:47

@ kjanderson2: Спасибо за ваш отчет! Если vBuffer доступен только для чтения, вы не можете доказать, что uBuffer и vBuffer действительно перекрываются, поэтому вам нужно идти безопасным путем, используя цикл строка ** и ** столбец. vBuffer.put() предназначен не для того, чтобы постоянно изменять буфер, а только для того, чтобы отследить это перекрытие. К сожалению, не существует общедоступного API для проверки того, доступен ли ByteBuffer только для чтения, поэтому исключение - единственный способ узнать. Я добавил соответствующий try … catch, чтобы изящно осветить вашу ситуацию. Обратите внимание, что по-прежнему можно выполнять прямые манипуляции на C++.

Alex Cohn 04.02.2020 09:25

Мне любопытно, правильно ли я это сделал. Чтобы обойти ReadOnlyException, я скопировал содержимое буфера плоскости в новый буфер: val vPlaneBuffer = image.planes[2].buffer // image.planes[2].buffer is read only, so we make a copy so we can update the buffer val vBuffer = ByteBuffer.allocate(vPlaneBuffer.capacity()) vBuffer.put(vPlaneBuffer) Будет ли это работать также при сравнении, или я больше не сравниваю правильную информацию?

Stephen Emery 04.02.2020 18:14

@StephenEmery Нет, как только вы копируете vBuffer, вы навсегда теряете его перекрытие с uBuffer. Теоретически вы можете просто проверить, что все значения в vBuffer с нечетными смещениями (т.е. get (1), get (3), get (5),…) равны значениям в uBuffer с четными смещениями (т.е. get (0) , получаем (2), получаем (4),…). Если все пары uvSize равны, вы можете использовать ярлык, только скопировав vBuffer в результирующий массив nv21. Однако это непрактично: ваша прибыль (если вам повезет) будет потеряна, а ваши потери (если вам не повезет и буферы не перекрываются) будут значительными.

Alex Cohn 04.02.2020 19:48
Еще один комментарий: Я ожидаю, что результат проверки перекрытия будет согласованным для данного устройства и даже таким же для тех же устройств (например, если один Pixel 3 с Andorid 10 не показывает перекрытие, вы не увидите перекрытия буферов на другом Pixel 3 с Android 10).
Alex Cohn 04.02.2020 19:51

yBufferPos должен продвигаться на rowStride на каждой итерации (как в ответе ниже). В этом коде он продвигается по rowStride-width, я думаю, что это неверно

Sarsaparilla 16.08.2020 07:09

@Sarsaparilla Готов поспорить, вы правы. Заблудился в рефакторинге. Фиксированный. Спасибо за внимание к деталям!

Alex Cohn 16.08.2020 09:13

Это спасло положение. Спасибо.

James Westgate 03.02.2021 21:36
    public static byte[] YUV420toNV21(Image image) {
        Rect crop = image.getCropRect();
        int format = image.getFormat();
        int width = crop.width();
        int height = crop.height();
        Image.Plane[] planes = image.getPlanes();
        byte[] data = new byte[width * height * ImageFormat.getBitsPerPixel(format) / 8];
        byte[] rowData = new byte[planes[0].getRowStride()];

        int channelOffset = 0;
        int outputStride = 1;
        for (int i = 0; i < planes.length; i++) {
            switch (i) {
                case 0:
                    channelOffset = 0;
                    outputStride = 1;
                    break;
                case 1:
                    channelOffset = width * height + 1;
                    outputStride = 2;
                    break;
                case 2:
                    channelOffset = width * height;
                    outputStride = 2;
                    break;
            }

            ByteBuffer buffer = planes[i].getBuffer();
            int rowStride = planes[i].getRowStride();
            int pixelStride = planes[i].getPixelStride();

            int shift = (i == 0) ? 0 : 1;
            int w = width >> shift;
            int h = height >> shift;
            buffer.position(rowStride * (crop.top >> shift) + pixelStride * (crop.left >> shift));
            for (int row = 0; row < h; row++) {
                int length;
                if (pixelStride == 1 && outputStride == 1) {
                    length = w;
                    buffer.get(data, channelOffset, length);
                    channelOffset += length;
                } else {
                    length = (w - 1) * pixelStride + 1;
                    buffer.get(rowData, 0, length);
                    for (int col = 0; col < w; col++) {
                        data[channelOffset] = rowData[col * pixelStride];
                        channelOffset += outputStride;
                    }
                }
                if (row < h - 1) {
                    buffer.position(buffer.position() + rowStride - length);
                }
            }
        }
        return data;
    }

Как этот фрагмент кода решает проблему? Пожалуйста, дополните.

Orestis Zekai 13.12.2019 11:52

Этот код только преобразует изображение в byteBuffer ...

Alex F. 02.05.2021 09:40

Да, и у него есть формат N21 :)

Max Filinski 03.05.2021 10:10

На основе @Alex Cohn ответ я реализовал его в части JNI, пытаясь извлечь выгоду из преимуществ байтового доступа и производительности. Я оставил его здесь, может быть, он может быть так же полезен, как ответ @Alex для меня. Это почти тот же алгоритм на C; на основе изображения в формате YUV_420_888:

uchar* yuvToNV21(jbyteArray yBuf, jbyteArray uBuf, jbyteArray vBuf, jbyte *fullArrayNV21,
    int width, int height, int yRowStride, int yPixelStride, int uRowStride,
    int uPixelStride, int vRowStride, int vPixelStride, JNIEnv *env) {

    /* Check that our frame has right format, as specified at android docs for
     * YUV_420_888 (https://developer.android.com/reference/android/graphics/ImageFormat?authuser=2#YUV_420_888):
     *      - Plane Y not overlaped with UV, and always with pixelStride = 1
     *      - Planes U and V have the same rowStride and pixelStride (overlaped or not)
     */
    if (yPixelStride != 1 || uPixelStride != vPixelStride || uRowStride != vRowStride) {
        jclass Exception = env->FindClass("java/lang/Exception");
        env->ThrowNew(Exception, "Invalid YUV_420_888 byte structure. Not agree with https://developer.android.com/reference/android/graphics/ImageFormat?authuser=2#YUV_420_888");
    }

    int ySize = width*height;
    int uSize = env->GetArrayLength(uBuf);
    int vSize = env->GetArrayLength(vBuf);
    int newArrayPosition = 0; //Posicion por la que vamos rellenando el array NV21
    if (fullArrayNV21 == nullptr) {
        fullArrayNV21 = new jbyte[ySize + uSize + vSize];
    }
    if (yRowStride == width) {
        //Best case. No padding, copy direct
        env->GetByteArrayRegion(yBuf, newArrayPosition, ySize, fullArrayNV21);
        newArrayPosition = ySize;
    }else {
        // Padding at plane Y. Copy Row by Row
        long yPlanePosition = 0;
        for(; newArrayPosition<ySize; newArrayPosition += width) {
            env->GetByteArrayRegion(yBuf, yPlanePosition, width, fullArrayNV21 + newArrayPosition);
            yPlanePosition += yRowStride;
        }
    }

    // Check UV channels in order to know if they are overlapped (best case)
    // If they are overlapped, U and B first bytes are consecutives and pixelStride = 2
    long uMemoryAdd = (long)&uBuf;
    long vMemoryAdd = (long)&vBuf;
    long diff = std::abs(uMemoryAdd - vMemoryAdd);
    if (vPixelStride == 2 && diff == 8) {
        if (width == vRowStride) {
            // Best Case: Valid NV21 representation (UV overlapped, no padding). Copy direct
            env->GetByteArrayRegion(uBuf, 0, uSize, fullArrayNV21 + ySize);
            env->GetByteArrayRegion(vBuf, 0, vSize, fullArrayNV21 + ySize + uSize);
        }else {
            // UV overlapped, but with padding. Copy row by row (too much performance improvement compared with copy byte-by-byte)
            int limit = height/2 - 1;
            for(int row = 0; row<limit; row++) {
                env->GetByteArrayRegion(uBuf, row * vRowStride, width, fullArrayNV21 + ySize + (row * width));
            }
        }
    }else {
        //WORST: not overlapped UV. Copy byte by byte
        for(int row = 0; row<height/2; row++) {
           for(int col = 0; col<width/2; col++) {
               int vuPos = col*uPixelStride + row*uRowStride;
               env->GetByteArrayRegion(vBuf, vuPos, 1, fullArrayNV21 + newArrayPosition);
               newArrayPosition++;
               env->GetByteArrayRegion(uBuf, vuPos, 1, fullArrayNV21 + newArrayPosition);
               newArrayPosition++;
           }
        }
    }
    return (uchar*)fullArrayNV21;
}

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

Спасибо за фрагмент. Работает отлично. Однако мне пришлось внести некоторые изменения. Из-за оптимизации компилятора вычисленное различие от uMemoryAdd и yMemoryAdd может отличаться для оптимизированного и не оптимизированного кода. Я заменил его проверкой uBuf [0] == vBuf [1]. Еще я изменил, убрал -1 в int limit = height / 2 - 1; потому что цикл for не обрабатывал последнюю строку, в результате чего внизу оставалась зеленая линия.

Martyns 28.05.2020 23:33

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