How to enhance this YUV420P to RGB conversion in C/C++?

Viewed 441

I'm trying to write an YUV420P to RGB888 for when I have the entire thing as one giant buffer with Y (of size width*height) then Cr (of size width*height/4) then Cb (of size width*height/4). The output should be an RGB buffer with size width*height*3.

I think my function below is very inefficient. For example, I use the ceiling function (shouldn't it return an int? In my case it's returning a double, why?) and I've never seen any color conversion function use this function. But this is the way I found to get the corresponding Cr and Cb to each Y.

JNIEXPORT void JNICALL Java_com_example_mediacodecdecoderexample_YuvToRgb_YUVtoRBGA2(JNIEnv * env, jobject obj, jbyteArray yuv420sp, jint width, jint height, jbyteArray rgbOut)
{
    //ITU-R BT.601 conversion
    //
    //     R = 1.164*(Y-16)+1.596*(Cr-128)
    //     G = 1.164*(Y-16)-0.392*(Cb-128)-0.813*(Cr-128)
    //     B = 1.164*(Y-16)+2.017*(Cb-128)
    //
    int Y;
    int Cr;
    int Cb;
    int R;
    int G;
    int B;
    int size = width * height;
    //After width*height luminance values we have the Cr values
    size_t CrBase = size;
    //After width*height luminance values + width*height/4 we have the Cb values
    size_t CbBase = size + width*height/4;
    jbyte *rgbData = (jbyte*) ((*env)->GetPrimitiveArrayCritical(env, rgbOut, 0));
    jbyte* yuv = (jbyte*) (*env)->GetPrimitiveArrayCritical(env, yuv420sp, 0);

    for (int i=0; i<size; i++) {
        Y  = rgbData[i] - 16;
        Cr = rgbData[CrBase + ceil(i/4)]  - 128;
        Cb = rgbData[CbBase + ceil(i/4)]  - 128;
        R = 1.164*Y+1.596*Cr;
        G = 1.164*Y-0.392*Cb-0.813*Cr;
        B = 1.164*Y+2.017*Cb;
        yuv[i*3] = R;
        yuv[i*3+1] = G;
        yuv[i*3+2] = B;
    }

    (*env)->ReleasePrimitiveArrayCritical(env, rgbOut, rgbData, 0);
    (*env)->ReleasePrimitiveArrayCritical(env, yuv420sp, yuv, 0);
}

I'm doing this because I haven't found a function that does exactly this and I need one for a MediaCodec decoded buffer. But even if there's one, I'd like to know what can be done to improve my function, just to learn.

UPDATE:

I modified the code based on the answer below in order for it to work with ByteBuffer:

JNIEXPORT void JNICALL Java_com_lucaszanella_mediacodecdecoderexample_YuvToRgb_YUVtoRBGA2(JNIEnv * env, jobject obj, jobject yuv420sp, jint width, jint height, jobject rgbOut)
{
    //ITU-R BT.601 conversion
    //
    //     R = 1.164*(Y-16)+1.596*(Cr-128)
    //     G = 1.164*(Y-16)-0.392*(Cb-128)-0.813*(Cr-128)
    //     B = 1.164*(Y-16)+2.017*(Cb-128)
    //

    char *rgbData = (char*)(*env)->GetDirectBufferAddress(env, rgbOut);
    char *yuv = (char*)(*env)->GetDirectBufferAddress(env, yuv420sp);

    const int size = width * height;

    //After width*height luminance values we have the Cr values
    const size_t CrBase = size;

    //After width*height luminance values + width*height/4 we have the Cb values
    const size_t CbBase = size + width*height/4;

    for (int i=0; i<size; i++) {
        int Y  = yuv[i] - 16;
        int Cr = yuv[CrBase + i/4]  - 128;
        int Cb = yuv[CbBase + i/4]  - 128;

        double R = 1.164*Y+1.596*Cr;
        double G = 1.164*Y-0.392*Cb-0.813*Cr;
        double B = 1.164*Y+2.017*Cb;

        rgbData[i*3] = (R > 255) ? 255 : ((R < 0) ? 0 : R);
        rgbData[i*3+1] = (G > 255) ? 255 : ((G < 0) ? 0 : G);
        rgbData[i*3+2] = (B > 255) ? 255 : ((B < 0) ? 0 : B);
    }
}

however it's crashing. I don't see anything being written outside of boundary. Anyone have any idea?

UPDATE:

Code above works if we call it with a direct byte buffer. Won't work if the buffer is not direct.

Added

    if (rgbData==NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "TRACKERS", "%s", "RGB data null");
    }

    if (yuv==NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "TRACKERS", "%s", "yuv data null");
    }
    if (rgbData==NULL || yuv==NULL) {
        return;
    }

for safety.

Anyways, color is not correct:

enter image description here

4 Answers

Is it just me, but but shouldn't you be reading from the yuv array and writing to the rgbData array? You actually have it reversed in your implementation.

There's not need to invoke ceil on an integer expression such as i/4. And when you implement an image processing route, invoking a function call on every pixel is just going to kill performance (been there, done that). Maybe the compiler can optimize it out, but why take that chance.

So change this:

    Cr = rgbData[CrBase + ceil(i/4)]  - 128;
    Cb = rgbData[CbBase + ceil(i/4)]  - 128;

To this:

    Cr = rgbData[CrBase + i/4]  - 128;
    Cb = rgbData[CbBase + i/4]  - 128;

