Gate Move stereo calibration on observable evidence
This commit is contained in:
parent
a741aef9bb
commit
d27f0d2d37
7 changed files with 237 additions and 8 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -215,6 +215,13 @@ both eyes. The same parent PID survived the run; transport health reported
|
|||
5,302 produced frames, 5,301 handoffs, 771 sends, and zero local-ring
|
||||
admissions. Full stereo calibration is still not implied: intrinsics and
|
||||
spatially diverse correspondences must earn the camera matrices.
|
||||
`MimirMoveStereoCalibrationAssessment` is the typed observability gate before
|
||||
any fit. It accepts only same aggregate-frame, same-Move correspondences and
|
||||
reports synchronized pair count, Move diversity, per-Eye 4x3 grid coverage,
|
||||
radius range, and missing physical inputs. The corrected live capture produced
|
||||
130 same-frame pairs with zero frame skew, but only 4/12 Eye 0 cells and 2/12
|
||||
Eye 1 cells; intrinsics and measured orb radius are also absent, so promotion is
|
||||
correctly false.
|
||||
`MimirMoveProofSurface` is the first Fensalir-visible proof surface for this
|
||||
chain. It consumes the Mimir admission receipt and the Mimir-owned pose stream
|
||||
frame, emits `mimir.move_proof_surface.v1`, and lowers an observer-only
|
||||
|
|
|
|||
|
|
@ -154,6 +154,17 @@ if (args.Any(arg => string.Equals(arg, "--muninn-move-visibility-window-smoke",
|
|||
ParseStringOption(args, "--output", "artifacts/move-calibration/muninn-visibility-window.mpack"));
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--move-stereo-calibration-assessment", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunMoveStereoCalibrationAssessment(
|
||||
ParseStringOption(args, "--input", "artifacts/move-calibration/muninn-clocked-aggregator.mpack"),
|
||||
ParseStringOption(args, "--output", "artifacts/move-calibration/stereo-assessment.mpack"),
|
||||
ParseIntOption(args, "--image-width", 640),
|
||||
ParseIntOption(args, "--image-height", 480),
|
||||
args.Any(arg => string.Equals(arg, "--intrinsics-available", StringComparison.OrdinalIgnoreCase)),
|
||||
ParseDoubleOption(args, "--orb-radius-meters", 0.0));
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--move-fusion-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunMoveFusionSmoke();
|
||||
|
|
@ -2071,16 +2082,20 @@ static int RunMuninnMoveVisibilityWindowSmoke(string odinCultMeshUri, int durati
|
|||
}
|
||||
|
||||
var correspondences = observations
|
||||
.GroupBy(observation => observation.MoveId, StringComparer.Ordinal)
|
||||
.SelectMany(group =>
|
||||
.GroupBy(observation => (observation.FrameId, observation.MoveId))
|
||||
.Select(group =>
|
||||
{
|
||||
var first = group.Where(value => value.CameraId == cameraIds[0]).ToArray();
|
||||
var second = group.Where(value => value.CameraId == cameraIds[1]).ToArray();
|
||||
return first.Select(left => second
|
||||
.Select(right => new MoveCrossCameraCorrespondence(left.MoveId, left, right, Math.Abs(left.PublishedAtNs - right.PublishedAtNs)))
|
||||
.OrderBy(pair => pair.AbsoluteSkewNs).FirstOrDefault())
|
||||
.Where(pair => pair is not null)!;
|
||||
var first = group.FirstOrDefault(value => value.CameraId == cameraIds[0]);
|
||||
var second = group.FirstOrDefault(value => value.CameraId == cameraIds[1]);
|
||||
return first is null || second is null
|
||||
? null
|
||||
: new MoveCrossCameraCorrespondence(
|
||||
group.Key.MoveId,
|
||||
first,
|
||||
second,
|
||||
Math.Abs(first.PublishedAtNs - second.PublishedAtNs));
|
||||
})
|
||||
.Where(pair => pair is not null)
|
||||
.Cast<MoveCrossCameraCorrespondence>()
|
||||
.ToArray();
|
||||
var receipt = new MoveVisibilityWindowReceipt(
|
||||
|
|
@ -2115,6 +2130,65 @@ static int RunMuninnMoveVisibilityWindowSmoke(string odinCultMeshUri, int durati
|
|||
}
|
||||
}
|
||||
|
||||
static int RunMoveStereoCalibrationAssessment(
|
||||
string inputPath,
|
||||
string outputPath,
|
||||
int imageWidth,
|
||||
int imageHeight,
|
||||
bool intrinsicsAvailable,
|
||||
double orbRadiusMeters)
|
||||
{
|
||||
if (!File.Exists(inputPath))
|
||||
{
|
||||
Console.Error.WriteLine($"move-stereo-calibration-assessment missing-input path={inputPath}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var receipt = MessagePackSerializer.Deserialize<MoveVisibilityWindowReceipt>(File.ReadAllBytes(inputPath));
|
||||
var cameraIds = receipt.Observations
|
||||
.Select(value => value.CameraId)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Order(StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var assessment = MimirMoveStereoCalibrationAssessment.Assess(
|
||||
receipt,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
cameraIds,
|
||||
intrinsicsAvailable,
|
||||
orbRadiusMeters);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath))!);
|
||||
File.WriteAllBytes(outputPath, MessagePackSerializer.Serialize(assessment));
|
||||
Console.WriteLine(JsonSerializer.Serialize(new
|
||||
{
|
||||
assessment.Schema,
|
||||
inputPath,
|
||||
outputPath,
|
||||
assessment.CorrespondenceCount,
|
||||
assessment.SynchronizedCorrespondenceCount,
|
||||
assessment.SameFrameCorrespondenceCount,
|
||||
assessment.DistinctMoveCount,
|
||||
assessment.MedianAbsoluteSkewMilliseconds,
|
||||
assessment.MaximumAbsoluteSkewMilliseconds,
|
||||
assessment.IntrinsicsAvailable,
|
||||
assessment.OrbRadiusMetersAvailable,
|
||||
assessment.ReadyForRelativePoseFit,
|
||||
assessment.Promoted,
|
||||
assessment.MissingRequirements,
|
||||
cameras = assessment.Cameras.Select(camera => new
|
||||
{
|
||||
camera.CameraId,
|
||||
camera.ObservationCount,
|
||||
camera.OccupiedGridCells,
|
||||
camera.GridCellCount,
|
||||
spanX = camera.MaximumXPx - camera.MinimumXPx,
|
||||
spanY = camera.MaximumYPx - camera.MinimumYPx,
|
||||
radiusRatio = camera.MinimumRadiusPx > 0.0 ? camera.MaximumRadiusPx / camera.MinimumRadiusPx : double.NaN
|
||||
})
|
||||
}));
|
||||
return assessment.Promoted ? 0 : 2;
|
||||
}
|
||||
|
||||
static int RunMuninnMoveCultMeshStreamSmoke(string nativeReservoirPath)
|
||||
{
|
||||
if (!File.Exists(nativeReservoirPath))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
|
||||
namespace Mimir.Runtime.Synchronization;
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirMoveCameraCoverageAssessment(
|
||||
[property: Key(0)] string CameraId,
|
||||
[property: Key(1)] int ObservationCount,
|
||||
[property: Key(2)] double MinimumXPx,
|
||||
[property: Key(3)] double MaximumXPx,
|
||||
[property: Key(4)] double MinimumYPx,
|
||||
[property: Key(5)] double MaximumYPx,
|
||||
[property: Key(6)] int OccupiedGridCells,
|
||||
[property: Key(7)] int GridCellCount,
|
||||
[property: Key(8)] double MinimumRadiusPx,
|
||||
[property: Key(9)] double MaximumRadiusPx);
|
||||
|
||||
[CultDocument("mimir.move_stereo_calibration_assessment", "mimir.move_stereo_calibration_assessment.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed record MimirMoveStereoCalibrationAssessmentDocument(
|
||||
[property: Key(0)] string Schema,
|
||||
[property: Key(1)] [property: CultName] string AssessmentId,
|
||||
[property: Key(2)] string SourceSchema,
|
||||
[property: Key(3)] long AssessedAtNs,
|
||||
[property: Key(4)] int CorrespondenceCount,
|
||||
[property: Key(5)] int SynchronizedCorrespondenceCount,
|
||||
[property: Key(6)] int SameFrameCorrespondenceCount,
|
||||
[property: Key(7)] int DistinctMoveCount,
|
||||
[property: Key(8)] double MedianAbsoluteSkewMilliseconds,
|
||||
[property: Key(9)] double MaximumAbsoluteSkewMilliseconds,
|
||||
[property: Key(10)] MimirMoveCameraCoverageAssessment[] Cameras,
|
||||
[property: Key(11)] bool IntrinsicsAvailable,
|
||||
[property: Key(12)] bool OrbRadiusMetersAvailable,
|
||||
[property: Key(13)] bool ReadyForRelativePoseFit,
|
||||
[property: Key(14)] bool Promoted,
|
||||
[property: Key(15)] string[] MissingRequirements);
|
||||
|
||||
public static class MimirMoveStereoCalibrationAssessment
|
||||
{
|
||||
public const string Schema = "mimir.move_stereo_calibration_assessment.v1";
|
||||
|
||||
public static MimirMoveStereoCalibrationAssessmentDocument Assess(
|
||||
MoveVisibilityWindowReceipt receipt,
|
||||
int imageWidth,
|
||||
int imageHeight,
|
||||
IReadOnlyCollection<string> cameraIds,
|
||||
bool intrinsicsAvailable,
|
||||
double orbRadiusMeters,
|
||||
double maximumAssociationSkewMilliseconds = 20.0,
|
||||
DateTimeOffset? assessedAt = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(receipt);
|
||||
ArgumentNullException.ThrowIfNull(cameraIds);
|
||||
if (imageWidth <= 0 || imageHeight <= 0) throw new ArgumentOutOfRangeException(nameof(imageWidth));
|
||||
if (cameraIds.Count != 2) throw new ArgumentException("Stereo calibration requires exactly two camera IDs.", nameof(cameraIds));
|
||||
|
||||
var cameras = cameraIds.Distinct(StringComparer.Ordinal).ToArray();
|
||||
if (cameras.Length != 2) throw new ArgumentException("Stereo camera IDs must be distinct.", nameof(cameraIds));
|
||||
var maximumSkewNs = maximumAssociationSkewMilliseconds * 1_000_000.0;
|
||||
var correspondences = receipt.Correspondences
|
||||
.Where(value => cameras.Contains(value.First.CameraId, StringComparer.Ordinal) &&
|
||||
cameras.Contains(value.Second.CameraId, StringComparer.Ordinal))
|
||||
.ToArray();
|
||||
var synchronized = correspondences.Where(value => value.AbsoluteSkewNs <= maximumSkewNs).ToArray();
|
||||
var sameFrame = synchronized.Where(value => string.Equals(value.First.FrameId, value.Second.FrameId, StringComparison.Ordinal)).ToArray();
|
||||
var coverage = cameras.Select(cameraId => BuildCoverage(
|
||||
cameraId,
|
||||
receipt.Observations.Where(value => string.Equals(value.CameraId, cameraId, StringComparison.Ordinal)).ToArray(),
|
||||
imageWidth,
|
||||
imageHeight)).ToArray();
|
||||
var missing = new List<string>();
|
||||
if (!intrinsicsAvailable) missing.Add("calibrated-intrinsics-for-both-eyes");
|
||||
if (!(double.IsFinite(orbRadiusMeters) && orbRadiusMeters > 0.0)) missing.Add("measured-orb-radius-meters");
|
||||
if (synchronized.Length < 300) missing.Add("at-least-300-synchronized-correspondences");
|
||||
if (sameFrame.Length < 100) missing.Add("at-least-100-same-frame-correspondences");
|
||||
if (synchronized.Select(value => value.MoveId).Distinct(StringComparer.Ordinal).Count() < 2) missing.Add("at-least-two-stable-move-identities");
|
||||
foreach (var camera in coverage)
|
||||
{
|
||||
if (camera.OccupiedGridCells < 6) missing.Add($"{camera.CameraId}:coverage-at-least-6-of-12-grid-cells");
|
||||
if (camera.MaximumRadiusPx <= camera.MinimumRadiusPx * 1.25) missing.Add($"{camera.CameraId}:radius-range-at-least-1.25x");
|
||||
}
|
||||
|
||||
var skews = correspondences.Select(value => value.AbsoluteSkewNs / 1_000_000.0).Order().ToArray();
|
||||
var ready = missing.Count == 0;
|
||||
return new MimirMoveStereoCalibrationAssessmentDocument(
|
||||
Schema,
|
||||
$"move-stereo-assessment:{receipt.StartedAtNs}:{receipt.EndedAtNs}",
|
||||
receipt.Schema,
|
||||
(assessedAt ?? DateTimeOffset.UtcNow).ToUnixTimeMilliseconds() * 1_000_000,
|
||||
correspondences.Length,
|
||||
synchronized.Length,
|
||||
sameFrame.Length,
|
||||
synchronized.Select(value => value.MoveId).Distinct(StringComparer.Ordinal).Count(),
|
||||
Median(skews),
|
||||
skews.Length == 0 ? double.NaN : skews[^1],
|
||||
coverage,
|
||||
intrinsicsAvailable,
|
||||
double.IsFinite(orbRadiusMeters) && orbRadiusMeters > 0.0,
|
||||
ready,
|
||||
Promoted: false,
|
||||
missing.Distinct(StringComparer.Ordinal).ToArray());
|
||||
}
|
||||
|
||||
private static MimirMoveCameraCoverageAssessment BuildCoverage(
|
||||
string cameraId,
|
||||
MoveVisibilityObservation[] observations,
|
||||
int imageWidth,
|
||||
int imageHeight)
|
||||
{
|
||||
var valid = observations.Where(value =>
|
||||
float.IsFinite(value.CenterXPx) && float.IsFinite(value.CenterYPx) &&
|
||||
float.IsFinite(value.RadiusPx) && value.RadiusPx > 0.0f).ToArray();
|
||||
var cells = valid.Select(value =>
|
||||
{
|
||||
var column = Math.Clamp((int)(value.CenterXPx * 4.0 / imageWidth), 0, 3);
|
||||
var row = Math.Clamp((int)(value.CenterYPx * 3.0 / imageHeight), 0, 2);
|
||||
return row * 4 + column;
|
||||
}).Distinct().Count();
|
||||
return new MimirMoveCameraCoverageAssessment(
|
||||
cameraId,
|
||||
valid.Length,
|
||||
valid.Length == 0 ? double.NaN : valid.Min(value => value.CenterXPx),
|
||||
valid.Length == 0 ? double.NaN : valid.Max(value => value.CenterXPx),
|
||||
valid.Length == 0 ? double.NaN : valid.Min(value => value.CenterYPx),
|
||||
valid.Length == 0 ? double.NaN : valid.Max(value => value.CenterYPx),
|
||||
cells,
|
||||
12,
|
||||
valid.Length == 0 ? double.NaN : valid.Min(value => value.RadiusPx),
|
||||
valid.Length == 0 ? double.NaN : valid.Max(value => value.RadiusPx));
|
||||
}
|
||||
|
||||
private static double Median(double[] sorted) => sorted.Length switch
|
||||
{
|
||||
0 => double.NaN,
|
||||
_ when sorted.Length % 2 == 1 => sorted[sorted.Length / 2],
|
||||
_ => (sorted[sorted.Length / 2 - 1] + sorted[sorted.Length / 2]) * 0.5
|
||||
};
|
||||
}
|
||||
|
|
@ -184,3 +184,4 @@
|
|||
{"ts": "2026-07-13T01:29:15+01:00", "type": "verification", "status": "partial", "note": "Per-camera optical evidence and exposure ownership restored Eye 0. At exposure 0.3 Eye 0 peak RGB rose from [72,25,43] to [178,119,112], calibrated 4 Moves, and produced 3,312 observations/latest 4; Eye 1 remains at exposure 0.1. Mimir's persistent window then saw all 4 IDs on Eye 0, 2 on Eye 1, and 2 same-ID cross-camera correspondences. Do not claim full stereo: Eye 1 overlap is incomplete and only 2 aggregate RUDP frames arrived in 20s and 60s windows."}
|
||||
{"ts": "2026-07-13T01:41:29+01:00", "type": "verification", "status": "partial", "note": "After per-eye exposure tuning and latest-state RUDP handoff, Mimir's 15s dual-eye receipt saw all four stable Move IDs on both cameras: 16 camera observations and 8 pair records across 2 aggregate frames. Full overlap is proven; calibration is not. Only 2 unique frame IDs arrived, so directly instrument parent publication and RUDP send/receive counters before camera-matrix fitting."}
|
||||
{"ts": "2026-07-13T02:32:31+01:00", "type": "verification", "status": "ok", "note": "2026-07-13 Nightwing single-provider dual-Eye field proof: one stable Muninn parent received 622 unique evidence frames in 15 s through Mimir, with 2,647 observations, 1,384 same-ID cross-camera correspondences, and all four Move IDs visible on both Eyes. Producer transport health reported 5,302 produced, 5,301 handoffs, 771 sends, and zero local-ring admissions. Artifact: artifacts/move-calibration/muninn-clocked-aggregator.mpack. Odin owner commits: 39c839a, a340c33, 8aefd1c, f82ca7a."}
|
||||
{"ts": "2026-07-13T02:39:32+01:00", "type": "verification", "status": "rejected", "note": "2026-07-13 typed Move stereo assessment rejected calibration promotion. Corrected same-frame collector captured 798 frames and 130 same-frame/same-Move pairs at zero published-frame skew. Eye 0 covered 4/12 image grid cells; Eye 1 covered 2/12 and saw only two Move IDs in this window. Both calibrated intrinsics and measured orb radius were absent. No camera matrices were emitted. Artifacts: artifacts/move-calibration/muninn-same-frame-correspondence.mpack and muninn-same-frame-stereo-assessment.mpack."}
|
||||
|
|
|
|||
|
|
@ -267,6 +267,14 @@ accepted_design:
|
|||
remote handoffs, 771 sends during the subscription, and zero local-ring
|
||||
admissions. Cadence and full overlap are earned; calibrated camera matrices
|
||||
still require intrinsics and spatially diverse correspondence capture.
|
||||
- `MimirMoveStereoCalibrationAssessment` owns the pre-fit observability gate
|
||||
as `mimir.move_stereo_calibration_assessment.v1`. It consumes a typed
|
||||
visibility receipt, counts only same-frame/same-Move stereo pairs, measures
|
||||
per-Eye image-grid and radius coverage, and refuses promotion when
|
||||
intrinsics, measured orb radius, synchronized sample count, or spatial
|
||||
diversity are missing. The corrected 2026-07-13 live receipt contained 130
|
||||
honest same-frame pairs at zero frame skew, but Eye 0 occupied only 4/12
|
||||
grid cells and Eye 1 only 2/12; no camera matrices were promoted.
|
||||
- `MimirMuninnMoveEvidenceAdapter.TryAdmitLatestCultMeshFrame` now exposes a
|
||||
typed admission receipt with decoded frame id, producer, publish/read
|
||||
timestamps, sample counts by kind, source/arrival ranges, native reservoir
|
||||
|
|
|
|||
Loading…
Reference in a new issue