diff --git a/src/util/meson.build b/src/util/meson.build index 62fc844f07f..4f3b71dd730 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -466,6 +466,7 @@ if with_tests 'tests/u_memstream_test.cpp', 'tests/u_printf_test.cpp', 'tests/u_qsort_test.cpp', + 'tests/u_ycbcr_test.cpp', 'tests/vector_test.cpp', ) diff --git a/src/util/tests/u_ycbcr_test.cpp b/src/util/tests/u_ycbcr_test.cpp new file mode 100644 index 00000000000..e3b1a48292d --- /dev/null +++ b/src/util/tests/u_ycbcr_test.cpp @@ -0,0 +1,520 @@ +/** + * Copyright (c) 2026 Collabora Ltd. + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include + +#include "util/u_ycbcr.h" + +#include "macros.h" + +static void +test_to_rgb_coeffs(const float coeffs[3]) +{ + float mat[3][4]; + util_get_ycbcr_to_rgb_matrix(mat, coeffs); + + float a = coeffs[0]; + float b = coeffs[1]; + float c = coeffs[2]; + float d = 2 - 2 * c; + float e = 2 - 2 * a; + + const struct { + float input[3]; + float expected[3]; + } test_data[] = { { + { 1.0f, 0.0f, 0.0f}, + { 1.0f, 1.0f, 1.0f }, + }, { + { 0.0f, 0.0f, 0.0f}, + { 0.0f, 0.0f, 0.0f }, + }, { + { 0.5f, 0.0f, 0.0f }, + { 0.5f, 0.5f, 0.5f }, + }, { + { a, -a / d, 0.5f }, + { 1.0f, 0.0f, 0.0f }, + }, { + { b, -b / d, -b / e }, + { 0.0f, 1.0f, 0.0f }, + }, { + { c, 0.5f, -c / e }, + { 0.0f, 0.0f, 1.0f }, + }, { + { 1 - c, -0.5f, c / e }, + { 1.0f, 1.0f, 0.0f }, + }, { + { 1 - a, a / d, (a - 1) / e }, + { 0.0f, 1.0f, 1.0f }, + }, { + { 1 - b, b / d, b / e }, + { 1.0f, 0.0f, 1.0f }, + } + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) { + float result[3]; + for (int c = 0; c < 3; ++c) { + result[c] = test_data[i].input[0] * mat[c][0] + + test_data[i].input[1] * mat[c][1] + + test_data[i].input[2] * mat[c][2]; + + } + + EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7); + EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7); + EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7); + + /* verify with reference equation */ + float Y = test_data[i].input[0]; + float Cb = test_data[i].input[1]; + float Cr = test_data[i].input[2]; + + result[0] = Y + e * Cr; + result[1] = Y - (a * e / b) * Cr - (c * d / b) * Cb; + result[2] = Y + d * Cb; + + EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-6); + EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-6); + EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-6); + } +} + +TEST(u_ycbcr_test, to_rgb) +{ + test_to_rgb_coeffs(util_ycbcr_bt601_coeffs); + test_to_rgb_coeffs(util_ycbcr_bt709_coeffs); + test_to_rgb_coeffs(util_ycbcr_bt2020_coeffs); +} + +static void +test_to_ycbcr_coeffs(const float coeffs[3]) +{ + float mat[3][4]; + util_get_rgb_to_ycbcr_matrix(mat, coeffs); + + float a = coeffs[0]; + float b = coeffs[1]; + float c = coeffs[2]; + float d = 2 - 2 * c; + float e = 2 - 2 * a; + + const struct { + float input[3]; + float expected[3]; + } test_data[] = { { + { 1.0f, 1.0f, 1.0f }, + { 1.0f, 0.0f, 0.0f}, + }, { + { 0.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f}, + }, { + { 0.5f, 0.5f, 0.5f }, + { 0.5f, 0.0f, 0.0f }, + }, { + { 1.0f, 0.0f, 0.0f }, + { a, -a / d, 0.5f }, + }, { + { 0.0f, 1.0f, 0.0f }, + { b, -b / d, -b / e }, + }, { + { 0.0f, 0.0f, 1.0f }, + { c, 0.5f, -c / e }, + }, { + { 1.0f, 1.0f, 0.0f }, + { 1 - c, -0.5f, c / e }, + }, { + { 0.0f, 1.0f, 1.0f }, + { 1 - a, a / d, (a - 1) / e }, + }, { + { 1.0f, 0.0f, 1.0f }, + { 1 - b, b / d, b / e }, + } + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) { + float result[3]; + for (int c = 0; c < 3; ++c) { + result[c] = test_data[i].input[0] * mat[c][0] + + test_data[i].input[1] * mat[c][1] + + test_data[i].input[2] * mat[c][2]; + + } + + EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7); + EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7); + EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7); + + /* verify with reference equation */ + float R = test_data[i].input[0]; + float G = test_data[i].input[1]; + float B = test_data[i].input[2]; + + float Y = a * R + b * G + c * B;; + result[0] = Y; + result[1] = (B - Y) / d; + result[2] = (R - Y) / e; + + EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7); + EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7); + EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7); + } +} + +TEST(u_ycbcr_test, to_ycbcr) +{ + test_to_ycbcr_coeffs(util_ycbcr_bt601_coeffs); + test_to_ycbcr_coeffs(util_ycbcr_bt709_coeffs); + test_to_ycbcr_coeffs(util_ycbcr_bt2020_coeffs); +} + +static void +test_to_ycbcr_and_back_coeffs(const float coeffs[3]) +{ + float to_ycbcr[3][4], to_rgb[3][4]; + util_get_rgb_to_ycbcr_matrix(to_ycbcr, coeffs); + util_get_ycbcr_to_rgb_matrix(to_rgb, coeffs); + + float inputs[][3] = { + { 1.0f, 1.0f, 1.0f }, + { 0.0f, 0.0f, 0.0f }, + { 0.5f, 0.5f, 0.5f }, + { 1.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f }, + { 1.0f, 1.0f, 0.0f }, + { 0.0f, 1.0f, 1.0f }, + { 1.0f, 0.0f, 1.0f }, + }; + + for (size_t i = 0; i < ARRAY_SIZE(inputs); ++i) { + float ycbcr[3]; + for (int c = 0; c < 3; ++c) { + ycbcr[c] = inputs[i][0] * to_ycbcr[c][0] + + inputs[i][1] * to_ycbcr[c][1] + + inputs[i][2] * to_ycbcr[c][2]; + + } + + float result[3]; + for (int c = 0; c < 3; ++c) { + result[c] = ycbcr[0] * to_rgb[c][0] + + ycbcr[1] * to_rgb[c][1] + + ycbcr[2] * to_rgb[c][2]; + + } + + EXPECT_NEAR(inputs[i][0], result[0], 1e-6); + EXPECT_NEAR(inputs[i][1], result[1], 1e-6); + EXPECT_NEAR(inputs[i][2], result[2], 1e-6); + } +} + +TEST(u_ycbcr_test, to_ycbcr_and_back) +{ + test_to_ycbcr_and_back_coeffs(util_ycbcr_bt601_coeffs); + test_to_ycbcr_and_back_coeffs(util_ycbcr_bt709_coeffs); + test_to_ycbcr_and_back_coeffs(util_ycbcr_bt2020_coeffs); +} + +TEST(u_ycbcr_test, full_range) +{ + float range[3][2]; + unsigned bpc[3] = {8, 8, 8}; + util_get_full_range_coeffs(range, bpc); + + const struct { + uint8_t input[3]; + float expected[3]; + } test_data[] = { { + { 0, 128, 128 }, + { 0.0f, 0.0f, 0.0f }, + }, { + { 255, 128, 128 }, + { 1.0f, 0.0f, 0.0f }, + }, { + { 0, 0, 255 }, + { 0.0f, -128.0f / 255, 127.0f / 255 }, + }, { + { 255, 255, 0 }, + { 1.0f, 127.0f / 255, -128.0f / 255 }, + } + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) { + float input[3] = { + test_data[i].input[0] / 255.0f, + test_data[i].input[1] / 255.0f, + test_data[i].input[2] / 255.0f, + }; + + float result[3]; + for (int c = 0; c < 3; ++c) + result[c] = input[c] * range[c][0] + range[c][1]; + + EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7); + EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7); + EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7); + } +} + +TEST(u_ycbcr_test, narrow_range) +{ + float range[3][2]; + unsigned bpc[3] = {8, 8, 8}; + util_get_narrow_range_coeffs(range, bpc); + + const struct { + uint8_t input[3]; + float expected[3]; + } test_data[] = { { + { 16, 128, 128 }, + { 0.0f, 0.0f, 0.0f }, + }, { + { 235, 128, 128 }, + { 1.0f, 0.0f, 0.0f }, + }, { + { 16, 16, 240 }, + { 0.0f, -0.5f, 0.5f }, + }, { + { 235, 240, 16 }, + { 1.0f, 0.5f, -0.5f }, + } + }; + + for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) { + float input[3] = { + test_data[i].input[0] / 255.0f, + test_data[i].input[1] / 255.0f, + test_data[i].input[2] / 255.0f, + }; + + float result[3]; + for (int c = 0; c < 3; ++c) + result[c] = input[c] * range[c][0] + range[c][1]; + + EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-7); + EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-7); + EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-7); + } +} + +static void +test_to_rgb_narrow_range_coeffs(const float coeffs[3]) +{ + const unsigned bpc[3] = {8, 8, 10}; + float range[3][2]; + util_get_narrow_range_coeffs(range, bpc); + + float mat[3][4]; + util_get_ycbcr_to_rgb_matrix(mat, coeffs); + util_ycbcr_adjust_from_range(mat, range); + + float a = coeffs[0]; + float b = coeffs[1]; + float c = coeffs[2]; + float d = 2 - 2 * c; + float e = 2 - 2 * a; + + const struct { + float input[3]; + float expected[3]; + } test_data[] = { { + { 1.0f, 0.0f, 0.0f}, + { 1.0f, 1.0f, 1.0f }, + }, { + { 0.0f, 0.0f, 0.0f}, + { 0.0f, 0.0f, 0.0f }, + }, { + { 0.5f, 0.0f, 0.0f }, + { 0.5f, 0.5f, 0.5f }, + }, { + { a, -a / d, 0.5f }, + { 1.0f, 0.0f, 0.0f }, + }, { + { b, -b / d, -b / e }, + { 0.0f, 1.0f, 0.0f }, + }, { + { c, 0.5f, -c / e }, + { 0.0f, 0.0f, 1.0f }, + }, { + { 1 - c, -0.5f, c / e }, + { 1.0f, 1.0f, 0.0f }, + }, { + { 1 - a, a / d, (a - 1) / e }, + { 0.0f, 1.0f, 1.0f }, + }, { + { 1 - b, b / d, b / e }, + { 1.0f, 0.0f, 1.0f }, + } + }; + for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) { + float input[3] = { + (16 + test_data[i].input[0] * (235 - 16)) / 255.0f, + (16 + (test_data[i].input[1] + 0.5f) * (240 - 16)) / 255.0f, + (16 + (test_data[i].input[2] + 0.5f) * (240 - 16)) / 255.75f, + }; + + float result[3]; + for (int c = 0; c < 3; ++c) { + result[c] = input[0] * mat[c][0] + + input[1] * mat[c][1] + + input[2] * mat[c][2] + + mat[c][3]; + + } + + EXPECT_NEAR(test_data[i].expected[0], result[0], 1e-6); + EXPECT_NEAR(test_data[i].expected[1], result[1], 1e-6); + EXPECT_NEAR(test_data[i].expected[2], result[2], 1e-6); + } +} + +TEST(u_ycbcr_test, bt601_to_rgb_narrow_range) +{ + test_to_rgb_narrow_range_coeffs(util_ycbcr_bt601_coeffs); + test_to_rgb_narrow_range_coeffs(util_ycbcr_bt709_coeffs); + test_to_rgb_narrow_range_coeffs(util_ycbcr_bt2020_coeffs); +} + + +static void +test_to_ycbcr_narrow_range_coeffs(const float coeffs[3]) +{ + const unsigned bpc[3] = {8, 8, 10}; + + float range[3][2]; + util_get_narrow_range_coeffs(range, bpc); + + float mat[3][4]; + util_get_rgb_to_ycbcr_matrix(mat, coeffs); + util_ycbcr_adjust_to_range(mat, range); + + float a = coeffs[0]; + float b = coeffs[1]; + float c = coeffs[2]; + float d = 2 - 2 * c; + float e = 2 - 2 * a; + + const struct { + float input[3]; + float expected[3]; + } test_data[] = { { + { 1.0f, 1.0f, 1.0f }, + { 1.0f, 0.0f, 0.0f}, + }, { + { 0.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f}, + }, { + { 0.5f, 0.5f, 0.5f }, + { 0.5f, 0.0f, 0.0f }, + }, { + { 1.0f, 0.0f, 0.0f }, + { a, -a / d, 0.5f }, + }, { + { 0.0f, 1.0f, 0.0f }, + { b, -b / d, -b / e }, + }, { + { 0.0f, 0.0f, 1.0f }, + { c, 0.5f, -c / e }, + }, { + { 1.0f, 1.0f, 0.0f }, + { 1 - c, -0.5f, c / e }, + }, { + { 0.0f, 1.0f, 1.0f }, + { 1 - a, a / d, (a - 1) / e }, + }, { + { 1.0f, 0.0f, 1.0f }, + { 1 - b, b / d, b / e }, + } + }; + for (size_t i = 0; i < ARRAY_SIZE(test_data); ++i) { + float result[3]; + for (int c = 0; c < 3; ++c) { + result[c] = test_data[i].input[0] * mat[c][0] + + test_data[i].input[1] * mat[c][1] + + test_data[i].input[2] * mat[c][2] + + mat[c][3]; + } + + float expected[3] = { + (16 + test_data[i].expected[0] * (235 - 16)) / 255.0f, + (16 + (test_data[i].expected[1] + 0.5f) * (240 - 16)) / 255.0f, + (16 + (test_data[i].expected[2] + 0.5f) * (240 - 16)) / 255.75f, + }; + + EXPECT_NEAR(expected[0], result[0], 1e-6); + EXPECT_NEAR(expected[1], result[1], 1e-6); + EXPECT_NEAR(expected[2], result[2], 1e-6); + } +} + +TEST(u_ycbcr_test, bt601_to_ycbcr_narrow_range) +{ + test_to_ycbcr_narrow_range_coeffs(util_ycbcr_bt601_coeffs); + test_to_ycbcr_narrow_range_coeffs(util_ycbcr_bt709_coeffs); + test_to_ycbcr_narrow_range_coeffs(util_ycbcr_bt2020_coeffs); +} + +static void +test_to_ycbcr_range_and_back_coeffs(const float coeffs[3]) +{ + const unsigned bpc[3] = {8, 8, 10}; + + float range[3][2]; + util_get_narrow_range_coeffs(range, bpc); + + float to_ycbcr[3][4]; + util_get_rgb_to_ycbcr_matrix(to_ycbcr, coeffs); + util_ycbcr_adjust_to_range(to_ycbcr, range); + + float to_rgb[3][4]; + util_get_ycbcr_to_rgb_matrix(to_rgb, coeffs); + util_ycbcr_adjust_from_range(to_rgb, range); + + float inputs[][3] = { + { 1.0f, 1.0f, 1.0f }, + { 0.0f, 0.0f, 0.0f }, + { 0.5f, 0.5f, 0.5f }, + { 1.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f }, + { 1.0f, 1.0f, 0.0f }, + { 0.0f, 1.0f, 1.0f }, + { 1.0f, 0.0f, 1.0f }, + }; + + for (size_t i = 0; i < ARRAY_SIZE(inputs); ++i) { + float ycbcr[3]; + for (int c = 0; c < 3; ++c) { + ycbcr[c] = inputs[i][0] * to_ycbcr[c][0] + + inputs[i][1] * to_ycbcr[c][1] + + inputs[i][2] * to_ycbcr[c][2]; + + } + + float result[3]; + for (int c = 0; c < 3; ++c) { + result[c] = ycbcr[0] * to_rgb[c][0] + + ycbcr[1] * to_rgb[c][1] + + ycbcr[2] * to_rgb[c][2]; + + } + + EXPECT_NEAR(inputs[i][0], result[0], 1e-6); + EXPECT_NEAR(inputs[i][1], result[1], 1e-6); + EXPECT_NEAR(inputs[i][2], result[2], 1e-6); + } +} + +TEST(u_ycbcr_test, to_ycbcr_range_and_back) +{ + test_to_ycbcr_range_and_back_coeffs(util_ycbcr_bt601_coeffs); + test_to_ycbcr_range_and_back_coeffs(util_ycbcr_bt709_coeffs); + test_to_ycbcr_range_and_back_coeffs(util_ycbcr_bt2020_coeffs); +} diff --git a/src/util/u_ycbcr.h b/src/util/u_ycbcr.h new file mode 100644 index 00000000000..448da7f12d0 --- /dev/null +++ b/src/util/u_ycbcr.h @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2026 Collabora Ltd. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef U_YCBCR_H +#define U_YCBCR_H + +/* BT.601 coefficients */ +static const float util_ycbcr_bt601_coeffs[3] = { + 0.299f, 0.587f, 0.114f +}; + +/* BT.701 coefficients */ +static const float util_ycbcr_bt709_coeffs[3] = { + 0.2126f, 0.7152f, 0.0722f +}; + +/* BT.2020 coefficients */ +static const float util_ycbcr_bt2020_coeffs[3] = { + 0.2627f, 0.6780f, 0.0593f +}; + +/* SMPTE 240M coefficients */ +static const float util_ycbcr_smpte240m_coeffs[3] = { + 0.2122f, 0.7013f, 0.0865f +}; + +static inline void +util_get_ycbcr_to_rgb_matrix(float m[3][4], const float coeffs[3]) +{ + /** + * Sets up a 3x4 matrix that computes: + * + * R = Y + e * Cr + * G = Y - (a * e / b) * Cr - (c * d / b) * Cb + * B = Y + d * Cb + */ + + float a = coeffs[0]; + float b = coeffs[1]; + float c = coeffs[2]; + float d = 2 - 2 * c; + float e = 2 - 2 * a; + float f = 1.0f / b; + + m[0][0] = 1; m[0][1] = 0; m[0][2] = e; m[0][3] = 0; + m[1][0] = 1; m[1][1] = -c * d * f; m[1][2] = -a * e * f; m[1][3] = 0; + m[2][0] = 1; m[2][1] = d; m[2][2] = 0; m[2][3] = 0; +} + +static inline void +util_get_rgb_to_ycbcr_matrix(float m[3][4], const float coeffs[3]) +{ + /** + * Sets up a 3x4 matrix that computes: + * + * Y = a * R + b * G + c * B + * Cb = (B - Y) / d + * Cr = (R - Y) / e + */ + + float a = coeffs[0]; + float b = coeffs[1]; + float c = coeffs[2]; + float d = 0.5f / (c - 1); + float e = 0.5f / (a - 1); + + m[0][0] = a; m[0][1] = b; m[0][2] = c; m[0][3] = 0; + m[1][0] = d * a; m[1][1] = d * b; m[1][2] = 0.5f; m[1][3] = 0; + m[2][0] = 0.5f; m[2][1] = e * b; m[2][2] = e * c; m[2][3] = 0; +} + +static inline float +util_get_full_range_chroma_bias(unsigned bpc) +{ + return -(1 << (bpc - 1)) / ((1 << bpc) - 1.0f); +} + +static inline void +util_get_full_range_coeffs(float out[3][2], const unsigned bpc[3]) +{ + out[0][0] = 1; out[0][1] = 0; + out[1][0] = 1; out[1][1] = util_get_full_range_chroma_bias(bpc[1]); + out[2][0] = 1; out[2][1] = util_get_full_range_chroma_bias(bpc[2]); +} + +static inline float +util_get_narrow_range(unsigned bpc) +{ + return 1 - 1.0f / (1 << bpc); +} + +static inline float +util_get_narrow_range_luma_factor(unsigned bpc) +{ + return util_get_narrow_range(bpc) * (256.0f / 219); +} + +static inline float +util_get_narrow_range_chroma_factor(unsigned bpc) +{ + return util_get_narrow_range(bpc) * (256.0f / 224); +} + +static inline void +util_get_narrow_range_coeffs(float out[3][2], const unsigned bpc[3]) +{ + float y_factor = util_get_narrow_range_luma_factor(bpc[0]); + float cb_factor = util_get_narrow_range_chroma_factor(bpc[1]); + float cr_factor = util_get_narrow_range_chroma_factor(bpc[2]); + + float y_bias = -16.0f / 219; + float c_bias = -128.0f / 224; + + out[0][0] = y_factor; out[0][1] = y_bias; + out[1][0] = cb_factor; out[1][1] = c_bias; + out[2][0] = cr_factor; out[2][1] = c_bias; +} + +static inline void +util_get_identity_range_coeffs(float out[3][2]) +{ + out[0][0] = out[1][0] = out[2][0] = 1.0f; + out[0][1] = out[1][1] = out[2][1] = 0.0f; +} + +static inline void +util_ycbcr_adjust_from_range(float mat[3][4], + const float range[3][2]) +{ + for (int i = 0; i < 3; ++i) { + mat[i][3] = range[0][1] * mat[i][0] + + range[1][1] * mat[i][1] + + range[2][1] * mat[i][2] + + mat[i][3]; + + mat[i][0] = mat[i][0] * range[0][0]; + mat[i][1] = mat[i][1] * range[1][0]; + mat[i][2] = mat[i][2] * range[2][0]; + } +} + +static inline void +util_ycbcr_adjust_to_range(float mat[3][4], + const float range[3][2]) +{ + for (int i = 0; i < 3; ++i) { + float tmp = 1.0f / range[i][0]; + mat[i][0] = mat[i][0] * tmp; + mat[i][1] = mat[i][1] * tmp; + mat[i][2] = mat[i][2] * tmp; + mat[i][3] -= range[i][1] * tmp; + } +} + +#endif /* U_YCBCR_H */