Last active
August 26, 2025 11:51
-
-
Save curioustorvald/d651193eb24e10c2d4c096010af18c03 to your computer and use it in GitHub Desktop.
sRGB to XYB conversion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // XYB Color Space Conversion Functions | |
| // Based on JPEG XL XYB specification with proper sRGB linearization | |
| // test with: | |
| //// gcc -DXYB_TEST_MAIN -o test_xyb xyb_conversion.c -lm && ./test_xyb | |
| #include <stdio.h> | |
| #include <math.h> | |
| #include <stdint.h> | |
| #include <stdlib.h> | |
| #define CLAMP(x, min, max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x))) | |
| // XYB conversion constants from JPEG XL specification | |
| static const double XYB_BIAS = 0.00379307325527544933; | |
| static const double CBRT_BIAS = 0.155954200549248620; // cbrt(XYB_BIAS) | |
| // RGB to LMS mixing coefficients | |
| static const double RGB_TO_LMS[3][3] = { | |
| {0.3, 0.622, 0.078}, // L coefficients | |
| {0.23, 0.692, 0.078}, // M coefficients | |
| {0.24342268924547819, 0.20476744424496821, 0.55180986650955360} // S coefficients | |
| }; | |
| // LMS to RGB inverse matrix (calculated via matrix inversion) | |
| static const double LMS_TO_RGB[3][3] = { | |
| {11.0315669046, -9.8669439081, -0.1646229965}, | |
| {-3.2541473811, 4.4187703776, -0.1646229965}, | |
| {-3.6588512867, 2.7129230459, 1.9459282408} | |
| }; | |
| // sRGB linearization (0..1 range) | |
| static inline double srgb_linearize(double val) { | |
| if (val > 0.04045) { | |
| return pow((val + 0.055) / 1.055, 2.4); | |
| } else { | |
| return val / 12.92; | |
| } | |
| } | |
| // sRGB unlinearization (0..1 range) | |
| static inline double srgb_unlinearize(double val) { | |
| if (val > 0.0031308) { | |
| return 1.055 * pow(val, 1.0 / 2.4) - 0.055; | |
| } else { | |
| return val * 12.92; | |
| } | |
| } | |
| // Fast cube root approximation for performance | |
| static inline double fast_cbrt(double x) { | |
| if (x < 0) return -cbrt(-x); | |
| return cbrt(x); | |
| } | |
| // RGB to XYB conversion with proper sRGB linearization | |
| void rgb_to_xyb(uint8_t r, uint8_t g, uint8_t b, double *x, double *y, double *xyb_b) { | |
| // Convert RGB to 0-1 range and linearize sRGB | |
| double r_norm = srgb_linearize(r / 255.0); | |
| double g_norm = srgb_linearize(g / 255.0); | |
| double b_norm = srgb_linearize(b / 255.0); | |
| // RGB to LMS mixing with bias | |
| double lmix = RGB_TO_LMS[0][0] * r_norm + RGB_TO_LMS[0][1] * g_norm + RGB_TO_LMS[0][2] * b_norm + XYB_BIAS; | |
| double mmix = RGB_TO_LMS[1][0] * r_norm + RGB_TO_LMS[1][1] * g_norm + RGB_TO_LMS[1][2] * b_norm + XYB_BIAS; | |
| double smix = RGB_TO_LMS[2][0] * r_norm + RGB_TO_LMS[2][1] * g_norm + RGB_TO_LMS[2][2] * b_norm + XYB_BIAS; | |
| // Apply gamma correction (cube root) | |
| double lgamma = fast_cbrt(lmix) - CBRT_BIAS; | |
| double mgamma = fast_cbrt(mmix) - CBRT_BIAS; | |
| double sgamma = fast_cbrt(smix) - CBRT_BIAS; | |
| // LMS to XYB transformation | |
| *x = (lgamma - mgamma) / 2.0; | |
| *y = (lgamma + mgamma) / 2.0; | |
| *xyb_b = sgamma; | |
| } | |
| // XYB to RGB conversion with proper sRGB unlinearization | |
| void xyb_to_rgb(double x, double y, double xyb_b, uint8_t *r, uint8_t *g, uint8_t *b) { | |
| // XYB to LMS gamma | |
| double lgamma = x + y; | |
| double mgamma = y - x; | |
| double sgamma = xyb_b; | |
| // Remove gamma correction | |
| double lmix = pow(lgamma + CBRT_BIAS, 3.0) - XYB_BIAS; | |
| double mmix = pow(mgamma + CBRT_BIAS, 3.0) - XYB_BIAS; | |
| double smix = pow(sgamma + CBRT_BIAS, 3.0) - XYB_BIAS; | |
| // LMS to linear RGB using inverse matrix | |
| double r_linear = LMS_TO_RGB[0][0] * lmix + LMS_TO_RGB[0][1] * mmix + LMS_TO_RGB[0][2] * smix; | |
| double g_linear = LMS_TO_RGB[1][0] * lmix + LMS_TO_RGB[1][1] * mmix + LMS_TO_RGB[1][2] * smix; | |
| double b_linear = LMS_TO_RGB[2][0] * lmix + LMS_TO_RGB[2][1] * mmix + LMS_TO_RGB[2][2] * smix; | |
| // Clamp linear RGB to valid range | |
| r_linear = CLAMP(r_linear, 0.0, 1.0); | |
| g_linear = CLAMP(g_linear, 0.0, 1.0); | |
| b_linear = CLAMP(b_linear, 0.0, 1.0); | |
| // Convert back to sRGB gamma and 0-255 range | |
| *r = CLAMP((int)(srgb_unlinearize(r_linear) * 255.0 + 0.5), 0, 255); | |
| *g = CLAMP((int)(srgb_unlinearize(g_linear) * 255.0 + 0.5), 0, 255); | |
| *b = CLAMP((int)(srgb_unlinearize(b_linear) * 255.0 + 0.5), 0, 255); | |
| } | |
| // Test function to verify conversion accuracy | |
| int test_xyb_conversion() { | |
| printf("Testing XYB conversion accuracy with sRGB linearization...\n"); | |
| // Test with various RGB values | |
| uint8_t test_colors[][3] = { | |
| {255, 0, 0}, // Red | |
| {0, 255, 0}, // Green | |
| {0, 0, 255}, // Blue | |
| {255, 255, 255}, // White | |
| {0, 0, 0}, // Black | |
| {128, 128, 128}, // Gray | |
| {255, 255, 0}, // Yellow | |
| {255, 0, 255}, // Magenta | |
| {0, 255, 255}, // Cyan | |
| // MacBeth chart colours converted to sRGB | |
| {0x73,0x52,0x44}, | |
| {0xc2,0x96,0x82}, | |
| {0x62,0x7a,0x9d}, | |
| {0x57,0x6c,0x43}, | |
| {0x85,0x80,0xb1}, | |
| {0x67,0xbd,0xaa}, | |
| {0xd6,0x7e,0x2c}, | |
| {0x50,0x5b,0xa6}, | |
| {0xc1,0x5a,0x63}, | |
| {0x5e,0x3c,0x6c}, | |
| {0x9d,0xbc,0x40}, | |
| {0xe0,0xa3,0x2e}, | |
| {0x38,0x3d,0x96}, | |
| {0x46,0x94,0x49}, | |
| {0xaf,0x36,0x3c}, | |
| {0xe7,0xc7,0x1f}, | |
| {0xbb,0x56,0x95}, | |
| {0x08,0x85,0xa1}, | |
| {0xf3,0xf3,0xf3}, | |
| {0xc8,0xc8,0xc8}, | |
| {0xa0,0xa0,0xa0}, | |
| {0x7a,0x7a,0x7a}, | |
| {0x55,0x55,0x55}, | |
| {0x34,0x34,0x34} | |
| }; | |
| int num_tests = sizeof(test_colors) / sizeof(test_colors[0]); | |
| int errors = 0; | |
| for (int i = 0; i < num_tests; i++) { | |
| uint8_t r_orig = test_colors[i][0]; | |
| uint8_t g_orig = test_colors[i][1]; | |
| uint8_t b_orig = test_colors[i][2]; | |
| double x, y, xyb_b; | |
| uint8_t r_conv, g_conv, b_conv; | |
| // Forward and reverse conversion | |
| rgb_to_xyb(r_orig, g_orig, b_orig, &x, &y, &xyb_b); | |
| xyb_to_rgb(x, y, xyb_b, &r_conv, &g_conv, &b_conv); | |
| // Check accuracy (allow small rounding errors) | |
| int r_error = abs((int)r_orig - (int)r_conv); | |
| int g_error = abs((int)g_orig - (int)g_conv); | |
| int b_error = abs((int)b_orig - (int)b_conv); | |
| printf("RGB(%3d,%3d,%3d) -> XYB(%6.3f,%6.3f,%6.3f) -> RGB(%3d,%3d,%3d) [Error: %d,%d,%d]\n", | |
| r_orig, g_orig, b_orig, x, y, xyb_b, r_conv, g_conv, b_conv, r_error, g_error, b_error); | |
| if (r_error > 2 || g_error > 2 || b_error > 2) { | |
| errors++; | |
| } | |
| } | |
| printf("Test completed: %d/%d passed\n", num_tests - errors, num_tests); | |
| return errors == 0; | |
| } | |
| #ifdef XYB_TEST_MAIN | |
| int main() { | |
| return test_xyb_conversion() ? 0 : 1; | |
| } | |
| #endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment