Powershell version to bypass top_count var limitation in batch script
#Requires -Version 5.1
<#
===========================================================================================================================================================
HDR MaxCLL / MaxFALL scanner with percentile analysis
Description: Converts PQ signal to linear luminance via zscale, measures per-frame YMAX/YAVG with signalstats
on 16-bit precision, then computes absolute MaxCLL/MaxFALL and percentile distribution of peak values.
Percentiles help identify whether a high MaxCLL is sustained or caused by isolated specular frames.
Dependencies: FFmpeg (with zscale, signalstats filters)
===========================================================================================================================================================
#>
# --- Force UTF-8 console output for box-drawing characters ---
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Tool paths ---
$ffmpeg = "C:\FFmpeg\bin\ffmpeg.exe"
# --- Input ---
$input_path = "$env:USERPROFILE\Videos"
$input_file = "Test_8k_prores422_prob4_iris2_hyp1_p2_20Mbps_80pct_crf16.mkv"
$input = Join-Path $input_path $input_file
# --- Scan parameters ---
$scan_sec = 60
$npl = 10000
$color_range_ffmpeg = "tv"
# --- Percentile tiers to report ---
$percentiles = @(92, 96, 98, 99, 99.5, 99.9, 99.95, 99.99)
# --- Temp files (derived from source name) ---
$input_name = [System.IO.Path]::GetFileNameWithoutExtension($input_file)
$stats_file = "${input_name}_stats.tmp"
$log_file = "${input_name}_ffmpeg.log"
# --- Cleanup previous run ---
Remove-Item $stats_file, $log_file -ErrorAction SilentlyContinue
if (-not (Test-Path $input)) {
Write-Host "Error: input file not found: $input" -ForegroundColor Red
Read-Host "Press Enter to exit"; exit 1
}
Write-Host "Analysing HDR peak luminance..."
Write-Host "Source : $input"
Write-Host "Scan : ${scan_sec}s, npl=$npl, range=$color_range_ffmpeg"
Write-Host ""
# ===========================================================================================================================================================
# Stage 1 : FFmpeg signalstats on PQ-to-linear converted frames
# Description: zscale linearises the PQ EOTF (ST 2084) into scene-referred light with npl as the normalisation peak.
# signalstats then measures YMAX (brightest pixel) and YAVG (frame average) on yuv444p16le (0..65535).
# Results are dumped frame-by-frame to stats_file via metadata=mode=print.
# ===========================================================================================================================================================
$vf = "zscale=rin=${color_range_ffmpeg}:tin=smpte2084:pin=bt2020:min=bt2020nc:r=pc:t=linear:npl=${npl}," +
"format=yuv444p16le,signalstats,metadata=mode=print:file=$stats_file"
$ffmpeg_args = @(
"-threads", "0", "-filter_threads", "0", "-hide_banner", "-nostdin",
"-i", $input, "-t", $scan_sec, "-map", "0:v:0", "-an", "-sn", "-dn",
"-vf", $vf,
"-f", "null", "NUL"
)
& $ffmpeg @ffmpeg_args *> $log_file
if (-not (Test-Path $stats_file)) {
Write-Host "Error: stats file not generated. FFmpeg log:" -ForegroundColor Red
Get-Content $log_file | Write-Host
Read-Host "Press Enter to exit"; exit 1
}
# ===========================================================================================================================================================
# Stage 2 : Parse YMAX / YAVG from signalstats output
# Description: Reads all signalstats metadata lines, extracts integer part of each YMAX and YAVG value.
# Collects all per-frame YMAX into an array for sorting, tracks global max for YAVG.
# ===========================================================================================================================================================
$ymax_values = [System.Collections.Generic.List[int]]::new()
$yavg_max = 0
switch -Regex -File $stats_file {
'lavfi\.signalstats\.YMAX=(\d+)' {
$ymax_values.Add([int]$Matches[1])
}
'lavfi\.signalstats\.YAVG=(\d+)' {
$val = [int]$Matches[1]
if ($val -gt $yavg_max) { $yavg_max = $val }
}
}
$total_frames = $ymax_values.Count
$ymax_abs = ($ymax_values | Measure-Object -Maximum).Maximum
Write-Host "Frames analysed : $total_frames"
Write-Host ""
if ($total_frames -eq 0) {
Write-Host "Error: no frames parsed from stats file." -ForegroundColor Red
Read-Host "Press Enter to exit"; exit 1
}
# ===========================================================================================================================================================
# Stage 3 : Sort YMAX values descending for percentile lookup
# Description: Native numeric sort replaces the batch padding/depadding hack.
# Sorted array is indexed directly by rank for O(1) percentile access.
# ===========================================================================================================================================================
$ymax_sorted = $ymax_values | Sort-Object -Descending
# ===========================================================================================================================================================
# Stage 4 : Percentile table
# Description: For each percentile P, rank = floor(total_frames * (1 - P/100)) + 1 gives the index into the
# sorted YMAX array. Conversion to nits: nits = YMAX_16bit * npl / 65535.
# Box-drawing table with aligned columns for clean console output.
# ===========================================================================================================================================================
# Box-drawing characters
$h = [char]0x2500 # horizontal line
$v = [char]0x2502 # vertical line
$tl = [char]0x250C # top-left corner
$tr = [char]0x2510 # top-right corner
$bl = [char]0x2514 # bottom-left corner
$br = [char]0x2518 # bottom-right corner
$tj = [char]0x252C # top junction
$bj = [char]0x2534 # bottom junction
$lj = [char]0x251C # left junction
$rj = [char]0x2524 # right junction
$xj = [char]0x253C # cross junction
# Column widths: Perc=7, Rank=4, YMAX=5, Nits=5, Skipped=12
$hl = "$h"
$sep_top = "$tl$($hl*9)$tj$($hl*6)$tj$($hl*7)$tj$($hl*7)$tj$($hl*14)$tr"
$sep_mid = "$lj$($hl*9)$xj$($hl*6)$xj$($hl*7)$xj$($hl*7)$xj$($hl*14)$rj"
$sep_bot = "$bl$($hl*9)$bj$($hl*6)$bj$($hl*7)$bj$($hl*7)$bj$($hl*14)$br"
Write-Host $sep_top
Write-Host "$v Perc $v Rank $v YMAX $v Nits $v Skipped $v"
Write-Host $sep_mid
foreach ($perc in $percentiles) {
$ignored = [Math]::Floor($total_frames * (100 - $perc) / 100)
$rank = $ignored + 1
if ($rank -le $total_frames) {
$y_rank = $ymax_sorted[$rank - 1]
$nits = [Math]::Round($y_rank * $npl / 65535)
$col_perc = ("{0}%" -f $perc).PadRight(7)
$col_rank = ("~{0}" -f $rank).PadLeft(4)
$col_ymax = "$y_rank".PadLeft(5)
$col_nits = ("~{0}" -f $nits).PadLeft(5)
$col_skip = ("{0} frames" -f $ignored).PadLeft(12)
Write-Host "$v $col_perc $v $col_rank $v $col_ymax $v $col_nits $v $col_skip $v"
} else {
$col_perc = ("{0}%" -f $perc).PadRight(7)
Write-Host "$v $col_perc $v n/a $v n/a $v n/a $v n/a $v"
}
}
Write-Host $sep_bot
# ===========================================================================================================================================================
# Stage 5 : Final MaxCLL / MaxFALL computation
# Description: MaxCLL = absolute brightest pixel across all frames (global YMAX)
# MaxFALL = brightest frame-average across all frames (global YAVG max)
# Conversion: nits = Y16 * npl / 65535, rounded to nearest integer.
# ===========================================================================================================================================================
$max_cll = [Math]::Round($ymax_abs * $npl / 65535)
$max_fall = [Math]::Round($yavg_max * $npl / 65535)
Write-Host ""
Write-Host "MaxCLL absolute : $max_cll nits (YMAX16=$ymax_abs)"
Write-Host "MaxFALL absolute : $max_fall nits (YAVG16=$yavg_max)"
# --- Cleanup ---
Remove-Item $stats_file, $log_file -ErrorAction SilentlyContinue
Read-Host "Press Enter to exit"
Analysing HDR peak luminance...
Source : C:\Users\Nicolas\Videos\Test_8k_prores422_prob4_iris2_hyp1_p2_20Mbps_80pct_crf16.mkv
Scan : 7980s, npl=10000, range=tv
Frames analysed : 191330
┌─────────┬────────┬───────┬───────┬──────────────┐
│ Perc │ Rank │ YMAX │ Nits │ Skipped │
├─────────┼────────┼───────┼───────┼──────────────┤
│ 92% │ ~15307 │ 8207 │ ~1252 │ 15306 frames │
│ 96% │ ~7654 │ 8924 │ ~1362 │ 7653 frames │
│ 98% │ ~3827 │ 9699 │ ~1480 │ 3826 frames │
│ 99% │ ~1914 │ 10557 │ ~1611 │ 1913 frames │
│ 99,5% │ ~957 │ 11845 │ ~1807 │ 956 frames │
│ 99,9% │ ~192 │ 20217 │ ~3085 │ 191 frames │ <- Start of turning crazy
│ 99,95% │ ~96 │ 24191 │ ~3691 │ 95 frames │
│ 99,99% │ ~20 │ 36233 │ ~5529 │ 19 frames │
└─────────┴────────┴───────┴───────┴──────────────┘
MaxCLL absolute : 10000 nits (YMAX16=65535)
MaxFALL absolute : 1150 nits (YAVG16=7537)