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;
}