struct heif_error aom_encode_image()

in libheif/plugins/encoder_aom.cc [734:1124]


struct heif_error aom_encode_image(void* encoder_raw, const struct heif_image* image,
                                   heif_image_input_class input_class)
{
  struct encoder_struct_aom* encoder = (struct encoder_struct_aom*) encoder_raw;

  struct heif_error err;

  const int source_width = heif_image_get_width(image, heif_channel_Y);
  const int source_height = heif_image_get_height(image, heif_channel_Y);

  const heif_chroma chroma = heif_image_get_chroma_format(image);

  int bpp_y = heif_image_get_bits_per_pixel_range(image, heif_channel_Y);


  // --- check for AOM 3.6.0 bug

  bool is_aom_3_6_0 = (aom_codec_version() == 0x030600);

  if (is_aom_3_6_0) {
    // This bound might be too tight, as I still could encode images with 8193 x 4353 correctly. Even 8200x4400, but 8200x4800 fails.
    // Let's still keep it as most images will be smaller anyway.
    if (!(source_width <= 8192 * 2 && source_height <= 4352 * 2 && source_width * source_height <= 8192 * 4352)) {
      err = {heif_error_Encoding_error,
             heif_suberror_Encoder_encoding,
             "AOM v3.6.0 has a bug when encoding large images. Please upgrade to at least AOM v3.6.1."};
      return err;
    }
  }


  // --- copy libheif image to aom image

  aom_image_t input_image;

  aom_img_fmt_t img_format = AOM_IMG_FMT_NONE;

  int chroma_height = 0;
  int chroma_sample_position = AOM_CSP_UNKNOWN;

  switch (chroma) {
    case heif_chroma_420:
    case heif_chroma_monochrome:
      img_format = AOM_IMG_FMT_I420;
      chroma_height = (source_height+1)/2;
      chroma_sample_position = AOM_CSP_UNKNOWN; // TODO: change this to CSP_CENTER in the future (https://github.com/AOMediaCodec/av1-avif/issues/88)
      break;
    case heif_chroma_422:
      img_format = AOM_IMG_FMT_I422;
      chroma_height = (source_height+1)/2;
      chroma_sample_position = AOM_CSP_COLOCATED;
      break;
    case heif_chroma_444:
      img_format = AOM_IMG_FMT_I444;
      chroma_height = source_height;
      chroma_sample_position = AOM_CSP_COLOCATED;
      break;
    default:
      img_format = AOM_IMG_FMT_NONE;
      chroma_sample_position = AOM_CSP_UNKNOWN;
      assert(false);
      break;
  }

  if (bpp_y > 8) {
    img_format = (aom_img_fmt_t) (img_format | AOM_IMG_FMT_HIGHBITDEPTH);
  }

  if (!aom_img_alloc(&input_image, img_format,
                     source_width, source_height, 1)) {
    err = {heif_error_Memory_allocation_error,
           heif_suberror_Unspecified,
           "Failed to allocate image"};
    return err;
  }


  for (int plane = 0; plane < 3; plane++) {
    unsigned char* buf = input_image.planes[plane];
    const int stride = input_image.stride[plane];

    if (chroma == heif_chroma_monochrome && plane != 0) {
      if (bpp_y == 8) {
        memset(buf, 128, chroma_height * stride);
      }
      else {
        uint16_t* buf16 = (uint16_t*) buf;
        uint16_t half_range = (uint16_t) (1 << (bpp_y - 1));
        for (int i = 0; i < chroma_height * stride / 2; i++) {
          buf16[i] = half_range;
        }
      }

      continue;
    }

    /*
    const int w = aom_img_plane_width(img, plane) *
                  ((img->fmt & AOM_IMG_FMT_HIGHBITDEPTH) ? 2 : 1);
    const int h = aom_img_plane_height(img, plane);
    */

    int in_stride = 0;
    const uint8_t* in_p = heif_image_get_plane_readonly(image, (heif_channel) plane, &in_stride);

    int w = source_width;
    int h = source_height;

    if (plane != 0) {
      if (chroma != heif_chroma_444) { w = (w + 1) / 2; }
      if (chroma == heif_chroma_420) { h = (h + 1) / 2; }

      assert(w == heif_image_get_width(image, (heif_channel) plane));
      assert(h == heif_image_get_height(image, (heif_channel) plane));
    }

    if (bpp_y > 8) {
      w *= 2;
    }

    for (int y = 0; y < h; y++) {
      memcpy(buf, &in_p[y * in_stride], w);
      buf += stride;
    }
  }



  // --- configure codec

  aom_codec_iface_t* iface;
  aom_codec_ctx_t codec;

  iface = aom_codec_av1_cx();
  //encoder->encoder = get_aom_encoder_by_name("av1");
  if (!iface) {
    err = {heif_error_Unsupported_feature,
           heif_suberror_Unsupported_codec,
           "Unsupported codec: AOMedia Project AV1 Encoder"};
    return err;
  }


#if defined(AOM_USAGE_ALL_INTRA)
  // aom 3.1.0
  unsigned int aomUsage = AOM_USAGE_ALL_INTRA;
#else
  // aom 2.0
  unsigned int aomUsage = AOM_USAGE_GOOD_QUALITY;
#endif
  if (encoder->realtime_mode) {
    aomUsage = AOM_USAGE_REALTIME;
  }

  aom_codec_enc_cfg_t cfg;
  aom_codec_err_t res = aom_codec_enc_config_default(iface, &cfg, aomUsage);
  if (res) {
    err = {heif_error_Encoder_plugin_error,
           heif_suberror_Encoder_initialization,
           kError_codec_enc_config_default};
    return err;
  }

  int seq_profile = compute_avif_profile(heif_image_get_bits_per_pixel_range(image, heif_channel_Y),
                                         heif_image_get_chroma_format(image));

  cfg.g_w = source_width;
  cfg.g_h = source_height;
  // Set the max number of frames to encode to 1. This makes the libaom encoder
  // set still_picture and reduced_still_picture_header to 1 in the AV1 sequence
  // header OBU.
  cfg.g_limit = 1;

  // Use the default settings of the new AOM_USAGE_ALL_INTRA (added in
  // https://crbug.com/aomedia/2959).
  //
  // Set g_lag_in_frames to 0 to reduce the number of frame buffers (from 20
  // to 2) in libaom's lookahead structure. This reduces memory consumption when
  // encoding a single image.
  cfg.g_lag_in_frames = 0;
  // Disable automatic placement of key frames by the encoder.
  cfg.kf_mode = AOM_KF_DISABLED;
  // Tell libaom that all frames will be key frames.
  cfg.kf_max_dist = 0;

  cfg.g_profile = seq_profile;
  cfg.g_bit_depth = (aom_bit_depth_t) bpp_y;
  cfg.g_input_bit_depth = bpp_y;

  cfg.rc_end_usage = AOM_Q;

  int min_q = encoder->min_q;
  int max_q = encoder->max_q;

  if (input_class == heif_image_input_class_alpha && encoder->alpha_min_q_set && encoder->alpha_max_q_set) {
      min_q = encoder->alpha_min_q;
      max_q = encoder->alpha_max_q;
  }

  int quality = encoder->quality;

  if (input_class == heif_image_input_class_alpha && encoder->alpha_quality_set) {
      quality = encoder->alpha_quality;
  }

  int 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
  // release with the bug, set cfg.rc_min_quantizer to cq_level to prevent
  // libaom from incorrectly using a quantizer index lower than cq_level.
  bool aom_2_0_2_or_older = aom_codec_version() <= 0x020002;

  cfg.rc_min_quantizer = aom_2_0_2_or_older ? cq_level : min_q;
  cfg.rc_max_quantizer = max_q;
  cfg.g_error_resilient = 0;
  cfg.g_threads = encoder->threads;

  if (chroma == heif_chroma_monochrome) {
    cfg.monochrome = 1;
  }

  // --- initialize codec

  aom_codec_flags_t encoder_flags = 0;
  if (bpp_y > 8) {
    encoder_flags = (aom_codec_flags_t) (encoder_flags | AOM_CODEC_USE_HIGHBITDEPTH);
  }

  if (aom_codec_enc_init(&codec, iface, &cfg, encoder_flags)) {
    // AOM makes sure that the error text returned by aom_codec_error_detail() is always a static
    // text that is valid even through the codec allocation failed (#788).
    err = {heif_error_Encoder_plugin_error,
           heif_suberror_Encoder_initialization,
           encoder->set_aom_error(aom_codec_error_detail(&codec))};
    return err;
  }

  aom_codec_control(&codec, AOME_SET_CPUUSED, encoder->cpu_used);

  aom_codec_control(&codec, AOME_SET_CQ_LEVEL, cq_level);

  if (encoder->threads > 1) {
#if defined(AOM_CTRL_AV1E_SET_ROW_MT)
    // aom 2.0
    aom_codec_control(&codec, AV1E_SET_ROW_MT, 1);
#endif
  }

  // TODO: set AV1E_SET_TILE_ROWS and AV1E_SET_TILE_COLUMNS.


  struct heif_color_profile_nclx* nclx = nullptr;
  err = heif_image_get_nclx_color_profile(image, &nclx);
  if (err.code != heif_error_Ok) {
    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_codec_control(&codec, AV1E_SET_COLOR_RANGE, nclx ? nclx->full_range_flag : 1);
  aom_codec_control(&codec, AV1E_SET_CHROMA_SAMPLE_POSITION, chroma_sample_position);

  if (nclx &&
      (input_class == heif_image_input_class_normal ||
       input_class == heif_image_input_class_thumbnail)) {
    aom_codec_control(&codec, AV1E_SET_COLOR_PRIMARIES, nclx->color_primaries);
    aom_codec_control(&codec, AV1E_SET_MATRIX_COEFFICIENTS, nclx->matrix_coefficients);
    aom_codec_control(&codec, AV1E_SET_TRANSFER_CHARACTERISTICS, nclx->transfer_characteristics);
  }

  aom_codec_control(&codec, AOME_SET_TUNING, encoder->tune);

  if (encoder->lossless || (input_class == heif_image_input_class_alpha && encoder->lossless_alpha)) {
    aom_codec_control(&codec, AV1E_SET_LOSSLESS, 1);
  }

#if defined(AOM_CTRL_AV1E_SET_SKIP_POSTPROC_FILTERING)
  if (cfg.g_usage == AOM_USAGE_ALL_INTRA) {
    // Enable AV1E_SET_SKIP_POSTPROC_FILTERING for still-picture encoding,
    // which is disabled by default.
    aom_codec_control(&codec, AV1E_SET_SKIP_POSTPROC_FILTERING, 1);
  }
#endif

#if defined(HAVE_AOM_CODEC_SET_OPTION)
  // Apply the custom AOM encoder options.
  // These should always be applied last as they can override the values that were set above.
  for (const auto& p : encoder->custom_options) {
    aom_codec_set_option(&codec, p.name.c_str(), p.value.c_str());
  }
#endif

  // --- encode frame

  res = aom_codec_encode(&codec, &input_image,
                         0, // only encoding a single frame
                         1,
                         0); // no flags

  // Note: we are freeing the input image directly after use.
  // This covers the usual success case and also all error cases that occur below.
  aom_img_free(&input_image);

  if (res != AOM_CODEC_OK) {
    err = {
        heif_error_Encoder_plugin_error,
        heif_suberror_Encoder_encoding,
        encoder->set_aom_error(aom_codec_error_detail(&codec))
    };
    aom_codec_destroy(&codec);
    return err;
  }

  encoder->compressedData.clear();
  const aom_codec_cx_pkt_t* pkt = NULL;
  aom_codec_iter_t iter = NULL; // for extracting the compressed packets

  while ((pkt = aom_codec_get_cx_data(&codec, &iter)) != NULL) {

    if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) {
      //std::cerr.write((char*)pkt->data.frame.buf, pkt->data.frame.sz);

      //printf("packet of size: %d\n",(int)pkt->data.frame.sz);


      // TODO: split the received data into separate OBUs
      // This allows libheif to easily extract the sequence header for the av1C header

      size_t n = pkt->data.frame.sz;
      size_t oldSize = encoder->compressedData.size();
      encoder->compressedData.resize(oldSize + n);

      memcpy(encoder->compressedData.data() + oldSize,
             pkt->data.frame.buf,
             n);

      encoder->data_read = false;
    }
  }

  int flags = 0;
  res = aom_codec_encode(&codec, NULL, -1, 0, flags);
  if (res != AOM_CODEC_OK) {
    err = {heif_error_Encoder_plugin_error,
           heif_suberror_Encoder_encoding,
           encoder->set_aom_error(aom_codec_error_detail(&codec))};
    aom_codec_destroy(&codec);
    return err;
  }

  iter = NULL;

  while ((pkt = aom_codec_get_cx_data(&codec, &iter)) != NULL) {

    if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) {
      //std::cerr.write((char*)pkt->data.frame.buf, pkt->data.frame.sz);

      //printf("packet of size: %d\n",(int)pkt->data.frame.sz);


      // TODO: split the received data into separate OBUs
      // This allows libheif to easily extract the sequence header for the av1C header

      size_t n = pkt->data.frame.sz;
      size_t oldSize = encoder->compressedData.size();
      encoder->compressedData.resize(oldSize + n);

      memcpy(encoder->compressedData.data() + oldSize,
             pkt->data.frame.buf,
             n);

      encoder->data_read = false;
    }
  }


  // --- clean up

  if (aom_codec_destroy(&codec)) {
    // Note: do not call aom_codec_error_detail(), because it is not set in aom_codec_destroy(). (see #788)
    err = {heif_error_Encoder_plugin_error,
           heif_suberror_Encoder_cleanup,
           kError_undefined_error};
    return err;
  }

  return heif_error_ok;
}