| /* |
| * Copyright (c) 2022, Alliance for Open Media. All rights reserved |
| * |
| * This source code is subject to the terms of the BSD 2 Clause License and |
| * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License |
| * was not distributed with this source code in the LICENSE file, you can |
| * obtain it at www.aomedia.org/license/software. If the Alliance for Open |
| * Media Patent License 1.0 was not distributed with this source code in the |
| * PATENTS file, you can obtain it at www.aomedia.org/license/patent. |
| */ |
| #include "av1/ratectrl_qmode.h" |
| |
| #include <algorithm> |
| #include <cassert> |
| #include <climits> |
| #include <functional> |
| #include <numeric> |
| #include <sstream> |
| #include <vector> |
| |
| #include "aom/aom_codec.h" |
| #include "av1/encoder/pass2_strategy.h" |
| #include "av1/encoder/tpl_model.h" |
| |
| namespace aom { |
| |
| // This is used before division to ensure that the divisor isn't zero or |
| // too close to zero. |
| static double ModifyDivisor(double divisor) { |
| const double kEpsilon = 0.0000001; |
| return (divisor < 0 ? std::min(divisor, -kEpsilon) |
| : std::max(divisor, kEpsilon)); |
| } |
| |
| GopFrame GopFrameInvalid() { |
| GopFrame gop_frame = {}; |
| gop_frame.is_valid = false; |
| gop_frame.coding_idx = -1; |
| gop_frame.order_idx = -1; |
| return gop_frame; |
| } |
| |
| void SetGopFrameByType(GopFrameType gop_frame_type, GopFrame *gop_frame) { |
| gop_frame->update_type = gop_frame_type; |
| switch (gop_frame_type) { |
| case GopFrameType::kRegularKey: |
| gop_frame->is_key_frame = 1; |
| gop_frame->is_arf_frame = 0; |
| gop_frame->is_show_frame = 1; |
| gop_frame->is_golden_frame = 1; |
| gop_frame->encode_ref_mode = EncodeRefMode::kRegular; |
| break; |
| case GopFrameType::kRegularGolden: |
| gop_frame->is_key_frame = 0; |
| gop_frame->is_arf_frame = 0; |
| gop_frame->is_show_frame = 1; |
| gop_frame->is_golden_frame = 1; |
| gop_frame->encode_ref_mode = EncodeRefMode::kRegular; |
| break; |
| case GopFrameType::kRegularArf: |
| gop_frame->is_key_frame = 0; |
| gop_frame->is_arf_frame = 1; |
| gop_frame->is_show_frame = 0; |
| gop_frame->is_golden_frame = 1; |
| gop_frame->encode_ref_mode = EncodeRefMode::kRegular; |
| break; |
| case GopFrameType::kIntermediateArf: |
| gop_frame->is_key_frame = 0; |
| gop_frame->is_arf_frame = 1; |
| gop_frame->is_show_frame = 0; |
| gop_frame->is_golden_frame = 0; |
| gop_frame->encode_ref_mode = EncodeRefMode::kRegular; |
| break; |
| case GopFrameType::kRegularLeaf: |
| gop_frame->is_key_frame = 0; |
| gop_frame->is_arf_frame = 0; |
| gop_frame->is_show_frame = 1; |
| gop_frame->is_golden_frame = 0; |
| gop_frame->encode_ref_mode = EncodeRefMode::kRegular; |
| break; |
| case GopFrameType::kIntermediateOverlay: |
| gop_frame->is_key_frame = 0; |
| gop_frame->is_arf_frame = 0; |
| gop_frame->is_show_frame = 1; |
| gop_frame->is_golden_frame = 0; |
| gop_frame->encode_ref_mode = EncodeRefMode::kShowExisting; |
| break; |
| case GopFrameType::kOverlay: |
| gop_frame->is_key_frame = 0; |
| gop_frame->is_arf_frame = 0; |
| gop_frame->is_show_frame = 1; |
| gop_frame->is_golden_frame = 0; |
| gop_frame->encode_ref_mode = EncodeRefMode::kOverlay; |
| break; |
| } |
| } |
| |
| GopFrame GopFrameBasic(int global_coding_idx_offset, |
| int global_order_idx_offset, int coding_idx, |
| int order_idx, int depth, int display_idx, |
| GopFrameType gop_frame_type) { |
| GopFrame gop_frame = {}; |
| gop_frame.is_valid = true; |
| gop_frame.coding_idx = coding_idx; |
| gop_frame.order_idx = order_idx; |
| gop_frame.display_idx = display_idx; |
| gop_frame.global_coding_idx = global_coding_idx_offset + coding_idx; |
| gop_frame.global_order_idx = global_order_idx_offset + order_idx; |
| SetGopFrameByType(gop_frame_type, &gop_frame); |
| gop_frame.colocated_ref_idx = -1; |
| gop_frame.update_ref_idx = -1; |
| gop_frame.layer_depth = depth + kLayerDepthOffset; |
| return gop_frame; |
| } |
| |
| // This function create gop frames with indices of display order from |
| // order_start to order_end - 1. The function will recursively introduce |
| // intermediate ARF untill maximum depth is met or the number of regular frames |
| // in between two ARFs are less than 3. Than the regular frames will be added |
| // into the gop_struct. |
| void ConstructGopMultiLayer(GopStruct *gop_struct, |
| RefFrameManager *ref_frame_manager, int max_depth, |
| int depth, int order_start, int order_end) { |
| GopFrame gop_frame; |
| int num_frames = order_end - order_start; |
| const int global_coding_idx_offset = gop_struct->global_coding_idx_offset; |
| const int global_order_idx_offset = gop_struct->global_order_idx_offset; |
| // If there are less than kMinIntervalToAddArf frames, stop introducing ARF |
| if (depth < max_depth && num_frames >= kMinIntervalToAddArf) { |
| int order_mid = (order_start + order_end) / 2; |
| // intermediate ARF |
| gop_frame = GopFrameBasic( |
| global_coding_idx_offset, global_order_idx_offset, |
| static_cast<int>(gop_struct->gop_frame_list.size()), order_mid, depth, |
| gop_struct->display_tracker, GopFrameType::kIntermediateArf); |
| ref_frame_manager->UpdateRefFrameTable(&gop_frame); |
| gop_struct->gop_frame_list.push_back(gop_frame); |
| ConstructGopMultiLayer(gop_struct, ref_frame_manager, max_depth, depth + 1, |
| order_start, order_mid); |
| // show existing intermediate ARF |
| gop_frame = |
| GopFrameBasic(global_coding_idx_offset, global_order_idx_offset, |
| static_cast<int>(gop_struct->gop_frame_list.size()), |
| order_mid, max_depth, gop_struct->display_tracker, |
| GopFrameType::kIntermediateOverlay); |
| ref_frame_manager->UpdateRefFrameTable(&gop_frame); |
| gop_struct->gop_frame_list.push_back(gop_frame); |
| ++gop_struct->display_tracker; |
| ConstructGopMultiLayer(gop_struct, ref_frame_manager, max_depth, depth + 1, |
| order_mid + 1, order_end); |
| } else { |
| // regular frame |
| for (int i = order_start; i < order_end; ++i) { |
| gop_frame = GopFrameBasic( |
| global_coding_idx_offset, global_order_idx_offset, |
| static_cast<int>(gop_struct->gop_frame_list.size()), i, max_depth, |
| gop_struct->display_tracker, GopFrameType::kRegularLeaf); |
| ref_frame_manager->UpdateRefFrameTable(&gop_frame); |
| gop_struct->gop_frame_list.push_back(gop_frame); |
| ++gop_struct->display_tracker; |
| } |
| } |
| } |
| |
| GopStruct ConstructGop(RefFrameManager *ref_frame_manager, int show_frame_count, |
| bool has_key_frame, int global_coding_idx_offset, |
| int global_order_idx_offset) { |
| GopStruct gop_struct; |
| gop_struct.show_frame_count = show_frame_count; |
| gop_struct.global_coding_idx_offset = global_coding_idx_offset; |
| gop_struct.global_order_idx_offset = global_order_idx_offset; |
| int order_start = 0; |
| int order_end = show_frame_count - 1; |
| |
| // TODO(jingning): Re-enable the use of pyramid coding structure. |
| bool has_arf_frame = show_frame_count > kMinIntervalToAddArf; |
| |
| gop_struct.display_tracker = 0; |
| |
| GopFrame gop_frame; |
| if (has_key_frame) { |
| const int key_frame_depth = -1; |
| ref_frame_manager->Reset(); |
| gop_frame = GopFrameBasic( |
| global_coding_idx_offset, global_order_idx_offset, |
| static_cast<int>(gop_struct.gop_frame_list.size()), order_start, |
| key_frame_depth, gop_struct.display_tracker, GopFrameType::kRegularKey); |
| ref_frame_manager->UpdateRefFrameTable(&gop_frame); |
| gop_struct.gop_frame_list.push_back(gop_frame); |
| order_start++; |
| ++gop_struct.display_tracker; |
| } |
| |
| const int arf_depth = 0; |
| if (has_arf_frame) { |
| // Use multi-layer pyrmaid coding structure. |
| gop_frame = GopFrameBasic( |
| global_coding_idx_offset, global_order_idx_offset, |
| static_cast<int>(gop_struct.gop_frame_list.size()), order_end, |
| arf_depth, gop_struct.display_tracker, GopFrameType::kRegularArf); |
| ref_frame_manager->UpdateRefFrameTable(&gop_frame); |
| gop_struct.gop_frame_list.push_back(gop_frame); |
| ConstructGopMultiLayer(&gop_struct, ref_frame_manager, |
| ref_frame_manager->ForwardMaxSize(), arf_depth + 1, |
| order_start, order_end); |
| // Overlay |
| gop_frame = |
| GopFrameBasic(global_coding_idx_offset, global_order_idx_offset, |
| static_cast<int>(gop_struct.gop_frame_list.size()), |
| order_end, ref_frame_manager->ForwardMaxSize(), |
| gop_struct.display_tracker, GopFrameType::kOverlay); |
| ref_frame_manager->UpdateRefFrameTable(&gop_frame); |
| gop_struct.gop_frame_list.push_back(gop_frame); |
| ++gop_struct.display_tracker; |
| } else { |
| // Use IPPP format. |
| for (int i = order_start; i <= order_end; ++i) { |
| gop_frame = GopFrameBasic( |
| global_coding_idx_offset, global_order_idx_offset, |
| static_cast<int>(gop_struct.gop_frame_list.size()), i, arf_depth + 1, |
| gop_struct.display_tracker, GopFrameType::kRegularLeaf); |
| ref_frame_manager->UpdateRefFrameTable(&gop_frame); |
| gop_struct.gop_frame_list.push_back(gop_frame); |
| ++gop_struct.display_tracker; |
| } |
| } |
| |
| return gop_struct; |
| } |
| |
| Status AV1RateControlQMode::SetRcParam(const RateControlParam &rc_param) { |
| std::ostringstream error_message; |
| if (rc_param.max_gop_show_frame_count < |
| std::max(4, rc_param.min_gop_show_frame_count)) { |
| error_message << "max_gop_show_frame_count (" |
| << rc_param.max_gop_show_frame_count |
| << ") must be at least 4 and may not be less than " |
| "min_gop_show_frame_count (" |
| << rc_param.min_gop_show_frame_count << ")"; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| if (rc_param.ref_frame_table_size < 1 || rc_param.ref_frame_table_size > 8) { |
| error_message << "ref_frame_table_size (" << rc_param.ref_frame_table_size |
| << ") must be in the range [1, 8]."; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| if (rc_param.max_ref_frames < 1 || rc_param.max_ref_frames > 7) { |
| error_message << "max_ref_frames (" << rc_param.max_ref_frames |
| << ") must be in the range [1, 7]."; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| if (rc_param.base_q_index < 0 || rc_param.base_q_index > 255) { |
| error_message << "base_q_index (" << rc_param.base_q_index |
| << ") must be in the range [0, 255]."; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| if (rc_param.frame_width < 16 || rc_param.frame_width > 16384 || |
| rc_param.frame_height < 16 || rc_param.frame_height > 16384) { |
| error_message << "frame_width (" << rc_param.frame_width |
| << ") and frame_height (" << rc_param.frame_height |
| << ") must be in the range [16, 16384]."; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| rc_param_ = rc_param; |
| return { AOM_CODEC_OK, "" }; |
| } |
| |
| // Threshold for use of the lagging second reference frame. High second ref |
| // usage may point to a transient event like a flash or occlusion rather than |
| // a real scene cut. |
| // We adapt the threshold based on number of frames in this key-frame group so |
| // far. |
| static double GetSecondRefUsageThreshold(int frame_count_so_far) { |
| const int adapt_upto = 32; |
| const double min_second_ref_usage_thresh = 0.085; |
| const double second_ref_usage_thresh_max_delta = 0.035; |
| if (frame_count_so_far >= adapt_upto) { |
| return min_second_ref_usage_thresh + second_ref_usage_thresh_max_delta; |
| } |
| return min_second_ref_usage_thresh + |
| ((double)frame_count_so_far / (adapt_upto - 1)) * |
| second_ref_usage_thresh_max_delta; |
| } |
| |
| // Slide show transition detection. |
| // Tests for case where there is very low error either side of the current frame |
| // but much higher just for this frame. This can help detect key frames in |
| // slide shows even where the slides are pictures of different sizes. |
| // Also requires that intra and inter errors are very similar to help eliminate |
| // harmful false positives. |
| // It will not help if the transition is a fade or other multi-frame effect. |
| static bool DetectSlideTransition(const FIRSTPASS_STATS &this_frame, |
| const FIRSTPASS_STATS &last_frame, |
| const FIRSTPASS_STATS &next_frame) { |
| // Intra / Inter threshold very low |
| constexpr double kVeryLowII = 1.5; |
| // Clean slide transitions we expect a sharp single frame spike in error. |
| constexpr double kErrorSpike = 5.0; |
| |
| // TODO(angiebird): Understand the meaning of these conditions. |
| return (this_frame.intra_error < (this_frame.coded_error * kVeryLowII)) && |
| (this_frame.coded_error > (last_frame.coded_error * kErrorSpike)) && |
| (this_frame.coded_error > (next_frame.coded_error * kErrorSpike)); |
| } |
| |
| // Check if there is a significant intra/inter error change between the current |
| // frame and its neighbor. If so, we should further test whether the current |
| // frame should be a key frame. |
| static bool DetectIntraInterErrorChange(const FIRSTPASS_STATS &this_stats, |
| const FIRSTPASS_STATS &last_stats, |
| const FIRSTPASS_STATS &next_stats) { |
| // Minimum % intra coding observed in first pass (1.0 = 100%) |
| constexpr double kMinIntraLevel = 0.25; |
| // Minimum ratio between the % of intra coding and inter coding in the first |
| // pass after discounting neutral blocks (discounting neutral blocks in this |
| // way helps catch scene cuts in clips with very flat areas or letter box |
| // format clips with image padding. |
| constexpr double kIntraVsInterRatio = 2.0; |
| |
| const double modified_pcnt_inter = |
| this_stats.pcnt_inter - this_stats.pcnt_neutral; |
| const double pcnt_intra_min = |
| std::max(kMinIntraLevel, kIntraVsInterRatio * modified_pcnt_inter); |
| |
| // In real scene cuts there is almost always a sharp change in the intra |
| // or inter error score. |
| constexpr double kErrorChangeThreshold = 0.4; |
| const double last_this_error_ratio = |
| fabs(last_stats.coded_error - this_stats.coded_error) / |
| ModifyDivisor(this_stats.coded_error); |
| |
| const double this_next_error_ratio = |
| fabs(last_stats.intra_error - this_stats.intra_error) / |
| ModifyDivisor(this_stats.intra_error); |
| |
| // Maximum threshold for the relative ratio of intra error score vs best |
| // inter error score. |
| constexpr double kThisIntraCodedErrorRatioMax = 1.9; |
| const double this_intra_coded_error_ratio = |
| this_stats.intra_error / ModifyDivisor(this_stats.coded_error); |
| |
| // For real scene cuts we expect an improvment in the intra inter error |
| // ratio in the next frame. |
| constexpr double kNextIntraCodedErrorRatioMin = 3.5; |
| const double next_intra_coded_error_ratio = |
| next_stats.intra_error / ModifyDivisor(next_stats.coded_error); |
| |
| double pcnt_intra = 1.0 - this_stats.pcnt_inter; |
| return pcnt_intra > pcnt_intra_min && |
| this_intra_coded_error_ratio < kThisIntraCodedErrorRatioMax && |
| (last_this_error_ratio > kErrorChangeThreshold || |
| this_next_error_ratio > kErrorChangeThreshold || |
| next_intra_coded_error_ratio > kNextIntraCodedErrorRatioMin); |
| } |
| |
| // Check whether the candidate can be a key frame. |
| // This is a rewrite of test_candidate_kf(). |
| static bool TestCandidateKey(const FirstpassInfo &first_pass_info, |
| int candidate_key_idx, int frames_since_prev_key) { |
| const auto &stats_list = first_pass_info.stats_list; |
| const int stats_count = static_cast<int>(stats_list.size()); |
| if (candidate_key_idx + 1 >= stats_count || candidate_key_idx - 1 < 0) { |
| return false; |
| } |
| const auto &last_stats = stats_list[candidate_key_idx - 1]; |
| const auto &this_stats = stats_list[candidate_key_idx]; |
| const auto &next_stats = stats_list[candidate_key_idx + 1]; |
| |
| if (frames_since_prev_key < 3) return false; |
| const double second_ref_usage_threshold = |
| GetSecondRefUsageThreshold(frames_since_prev_key); |
| if (this_stats.pcnt_second_ref >= second_ref_usage_threshold) return false; |
| if (next_stats.pcnt_second_ref >= second_ref_usage_threshold) return false; |
| |
| // Hard threshold where the first pass chooses intra for almost all blocks. |
| // In such a case even if the frame is not a scene cut coding a key frame |
| // may be a good option. |
| constexpr double kVeryLowInterThreshold = 0.05; |
| if (this_stats.pcnt_inter < kVeryLowInterThreshold || |
| DetectSlideTransition(this_stats, last_stats, next_stats) || |
| DetectIntraInterErrorChange(this_stats, last_stats, next_stats)) { |
| double boost_score = 0.0; |
| double decay_accumulator = 1.0; |
| |
| // We do "-1" because the candidate key is not counted. |
| int stats_after_this_stats = stats_count - candidate_key_idx - 1; |
| |
| // Number of frames required to test for scene cut detection |
| constexpr int kSceneCutKeyTestIntervalMax = 16; |
| |
| // Make sure we have enough stats after the candidate key. |
| const int frames_to_test_after_candidate_key = |
| std::min(kSceneCutKeyTestIntervalMax, stats_after_this_stats); |
| |
| // Examine how well the key frame predicts subsequent frames. |
| int i; |
| for (i = 1; i <= frames_to_test_after_candidate_key; ++i) { |
| // Get the next frame details |
| const auto &stats = stats_list[candidate_key_idx + i]; |
| |
| // Cumulative effect of decay in prediction quality. |
| if (stats.pcnt_inter > 0.85) { |
| decay_accumulator *= stats.pcnt_inter; |
| } else { |
| decay_accumulator *= (0.85 + stats.pcnt_inter) / 2.0; |
| } |
| |
| constexpr double kBoostFactor = 12.5; |
| double next_iiratio = |
| (kBoostFactor * stats.intra_error / ModifyDivisor(stats.coded_error)); |
| next_iiratio = std::min(next_iiratio, 128.0); |
| double boost_score_increment = decay_accumulator * next_iiratio; |
| |
| // Keep a running total. |
| boost_score += boost_score_increment; |
| |
| // Test various breakout clauses. |
| // TODO(any): Test of intra error should be normalized to an MB. |
| // TODO(angiebird): Investigate the following questions. |
| // Question 1: next_iiratio (intra_error / coded_error) * kBoostFactor |
| // We know intra_error / coded_error >= 1 and kBoostFactor = 12.5, |
| // therefore, (intra_error / coded_error) * kBoostFactor will always |
| // greater than 1.5. Is "next_iiratio < 1.5" always false? |
| // Question 2: Similar to question 1, is "next_iiratio < 3.0" always true? |
| // Question 3: Why do we need to divide 200 with num_mbs_16x16? |
| if ((stats.pcnt_inter < 0.05) || (next_iiratio < 1.5) || |
| (((stats.pcnt_inter - stats.pcnt_neutral) < 0.20) && |
| (next_iiratio < 3.0)) || |
| (boost_score_increment < 3.0) || |
| (stats.intra_error < |
| (200.0 / static_cast<double>(first_pass_info.num_mbs_16x16)))) { |
| break; |
| } |
| } |
| |
| // If there is tolerable prediction for at least the next 3 frames then |
| // break out else discard this potential key frame and move on |
| const int count_for_tolerable_prediction = 3; |
| if (boost_score > 30.0 && (i > count_for_tolerable_prediction)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Compute key frame location from first_pass_info. |
| std::vector<int> GetKeyFrameList(const FirstpassInfo &first_pass_info) { |
| std::vector<int> key_frame_list; |
| key_frame_list.push_back(0); // The first frame is always a key frame |
| int candidate_key_idx = 1; |
| while (candidate_key_idx < |
| static_cast<int>(first_pass_info.stats_list.size())) { |
| const int frames_since_prev_key = candidate_key_idx - key_frame_list.back(); |
| // Check for a scene cut. |
| const bool scenecut_detected = TestCandidateKey( |
| first_pass_info, candidate_key_idx, frames_since_prev_key); |
| if (scenecut_detected) { |
| key_frame_list.push_back(candidate_key_idx); |
| } |
| ++candidate_key_idx; |
| } |
| return key_frame_list; |
| } |
| |
| // initialize GF_GROUP_STATS |
| static void InitGFStats(GF_GROUP_STATS *gf_stats) { |
| gf_stats->gf_group_err = 0.0; |
| gf_stats->gf_group_raw_error = 0.0; |
| gf_stats->gf_group_skip_pct = 0.0; |
| gf_stats->gf_group_inactive_zone_rows = 0.0; |
| |
| gf_stats->mv_ratio_accumulator = 0.0; |
| gf_stats->decay_accumulator = 1.0; |
| gf_stats->zero_motion_accumulator = 1.0; |
| gf_stats->loop_decay_rate = 1.0; |
| gf_stats->last_loop_decay_rate = 1.0; |
| gf_stats->this_frame_mv_in_out = 0.0; |
| gf_stats->mv_in_out_accumulator = 0.0; |
| gf_stats->abs_mv_in_out_accumulator = 0.0; |
| |
| gf_stats->avg_sr_coded_error = 0.0; |
| gf_stats->avg_pcnt_second_ref = 0.0; |
| gf_stats->avg_new_mv_count = 0.0; |
| gf_stats->avg_wavelet_energy = 0.0; |
| gf_stats->avg_raw_err_stdev = 0.0; |
| gf_stats->non_zero_stdev_count = 0; |
| } |
| |
| static int FindRegionIndex(const std::vector<REGIONS> ®ions, int frame_idx) { |
| for (int k = 0; k < static_cast<int>(regions.size()); k++) { |
| if (regions[k].start <= frame_idx && regions[k].last >= frame_idx) { |
| return k; |
| } |
| } |
| return -1; |
| } |
| |
| // This function detects a flash through the high relative pcnt_second_ref |
| // score in the frame following a flash frame. The offset passed in should |
| // reflect this. |
| static bool DetectFlash(const std::vector<FIRSTPASS_STATS> &stats_list, |
| int index) { |
| int next_index = index + 1; |
| if (next_index >= static_cast<int>(stats_list.size())) return false; |
| const FIRSTPASS_STATS &next_frame = stats_list[next_index]; |
| |
| // What we are looking for here is a situation where there is a |
| // brief break in prediction (such as a flash) but subsequent frames |
| // are reasonably well predicted by an earlier (pre flash) frame. |
| // The recovery after a flash is indicated by a high pcnt_second_ref |
| // compared to pcnt_inter. |
| return next_frame.pcnt_second_ref > next_frame.pcnt_inter && |
| next_frame.pcnt_second_ref >= 0.5; |
| } |
| |
| #define MIN_SHRINK_LEN 6 |
| |
| // This function takes in a suggesting gop interval from cur_start to cur_last, |
| // analyzes firstpass stats and region stats and then return a better gop cut |
| // location. |
| // TODO(b/231517281): Simplify the indices once we have an unit test. |
| // We are using four indices here, order_index, cur_start, cur_last, and |
| // frames_since_key. Ideally, only three indices are needed. |
| // 1) start_index = order_index + cur_start |
| // 2) end_index = order_index + cur_end |
| // 3) key_index |
| int FindBetterGopCut(const std::vector<FIRSTPASS_STATS> &stats_list, |
| const std::vector<REGIONS> ®ions_list, |
| int min_gop_show_frame_count, int max_gop_show_frame_count, |
| int order_index, int cur_start, int cur_last, |
| int frames_since_key) { |
| // only try shrinking if interval smaller than active_max_gf_interval |
| if (cur_last - cur_start > max_gop_show_frame_count || |
| cur_start >= cur_last) { |
| return cur_last; |
| } |
| int num_regions = static_cast<int>(regions_list.size()); |
| int num_stats = static_cast<int>(stats_list.size()); |
| const int min_shrink_int = std::max(MIN_SHRINK_LEN, min_gop_show_frame_count); |
| |
| // find the region indices of where the first and last frame belong. |
| int k_start = FindRegionIndex(regions_list, cur_start + frames_since_key); |
| int k_last = FindRegionIndex(regions_list, cur_last + frames_since_key); |
| if (cur_start + frames_since_key == 0) k_start = 0; |
| |
| int scenecut_idx = -1; |
| // See if we have a scenecut in between |
| for (int r = k_start + 1; r <= k_last; r++) { |
| if (regions_list[r].type == SCENECUT_REGION && |
| regions_list[r].last - frames_since_key - cur_start > |
| min_gop_show_frame_count) { |
| scenecut_idx = r; |
| break; |
| } |
| } |
| |
| // if the found scenecut is very close to the end, ignore it. |
| if (scenecut_idx >= 0 && |
| regions_list[num_regions - 1].last - regions_list[scenecut_idx].last < |
| 4) { |
| scenecut_idx = -1; |
| } |
| |
| if (scenecut_idx != -1) { |
| // If we have a scenecut, then stop at it. |
| // TODO(bohanli): add logic here to stop before the scenecut and for |
| // the next gop start from the scenecut with GF |
| int is_minor_sc = |
| (regions_list[scenecut_idx].avg_cor_coeff * |
| (1 - stats_list[order_index + regions_list[scenecut_idx].start - |
| frames_since_key] |
| .noise_var / |
| regions_list[scenecut_idx].avg_intra_err) > |
| 0.6); |
| cur_last = |
| regions_list[scenecut_idx].last - frames_since_key - !is_minor_sc; |
| } else { |
| int is_last_analysed = |
| (k_last == num_regions - 1) && |
| (cur_last + frames_since_key == regions_list[k_last].last); |
| int not_enough_regions = |
| k_last - k_start <= 1 + (regions_list[k_start].type == SCENECUT_REGION); |
| // if we are very close to the end, then do not shrink since it may |
| // introduce intervals that are too short |
| if (!(is_last_analysed && not_enough_regions)) { |
| const double arf_length_factor = 0.1; |
| double best_score = 0; |
| int best_j = -1; |
| const int first_frame = regions_list[0].start - frames_since_key; |
| const int last_frame = |
| regions_list[num_regions - 1].last - frames_since_key; |
| // score of how much the arf helps the whole GOP |
| double base_score = 0.0; |
| // Accumulate base_score in |
| for (int j = cur_start + 1; j < cur_start + min_shrink_int; j++) { |
| if (order_index + j >= num_stats) break; |
| base_score = (base_score + 1.0) * stats_list[order_index + j].cor_coeff; |
| } |
| int met_blending = 0; // Whether we have met blending areas before |
| int last_blending = 0; // Whether the previous frame if blending |
| for (int j = cur_start + min_shrink_int; j <= cur_last; j++) { |
| if (order_index + j >= num_stats) break; |
| base_score = (base_score + 1.0) * stats_list[order_index + j].cor_coeff; |
| int this_reg = FindRegionIndex(regions_list, j + frames_since_key); |
| if (this_reg < 0) continue; |
| // A GOP should include at most 1 blending region. |
| if (regions_list[this_reg].type == BLENDING_REGION) { |
| last_blending = 1; |
| if (met_blending) { |
| break; |
| } else { |
| base_score = 0; |
| continue; |
| } |
| } else { |
| if (last_blending) met_blending = 1; |
| last_blending = 0; |
| } |
| |
| // Add the factor of how good the neighborhood is for this |
| // candidate arf. |
| double this_score = arf_length_factor * base_score; |
| double temp_accu_coeff = 1.0; |
| // following frames |
| int count_f = 0; |
| for (int n = j + 1; n <= j + 3 && n <= last_frame; n++) { |
| if (order_index + n >= num_stats) break; |
| temp_accu_coeff *= stats_list[order_index + n].cor_coeff; |
| this_score += |
| temp_accu_coeff * |
| (1 - stats_list[order_index + n].noise_var / |
| AOMMAX(regions_list[this_reg].avg_intra_err, 0.001)); |
| count_f++; |
| } |
| // preceding frames |
| temp_accu_coeff = 1.0; |
| for (int n = j; n > j - 3 * 2 + count_f && n > first_frame; n--) { |
| if (order_index + n < 0) break; |
| temp_accu_coeff *= stats_list[order_index + n].cor_coeff; |
| this_score += |
| temp_accu_coeff * |
| (1 - stats_list[order_index + n].noise_var / |
| AOMMAX(regions_list[this_reg].avg_intra_err, 0.001)); |
| } |
| |
| if (this_score > best_score) { |
| best_score = this_score; |
| best_j = j; |
| } |
| } |
| |
| // For blending areas, move one more frame in case we missed the |
| // first blending frame. |
| int best_reg = FindRegionIndex(regions_list, best_j + frames_since_key); |
| if (best_reg < num_regions - 1 && best_reg > 0) { |
| if (regions_list[best_reg - 1].type == BLENDING_REGION && |
| regions_list[best_reg + 1].type == BLENDING_REGION) { |
| if (best_j + frames_since_key == regions_list[best_reg].start && |
| best_j + frames_since_key < regions_list[best_reg].last) { |
| best_j += 1; |
| } else if (best_j + frames_since_key == regions_list[best_reg].last && |
| best_j + frames_since_key > regions_list[best_reg].start) { |
| best_j -= 1; |
| } |
| } |
| } |
| |
| if (cur_last - best_j < 2) best_j = cur_last; |
| if (best_j > 0 && best_score > 0.1) cur_last = best_j; |
| // if cannot find anything, just cut at the original place. |
| } |
| } |
| |
| return cur_last; |
| } |
| |
| // Function to test for a condition where a complex transition is followed |
| // by a static section. For example in slide shows where there is a fade |
| // between slides. This is to help with more optimal kf and gf positioning. |
| static bool DetectTransitionToStill( |
| const std::vector<FIRSTPASS_STATS> &stats_list, int next_stats_index, |
| int min_gop_show_frame_count, int frame_interval, int still_interval, |
| double loop_decay_rate, double last_decay_rate) { |
| // Break clause to detect very still sections after motion |
| // For example a static image after a fade or other transition |
| // instead of a clean scene cut. |
| if (frame_interval > min_gop_show_frame_count && loop_decay_rate >= 0.999 && |
| last_decay_rate < 0.9) { |
| int stats_count = static_cast<int>(stats_list.size()); |
| int stats_left = stats_count - next_stats_index; |
| if (stats_left >= still_interval) { |
| // Look ahead a few frames to see if static condition persists... |
| int j; |
| for (j = 0; j < still_interval; ++j) { |
| const FIRSTPASS_STATS &stats = stats_list[next_stats_index + j]; |
| if (stats.pcnt_inter - stats.pcnt_motion < 0.999) break; |
| } |
| // Only if it does do we signal a transition to still. |
| return j == still_interval; |
| } |
| } |
| return false; |
| } |
| |
| static int DetectGopCut(const std::vector<FIRSTPASS_STATS> &stats_list, |
| int start_idx, int candidate_cut_idx, int next_key_idx, |
| int flash_detected, int min_gop_show_frame_count, |
| int max_gop_show_frame_count, int frame_width, |
| int frame_height, const GF_GROUP_STATS &gf_stats) { |
| (void)max_gop_show_frame_count; |
| const int candidate_gop_size = candidate_cut_idx - start_idx; |
| |
| if (!flash_detected) { |
| // Break clause to detect very still sections after motion. For example, |
| // a static image after a fade or other transition. |
| if (DetectTransitionToStill(stats_list, start_idx, min_gop_show_frame_count, |
| candidate_gop_size, 5, gf_stats.loop_decay_rate, |
| gf_stats.last_loop_decay_rate)) { |
| return 1; |
| } |
| const double arf_abs_zoom_thresh = 4.4; |
| // Motion breakout threshold for loop below depends on image size. |
| const double mv_ratio_accumulator_thresh = |
| (frame_height + frame_width) / 4.0; |
| // Some conditions to breakout after min interval. |
| if (candidate_gop_size >= min_gop_show_frame_count && |
| // If possible don't break very close to a kf |
| (next_key_idx - candidate_cut_idx >= min_gop_show_frame_count) && |
| (candidate_gop_size & 0x01) && |
| (gf_stats.mv_ratio_accumulator > mv_ratio_accumulator_thresh || |
| gf_stats.abs_mv_in_out_accumulator > arf_abs_zoom_thresh)) { |
| return 1; |
| } |
| } |
| |
| // TODO(b/231489624): Check if we need this part. |
| // If almost totally static, we will not use the the max GF length later, |
| // so we can continue for more frames. |
| // if ((candidate_gop_size >= active_max_gf_interval + 1) && |
| // !is_almost_static(gf_stats->zero_motion_accumulator, |
| // twopass->kf_zeromotion_pct, cpi->ppi->lap_enabled)) { |
| // return 0; |
| // } |
| return 0; |
| } |
| |
| /*!\brief Determine the length of future GF groups. |
| * |
| * \ingroup gf_group_algo |
| * This function decides the gf group length of future frames in batch |
| * |
| * \param[in] rc_param Rate control parameters |
| * \param[in] stats_list List of first pass stats |
| * \param[in] regions_list List of regions from av1_identify_regions |
| * \param[in] order_index Index of current frame in stats_list |
| * \param[in] frames_since_key Number of frames since the last key frame |
| * \param[in] frames_to_key Number of frames to the next key frame |
| * |
| * \return Returns a vector of decided GF group lengths. |
| */ |
| static std::vector<int> PartitionGopIntervals( |
| const RateControlParam &rc_param, |
| const std::vector<FIRSTPASS_STATS> &stats_list, |
| const std::vector<REGIONS> ®ions_list, int order_index, |
| int frames_since_key, int frames_to_key) { |
| int i = 0; |
| // If cpi->gf_state.arf_gf_boost_lst is 0, we are starting with a KF or GF. |
| int cur_start = 0; |
| // Each element is the last frame of the previous GOP. If there are n GOPs, |
| // you need n + 1 cuts to find the durations. So cut_pos starts out with -1, |
| // which is the last frame of the previous GOP. |
| std::vector<int> cut_pos(1, -1); |
| int cut_here = 0; |
| GF_GROUP_STATS gf_stats; |
| InitGFStats(&gf_stats); |
| int num_stats = static_cast<int>(stats_list.size()); |
| |
| while (i + order_index < num_stats) { |
| // reaches next key frame, break here |
| if (i >= frames_to_key - 1) { |
| cut_here = 2; |
| } else if (i - cur_start >= rc_param.max_gop_show_frame_count) { |
| // reached maximum len, but nothing special yet (almost static) |
| // let's look at the next interval |
| cut_here = 2; |
| } else { |
| // Test for the case where there is a brief flash but the prediction |
| // quality back to an earlier frame is then restored. |
| const int gop_start_idx = cur_start + order_index; |
| const int candidate_gop_cut_idx = i + order_index; |
| const int next_key_idx = frames_to_key + order_index; |
| const bool flash_detected = |
| DetectFlash(stats_list, candidate_gop_cut_idx); |
| |
| // TODO(bohanli): remove redundant accumulations here, or unify |
| // this and the ones in define_gf_group |
| const FIRSTPASS_STATS *stats = &stats_list[candidate_gop_cut_idx]; |
| av1_accumulate_next_frame_stats(stats, flash_detected, frames_since_key, |
| i, &gf_stats, rc_param.frame_width, |
| rc_param.frame_height); |
| |
| // TODO(angiebird): Can we simplify this part? Looks like we are going to |
| // change the gop cut index with FindBetterGopCut() anyway. |
| cut_here = DetectGopCut( |
| stats_list, gop_start_idx, candidate_gop_cut_idx, next_key_idx, |
| flash_detected, rc_param.min_gop_show_frame_count, |
| rc_param.max_gop_show_frame_count, rc_param.frame_width, |
| rc_param.frame_height, gf_stats); |
| } |
| |
| if (!cut_here) { |
| ++i; |
| continue; |
| } |
| |
| // the current last frame in the gf group |
| int original_last = cut_here > 1 ? i : i - 1; |
| int cur_last = FindBetterGopCut( |
| stats_list, regions_list, rc_param.min_gop_show_frame_count, |
| rc_param.max_gop_show_frame_count, order_index, cur_start, |
| original_last, frames_since_key); |
| // only try shrinking if interval smaller than active_max_gf_interval |
| cut_pos.push_back(cur_last); |
| |
| // reset pointers to the shrunken location |
| cur_start = cur_last; |
| int cur_region_idx = |
| FindRegionIndex(regions_list, cur_start + 1 + frames_since_key); |
| if (cur_region_idx >= 0) |
| if (regions_list[cur_region_idx].type == SCENECUT_REGION) cur_start++; |
| |
| // reset accumulators |
| InitGFStats(&gf_stats); |
| i = cur_last + 1; |
| |
| if (cut_here == 2 && i >= frames_to_key) break; |
| } |
| |
| std::vector<int> gf_intervals; |
| // save intervals |
| for (size_t n = 1; n < cut_pos.size(); n++) { |
| gf_intervals.push_back(cut_pos[n] - cut_pos[n - 1]); |
| } |
| |
| return gf_intervals; |
| } |
| |
| StatusOr<GopStructList> AV1RateControlQMode::DetermineGopInfo( |
| const FirstpassInfo &firstpass_info) { |
| const int stats_size = static_cast<int>(firstpass_info.stats_list.size()); |
| GopStructList gop_list; |
| RefFrameManager ref_frame_manager(rc_param_.ref_frame_table_size, |
| rc_param_.max_ref_frames); |
| |
| int global_coding_idx_offset = 0; |
| int global_order_idx_offset = 0; |
| std::vector<int> key_frame_list = GetKeyFrameList(firstpass_info); |
| key_frame_list.push_back(stats_size); // a sentinel value |
| for (size_t ki = 0; ki + 1 < key_frame_list.size(); ++ki) { |
| int frames_to_key = key_frame_list[ki + 1] - key_frame_list[ki]; |
| int key_order_index = key_frame_list[ki]; // The key frame's display order |
| |
| std::vector<REGIONS> regions_list(MAX_FIRSTPASS_ANALYSIS_FRAMES); |
| int total_regions = 0; |
| av1_identify_regions(firstpass_info.stats_list.data() + key_order_index, |
| frames_to_key, 0, regions_list.data(), &total_regions); |
| regions_list.resize(total_regions); |
| std::vector<int> gf_intervals = PartitionGopIntervals( |
| rc_param_, firstpass_info.stats_list, regions_list, key_order_index, |
| /*frames_since_key=*/0, frames_to_key); |
| for (size_t gi = 0; gi < gf_intervals.size(); ++gi) { |
| const bool has_key_frame = gi == 0; |
| const int show_frame_count = gf_intervals[gi]; |
| GopStruct gop = |
| ConstructGop(&ref_frame_manager, show_frame_count, has_key_frame, |
| global_coding_idx_offset, global_order_idx_offset); |
| assert(gop.show_frame_count == show_frame_count); |
| global_coding_idx_offset += static_cast<int>(gop.gop_frame_list.size()); |
| global_order_idx_offset += gop.show_frame_count; |
| gop_list.push_back(gop); |
| } |
| } |
| return gop_list; |
| } |
| |
| TplFrameDepStats CreateTplFrameDepStats(int frame_height, int frame_width, |
| int min_block_size) { |
| const int unit_rows = (frame_height + min_block_size - 1) / min_block_size; |
| const int unit_cols = (frame_width + min_block_size - 1) / min_block_size; |
| TplFrameDepStats frame_dep_stats; |
| frame_dep_stats.unit_size = min_block_size; |
| frame_dep_stats.unit_stats.resize(unit_rows); |
| for (auto &row : frame_dep_stats.unit_stats) { |
| row.resize(unit_cols); |
| } |
| return frame_dep_stats; |
| } |
| |
| TplUnitDepStats TplBlockStatsToDepStats(const TplBlockStats &block_stats, |
| int unit_count) { |
| TplUnitDepStats dep_stats = {}; |
| dep_stats.intra_cost = block_stats.intra_cost * 1.0 / unit_count; |
| dep_stats.inter_cost = block_stats.inter_cost * 1.0 / unit_count; |
| // In rare case, inter_cost may be greater than intra_cost. |
| // If so, we need to modify inter_cost such that inter_cost <= intra_cost |
| // because it is required by GetPropagationFraction() |
| dep_stats.inter_cost = std::min(dep_stats.intra_cost, dep_stats.inter_cost); |
| dep_stats.mv = block_stats.mv; |
| dep_stats.ref_frame_index = block_stats.ref_frame_index; |
| return dep_stats; |
| } |
| |
| namespace { |
| Status ValidateBlockStats(const TplFrameStats &frame_stats, |
| const TplBlockStats &block_stats, |
| int min_block_size) { |
| if (block_stats.col >= frame_stats.frame_width || |
| block_stats.row >= frame_stats.frame_height) { |
| std::ostringstream error_message; |
| error_message << "Block position (" << block_stats.col << ", " |
| << block_stats.row |
| << ") is out of range; frame dimensions are " |
| << frame_stats.frame_width << " x " |
| << frame_stats.frame_height; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| if (block_stats.col % min_block_size != 0 || |
| block_stats.row % min_block_size != 0 || |
| block_stats.width % min_block_size != 0 || |
| block_stats.height % min_block_size != 0) { |
| std::ostringstream error_message; |
| error_message |
| << "Invalid block position or dimension, must be a multiple of " |
| << min_block_size << "; col = " << block_stats.col |
| << ", row = " << block_stats.row << ", width = " << block_stats.width |
| << ", height = " << block_stats.height; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| return { AOM_CODEC_OK, "" }; |
| } |
| |
| Status ValidateTplStats(const GopStruct &gop_struct, |
| const TplGopStats &tpl_gop_stats) { |
| constexpr char kAdvice[] = |
| "Do the current RateControlParam settings match those used to generate " |
| "the TPL stats?"; |
| if (gop_struct.gop_frame_list.size() != |
| tpl_gop_stats.frame_stats_list.size()) { |
| std::ostringstream error_message; |
| error_message << "Frame count of GopStruct (" |
| << gop_struct.gop_frame_list.size() |
| << ") doesn't match frame count of TPL stats (" |
| << tpl_gop_stats.frame_stats_list.size() << "). " << kAdvice; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| for (int i = 0; i < static_cast<int>(gop_struct.gop_frame_list.size()); ++i) { |
| const bool is_ref_frame = gop_struct.gop_frame_list[i].update_ref_idx >= 0; |
| const bool has_tpl_stats = |
| !tpl_gop_stats.frame_stats_list[i].block_stats_list.empty(); |
| if (is_ref_frame && !has_tpl_stats) { |
| std::ostringstream error_message; |
| error_message << "The frame with global_coding_idx " |
| << gop_struct.gop_frame_list[i].global_coding_idx |
| << " is a reference frame, but has no TPL stats. " |
| << kAdvice; |
| return { AOM_CODEC_INVALID_PARAM, error_message.str() }; |
| } |
| } |
| return { AOM_CODEC_OK, "" }; |
| } |
| } // namespace |
| |
| StatusOr<TplFrameDepStats> CreateTplFrameDepStatsWithoutPropagation( |
| const TplFrameStats &frame_stats) { |
| if (frame_stats.block_stats_list.empty()) { |
| return TplFrameDepStats(); |
| } |
| const int min_block_size = frame_stats.min_block_size; |
| const int unit_rows = |
| (frame_stats.frame_height + min_block_size - 1) / min_block_size; |
| const int unit_cols = |
| (frame_stats.frame_width + min_block_size - 1) / min_block_size; |
| TplFrameDepStats frame_dep_stats = CreateTplFrameDepStats( |
| frame_stats.frame_height, frame_stats.frame_width, min_block_size); |
| for (const TplBlockStats &block_stats : frame_stats.block_stats_list) { |
| Status status = |
| ValidateBlockStats(frame_stats, block_stats, min_block_size); |
| if (!status.ok()) { |
| return status; |
| } |
| const int block_unit_row = block_stats.row / min_block_size; |
| const int block_unit_col = block_stats.col / min_block_size; |
| // The block must start within the frame boundaries, but it may extend past |
| // the right edge or bottom of the frame. Find the number of unit rows and |
| // columns in the block which are fully within the frame. |
| const int block_unit_rows = std::min(block_stats.height / min_block_size, |
| unit_rows - block_unit_row); |
| const int block_unit_cols = std::min(block_stats.width / min_block_size, |
| unit_cols - block_unit_col); |
| const int unit_count = block_unit_rows * block_unit_cols; |
| TplUnitDepStats unit_stats = |
| TplBlockStatsToDepStats(block_stats, unit_count); |
| for (int r = 0; r < block_unit_rows; r++) { |
| for (int c = 0; c < block_unit_cols; c++) { |
| frame_dep_stats.unit_stats[block_unit_row + r][block_unit_col + c] = |
| unit_stats; |
| } |
| } |
| } |
| return frame_dep_stats; |
| } |
| |
| int GetRefCodingIdxList(const TplUnitDepStats &unit_dep_stats, |
| const RefFrameTable &ref_frame_table, |
| int *ref_coding_idx_list) { |
| int ref_frame_count = 0; |
| for (int i = 0; i < kBlockRefCount; ++i) { |
| ref_coding_idx_list[i] = -1; |
| int ref_frame_index = unit_dep_stats.ref_frame_index[i]; |
| if (ref_frame_index != -1) { |
| assert(ref_frame_index < static_cast<int>(ref_frame_table.size())); |
| ref_coding_idx_list[i] = ref_frame_table[ref_frame_index].coding_idx; |
| ref_frame_count++; |
| } |
| } |
| return ref_frame_count; |
| } |
| |
| int GetBlockOverlapArea(int r0, int c0, int r1, int c1, int size) { |
| const int r_low = std::max(r0, r1); |
| const int r_high = std::min(r0 + size, r1 + size); |
| const int c_low = std::max(c0, c1); |
| const int c_high = std::min(c0 + size, c1 + size); |
| if (r_high >= r_low && c_high >= c_low) { |
| return (r_high - r_low) * (c_high - c_low); |
| } |
| return 0; |
| } |
| |
| // TODO(angiebird): Merge TplFrameDepStatsAccumulateIntraCost and |
| // TplFrameDepStatsAccumulate. |
| double TplFrameDepStatsAccumulateIntraCost( |
| const TplFrameDepStats &frame_dep_stats) { |
| auto getIntraCost = [](double sum, const TplUnitDepStats &unit) { |
| return sum + unit.intra_cost; |
| }; |
| double sum = 0; |
| for (const auto &row : frame_dep_stats.unit_stats) { |
| sum = std::accumulate(row.begin(), row.end(), sum, getIntraCost); |
| } |
| return sum; |
| } |
| |
| double TplFrameDepStatsAccumulate(const TplFrameDepStats &frame_dep_stats) { |
| auto getOverallCost = [](double sum, const TplUnitDepStats &unit) { |
| return sum + unit.propagation_cost + unit.intra_cost; |
| }; |
| double sum = 0; |
| for (const auto &row : frame_dep_stats.unit_stats) { |
| sum = std::accumulate(row.begin(), row.end(), sum, getOverallCost); |
| } |
| return sum; |
| } |
| |
| // This is a generalization of GET_MV_RAWPEL that allows for an arbitrary |
| // number of fractional bits. |
| // TODO(angiebird): Add unit test to this function |
| int GetFullpelValue(int subpel_value, int subpel_bits) { |
| const int subpel_scale = (1 << subpel_bits); |
| const int sign = subpel_value >= 0 ? 1 : -1; |
| int fullpel_value = (abs(subpel_value) + subpel_scale / 2) >> subpel_bits; |
| fullpel_value *= sign; |
| return fullpel_value; |
| } |
| |
| double GetPropagationFraction(const TplUnitDepStats &unit_dep_stats) { |
| assert(unit_dep_stats.intra_cost >= unit_dep_stats.inter_cost); |
| return (unit_dep_stats.intra_cost - unit_dep_stats.inter_cost) / |
| ModifyDivisor(unit_dep_stats.intra_cost); |
| } |
| |
| void TplFrameDepStatsPropagate(int coding_idx, |
| const RefFrameTable &ref_frame_table, |
| TplGopDepStats *tpl_gop_dep_stats) { |
| assert(!tpl_gop_dep_stats->frame_dep_stats_list.empty()); |
| TplFrameDepStats *frame_dep_stats = |
| &tpl_gop_dep_stats->frame_dep_stats_list[coding_idx]; |
| |
| if (frame_dep_stats->unit_stats.empty()) return; |
| |
| const int unit_size = frame_dep_stats->unit_size; |
| const int frame_unit_rows = |
| static_cast<int>(frame_dep_stats->unit_stats.size()); |
| const int frame_unit_cols = |
| static_cast<int>(frame_dep_stats->unit_stats[0].size()); |
| for (int unit_row = 0; unit_row < frame_unit_rows; ++unit_row) { |
| for (int unit_col = 0; unit_col < frame_unit_cols; ++unit_col) { |
| TplUnitDepStats &unit_dep_stats = |
| frame_dep_stats->unit_stats[unit_row][unit_col]; |
| int ref_coding_idx_list[kBlockRefCount] = { -1, -1 }; |
| int ref_frame_count = GetRefCodingIdxList(unit_dep_stats, ref_frame_table, |
| ref_coding_idx_list); |
| if (ref_frame_count == 0) continue; |
| for (int i = 0; i < kBlockRefCount; ++i) { |
| if (ref_coding_idx_list[i] == -1) continue; |
| assert( |
| ref_coding_idx_list[i] < |
| static_cast<int>(tpl_gop_dep_stats->frame_dep_stats_list.size())); |
| TplFrameDepStats &ref_frame_dep_stats = |
| tpl_gop_dep_stats->frame_dep_stats_list[ref_coding_idx_list[i]]; |
| assert(!ref_frame_dep_stats.unit_stats.empty()); |
| const auto &mv = unit_dep_stats.mv[i]; |
| const int mv_row = GetFullpelValue(mv.row, mv.subpel_bits); |
| const int mv_col = GetFullpelValue(mv.col, mv.subpel_bits); |
| const int ref_pixel_r = unit_row * unit_size + mv_row; |
| const int ref_pixel_c = unit_col * unit_size + mv_col; |
| const int ref_unit_row_low = |
| (unit_row * unit_size + mv_row) / unit_size; |
| const int ref_unit_col_low = |
| (unit_col * unit_size + mv_col) / unit_size; |
| |
| for (int j = 0; j < 2; ++j) { |
| for (int k = 0; k < 2; ++k) { |
| const int ref_unit_row = ref_unit_row_low + j; |
| const int ref_unit_col = ref_unit_col_low + k; |
| if (ref_unit_row >= 0 && ref_unit_row < frame_unit_rows && |
| ref_unit_col >= 0 && ref_unit_col < frame_unit_cols) { |
| const int overlap_area = GetBlockOverlapArea( |
| ref_pixel_r, ref_pixel_c, ref_unit_row * unit_size, |
| ref_unit_col * unit_size, unit_size); |
| const double overlap_ratio = |
| overlap_area * 1.0 / (unit_size * unit_size); |
| const double propagation_fraction = |
| GetPropagationFraction(unit_dep_stats); |
| const double propagation_ratio = |
| 1.0 / ref_frame_count * overlap_ratio * propagation_fraction; |
| TplUnitDepStats &ref_unit_stats = |
| ref_frame_dep_stats.unit_stats[ref_unit_row][ref_unit_col]; |
| ref_unit_stats.propagation_cost += |
| (unit_dep_stats.intra_cost + |
| unit_dep_stats.propagation_cost) * |
| propagation_ratio; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| std::vector<RefFrameTable> AV1RateControlQMode::GetRefFrameTableList( |
| const GopStruct &gop_struct, |
| const std::vector<LookaheadStats> &lookahead_stats, |
| RefFrameTable ref_frame_table) { |
| if (gop_struct.global_coding_idx_offset == 0) { |
| // For the first GOP, ref_frame_table need not be initialized. This is fine, |
| // because the first frame (a key frame) will fully initialize it. |
| ref_frame_table.assign(rc_param_.ref_frame_table_size, GopFrameInvalid()); |
| } else { |
| // It's not the first GOP, so ref_frame_table must be valid. |
| assert(static_cast<int>(ref_frame_table.size()) == |
| rc_param_.ref_frame_table_size); |
| assert(std::all_of(ref_frame_table.begin(), ref_frame_table.end(), |
| std::mem_fn(&GopFrame::is_valid))); |
| // Reset the frame processing order of the initial ref_frame_table. |
| for (GopFrame &gop_frame : ref_frame_table) gop_frame.coding_idx = -1; |
| } |
| |
| std::vector<RefFrameTable> ref_frame_table_list; |
| ref_frame_table_list.push_back(ref_frame_table); |
| for (const GopFrame &gop_frame : gop_struct.gop_frame_list) { |
| if (gop_frame.is_key_frame) { |
| ref_frame_table.assign(rc_param_.ref_frame_table_size, gop_frame); |
| } else if (gop_frame.update_ref_idx != -1) { |
| assert(gop_frame.update_ref_idx < |
| static_cast<int>(ref_frame_table.size())); |
| ref_frame_table[gop_frame.update_ref_idx] = gop_frame; |
| } |
| ref_frame_table_list.push_back(ref_frame_table); |
| } |
| |
| int gop_size_offset = static_cast<int>(gop_struct.gop_frame_list.size()); |
| |
| for (const auto &lookahead_stat : lookahead_stats) { |
| for (GopFrame gop_frame : lookahead_stat.gop_struct->gop_frame_list) { |
| if (gop_frame.is_key_frame) { |
| ref_frame_table.assign(rc_param_.ref_frame_table_size, gop_frame); |
| } else if (gop_frame.update_ref_idx != -1) { |
| assert(gop_frame.update_ref_idx < |
| static_cast<int>(ref_frame_table.size())); |
| gop_frame.coding_idx += gop_size_offset; |
| ref_frame_table[gop_frame.update_ref_idx] = gop_frame; |
| } |
| ref_frame_table_list.push_back(ref_frame_table); |
| } |
| gop_size_offset += |
| static_cast<int>(lookahead_stat.gop_struct->gop_frame_list.size()); |
| } |
| |
| return ref_frame_table_list; |
| } |
| |
| StatusOr<TplGopDepStats> ComputeTplGopDepStats( |
| const TplGopStats &tpl_gop_stats, |
| const std::vector<LookaheadStats> &lookahead_stats, |
| const std::vector<RefFrameTable> &ref_frame_table_list) { |
| std::vector<const TplFrameStats *> tpl_frame_stats_list_with_lookahead; |
| for (const auto &tpl_frame_stats : tpl_gop_stats.frame_stats_list) { |
| tpl_frame_stats_list_with_lookahead.push_back(&tpl_frame_stats); |
| } |
| for (auto &lookahead_stat : lookahead_stats) { |
| for (const auto &tpl_frame_stats : |
| lookahead_stat.tpl_gop_stats->frame_stats_list) { |
| tpl_frame_stats_list_with_lookahead.push_back(&tpl_frame_stats); |
| } |
| } |
| |
| const int frame_count = |
| static_cast<int>(tpl_frame_stats_list_with_lookahead.size()); |
| |
| // Create the struct to store TPL dependency stats |
| TplGopDepStats tpl_gop_dep_stats; |
| |
| tpl_gop_dep_stats.frame_dep_stats_list.reserve(frame_count); |
| for (int coding_idx = 0; coding_idx < frame_count; coding_idx++) { |
| const StatusOr<TplFrameDepStats> tpl_frame_dep_stats = |
| CreateTplFrameDepStatsWithoutPropagation( |
| *tpl_frame_stats_list_with_lookahead[coding_idx]); |
| if (!tpl_frame_dep_stats.ok()) { |
| return tpl_frame_dep_stats.status(); |
| } |
| tpl_gop_dep_stats.frame_dep_stats_list.push_back( |
| std::move(*tpl_frame_dep_stats)); |
| } |
| |
| // Back propagation |
| for (int coding_idx = frame_count - 1; coding_idx >= 0; coding_idx--) { |
| auto &ref_frame_table = ref_frame_table_list[coding_idx]; |
| // TODO(angiebird): Handle/test the case where reference frame |
| // is in the previous GOP |
| TplFrameDepStatsPropagate(coding_idx, ref_frame_table, &tpl_gop_dep_stats); |
| } |
| return tpl_gop_dep_stats; |
| } |
| |
| static int GetRDMult(const GopFrame &gop_frame, int qindex) { |
| // TODO(angiebird): |
| // 1) Check if these rdmult rules are good in our use case. |
| // 2) Support high-bit-depth mode |
| if (gop_frame.is_golden_frame) { |
| // Assume ARF_UPDATE/GF_UPDATE share the same remult rule. |
| return av1_compute_rd_mult_based_on_qindex(AOM_BITS_8, GF_UPDATE, qindex); |
| } else if (gop_frame.is_key_frame) { |
| return av1_compute_rd_mult_based_on_qindex(AOM_BITS_8, KF_UPDATE, qindex); |
| } else { |
| // Assume LF_UPDATE/OVERLAY_UPDATE/INTNL_OVERLAY_UPDATE/INTNL_ARF_UPDATE |
| // share the same remult rule. |
| return av1_compute_rd_mult_based_on_qindex(AOM_BITS_8, LF_UPDATE, qindex); |
| } |
| } |
| |
| StatusOr<GopEncodeInfo> AV1RateControlQMode::GetGopEncodeInfo( |
| const GopStruct &gop_struct, const TplGopStats &tpl_gop_stats, |
| const std::vector<LookaheadStats> &lookahead_stats, |
| const RefFrameTable &ref_frame_table_snapshot_init) { |
| Status status = ValidateTplStats(gop_struct, tpl_gop_stats); |
| if (!status.ok()) { |
| return status; |
| } |
| |
| for (auto &lookahead_stat : lookahead_stats) { |
| Status status = ValidateTplStats(*lookahead_stat.gop_struct, |
| *lookahead_stat.tpl_gop_stats); |
| if (!status.ok()) { |
| return status; |
| } |
| } |
| |
| const std::vector<RefFrameTable> ref_frame_table_list = GetRefFrameTableList( |
| gop_struct, lookahead_stats, ref_frame_table_snapshot_init); |
| |
| GopEncodeInfo gop_encode_info; |
| gop_encode_info.final_snapshot = ref_frame_table_list.back(); |
| StatusOr<TplGopDepStats> gop_dep_stats = ComputeTplGopDepStats( |
| tpl_gop_stats, lookahead_stats, ref_frame_table_list); |
| if (!gop_dep_stats.ok()) { |
| return gop_dep_stats.status(); |
| } |
| const int frame_count = |
| static_cast<int>(tpl_gop_stats.frame_stats_list.size()); |
| for (int i = 0; i < frame_count; i++) { |
| FrameEncodeParameters param; |
| const GopFrame &gop_frame = gop_struct.gop_frame_list[i]; |
| |
| if (gop_frame.update_type == GopFrameType::kOverlay || |
| gop_frame.update_type == GopFrameType::kIntermediateOverlay) { |
| param.q_index = rc_param_.base_q_index; |
| } else { |
| const TplFrameDepStats &frame_dep_stats = |
| gop_dep_stats->frame_dep_stats_list[i]; |
| const double cost_without_propagation = |
| TplFrameDepStatsAccumulateIntraCost(frame_dep_stats); |
| const double cost_with_propagation = |
| TplFrameDepStatsAccumulate(frame_dep_stats); |
| const double frame_importance = |
| cost_with_propagation / cost_without_propagation; |
| // Imitate the behavior of av1_tpl_get_qstep_ratio() |
| const double qstep_ratio = sqrt(1 / frame_importance); |
| param.q_index = av1_get_q_index_from_qstep_ratio(rc_param_.base_q_index, |
| qstep_ratio, AOM_BITS_8); |
| if (rc_param_.base_q_index) param.q_index = AOMMAX(param.q_index, 1); |
| } |
| param.rdmult = GetRDMult(gop_frame, param.q_index); |
| gop_encode_info.param_list.push_back(param); |
| } |
| return gop_encode_info; |
| } |
| |
| } // namespace aom |