mirror of
https://github.com/master-of-zen/Av1an.git
synced 2024-11-25 10:40:51 +00:00
196 lines
8.2 KiB
Python
Executable file
196 lines
8.2 KiB
Python
Executable file
#!/bin/env python
|
|
import os
|
|
import re
|
|
import struct
|
|
import subprocess
|
|
from collections import deque
|
|
from pathlib import Path
|
|
from subprocess import PIPE, STDOUT
|
|
|
|
import cv2
|
|
from tqdm import tqdm
|
|
|
|
from .compose import compose_aomsplit_first_pass_command
|
|
from .logger import log
|
|
from .utils import frame_probe
|
|
|
|
# This is a script that returns a list of keyframes that aom would likely place. Port of aom's C code.
|
|
# It requires an aom first-pass stats file as input. FFMPEG first-pass file is not OK. Default filename is stats.bin.
|
|
# Script has been tested to have ~99% accuracy vs final aom encode.
|
|
|
|
# Elements related to parsing the stats file were written by MrSmilingWolf
|
|
|
|
# All of my contributions to this script are hereby public domain.
|
|
# I retain no rights or control over distribution.
|
|
|
|
|
|
# default params for 1st pass when aom isn't the final encoder and -v won't match aom's options
|
|
AOM_KEYFRAMES_DEFAULT_PARAMS = '--threads=12 --cpu-used=0 --end-usage=q --cq-level=40'
|
|
|
|
|
|
# Fields meanings: <source root>/av1/encoder/firstpass.h
|
|
fields = ['frame', 'weight', 'intra_error', 'frame_avg_wavelet_energy', 'coded_error', 'sr_coded_error', 'tr_coded_error',
|
|
'pcnt_inter', 'pcnt_motion', 'pcnt_second_ref', 'pcnt_third_ref', 'pcnt_neutral', 'intra_skip_pct', 'inactive_zone_rows',
|
|
'inactive_zone_cols', 'MVr', 'mvr_abs', 'MVc', 'mvc_abs', 'MVrv', 'MVcv', 'mv_in_out_count', 'new_mv_count', 'duration', 'count', 'raw_error_stdev']
|
|
|
|
|
|
def get_second_ref_usage_thresh(frame_count_so_far):
|
|
adapt_upto = 32
|
|
min_second_ref_usage_thresh = 0.085
|
|
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 + (frame_count_so_far / (adapt_upto - 1)) * second_ref_usage_thresh_max_delta
|
|
|
|
|
|
# I have no idea if the following function is necessary in the python implementation or what its purpose even is.
|
|
def DOUBLE_DIVIDE_CHECK(x):
|
|
if x < 0:
|
|
return x - 0.000001
|
|
else:
|
|
return x + 0.000001
|
|
|
|
|
|
def test_candidate_kf(dict_list, current_frame_index, frame_count_so_far):
|
|
previous_frame_dict = dict_list[current_frame_index - 1]
|
|
current_frame_dict = dict_list[current_frame_index]
|
|
future_frame_dict = dict_list[current_frame_index + 1]
|
|
|
|
p = previous_frame_dict
|
|
c = current_frame_dict
|
|
f = future_frame_dict
|
|
|
|
BOOST_FACTOR = 12.5
|
|
|
|
# For more documentation on the below, see https://aomedia.googlesource.com/aom/+/8ac928be918de0d502b7b492708d57ad4d817676/av1/encoder/pass2_strategy.c#1897
|
|
MIN_INTRA_LEVEL = 0.25
|
|
INTRA_VS_INTER_THRESH = 2.0
|
|
VERY_LOW_INTER_THRESH = 0.05
|
|
KF_II_ERR_THRESHOLD = 2.5
|
|
ERR_CHANGE_THRESHOLD = 0.4
|
|
II_IMPROVEMENT_THRESHOLD = 3.5
|
|
KF_II_MAX = 128.0
|
|
|
|
qmode = True
|
|
# TODO: allow user to set whether we're testing for constant-q mode keyframe placement or not. it's not a big difference.
|
|
|
|
is_keyframe = False
|
|
|
|
pcnt_intra = 1.0 - c['pcnt_inter']
|
|
modified_pcnt_inter = c['pcnt_inter'] - c['pcnt_neutral']
|
|
|
|
second_ref_usage_thresh = get_second_ref_usage_thresh(frame_count_so_far)
|
|
|
|
if ((qmode == False) or (frame_count_so_far > 2)) and (c['pcnt_second_ref'] < second_ref_usage_thresh) and (f['pcnt_second_ref'] < second_ref_usage_thresh) and ((c['pcnt_inter'] < VERY_LOW_INTER_THRESH) or ((pcnt_intra > MIN_INTRA_LEVEL) and (pcnt_intra > (INTRA_VS_INTER_THRESH * modified_pcnt_inter)) and ((c['intra_error'] / DOUBLE_DIVIDE_CHECK(c['coded_error'])) < KF_II_ERR_THRESHOLD) and ((abs(p['coded_error'] - c['coded_error']) / DOUBLE_DIVIDE_CHECK(c['coded_error']) > ERR_CHANGE_THRESHOLD) or (abs(p['intra_error'] - c['intra_error']) / DOUBLE_DIVIDE_CHECK(c['intra_error']) > ERR_CHANGE_THRESHOLD) or ((f['intra_error'] / DOUBLE_DIVIDE_CHECK(f['coded_error'])) > II_IMPROVEMENT_THRESHOLD)))):
|
|
boost_score = 0.0
|
|
old_boost_score = 0.0
|
|
decay_accumulator = 1.0
|
|
for i in range(0, 16):
|
|
lnf = dict_list[current_frame_index + 1 + i]
|
|
next_iiratio = (BOOST_FACTOR * lnf['intra_error'] / DOUBLE_DIVIDE_CHECK(lnf['coded_error']))
|
|
if next_iiratio > KF_II_MAX:
|
|
next_iiratio = KF_II_MAX
|
|
|
|
#Cumulative effect of decay in prediction quality.
|
|
if lnf['pcnt_inter'] > 0.85:
|
|
decay_accumulator = decay_accumulator * lnf['pcnt_inter']
|
|
else:
|
|
decay_accumulator = decay_accumulator * ((0.85 + lnf['pcnt_inter']) / 2.0)
|
|
|
|
#Keep a running total.
|
|
boost_score += (decay_accumulator * next_iiratio)
|
|
|
|
#Test various breakout clauses.
|
|
if (lnf['pcnt_inter'] < 0.05) or (next_iiratio < 1.5) or (((lnf['pcnt_inter'] - lnf['pcnt_neutral']) < 0.20) and (next_iiratio < 3.0)) or ((boost_score - old_boost_score) < 3.0) or (lnf['intra_error'] < 200):
|
|
break
|
|
old_boost_score = boost_score
|
|
|
|
#If there is tolerable prediction for at least the next 3 frames then break out else discard this potential key frame and move on
|
|
if (boost_score > 30.0 and (i > 3)):
|
|
is_keyframe = True
|
|
return is_keyframe
|
|
|
|
|
|
def find_aom_keyframes(stat_file, key_freq_min):
|
|
#I don't know what data format you want as output
|
|
keyframes_list = []
|
|
|
|
number_of_frames = round(os.stat(stat_file).st_size / 208) - 1
|
|
dict_list = []
|
|
|
|
with open(stat_file, 'rb') as file:
|
|
frame_buf = file.read(208)
|
|
while len(frame_buf) > 0:
|
|
stats = struct.unpack('d' * 26, frame_buf)
|
|
p = dict(zip(fields, stats))
|
|
dict_list.append(p)
|
|
frame_buf = file.read(208)
|
|
|
|
#intentionally skipping 0th frame and last 16 frames
|
|
frame_count_so_far = 1
|
|
for i in range(1, number_of_frames - 16):
|
|
is_keyframe = False
|
|
if frame_count_so_far >= key_freq_min: # https://aomedia.googlesource.com/aom/+/ce97de2724d7ffdfdbe986a14d49366936187298/av1/encoder/pass2_strategy.c#2065
|
|
is_keyframe = test_candidate_kf(dict_list, i, frame_count_so_far)
|
|
if is_keyframe:
|
|
keyframes_list.append(i)
|
|
frame_count_so_far = 0
|
|
frame_count_so_far += 1
|
|
|
|
return keyframes_list
|
|
|
|
|
|
def aom_keyframes(video_path: Path, stat_file, min_scene_len, ffmpeg_pipe, video_params):
|
|
"""[Get frame numbers for splits from aomenc 1 pass stat file]
|
|
"""
|
|
|
|
log(f'Started aom_keyframes scenedetection\nParams: {video_params}\n')
|
|
video = cv2.VideoCapture(video_path.as_posix()) # TODO(n9Mtq4): use a frame probe for this?
|
|
total = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
video.release()
|
|
|
|
if total < 1:
|
|
total = frame_probe(video_path)
|
|
|
|
f, e = compose_aomsplit_first_pass_command(video_path, stat_file, ffmpeg_pipe, video_params)
|
|
f, e = f.split(), e.split()
|
|
|
|
tqdm_bar = tqdm(total=total, initial=0, dynamic_ncols=True, unit="fr", leave=True, smoothing=0.2)
|
|
|
|
ffmpeg_pipe = subprocess.Popen(f, stdout=PIPE, stderr=STDOUT)
|
|
pipe = subprocess.Popen(e, stdin=ffmpeg_pipe.stdout, stdout=PIPE,
|
|
stderr=STDOUT, universal_newlines=True)
|
|
|
|
encoder_history = deque(maxlen=20)
|
|
frame = 0
|
|
|
|
while True:
|
|
line = pipe.stdout.readline()
|
|
if len(line) == 0 and pipe.poll() is not None:
|
|
break
|
|
line = line.strip()
|
|
|
|
if line:
|
|
encoder_history.append(line)
|
|
|
|
match = re.search(r"frame.*?\/([^ ]+?) ", line)
|
|
if match:
|
|
new = int(match.group(1))
|
|
if new > frame:
|
|
tqdm_bar.update(new - frame)
|
|
frame = new
|
|
|
|
if pipe.returncode != 0 and pipe.returncode != -2: # -2 is Ctrl+C for aom
|
|
enc_hist = '\n'.join(encoder_history)
|
|
er = f"\nAom first pass encountered an error: {pipe.returncode}\n{enc_hist}"
|
|
log(er)
|
|
print(er)
|
|
|
|
# aom kf-min-dist defaults to 0, but hardcoded to 3 in pass2_strategy.c test_candidate_kf. 0 matches default aom behavior
|
|
# https://aomedia.googlesource.com/aom/+/8ac928be918de0d502b7b492708d57ad4d817676/av1/av1_cx_iface.c#2816
|
|
# https://aomedia.googlesource.com/aom/+/ce97de2724d7ffdfdbe986a14d49366936187298/av1/encoder/pass2_strategy.c#1907
|
|
min_scene_len = 0 if min_scene_len is None else min_scene_len
|
|
|
|
keyframes = find_aom_keyframes(stat_file, min_scene_len)
|
|
|
|
return keyframes
|