| // Copyright 2019 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // ----------------------------------------------------------------------------- |
| // |
| // AOM / AV1 wrappers |
| // |
| // Author: Skal (pascal.massimino@gmail.com) |
| |
| #include "extras/aom_utils.h" |
| |
| #include <cmath> |
| #include <cstdint> |
| #include <cstring> |
| #include <string> |
| |
| #include "examples/example_utils.h" |
| #include "extras/ccsp_imageio.h" |
| #include "imageio/imageio_util.h" |
| #include "src/common/av1_common.h" |
| #include "src/utils/utils.h" |
| #include "src/wp2/decode.h" |
| #include "src/wp2/encode.h" |
| |
| #if defined(WP2_HAVE_AOM) |
| #include "aom/aom_decoder.h" |
| #include "aom/aom_encoder.h" |
| #include "aom/aomcx.h" |
| #include "aom/aomdx.h" |
| #include "config/aom_config.h" |
| #include "config/aom_version.h" |
| |
| #if defined(WP2_HAVE_AOM_DBG) |
| #include "stats/aomstats.h" |
| // to benefit from these, one should build libaom using the config: |
| // cmake ../ -DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1 |
| #include "av1/common/common_data.h" |
| #if defined(CONFIG_ACCOUNTING) && CONFIG_ACCOUNTING |
| #include "av1/decoder/accounting.h" |
| #endif |
| #if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION |
| #include "av1/decoder/inspection.h" |
| #endif |
| #endif // defined(WP2_HAVE_AOM_DBG) |
| |
| // libgav1 can be used as decoder instead of libaom |
| #if defined(WP2_HAVE_LIBGAV1) |
| #include "gav1/decoder.h" |
| #include "gav1/status_code.h" |
| #endif // defined(WP2_HAVE_LIBGAV1) |
| |
| #include "examples/stopwatch.h" |
| |
| #if !defined(WP2_HAVE_WEBP) |
| #error "WEBP support is required by WP2_HAVE_AOM" |
| #else |
| #include "webp/decode.h" |
| #include "webp/encode.h" |
| #endif // !defined(WP2_HAVE_WEBP) |
| |
| #endif // WP2_HAVE_AOM |
| |
| #if defined(WP2_HAVE_AVIF) |
| #include "avif/avif.h" |
| #endif |
| |
| namespace WP2 { |
| namespace { |
| |
| #if defined(WP2_HAVE_AOM) |
| |
| //------------------------------------------------------------------------------ |
| |
| bool encode_frame(aom_codec_ctx_t* const codec, aom_image_t* const img, int pts, |
| aom_enc_frame_flags_t flags, std::string* const out, |
| bool report_error = true) { |
| const aom_codec_err_t err = |
| aom_codec_encode(codec, img, pts, /*duration=*/1, flags); |
| if (err != AOM_CODEC_OK) { |
| if (report_error) { |
| fprintf(stderr, "aom_codec_encode() failed ['%s']\n", |
| aom_codec_err_to_string(err)); |
| const char* const details = aom_codec_error_detail(codec); |
| if (details != nullptr) fprintf(stderr, "Details: [%s]\n", details); |
| } |
| return false; |
| } |
| |
| aom_codec_iter_t iter = NULL; |
| const aom_codec_cx_pkt_t* pkt = NULL; |
| bool got_pkts = false; |
| while ((pkt = aom_codec_get_cx_data(codec, &iter)) != NULL) { |
| got_pkts = true; |
| if (pkt->kind == AOM_CODEC_CX_FRAME_PKT) { |
| out->append((const char*)pkt->data.frame.buf, pkt->data.frame.sz); |
| } |
| } |
| return got_pkts; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| #if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION |
| void inspect_cb(void* pbi, void* data) { |
| insp_frame_data* const frame_data = (insp_frame_data*)data; |
| ifd_inspect(frame_data, pbi, /*skip_non_transform=*/0); |
| } |
| |
| // Given a 'mi_data_name', returns the value of the variable with that name in |
| // the instance 'mi' of the 'insp_mi_data' struct. |
| int16_t GetMiData(const insp_mi_data& mi, const std::string& mi_data_name) { |
| if (mi_data_name == "mode") return mi.mode; |
| if (mi_data_name == "uv_mode") return mi.uv_mode; |
| if (mi_data_name == "sb_type") return mi.bsize; |
| if (mi_data_name == "skip") return mi.skip; |
| if (mi_data_name == "segment_id") return mi.segment_id; |
| if (mi_data_name == "dual_filter_type") return mi.dual_filter_type; |
| if (mi_data_name == "filter[0]") return mi.filter[0]; |
| if (mi_data_name == "filter[1]") return mi.filter[1]; |
| if (mi_data_name == "tx_type") return mi.tx_type; |
| if (mi_data_name == "tx_size") return mi.tx_size; |
| if (mi_data_name == "cdef_level") return mi.cdef_level; |
| if (mi_data_name == "cdef_strength") return mi.cdef_strength; |
| if (mi_data_name == "cfl_alpha_idx") return mi.cfl_alpha_idx; |
| if (mi_data_name == "cfl_alpha_sign") return mi.cfl_alpha_sign; |
| if (mi_data_name == "current_qindex") return mi.current_qindex; |
| if (mi_data_name == "compound_type") return mi.compound_type; |
| if (mi_data_name == "motion_mode") return mi.motion_mode; |
| if (mi_data_name == "intrabc") return mi.intrabc; |
| if (mi_data_name == "palette") return mi.palette; |
| if (mi_data_name == "uv_palette") return mi.uv_palette; |
| assert(false); |
| return 0; |
| } |
| |
| // Draws a 3x3 red square representing the 'value' as 9 bits. |
| void DrawNumber(uint32_t value, int x, int y, ArgbBuffer* const out) { |
| for (uint32_t j = 0; j < 3; ++j) { |
| for (uint32_t i = 0; i < 3; ++i) { |
| if (x + i < out->width() && y + j < out->height()) { |
| ((uint32_t*)out->GetRow(y + j))[(x + i)] = |
| (value & 1) ? 0xffff8888u : 0xffff0000u; |
| } |
| value >>= 1; |
| } |
| } |
| } |
| |
| // Fills rectangles with uniform colors representing the values of |
| // 'mi_data_name' in 'data' blocks. |
| void DrawMiData(const insp_frame_data& data, const std::string& mi_data_name, |
| bool draw_mi_number, ArgbBuffer* const out) { |
| const uint32_t num_cols = data.mi_cols; |
| const uint32_t num_rows = data.mi_rows; |
| assert(out != nullptr); |
| |
| const auto minmax = std::minmax_element( |
| data.mi_grid, data.mi_grid + num_cols * num_rows, |
| [&](const insp_mi_data& lhs, const insp_mi_data& rhs) { |
| return (GetMiData(lhs, mi_data_name) < GetMiData(rhs, mi_data_name)); |
| }); |
| const int16_t min = GetMiData(*minmax.first, mi_data_name); |
| const int16_t max = GetMiData(*minmax.second, mi_data_name); |
| |
| // To remember already painted areas. |
| std::vector<int32_t> values(num_cols * num_rows, max + 1); |
| |
| for (uint32_t r = 0; r < num_rows && r * 4 < out->height(); ++r) { |
| for (uint32_t c = 0; c < num_cols && c * 4 < out->width(); ++c) { |
| const insp_mi_data& mi = data.mi_grid[r * num_cols + c]; |
| const int16_t mi_data = GetMiData(mi, mi_data_name); |
| |
| const uint8_t intensity = (mi_data - min) * 0xff / std::max(1, max - min); |
| const uint32_t color = 0xff000000u + 0x010101u * intensity; |
| |
| if (values[r * num_cols + c] == max + 1) { // Not yet drawn. |
| const BLOCK_SIZE bsize = (BLOCK_SIZE)mi.bsize; |
| const uint32_t w = mi_size_wide[bsize], h = mi_size_high[bsize]; |
| const uint32_t min_x = c * 4, min_y = r * 4; |
| out->Fill({min_x, min_y, w * 4, h * 4}, |
| Argb32b{0xff, intensity, intensity, intensity}); |
| |
| if (draw_mi_number) { |
| DrawNumber((uint32_t)(mi_data - min), min_x + 1, min_y + 1, out); |
| } |
| |
| for (uint32_t sub_r = r; sub_r < std::min(num_rows, r + h); ++sub_r) { |
| for (uint32_t sub_c = c; sub_c < std::min(num_cols, c + w); ++sub_c) { |
| values[sub_r * num_cols + sub_c] = mi_data; |
| } |
| } |
| } else if (mi_data != values[r * num_cols + c]) { |
| // Already drawn but non-uniform values were found within a block. |
| const uint32_t min_x = c * 4, min_y = r * 4; |
| const uint32_t max_x = std::min((c + 1) * 4, out->width()); |
| const uint32_t max_y = std::min((r + 1) * 4, out->height()); |
| |
| for (uint32_t y = min_y; y < max_y; ++y) { |
| for (uint32_t x = min_x; x < max_x; ++x) { |
| if ((x + y) & 1) continue; // Semi-transparent checkerboard. |
| (out->GetRow8(y))[x] = color; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| // Map from "qindex" to DC quantization value. |
| constexpr uint32_t kQIndexRange = 256; |
| constexpr int16_t kDcQuant[] = { |
| 4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, |
| 18, 19, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 28, |
| 29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39, |
| 40, 41, 42, 43, 43, 44, 45, 46, 47, 48, 48, 49, 50, |
| 51, 52, 53, 53, 54, 55, 56, 57, 57, 58, 59, 60, 61, |
| 62, 62, 63, 64, 65, 66, 66, 67, 68, 69, 70, 70, 71, |
| 72, 73, 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81, |
| 82, 83, 84, 85, 85, 87, 88, 90, 92, 93, 95, 96, 98, |
| 99, 101, 102, 104, 105, 107, 108, 110, 111, 113, 114, 116, 117, |
| 118, 120, 121, 123, 125, 127, 129, 131, 134, 136, 138, 140, 142, |
| 144, 146, 148, 150, 152, 154, 156, 158, 161, 164, 166, 169, 172, |
| 174, 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, 208, |
| 211, 214, 217, 220, 223, 226, 230, 233, 237, 240, 243, 247, 250, |
| 253, 257, 261, 265, 269, 272, 276, 280, 284, 288, 292, 296, 300, |
| 304, 309, 313, 317, 322, 326, 330, 335, 340, 344, 349, 354, 359, |
| 364, 369, 374, 379, 384, 389, 395, 400, 406, 411, 417, 423, 429, |
| 435, 441, 447, 454, 461, 467, 475, 482, 489, 497, 505, 513, 522, |
| 530, 539, 549, 559, 569, 579, 590, 602, 614, 626, 640, 654, 668, |
| 684, 700, 717, 736, 755, 775, 796, 819, 843, 869, 896, 925, 955, |
| 988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336, |
| }; |
| STATIC_ASSERT_ARRAY_SIZE(kDcQuant, kQIndexRange); |
| constexpr int16_t kMaxDcQuant = 1336; |
| |
| // Map from "qindex" to AC quantization value. |
| constexpr int16_t kAcQuant[] = { |
| 4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, |
| 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, |
| 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, |
| 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, |
| 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, |
| 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, |
| 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, |
| 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, |
| 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, |
| 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, |
| 152, 155, 158, 161, 164, 167, 170, 173, 176, 179, 182, 185, |
| 188, 191, 194, 197, 200, 203, 207, 211, 215, 219, 223, 227, |
| 231, 235, 239, 243, 247, 251, 255, 260, 265, 270, 275, 280, |
| 285, 290, 295, 300, 305, 311, 317, 323, 329, 335, 341, 347, |
| 353, 359, 366, 373, 380, 387, 394, 401, 408, 416, 424, 432, |
| 440, 448, 456, 465, 474, 483, 492, 501, 510, 520, 530, 540, |
| 550, 560, 571, 582, 593, 604, 615, 627, 639, 651, 663, 676, |
| 689, 702, 715, 729, 743, 757, 771, 786, 801, 816, 832, 848, |
| 864, 881, 898, 915, 933, 951, 969, 988, 1007, 1026, 1046, 1066, |
| 1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343, |
| 1369, 1396, 1423, 1451, 1479, 1508, 1537, 1567, 1597, 1628, 1660, 1692, |
| 1725, 1759, 1793, 1828, |
| }; |
| STATIC_ASSERT_ARRAY_SIZE(kAcQuant, kQIndexRange); |
| constexpr int16_t kMaxAcQuant = 1828; |
| |
| constexpr uint32_t kMaxSegments = 8; |
| |
| // Returns the qindex corresponding to a given quant value. The quant value |
| // is assumed to appear in kDcQuant or kAcQuant (depending on is_dc). |
| uint32_t GetQIndex(int16_t quant, bool is_dc) { |
| const int16_t* const lookup = (is_dc ? kDcQuant : kAcQuant); |
| for (uint32_t i = 0; i < kQIndexRange; ++i) { |
| if (lookup[i] == quant) return i; |
| } |
| assert(false); |
| return 0; |
| } |
| |
| void DrawQuant(const insp_frame_data& data, int channel, bool is_dc, |
| ArgbBuffer* const out) { |
| const uint32_t num_cols = data.mi_cols; |
| const uint32_t num_rows = data.mi_rows; |
| assert(out != nullptr); |
| |
| // Base quant values per segment. These values do not include the deltaq |
| // adjustment per superblock. |
| const int16_t(*dequant)[2] = (channel == 0) ? &data.y_dequant[0] |
| : (channel == 1) ? &data.u_dequant[0] |
| : &data.v_dequant[0]; |
| const int16_t max_quant = (is_dc ? kMaxDcQuant : kMaxAcQuant); |
| |
| // Deltaq (i.e. qindex offset) per segment. |
| int16_t segment_deltaq[kMaxSegments] = {0}; |
| for (uint32_t i = 0; i < kMaxSegments; ++i) { |
| const int16_t segment_quant = dequant[i][is_dc ? 0 : 1]; |
| if (segment_quant == 0) continue; // This segment is not used. |
| segment_deltaq[i] = GetQIndex(segment_quant, is_dc) - data.base_qindex; |
| } |
| |
| for (uint32_t r = 0; r < num_rows && r * 4 < out->height(); ++r) { |
| for (uint32_t c = 0; c < num_cols && c * 4 < out->width(); ++c) { |
| const insp_mi_data& mi = data.mi_grid[r * num_cols + c]; |
| // Base qindex for the current superblock. |
| const int16_t sb_qindex = GetMiData(mi, "current_qindex"); |
| const int16_t deltaq = segment_deltaq[GetMiData(mi, "segment_id")]; |
| // Final qindex that includes the delta from the current segment. |
| const int16_t qindex = |
| Clamp<int16_t>(sb_qindex + deltaq, 0, kQIndexRange - 1); |
| const int16_t quant = (is_dc ? kDcQuant[qindex] : kAcQuant[qindex]); |
| |
| const float normalized_quant = (float)quant / max_quant; |
| const uint8_t gray = Clamp(normalized_quant * 255.f, 0.f, 255.f); |
| const Argb32b color{255, gray, gray, gray}; |
| out->Fill({c * 4, r * 4, 4, 4}, color); |
| } |
| } |
| } |
| |
| std::vector<Rectangle> ExtractBoxes(const insp_frame_data& data, uint32_t width, |
| uint32_t height, |
| bool extract_transform_boxes) { |
| std::vector<Rectangle> boxes; |
| const uint32_t w = (uint32_t)data.mi_cols; |
| const uint32_t h = (uint32_t)data.mi_rows; |
| std::vector<bool> done(w * h, false); |
| for (uint32_t y = 0; y < h && y * 4u < height; ++y) { |
| for (uint32_t x = 0; x < w && x * 4u < width; ++x) { |
| if (done[y * w + x]) continue; |
| |
| const insp_mi_data& mi = data.mi_grid[y * w + x]; |
| const BLOCK_SIZE bsize = (BLOCK_SIZE)mi.bsize; |
| const uint32_t bw = mi_size_wide[bsize]; |
| const uint32_t bh = mi_size_high[bsize]; |
| |
| if (extract_transform_boxes) { |
| // For lossy intra luma only. See av1_loopfilter.c, get_transform_size() |
| const TX_SIZE tx_size = (TX_SIZE)mi.tx_size; |
| const uint32_t tw_px = tx_size_wide[tx_size]; |
| const uint32_t th_px = tx_size_high[tx_size]; |
| const uint32_t tw = tw_px / 4, th = th_px / 4; |
| |
| for (uint32_t ty = 0; ty < bh && (y + ty) * 4u < height; ty += th) { |
| for (uint32_t tx = 0; tx < bw && (x + tx) * 4u < width; tx += tw) { |
| const uint32_t tx_px = (x + tx) * 4u, ty_px = (y + ty) * 4u; |
| boxes.emplace_back(tx_px, ty_px, // Transform position/dimensions |
| std::min(tw_px, width - tx_px), |
| std::min(th_px, height - ty_px)); |
| } |
| } |
| } else { |
| const uint32_t x_px = x * 4u, y_px = y * 4u; |
| const uint32_t bw_px = bw * 4u, bh_px = bh * 4u; |
| boxes.emplace_back(x_px, y_px, // Block position/dimensions |
| std::min(bw_px, width - x_px), |
| std::min(bh_px, height - y_px)); |
| } |
| |
| for (uint32_t yd = y; yd < std::min(h, y + bh); ++yd) { |
| for (uint32_t xd = x; xd < std::min(w, x + bw); ++xd) { |
| done[yd * w + xd] = true; |
| } |
| } |
| } |
| } |
| return boxes; |
| } |
| |
| void DrawBoxes(const std::vector<Rectangle>& boxes, Argb32b color, |
| ArgbBuffer* const out) { |
| assert(out != nullptr); |
| for (const auto& rect : boxes) out->DrawRect(rect, color); |
| } |
| |
| #endif // CONFIG_INSPECTION |
| |
| // category id, similar to those reported by 'dwebp' |
| // clang-format off |
| static const char* const kCtgCoeffs = "coeffs"; |
| static const char* const kCtgPaletteCoeffs = "palette-coeffs"; |
| static const char* const kCtgPredModes = "pred-modes"; |
| static const char* const kCtgSegmentId = "segment-id"; |
| static const char* const kCtgHeader = "header"; |
| static const char* const kCtgPaletteHeader = "palette-header"; |
| static const char* const kCtgBlockSize = "block-size"; |
| static std::map<std::string, const char*> kCategories = { |
| // coeffs |
| {"read_coeffs_reverse_2d", kCtgCoeffs}, |
| {"av1_read_coeffs_txb", kCtgCoeffs}, |
| {"read_skip_txfm", kCtgCoeffs}, |
| {"read_coeffs_reverse", kCtgCoeffs}, |
| {"read_golomb", kCtgCoeffs}, |
| // pred-modes |
| {"read_intra_mode", kCtgPredModes}, |
| {"read_intra_mode_uv", kCtgPredModes}, |
| {"read_angle_delta", kCtgPredModes}, |
| {"read_filter_intra_mode_info", kCtgPredModes}, |
| // segment-id |
| {"read_segment_id", kCtgSegmentId}, |
| // block-size |
| {"av1_read_tx_type", kCtgBlockSize}, |
| {"read_selected_tx_size", kCtgBlockSize}, |
| {"read_partition", kCtgHeader}, |
| {"read_skip", kCtgHeader}, |
| // everything about filters |
| {"read_wiener_filter", kCtgHeader}, |
| {"loop_restoration_read_sb_coeffs", kCtgHeader}, |
| {"read_cdef", kCtgHeader}, |
| {"cfl:signs", kCtgHeader}, |
| {"cfl:alpha_u", kCtgHeader}, |
| {"cfl:alpha_v", kCtgHeader}, |
| {"read_sgrproj_filter", kCtgHeader}, |
| // palette |
| {"decode_color_map_tokens", kCtgPaletteCoeffs}, |
| {"av1_read_uniform", kCtgPaletteHeader}, |
| {"read_palette_colors_y", kCtgPaletteHeader}, |
| {"read_palette_colors_uv", kCtgPaletteHeader}, |
| {"read_palette_mode_info", kCtgPaletteHeader}, |
| }; |
| // clang-format on |
| |
| #endif // WP2_HAVE_AOM_DBG |
| |
| //------------------------------------------------------------------------------ |
| |
| #if defined(WP2_HAVE_AOM) |
| |
| // Prints the error and returns it, if any. |
| #define CHECK_AOM_STATUS(status, ctx) \ |
| do { \ |
| const aom_codec_err_t __S__ = (status); \ |
| if (__S__ != AOM_CODEC_OK) { \ |
| fprintf(stderr, "Error: %s\n", aom_codec_err_to_string(__S__)); \ |
| if ((ctx) != nullptr) { \ |
| const char* const __D__ = aom_codec_error_detail(ctx); \ |
| if (__D__ != nullptr) fprintf(stderr, "(%s)\n", __D__); \ |
| } \ |
| return MapAomStatus(__S__); \ |
| } \ |
| } while (0) |
| |
| // From twopass_encoder.c / aomenc.c |
| WP2Status wp2_get_frame_stats(aom_codec_ctx_t* ctx, const aom_image_t* img, |
| aom_codec_pts_t pts, unsigned int duration, |
| aom_enc_frame_flags_t flags, |
| aom_fixed_buf_t* stats, bool* const got_stats) { |
| *got_stats = false; |
| aom_codec_iter_t iter = NULL; |
| const aom_codec_cx_pkt_t* pkt = NULL; |
| CHECK_AOM_STATUS(aom_codec_encode(ctx, img, pts, duration, flags), ctx); |
| |
| while ((pkt = aom_codec_get_cx_data(ctx, &iter)) != NULL) { |
| *got_stats = true; |
| |
| if (pkt->kind == AOM_CODEC_STATS_PKT) { |
| const uint8_t* const pkt_buf = |
| (const uint8_t*)pkt->data.twopass_stats.buf; |
| const size_t pkt_size = pkt->data.twopass_stats.sz; |
| stats->buf = realloc(stats->buf, stats->sz + pkt_size); |
| memcpy((uint8_t*)stats->buf + stats->sz, pkt_buf, pkt_size); |
| stats->sz += pkt_size; |
| } |
| } |
| |
| return WP2_STATUS_OK; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| // Converts 'ref' image into AV1 struct 'raw'. |
| WP2Status CreateAV1Img(const ArgbBuffer& ref, aom_image_t* const raw, |
| bool use_yuv444) { |
| YUVPlane yuv; |
| WP2_CHECK_STATUS(ToYCbCr(ref, /*ycbcr_bit_depth=*/{8, /*is_signed=*/false}, |
| use_yuv444 ? nullptr : &SamplingTaps::kDownSharp, |
| &yuv)); |
| const bool is_monochrome = yuv.IsMonochrome(); |
| if (is_monochrome) { |
| use_yuv444 = false; |
| } |
| |
| const int w = ref.width(); |
| const int h = ref.height(); |
| if (!aom_img_alloc(raw, use_yuv444 ? AOM_IMG_FMT_I444 : AOM_IMG_FMT_I420, w, |
| h, /*align=*/16)) { |
| fprintf(stderr, "aom_img_alloc() failed!"); |
| return WP2_STATUS_OUT_OF_MEMORY; |
| } |
| // Match y4m format with luma in [16:235] range. |
| yuv.Y.To(raw->planes[AOM_PLANE_Y], raw->stride[AOM_PLANE_Y], 16); |
| if (is_monochrome) { |
| raw->monochrome = 1; |
| } else { |
| yuv.U.To(raw->planes[AOM_PLANE_U], raw->stride[AOM_PLANE_U], 128); |
| yuv.V.To(raw->planes[AOM_PLANE_V], raw->stride[AOM_PLANE_V], 128); |
| } |
| if (ref.HasTransparency()) { |
| fprintf(stderr, "# WARNING! Alpha channel is discarded with AV1!!\n"); |
| } |
| |
| return WP2_STATUS_OK; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| // Initializes the 'codec'. |
| WP2Status CreateCodec(int w, int h, bool is_monochrome, const ParamsAV1& params, |
| aom_enc_pass pass, aom_fixed_buf_t stats, |
| aom_codec_ctx_t* const codec) { |
| const aom_codec_iface_t* const itf = aom_codec_av1_cx(); |
| if (itf == NULL) { |
| fprintf(stderr, "aom_codec_av1_cx() failed!\n"); |
| return WP2_STATUS_INVALID_PARAMETER; |
| } |
| |
| aom_codec_enc_cfg_t cfg; |
| CHECK_AOM_STATUS(aom_codec_enc_config_default(itf, &cfg, 0), nullptr); |
| cfg.g_w = w; |
| cfg.g_h = h; |
| if (pass == AOM_RC_ONE_PASS) { |
| // Set the maximum number of frames to encode to 1. This instructs libaom to |
| // set still_picture and reduced_still_picture_header to 1 in AV1 sequence |
| // headers. In two-pass encoding the number of frames is available in the |
| // first-pass stats, so this is not necessary. |
| cfg.g_limit = 1; |
| } |
| cfg.g_timebase.num = 1; |
| cfg.g_timebase.den = 25; |
| cfg.g_bit_depth = AOM_BITS_8; |
| cfg.g_usage = |
| (params.effort == 0) ? AOM_USAGE_REALTIME : AOM_USAGE_GOOD_QUALITY; |
| cfg.g_profile = ((params.use_yuv444 && !is_monochrome) ? 1 : 0); |
| cfg.g_threads = params.threads; |
| cfg.rc_end_usage = AOM_Q; |
| cfg.g_pass = pass; |
| cfg.rc_twopass_stats_in = stats; |
| cfg.monochrome = is_monochrome; |
| |
| CHECK_AOM_STATUS(aom_codec_enc_init(codec, itf, &cfg, 0), nullptr); |
| return WP2_STATUS_OK; |
| } |
| |
| // Configures the 'codec'. |
| WP2Status ConfigureCodec(const ParamsAV1& params, |
| aom_codec_ctx_t* const codec) { |
| if (params.disable_transforms) { |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_INTRA_DCT_ONLY, 1), |
| codec); |
| } |
| if (params.disable_palette) { |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_PALETTE, 0), |
| codec); |
| } |
| if (params.disable_block_copy) { |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_INTRABC, 0), |
| codec); |
| } |
| if (params.disable_trellis) { |
| CHECK_AOM_STATUS( |
| aom_codec_control(codec, AV1E_SET_DISABLE_TRELLIS_QUANT, 1), codec); |
| } |
| |
| if (params.disable_predictors) { |
| // TODO(maryla): replace with AV1E_SET_ENABLE_DIRECTIONAL_INTRA when |
| // available |
| CHECK_AOM_STATUS( |
| aom_codec_control(codec, AV1E_SET_ENABLE_DIAGONAL_INTRA, 0), codec); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_FILTER_INTRA, 0), |
| codec); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_SMOOTH_INTRA, 0), |
| codec); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_PAETH_INTRA, 0), |
| codec); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_CFL_INTRA, 0), |
| codec); |
| } |
| |
| if (params.force_block_size > 0) { |
| WP2_CHECK_OK( |
| params.force_block_size == 4 || params.force_block_size == 8 || |
| params.force_block_size == 16 || params.force_block_size == 32 || |
| params.force_block_size == 64 || params.force_block_size == 128, |
| WP2_STATUS_INVALID_PARAMETER); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_MAX_PARTITION_SIZE, |
| params.force_block_size), |
| codec); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_MIN_PARTITION_SIZE, |
| params.force_block_size), |
| codec); |
| } |
| |
| if (params.quality > 95) { |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_LOSSLESS, 1), codec); |
| } else { |
| const int level = std::lround(63 - params.quality * 63 / 95); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AOME_SET_CQ_LEVEL, level), codec); |
| } |
| |
| // we avoid including aomcx.h to get aom_tune_metric (=moving target) directly |
| const int tuning = |
| (params.tuning == ParamsAV1::Tuning::kPsnr) ? AOM_TUNE_PSNR |
| : (params.tuning == ParamsAV1::Tuning::kSsim) ? AOM_TUNE_SSIM |
| : (params.tuning == ParamsAV1::Tuning::kVmaf) ? AOM_TUNE_VMAF_MAX_GAIN |
| : AOM_TUNE_BUTTERAUGLI; |
| CHECK_AOM_STATUS(aom_codec_control(codec, AOME_SET_TUNING, tuning), codec); |
| |
| // AV1's speed param goes opposite to WP2's |
| const int speed = std::max(8 - (int)params.effort * 8 / 9, 0); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AOME_SET_CPUUSED, speed), codec); |
| if (params.filter_strength == 0) { |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_CDEF, 0), codec); |
| CHECK_AOM_STATUS(aom_codec_control(codec, AV1E_SET_ENABLE_RESTORATION, 0), |
| codec); |
| } |
| |
| for (const auto& kv : params.extra_options) { |
| CHECK_AOM_STATUS(aom_codec_control(codec, kv.first, kv.second), codec); |
| } |
| |
| return WP2_STATUS_OK; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| constexpr int kFlags = AOM_EFLAG_FORCE_KF; |
| |
| WP2Status FirstPassAV1(const ArgbBuffer& ref, const ParamsAV1& params, |
| double* const timing, aom_fixed_buf_t* const stats) { |
| aom_image_t raw; |
| WP2_CHECK_STATUS(CreateAV1Img(ref, &raw, params.use_yuv444)); |
| Cleaner<aom_image_t> img_cleaner(&raw, aom_img_free); |
| |
| aom_codec_ctx_t codec; |
| WP2_CHECK_STATUS(CreateCodec((int)ref.width(), (int)ref.height(), |
| raw.monochrome, params, AOM_RC_FIRST_PASS, |
| /*stats=*/{NULL, 0}, &codec)); |
| Cleaner<aom_codec_ctx_t, aom_codec_err_t> codec_cleaner(&codec, |
| aom_codec_destroy); |
| WP2_CHECK_STATUS(ConfigureCodec(params, &codec)); |
| |
| const double start_time = GetStopwatchTime(); |
| bool got_stats; |
| // Calculate frame statistics. |
| WP2_CHECK_STATUS(wp2_get_frame_stats(&codec, &raw, /*pts=*/0, /*duration=*/1, |
| kFlags, stats, &got_stats)); |
| // Flush encoder. |
| do { |
| WP2_CHECK_STATUS(wp2_get_frame_stats( |
| &codec, NULL, /*pts=*/0, /*duration=*/1, kFlags, stats, &got_stats)); |
| } while (got_stats); |
| if (timing != nullptr) *timing = GetStopwatchTime() - start_time; |
| CHECK_AOM_STATUS(aom_codec_destroy(&codec), nullptr); |
| return WP2_STATUS_OK; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| // Encodes 'image' into 'bitstream'. |
| WP2Status EncodeAV1WithLibaom(const ArgbBuffer& image, const ParamsAV1& params, |
| aom_fixed_buf_t stats, |
| std::string* const bitstream, |
| double* const timing) { |
| WP2_CHECK_OK(bitstream != nullptr, WP2_STATUS_NULL_PARAMETER); |
| bitstream->clear(); |
| |
| aom_image_t raw; |
| WP2_CHECK_STATUS(CreateAV1Img(image, &raw, params.use_yuv444)); |
| Cleaner<aom_image_t> img_cleaner(&raw, aom_img_free); |
| |
| aom_codec_ctx_t codec; |
| // TODO(yguyon): Check possible leaks of itf/cfg/codec |
| WP2_CHECK_STATUS(CreateCodec( |
| (int)image.width(), (int)image.height(), raw.monochrome, params, |
| (stats.buf != NULL) ? AOM_RC_LAST_PASS : AOM_RC_ONE_PASS, stats, &codec)); |
| Cleaner<aom_codec_ctx_t, aom_codec_err_t> codec_cleaner(&codec, |
| aom_codec_destroy); |
| WP2_CHECK_STATUS(ConfigureCodec(params, &codec)); |
| const double start_time = GetStopwatchTime(); |
| encode_frame(&codec, &raw, 0, kFlags, bitstream, false); |
| while (encode_frame(&codec, NULL, -1, 0, bitstream)) { /* flush encoder */ |
| } |
| if (timing != nullptr) *timing = GetStopwatchTime() - start_time; |
| CHECK_AOM_STATUS(aom_codec_destroy(&codec), nullptr); |
| |
| return WP2_STATUS_OK; |
| } |
| |
| // Decodes 'bitstream' into 'decoded'. 'ref' is only needed when debug |
| // information (blocks, transforms, quant) is requested, in order to get the |
| // width/height. |
| WP2Status DecodeAV1WithLibaom(const ArgbBuffer& ref, const ParamsAV1& params, |
| const std::string bitstream, |
| ArgbBuffer* const decoded, double* const timing, |
| std::vector<Rectangle>* const blocks, |
| std::vector<Rectangle>* const transforms, |
| QuantAV1* const quant) { |
| (void)blocks, (void)transforms, (void)quant; |
| WP2_CHECK_OK(decoded != nullptr, WP2_STATUS_NULL_PARAMETER); |
| |
| const aom_codec_iface_t* const itf = aom_codec_av1_dx(); |
| if (itf == NULL) { |
| fprintf(stderr, "aom_codec_av1_dx() failed!\n"); |
| return WP2_STATUS_INVALID_PARAMETER; |
| } |
| |
| const aom_codec_dec_cfg cfg = {params.threads, ref.width(), ref.height(), 1}; |
| aom_codec_ctx_t codec; |
| CHECK_AOM_STATUS(aom_codec_dec_init(&codec, itf, &cfg, 0), &codec); |
| |
| CHECK_AOM_STATUS(aom_codec_control(&codec, AV1_SET_SKIP_LOOP_FILTER), &codec); |
| |
| #if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION |
| insp_frame_data frame_data; |
| ifd_init(&frame_data, ref.width(), ref.height()); |
| WP2_CHECK_OK(frame_data.mi_grid != NULL, WP2_STATUS_OUT_OF_MEMORY); |
| Cleaner<insp_frame_data> frame_data_cleaner(&frame_data, ifd_clear); |
| |
| struct aom_inspect_init inspect = {inspect_cb, &frame_data}; |
| const bool use_inspect = |
| (params.draw_mi_data != nullptr || params.draw_blocks || |
| params.draw_transforms || blocks != nullptr || transforms != nullptr || |
| quant != nullptr) && |
| (aom_codec_control(&codec, AV1_SET_INSPECTION_CALLBACK, &inspect) == |
| AOM_CODEC_OK); |
| #endif // CONFIG_INSPECTION |
| |
| Av1DecodeReturn user_priv = {nullptr, 0, 0}; // Necessary to enable inspect |
| const double start_time = GetStopwatchTime(); |
| CHECK_AOM_STATUS( |
| aom_codec_decode(&codec, (const uint8_t*)bitstream.data(), |
| bitstream.size(), &user_priv), &codec); |
| if (timing != nullptr) *timing = GetStopwatchTime() - start_time; |
| aom_image_t* img = NULL; |
| aom_codec_iter_t iter = NULL; |
| bool ok = false; |
| while ((img = aom_codec_get_frame(&codec, &iter)) != NULL) { |
| YUVPlane yuv; |
| WP2_CHECK_STATUS(yuv.Resize(img->d_w, img->d_h, /*pad=*/1, |
| /*has_alpha=*/false, !params.use_yuv444)); |
| yuv.Y.From(img->planes[AOM_PLANE_Y], img->stride[AOM_PLANE_Y], -16); |
| yuv.U.From(img->planes[AOM_PLANE_U], img->stride[AOM_PLANE_U], -128); |
| yuv.V.From(img->planes[AOM_PLANE_V], img->stride[AOM_PLANE_V], -128); |
| WP2_CHECK_STATUS(ToRGBA( |
| yuv, params.use_yuv444 ? nullptr : &SamplingTaps::kUpSmooth, decoded)); |
| ok = true; |
| break; |
| } |
| if (!ok) { |
| fprintf(stderr, "Round-trip error!\n"); |
| return WP2_STATUS_OUT_OF_MEMORY; |
| } |
| |
| #if defined(WP2_BITTRACE) && defined(CONFIG_ACCOUNTING) && CONFIG_ACCOUNTING |
| if (params.bittrace > 0) { |
| Accounting* acc = NULL; |
| if (aom_codec_control(&codec, AV1_GET_ACCOUNTING, &acc) == AOM_CODEC_OK) { |
| std::map<std::string, BitCounts::mapped_type> accum; |
| const uint32_t num_syms = acc->syms.num_syms; |
| for (uint32_t i = 0; i < num_syms; ++i) { |
| const AccountingSymbol& sym = acc->syms.syms[i]; |
| const double bits = sym.bits / 8.; |
| const char* str = acc->syms.dictionary.strs[sym.id]; |
| const uint32_t num_samples = sym.samples; |
| if (params.compact_trace) { |
| if (kCategories[str] == nullptr) { |
| fprintf(stderr, "Unmapped category! Missing line:\n"); |
| fprintf(stderr, " {\"%s\", kCtgXXX},\n", str); |
| } else { |
| str = kCategories[str]; |
| } |
| } |
| accum[str].bits += bits; |
| accum[str].num_occurrences += num_samples; |
| } |
| std::vector<TraceType> values; |
| values.reserve(accum.size()); |
| for (const auto& p : accum) values.push_back(p); |
| const bool use_bytes = (params.bittrace == 2); |
| PrintBitTraceCluster(values, true, use_bytes, /*show_histograms=*/false); |
| } else { |
| fprintf(stderr, "Error! AV1 accounting (bit traces) not compiled in\n"); |
| } |
| } |
| #endif // WP2_BITTRACE && CONFIG_ACCOUNTING |
| #if defined(CONFIG_INSPECTION) && CONFIG_INSPECTION |
| if (use_inspect) { |
| if (params.draw_mi_data != nullptr) { |
| if (std::strcmp(params.draw_mi_data, "quanty") == 0) { |
| DrawQuant(frame_data, /*channel=*/0, /*is_dc=*/true, decoded); |
| } else if (std::strcmp(params.draw_mi_data, "quantu") == 0) { |
| DrawQuant(frame_data, /*channel=*/1, /*is_dc=*/true, decoded); |
| } else if (std::strcmp(params.draw_mi_data, "quantv") == 0) { |
| DrawQuant(frame_data, /*channel=*/2, /*is_dc=*/true, decoded); |
| } else { |
| DrawMiData(frame_data, params.draw_mi_data, params.draw_mi_number, |
| decoded); |
| } |
| } |
| // Print transforms before blocks because transforms are smaller. |
| if (params.draw_transforms || transforms != nullptr) { |
| std::vector<Rectangle> t = |
| ExtractBoxes(frame_data, ref.width(), ref.height(), |
| /*extract_transform_boxes=*/true); |
| if (params.draw_transforms) { |
| DrawBoxes(t, {0xff, 0x08, 0x80, 0xf0}, decoded); |
| } |
| if (transforms != nullptr) std::swap(*transforms, t); |
| } |
| if (params.draw_blocks || blocks != nullptr) { |
| std::vector<Rectangle> b = |
| ExtractBoxes(frame_data, ref.width(), ref.height(), |
| /*extract_transform_boxes=*/false); |
| if (params.draw_blocks) DrawBoxes(b, {0xff, 0x08, 0xf0, 0x80}, decoded); |
| if (blocks != nullptr) std::swap(*blocks, b); |
| } |
| if (quant != nullptr) { |
| for (uint32_t i = 0; i < 8; ++i) { |
| for (uint32_t j = 0; j < 2; ++j) { |
| // Raw quant values are multiplied by 4. We shift them back to |
| // make them comparable with those used by wp2. |
| quant->y_dequant[i][j] = frame_data.y_dequant[i][j] >> 2; |
| quant->u_dequant[i][j] = frame_data.u_dequant[i][j] >> 2; |
| quant->v_dequant[i][j] = frame_data.v_dequant[i][j] >> 2; |
| } |
| } |
| } |
| } |
| #endif // CONFIG_INSPECTION |
| |
| CHECK_AOM_STATUS(aom_codec_destroy(&codec), &codec); |
| return WP2_STATUS_OK; |
| } |
| |
| #if defined(WP2_HAVE_LIBGAV1) |
| WP2Status ConvertStatus(::libgav1::StatusCode code) { |
| switch (code) { |
| case ::libgav1::kStatusOk: |
| return WP2_STATUS_OK; |
| case ::libgav1::kStatusUnknownError: |
| case ::libgav1::kStatusInvalidArgument: |
| case ::libgav1::kStatusAlready: |
| case ::libgav1::kStatusNothingToDequeue: |
| return WP2_STATUS_INVALID_PARAMETER; |
| case ::libgav1::kStatusOutOfMemory: |
| return WP2_STATUS_OUT_OF_MEMORY; |
| case ::libgav1::kStatusResourceExhausted: |
| return WP2_STATUS_NOT_ENOUGH_DATA; |
| case ::libgav1::kStatusNotInitialized: |
| case ::libgav1::kStatusInternalError: |
| return WP2_STATUS_INVALID_CONFIGURATION; |
| case ::libgav1::kStatusUnimplemented: |
| case ::libgav1::kStatusTryAgain: |
| return WP2_STATUS_UNSUPPORTED_FEATURE; |
| case ::libgav1::kStatusBitstreamError: |
| return WP2_STATUS_BITSTREAM_ERROR; |
| default: |
| assert(false); |
| return WP2_STATUS_LAST; |
| } |
| } |
| #endif // defined(WP2_HAVE_LIBGAV1) |
| |
| // Decodes 'bitstream' into 'decoded'. |
| WP2Status DecodeAV1WithLibgav1(const ArgbBuffer& ref, const ParamsAV1& params, |
| const std::string bitstream, |
| ArgbBuffer* const decoded, |
| double* const timing) { |
| #if defined(WP2_HAVE_LIBGAV1) |
| WP2_CHECK_OK(decoded != nullptr, WP2_STATUS_NULL_PARAMETER); |
| ::libgav1::DecoderSettings settings; |
| settings.threads = params.threads; |
| if (params.filter_strength == 0) settings.post_filter_mask = 0x0; |
| ::libgav1::Decoder decoder; |
| |
| ::libgav1::StatusCode status = decoder.Init(&settings); |
| if (status != ::libgav1::kStatusOk) { |
| fprintf(stderr, "libgav1::DecoderSettings::Init() failed: %s\n", |
| ::libgav1::GetErrorString(status)); |
| return ConvertStatus(status); |
| } |
| |
| const double start_time = GetStopwatchTime(); |
| status = decoder.EnqueueFrame( |
| reinterpret_cast<const uint8_t*>(bitstream.data()), bitstream.size(), |
| /*user_private_data=*/0, /*buffer_private_data=*/nullptr); |
| if (status != ::libgav1::kStatusOk) { |
| fprintf(stderr, "libgav1::DecoderSettings::EnqueueFrame() failed: %s\n", |
| ::libgav1::GetErrorString(status)); |
| return ConvertStatus(status); |
| } |
| |
| const ::libgav1::DecoderBuffer* buffer; |
| status = decoder.DequeueFrame(&buffer); |
| if (status != ::libgav1::kStatusOk || buffer == nullptr) { |
| fprintf(stderr, "libgav1::DecoderSettings::DequeueFrame() failed: %s\n", |
| ::libgav1::GetErrorString(status)); |
| return ConvertStatus(status); |
| } |
| if (timing != nullptr) *timing = GetStopwatchTime() - start_time; |
| |
| WP2_CHECK_OK(buffer->displayed_width[AOM_PLANE_Y] == (int)ref.width() && |
| buffer->displayed_height[AOM_PLANE_Y] == (int)ref.height(), |
| WP2_STATUS_BAD_DIMENSION); |
| WP2_CHECK_OK(buffer->image_format == kLibgav1ImageFormatYuv420 || |
| buffer->image_format == kLibgav1ImageFormatYuv444, |
| WP2_STATUS_INVALID_COLORSPACE); |
| const bool is420 = (buffer->image_format == kLibgav1ImageFormatYuv420); |
| |
| YUVPlane yuv; |
| WP2_CHECK_STATUS(yuv.Resize(ref.width(), ref.height(), /*pad=*/1, |
| /*has_alpha=*/false, is420)); |
| yuv.Y.From(buffer->plane[AOM_PLANE_Y], buffer->stride[AOM_PLANE_Y], -16); |
| yuv.U.From(buffer->plane[AOM_PLANE_U], buffer->stride[AOM_PLANE_U], -128); |
| yuv.V.From(buffer->plane[AOM_PLANE_V], buffer->stride[AOM_PLANE_V], -128); |
| WP2_CHECK_STATUS(ToRGBA( |
| yuv, (is420 && params.use_yuv444) ? nullptr : &SamplingTaps::kUpSmooth, |
| decoded)); |
| |
| return WP2_STATUS_OK; |
| #else |
| (void)ref, (void)params, (void)bitstream, (void)decoded, (void)timing; |
| return WP2_STATUS_UNSUPPORTED_FEATURE; |
| #endif // defined(WP2_HAVE_LIBGAV1) |
| } |
| |
| // Encodes 'ref' into 'out', decodes it into 'decoded'. |
| WP2Status CompressAV1(const ArgbBuffer& ref, const ParamsAV1& params, |
| aom_fixed_buf_t stats, ArgbBuffer* const decoded, |
| std::string* const out, double timing[], |
| std::vector<Rectangle>* const blocks, |
| std::vector<Rectangle>* const transforms, |
| QuantAV1* const quant) { |
| if (aom_codec_version() != VERSION_PACKED) { |
| fprintf(stderr, "Version mismatch! (%x vs %x)\n", |
| aom_codec_version(), VERSION_PACKED); |
| fprintf(stderr, "Please check includes and lib locations.\n"); |
| return WP2_STATUS_VERSION_MISMATCH; |
| } |
| WP2_CHECK_STATUS(EncodeAV1WithLibaom( |
| ref, params, stats, out, (timing != nullptr) ? &timing[0] : nullptr)); |
| |
| WP2_CHECK_STATUS(decoded->Resize(ref.width(), ref.height())); |
| if (params.use_libgav1_decoder) { |
| WP2_CHECK_STATUS( |
| DecodeAV1WithLibgav1(ref, params, *out, decoded, |
| (timing != nullptr) ? &timing[1] : nullptr)); |
| } else { |
| WP2_CHECK_STATUS(DecodeAV1WithLibaom( |
| ref, params, *out, decoded, (timing != nullptr) ? &timing[1] : nullptr, |
| blocks, transforms, quant)); |
| } |
| return WP2_STATUS_OK; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| #endif // defined(WP2_HAVE_AOM) |
| |
| } // namespace |
| |
| //------------------------------------------------------------------------------ |
| |
| #if defined(WP2_HAVE_AOM) |
| |
| WP2Status CompressAV1(const ArgbBuffer& ref, const ParamsAV1& params, |
| ArgbBuffer* const decoded, std::string* const out, |
| double timing[2], std::vector<Rectangle>* const blocks, |
| std::vector<Rectangle>* const transforms, |
| QuantAV1* const quant) { |
| if (params.pass < 2) { |
| return CompressAV1(ref, params, /*stats=*/{NULL, 0}, decoded, out, timing, |
| blocks, transforms, quant); |
| } else { |
| aom_fixed_buf_t stats = {NULL, 0}; |
| double first_pass_timing; |
| WP2_CHECK_STATUS(FirstPassAV1(ref, params, &first_pass_timing, &stats)); |
| WP2_CHECK_STATUS(CompressAV1(ref, params, stats, decoded, out, timing, |
| blocks, transforms, quant)); |
| if (timing != nullptr) timing[0] += first_pass_timing; |
| free(stats.buf); |
| return WP2_STATUS_OK; |
| } |
| } |
| |
| WP2Status DecodeAV1WithLibaom(const std::string& bitstream, |
| ArgbBuffer* const decoded) { |
| return DecodeAV1WithLibaom(ArgbBuffer(), ParamsAV1(), bitstream, decoded, |
| /*timing=*/nullptr, /*blocks=*/nullptr, |
| /*transforms=*/nullptr, /*quant=*/nullptr); |
| } |
| |
| #else |
| |
| WP2Status CompressAV1(const ArgbBuffer&, const ParamsAV1&, ArgbBuffer* const, |
| std::string* const, double[2], |
| std::vector<Rectangle>* const, |
| std::vector<Rectangle>* const, QuantAV1* const) { |
| fprintf(stderr, "CompressAV1: AOM support not enabled at compile time.\n"); |
| return WP2_STATUS_UNSUPPORTED_FEATURE; |
| } |
| |
| WP2Status DecodeAV1WithLibaom(const std::string&, ArgbBuffer* const) { |
| fprintf(stderr, |
| "DecodeAV1WithLibaom: AOM support not enabled at compile time.\n"); |
| return WP2_STATUS_UNSUPPORTED_FEATURE; |
| } |
| |
| #endif // defined(WP2_HAVE_AOM) |
| |
| //------------------------------------------------------------------------------ |
| // AVIF |
| |
| #if defined(WP2_HAVE_AVIF) |
| |
| static float ToAvifQuantizer(float quality) { |
| // Note AVIF_QUANTIZER_LOSSLESS doesn't actually seem lossless. |
| if (quality > 95) return AVIF_QUANTIZER_LOSSLESS; |
| return AVIF_QUANTIZER_WORST_QUALITY + |
| quality * |
| (AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY) / 100; |
| } |
| |
| WP2Status CompressAVIF(const ArgbBuffer& ref, const ParamsAV1& params, |
| ArgbBuffer* const decoded, std::string* const out, |
| double timing[2]) { |
| WP2_CHECK_OK(decoded != nullptr, WP2_STATUS_NULL_PARAMETER); |
| WP2_CHECK_OK(out != nullptr, WP2_STATUS_NULL_PARAMETER); |
| out->clear(); |
| |
| YUVPlane yuv; |
| WP2_CHECK_STATUS( |
| ToYCbCr(ref, /*ycbcr_bit_depth=*/{8, /*is_signed=*/false}, |
| params.use_yuv444 ? nullptr : &SamplingTaps::kDownSharp, &yuv)); |
| const bool is_monochrome = yuv.IsMonochrome(); |
| |
| const uint32_t w = ref.width(); |
| const uint32_t h = ref.height(); |
| constexpr int kDepth = 8; |
| const avifPixelFormat format = is_monochrome ? AVIF_PIXEL_FORMAT_YUV400 |
| : params.use_yuv444 ? AVIF_PIXEL_FORMAT_YUV444 |
| : AVIF_PIXEL_FORMAT_YUV420; |
| avifImage* const image = avifImageCreate(w, h, kDepth, format); |
| WP2_CHECK_ALLOC_OK(image != nullptr); |
| Cleaner<avifImage> image_cleaner(image, avifImageDestroy); |
| |
| const bool has_alpha = ref.HasTransparency(); |
| if (has_alpha) { |
| avifImageAllocatePlanes(image, AVIF_PLANES_YUV | AVIF_PLANES_A); |
| } else { |
| avifImageAllocatePlanes(image, AVIF_PLANES_YUV); |
| } |
| |
| // import into yuv |
| yuv.Y.To(image->yuvPlanes[AVIF_CHAN_Y], image->yuvRowBytes[AVIF_CHAN_Y]); |
| if (!is_monochrome) { |
| yuv.U.To(image->yuvPlanes[AVIF_CHAN_U], image->yuvRowBytes[AVIF_CHAN_U], |
| 128); |
| yuv.V.To(image->yuvPlanes[AVIF_CHAN_V], image->yuvRowBytes[AVIF_CHAN_V], |
| 128); |
| } |
| if (has_alpha) { |
| WP2_CHECK_STATUS(yuv.A.Resize(w, h)); |
| for (uint32_t y = 0; y < ref.height(); ++y) { |
| auto* const row_yuv = yuv.A.Row(y); |
| const uint8_t* const row = ref.GetRow8(y); |
| for (uint32_t x = 0; x < ref.width(); ++x) { |
| const uint8_t* argb = &row[x * WP2FormatBpp(ref.format())]; |
| row_yuv[x] = argb[0]; |
| } |
| } |
| yuv.A.To(image->alphaPlane, image->alphaRowBytes); |
| } |
| |
| // Encode. |
| avifRWData avif_enc = AVIF_DATA_EMPTY; |
| Cleaner<avifRWData> avif_enc_cleaner(&avif_enc, avifRWDataFree); |
| avifEncoder* const encoder = avifEncoderCreate(); |
| WP2_CHECK_ALLOC_OK(encoder != nullptr); |
| Cleaner<avifEncoder> encoder_cleaner(encoder, avifEncoderDestroy); |
| encoder->maxThreads = params.threads; |
| // Scale to go from AVIF_QUANTIZER_BEST_QUALITY to |
| // AVIF_QUANTIZER_WORST_QUALITY. |
| encoder->minQuantizer = ToAvifQuantizer(params.quality); |
| encoder->maxQuantizer = ToAvifQuantizer(params.quality); |
| encoder->minQuantizerAlpha = ToAvifQuantizer(params.alpha_quality); |
| encoder->maxQuantizerAlpha = ToAvifQuantizer(params.alpha_quality); |
| encoder->speed = (int)AVIF_SPEED_FASTEST + |
| ((int)AVIF_SPEED_SLOWEST - (int)AVIF_SPEED_FASTEST) * |
| (int)params.effort / 9; |
| double start_time = GetStopwatchTime(); |
| const avifResult encodeResult = avifEncoderWrite(encoder, image, &avif_enc); |
| if (timing != nullptr) timing[0] = GetStopwatchTime() - start_time; |
| if (encodeResult == AVIF_RESULT_OK) { |
| // output contains a valid .avif file's contents |
| *out = std::string(reinterpret_cast<char*>(avif_enc.data), avif_enc.size); |
| } else { |
| fprintf(stderr, "ERROR: Failed to encode: %s\n", |
| avifResultToString(encodeResult)); |
| } |
| |
| // Decode back. |
| avifImage* const avif_dec = avifImageCreateEmpty(); |
| avifDecoder* const decoder = avifDecoderCreate(); |
| start_time = GetStopwatchTime(); |
| const avifResult decodeResult = |
| avifDecoderReadMemory(decoder, avif_dec, avif_enc.data, avif_enc.size); |
| if (timing != nullptr) timing[1] = GetStopwatchTime() - start_time; |
| avifDecoderDestroy(decoder); |
| if (decodeResult == AVIF_RESULT_OK) { |
| // re-use 'yuv' plane |
| yuv.Y.From(avif_dec->yuvPlanes[AVIF_CHAN_Y], |
| avif_dec->yuvRowBytes[AVIF_CHAN_Y]); |
| if (avif_dec->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { |
| yuv.U.Fill(0); |
| yuv.V.Fill(0); |
| } else { |
| yuv.U.From(avif_dec->yuvPlanes[AVIF_CHAN_U], |
| avif_dec->yuvRowBytes[AVIF_CHAN_U], -128); |
| yuv.V.From(avif_dec->yuvPlanes[AVIF_CHAN_V], |
| avif_dec->yuvRowBytes[AVIF_CHAN_V], -128); |
| } |
| if (has_alpha) { |
| yuv.A.From(avif_dec->alphaPlane, avif_dec->alphaRowBytes); |
| } |
| WP2_CHECK_STATUS(ToRGBA(yuv, &SamplingTaps::kUpSmooth, decoded)); |
| } else { |
| fprintf(stderr, "ERROR: Failed to decode: %s\n", |
| avifResultToString(decodeResult)); |
| } |
| |
| return WP2_STATUS_OK; |
| } |
| |
| #else |
| |
| WP2Status CompressAVIF(const ArgbBuffer& ref, const ParamsAV1& params, |
| ArgbBuffer* const decoded, std::string* const out, |
| double timing[2]) { |
| (void)ref; |
| (void)params; |
| (void)decoded; |
| (void)out; |
| (void)timing; |
| fprintf(stderr, "CompressAVIF: AVIF support not enabled at compile time.\n"); |
| return WP2_STATUS_UNSUPPORTED_FEATURE; |
| } |
| |
| #endif // WP2_HAVE_AVIF |
| |
| //------------------------------------------------------------------------------ |
| |
| // Mapping from aom option names to aome_enc_control_id enum values. |
| static const std::pair<const char*, int> kAomOptions[] = { |
| {"use-reference", 7}, |
| {"roi-map", 8}, |
| {"activemap", 9}, |
| {"scalemode", 11}, |
| {"spatial-layer-id", 12}, |
| {"cpuused", 13}, |
| {"enableautoaltref", 14}, |
| {"sharpness", 16}, |
| {"static-threshold", 17}, |
| {"get-last-quantizer", 19}, |
| {"get-last-quantizer-64", 20}, |
| {"arnr-maxframes", 21}, |
| {"arnr-strength", 22}, |
| {"tuning", 24}, |
| {"cq-level", 25}, |
| {"max-intra-bitrate-pct", 26}, |
| {"number-spatial-layers", 27}, |
| {"max-inter-bitrate-pct", 28}, |
| {"gf-cbr-boost-pct", 29}, |
| {"lossless", 31}, |
| {"row-mt", 32}, |
| {"tile-columns", 33}, |
| {"tile-rows", 34}, |
| {"enable-tpl-model", 35}, |
| {"enable-keyframe-filtering", 36}, |
| {"frame-parallel-decoding", 37}, |
| {"error-resilient-mode", 38}, |
| {"s-frame-mode", 39}, |
| {"aq-mode", 40}, |
| {"frame-periodic-boost", 41}, |
| {"noise-sensitivity", 42}, |
| {"tune-content", 43}, |
| {"cdf-update-mode", 44}, |
| {"color-primaries", 45}, |
| {"transfer-characteristics", 46}, |
| {"matrix-coefficients", 47}, |
| {"chroma-sample-position", 48}, |
| {"min-gf-interval", 49}, |
| {"max-gf-interval", 50}, |
| {"get-activemap", 51}, |
| {"color-range", 52}, |
| {"render-size", 53}, |
| {"target-seq-level-idx", 54}, |
| {"get-seq-level-idx", 55}, |
| {"superblock-size", 56}, |
| {"enableautobwdref", 57}, |
| {"enable-cdef", 58}, |
| {"enable-restoration", 59}, |
| {"force-video-mode", 60}, |
| {"enable-obmc", 61}, |
| {"disable-trellis-quant", 62}, |
| {"enable-qm", 63}, |
| {"qm-min", 64}, |
| {"qm-max", 65}, |
| {"qm-y", 66}, |
| {"qm-u", 67}, |
| {"qm-v", 68}, |
| {"num-tg", 70}, |
| {"mtu", 71}, |
| {"enable-rect-partitions", 73}, |
| {"enable-ab-partitions", 74}, |
| {"enable-1to4-partitions", 75}, |
| {"min-partition-size", 76}, |
| {"max-partition-size", 77}, |
| {"enable-intra-edge-filter", 78}, |
| {"enable-order-hint", 79}, |
| {"enable-tx64", 80}, |
| {"enable-flip-idtx", 81}, |
| {"enable-rect-tx", 82}, |
| {"enable-dist-wtd-comp", 83}, |
| {"enable-ref-frame-mvs", 84}, |
| {"allow-ref-frame-mvs", 85}, |
| {"enable-dual-filter", 86}, |
| {"enable-chroma-deltaq", 87}, |
| {"enable-masked-comp", 88}, |
| {"enable-onesided-comp", 89}, |
| {"enable-interintra-comp", 90}, |
| {"enable-smooth-interintra", 91}, |
| {"enable-diff-wtd-comp", 92}, |
| {"enable-interinter-wedge", 93}, |
| {"enable-interintra-wedge", 94}, |
| {"enable-global-motion", 95}, |
| {"enable-warped-motion", 96}, |
| {"allow-warped-motion", 97}, |
| {"enable-filter-intra", 98}, |
| {"enable-smooth-intra", 99}, |
| {"enable-paeth-intra", 100}, |
| {"enable-cfl-intra", 101}, |
| {"enable-superres", 102}, |
| {"enable-overlay", 103}, |
| {"enable-palette", 104}, |
| {"enable-intrabc", 105}, |
| {"enable-angle-delta", 106}, |
| {"deltaq-mode", 107}, |
| {"deltalf-mode", 108}, |
| {"single-tile-decoding", 109}, |
| {"enable-motion-vector-unit-test", 110}, |
| {"timing-info-type", 111}, |
| {"film-grain-test-vector", 112}, |
| {"film-grain-table", 113}, |
| {"denoise-noise-level", 114}, |
| {"denoise-block-size", 115}, |
| {"chroma-subsampling-x", 116}, |
| {"chroma-subsampling-y", 117}, |
| {"reduced-tx-type-set", 118}, |
| {"intra-dct-only", 119}, |
| {"inter-dct-only", 120}, |
| {"intra-default-tx-only", 121}, |
| {"quant-b-adapt", 122}, |
| {"gf-max-pyramid-height", 123}, |
| {"max-reference-frames", 124}, |
| {"reduced-reference-set", 125}, |
| {"coeff-cost-upd-freq", 126}, |
| {"mode-cost-upd-freq", 127}, |
| {"mv-cost-upd-freq", 128}, |
| {"tier-mask", 129}, |
| {"min-cr", 130}, |
| {"svc-layer-id", 131}, |
| {"svc-params", 132}, |
| {"svc-ref-frame-config", 133}, |
| {"vmaf-model-path", 134}, |
| {"enable-ext-tile-debug", 135}, |
| {"enable-sb-multipass-unit-test", 136}, |
| {"gf-min-pyramid-height", 137}, |
| {"vbr-corpus-complexity-lap", 138}, |
| {"get-baseline-gf-interval", 139}, |
| {"enable-dnl-denoising", 140}, |
| {"enable-diagonal-intra", 141}, |
| {"dv-cost-upd-freq", 142}, |
| {"partition-info-path", 143}, |
| {"external-partition", 144}, |
| {"enable-directional-intra", 145}, |
| {"enable-tx-size-search", 146}, |
| }; |
| |
| bool ParseLibaomOption(const std::string& str, int* const option_enum, |
| int* const option_value) { |
| std::string option_name; |
| const size_t pos = str.find('='); |
| if (pos == std::string::npos) { // Assume value is 1. |
| option_name = str; |
| *option_value = 1; |
| } else { |
| option_name = str.substr(0, pos); |
| *option_value = std::stoi(str.substr(pos + 1, std::string::npos).c_str()); |
| } |
| *option_enum = -1; |
| for (const auto& pair : kAomOptions) { |
| if (!strcmp(option_name.c_str(), pair.first)) { |
| *option_enum = pair.second; |
| break; |
| } |
| } |
| if (*option_enum == -1) { |
| fprintf(stderr, "Unknown AOM option '%s'. Must be one of:\n", |
| option_name.c_str()); |
| for (const auto& pair : kAomOptions) { |
| fprintf(stderr, "%s ", pair.first); |
| } |
| fprintf(stderr, "\n"); |
| return false; |
| } |
| return true; |
| } |
| |
| //------------------------------------------------------------------------------ |
| // IVF |
| |
| namespace { |
| |
| static void WriteLittleEndian16(uint16_t v, char* output) { |
| uint8_t* mem = (uint8_t*)output; |
| |
| mem[0] = (uint8_t)((v >> 0) & 0xff); |
| mem[1] = (uint8_t)((v >> 8) & 0xff); |
| } |
| |
| static void WriteLittleEndian32(uint32_t v, char* output) { |
| uint8_t* mem = (uint8_t*)output; |
| |
| mem[0] = (uint8_t)((v >> 0) & 0xff); |
| mem[1] = (uint8_t)((v >> 8) & 0xff); |
| mem[2] = (uint8_t)((v >> 16) & 0xff); |
| mem[3] = (uint8_t)((v >> 24) & 0xff); |
| } |
| |
| // Ivf file header. |
| // See https://wiki.multimedia.cx/index.php/IVF |
| std::string IvfFileHeader(uint32_t width, uint32_t height, |
| uint32_t timebase_denum, uint32_t timebase_num, |
| uint32_t fourcc, uint32_t num_frames) { |
| std::string header; |
| header.resize(32); |
| header[0] = 'D'; |
| header[1] = 'K'; |
| header[2] = 'I'; |
| header[3] = 'F'; |
| WriteLittleEndian16(0, &header[0] + 4); // version |
| WriteLittleEndian16(32, &header[0] + 6); // header size |
| WriteLittleEndian32(fourcc, &header[0] + 8); // codec fourcc |
| WriteLittleEndian16(width, &header[0] + 12); // width in pixels |
| WriteLittleEndian16(height, &header[0] + 14); // height in pixels |
| WriteLittleEndian32(timebase_denum, &header[0] + 16); // timebase denominator |
| WriteLittleEndian32(timebase_num, &header[0] + 20); // timebase numerator |
| WriteLittleEndian32(num_frames, &header[0] + 24); // number of frames in file |
| WriteLittleEndian32(0, &header[0] + 28); // unused |
| return header; |
| } |
| |
| // Ivf frame header. |
| // See https://wiki.multimedia.cx/index.php/IVF |
| // 'timestamp' is a 64-bit presentation timestamp in unit timebase, where |
| // timebase = timebase_num/timebase_denum (in file header). |
| // 'frame_size' is the frame data size in bytes. |
| std::string IvfFrameHeader(uint64_t timestamp, uint32_t frame_size) { |
| std::string header; |
| header.resize(12); |
| WriteLittleEndian32(frame_size, &header[0]); |
| WriteLittleEndian32((uint32_t)(timestamp & 0xFFFFFFFF), &header[0] + 4); |
| WriteLittleEndian32((uint32_t)(timestamp >> 32), &header[0] + 8); |
| return header; |
| } |
| |
| constexpr int kAv1Fourcc = 0x31305641; |
| |
| } // namespace |
| |
| std::string IvfStillImageHeader(uint32_t width, uint32_t height, |
| uint32_t bitstream_size) { |
| return IvfFileHeader(width, height, /*timebase_denum=*/1, /*timebase_num=*/0, |
| kAv1Fourcc, /*num_frames=*/0) + |
| IvfFrameHeader(/*timestamp=*/0, bitstream_size); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| std::string WP2::ParamsAV1::ToString() const { |
| const char* kTuningStr[] = {"PSNR", "SSIM", "VMAF", "BUTTERAUGLI"}; |
| std::string str; |
| str += WP2SPrint("quality = %f\n", quality); |
| str += WP2SPrint("alpha_quality = %f\n", alpha_quality); |
| str += WP2SPrint("size = %d\n", size); |
| str += WP2SPrint("effort = %d\n", effort); |
| str += WP2SPrint("threads = %d\n", threads); |
| str += WP2SPrint("pass = %d\n", pass); |
| str += WP2SPrint("use_yuv444 = %d\n", use_yuv444); |
| str += WP2SPrint("filter_strength = %f\n", filter_strength); |
| str += WP2SPrint("tuning = %d\n", kTuningStr[(int)tuning]); |
| str += WP2SPrint("bittrace = %d\n", bittrace); |
| str += WP2SPrint("compact_trace = %d\n", compact_trace); |
| str += WP2SPrint("draw_blocks = %d\n", draw_blocks); |
| str += WP2SPrint("draw_transforms = %d\n", draw_transforms); |
| str += WP2SPrint("draw_mi_data = %s\n", draw_mi_data ? draw_mi_data : "---"); |
| str += WP2SPrint("draw_mi_number = %d\n", draw_mi_number); |
| str += WP2SPrint("use_libgav1_decoder = %d\n", use_libgav1_decoder); |
| str += WP2SPrint("disable_transforms = %d\n", disable_transforms); |
| str += WP2SPrint("extra_options = %d option(s)\n", extra_options.size()); |
| for (const auto& kv : extra_options) { |
| for (const auto& str_to_enum : kAomOptions) { |
| if (str_to_enum.second == kv.first) { |
| str += WP2SPrint(" - %s = %d\n", str_to_enum.first, kv.second); |
| break; |
| } |
| } |
| } |
| return str; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| } // namespace WP2 |