8 votes

Detect video noise using FFMpeg

Hi Folks

I've been working on an autoconversion Bash script to pick up videos and convert them to AV1. Yes, I know converting a source to another source means degradation and yada yada yada, but it's something I can live with as most of my sources are of very high quality to begin with, and I'm going for space-saving. Plus, my eyes aren't what they once were.

The conversion into AV1 I'm mostly happy with. I'm currently going through some old 90s shows which are of lesser quality, so they would need a little help to look better with AV1 by adding some natural film grain, else AV1 makes them look a little bit too clean.

I can easily pop into the script and enable film grain in the variables, or add in a simple option, but that's boring and tedious. Why do that when we can automate the world :)

Where I have got to is using the signalstats filter. The issue I have is I don't know how to analyse it's output enough to work out whether I should or shouldn't enable film grain or not. I know it's subjective either way.

Does anyone have experience with this? The output per frame looks like:

frame:1438 pts:59977 pts_time:59.977
lavfi.signalstats.YMIN=0
lavfi.signalstats.YLOW=0
lavfi.signalstats.YAVG=60.6913
lavfi.signalstats.YHIGH=149
lavfi.signalstats.YMAX=239
lavfi.signalstats.UMIN=91
lavfi.signalstats.ULOW=108
lavfi.signalstats.UAVG=121.955
lavfi.signalstats.UHIGH=131
lavfi.signalstats.UMAX=148
lavfi.signalstats.VMIN=124
lavfi.signalstats.VLOW=128
lavfi.signalstats.VAVG=134.535
lavfi.signalstats.VHIGH=146
lavfi.signalstats.VMAX=154
lavfi.signalstats.SATMIN=0
lavfi.signalstats.SATLOW=0
lavfi.signalstats.SATAVG=9.74682
lavfi.signalstats.SATHIGH=27
lavfi.signalstats.SATMAX=43
lavfi.signalstats.HUEMED=147
lavfi.signalstats.HUEAVG=162.949
lavfi.signalstats.YDIF=0.737433
lavfi.signalstats.UDIF=0.642897
lavfi.signalstats.VDIF=0.162755
lavfi.signalstats.YBITDEPTH=8
lavfi.signalstats.UBITDEPTH=8
lavfi.signalstats.VBITDEPTH=8

I'm happy to analyse a random 60-second segment and then grab an average from a couple of these outputs, but I'm not sure if this is a good method or not. I'm asked a couple of the biggest LLMs, they have come back with older ways that no longer exist in ffmpeg 7.1.

I'm trying not to use too many other pieces of software in this script. The dependencies are fairly simple with ffmpeg, awk, grep, etc., the kind of thing you get on nearly every distro of Linux. Any thoughts and/or ideas?

1 comment

  1. g33kphr33k
    Link
    I think I have cured my issue already. I moved to use PSNR and found a half decent write up on values. # Detect if film grain should be used and set sane values detect_film_grain() { local...

    I think I have cured my issue already. I moved to use PSNR and found a half decent write up on values.

    # Detect if film grain should be used and set sane values
    detect_film_grain() {
        local file="$1"
        local sample_duration=10
        
        log "Analyzing source material for grain profile..." "33" true
    
        # Clear the temp file first
        local temp_file="/tmp/noise_analysis.txt"
        > "$temp_file"
    
        # Run ffmpeg with reduced output
        ffmpeg -hide_banner -i "$file" -t "$sample_duration" \
            -filter_complex "[0:v]split=2[ref][dist];[dist]framerate=fps=24000/1001[dist1];[ref][dist1]psnr=stats_file=-" \
            -f null - 2>&1 | grep "PSNR" > "$temp_file"
    
        # Extract detailed PSNR values
        local psnr_line=$(grep 'PSNR.*average' "$temp_file")
        local psnr_avg=$(echo "$psnr_line" | grep -oP 'average:\K[0-9.]+')
        local psnr_y=$(echo "$psnr_line" | grep -oP 'y:\K[0-9.]+')
        local psnr_min=$(echo "$psnr_line" | grep -oP 'min:\K[0-9.]+')
    
        # Full details to log only
        log "Analysis Details:" "33"
        log "Average PSNR: $psnr_avg" "33"
        log "Luma PSNR (Y): $psnr_y" "33"
        log "Minimum PSNR: $psnr_min" "33"
    
        # Determine source type and grain settings
        local source_type=""
        if (( $(echo "$psnr_avg > 55" | bc -l) )); then
            source_type="Very clean digital source (modern)"
            svt_film_grain=0
        elif (( $(echo "$psnr_avg > 45" | bc -l) )); then
            source_type="Clean digital source"
            svt_film_grain=2
        elif (( $(echo "$psnr_avg > 35" | bc -l) )); then
            source_type="Good quality source with natural noise"
            svt_film_grain=5
        elif (( $(echo "$psnr_avg > 30" | bc -l) )); then
            source_type="Source contains noticeable grain"
            svt_film_grain=8
        else
            source_type="High grain/noise content"
            svt_film_grain=12
        fi
    
        # Console output - brief and clear
        echo "Source Analysis:"
        echo "→ Content type: $source_type"
        echo "→ Setting grain strength to: $svt_film_grain"
        echo
    
        return 0
    }
    

    I do wonder if the grain levels are good, so let me know if you think those need a tweak.

    One thing that it does seriously impact is encoding time. Shows that usually encode at 2-3x speed suddenly drop off to 0.886x-0.6x encode speed. This is all on the CPU and I only have an AMD Ryzen 5 5600X 6-Core Processor.

    2 votes