Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions src/codec_aom.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
120 changes: 120 additions & 0 deletions tests/gtest/aviftunetest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t>& 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<uint8_t> 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
Expand Down
Loading