Skip to content

Commit

Permalink
Refactor gain map API (AOMediaCodec#2444)
Browse files Browse the repository at this point in the history
Remove avifGainMapMetadata and avifGainMapMetadataDouble.
These structs were not extensible, i.e. new fields could not be added without
breaking ABI backward compatibility.
Instead, the fields of avifGainMapMetadata are added to avifGainMap directly
(which is an extensible struct since it uses Create()/Destroy() functions).
Expose avifDoubleTo(Un)SignedFraction to replace avifGainMapMetadataDoubleToFractions.
  • Loading branch information
maryla-uc authored Sep 30, 2024
1 parent 9100176 commit cdb89c9
Show file tree
Hide file tree
Showing 22 changed files with 527 additions and 668 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ The changes are relative to the previous release, unless the baseline is specifi
* Write an empty HandlerBox name field instead of "libavif" (saves 7 bytes).
* Update aom.cmd/LocalAom.cmake: v3.10.0
* Update svt.cmd/svt.sh/LocalSvt.cmake: v2.2.1
* Change experimental gainmap API: remove avifGainMapMetadata and
avifGainMapMetadataDouble structs.
* Add avif(Un)SignedFraction structs and avifDoubleTo(Un)SignedFraction
utility functions.

## [1.1.1] - 2024-07-30

Expand Down
2 changes: 1 addition & 1 deletion apps/avifgainmaputil/convert_command.cc
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ avifResult ConvertCommand::Run() {
if (arg_swap_base_) {
int depth = arg_image_read_.depth;
if (depth == 0) {
depth = image->gainMap->metadata.alternateHdrHeadroomN == 0 ? 8 : 10;
depth = image->gainMap->alternateHdrHeadroom.n == 0 ? 8 : 10;
}
ImagePtr new_base(avifImageCreateEmpty());
if (new_base == nullptr) {
Expand Down
54 changes: 22 additions & 32 deletions apps/avifgainmaputil/printmetadata_command.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,20 @@ namespace avif {

namespace {
template <typename T>
std::string FormatFraction(T numerator, uint32_t denominator) {
std::string FormatFraction(T fraction) {
std::stringstream stream;
stream << (denominator != 0 ? (double)numerator / denominator : 0)
<< " (as fraction: " << numerator << "/" << denominator << ")";
stream << (fraction->d != 0 ? (double)fraction->n / fraction->d : 0)
<< " (as fraction: " << fraction->n << "/" << fraction->d << ")";
return stream.str();
}

template <typename T>
std::string FormatFractions(const T numerator[3],
const uint32_t denominator[3]) {
std::string FormatFractions(const T fractions[3]) {
std::stringstream stream;
const int w = 40;
stream << "R " << std::left << std::setw(w)
<< FormatFraction(numerator[0], denominator[0]) << " G " << std::left
<< std::setw(w) << FormatFraction(numerator[1], denominator[1])
<< " B " << std::left << std::setw(w)
<< FormatFraction(numerator[2], denominator[2]);
stream << "R " << std::left << std::setw(w) << FormatFraction(fractions)
<< " G " << std::left << std::setw(w) << FormatFraction(fractions)
<< " B " << std::left << std::setw(w) << FormatFraction(fractions);
return stream.str();
}
} // namespace
Expand Down Expand Up @@ -65,34 +62,27 @@ avifResult PrintMetadataCommand::Run() {
}
assert(decoder->image->gainMap);

const avifGainMapMetadata& metadata = decoder->image->gainMap->metadata;
const avifGainMap& gainMap = *decoder->image->gainMap;
const int w = 20;
std::cout << " * " << std::left << std::setw(w) << "Base headroom: "
<< FormatFraction(metadata.baseHdrHeadroomN,
metadata.baseHdrHeadroomD)
std::cout << " * " << std::left << std::setw(w)
<< "Base headroom: " << FormatFraction(&gainMap.baseHdrHeadroom)
<< "\n";
std::cout << " * " << std::left << std::setw(w) << "Alternate headroom: "
<< FormatFraction(metadata.alternateHdrHeadroomN,
metadata.alternateHdrHeadroomD)
<< FormatFraction(&gainMap.alternateHdrHeadroom) << "\n";
std::cout << " * " << std::left << std::setw(w)
<< "Gain Map Min: " << FormatFractions(gainMap.gainMapMin) << "\n";
std::cout << " * " << std::left << std::setw(w)
<< "Gain Map Max: " << FormatFractions(gainMap.gainMapMax) << "\n";
std::cout << " * " << std::left << std::setw(w)
<< "Base Offset: " << FormatFractions(gainMap.baseOffset) << "\n";
std::cout << " * " << std::left << std::setw(w)
<< "Alternate Offset: " << FormatFractions(gainMap.alternateOffset)
<< "\n";
std::cout << " * " << std::left << std::setw(w) << "Gain Map Min: "
<< FormatFractions(metadata.gainMapMinN, metadata.gainMapMinD)
<< "\n";
std::cout << " * " << std::left << std::setw(w) << "Gain Map Max: "
<< FormatFractions(metadata.gainMapMaxN, metadata.gainMapMaxD)
<< "\n";
std::cout << " * " << std::left << std::setw(w) << "Base Offset: "
<< FormatFractions(metadata.baseOffsetN, metadata.baseOffsetD)
<< "\n";
std::cout << " * " << std::left << std::setw(w) << "Alternate Offset: "
<< FormatFractions(metadata.alternateOffsetN,
metadata.alternateOffsetD)
<< "\n";
std::cout << " * " << std::left << std::setw(w) << "Gain Map Gamma: "
<< FormatFractions(metadata.gainMapGammaN, metadata.gainMapGammaD)
std::cout << " * " << std::left << std::setw(w)
<< "Gain Map Gamma: " << FormatFractions(gainMap.gainMapGamma)
<< "\n";
std::cout << " * " << std::left << std::setw(w) << "Use Base Color Space: "
<< (metadata.useBaseColorSpace ? "True" : "False") << "\n";
<< (gainMap.useBaseColorSpace ? "True" : "False") << "\n";

return AVIF_RESULT_OK;
}
Expand Down
19 changes: 10 additions & 9 deletions apps/avifgainmaputil/swapbase_command.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ avifResult ChangeBase(const avifImage& image, int depth,
swapped->depth = depth;
swapped->yuvFormat = yuvFormat;

if (image.gainMap->alternateHdrHeadroom.d == 0) {
return AVIF_RESULT_INVALID_ARGUMENT;
}
const float headroom =
static_cast<float>(image.gainMap->metadata.alternateHdrHeadroomN) /
image.gainMap->metadata.alternateHdrHeadroomD;
static_cast<float>(image.gainMap->alternateHdrHeadroom.n) /
image.gainMap->alternateHdrHeadroom.d;
const bool tone_mapping_to_sdr = (headroom == 0.0f);

swapped->colorPrimaries = image.gainMap->altColorPrimaries;
Expand Down Expand Up @@ -97,14 +100,12 @@ avifResult ChangeBase(const avifImage& image, int depth,
(image.yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3;
swapped->gainMap->altCLLI = image.clli;

// Swap base and alternate in the gain map metadata.
avifGainMapMetadata& metadata = swapped->gainMap->metadata;
metadata.useBaseColorSpace = !metadata.useBaseColorSpace;
std::swap(metadata.baseHdrHeadroomN, metadata.alternateHdrHeadroomN);
std::swap(metadata.baseHdrHeadroomD, metadata.alternateHdrHeadroomD);
// Swap base and alternate in the gain map
avifGainMap* gainMap = swapped->gainMap;
gainMap->useBaseColorSpace = !gainMap->useBaseColorSpace;
std::swap(gainMap->baseHdrHeadroom, gainMap->alternateHdrHeadroom);
for (int c = 0; c < 3; ++c) {
std::swap(metadata.baseOffsetN, metadata.alternateOffsetN);
std::swap(metadata.baseOffsetD, metadata.alternateOffsetD);
std::swap(gainMap->baseOffset, gainMap->alternateOffset);
}

return AVIF_RESULT_OK;
Expand Down
32 changes: 17 additions & 15 deletions apps/avifgainmaputil/tonemap_command.cc
Original file line number Diff line number Diff line change
Expand Up @@ -85,29 +85,31 @@ avifResult TonemapCommand::Run() {
<< " does not contain a gain map\n";
return AVIF_RESULT_INVALID_ARGUMENT;
}

avifGainMapMetadataDouble metadata;
if (!avifGainMapMetadataFractionsToDouble(&metadata,
&image->gainMap->metadata)) {
std::cerr << "Input image " << arg_input_filename_
<< " has invalid gain map metadata\n";
if (image->gainMap->baseHdrHeadroom.d == 0 ||
image->gainMap->alternateHdrHeadroom.d == 0) {
return AVIF_RESULT_INVALID_ARGUMENT;
}

const float base_hdr_hreadroom =
static_cast<float>(image->gainMap->baseHdrHeadroom.n) /
image->gainMap->baseHdrHeadroom.d;
const float alternate_hdr_hreadroom =
static_cast<float>(image->gainMap->alternateHdrHeadroom.n /
image->gainMap->alternateHdrHeadroom.d);
// We are either tone mapping to the base image (i.e. leaving it as is),
// or tone mapping to the alternate image (i.e. fully applying the gain map),
// or tone mapping in between (partially applying the gain map).
const bool tone_mapping_to_base =
(headroom <= metadata.baseHdrHeadroom &&
metadata.baseHdrHeadroom <= metadata.alternateHdrHeadroom) ||
(headroom >= metadata.baseHdrHeadroom &&
metadata.baseHdrHeadroom >= metadata.alternateHdrHeadroom);
(headroom <= base_hdr_hreadroom &&
base_hdr_hreadroom <= alternate_hdr_hreadroom) ||
(headroom >= base_hdr_hreadroom &&
base_hdr_hreadroom >= alternate_hdr_hreadroom);
const bool tone_mapping_to_alternate =
(headroom <= metadata.alternateHdrHeadroom &&
metadata.alternateHdrHeadroom <= metadata.baseHdrHeadroom) ||
(headroom >= metadata.alternateHdrHeadroom &&
metadata.alternateHdrHeadroom >= metadata.baseHdrHeadroom);
const bool base_is_hdr = (metadata.baseHdrHeadroom != 0.0f);
(headroom <= alternate_hdr_hreadroom &&
alternate_hdr_hreadroom <= base_hdr_hreadroom) ||
(headroom >= alternate_hdr_hreadroom &&
alternate_hdr_hreadroom >= base_hdr_hreadroom);
const bool base_is_hdr = (base_hdr_hreadroom != 0.0f);

// Determine output CICP.
CicpValues cicp;
Expand Down
70 changes: 37 additions & 33 deletions apps/shared/avifjpeg.c
Original file line number Diff line number Diff line change
Expand Up @@ -592,75 +592,79 @@ static inline void SwapDoubles(double * x, double * y)
// Parses gain map metadata from XMP.
// See https://helpx.adobe.com/camera-raw/using/gain-map.html
// Returns AVIF_TRUE if the gain map metadata was successfully read.
static avifBool avifJPEGParseGainMapXMPProperties(const xmlNode * rootNode, avifGainMapMetadata * metadata)
static avifBool avifJPEGParseGainMapXMPProperties(const xmlNode * rootNode, avifGainMap * gainMap)
{
const xmlNode * descNode = avifJPEGFindGainMapXMPNode(rootNode);
if (descNode == NULL) {
return AVIF_FALSE;
}

avifGainMapMetadataDouble metadataDouble;
// Set default values from Adobe's spec.
metadataDouble.baseHdrHeadroom = 0.0;
metadataDouble.alternateHdrHeadroom = 1.0;
for (int i = 0; i < 3; ++i) {
metadataDouble.gainMapMin[i] = 0.0;
metadataDouble.gainMapMax[i] = 1.0;
metadataDouble.baseOffset[i] = 1.0 / 64.0;
metadataDouble.alternateOffset[i] = 1.0 / 64.0;
metadataDouble.gainMapGamma[i] = 1.0;
}
// Not in Adobe's spec but both color spaces should be the same so this value doesn't matter.
metadataDouble.useBaseColorSpace = AVIF_TRUE;

AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMin", &metadataDouble.baseHdrHeadroom, /*numDoubles=*/1));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMax", &metadataDouble.alternateHdrHeadroom, /*numDoubles=*/1));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", metadataDouble.baseOffset, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", metadataDouble.alternateOffset, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMin", metadataDouble.gainMapMin, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMax", metadataDouble.gainMapMax, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "Gamma", metadataDouble.gainMapGamma, /*numDoubles=*/3));
double baseHdrHeadroom = 0.0;
double alternateHdrHeadroom = 1.0;
double gainMapMin[3] = { 0.0, 0.0, 0.0 };
double gainMapMax[3] = { 1.0, 1.0, 1.0 };
double gainMapGamma[3] = { 1.0, 1.0, 1.0 };
double baseOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 };
double alternateOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 };
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMin", &baseHdrHeadroom, /*numDoubles=*/1));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMax", &alternateHdrHeadroom, /*numDoubles=*/1));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", baseOffset, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", alternateOffset, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMin", gainMapMin, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMax", gainMapMax, /*numDoubles=*/3));
AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "Gamma", gainMapGamma, /*numDoubles=*/3));

