blob: 6fdf78adecad9883e8a877510f885e6405b69da8 [file] [log] [blame] [edit]
// 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(avifDoubleToUnsignedFraction(src->gainMapMin[i], &dst->gainMapMinN[i], &dst->gainMapMinD[i]));
AVIF_CHECK(avifDoubleToUnsignedFraction(src->gainMapMax[i], &dst->gainMapMaxN[i], &dst->gainMapMaxD[i]));
AVIF_CHECK(avifDoubleToUnsignedFraction(src->gainMapGamma[i], &dst->gainMapGammaN[i], &dst->gainMapGammaD[i]));
AVIF_CHECK(avifDoubleToUnsignedFraction(src->offsetSdr[i], &dst->offsetSdrN[i], &dst->offsetSdrD[i]));
AVIF_CHECK(avifDoubleToUnsignedFraction(src->offsetHdr[i], &dst->offsetHdrN[i], &dst->offsetHdrD[i]));
}
AVIF_CHECK(avifDoubleToUnsignedFraction(src->hdrCapacityMin, &dst->hdrCapacityMinN, &dst->hdrCapacityMinD));
AVIF_CHECK(avifDoubleToUnsignedFraction(src->hdrCapacityMax, &dst->hdrCapacityMaxN, &dst->hdrCapacityMaxD));
dst->baseRenditionIsHDR = src->baseRenditionIsHDR;
return AVIF_TRUE;
}
avifBool avifGainMapMetadataFractionsToDouble(avifGainMapMetadataDouble * dst, const avifGainMapMetadata * src)
{
AVIF_CHECK(dst != NULL && src != NULL);
AVIF_CHECK(src->hdrCapacityMinD != 0);
AVIF_CHECK(src->hdrCapacityMaxD != 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->offsetSdrD[i] != 0);
AVIF_CHECK(src->offsetHdrD[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->offsetSdr[i] = (double)src->offsetSdrN[i] / src->offsetSdrD[i];
dst->offsetHdr[i] = (double)src->offsetHdrN[i] / src->offsetHdrD[i];
}
dst->hdrCapacityMin = (double)src->hdrCapacityMinN / src->hdrCapacityMinD;
dst->hdrCapacityMax = (double)src->hdrCapacityMaxN / src->hdrCapacityMaxD;
dst->baseRenditionIsHDR = src->baseRenditionIsHDR;
return AVIF_TRUE;
}
// ---------------------------------------------------------------------------
// Returns a weight in [-1.0, 1.0] that represents how much the gain map should be applied.
static float avifGetGainMapWeight(float hdrCapacity, const avifGainMapMetadataDouble * metadata)
{
const float hdrCapacityMin = (float)metadata->hdrCapacityMin;
const float hdrCapacityMax = (float)metadata->hdrCapacityMax;
float w = 0.0f;
if (hdrCapacity > hdrCapacityMin) {
if (hdrCapacity < hdrCapacityMax) {
w = (logf(hdrCapacity) - logf(hdrCapacityMin)) / (logf(hdrCapacityMax) - logf(hdrCapacityMin));
} else {
w = 1.0f;
}
}
if (metadata->baseRenditionIsHDR) {
w -= 1.0f;
}
return 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,
avifTransferCharacteristics transferCharacteristics,
const avifGainMap * gainMap,
float hdrCapacity,
avifTransferCharacteristics outputTransferCharacteristics,
avifRGBImage * toneMappedImage,
avifContentLightLevelInformationBox * clli,
avifDiagnostics * diag)
{
avifDiagnosticsClearError(diag);
if (hdrCapacity < 1.0) {
avifDiagnosticsPrintf(diag, "hdrCapacity should be >= 1, got %f", hdrCapacity);
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;
}
if (metadata.hdrCapacityMin > metadata.hdrCapacityMax) {
avifDiagnosticsPrintf(diag,
"Invalid gain map metadata, hdrCapacityMin should be <= hdrCapacityMax, got min %f and max %f",
metadata.hdrCapacityMin,
metadata.hdrCapacityMax);
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;
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(hdrCapacity, &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 &&
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(transferCharacteristics);
const avifTransferFunction linearToGamma = avifTransferCharacteristicsGetLinearToGammaFunction(outputTransferCharacteristics);
// Early exit if the gain map does not need to be applied.
if (weight == 0.0f) {
// 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) {
for (int c = 0; c < 3; ++c) {
basePixelRGBA[c] = AVIF_CLAMP(linearToGamma(gammaToLinear(basePixelRGBA[c])), 0.0f, 1.0f);
}
}
avifSetRGBAPixel(toneMappedImage, i, j, &toneMappedPixelRGBInfo, basePixelRGBA);
}
}
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 gainMapMinLog[3];
float gainMapMaxLog[3];
for (int i = 0; i < 3; ++i) {
gainMapMinLog[i] = (float)log(metadata.gainMapMin[i]);
gainMapMaxLog[i] = (float)log(metadata.gainMapMax[i]);
}
const double * offsetBase = gainMap->metadata.baseRenditionIsHDR ? metadata.offsetHdr : metadata.offsetSdr;
const double * offsetOther = gainMap->metadata.baseRenditionIsHDR ? metadata.offsetSdr : metadata.offsetHdr;
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.
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) {
const float baseLinear = gammaToLinear(basePixelRGBA[c]);
const float gainMapValue = gainMapRGBA[c];
// Undo gamma & affine transform; the result is in log space.
const float gainMapLog = lerp(gainMapMinLog[c], gainMapMaxLog[c], powf(gainMapValue, (float)metadata.gainMapGamma[c]));
const float toneMappedLinear = (baseLinear + (float)offsetBase[c]) * expf(gainMapLog * weight) - (float)offsetOther[c];
if (toneMappedLinear > rgbMaxLinear) {
rgbMaxLinear = toneMappedLinear;
}
if (toneMappedLinear > pixelRgbMaxLinear) {
pixelRgbMaxLinear = toneMappedLinear;
}
const float toneMappedGamma = linearToGamma(toneMappedLinear);
toneMappedPixelRGBA[c] = AVIF_CLAMP(toneMappedGamma, 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
// 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 hdrCapacity,
avifTransferCharacteristics outputTransferCharacteristics,
avifRGBImage * toneMappedImage,
avifContentLightLevelInformationBox * clli,
avifDiagnostics * diag)
{
avifDiagnosticsClearError(diag);
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->transferCharacteristics,
gainMap,
hdrCapacity,
outputTransferCharacteristics,
toneMappedImage,
clli,
diag);
cleanup:
avifRGBImageFreePixels(&baseImageRgb);
return res;
}
#endif // AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP