From 7bbff15114a4e4d898371cfbf7fa451322e833fa Mon Sep 17 00:00:00 2001 From: Yannis Date: Tue, 16 Dec 2025 17:30:38 +0100 Subject: [PATCH] Move default tune setting earlier in codec_aom.c --- src/codec_aom.c | 28 +++++++-- tests/gtest/aviftunetest.cc | 120 ++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/src/codec_aom.c b/src/codec_aom.c index 35ecbe17eb..b5ffb199a9 100644 --- a/src/codec_aom.c +++ b/src/codec_aom.c @@ -420,6 +420,10 @@ static avifBool avifImageUsesTuneIq(const avifCodec * codec, avifBool alpha) } } + // In practice this function should also return true if avifEncoderSetCodecSpecificOption("tune", "iq") + // was called for a previous frame and not called (or called with NULL) for this frame, because the tune + // option persists across frames in libaom. However AOM_TUNE_IQ is only supported with still images in + // libavif and libaom as of today, so there is no need to remember this option across frames. return ret; } @@ -648,10 +652,21 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, avifAddImageFlags addImageFlags, avifCodecEncodeOutput * output) { + avifBool useLibavifDefaultTuneMetric = AVIF_FALSE; // If true, override libaom's default tune option. + aom_tune_metric libavifDefaultTuneMetric = AOM_TUNE_PSNR; // Meaningless unless useLibavifDefaultTuneMetric. + if (quality != AVIF_QUALITY_LOSSLESS && !avifAOMOptionsContainExplicitTuning(codec, alpha)) { + useLibavifDefaultTuneMetric = AVIF_TRUE; + libavifDefaultTuneMetric = AOM_TUNE_SSIM; + } + struct aom_codec_enc_cfg * cfg = &codec->internal->cfg; avifBool quantizerUpdated = AVIF_FALSE; - const avifBool tuneIq = avifImageUsesTuneIq(codec, alpha); - const int quantizer = aomQualityToQuantizer(quality, tuneIq); + // True if libavif knows that tune=iq is used, either by default by libavif, or explicitly set by the user. + // False otherwise (including if libaom uses tune=iq by default, which is not the case as of v1.13.1 and earlier versions). + // This is only accurate for the first frame but tune=iq is only supported for still images in libavif and + // for all-intra coding in libaom (at least up to v1.13.1) anyway. + const avifBool useTuneIq = useLibavifDefaultTuneMetric ? libavifDefaultTuneMetric == AOM_TUNE_IQ : avifImageUsesTuneIq(codec, alpha); + const int quantizer = aomQualityToQuantizer(quality, useTuneIq); // For encoder->scalingMode.horizontal and encoder->scalingMode.vertical to take effect in AOM // encoder, config should be applied for each frame, so we don't care about changes on these @@ -990,10 +1005,9 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, } #endif - if (!avifAOMOptionsContainExplicitTuning(codec, alpha)) { - if (aom_codec_control(&codec->internal->encoder, AOME_SET_TUNING, AOM_TUNE_SSIM) != AOM_CODEC_OK) { - return AVIF_RESULT_UNKNOWN_ERROR; - } + if (useLibavifDefaultTuneMetric && + aom_codec_control(&codec->internal->encoder, AOME_SET_TUNING, libavifDefaultTuneMetric) != AOM_CODEC_OK) { + return AVIF_RESULT_UNKNOWN_ERROR; } if (!avifProcessAOMOptionsPostInit(codec, alpha)) { return AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION; @@ -1074,6 +1088,8 @@ static avifResult aomCodecEncodeImage(avifCodec * codec, aom_codec_control(&codec->internal->encoder, AV1E_SET_LOSSLESS, lossless); } if (encoderChanges & AVIF_ENCODER_CHANGE_CODEC_SPECIFIC) { + // Do not apply libavifDefaultTuneMetric even if useLibavifDefaultTuneMetric is true: + // codec-specific settings persist on the libaom side, so keep the same behavior for tune. if (!avifProcessAOMOptionsPostInit(codec, alpha)) { return AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION; } diff --git a/tests/gtest/aviftunetest.cc b/tests/gtest/aviftunetest.cc index b234b79dbb..890ba6ef86 100644 --- a/tests/gtest/aviftunetest.cc +++ b/tests/gtest/aviftunetest.cc @@ -112,6 +112,126 @@ TEST(AomSharpnessTest, GenerateDifferentBitstreams) { } } +constexpr uint64_t kDuration = 1; + +void EncodeAnimation(const char* key, const char* value_before_first_frame, + const char* value_after_first_frame, + const char* value_before_second_frame, + std::vector& encoded_bitstream) { + // Generate an animation with two different frames. + const ImagePtr first_frame = + testutil::ReadImage(data_path, "paris_exif_xmp_icc.jpg"); + ASSERT_NE(first_frame, nullptr); + // Speed up the test. + first_frame->width = 64; + first_frame->height = 64; + const ImagePtr second_frame(avifImageCreateEmpty()); + ASSERT_NE(second_frame, nullptr); + ASSERT_EQ( + avifImageCopy(second_frame.get(), first_frame.get(), AVIF_PLANES_ALL), + AVIF_RESULT_OK); + testutil::FillImageGradient(first_frame.get()); + + EncoderPtr encoder(avifEncoderCreate()); + ASSERT_NE(encoder, nullptr); + encoder->codecChoice = AVIF_CODEC_CHOICE_AOM; + encoder->creationTime = encoder->modificationTime = 1; // Deterministic. + const avifAddImageFlag flag = AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME; + + // First frame. + ASSERT_EQ(avifEncoderSetCodecSpecificOption(encoder.get(), key, + value_before_first_frame), + AVIF_RESULT_OK); + ASSERT_EQ( + avifEncoderAddImage(encoder.get(), first_frame.get(), kDuration, flag), + AVIF_RESULT_OK); + ASSERT_EQ(avifEncoderSetCodecSpecificOption(encoder.get(), key, + value_after_first_frame), + AVIF_RESULT_OK); + + // Second frame. + ASSERT_EQ(avifEncoderSetCodecSpecificOption(encoder.get(), key, + value_before_second_frame), + AVIF_RESULT_OK); + ASSERT_EQ( + avifEncoderAddImage(encoder.get(), second_frame.get(), kDuration, flag), + AVIF_RESULT_OK); + + testutil::AvifRwData encoded; + ASSERT_EQ(avifEncoderFinish(encoder.get(), &encoded), AVIF_RESULT_OK); + + // Make sure it decodes fine, even if unrelated to the current test. + DecoderPtr decoder(avifDecoderCreate()); + ASSERT_NE(decoder, nullptr); + ASSERT_EQ(avifDecoderSetIOMemory(decoder.get(), encoded.data, encoded.size), + AVIF_RESULT_OK); + ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); + ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); + ASSERT_GT(testutil::GetPsnr(*first_frame, *decoder->image), 32.0); + ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); + ASSERT_GT(testutil::GetPsnr(*second_frame, *decoder->image), 32.0); + + encoded_bitstream = std::vector(encoded.data, encoded.data + encoded.size); +} + +TEST(AomTuneMetricTest, TuneOptionHasSameBehaviorAsOtherCodecSpecificOptions) { + if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) == + nullptr) { + GTEST_SKIP() << "Codec unavailable, skip test."; + } + std::vector a, b; + + EncodeAnimation("tune", nullptr, nullptr, nullptr, a); + EncodeAnimation("tune", nullptr, nullptr, nullptr, b); + // Make sure the comparison works as intended for identical input. + EXPECT_EQ(a, b); + + EncodeAnimation("tune", "psnr", nullptr, nullptr, a); + EncodeAnimation("tune", nullptr, nullptr, nullptr, b); + // AOM_TUNE_PSNR is not the default. + EXPECT_NE(a, b); + + EncodeAnimation("tune", nullptr, nullptr, nullptr, a); + EncodeAnimation("tune", nullptr, nullptr, "psnr", b); + // The second frame differs. + EXPECT_NE(a, b); + + EncodeAnimation("tune", nullptr, "ssim", "psnr", a); + EncodeAnimation("tune", nullptr, nullptr, "psnr", b); + // The option is overwritten successfully. + EXPECT_EQ(a, b); + + EncodeAnimation("tune", nullptr, nullptr, nullptr, a); + EncodeAnimation("tune", nullptr, "psnr", nullptr, b); + // The pending key is successfully deleted. + EXPECT_EQ(a, b); + + EncodeAnimation("tune", "psnr", "psnr", "psnr", a); + EncodeAnimation("tune", "psnr", nullptr, nullptr, b); + // avifEncoderSetCodecSpecificOption(NULL) only deletes the *pending* key. + EXPECT_EQ(a, b); +} + +TEST(AomTuneMetricTest, TuneIqOnlySupportsAllIntra) { + if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) == + nullptr) { + GTEST_SKIP() << "Codec unavailable, skip test."; + } + + const ImagePtr image = + testutil::ReadImage(data_path, "paris_exif_xmp_icc.jpg"); + ASSERT_NE(image, nullptr); + + EncoderPtr encoder(avifEncoderCreate()); + ASSERT_NE(encoder, nullptr); + encoder->codecChoice = AVIF_CODEC_CHOICE_AOM; + ASSERT_EQ(avifEncoderSetCodecSpecificOption(encoder.get(), "tune", "iq"), + AVIF_RESULT_OK); + ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(), kDuration, + AVIF_ADD_IMAGE_FLAG_NONE), + AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION); +} + //------------------------------------------------------------------------------ } // namespace