/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "gtest/gtest.h" #include "Colorspaces.h" #include #include namespace mozilla::color { mat4 YuvFromYcbcr(const YcbcrDesc&); float TfFromLinear(const PiecewiseGammaDesc&, float linear); float LinearFromTf(const PiecewiseGammaDesc&, float tf); mat3 XyzFromLinearRgb(const Chromaticities&); } // namespace mozilla::color using namespace mozilla::color; auto Calc8From8(const ColorspaceTransform& ct, const ivec3 in8) { const auto in = vec3(in8) / vec3(255); const auto out = ct.DstFromSrc(in); const auto out8 = ivec3(round(out * vec3(255))); return out8; } auto Sample8From8(const Lut3& lut, const vec3 in8) { const auto in = in8 / vec3(255); const auto out = lut.Sample(in); const auto out8 = ivec3(round(out * vec3(255))); return out8; } TEST(Colorspaces, YcbcrDesc_Narrow8) { const auto m = YuvFromYcbcr(YcbcrDesc::Narrow8()); const auto Yuv8 = [&](const ivec3 ycbcr8) { const auto ycbcr = vec4(vec3(ycbcr8) / 255, 1); const auto yuv = m * ycbcr; return ivec3(round(yuv * 255)); }; EXPECT_EQ(Yuv8({{16, 128, 128}}), (ivec3{{0, 0, 0}})); EXPECT_EQ(Yuv8({{17, 128, 128}}), (ivec3{{1, 0, 0}})); // y = 0.5 => (16 + 235) / 2 = 125.5 EXPECT_EQ(Yuv8({{125, 128, 128}}), (ivec3{{127, 0, 0}})); EXPECT_EQ(Yuv8({{126, 128, 128}}), (ivec3{{128, 0, 0}})); EXPECT_EQ(Yuv8({{234, 128, 128}}), (ivec3{{254, 0, 0}})); EXPECT_EQ(Yuv8({{235, 128, 128}}), (ivec3{{255, 0, 0}})); // Check that we get the naive out-of-bounds behavior we'd expect: EXPECT_EQ(Yuv8({{15, 128, 128}}), (ivec3{{-1, 0, 0}})); EXPECT_EQ(Yuv8({{236, 128, 128}}), (ivec3{{256, 0, 0}})); } TEST(Colorspaces, YcbcrDesc_Full8) { const auto m = YuvFromYcbcr(YcbcrDesc::Full8()); const auto Yuv8 = [&](const ivec3 ycbcr8) { const auto ycbcr = vec4(vec3(ycbcr8) / 255, 1); const auto yuv = m * ycbcr; return ivec3(round(yuv * 255)); }; EXPECT_EQ(Yuv8({{0, 128, 128}}), (ivec3{{0, 0, 0}})); EXPECT_EQ(Yuv8({{1, 128, 128}}), (ivec3{{1, 0, 0}})); EXPECT_EQ(Yuv8({{127, 128, 128}}), (ivec3{{127, 0, 0}})); EXPECT_EQ(Yuv8({{128, 128, 128}}), (ivec3{{128, 0, 0}})); EXPECT_EQ(Yuv8({{254, 128, 128}}), (ivec3{{254, 0, 0}})); EXPECT_EQ(Yuv8({{255, 128, 128}}), (ivec3{{255, 0, 0}})); } TEST(Colorspaces, YcbcrDesc_Float) { const auto m = YuvFromYcbcr(YcbcrDesc::Float()); const auto Yuv8 = [&](const vec3 ycbcr8) { const auto ycbcr = vec4(vec3(ycbcr8) / 255, 1); const auto yuv = m * ycbcr; return ivec3(round(yuv * 255)); }; EXPECT_EQ(Yuv8({{0, 0.5 * 255, 0.5 * 255}}), (ivec3{{0, 0, 0}})); EXPECT_EQ(Yuv8({{1, 0.5 * 255, 0.5 * 255}}), (ivec3{{1, 0, 0}})); EXPECT_EQ(Yuv8({{127, 0.5 * 255, 0.5 * 255}}), (ivec3{{127, 0, 0}})); EXPECT_EQ(Yuv8({{128, 0.5 * 255, 0.5 * 255}}), (ivec3{{128, 0, 0}})); EXPECT_EQ(Yuv8({{254, 0.5 * 255, 0.5 * 255}}), (ivec3{{254, 0, 0}})); EXPECT_EQ(Yuv8({{255, 0.5 * 255, 0.5 * 255}}), (ivec3{{255, 0, 0}})); } TEST(Colorspaces, ColorspaceTransform_Rec709Narrow) { const auto src = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {{YuvLumaCoeffs::Rec709(), YcbcrDesc::Narrow8()}}, }; const auto dst = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {}, }; const auto ct = ColorspaceTransform::Create(src, dst); EXPECT_EQ(Calc8From8(ct, {{16, 128, 128}}), (ivec3{0})); EXPECT_EQ(Calc8From8(ct, {{17, 128, 128}}), (ivec3{1})); EXPECT_EQ(Calc8From8(ct, {{126, 128, 128}}), (ivec3{128})); EXPECT_EQ(Calc8From8(ct, {{234, 128, 128}}), (ivec3{254})); EXPECT_EQ(Calc8From8(ct, {{235, 128, 128}}), (ivec3{255})); // Check that we get the naive out-of-bounds behavior we'd expect: EXPECT_EQ(Calc8From8(ct, {{15, 128, 128}}), (ivec3{-1})); EXPECT_EQ(Calc8From8(ct, {{236, 128, 128}}), (ivec3{256})); } TEST(Colorspaces, LutSample_Rec709Float) { const auto src = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {{YuvLumaCoeffs::Rec709(), YcbcrDesc::Float()}}, }; const auto dst = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {}, }; const auto lut = ColorspaceTransform::Create(src, dst).ToLut3(); EXPECT_EQ(Sample8From8(lut, {{0, 0.5 * 255, 0.5 * 255}}), (ivec3{0})); EXPECT_EQ(Sample8From8(lut, {{1, 0.5 * 255, 0.5 * 255}}), (ivec3{1})); EXPECT_EQ(Sample8From8(lut, {{127, 0.5 * 255, 0.5 * 255}}), (ivec3{127})); EXPECT_EQ(Sample8From8(lut, {{128, 0.5 * 255, 0.5 * 255}}), (ivec3{128})); EXPECT_EQ(Sample8From8(lut, {{254, 0.5 * 255, 0.5 * 255}}), (ivec3{254})); EXPECT_EQ(Sample8From8(lut, {{255, 0.5 * 255, 0.5 * 255}}), (ivec3{255})); } TEST(Colorspaces, LutSample_Rec709Narrow) { const auto src = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {{YuvLumaCoeffs::Rec709(), YcbcrDesc::Narrow8()}}, }; const auto dst = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {}, }; const auto lut = ColorspaceTransform::Create(src, dst).ToLut3(); EXPECT_EQ(Sample8From8(lut, {{16, 128, 128}}), (ivec3{0})); EXPECT_EQ(Sample8From8(lut, {{17, 128, 128}}), (ivec3{1})); EXPECT_EQ(Sample8From8(lut, {{int((235 + 16) / 2), 128, 128}}), (ivec3{127})); EXPECT_EQ(Sample8From8(lut, {{int((235 + 16) / 2) + 1, 128, 128}}), (ivec3{128})); EXPECT_EQ(Sample8From8(lut, {{234, 128, 128}}), (ivec3{254})); EXPECT_EQ(Sample8From8(lut, {{235, 128, 128}}), (ivec3{255})); } TEST(Colorspaces, LutSample_Rec709Full) { const auto src = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {{YuvLumaCoeffs::Rec709(), YcbcrDesc::Full8()}}, }; const auto dst = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {}, }; const auto lut = ColorspaceTransform::Create(src, dst).ToLut3(); EXPECT_EQ(Sample8From8(lut, {{0, 128, 128}}), (ivec3{0})); EXPECT_EQ(Sample8From8(lut, {{1, 128, 128}}), (ivec3{1})); EXPECT_EQ(Sample8From8(lut, {{16, 128, 128}}), (ivec3{16})); EXPECT_EQ(Sample8From8(lut, {{128, 128, 128}}), (ivec3{128})); EXPECT_EQ(Sample8From8(lut, {{235, 128, 128}}), (ivec3{235})); EXPECT_EQ(Sample8From8(lut, {{254, 128, 128}}), (ivec3{254})); EXPECT_EQ(Sample8From8(lut, {{255, 128, 128}}), (ivec3{255})); } TEST(Colorspaces, PiecewiseGammaDesc_Srgb) { const auto tf = PiecewiseGammaDesc::Srgb(); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x00 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x01 / 255.0) * 255)), 0x0d); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x37 / 255.0) * 255)), 0x80); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x80 / 255.0) * 255)), 0xbc); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0xfd / 255.0) * 255)), 0xfe); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0xfe / 255.0) * 255)), 0xff); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0xff / 255.0) * 255)), 0xff); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x00 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x01 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x06 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x07 / 255.0) * 255)), 0x01); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x0d / 255.0) * 255)), 0x01); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x80 / 255.0) * 255)), 0x37); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0xbc / 255.0) * 255)), 0x80); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0xfe / 255.0) * 255)), 0xfd); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0xff / 255.0) * 255)), 0xff); } TEST(Colorspaces, PiecewiseGammaDesc_Rec709) { const auto tf = PiecewiseGammaDesc::Rec709(); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x00 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x01 / 255.0) * 255)), 0x05); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x43 / 255.0) * 255)), 0x80); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0x80 / 255.0) * 255)), 0xb4); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0xfd / 255.0) * 255)), 0xfe); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0xfe / 255.0) * 255)), 0xff); EXPECT_EQ(int(roundf(TfFromLinear(tf, 0xff / 255.0) * 255)), 0xff); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x00 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x01 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x02 / 255.0) * 255)), 0x00); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x03 / 255.0) * 255)), 0x01); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x05 / 255.0) * 255)), 0x01); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0x80 / 255.0) * 255)), 0x43); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0xb4 / 255.0) * 255)), 0x80); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0xfe / 255.0) * 255)), 0xfd); EXPECT_EQ(int(roundf(LinearFromTf(tf, 0xff / 255.0) * 255)), 0xff); } TEST(Colorspaces, ColorspaceTransform_PiecewiseGammaDesc) { const auto src = ColorspaceDesc{ Chromaticities::Srgb(), {}, {}, }; const auto dst = ColorspaceDesc{ Chromaticities::Srgb(), PiecewiseGammaDesc::Srgb(), {}, }; const auto toGamma = ColorspaceTransform::Create(src, dst); const auto toLinear = ColorspaceTransform::Create(dst, src); EXPECT_EQ(Calc8From8(toGamma, ivec3{0x00}), (ivec3{0x00})); EXPECT_EQ(Calc8From8(toGamma, ivec3{0x01}), (ivec3{0x0d})); EXPECT_EQ(Calc8From8(toGamma, ivec3{0x37}), (ivec3{0x80})); EXPECT_EQ(Calc8From8(toGamma, ivec3{0x80}), (ivec3{0xbc})); EXPECT_EQ(Calc8From8(toGamma, ivec3{0xfd}), (ivec3{0xfe})); EXPECT_EQ(Calc8From8(toGamma, ivec3{0xff}), (ivec3{0xff})); EXPECT_EQ(Calc8From8(toLinear, ivec3{0x00}), (ivec3{0x00})); EXPECT_EQ(Calc8From8(toLinear, ivec3{0x0d}), (ivec3{0x01})); EXPECT_EQ(Calc8From8(toLinear, ivec3{0x80}), (ivec3{0x37})); EXPECT_EQ(Calc8From8(toLinear, ivec3{0xbc}), (ivec3{0x80})); EXPECT_EQ(Calc8From8(toLinear, ivec3{0xfe}), (ivec3{0xfd})); EXPECT_EQ(Calc8From8(toLinear, ivec3{0xff}), (ivec3{0xff})); } // - // Actual end-to-end tests TEST(Colorspaces, SrgbFromRec709) { const auto src = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {{YuvLumaCoeffs::Rec709(), YcbcrDesc::Narrow8()}}, }; const auto dst = ColorspaceDesc{ Chromaticities::Srgb(), PiecewiseGammaDesc::Srgb(), {}, }; const auto ct = ColorspaceTransform::Create(src, dst); EXPECT_EQ(Calc8From8(ct, ivec3{{16, 128, 128}}), (ivec3{0})); EXPECT_EQ(Calc8From8(ct, ivec3{{17, 128, 128}}), (ivec3{3})); EXPECT_EQ(Calc8From8(ct, ivec3{{115, 128, 128}}), (ivec3{128})); EXPECT_EQ(Calc8From8(ct, ivec3{{126, 128, 128}}), (ivec3{140})); EXPECT_EQ(Calc8From8(ct, ivec3{{234, 128, 128}}), (ivec3{254})); EXPECT_EQ(Calc8From8(ct, ivec3{{235, 128, 128}}), (ivec3{255})); } TEST(Colorspaces, SrgbFromDisplayP3) { const auto p3C = ColorspaceDesc{ Chromaticities::DisplayP3(), PiecewiseGammaDesc::DisplayP3(), }; const auto srgbC = ColorspaceDesc{ Chromaticities::Srgb(), PiecewiseGammaDesc::Srgb(), }; const auto srgbLinearC = ColorspaceDesc{ Chromaticities::Srgb(), {}, }; const auto srgbFromP3 = ColorspaceTransform::Create(p3C, srgbC); const auto srgbLinearFromP3 = ColorspaceTransform::Create(p3C, srgbLinearC); // E.g. // https://colorjs.io/apps/convert/?color=color(display-p3%200.4%200.8%200.4)&precision=4 auto srgb = srgbFromP3.DstFromSrc(vec3{{0.4, 0.8, 0.4}}); EXPECT_NEAR(srgb.x(), 0.179, 0.001); EXPECT_NEAR(srgb.y(), 0.812, 0.001); EXPECT_NEAR(srgb.z(), 0.342, 0.001); auto srgbLinear = srgbLinearFromP3.DstFromSrc(vec3{{0.4, 0.8, 0.4}}); EXPECT_NEAR(srgbLinear.x(), 0.027, 0.001); EXPECT_NEAR(srgbLinear.y(), 0.624, 0.001); EXPECT_NEAR(srgbLinear.z(), 0.096, 0.001); } // - template constexpr auto map_tups_seq(const Tuple& a, const Tuple& b, const Fn& fn, std::index_sequence) { return std::tuple{fn(std::get(a), std::get(b))...}; } template constexpr auto map_tups(const Tuple& a, const Tuple& b, const Fn& fn) { return map_tups_seq(a, b, fn, std::make_index_sequence>{}); } template constexpr auto cmp_tups_all(const Tuple& a, const Tuple& b, const Fn& fn) { bool all = true; map_tups(a, b, [&](const auto& a, const auto& b) { return all &= fn(a, b); }); return all; } struct Stats { double mean = 0; double variance = 0; double min = std::numeric_limits::infinity(); double max = -std::numeric_limits::infinity(); template static Stats For(const T& iterable) { auto ret = Stats{}; for (const auto& cur : iterable) { ret.mean += cur; ret.min = std::min(ret.min, cur); ret.max = std::max(ret.max, cur); } ret.mean /= iterable.size(); // Gather mean first before we can calc variance. for (const auto& cur : iterable) { ret.variance += pow(cur - ret.mean, 2); } ret.variance /= iterable.size(); return ret; } template static Stats Diff(const T& a, const U& b) { MOZ_ASSERT(a.size() == b.size()); std::vector diff; diff.reserve(a.size()); for (size_t i = 0; i < diff.capacity(); i++) { diff.push_back(a[i] - b[i]); } return Stats::For(diff); } double standardDeviation() const { return sqrt(variance); } friend std::ostream& operator<<(std::ostream& s, const Stats& a) { return s << "Stats" << "{ mean:" << a.mean << ", stddev:" << a.standardDeviation() << ", min:" << a.min << ", max:" << a.max << " }"; } struct Error { double absmean = std::numeric_limits::infinity(); double stddev = std::numeric_limits::infinity(); double absmax = std::numeric_limits::infinity(); constexpr auto Fields() const { return std::tie(absmean, stddev, absmax); } template friend constexpr bool cmp_all(const Error& a, const Error& b, const Fn& fn) { return cmp_tups_all(a.Fields(), b.Fields(), fn); } friend constexpr bool operator<(const Error& a, const Error& b) { return cmp_all(a, b, [](const auto& a, const auto& b) { return a < b; }); } friend constexpr bool operator<=(const Error& a, const Error& b) { return cmp_all(a, b, [](const auto& a, const auto& b) { return a <= b; }); } friend std::ostream& operator<<(std::ostream& s, const Error& a) { return s << "Stats::Error" << "{ absmean:" << a.absmean << ", stddev:" << a.stddev << ", absmax:" << a.absmax << " }"; } }; operator Error() const { return {abs(mean), standardDeviation(), std::max(abs(min), abs(max))}; } }; static_assert(Stats::Error{0, 0, 0} < Stats::Error{1, 1, 1}); static_assert(!(Stats::Error{0, 1, 0} < Stats::Error{1, 1, 1})); static_assert(Stats::Error{0, 1, 0} <= Stats::Error{1, 1, 1}); static_assert(!(Stats::Error{0, 2, 0} <= Stats::Error{1, 1, 1})); // - static Stats StatsForLutError(const ColorspaceTransform& ct, const ivec3 srcQuants, const ivec3 dstQuants) { const auto lut = ct.ToLut3(); const auto dstScale = vec3(dstQuants - 1); std::vector quantErrors; quantErrors.reserve(srcQuants.x() * srcQuants.y() * srcQuants.z()); ForEachSampleWithin(srcQuants, [&](const vec3& src) { const auto sampled = lut.Sample(src); const auto actual = ct.DstFromSrc(src); const auto isampled = ivec3(round(sampled * dstScale)); const auto iactual = ivec3(round(actual * dstScale)); const auto ierr = abs(isampled - iactual); const auto quantError = dot(ierr, ivec3{1}); quantErrors.push_back(quantError); if (quantErrors.size() % 100000 == 0) { printf("%zu of %zu\n", quantErrors.size(), quantErrors.capacity()); } }); const auto quantErrStats = Stats::For(quantErrors); return quantErrStats; } TEST(Colorspaces, LutError_Rec709Full_Rec709Rgb) { const auto src = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {{YuvLumaCoeffs::Rec709(), YcbcrDesc::Full8()}}, }; const auto dst = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {}, }; const auto ct = ColorspaceTransform::Create(src, dst); const auto stats = StatsForLutError(ct, ivec3{64}, ivec3{256}); EXPECT_NEAR(stats.mean, 0.000, 0.001); EXPECT_NEAR(stats.standardDeviation(), 0.008, 0.001); EXPECT_NEAR(stats.min, 0, 0.001); EXPECT_NEAR(stats.max, 1, 0.001); } TEST(Colorspaces, LutError_Rec709Full_Srgb) { const auto src = ColorspaceDesc{ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), {{YuvLumaCoeffs::Rec709(), YcbcrDesc::Full8()}}, }; const auto dst = ColorspaceDesc{ Chromaticities::Srgb(), PiecewiseGammaDesc::Srgb(), {}, }; const auto ct = ColorspaceTransform::Create(src, dst); const auto stats = StatsForLutError(ct, ivec3{64}, ivec3{256}); EXPECT_NEAR(stats.mean, 0.530, 0.001); EXPECT_NEAR(stats.standardDeviation(), 1.674, 0.001); EXPECT_NEAR(stats.min, 0, 0.001); EXPECT_NEAR(stats.max, 17, 0.001); } // - // https://www.reedbeta.com/blog/python-like-enumerate-in-cpp17/ template ())), typename = decltype(std::end(std::declval()))> constexpr auto enumerate(T&& iterable) { struct iterator { size_t i; TIter iter; bool operator!=(const iterator& other) const { return iter != other.iter; } void operator++() { ++i; ++iter; } auto operator*() const { return std::tie(i, *iter); } }; struct iterable_wrapper { T iterable; auto begin() { return iterator{0, std::begin(iterable)}; } auto end() { return iterator{0, std::end(iterable)}; } }; return iterable_wrapper{std::forward(iterable)}; } inline auto MakeLinear(const float from, const float to, const int n) { std::vector ret; ret.resize(n); for (auto [i, val] : enumerate(ret)) { const auto t = i / float(ret.size() - 1); val = from + (to - from) * t; } return ret; }; inline auto MakeGamma(const float exp, const int n) { std::vector ret; ret.resize(n); for (auto [i, val] : enumerate(ret)) { const auto t = i / float(ret.size() - 1); val = powf(t, exp); } return ret; }; // - TEST(Colorspaces, GuessGamma) { EXPECT_NEAR(GuessGamma(MakeGamma(1, 11)), 1.0, 0); EXPECT_NEAR(GuessGamma(MakeGamma(2.2, 11)), 2.2, 4.8e-8); EXPECT_NEAR(GuessGamma(MakeGamma(1 / 2.2, 11)), 1 / 2.2, 1.7e-7); } // - template float StdDev(const T& test, const U& ref) { float sum = 0; for (size_t i = 0; i < test.size(); i++) { const auto diff = test[i] - ref[i]; sum += diff * diff; } const auto variance = sum / test.size(); return sqrt(variance); } template inline void AutoLinearFill(T& vals) { LinearFill(vals, { {0, 0}, {vals.size() - 1.0f, 1}, }); } template auto MakeArray(const T& a0, const More&... args) { return std::array{a0, static_cast(args)...}; } TEST(Colorspaces, LinearFill) { EXPECT_NEAR(StdDev(MakeLinear(0, 1, 3), MakeArray(0, 0.5, 1)), 0, 0.001); auto vals = std::vector(3); LinearFill(vals, { {0, 0}, {vals.size() - 1.0f, 1}, }); EXPECT_NEAR(StdDev(vals, MakeArray(0, 0.5, 1)), 0, 0.001); LinearFill(vals, { {0, 1}, {vals.size() - 1.0f, 0}, }); EXPECT_NEAR(StdDev(vals, MakeArray(1, 0.5, 0)), 0, 0.001); } TEST(Colorspaces, DequantizeMonotonic) { auto orig = std::vector{0, 0, 0, 1, 1, 2}; auto vals = orig; EXPECT_TRUE(IsMonotonic(vals)); EXPECT_TRUE(!IsMonotonic(vals, std::less{})); DequantizeMonotonic(vals); EXPECT_TRUE(IsMonotonic(vals, std::less{})); EXPECT_LT(StdDev(vals, orig), StdDev(MakeLinear(orig.front(), orig.back(), vals.size()), orig)); } TEST(Colorspaces, InvertLut) { const auto linear = MakeLinear(0, 1, 256); auto linearFromSrgb = linear; for (auto& val : linearFromSrgb) { val = powf(val, 2.2); } auto srgbFromLinearExpected = linear; for (auto& val : srgbFromLinearExpected) { val = powf(val, 1 / 2.2); } auto srgbFromLinearViaInvert = linearFromSrgb; InvertLut(linearFromSrgb, &srgbFromLinearViaInvert); // I just want to appreciate that InvertLut is a non-analytical approximation, // and yet it's extraordinarily close to the analytical inverse. EXPECT_LE(Stats::Diff(srgbFromLinearViaInvert, srgbFromLinearExpected), (Stats::Error{3e-6, 3e-6, 3e-5})); const auto srcSrgb = MakeLinear(0, 1, 256); auto roundtripSrgb = srcSrgb; for (auto& srgb : roundtripSrgb) { const auto linear = SampleOutByIn(linearFromSrgb, srgb); const auto srgb2 = SampleOutByIn(srgbFromLinearViaInvert, linear); // printf("[%f] %f -> %f -> %f\n", srgb2-srgb, srgb, linear, srgb2); srgb = srgb2; } EXPECT_LE(Stats::Diff(roundtripSrgb, srcSrgb), (Stats::Error{0.0013, 0.0046, 0.023})); } TEST(Colorspaces, XyzFromLinearRgb) { const auto xyzd65FromLinearRgb = XyzFromLinearRgb(Chromaticities::Srgb()); // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html const auto XYZD65_FROM_LINEAR_RGB = mat3({ vec3{{0.4124564, 0.3575761, 0.1804375}}, vec3{{0.2126729, 0.7151522, 0.0721750}}, vec3{{0.0193339, 0.1191920, 0.9503041}}, }); EXPECT_NEAR(sqrt(dotDifference(xyzd65FromLinearRgb, XYZD65_FROM_LINEAR_RGB)), 0, 0.001); } TEST(Colorspaces, ColorProfileConversionDesc_SrgbFromRec709) { const auto srgb = ColorProfileDesc::From({ Chromaticities::Srgb(), PiecewiseGammaDesc::Srgb(), }); const auto rec709 = ColorProfileDesc::From({ Chromaticities::Rec709(), PiecewiseGammaDesc::Rec709(), }); { const auto conv = ColorProfileConversionDesc::From({ .src = srgb, .dst = srgb, }); auto src = vec3(16.0); auto dst = conv.Apply(src / 255) * 255; const auto tfa = PiecewiseGammaDesc::Srgb(); const auto tfb = PiecewiseGammaDesc::Srgb(); const auto expected = TfFromLinear(tfb, LinearFromTf(tfa, src.x() / 255)) * 255; printf("%f %f %f\n", src.x(), src.y(), src.z()); printf("%f %f %f\n", dst.x(), dst.y(), dst.z()); EXPECT_LT(Stats::Diff(dst.data, vec3(expected).data), (Stats::Error{0.42})); } { const auto conv = ColorProfileConversionDesc::From({ .src = rec709, .dst = rec709, }); auto src = vec3(16.0); auto dst = conv.Apply(src / 255) * 255; const auto tfa = PiecewiseGammaDesc::Rec709(); const auto tfb = PiecewiseGammaDesc::Rec709(); const auto expected = TfFromLinear(tfb, LinearFromTf(tfa, src.x() / 255)) * 255; printf("%f %f %f\n", src.x(), src.y(), src.z()); printf("%f %f %f\n", dst.x(), dst.y(), dst.z()); EXPECT_LT(Stats::Diff(dst.data, vec3(expected).data), (Stats::Error{1e-6})); } { const auto conv = ColorProfileConversionDesc::From({ .src = rec709, .dst = srgb, }); auto src = vec3(16.0); auto dst = conv.Apply(src / 255) * 255; const auto tfa = PiecewiseGammaDesc::Rec709(); const auto tfb = PiecewiseGammaDesc::Srgb(); const auto expected = TfFromLinear(tfb, LinearFromTf(tfa, src.x() / 255)) * 255; printf("expected: %f\n", expected); printf("%f %f %f\n", src.x(), src.y(), src.z()); printf("%f %f %f\n", dst.x(), dst.y(), dst.z()); EXPECT_LT(Stats::Diff(dst.data, vec3(expected).data), (Stats::Error{0.12})); } }