| // Copyright 2023 Google LLC |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "avif/internal.h" |
| #include <assert.h> |
| #include <float.h> |
| #include <math.h> |
| #include <string.h> |
| |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| |
| avifBool avifGainMapMetadataDoubleToFractions(avifGainMapMetadata * dst, const avifGainMapMetadataDouble * src) |
| { |
| AVIF_CHECK(dst != NULL && src != NULL); |
| |
| for (int i = 0; i < 3; ++i) { |
| AVIF_CHECK(avifDoubleToSignedFraction(src->gainMapMin[i], &dst->gainMapMinN[i], &dst->gainMapMinD[i])); |
| AVIF_CHECK(avifDoubleToSignedFraction(src->gainMapMax[i], &dst->gainMapMaxN[i], &dst->gainMapMaxD[i])); |
| AVIF_CHECK(avifDoubleToUnsignedFraction(src->gainMapGamma[i], &dst->gainMapGammaN[i], &dst->gainMapGammaD[i])); |
| AVIF_CHECK(avifDoubleToSignedFraction(src->baseOffset[i], &dst->baseOffsetN[i], &dst->baseOffsetD[i])); |
| AVIF_CHECK(avifDoubleToSignedFraction(src->alternateOffset[i], &dst->alternateOffsetN[i], &dst->alternateOffsetD[i])); |
| } |
| AVIF_CHECK(avifDoubleToUnsignedFraction(src->baseHdrHeadroom, &dst->baseHdrHeadroomN, &dst->baseHdrHeadroomD)); |
| AVIF_CHECK(avifDoubleToUnsignedFraction(src->alternateHdrHeadroom, &dst->alternateHdrHeadroomN, &dst->alternateHdrHeadroomD)); |
| dst->useBaseColorSpace = src->useBaseColorSpace; |
| return AVIF_TRUE; |
| } |
| |
| avifBool avifGainMapMetadataFractionsToDouble(avifGainMapMetadataDouble * dst, const avifGainMapMetadata * src) |
| { |
| AVIF_CHECK(dst != NULL && src != NULL); |
| |
| AVIF_CHECK(src->baseHdrHeadroomD != 0); |
| AVIF_CHECK(src->alternateHdrHeadroomD != 0); |
| for (int i = 0; i < 3; ++i) { |
| AVIF_CHECK(src->gainMapMaxD[i] != 0); |
| AVIF_CHECK(src->gainMapGammaD[i] != 0); |
| AVIF_CHECK(src->gainMapMinD[i] != 0); |
| AVIF_CHECK(src->baseOffsetD[i] != 0); |
| AVIF_CHECK(src->alternateOffsetD[i] != 0); |
| } |
| |
| for (int i = 0; i < 3; ++i) { |
| dst->gainMapMin[i] = (double)src->gainMapMinN[i] / src->gainMapMinD[i]; |
| dst->gainMapMax[i] = (double)src->gainMapMaxN[i] / src->gainMapMaxD[i]; |
| dst->gainMapGamma[i] = (double)src->gainMapGammaN[i] / src->gainMapGammaD[i]; |
| dst->baseOffset[i] = (double)src->baseOffsetN[i] / src->baseOffsetD[i]; |
| dst->alternateOffset[i] = (double)src->alternateOffsetN[i] / src->alternateOffsetD[i]; |
| } |
| dst->baseHdrHeadroom = (double)src->baseHdrHeadroomN / src->baseHdrHeadroomD; |
| dst->alternateHdrHeadroom = (double)src->alternateHdrHeadroomN / src->alternateHdrHeadroomD; |
| dst->useBaseColorSpace = src->useBaseColorSpace; |
| return AVIF_TRUE; |
| } |
| |
| static void avifGainMapMetadataDoubleSetDefaults(avifGainMapMetadataDouble * metadata) |
| { |
| memset(metadata, 0, sizeof(*metadata)); |
| for (int i = 0; i < 3; ++i) { |
| metadata->baseOffset[i] = 0.015625; // 1/64 |
| metadata->alternateOffset[i] = 0.015625; // 1/64 |
| metadata->gainMapGamma[i] = 1.0; |
| } |
| metadata->baseHdrHeadroom = 0.0; |
| metadata->alternateHdrHeadroom = 1.0; |
| metadata->useBaseColorSpace = AVIF_TRUE; |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Apply a gain map. |
| |
| // Returns a weight in [-1.0, 1.0] that represents how much the gain map should be applied. |
| static float avifGetGainMapWeight(float hdrHeadroom, const avifGainMapMetadataDouble * metadata) |
| { |
| const float baseHdrHeadroom = (float)metadata->baseHdrHeadroom; |
| const float alternateHdrHeadroom = (float)metadata->alternateHdrHeadroom; |
| if (baseHdrHeadroom == alternateHdrHeadroom) { |
| // Do not apply the gain map if the HDR headroom is the same. |
| // This case is not handled in the specification and does not make practical sense. |
| return 0.0f; |
| } |
| const float w = AVIF_CLAMP((hdrHeadroom - baseHdrHeadroom) / (alternateHdrHeadroom - baseHdrHeadroom), 0.0f, 1.0f); |
| return (alternateHdrHeadroom < baseHdrHeadroom) ? -w : w; |
| } |
| |
| // Linear interpolation between 'a' and 'b' (returns 'a' if w == 0.0f, returns 'b' if w == 1.0f). |
| static inline float lerp(float a, float b, float w) |
| { |
| return (1.0f - w) * a + w * b; |
| } |
| |
| #define SDR_WHITE_NITS 203.0f |
| |
| avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, |
| avifColorPrimaries baseColorPrimaries, |
| avifTransferCharacteristics baseTransferCharacteristics, |
| const avifGainMap * gainMap, |
| float hdrHeadroom, |
| avifColorPrimaries outputColorPrimaries, |
| avifTransferCharacteristics outputTransferCharacteristics, |
| avifRGBImage * toneMappedImage, |
| avifContentLightLevelInformationBox * clli, |
| avifDiagnostics * diag) |
| { |
| avifDiagnosticsClearError(diag); |
| |
| if (hdrHeadroom < 0.0f) { |
| avifDiagnosticsPrintf(diag, "hdrHeadroom should be >= 0, got %f", hdrHeadroom); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| if (baseImage == NULL || gainMap == NULL || toneMappedImage == NULL) { |
| avifDiagnosticsPrintf(diag, "NULL input image"); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| |
| avifGainMapMetadataDouble metadata; |
| if (!avifGainMapMetadataFractionsToDouble(&metadata, &gainMap->metadata)) { |
| avifDiagnosticsPrintf(diag, "Invalid gain map metadata, a denominator value is zero"); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| for (int i = 0; i < 3; ++i) { |
| if (metadata.gainMapGamma[i] <= 0) { |
| avifDiagnosticsPrintf(diag, "Invalid gain map metadata, gamma should be strictly positive"); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| } |
| |
| 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. |
| memset(&rgbGainMap, 0, sizeof(rgbGainMap)); |
| |
| avifResult res = AVIF_RESULT_OK; |
| toneMappedImage->width = width; |
| toneMappedImage->height = height; |
| AVIF_CHECKRES(avifRGBImageAllocatePixels(toneMappedImage)); |
| |
| // --- After this point, the function should exit with 'goto cleanup' to free allocated pixels. |
| |
| 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 == 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); |
| // Copy the base image. |
| memcpy(toneMappedImage->pixels, baseImage->pixels, baseImage->rowBytes * baseImage->height); |
| goto cleanup; |
| } |
| |
| avifRGBColorSpaceInfo baseRGBInfo; |
| avifRGBColorSpaceInfo toneMappedPixelRGBInfo; |
| if (!avifGetRGBColorSpaceInfo(baseImage, &baseRGBInfo) || !avifGetRGBColorSpaceInfo(toneMappedImage, &toneMappedPixelRGBInfo)) { |
| avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
| res = AVIF_RESULT_NOT_IMPLEMENTED; |
| goto cleanup; |
| } |
| |
| 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 != baseTransferCharacteristics || primariesDiffer) { |
| for (int c = 0; c < 3; ++c) { |
| 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); |
| } |
| } |
| 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 }; |
| res = avifImageSetViewRect(rescaledGainMap, gainMap->image, &rect); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| res = avifImageScale(rescaledGainMap, width, height, diag); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| } |
| const avifImage * const gainMapImage = (rescaledGainMap != NULL) ? rescaledGainMap : gainMap->image; |
| |
| avifRGBImageSetDefaults(&rgbGainMap, gainMapImage); |
| res = avifRGBImageAllocatePixels(&rgbGainMap); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| res = avifImageYUVToRGB(gainMapImage, &rgbGainMap); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| |
| avifRGBColorSpaceInfo gainMapRGBInfo; |
| if (!avifGetRGBColorSpaceInfo(&rgbGainMap, &gainMapRGBInfo)) { |
| avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
| res = AVIF_RESULT_NOT_IMPLEMENTED; |
| goto cleanup; |
| } |
| |
| float rgbMaxLinear = 0; // Max tone mapped pixel value across R, G and B channels. |
| float rgbSumLinear = 0; // Sum of max(r, g, b) for mapped pixels. |
| 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]; |
| avifGetRGBAPixel(baseImage, i, j, &baseRGBInfo, basePixelRGBA); |
| float gainMapRGBA[4]; |
| avifGetRGBAPixel(&rgbGainMap, i, j, &gainMapRGBInfo, gainMapRGBA); |
| |
| // Apply gain map. |
| float toneMappedPixelRGBA[4]; |
| float pixelRgbMaxLinear = 0.0f; // = max(r, g, b) for this pixel |
| |
| for (int c = 0; c < 3; ++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. |
| const float gainMapLog2 = |
| lerp((float)metadata.gainMapMin[c], (float)metadata.gainMapMax[c], powf(gainMapValue, gammaInv[c])); |
| const float toneMappedLinear = (baseLinear + (float)metadata.baseOffset[c]) * exp2f(gainMapLog2 * weight) - |
| (float)metadata.alternateOffset[c]; |
| |
| if (toneMappedLinear > rgbMaxLinear) { |
| rgbMaxLinear = toneMappedLinear; |
| } |
| if (toneMappedLinear > pixelRgbMaxLinear) { |
| pixelRgbMaxLinear = toneMappedLinear; |
| } |
| |
| 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); |
| } |
| } |
| if (clli != NULL) { |
| // For exact CLLI value definitions, see ISO/IEC 23008-2 section D.3.35 |
| // at https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html |
| // See also discussion in https://github.com/AOMediaCodec/libavif/issues/1727 |
| |
| // Convert extended SDR (where 1.0 is SDR white) to nits. |
| clli->maxCLL = (uint16_t)AVIF_CLAMP(avifRoundf(rgbMaxLinear * SDR_WHITE_NITS), 0.0f, (float)UINT16_MAX); |
| const float rgbAverageLinear = rgbSumLinear / (width * height); |
| clli->maxPALL = (uint16_t)AVIF_CLAMP(avifRoundf(rgbAverageLinear * SDR_WHITE_NITS), 0.0f, (float)UINT16_MAX); |
| } |
| |
| cleanup: |
| avifRGBImageFreePixels(&rgbGainMap); |
| if (rescaledGainMap != NULL) { |
| avifImageDestroy(rescaledGainMap); |
| } |
| |
| return res; |
| } |
| |
| avifResult avifImageApplyGainMap(const avifImage * baseImage, |
| const avifGainMap * gainMap, |
| float hdrHeadroom, |
| avifColorPrimaries outputColorPrimaries, |
| avifTransferCharacteristics outputTransferCharacteristics, |
| avifRGBImage * toneMappedImage, |
| avifContentLightLevelInformationBox * clli, |
| avifDiagnostics * diag) |
| { |
| avifDiagnosticsClearError(diag); |
| |
| if (baseImage->icc.size > 0 || gainMap->altICC.size > 0) { |
| avifDiagnosticsPrintf(diag, "Tone mapping for images with ICC profiles is not supported"); |
| return AVIF_RESULT_NOT_IMPLEMENTED; |
| } |
| |
| avifRGBImage baseImageRgb; |
| avifRGBImageSetDefaults(&baseImageRgb, baseImage); |
| AVIF_CHECKRES(avifRGBImageAllocatePixels(&baseImageRgb)); |
| avifResult res = avifImageYUVToRGB(baseImage, &baseImageRgb); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| |
| res = avifRGBImageApplyGainMap(&baseImageRgb, |
| baseImage->colorPrimaries, |
| baseImage->transferCharacteristics, |
| gainMap, |
| hdrHeadroom, |
| outputColorPrimaries, |
| outputTransferCharacteristics, |
| toneMappedImage, |
| clli, |
| diag); |
| |
| cleanup: |
| avifRGBImageFreePixels(&baseImageRgb); |
| |
| return res; |
| } |
| |
| // --------------------------------------------------------------------------- |
| // Create a gain map. |
| |
| // Returns the index of the histogram bucket for a given value, for a histogram with 'numBuckets' buckets, |
| // and values ranging in [bucketMin, bucketMax]Â (values outside of the range are added to the first/last buckets). |
| static int avifValueToBucketIdx(float v, float bucketMin, float bucketMax, int numBuckets) |
| { |
| v = AVIF_CLAMP(v, bucketMin, bucketMax); |
| return AVIF_MIN((int)avifRoundf((v - bucketMin) / (bucketMax - bucketMin) * numBuckets), numBuckets - 1); |
| } |
| // Returns the lower end of the value range belonging to the given histogram bucket. |
| static float avifBucketIdxToValue(int idx, float bucketMin, float bucketMax, int numBuckets) |
| { |
| return idx * (bucketMax - bucketMin) / numBuckets + bucketMin; |
| } |
| |
| avifResult avifFindMinMaxWithoutOutliers(const float * gainMapF, int numPixels, float * rangeMin, float * rangeMax) |
| { |
| const float bucketSize = 0.01f; // Size of one bucket. Empirical value. |
| const float maxOutliersRatio = 0.001f; // 0.1% |
| const int maxOutliersOnEachSide = (int)avifRoundf(numPixels * maxOutliersRatio / 2.0f); |
| |
| float min = gainMapF[0]; |
| float max = gainMapF[0]; |
| for (int i = 0; i < numPixels; ++i) { |
| min = AVIF_MIN(min, gainMapF[i]); |
| max = AVIF_MAX(max, gainMapF[i]); |
| } |
| |
| *rangeMin = min; |
| *rangeMax = max; |
| if ((max - min) <= (bucketSize * 2) || maxOutliersOnEachSide == 0) { |
| return AVIF_RESULT_OK; |
| } |
| |
| const int maxNumBuckets = 10000; |
| const int numBuckets = AVIF_MIN((int)ceilf((max - min) / bucketSize), maxNumBuckets); |
| int * histogram = avifAlloc(sizeof(int) * numBuckets); |
| if (histogram == NULL) { |
| return AVIF_RESULT_OUT_OF_MEMORY; |
| } |
| memset(histogram, 0, sizeof(int) * numBuckets); |
| for (int i = 0; i < numPixels; ++i) { |
| ++(histogram[avifValueToBucketIdx(gainMapF[i], min, max, numBuckets)]); |
| } |
| |
| int leftOutliers = 0; |
| for (int i = 0; i < numBuckets; ++i) { |
| leftOutliers += histogram[i]; |
| if (leftOutliers > maxOutliersOnEachSide) { |
| break; |
| } |
| if (histogram[i] == 0) { |
| // +1 to get the higher end of the bucket. |
| *rangeMin = avifBucketIdxToValue(i + 1, min, max, numBuckets); |
| } |
| } |
| |
| int rightOutliers = 0; |
| for (int i = numBuckets - 1; i >= 0; --i) { |
| rightOutliers += histogram[i]; |
| if (rightOutliers > maxOutliersOnEachSide) { |
| break; |
| } |
| if (histogram[i] == 0) { |
| *rangeMax = avifBucketIdxToValue(i, min, max, numBuckets); |
| } |
| } |
| |
| avifFree(histogram); |
| 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, |
| avifGainMap * gainMap, |
| avifDiagnostics * diag) |
| { |
| avifDiagnosticsClearError(diag); |
| |
| AVIF_CHECKERR(baseRgbImage != NULL && altRgbImage != NULL && gainMap != NULL && gainMap->image != NULL, AVIF_RESULT_INVALID_ARGUMENT); |
| if (baseRgbImage->width != altRgbImage->width || baseRgbImage->height != altRgbImage->height) { |
| avifDiagnosticsPrintf(diag, "Both images should have the same dimensions"); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| 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, "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 int width = baseRgbImage->width; |
| const int height = baseRgbImage->height; |
| |
| avifRGBColorSpaceInfo baseRGBInfo; |
| avifRGBColorSpaceInfo altRGBInfo; |
| if (!avifGetRGBColorSpaceInfo(baseRgbImage, &baseRGBInfo) || !avifGetRGBColorSpaceInfo(altRgbImage, &altRGBInfo)) { |
| avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
| return AVIF_RESULT_NOT_IMPLEMENTED; |
| } |
| |
| float * gainMapF[3] = { 0 }; // Temporary buffers for the gain map as floating point values, one per RGB channel. |
| avifRGBImage gainMapRGB; |
| memset(&gainMapRGB, 0, sizeof(gainMapRGB)); |
| avifImage * gainMapImage = gainMap->image; |
| |
| avifResult res = AVIF_RESULT_OK; |
| // --- After this point, the function should exit with 'goto cleanup' to free allocated resources. |
| |
| const avifBool singleChannel = (gainMap->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400); |
| const int numGainMapChannels = singleChannel ? 1 : 3; |
| for (int c = 0; c < numGainMapChannels; ++c) { |
| gainMapF[c] = avifAlloc(width * height * sizeof(float)); |
| if (gainMapF[c] == NULL) { |
| res = AVIF_RESULT_OUT_OF_MEMORY; |
| goto cleanup; |
| } |
| } |
| |
| avifGainMapMetadataDouble gainMapMetadata; |
| avifGainMapMetadataDoubleSetDefaults(&gainMapMetadata); |
| gainMapMetadata.useBaseColorSpace = (gainMapMathPrimaries == baseColorPrimaries); |
| |
| float (*baseGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics); |
| float (*altGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(altTransferCharacteristics); |
| float yCoeffs[3]; |
| avifColorPrimariesComputeYCoeffs(gainMapMathPrimaries, yCoeffs); |
| |
| double rgbConversionCoeffs[3][3]; |
| if (colorSpacesDiffer) { |
| if (gainMapMetadata.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(gainMapMetadata.useBaseColorSpace ? altRgbImage : baseRgbImage, |
| i, |
| j, |
| gainMapMetadata.useBaseColorSpace ? &altRGBInfo : &baseRGBInfo, |
| rgba); |
| |
| // Convert to linear. |
| for (int c = 0; c < 3; ++c) { |
| if (gainMapMetadata.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 (gainMapMetadata.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; |
| float altMax = 1.0f; |
| for (int j = 0; j < height; ++j) { |
| for (int i = 0; i < width; ++i) { |
| float baseRGBA[4]; |
| avifGetRGBAPixel(baseRgbImage, i, j, &baseRGBInfo, baseRGBA); |
| float altRGBA[4]; |
| avifGetRGBAPixel(altRgbImage, i, j, &altRGBInfo, altRGBA); |
| |
| // Convert to linear. |
| for (int c = 0; c < 3; ++c) { |
| baseRGBA[c] = baseGammaToLinear(baseRGBA[c]); |
| altRGBA[c] = altGammaToLinear(altRGBA[c]); |
| } |
| |
| if (colorSpacesDiffer) { |
| if (gainMapMetadata.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 = 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; |
| } |
| if (alt > altMax) { |
| altMax = alt; |
| } |
| 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; |
| } |
| } |
| } |
| |
| // Populate the gain map metadata's headrooms. |
| gainMapMetadata.baseHdrHeadroom = log2f(AVIF_MAX(baseMax, kEpsilon)); |
| gainMapMetadata.alternateHdrHeadroom = log2f(AVIF_MAX(altMax, kEpsilon)); |
| |
| // Multiply the gainmap by sign(alternateHdrHeadroom - baseHdrHeadroom), to |
| // ensure that it stores the log-ratio of the HDR representation to the SDR |
| // representation. |
| if (gainMapMetadata.alternateHdrHeadroom < gainMapMetadata.baseHdrHeadroom) { |
| for (int c = 0; c < numGainMapChannels; ++c) { |
| for (int j = 0; j < height; ++j) { |
| for (int i = 0; i < width; ++i) { |
| gainMapF[c][j * width + i] *= -1.f; |
| } |
| } |
| } |
| } |
| |
| // Find approximate min/max for each channel, discarding outliers. |
| float gainMapMinLog2[3] = { 0.0f, 0.0f, 0.0f }; |
| float gainMapMaxLog2[3] = { 0.0f, 0.0f, 0.0f }; |
| for (int c = 0; c < numGainMapChannels; ++c) { |
| res = avifFindMinMaxWithoutOutliers(gainMapF[c], width * height, &gainMapMinLog2[c], &gainMapMaxLog2[c]); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| } |
| |
| // Populate the gain map metadata's min and max values. |
| for (int c = 0; c < 3; ++c) { |
| gainMapMetadata.gainMapMin[c] = gainMapMinLog2[singleChannel ? 0 : c]; |
| gainMapMetadata.gainMapMax[c] = gainMapMaxLog2[singleChannel ? 0 : c]; |
| } |
| |
| // All of gainMapMetadata has been populated now (except for gamma which is left to the default |
| // value), so convert to the fraction form in which it will be stored. |
| if (!avifGainMapMetadataDoubleToFractions(&gainMap->metadata, &gainMapMetadata)) { |
| res = AVIF_RESULT_UNKNOWN_ERROR; |
| goto cleanup; |
| } |
| |
| // Scale the gain map values to map [min, max] range to [0, 1]. |
| for (int c = 0; c < numGainMapChannels; ++c) { |
| const float range = gainMapMaxLog2[c] - gainMapMinLog2[c]; |
| if (range <= 0.0f) { |
| continue; |
| } |
| |
| for (int j = 0; j < height; ++j) { |
| for (int i = 0; i < width; ++i) { |
| // Remap [min; max] range to [0; 1] |
| const float v = AVIF_CLAMP(gainMapF[c][j * width + i], gainMapMinLog2[c], gainMapMaxLog2[c]); |
| gainMapF[c][j * width + i] = powf((v - gainMapMinLog2[c]) / range, (float)gainMapMetadata.gainMapGamma[c]); |
| } |
| } |
| } |
| |
| // Convert the gain map to YUV. |
| const uint32_t requestedWidth = gainMapImage->width; |
| const uint32_t requestedHeight = gainMapImage->height; |
| gainMapImage->width = width; |
| gainMapImage->height = height; |
| |
| avifImageFreePlanes(gainMapImage, AVIF_PLANES_ALL); // Free planes in case they were already allocated. |
| res = avifImageAllocatePlanes(gainMapImage, AVIF_PLANES_YUV); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| |
| avifRGBImageSetDefaults(&gainMapRGB, gainMapImage); |
| res = avifRGBImageAllocatePixels(&gainMapRGB); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| |
| avifRGBColorSpaceInfo gainMapRGBInfo; |
| if (!avifGetRGBColorSpaceInfo(&gainMapRGB, &gainMapRGBInfo)) { |
| avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
| return AVIF_RESULT_NOT_IMPLEMENTED; |
| } |
| for (int j = 0; j < height; ++j) { |
| for (int i = 0; i < width; ++i) { |
| const int offset = j * width + i; |
| const float r = gainMapF[0][offset]; |
| const float g = singleChannel ? r : gainMapF[1][offset]; |
| const float b = singleChannel ? r : gainMapF[2][offset]; |
| const float rgbaPixel[4] = { r, g, b, 1.0f }; |
| avifSetRGBAPixel(&gainMapRGB, i, j, &gainMapRGBInfo, rgbaPixel); |
| } |
| } |
| |
| res = avifImageRGBToYUV(gainMapImage, &gainMapRGB); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| |
| // Scale down the gain map if requested. |
| // Another way would be to scale the source images, but it seems to perform worse. |
| if (requestedWidth != gainMapImage->width || requestedHeight != gainMapImage->height) { |
| AVIF_CHECKRES(avifImageScale(gainMap->image, requestedWidth, requestedHeight, diag)); |
| } |
| |
| cleanup: |
| for (int c = 0; c < 3; ++c) { |
| avifFree(gainMapF[c]); |
| } |
| avifRGBImageFreePixels(&gainMapRGB); |
| if (res != AVIF_RESULT_OK) { |
| avifImageFreePlanes(gainMapImage, AVIF_PLANES_ALL); |
| } |
| |
| return res; |
| } |
| |
| avifResult avifImageComputeGainMap(const avifImage * baseImage, const avifImage * altImage, avifGainMap * gainMap, avifDiagnostics * diag) |
| { |
| avifDiagnosticsClearError(diag); |
| |
| if (baseImage == NULL || altImage == NULL || gainMap == NULL) { |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| if (baseImage->icc.size > 0 || altImage->icc.size > 0) { |
| avifDiagnosticsPrintf(diag, "Computing gain maps for images with ICC profiles is not supported"); |
| 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", |
| baseImage->width, |
| baseImage->height, |
| altImage->width, |
| altImage->height); |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| |
| avifResult res = AVIF_RESULT_OK; |
| |
| avifRGBImage baseImageRgb; |
| avifRGBImageSetDefaults(&baseImageRgb, baseImage); |
| avifRGBImage altImageRgb; |
| avifRGBImageSetDefaults(&altImageRgb, altImage); |
| |
| AVIF_CHECKRES(avifRGBImageAllocatePixels(&baseImageRgb)); |
| // --- After this point, the function should exit with 'goto cleanup' to free allocated resources. |
| |
| res = avifImageYUVToRGB(baseImage, &baseImageRgb); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| res = avifRGBImageAllocatePixels(&altImageRgb); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| res = avifImageYUVToRGB(altImage, &altImageRgb); |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| |
| res = avifRGBImageComputeGainMap(&baseImageRgb, |
| baseImage->colorPrimaries, |
| baseImage->transferCharacteristics, |
| &altImageRgb, |
| altImage->colorPrimaries, |
| altImage->transferCharacteristics, |
| gainMap, |
| diag); |
| |
| if (res != AVIF_RESULT_OK) { |
| goto cleanup; |
| } |
| |
| AVIF_CHECKRES(avifRWDataSet(&gainMap->altICC, altImage->icc.data, altImage->icc.size)); |
| gainMap->altColorPrimaries = altImage->colorPrimaries; |
| gainMap->altTransferCharacteristics = altImage->transferCharacteristics; |
| gainMap->altMatrixCoefficients = altImage->matrixCoefficients; |
| gainMap->altDepth = altImage->depth; |
| gainMap->altPlaneCount = (altImage->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3; |
| gainMap->altCLLI = altImage->clli; |
| |
| cleanup: |
| avifRGBImageFreePixels(&baseImageRgb); |
| avifRGBImageFreePixels(&altImageRgb); |
| return res; |
| } |
| |
| #endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP |