Skip to content

Instantly share code, notes, and snippets.

@curioustorvald
Last active August 26, 2025 11:51
Show Gist options
  • Select an option

  • Save curioustorvald/d651193eb24e10c2d4c096010af18c03 to your computer and use it in GitHub Desktop.

Select an option

Save curioustorvald/d651193eb24e10c2d4c096010af18c03 to your computer and use it in GitHub Desktop.
sRGB to XYB conversion
// 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