From e0778af38864bba88f2c6ff2cd30fc7abf007500 Mon Sep 17 00:00:00 2001 From: maryla-uc Date: Wed, 13 Dec 2023 16:43:26 +0100 Subject: [PATCH] Add support for creating gain maps for image with different primaries. (#1873) --- apps/avifgainmaputil/swapbase_command.cc | 1 + apps/avifgainmaputil/tonemap_command.cc | 8 +- include/avif/avif.h | 9 +- src/avif.c | 1 + src/gainmap.c | 230 +++++++++++++++++--- src/reformat.c | 3 + tests/data/README.md | 23 +- tests/data/colors_text_wcg_hdr_rec2020.avif | Bin 0 -> 33242 bytes tests/data/colors_text_wcg_sdr_rec2020.avif | Bin 0 -> 25007 bytes tests/data/colors_wcg_hdr_rec2020.avif | Bin 0 -> 20613 bytes tests/gtest/avifgainmaptest.cc | 211 +++++++++++------- tests/gtest/aviftest_helpers.cc | 15 ++ tests/test_cmd_avifgainmaputil.sh | 3 + 13 files changed, 388 insertions(+), 116 deletions(-) create mode 100644 tests/data/colors_text_wcg_hdr_rec2020.avif create mode 100644 tests/data/colors_text_wcg_sdr_rec2020.avif create mode 100644 tests/data/colors_wcg_hdr_rec2020.avif diff --git a/apps/avifgainmaputil/swapbase_command.cc b/apps/avifgainmaputil/swapbase_command.cc index 2c2e550bdb..e23f3e3986 100644 --- a/apps/avifgainmaputil/swapbase_command.cc +++ b/apps/avifgainmaputil/swapbase_command.cc @@ -58,6 +58,7 @@ avifResult ChangeBase(const avifImage& image, int depth, avifDiagnostics diag; result = avifImageApplyGainMap(&image, image.gainMap, headroom, + swapped->colorPrimaries, swapped->transferCharacteristics, &swapped_rgb, (compute_clli ? &clli : nullptr), &diag); if (result != AVIF_RESULT_OK) { diff --git a/apps/avifgainmaputil/tonemap_command.cc b/apps/avifgainmaputil/tonemap_command.cc index 147ba6841a..7c0531131f 100644 --- a/apps/avifgainmaputil/tonemap_command.cc +++ b/apps/avifgainmaputil/tonemap_command.cc @@ -191,10 +191,10 @@ avifResult TonemapCommand::Run() { avifRGBImage tone_mapped_rgb; avifRGBImageSetDefaults(&tone_mapped_rgb, tone_mapped.get()); avifDiagnostics diag; - result = - avifImageApplyGainMap(decoder->image, image->gainMap, arg_headroom_, - cicp.transfer_characteristics, &tone_mapped_rgb, - clli_set ? nullptr : &clli_box, &diag); + result = avifImageApplyGainMap( + decoder->image, image->gainMap, arg_headroom_, cicp.color_primaries, + cicp.transfer_characteristics, &tone_mapped_rgb, + clli_set ? nullptr : &clli_box, &diag); if (result != AVIF_RESULT_OK) { std::cout << "Failed to tone map image: " << avifResultToString(result) << " (" << diag.error << ")\n"; diff --git a/include/avif/avif.h b/include/avif/avif.h index ff9d1cfabd..a6a103b2a9 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -632,7 +632,6 @@ typedef struct avifGainMapMetadata // True if tone mapping should be performed in the color space of the // base image. If false, the color space of the alternate image should // be used. - // TODO(maryla): implement. avifBool useBaseColorSpace; } avifGainMapMetadata; @@ -1551,15 +1550,18 @@ AVIF_NODISCARD AVIF_API avifBool avifPeekCompatibleFileType(const avifROData * i AVIF_API avifResult avifImageApplyGainMap(const avifImage * baseImage, const avifGainMap * gainMap, float hdrHeadroom, + avifColorPrimaries outputColorPrimaries, avifTransferCharacteristics outputTransferCharacteristics, avifRGBImage * toneMappedImage, avifContentLightLevelInformationBox * clli, avifDiagnostics * diag); // Same as above but takes an avifRGBImage as input instead of avifImage. AVIF_API avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, - avifTransferCharacteristics transferCharacteristics, + avifColorPrimaries baseColorPrimaries, + avifTransferCharacteristics baseTransferCharacteristics, const avifGainMap * gainMap, float hdrHeadroom, + avifColorPrimaries outputColorPrimaries, avifTransferCharacteristics outputTransferCharacteristics, avifRGBImage * toneMappedImage, avifContentLightLevelInformationBox * clli, @@ -1572,10 +1574,11 @@ AVIF_API avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, // height, depth and yuvFormat fields set to the desired output values for the // gain map. All of these fields may differ from the source images. AVIF_API avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, + avifColorPrimaries baseColorPrimaries, avifTransferCharacteristics baseTransferCharacteristics, const avifRGBImage * altRgbImage, + avifColorPrimaries altColorPrimaries, avifTransferCharacteristics altTransferCharacteristics, - avifColorPrimaries colorPrimaries, avifGainMap * gainMap, avifDiagnostics * diag); // Convenience function. Same as above but takes avifImage images as input diff --git a/src/avif.c b/src/avif.c index b2548191ed..9ca9295d79 100644 --- a/src/avif.c +++ b/src/avif.c @@ -1161,6 +1161,7 @@ avifGainMap * avifGainMapCreate() gainMap->altTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; gainMap->altMatrixCoefficients = AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED; gainMap->altYUVRange = AVIF_RANGE_FULL; + gainMap->metadata.useBaseColorSpace = AVIF_TRUE; return gainMap; } diff --git a/src/gainmap.c b/src/gainmap.c index e1c84badfc..f61cdf0ff7 100644 --- a/src/gainmap.c +++ b/src/gainmap.c @@ -5,7 +5,6 @@ #include #include #include -#include #include #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) @@ -98,9 +97,11 @@ static inline float lerp(float a, float b, float w) #define SDR_WHITE_NITS 203.0f avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, - avifTransferCharacteristics transferCharacteristics, + avifColorPrimaries baseColorPrimaries, + avifTransferCharacteristics baseTransferCharacteristics, const avifGainMap * gainMap, float hdrHeadroom, + avifColorPrimaries outputColorPrimaries, avifTransferCharacteristics outputTransferCharacteristics, avifRGBImage * toneMappedImage, avifContentLightLevelInformationBox * clli, @@ -131,6 +132,14 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, const uint32_t width = baseImage->width; const uint32_t height = baseImage->height; + + const avifBool useBaseColorSpace = gainMap->metadata.useBaseColorSpace; + const avifColorPrimaries gainMapMathPrimaries = + (useBaseColorSpace || (gainMap->altColorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED)) ? baseColorPrimaries + : gainMap->altColorPrimaries; + const avifBool needsInputColorConversion = (baseColorPrimaries != gainMapMathPrimaries); + const avifBool needsOutputColorConversion = (gainMapMathPrimaries != outputColorPrimaries); + avifImage * rescaledGainMap = NULL; avifRGBImage rgbGainMap; // Basic zero-initialization for now, avifRGBImageSetDefaults() is called later on. @@ -146,7 +155,8 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, const float weight = avifGetGainMapWeight(hdrHeadroom, &metadata); // Early exit if the gain map does not need to be applied and the pixel format is the same. - if (weight == 0.0f && outputTransferCharacteristics == transferCharacteristics && baseImage->format == toneMappedImage->format && + if (weight == 0.0f && outputTransferCharacteristics == baseTransferCharacteristics && + outputColorPrimaries == baseColorPrimaries && baseImage->format == toneMappedImage->format && baseImage->depth == toneMappedImage->depth && baseImage->isFloat == toneMappedImage->isFloat) { assert(baseImage->rowBytes == toneMappedImage->rowBytes); assert(baseImage->height == toneMappedImage->height); @@ -163,19 +173,32 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, goto cleanup; } - const avifTransferFunction gammaToLinear = avifTransferCharacteristicsGetGammaToLinearFunction(transferCharacteristics); + const avifTransferFunction gammaToLinear = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics); const avifTransferFunction linearToGamma = avifTransferCharacteristicsGetLinearToGammaFunction(outputTransferCharacteristics); // Early exit if the gain map does not need to be applied. if (weight == 0.0f) { + const avifBool primariesDiffer = (baseColorPrimaries != outputColorPrimaries); + double conversionCoeffs[3][3]; + if (primariesDiffer && !avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, outputColorPrimaries, conversionCoeffs)) { + avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); + res = AVIF_RESULT_NOT_IMPLEMENTED; + goto cleanup; + } // Just convert from one rgb format to another. for (uint32_t j = 0; j < height; ++j) { for (uint32_t i = 0; i < width; ++i) { float basePixelRGBA[4]; avifGetRGBAPixel(baseImage, i, j, &baseRGBInfo, basePixelRGBA); - if (outputTransferCharacteristics != transferCharacteristics) { + if (outputTransferCharacteristics != baseTransferCharacteristics || primariesDiffer) { for (int c = 0; c < 3; ++c) { - basePixelRGBA[c] = AVIF_CLAMP(linearToGamma(gammaToLinear(basePixelRGBA[c])), 0.0f, 1.0f); + basePixelRGBA[c] = gammaToLinear(basePixelRGBA[c]); + } + if (primariesDiffer) { + avifLinearRGBConvertColorSpace(basePixelRGBA, conversionCoeffs); + } + for (int c = 0; c < 3; ++c) { + basePixelRGBA[c] = AVIF_CLAMP(linearToGamma(basePixelRGBA[c]), 0.0f, 1.0f); } } avifSetRGBAPixel(toneMappedImage, i, j, &toneMappedPixelRGBInfo, basePixelRGBA); @@ -184,6 +207,21 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, goto cleanup; } + double inputConversionCoeffs[3][3]; + double outputConversionCoeffs[3][3]; + if (needsInputColorConversion && + !avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, gainMapMathPrimaries, inputConversionCoeffs)) { + avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); + res = AVIF_RESULT_NOT_IMPLEMENTED; + goto cleanup; + } + if (needsOutputColorConversion && + !avifColorPrimariesComputeRGBToRGBMatrix(gainMapMathPrimaries, outputColorPrimaries, outputConversionCoeffs)) { + avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); + res = AVIF_RESULT_NOT_IMPLEMENTED; + goto cleanup; + } + if (gainMap->image->width != width || gainMap->image->height != height) { rescaledGainMap = avifImageCreateEmpty(); const avifCropRect rect = { 0, 0, gainMap->image->width, gainMap->image->height }; @@ -220,7 +258,6 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, const float gammaInv[3] = { 1.0f / (float)metadata.gainMapGamma[0], 1.0f / (float)metadata.gainMapGamma[1], 1.0f / (float)metadata.gainMapGamma[2] }; - for (uint32_t j = 0; j < height; ++j) { for (uint32_t i = 0; i < width; ++i) { float basePixelRGBA[4]; @@ -231,8 +268,18 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, // Apply gain map. float toneMappedPixelRGBA[4]; float pixelRgbMaxLinear = 0.0f; // = max(r, g, b) for this pixel + for (int c = 0; c < 3; ++c) { - const float baseLinear = gammaToLinear(basePixelRGBA[c]); + basePixelRGBA[c] = gammaToLinear(basePixelRGBA[c]); + } + + if (needsInputColorConversion) { + // Convert basePixelRGBA to gainMapMathPrimaries. + avifLinearRGBConvertColorSpace(basePixelRGBA, inputConversionCoeffs); + } + + for (int c = 0; c < 3; ++c) { + const float baseLinear = basePixelRGBA[c]; const float gainMapValue = gainMapRGBA[c]; // Undo gamma & affine transform; the result is in log2 space. @@ -248,9 +295,18 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, pixelRgbMaxLinear = toneMappedLinear; } - const float toneMappedGamma = linearToGamma(toneMappedLinear); - toneMappedPixelRGBA[c] = AVIF_CLAMP(toneMappedGamma, 0.0f, 1.0f); + toneMappedPixelRGBA[c] = toneMappedLinear; + } + + if (needsOutputColorConversion) { + // Convert toneMappedPixelRGBA to outputColorPrimaries. + avifLinearRGBConvertColorSpace(toneMappedPixelRGBA, outputConversionCoeffs); + } + + for (int c = 0; c < 3; ++c) { + toneMappedPixelRGBA[c] = AVIF_CLAMP(linearToGamma(toneMappedPixelRGBA[c]), 0.0f, 1.0f); } + toneMappedPixelRGBA[3] = basePixelRGBA[3]; // Alpha is unaffected by tone mapping. rgbSumLinear += pixelRgbMaxLinear; avifSetRGBAPixel(toneMappedImage, i, j, &toneMappedPixelRGBInfo, toneMappedPixelRGBA); @@ -279,6 +335,7 @@ avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, avifResult avifImageApplyGainMap(const avifImage * baseImage, const avifGainMap * gainMap, float hdrHeadroom, + avifColorPrimaries outputColorPrimaries, avifTransferCharacteristics outputTransferCharacteristics, avifRGBImage * toneMappedImage, avifContentLightLevelInformationBox * clli, @@ -300,9 +357,11 @@ avifResult avifImageApplyGainMap(const avifImage * baseImage, } res = avifRGBImageApplyGainMap(&baseImageRgb, + baseImage->colorPrimaries, baseImage->transferCharacteristics, gainMap, hdrHeadroom, + outputColorPrimaries, outputTransferCharacteristics, toneMappedImage, clli, @@ -387,11 +446,59 @@ avifResult avifFindMinMaxWithoutOutliers(const float * gainMapF, int numPixels, return AVIF_RESULT_OK; } +static const float kEpsilon = 1e-10f; + +// Decides which of 'basePrimaries' or 'altPrimaries' should be used for doing gain map math when creating a gain map. +// The other image (base or alternate) will be converted to this color space before computing +// the ratio between the two images. +// If a pixel color is outside of the target color space, some of the converted channel values will be negative. +// This should be avoided, as the negative values must either be clamped or offset before computing the log2() +// (since log2 only works on > 0 values). But a large offset causes artefacts when partially applying the gain map. +// Therefore we want to do gain map math in the larger of the two color spaces. +static avifResult avifChooseColorSpaceForGainMapMath(avifColorPrimaries basePrimaries, + avifColorPrimaries altPrimaries, + avifColorPrimaries * gainMapMathColorSpace) +{ + if (basePrimaries == altPrimaries) { + *gainMapMathColorSpace = basePrimaries; + return AVIF_RESULT_OK; + } + // Color convert pure red, pure green and pure blue in turn and see if they result in negative values. + float rgba[4] = { 0 }; + double baseToAltCoeffs[3][3]; + double altToBaseCoeffs[3][3]; + if (!avifColorPrimariesComputeRGBToRGBMatrix(basePrimaries, altPrimaries, baseToAltCoeffs) || + !avifColorPrimariesComputeRGBToRGBMatrix(altPrimaries, basePrimaries, altToBaseCoeffs)) { + return AVIF_RESULT_NOT_IMPLEMENTED; + } + + float baseColorspaceChannelMin = 0; + float altColorspaceChannelMin = 0; + for (int c = 0; c < 3; ++c) { + rgba[0] = rgba[1] = rgba[2] = 0; + rgba[c] = 1.0f; + avifLinearRGBConvertColorSpace(rgba, altToBaseCoeffs); + for (int i = 0; i < 3; ++i) { + baseColorspaceChannelMin = AVIF_MIN(baseColorspaceChannelMin, rgba[i]); + } + rgba[0] = rgba[1] = rgba[2] = 0; + rgba[c] = 1.0f; + avifLinearRGBConvertColorSpace(rgba, baseToAltCoeffs); + for (int i = 0; i < 3; ++i) { + altColorspaceChannelMin = AVIF_MIN(altColorspaceChannelMin, rgba[i]); + } + } + // Pick the colorspace that has the largest min value (which is more or less the largest color space). + *gainMapMathColorSpace = (altColorspaceChannelMin <= baseColorspaceChannelMin) ? basePrimaries : altPrimaries; + return AVIF_RESULT_OK; +} + avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, + avifColorPrimaries baseColorPrimaries, avifTransferCharacteristics baseTransferCharacteristics, const avifRGBImage * altRgbImage, + avifColorPrimaries altColorPrimaries, avifTransferCharacteristics altTransferCharacteristics, - avifColorPrimaries colorPrimaries, avifGainMap * gainMap, avifDiagnostics * diag) { @@ -404,9 +511,13 @@ avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, } if (gainMap->image->width == 0 || gainMap->image->height == 0 || gainMap->image->depth == 0 || gainMap->image->yuvFormat <= AVIF_PIXEL_FORMAT_NONE || gainMap->image->yuvFormat >= AVIF_PIXEL_FORMAT_COUNT) { - avifDiagnosticsPrintf(diag, "gianMap->image should be non null with desired width, height, depth and yuvFormat set"); + avifDiagnosticsPrintf(diag, "gainMap->image should be non null with desired width, height, depth and yuvFormat set"); return AVIF_RESULT_INVALID_ARGUMENT; } + const avifBool colorSpacesDiffer = (baseColorPrimaries != altColorPrimaries); + avifColorPrimaries gainMapMathPrimaries; + AVIF_CHECKRES(avifChooseColorSpaceForGainMapMath(baseColorPrimaries, altColorPrimaries, &gainMapMathPrimaries)); + const avifBool useBaseColorSpace = (gainMapMathPrimaries == baseColorPrimaries); const int width = baseRgbImage->width; const int height = baseRgbImage->height; @@ -441,8 +552,66 @@ avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, float (*baseGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics); float (*altGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(altTransferCharacteristics); - float y_coeffs[3]; - avifColorPrimariesComputeYCoeffs(colorPrimaries, y_coeffs); + float yCoeffs[3]; + avifColorPrimariesComputeYCoeffs(gainMapMathPrimaries, yCoeffs); + + double rgbConversionCoeffs[3][3]; + if (colorSpacesDiffer) { + if (useBaseColorSpace) { + if (!avifColorPrimariesComputeRGBToRGBMatrix(altColorPrimaries, baseColorPrimaries, rgbConversionCoeffs)) { + avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); + res = AVIF_RESULT_NOT_IMPLEMENTED; + goto cleanup; + } + } else { + if (!avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, altColorPrimaries, rgbConversionCoeffs)) { + avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); + res = AVIF_RESULT_NOT_IMPLEMENTED; + goto cleanup; + } + } + } + + // If we are converting from one colorspace to another, some RGB values may be negative and an offset must be added to + // avoid clamping (although the choice of color space to do the gain map computation with + // avifChooseColorSpaceForGainMapMath() should mostly avoid this). + if (colorSpacesDiffer) { + // Color convert pure red, pure green and pure blue in turn and see if they result in negative values. + float rgba[4] = { 0 }; + float channelMin[3] = { 0 }; + for (int j = 0; j < height; ++j) { + for (int i = 0; i < width; ++i) { + avifGetRGBAPixel(useBaseColorSpace ? altRgbImage : baseRgbImage, i, j, &baseRGBInfo, rgba); + + // Convert to linear. + for (int c = 0; c < 3; ++c) { + if (useBaseColorSpace) { + rgba[c] = altGammaToLinear(rgba[c]); + } else { + rgba[c] = baseGammaToLinear(rgba[c]); + } + } + avifLinearRGBConvertColorSpace(rgba, rgbConversionCoeffs); + for (int c = 0; c < 3; ++c) { + channelMin[c] = AVIF_MIN(channelMin[c], rgba[c]); + } + } + } + + for (int c = 0; c < 3; ++c) { + // Large offsets cause artefacts when partially applying the gain map, so set a max (empirical) offset value. + // If the offset is clamped, some gain map values will get clamped as well. + const float maxOffset = 0.1f; + if (channelMin[c] < -kEpsilon) { + // Increase the offset to avoid negative values. + if (useBaseColorSpace) { + gainMapMetadata.alternateOffset[c] = AVIF_MIN(gainMapMetadata.alternateOffset[c] - channelMin[c], maxOffset); + } else { + gainMapMetadata.baseOffset[c] = AVIF_MIN(gainMapMetadata.baseOffset[c] - channelMin[c], maxOffset); + } + } + } + } // Compute raw gain map values. float baseMax = 1.0f; @@ -460,13 +629,23 @@ avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, altRGBA[c] = altGammaToLinear(altRGBA[c]); } + if (colorSpacesDiffer) { + if (useBaseColorSpace) { + // convert altRGBA to baseRGBA's color space + avifLinearRGBConvertColorSpace(altRGBA, rgbConversionCoeffs); + } else { + // convert baseRGBA to altRGBA's color space + avifLinearRGBConvertColorSpace(baseRGBA, rgbConversionCoeffs); + } + } + for (int c = 0; c < numGainMapChannels; ++c) { float base = baseRGBA[c]; float alt = altRGBA[c]; if (singleChannel) { // Convert to grayscale. - base = y_coeffs[0] * baseRGBA[0] + y_coeffs[1] * baseRGBA[1] + y_coeffs[2] * baseRGBA[2]; - alt = y_coeffs[0] * altRGBA[0] + y_coeffs[1] * altRGBA[1] + y_coeffs[2] * altRGBA[2]; + base = yCoeffs[0] * baseRGBA[0] + yCoeffs[1] * baseRGBA[1] + yCoeffs[2] * baseRGBA[2]; + alt = yCoeffs[0] * altRGBA[0] + yCoeffs[1] * altRGBA[1] + yCoeffs[2] * altRGBA[2]; } if (base > baseMax) { baseMax = base; @@ -474,8 +653,8 @@ avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, if (alt > altMax) { altMax = alt; } - const float ratioLog2 = - log2f((alt + (float)gainMapMetadata.alternateOffset[c]) / (base + (float)gainMapMetadata.baseOffset[c])); + const float ratio = (alt + (float)gainMapMetadata.alternateOffset[c]) / (base + (float)gainMapMetadata.baseOffset[c]); + const float ratioLog2 = log2f(AVIF_MAX(ratio, kEpsilon)); gainMapF[c][j * width + i] = ratioLog2; } } @@ -495,11 +674,12 @@ avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, for (int c = 0; c < 3; ++c) { gainMapMetadata.gainMapMin[c] = gainMapMinLog2[singleChannel ? 0 : c]; gainMapMetadata.gainMapMax[c] = gainMapMaxLog2[singleChannel ? 0 : c]; - gainMapMetadata.baseHdrHeadroom = log2f(baseMax); - gainMapMetadata.alternateHdrHeadroom = log2f(altMax); + gainMapMetadata.baseHdrHeadroom = log2f(AVIF_MAX(baseMax, kEpsilon)); + gainMapMetadata.alternateHdrHeadroom = log2f(AVIF_MAX(altMax, kEpsilon)); // baseOffset, alternateOffset and gainMapGamma are all left to their default values. // They could be tweaked based on the images to optimize quality/compression. } + gainMapMetadata.useBaseColorSpace = useBaseColorSpace; if (!avifGainMapMetadataDoubleToFractions(&gainMap->metadata, &gainMapMetadata)) { res = AVIF_RESULT_UNKNOWN_ERROR; goto cleanup; @@ -589,13 +769,6 @@ avifResult avifImageComputeGainMap(const avifImage * baseImage, const avifImage avifDiagnosticsPrintf(diag, "Computing gain maps for images with ICC profiles is not supported"); return AVIF_RESULT_NOT_IMPLEMENTED; } - if (baseImage->colorPrimaries != altImage->colorPrimaries) { - avifDiagnosticsPrintf(diag, - "Computing gain maps for images with different color primaries is not supported, got %d and %d", - baseImage->colorPrimaries, - altImage->colorPrimaries); - return AVIF_RESULT_NOT_IMPLEMENTED; - } if (baseImage->width != altImage->width || baseImage->height != altImage->height) { avifDiagnosticsPrintf(diag, "Image dimensions don't match, got %dx%d and %dx%d", @@ -630,10 +803,11 @@ avifResult avifImageComputeGainMap(const avifImage * baseImage, const avifImage } res = avifRGBImageComputeGainMap(&baseImageRgb, + baseImage->colorPrimaries, baseImage->transferCharacteristics, &altImageRgb, + altImage->colorPrimaries, altImage->transferCharacteristics, - baseImage->colorPrimaries, gainMap, diag); diff --git a/src/reformat.c b/src/reformat.c index a8e2a553e9..01d4295d43 100644 --- a/src/reformat.c +++ b/src/reformat.c @@ -1794,6 +1794,9 @@ void avifSetRGBAPixel(const avifRGBImage * dst, uint32_t x, uint32_t y, const av assert(dst != NULL); assert(!dst->isFloat || dst->depth == 16); assert(dst->format != AVIF_RGB_FORMAT_RGB_565 || dst->depth == 8); + assert(rgbaPixel[0] >= 0.0f && rgbaPixel[0] <= 1.0f); + assert(rgbaPixel[1] >= 0.0f && rgbaPixel[1] <= 1.0f); + assert(rgbaPixel[2] >= 0.0f && rgbaPixel[2] <= 1.0f); uint8_t * const dstPixel = &dst->pixels[y * dst->rowBytes + x * info->pixelBytes]; diff --git a/tests/data/README.md b/tests/data/README.md index 0b23b67173..cb849201b0 100644 --- a/tests/data/README.md +++ b/tests/data/README.md @@ -544,11 +544,17 @@ SDR image with a gain map to allow tone mapping to HDR. The gain map's width and ## Files colors*_hdr_*.avif and colors*_sdr_srgb.avif +![](colors_wcg_hdr_rec2020.avif) + SDR and HDR (PQ) AVIF images in various colorspaces. -The HDR versions all show the same colors: all colors fit in sRGB but are encoded in various colorspaces. +The files with 'wcg' (wide color gamut) in their name have colors outside of the sRGB color space. +The files without 'wcg' in their name have sRGB colors, but expressed in various color spaces. + +The files with 'text' in their name have text on them. They are not currently used in tests but can be used for manual +testing of gain maps (e.g. with `avifgainmaputil combine ...`), as they make it easy to see which version +the browser is displaying. -The colors_text* files have text on them. They are not currently used in tests but can be used for manual -testing of gain maps, as they make it easy to see which version the browser is displaying. +HDR/wide color gamut images should be viewed on an HDR display, such as on a M1+ Mac Book Pro. Source : created with Photoshop 25.1.0 (Camera Raw 16.0.1.1683), see sources/colors.psd and https://helpx.adobe.com/camera-raw/using/hdr-output.html, @@ -558,8 +564,15 @@ Basic process: create a 32bit image, export it as png for the SDR version. Then open the Camera Raw filter (Filter > Camera Raw Filter...), click HDR at the top right, and drag the histogram towards the right to create brighter pixels. Click the save icon on the top right. Select AVIF as output format and check "HDR output" then save. - -To export more images from sources/colors.psd, flatten desired layers before opening the Camera Raw dialog. +To create an image with a wider color gamut, choose Edit > Assign Profile... and set the color space +to e.g. BT 2020. + +To export more images from sources/colors.psd: +- For SDR, show/hide the layers as desired then export to PNG then convert to avif with avifenc +- For HDR, show/hide the layers as desired then flatten the image, (Layers > Flatten Image), open + the Camera Raw dialog, and click the save icon on the top right. +- For the wide color gamut version, choose Edit > Assign Profile... and set the color space to + Rec.ITU-R BT.2020-1 ## Animated Images diff --git a/tests/data/colors_text_wcg_hdr_rec2020.avif b/tests/data/colors_text_wcg_hdr_rec2020.avif new file mode 100644 index 0000000000000000000000000000000000000000..b3e313c504891273ed0a69b7bfb96bf8fa82ee2a GIT binary patch literal 33242 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 literal 0 HcmV?d00001 diff --git a/tests/data/colors_wcg_hdr_rec2020.avif b/tests/data/colors_wcg_hdr_rec2020.avif new file mode 100644 index 0000000000000000000000000000000000000000..063d61ccffce96031e143949ad76dfef30596445 GIT binary patch literal 20613 zcmeHuc|6qL_y5?pkVux)Af#+#8GB`4$}XaXF$QCqVP@>ID`ZKw5M|3A+U!xb>{Mil zgzVXpJ$|pDwCVTzynny%_xtK?wvaz5D096bG3&#AuT|_ zg@4*1Eg*JCn1zNML=FTZh_XZAV0apEiCCH2Vs?@skUbKP`<})>01*H;3W-E*R|Fyg-dKPa1mQOt%t;8MfVM?%%V*xD0AwW>qy_%Q zGxF~e00DMLJH!PIZ4k`f-WCam;gD#QAjaI{q>G&`o);vAv_-@52}KZ4AHIIzCMXAi zIEg^`YJs<7mB91Wwl=^UR|E)jViCx3M%rWSfu|wT9*zc{3`nf~c5{IA%WwC7d!!2z zNP~zdDJg-+KA00k!3TT>pG6Nx+hR~~TNe-oXg`R;8_3!Rx3xtQtPxNHX(pt-9SnHl zF9Hw&2>~Io9|@2?j>I5#-~oVxn`7aCngAW3?7ZO{fqw(pnZs}gK$H{`Z1eNoFBA#Q zg+D7Pl9Pf!-hG4@t_b*G8oNracIUNM%cS}FtrspHuRn^iTT^@o?xx|_@EX4na9N1& zY-0$+?KOqj(12pmcAh?r@etVH<$ip zx_qEZ>ls>`sjJ1nIBg?+jC|6&lBldV@B$mJDHSrez>qyWU+GP3?bd-8yh^E!huH5| z3V!Cea8S0efl;Q*Lu_`4JOFiU;$G|ISEsKRxH6qvLwpdc#WyXl7gwnz6>kw;XkNm} z%`~^rtEAd)G z-`ST!qG|>Q_zaU})R$PVPR=&JqD2YDq2!?mpqro-r8PM#hJnLd2;GbhWlu} zIw;HNZ$(!h7yjBS{na5Aa8{A^=YT1>8tAjFR_qs%PWDKPCk>{^;Z!0UjHZG4KHR|6I@ggZrm_oET_U|XW6rg)|9bW#H`V(YO zQA8(0fUcRyA2$(}!gNE7&IU8v1vz3Pqu!z`OJjVE!g$f^V02AiYP@gR0fl%-*5Oag zotdPx{*$NGf@rg6FBaeSMBiwn_;j(-yily@Ju6XM*F;J0)%U6u5y5v2%9_Wp9m>%O zviGGLD6oA@oOHyg1}z^*EDsY^D=M#gZ*-BNgYHnW=holIeqeO^>)^WArLm#;eKFE- z6BmZ;O{;f>HLI@I8MtXj{kayF#0q0cmtn58Wfv6~nx(3s&$TC=4vHV(SPgoK;#DH= z?Qd_^;(Ju?ez%m8ZkQ5LC`)Q5Al3 z$t}bU*@N|})f9J)mcOm?PD}M*YNF|9xrq0fZd-GNr&HQL+-dIJ;mvlJm~5l5dN|0_F=Kvl@4}$RZoKA z6FPNTRrjqFt>!oKUHRqx#-$mCZui`2IDEFhF@x;!ClEY7N+PGT>ta6%yfNS`sa%0K zdNa#zP#hZ7!kea8JaFd1g!Lt7rE}sJ^?j@T*s?j?nxk}PR_6*+t_Wo2C=8>`sE2yc z{MKGMxTN}xRxRI>_}CSCBLhWCX=&oiIHCzt<=_duB&W!*Yju>q)ka17j-`cnr4!ag z=k3S14~A%D5NhSHT2Ofk)W-N-ElYXb*qzyd#+C_AS{sER^=svb=O!Z$m%R@zBgPPT zPQin^gl>bR(g~AJ9Gdrd5mu(m&^9M-lw1_|j;d$yNM>=LaVb}n2lOqM9_NU+zyN2T zGGc)gaiYL-Fl4NLc7ZKY?9{VEZH!%fA!mok)~~O4+(juXsG3I_>5*d$hM=)w>@ml& zA|OFzLX^NZgS)0&1#*In%}03ZBkziJ zCH5{@tFY4T6W!z%WUb9fb-%lXxDM?~L%g!3%V7w}a`MZ_vxeMYyOB$v+}}-ib|rh@ z{Us>a>Wx@l!VD}pr@a1|aiFYJ-Ij;cG>A=}q&;?EJ}}OM&h29wF^9KSroIT(!=z0@ zs*CjkA4mue?dRsFzm#43jy*E?=Hv2~c-2-{pE9==VWhZ$tAOdPYmkr16w$(v>mC z5xnL~-*{)aF-Th*oK{_;q3U?CDy6P6VW03#*~)dfeTG(YhXRUBM#V&|(}q{-xO2vm zyH~;=tZMChA307bZgr}kz^=p8iljVjU$H5KSgu>3wXQwwHffH`4L@iJ-zJ-6Y;4`f zvGjDQQsF+Ca_zf<8l4nJ2oJ`E4=C9`&}YAz(cpd*tzrJp>bmHVTQCi^?~-d^ zzS=>%qT6N_Qr=3EDtFJl8JN37({Lx@wI@{c($&?TIM~Ib#v~%5(yf`EM+%J@726+D zLwjJ1M@8#M3$AkdRRwU8yRcq{J{Mfsggz*cOj^@}^;&aW4Wg}B&3G^lu!dnn!2v=A&5` z!bb9x8kqw6`hD-CpD=`e{#39)B4;qy7SmxHXLKk=X2Lmah|y~EsaMJuCG>Da4a9cd z;;V96venbm@>^~Viw*Hng8PkIQ@PJOf*xK|d6$c(s5GCA*Cw_fJn3oA^DOaTAuZdr zhFoH}h=p?@d$P zyB(#z%ak|M-aCo36YWE?8I$U8+gy?UVtoJPm6=kh{;D`}-?!a#8?@?Ivr|!7$N)Pt zs< zA`+wVIpvr{^V}&Kw@(!UcBAn|fksHSh&=t)^F*G4ha5lY-KUSRX4z;Q$)<}(`*uZYkZ}r{vn)P#)O;MGA{42=A8*19U5&txAu#( zl&hjz{hPHHHrjeB70%VKZRiI!-5n_%@VqG1p509^{&srIM4z1aW_F+Rfq8PZjEZ$d zK5yH@={5`!pQ*RllY)a_JkeIg!){vdiY~`Lls_~lX4qg5fm(Fedg{ARqAaNDgy36h zr&&&+NbnpnX|`w<*+vlOovKf|W64X?eIkJgcP`D8g&q3z(ct_+t|GJ-c3(^kH3!7@ z$cPs1p5Alc@T)JA85bA6I`P=4(kaP5Ys}zpqg}Wpd=G3rw2>^SaUp7rZGg(}k!IVy zG`gg%yp;5W6x#v)EAJn)-xR5uN(+$IV+_zg#a_rQbq?gk&*zd)p}<6Q;N&2Lol2d? z`0cIh!HjW-?G7ZYoIx20cRa=wG?<-UqR6JMe4xX=P)r?jdMwrX$QzDzkD*TqEij|g zRHbp3DQ-*i=!^G$mW_>A%|4cLWwBeN`8|O$~Ha}DJSV)aA-?O5bfi>Y%h5~*3V>-6v;{$nV zLP}|Yygqo0%7IqNf~;T6`rXTNv!o|Mi+zj+$JWU$LQlTxtFR_1VohYVbI;sgK|Olz ze)JfH0af~o04DU@gsn(TIM0$rxUb4;ks|$Cg-R+vHJn|SO@@igf{<4`OQ@yfvIiqp zf^qAU=f*mhi*dDR;&kW-M>o!Ua+x6xW~kS7@r~D=dY|5)QNGsuK77t(d79hn6MFfa zIL-ZFeKs!`&!wS{w5+A~+>Jc)_R~LtZ5VZl-{Kn|pwUaVud!|tH{@`gJ>3>3G;_6) zKx2AwT)xX_`g!`vBO9+p#8RTBzK&QHl7KuzZ46B!Hmcbg)m?3T(hFZ(csv@@)||O~ zf-zdznPOh#BY*M{^71(G14bEqF_)~DVkn@bHlT7x5EqXqyAdTbG`O|)z^tltuN6{1 z+;cN}wdS6f-@cwA@8P$NMd{)(>x2vxBD7j@8qi0Nb&M{8-kc$?OMj5`$Z>*Nm6%R( z342OYyH!J1F;`ZbaypOxanGBLbA`Ig)|#2LvQYs;*3T1w=mY$X_vngPQSZISaR&4W1@;r73K1{vY~gzWMil4h{h2v zF2*<4q-PAku8FMK+_4)bukzCG+DryJJy{}I9;|Xw7HT=jx$*vkuU1P_7u5+2YcQmn z0V7mBJY?$Ec|lu3CD*y5-(zaR_Qe}Se-G00{rkgWx8KUOv#H7lHk(m!ic91xL>YZR zk&)e?p=~|fL(E$txCTE)#+x#UUF<2OjJQs!72EX8GWenr^z15p@S0SzutaDF>LBQ2 z@dLz`WmPtY;Zc~43fuYeO5M=}O{uH$EagtH=T(C#0ig_s>kC7Chcxp2ZQ?qQyB*6e zDsFnj1vc`~ z8zD=FBwn53*<#UIH*;r~us6`YozqYrsZ>G{P~B?gQAQ@1&!OUBGd_N+#m(gyN^MNe z@v!J%e8YoA`w`dpI_0n#=I+eY*hj13E!UgXS9qL9sj4=iUdgw};m;}_1S<^nt%SH= zcs~B{3%zc~POZ7Fgn9{Vv z)u)VJfjfFWh6OUuSKoY`qB?*q;d(+7P1%0tAcZj{b>pTPs+p4e&iS+iW9ucsCMpdp zhGq|D?#QmqykFzP-z$DG zEJOQ@WxW5BW5O2*T50-I6~v3}PTeSdmigFjqtW-c)oe#~w3t^#iGXOOOk*ML1Ff_R zrDQWDOAVKU(8Br0md_4Xg!^QXpLQ1OdBm3irG+O3aC^$>MP7z`-Y+?J_`}`TK@v8h zT%1Q|neGfeA-pnx(eu6K@vPp~VTpfU^uZNt-x7InUfW2T#RQT!x2)cDet^c*_l9_D zoW0|vsu0IRLfY-6uqo8SL?RC(4@3#%8FC`&0@Qr&S+2wTxl!kRac6lvuHW{gYc0?v z8fO2JHe;%G)#I^bh3mVdR!RlqWk(dj#OtL)are1Dm^>gnak&IGSxRrk1LA0LvWbT+KV0{hk_f6BAC@NLS2dmZuw)NRrw!gzqf#Lz=Gg zc%Glt5lj|1I_h#KPdPXcBSrsMDCETGTViKvnSn{0dgeE*MUTbHt=@lH5r*3agUp&C zLe?Vp8$KpzoVcQ92h} zzBCLG9Gs}tFv!F9&SZNGJ?afvoXpFveS7%hitq}bjGTW2{g`CI`^tiwN571pT|E8z z(u5RSy|BamrgGK-M?pOZo%VpymzWE>t>}5N1F|j&+!XGb+~fVmwRMz6k5TSNs^3RD zt(}5^ie6zUDyo8CKWXuhD_}G%8D+ewelvfnGddly?8b3GbcUu;?R@#bRr!^ypF z7Qx=U*15hG586R7XAa{mV-wn&f)p(t*XGrc&=!-=kxrGMh~w0m6v-0E=}(%XJXYA* zEwZ=#Bb!m;t7-i&G*-sgRXhW(5F@^7KAh{krF7%W`OOxCi9`W`hsN)MwFlemuW7_Q z6}HOMu6J5aNxW2( zzNpy$;tN76cGKhOBbkGJA%gQH<4~Al+A03hX79q6Azz^2mr%GnV}d{p z#BRhv-n_$oxsvbK*$lZ22o>YREVH_TOjKy;woHmR*T`m)E_%ApB+vGe8tBogDg@L% z=+Jai)~{8N_SD~hecqHfPwalas_#wicUcRs4EteC2gs~vt)I`n8qOw3>8AVON96lC zKkTAu@jDM^B#VzkJ$r1A>%twB$r!77#PccY^}{2N&c;TlHnM*B{G#uUr!TKIEGzk} zJTL3R+iWxUkzmKbmXW8)PoB@xlh>WBBV~5x(`pYQ>1uAx%eu#~(HFb)%oQ%4xU&g4-eTz@4vgt53Vqx`V+=2LxoD+~Cb>B9u1{vK( zclHKIFS3unLc59T>7*55SwKP7MD-CpXG_VbG0!)Wepk;M`zThW*R=92aT1L$sm2n> zFz{UC=Z(R96tOn-W1ePtNHQ#8$gDA36$K}+w0)p7V>cJrn6bcmAJcVpwUQc&hI%3g z1!0J2|GI!Q(|yb#)AwIiPa24CT=IVLmXVa>dXb3$qiCcm43)^*cQ%Q7Pp#d#?bvx>1b3?GHGmWA2mqI<^0qM+ir|a~4 zxn6{;p9bkL=`f5LitEOVba6fvwkTYwk%$)Kp1k%fpTLESz{~ZH<=oYWBlJPzH+lvI zV@!{4b!aLqzIjimMq;%Fbzc4AU0r3*b7Ji7%;RWT+JKCD%5&v1Ot{WYM+K*i(^C|@ zoS~;rsIcbNG!-Z48!?S3g5jf7W5A!XTIHWGMJZzlRWoRtNctBYLKO}x`8@EKQq3_ z%p_^wd`x5IvDKHxqWo#K+4UBhxq<)uG%x z^{*UHIkK8X=TB+bb7;P7mND#QrhH-1)*b}bTB3&VT+3&qqZ&_D8jiS@cXuu_iX*0F zP@Oc#!-_nB^0t0+P1HaJZ_|*RBppQj;bPuD1FT{~>~V#U)*-+g(^*-DyW z>*?`UV=rvURpFH0iqVLauu^bPTPt@eSkMZ&c%=KmrNzLgO7$cL(yh%f@MFRe8m)zi z0*fok3XT9qN6@9R&`Dc9gU>C zypDjLd~Yr*JV`?H$sAHdztWJEcFN>wQTF)k@mXW!qm3Y+MNfkJ-iguKw(0qbXI)2e zfkPegX}CnShtuY9n{$-WZl$&_;PUO4GjFe<1)Yl%9M(a5zo%Bd6 z`m+m@xNhe8T_OK&L($k6*{qWAN8L$F^$ps?O+NeeGL;H{7mt?O>bVb^D0er2UR zz-_dOnf`?4;}gs1Ah+cmx*+2;c`$@Zn&-(#Sg1blN2dHuV@GO>{0__UB<2TC#zc)} z?%d`{G?8J<6t?%vyB#sXMxB`>am43{C@XC6~Km;eBO;PZP9M=NKGn>Nm10)}L36>MN}(h4z`$jKu~~yoi!J z!NR}pn`#rKd@hwXzjg`kuFB<=??IOfbz3bQy7BQrQJ#WvW1QO|wji?>76BH&M%sjv=*E;#u& zy2dX_i-hUp?A14>OU$>JWV)rvEvX$Izw=ych&Vf}e5?NyPlxbGCAn8l=~HoMOD_xO zm`u9om)jUVzsau^Myk$h299y5h}l!|`8d^bYuVA&q_mZA9!!?ki=3&hhi*-3_6Q|#g3sBAsUk2&QZ#Jxor z*z$QtvyLx<9S?pM zZ{!uKq8y{6)&VBP$R)a!pX~e8zvAj_Ru(Mtl)e3d<(12c{Rbi@4U}HoV+=TZQK(Bs z_Sy^hP~Dgeu9l>@I@jVjeRp0(|C{lGJgy|&VV#i@a7D;tzAP3nN+u13Yd$WrBIJJb4p{pW0?pXB}H;-*v_Cr=ze=r&k5Tqoukf(0!X6JgJQEzs(V#J5oWX9>^rR#QlS2MUMGw0-C`kFoI z@gHdJOW&*cy1u{UL_F&JWgil(u45A#Uf`fIko-1X(u!KRRJS2a#?-mcA96nYb`$H+^lA*d zg*rr*KIa%Cp?!+ue0=DJZpBq&db&n80~4n1{<6qhAEmB$gR7$&StM8NWNEmR+0cZ+ z7h58}x?JWUj`C|^>YMKjv^yyI#VPj{4^fvzkar|K#^LlW=vea+$3XS=A)4-r4eo0F zfHNKktQ{RiJ1hEQUF$mQ4`l?0o{Sr@D@iH1lH_Mpbe84(d*YIDM_IO+woML4QAslC zSJy+UU8k>xg`Xv%n^bj6m6U&UMA(&85{u=Fn;Gs1JD}W^EG=rdHO1r5LUYU*C|_pNn`41xT#6LblpG`E~9KbTpPFELn03aaNjvtm!pwbl&w-4fPBEnP^x z&^XaWJm;_uO@z>>nsggLZw!3dSAM&f+CWdRG9APppHVn{sDY6B#M2E&g*!D*)>7Gd zWFIr@XWuAn;Dlj6DWJ;$iu29_98k(;>ox zPefkw=zQ+)joLIl8qtt-U9Un~E-mdhiRUGI}K(L7a2Z6`T%VSpjeOUgm9>aL;!$*LNCUBZKI%DxXaMqiiS&Cz^( z*!WVVjH{V&Xt7f@ZiHOoXuZt~Ge>*-<>i_-X_;H5kC4nD`K6{Oe0{b0qaL-RqP8+F z0&gmwOe#19d~I6kjIB9^4VWO$gL-@#A%RL^Ix&wSi2IuaGoO^Vg z>1)c}uXoRokiIE3)+W&uKNbYLEPI-sD{7jcS849EVA=7!fj;PZ7Ny>k%B6{hyQ6vf zkx&~gezk|h<8)&FV5^=l91(5RF2_%Hw!#}f9#eA}ss#L!)UA8Tp+i+XM4LT1a|4?1 zUxS#34f)tuR!$PrY?%v8MI7o0*FW$!U&u2l(i`?5pKk3zxLnD5zoe5zqOah}d$rOtL&T8s%F!uSDhG>p0N9Bhe zpDmC~3URO!HLYqt`z99^`_;`N*#gsxw=xbkJ{0L&QPlqGL3_CQO|*On1vB}nWDa*T z`9sIaMgmcp=N64PO*3dtmC)Df-*=E~Q8y+q@?(>zKWgqikQz<@RjmJ&(S_wv&+DPr ze2%DyPqS5V(OujBQ3{;SR-9vybzD~bn*7#q)%g^ECbl{JQ5ZF~6CiTnNd$rr5P}4N zlra7V(hR`U6u6PsiMLP9d_S!uXO1>QfVHjAI5gG@Z4Xv}*.urACQ41o#=2|&Qd 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 X{)OwGA@JjXEATV6EQd32eD!|;$ZR&o literal 0 HcmV?d00001 diff --git a/tests/gtest/avifgainmaptest.cc b/tests/gtest/avifgainmaptest.cc index 6d63aa6bae..d5fde409c1 100644 --- a/tests/gtest/avifgainmaptest.cc +++ b/tests/gtest/avifgainmaptest.cc @@ -907,7 +907,9 @@ TEST(GainMapTest, ConvertMetadataToDoubleInvalid) { avifGainMapMetadataFractionsToDouble(&metadata_double, &metadata)); } -static void SwapBaseAndAlternate(avifGainMapMetadata& metadata) { +static void SwapBaseAndAlternate(const avifImage& new_alternate, + avifGainMap& gain_map) { + avifGainMapMetadata& metadata = gain_map.metadata; metadata.backwardDirection = !metadata.backwardDirection; metadata.useBaseColorSpace = !metadata.useBaseColorSpace; std::swap(metadata.baseHdrHeadroomN, metadata.alternateHdrHeadroomN); @@ -916,6 +918,14 @@ static void SwapBaseAndAlternate(avifGainMapMetadata& metadata) { 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. @@ -939,6 +949,7 @@ TEST(GainMapTest, CreateTestImages) { 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; @@ -981,7 +992,7 @@ TEST(GainMapTest, CreateTestImages) { // 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(hdr_image->gainMap->metadata); + SwapBaseAndAlternate(*sdr_with_gainmap, *hdr_image->gainMap); hdr_image->gainMap->altColorPrimaries = sdr_with_gainmap->colorPrimaries; hdr_image->gainMap->altTransferCharacteristics = sdr_with_gainmap->transferCharacteristics; @@ -1041,13 +1052,18 @@ void ToneMapImageAndCompareToReference( 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 = base_image->colorPrimaries; - tone_mapped->matrixCoefficients = base_image->matrixCoefficients; + 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->transferCharacteristics, - &tone_mapped_rgb, &tone_mapped->clli, &diag); + 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), @@ -1102,7 +1118,7 @@ TEST_P(ToneMapTest, ToneMapImage) { 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, @@ -1255,40 +1271,41 @@ TEST(ToneMapTest, ToneMapImageSameHeadroom) { } } -class CreateGainMapTest : public testing::TestWithParam> {}; +class CreateGainMapTest + : public testing::TestWithParam> {}; +// 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 int downscaling = std::get<0>(GetParam()); - const int gain_map_depth = std::get<1>(GetParam()); - const avifPixelFormat gain_map_format = std::get<2>(GetParam()); - const float min_psnr = std::get<3>(GetParam()); - const float max_psnr = std::get<4>(GetParam()); - - const std::string sdr_image_name = "seine_sdr_gainmap_srgb.avif"; - const std::string hdr_image_name = "seine_hdr_gainmap_srgb.avif"; - ImagePtr sdr_image = - testutil::DecodeFile(std::string(data_path) + sdr_image_name); - ASSERT_NE(sdr_image, nullptr); - ImagePtr hdr_image = - testutil::DecodeFile(std::string(data_path) + hdr_image_name); - ASSERT_NE(hdr_image, nullptr); + 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)std::round((float)sdr_image->width / downscaling), 1u); + (uint32_t)std::round((float)image1->width / downscaling), 1u); const uint32_t gain_map_height = std::max( - (uint32_t)std::round((float)sdr_image->height / downscaling), 1u); + (uint32_t)std::round((float)image1->height / downscaling), 1u); std::unique_ptr gain_map( avifGainMapCreate(), avifGainMapDestroy); - ImagePtr gain_map_image(avifImageCreate(gain_map_width, gain_map_height, - gain_map_depth, gain_map_format)); - gain_map->image = - gain_map_image.release(); // 'gain_map' now owns gain_map_image; + gain_map->image = avifImageCreate(gain_map_width, gain_map_height, + gain_map_depth, gain_map_format); avifDiagnostics diag; - avifResult result = avifImageComputeGainMap(sdr_image.get(), hdr_image.get(), + 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; @@ -1296,98 +1313,140 @@ TEST_P(CreateGainMapTest, Create) { EXPECT_EQ(gain_map->image->width, gain_map_width); EXPECT_EQ(gain_map->image->height, gain_map_height); - const float hdr_headroom = (float)gain_map->metadata.alternateHdrHeadroomN / - gain_map->metadata.alternateHdrHeadroomD; + 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 sdr to hdr. - float psnr_sdr_to_hdr_forward; + // Tone map from image1 to image2 by applying the gainmap forward. + float psnr_image1_to_image2_forward; ToneMapImageAndCompareToReference( - sdr_image.get(), *gain_map, hdr_headroom, hdr_image->depth, - hdr_image->transferCharacteristics, AVIF_RGB_FORMAT_RGB, hdr_image.get(), - min_psnr, max_psnr, &psnr_sdr_to_hdr_forward); + 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 hdr to sdr. - SwapBaseAndAlternate(gain_map->metadata); - float psnr_hdr_to_sdr_backward; + // Tone map from image2 to image1 by applying the gainmap backward. + SwapBaseAndAlternate(*image1, *gain_map); + float psnr_image2_to_image1_backward; ToneMapImageAndCompareToReference( - hdr_image.get(), *gain_map, /*hdr_headroom=*/0.0, sdr_image->depth, - sdr_image->transferCharacteristics, AVIF_RGB_FORMAT_RGB, sdr_image.get(), - min_psnr, max_psnr, &psnr_hdr_to_sdr_backward); + 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_sdr_to_hdr.png")); + // ASSERT_TRUE(testutil::WriteImage(gain_map->image, + // "/tmp/gain_map_image1_to_image2.png")); - // Compute the gain map in the other direction (from hdr to sdr). - result = avifImageComputeGainMap(hdr_image.get(), sdr_image.get(), - gain_map.get(), &diag); + // 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 hdr_headroom2 = (float)gain_map->metadata.baseHdrHeadroomN / - gain_map->metadata.baseHdrHeadroomD; - EXPECT_NEAR(hdr_headroom2, hdr_headroom, 0.001); + const float image2_headroom2 = (float)gain_map->metadata.baseHdrHeadroomN / + gain_map->metadata.baseHdrHeadroomD; + EXPECT_NEAR(image2_headroom2, image2_headroom, 0.001); - // Tone map from hdr to sdr. - float psnr_hdr_to_sdr_forward; + // Tone map from image2 to image1 by applying the new gainmap forward. + float psnr_image2_to_image1_forward; ToneMapImageAndCompareToReference( - hdr_image.get(), *gain_map, /*hdr_headroom=*/0.0, sdr_image->depth, - sdr_image->transferCharacteristics, AVIF_RGB_FORMAT_RGB, sdr_image.get(), - min_psnr, max_psnr, &psnr_hdr_to_sdr_forward); + 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 sdr to hdr. - SwapBaseAndAlternate(gain_map->metadata); - float psnr_sdr_to_hdr_backward; + // Tone map from image1 to image2 by applying the new gainmap backward. + SwapBaseAndAlternate(*image2, *gain_map); + float psnr_image1_to_image2_backward; ToneMapImageAndCompareToReference( - sdr_image.get(), *gain_map, hdr_headroom, hdr_image->depth, - hdr_image->transferCharacteristics, AVIF_RGB_FORMAT_RGB, hdr_image.get(), - min_psnr, max_psnr, &psnr_sdr_to_hdr_backward); + 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_hdr_to_sdr.png")); + // "/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_sdr_to_hdr_backward, psnr_sdr_to_hdr_forward, 0.6f); - EXPECT_NEAR(psnr_hdr_to_sdr_forward, psnr_hdr_to_sdr_backward, 0.6f); + 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(/*downscaling=*/1, /*gain_map_depth=*/10, + 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(/*downscaling=*/1, /*gain_map_depth=*/8, + 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(/*downscaling=*/1, /*gain_map_depth=*/8, + 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(/*downscaling=*/2, /*gain_map_depth=*/8, + 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(/*downscaling=*/3, /*gain_map_depth=*/8, + 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(/*downscaling=*/255, /*gain_map_depth=*/8, + 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(/*downscaling=*/1, /*gain_map_depth=*/8, + 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(/*downscaling=*/2, /*gain_map_depth=*/8, + 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))); + /*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; diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc index 592a0841ad..3813103cfd 100644 --- a/tests/gtest/aviftest_helpers.cc +++ b/tests/gtest/aviftest_helpers.cc @@ -305,6 +305,21 @@ double GetPsnr(const avifImage& image1, const avifImage& image2, } assert(image1.width * image1.height > 0); + if (image1.colorPrimaries != image2.colorPrimaries || + image1.transferCharacteristics != image2.transferCharacteristics || + image1.matrixCoefficients != image2.matrixCoefficients || + image1.yuvRange != image2.yuvRange) { + fprintf(stderr, + "WARNING: computing PSNR of images with different CICP: %d/%d/%d%s " + "vs %d/%d/%d%s\n", + image1.colorPrimaries, image1.transferCharacteristics, + image1.matrixCoefficients, + (image1.yuvRange == AVIF_RANGE_FULL) ? "f" : "l", + image2.colorPrimaries, image2.transferCharacteristics, + image2.matrixCoefficients, + (image2.yuvRange == AVIF_RANGE_FULL) ? "f" : "l"); + } + uint64_t squared_diff_sum = 0; uint32_t num_samples = 0; const uint32_t max_sample_value = (1 << image1.depth) - 1; diff --git a/tests/test_cmd_avifgainmaputil.sh b/tests/test_cmd_avifgainmaputil.sh index c48e323c7a..08acaf72ab 100755 --- a/tests/test_cmd_avifgainmaputil.sh +++ b/tests/test_cmd_avifgainmaputil.sh @@ -31,6 +31,7 @@ ARE_IMAGES_EQUAL="${BINARY_DIR}/tests/are_images_equal" # Input file paths. INPUT_AVIF_GAINMAP_SDR="${TESTDATA_DIR}/seine_sdr_gainmap_srgb.avif" INPUT_AVIF_GAINMAP_HDR="${TESTDATA_DIR}/seine_hdr_gainmap_srgb.avif" +INPUT_AVIF_GAINMAP_HDR2020="${TESTDATA_DIR}/seine_hdr_rec2020.avif" JPEG_AVIF_GAINMAP_SDR="${TESTDATA_DIR}/seine_sdr_gainmap_srgb.jpg" # Output file names. AVIF_OUTPUT="avif_test_cmd_avifgainmaputil_output.avif" @@ -60,6 +61,8 @@ pushd ${TMP_DIR} -q 50 --qgain-map 90 && exit 1 # should fail because icc profiles are not supported "${AVIFGAINMAPUTIL}" combine "${JPEG_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR}" "${AVIF_OUTPUT}" \ -q 50 --qgain-map 90 --ignore-profile + "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR2020}" "${AVIF_OUTPUT}" \ + -q 50 --downscaling 2 --yuv-gain-map 400 "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_HDR}" "${INPUT_AVIF_GAINMAP_SDR}" "${AVIF_OUTPUT}" \ -q 90 --qgain-map 90