blob: 75c94660b580a1f87990a80ddb6bb165d8c6c02f [file] [log] [blame]
// Copyright 2023 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
#include <cmath>
#include <fstream>
#include <iostream>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "avif/avif_cxx.h"
#include "avif/internal.h"
#include "avifincrtest_helpers.h"
#include "aviftest_helpers.h"
#include "gtest/gtest.h"
namespace avif {
namespace {
using ::testing::Values;
// Used to pass the data folder path to the GoogleTest suites.
const char* data_path = nullptr;
void CheckGainMapMetadataMatches(const avifGainMapMetadata& lhs,
const avifGainMapMetadata& rhs) {
EXPECT_EQ(lhs.baseHdrHeadroomN, rhs.baseHdrHeadroomN);
EXPECT_EQ(lhs.baseHdrHeadroomD, rhs.baseHdrHeadroomD);
EXPECT_EQ(lhs.alternateHdrHeadroomN, rhs.alternateHdrHeadroomN);
EXPECT_EQ(lhs.alternateHdrHeadroomD, rhs.alternateHdrHeadroomD);
for (int c = 0; c < 3; ++c) {
SCOPED_TRACE(c);
EXPECT_EQ(lhs.baseOffsetN[c], rhs.baseOffsetN[c]);
EXPECT_EQ(lhs.baseOffsetD[c], rhs.baseOffsetD[c]);
EXPECT_EQ(lhs.alternateOffsetN[c], rhs.alternateOffsetN[c]);
EXPECT_EQ(lhs.alternateOffsetD[c], rhs.alternateOffsetD[c]);
EXPECT_EQ(lhs.gainMapGammaN[c], rhs.gainMapGammaN[c]);
EXPECT_EQ(lhs.gainMapGammaD[c], rhs.gainMapGammaD[c]);
EXPECT_EQ(lhs.gainMapMinN[c], rhs.gainMapMinN[c]);
EXPECT_EQ(lhs.gainMapMinD[c], rhs.gainMapMinD[c]);
EXPECT_EQ(lhs.gainMapMaxN[c], rhs.gainMapMaxN[c]);
EXPECT_EQ(lhs.gainMapMaxD[c], rhs.gainMapMaxD[c]);
}
}
avifGainMapMetadata GetTestGainMapMetadata(bool base_rendition_is_hdr) {
avifGainMapMetadata metadata = {};
metadata.useBaseColorSpace = true;
metadata.baseHdrHeadroomN = 0;
metadata.baseHdrHeadroomD = 1;
metadata.alternateHdrHeadroomN = 6;
metadata.alternateHdrHeadroomD = 2;
if (base_rendition_is_hdr) {
std::swap(metadata.baseHdrHeadroomN, metadata.alternateHdrHeadroomN);
std::swap(metadata.baseHdrHeadroomD, metadata.alternateHdrHeadroomD);
}
for (int c = 0; c < 3; ++c) {
metadata.baseOffsetN[c] = 10 * c;
metadata.baseOffsetD[c] = 1000;
metadata.alternateOffsetN[c] = 20 * c;
metadata.alternateOffsetD[c] = 1000;
metadata.gainMapGammaN[c] = 1;
metadata.gainMapGammaD[c] = c + 1;
metadata.gainMapMinN[c] = -1;
metadata.gainMapMinD[c] = c + 1;
metadata.gainMapMaxN[c] = 10 + c + 1;
metadata.gainMapMaxD[c] = c + 1;
}
return metadata;
}
ImagePtr CreateTestImageWithGainMap(bool base_rendition_is_hdr) {
ImagePtr image =
testutil::CreateImage(/*width=*/12, /*height=*/34, /*depth=*/10,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_ALL);
if (image == nullptr) {
return nullptr;
}
image->transferCharacteristics =
(avifTransferCharacteristics)(base_rendition_is_hdr
? AVIF_TRANSFER_CHARACTERISTICS_PQ
: AVIF_TRANSFER_CHARACTERISTICS_SRGB);
testutil::FillImageGradient(image.get());
ImagePtr gain_map =
testutil::CreateImage(/*width=*/6, /*height=*/17, /*depth=*/8,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_YUV);
if (gain_map == nullptr) {
return nullptr;
}
testutil::FillImageGradient(gain_map.get());
image->gainMap = avifGainMapCreate();
if (image->gainMap == nullptr) {
return nullptr;
}
image->gainMap->image = gain_map.release(); // 'image' now owns the gain map.
image->gainMap->metadata = GetTestGainMapMetadata(base_rendition_is_hdr);
if (base_rendition_is_hdr) {
image->clli.maxCLL = 10;
image->clli.maxPALL = 5;
image->gainMap->altDepth = 8;
image->gainMap->altPlaneCount = 3;
image->gainMap->altColorPrimaries = AVIF_COLOR_PRIMARIES_BT601;
image->gainMap->altTransferCharacteristics =
AVIF_TRANSFER_CHARACTERISTICS_SRGB;
image->gainMap->altMatrixCoefficients = AVIF_MATRIX_COEFFICIENTS_SMPTE2085;
} else {
image->gainMap->altCLLI.maxCLL = 10;
image->gainMap->altCLLI.maxPALL = 5;
image->gainMap->altDepth = 10;
image->gainMap->altPlaneCount = 3;
image->gainMap->altColorPrimaries = AVIF_COLOR_PRIMARIES_BT2020;
image->gainMap->altTransferCharacteristics =
AVIF_TRANSFER_CHARACTERISTICS_PQ;
image->gainMap->altMatrixCoefficients = AVIF_MATRIX_COEFFICIENTS_SMPTE2085;
}
return image;
}
TEST(GainMapTest, EncodeDecodeBaseImageSdr) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = AVIF_TRUE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
result = avifDecoderSetIOMemory(decoder.get(), encoded.data, encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Just parse the image first.
result = avifDecoderParse(decoder.get());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
avifImage* decoded = decoder->image;
ASSERT_NE(decoded, nullptr);
// Verify that the gain map is present and matches the input.
EXPECT_TRUE(decoder->gainMapPresent);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
EXPECT_EQ(decoded->gainMap->image->matrixCoefficients,
image->gainMap->image->matrixCoefficients);
EXPECT_EQ(decoded->gainMap->altCLLI.maxCLL, image->gainMap->altCLLI.maxCLL);
EXPECT_EQ(decoded->gainMap->altCLLI.maxPALL, image->gainMap->altCLLI.maxPALL);
EXPECT_EQ(decoded->gainMap->altDepth, 10u);
EXPECT_EQ(decoded->gainMap->altPlaneCount, 3u);
EXPECT_EQ(decoded->gainMap->altColorPrimaries, AVIF_COLOR_PRIMARIES_BT2020);
EXPECT_EQ(decoded->gainMap->altTransferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_PQ);
EXPECT_EQ(decoded->gainMap->altMatrixCoefficients,
AVIF_MATRIX_COEFFICIENTS_SMPTE2085);
EXPECT_EQ(decoded->gainMap->image->width, image->gainMap->image->width);
EXPECT_EQ(decoded->gainMap->image->height, image->gainMap->image->height);
EXPECT_EQ(decoded->gainMap->image->depth, image->gainMap->image->depth);
CheckGainMapMetadataMatches(decoded->gainMap->metadata,
image->gainMap->metadata);
// Decode the image.
result = avifDecoderNextImage(decoder.get());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the input and decoded images are close.
EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0);
EXPECT_GT(testutil::GetPsnr(*image->gainMap->image, *decoded->gainMap->image),
40.0);
// Uncomment the following to save the encoded image as an AVIF file.
// std::ofstream("/tmp/avifgainmaptest_basesdr.avif", std::ios::binary)
// .write(reinterpret_cast<char*>(encoded.data), encoded.size);
}
TEST(GainMapTest, EncodeDecodeBaseImageHdr) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/true);
ASSERT_NE(image, nullptr);
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = AVIF_TRUE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the input and decoded images are close.
EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0);
// Verify that the gain map is present and matches the input.
EXPECT_TRUE(decoder->gainMapPresent);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
EXPECT_GT(testutil::GetPsnr(*image->gainMap->image, *decoded->gainMap->image),
40.0);
EXPECT_EQ(decoded->clli.maxCLL, image->clli.maxCLL);
EXPECT_EQ(decoded->clli.maxPALL, image->clli.maxPALL);
EXPECT_EQ(decoded->gainMap->altCLLI.maxCLL, 0u);
EXPECT_EQ(decoded->gainMap->altCLLI.maxPALL, 0u);
EXPECT_EQ(decoded->gainMap->altDepth, 8u);
EXPECT_EQ(decoded->gainMap->altPlaneCount, 3u);
EXPECT_EQ(decoded->gainMap->altColorPrimaries, AVIF_COLOR_PRIMARIES_BT601);
EXPECT_EQ(decoded->gainMap->altTransferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_SRGB);
EXPECT_EQ(decoded->gainMap->altMatrixCoefficients,
AVIF_MATRIX_COEFFICIENTS_SMPTE2085);
EXPECT_EQ(decoded->gainMap->image->width, image->gainMap->image->width);
EXPECT_EQ(decoded->gainMap->image->height, image->gainMap->image->height);
EXPECT_EQ(decoded->gainMap->image->depth, image->gainMap->image->depth);
CheckGainMapMetadataMatches(decoded->gainMap->metadata,
image->gainMap->metadata);
// Uncomment the following to save the encoded image as an AVIF file.
// std::ofstream("/tmp/avifgainmaptest_basehdr.avif", std::ios::binary)
// .write(reinterpret_cast<char*>(encoded.data), encoded.size);
}
TEST(GainMapTest, EncodeDecodeOrientedNotEqual) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
image->gainMap->image->transformFlags = AVIF_TRANSFORM_IMIR;
// The gain map should have no transformative property. Expect a failure.
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded),
AVIF_RESULT_ENCODE_GAIN_MAP_FAILED);
}
TEST(GainMapTest, EncodeDecodeOriented) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
image->transformFlags = AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR;
image->irot.angle = 1;
image->imir.axis = 0;
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded),
AVIF_RESULT_OK);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = AVIF_TRUE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
ASSERT_EQ(avifDecoderSetIOMemory(decoder.get(), encoded.data, encoded.size),
AVIF_RESULT_OK);
ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK);
// Verify that the transformative properties were kept.
EXPECT_EQ(decoder->image->transformFlags, image->transformFlags);
EXPECT_EQ(decoder->image->irot.angle, image->irot.angle);
EXPECT_EQ(decoder->image->imir.axis, image->imir.axis);
EXPECT_EQ(decoder->image->gainMap->image->transformFlags,
AVIF_TRANSFORM_NONE);
}
TEST(GainMapTest, EncodeDecodeMetadataSameDenominator) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/true);
ASSERT_NE(image, nullptr);
const uint32_t kDenominator = 1000;
image->gainMap->metadata.baseHdrHeadroomD = kDenominator;
image->gainMap->metadata.alternateHdrHeadroomD = kDenominator;
for (int c = 0; c < 3; ++c) {
image->gainMap->metadata.baseOffsetD[c] = kDenominator;
image->gainMap->metadata.alternateOffsetD[c] = kDenominator;
image->gainMap->metadata.gainMapGammaD[c] = kDenominator;
image->gainMap->metadata.gainMapMinD[c] = kDenominator;
image->gainMap->metadata.gainMapMaxD[c] = kDenominator;
}
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = AVIF_FALSE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the gain map metadata matches the input.
CheckGainMapMetadataMatches(decoded->gainMap->metadata,
image->gainMap->metadata);
}
TEST(GainMapTest, EncodeDecodeMetadataAllChannelsIdentical) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/true);
ASSERT_NE(image, nullptr);
for (int c = 0; c < 3; ++c) {
image->gainMap->metadata.baseOffsetN[c] = 1;
image->gainMap->metadata.baseOffsetD[c] = 2;
image->gainMap->metadata.alternateOffsetN[c] = 3;
image->gainMap->metadata.alternateOffsetD[c] = 4;
image->gainMap->metadata.gainMapGammaN[c] = 5;
image->gainMap->metadata.gainMapGammaD[c] = 6;
image->gainMap->metadata.gainMapMinN[c] = 7;
image->gainMap->metadata.gainMapMinD[c] = 8;
image->gainMap->metadata.gainMapMaxN[c] = 9;
image->gainMap->metadata.gainMapMaxD[c] = 10;
}
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = AVIF_FALSE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the gain map metadata matches the input.
CheckGainMapMetadataMatches(decoded->gainMap->metadata,
image->gainMap->metadata);
}
TEST(GainMapTest, EncodeDecodeGrid) {
std::vector<ImagePtr> cells;
std::vector<const avifImage*> cell_ptrs;
std::vector<const avifImage*> gain_map_ptrs;
constexpr int kGridCols = 2;
constexpr int kGridRows = 2;
constexpr int kCellWidth = 128;
constexpr int kCellHeight = 200;
avifGainMapMetadata gain_map_metadata =
GetTestGainMapMetadata(/*base_rendition_is_hdr=*/true);
for (int i = 0; i < kGridCols * kGridRows; ++i) {
ImagePtr image =
testutil::CreateImage(kCellWidth, kCellHeight, /*depth=*/10,
AVIF_PIXEL_FORMAT_YUV444, AVIF_PLANES_ALL);
ASSERT_NE(image, nullptr);
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ;
testutil::FillImageGradient(image.get());
ImagePtr gain_map =
testutil::CreateImage(kCellWidth / 2, kCellHeight / 2, /*depth=*/8,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_YUV);
ASSERT_NE(gain_map, nullptr);
testutil::FillImageGradient(gain_map.get());
// 'image' now owns the gain map.
image->gainMap = avifGainMapCreate();
ASSERT_NE(image->gainMap, nullptr);
image->gainMap->image = gain_map.release();
// all cells must have the same metadata
image->gainMap->metadata = gain_map_metadata;
cell_ptrs.push_back(image.get());
gain_map_ptrs.push_back(image->gainMap->image);
cells.push_back(std::move(image));
}
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result =
avifEncoderAddImageGrid(encoder.get(), kGridCols, kGridRows,
cell_ptrs.data(), AVIF_ADD_IMAGE_FLAG_SINGLE);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
result = avifEncoderFinish(encoder.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = AVIF_TRUE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
ImagePtr merged = testutil::CreateImage(
static_cast<int>(decoded->width), static_cast<int>(decoded->height),
decoded->depth, decoded->yuvFormat, AVIF_PLANES_ALL);
ASSERT_EQ(testutil::MergeGrid(kGridCols, kGridRows, cell_ptrs, merged.get()),
AVIF_RESULT_OK);
ImagePtr merged_gain_map = testutil::CreateImage(
static_cast<int>(decoded->gainMap->image->width),
static_cast<int>(decoded->gainMap->image->height),
decoded->gainMap->image->depth, decoded->gainMap->image->yuvFormat,
AVIF_PLANES_YUV);
ASSERT_EQ(testutil::MergeGrid(kGridCols, kGridRows, gain_map_ptrs,
merged_gain_map.get()),
AVIF_RESULT_OK);
// Verify that the input and decoded images are close.
ASSERT_GT(testutil::GetPsnr(*merged, *decoded), 40.0);
// Verify that the gain map is present and matches the input.
EXPECT_TRUE(decoder->gainMapPresent);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
ASSERT_GT(testutil::GetPsnr(*merged_gain_map, *decoded->gainMap->image),
40.0);
CheckGainMapMetadataMatches(decoded->gainMap->metadata, gain_map_metadata);
// Check that non-incremental and incremental decodings of a grid AVIF produce
// the same pixels.
ASSERT_EQ(testutil::DecodeNonIncrementallyAndIncrementally(
encoded, decoder.get(),
/*is_persistent=*/true, /*give_size_hint=*/true,
/*use_nth_image_api=*/false, kCellHeight,
/*enable_fine_incremental_check=*/true),
AVIF_RESULT_OK);
// Uncomment the following to save the encoded image as an AVIF file.
// std::ofstream("/tmp/avifgainmaptest_grid.avif", std::ios::binary)
// .write(reinterpret_cast<char*>(encoded.data), encoded.size);
}
TEST(GainMapTest, InvalidGrid) {
std::vector<ImagePtr> cells;
std::vector<const avifImage*> cell_ptrs;
constexpr int kGridCols = 2;
constexpr int kGridRows = 2;
avifGainMapMetadata gain_map_metadata =
GetTestGainMapMetadata(/*base_rendition_is_hdr=*/true);
for (int i = 0; i < kGridCols * kGridRows; ++i) {
ImagePtr image =
testutil::CreateImage(/*width=*/64, /*height=*/100, /*depth=*/10,
AVIF_PIXEL_FORMAT_YUV444, AVIF_PLANES_ALL);
ASSERT_NE(image, nullptr);
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ;
testutil::FillImageGradient(image.get());
ImagePtr gain_map =
testutil::CreateImage(/*width=*/64, /*height=*/100, /*depth=*/8,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_YUV);
ASSERT_NE(gain_map, nullptr);
testutil::FillImageGradient(gain_map.get());
// 'image' now owns the gain map.
image->gainMap = avifGainMapCreate();
ASSERT_NE(image->gainMap, nullptr);
image->gainMap->image = gain_map.release();
// all cells must have the same metadata
image->gainMap->metadata = gain_map_metadata;
cell_ptrs.push_back(image.get());
cells.push_back(std::move(image));
}
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result;
// Invalid: one cell has the wrong size.
cells[1]->gainMap->image->height = 90;
result =
avifEncoderAddImageGrid(encoder.get(), kGridCols, kGridRows,
cell_ptrs.data(), AVIF_ADD_IMAGE_FLAG_SINGLE);
EXPECT_EQ(result, AVIF_RESULT_INVALID_IMAGE_GRID)
<< avifResultToString(result) << " " << encoder->diag.error;
cells[1]->gainMap->image->height =
cells[0]->gainMap->image->height; // Revert.
// Invalid: one cell has a different depth.
cells[1]->gainMap->image->depth = 12;
result =
avifEncoderAddImageGrid(encoder.get(), kGridCols, kGridRows,
cell_ptrs.data(), AVIF_ADD_IMAGE_FLAG_SINGLE);
EXPECT_EQ(result, AVIF_RESULT_INVALID_IMAGE_GRID)
<< avifResultToString(result) << " " << encoder->diag.error;
cells[1]->gainMap->image->depth = cells[0]->gainMap->image->depth; // Revert.
// Invalid: one cell has different gain map metadata.
cells[1]->gainMap->metadata.gainMapGammaN[0] = 42;
result =
avifEncoderAddImageGrid(encoder.get(), kGridCols, kGridRows,
cell_ptrs.data(), AVIF_ADD_IMAGE_FLAG_SINGLE);
EXPECT_EQ(result, AVIF_RESULT_INVALID_IMAGE_GRID)
<< avifResultToString(result) << " " << encoder->diag.error;
cells[1]->gainMap->metadata.gainMapGammaN[0] =
cells[0]->gainMap->metadata.gainMapGammaN[0]; // Revert.
}
TEST(GainMapTest, SequenceNotSupported) {
ImagePtr image =
testutil::CreateImage(/*width=*/64, /*height=*/100, /*depth=*/10,
AVIF_PIXEL_FORMAT_YUV444, AVIF_PLANES_ALL);
ASSERT_NE(image, nullptr);
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ;
testutil::FillImageGradient(image.get());
ImagePtr gain_map =
testutil::CreateImage(/*width=*/64, /*height=*/100, /*depth=*/8,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_YUV);
ASSERT_NE(gain_map, nullptr);
testutil::FillImageGradient(gain_map.get());
// 'image' now owns the gain map.
image->gainMap = avifGainMapCreate();
ASSERT_NE(image->gainMap, nullptr);
image->gainMap->image = gain_map.release();
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
// Add a first frame.
avifResult result =
avifEncoderAddImage(encoder.get(), image.get(),
/*durationInTimescales=*/2, AVIF_ADD_IMAGE_FLAG_NONE);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
// Add a second frame.
result =
avifEncoderAddImage(encoder.get(), image.get(),
/*durationInTimescales=*/2, AVIF_ADD_IMAGE_FLAG_NONE);
// Image sequences with gain maps are not supported.
ASSERT_EQ(result, AVIF_RESULT_NOT_IMPLEMENTED)
<< avifResultToString(result) << " " << encoder->diag.error;
}
TEST(GainMapTest, IgnoreGainMap) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
// Decode image, with enableDecodingGainMap false by default.
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the input and decoded images are close.
EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0);
// Verify that the gain map was detected...
EXPECT_TRUE(decoder->gainMapPresent);
// ... but not decoded because enableDecodingGainMap is false by default.
EXPECT_EQ(decoded->gainMap, nullptr);
}
TEST(GainMapTest, IgnoreGainMapButReadMetadata) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
// Decode image, with enableDecodingGainMap false by default.
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableParsingGainMapMetadata = AVIF_TRUE; // Read gain map metadata.
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the input and decoded images are close.
EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0);
// Verify that the gain map was detected...
EXPECT_TRUE(decoder->gainMapPresent);
ASSERT_NE(decoded->gainMap, nullptr);
// ... but not decoded because enableDecodingGainMap is false by default.
EXPECT_EQ(decoded->gainMap->image, nullptr);
// Check that the gain map metadata WAS populated.
CheckGainMapMetadataMatches(decoded->gainMap->metadata,
image->gainMap->metadata);
EXPECT_EQ(decoded->gainMap->altDepth, image->gainMap->altDepth);
EXPECT_EQ(decoded->gainMap->altPlaneCount, image->gainMap->altPlaneCount);
EXPECT_EQ(decoded->gainMap->altColorPrimaries,
image->gainMap->altColorPrimaries);
EXPECT_EQ(decoded->gainMap->altTransferCharacteristics,
image->gainMap->altTransferCharacteristics);
EXPECT_EQ(decoded->gainMap->altMatrixCoefficients,
image->gainMap->altMatrixCoefficients);
}
TEST(GainMapTest, DecodeGainMapButIgnoreMetadata) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableParsingGainMapMetadata = AVIF_FALSE;
decoder->enableDecodingGainMap = AVIF_TRUE;
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the gain map was detected...
EXPECT_TRUE(decoder->gainMapPresent);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
EXPECT_GT(testutil::GetPsnr(*image->gainMap->image, *decoded->gainMap->image),
40.0);
// Check that the gain map metadata was not populated.
CheckGainMapMetadataMatches(decoded->gainMap->metadata,
avifGainMapMetadata());
EXPECT_EQ(decoded->gainMap->altDepth, 0);
EXPECT_EQ(decoded->gainMap->altPlaneCount, 0);
EXPECT_EQ(decoded->gainMap->altColorPrimaries,
AVIF_COLOR_PRIMARIES_UNSPECIFIED);
EXPECT_EQ(decoded->gainMap->altTransferCharacteristics,
AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED);
EXPECT_EQ(decoded->gainMap->altMatrixCoefficients,
AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED);
}
TEST(GainMapTest, IgnoreColorAndAlpha) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
// Decode just the gain map.
decoder->ignoreColorAndAlpha = AVIF_TRUE;
decoder->enableDecodingGainMap = AVIF_TRUE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Main image metadata is available.
EXPECT_EQ(decoder->image->width, 12u);
EXPECT_EQ(decoder->image->height, 34u);
// But pixels are not.
EXPECT_EQ(decoder->image->yuvRowBytes[0], 0u);
EXPECT_EQ(decoder->image->yuvRowBytes[1], 0u);
EXPECT_EQ(decoder->image->yuvRowBytes[2], 0u);
EXPECT_EQ(decoder->image->alphaRowBytes, 0u);
// The gain map was decoded.
EXPECT_TRUE(decoder->gainMapPresent);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
EXPECT_GT(testutil::GetPsnr(*image->gainMap->image, *decoded->gainMap->image),
40.0);
CheckGainMapMetadataMatches(decoded->gainMap->metadata,
image->gainMap->metadata);
}
TEST(GainMapTest, IgnoreAll) {
ImagePtr image = CreateTestImageWithGainMap(/*base_rendition_is_hdr=*/false);
ASSERT_NE(image, nullptr);
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
// Ignore both the main image and the gain map.
decoder->ignoreColorAndAlpha = AVIF_TRUE;
decoder->enableDecodingGainMap = AVIF_FALSE;
// But do read the gain map metadata.
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
// Parsing just the header should work.
ASSERT_EQ(avifDecoderSetIOMemory(decoder.get(), encoded.data, encoded.size),
AVIF_RESULT_OK);
ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK);
EXPECT_TRUE(decoder->gainMapPresent);
CheckGainMapMetadataMatches(decoder->image->gainMap->metadata,
image->gainMap->metadata);
ASSERT_EQ(decoder->image->gainMap->image, nullptr);
// But trying to access the next image should give an error because both
// ignoreColorAndAlpha and enableDecodingGainMap are set.
ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_NO_CONTENT);
}
TEST(GainMapTest, NoGainMap) {
// Create a simple image without a gain map.
ImagePtr image =
testutil::CreateImage(/*width=*/12, /*height=*/34, /*depth=*/10,
AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_ALL);
ASSERT_NE(image, nullptr);
image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
testutil::FillImageGradient(image.get());
EncoderPtr encoder(avifEncoderCreate());
ASSERT_NE(encoder, nullptr);
testutil::AvifRwData encoded;
avifResult result = avifEncoderWrite(encoder.get(), image.get(), &encoded);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << encoder->diag.error;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
// Enable gain map decoding.
decoder->enableDecodingGainMap = AVIF_TRUE;
decoder->enableParsingGainMapMetadata = AVIF_TRUE;
result = avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
encoded.size);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Verify that the input and decoded images are close.
EXPECT_GT(testutil::GetPsnr(*image, *decoded), 40.0);
// Verify that no gain map was found.
EXPECT_FALSE(decoder->gainMapPresent);
EXPECT_EQ(decoded->gainMap, nullptr);
}
TEST(GainMapTest, DecodeGainMapGrid) {
const std::string path =
std::string(data_path) + "color_grid_gainmap_different_grid.avif";
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
avifResult result = avifDecoderSetIOFile(decoder.get(), path.c_str());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
// Just parse the image first.
result = avifDecoderParse(decoder.get());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
avifImage* decoded = decoder->image;
ASSERT_NE(decoded, nullptr);
// Verify that the gain map is present and matches the input.
EXPECT_TRUE(decoder->gainMapPresent);
// Color+alpha: 4x3 grid of 128x200 tiles.
EXPECT_EQ(decoded->width, 128u * 4u);
EXPECT_EQ(decoded->height, 200u * 3u);
EXPECT_EQ(decoded->depth, 10u);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
// Gain map: 2x2 grid of 64x80 tiles.
EXPECT_EQ(decoded->gainMap->image->width, 64u * 2u);
EXPECT_EQ(decoded->gainMap->image->height, 80u * 2u);
EXPECT_EQ(decoded->gainMap->image->depth, 8u);
EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomN, 6u);
EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomD, 2u);
// Decode the image.
result = avifDecoderNextImage(decoder.get());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
}
TEST(GainMapTest, DecodeColorGridGainMapNoGrid) {
const std::string path =
std::string(data_path) + "color_grid_alpha_grid_gainmap_nogrid.avif";
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_OK);
// Color+alpha: 4x3 grid of 128x200 tiles.
EXPECT_EQ(decoded->width, 128u * 4u);
EXPECT_EQ(decoded->height, 200u * 3u);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
// Gain map: single image of size 64x80.
EXPECT_EQ(decoded->gainMap->image->width, 64u);
EXPECT_EQ(decoded->gainMap->image->height, 80u);
EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomN, 6u);
EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomD, 2u);
}
TEST(GainMapTest, DecodeColorNoGridGainMapGrid) {
const std::string path =
std::string(data_path) + "color_nogrid_alpha_nogrid_gainmap_grid.avif";
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_OK);
// Color+alpha: single image of size 128x200 .
EXPECT_EQ(decoded->width, 128u);
EXPECT_EQ(decoded->height, 200u);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_NE(decoded->gainMap->image, nullptr);
// Gain map: 2x2 grid of 64x80 tiles.
EXPECT_EQ(decoded->gainMap->image->width, 64u * 2u);
EXPECT_EQ(decoded->gainMap->image->height, 80u * 2u);
EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomN, 6u);
EXPECT_EQ(decoded->gainMap->metadata.baseHdrHeadroomD, 2u);
}
TEST(GainMapTest, DecodeUnsupportedVersion) {
// The two test files should produce the same results:
// One has an unsupported 'version' field, the other an unsupported
// 'minimum_version' field, but the behavior of these two fields is the same.
for (const std::string image : {"unsupported_gainmap_version.avif",
"unsupported_gainmap_minimum_version.avif"}) {
SCOPED_TRACE(image);
const std::string path = std::string(data_path) + image;
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
// Parse with various enableDecodingGainMap and enableParsingGainMapMetadata
// settings.
decoder->enableDecodingGainMap = false;
decoder->enableParsingGainMapMetadata = false;
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_OK);
// Gain map marked as present because the metadata was not parsed, so we
// don't know it's not supported.
EXPECT_EQ(decoder->gainMapPresent, true);
ASSERT_EQ(decoded->gainMap, nullptr);
ASSERT_EQ(avifDecoderReset(decoder.get()), AVIF_RESULT_OK);
decoder->enableDecodingGainMap = false;
decoder->enableParsingGainMapMetadata = true;
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_OK);
// Gain map marked as not present because the metadata is not supported.
EXPECT_EQ(decoder->gainMapPresent, false);
ASSERT_EQ(decoded->gainMap, nullptr);
ASSERT_EQ(avifDecoderReset(decoder.get()), AVIF_RESULT_OK);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = false;
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_OK);
// Gain map marked as present because the metadata was not parsed, so we
// don't know it's not supported.
EXPECT_EQ(decoder->gainMapPresent, true);
ASSERT_NE(decoded->gainMap, nullptr);
ASSERT_EQ(avifDecoderReset(decoder.get()), AVIF_RESULT_OK);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_OK);
// Gain map marked as not present because the metadata is not supported.
EXPECT_EQ(decoder->gainMapPresent, false);
ASSERT_EQ(decoded->gainMap, nullptr);
}
}
TEST(GainMapTest, ExtraBytesAfterGainMapMetadataUnsupportedWriterVersion) {
const std::string path =
std::string(data_path) +
"unsupported_gainmap_writer_version_with_extra_bytes.avif";
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = false;
decoder->enableParsingGainMapMetadata = true;
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_OK);
// Decodes successfully: there are extra bytes at the end of the gain map
// metadata but that's expected as the writer_version field is higher
// that supported.
EXPECT_EQ(decoder->gainMapPresent, true);
ASSERT_NE(decoded->gainMap, nullptr);
}
TEST(GainMapTest, ExtraBytesAfterGainMapMetadataSupporterWriterVersion) {
const std::string path =
std::string(data_path) +
"supported_gainmap_writer_version_with_extra_bytes.avif";
ImagePtr decoded(avifImageCreateEmpty());
ASSERT_NE(decoded, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = false;
decoder->enableParsingGainMapMetadata = true;
// Fails to decode: there are extra bytes at the end of the gain map metadata
// that shouldn't be there.
ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()),
AVIF_RESULT_INVALID_TONE_MAPPED_IMAGE);
}
#define EXPECT_FRACTION_NEAR(numerator, denominator, expected) \
EXPECT_NEAR(std::abs((double)numerator / denominator), expected, \
expected * 0.001);
TEST(GainMapTest, ConvertMetadata) {
avifGainMapMetadataDouble metadata_double = {};
metadata_double.gainMapMin[0] = 1.0;
metadata_double.gainMapMin[1] = 1.1;
metadata_double.gainMapMin[2] = 1.2;
metadata_double.gainMapMax[0] = 10.0;
metadata_double.gainMapMax[1] = 10.1;
metadata_double.gainMapMax[2] = 10.2;
metadata_double.gainMapGamma[0] = 1.0;
metadata_double.gainMapGamma[1] = 1.0;
metadata_double.gainMapGamma[2] = 1.2;
metadata_double.baseOffset[0] = 1.0 / 32.0;
metadata_double.baseOffset[1] = 1.0 / 64.0;
metadata_double.baseOffset[2] = 1.0 / 128.0;
metadata_double.alternateOffset[0] = 0.004564;
metadata_double.alternateOffset[1] = 0.0;
metadata_double.baseHdrHeadroom = 1.0;
metadata_double.alternateHdrHeadroom = 10.0;
// Convert to avifGainMapMetadata.
avifGainMapMetadata metadata = {};
ASSERT_TRUE(
avifGainMapMetadataDoubleToFractions(&metadata, &metadata_double));
for (int i = 0; i < 3; ++i) {
EXPECT_FRACTION_NEAR(metadata.gainMapMinN[i], metadata.gainMapMinD[i],
metadata_double.gainMapMin[i]);
EXPECT_FRACTION_NEAR(metadata.gainMapMaxN[i], metadata.gainMapMaxD[i],
metadata_double.gainMapMax[i]);
EXPECT_FRACTION_NEAR(metadata.gainMapGammaN[i], metadata.gainMapGammaD[i],
metadata_double.gainMapGamma[i]);
EXPECT_FRACTION_NEAR(metadata.baseOffsetN[i], metadata.baseOffsetD[i],
metadata_double.baseOffset[i]);
EXPECT_FRACTION_NEAR(metadata.alternateOffsetN[i],
metadata.alternateOffsetD[i],
metadata_double.alternateOffset[i]);
}
EXPECT_FRACTION_NEAR(metadata.baseHdrHeadroomN, metadata.baseHdrHeadroomD,
metadata_double.baseHdrHeadroom);
EXPECT_FRACTION_NEAR(metadata.alternateHdrHeadroomN,
metadata.alternateHdrHeadroomD,
metadata_double.alternateHdrHeadroom);
// Convert back to avifGainMapMetadataDouble.
avifGainMapMetadataDouble metadata_double2 = {};
ASSERT_TRUE(
avifGainMapMetadataFractionsToDouble(&metadata_double2, &metadata));
constexpr double kEpsilon = 0.000001;
for (int i = 0; i < 3; ++i) {
EXPECT_NEAR(metadata_double2.gainMapMin[i], metadata_double.gainMapMin[i],
kEpsilon);
EXPECT_NEAR(metadata_double2.gainMapMax[i], metadata_double.gainMapMax[i],
kEpsilon);
EXPECT_NEAR(metadata_double2.gainMapGamma[i],
metadata_double.gainMapGamma[i], kEpsilon);
EXPECT_NEAR(metadata_double2.baseOffset[i], metadata_double.baseOffset[i],
kEpsilon);
EXPECT_NEAR(metadata_double2.alternateOffset[i],
metadata_double.alternateOffset[i], kEpsilon);
}
EXPECT_NEAR(metadata_double2.baseHdrHeadroom, metadata_double.baseHdrHeadroom,
kEpsilon);
EXPECT_NEAR(metadata_double2.alternateHdrHeadroom,
metadata_double.alternateHdrHeadroom, kEpsilon);
}
TEST(GainMapTest, ConvertMetadataToFractionInvalid) {
avifGainMapMetadataDouble metadata_double = {};
metadata_double.gainMapGamma[0] = -42; // A negative value is invalid!
avifGainMapMetadata metadata = {};
ASSERT_FALSE(
avifGainMapMetadataDoubleToFractions(&metadata, &metadata_double));
}
TEST(GainMapTest, ConvertMetadataToDoubleInvalid) {
avifGainMapMetadata metadata = {}; // Denominators are zero.
avifGainMapMetadataDouble metadata_double = {};
ASSERT_FALSE(
avifGainMapMetadataFractionsToDouble(&metadata_double, &metadata));
}
static void SwapBaseAndAlternate(const avifImage& new_alternate,
avifGainMap& gain_map) {
avifGainMapMetadata& metadata = gain_map.metadata;
metadata.useBaseColorSpace = !metadata.useBaseColorSpace;
std::swap(metadata.baseHdrHeadroomN, metadata.alternateHdrHeadroomN);
std::swap(metadata.baseHdrHeadroomD, metadata.alternateHdrHeadroomD);
for (int c = 0; c < 3; ++c) {
std::swap(metadata.baseOffsetN[c], metadata.alternateOffsetN[c]);
std::swap(metadata.baseOffsetD[c], metadata.alternateOffsetD[c]);
}
gain_map.altColorPrimaries = new_alternate.colorPrimaries;
gain_map.altTransferCharacteristics = new_alternate.transferCharacteristics;
gain_map.altMatrixCoefficients = new_alternate.matrixCoefficients;
gain_map.altYUVRange = new_alternate.yuvRange;
gain_map.altDepth = new_alternate.depth;
gain_map.altPlaneCount =
(new_alternate.yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3;
gain_map.altCLLI = new_alternate.clli;
}
// Test to generate some test images used by other tests and fuzzers.
// Allows regenerating the images if the gain map format changes.
TEST(GainMapTest, CreateTestImages) {
// Set to true to update test images.
constexpr bool kUpdateTestImages = false;
// Generate seine_sdr_gainmap_big_srgb.jpg
{
const std::string path =
std::string(data_path) + "seine_sdr_gainmap_srgb.avif";
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
ImagePtr image(avifImageCreateEmpty());
ASSERT_NE(image, nullptr);
avifResult result =
avifDecoderReadFile(decoder.get(), image.get(), path.c_str());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
ASSERT_NE(image->gainMap, nullptr);
ASSERT_NE(image->gainMap->image, nullptr);
avifDiagnostics diag;
result =
avifImageScale(image->gainMap->image, image->gainMap->image->width * 2,
image->gainMap->image->height * 2, &diag);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
const testutil::AvifRwData encoded =
testutil::Encode(image.get(), /*speed=*/9, /*quality=*/90);
ASSERT_GT(encoded.size, 0u);
if (kUpdateTestImages) {
std::ofstream(std::string(data_path) + "seine_sdr_gainmap_big_srgb.avif",
std::ios::binary)
.write(reinterpret_cast<char*>(encoded.data), encoded.size);
}
}
// Generate seine_hdr_gainmap_srgb.avif and seine_hdr_gainmap_small_srgb.avif
{
ImagePtr hdr_image =
testutil::DecodeFile(std::string(data_path) + "seine_hdr_srgb.avif");
ASSERT_NE(hdr_image, nullptr);
const std::string sdr_path =
std::string(data_path) + "seine_sdr_gainmap_srgb.avif";
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
ImagePtr sdr_with_gainmap(avifImageCreateEmpty());
ASSERT_NE(sdr_with_gainmap, nullptr);
avifResult result = avifDecoderReadFile(
decoder.get(), sdr_with_gainmap.get(), sdr_path.c_str());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
ASSERT_NE(sdr_with_gainmap->gainMap->image, nullptr);
// Move the gain map from the sdr image to the hdr image.
hdr_image->gainMap = sdr_with_gainmap->gainMap;
sdr_with_gainmap->gainMap = nullptr;
SwapBaseAndAlternate(*sdr_with_gainmap, *hdr_image->gainMap);
hdr_image->gainMap->altColorPrimaries = sdr_with_gainmap->colorPrimaries;
hdr_image->gainMap->altTransferCharacteristics =
sdr_with_gainmap->transferCharacteristics;
hdr_image->gainMap->altMatrixCoefficients =
sdr_with_gainmap->matrixCoefficients;
hdr_image->gainMap->altDepth = sdr_with_gainmap->depth;
hdr_image->gainMap->altPlaneCount = 3;
const testutil::AvifRwData encoded =
testutil::Encode(hdr_image.get(), /*speed=*/9, /*quality=*/90);
ASSERT_GT(encoded.size, 0u);
if (kUpdateTestImages) {
std::ofstream(std::string(data_path) + "seine_hdr_gainmap_srgb.avif",
std::ios::binary)
.write(reinterpret_cast<char*>(encoded.data), encoded.size);
}
avifDiagnostics diag;
result = avifImageScale(hdr_image->gainMap->image,
hdr_image->gainMap->image->width / 2,
hdr_image->gainMap->image->height / 2, &diag);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
const testutil::AvifRwData encoded_small_gainmap =
testutil::Encode(hdr_image.get(), /*speed=*/9, /*quality=*/90);
ASSERT_GT(encoded.size, 0u);
if (kUpdateTestImages) {
std::ofstream(
std::string(data_path) + "seine_hdr_gainmap_small_srgb.avif",
std::ios::binary)
.write(reinterpret_cast<char*>(encoded_small_gainmap.data),
encoded_small_gainmap.size);
}
}
}
class ToneMapTest
: public testing::TestWithParam<std::tuple<
/*source=*/std::string, /*hdr_headroom=*/float,
/*out_depth=*/int,
/*out_transfer=*/avifTransferCharacteristics,
/*out_rgb_format=*/avifRGBFormat,
/*reference=*/std::string, /*min_psnr=*/float, /*max_psnr=*/float>> {
};
void ToneMapImageAndCompareToReference(
const avifImage* base_image, const avifGainMap& gain_map,
float hdr_headroom, int out_depth,
avifTransferCharacteristics out_transfer_characteristics,
avifRGBFormat out_rgb_format, const avifImage* reference_image,
float min_psnr, float max_psnr, float* psnr_out = nullptr) {
SCOPED_TRACE("hdr_headroom: " + std::to_string(hdr_headroom));
testutil::AvifRgbImage tone_mapped_rgb(base_image, out_depth, out_rgb_format);
ImagePtr tone_mapped(
avifImageCreate(tone_mapped_rgb.width, tone_mapped_rgb.height,
tone_mapped_rgb.depth, AVIF_PIXEL_FORMAT_YUV444));
tone_mapped->transferCharacteristics = out_transfer_characteristics;
tone_mapped->colorPrimaries = reference_image
? reference_image->colorPrimaries
: base_image->colorPrimaries;
tone_mapped->matrixCoefficients = reference_image
? reference_image->matrixCoefficients
: base_image->matrixCoefficients;
avifDiagnostics diag;
avifResult result = avifImageApplyGainMap(
base_image, &gain_map, hdr_headroom, tone_mapped->colorPrimaries,
tone_mapped->transferCharacteristics, &tone_mapped_rgb,
&tone_mapped->clli, &diag);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << diag.error;
ASSERT_EQ(avifImageRGBToYUV(tone_mapped.get(), &tone_mapped_rgb),
AVIF_RESULT_OK);
if (reference_image != nullptr) {
EXPECT_EQ(out_depth, (int)reference_image->depth);
const double psnr = testutil::GetPsnr(*reference_image, *tone_mapped);
std::cout << "PSNR (tone mapped vs reference): " << psnr << "\n";
EXPECT_GE(psnr, min_psnr);
EXPECT_LE(psnr, max_psnr);
if (psnr_out != nullptr) {
*psnr_out = (float)psnr;
}
}
// Uncomment the following to save the encoded image as an AVIF file.
// const testutil::AvifRwData encoded =
// testutil::Encode(tone_mapped.get(), /*speed=*/9, /*quality=*/90);
// ASSERT_GT(encoded.size, 0u);
// std::ofstream("/tmp/tone_mapped_" + std::to_string(hdr_headroom) +
// ".avif", std::ios::binary)
// .write(reinterpret_cast<char*>(encoded.data), encoded.size);
}
TEST_P(ToneMapTest, ToneMapImage) {
const std::string source = std::get<0>(GetParam());
const float hdr_headroom = std::get<1>(GetParam());
// out_depth and out_transfer_characteristics should match the reference image
// when ther eis one, so that GetPsnr works.
const int out_depth = std::get<2>(GetParam());
const avifTransferCharacteristics out_transfer_characteristics =
std::get<3>(GetParam());
const avifRGBFormat out_rgb_format = std::get<4>(GetParam());
const std::string reference = std::get<5>(GetParam());
const float min_psnr = std::get<6>(GetParam());
const float max_psnr = std::get<7>(GetParam());
ImagePtr reference_image = nullptr;
if (!source.empty()) {
reference_image = testutil::DecodeFile(std::string(data_path) + reference);
}
// Load the source image (that should contain a gain map).
const std::string path = std::string(data_path) + source;
ImagePtr image(avifImageCreateEmpty());
ASSERT_NE(image, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
avifResult result =
avifDecoderReadFile(decoder.get(), image.get(), path.c_str());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
ASSERT_NE(image->gainMap, nullptr);
ASSERT_NE(image->gainMap->image, nullptr);
ToneMapImageAndCompareToReference(image.get(), *image->gainMap, hdr_headroom,
out_depth, out_transfer_characteristics,
out_rgb_format, reference_image.get(),
min_psnr, max_psnr);
}
INSTANTIATE_TEST_SUITE_P(
All, ToneMapTest,
Values(
// ------ SDR BASE IMAGE ------
// hdr_headroom=0, the image should stay SDR (base image untouched).
// A small loss is expected due to YUV/RGB conversion.
std::make_tuple(
/*source=*/"seine_sdr_gainmap_srgb.avif", /*hdr_headroom=*/0.0f,
/*out_depth=*/8,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_SRGB,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGB,
/*reference=*/"seine_sdr_gainmap_srgb.avif", /*min_psnr=*/60.0f,
/*max_psnr=*/80.0f),
// Same as above, outputting to RGBA.
std::make_tuple(
/*source=*/"seine_sdr_gainmap_srgb.avif", /*hdr_headroom=*/0.0f,
/*out_depth=*/8,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_SRGB,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGBA,
/*reference=*/"seine_sdr_gainmap_srgb.avif", /*min_psnr=*/60.0f,
/*max_psnr=*/80.0f),
// Same as above, outputting to a different transfer characteristic.
// As a result we expect a low PSNR (since the PSNR function is not
// aware of the transfer curve difference).
std::make_tuple(
/*source=*/"seine_sdr_gainmap_srgb.avif", /*hdr_headroom=*/0.0f,
/*out_depth=*/8,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_LOG100,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGBA,
/*reference=*/"seine_sdr_gainmap_srgb.avif", /*min_psnr=*/20.0f,
/*max_psnr=*/30.0f),
// hdr_headroom=3, the gain map should be fully applied.
std::make_tuple(
/*source=*/"seine_sdr_gainmap_srgb.avif", /*hdr_headroom=*/3.0f,
/*out_depth=*/10,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_PQ,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGB,
/*reference=*/"seine_hdr_srgb.avif", /*min_psnr=*/40.0f,
/*max_psnr=*/60.0f),
// hdr_headroom=3, the gain map should be fully applied.
// Version with a gain map that is larger than the base image (needs
// rescaling).
std::make_tuple(
/*source=*/"seine_sdr_gainmap_big_srgb.avif", /*hdr_headroom=*/3.0f,
/*out_depth=*/10,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_PQ,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGB,
/*reference=*/"seine_hdr_srgb.avif", /*min_psnr=*/40.0f,
/*max_psnr=*/60.0f),
// hdr_headroom=0.5 No reference image.
std::make_tuple(
/*source=*/"seine_sdr_gainmap_srgb.avif", /*hdr_headroom=*/1.5f,
/*out_depth=*/10,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_PQ,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGB,
/*reference=*/"", /*min_psnr=*/0.0f,
/*max_psnr=*/0.0f),
// ------ HDR BASE IMAGE ------
// hdr_headroom=0, the gain map should be fully applied.
std::make_tuple(
/*source=*/"seine_hdr_gainmap_srgb.avif", /*hdr_headroom=*/0.0f,
/*out_depth=*/8,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_SRGB,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGB,
/*reference=*/"seine_sdr_gainmap_srgb.avif", /*min_psnr=*/38.0f,
/*max_psnr=*/60.0f),
// hdr_headroom=0, the gain map should be fully applied.
// Version with a gain map that is smaller than the base image (needs
// rescaling). The PSNR is a bit lower than above due to quality loss on
// the gain map.
std::make_tuple(
/*source=*/"seine_hdr_gainmap_small_srgb.avif",
/*hdr_headroom=*/0.0f,
/*out_depth=*/8,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_SRGB,
AVIF_RGB_FORMAT_RGB,
/*reference=*/"seine_sdr_gainmap_srgb.avif", /*min_psnr=*/36.0f,
/*max_psnr=*/60.0f),
// hdr_headroom=3, the image should stay HDR (base image untouched).
// A small loss is expected due to YUV/RGB conversion.
std::make_tuple(
/*source=*/"seine_hdr_gainmap_srgb.avif", /*hdr_headroom=*/3.0f,
/*out_depth=*/10,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_PQ,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGB,
/*reference=*/"seine_hdr_gainmap_srgb.avif", /*min_psnr=*/60.0f,
/*max_psnr=*/80.0f),
// hdr_headroom=0.5 No reference image.
std::make_tuple(
/*source=*/"seine_hdr_gainmap_srgb.avif", /*hdr_headroom=*/1.5f,
/*out_depth=*/10,
/*out_transfer=*/AVIF_TRANSFER_CHARACTERISTICS_PQ,
/*out_rgb_format=*/AVIF_RGB_FORMAT_RGB,
/*reference=*/"", /*min_psnr=*/0.0f, /*max_psnr=*/0.0f)));
TEST(ToneMapTest, ToneMapImageSameHeadroom) {
const std::string path =
std::string(data_path) + "seine_sdr_gainmap_srgb.avif";
ImagePtr image(avifImageCreateEmpty());
ASSERT_NE(image, nullptr);
DecoderPtr decoder(avifDecoderCreate());
ASSERT_NE(decoder, nullptr);
decoder->enableDecodingGainMap = true;
decoder->enableParsingGainMapMetadata = true;
avifResult result =
avifDecoderReadFile(decoder.get(), image.get(), path.c_str());
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << decoder->diag.error;
ASSERT_NE(image->gainMap, nullptr);
ASSERT_NE(image->gainMap->image, nullptr);
// Force the alternate and base HDR headroom to the same value.
image->gainMap->metadata.baseHdrHeadroomN =
image->gainMap->metadata.alternateHdrHeadroomN;
image->gainMap->metadata.baseHdrHeadroomD =
image->gainMap->metadata.alternateHdrHeadroomD;
const float headroom = static_cast<float>(
static_cast<float>(image->gainMap->metadata.baseHdrHeadroomN) /
image->gainMap->metadata.baseHdrHeadroomD);
// Check that when the two headrooms are the same, the gain map is not applied
// whatever the target headroom is.
for (const float tonemap_to : {headroom, headroom - 0.5f, headroom + 0.5f}) {
ToneMapImageAndCompareToReference(
image.get(), *image->gainMap, /*hdr_headroom=*/tonemap_to,
/*out_depth=*/image->depth,
/*out_transfer_characteristics=*/image->transferCharacteristics,
AVIF_RGB_FORMAT_RGB, /*reference_image=*/image.get(),
/*min_psnr=*/60, /*max_psnr=*/100);
}
}
class CreateGainMapTest
: public testing::TestWithParam<std::tuple<
/*image1_name=*/std::string, /*image2_name=*/std::string,
/*downscaling=*/int, /*gain_map_depth=*/int,
/*gain_map_format=*/avifPixelFormat,
/*min_psnr=*/float, /*max_psnr=*/float>> {};
// Creates a gain map to go from image1 to image2, and tone maps to check we get
// the correct result. Then does the same thing going from image2 to image1.
TEST_P(CreateGainMapTest, Create) {
const std::string image1_name = std::get<0>(GetParam());
const std::string image2_name = std::get<1>(GetParam());
const int downscaling = std::get<2>(GetParam());
const int gain_map_depth = std::get<3>(GetParam());
const avifPixelFormat gain_map_format = std::get<4>(GetParam());
const float min_psnr = std::get<5>(GetParam());
const float max_psnr = std::get<6>(GetParam());
ImagePtr image1 = testutil::DecodeFile(std::string(data_path) + image1_name);
ASSERT_NE(image1, nullptr);
ImagePtr image2 = testutil::DecodeFile(std::string(data_path) + image2_name);
ASSERT_NE(image2, nullptr);
const uint32_t gain_map_width = std::max<uint32_t>(
(uint32_t)std::round((float)image1->width / downscaling), 1u);
const uint32_t gain_map_height = std::max<uint32_t>(
(uint32_t)std::round((float)image1->height / downscaling), 1u);
std::unique_ptr<avifGainMap, decltype(&avifGainMapDestroy)> gain_map(
avifGainMapCreate(), avifGainMapDestroy);
gain_map->image = avifImageCreate(gain_map_width, gain_map_height,
gain_map_depth, gain_map_format);
avifDiagnostics diag;
gain_map->metadata.useBaseColorSpace = true;
avifResult result = avifImageComputeGainMap(image1.get(), image2.get(),
gain_map.get(), &diag);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << diag.error;
EXPECT_EQ(gain_map->image->width, gain_map_width);
EXPECT_EQ(gain_map->image->height, gain_map_height);
const float image1_headroom = (float)gain_map->metadata.baseHdrHeadroomN /
gain_map->metadata.baseHdrHeadroomD;
const float image2_headroom =
(float)gain_map->metadata.alternateHdrHeadroomN /
gain_map->metadata.alternateHdrHeadroomD;
// Tone map from image1 to image2 by applying the gainmap forward.
float psnr_image1_to_image2_forward;
ToneMapImageAndCompareToReference(
image1.get(), *gain_map, image2_headroom, image2->depth,
image2->transferCharacteristics, AVIF_RGB_FORMAT_RGB, image2.get(),
min_psnr, max_psnr, &psnr_image1_to_image2_forward);
// Tone map from image2 to image1 by applying the gainmap backward.
SwapBaseAndAlternate(*image1, *gain_map);
float psnr_image2_to_image1_backward;
ToneMapImageAndCompareToReference(
image2.get(), *gain_map, image1_headroom, image1->depth,
image1->transferCharacteristics, AVIF_RGB_FORMAT_RGB, image1.get(),
min_psnr, max_psnr, &psnr_image2_to_image1_backward);
// Uncomment the following to save the gain map as a PNG file.
// ASSERT_TRUE(testutil::WriteImage(gain_map->image,
// "/tmp/gain_map_image1_to_image2.png"));
// Compute the gain map in the other direction (from image2 to image1).
gain_map->metadata.useBaseColorSpace = false;
result = avifImageComputeGainMap(image2.get(), image1.get(), gain_map.get(),
&diag);
ASSERT_EQ(result, AVIF_RESULT_OK)
<< avifResultToString(result) << " " << diag.error;
const float image2_headroom2 = (float)gain_map->metadata.baseHdrHeadroomN /
gain_map->metadata.baseHdrHeadroomD;
EXPECT_NEAR(image2_headroom2, image2_headroom, 0.001);
// Tone map from image2 to image1 by applying the new gainmap forward.
float psnr_image2_to_image1_forward;
ToneMapImageAndCompareToReference(
image2.get(), *gain_map, image1_headroom, image1->depth,
image1->transferCharacteristics, AVIF_RGB_FORMAT_RGB, image1.get(),
min_psnr, max_psnr, &psnr_image2_to_image1_forward);
// Tone map from image1 to image2 by applying the new gainmap backward.
SwapBaseAndAlternate(*image2, *gain_map);
float psnr_image1_to_image2_backward;
ToneMapImageAndCompareToReference(
image1.get(), *gain_map, image2_headroom, image2->depth,
image2->transferCharacteristics, AVIF_RGB_FORMAT_RGB, image2.get(),
min_psnr, max_psnr, &psnr_image1_to_image2_backward);
// Uncomment the following to save the gain map as a PNG file.
// ASSERT_TRUE(testutil::WriteImage(gain_map->image,
// "/tmp/gain_map_image2_to_image1.png"));
// Results should be about the same whether the gain map was computed from sdr
// to hdr or the other way around.
EXPECT_NEAR(psnr_image1_to_image2_backward, psnr_image1_to_image2_forward,
min_psnr * 0.1f);
EXPECT_NEAR(psnr_image2_to_image1_forward, psnr_image2_to_image1_backward,
min_psnr * 0.1f);
}
INSTANTIATE_TEST_SUITE_P(
All, CreateGainMapTest,
Values(
// Full scale gain map, 3 channels, 10 bit gain map.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/1, /*gain_map_depth=*/10,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/55.0f, /*max_psnr=*/80.0f),
// 8 bit gain map, expect a slightly lower PSNR.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/1, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/50.0f, /*max_psnr=*/70.0f),
// 420 gain map, expect a lower PSNR.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/1, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV420,
/*min_psnr=*/40.0f, /*max_psnr=*/60.0f),
// Downscaled gain map, expect a lower PSNR.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/2, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/35.0f, /*max_psnr=*/45.0f),
// Even more downscaled gain map, expect a lower PSNR.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/3, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/35.0f, /*max_psnr=*/45.0f),
// Extreme downscaling, just for fun.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/255, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/20.0f, /*max_psnr=*/35.0f),
// Grayscale gain map.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/1, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV400,
/*min_psnr=*/40.0f, /*max_psnr=*/60.0f),
// Downscaled AND grayscale.
std::make_tuple(/*image1_name=*/"seine_sdr_gainmap_srgb.avif",
/*image2_name=*/"seine_hdr_gainmap_srgb.avif",
/*downscaling=*/2, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV400,
/*min_psnr=*/35.0f, /*max_psnr=*/45.0f),
// Color space conversions.
std::make_tuple(/*image1_name=*/"colors_sdr_srgb.avif",
/*image2_name=*/"colors_hdr_rec2020.avif",
/*downscaling=*/1, /*gain_map_depth=*/10,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/55.0f, /*max_psnr=*/100.0f),
// The PSNR is very high because there are essentially the same image,
// simply expresed in different colorspaces.
std::make_tuple(/*image1_name=*/"colors_hdr_rec2020.avif",
/*image2_name=*/"colors_hdr_p3.avif",
/*downscaling=*/1, /*gain_map_depth=*/8,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/90.0f, /*max_psnr=*/100.0f),
// Color space conversions with wider color gamut.
std::make_tuple(/*image1_name=*/"colors_sdr_srgb.avif",
/*image2_name=*/"colors_wcg_hdr_rec2020.avif",
/*downscaling=*/1, /*gain_map_depth=*/10,
/*gain_map_format=*/AVIF_PIXEL_FORMAT_YUV444,
/*min_psnr=*/55.0f, /*max_psnr=*/80.0f)));
TEST(FindMinMaxWithoutOutliers, AllSame) {
constexpr int kNumValues = 10000;
for (float v : {0.0f, 42.f, -12.f, 1.52f}) {
std::vector<float> values(kNumValues, v);
float min, max;
ASSERT_EQ(
avifFindMinMaxWithoutOutliers(values.data(), kNumValues, &min, &max),
AVIF_RESULT_OK);
EXPECT_EQ(min, v);
EXPECT_EQ(max, v);
}
}
TEST(FindMinMaxWithoutOutliers, Test) {
constexpr int kNumValues = 10000;
for (const float value_shift : {0.0f, -20.0f, 20.0f}) {
SCOPED_TRACE("value_shift: " + std::to_string(value_shift));
std::vector<float> values(kNumValues, value_shift + 2.0f);
int k = 0;
for (int i = 0; i < 5; ++i, ++k) {
values[k] = value_shift + 1.99f;
}
for (int i = 0; i < 5; ++i, ++k) {
values[k] = value_shift + 1.98f;
}
for (int i = 0; i < 1; ++i, ++k) {
values[k] = value_shift + 1.97f;
}
for (int i = 0; i < 2; ++i, ++k) {
values[k] = value_shift + 1.93f; // Outliers.
}
for (int i = 0; i < 3; ++i, ++k) {
values[k] = value_shift + 10.2f; // Outliers.
}
float min, max;
ASSERT_EQ(
avifFindMinMaxWithoutOutliers(values.data(), kNumValues, &min, &max),
AVIF_RESULT_OK);
const float kEpsilon = 0.001f;
EXPECT_NEAR(min, value_shift + 1.97f, kEpsilon);
const float bucketSize = 0.01f; // Size of one bucket.
EXPECT_NEAR(max, value_shift + 2.0f + bucketSize, kEpsilon);
}
}
} // namespace
} // namespace avif
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
if (argc != 2) {
std::cerr << "There must be exactly one argument containing the path to "
"the test data folder"
<< std::endl;
return 1;
}
avif::data_path = argv[1];
return RUN_ALL_TESTS();
}