/src/libavif/tests/gtest/aviftest_helpers.cc
Line | Count | Source |
1 | | // Copyright 2022 Google LLC |
2 | | // SPDX-License-Identifier: BSD-2-Clause |
3 | | |
4 | | #include "aviftest_helpers.h" |
5 | | |
6 | | #include <algorithm> |
7 | | #include <cassert> |
8 | | #include <cmath> |
9 | | #include <cstdint> |
10 | | #include <cstdlib> |
11 | | #include <cstring> |
12 | | #include <fstream> |
13 | | #include <iostream> |
14 | | #include <limits> |
15 | | #include <string> |
16 | | #include <vector> |
17 | | |
18 | | #include "avif/avif.h" |
19 | | #include "avif/avif_cxx.h" |
20 | | #include "avif/internal.h" |
21 | | #include "avifpng.h" |
22 | | #include "avifutil.h" |
23 | | |
24 | | namespace avif { |
25 | | namespace testutil { |
26 | | |
27 | | //------------------------------------------------------------------------------ |
28 | | // CopyImageSamples is a copy of avifImageCopySamples |
29 | | |
30 | | namespace { |
31 | | void CopyImageSamples(avifImage* dstImage, const avifImage* srcImage, |
32 | 0 | avifPlanesFlags planes) { |
33 | 0 | assert(srcImage->depth == dstImage->depth); |
34 | 0 | if (planes & AVIF_PLANES_YUV) { |
35 | 0 | assert(srcImage->yuvFormat == dstImage->yuvFormat); |
36 | | // Note that there may be a mismatch between srcImage->yuvRange and |
37 | | // dstImage->yuvRange because libavif allows for 'colr' and AV1 OBU video |
38 | | // range values to differ. |
39 | 0 | } |
40 | 0 | const size_t bytesPerPixel = avifImageUsesU16(srcImage) ? 2 : 1; |
41 | |
|
42 | 0 | const avifBool skipColor = !(planes & AVIF_PLANES_YUV); |
43 | 0 | const avifBool skipAlpha = !(planes & AVIF_PLANES_A); |
44 | 0 | for (int c = AVIF_CHAN_Y; c <= AVIF_CHAN_A; ++c) { |
45 | 0 | const avifBool alpha = c == AVIF_CHAN_A; |
46 | 0 | if ((skipColor && !alpha) || (skipAlpha && alpha)) { |
47 | 0 | continue; |
48 | 0 | } |
49 | | |
50 | 0 | const uint32_t planeWidth = avifImagePlaneWidth(srcImage, c); |
51 | 0 | const uint32_t planeHeight = avifImagePlaneHeight(srcImage, c); |
52 | 0 | const uint8_t* srcRow = avifImagePlane(srcImage, c); |
53 | 0 | uint8_t* dstRow = avifImagePlane(dstImage, c); |
54 | 0 | const uint32_t srcRowBytes = avifImagePlaneRowBytes(srcImage, c); |
55 | 0 | const uint32_t dstRowBytes = avifImagePlaneRowBytes(dstImage, c); |
56 | 0 | assert(!srcRow == !dstRow); |
57 | 0 | if (!srcRow) { |
58 | 0 | continue; |
59 | 0 | } |
60 | 0 | assert(planeWidth == avifImagePlaneWidth(dstImage, c)); |
61 | 0 | assert(planeHeight == avifImagePlaneHeight(dstImage, c)); |
62 | |
|
63 | 0 | const size_t planeWidthBytes = planeWidth * bytesPerPixel; |
64 | 0 | for (uint32_t y = 0; y < planeHeight; ++y) { |
65 | 0 | memcpy(dstRow, srcRow, planeWidthBytes); |
66 | 0 | srcRow += srcRowBytes; |
67 | 0 | dstRow += dstRowBytes; |
68 | 0 | } |
69 | 0 | } |
70 | 0 | } |
71 | | } // namespace |
72 | | |
73 | | //------------------------------------------------------------------------------ |
74 | | |
75 | | AvifRgbImage::AvifRgbImage(const avifImage* yuv, int rgbDepth, |
76 | 5.09k | avifRGBFormat rgbFormat) { |
77 | 5.09k | avifRGBImageSetDefaults(this, yuv); |
78 | 5.09k | depth = rgbDepth; |
79 | 5.09k | format = rgbFormat; |
80 | 5.09k | if (avifRGBImageAllocatePixels(this) != AVIF_RESULT_OK) { |
81 | 0 | std::abort(); |
82 | 0 | } |
83 | 5.09k | } |
84 | | |
85 | 0 | AvifRwData::AvifRwData(AvifRwData&& other) : avifRWData{other} { |
86 | 0 | other.data = nullptr; |
87 | 0 | other.size = 0; |
88 | 0 | } |
89 | | |
90 | | //------------------------------------------------------------------------------ |
91 | | |
92 | 0 | RgbChannelOffsets GetRgbChannelOffsets(avifRGBFormat format) { |
93 | 0 | switch (format) { |
94 | 0 | case AVIF_RGB_FORMAT_RGB: |
95 | 0 | return {/*r=*/0, /*g=*/1, /*b=*/2, /*a=*/0}; |
96 | 0 | case AVIF_RGB_FORMAT_RGBA: |
97 | 0 | return {/*r=*/0, /*g=*/1, /*b=*/2, /*a=*/3}; |
98 | 0 | case AVIF_RGB_FORMAT_ARGB: |
99 | 0 | return {/*r=*/1, /*g=*/2, /*b=*/3, /*a=*/0}; |
100 | 0 | case AVIF_RGB_FORMAT_BGR: |
101 | 0 | return {/*r=*/2, /*g=*/1, /*b=*/0, /*a=*/0}; |
102 | 0 | case AVIF_RGB_FORMAT_BGRA: |
103 | 0 | return {/*r=*/2, /*g=*/1, /*b=*/0, /*a=*/3}; |
104 | 0 | case AVIF_RGB_FORMAT_ABGR: |
105 | 0 | return {/*r=*/3, /*g=*/2, /*b=*/1, /*a=*/0}; |
106 | 0 | case AVIF_RGB_FORMAT_RGB_565: |
107 | 0 | case AVIF_RGB_FORMAT_COUNT: |
108 | 0 | default: |
109 | 0 | return {/*r=*/0, /*g=*/0, /*b=*/0, /*a=*/0}; |
110 | 0 | } |
111 | 0 | } |
112 | | |
113 | | //------------------------------------------------------------------------------ |
114 | | |
115 | | ImagePtr CreateImage(int width, int height, int depth, |
116 | | avifPixelFormat yuv_format, avifPlanesFlags planes, |
117 | 0 | avifRange yuv_range) { |
118 | 0 | ImagePtr image(avifImageCreate(width, height, depth, yuv_format)); |
119 | 0 | if (!image) { |
120 | 0 | return nullptr; |
121 | 0 | } |
122 | 0 | image->yuvRange = yuv_range; |
123 | 0 | if (avifImageAllocatePlanes(image.get(), planes) != AVIF_RESULT_OK) { |
124 | 0 | return nullptr; |
125 | 0 | } |
126 | 0 | return image; |
127 | 0 | } |
128 | | |
129 | 0 | void FillImagePlain(avifImage* image, const uint32_t yuva[4]) { |
130 | 0 | for (avifChannelIndex c : |
131 | 0 | {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
132 | 0 | const uint32_t plane_width = avifImagePlaneWidth(image, c); |
133 | | // 0 for A if no alpha and 0 for UV if 4:0:0. |
134 | 0 | const uint32_t plane_height = avifImagePlaneHeight(image, c); |
135 | 0 | uint8_t* row = avifImagePlane(image, c); |
136 | 0 | const uint32_t row_bytes = avifImagePlaneRowBytes(image, c); |
137 | 0 | for (uint32_t y = 0; y < plane_height; ++y) { |
138 | 0 | if (avifImageUsesU16(image)) { |
139 | 0 | std::fill(reinterpret_cast<uint16_t*>(row), |
140 | 0 | reinterpret_cast<uint16_t*>(row) + plane_width, |
141 | 0 | static_cast<uint16_t>(yuva[c])); |
142 | 0 | } else { |
143 | 0 | std::fill(row, row + plane_width, static_cast<uint8_t>(yuva[c])); |
144 | 0 | } |
145 | 0 | row += row_bytes; |
146 | 0 | } |
147 | 0 | } |
148 | 0 | } |
149 | | |
150 | 0 | void FillImageGradient(avifImage* image, int offset) { |
151 | 0 | for (avifChannelIndex c : |
152 | 0 | {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
153 | 0 | const uint32_t limitedRangeMin = |
154 | 0 | c == AVIF_CHAN_Y ? 16 << (image->depth - 8) : 0; |
155 | 0 | const uint32_t limitedRangeMax = (c == AVIF_CHAN_Y ? 219 : 224) |
156 | 0 | << (image->depth - 8); |
157 | |
|
158 | 0 | const uint32_t plane_width = avifImagePlaneWidth(image, c); |
159 | | // 0 for A if no alpha and 0 for UV if 4:0:0. |
160 | 0 | const uint32_t plane_height = avifImagePlaneHeight(image, c); |
161 | 0 | uint8_t* row = avifImagePlane(image, c); |
162 | 0 | const uint32_t row_bytes = avifImagePlaneRowBytes(image, c); |
163 | 0 | const uint32_t max_xy_sum = plane_width + plane_height - 2; |
164 | 0 | for (uint32_t y = 0; y < plane_height; ++y) { |
165 | 0 | for (uint32_t x = 0; x < plane_width; ++x) { |
166 | 0 | uint32_t value = (x + y + offset) % (max_xy_sum + 1); |
167 | 0 | if (image->yuvRange == AVIF_RANGE_FULL || c == AVIF_CHAN_A) { |
168 | 0 | value = |
169 | 0 | value * ((1u << image->depth) - 1u) / std::max(1u, max_xy_sum); |
170 | 0 | } else { |
171 | 0 | value = limitedRangeMin + value * |
172 | 0 | (limitedRangeMax - limitedRangeMin) / |
173 | 0 | std::max(1u, max_xy_sum); |
174 | 0 | } |
175 | 0 | if (avifImageUsesU16(image)) { |
176 | 0 | reinterpret_cast<uint16_t*>(row)[x] = static_cast<uint16_t>(value); |
177 | 0 | } else { |
178 | 0 | row[x] = static_cast<uint8_t>(value); |
179 | 0 | } |
180 | 0 | } |
181 | 0 | row += row_bytes; |
182 | 0 | } |
183 | 0 | } |
184 | 0 | } |
185 | | |
186 | | namespace { |
187 | | template <typename PixelType> |
188 | | void FillImageChannel(avifRGBImage* image, uint32_t channel_offset, |
189 | 0 | uint32_t value) { |
190 | 0 | const uint32_t channel_count = avifRGBFormatChannelCount(image->format); |
191 | 0 | assert(channel_offset < channel_count); |
192 | 0 | for (uint32_t y = 0; y < image->height; ++y) { |
193 | 0 | PixelType* pixel = |
194 | 0 | reinterpret_cast<PixelType*>(image->pixels + image->rowBytes * y); |
195 | 0 | for (uint32_t x = 0; x < image->width; ++x) { |
196 | 0 | pixel[channel_offset] = static_cast<PixelType>(value); |
197 | 0 | pixel += channel_count; |
198 | 0 | } |
199 | 0 | } |
200 | 0 | } Unexecuted instantiation: aviftest_helpers.cc:void avif::testutil::(anonymous namespace)::FillImageChannel<unsigned char>(avifRGBImage*, unsigned int, unsigned int) Unexecuted instantiation: aviftest_helpers.cc:void avif::testutil::(anonymous namespace)::FillImageChannel<unsigned short>(avifRGBImage*, unsigned int, unsigned int) |
201 | | } // namespace |
202 | | |
203 | | void FillImageChannel(avifRGBImage* image, uint32_t channel_offset, |
204 | 0 | uint32_t value) { |
205 | 0 | (image->depth <= 8) |
206 | 0 | ? FillImageChannel<uint8_t>(image, channel_offset, value) |
207 | 0 | : FillImageChannel<uint16_t>(image, channel_offset, value); |
208 | 0 | } |
209 | | |
210 | | //------------------------------------------------------------------------------ |
211 | | |
212 | | bool AreByteSequencesEqual(const uint8_t data1[], size_t data1_length, |
213 | 0 | const uint8_t data2[], size_t data2_length) { |
214 | 0 | if (data1_length != data2_length) return false; |
215 | 0 | return data1_length == 0 || std::equal(data1, data1 + data1_length, data2); |
216 | 0 | } |
217 | | |
218 | 0 | bool AreByteSequencesEqual(const avifRWData& data1, const avifRWData& data2) { |
219 | 0 | return AreByteSequencesEqual(data1.data, data1.size, data2.data, data2.size); |
220 | 0 | } |
221 | | |
222 | | namespace { |
223 | | // Returns true if all properties of image1 are present in image2 and equal. |
224 | | bool MatchEachPropertyOfFirstImageInSecondImage(const avifImage& image1, |
225 | 0 | const avifImage& image2) { |
226 | 0 | for (size_t i = 0; i < image1.numProperties; ++i) { |
227 | 0 | const avifImageItemProperty& property1 = image1.properties[i]; |
228 | | |
229 | | // libavif may write a 'ccsp' box in Sample Entries. |
230 | | // libavif does not read and expose those except in avifImage::properties. |
231 | | // Ignore these boxes because it is an easy source of valid difference |
232 | | // between an original avifImage and a decoded avifImage. |
233 | 0 | if (AreByteSequencesEqual(property1.boxtype, sizeof(property1.boxtype), |
234 | 0 | reinterpret_cast<const uint8_t*>("ccst"), 4)) { |
235 | 0 | continue; |
236 | 0 | } |
237 | | |
238 | 0 | bool found = false; |
239 | 0 | for (size_t j = 0; j < image2.numProperties; ++j) { |
240 | 0 | const avifImageItemProperty& property2 = image2.properties[j]; |
241 | 0 | if (!AreByteSequencesEqual(property1.boxtype, sizeof(property1.boxtype), |
242 | 0 | property2.boxtype, |
243 | 0 | sizeof(property2.boxtype))) { |
244 | 0 | continue; |
245 | 0 | } |
246 | 0 | if (AreByteSequencesEqual(property1.boxtype, sizeof(property1.boxtype), |
247 | 0 | reinterpret_cast<const uint8_t*>("uuid"), 4) && |
248 | 0 | !AreByteSequencesEqual(property1.usertype, sizeof(property1.usertype), |
249 | 0 | property2.usertype, |
250 | 0 | sizeof(property2.usertype))) { |
251 | 0 | continue; |
252 | 0 | } |
253 | 0 | if (found) return false; // Consider duplicates as invalid. |
254 | 0 | found = true; |
255 | 0 | if (!AreByteSequencesEqual(property1.boxPayload, property2.boxPayload)) { |
256 | 0 | return false; |
257 | 0 | } |
258 | 0 | } |
259 | 0 | if (!found) return false; |
260 | 0 | } |
261 | 0 | return true; |
262 | 0 | } |
263 | | |
264 | | // Returns true if image1 and image2 are identical, pixel values excepted. |
265 | | bool AreImageFeaturesEqual(const avifImage& image1, const avifImage& image2, |
266 | 0 | bool ignore_alpha) { |
267 | 0 | if (image1.width != image2.width || image1.height != image2.height || |
268 | 0 | image1.depth != image2.depth || image1.yuvFormat != image2.yuvFormat || |
269 | 0 | image1.yuvRange != image2.yuvRange) { |
270 | 0 | std::cerr |
271 | 0 | << "Image features mismatch: dimensions, depth, yuvFormat or yuvRange" |
272 | 0 | << std::endl; |
273 | 0 | return false; |
274 | 0 | } |
275 | 0 | assert(image1.width * image1.height > 0); |
276 | |
|
277 | 0 | for (avifChannelIndex c : |
278 | 0 | {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
279 | 0 | if (ignore_alpha && c == AVIF_CHAN_A) continue; |
280 | 0 | const uint8_t* row1 = avifImagePlane(&image1, c); |
281 | 0 | const uint8_t* row2 = avifImagePlane(&image2, c); |
282 | 0 | if (!row1 != !row2) { |
283 | | // Maybe one image contains an opaque alpha channel while the other has no |
284 | | // alpha channel, but the features should still be considered equal. |
285 | 0 | if (c == AVIF_CHAN_A && avifImageIsOpaque(&image1) && |
286 | 0 | avifImageIsOpaque(&image2)) { |
287 | 0 | continue; |
288 | 0 | } |
289 | 0 | std::cerr |
290 | 0 | << "Image features mismatch: plane presence differs for channel " << c |
291 | 0 | << std::endl; |
292 | 0 | return false; |
293 | 0 | } |
294 | 0 | if (c == AVIF_CHAN_A && row1 != nullptr && |
295 | 0 | image1.alphaPremultiplied != image2.alphaPremultiplied && |
296 | 0 | !avifImageIsOpaque(&image1)) { |
297 | | // Alpha premultiplication is ignored if alpha is opaque. |
298 | 0 | std::cerr << "Image features mismatch: alphaPremultiplied" << std::endl; |
299 | 0 | return false; |
300 | 0 | } |
301 | 0 | } |
302 | | |
303 | 0 | if (!AreByteSequencesEqual(image1.icc, image2.icc)) { |
304 | 0 | std::cerr << "Image features mismatch: icc" << std::endl; |
305 | 0 | return false; |
306 | 0 | } |
307 | | |
308 | 0 | if (image1.colorPrimaries != image2.colorPrimaries || |
309 | 0 | image1.transferCharacteristics != image2.transferCharacteristics || |
310 | 0 | image1.matrixCoefficients != image2.matrixCoefficients) { |
311 | 0 | std::cerr << "Image features mismatch: CICP" << std::endl; |
312 | 0 | return false; |
313 | 0 | } |
314 | | |
315 | 0 | if (image1.clli.maxCLL != image2.clli.maxCLL || |
316 | 0 | image1.clli.maxPALL != image2.clli.maxPALL) { |
317 | 0 | std::cerr << "Image features mismatch: clli" << std::endl; |
318 | 0 | return false; |
319 | 0 | } |
320 | 0 | if (image1.transformFlags != image2.transformFlags || |
321 | 0 | ((image1.transformFlags & AVIF_TRANSFORM_PASP) && |
322 | 0 | std::memcmp(&image1.pasp, &image2.pasp, sizeof(image1.pasp))) || |
323 | 0 | ((image1.transformFlags & AVIF_TRANSFORM_CLAP) && |
324 | 0 | std::memcmp(&image1.clap, &image2.clap, sizeof(image1.clap))) || |
325 | 0 | ((image1.transformFlags & AVIF_TRANSFORM_IROT) && |
326 | 0 | std::memcmp(&image1.irot, &image2.irot, sizeof(image1.irot))) || |
327 | 0 | ((image1.transformFlags & AVIF_TRANSFORM_IMIR) && |
328 | 0 | std::memcmp(&image1.imir, &image2.imir, sizeof(image1.imir)))) { |
329 | 0 | std::cerr << "Image features mismatch: transformFlags" << std::endl; |
330 | 0 | return false; |
331 | 0 | } |
332 | | |
333 | 0 | if (!AreByteSequencesEqual(image1.exif, image2.exif)) { |
334 | 0 | std::cerr << "Image features mismatch: exif" << std::endl; |
335 | 0 | return false; |
336 | 0 | } |
337 | 0 | if (!AreByteSequencesEqual(image1.xmp, image2.xmp)) { |
338 | 0 | std::cerr << "Image features mismatch: xmp" << std::endl; |
339 | 0 | return false; |
340 | 0 | } |
341 | | |
342 | 0 | if (!MatchEachPropertyOfFirstImageInSecondImage(image1, image2) || |
343 | 0 | !MatchEachPropertyOfFirstImageInSecondImage(image2, image1)) { |
344 | 0 | std::cerr << "Image features mismatch: properties" << std::endl; |
345 | 0 | return false; |
346 | 0 | } |
347 | | |
348 | 0 | if (!image1.gainMap != !image2.gainMap) { |
349 | 0 | std::cerr << "Image features mismatch: gainMap presence" << std::endl; |
350 | 0 | return false; |
351 | 0 | } |
352 | 0 | if (image1.gainMap != nullptr) { |
353 | 0 | if (!avifSameGainMapMetadata(image1.gainMap, image2.gainMap) || |
354 | 0 | !avifSameGainMapAltMetadata(image1.gainMap, image2.gainMap)) { |
355 | 0 | std::cerr << "Image features mismatch: gainMap metadata" << std::endl; |
356 | 0 | return false; |
357 | 0 | } |
358 | | |
359 | 0 | if (!image1.gainMap->image != !image2.gainMap->image) { |
360 | 0 | std::cerr << "Image features mismatch: gainMap image presence" |
361 | 0 | << std::endl; |
362 | 0 | return false; |
363 | 0 | } |
364 | 0 | } |
365 | 0 | return true; |
366 | 0 | } |
367 | | } // namespace |
368 | | |
369 | | // Returns true if image1 and image2 are identical. |
370 | | bool AreImagesEqual(const avifImage& image1, const avifImage& image2, |
371 | 0 | bool ignore_alpha) { |
372 | 0 | if (!AreImageFeaturesEqual(image1, image2, ignore_alpha)) { |
373 | 0 | return false; |
374 | 0 | } |
375 | | |
376 | 0 | if ((image1.gainMap != nullptr) != (image2.gainMap != nullptr)) { |
377 | 0 | std::cerr << "AreImagesEqual failed: gain map presence differs" |
378 | 0 | << std::endl; |
379 | 0 | return false; |
380 | 0 | } |
381 | | |
382 | 0 | for (avifChannelIndex c : |
383 | 0 | {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
384 | 0 | if (ignore_alpha && c == AVIF_CHAN_A) continue; |
385 | 0 | const uint8_t* row1 = avifImagePlane(&image1, c); |
386 | 0 | const uint8_t* row2 = avifImagePlane(&image2, c); |
387 | 0 | if (row1 == nullptr || row2 == nullptr) { |
388 | 0 | continue; // Verified in AreImageFeaturesEqual(). |
389 | 0 | } |
390 | 0 | const uint32_t row_bytes1 = avifImagePlaneRowBytes(&image1, c); |
391 | 0 | const uint32_t row_bytes2 = avifImagePlaneRowBytes(&image2, c); |
392 | 0 | const uint32_t plane_width = avifImagePlaneWidth(&image1, c); |
393 | | // 0 for A if no alpha and 0 for UV if 4:0:0. |
394 | 0 | const uint32_t plane_height = avifImagePlaneHeight(&image1, c); |
395 | 0 | for (uint32_t y = 0; y < plane_height; ++y) { |
396 | 0 | if (avifImageUsesU16(&image1)) { |
397 | 0 | if (!std::equal(reinterpret_cast<const uint16_t*>(row1), |
398 | 0 | reinterpret_cast<const uint16_t*>(row1) + plane_width, |
399 | 0 | reinterpret_cast<const uint16_t*>(row2))) { |
400 | 0 | std::cerr << "AreImagesEqual failed: pixels differ in channel " << c |
401 | 0 | << " at row " << y << std::endl; |
402 | 0 | return false; |
403 | 0 | } |
404 | 0 | } else { |
405 | 0 | if (!std::equal(row1, row1 + plane_width, row2)) { |
406 | 0 | std::cerr << "AreImagesEqual failed: pixels differ in channel " << c |
407 | 0 | << " at row " << y << std::endl; |
408 | 0 | return false; |
409 | 0 | } |
410 | 0 | } |
411 | 0 | row1 += row_bytes1; |
412 | 0 | row2 += row_bytes2; |
413 | 0 | } |
414 | 0 | } |
415 | | |
416 | 0 | if (image1.gainMap != nullptr && image1.gainMap->image != nullptr && |
417 | 0 | image2.gainMap != nullptr && image2.gainMap->image != nullptr && |
418 | 0 | !AreImagesEqual(*image1.gainMap->image, *image2.gainMap->image)) { |
419 | 0 | std::cerr << "AreImagesEqual failed: gain map images differ" << std::endl; |
420 | 0 | return false; |
421 | 0 | } |
422 | 0 | return true; |
423 | 0 | } |
424 | | |
425 | | bool AreImagesSimilar(const avifImage& image1, const avifImage& image2, |
426 | 0 | double min_psnr, bool ignore_alpha) { |
427 | 0 | if (!AreImageFeaturesEqual(image1, image2, ignore_alpha)) { |
428 | 0 | return false; |
429 | 0 | } |
430 | | |
431 | 0 | if (GetPsnr(image1, image2, ignore_alpha) < min_psnr) { |
432 | 0 | return true; |
433 | 0 | } |
434 | | |
435 | 0 | if (image1.gainMap != nullptr && image1.gainMap->image != nullptr && |
436 | 0 | image2.gainMap != nullptr && image2.gainMap->image != nullptr && |
437 | 0 | GetPsnr(*image1.gainMap->image, *image2.gainMap->image) < min_psnr) { |
438 | 0 | return false; |
439 | 0 | } |
440 | 0 | return true; |
441 | 0 | } |
442 | | |
443 | | namespace { |
444 | | |
445 | | template <typename Sample> |
446 | | uint64_t SquaredDiffSum(const Sample* samples1, const Sample* samples2, |
447 | 0 | uint32_t num_samples) { |
448 | 0 | uint64_t sum = 0; |
449 | 0 | for (uint32_t i = 0; i < num_samples; ++i) { |
450 | 0 | const int32_t diff = static_cast<int32_t>(samples1[i]) - samples2[i]; |
451 | 0 | sum += diff * diff; |
452 | 0 | } |
453 | 0 | return sum; |
454 | 0 | } Unexecuted instantiation: aviftest_helpers.cc:unsigned long avif::testutil::(anonymous namespace)::SquaredDiffSum<unsigned short>(unsigned short const*, unsigned short const*, unsigned int) Unexecuted instantiation: aviftest_helpers.cc:unsigned long avif::testutil::(anonymous namespace)::SquaredDiffSum<unsigned char>(unsigned char const*, unsigned char const*, unsigned int) |
455 | | |
456 | | } // namespace |
457 | | |
458 | | double GetPsnr(const avifImage& image1, const avifImage& image2, |
459 | 0 | bool ignore_alpha) { |
460 | 0 | if (image1.width != image2.width || image1.height != image2.height || |
461 | 0 | image1.depth != image2.depth || image1.yuvFormat != image2.yuvFormat || |
462 | 0 | image1.yuvRange != image2.yuvRange) { |
463 | 0 | std::cerr << "Error: cannot compute PSNR because of dimensions or yuv " |
464 | 0 | "format mismatch (" |
465 | 0 | << image1.width << "x" << image1.height << " format " |
466 | 0 | << image1.yuvFormat << " vs " << image2.width << "x" |
467 | 0 | << image2.height << " format " << image2.yuvFormat << ")" |
468 | 0 | << std::endl; |
469 | 0 | return -1; |
470 | 0 | } |
471 | 0 | assert(image1.width * image1.height > 0); |
472 | |
|
473 | 0 | if (image1.colorPrimaries != image2.colorPrimaries || |
474 | 0 | image1.transferCharacteristics != image2.transferCharacteristics || |
475 | 0 | image1.matrixCoefficients != image2.matrixCoefficients || |
476 | 0 | image1.yuvRange != image2.yuvRange) { |
477 | 0 | fprintf(stderr, |
478 | 0 | "WARNING: computing PSNR of images with different CICP: %d/%d/%d%s " |
479 | 0 | "vs %d/%d/%d%s\n", |
480 | 0 | image1.colorPrimaries, image1.transferCharacteristics, |
481 | 0 | image1.matrixCoefficients, |
482 | 0 | (image1.yuvRange == AVIF_RANGE_FULL) ? "f" : "l", |
483 | 0 | image2.colorPrimaries, image2.transferCharacteristics, |
484 | 0 | image2.matrixCoefficients, |
485 | 0 | (image2.yuvRange == AVIF_RANGE_FULL) ? "f" : "l"); |
486 | 0 | } |
487 | |
|
488 | 0 | uint64_t squared_diff_sum = 0; |
489 | 0 | uint32_t num_samples = 0; |
490 | 0 | const uint32_t max_sample_value = (1 << image1.depth) - 1; |
491 | 0 | for (avifChannelIndex c : |
492 | 0 | {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
493 | 0 | if (ignore_alpha && c == AVIF_CHAN_A) continue; |
494 | | |
495 | 0 | const uint32_t plane_width = std::max(avifImagePlaneWidth(&image1, c), |
496 | 0 | avifImagePlaneWidth(&image2, c)); |
497 | 0 | const uint32_t plane_height = std::max(avifImagePlaneHeight(&image1, c), |
498 | 0 | avifImagePlaneHeight(&image2, c)); |
499 | 0 | if (plane_width == 0 || plane_height == 0) continue; |
500 | | |
501 | 0 | const uint8_t* row1 = avifImagePlane(&image1, c); |
502 | 0 | const uint8_t* row2 = avifImagePlane(&image2, c); |
503 | 0 | if (!row1 != !row2 && c != AVIF_CHAN_A) { |
504 | 0 | return -1; |
505 | 0 | } |
506 | 0 | uint32_t row_bytes1 = avifImagePlaneRowBytes(&image1, c); |
507 | 0 | uint32_t row_bytes2 = avifImagePlaneRowBytes(&image2, c); |
508 | | |
509 | | // Consider missing alpha planes as samples set to the maximum value. |
510 | 0 | std::vector<uint8_t> opaque_alpha_samples; |
511 | 0 | if (!row1 != !row2) { |
512 | 0 | opaque_alpha_samples.resize(std::max(row_bytes1, row_bytes2)); |
513 | 0 | if (avifImageUsesU16(&image1)) { |
514 | 0 | uint16_t* opaque_alpha_samples_16b = |
515 | 0 | reinterpret_cast<uint16_t*>(opaque_alpha_samples.data()); |
516 | 0 | std::fill(opaque_alpha_samples_16b, |
517 | 0 | opaque_alpha_samples_16b + plane_width, |
518 | 0 | static_cast<int16_t>(max_sample_value)); |
519 | 0 | } else { |
520 | 0 | std::fill(opaque_alpha_samples.begin(), opaque_alpha_samples.end(), |
521 | 0 | uint8_t{255}); |
522 | 0 | } |
523 | 0 | if (!row1) { |
524 | 0 | row1 = opaque_alpha_samples.data(); |
525 | 0 | row_bytes1 = 0; |
526 | 0 | } else { |
527 | 0 | row2 = opaque_alpha_samples.data(); |
528 | 0 | row_bytes2 = 0; |
529 | 0 | } |
530 | 0 | } |
531 | |
|
532 | 0 | for (uint32_t y = 0; y < plane_height; ++y) { |
533 | 0 | if (avifImageUsesU16(&image1)) { |
534 | 0 | squared_diff_sum += SquaredDiffSum( |
535 | 0 | reinterpret_cast<const uint16_t*>(row1), |
536 | 0 | reinterpret_cast<const uint16_t*>(row2), plane_width); |
537 | 0 | } else { |
538 | 0 | squared_diff_sum += SquaredDiffSum(row1, row2, plane_width); |
539 | 0 | } |
540 | 0 | row1 += row_bytes1; |
541 | 0 | row2 += row_bytes2; |
542 | 0 | num_samples += plane_width; |
543 | 0 | } |
544 | 0 | } |
545 | | |
546 | 0 | if (squared_diff_sum == 0) { |
547 | 0 | return 99.0; |
548 | 0 | } |
549 | 0 | const double normalized_error = |
550 | 0 | squared_diff_sum / |
551 | 0 | (static_cast<double>(num_samples) * max_sample_value * max_sample_value); |
552 | 0 | if (normalized_error <= std::numeric_limits<double>::epsilon()) { |
553 | 0 | return 98.99; // Very small distortion but not lossless. |
554 | 0 | } |
555 | 0 | return std::min(-10 * std::log10(normalized_error), 98.99); |
556 | 0 | } |
557 | | |
558 | 0 | bool AreImagesEqual(const avifRGBImage& image1, const avifRGBImage& image2) { |
559 | 0 | if (image1.width != image2.width || image1.height != image2.height || |
560 | 0 | image1.depth != image2.depth || image1.format != image2.format || |
561 | 0 | image1.alphaPremultiplied != image2.alphaPremultiplied || |
562 | 0 | image1.isFloat != image2.isFloat) { |
563 | 0 | std::cerr << "AreImagesEqual(avifRGBImage) failed: features do not match" |
564 | 0 | << std::endl; |
565 | 0 | return false; |
566 | 0 | } |
567 | 0 | const uint8_t* row1 = image1.pixels; |
568 | 0 | const uint8_t* row2 = image2.pixels; |
569 | 0 | const unsigned int row_width = image1.width * avifRGBImagePixelSize(&image1); |
570 | 0 | for (unsigned int y = 0; y < image1.height; ++y) { |
571 | 0 | if (!std::equal(row1, row1 + row_width, row2)) { |
572 | 0 | std::cerr << "AreImagesEqual(avifRGBImage) failed: pixels differ at row " |
573 | 0 | << y << std::endl; |
574 | 0 | return false; |
575 | 0 | } |
576 | 0 | row1 += image1.rowBytes; |
577 | 0 | row2 += image2.rowBytes; |
578 | 0 | } |
579 | 0 | return true; |
580 | 0 | } |
581 | | |
582 | | avifResult MergeGrid(int grid_cols, int grid_rows, |
583 | 0 | const std::vector<ImagePtr>& cells, avifImage* merged) { |
584 | 0 | std::vector<const avifImage*> ptrs(cells.size()); |
585 | 0 | for (size_t i = 0; i < cells.size(); ++i) { |
586 | 0 | ptrs[i] = cells[i].get(); |
587 | 0 | } |
588 | 0 | return MergeGrid(grid_cols, grid_rows, ptrs, merged); |
589 | 0 | } |
590 | | |
591 | | avifResult MergeGrid(int grid_cols, int grid_rows, |
592 | | const std::vector<const avifImage*>& cells, |
593 | 0 | avifImage* merged) { |
594 | 0 | const uint32_t tile_width = cells[0]->width; |
595 | 0 | const uint32_t tile_height = cells[0]->height; |
596 | 0 | const uint32_t grid_width = |
597 | 0 | (grid_cols - 1) * tile_width + cells.back()->width; |
598 | 0 | const uint32_t grid_height = |
599 | 0 | (grid_rows - 1) * tile_height + cells.back()->height; |
600 | |
|
601 | 0 | ImagePtr view(avifImageCreateEmpty()); |
602 | 0 | AVIF_CHECKERR(view, AVIF_RESULT_OUT_OF_MEMORY); |
603 | | |
604 | 0 | avifCropRect rect = {}; |
605 | 0 | for (int j = 0; j < grid_rows; ++j) { |
606 | 0 | rect.x = 0; |
607 | 0 | for (int i = 0; i < grid_cols; ++i) { |
608 | 0 | const avifImage* image = cells[j * grid_cols + i]; |
609 | 0 | rect.width = image->width; |
610 | 0 | rect.height = image->height; |
611 | 0 | AVIF_CHECKRES(avifImageSetViewRect(view.get(), merged, &rect)); |
612 | 0 | CopyImageSamples(/*dstImage=*/view.get(), image, AVIF_PLANES_ALL); |
613 | 0 | assert(!view->imageOwnsYUVPlanes); |
614 | 0 | rect.x += rect.width; |
615 | 0 | } |
616 | 0 | rect.y += rect.height; |
617 | 0 | } |
618 | | |
619 | 0 | if ((rect.x != grid_width) || (rect.y != grid_height)) { |
620 | 0 | return AVIF_RESULT_UNKNOWN_ERROR; |
621 | 0 | } |
622 | | |
623 | 0 | return AVIF_RESULT_OK; |
624 | 0 | } |
625 | | |
626 | | //------------------------------------------------------------------------------ |
627 | | |
628 | 0 | testutil::AvifRwData ReadFile(const std::string& file_path) { |
629 | 0 | std::ifstream file(file_path, std::ios::binary | std::ios::ate); |
630 | 0 | testutil::AvifRwData bytes; |
631 | 0 | if (avifRWDataRealloc(&bytes, file.good() ? static_cast<size_t>(file.tellg()) |
632 | 0 | : 0) != AVIF_RESULT_OK) { |
633 | 0 | return {}; |
634 | 0 | } |
635 | 0 | file.seekg(0, std::ios::beg); |
636 | 0 | file.read(reinterpret_cast<char*>(bytes.data), |
637 | 0 | static_cast<std::streamsize>(bytes.size)); |
638 | 0 | return bytes; |
639 | 0 | } |
640 | | |
641 | | //------------------------------------------------------------------------------ |
642 | | |
643 | | ImagePtr ReadImage(const char* folder_path, const char* file_name, |
644 | | avifPixelFormat requested_format, int requested_depth, |
645 | | avifChromaDownsampling chroma_downsampling, |
646 | | avifBool ignore_icc, avifBool ignore_exif, |
647 | 0 | avifBool ignore_xmp, avifBool ignore_gain_map) { |
648 | 0 | ImagePtr image(avifImageCreateEmpty()); |
649 | 0 | if (!image || |
650 | 0 | avifReadImage((std::string(folder_path) + file_name).c_str(), |
651 | 0 | AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */, |
652 | 0 | requested_format, requested_depth, chroma_downsampling, |
653 | 0 | ignore_icc, ignore_exif, ignore_xmp, /*ignoreAlpha=*/false, |
654 | 0 | ignore_gain_map, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), |
655 | 0 | /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, |
656 | 0 | /*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { |
657 | 0 | return nullptr; |
658 | 0 | } |
659 | 0 | return image; |
660 | 0 | } |
661 | | |
662 | 0 | bool WriteImage(const avifImage* image, const char* file_path) { |
663 | 0 | if (!image || !file_path) return false; |
664 | 0 | const size_t str_len = std::strlen(file_path); |
665 | 0 | if (str_len >= 4 && !std::strncmp(file_path + str_len - 4, ".png", 4)) { |
666 | 0 | return avifPNGWrite(file_path, image, /*requestedDepth=*/0, |
667 | 0 | AVIF_CHROMA_UPSAMPLING_BEST_QUALITY, |
668 | 0 | /*compressionLevel=*/0); |
669 | 0 | } |
670 | | // Other formats are not supported. |
671 | 0 | return false; |
672 | 0 | } |
673 | | |
674 | 0 | AvifRwData Encode(const avifImage* image, int speed, int quality) { |
675 | 0 | EncoderPtr encoder(avifEncoderCreate()); |
676 | 0 | if (!encoder) return {}; |
677 | 0 | encoder->speed = speed; |
678 | 0 | encoder->quality = quality; |
679 | 0 | encoder->qualityAlpha = quality; |
680 | 0 | encoder->qualityGainMap = quality; |
681 | 0 | testutil::AvifRwData bytes; |
682 | 0 | if (avifEncoderWrite(encoder.get(), image, &bytes) != AVIF_RESULT_OK) { |
683 | 0 | return {}; |
684 | 0 | } |
685 | 0 | return bytes; |
686 | 0 | } |
687 | | |
688 | 0 | ImagePtr Decode(const uint8_t* bytes, size_t num_bytes) { |
689 | 0 | ImagePtr decoded(avifImageCreateEmpty()); |
690 | 0 | DecoderPtr decoder(avifDecoderCreate()); |
691 | 0 | if (!decoded || !decoder || |
692 | 0 | (avifDecoderReadMemory(decoder.get(), decoded.get(), bytes, num_bytes) != |
693 | 0 | AVIF_RESULT_OK)) { |
694 | 0 | return nullptr; |
695 | 0 | } |
696 | 0 | return decoded; |
697 | 0 | } |
698 | | |
699 | 0 | ImagePtr DecodeFile(const std::string& path) { |
700 | 0 | ImagePtr decoded(avifImageCreateEmpty()); |
701 | 0 | DecoderPtr decoder(avifDecoderCreate()); |
702 | 0 | if (!decoded || !decoder || |
703 | 0 | (avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()) != |
704 | 0 | AVIF_RESULT_OK)) { |
705 | 0 | return nullptr; |
706 | 0 | } |
707 | 0 | return decoded; |
708 | 0 | } |
709 | | |
710 | 0 | bool Av1EncoderAvailable() { |
711 | 0 | const char* encoding_codec = |
712 | 0 | avifCodecName(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_ENCODE); |
713 | 0 | return encoding_codec != nullptr && std::string(encoding_codec) != "avm"; |
714 | 0 | } |
715 | | |
716 | 0 | bool Av1DecoderAvailable() { |
717 | 0 | const char* decoding_codec = |
718 | 0 | avifCodecName(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_DECODE); |
719 | 0 | return decoding_codec != nullptr && std::string(decoding_codec) != "avm"; |
720 | 0 | } |
721 | | |
722 | | //------------------------------------------------------------------------------ |
723 | | |
724 | | static avifResult avifIOLimitedReaderRead(avifIO* io, uint32_t readFlags, |
725 | | uint64_t offset, size_t size, |
726 | 0 | avifROData* out) { |
727 | 0 | auto reader = reinterpret_cast<AvifIOLimitedReader*>(io); |
728 | |
|
729 | 0 | if (offset > UINT64_MAX - size) { |
730 | 0 | return AVIF_RESULT_IO_ERROR; |
731 | 0 | } |
732 | 0 | if (offset + size > reader->clamp) { |
733 | 0 | return AVIF_RESULT_WAITING_ON_IO; |
734 | 0 | } |
735 | | |
736 | 0 | return reader->underlyingIO->read(reader->underlyingIO, readFlags, offset, |
737 | 0 | size, out); |
738 | 0 | } |
739 | | |
740 | 0 | static void avifIOLimitedReaderDestroy(avifIO* io) { |
741 | 0 | auto reader = reinterpret_cast<AvifIOLimitedReader*>(io); |
742 | 0 | reader->underlyingIO->destroy(reader->underlyingIO); |
743 | 0 | delete reader; |
744 | 0 | } |
745 | | |
746 | 0 | avifIO* AvifIOCreateLimitedReader(avifIO* underlyingIO, uint64_t clamp) { |
747 | 0 | return reinterpret_cast<avifIO*>( |
748 | 0 | new AvifIOLimitedReader{{ |
749 | 0 | avifIOLimitedReaderDestroy, |
750 | 0 | avifIOLimitedReaderRead, |
751 | 0 | nullptr, |
752 | 0 | underlyingIO->sizeHint, |
753 | 0 | underlyingIO->persistent, |
754 | 0 | nullptr, |
755 | 0 | }, |
756 | 0 | underlyingIO, |
757 | 0 | clamp}); |
758 | 0 | } |
759 | | |
760 | | //------------------------------------------------------------------------------ |
761 | | |
762 | | std::vector<ImagePtr> ImageToGrid(const avifImage* image, uint32_t grid_cols, |
763 | 11.5k | uint32_t grid_rows) { |
764 | 11.5k | if (image->width < grid_cols || image->height < grid_rows) return {}; |
765 | | |
766 | | // Round up, to make sure all samples are used by exactly one cell. |
767 | 11.4k | uint32_t cell_width = (image->width + grid_cols - 1) / grid_cols; |
768 | 11.4k | uint32_t cell_height = (image->height + grid_rows - 1) / grid_rows; |
769 | | |
770 | 11.4k | if ((grid_cols - 1) * cell_width >= image->width) { |
771 | | // Some cells are completely outside the image. Fallback to a grid entirely |
772 | | // contained within the image boundaries. Some samples will be discarded but |
773 | | // at least the test can go on. |
774 | 31 | cell_width = image->width / grid_cols; |
775 | 31 | } |
776 | 11.4k | if ((grid_rows - 1) * cell_height >= image->height) { |
777 | 34 | cell_height = image->height / grid_rows; |
778 | 34 | } |
779 | | |
780 | 11.4k | std::vector<ImagePtr> cells; |
781 | 26.5k | for (uint32_t row = 0; row < grid_rows; ++row) { |
782 | 46.1k | for (uint32_t col = 0; col < grid_cols; ++col) { |
783 | 31.0k | avifCropRect rect{col * cell_width, row * cell_height, cell_width, |
784 | 31.0k | cell_height}; |
785 | 31.0k | assert(rect.x < image->width); |
786 | 31.0k | assert(rect.y < image->height); |
787 | | // The right-most and bottom-most cells may be smaller than others. |
788 | | // The encoder will pad them. |
789 | 31.0k | if (rect.x + rect.width > image->width) { |
790 | 1.97k | rect.width = image->width - rect.x; |
791 | 1.97k | } |
792 | 31.0k | if (rect.y + rect.height > image->height) { |
793 | 2.05k | rect.height = image->height - rect.y; |
794 | 2.05k | } |
795 | 31.0k | cells.emplace_back(avifImageCreateEmpty()); |
796 | 31.0k | if (avifImageSetViewRect(cells.back().get(), image, &rect) != |
797 | 31.0k | AVIF_RESULT_OK) { |
798 | 5 | return {}; |
799 | 5 | } |
800 | 31.0k | } |
801 | 15.1k | } |
802 | 11.4k | return cells; |
803 | 11.4k | } |
804 | | |
805 | | std::vector<const avifImage*> UniquePtrToRawPtr( |
806 | 10.3k | const std::vector<ImagePtr>& unique_ptrs) { |
807 | 10.3k | std::vector<const avifImage*> rawPtrs; |
808 | 10.3k | rawPtrs.reserve(unique_ptrs.size()); |
809 | 28.3k | for (const ImagePtr& unique_ptr : unique_ptrs) { |
810 | 28.3k | rawPtrs.emplace_back(unique_ptr.get()); |
811 | 28.3k | } |
812 | 10.3k | return rawPtrs; |
813 | 10.3k | } |
814 | | |
815 | | //------------------------------------------------------------------------------ |
816 | | |
817 | | } // namespace testutil |
818 | | } // namespace avif |