c++opencvrgbyuv

Full-range YCbCr to RGB and back in OpenCV not reversible


I'm trying to convert a simple variant of YCbCr to RGB and back. The formula I'm using on this page (RGB to full-range YCbCr and vice versa).

inline cv::Mat rgb_to_ycrcb(cv::Mat input) {
    cv::Mat output(input.size(), input.type(), cv::Scalar(0, 0, 0));
    float rgb_to_ycrcb_data[] = {
                0.2990, 0.5870, 0.114,
                -0.169, -0.331, 0.500,
                0.5000, -0.419, -0.081
            };
    cv::Mat rgb_to_ycrcb = cv::Mat(3, 3, CV_32FC1, rgb_to_ycrcb_data);

    for (int r = 0; r < input.rows; r++) {
        for (int c = 0; c < input.cols; c++) {
            cv::Vec3f rgb_value;
            cv::Mat input_rgb = cv::Mat(input.ptr<cv::Vec3b>(r)[c], false);
            input_rgb.convertTo(rgb_value, CV_32F);
            cv::Vec3f offset(0., 128., 128.);

            cv::Mat converted = cv::Mat(offset, false) +
                                rgb_to_ycrcb * cv::Mat(rgb_value, false);
            cv::Vec3b final_vec;
            converted.convertTo(final_vec,
                                CV_8U);
            output.ptr<cv::Vec3b>(r)[c] = converted;
        }
    }
    return output;
}

inline cv::Mat ycrcb_to_rgb(cv::Mat input) {
    cv::Mat output(input.size(), input.type(), cv::Scalar(0, 0, 0));
    float ycrcb_to_rgb_data[] = {
                1.000, 0.0000, 1.4000,
                1.000, -0.343, -0.711,
                1.000, 1.7650, 0.000
            };
    cv::Mat ycrcb_to_rgb = cv::Mat(3, 3, CV_32FC1, ycrcb_to_rgb_data);
    for (int r = 0; r < input.rows; r++) {
        for (int c = 0; c < input.cols; c++) {
            cv::Vec3f ycrcb_value;
            cv::Mat(input.ptr<cv::Vec3b>(r)[c], false).
            convertTo(ycrcb_value, CV_32F);
            ycrcb_value[1] -= 128;
            ycrcb_value[2] -= 128;
            cv::Mat converted = ycrcb_to_rgb * cv::Mat(ycrcb_value, false);
            cv::Vec3b final_vec;
            converted.convertTo(final_vec,
                                CV_8U);
            output.ptr<cv::Vec3b>(r)[c] = converted;
        }
    }
    return output;
}

When I convert an image in the YCrCb color format consisting of the tuple (28, 113, 14) to RGB, I get (0, 114, 2). Converting that RGB value back to YCrCb yields (67, 91, 80).

The formula for full-range YCrCb specifies that the domain of all the components of YCrCb is [0, 255] and the corresponding codomain of the RGB components is [0, 255] as well. Did I implement this formula incorrectly? If not, why isn't it reversible? And if it isn't reversible, is there another YUV-like formula that is?


Solution

  • Your YCbCr tuple exceeds valid range of RGB.
    The conversion formula (BT.601) standard is as follows:

    R' = 1.164*(Y' - 16) + 1.596*(Cr' - 128)

    G' = 1.164*(Y' - 16) - 0.813*(Cr' - 128) - 0.392*(Cb' - 128)

    B' = 1.164*(Y' - 16) + 2.017*(Cb' - 128)

    See https://software.intel.com/en-us/node/503907

    This is not the exact formula you used in your code, but the principle is the same.

    Place the YCbCr tuple values in the conversion formula:

    R' = 1.164*(Y' - 16) + 1.596*(Cr' - 128) = -167.9760
    G' = 1.164*(Y' - 16) - 0.813*(Cr' - 128) - 0.392*(Cb' - 128) = 112.5300
    B' = 1.164*(Y' - 16) + 2.017*(Cb' - 128) = -16.2870
    

    As you can see R and B exceeds range [0, 255].
    In my example, the values of R and B are clamped to zero.
    In your conversion, only R was clamped to zero.
    When values are clamped, the result is not reversible.

    Try to illustrate by a diagram (not sure it's the right one).
    As you can see values of YCbCr exceeds the sRGB range.
    enter image description here