color_management_test.cc (17367B)
1 // Copyright (c) the JPEG XL Project Authors. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 #include <jxl/cms.h> 7 #include <jxl/cms_interface.h> 8 #include <stdint.h> 9 10 #include <algorithm> 11 #include <cstddef> 12 #include <cstdint> 13 #include <cstdio> 14 #include <cstdlib> 15 #include <ostream> 16 #include <string> 17 #include <utility> 18 #include <vector> 19 20 #include "lib/jxl/base/common.h" 21 #include "lib/jxl/base/compiler_specific.h" 22 #include "lib/jxl/base/span.h" 23 #include "lib/jxl/cms/color_encoding_cms.h" 24 #include "lib/jxl/cms/opsin_params.h" 25 #include "lib/jxl/color_encoding_internal.h" 26 #include "lib/jxl/enc_xyb.h" 27 #include "lib/jxl/image.h" 28 #include "lib/jxl/image_bundle.h" 29 #include "lib/jxl/image_metadata.h" 30 #include "lib/jxl/image_ops.h" 31 #include "lib/jxl/image_test_utils.h" 32 #include "lib/jxl/test_utils.h" 33 #include "lib/jxl/testing.h" 34 35 namespace jxl { 36 37 std::ostream& operator<<(std::ostream& os, const CIExy& xy) { 38 return os << "{x=" << xy.x << ", y=" << xy.y << "}"; 39 } 40 41 std::ostream& operator<<(std::ostream& os, const PrimariesCIExy& primaries) { 42 return os << "{r=" << primaries.r << ", g=" << primaries.g 43 << ", b=" << primaries.b << "}"; 44 } 45 46 namespace { 47 48 using ::testing::ElementsAre; 49 using ::testing::FloatNear; 50 51 // Small enough to be fast. If changed, must update Generate*. 52 constexpr size_t kWidth = 16; 53 54 constexpr size_t kNumThreads = 1; // only have a single row. 55 56 MATCHER_P(HasSameFieldsAs, expected, "") { 57 if (arg.GetRenderingIntent() != expected.GetRenderingIntent()) { 58 *result_listener << "which has a different rendering intent: " 59 << ToString(arg.GetRenderingIntent()) << " instead of " 60 << ToString(expected.GetRenderingIntent()); 61 return false; 62 } 63 if (arg.GetColorSpace() != expected.GetColorSpace()) { 64 *result_listener << "which has a different color space: " 65 << ToString(arg.GetColorSpace()) << " instead of " 66 << ToString(expected.GetColorSpace()); 67 return false; 68 } 69 if (arg.GetWhitePointType() != expected.GetWhitePointType()) { 70 *result_listener << "which has a different white point: " 71 << ToString(arg.GetWhitePointType()) << " instead of " 72 << ToString(expected.GetWhitePointType()); 73 return false; 74 } 75 if (arg.HasPrimaries() && 76 arg.GetPrimariesType() != expected.GetPrimariesType()) { 77 *result_listener << "which has different primaries: " 78 << ToString(arg.GetPrimariesType()) << " instead of " 79 << ToString(expected.GetPrimariesType()); 80 return false; 81 } 82 if (!arg.Tf().IsSame(expected.Tf())) { 83 static const auto tf_to_string = 84 [](const jxl::cms::CustomTransferFunction& tf) { 85 if (tf.have_gamma) { 86 return "g" + ToString(tf.GetGamma()); 87 } 88 return ToString(tf.transfer_function); 89 }; 90 *result_listener << "which has a different transfer function: " 91 << tf_to_string(arg.Tf()) << " instead of " 92 << tf_to_string(expected.Tf()); 93 return false; 94 } 95 return true; 96 } 97 98 struct Globals { 99 // TODO(deymo): Make this a const. 100 static Globals* GetInstance() { 101 static Globals ret; 102 return &ret; 103 } 104 105 private: 106 Globals() { 107 in_gray = GenerateGray(); 108 in_color = GenerateColor(); 109 JXL_ASSIGN_OR_DIE(out_gray, ImageF::Create(kWidth, 1)); 110 JXL_ASSIGN_OR_DIE(out_color, ImageF::Create(kWidth * 3, 1)); 111 112 c_native = ColorEncoding::LinearSRGB(/*is_gray=*/false); 113 c_gray = ColorEncoding::LinearSRGB(/*is_gray=*/true); 114 } 115 116 static ImageF GenerateGray() { 117 JXL_ASSIGN_OR_DIE(ImageF gray, ImageF::Create(kWidth, 1)); 118 float* JXL_RESTRICT row = gray.Row(0); 119 // Increasing left to right 120 for (uint32_t x = 0; x < kWidth; ++x) { 121 row[x] = x * 1.0f / (kWidth - 1); // [0, 1] 122 } 123 return gray; 124 } 125 126 static ImageF GenerateColor() { 127 JXL_ASSIGN_OR_DIE(ImageF image, ImageF::Create(kWidth * 3, 1)); 128 float* JXL_RESTRICT interleaved = image.Row(0); 129 std::fill(interleaved, interleaved + kWidth * 3, 0.0f); 130 131 // [0, 4): neutral 132 for (int32_t x = 0; x < 4; ++x) { 133 interleaved[3 * x + 0] = x * 1.0f / 3; // [0, 1] 134 interleaved[3 * x + 2] = interleaved[3 * x + 1] = interleaved[3 * x + 0]; 135 } 136 137 // [4, 13): pure RGB with low/medium/high saturation 138 for (int32_t c = 0; c < 3; ++c) { 139 interleaved[3 * (4 + c) + c] = 0.08f + c * 0.01f; 140 interleaved[3 * (7 + c) + c] = 0.75f + c * 0.01f; 141 interleaved[3 * (10 + c) + c] = 1.0f; 142 } 143 144 // [13, 16): impure, not quite saturated RGB 145 interleaved[3 * 13 + 0] = 0.86f; 146 interleaved[3 * 13 + 2] = interleaved[3 * 13 + 1] = 0.16f; 147 interleaved[3 * 14 + 1] = 0.87f; 148 interleaved[3 * 14 + 2] = interleaved[3 * 14 + 0] = 0.16f; 149 interleaved[3 * 15 + 2] = 0.88f; 150 interleaved[3 * 15 + 1] = interleaved[3 * 15 + 0] = 0.16f; 151 152 return image; 153 } 154 155 public: 156 // ImageF so we can use VerifyRelativeError; all are interleaved RGB. 157 ImageF in_gray; 158 ImageF in_color; 159 ImageF out_gray; 160 ImageF out_color; 161 ColorEncoding c_native; 162 ColorEncoding c_gray; 163 }; 164 165 class ColorManagementTest 166 : public ::testing::TestWithParam<test::ColorEncodingDescriptor> { 167 public: 168 // "Same" pixels after converting g->c_native -> c -> g->c_native. 169 static void VerifyPixelRoundTrip(const ColorEncoding& c) { 170 Globals* g = Globals::GetInstance(); 171 const ColorEncoding& c_native = c.IsGray() ? g->c_gray : g->c_native; 172 const JxlCmsInterface& cms = *JxlGetDefaultCms(); 173 ColorSpaceTransform xform_fwd(cms); 174 ColorSpaceTransform xform_rev(cms); 175 const float intensity_target = 176 c.Tf().IsHLG() ? 1000 : kDefaultIntensityTarget; 177 ASSERT_TRUE( 178 xform_fwd.Init(c_native, c, intensity_target, kWidth, kNumThreads)); 179 ASSERT_TRUE( 180 xform_rev.Init(c, c_native, intensity_target, kWidth, kNumThreads)); 181 182 const size_t thread = 0; 183 const ImageF& in = c.IsGray() ? g->in_gray : g->in_color; 184 ImageF* JXL_RESTRICT out = c.IsGray() ? &g->out_gray : &g->out_color; 185 ASSERT_TRUE( 186 xform_fwd.Run(thread, in.Row(0), xform_fwd.BufDst(thread), kWidth)); 187 ASSERT_TRUE( 188 xform_rev.Run(thread, xform_fwd.BufDst(thread), out->Row(0), kWidth)); 189 190 // With lcms2, this value is lower: 5E-5 191 double max_l1 = 7E-4; 192 // Most are lower; reached 3E-7 with D60 AP0. 193 double max_rel = 4E-7; 194 if (c.IsGray()) max_rel = 2E-5; 195 JXL_ASSERT_OK(VerifyRelativeError(in, *out, max_l1, max_rel, _)); 196 } 197 }; 198 JXL_GTEST_INSTANTIATE_TEST_SUITE_P(ColorManagementTestInstantiation, 199 ColorManagementTest, 200 ::testing::ValuesIn(test::AllEncodings())); 201 202 // Exercises the ColorManagement interface for ALL ColorEncoding synthesizable 203 // via enums. 204 TEST_P(ColorManagementTest, VerifyAllProfiles) { 205 ColorEncoding c = ColorEncodingFromDescriptor(GetParam()); 206 printf("%s\n", Description(c).c_str()); 207 208 // Can create profile. 209 ASSERT_TRUE(c.CreateICC()); 210 211 // Can set an equivalent ColorEncoding from the generated ICC profile. 212 ColorEncoding c3; 213 ASSERT_TRUE(c3.SetICC(IccBytes(c.ICC()), JxlGetDefaultCms())); 214 EXPECT_THAT(c3, HasSameFieldsAs(c)); 215 216 VerifyPixelRoundTrip(c); 217 } 218 219 testing::Matcher<CIExy> CIExyIs(const double x, const double y) { 220 static constexpr double kMaxError = 1e-4; 221 return testing::AllOf( 222 testing::Field(&CIExy::x, testing::DoubleNear(x, kMaxError)), 223 testing::Field(&CIExy::y, testing::DoubleNear(y, kMaxError))); 224 } 225 226 testing::Matcher<PrimariesCIExy> PrimariesAre( 227 const testing::Matcher<CIExy>& r, const testing::Matcher<CIExy>& g, 228 const testing::Matcher<CIExy>& b) { 229 return testing::AllOf(testing::Field(&PrimariesCIExy::r, r), 230 testing::Field(&PrimariesCIExy::g, g), 231 testing::Field(&PrimariesCIExy::b, b)); 232 } 233 234 TEST_F(ColorManagementTest, sRGBChromaticity) { 235 const ColorEncoding sRGB = ColorEncoding::SRGB(); 236 EXPECT_THAT(sRGB.GetWhitePoint(), CIExyIs(0.3127, 0.3290)); 237 EXPECT_THAT(sRGB.GetPrimaries(), 238 PrimariesAre(CIExyIs(0.64, 0.33), CIExyIs(0.30, 0.60), 239 CIExyIs(0.15, 0.06))); 240 } 241 242 TEST_F(ColorManagementTest, D2700Chromaticity) { 243 std::vector<uint8_t> icc_data = 244 jxl::test::ReadTestData("jxl/color_management/sRGB-D2700.icc"); 245 IccBytes icc; 246 Bytes(icc_data).AppendTo(icc); 247 ColorEncoding sRGB_D2700; 248 ASSERT_TRUE(sRGB_D2700.SetICC(std::move(icc), JxlGetDefaultCms())); 249 250 EXPECT_THAT(sRGB_D2700.GetWhitePoint(), CIExyIs(0.45986, 0.41060)); 251 // The illuminant-relative chromaticities of this profile's primaries are the 252 // same as for sRGB. It is the PCS-relative chromaticities that would be 253 // different. 254 EXPECT_THAT(sRGB_D2700.GetPrimaries(), 255 PrimariesAre(CIExyIs(0.64, 0.33), CIExyIs(0.30, 0.60), 256 CIExyIs(0.15, 0.06))); 257 } 258 259 TEST_F(ColorManagementTest, D2700ToSRGB) { 260 std::vector<uint8_t> icc_data = 261 jxl::test::ReadTestData("jxl/color_management/sRGB-D2700.icc"); 262 IccBytes icc; 263 Bytes(icc_data).AppendTo(icc); 264 ColorEncoding sRGB_D2700; 265 ASSERT_TRUE(sRGB_D2700.SetICC(std::move(icc), JxlGetDefaultCms())); 266 267 ColorSpaceTransform transform(*JxlGetDefaultCms()); 268 ASSERT_TRUE(transform.Init(sRGB_D2700, ColorEncoding::SRGB(), 269 kDefaultIntensityTarget, 1, 1)); 270 const float sRGB_D2700_values[3] = {0.863, 0.737, 0.490}; 271 float sRGB_values[3]; 272 ASSERT_TRUE(transform.Run(0, sRGB_D2700_values, sRGB_values, 1)); 273 EXPECT_THAT(sRGB_values, 274 ElementsAre(FloatNear(0.914, 1e-3), FloatNear(0.745, 1e-3), 275 FloatNear(0.601, 1e-3))); 276 } 277 278 TEST_F(ColorManagementTest, P3HlgTo2020Hlg) { 279 ColorEncoding p3_hlg; 280 p3_hlg.SetColorSpace(ColorSpace::kRGB); 281 ASSERT_TRUE(p3_hlg.SetWhitePointType(WhitePoint::kD65)); 282 ASSERT_TRUE(p3_hlg.SetPrimariesType(Primaries::kP3)); 283 p3_hlg.Tf().SetTransferFunction(TransferFunction::kHLG); 284 ASSERT_TRUE(p3_hlg.CreateICC()); 285 286 ColorEncoding rec2020_hlg = p3_hlg; 287 ASSERT_TRUE(rec2020_hlg.SetPrimariesType(Primaries::k2100)); 288 ASSERT_TRUE(rec2020_hlg.CreateICC()); 289 290 ColorSpaceTransform transform(*JxlGetDefaultCms()); 291 ASSERT_TRUE(transform.Init(p3_hlg, rec2020_hlg, 1000, 1, 1)); 292 const float p3_hlg_values[3] = {0., 0.75, 0.}; 293 float rec2020_hlg_values[3]; 294 ASSERT_TRUE(transform.Run(0, p3_hlg_values, rec2020_hlg_values, 1)); 295 EXPECT_THAT(rec2020_hlg_values, 296 ElementsAre(FloatNear(0.3973, 1e-4), FloatNear(0.7382, 1e-4), 297 FloatNear(0.1183, 1e-4))); 298 } 299 300 TEST_F(ColorManagementTest, HlgOotf) { 301 ColorEncoding p3_hlg; 302 p3_hlg.SetColorSpace(ColorSpace::kRGB); 303 ASSERT_TRUE(p3_hlg.SetWhitePointType(WhitePoint::kD65)); 304 ASSERT_TRUE(p3_hlg.SetPrimariesType(Primaries::kP3)); 305 p3_hlg.Tf().SetTransferFunction(TransferFunction::kHLG); 306 ASSERT_TRUE(p3_hlg.CreateICC()); 307 308 ColorSpaceTransform transform_to_1000(*JxlGetDefaultCms()); 309 ASSERT_TRUE( 310 transform_to_1000.Init(p3_hlg, ColorEncoding::LinearSRGB(), 1000, 1, 1)); 311 // HDR reference white: https://www.itu.int/pub/R-REP-BT.2408-4-2021 312 float p3_hlg_values[3] = {0.75, 0.75, 0.75}; 313 float linear_srgb_values[3]; 314 ASSERT_TRUE(transform_to_1000.Run(0, p3_hlg_values, linear_srgb_values, 1)); 315 // On a 1000-nit display, HDR reference white should be 203 cd/m² which is 316 // 0.203 times the maximum. 317 EXPECT_THAT(linear_srgb_values, 318 ElementsAre(FloatNear(0.203, 1e-3), FloatNear(0.203, 1e-3), 319 FloatNear(0.203, 1e-3))); 320 321 ColorSpaceTransform transform_to_400(*JxlGetDefaultCms()); 322 ASSERT_TRUE( 323 transform_to_400.Init(p3_hlg, ColorEncoding::LinearSRGB(), 400, 1, 1)); 324 ASSERT_TRUE(transform_to_400.Run(0, p3_hlg_values, linear_srgb_values, 1)); 325 // On a 400-nit display, it should be 100 cd/m². 326 EXPECT_THAT(linear_srgb_values, 327 ElementsAre(FloatNear(0.250, 1e-3), FloatNear(0.250, 1e-3), 328 FloatNear(0.250, 1e-3))); 329 330 p3_hlg_values[2] = 0.50; 331 ASSERT_TRUE(transform_to_1000.Run(0, p3_hlg_values, linear_srgb_values, 1)); 332 EXPECT_THAT(linear_srgb_values, 333 ElementsAre(FloatNear(0.201, 1e-3), FloatNear(0.201, 1e-3), 334 FloatNear(0.050, 1e-3))); 335 336 ColorSpaceTransform transform_from_400(*JxlGetDefaultCms()); 337 ASSERT_TRUE( 338 transform_from_400.Init(ColorEncoding::LinearSRGB(), p3_hlg, 400, 1, 1)); 339 linear_srgb_values[0] = linear_srgb_values[1] = linear_srgb_values[2] = 0.250; 340 ASSERT_TRUE(transform_from_400.Run(0, linear_srgb_values, p3_hlg_values, 1)); 341 EXPECT_THAT(p3_hlg_values, 342 ElementsAre(FloatNear(0.75, 1e-3), FloatNear(0.75, 1e-3), 343 FloatNear(0.75, 1e-3))); 344 345 ColorEncoding grayscale_hlg; 346 grayscale_hlg.SetColorSpace(ColorSpace::kGray); 347 ASSERT_TRUE(grayscale_hlg.SetWhitePointType(WhitePoint::kD65)); 348 grayscale_hlg.Tf().SetTransferFunction(TransferFunction::kHLG); 349 ASSERT_TRUE(grayscale_hlg.CreateICC()); 350 351 ColorSpaceTransform grayscale_transform(*JxlGetDefaultCms()); 352 ASSERT_TRUE(grayscale_transform.Init( 353 grayscale_hlg, ColorEncoding::LinearSRGB(/*is_gray=*/true), 1000, 1, 1)); 354 const float grayscale_hlg_value = 0.75; 355 float linear_grayscale_value; 356 ASSERT_TRUE(grayscale_transform.Run(0, &grayscale_hlg_value, 357 &linear_grayscale_value, 1)); 358 EXPECT_THAT(linear_grayscale_value, FloatNear(0.203, 1e-3)); 359 } 360 361 TEST_F(ColorManagementTest, XYBProfile) { 362 ColorEncoding c_xyb; 363 c_xyb.SetColorSpace(ColorSpace::kXYB); 364 c_xyb.SetRenderingIntent(RenderingIntent::kPerceptual); 365 ASSERT_TRUE(c_xyb.CreateICC()); 366 ColorEncoding c_native = ColorEncoding::LinearSRGB(false); 367 368 static const size_t kGridDim = 17; 369 static const size_t kNumColors = kGridDim * kGridDim * kGridDim; 370 const JxlCmsInterface& cms = *JxlGetDefaultCms(); 371 ColorSpaceTransform xform(cms); 372 ASSERT_TRUE( 373 xform.Init(c_xyb, c_native, kDefaultIntensityTarget, kNumColors, 1)); 374 375 ImageMetadata metadata; 376 metadata.color_encoding = c_native; 377 ImageBundle ib(&metadata); 378 JXL_ASSIGN_OR_DIE(Image3F native, Image3F::Create(kNumColors, 1)); 379 float mul = 1.0f / (kGridDim - 1); 380 for (size_t ir = 0, x = 0; ir < kGridDim; ++ir) { 381 for (size_t ig = 0; ig < kGridDim; ++ig) { 382 for (size_t ib = 0; ib < kGridDim; ++ib, ++x) { 383 native.PlaneRow(0, 0)[x] = ir * mul; 384 native.PlaneRow(1, 0)[x] = ig * mul; 385 native.PlaneRow(2, 0)[x] = ib * mul; 386 } 387 } 388 } 389 ib.SetFromImage(std::move(native), c_native); 390 const Image3F& in = *ib.color(); 391 JXL_ASSIGN_OR_DIE(Image3F opsin, Image3F::Create(kNumColors, 1)); 392 JXL_CHECK(ToXYB(ib, nullptr, &opsin, cms, nullptr)); 393 394 JXL_ASSIGN_OR_DIE(Image3F opsin2, Image3F::Create(kNumColors, 1)); 395 CopyImageTo(opsin, &opsin2); 396 ScaleXYB(&opsin2); 397 398 float* src = xform.BufSrc(0); 399 for (size_t i = 0; i < kNumColors; ++i) { 400 for (size_t c = 0; c < 3; ++c) { 401 src[3 * i + c] = opsin2.PlaneRow(c, 0)[i]; 402 } 403 } 404 405 float* dst = xform.BufDst(0); 406 ASSERT_TRUE(xform.Run(0, src, dst, kNumColors)); 407 408 JXL_ASSIGN_OR_DIE(Image3F out, Image3F::Create(kNumColors, 1)); 409 for (size_t i = 0; i < kNumColors; ++i) { 410 for (size_t c = 0; c < 3; ++c) { 411 out.PlaneRow(c, 0)[i] = dst[3 * i + c]; 412 } 413 } 414 415 auto debug_print_color = [&](size_t i) { 416 printf( 417 "(%f, %f, %f) -> (%9.6f, %f, %f) -> (%f, %f, %f) -> " 418 "(%9.6f, %9.6f, %9.6f)", 419 in.PlaneRow(0, 0)[i], in.PlaneRow(1, 0)[i], in.PlaneRow(2, 0)[i], 420 opsin.PlaneRow(0, 0)[i], opsin.PlaneRow(1, 0)[i], 421 opsin.PlaneRow(2, 0)[i], opsin2.PlaneRow(0, 0)[i], 422 opsin2.PlaneRow(1, 0)[i], opsin2.PlaneRow(2, 0)[i], 423 out.PlaneRow(0, 0)[i], out.PlaneRow(1, 0)[i], out.PlaneRow(2, 0)[i]); 424 }; 425 426 float max_err[3] = {}; 427 size_t max_err_i[3] = {}; 428 for (size_t i = 0; i < kNumColors; ++i) { 429 for (size_t c = 0; c < 3; ++c) { 430 // debug_print_color(i); printf("\n"); 431 float err = std::abs(in.PlaneRow(c, 0)[i] - out.PlaneRow(c, 0)[i]); 432 if (err > max_err[c]) { 433 max_err[c] = err; 434 max_err_i[c] = i; 435 } 436 } 437 } 438 static float kMaxError[3] = {9e-4, 4e-4, 5e-4}; 439 printf("Maximum errors:\n"); 440 for (size_t c = 0; c < 3; ++c) { 441 debug_print_color(max_err_i[c]); 442 printf(" %f\n", max_err[c]); 443 EXPECT_LT(max_err[c], kMaxError[c]); 444 } 445 } 446 447 TEST_F(ColorManagementTest, GoldenXYBCube) { 448 std::vector<int32_t> actual; 449 const jxl::cms::ColorCube3D& cube = jxl::cms::UnscaledA2BCube(); 450 for (size_t ix = 0; ix < 2; ++ix) { 451 for (size_t iy = 0; iy < 2; ++iy) { 452 for (size_t ib = 0; ib < 2; ++ib) { 453 const jxl::cms::ColorCube0D& out_f = cube[ix][iy][ib]; 454 for (int i = 0; i < 3; ++i) { 455 int32_t val = static_cast<int32_t>(0.5f + 65535 * out_f[i]); 456 ASSERT_TRUE(val >= 0 && val <= 65535); 457 actual.push_back(val); 458 } 459 } 460 } 461 } 462 463 std::vector<int32_t> expected = {0, 3206, 0, 0, 3206, 28873, 464 62329, 65535, 36662, 62329, 65535, 65535, 465 3206, 0, 0, 3206, 0, 28873, 466 65535, 62329, 36662, 65535, 62329, 65535}; 467 EXPECT_EQ(actual, expected); 468 } 469 470 } // namespace 471 } // namespace jxl