// See inequality requirements in section 'XMP Representation of Gain Map Metadata' of Adobe's gain map specification
// https://helpx.adobe.com/camera-raw/using/gain-map.html
AVIF_CHECK(metadataDouble.alternateHdrHeadroom > metadataDouble.baseHdrHeadroom);
AVIF_CHECK(metadataDouble.baseHdrHeadroom >= 0);
AVIF_CHECK(alternateHdrHeadroom > baseHdrHeadroom);
AVIF_CHECK(baseHdrHeadroom >= 0);
for (int i = 0; i < 3; ++i) {
AVIF_CHECK(metadataDouble.gainMapMax[i] >= metadataDouble.gainMapMin[i]);
AVIF_CHECK(metadataDouble.baseOffset[i] >= 0.0);
AVIF_CHECK(metadataDouble.alternateOffset[i] >= 0.0);
AVIF_CHECK(metadataDouble.gainMapGamma[i] > 0.0);
AVIF_CHECK(gainMapMax[i] >= gainMapMin[i]);
AVIF_CHECK(baseOffset[i] >= 0.0);
AVIF_CHECK(alternateOffset[i] >= 0.0);
AVIF_CHECK(gainMapGamma[i] > 0.0);
}

uint32_t numValues;
const char * baseRenditionIsHDR;
if (avifJPEGFindGainMapProperty(descNode, "BaseRenditionIsHDR", /*maxValues=*/1, &baseRenditionIsHDR, &numValues)) {
if (!strcmp(baseRenditionIsHDR, "True")) {
SwapDoubles(&metadataDouble.baseHdrHeadroom, &metadataDouble.alternateHdrHeadroom);
SwapDoubles(&baseHdrHeadroom, &alternateHdrHeadroom);
for (int c = 0; c < 3; ++c) {
SwapDoubles(&metadataDouble.baseOffset[c], &metadataDouble.alternateOffset[c]);
SwapDoubles(&baseOffset[c], &alternateOffset[c]);
}
} else if (!strcmp(baseRenditionIsHDR, "False")) {
} else {
return AVIF_FALSE; // Unexpected value.
}
}

