diff --git a/Netgrimoire/Nucking-Futz/Scripts/vhs_restoration.md b/Netgrimoire/Nucking-Futz/Scripts/vhs_restoration.md new file mode 100644 index 0000000..8286f1a --- /dev/null +++ b/Netgrimoire/Nucking-Futz/Scripts/vhs_restoration.md @@ -0,0 +1,531 @@ +--- +title: Video Restoration Script +description: Restore VHS Video Captures +published: true +date: 2026-03-06T03:48:05.841Z +tags: +editor: markdown +dateCreated: 2026-03-06T03:48:05.841Z +--- + +# VHS Video Restoration — User Guide + +A pipeline script for cleaning up and upscaling old VHS captures on Ubuntu 24.04. +Runs in two modes: a fast FFmpeg-only cleanup pass, and a full AI upscale using Real-ESRGAN. + +--- + +## Requirements + +- **Ubuntu 24.04** +- **FFmpeg** — `sudo apt install ffmpeg` +- **bc** — `sudo apt install bc` +- **Real-ESRGAN** (optional, for AI upscaling — see setup below) + +--- + +## File Setup + +Place everything in a working folder with this structure: + +``` +~/your-folder/ +├── vhs_restore.sh +├── realesrgan-ncnn-vulkan ← AI upscaler binary (optional) +├── models/ ← Real-ESRGAN model files +├── input/ ← Put your source videos here +├── output/ ← Restored videos appear here +└── work/ ← Temporary scratch files (auto-created) +``` + +Supported input formats: `.mpg`, `.mpeg`, `.mp4`, `.avi`, `.mov`, `.mkv`, `.wmv`, `.m4v`, `.ts` + +--- + +## First-Time Setup + +```bash +# Make the script executable +chmod +x vhs_restore.sh + +# Create the input folder and add your videos +mkdir input +cp /path/to/your/videos/*.mpg input/ +``` + +### Installing Real-ESRGAN (one-time, for AI upscaling) + +1. Download the latest Ubuntu release from: + https://github.com/xinntao/Real-ESRGAN/releases + → look for `realesrgan-ncnn-vulkan-*-ubuntu.zip` +2. Unzip into your working folder +3. `chmod +x realesrgan-ncnn-vulkan` + +--- + +## Running the Script + +### Quick cleanup only (recommended first pass) + +Fast — processes in a few minutes per file. No AI upscaling. + +```bash +./vhs_restore.sh --no-ai +``` + +### Full pipeline with AI upscaling + +Slow on CPU (plan for several hours per hour of footage). Produces the best results. + +```bash +./vhs_restore.sh +``` + +### All options + +| Flag | Description | Default | +|------|-------------|---------| +| `-i DIR` | Input directory | `./input` | +| `-o DIR` | Output directory | `./output` | +| `-w DIR` | Scratch/work directory | `./work` | +| `-b PATH` | Path to Real-ESRGAN binary | `./realesrgan-ncnn-vulkan` | +| `-s 2` or `-s 4` | Upscale factor | `2` | +| `-q 16` | Output quality (0–51, lower = better) | `16` | +| `--no-ai` | Skip AI upscaling, FFmpeg only | off | +| `--keep` | Keep extracted PNG frames after processing | off | +| `-h` | Show help | | + +**Examples:** + +```bash +# Process files from a custom folder +./vhs_restore.sh -i ~/Videos/VHS -o ~/Videos/Restored + +# 4x upscale with slightly smaller output file +./vhs_restore.sh -s 4 -q 18 + +# FFmpeg cleanup only, custom folders +./vhs_restore.sh -i ~/Videos/VHS -o ~/Videos/Restored --no-ai +``` + +--- + +## What the Script Does + +**Stage 1 — FFmpeg cleanup** (always runs): +- Deinterlaces the video (`yadif`) — removes the horizontal combing artifacts common in VHS captures +- Denoises (`hqdn3d=2:1:2:2`) — gentle noise reduction that avoids motion blocking +- Sharpens edges (`unsharp`) — recovers detail softened by the denoise step +- Colour corrects — boosts washed-out VHS colour, adjusts contrast and gamma, corrects the green/yellow cast common in aged tape + +**Stage 2 — Frame extraction** (AI mode only): +- Extracts every frame as a PNG into a temporary folder + +**Stage 3 — Real-ESRGAN upscaling** (AI mode only): +- Runs the `realesr-animevideov3` model on each frame +- Default: 2× upscale (e.g. 640×480 → 1280×960) + +**Reassembly:** +- Rebuilds the video from upscaled frames with the original audio + +--- + +## Live Progress + +The script shows live FFmpeg output. Watch for: + +- `speed=3.5x` — processing at 3.5× realtime (good) +- `speed=0.5x` — slow, likely a very heavy filter load +- `corrupt decoded frame` — normal for damaged VHS files, FFmpeg will push through + +--- + +## Troubleshooting + +**Script hangs with no output** +Run with `--no-ai` first to confirm FFmpeg is working, then check that your Real-ESRGAN binary is executable (`chmod +x realesrgan-ncnn-vulkan`). + +**Output looks blocky during motion** +The denoise values may still be too high for your footage. Edit the script and reduce `hqdn3d=2:1:2:2` to `hqdn3d=1:1:1:1`, or remove `hqdn3d` entirely — Real-ESRGAN handles noise well on its own. + +**Colour looks over-saturated** +Reduce `saturation=1.8` in the filter chain to `saturation=1.4` or `1.2`. + +**Real-ESRGAN not found** +Ensure the binary is in the same folder as the script and is executable. Or pass the path explicitly: `./vhs_restore.sh -b /path/to/realesrgan-ncnn-vulkan` + +**Error logs** +All FFmpeg and Real-ESRGAN logs are saved to `/tmp/` for diagnosis: +- `/tmp/ffmpeg_stage1.log` +- `/tmp/ffmpeg_extract.log` +- `/tmp/realesrgan.log` +- `/tmp/ffmpeg_reassemble.log` + +--- + +## Workflow Recommendation + +1. Run `--no-ai` first on one file to check the cleanup result +2. If it looks good, run the full pipeline on all files overnight +3. For heavily damaged footage, consider also running **CodeFormer** (face restoration) on top of the output — particularly effective if the video contains people + +--- + +## Output + +Restored files are saved to `./output/` as `_restored.mp4` encoded as H.264 with AAC audio. + + +## vhs_restore.sh Script + +`#!/usr/bin/env bash +# ============================================================================= +# vhs_restore.sh — Automated VHS Video Restoration Pipeline +# Stages: Deinterlace → Denoise → Colour correct → AI Upscale → Reassemble +# +# Changes from v1: +# - Gentle hqdn3d (2:1:2:2) to prevent motion blocking/pixelation +# - Aggressive colour correction for washed-out VHS footage +# - Live FFmpeg progress shown in terminal (no silent hanging) +# - Logs still saved to /tmp/ for error diagnosis +# ============================================================================= +set -euo pipefail + +# ── Colour output helpers ──────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +success() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +header() { echo -e "\n${BOLD}${CYAN}══ $* ══${NC}"; } + +# ── Default configuration ──────────────────────────────────────────────────── +INPUT_DIR="./input" # Folder containing your source VHS videos +OUTPUT_DIR="./output" # Final restored videos land here +WORK_DIR="./work" # Scratch space (frames, temp files) +REALESRGAN_BIN="./realesrgan-ncnn-vulkan" # Path to Real-ESRGAN binary +REALESRGAN_MODEL="realesr-animevideov3" # Best model for home video +UPSCALE_FACTOR=2 # 2x or 4x (4x is very slow on CPU) +OUTPUT_WIDTH=1920 # Target width used in --no-ai mode +OUTPUT_HEIGHT=1080 # Target height used in --no-ai mode +CRF=16 # Output quality 0-51, lower = better +PRESET="slow" # FFmpeg encode preset +SKIP_UPSCALE=false # --no-ai flag sets this true +KEEP_FRAMES=false # --keep flag sets this true + +# ── Parse CLI flags ────────────────────────────────────────────────────────── +usage() { + cat </dev/null; then + success "$1 found" + else + error "$1 not found. Install with: $2" + exit 1 + fi +} + +check_cmd ffmpeg "sudo apt install ffmpeg" +check_cmd ffprobe "sudo apt install ffmpeg" +check_cmd bc "sudo apt install bc" + +if [[ "$SKIP_UPSCALE" == false ]]; then + if [[ ! -x "$REALESRGAN_BIN" ]]; then + warn "Real-ESRGAN binary not found at: $REALESRGAN_BIN" + echo + echo -e "${YELLOW}To install Real-ESRGAN:${NC}" + echo " 1. Download: https://github.com/xinntao/Real-ESRGAN/releases" + echo " -> realesrgan-ncnn-vulkan-*-ubuntu.zip" + echo " 2. Unzip into this directory" + echo " 3. chmod +x realesrgan-ncnn-vulkan" + echo " 4. Re-run this script" + echo + echo "Or run with --no-ai for FFmpeg-only cleanup (no upscaling)." + exit 1 + fi + success "Real-ESRGAN found" +fi + +# ── Locate input files ─────────────────────────────────────────────────────── +header "Scanning input directory: $INPUT_DIR" + +if [[ ! -d "$INPUT_DIR" ]]; then + error "Input directory not found: $INPUT_DIR" + exit 1 +fi + +mapfile -t VIDEO_FILES < <(find "$INPUT_DIR" -maxdepth 1 \ + -type f \( -iname "*.mp4" -o -iname "*.avi" -o -iname "*.mov" \ + -o -iname "*.mkv" -o -iname "*.mpg" -o -iname "*.mpeg" \ + -o -iname "*.wmv" -o -iname "*.m4v" -o -iname "*.ts" \) \ + | sort) + +if [[ ${#VIDEO_FILES[@]} -eq 0 ]]; then + error "No video files found in $INPUT_DIR" + exit 1 +fi + +info "Found ${#VIDEO_FILES[@]} video file(s):" +for f in "${VIDEO_FILES[@]}"; do echo " * $(basename "$f")"; done + +# ── Helpers ────────────────────────────────────────────────────────────────── +probe() { + ffprobe -v error -select_streams v:0 \ + -show_entries "stream=$2" -of csv=p=0 "$1" 2>/dev/null | head -1 +} + +human_time() { + local s="${1%.*}" + printf '%dh %dm %ds' $((s/3600)) $(( (s%3600)/60 )) $((s%60)) +} + +# ── Create directories ─────────────────────────────────────────────────────── +mkdir -p "$OUTPUT_DIR" "$WORK_DIR" + +# ── Overall stats ──────────────────────────────────────────────────────────── +TOTAL_FILES=${#VIDEO_FILES[@]} +PROCESSED=0 +FAILED=0 +PIPELINE_START=$(date +%s) + +# ════════════════════════════════════════════════════════════════════════════ +# MAIN LOOP +# ════════════════════════════════════════════════════════════════════════════ +for INPUT_FILE in "${VIDEO_FILES[@]}"; do + + BASENAME=$(basename "$INPUT_FILE") + STEM="${BASENAME%.*}" + CLEANED="$WORK_DIR/${STEM}_cleaned.mp4" + FRAMES_IN="$WORK_DIR/${STEM}_frames_in" + FRAMES_OUT="$WORK_DIR/${STEM}_frames_out" + FINAL_OUTPUT="$OUTPUT_DIR/${STEM}_restored.mp4" + + header "Processing: $BASENAME ($((PROCESSED+1))/$TOTAL_FILES)" + FILE_START=$(date +%s) + + # ── Probe source ────────────────────────────────────────────────────────── + FPS=$(probe "$INPUT_FILE" "r_frame_rate") + FPS_DEC=$(echo "scale=3; $FPS" | bc 2>/dev/null || echo "25") + WIDTH=$(probe "$INPUT_FILE" "width") + HEIGHT=$(probe "$INPUT_FILE" "height") + FIELD_ORDER=$(probe "$INPUT_FILE" "field_order") + DURATION=$(ffprobe -v error -show_entries format=duration \ + -of csv=p=0 "$INPUT_FILE" 2>/dev/null | head -1) + + info "Source: ${WIDTH}x${HEIGHT} ${FPS_DEC}fps $(human_time "${DURATION%.*}") field_order=${FIELD_ORDER:-unknown}" + + # Always deinterlace for VHS -- safe even if not flagged as interlaced + if [[ "$FIELD_ORDER" =~ ^(tt|tb|bt|bb)$ ]]; then + DEINTERLACE_FILTER="yadif=mode=1," + info "Interlacing detected — applying yadif deinterlacer" + else + DEINTERLACE_FILTER="yadif=mode=1," + warn "Interlacing not confirmed by probe — applying yadif anyway (safe for VHS)" + fi + + # ── Stage 1: FFmpeg cleanup ─────────────────────────────────────────────── + header "Stage 1/3 — FFmpeg cleanup & colour correction" + info "Watch fps= and speed= for live progress." + info "Corrupt frame warnings are normal for old VHS captures." + echo + + if [[ "$SKIP_UPSCALE" == true ]]; then + SCALE_FILTER="scale=${OUTPUT_WIDTH}:${OUTPUT_HEIGHT}:flags=lanczos," + else + SCALE_FILTER="" + fi + + # Filter chain notes: + # hqdn3d=2:1:2:2 -- gentle denoise; low temporal values (3rd/4th) + # prevent the motion blocking seen with higher values + # unsharp -- moderate sharpening to recover edge detail + # eq -- aggressive colour boost for washed-out VHS + # colorbalance -- corrects the green/yellow cast common in aged VHS + VFILTER="${DEINTERLACE_FILTER}\ +hqdn3d=2:1:2:2,\ +unsharp=3:3:0.5:3:3:0.3,\ +eq=contrast=1.2:brightness=0.05:saturation=1.8:gamma=1.1,\ +colorbalance=rs=0.1:gs=0.0:bs=-0.1,\ +${SCALE_FILTER}\ +format=yuv420p" + + if ! ffmpeg -y -i "$INPUT_FILE" \ + -vf "$VFILTER" \ + -c:v libx264 -crf 18 -preset medium \ + -c:a aac -b:a 192k -ac 2 \ + -stats \ + "$CLEANED" 2>&1 | tee /tmp/ffmpeg_stage1.log | \ + grep --line-buffered -E "(frame=|speed=|error|Error|Invalid)"; then + error "FFmpeg stage 1 failed. Full log: /tmp/ffmpeg_stage1.log" + FAILED=$((FAILED+1)) + continue + fi + + echo + success "Stage 1 complete -> $(du -sh "$CLEANED" | cut -f1)" + + if [[ "$SKIP_UPSCALE" == true ]]; then + cp "$CLEANED" "$FINAL_OUTPUT" + success "Output (no AI): $FINAL_OUTPUT" + PROCESSED=$((PROCESSED+1)) + [[ "$KEEP_FRAMES" == false ]] && rm -f "$CLEANED" + continue + fi + + # ── Stage 2: Extract frames ─────────────────────────────────────────────── + header "Stage 2/3 — Extracting frames for AI upscaling" + mkdir -p "$FRAMES_IN" "$FRAMES_OUT" + + FRAME_COUNT=$(ffprobe -v error -count_packets \ + -select_streams v:0 -show_entries stream=nb_read_packets \ + -of csv=p=0 "$CLEANED" 2>/dev/null | head -1) + FRAME_COUNT=${FRAME_COUNT:-0} + info "Extracting ~${FRAME_COUNT} frames..." + + if ! ffmpeg -y -i "$CLEANED" \ + -vsync 0 -stats \ + "$FRAMES_IN/frame%08d.png" 2>&1 | tee /tmp/ffmpeg_extract.log | \ + grep --line-buffered -E "(frame=|speed=|error|Error)"; then + error "Frame extraction failed. Full log: /tmp/ffmpeg_extract.log" + FAILED=$((FAILED+1)) + continue + fi + + ACTUAL_FRAMES=$(find "$FRAMES_IN" -name "*.png" | wc -l) + echo + success "Extracted $ACTUAL_FRAMES frames" + + # ── Stage 3: Real-ESRGAN ────────────────────────────────────────────────── + header "Stage 3/3 — Real-ESRGAN AI upscaling (${UPSCALE_FACTOR}x)" + warn "Slow on CPU — est. $(echo "scale=0; $ACTUAL_FRAMES * 10 / 60" | bc)-$(echo "scale=0; $ACTUAL_FRAMES * 30 / 60" | bc) minutes" + info "Upscaled frames will appear in: $FRAMES_OUT" + echo + + UPSCALE_START=$(date +%s) + if ! "$REALESRGAN_BIN" \ + -i "$FRAMES_IN" \ + -o "$FRAMES_OUT" \ + -n "$REALESRGAN_MODEL" \ + -s "$UPSCALE_FACTOR" \ + -f png 2>&1 | tee /tmp/realesrgan.log; then + error "Real-ESRGAN failed. Full log: /tmp/realesrgan.log" + FAILED=$((FAILED+1)) + continue + fi + + UPSCALE_END=$(date +%s) + UPSCALE_ELAPSED=$((UPSCALE_END - UPSCALE_START)) + success "AI upscaling complete in $(human_time $UPSCALE_ELAPSED)" + + # ── Reassemble ──────────────────────────────────────────────────────────── + REASSEMBLE_FPS=$(ffprobe -v error -select_streams v:0 \ + -show_entries stream=r_frame_rate \ + -of csv=p=0 "$CLEANED" 2>/dev/null | head -1) + + info "Reassembling video from upscaled frames..." + echo + + if ! ffmpeg -y \ + -framerate "$REASSEMBLE_FPS" \ + -i "$FRAMES_OUT/frame%08d.png" \ + -i "$CLEANED" \ + -map 0:v -map 1:a \ + -c:v libx264 -crf "$CRF" -preset "$PRESET" \ + -c:a copy \ + -movflags +faststart \ + -stats \ + "$FINAL_OUTPUT" 2>&1 | tee /tmp/ffmpeg_reassemble.log | \ + grep --line-buffered -E "(frame=|speed=|error|Error)"; then + error "Reassembly failed. Full log: /tmp/ffmpeg_reassemble.log" + FAILED=$((FAILED+1)) + continue + fi + + # ── Cleanup ─────────────────────────────────────────────────────────────── + if [[ "$KEEP_FRAMES" == false ]]; then + rm -rf "$FRAMES_IN" "$FRAMES_OUT" "$CLEANED" + info "Scratch files cleaned up" + else + info "Frames kept in: $FRAMES_IN / $FRAMES_OUT" + fi + + FILE_END=$(date +%s) + FILE_ELAPSED=$((FILE_END - FILE_START)) + PROCESSED=$((PROCESSED+1)) + + OUT_SIZE=$(du -sh "$FINAL_OUTPUT" | cut -f1) + echo + success "Done: $FINAL_OUTPUT" + info " File size : $OUT_SIZE" + info " Time taken: $(human_time $FILE_ELAPSED)" + +done + +# ════════════════════════════════════════════════════════════════════════════ +# Final summary +# ════════════════════════════════════════════════════════════════════════════ +PIPELINE_END=$(date +%s) +PIPELINE_ELAPSED=$((PIPELINE_END - PIPELINE_START)) + +header "Pipeline Complete" +echo -e " ${GREEN}Processed : $PROCESSED / $TOTAL_FILES${NC}" +[[ $FAILED -gt 0 ]] && echo -e " ${RED}Failed : $FAILED${NC}" +echo -e " Total time: $(human_time $PIPELINE_ELAPSED)" +echo -e " Output dir: $OUTPUT_DIR" +echo + +if [[ $PROCESSED -gt 0 ]]; then + echo "Restored files:" + find "$OUTPUT_DIR" -name "*_restored.mp4" | while read -r f; do + SIZE=$(du -sh "$f" | cut -f1) + echo " * $(basename "$f") ($SIZE)" + done +fi +` + + + + + + +