Compare commits

..

6 Commits

7 changed files with 441 additions and 2 deletions
@@ -0,0 +1,78 @@
# M00-QAR-CAMERA-POSE-EVIDENCE-001 — Goldshire camera pose evidence
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-camera-pose-evidence`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Produce coarse-to-fine yaw/pitch evidence for the build 12340 Goldshire Inn reference and classify whether framing dominates the paired-image gap.
## Non-goals
- Changing manifest camera defaults before human approval.
- Treating minimum perceptual error as proof of exact camera parity.
- Committing proprietary reference images or generated candidates.
## Paths
- Exclusive: camera-pose evidence record
- Shared/hotspots: renderer baseline and module fidelity findings
- Generated/ignored: all sweep PNGs and JSON under `user://`
## Contracts and data
- Public API/events: none
- Schema/format version: unchanged
- Migration/compatibility: none
- Consumers: M00 fidelity review
## Dependencies
- Requires: merged camera-pose sweep and local build 12340 reference
- Blocks: classification of the Goldshire WMO paired gap
- External state: `sources/OpenWCReferenceCheckpoints/goldshire_inn_large_wmo.jpg`
## Verification
- Commands: coarse and fine real sweeps, ranking inspection, repository gates
- Fixtures: private Goldshire Inn build 12340 screenshot
- Fidelity evidence: ranked mean perceptual error and changed-pixel ratio
- Performance budget: offline diagnostic only
## Documentation deliverables
- Inline public API docs: unchanged
- Module specification: fidelity finding and remaining risk
- Data-flow diagram: unchanged
- Sequence/state/dependency diagrams: unchanged
- Source map/status updates: baseline evidence
## Simplicity and naming
- Important names introduced: none
- Simplest considered solution: use the merged bounded grid runner
- Rejected complexity/abstractions: optimizer or renderer changes
- Unavoidable complexity and justification: real images must be rendered for each candidate
- Measured optimization evidence: single pass per candidate
## Status
- State: ready-for-review
- Done: private reference dimensions verified; coarse, extended and limit sweeps completed; representative candidates inspected; runner failure handling hardened
- Next: investigate Goldshire Inn spatial/placement composition separately; do not change manifest yaw/pitch from this metric
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: zero offset mean error 0.099632; coarse best (-10,-20) 0.077575; metric decreased monotonically to (0,-60) 0.063574/changed ratio 0.549556, while visual inspection showed only grass and no inn
- Remaining risks: full-frame color error cannot register a missing landmark; WMO placement/streaming at this checkpoint remains unresolved
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`, `docs/modules/world-renderer.md`
@@ -0,0 +1,78 @@
# M00-QAR-CAMERA-POSE-SWEEP-001 — Empirical camera pose sweep
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-SWEEP-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-camera-pose-sweep`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Recover reproducible checkpoint framing by sweeping bounded yaw/pitch offsets and ranking paired-image error.
## Non-goals
- Changing runtime player camera defaults.
- Claiming an exact original-client camera pose from perceptual score alone.
- Adding computer-vision registration or image warping.
## Paths
- Exclusive: camera-pose CLI and sweep orchestration
- Shared/hotspots: renderer baseline documentation
- Generated/ignored: local candidate PNGs and JSON reports
## Contracts and data
- Public API/events: additive camera yaw/pitch offset and single-pass capture options; new PowerShell sweep command
- Schema/format version: capture report schema remains version 1 with additive camera-offset fields
- Migration/compatibility: existing capture commands retain zero offsets and two passes
- Consumers: M00 paired-fidelity workflow
## Dependencies
- Requires: build 12340 reference image, capture tool, paired-image comparator
- Blocks: reproducible manual-reference framing
- External state: original screenshots remain outside Git
## Verification
- Commands: capture dry-run offset regression, comparator self-test, bounded local sweep when a display/reference is available, repository gates
- Fixtures: one named M00 checkpoint
- Fidelity evidence: ranked yaw/pitch candidates with explicit human-approval requirement
- Performance budget: offline diagnostic; single-pass mode avoids redundant warm capture
## Documentation deliverables
- Inline public API docs: capture and sweep usage
- Module specification: camera-pose data flow, verification and source map
- Data-flow diagram: updated pose-sweep flow
- Sequence/state/dependency diagrams: unchanged
- Source map/status updates: baseline workflow and findings
## Simplicity and naming
- Important names introduced: `camera_yaw_offset_degrees`, `camera_pitch_offset_degrees`, `single_pass`
- Simplest considered solution: bounded grid over existing capture/comparator tools
- Rejected complexity/abstractions: feature matching, optimizer framework, image transforms
- Unavoidable complexity and justification: original-client camera angles are not exposed by tested APIs
- Measured optimization evidence: single-pass mode halves captures per candidate
## Status
- State: ready-for-review
- Done: additive capture offsets, single-pass calibration mode, bounded grid runner, ranked JSON contract, grid-plan regression and documentation
- Next: run a coarse-to-fine real sweep with the private build 12340 reference directory, then obtain human framing approval
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: dry-run reported yaw 12.50/pitch -7.50; comparator self-test passed; a 3x2 plan produced six unique candidates; full seven-checkpoint M00 dry-run passed with default zero offsets
- Remaining risks: no private reference directory was available in this worktree, so real perceptual ranking and human approval remain external evidence
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`; renderer module verification, risk and source map
@@ -0,0 +1,78 @@
# M00-QAR-WMO-READINESS-001 — Goldshire WMO readiness
<!-- OPENWC_CLAIM:M00-QAR-WMO-READINESS-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-wmo-readiness`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Prevent pose calibration against an under-loaded WMO scene and replace invalid Goldshire evidence with a baseline-ready comparison.
## Non-goals
- Changing runtime streaming budgets.
- Claiming all WMO queues must globally drain before a checkpoint.
- Changing manifest camera pose without valid paired evidence.
## Paths
- Exclusive: pose-sweep readiness default and Goldshire timing evidence
- Shared/hotspots: renderer baseline/module fidelity findings
- Generated/ignored: timing and sweep images under `user://`
## Contracts and data
- Public API/events: pose sweep default wait becomes 8 seconds, matching the M00 manifest
- Schema/format version: unchanged
- Migration/compatibility: explicit `-WaitSeconds` remains supported
- Consumers: M00 paired-fidelity workflow
## Dependencies
- Requires: Goldshire pose evidence and private build 12340 reference
- Blocks: valid Goldshire camera-pose ranking
- External state: private reference and generated captures remain outside Git
## Verification
- Commands: 2/8/15 second timing audit, ready-scene 3x3 pose sweep, baseline dry-run, repository gates
- Fixtures: private Goldshire Inn screenshot
- Fidelity evidence: visual WMO presence plus streaming snapshot and paired metrics
- Performance budget: offline diagnostic
## Documentation deliverables
- Inline public API docs: PowerShell default
- Module specification: corrected readiness/fidelity finding
- Data-flow diagram: readiness precondition
- Sequence/state/dependency diagrams: unchanged
- Source map/status updates: camera-pose operational guide
## Simplicity and naming
- Important names introduced: none
- Simplest considered solution: reuse the manifest's measured 8-second stabilization default
- Rejected complexity/abstractions: global queue barrier that would include unrelated distant work
- Unavoidable complexity and justification: WMO publication is asynchronous
- Measured optimization evidence: 2/8/15 second snapshots
## Status
- State: ready-for-review
- Done: timing audit invalidated the 2-second evidence; sweep default now matches the 8-second manifest; ready-scene 3x3 grid and human inspection completed
- Next: jointly refine negative pitch and FOV around the ready-scene Goldshire candidate
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: WMO instances 9/113/328 at 2/8/15 seconds; inn absent at 2 and visible at 8/15; ready grid improved mean error from 0.101402 at (0,0) to 0.087952 at (10,-10)
- Remaining risks: global WMO queues still contain unrelated work; best pitch is at the tested boundary and building scale indicates unresolved FOV/framing
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`, `docs/modules/world-renderer.md`
+50
View File
@@ -0,0 +1,50 @@
# Renderer Camera Pose Sweep
This offline M00 diagnostic recovers reproducible checkpoint framing when an original-client screenshot has no recorded camera yaw or pitch. It ranks a bounded grid; it does not change renderer or player-camera defaults and does not prove the exact build 12340 camera pose without human approval.
## Camera contract
`capture_render_checkpoints.gd` accepts additive `--camera-yaw-offset` and `--camera-pitch-offset` values in degrees. Yaw rotates the no-roll checkpoint basis around Godot world `Vector3.UP`; pitch then rotates around the camera-local right axis. Zero offsets preserve the manifest target exactly. `--single-pass` captures only `cold_process` and is intended for pose calibration, not performance baselines.
## Workflow
```powershell
.\tools\sweep_render_checkpoint_camera_pose.ps1 `
-ReferenceDirectory 'D:\private-fixtures\wow-3.3.5a-checkpoints' `
-Checkpoint elwynn_adt_boundary `
-YawOffsets -15,-10,-5,0,5,10,15 `
-PitchOffsets -10,-5,0,5,10 `
-ViewportWidth 1280 -ViewportHeight 960 `
-WaitSeconds 8
```
Each candidate receives its own output directory and comparison report. The viewport must exactly match the reference image dimensions; the runner fails on `size_mismatch` instead of inventing a score. The default 8-second wait matches the M00 manifest and is a readiness precondition: lowering it requires evidence that the checkpoint's WMO/M2 content has already appeared. `ranking.json` sorts candidates by mean perceptual error and then changed-pixel ratio. Use `-PlanOnly` to validate the Cartesian grid and output paths without rendering. Run a coarse grid first, then a finer grid around the best candidate.
```mermaid
flowchart LR
Y[Yaw offsets in degrees] --> G[Bounded Cartesian grid]
P[Pitch offsets in degrees] --> G
G --> C[Single-pass checkpoint captures]
R[Original-client reference] --> D[Perceptual comparator]
C --> D
D --> J[ranking.json]
J --> H[Human framing approval]
```
## Interpretation and recovery
The smallest error is a candidate, not automatic fidelity approval. Geometry, materials, lighting and FOV can move the perceptual optimum away from the true original-client pose. Inspect the best images manually and retain the original reference outside Git. A missing matching reference or candidate is a hard error; correct the checkpoint filter or filenames and rerun. Generated PNG and JSON outputs are disposable local evidence.
## Verification
The headless capture dry-run must report requested yaw and pitch offsets. Comparator `--self-test` covers paired metrics. Sweep `-PlanOnly` covers deterministic grid expansion and output naming without requiring a display. A real ranked sweep requires a display and a private build 12340 reference image.
## Goldshire Inn evidence — 2026-07-12
The private `2560x1440` build 12340 `goldshire_inn_large_wmo` reference was evaluated at FOV 62 degrees. A coarse 5x5 grid over yaw/pitch `[-20, -10, 0, 10, 20]` reduced mean error from `0.099632` at `(0, 0)` to `0.077575` at `(-10, -20)`. Extending pitch produced `0.070901` at `(0, -40)` and `0.063574` at `(0, -60)` with changed-pixel ratio `0.549556`.
These values are invalid camera-pose evidence because the sweep used a 2-second wait. A subsequent readiness audit showed 9 WMO instances and no inn after 2 seconds, 113 instances with the inn visible after 8 seconds, and 328 instances after 15 seconds. The apparent monotonic improvement rewarded grass while the landmark was still absent; it does not demonstrate a spatial/placement defect. The manifest pose remains unchanged pending a ready-scene rerun.
The ready-scene 3x3 rerun at FOV 62 degrees ranked `(yaw=10, pitch=-10)` first with mean error `0.087952` and changed-pixel ratio `0.665527`, compared with `0.101402`/`0.702489` at zero offsets. Human inspection confirms that the inn is present and the direction is plausible. Pitch remains on the tested boundary and the building scale differs from the reference, so these offsets are coarse evidence only; a joint pitch/FOV refinement is required before changing manifest camera values.
During this run the sweep orchestration was hardened in two ways: expected comparator exit code `1` is now collected through an explicit child process, and viewport dimensions are explicit. A reference/candidate `size_mismatch` now fails before ranking instead of producing an empty metric.
+6 -1
View File
@@ -195,7 +195,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Verification
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff.
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes and seven cold/warm checkpoints.
- Fidelity evidence: пять локальных build 12340 reference JPG откалибровали terrain/ADT/M2/WMO/liquid viewpoints; automated paired-image metrics exist, но synthetic animation/dusk и полный human approval ещё не закрыты.
- Performance budgets: M00 report records cold/warm p95 and max hitch; no final acceptance threshold yet.
@@ -229,6 +229,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- Terrain-height probe исключил under-terrain состояние для всех пяти точек; waterfall exact-XZ miss классифицирован как TriangleMesh seam/edge и подтверждён nearby sample в 2 units.
- Camera-occluder probe не нашёл camera containment в пяти точках; paired mismatch локализован прежде всего в manual look direction/target/FOV calibration, с явным ограничением по RID-only geometry.
- Empirical FOV sweep выявил, что checkpoint camera должна явно вызывать `make_current()`; после исправления projection ranking остаётся inconclusive из-за неизвестного manual yaw/pitch/framing reference.
- Camera-pose sweep can now rank bounded yaw/pitch grids without changing manifest defaults; perceptual ranking remains diagnostic and requires human framing approval.
- The first Goldshire pose grid was invalidated by WMO readiness: the inn is absent after 2 seconds but visible after the manifest-standard 8-second wait. Pose comparisons must stabilize asynchronous checkpoint content first.
- A ready-scene Goldshire 3x3 grid improved mean error from `0.101402` at zero offsets to `0.087952` at yaw `10`/pitch `-10`; pitch/FOV refinement remains required before manifest calibration.
- D3D12 descriptor issues remain; the capture-path anonymous `RefCounted` shutdown leak is regression-covered by a clean verbose dry-run, while other RID/resource diagnostics still require independent evidence.
- M2/WMO/material/particle/ribbon/portal parity incomplete.
- Public API is mostly exported configuration rather than stable contracts.
@@ -248,6 +251,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `tools/run_render_baseline.ps1` | Unified M00 baseline runner |
| `src/tools/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
| `src/tools/probe_render_terrain_height.gd` | Offline active-mesh terrain height and camera-clearance report |
| `src/tools/probe_render_camera_occluders.gd` | Scene-tree placement containment and camera-to-target AABB intersection report |
@@ -256,5 +260,6 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- [`../../RENDER.md`](../../RENDER.md)
- [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md)
- [`../CAMERA_POSE_SWEEP.md`](../CAMERA_POSE_SWEEP.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
+16 -1
View File
@@ -24,6 +24,9 @@ func _capture_async() -> void:
var revision := _arg(args, "--revision", "worktree")
var cache_state := _arg(args, "--cache-state", "existing")
var camera_fov_override := float(_arg(args, "--camera-fov", str(manifest.get("camera_fov", 62.0))))
var camera_yaw_offset_degrees := float(_arg(args, "--camera-yaw-offset", "0.0"))
var camera_pitch_offset_degrees := float(_arg(args, "--camera-pitch-offset", "0.0"))
var single_pass := args.has("--single-pass")
var headless := DisplayServer.get_name().to_lower() == "headless"
var dry_run := args.has("--dry-run") or headless
var viewport: Array = manifest.get("viewport", [1280, 900])
@@ -86,6 +89,8 @@ func _capture_async() -> void:
"dry_run": dry_run,
"cache_state": cache_state,
"camera_fov": camera_fov_override,
"camera_yaw_offset_degrees": camera_yaw_offset_degrees,
"camera_pitch_offset_degrees": camera_pitch_offset_degrees,
"environment": _environment_metadata(),
"comparison_budgets": manifest.get("comparison_budgets", {}),
"cache_contract": manifest.get("cache_contract", {}),
@@ -94,6 +99,8 @@ func _capture_async() -> void:
}
var captured := 0
var passes := ["cold_process", "warm_revisit"]
if single_pass:
passes = ["cold_process"]
if dry_run:
passes = ["dry_run"]
@@ -114,6 +121,7 @@ func _capture_async() -> void:
camera.global_position = _vector3(checkpoint.get("camera", [0.0, 0.0, 0.0]))
_orient_camera_without_roll(camera, _vector3(checkpoint.get("target", [0.0, 0.0, 0.0])))
_apply_camera_pose_offsets(camera, camera_yaw_offset_degrees, camera_pitch_offset_degrees)
camera.make_current()
if player != null:
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
@@ -122,11 +130,13 @@ func _capture_async() -> void:
world.call("_refresh_streaming_targets_at", camera.global_position, true)
if dry_run:
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s time=%.2f" % [
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s yaw_offset=%.2f pitch_offset=%.2f time=%.2f" % [
checkpoint_name,
str(checkpoint.get("coverage", [])),
camera.global_position,
_vector3(checkpoint.get("target", [0.0, 0.0, 0.0])),
camera_yaw_offset_degrees,
camera_pitch_offset_degrees,
float(checkpoint.get("time_hours", 13.0)),
])
(report["results"] as Array).append(_result_record(checkpoint, pass_name, 0.0, {}, ""))
@@ -215,6 +225,11 @@ func _orient_camera_without_roll(camera: Camera3D, target_position: Vector3) ->
camera.global_basis = Basis(right, corrected_up, -forward)
func _apply_camera_pose_offsets(camera: Camera3D, yaw_offset_degrees: float, pitch_offset_degrees: float) -> void:
camera.global_rotate(Vector3.UP, deg_to_rad(yaw_offset_degrees))
camera.rotate_object_local(Vector3.RIGHT, deg_to_rad(pitch_offset_degrees))
func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: float, metrics: Dictionary, sha256: String) -> Dictionary:
return {
"name": checkpoint.get("name", "checkpoint"),
@@ -0,0 +1,135 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReferenceDirectory,
[Parameter(Mandatory = $true)]
[string]$Checkpoint,
[string]$GodotPath,
[string]$Output = "user://render_camera_pose_sweep",
[double[]]$YawOffsets = @(-10.0, 0.0, 10.0),
[double[]]$PitchOffsets = @(-10.0, 0.0, 10.0),
[double]$CameraFov = 62.0,
[int]$ViewportWidth = 1280,
[int]$ViewportHeight = 900,
[double]$WaitSeconds = 8.0,
[double]$MeasureSeconds = 0.1,
[switch]$PlanOnly
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
if (-not $GodotPath) {
$godotCommand = Get-Command godot -ErrorAction SilentlyContinue
if ($godotCommand) {
$GodotPath = $godotCommand.Source
} else {
$GodotPath = Join-Path $env:TEMP "godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe"
}
}
if (-not (Test-Path -LiteralPath $GodotPath)) {
throw "Godot executable not found: $GodotPath"
}
if (-not (Test-Path -LiteralPath $ReferenceDirectory)) {
throw "Reference directory not found: $ReferenceDirectory"
}
$invariantCulture = [Globalization.CultureInfo]::InvariantCulture
$cameraFovText = $CameraFov.ToString($invariantCulture)
$waitSecondsText = $WaitSeconds.ToString($invariantCulture)
$measureSecondsText = $MeasureSeconds.ToString($invariantCulture)
$absoluteOutput = if ($Output.StartsWith("user://")) {
Join-Path $env:APPDATA "Godot\app_userdata\OpenWC\$($Output.Substring(7))"
} else {
[IO.Path]::GetFullPath((Join-Path $repoRoot $Output))
}
$ranking = [Collections.Generic.List[object]]::new()
Push-Location $repoRoot
try {
foreach ($yawOffset in $YawOffsets) {
foreach ($pitchOffset in $PitchOffsets) {
$yawText = $yawOffset.ToString("0.###", $invariantCulture)
$pitchText = $pitchOffset.ToString("0.###", $invariantCulture)
$candidateName = "yaw_$($yawText.Replace('-', 'n').Replace('.', '_'))__pitch_$($pitchText.Replace('-', 'n').Replace('.', '_'))"
$candidateOutput = "$Output/$candidateName"
$comparisonReport = Join-Path $absoluteOutput "$candidateName.json"
if ($PlanOnly) {
$ranking.Add([pscustomobject]@{
yaw_offset_degrees = $yawOffset
pitch_offset_degrees = $pitchOffset
camera_fov_degrees = $CameraFov
candidate_directory = $candidateOutput
})
continue
}
$captureArguments = @(
"--path", ".", "--script", "res://src/tools/capture_render_checkpoints.gd", "--",
"--output", $candidateOutput, "--only", $Checkpoint, "--single-pass",
"--camera-fov", $cameraFovText,
"--camera-yaw-offset", $yawText, "--camera-pitch-offset", $pitchText,
"--viewport-width", $ViewportWidth, "--viewport-height", $ViewportHeight,
"--wait", $waitSecondsText, "--measure", $measureSecondsText
)
$captureProcess = Start-Process -FilePath $GodotPath -ArgumentList $captureArguments -Wait -PassThru
if ($captureProcess.ExitCode -ne 0) {
throw "Capture failed for yaw=$yawText pitch=$pitchText"
}
$comparisonArguments = @(
"--headless", "--path", ".", "--script", "res://src/tools/compare_render_checkpoints.gd", "--",
"--reference", $ReferenceDirectory, "--candidate", $candidateOutput,
"--only", $Checkpoint, "--output", $comparisonReport
)
$comparisonProcess = Start-Process -FilePath $GodotPath -ArgumentList $comparisonArguments -Wait -PassThru -NoNewWindow
if ($comparisonProcess.ExitCode -notin @(0, 1)) {
throw "Comparison failed for yaw=$yawText pitch=$pitchText"
}
$comparison = Get-Content -Raw -LiteralPath $comparisonReport | ConvertFrom-Json
if ($comparison.compared_count -lt 1) {
throw "No paired image was compared for yaw=$yawText pitch=$pitchText"
}
$metricResults = @($comparison.results | Where-Object { $null -ne $_.mean_perceptual_error })
if ($metricResults.Count -ne $comparison.compared_count) {
$statuses = ($comparison.results.status | Sort-Object -Unique) -join ","
throw "Comparison produced no numeric metric for yaw=$yawText pitch=$pitchText statuses=$statuses"
}
$meanError = ($metricResults | Measure-Object -Property mean_perceptual_error -Average).Average
$changedRatio = ($metricResults | Measure-Object -Property changed_pixel_ratio -Average).Average
$ranking.Add([pscustomobject]@{
yaw_offset_degrees = $yawOffset
pitch_offset_degrees = $pitchOffset
camera_fov_degrees = $CameraFov
mean_perceptual_error = $meanError
changed_pixel_ratio = $changedRatio
candidate_directory = $candidateOutput
comparison_report = $comparisonReport
})
}
}
$rankedCandidates = if ($PlanOnly) {
@($ranking)
} else {
@($ranking | Sort-Object mean_perceptual_error, changed_pixel_ratio)
}
$rankingPath = Join-Path $absoluteOutput "ranking.json"
$rankingDocument = [ordered]@{
schema_version = 1
checkpoint = $Checkpoint
reference_directory = [IO.Path]::GetFullPath($ReferenceDirectory)
viewport = @($ViewportWidth, $ViewportHeight)
wait_seconds = $WaitSeconds
measure_seconds = $MeasureSeconds
candidates = $rankedCandidates
}
New-Item -ItemType Directory -Force -Path $absoluteOutput | Out-Null
$rankingDocument | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -LiteralPath $rankingPath
$rankedCandidates | Format-Table yaw_offset_degrees, pitch_offset_degrees, mean_perceptual_error, changed_pixel_ratio
$completionKind = if ($PlanOnly) { "plan" } else { "ranking" }
Write-Output "Camera pose sweep $completionKind completed: $rankingPath"
} finally {
Pop-Location
}