AVIF_CHECK(avifGainMapMetadataDoubleToFractions(metadata, &metadataDouble));
for (int i = 0; i < 3; ++i) {
AVIF_CHECK(avifDoubleToSignedFraction(gainMapMin[i], &gainMap->gainMapMin[i]));
AVIF_CHECK(avifDoubleToSignedFraction(gainMapMax[i], &gainMap->gainMapMax[i]));
AVIF_CHECK(avifDoubleToUnsignedFraction(gainMapGamma[i], &gainMap->gainMapGamma[i]));
AVIF_CHECK(avifDoubleToSignedFraction(baseOffset[i], &gainMap->baseOffset[i]));
AVIF_CHECK(avifDoubleToSignedFraction(alternateOffset[i], &gainMap->alternateOffset[i]));
}
AVIF_CHECK(avifDoubleToUnsignedFraction(baseHdrHeadroom, &gainMap->baseHdrHeadroom));
AVIF_CHECK(avifDoubleToUnsignedFraction(alternateHdrHeadroom, &gainMap->alternateHdrHeadroom));
// Not in Adobe's spec but both color spaces should be the same so this value doesn't matter.
gainMap->useBaseColorSpace = AVIF_TRUE;

return AVIF_TRUE;
}

// Parses gain map metadata from an XMP payload.
// Returns AVIF_TRUE if the gain map metadata was successfully read.
avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMapMetadata * metadata)
avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMap * gainMap)
{
xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, LIBXML2_XML_PARSING_FLAGS);
if (document == NULL) {
return AVIF_FALSE; // Probably an out of memory error.
}
xmlNode * rootNode = xmlDocGetRootElement(document);
const avifBool res = avifJPEGParseGainMapXMPProperties(rootNode, metadata);
const avifBool res = avifJPEGParseGainMapXMPProperties(rootNode, gainMap);
xmlFreeDoc(document);
return res;
}
Expand Down Expand Up @@ -817,7 +821,7 @@ static avifBool avifJPEGExtractGainMapImage(FILE * f,
avifImageDestroy(image);
return AVIF_FALSE;
}
if (!avifJPEGParseGainMapXMP(image->xmp.data, image->xmp.size, &gainMap->metadata)) {
if (!avifJPEGParseGainMapXMP(image->xmp.data, image->xmp.size, gainMap)) {
fprintf(stderr, "Warning: failed to parse gain map metadata\n");
avifImageDestroy(image);
return AVIF_FALSE;
Expand Down
2 changes: 1 addition & 1 deletion apps/shared/avifjpeg.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int

#if defined(AVIF_ENABLE_EXPERIMENTAL_JPEG_GAIN_MAP_CONVERSION)
// Parses XMP gain map metadata. Visible for testing.
avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMapMetadata * metadata);
avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMap * gainMap);
#endif

#ifdef __cplusplus
Expand Down
2 changes: 1 addition & 1 deletion apps/shared/avifutil.c
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ static void avifImageDumpInternal(const avifImage * avif,
avifPixelFormatToString(gainMapImage->yuvFormat),
(gainMapImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited",
gainMapImage->matrixCoefficients,
(avif->gainMap->metadata.baseHdrHeadroomN == 0) ? "SDR" : "HDR");
(avif->gainMap->baseHdrHeadroom.n == 0) ? "SDR" : "HDR");
printf(" * Alternate image:\n");
printf(" * Color Primaries: %u\n", avif->gainMap->altColorPrimaries);
printf(" * Transfer Char. : %u\n", avif->gainMap->altTransferCharacteristics);
Expand Down
Loading

0 comments on commit cdb89c9

Please sign in to comment.