/src/libavif/src/gainmap.c
Line | Count | Source |
1 | | // Copyright 2023 Google LLC |
2 | | // SPDX-License-Identifier: BSD-2-Clause |
3 | | |
4 | | #include "avif/internal.h" |
5 | | #include <assert.h> |
6 | | #include <float.h> |
7 | | #include <math.h> |
8 | | #include <string.h> |
9 | | |
10 | | static void avifGainMapSetEncodingDefaults(avifGainMap * gainMap) |
11 | 0 | { |
12 | 0 | for (int i = 0; i < 3; ++i) { |
13 | 0 | gainMap->gainMapMin[i] = (avifSignedFraction) { 1, 1 }; |
14 | 0 | gainMap->gainMapMax[i] = (avifSignedFraction) { 1, 1 }; |
15 | 0 | gainMap->baseOffset[i] = (avifSignedFraction) { 1, 64 }; |
16 | 0 | gainMap->alternateOffset[i] = (avifSignedFraction) { 1, 64 }; |
17 | 0 | gainMap->gainMapGamma[i] = (avifUnsignedFraction) { 1, 1 }; |
18 | 0 | } |
19 | 0 | gainMap->baseHdrHeadroom = (avifUnsignedFraction) { 0, 1 }; |
20 | 0 | gainMap->alternateHdrHeadroom = (avifUnsignedFraction) { 1, 1 }; |
21 | 0 | gainMap->useBaseColorSpace = AVIF_TRUE; |
22 | 0 | } |
23 | | |
24 | | static float avifSignedFractionToFloat(avifSignedFraction f) |
25 | 0 | { |
26 | 0 | if (f.d == 0) { |
27 | 0 | return 0.0f; |
28 | 0 | } |
29 | 0 | return (float)f.n / f.d; |
30 | 0 | } |
31 | | |
32 | | static float avifUnsignedFractionToFloat(avifUnsignedFraction f) |
33 | 0 | { |
34 | 0 | if (f.d == 0) { |
35 | 0 | return 0.0f; |
36 | 0 | } |
37 | 0 | return (float)f.n / f.d; |
38 | 0 | } |
39 | | |
40 | | // --------------------------------------------------------------------------- |
41 | | // Apply a gain map. |
42 | | |
43 | | // Returns a weight in [-1.0, 1.0] that represents how much the gain map should be applied. |
44 | | static float avifGetGainMapWeight(float hdrHeadroom, const avifGainMap * gainMap) |
45 | 0 | { |
46 | 0 | const float baseHdrHeadroom = avifUnsignedFractionToFloat(gainMap->baseHdrHeadroom); |
47 | 0 | const float alternateHdrHeadroom = avifUnsignedFractionToFloat(gainMap->alternateHdrHeadroom); |
48 | 0 | if (baseHdrHeadroom == alternateHdrHeadroom) { |
49 | | // Do not apply the gain map if the HDR headroom is the same. |
50 | | // This case is not handled in the specification and does not make practical sense. |
51 | 0 | return 0.0f; |
52 | 0 | } |
53 | 0 | const float w = AVIF_CLAMP((hdrHeadroom - baseHdrHeadroom) / (alternateHdrHeadroom - baseHdrHeadroom), 0.0f, 1.0f); |
54 | 0 | return (alternateHdrHeadroom < baseHdrHeadroom) ? -w : w; |
55 | 0 | } |
56 | | |
57 | | // Linear interpolation between 'a' and 'b' (returns 'a' if w == 0.0f, returns 'b' if w == 1.0f). |
58 | | static inline float lerp(float a, float b, float w) |
59 | 0 | { |
60 | 0 | return (1.0f - w) * a + w * b; |
61 | 0 | } |
62 | | |
63 | | #define SDR_WHITE_NITS 203.0f |
64 | | |
65 | | avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, |
66 | | avifColorPrimaries baseColorPrimaries, |
67 | | avifTransferCharacteristics baseTransferCharacteristics, |
68 | | const avifGainMap * gainMap, |
69 | | float hdrHeadroom, |
70 | | avifColorPrimaries outputColorPrimaries, |
71 | | avifTransferCharacteristics outputTransferCharacteristics, |
72 | | avifRGBImage * toneMappedImage, |
73 | | avifContentLightLevelInformationBox * clli, |
74 | | avifDiagnostics * diag) |
75 | 0 | { |
76 | 0 | avifDiagnosticsClearError(diag); |
77 | |
|
78 | 0 | if (hdrHeadroom < 0.0f) { |
79 | 0 | avifDiagnosticsPrintf(diag, "hdrHeadroom should be >= 0, got %f", hdrHeadroom); |
80 | 0 | return AVIF_RESULT_INVALID_ARGUMENT; |
81 | 0 | } |
82 | 0 | if (baseImage == NULL || gainMap == NULL || toneMappedImage == NULL) { |
83 | 0 | avifDiagnosticsPrintf(diag, "NULL input image"); |
84 | 0 | return AVIF_RESULT_INVALID_ARGUMENT; |
85 | 0 | } |
86 | 0 | AVIF_CHECKRES(avifGainMapValidateMetadata(gainMap, diag)); |
87 | | |
88 | 0 | const uint32_t width = baseImage->width; |
89 | 0 | const uint32_t height = baseImage->height; |
90 | |
|
91 | 0 | const avifBool useBaseColorSpace = gainMap->useBaseColorSpace; |
92 | 0 | const avifColorPrimaries gainMapMathPrimaries = |
93 | 0 | (useBaseColorSpace || (gainMap->altColorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED)) ? baseColorPrimaries |
94 | 0 | : gainMap->altColorPrimaries; |
95 | 0 | const avifBool needsInputColorConversion = (baseColorPrimaries != gainMapMathPrimaries); |
96 | 0 | const avifBool needsOutputColorConversion = (gainMapMathPrimaries != outputColorPrimaries); |
97 | |
|
98 | 0 | avifImage * rescaledGainMap = NULL; |
99 | 0 | avifRGBImage rgbGainMap; |
100 | | // Basic zero-initialization for now, avifRGBImageSetDefaults() is called later on. |
101 | 0 | memset(&rgbGainMap, 0, sizeof(rgbGainMap)); |
102 | |
|
103 | 0 | avifResult res = AVIF_RESULT_OK; |
104 | 0 | toneMappedImage->width = width; |
105 | 0 | toneMappedImage->height = height; |
106 | 0 | AVIF_CHECKRES(avifRGBImageAllocatePixels(toneMappedImage)); |
107 | | |
108 | | // --- After this point, the function should exit with 'goto cleanup' to free allocated pixels. |
109 | | |
110 | 0 | const float weight = avifGetGainMapWeight(hdrHeadroom, gainMap); |
111 | | |
112 | | // Early exit if the gain map does not need to be applied and the pixel format is the same. |
113 | 0 | if (weight == 0.0f && outputTransferCharacteristics == baseTransferCharacteristics && |
114 | 0 | outputColorPrimaries == baseColorPrimaries && baseImage->format == toneMappedImage->format && |
115 | 0 | baseImage->depth == toneMappedImage->depth && baseImage->isFloat == toneMappedImage->isFloat) { |
116 | 0 | assert(baseImage->rowBytes == toneMappedImage->rowBytes); |
117 | 0 | assert(baseImage->height == toneMappedImage->height); |
118 | | // Copy the base image. |
119 | 0 | memcpy(toneMappedImage->pixels, baseImage->pixels, baseImage->rowBytes * baseImage->height); |
120 | 0 | goto cleanup; |
121 | 0 | } |
122 | | |
123 | 0 | avifRGBColorSpaceInfo baseRGBInfo; |
124 | 0 | avifRGBColorSpaceInfo toneMappedPixelRGBInfo; |
125 | 0 | if (!avifGetRGBColorSpaceInfo(baseImage, &baseRGBInfo) || !avifGetRGBColorSpaceInfo(toneMappedImage, &toneMappedPixelRGBInfo)) { |
126 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
127 | 0 | res = AVIF_RESULT_NOT_IMPLEMENTED; |
128 | 0 | goto cleanup; |
129 | 0 | } |
130 | | |
131 | 0 | const avifTransferFunction gammaToLinear = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics); |
132 | 0 | const avifTransferFunction linearToGamma = avifTransferCharacteristicsGetLinearToGammaFunction(outputTransferCharacteristics); |
133 | | |
134 | | // Early exit if the gain map does not need to be applied. |
135 | 0 | if (weight == 0.0f) { |
136 | 0 | const avifBool primariesDiffer = (baseColorPrimaries != outputColorPrimaries); |
137 | 0 | double conversionCoeffs[3][3]; |
138 | 0 | if (primariesDiffer && !avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, outputColorPrimaries, conversionCoeffs)) { |
139 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); |
140 | 0 | res = AVIF_RESULT_NOT_IMPLEMENTED; |
141 | 0 | goto cleanup; |
142 | 0 | } |
143 | | // Just convert from one rgb format to another. |
144 | 0 | for (uint32_t j = 0; j < height; ++j) { |
145 | 0 | for (uint32_t i = 0; i < width; ++i) { |
146 | 0 | float basePixelRGBA[4]; |
147 | 0 | avifGetRGBAPixel(baseImage, i, j, &baseRGBInfo, basePixelRGBA); |
148 | 0 | if (outputTransferCharacteristics != baseTransferCharacteristics || primariesDiffer) { |
149 | 0 | for (int c = 0; c < 3; ++c) { |
150 | 0 | basePixelRGBA[c] = gammaToLinear(basePixelRGBA[c]); |
151 | 0 | } |
152 | 0 | if (primariesDiffer) { |
153 | 0 | avifLinearRGBConvertColorSpace(basePixelRGBA, conversionCoeffs); |
154 | 0 | } |
155 | 0 | for (int c = 0; c < 3; ++c) { |
156 | 0 | basePixelRGBA[c] = AVIF_CLAMP(linearToGamma(basePixelRGBA[c]), 0.0f, 1.0f); |
157 | 0 | } |
158 | 0 | } |
159 | 0 | avifSetRGBAPixel(toneMappedImage, i, j, &toneMappedPixelRGBInfo, basePixelRGBA); |
160 | 0 | } |
161 | 0 | } |
162 | 0 | goto cleanup; |
163 | 0 | } |
164 | | |
165 | 0 | double inputConversionCoeffs[3][3]; |
166 | 0 | double outputConversionCoeffs[3][3]; |
167 | 0 | if (needsInputColorConversion && |
168 | 0 | !avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, gainMapMathPrimaries, inputConversionCoeffs)) { |
169 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); |
170 | 0 | res = AVIF_RESULT_NOT_IMPLEMENTED; |
171 | 0 | goto cleanup; |
172 | 0 | } |
173 | 0 | if (needsOutputColorConversion && |
174 | 0 | !avifColorPrimariesComputeRGBToRGBMatrix(gainMapMathPrimaries, outputColorPrimaries, outputConversionCoeffs)) { |
175 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); |
176 | 0 | res = AVIF_RESULT_NOT_IMPLEMENTED; |
177 | 0 | goto cleanup; |
178 | 0 | } |
179 | | |
180 | 0 | if (gainMap->image->width != width || gainMap->image->height != height) { |
181 | 0 | rescaledGainMap = avifImageCreateEmpty(); |
182 | 0 | const avifCropRect rect = { 0, 0, gainMap->image->width, gainMap->image->height }; |
183 | 0 | res = avifImageSetViewRect(rescaledGainMap, gainMap->image, &rect); |
184 | 0 | if (res != AVIF_RESULT_OK) { |
185 | 0 | goto cleanup; |
186 | 0 | } |
187 | 0 | res = avifImageScale(rescaledGainMap, width, height, diag); |
188 | 0 | if (res != AVIF_RESULT_OK) { |
189 | 0 | goto cleanup; |
190 | 0 | } |
191 | 0 | } |
192 | 0 | const avifImage * const gainMapImage = (rescaledGainMap != NULL) ? rescaledGainMap : gainMap->image; |
193 | |
|
194 | 0 | avifRGBImageSetDefaults(&rgbGainMap, gainMapImage); |
195 | 0 | res = avifRGBImageAllocatePixels(&rgbGainMap); |
196 | 0 | if (res != AVIF_RESULT_OK) { |
197 | 0 | goto cleanup; |
198 | 0 | } |
199 | 0 | res = avifImageYUVToRGB(gainMapImage, &rgbGainMap); |
200 | 0 | if (res != AVIF_RESULT_OK) { |
201 | 0 | goto cleanup; |
202 | 0 | } |
203 | | |
204 | 0 | avifRGBColorSpaceInfo gainMapRGBInfo; |
205 | 0 | if (!avifGetRGBColorSpaceInfo(&rgbGainMap, &gainMapRGBInfo)) { |
206 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
207 | 0 | res = AVIF_RESULT_NOT_IMPLEMENTED; |
208 | 0 | goto cleanup; |
209 | 0 | } |
210 | | |
211 | 0 | float rgbMaxLinear = 0; // Max tone mapped pixel value across R, G and B channels. |
212 | 0 | float rgbSumLinear = 0; // Sum of max(r, g, b) for mapped pixels. |
213 | | // The gain map metadata contains the encoding gamma, and 1/gamma should be used for decoding. |
214 | 0 | const float gammaInv[3] = { 1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[0]), |
215 | 0 | 1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[1]), |
216 | 0 | 1.0f / avifUnsignedFractionToFloat(gainMap->gainMapGamma[2]) }; |
217 | 0 | const float gainMapMin[3] = { avifSignedFractionToFloat(gainMap->gainMapMin[0]), |
218 | 0 | avifSignedFractionToFloat(gainMap->gainMapMin[1]), |
219 | 0 | avifSignedFractionToFloat(gainMap->gainMapMin[2]) }; |
220 | 0 | const float gainMapMax[3] = { avifSignedFractionToFloat(gainMap->gainMapMax[0]), |
221 | 0 | avifSignedFractionToFloat(gainMap->gainMapMax[1]), |
222 | 0 | avifSignedFractionToFloat(gainMap->gainMapMax[2]) }; |
223 | 0 | const float baseOffset[3] = { avifSignedFractionToFloat(gainMap->baseOffset[0]), |
224 | 0 | avifSignedFractionToFloat(gainMap->baseOffset[1]), |
225 | 0 | avifSignedFractionToFloat(gainMap->baseOffset[2]) }; |
226 | 0 | const float alternateOffset[3] = { avifSignedFractionToFloat(gainMap->alternateOffset[0]), |
227 | 0 | avifSignedFractionToFloat(gainMap->alternateOffset[1]), |
228 | 0 | avifSignedFractionToFloat(gainMap->alternateOffset[2]) }; |
229 | 0 | for (uint32_t j = 0; j < height; ++j) { |
230 | 0 | for (uint32_t i = 0; i < width; ++i) { |
231 | 0 | float basePixelRGBA[4]; |
232 | 0 | avifGetRGBAPixel(baseImage, i, j, &baseRGBInfo, basePixelRGBA); |
233 | 0 | float gainMapRGBA[4]; |
234 | 0 | avifGetRGBAPixel(&rgbGainMap, i, j, &gainMapRGBInfo, gainMapRGBA); |
235 | | |
236 | | // Apply gain map. |
237 | 0 | float toneMappedPixelRGBA[4]; |
238 | 0 | float pixelRgbMaxLinear = 0.0f; // = max(r, g, b) for this pixel |
239 | |
|
240 | 0 | for (int c = 0; c < 3; ++c) { |
241 | 0 | basePixelRGBA[c] = gammaToLinear(basePixelRGBA[c]); |
242 | 0 | } |
243 | |
|
244 | 0 | if (needsInputColorConversion) { |
245 | | // Convert basePixelRGBA to gainMapMathPrimaries. |
246 | 0 | avifLinearRGBConvertColorSpace(basePixelRGBA, inputConversionCoeffs); |
247 | 0 | } |
248 | |
|
249 | 0 | for (int c = 0; c < 3; ++c) { |
250 | 0 | const float baseLinear = basePixelRGBA[c]; |
251 | 0 | const float gainMapValue = gainMapRGBA[c]; |
252 | | |
253 | | // Undo gamma & affine transform; the result is in log2 space. |
254 | 0 | const float gainMapLog2 = lerp(gainMapMin[c], gainMapMax[c], powf(gainMapValue, gammaInv[c])); |
255 | 0 | const float toneMappedLinear = (baseLinear + baseOffset[c]) * exp2f(gainMapLog2 * weight) - alternateOffset[c]; |
256 | |
|
257 | 0 | if (toneMappedLinear > rgbMaxLinear) { |
258 | 0 | rgbMaxLinear = toneMappedLinear; |
259 | 0 | } |
260 | 0 | if (toneMappedLinear > pixelRgbMaxLinear) { |
261 | 0 | pixelRgbMaxLinear = toneMappedLinear; |
262 | 0 | } |
263 | |
|
264 | 0 | toneMappedPixelRGBA[c] = toneMappedLinear; |
265 | 0 | } |
266 | |
|
267 | 0 | if (needsOutputColorConversion) { |
268 | | // Convert toneMappedPixelRGBA to outputColorPrimaries. |
269 | 0 | avifLinearRGBConvertColorSpace(toneMappedPixelRGBA, outputConversionCoeffs); |
270 | 0 | } |
271 | |
|
272 | 0 | for (int c = 0; c < 3; ++c) { |
273 | 0 | toneMappedPixelRGBA[c] = AVIF_CLAMP(linearToGamma(toneMappedPixelRGBA[c]), 0.0f, 1.0f); |
274 | 0 | } |
275 | |
|
276 | 0 | toneMappedPixelRGBA[3] = basePixelRGBA[3]; // Alpha is unaffected by tone mapping. |
277 | 0 | rgbSumLinear += pixelRgbMaxLinear; |
278 | 0 | avifSetRGBAPixel(toneMappedImage, i, j, &toneMappedPixelRGBInfo, toneMappedPixelRGBA); |
279 | 0 | } |
280 | 0 | } |
281 | 0 | if (clli != NULL) { |
282 | | // For exact CLLI value definitions, see ISO/IEC 23008-2 section D.3.35 |
283 | | // at https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html |
284 | | // See also discussion in https://github.com/AOMediaCodec/libavif/issues/1727 |
285 | | |
286 | | // Convert extended SDR (where 1.0 is SDR white) to nits. |
287 | 0 | clli->maxCLL = (uint16_t)AVIF_CLAMP(avifRoundf(rgbMaxLinear * SDR_WHITE_NITS), 0.0f, (float)UINT16_MAX); |
288 | 0 | const float rgbAverageLinear = rgbSumLinear / (width * height); |
289 | 0 | clli->maxPALL = (uint16_t)AVIF_CLAMP(avifRoundf(rgbAverageLinear * SDR_WHITE_NITS), 0.0f, (float)UINT16_MAX); |
290 | 0 | } |
291 | |
|
292 | 0 | cleanup: |
293 | 0 | avifRGBImageFreePixels(&rgbGainMap); |
294 | 0 | if (rescaledGainMap != NULL) { |
295 | 0 | avifImageDestroy(rescaledGainMap); |
296 | 0 | } |
297 | |
|
298 | 0 | return res; |
299 | 0 | } |
300 | | |
301 | | avifResult avifImageApplyGainMap(const avifImage * baseImage, |
302 | | const avifGainMap * gainMap, |
303 | | float hdrHeadroom, |
304 | | avifColorPrimaries outputColorPrimaries, |
305 | | avifTransferCharacteristics outputTransferCharacteristics, |
306 | | avifRGBImage * toneMappedImage, |
307 | | avifContentLightLevelInformationBox * clli, |
308 | | avifDiagnostics * diag) |
309 | 0 | { |
310 | 0 | avifDiagnosticsClearError(diag); |
311 | |
|
312 | 0 | if (baseImage->icc.size > 0 || gainMap->altICC.size > 0) { |
313 | 0 | avifDiagnosticsPrintf(diag, "Tone mapping for images with ICC profiles is not supported"); |
314 | 0 | return AVIF_RESULT_NOT_IMPLEMENTED; |
315 | 0 | } |
316 | | |
317 | 0 | avifRGBImage baseImageRgb; |
318 | 0 | avifRGBImageSetDefaults(&baseImageRgb, baseImage); |
319 | 0 | AVIF_CHECKRES(avifRGBImageAllocatePixels(&baseImageRgb)); |
320 | 0 | avifResult res = avifImageYUVToRGB(baseImage, &baseImageRgb); |
321 | 0 | if (res != AVIF_RESULT_OK) { |
322 | 0 | goto cleanup; |
323 | 0 | } |
324 | | |
325 | 0 | res = avifRGBImageApplyGainMap(&baseImageRgb, |
326 | 0 | baseImage->colorPrimaries, |
327 | 0 | baseImage->transferCharacteristics, |
328 | 0 | gainMap, |
329 | 0 | hdrHeadroom, |
330 | 0 | outputColorPrimaries, |
331 | 0 | outputTransferCharacteristics, |
332 | 0 | toneMappedImage, |
333 | 0 | clli, |
334 | 0 | diag); |
335 | |
|
336 | 0 | cleanup: |
337 | 0 | avifRGBImageFreePixels(&baseImageRgb); |
338 | |
|
339 | 0 | return res; |
340 | 0 | } |
341 | | |
342 | | // --------------------------------------------------------------------------- |
343 | | // Create a gain map. |
344 | | |
345 | | // Returns the index of the histogram bucket for a given value, for a histogram with 'numBuckets' buckets, |
346 | | // and values ranging in [bucketMin, bucketMax]Â (values outside of the range are added to the first/last buckets). |
347 | | static int avifValueToBucketIdx(float v, float bucketMin, float bucketMax, int numBuckets) |
348 | 0 | { |
349 | 0 | v = AVIF_CLAMP(v, bucketMin, bucketMax); |
350 | 0 | return AVIF_MIN((int)avifRoundf((v - bucketMin) / (bucketMax - bucketMin) * numBuckets), numBuckets - 1); |
351 | 0 | } |
352 | | // Returns the lower end of the value range belonging to the given histogram bucket. |
353 | | static float avifBucketIdxToValue(int idx, float bucketMin, float bucketMax, int numBuckets) |
354 | 0 | { |
355 | 0 | return idx * (bucketMax - bucketMin) / numBuckets + bucketMin; |
356 | 0 | } |
357 | | |
358 | | avifResult avifFindMinMaxWithoutOutliers(const float * gainMapF, int numPixels, float * rangeMin, float * rangeMax) |
359 | 0 | { |
360 | 0 | const float bucketSize = 0.01f; // Size of one bucket. Empirical value. |
361 | 0 | const float maxOutliersRatio = 0.001f; // 0.1% |
362 | 0 | const int maxOutliersOnEachSide = (int)avifRoundf(numPixels * maxOutliersRatio / 2.0f); |
363 | |
|
364 | 0 | float min = gainMapF[0]; |
365 | 0 | float max = gainMapF[0]; |
366 | 0 | for (int i = 1; i < numPixels; ++i) { |
367 | 0 | min = AVIF_MIN(min, gainMapF[i]); |
368 | 0 | max = AVIF_MAX(max, gainMapF[i]); |
369 | 0 | } |
370 | |
|
371 | 0 | *rangeMin = min; |
372 | 0 | *rangeMax = max; |
373 | 0 | if ((max - min) <= (bucketSize * 2) || maxOutliersOnEachSide == 0) { |
374 | 0 | return AVIF_RESULT_OK; |
375 | 0 | } |
376 | | |
377 | 0 | const int maxNumBuckets = 10000; |
378 | 0 | const int numBuckets = AVIF_MIN((int)ceilf((max - min) / bucketSize), maxNumBuckets); |
379 | 0 | int * histogram = avifAlloc(sizeof(int) * numBuckets); |
380 | 0 | if (histogram == NULL) { |
381 | 0 | return AVIF_RESULT_OUT_OF_MEMORY; |
382 | 0 | } |
383 | 0 | memset(histogram, 0, sizeof(int) * numBuckets); |
384 | 0 | for (int i = 0; i < numPixels; ++i) { |
385 | 0 | ++(histogram[avifValueToBucketIdx(gainMapF[i], min, max, numBuckets)]); |
386 | 0 | } |
387 | |
|
388 | 0 | int leftOutliers = 0; |
389 | 0 | for (int i = 0; i < numBuckets; ++i) { |
390 | 0 | leftOutliers += histogram[i]; |
391 | 0 | if (leftOutliers > maxOutliersOnEachSide) { |
392 | 0 | break; |
393 | 0 | } |
394 | 0 | if (histogram[i] == 0) { |
395 | | // +1 to get the higher end of the bucket. |
396 | 0 | *rangeMin = avifBucketIdxToValue(i + 1, min, max, numBuckets); |
397 | 0 | } |
398 | 0 | } |
399 | |
|
400 | 0 | int rightOutliers = 0; |
401 | 0 | for (int i = numBuckets - 1; i >= 0; --i) { |
402 | 0 | rightOutliers += histogram[i]; |
403 | 0 | if (rightOutliers > maxOutliersOnEachSide) { |
404 | 0 | break; |
405 | 0 | } |
406 | 0 | if (histogram[i] == 0) { |
407 | 0 | *rangeMax = avifBucketIdxToValue(i, min, max, numBuckets); |
408 | 0 | } |
409 | 0 | } |
410 | |
|
411 | 0 | avifFree(histogram); |
412 | 0 | return AVIF_RESULT_OK; |
413 | 0 | } |
414 | | |
415 | | avifResult avifGainMapValidateMetadata(const avifGainMap * gainMap, avifDiagnostics * diag) |
416 | 44.7k | { |
417 | 178k | for (int i = 0; i < 3; ++i) { |
418 | 134k | if (gainMap->gainMapMin[i].d == 0 || gainMap->gainMapMax[i].d == 0 || gainMap->gainMapGamma[i].d == 0 || |
419 | 134k | gainMap->baseOffset[i].d == 0 || gainMap->alternateOffset[i].d == 0) { |
420 | 37 | avifDiagnosticsPrintf(diag, "Per-channel denominator is 0 in gain map metadata"); |
421 | 37 | return AVIF_RESULT_INVALID_ARGUMENT; |
422 | 37 | } |
423 | 134k | if ((int64_t)gainMap->gainMapMax[i].n * gainMap->gainMapMin[i].d < |
424 | 134k | (int64_t)gainMap->gainMapMin[i].n * gainMap->gainMapMax[i].d) { |
425 | 20 | avifDiagnosticsPrintf(diag, "Per-channel max is less than per-channel min in gain map metadata"); |
426 | 20 | return AVIF_RESULT_INVALID_ARGUMENT; |
427 | 20 | } |
428 | 134k | if (gainMap->gainMapGamma[i].n == 0) { |
429 | 7 | avifDiagnosticsPrintf(diag, "Per-channel gamma is 0 in gain map metadata"); |
430 | 7 | return AVIF_RESULT_INVALID_ARGUMENT; |
431 | 7 | } |
432 | 134k | } |
433 | 44.6k | if (gainMap->baseHdrHeadroom.d == 0 || gainMap->alternateHdrHeadroom.d == 0) { |
434 | 27 | avifDiagnosticsPrintf(diag, "Headroom denominator is 0 in gain map metadata"); |
435 | 27 | return AVIF_RESULT_INVALID_ARGUMENT; |
436 | 27 | } |
437 | 44.6k | if (gainMap->useBaseColorSpace != 0 && gainMap->useBaseColorSpace != 1) { |
438 | 0 | avifDiagnosticsPrintf(diag, "useBaseColorSpace is %d in gain map metadata", gainMap->useBaseColorSpace); |
439 | 0 | return AVIF_RESULT_INVALID_ARGUMENT; |
440 | 0 | } |
441 | 44.6k | return AVIF_RESULT_OK; |
442 | 44.6k | } |
443 | | |
444 | | avifBool avifSameGainMapMetadata(const avifGainMap * a, const avifGainMap * b) |
445 | 2.15k | { |
446 | 2.15k | if (a->baseHdrHeadroom.n != b->baseHdrHeadroom.n || a->baseHdrHeadroom.d != b->baseHdrHeadroom.d || |
447 | 2.15k | a->alternateHdrHeadroom.n != b->alternateHdrHeadroom.n || a->alternateHdrHeadroom.d != b->alternateHdrHeadroom.d) { |
448 | 0 | return AVIF_FALSE; |
449 | 0 | } |
450 | 8.63k | for (int c = 0; c < 3; ++c) { |
451 | 6.47k | if (a->gainMapMin[c].n != b->gainMapMin[c].n || a->gainMapMin[c].d != b->gainMapMin[c].d || |
452 | 6.47k | a->gainMapMax[c].n != b->gainMapMax[c].n || a->gainMapMax[c].d != b->gainMapMax[c].d || |
453 | 6.47k | a->gainMapGamma[c].n != b->gainMapGamma[c].n || a->gainMapGamma[c].d != b->gainMapGamma[c].d || |
454 | 6.47k | a->baseOffset[c].n != b->baseOffset[c].n || a->baseOffset[c].d != b->baseOffset[c].d || |
455 | 6.47k | a->alternateOffset[c].n != b->alternateOffset[c].n || a->alternateOffset[c].d != b->alternateOffset[c].d) { |
456 | 0 | return AVIF_FALSE; |
457 | 0 | } |
458 | 6.47k | } |
459 | 2.15k | return AVIF_TRUE; |
460 | 2.15k | } |
461 | | |
462 | | avifBool avifSameGainMapAltMetadata(const avifGainMap * a, const avifGainMap * b) |
463 | 2.15k | { |
464 | 2.15k | if (a->altICC.size != b->altICC.size || memcmp(a->altICC.data, b->altICC.data, a->altICC.size) != 0 || |
465 | 2.15k | a->altColorPrimaries != b->altColorPrimaries || a->altTransferCharacteristics != b->altTransferCharacteristics || |
466 | 2.15k | a->altMatrixCoefficients != b->altMatrixCoefficients || a->altYUVRange != b->altYUVRange || a->altDepth != b->altDepth || |
467 | 2.15k | a->altPlaneCount != b->altPlaneCount || a->altCLLI.maxCLL != b->altCLLI.maxCLL || a->altCLLI.maxPALL != b->altCLLI.maxPALL) { |
468 | 0 | return AVIF_FALSE; |
469 | 0 | } |
470 | 2.15k | return AVIF_TRUE; |
471 | 2.15k | } |
472 | | |
473 | | static const float kEpsilon = 1e-10f; |
474 | | |
475 | | // Decides which of 'basePrimaries' or 'altPrimaries' should be used for doing gain map math when creating a gain map. |
476 | | // The other image (base or alternate) will be converted to this color space before computing |
477 | | // the ratio between the two images. |
478 | | // If a pixel color is outside of the target color space, some of the converted channel values will be negative. |
479 | | // This should be avoided, as the negative values must either be clamped or offset before computing the log2() |
480 | | // (since log2 only works on > 0 values). But a large offset causes artefacts when partially applying the gain map. |
481 | | // Therefore we want to do gain map math in the larger of the two color spaces. |
482 | | static avifResult avifChooseColorSpaceForGainMapMath(avifColorPrimaries basePrimaries, |
483 | | avifColorPrimaries altPrimaries, |
484 | | avifColorPrimaries * gainMapMathColorSpace) |
485 | 0 | { |
486 | 0 | if (basePrimaries == altPrimaries) { |
487 | 0 | *gainMapMathColorSpace = basePrimaries; |
488 | 0 | return AVIF_RESULT_OK; |
489 | 0 | } |
490 | | // Color convert pure red, pure green and pure blue in turn and see if they result in negative values. |
491 | 0 | float rgba[4] = { 0 }; |
492 | 0 | double baseToAltCoeffs[3][3]; |
493 | 0 | double altToBaseCoeffs[3][3]; |
494 | 0 | if (!avifColorPrimariesComputeRGBToRGBMatrix(basePrimaries, altPrimaries, baseToAltCoeffs) || |
495 | 0 | !avifColorPrimariesComputeRGBToRGBMatrix(altPrimaries, basePrimaries, altToBaseCoeffs)) { |
496 | 0 | return AVIF_RESULT_NOT_IMPLEMENTED; |
497 | 0 | } |
498 | | |
499 | 0 | float baseColorspaceChannelMin = 0; |
500 | 0 | float altColorspaceChannelMin = 0; |
501 | 0 | for (int c = 0; c < 3; ++c) { |
502 | 0 | rgba[0] = rgba[1] = rgba[2] = 0; |
503 | 0 | rgba[c] = 1.0f; |
504 | 0 | avifLinearRGBConvertColorSpace(rgba, altToBaseCoeffs); |
505 | 0 | for (int i = 0; i < 3; ++i) { |
506 | 0 | baseColorspaceChannelMin = AVIF_MIN(baseColorspaceChannelMin, rgba[i]); |
507 | 0 | } |
508 | 0 | rgba[0] = rgba[1] = rgba[2] = 0; |
509 | 0 | rgba[c] = 1.0f; |
510 | 0 | avifLinearRGBConvertColorSpace(rgba, baseToAltCoeffs); |
511 | 0 | for (int i = 0; i < 3; ++i) { |
512 | 0 | altColorspaceChannelMin = AVIF_MIN(altColorspaceChannelMin, rgba[i]); |
513 | 0 | } |
514 | 0 | } |
515 | | // Pick the colorspace that has the largest min value (which is more or less the largest color space). |
516 | 0 | *gainMapMathColorSpace = (altColorspaceChannelMin <= baseColorspaceChannelMin) ? basePrimaries : altPrimaries; |
517 | 0 | return AVIF_RESULT_OK; |
518 | 0 | } |
519 | | |
520 | | avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, |
521 | | avifColorPrimaries baseColorPrimaries, |
522 | | avifTransferCharacteristics baseTransferCharacteristics, |
523 | | const avifRGBImage * altRgbImage, |
524 | | avifColorPrimaries altColorPrimaries, |
525 | | avifTransferCharacteristics altTransferCharacteristics, |
526 | | avifGainMap * gainMap, |
527 | | avifDiagnostics * diag) |
528 | 0 | { |
529 | 0 | avifDiagnosticsClearError(diag); |
530 | |
|
531 | 0 | AVIF_CHECKERR(baseRgbImage != NULL && altRgbImage != NULL && gainMap != NULL && gainMap->image != NULL, AVIF_RESULT_INVALID_ARGUMENT); |
532 | 0 | if (baseRgbImage->width != altRgbImage->width || baseRgbImage->height != altRgbImage->height) { |
533 | 0 | avifDiagnosticsPrintf(diag, "Both images should have the same dimensions"); |
534 | 0 | return AVIF_RESULT_INVALID_ARGUMENT; |
535 | 0 | } |
536 | 0 | if (gainMap->image->width == 0 || gainMap->image->height == 0 || gainMap->image->depth == 0 || |
537 | 0 | gainMap->image->yuvFormat <= AVIF_PIXEL_FORMAT_NONE || gainMap->image->yuvFormat >= AVIF_PIXEL_FORMAT_COUNT) { |
538 | 0 | avifDiagnosticsPrintf(diag, "gainMap->image should be non null with desired width, height, depth and yuvFormat set"); |
539 | 0 | return AVIF_RESULT_INVALID_ARGUMENT; |
540 | 0 | } |
541 | 0 | const avifBool colorSpacesDiffer = (baseColorPrimaries != altColorPrimaries); |
542 | 0 | avifColorPrimaries gainMapMathPrimaries; |
543 | 0 | AVIF_CHECKRES(avifChooseColorSpaceForGainMapMath(baseColorPrimaries, altColorPrimaries, &gainMapMathPrimaries)); |
544 | 0 | const int width = baseRgbImage->width; |
545 | 0 | const int height = baseRgbImage->height; |
546 | |
|
547 | 0 | avifRGBColorSpaceInfo baseRGBInfo; |
548 | 0 | avifRGBColorSpaceInfo altRGBInfo; |
549 | 0 | if (!avifGetRGBColorSpaceInfo(baseRgbImage, &baseRGBInfo) || !avifGetRGBColorSpaceInfo(altRgbImage, &altRGBInfo)) { |
550 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
551 | 0 | return AVIF_RESULT_NOT_IMPLEMENTED; |
552 | 0 | } |
553 | | |
554 | 0 | float * gainMapF[3] = { 0 }; // Temporary buffers for the gain map as floating point values, one per RGB channel. |
555 | 0 | avifRGBImage gainMapRGB; |
556 | 0 | memset(&gainMapRGB, 0, sizeof(gainMapRGB)); |
557 | 0 | avifImage * gainMapImage = gainMap->image; |
558 | |
|
559 | 0 | avifResult res = AVIF_RESULT_OK; |
560 | | // --- After this point, the function should exit with 'goto cleanup' to free allocated resources. |
561 | |
|
562 | 0 | const avifBool singleChannel = (gainMap->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400); |
563 | 0 | const int numGainMapChannels = singleChannel ? 1 : 3; |
564 | 0 | for (int c = 0; c < numGainMapChannels; ++c) { |
565 | 0 | gainMapF[c] = avifAlloc(width * height * sizeof(float)); |
566 | 0 | if (gainMapF[c] == NULL) { |
567 | 0 | res = AVIF_RESULT_OUT_OF_MEMORY; |
568 | 0 | goto cleanup; |
569 | 0 | } |
570 | 0 | } |
571 | | |
572 | 0 | avifGainMapSetEncodingDefaults(gainMap); |
573 | 0 | gainMap->useBaseColorSpace = (gainMapMathPrimaries == baseColorPrimaries); |
574 | |
|
575 | 0 | float (*baseGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(baseTransferCharacteristics); |
576 | 0 | float (*altGammaToLinear)(float) = avifTransferCharacteristicsGetGammaToLinearFunction(altTransferCharacteristics); |
577 | 0 | float yCoeffs[3]; |
578 | 0 | avifColorPrimariesComputeYCoeffs(gainMapMathPrimaries, yCoeffs); |
579 | |
|
580 | 0 | double rgbConversionCoeffs[3][3]; |
581 | 0 | if (colorSpacesDiffer) { |
582 | 0 | if (gainMap->useBaseColorSpace) { |
583 | 0 | if (!avifColorPrimariesComputeRGBToRGBMatrix(altColorPrimaries, baseColorPrimaries, rgbConversionCoeffs)) { |
584 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); |
585 | 0 | res = AVIF_RESULT_NOT_IMPLEMENTED; |
586 | 0 | goto cleanup; |
587 | 0 | } |
588 | 0 | } else { |
589 | 0 | if (!avifColorPrimariesComputeRGBToRGBMatrix(baseColorPrimaries, altColorPrimaries, rgbConversionCoeffs)) { |
590 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space conversion"); |
591 | 0 | res = AVIF_RESULT_NOT_IMPLEMENTED; |
592 | 0 | goto cleanup; |
593 | 0 | } |
594 | 0 | } |
595 | 0 | } |
596 | | |
597 | 0 | float baseOffset[3] = { avifSignedFractionToFloat(gainMap->baseOffset[0]), |
598 | 0 | avifSignedFractionToFloat(gainMap->baseOffset[1]), |
599 | 0 | avifSignedFractionToFloat(gainMap->baseOffset[2]) }; |
600 | 0 | float alternateOffset[3] = { avifSignedFractionToFloat(gainMap->alternateOffset[0]), |
601 | 0 | avifSignedFractionToFloat(gainMap->alternateOffset[1]), |
602 | 0 | avifSignedFractionToFloat(gainMap->alternateOffset[2]) }; |
603 | | |
604 | | // If we are converting from one colorspace to another, some RGB values may be negative and an offset must be added to |
605 | | // avoid clamping (although the choice of color space to do the gain map computation with |
606 | | // avifChooseColorSpaceForGainMapMath() should mostly avoid this). |
607 | 0 | if (colorSpacesDiffer) { |
608 | | // Color convert pure red, pure green and pure blue in turn and see if they result in negative values. |
609 | 0 | float rgba[4] = { 0.0f }; |
610 | 0 | float channelMin[3] = { 0.0f }; |
611 | 0 | for (int j = 0; j < height; ++j) { |
612 | 0 | for (int i = 0; i < width; ++i) { |
613 | 0 | avifGetRGBAPixel(gainMap->useBaseColorSpace ? altRgbImage : baseRgbImage, |
614 | 0 | i, |
615 | 0 | j, |
616 | 0 | gainMap->useBaseColorSpace ? &altRGBInfo : &baseRGBInfo, |
617 | 0 | rgba); |
618 | | |
619 | | // Convert to linear. |
620 | 0 | for (int c = 0; c < 3; ++c) { |
621 | 0 | if (gainMap->useBaseColorSpace) { |
622 | 0 | rgba[c] = altGammaToLinear(rgba[c]); |
623 | 0 | } else { |
624 | 0 | rgba[c] = baseGammaToLinear(rgba[c]); |
625 | 0 | } |
626 | 0 | } |
627 | 0 | avifLinearRGBConvertColorSpace(rgba, rgbConversionCoeffs); |
628 | 0 | for (int c = 0; c < 3; ++c) { |
629 | 0 | channelMin[c] = AVIF_MIN(channelMin[c], rgba[c]); |
630 | 0 | } |
631 | 0 | } |
632 | 0 | } |
633 | |
|
634 | 0 | for (int c = 0; c < 3; ++c) { |
635 | | // Large offsets cause artefacts when partially applying the gain map, so set a max (empirical) offset value. |
636 | | // If the offset is clamped, some gain map values will get clamped as well. |
637 | 0 | const float maxOffset = 0.1f; |
638 | 0 | if (channelMin[c] < -kEpsilon) { |
639 | | // Increase the offset to avoid negative values. |
640 | 0 | if (gainMap->useBaseColorSpace) { |
641 | 0 | alternateOffset[c] = AVIF_MIN(alternateOffset[c] - channelMin[c], maxOffset); |
642 | 0 | } else { |
643 | 0 | baseOffset[c] = AVIF_MIN(baseOffset[c] - channelMin[c], maxOffset); |
644 | 0 | } |
645 | 0 | } |
646 | 0 | } |
647 | 0 | } |
648 | | |
649 | | // Compute raw gain map values. |
650 | 0 | float baseMax = 1.0f; |
651 | 0 | float altMax = 1.0f; |
652 | 0 | for (int j = 0; j < height; ++j) { |
653 | 0 | for (int i = 0; i < width; ++i) { |
654 | 0 | float baseRGBA[4]; |
655 | 0 | avifGetRGBAPixel(baseRgbImage, i, j, &baseRGBInfo, baseRGBA); |
656 | 0 | float altRGBA[4]; |
657 | 0 | avifGetRGBAPixel(altRgbImage, i, j, &altRGBInfo, altRGBA); |
658 | | |
659 | | // Convert to linear. |
660 | 0 | for (int c = 0; c < 3; ++c) { |
661 | 0 | baseRGBA[c] = baseGammaToLinear(baseRGBA[c]); |
662 | 0 | altRGBA[c] = altGammaToLinear(altRGBA[c]); |
663 | 0 | } |
664 | |
|
665 | 0 | if (colorSpacesDiffer) { |
666 | 0 | if (gainMap->useBaseColorSpace) { |
667 | | // convert altRGBA to baseRGBA's color space |
668 | 0 | avifLinearRGBConvertColorSpace(altRGBA, rgbConversionCoeffs); |
669 | 0 | } else { |
670 | | // convert baseRGBA to altRGBA's color space |
671 | 0 | avifLinearRGBConvertColorSpace(baseRGBA, rgbConversionCoeffs); |
672 | 0 | } |
673 | 0 | } |
674 | |
|
675 | 0 | for (int c = 0; c < numGainMapChannels; ++c) { |
676 | 0 | float base = baseRGBA[c]; |
677 | 0 | float alt = altRGBA[c]; |
678 | 0 | if (singleChannel) { |
679 | | // Convert to grayscale. |
680 | 0 | base = yCoeffs[0] * baseRGBA[0] + yCoeffs[1] * baseRGBA[1] + yCoeffs[2] * baseRGBA[2]; |
681 | 0 | alt = yCoeffs[0] * altRGBA[0] + yCoeffs[1] * altRGBA[1] + yCoeffs[2] * altRGBA[2]; |
682 | 0 | } |
683 | 0 | if (base > baseMax) { |
684 | 0 | baseMax = base; |
685 | 0 | } |
686 | 0 | if (alt > altMax) { |
687 | 0 | altMax = alt; |
688 | 0 | } |
689 | 0 | const float ratio = (alt + alternateOffset[c]) / (base + baseOffset[c]); |
690 | 0 | const float ratioLog2 = log2f(AVIF_MAX(ratio, kEpsilon)); |
691 | 0 | gainMapF[c][j * width + i] = ratioLog2; |
692 | 0 | } |
693 | 0 | } |
694 | 0 | } |
695 | | |
696 | | // Populate the gain map metadata's headrooms. |
697 | 0 | const double baseHeadroom = log2f(AVIF_MAX(baseMax, kEpsilon)); |
698 | 0 | const double alternateHeadroom = log2f(AVIF_MAX(altMax, kEpsilon)); |
699 | 0 | if (!avifDoubleToUnsignedFraction(baseHeadroom, &gainMap->baseHdrHeadroom) || |
700 | 0 | !avifDoubleToUnsignedFraction(alternateHeadroom, &gainMap->alternateHdrHeadroom)) { |
701 | 0 | res = AVIF_RESULT_INVALID_ARGUMENT; |
702 | 0 | goto cleanup; |
703 | 0 | } |
704 | | |
705 | | // Multiply the gainmap by sign(alternateHdrHeadroom - baseHdrHeadroom), to |
706 | | // ensure that it stores the log-ratio of the HDR representation to the SDR |
707 | | // representation. |
708 | 0 | if (alternateHeadroom < baseHeadroom) { |
709 | 0 | for (int c = 0; c < numGainMapChannels; ++c) { |
710 | 0 | for (int j = 0; j < height; ++j) { |
711 | 0 | for (int i = 0; i < width; ++i) { |
712 | 0 | gainMapF[c][j * width + i] *= -1.f; |
713 | 0 | } |
714 | 0 | } |
715 | 0 | } |
716 | 0 | } |
717 | | |
718 | | // Find approximate min/max for each channel, discarding outliers. |
719 | 0 | float gainMapMinLog2[3] = { 0.0f, 0.0f, 0.0f }; |
720 | 0 | float gainMapMaxLog2[3] = { 0.0f, 0.0f, 0.0f }; |
721 | 0 | for (int c = 0; c < numGainMapChannels; ++c) { |
722 | 0 | res = avifFindMinMaxWithoutOutliers(gainMapF[c], width * height, &gainMapMinLog2[c], &gainMapMaxLog2[c]); |
723 | 0 | if (res != AVIF_RESULT_OK) { |
724 | 0 | goto cleanup; |
725 | 0 | } |
726 | 0 | } |
727 | | |
728 | | // Populate the gain map metadata's min and max values. |
729 | 0 | for (int c = 0; c < 3; ++c) { |
730 | 0 | if (!avifDoubleToSignedFraction(gainMapMinLog2[singleChannel ? 0 : c], &gainMap->gainMapMin[c]) || |
731 | 0 | !avifDoubleToSignedFraction(gainMapMaxLog2[singleChannel ? 0 : c], &gainMap->gainMapMax[c]) || |
732 | 0 | !avifDoubleToSignedFraction(alternateOffset[c], &gainMap->alternateOffset[c]) || |
733 | 0 | !avifDoubleToSignedFraction(baseOffset[c], &gainMap->baseOffset[c])) { |
734 | 0 | res = AVIF_RESULT_INVALID_ARGUMENT; |
735 | 0 | goto cleanup; |
736 | 0 | } |
737 | 0 | } |
738 | | |
739 | | // Scale the gain map values to map [min, max] range to [0, 1]. |
740 | 0 | for (int c = 0; c < numGainMapChannels; ++c) { |
741 | 0 | const float range = AVIF_MAX(gainMapMaxLog2[c] - gainMapMinLog2[c], 0.0f); |
742 | |
|
743 | 0 | if (range == 0.0f) { |
744 | 0 | for (int j = 0; j < height; ++j) { |
745 | 0 | for (int i = 0; i < width; ++i) { |
746 | | // If the range is 0, the gain map values will be multiplied by zero when tonemapping so the values |
747 | | // don't matter, but we still need to make sure that gainMapF is in [0,1]. |
748 | 0 | gainMapF[c][j * width + i] = 0.0f; |
749 | 0 | } |
750 | 0 | } |
751 | 0 | } else { |
752 | | // Remap [min; max] range to [0; 1] |
753 | 0 | const float gainMapGamma = avifUnsignedFractionToFloat(gainMap->gainMapGamma[c]); |
754 | 0 | for (int j = 0; j < height; ++j) { |
755 | 0 | for (int i = 0; i < width; ++i) { |
756 | 0 | float v = gainMapF[c][j * width + i]; |
757 | 0 | v = AVIF_CLAMP(v, gainMapMinLog2[c], gainMapMaxLog2[c]); |
758 | 0 | v = powf((v - gainMapMinLog2[c]) / range, gainMapGamma); |
759 | 0 | gainMapF[c][j * width + i] = AVIF_CLAMP(v, 0.0f, 1.0f); |
760 | 0 | } |
761 | 0 | } |
762 | 0 | } |
763 | 0 | } |
764 | | |
765 | | // Convert the gain map to YUV. |
766 | 0 | const uint32_t requestedWidth = gainMapImage->width; |
767 | 0 | const uint32_t requestedHeight = gainMapImage->height; |
768 | 0 | gainMapImage->width = width; |
769 | 0 | gainMapImage->height = height; |
770 | |
|
771 | 0 | avifImageFreePlanes(gainMapImage, AVIF_PLANES_ALL); // Free planes in case they were already allocated. |
772 | 0 | res = avifImageAllocatePlanes(gainMapImage, AVIF_PLANES_YUV); |
773 | 0 | if (res != AVIF_RESULT_OK) { |
774 | 0 | goto cleanup; |
775 | 0 | } |
776 | | |
777 | 0 | avifRGBImageSetDefaults(&gainMapRGB, gainMapImage); |
778 | 0 | res = avifRGBImageAllocatePixels(&gainMapRGB); |
779 | 0 | if (res != AVIF_RESULT_OK) { |
780 | 0 | goto cleanup; |
781 | 0 | } |
782 | | |
783 | 0 | avifRGBColorSpaceInfo gainMapRGBInfo; |
784 | 0 | if (!avifGetRGBColorSpaceInfo(&gainMapRGB, &gainMapRGBInfo)) { |
785 | 0 | avifDiagnosticsPrintf(diag, "Unsupported RGB color space"); |
786 | 0 | return AVIF_RESULT_NOT_IMPLEMENTED; |
787 | 0 | } |
788 | 0 | for (int j = 0; j < height; ++j) { |
789 | 0 | for (int i = 0; i < width; ++i) { |
790 | 0 | const int offset = j * width + i; |
791 | 0 | const float r = gainMapF[0][offset]; |
792 | 0 | const float g = singleChannel ? r : gainMapF[1][offset]; |
793 | 0 | const float b = singleChannel ? r : gainMapF[2][offset]; |
794 | 0 | const float rgbaPixel[4] = { r, g, b, 1.0f }; |
795 | 0 | avifSetRGBAPixel(&gainMapRGB, i, j, &gainMapRGBInfo, rgbaPixel); |
796 | 0 | } |
797 | 0 | } |
798 | |
|
799 | 0 | res = avifImageRGBToYUV(gainMapImage, &gainMapRGB); |
800 | 0 | if (res != AVIF_RESULT_OK) { |
801 | 0 | goto cleanup; |
802 | 0 | } |
803 | | |
804 | | // Scale down the gain map if requested. |
805 | | // Another way would be to scale the source images, but it seems to perform worse. |
806 | 0 | if (requestedWidth != gainMapImage->width || requestedHeight != gainMapImage->height) { |
807 | 0 | AVIF_CHECKRES(avifImageScale(gainMap->image, requestedWidth, requestedHeight, diag)); |
808 | 0 | } |
809 | | |
810 | 0 | cleanup: |
811 | 0 | for (int c = 0; c < 3; ++c) { |
812 | 0 | avifFree(gainMapF[c]); |
813 | 0 | } |
814 | 0 | avifRGBImageFreePixels(&gainMapRGB); |
815 | 0 | if (res != AVIF_RESULT_OK) { |
816 | 0 | avifImageFreePlanes(gainMapImage, AVIF_PLANES_ALL); |
817 | 0 | } |
818 | |
|
819 | 0 | return res; |
820 | 0 | } |
821 | | |
822 | | avifResult avifImageComputeGainMap(const avifImage * baseImage, const avifImage * altImage, avifGainMap * gainMap, avifDiagnostics * diag) |
823 | 0 | { |
824 | 0 | avifDiagnosticsClearError(diag); |
825 | |
|
826 | 0 | if (baseImage == NULL || altImage == NULL || gainMap == NULL) { |
827 | 0 | return AVIF_RESULT_INVALID_ARGUMENT; |
828 | 0 | } |
829 | 0 | if (baseImage->icc.size > 0 || altImage->icc.size > 0) { |
830 | 0 | avifDiagnosticsPrintf(diag, "Computing gain maps for images with ICC profiles is not supported"); |
831 | 0 | return AVIF_RESULT_NOT_IMPLEMENTED; |
832 | 0 | } |
833 | 0 | if (baseImage->width != altImage->width || baseImage->height != altImage->height) { |
834 | 0 | avifDiagnosticsPrintf(diag, |
835 | 0 | "Image dimensions don't match, got %dx%d and %dx%d", |
836 | 0 | baseImage->width, |
837 | 0 | baseImage->height, |
838 | 0 | altImage->width, |
839 | 0 | altImage->height); |
840 | 0 | return AVIF_RESULT_INVALID_ARGUMENT; |
841 | 0 | } |
842 | | |
843 | 0 | avifResult res = AVIF_RESULT_OK; |
844 | |
|
845 | 0 | avifRGBImage baseImageRgb; |
846 | 0 | avifRGBImageSetDefaults(&baseImageRgb, baseImage); |
847 | 0 | avifRGBImage altImageRgb; |
848 | 0 | avifRGBImageSetDefaults(&altImageRgb, altImage); |
849 | |
|
850 | 0 | AVIF_CHECKRES(avifRGBImageAllocatePixels(&baseImageRgb)); |
851 | | // --- After this point, the function should exit with 'goto cleanup' to free allocated resources. |
852 | | |
853 | 0 | res = avifImageYUVToRGB(baseImage, &baseImageRgb); |
854 | 0 | if (res != AVIF_RESULT_OK) { |
855 | 0 | goto cleanup; |
856 | 0 | } |
857 | 0 | res = avifRGBImageAllocatePixels(&altImageRgb); |
858 | 0 | if (res != AVIF_RESULT_OK) { |
859 | 0 | goto cleanup; |
860 | 0 | } |
861 | 0 | res = avifImageYUVToRGB(altImage, &altImageRgb); |
862 | 0 | if (res != AVIF_RESULT_OK) { |
863 | 0 | goto cleanup; |
864 | 0 | } |
865 | | |
866 | 0 | res = avifRGBImageComputeGainMap(&baseImageRgb, |
867 | 0 | baseImage->colorPrimaries, |
868 | 0 | baseImage->transferCharacteristics, |
869 | 0 | &altImageRgb, |
870 | 0 | altImage->colorPrimaries, |
871 | 0 | altImage->transferCharacteristics, |
872 | 0 | gainMap, |
873 | 0 | diag); |
874 | |
|
875 | 0 | if (res != AVIF_RESULT_OK) { |
876 | 0 | goto cleanup; |
877 | 0 | } |
878 | | |
879 | 0 | AVIF_CHECKRES(avifRWDataSet(&gainMap->altICC, altImage->icc.data, altImage->icc.size)); |
880 | 0 | gainMap->altColorPrimaries = altImage->colorPrimaries; |
881 | 0 | gainMap->altTransferCharacteristics = altImage->transferCharacteristics; |
882 | 0 | gainMap->altMatrixCoefficients = altImage->matrixCoefficients; |
883 | 0 | gainMap->altDepth = altImage->depth; |
884 | 0 | gainMap->altPlaneCount = (altImage->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3; |
885 | 0 | gainMap->altCLLI = altImage->clli; |
886 | |
|
887 | 0 | cleanup: |
888 | 0 | avifRGBImageFreePixels(&baseImageRgb); |
889 | 0 | avifRGBImageFreePixels(&altImageRgb); |
890 | 0 | return res; |
891 | 0 | } |