Преобразование YUV - >RGB (обработка изображений)->YUV во время onPreviewFrame в android?

я захватываю изображение с помощью SurfaceView и получаю данные предварительного просмотра YUV Raw в public void onPreviewFrame4 (байт [] данные, камера камеры)

Я должен выполнить некоторую предварительную обработку изображения в onPreviewFrame, поэтому мне нужно преобразовать данные предварительного просмотра Yuv в данные RGB, чем предварительная обработка изображения и обратно в данные Yuv.

я использовал обе функции для кодирования и декодирования данных YUV в RGB следующим образом :

public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = configManager.getCameraResolution();
    if (data != null) {
        Log.i("DEBUG", "data Not Null");

                // Preprocessing
                Log.i("DEBUG", "Try For Image Processing");
                Camera.Parameters mParameters = camera.getParameters();
                Size mSize = mParameters.getPreviewSize();
                int mWidth = mSize.width;
                int mHeight = mSize.height;
                int[] mIntArray = new int[mWidth * mHeight];

                // Decode Yuv data to integer array
                decodeYUV420SP(mIntArray, data, mWidth, mHeight);

                // Converting int mIntArray to Bitmap and 
                // than image preprocessing 
                // and back to mIntArray.

                // Encode intArray to Yuv data
                encodeYUV420SP(data, mIntArray, mWidth, mHeight);
                    }
}

    static public void decodeYUV420SP(int[] rgba, byte[] yuv420sp, int width,
        int height) {
    final int frameSize = width * height;

    for (int j = 0, yp = 0; j < height; j++) {
        int uvp = frameSize + (j >> 1) * width, u = 0, v = 0;
        for (int i = 0; i < width; i++, yp++) {
            int y = (0xff & ((int) yuv420sp[yp])) - 16;
            if (y < 0)
                y = 0;
            if ((i & 1) == 0) {
                v = (0xff & yuv420sp[uvp++]) - 128;
                u = (0xff & yuv420sp[uvp++]) - 128;
            }

            int y1192 = 1192 * y;
            int r = (y1192 + 1634 * v);
            int g = (y1192 - 833 * v - 400 * u);
            int b = (y1192 + 2066 * u);

            if (r < 0)
                r = 0;
            else if (r > 262143)
                r = 262143;
            if (g < 0)
                g = 0;
            else if (g > 262143)
                g = 262143;
            if (b < 0)
                b = 0;
            else if (b > 262143)
                b = 262143;

            // rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) &
            // 0xff00) | ((b >> 10) & 0xff);
            // rgba, divide 2^10 ( >> 10)
            rgba[yp] = ((r << 14) & 0xff000000) | ((g << 6) & 0xff0000)
                    | ((b >> 2) | 0xff00);
        }
    }
}


    static public void encodeYUV420SP_original(byte[] yuv420sp, int[] rgba,
        int width, int height) {
    final int frameSize = width * height;

    int[] U, V;
    U = new int[frameSize];
    V = new int[frameSize];

    final int uvwidth = width / 2;

    int r, g, b, y, u, v;
    for (int j = 0; j < height; j++) {
        int index = width * j;
        for (int i = 0; i < width; i++) {
            r = (rgba[index] & 0xff000000) >> 24;
            g = (rgba[index] & 0xff0000) >> 16;
            b = (rgba[index] & 0xff00) >> 8;

            // rgb to yuv
            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;

            // clip y
            yuv420sp[index++] = (byte) ((y < 0) ? 0 : ((y > 255) ? 255 : y));
            U[index] = u;
            V[index++] = v;
        }
    }

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

пожалуйста, помогите мне решить эту проблему. Я должен использовать этот код в OCR-сканировании, поэтому мне нужно реализовать этот тип логики.

Если любой другой способ сделать то же самое, чем пожалуйста, предоставьте мне.

спасибо заранее. :)

7 ответов


Почему бы не указать, что предварительный просмотр камеры должен предоставлять изображения RGB?

то есть камера.Параметры.setPreviewFormat (ImageFormat.RGB_565);


хотя документация предполагает, что вы можете установить, в каком формате данные изображения должны поступать с камеры, на практике у вас часто есть выбор: nv21, формат YUV. Для многих информация об этом формате см. http://www.fourcc.org/yuv.php#NV21 и для сведения по теории, за его преобразования в RGB см. http://www.fourcc.org/fccyvrgb.php. Есть картина, на основе объяснений извлечение черно-белого изображения из android формат NV21 камеры.

другой формат, называемый YUV420SP, также довольно распространен.

однако, как только вы настроили свою процедуру onPreviewFrame, механика перехода от массива байтов, который он отправляет вам, к полезным данным, несколько, МММ, неясна. Начиная с API 8, доступно следующее решение, чтобы добраться до ByteStream, отдыхающего JPEG изображения (compressToJpeg-единственный вариант преобразования, предлагаемый YuvImage):

// pWidth and pHeight define the size of the preview Frame
ByteArrayOutputStream out = new ByteArrayOutputStream();

// Alter the second parameter of this to the actual format you are receiving
YuvImage yuv = new YuvImage(data, ImageFormat.NV21, pWidth, pHeight, null);

// bWidth and bHeight define the size of the bitmap you wish the fill with the preview image
yuv.compressToJpeg(new Rect(0, 0, bWidth, bHeight), 50, out);

этот JPEG затем может потребоваться преобразовать в формат, который вы хотите. Если вы хотите растровое изображение:

byte[] bytes = out.toByteArray();
Bitmap bitmap= BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

Если по каким-то причинам вы не можете сделать это, вы можете сделать преобразование вручную. Некоторые проблемы, которые необходимо преодолеть при этом:

  1. данные поступают в массив байтов. По определению, байты являются знаковыми числами, что означает, что они идут от -128 до 127. Однако данные на самом деле являются байтами без знака (от 0 до 255). Если с этим не разобраться, результат обречен на провал. есть некоторые странные эффекты обрезки.

  2. данные находятся в очень определенном порядке (согласно ранее упомянутой веб-странице), и каждый пиксель должен быть извлечен тщательно.

  3. каждый пиксель должен быть помещен в нужное место растрового изображения, скажем. Это также требует довольно беспорядочного (на мой взгляд) подхода к созданию буфера данных, а затем заполнению растрового изображения из него.

  4. Если у вас действительно есть NV12 (или 420SP), затем вам нужно будет поменять чтения на U и V

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

переменная данных поступает из вызова onPreviewFrame

// the bitmap we want to fill with the image
Bitmap bitmap = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888);
int numPixels = imageWidth*imageHeight;

// the buffer we fill up which we then fill the bitmap with
IntBuffer intBuffer = IntBuffer.allocate(imageWidth*imageHeight);
// If you're reusing a buffer, next line imperative to refill from the start,
// if not good practice
intBuffer.position(0);

// Set the alpha for the image: 0 is transparent, 255 fully opaque
final byte alpha = (byte) 255;

// Get each pixel, one at a time
for (int y = 0; y < imageHeight; y++) {
    for (int x = 0; x < imageWidth; x++) {
        // Get the Y value, stored in the first block of data
        // The logical "AND 0xff" is needed to deal with the signed issue
        int Y = data[y*imageWidth + x] & 0xff;

        // Get U and V values, stored after Y values, one per 2x2 block
        // of pixels, interleaved. Prepare them as floats with correct range
        // ready for calculation later.
        int xby2 = x/2;
        int yby2 = y/2;

        // make this V for NV12/420SP
        float U = (float)(data[numPixels + 2*xby2 + yby2*imageWidth] & 0xff) - 128.0f;

        // make this U for NV12/420SP
        float V = (float)(data[numPixels + 2*xby2 + 1 + yby2*imageWidth] & 0xff) - 128.0f;

        // Do the YUV -> RGB conversion
        float Yf = 1.164f*((float)Y) - 16.0f;
        int R = (int)(Yf + 1.596f*V);
        int G = (int)(Yf - 0.813f*V - 0.391f*U);
        int B = (int)(Yf            + 2.018f*U);

        // Clip rgb values to 0-255
        R = R < 0 ? 0 : R > 255 ? 255 : R;
        G = G < 0 ? 0 : G > 255 ? 255 : G;
        B = B < 0 ? 0 : B > 255 ? 255 : B;

        // Put that pixel in the buffer
        intBuffer.put(alpha*16777216 + R*65536 + G*256 + B);
    }
}

// Get buffer ready to be read
intBuffer.flip();

// Push the pixel information from the buffer onto the bitmap.
bitmap.copyPixelsFromBuffer(intBuffer);

как указывает @Timmmm ниже, вы можете выполнить преобразование в int путем умножения коэффициентов масштабирования на 1000(т. е. 1.164 становится 1164), а затем делит конечные результаты на 1000.


после некоторых тестов на Samsung S4 mini самый быстрый код (120% быстрее, чем у Нила [плавает!] и на 30% быстрее, чем оригинальные Hitesh):

