Colorspaces.cpp (13460B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // We are going to be doing so, so many transforms, so descriptive labels are 6 // critical. 7 8 #include "Colorspaces.h" 9 10 #include "nsDebug.h" 11 #include "qcms.h" 12 13 namespace mozilla::color { 14 15 // tf = { k * linear | linear < b 16 // { a * pow(linear, 1/g) - (1-a) | linear >= b 17 float TfFromLinear(const PiecewiseGammaDesc& desc, const float linear) { 18 if (linear < desc.b) { 19 return linear * desc.k; 20 } 21 float ret = linear; 22 ret = powf(ret, 1.0f / desc.g); 23 ret *= desc.a; 24 ret -= (desc.a - 1); 25 return ret; 26 } 27 28 float LinearFromTf(const PiecewiseGammaDesc& desc, const float tf) { 29 const auto linear_if_low = tf / desc.k; 30 if (linear_if_low < desc.b) { 31 return linear_if_low; 32 } 33 float ret = tf; 34 ret += (desc.a - 1); 35 ret /= desc.a; 36 ret = powf(ret, 1.0f * desc.g); 37 return ret; 38 } 39 40 // - 41 42 mat3 YuvFromRgb(const YuvLumaCoeffs& yc) { 43 // Y is always [0,1] 44 // U and V are signed, and could be either [-1,+1] or [-0.5,+0.5]. 45 // Specs generally use [-0.5,+0.5], so we use that too. 46 // E.g. 47 // y = 0.2126*r + 0.7152*g + 0.0722*b 48 // u = (b - y) / (u_range = u_max - u_min) // u_min = -u_max 49 // = (b - y) / (u(0,0,1) - u(1,1,0)) 50 // = (b - y) / (2 * u(0,0,1)) 51 // = (b - y) / (2 * u.b)) 52 // = (b - y) / (2 * (1 - 0.0722)) 53 // = (-0.2126*r + -0.7152*g + (1-0.0722)*b) / 1.8556 54 // v = (r - y) / 1.5748; 55 // = ((1-0.2126)*r + -0.7152*g + -0.0722*b) / 1.5748 56 const auto y = vec3({yc.r, yc.g, yc.b}); 57 const auto u = vec3({0, 0, 1}) - y; 58 const auto v = vec3({1, 0, 0}) - y; 59 60 // From rows: 61 return mat3({y, u / (2 * u.z()), v / (2 * v.x())}); 62 } 63 64 mat4 YuvFromYcbcr(const YcbcrDesc& d) { 65 // E.g. 66 // y = (yy - 16) / (235 - 16); // 16->0, 235->1 67 // u = (cb - 128) / (240 - 16); // 16->-0.5, 128->0, 240->+0.5 68 // v = (cr - 128) / (240 - 16); 69 70 const auto yRange = d.y1 - d.y0; 71 const auto uHalfRange = d.uPlusHalf - d.u0; 72 const auto uRange = 2 * uHalfRange; 73 74 const auto ycbcrFromYuv = mat4{{vec4{{yRange, 0, 0, d.y0}}, 75 {{0, uRange, 0, d.u0}}, 76 {{0, 0, uRange, d.u0}}, 77 {{0, 0, 0, 1}}}}; 78 const auto yuvFromYcbcr = inverse(ycbcrFromYuv); 79 return yuvFromYcbcr; 80 } 81 82 inline vec3 CIEXYZ_from_CIExyY(const vec2 xy, const float Y = 1) { 83 const auto xyz = vec3(xy, 1 - xy.x() - xy.y()); 84 const auto XYZ = xyz * (Y / xy.y()); 85 return XYZ; 86 } 87 88 mat3 XyzFromLinearRgb(const Chromaticities& c) { 89 // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html 90 91 // Given red (xr, yr), green (xg, yg), blue (xb, yb), 92 // and whitepoint (XW, YW, ZW) 93 94 // [ X ] [ R ] 95 // [ Y ] = M x [ G ] 96 // [ Z ] [ B ] 97 98 // [ Sr*Xr Sg*Xg Sb*Xb ] 99 // M = [ Sr*Yr Sg*Yg Sb*Yb ] 100 // [ Sr*Zr Sg*Zg Sb*Zb ] 101 102 // Xr = xr / yr 103 // Yr = 1 104 // Zr = (1 - xr - yr) / yr 105 106 // Xg = xg / yg 107 // Yg = 1 108 // Zg = (1 - xg - yg) / yg 109 110 // Xb = xb / yb 111 // Yb = 1 112 // Zb = (1 - xb - yb) / yb 113 114 // [ Sr ] [ Xr Xg Xb ]^-1 [ XW ] 115 // [ Sg ] = [ Yr Yg Yb ] x [ YW ] 116 // [ Sb ] [ Zr Zg Zb ] [ ZW ] 117 118 const auto xrgb = vec3({c.rx, c.gx, c.bx}); 119 const auto yrgb = vec3({c.ry, c.gy, c.by}); 120 121 const auto Xrgb = xrgb / yrgb; 122 const auto Yrgb = vec3(1); 123 const auto Zrgb = (vec3(1) - xrgb - yrgb) / yrgb; 124 125 const auto XYZrgb = mat3({Xrgb, Yrgb, Zrgb}); 126 const auto XYZrgb_inv = inverse(XYZrgb); 127 const auto XYZwhitepoint = vec3({c.wx, c.wy, 1 - c.wx - c.wy}) / c.wy; 128 const auto Srgb = XYZrgb_inv * XYZwhitepoint; 129 130 const auto M = mat3({Srgb * Xrgb, Srgb * Yrgb, Srgb * Zrgb}); 131 return M; 132 } 133 134 // - 135 ColorspaceTransform ColorspaceTransform::Create(const ColorspaceDesc& src, 136 const ColorspaceDesc& dst) { 137 auto ct = ColorspaceTransform{src, dst}; 138 ct.srcTf = src.tf; 139 ct.dstTf = dst.tf; 140 141 const auto RgbTfFrom = [&](const ColorspaceDesc& cs) { 142 auto rgbFrom = mat4::Identity(); 143 if (cs.yuv) { 144 const auto yuvFromYcbcr = YuvFromYcbcr(cs.yuv->ycbcr); 145 const auto yuvFromRgb = YuvFromRgb(cs.yuv->yCoeffs); 146 const auto rgbFromYuv = inverse(yuvFromRgb); 147 const auto rgbFromYuv4 = mat4(rgbFromYuv); 148 149 const auto rgbFromYcbcr = rgbFromYuv4 * yuvFromYcbcr; 150 rgbFrom = rgbFromYcbcr; 151 } 152 return rgbFrom; 153 }; 154 155 ct.srcRgbTfFromSrc = RgbTfFrom(src); 156 const auto dstRgbTfFromDst = RgbTfFrom(dst); 157 ct.dstFromDstRgbTf = inverse(dstRgbTfFromDst); 158 159 // - 160 161 ct.dstRgbLinFromSrcRgbLin = mat3::Identity(); 162 if (!(src.chrom == dst.chrom)) { 163 const auto xyzFromSrcRgbLin = XyzFromLinearRgb(src.chrom); 164 const auto xyzFromDstRgbLin = XyzFromLinearRgb(dst.chrom); 165 const auto dstRgbLinFromXyz = inverse(xyzFromDstRgbLin); 166 ct.dstRgbLinFromSrcRgbLin = dstRgbLinFromXyz * xyzFromSrcRgbLin; 167 } 168 169 return ct; 170 } 171 172 vec3 ColorspaceTransform::DstFromSrc(const vec3 src) const { 173 const auto srcRgbTf = srcRgbTfFromSrc * vec4(src, 1); 174 auto srcRgbLin = srcRgbTf; 175 if (srcTf) { 176 srcRgbLin.x(LinearFromTf(*srcTf, srcRgbTf.x())); 177 srcRgbLin.y(LinearFromTf(*srcTf, srcRgbTf.y())); 178 srcRgbLin.z(LinearFromTf(*srcTf, srcRgbTf.z())); 179 } 180 181 const auto dstRgbLin = dstRgbLinFromSrcRgbLin * vec3(srcRgbLin); 182 auto dstRgbTf = dstRgbLin; 183 if (dstTf) { 184 dstRgbTf.x(TfFromLinear(*dstTf, dstRgbLin.x())); 185 dstRgbTf.y(TfFromLinear(*dstTf, dstRgbLin.y())); 186 dstRgbTf.z(TfFromLinear(*dstTf, dstRgbLin.z())); 187 } 188 189 const auto dst4 = dstFromDstRgbTf * vec4(dstRgbTf, 1); 190 return vec3(dst4); 191 } 192 193 // - 194 195 mat3 XyzAFromXyzB_BradfordLinear(const vec2 xyA, const vec2 xyB) { 196 // This is what ICC profiles use to do whitepoint transforms, 197 // because ICC also requires D50 for the Profile Connection Space. 198 199 // From https://www.color.org/specification/ICC.1-2022-05.pdf 200 // E.3 "Linearized Bradford transformation": 201 202 const auto M_BFD = mat3{{ 203 vec3{{0.8951, 0.2664f, -0.1614f}}, 204 vec3{{-0.7502f, 1.7135f, 0.0367f}}, 205 vec3{{0.0389f, -0.0685f, 1.0296f}}, 206 }}; 207 // NB: They use rho/gamma/beta, but we'll use R/G/B here. 208 const auto XYZDst = CIEXYZ_from_CIExyY(xyA); // "XYZ_W", WP of PCS 209 const auto XYZSrc = CIEXYZ_from_CIExyY(xyB); // "XYZ_NAW", WP of src 210 const auto rgbSrc = M_BFD * XYZSrc; // "RGB_SRC" 211 const auto rgbDst = M_BFD * XYZDst; // "RGB_PCS" 212 const auto rgbDstOverSrc = rgbDst / rgbSrc; 213 const auto M_dstOverSrc = mat3::Scale(rgbDstOverSrc); 214 const auto M_adapt = inverse(M_BFD) * M_dstOverSrc * M_BFD; 215 return M_adapt; 216 } 217 218 std::optional<mat4> ColorspaceTransform::ToMat4() const { 219 mat4 fromSrc = srcRgbTfFromSrc; 220 if (srcTf) return {}; 221 fromSrc = mat4(dstRgbLinFromSrcRgbLin) * fromSrc; 222 if (dstTf) return {}; 223 fromSrc = dstFromDstRgbTf * fromSrc; 224 return fromSrc; 225 } 226 227 Lut3 ColorspaceTransform::ToLut3(const ivec3 size) const { 228 auto lut = Lut3::Create(size); 229 lut.SetMap([&](const vec3& srcVal) { return DstFromSrc(srcVal); }); 230 return lut; 231 } 232 233 vec3 Lut3::Sample(const vec3 in01) const { 234 const auto coord = vec3(size - 1) * in01; 235 const auto p0 = floor(coord); 236 const auto dp = coord - p0; 237 const auto ip0 = ivec3(p0); 238 239 // Trilinear 240 const auto f000 = Fetch(ip0 + ivec3({0, 0, 0})); 241 const auto f100 = Fetch(ip0 + ivec3({1, 0, 0})); 242 const auto f010 = Fetch(ip0 + ivec3({0, 1, 0})); 243 const auto f110 = Fetch(ip0 + ivec3({1, 1, 0})); 244 const auto f001 = Fetch(ip0 + ivec3({0, 0, 1})); 245 const auto f101 = Fetch(ip0 + ivec3({1, 0, 1})); 246 const auto f011 = Fetch(ip0 + ivec3({0, 1, 1})); 247 const auto f111 = Fetch(ip0 + ivec3({1, 1, 1})); 248 249 const auto fx00 = mix(f000, f100, dp.x()); 250 const auto fx10 = mix(f010, f110, dp.x()); 251 const auto fx01 = mix(f001, f101, dp.x()); 252 const auto fx11 = mix(f011, f111, dp.x()); 253 254 const auto fxy0 = mix(fx00, fx10, dp.y()); 255 const auto fxy1 = mix(fx01, fx11, dp.y()); 256 257 const auto fxyz = mix(fxy0, fxy1, dp.z()); 258 return fxyz; 259 } 260 261 // - 262 263 ColorProfileDesc ColorProfileDesc::From(const ColorspaceDesc& cspace) { 264 auto ret = ColorProfileDesc{}; 265 266 if (cspace.yuv) { 267 const auto yuvFromYcbcr = YuvFromYcbcr(cspace.yuv->ycbcr); 268 const auto yuvFromRgb = YuvFromRgb(cspace.yuv->yCoeffs); 269 const auto rgbFromYuv = inverse(yuvFromRgb); 270 ret.rgbFromYcbcr = mat4(rgbFromYuv) * yuvFromYcbcr; 271 } 272 273 if (cspace.tf) { 274 const size_t tableSize = 256; 275 auto& tableR = ret.linearFromTf.r; 276 tableR.resize(tableSize); 277 for (size_t i = 0; i < tableR.size(); i++) { 278 const float tfVal = i / float(tableR.size() - 1); 279 const float linearVal = LinearFromTf(*cspace.tf, tfVal); 280 tableR[i] = linearVal; 281 } 282 ret.linearFromTf.g = tableR; 283 ret.linearFromTf.b = tableR; 284 } 285 286 ret.xyzd65FromLinearRgb = XyzFromLinearRgb(cspace.chrom); 287 288 return ret; 289 } 290 291 // - 292 293 template <class T> 294 constexpr inline T NewtonEstimateX(const T x1, const T y1, const T dydx, 295 const T y2 = 0) { 296 // Estimate x s.t. y=0 297 // y = y0 + x*dydx; 298 // y0 = y - x*dydx; 299 // y1 - x1*dydx = y2 - x2*dydx 300 // x2*dydx = y2 - y1 + x1*dydx 301 // x2 = (y2 - y1)/dydx + x1 302 return (y2 - y1) / dydx + x1; 303 } 304 305 float GuessGamma(const std::vector<float>& vals, float exp_guess) { 306 // Approximate (signed) error = 0.0. 307 constexpr float d_exp = 0.001; 308 constexpr float error_tolerance = 0.001; 309 struct Samples { 310 float y1, y2; 311 }; 312 const auto Sample = [&](const float exp) { 313 int i = -1; 314 auto samples = Samples{}; 315 for (const auto& expected : vals) { 316 i += 1; 317 const auto in = i / float(vals.size() - 1); 318 samples.y1 += powf(in, exp) - expected; 319 samples.y2 += powf(in, exp + d_exp) - expected; 320 } 321 samples.y1 /= vals.size(); // Normalize by val count. 322 samples.y2 /= vals.size(); 323 return samples; 324 }; 325 constexpr int MAX_ITERS = 10; 326 for (int i = 1;; i++) { 327 const auto err = Sample(exp_guess); 328 const auto derr = err.y2 - err.y1; 329 exp_guess = NewtonEstimateX(exp_guess, err.y1, derr / d_exp); 330 // Check if we were close before, because then this last round of estimation 331 // should get us pretty much right on it. 332 if (std::abs(err.y1) < error_tolerance) { 333 return exp_guess; 334 } 335 if (i >= MAX_ITERS) { 336 printf_stderr("GuessGamma() -> %f after %i iterations (avg err %f)\n", 337 exp_guess, i, err.y1); 338 MOZ_ASSERT(false, "GuessGamma failed."); 339 return exp_guess; 340 } 341 } 342 } 343 344 // - 345 346 ColorProfileDesc ColorProfileDesc::From(const qcms_profile& qcmsProfile) { 347 ColorProfileDesc ret; 348 349 qcms_profile_data data = {}; 350 qcms_profile_get_data(&qcmsProfile, &data); 351 352 auto xyzd50FromLinearRgb = mat3{}; 353 // X contributions from [R,G,B] 354 xyzd50FromLinearRgb.at(0, 0) = data.red_colorant_xyzd50[0]; 355 xyzd50FromLinearRgb.at(1, 0) = data.green_colorant_xyzd50[0]; 356 xyzd50FromLinearRgb.at(2, 0) = data.blue_colorant_xyzd50[0]; 357 // Y contributions from [R,G,B] 358 xyzd50FromLinearRgb.at(0, 1) = data.red_colorant_xyzd50[1]; 359 xyzd50FromLinearRgb.at(1, 1) = data.green_colorant_xyzd50[1]; 360 xyzd50FromLinearRgb.at(2, 1) = data.blue_colorant_xyzd50[1]; 361 // Z contributions from [R,G,B] 362 xyzd50FromLinearRgb.at(0, 2) = data.red_colorant_xyzd50[2]; 363 xyzd50FromLinearRgb.at(1, 2) = data.green_colorant_xyzd50[2]; 364 xyzd50FromLinearRgb.at(2, 2) = data.blue_colorant_xyzd50[2]; 365 366 const auto d65FromD50 = XyzAFromXyzB_BradfordLinear(D65, D50); 367 ret.xyzd65FromLinearRgb = d65FromD50 * xyzd50FromLinearRgb; 368 369 // - 370 371 const auto Fn = [&](std::vector<float>* const linearFromTf, 372 int32_t claimed_samples, 373 const qcms_color_channel channel) { 374 if (claimed_samples == 0) return; // No tf. 375 376 if (claimed_samples == -1) { 377 claimed_samples = 4096; // Ask it to generate a bunch. 378 claimed_samples = 256; // Ask it to generate a bunch. 379 } 380 381 linearFromTf->resize(AssertedCast<size_t>(claimed_samples)); 382 383 const auto begin = linearFromTf->data(); 384 qcms_profile_get_lut(&qcmsProfile, channel, begin, 385 begin + linearFromTf->size()); 386 }; 387 388 Fn(&ret.linearFromTf.r, data.linear_from_trc_red_samples, 389 qcms_color_channel::Red); 390 Fn(&ret.linearFromTf.b, data.linear_from_trc_blue_samples, 391 qcms_color_channel::Blue); 392 Fn(&ret.linearFromTf.g, data.linear_from_trc_green_samples, 393 qcms_color_channel::Green); 394 395 // - 396 397 return ret; 398 } 399 400 // - 401 402 ColorProfileConversionDesc ColorProfileConversionDesc::From( 403 const FromDesc& desc) { 404 const auto dstLinearRgbFromXyzd65 = inverse(desc.dst.xyzd65FromLinearRgb); 405 auto ret = ColorProfileConversionDesc{ 406 .srcRgbFromSrcYuv = desc.src.rgbFromYcbcr, 407 .srcLinearFromSrcTf = desc.src.linearFromTf, 408 .dstLinearFromSrcLinear = 409 dstLinearRgbFromXyzd65 * desc.src.xyzd65FromLinearRgb, 410 .dstTfFromDstLinear = {}, 411 }; 412 const auto Invert = [](const std::vector<float>& linearFromTf, 413 std::vector<float>* const tfFromLinear) { 414 const auto size = linearFromTf.size(); 415 MOZ_ASSERT(size != 1); // Less than two is uninvertable. 416 if (size < 2) return; 417 (*tfFromLinear).resize(size); 418 InvertLut(linearFromTf, &*tfFromLinear); 419 }; 420 Invert(desc.dst.linearFromTf.r, &ret.dstTfFromDstLinear.r); 421 Invert(desc.dst.linearFromTf.g, &ret.dstTfFromDstLinear.g); 422 Invert(desc.dst.linearFromTf.b, &ret.dstTfFromDstLinear.b); 423 return ret; 424 } 425 426 } // namespace mozilla::color