Commit b742ae32 for libheif
commit b742ae32f8ef87c6ac51f993f983868763c518de
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Tue May 19 14:28:12 2026 +0200
aom: use tuneIqQualityToQuantizer[] table when using IQ tune mode (#1725)
diff --git a/libheif/plugins/encoder_aom.cc b/libheif/plugins/encoder_aom.cc
index 166c9529..a33cb0f8 100644
--- a/libheif/plugins/encoder_aom.cc
+++ b/libheif/plugins/encoder_aom.cc
@@ -197,6 +197,41 @@ static const char* const kParam_tune_valid_values[] = {
"auto", "psnr", "ssim", "iq", nullptr
};
+#if defined(AOM_HAVE_TUNE_IQ)
+// This table has been copied from libavif/src/codec_aom.c
+
+// Quality (q) to quantizer (qp) formula for tune=iq (Image Quality), expressed as a look-up table for more clarity.
+// Copied from libavif (src/codec_aom.c). The formula is a piecewise linear function empirically selected
+// to correct for the non-linear bitrate increase of tune=iq relative to tune=ssim with the same qp.
+//
+// | Quality | Quantizer | Step size |
+// |---------|------------------------------------|-----------|
+// | 0 - 6 | 63 - floor(quality / 3) | 3 |
+// | 7 - 28 | 61 - round((quality - 7) / 2) | 2 |
+// | 29 - 53 | 50 - round((quality - 29) * 3 / 5) | 1.66 |
+// | 54 - 99 | 35 - round((quality - 54) * 3 / 4) | 1.33 |
+// | 100 | 0 (lossless) | 1 |
+//
+// The x axis of the table represents the ones digit, while the y axis represents the tens digit
+// of the q value [0-100], which is then mapped to a qp value [0-63].
+// clang-format off
+static const int tuneIqQualityToQuantizer[101] = {
+// 1s digit: *0 *1 *2 *3 *4 *5 *6 *7 *8 *9 10s digit:
+ 63, 63, 63, 62, 62, 62, 61, 61, 60, 60, // 0*
+ 59, 59, 58, 58, 57, 57, 56, 56, 55, 55, // 1*
+ 54, 54, 53, 53, 52, 52, 51, 51, 50, 50, // 2*
+ 49, 49, 48, 48, 47, 46, 46, 45, 45, 44, // 3*
+ 43, 43, 42, 42, 41, 40, 40, 39, 39, 38, // 4*
+ 37, 37, 36, 36, 35, 34, 33, 33, 32, 31, // 5*
+ 30, 30, 29, 28, 27, 27, 26, 25, 24, 24, // 6*
+ 23, 22, 21, 21, 20, 19, 18, 18, 17, 16, // 7*
+ 15, 15, 14, 13, 12, 12, 11, 10, 9, 9, // 8*
+ 8, 7, 6, 6, 5, 4, 3, 3, 2, 1, // 9*
+ 0 // quality 100
+};
+// clang-format on
+#endif
+
static const int AOM_PLUGIN_PRIORITY = 60;
#define MAX_PLUGIN_NAME_LENGTH 80
@@ -1013,7 +1048,63 @@ static heif_error aom_start_sequence_encoding_intern(void* encoder_raw, const he
quality = encoder->alpha_quality;
}
- int cq_level = ((100 - quality) * 63 + 50) / 100;
+ // Fetch NCLX and determine the effective tune metric early, since the
+ // quality-to-quantizer mapping for AOM_TUNE_IQ uses a different (non-linear) table.
+
+ heif_color_profile_nclx* nclx = nullptr;
+ err = heif_image_get_nclx_color_profile(image, &nclx);
+ if (err.code != heif_error_Ok) {
+ assert(nclx == nullptr);
+ }
+
+ // make sure NCLX profile is deleted at end of function
+ auto nclx_deleter = std::unique_ptr<heif_color_profile_nclx, void (*)(heif_color_profile_nclx*)>(nclx, heif_nclx_color_profile_free);
+
+ aom_tune_metric effective_tune = encoder->tune;
+ if (encoder->tune_auto) {
+ if (image_sequence) {
+ effective_tune = AOM_TUNE_SSIM;
+ }
+ else if (input_class == heif_image_input_class_alpha) {
+ // AOM_TUNE_SSIM causes ringing on alpha; PSNR avoids that.
+ effective_tune = AOM_TUNE_PSNR;
+ }
+ else {
+ effective_tune = AOM_TUNE_SSIM;
+
+#if defined(AOM_HAVE_TUNE_IQ)
+ // AOM_TUNE_IQ is tuned for the YCbCr family of color spaces (and other YUV-like
+ // spaces such as YCgCo, ICtCp, including monochrome). It does NOT generalize to
+ // GBR samples (matrix_coefficients = IDENTITY), so we keep SSIM for that case.
+ // AOM_TUNE_IQ stabilized in libaom v3.13.0 (all-intra only); v3.14.0 added
+ // support for the good-quality and realtime inter-frame modes.
+
+ static const int aom_version_3_13_0 = (3 << 16) | (13 << 8);
+ static const int aom_version_3_14_0 = (3 << 16) | (14 << 8);
+
+ bool is_identity_matrix = nclx && (nclx->matrix_coefficients == heif_matrix_coefficients_RGB_GBR);
+ int aom_version = aom_codec_version();
+ bool iq_supports_inter = (aom_version >= aom_version_3_14_0);
+
+ if (!is_identity_matrix &&
+ (cfg.g_usage == AOM_USAGE_ALL_INTRA || iq_supports_inter) &&
+ aom_version >= aom_version_3_13_0) {
+ effective_tune = AOM_TUNE_IQ;
+ }
+#endif
+ }
+ }
+
+ int cq_level;
+#if defined(AOM_HAVE_TUNE_IQ)
+ if (effective_tune == AOM_TUNE_IQ) {
+ cq_level = tuneIqQualityToQuantizer[quality];
+ }
+ else
+#endif
+ {
+ cq_level = ((100 - quality) * 63 + 50) / 100;
+ }
// Work around the bug in libaom v2.0.2 or older fixed by
// https://aomedia-review.googlesource.com/c/aom/+/113064. If using a libaom
@@ -1071,15 +1162,6 @@ static heif_error aom_start_sequence_encoding_intern(void* encoder_raw, const he
// TODO: set AV1E_SET_TILE_ROWS and AV1E_SET_TILE_COLUMNS.
- heif_color_profile_nclx* nclx = nullptr;
- err = heif_image_get_nclx_color_profile(image, &nclx);
- if (err.code != heif_error_Ok) {
- assert(nclx == nullptr);
- }
-
- // make sure NCLX profile is deleted at end of function
- auto nclx_deleter = std::unique_ptr<heif_color_profile_nclx, void (*)(heif_color_profile_nclx*)>(nclx, heif_nclx_color_profile_free);
-
// In aom, color_range defaults to limited range (0). Set it to full range (1).
aom_error = aom_codec_control(&codec, AV1E_SET_COLOR_RANGE, nclx ? nclx->full_range_flag : 1); CHECK_ERROR;
aom_error = aom_codec_control(&codec, AV1E_SET_CHROMA_SAMPLE_POSITION, chroma_info.chroma_sample_position); CHECK_ERROR;
@@ -1092,36 +1174,6 @@ static heif_error aom_start_sequence_encoding_intern(void* encoder_raw, const he
aom_error = aom_codec_control(&codec, AV1E_SET_TRANSFER_CHARACTERISTICS, nclx->transfer_characteristics); CHECK_ERROR;
}
- aom_tune_metric effective_tune = encoder->tune;
- if (encoder->tune_auto) {
- if (input_class == heif_image_input_class_alpha) {
- // AOM_TUNE_SSIM causes ringing on alpha; PSNR avoids that.
- effective_tune = AOM_TUNE_PSNR;
- }
- else {
- effective_tune = AOM_TUNE_SSIM;
-#if defined(AOM_HAVE_TUNE_IQ)
- // AOM_TUNE_IQ is tuned for the YCbCr family of color spaces (and other YUV-like
- // spaces such as YCgCo, ICtCp, including monochrome). It does NOT generalize to
- // GBR samples (matrix_coefficients = IDENTITY), so we keep SSIM for that case.
- // AOM_TUNE_IQ stabilized in libaom v3.13.0 (all-intra only); v3.14.0 added
- // support for the good-quality and realtime inter-frame modes.
-
- static const int aom_version_3_13_0 = (3 << 16) | (13 << 8);
- static const int aom_version_3_14_0 = (3 << 16) | (14 << 8);
-
- bool is_identity_matrix = nclx && (nclx->matrix_coefficients == heif_matrix_coefficients_RGB_GBR);
- int aom_version = aom_codec_version();
- bool iq_supports_inter = (aom_version >= aom_version_3_14_0);
-
- if (!is_identity_matrix &&
- (cfg.g_usage == AOM_USAGE_ALL_INTRA || iq_supports_inter) &&
- aom_version >= aom_version_3_13_0) {
- effective_tune = AOM_TUNE_IQ;
- }
-#endif
- }
- }
aom_error = aom_codec_control(&codec, AOME_SET_TUNING, effective_tune); CHECK_ERROR;
if (encoder->lossless || (input_class == heif_image_input_class_alpha && encoder->lossless_alpha)) {