static public void decodeYUV420SP(int[] rgba, byte[] yuv420sp, int width,
                                  int height) {


    final int frameSize = width * height;
// define variables before loops (+ 20-30% faster algorithm o0`)
int r, g, b, y1192, y, i, uvp, u, v;
        for (int j = 0, yp = 0; j < height; j++) {
            uvp = frameSize + (j >> 1) * width;
            u = 0;
        v = 0;
        for (i = 0; i < width; i++, yp++) {
            y = (0xff & ((int) yuv420sp[yp])) - 16;
            if (y < 0)
                y = 0;
            if ((i & 1) == 0) {
                v = (0xff & yuv420sp[uvp++]) - 128;
                u = (0xff & yuv420sp[uvp++]) - 128;
            }

                y1192 = 1192 * y;
                r = (y1192 + 1634 * v);
                g = (y1192 - 833 * v - 400 * u);
                b = (y1192 + 2066 * u);

// Java's functions are faster then 'IFs'
                    r = Math.max(0, Math.min(r, 262143));
                g = Math.max(0, Math.min(g, 262143));
                b = Math.max(0, Math.min(b, 262143));

                // rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) &
                // 0xff00) | ((b >> 10) & 0xff);
                // rgba, divide 2^10 ( >> 10)
                rgba[yp] = ((r << 14) & 0xff000000) | ((g << 6) & 0xff0000)
                        | ((b >> 2) | 0xff00);
            }
        }
    }

скорость сравнима с YuvImage.compressToJpeg() С ByteArrayOutputStream как выход (30-50 мс для изображения 640x480).

результат: Samsung S4 mini (2x1.7GHz) не может сжиматься в JPEG/конвертировать YUV в RGB в режиме реального времени (640x480@30fps)


реализация Java в 10 раз медленнее, чем версия c, я предлагаю вам использовать библиотеку GPUImage или просто переместить эту часть кода.

существует android-версия GPUImage: https://github.com/CyberAgent/android-gpuimage

вы можете включить эту библиотеку, если используете gradle, и вызвать метод: GPUImageNativeLibrary.YUVtoRBGA (inputArray, ширина, высота, outputArray);

Я сравниваю время, для изображения NV21 которое 960x540, используйте выше java-код, он стоит 200 мс+, с версией GPUImage, всего 10 мс~20 мс.


исправить приведенный выше фрагмент кода

static public void decodeYUV420SP(int[] rgba, byte[] yuv420sp, int width,
                              int height) {
    final int frameSize = width * height;
    int r, g, b, y1192, y, i, uvp, u, v;
    for (int j = 0, yp = 0; j < height; j++) {
        uvp = frameSize + (j >> 1) * width;
        u = 0;
        v = 0;
        for (i = 0; i < width; i++, yp++) {
            y = (0xff & ((int) yuv420sp[yp])) - 16;
            if (y < 0)
                y = 0;
            if ((i & 1) == 0) {
            // above answer is wrong at the following lines. just swap ***u*** and ***v*** 
                u = (0xff & yuv420sp[uvp++]) - 128;
                v = (0xff & yuv420sp[uvp++]) - 128;
            }

            y1192 = 1192 * y;
            r = (y1192 + 1634 * v);
            g = (y1192 - 833 * v - 400 * u);
            b = (y1192 + 2066 * u);

            r = Math.max(0, Math.min(r, 262143));
            g = Math.max(0, Math.min(g, 262143));
            b = Math.max(0, Math.min(b, 262143));

            // combine ARGB
            rgba[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00)
                    | ((b >> 10) | 0xff);
        }
    }
}

попробуйте RenderScript ScriptIntrinsicYuvToRGB, который поставляется с JellyBean 4.2 (Api 17+).

https://developer.android.com/reference/android/renderscript/ScriptIntrinsicYuvToRGB.html

на Nexus 7 (2013, JellyBean 4.3) преобразование изображения 1920x1080 (предварительный просмотр камеры full HD) занимает около 7 мс.


вы можете использовать RenderScript - > ScriptIntrinsicYuvToRGB

Образец Котлин

val rs = RenderScript.create(CONTEXT_HERE)
val yuvToRgbIntrinsic = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs))

val yuvType = Type.Builder(rs, Element.U8(rs)).setX(byteArray.size)
val inData = Allocation.createTyped(rs, yuvType.create(), Allocation.USAGE_SCRIPT)

val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)).setX(width).setY(height)
val outData = Allocation.createTyped(rs, rgbaType.create(), Allocation.USAGE_SCRIPT)

inData.copyFrom(byteArray)

yuvToRgbIntrinsic.setInput(inData)
yuvToRgbIntrinsic.forEach(outData)

val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
outData.copyTo(bitmap)