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)) {