The only other thing to be wary of is that you may want to clamp R, G, and B to be in the 8-bit byte range before assigning back to the yuv array. Those math equations can produce results < 0 and > 255.

Another micro-optimization is to declare all your variables within the for-loop block so the compiler has more hints about optimizing on it as temporaries. And declaring some of your other constants as const May I suggest:

JNIEXPORT void JNICALL Java_com_example_mediacodecdecoderexample_YuvToRgb_YUVtoRBGA2(JNIEnv * env, jobject obj, jbyteArray yuv420sp, jint width, jint height, jbyteArray rgbOut)
{
    //ITU-R BT.601 conversion
    //
    //     R = 1.164*(Y-16)+1.596*(Cr-128)
    //     G = 1.164*(Y-16)-0.392*(Cb-128)-0.813*(Cr-128)
    //     B = 1.164*(Y-16)+2.017*(Cb-128)
    //
    const int size = width * height;
    //After width*height luminance values we have the Cr values

    const size_t CrBase = size;
    //After width*height luminance values + width*height/4 we have the Cb values

    const size_t CbBase = size + width*height/4;

    jbyte *rgbData = (jbyte*) ((*env)->GetPrimitiveArrayCritical(env, rgbOut, 0));
    jbyte* yuv= (jbyte*) (*env)->GetPrimitiveArrayCritical(env, yuv420sp, 0);

    for (int i=0; i<size; i++) {
        int Y  = yuv[i] - 16;
        int Cr = yuv[CrBase + i/4]  - 128;
        int Cb = yuv[CbBase + i/4]  - 128;

        int R = 1.164*Y+1.596*Cr;
        int G = 1.164*Y-0.392*Cb-0.813*Cr;
        int B = 1.164*Y+2.017*Cb;

        rgbData[i*3] = (R > 255) ? 255 : ((R < 0) ? 0 : R);
        rgbData[i*3+1] = (G > 255) ? 255 : ((G < 0) ? 0 : G);
        rgbData[i*3+2] = (B > 255) ? 255 : ((B < 0) ? 0 : B);
    }

    (*env)->ReleasePrimitiveArrayCritical(env, rgbOut, rgbData, 0);
    (*env)->ReleasePrimitiveArrayCritical(env, yuv420sp, yuv, 0);
}

Then the only left to do is just to compile with max optimizations on. The compiler will take care of the rest.

After that, investigating SIMD optimizations, which some compilers offer as a compiler switch (or enabled via pragma).

A little modification to selbie's answer which uses ByteBuffer which is more useful since it's what Java produces when it decodes.

JNIEXPORT void JNICALL Java_com_example_mediacodecdecoderexample_YuvToRgb_YUVtoRBGA2(JNIEnv * env, jobject obj, jobject yuv420sp, jint width, jint height, jobject rgbOut)
{
    //ITU-R BT.601 conversion
    //
    //     R = 1.164*(Y-16)+1.596*(Cr-128)
    //     G = 1.164*(Y-16)-0.392*(Cb-128)-0.813*(Cr-128)
    //     B = 1.164*(Y-16)+2.017*(Cb-128)
    //
    const int size = width * height;
    
    //After width*height luminance values we have the Cr values
    const size_t CrBase = size;
    
    //After width*height luminance values + width*height/4 we have the Cb values
    const size_t CbBase = size + width*height/4;

    jbyte *rgbData = (*env)->GetDirectBufferAddress(env, rgbOut);
    jbyte *yuv = (*env)->GetDirectBufferAddress(env, yuv420sp);

    for (int i=0; i<size; i++) {
        int Y  = yuv[i] - 16;
        int Cr = yuv[CrBase + i/4]  - 128;
        int Cb = yuv[CbBase + i/4]  - 128;

        int R = 1.164*Y+1.596*Cr;
        int G = 1.164*Y-0.392*Cb-0.813*Cr;
        int B = 1.164*Y+2.017*Cb;

        rgbData[i*3] = (R > 255) ? 255 : ((R < 0) ? 0 : R);
        rgbData[i*3+1] = (G > 255) ? 255 : ((G < 0) ? 0 : G);
        rgbData[i*3+2] = (B > 255) ? 255 : ((B < 0) ? 0 : B);
    }
}

regarding:

*I use the ceiling function (shouldn't it return an int? In my case it's returning a double, why?)*

here is the syntax:

double ceil(double x);

Notice the returned type is double

MAN page for ceil()

Do not do it yourself! Do not do that directly in C++! The only proper approach is to use hardware acceleration for that. You will save lots of battery.

Basically you can utilize OpenGL to that and it will use hardware in your behalf.

Long long time ago I did this for iOS and I'm sure solution for Android will be quite similar. Sadly I left code behind (in old company) so I can't provide you example code. If I find something useful then I will update this answer. In my code YUV (and couple others color formats) was rendered directly on openGL view and OpenGL did required conversion.

Now I'm just pointing finger on OpenGL since other answers are doing this directly on CPU what is a bad choice since it will consume battery a lot and you will never achieve desired performance this way.

Edit: I've found similar question on SO with some example: https://stackoverflow.com/a/17110754/1387438

Disclaimer: didn't verified that this example is best approach, but this is a good way to start to look for better solutions.


If for some reason you need do this in C++ code anyway then drop floating point operations in favor of operations on integer types.
Related