Fensalir/scripts/measure-reservoir-sequence.ps1

257 lines
11 KiB
PowerShell

param(
[string]$CaptureDirectory = "",
[string]$Stamp,
[string]$ManifestPath = "",
[string]$OutputPath = ""
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
if ([string]::IsNullOrWhiteSpace($CaptureDirectory)) {
$CaptureDirectory = Join-Path $repoRoot "artifacts\fensalir-captures"
}
if ([string]::IsNullOrWhiteSpace($Stamp)) {
throw "Stamp is required, for example: -Stamp 20260529-234500"
}
$CaptureDirectory = [System.IO.Path]::GetFullPath($CaptureDirectory)
if ([string]::IsNullOrWhiteSpace($ManifestPath)) {
$ManifestPath = Join-Path $CaptureDirectory "reservoir-sequence-$Stamp-manifest.csv"
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
$OutputPath = Join-Path $CaptureDirectory "reservoir-sequence-$Stamp-metrics.md"
}
$ManifestPath = [System.IO.Path]::GetFullPath($ManifestPath)
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
$csvPath = [System.IO.Path]::ChangeExtension($OutputPath, ".csv")
if (-not (Test-Path -LiteralPath $ManifestPath)) {
throw "Missing sequence manifest: $ManifestPath"
}
Add-Type -AssemblyName System.Drawing
function Measure-ImageDelta {
param(
[string]$APath,
[string]$BPath,
[string]$Label,
[string]$AMaskPath = "",
[string]$BMaskPath = ""
)
$a = [System.Drawing.Bitmap]::new($APath)
$b = [System.Drawing.Bitmap]::new($BPath)
$aMask = if ([string]::IsNullOrWhiteSpace($AMaskPath)) { $null } else { [System.Drawing.Bitmap]::new($AMaskPath) }
$bMask = if ([string]::IsNullOrWhiteSpace($BMaskPath)) { $null } else { [System.Drawing.Bitmap]::new($BMaskPath) }
try {
if ($a.Width -ne $b.Width -or $a.Height -ne $b.Height) {
throw "Capture dimensions differ for $Label."
}
if ($aMask -ne $null -and ($aMask.Width -ne $a.Width -or $aMask.Height -ne $a.Height)) {
throw "A-mask dimensions differ for $Label."
}
if ($bMask -ne $null -and ($bMask.Width -ne $a.Width -or $bMask.Height -ne $a.Height)) {
throw "B-mask dimensions differ for $Label."
}
$changed = 0
$measured = 0
$sum = 0L
$max = 0
for ($y = 0; $y -lt $a.Height; $y++) {
for ($x = 0; $x -lt $a.Width; $x++) {
$maskActive = $true
if ($aMask -ne $null -or $bMask -ne $null) {
$maskActive = $false
if ($aMask -ne $null) {
$ma = $aMask.GetPixel($x, $y)
$maskActive = $maskActive -or ([Math]::Max([int]$ma.R, [Math]::Max([int]$ma.G, [int]$ma.B)) -gt 12)
}
if ($bMask -ne $null) {
$mb = $bMask.GetPixel($x, $y)
$maskActive = $maskActive -or ([Math]::Max([int]$mb.R, [Math]::Max([int]$mb.G, [int]$mb.B)) -gt 12)
}
}
if (-not $maskActive) {
continue
}
$ca = $a.GetPixel($x, $y)
$cb = $b.GetPixel($x, $y)
$delta = [Math]::Max(
[Math]::Abs([int]$ca.R - [int]$cb.R),
[Math]::Max(
[Math]::Abs([int]$ca.G - [int]$cb.G),
[Math]::Abs([int]$ca.B - [int]$cb.B)))
$measured++
if ($delta -gt 0) {
$changed++
}
$sum += $delta
if ($delta -gt $max) {
$max = $delta
}
}
}
$total = $a.Width * $a.Height
$denominator = if ($measured -gt 0) { $measured } else { 1 }
[pscustomobject]@{
Label = $Label
ChangedPixels = $changed
MeasuredPixels = $measured
TotalPixels = $total
ChangedPercent = $changed / $denominator
FramePercent = $measured / $total
MeanMaxChannelDelta = $sum / $denominator
MaxChannelDelta = $max
APath = $APath
BPath = $BPath
AMaskPath = $AMaskPath
BMaskPath = $BMaskPath
}
}
finally {
if ($aMask -ne $null) {
$aMask.Dispose()
}
if ($bMask -ne $null) {
$bMask.Dispose()
}
$a.Dispose()
$b.Dispose()
}
}
$captures = Import-Csv -Path $ManifestPath
$results = [System.Collections.Generic.List[object]]::new()
$byKey = @{}
foreach ($capture in $captures) {
$key = "{0}|{1}|{2}" -f $capture.Mode, $capture.ReadyFrames, $capture.Suffix
$byKey[$key] = $capture
}
function Get-PreferredMaskPath {
param(
[string]$Mode,
[string]$ReadyFrames
)
$disocclusionKey = "{0}|{1}|disocclusion" -f $Mode, $ReadyFrames
if ($byKey.ContainsKey($disocclusionKey)) {
return $byKey[$disocclusionKey].Path
}
$rejectionKey = "{0}|{1}|rejection" -f $Mode, $ReadyFrames
if ($byKey.ContainsKey($rejectionKey)) {
return $byKey[$rejectionKey].Path
}
return ""
}
$groups = $captures | Group-Object Suffix
foreach ($group in $groups) {
$suffix = $group.Name
$nativeFrames = @($group.Group |
Where-Object { $_.Mode -eq "native" } |
Sort-Object { [int]$_.ReadyFrames })
$baselineFrames = @($group.Group |
Where-Object { $_.Mode -eq "baseline" } |
Sort-Object { [int]$_.ReadyFrames })
$referenceFrames = @($group.Group |
Where-Object { $_.Mode -eq "reference" } |
Sort-Object { [int]$_.ReadyFrames })
foreach ($native in $nativeFrames) {
$baseline = $baselineFrames | Where-Object { [int]$_.ReadyFrames -eq [int]$native.ReadyFrames } | Select-Object -First 1
if ($null -ne $baseline) {
$results.Add((Measure-ImageDelta -APath $native.Path -BPath $baseline.Path -Label "$suffix mode-delta f$($native.ReadyFrames)"))
if ($suffix -eq "final") {
$nativeMask = Get-PreferredMaskPath -Mode "native" -ReadyFrames $native.ReadyFrames
$baselineMask = Get-PreferredMaskPath -Mode "baseline" -ReadyFrames $native.ReadyFrames
if (-not [string]::IsNullOrWhiteSpace($nativeMask) -or -not [string]::IsNullOrWhiteSpace($baselineMask)) {
$results.Add((Measure-ImageDelta -APath $native.Path -BPath $baseline.Path -Label "final masked-mode-delta f$($native.ReadyFrames)" -AMaskPath $nativeMask -BMaskPath $baselineMask))
}
}
}
}
foreach ($reference in $referenceFrames) {
foreach ($candidateFrames in @($nativeFrames, $baselineFrames)) {
$candidateFrames = @($candidateFrames)
$candidate = $candidateFrames | Where-Object { [int]$_.ReadyFrames -eq [int]$reference.ReadyFrames } | Select-Object -First 1
if ($null -eq $candidate) {
continue
}
$results.Add((Measure-ImageDelta -APath $candidate.Path -BPath $reference.Path -Label "$suffix reference-error-$($candidate.Mode) f$($candidate.ReadyFrames)"))
if ($suffix -eq "final") {
$candidateMask = Get-PreferredMaskPath -Mode $candidate.Mode -ReadyFrames $candidate.ReadyFrames
$referenceMask = Get-PreferredMaskPath -Mode "reference" -ReadyFrames $reference.ReadyFrames
if (-not [string]::IsNullOrWhiteSpace($candidateMask) -or -not [string]::IsNullOrWhiteSpace($referenceMask)) {
$results.Add((Measure-ImageDelta -APath $candidate.Path -BPath $reference.Path -Label "final masked-reference-error-$($candidate.Mode) f$($candidate.ReadyFrames)" -AMaskPath $candidateMask -BMaskPath $referenceMask))
}
}
}
}
foreach ($modeFrames in @($nativeFrames, $baselineFrames, $referenceFrames)) {
$modeFrames = @($modeFrames)
for ($index = 1; $index -lt $modeFrames.Count; $index++) {
$previous = $modeFrames[$index - 1]
$current = $modeFrames[$index]
$results.Add((Measure-ImageDelta -APath $previous.Path -BPath $current.Path -Label "$suffix temporal-$($current.Mode) f$($previous.ReadyFrames)-f$($current.ReadyFrames)"))
if ($suffix -eq "final") {
$previousMask = Get-PreferredMaskPath -Mode $current.Mode -ReadyFrames $previous.ReadyFrames
$currentMask = Get-PreferredMaskPath -Mode $current.Mode -ReadyFrames $current.ReadyFrames
if (-not [string]::IsNullOrWhiteSpace($previousMask) -or -not [string]::IsNullOrWhiteSpace($currentMask)) {
$results.Add((Measure-ImageDelta -APath $previous.Path -BPath $current.Path -Label "final masked-temporal-$($current.Mode) f$($previous.ReadyFrames)-f$($current.ReadyFrames)" -AMaskPath $previousMask -BMaskPath $currentMask))
}
}
}
}
}
$results | Export-Csv -NoTypeInformation -Path $csvPath
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("# Reservoir Sequence Metrics")
$lines.Add("")
$lines.Add(("Stamp: ``{0}``" -f $Stamp))
$lines.Add("")
$lines.Add("| Metric | Changed pixels | Mask/frame % | Changed % | Mean max-channel delta | Max channel delta |")
$lines.Add("| --- | ---: | ---: | ---: | ---: | ---: |")
foreach ($result in $results) {
$lines.Add(("| {0} | {1} / {2} | {3} | {4} | {5} | {6} |" -f
$result.Label,
$result.ChangedPixels,
$result.MeasuredPixels,
$result.FramePercent.ToString("P4"),
$result.ChangedPercent.ToString("P4"),
$result.MeanMaxChannelDelta.ToString("N4"),
$result.MaxChannelDelta))
}
$lines.Add("")
$lines.Add("These are sequence probes, not a final ghosting score. Mode deltas show native/baseline disagreement at each sampled time. Reference-error rows compare a candidate mode against a higher-work-grid native reference at the same ready-frame count when reference captures are present. Temporal deltas show frame-to-frame output movement per mode. Masked rows prefer disocclusion-debug captures when present, otherwise rejection-debug captures.")
Set-Content -LiteralPath $OutputPath -Value $lines -Encoding UTF8
Write-Host "Reservoir sequence metrics:"
foreach ($result in $results) {
Write-Host (" {0}: changed {1}/{2} mask/frame {3} changed {4} meanMax {5} max {6}" -f
$result.Label,
$result.ChangedPixels,
$result.MeasuredPixels,
$result.FramePercent.ToString("P4"),
$result.ChangedPercent.ToString("P4"),
$result.MeanMaxChannelDelta.ToString("N4"),
$result.MaxChannelDelta)
}
Write-Host "Wrote:"
Write-Host " $OutputPath"
Write-Host " $csvPath"