From 4d6353ed2a1f18a1f07556f3d724d30a330b7da4 Mon Sep 17 00:00:00 2001 From: Alan Orth Date: Mon, 1 Aug 2022 15:14:12 +0300 Subject: [PATCH] Add scripts from ~/.local/bin Some utility scripts. --- config.yaml | 5 + dotfiles/local/bin/add-shadow.sh | 3 + dotfiles/local/bin/convert-libaom.sh | 214 ++++++++++++++++++++++++ dotfiles/local/bin/pre-process-media.sh | 126 ++++++++++++++ 4 files changed, 348 insertions(+) create mode 100755 dotfiles/local/bin/add-shadow.sh create mode 100755 dotfiles/local/bin/convert-libaom.sh create mode 100755 dotfiles/local/bin/pre-process-media.sh diff --git a/config.yaml b/config.yaml index 1e40fc6..ba6b578 100644 --- a/config.yaml +++ b/config.yaml @@ -60,6 +60,9 @@ dotfiles: f_geoclue-agent.service: src: config/systemd/user/geoclue-agent.service dst: ~/.config/systemd/user/geoclue-agent.service + d_local_bin: + src: local/bin + dst: ~/.local/bin profiles: everywhere: dotfiles: @@ -86,6 +89,7 @@ profiles: - f_psqlrc - f_pam_environment - f_xdg_desktop_portal_wlr_config + - d_local_bin knafeh: dotfiles: - d_vim @@ -106,3 +110,4 @@ profiles: - f_pam_environment - f_xdg_desktop_portal_wlr_config - f_geoclue-agent.service + - d_local_bin diff --git a/dotfiles/local/bin/add-shadow.sh b/dotfiles/local/bin/add-shadow.sh new file mode 100755 index 0000000..d03258c --- /dev/null +++ b/dotfiles/local/bin/add-shadow.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +convert "$1" -trim \( +clone -background grey25 -shadow 80x40+5+30 \) +swap -background transparent -layers merge +repage "$1-shadow.png" diff --git a/dotfiles/local/bin/convert-libaom.sh b/dotfiles/local/bin/convert-libaom.sh new file mode 100755 index 0000000..3208dde --- /dev/null +++ b/dotfiles/local/bin/convert-libaom.sh @@ -0,0 +1,214 @@ +#!/usr/bin/env bash +# +# v2022-06-13 +# +# I run it like this (using zsh syntax): +# +# $ for video (~/Pictures/2021/**/*.mp4(ND.)); do ~/Downloads/av1-tests/convert-libaom.sh "$video"; done +# +# Reference: +# - https://ffmpeg.org/ffmpeg-codecs.html#libaom_002dav1 +# - https://trac.ffmpeg.org/wiki/Encode/AV1 +# +# Changes: +# 2022-06-13: use _max_crf in mkvpropedit if we don't find an acceptable vmaf +# 2022-04-05: update VMAF log parsing +# 2021-10-21: I tested 2-pass and found it doesn't do anything for constant +# quality mode. cpu-used 2 still takes ~5x longer than cpu-used 4 +# at all tiling levels. 4x4 tiles much faster than 2x1 and 2x2 on +# all cpu-used levels, with quality less than 0.5 VMAF difference. +# Using 4x4 creates a grid of 16 tiles which just seems wrong... +# especially if you don't have many threads (I was using 12), so +# I will default to 2x2 as a compromise. +# 2021-10-20: use tiles instead of columns and rows +# 2021-10-16: use tile-columns 2 and tile-rows 2 (oops, this is 4x4 tiles!) +# 2021-08-31: strip spaces and commas from VMAF log file name as well +# 2021-05-14: benchmarked two pass and file sizes are larger by .5-1MB +# 2021-04-29: make sure input file exists +# 2021-03-07: benchmarked 10-bit and it's only .5 or less improvment to VMAF +# 2021-02-22: allow overriding threads +# 2021-02-15: fix handling of apostrophes in ffmpeg's log_path +# 2021-01-27: round VMAF before comparing, simplifies code a bit +# 2021-01-25: stop trying lower CRFs if VMAF is not likely to improve +# 2021-01-19: embed encoding parameters in webm metadata with mkvpropedit +# 2021-01-18: use 1-pass encoding (2-pass is only for trying to hit a target +# bitrate!) +# 2021-01-18: allow overriding variables +# 2021-01-17: detect if video already processed +# 2021-01-08: use cpu-used 4 +# 2021-01-07: use tile-columns 2 and tile-rows 2 with cpu-used 4 +# 2021-01-07: use tile-columns 2 and tile-rows 2 with cpu-used 3 +# 2021-01-07: use -g 300 (30fps x 10) + +# exit on first error +set -o errexit + +if [[ -z "$1" ]]; then + echo "No input file specified." + + exit 1 +elif [[ ! -r "$1" ]]; then + echo "Input file missing or unreadable: $1" + + exit 1 +fi + +INPUT_FILE_BASENAME=$(basename "$1") +INPUT_FILE_EXTENSION=${INPUT_FILE_BASENAME##*.} +INPUT_FILE_DIRNAME=$(dirname "$1") + +# Check if an output file name was specified (like if we are calling from the +# benchmarking script, in which case we want to exit as soon as possible). If +# not then we can continue with a simple filename based on the input file's. +if [[ -z $_output_file ]]; then + _output_file="${INPUT_FILE_BASENAME/.*/}.webm"; + _benchmark_mode="false" +else + _benchmark_mode="true" +fi + +# aq-mode not recommended in AV1 yet +_aq_mode=${_aq_mode:-0} +# 2021-01-08 (libaom 2.0.1): realistic range between 3 and 5, lower takes *much* +# longer with very little boost to VMAF. In my experience, cpu-used 2 is *three* +# times slower than cpu-used 4 at the same CRF for only a 0.5% boost in VMAF. 6 +# is the same as 5 in all my tests *shrug*. +_cpu_used=${_cpu_used:-4} +# 2021-10-20 -tile-columns and -tile-rows are for compatibility with libvpx. The +# new option for libaom is -tiles. Convert any columns/rows to tiles using 2^n. +if [[ ! -z $_tile_columns || ! -z $_tile_rows ]]; then + _tile_columns=${_tile_columns:-1} + _tile_rows=${_tile_rows:-1} + + # Compute 2^n tile columns and tile rows (the power operator is ** in bash) + _tiles="$((2 ** $_tile_columns))x$((2 ** $_tile_rows))" +else + _tiles=${_tiles:-2x2} +fi + +# 2021-02-22 use nproc number of threads unless threads is already set. +_nproc=$(nproc) +_threads=${_threads:-$_nproc} + +# Range of CRFs to try, where higher is faster, but lower quality. Based on my +# testing 52 to 40 should cover most crappy phone videos. +_crf_max=${_crf_max:-52} +_crf_min=${_crf_min:-40} + +_libaom_version=$(pacman -Qi aom | grep Version | awk '{print $3}') + +# For grainy mobile phone videos 90 is fine +_target_vmaf=${_target_vmaf:-90} +# Don't bother trying lower CRF values if the CRF score is more than five points +# above the target. In my experience each successively lower CRF step gives you +# ~1 VMAF point. In these cases it isn't likely we'll ever reach the target, so +# we might as well just settle on the current CRF straight away. +_target_vmaf_threshold=${_target_vmaf_threshold:-5} + +_acceptable_output_found="no" + +# Change to the input file's directory +pushd "$INPUT_FILE_DIRNAME" >/dev/null + +# We want the highest VMAF score possible with the highest CRF possible. Start +# with high CRF first to see if we can get an acceptable VMAF score as soon as +# possible. Step through values by 2. +for _crf in $(seq $_crf_max -2 $_crf_min); do + _processed=$(find . -maxdepth 1 -type f -iname "$_output_file" | wc -l) + if [[ $_processed -gt 0 ]]; then + echo "${INPUT_FILE_BASENAME}: already processed" + + exit 0 + fi + + if [[ $_benchmark_mode == "false" ]]; then + echo "Processing ${INPUT_FILE_BASENAME} with libaom $_libaom_version CRF ${_crf}..." + fi + + chrt -b 0 nice ffmpeg -hide_banner -y -i "$INPUT_FILE_BASENAME" -c:v libaom-av1 -b:v 0 -crf $_crf \ + -aq-mode $_aq_mode -c:a libopus -b:a 16k \ + -sc_threshold 0 -cpu-used $_cpu_used \ + -tiles $_tiles -row-mt 1 \ + -auto-alt-ref 1 -lag-in-frames 25 \ + -g 300 -threads $_threads \ + -f webm "$_output_file" 2>/dev/null + + # Return quickly if we are in benchmark mode so the benchmark script can get + # an accurate time and compute its own VMAF score. + if [[ $_benchmark_mode == "true" ]]; then + exit 0 + fi + + _vmaf_log="${_output_file/.*/}.log" + + # strip apostrophes, spaces, and commas (if any) from log file name because + # ffmpeg's log_path doesn't seem to be able to handle them. The // performs + # a global replace. + _vmaf_log="${_vmaf_log//\'}" + _vmaf_log="${_vmaf_log//\,}" + _vmaf_log="${_vmaf_log// }" + + # Get VMAF score with harmonic mean to emphasize small outliers + # See: https://netflixtechblog.com/vmaf-the-journey-continues-44b51ee9ed12 + chrt -b 0 nice ffmpeg -hide_banner -y -i "$_output_file" -i "$INPUT_FILE_BASENAME" \ + -filter_complex "libvmaf=pool=harmonic_mean:log_path=${_vmaf_log}:log_fmt=json" \ + -f null - 2>/dev/null + + _vmaf_score=$(jq '.["pooled_metrics"]["vmaf"]["harmonic_mean"]' "$_vmaf_log") + # bash can't do floating point so we use bc + # See: https://stackoverflow.com/questions/8654051/how-to-compare-two-floating-point-numbers-in-bash + _vmaf_score_round=$(printf %.$2f $(echo "scale=0;(((10^0)*$_vmaf_score)+0.5)/(10^0)" | bc)) + + rm "$_vmaf_log" + + # Check if rounded VMAF score is >= target VMAF + if [[ $_vmaf_score_round -ge $_target_vmaf ]]; then + printf "$INPUT_FILE_BASENAME: acceptable VMAF (%.3f) at AV1 CRF ${_crf}.\n" $_vmaf_score + + # Set the title with information about the encoding in the MKV title + mkvpropedit --edit info --set "title=${INPUT_FILE_BASENAME/.*/}-libaom_${_libaom_version}-crf${_crf}-cpu${_cpu_used}-${_tiles}-tiles-vmaf_${_vmaf_score}" "$_output_file" >/dev/null + + _acceptable_output_found="yes" + + # Break from the for loop because we have an acceptable output + break + # Check if the VMAF score is anywhere near our target, otherwise just keep + # current output. + elif (($_target_vmaf - $_vmaf_score_round >= $_target_vmaf_threshold)); then + printf "$INPUT_FILE_BASENAME: unacceptable VMAF (%.3f) at AV1 CRF ${_crf}, unlikely to reach target soon.\n" $_vmaf_score + + _acceptable_output_found="yes" + + # Break from the for loop and keep the current output, even though it's + # unacceptable. + break + else + printf "$INPUT_FILE_BASENAME: unacceptable VMAF (%.3f) at AV1 CRF ${_crf}, continuing.\n" $_vmaf_score + + # Clean up the unacceptable output + rm "$_output_file" + fi +done + +# If we finished going over all the CRFs and still didn't get an acceptable VMAF +# score then the video is probably just really low quality so let's just convert +# using our max CRF and be done with it. +if [[ $_acceptable_output_found == "no" ]]; then + echo "${INPUT_FILE_BASENAME}: no acceptable output found, settling on AV1 CRF ${_crf_max}." + + chrt -b 0 nice ffmpeg -hide_banner -y -i "$INPUT_FILE_BASENAME" -c:v libaom-av1 -b:v 0 -crf $_crf_max \ + -aq-mode $_aq_mode -c:a libopus -b:a 16k \ + -sc_threshold 0 -cpu-used $_cpu_used \ + -tiles $_tiles -row-mt 1 \ + -auto-alt-ref 1 -lag-in-frames 25 \ + -g 300 -threads $_threads \ + -f webm "$_output_file" 2>/dev/null + + # Set the title with information about the encoding in the MKV title + mkvpropedit --edit info --set "title=${INPUT_FILE_BASENAME/.*/}-libaom_${_libaom_version}-crf${_max_crf}-cpu${_cpu_used}-${_tiles}-tiles" "$_output_file" >/dev/null +fi + +# Change back to our starting directory +popd >/dev/null + +exit 0 diff --git a/dotfiles/local/bin/pre-process-media.sh b/dotfiles/local/bin/pre-process-media.sh new file mode 100755 index 0000000..d9f90ad --- /dev/null +++ b/dotfiles/local/bin/pre-process-media.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# +# pre-process-media.sh v2022-08-01 +# +# Prepare a directory of images and videos for long-term archival by normalizing +# their names, optimizing JPEGs with jpeg-archive, and stripping embedded MP4s +# from Android Motion Photos. +# +# SPDX-License-Identifier: GPL-3.0-only + +# Changes: +# +# v2022-08-01: fix minor syntax issue +# v2022-01-15: support Pixel panorama images +# v2021-12-26: add 'ftypiso6' for Nokia Android 9 MVIMG files, anchor regexes to +# beginning of line to prevent renaming files prematurely. + +# Exit on first error +set -o errexit + +if [[ -z "$1" ]]; then + echo "No target directory specified." + + exit 1 +fi + +echo "Preprocessing images and videos in $1" + +# Change to the input file's directory +pushd "$1" >/dev/null + +echo "Changing permissions to 640..." +find . -type f -exec chmod 640 {} \; + +# Rename IMG_20210217_204834.jpg to 2021-02-17_204834.jpg +echo "Renaming images to ISO 8601 and removing IMG_..." +perl-rename 's/^IMG_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.jpg/$1-$2-$3_$4.jpg/' *.jpg +perl-rename 's/^IMG_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.JPG/$1-$2-$3_$4.jpg/' *.JPG +perl-rename 's/^IMG_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.jpeg/$1-$2-$3_$4.jpg/' *.jpeg +perl-rename 's/^IMG_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.JPEG/$1-$2-$3_$4.jpg/' *.JPEG +# Rename 20220501_174346.jpg (Samsung A52) +perl-rename 's/^([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.jpg/$1-$2-$3_$4.jpg/' *.jpg +# Rename IMG_2180.HEIC to 2180.HEIC (iPhone) +perl-rename 's/^IMG_([0-9]{4}).HEIC/$1.heic/' *.HEIC +# Rename PXL_20210717_043834784.jpg to 2021-07-17_043834784.jpg (Pixel) +perl-rename 's/^PXL_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.jpg/$1-$2-$3_$4.jpg/' *.jpg + +# Remove embedded MP4s from Pixel motion images. Depending on the Android vers- +# ion the MP4 header could be ftypmp42, ftypmp4, ftypisom, etc. We have to check +# each one in succession, but note that grep will return a non-zero exit code +# if it doesn't find the pattern, so we need to temporarily disable errexit. To +# make matters worse, it seems some images are called MP and appear to contain +# multiple images in the Android Photos app, but don't contain an MP4! +# +# See: https://stackoverflow.com/questions/53104989/how-to-extract-the-photo-video-component-of-a-mvimg +# See: https://medium.com/android-news/working-with-motion-photos-da0aa49b50c +# See: https://linuxreviews.org/Google_Pixel_%22Motion_Photo%22 +set +o errexit +for file in PXL_*.MP.jpg MVIMG_*.jpg; do + # Don't crash when there are no files matching the glob + [ -f "$file" ] || continue + + # Check MP4 header, newer versions first + unset ofs + for header in 'ftypisom' 'ftypmp4' 'ftypmp42' 'ftypiso6'; do + ofs=$(grep -F --byte-offset --only-matching --text "$header" "$file") + + if [[ $ofs ]]; then + ofs=${ofs%:*} + truncate -s $((ofs-4)) "$file" + + # Go to next image + break + fi + done +done + +# Re-set exit on first error +set -o errexit + +# Rename PXL_20210717_043834784.MP.jpg to 2021-07-17_043834784.jpg (Pixel Motion Images) +perl-rename 's/^PXL_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.MP\.jpg/$1-$2-$3_$4.jpg/' *.jpg + +# Rename MVIMG_20190618_124507.jpg to 2019-06-18_124507.jpg (Android Motion Images) +perl-rename 's/^MVIMG_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.jpg/$1-$2-$3_$4.jpg/' *.jpg + +# Rename PXL_20210910_193159741.NIGHT.jpg to 2021-09-10_193159741.jpg (Pixel Night mode) +perl-rename 's/^PXL_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.NIGHT\.jpg/$1-$2-$3_$4.jpg/' *.jpg + +# Rename PXL_20211118_162823829.PORTRAIT.jpg to 2021-11-18_162823829.jpg (Pixel Portrait mode) +perl-rename 's/^PXL_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.PORTRAIT\.jpg/$1-$2-$3_$4.jpg/' *.jpg + +# Rename PXL_20210925_150154460.PANO.jpg to 2021-09-25_150154460.jpg (Pixel Panorama mode) +perl-rename 's/^PXL_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.PANO\.jpg/$1-$2-$3_$4.jpg/' *.jpg + +for file in *.heic; do + [ -f "$file" ] || continue + + # We are going to rename the HEIC files according to their embedded dates, + # but exiftool syntax is hard so I will just check if these files were al- + # ready renamed and skip them. These files come from iPhones. + if [[ ! $file =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{4}\.HEIC$ ]]; then + # Rename file based on DateTimeOriginal EXIF tag and the original file + # name (I removed the "IMG_" part with perl-rename first). + exiftool '-filename<${DateTimeOriginal}_${FileName}' -d %Y-%m-%d "$file" + fi +done + +# Rename VID_20210205_112539.mp4 to 2021-02-05_112539.mp4 +echo "Renaming videos to ISO 8601 and removing VID_..." +perl-rename 's/VID_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.mp4/$1-$2-$3_$4.mp4/' *.mp4 +perl-rename 's/VID_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.MP4/$1-$2-$3_$4.mp4/' *.MP4 + +# Rename 20220502_124146.mp4 (Samsung A52) +perl-rename 's/^([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.mp4/$1-$2-$3_$4.mp4/' *.mp4 + +# Rename PXL_20210714_145336054.mp4 to 2021-07-14_145336054.mp4 +perl-rename 's/PXL_([0-9]{4})([0-9]{2})([0-9]{2})_([0-9]+)\.mp4/$1-$2-$3_$4\.mp4/' *.mp4 + +# Optimize JPEGs with jpeg-archive +find . -name '*.jpg' | chrt -b 0 parallel --no-notice "jpeg-recompress -q high {} {}" + +# Change back to our starting directory +popd >/dev/null + +exit 0