| // Copyright 2022 Google LLC |
| // SPDX-License-Identifier: BSD-2-Clause |
| // |
| // Compares two files and returns whether they are the same once decoded. |
| // |
| // Usage: |
| // are_images_equal file1 file2 ignore_alpha_flag [psnr_threshold] |
| // [ignore_gain_map_flag] |
| // If psnr_threshold is not provided or is 0, the images are compared for strict |
| // equality, including all metadata. If psnr_threshold is greater than 0, the |
| // images are compared for similarity using PSNR. Most metadata is NOT checked. |
| // If a gain map is present and ignore_gain_map_flag is absent or 0, the |
| // tone-mapped images are also compared using PSNR. ignore_alpha_flag and |
| // ignore_gain_map_flag are booleans (0 or 1). |
| |
| #include <cstdint> |
| #include <cstring> |
| #include <iostream> |
| #include <limits> |
| #include <string> |
| |
| #include "aviftest_helpers.h" |
| #include "avifutil.h" |
| |
| using avif::ImagePtr; |
| using avif::RGBImageCleanup; |
| |
| namespace { |
| |
| ImagePtr ToneMapToAlternate(const ImagePtr& image) { |
| avif::testutil::AvifRgbImage toneMappedRgb(image.get(), image->depth, |
| AVIF_RGB_FORMAT_RGB); |
| RGBImageCleanup rgbCleanup(&toneMappedRgb); |
| |
| avif::ImagePtr toneMapped( |
| avifImageCreate(toneMappedRgb.width, toneMappedRgb.height, |
| toneMappedRgb.depth, AVIF_PIXEL_FORMAT_YUV444)); |
| toneMapped->colorPrimaries = image->gainMap->altColorPrimaries; |
| toneMapped->transferCharacteristics = |
| image->gainMap->altTransferCharacteristics; |
| toneMapped->matrixCoefficients = image->gainMap->altMatrixCoefficients; |
| toneMapped->yuvRange = image->yuvRange; |
| |
| float altHeadroom = 0.0f; |
| if (image->gainMap->alternateHdrHeadroom.d != 0) { |
| altHeadroom = (float)image->gainMap->alternateHdrHeadroom.n / |
| image->gainMap->alternateHdrHeadroom.d; |
| } |
| |
| avifDiagnostics diag; |
| avifResult res = avifImageApplyGainMap( |
| image.get(), image->gainMap, altHeadroom, toneMapped->colorPrimaries, |
| toneMapped->transferCharacteristics, &toneMappedRgb, &toneMapped->clli, |
| &diag); |
| if (res != AVIF_RESULT_OK) { |
| std::cerr << "Failed to apply gain map: " << diag.error << std::endl; |
| return nullptr; |
| } |
| |
| res = avifImageRGBToYUV(toneMapped.get(), &toneMappedRgb); |
| if (res != AVIF_RESULT_OK) { |
| return nullptr; |
| } |
| return toneMapped; |
| } |
| |
| } // namespace |
| |
| int main(int argc, char** argv) { |
| if (argc < 4 || argc > 6) { |
| std::cerr << "Wrong argument: " << argv[0] |
| << " file1 file2 ignore_alpha_flag [psnr_threshold] " |
| "[ignore_gain_map_flag]" |
| << std::endl; |
| return 2; |
| } |
| const bool ignore_gain_map = argc > 5 && std::stoi(argv[5]) != 0; |
| const double psnr_threshold = argc > 4 ? std::stod(argv[4]) : 0; |
| |
| ImagePtr decoded[2] = {ImagePtr(avifImageCreateEmpty()), |
| ImagePtr(avifImageCreateEmpty())}; |
| if (!decoded[0] || !decoded[1]) { |
| std::cerr << "Cannot create AVIF images." << std::endl; |
| return 2; |
| } |
| uint32_t depth[2]; |
| // Request the bit depth closest to the bit depth of the input file. |
| constexpr int kRequestedDepth = 0; |
| constexpr avifPixelFormat requestedFormat = AVIF_PIXEL_FORMAT_NONE; |
| for (int i : {0, 1}) { |
| // Make sure no color conversion happens. |
| decoded[i]->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_IDENTITY; |
| if (avifReadImage( |
| argv[i + 1], AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */, |
| requestedFormat, kRequestedDepth, |
| AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, |
| /*ignoreColorProfile==*/AVIF_FALSE, /*ignoreExif=*/AVIF_FALSE, |
| /*ignoreXMP=*/AVIF_FALSE, ignore_gain_map, |
| /*imageSizeLimit=*/std::numeric_limits<uint32_t>::max(), |
| decoded[i].get(), &depth[i], nullptr, |
| nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { |
| std::cerr << "Image " << argv[i + 1] << " cannot be read." << std::endl; |
| return 2; |
| } |
| } |
| |
| if (depth[0] != depth[1]) { |
| std::cerr << "Images " << argv[1] << " and " << argv[2] |
| << " have different depths." << std::endl; |
| return 1; |
| } |
| |
| if (!ignore_gain_map && |
| ((decoded[0]->gainMap != nullptr) != (decoded[1]->gainMap != nullptr))) { |
| std::cerr << "Images " << argv[1] << " and " << argv[2] |
| << " have different gainmap presence" << std::endl; |
| return 1; |
| } |
| |
| bool ignore_alpha = std::stoi(argv[3]) != 0; |
| |
| if (psnr_threshold <= 0) { // Check for strict equality. |
| if (!avif::testutil::AreImagesEqual(*decoded[0], *decoded[1], |
| ignore_alpha)) { |
| auto psnr = |
| avif::testutil::GetPsnr(*decoded[0], *decoded[1], ignore_alpha); |
| std::cerr << "Images " << argv[1] << " and " << argv[2] |
| << " are different (base image PSNR: " << psnr << ")." |
| << std::endl; |
| return 1; |
| } |
| std::cout << "Images " << argv[1] << " and " << argv[2] << " are identical." |
| << std::endl; |
| } else { |
| // If there is a gain map, this is the PSNR of the base images only. |
| auto psnr = avif::testutil::GetPsnr(*decoded[0], *decoded[1], ignore_alpha); |
| if (psnr < psnr_threshold) { |
| std::cerr << "PSNR: " << psnr << ", images " << argv[1] << " and " |
| << argv[2] << " are not similar enough (threshold: " << argv[4] |
| << ")." << std::endl; |
| return 1; |
| } |
| std::cout << "PSNR: " << psnr << ", images " << argv[1] << " and " |
| << argv[2] << " are similar." << std::endl; |
| |
| // If there is a gain map, tone map both images to their alternate headroom |
| // and check the PSNR of the tone mapped images. |
| if (decoded[0]->gainMap && decoded[1]->gainMap && |
| decoded[0]->gainMap->image && decoded[1]->gainMap->image) { |
| // We don't support ICC profiles when tonemapping, so clear them after |
| // checking for equality. |
| if (decoded[0]->icc.size != decoded[1]->icc.size || |
| (decoded[0]->icc.size > 0 && |
| std::memcmp(decoded[0]->icc.data, decoded[1]->icc.data, |
| decoded[0]->icc.size) != 0)) { |
| std::cerr << "Gainmap PSNR: Images " << argv[1] << " and " << argv[2] |
| << " have different ICC profiles." << std::endl; |
| return 1; |
| } |
| if (decoded[0]->gainMap->altICC.size != |
| decoded[1]->gainMap->altICC.size || |
| (decoded[0]->gainMap->altICC.size > 0 && |
| std::memcmp(decoded[0]->gainMap->altICC.data, |
| decoded[1]->gainMap->altICC.data, |
| decoded[0]->gainMap->altICC.size) != 0)) { |
| std::cerr << "Gainmap PSNR: Images " << argv[1] << " and " << argv[2] |
| << " have different ICC profiles." << std::endl; |
| return 1; |
| } |
| avifRWDataFree(&decoded[0]->icc); |
| avifRWDataFree(&decoded[1]->icc); |
| avifRWDataFree(&decoded[0]->gainMap->altICC); |
| avifRWDataFree(&decoded[1]->gainMap->altICC); |
| |
| avif::ImagePtr toneMapped0 = ToneMapToAlternate(decoded[0]); |
| avif::ImagePtr toneMapped1 = ToneMapToAlternate(decoded[1]); |
| if (!toneMapped0 || !toneMapped1) { |
| std::cerr << "Failed to tone map images." << std::endl; |
| return 1; |
| } |
| psnr = avif::testutil::GetPsnr(*toneMapped0, *toneMapped1); |
| if (psnr < psnr_threshold) { |
| std::cerr << "Tone-mapped PSNR: " << psnr << ", images " << argv[1] |
| << " and " << argv[2] |
| << " are not similar enough (threshold: " << argv[4] << ")." |
| << std::endl; |
| return 1; |
| } |
| std::cout << "Tone-mapped PSNR: " << psnr << ", images " << argv[1] |
| << " and " << argv[2] << " are similar." << std::endl; |
| } |
| } |
| |
| return 0; |
| } |