| // Copyright 2023 Google LLC | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include <cmath> | |
| #include "avif/internal.h" | |
| #include "aviftest_helpers.h" | |
| #include "gtest/gtest.h" | |
| namespace avif { | |
| namespace { | |
| constexpr int kMaxTransferCharacteristic = 18; | |
| // Thresholds in the transfer curve formulas. | |
| // Add a little tolerance for test to pass on MinGW32. | |
| constexpr float kTransferLog100Threshold = 0.01f + 1e-7f; | |
| constexpr float kTransferLog100Sqrt10Threshold = 0.00316227766f; | |
| TEST(TransferCharacteristicsTest, RoundTrip) { | |
| for (int tc_idx = 0; tc_idx <= kMaxTransferCharacteristic; ++tc_idx) { | |
| const avifTransferCharacteristics tc = (avifTransferCharacteristics)tc_idx; | |
| SCOPED_TRACE("transfer characteristics: " + std::to_string(tc)); | |
| const avifTransferFunction to_linear = | |
| avifTransferCharacteristicsGetGammaToLinearFunction(tc); | |
| const avifTransferFunction to_gamma = | |
| avifTransferCharacteristicsGetLinearToGammaFunction(tc); | |
| constexpr int kSteps = 1000; | |
| float min_linear = std::numeric_limits<float>::max(); | |
| float max_linear = 0.0f; | |
| for (int j = 0; j <= kSteps; ++j) { | |
| const float v = static_cast<float>(j) / kSteps; | |
| float epsilon = 0.0001f; | |
| // Non bijective part of some transfer functions. | |
| if (tc == AVIF_TRANSFER_CHARACTERISTICS_LOG100 && | |
| v <= kTransferLog100Threshold) { | |
| epsilon = kTransferLog100Threshold / 2.0f; | |
| } else if (tc == AVIF_TRANSFER_CHARACTERISTICS_LOG100_SQRT10 && | |
| v <= kTransferLog100Sqrt10Threshold) { | |
| epsilon = kTransferLog100Sqrt10Threshold / 2.0f; | |
| } | |
| // Check round trips. | |
| ASSERT_NEAR(to_linear(to_gamma(v)), v, epsilon); | |
| ASSERT_NEAR(to_gamma(to_linear(v)), v, epsilon); | |
| const float linear = to_linear(v); | |
| if (linear > max_linear) max_linear = linear; | |
| if (linear < min_linear) min_linear = linear; | |
| } | |
| if (tc == AVIF_TRANSFER_CHARACTERISTICS_LOG100) { | |
| EXPECT_NEAR(min_linear, kTransferLog100Threshold / 2.0f, 1e-7f); | |
| } else if (tc == AVIF_TRANSFER_CHARACTERISTICS_LOG100_SQRT10) { | |
| EXPECT_EQ(min_linear, kTransferLog100Sqrt10Threshold / 2.0f); | |
| } else { | |
| EXPECT_EQ(min_linear, 0.0f); | |
| } | |
| if (tc == AVIF_TRANSFER_CHARACTERISTICS_PQ) { | |
| EXPECT_NEAR(max_linear, 10000.0f / 203.0f, | |
| 0.00001); // PQ max extended SDR value. | |
| } else if (tc == AVIF_TRANSFER_CHARACTERISTICS_HLG) { | |
| EXPECT_NEAR(max_linear, 1000.0f / 203.0f, | |
| 0.00001); // HLG max extended SDR value. | |
| } else if (tc == AVIF_TRANSFER_CHARACTERISTICS_SMPTE428) { | |
| // See formula in Table 3 of ITU-T H.273. | |
| EXPECT_NEAR(max_linear, 52.37f / 48.0f, 0.00001f); | |
| } else { | |
| EXPECT_EQ(max_linear, 1.0f); | |
| } | |
| } | |
| } | |
| TEST(TransferCharacteristicsTest, NegativeRoundTrip) { | |
| // Transfer functions that accept negative values. | |
| const avifTransferCharacteristics tcs[] = { | |
| AVIF_TRANSFER_CHARACTERISTICS_IEC61966, | |
| AVIF_TRANSFER_CHARACTERISTICS_BT1361, | |
| }; | |
| for (const avifTransferCharacteristics tc : tcs) { | |
| SCOPED_TRACE("transfer characteristics: " + std::to_string(tc)); | |
| const avifTransferFunction to_linear = | |
| avifTransferCharacteristicsGetGammaToLinearFunction(tc); | |
| const avifTransferFunction to_gamma = | |
| avifTransferCharacteristicsGetLinearToGammaFunction(tc); | |
| const float test_values[] = {-0.1f, -0.05f, -0.2f}; | |
| for (const float v : test_values) { | |
| // Check round trips. | |
| EXPECT_NEAR(to_linear(to_gamma(v)), v, 0.00001f); | |
| EXPECT_NEAR(to_gamma(to_linear(v)), v, 0.00001f); | |
| } | |
| } | |
| } | |
| // Check that the liner->gamma function has the right shape, i.e. it's mostly | |
| // above the y=x diagonal. | |
| // This detects bugs where the linear->gamma and | |
| // gamma->linear implementations are swapped. | |
| TEST(TransferCharacteristicsTest, ToGammaHasCorrectShape) { | |
| for (int tc_idx = 0; tc_idx <= kMaxTransferCharacteristic; ++tc_idx) { | |
| const avifTransferCharacteristics tc = (avifTransferCharacteristics)tc_idx; | |
| SCOPED_TRACE("transfer characteristics: " + std::to_string(tc)); | |
| const avifTransferFunction to_gamma = | |
| avifTransferCharacteristicsGetLinearToGammaFunction(tc); | |
| constexpr int kSteps = 20; | |
| for (int j = 0; j <= kSteps; ++j) { | |
| const float linear = static_cast<float>(j) / kSteps; | |
| float extended_sdr_scaled = linear; | |
| if (tc == AVIF_TRANSFER_CHARACTERISTICS_PQ) { | |
| // Scale to the whole range. | |
| extended_sdr_scaled *= 10000.0f / 203.0f; | |
| } else if (tc == AVIF_TRANSFER_CHARACTERISTICS_HLG) { | |
| extended_sdr_scaled *= 1000.0f / 203.0f; | |
| } | |
| const float gamma = to_gamma(extended_sdr_scaled); | |
| if (tc == AVIF_TRANSFER_CHARACTERISTICS_SMPTE428 && linear > 0.9f) { | |
| continue; // Smpte428 is a bit below the y=x diagonal at the high end. | |
| } | |
| // Check the point is above (or at) the y=x diagonal, with some tolerance. | |
| ASSERT_GE(gamma, linear - 1e-6f); | |
| } | |
| } | |
| } | |
| } // namespace | |
| } // namespace avif |