Score exposure sweeps by LED spline quality
This commit is contained in:
parent
43743d9c07
commit
2f1e5f856e
7 changed files with 296 additions and 6 deletions
|
|
@ -145,6 +145,11 @@ if (args.Any(arg => string.Equals(arg, "--led-spline-calibration-smoke", StringC
|
|||
return RunLedSplineCalibrationSmoke();
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--led-spline-quality-sweep-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunLedSplineQualitySweepSmoke();
|
||||
}
|
||||
|
||||
if (args.Any(arg => string.Equals(arg, "--fensalir-texture-lease-smoke", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return RunFensalirTextureLeaseSmoke();
|
||||
|
|
@ -891,6 +896,72 @@ static int RunLedSplineCalibrationSmoke()
|
|||
: 1;
|
||||
}
|
||||
|
||||
static int RunLedSplineQualitySweepSmoke()
|
||||
{
|
||||
var scenePoints = new[]
|
||||
{
|
||||
new MimirLedSplineScenePoint(0, new Vector3(-0.20f, 0.30f, 2.20f), 0.95),
|
||||
new MimirLedSplineScenePoint(1, new Vector3(-0.08f, 0.18f, 2.05f), 0.96),
|
||||
new MimirLedSplineScenePoint(2, new Vector3(0.04f, 0.02f, 1.95f), 0.97),
|
||||
new MimirLedSplineScenePoint(3, new Vector3(0.18f, -0.14f, 1.88f), 0.95),
|
||||
new MimirLedSplineScenePoint(4, new Vector3(0.35f, -0.28f, 1.82f), 0.93),
|
||||
};
|
||||
var pose = new MimirCameraPoseEstimate("kiyo-basic", new Vector3(0.24f, 0.02f, -0.03f), Quaternion.Identity, 1.0);
|
||||
var settings = new[]
|
||||
{
|
||||
new MimirCameraExposureGainSetting("underexposed", -10, 0, "LEDs barely clear detector threshold."),
|
||||
new MimirCameraExposureGainSetting("balanced", -8, 8, "LED centroids are bright but not clipped."),
|
||||
new MimirCameraExposureGainSetting("blown-out", -5, 32, "Halos merge and saturated centroids lose precision."),
|
||||
};
|
||||
var curveSolver = new MimirLedSplineCurveSolver();
|
||||
var scorer = new MimirLedSplineQualityScorer();
|
||||
var results = new List<MimirLedSplineSweepResult>(settings.Length);
|
||||
foreach (var setting in settings)
|
||||
{
|
||||
var detections = SyntheticSweepDetections(setting.SettingId, scenePoints, pose, 1920, 1080);
|
||||
var curve = curveSolver.SolveCameraCurve(
|
||||
pose.SourceId,
|
||||
$"{pose.SourceId}:led-spline:{setting.SettingId}",
|
||||
1920,
|
||||
1080,
|
||||
1_000_000_000L,
|
||||
detections);
|
||||
var detectionArray = detections.ToArray();
|
||||
var saturated = detectionArray.Length == 0
|
||||
? 0.0
|
||||
: detectionArray.Count(static detection => detection.IsSaturated) / (double)detectionArray.Length;
|
||||
var exposureFitness = detectionArray.Length == 0
|
||||
? 0.0
|
||||
: detectionArray.Average(static detection =>
|
||||
{
|
||||
var peak = Math.Clamp(detection.PeakLuma, 0.0, 1.0);
|
||||
return peak < 0.18 ? peak / 0.18 : peak > 0.92 ? Math.Max(0.0, 1.0 - (peak - 0.92) / 0.08) : 1.0;
|
||||
});
|
||||
results.Add(new MimirLedSplineSweepResult(
|
||||
pose.SourceId,
|
||||
setting,
|
||||
curve,
|
||||
scorer.Score(curve, scenePoints.Length, saturated, exposureFitness)));
|
||||
}
|
||||
|
||||
var best = new MimirLedSplineSweepSelector().SelectBest(results);
|
||||
foreach (var result in results)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"led-spline-quality source={result.SourceId} setting={result.Setting.SettingId} detected={result.Quality.DetectedLedCount} score={result.Quality.Score:0.000} coverage={result.Quality.CurveCoverage:0.000} spacing={result.Quality.SpacingCoherence:0.000} smooth={result.Quality.Smoothness:0.000} exposure={result.Quality.ExposureFitness:0.000} saturated={result.Quality.SaturatedFraction:0.000} usable={result.Quality.UsableForCalibration}");
|
||||
}
|
||||
|
||||
Console.WriteLine($"led-spline-quality-best setting={best?.Setting.SettingId ?? "none"} score={best?.Quality.Score ?? 0.0:0.000}");
|
||||
|
||||
return best?.Setting.SettingId == "balanced" &&
|
||||
best.Quality.UsableForCalibration &&
|
||||
results.Single(static result => result.Setting.SettingId == "underexposed").Quality.Score < best.Quality.Score &&
|
||||
results.Single(static result => result.Setting.SettingId == "blown-out").Quality.SaturatedFraction > 0.0 &&
|
||||
results.Single(static result => result.Setting.SettingId == "blown-out").Quality.Score < best.Quality.Score
|
||||
? 0
|
||||
: 1;
|
||||
}
|
||||
|
||||
static IReadOnlyList<MimirLedPixelDetection> ProjectLedDetections(
|
||||
IReadOnlyList<MimirLedSplineScenePoint> scenePoints,
|
||||
MimirCameraPoseEstimate pose,
|
||||
|
|
@ -919,6 +990,52 @@ static IReadOnlyList<MimirLedPixelDetection> ProjectLedDetections(
|
|||
return output;
|
||||
}
|
||||
|
||||
static IReadOnlyList<MimirLedPixelDetection> SyntheticSweepDetections(
|
||||
string settingId,
|
||||
IReadOnlyList<MimirLedSplineScenePoint> scenePoints,
|
||||
MimirCameraPoseEstimate pose,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
var projected = ProjectLedDetections(scenePoints, pose, width, height, radiusPixels: 4.5).ToArray();
|
||||
return settingId switch
|
||||
{
|
||||
"underexposed" => projected
|
||||
.Take(3)
|
||||
.Select((detection, index) => detection with
|
||||
{
|
||||
ImageX = detection.ImageX + (index - 1) * 2.5,
|
||||
ImageY = detection.ImageY + (index - 1) * -1.5,
|
||||
Confidence = 0.32 + index * 0.04,
|
||||
PeakLuma = 0.16 + index * 0.01,
|
||||
LocalContrast = 0.22 + index * 0.03,
|
||||
})
|
||||
.ToArray(),
|
||||
"blown-out" => projected
|
||||
.Select((detection, index) => detection with
|
||||
{
|
||||
ImageX = detection.ImageX + (index % 2 == 0 ? 14.0 : -11.0),
|
||||
ImageY = detection.ImageY + (index % 2 == 0 ? -9.0 : 13.0),
|
||||
RadiusPixels = 13.0 + index,
|
||||
Confidence = 0.56,
|
||||
PeakLuma = 1.0,
|
||||
LocalContrast = 0.50,
|
||||
IsSaturated = true,
|
||||
})
|
||||
.ToArray(),
|
||||
_ => projected
|
||||
.Select((detection, index) => detection with
|
||||
{
|
||||
ImageX = detection.ImageX + (index - 2) * 0.35,
|
||||
ImageY = detection.ImageY + (2 - index) * 0.20,
|
||||
Confidence = 0.90 + index * 0.01,
|
||||
PeakLuma = 0.74 + index * 0.02,
|
||||
LocalContrast = 0.88,
|
||||
})
|
||||
.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
static int RunFensalirTextureLeaseSmoke()
|
||||
{
|
||||
var broker = new FakeFieldResourceBroker();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ public readonly record struct MimirLedPixelDetection(
|
|||
double ImageX,
|
||||
double ImageY,
|
||||
double RadiusPixels,
|
||||
double Confidence);
|
||||
double Confidence,
|
||||
double PeakLuma = 1.0,
|
||||
double LocalContrast = 1.0,
|
||||
bool IsSaturated = false);
|
||||
|
||||
public sealed record MimirLedSplineCurveFit(
|
||||
string SourceId,
|
||||
|
|
@ -16,6 +19,30 @@ public sealed record MimirLedSplineCurveFit(
|
|||
double CurveLengthPixels,
|
||||
double Confidence);
|
||||
|
||||
public sealed record MimirLedSplineQualityReport(
|
||||
string SourceId,
|
||||
string ObservationKey,
|
||||
int DetectedLedCount,
|
||||
double CurveCoverage,
|
||||
double SpacingCoherence,
|
||||
double Smoothness,
|
||||
double ExposureFitness,
|
||||
double SaturatedFraction,
|
||||
double Score,
|
||||
bool UsableForCalibration);
|
||||
|
||||
public sealed record MimirCameraExposureGainSetting(
|
||||
string SettingId,
|
||||
int Exposure,
|
||||
int Gain,
|
||||
string Notes);
|
||||
|
||||
public sealed record MimirLedSplineSweepResult(
|
||||
string SourceId,
|
||||
MimirCameraExposureGainSetting Setting,
|
||||
MimirLedSplineCurveFit Curve,
|
||||
MimirLedSplineQualityReport Quality);
|
||||
|
||||
public enum MimirVisualCalibrationResidualStatus
|
||||
{
|
||||
Empty,
|
||||
|
|
@ -199,6 +226,134 @@ public sealed class MimirLedSplineCurveSolver
|
|||
double.IsFinite(value) ? Math.Clamp(value, 0.0, 1.0) : 0.0;
|
||||
}
|
||||
|
||||
public sealed class MimirLedSplineQualityScorer
|
||||
{
|
||||
public MimirLedSplineQualityReport Score(
|
||||
MimirLedSplineCurveFit curve,
|
||||
int expectedLedCount,
|
||||
double saturatedFraction = 0.0,
|
||||
double exposureFitness = 1.0)
|
||||
{
|
||||
var points = curve.Points
|
||||
.OrderBy(static point => point.CurveT)
|
||||
.ToArray();
|
||||
if (points.Length == 0 || expectedLedCount <= 0)
|
||||
{
|
||||
return new MimirLedSplineQualityReport(
|
||||
curve.SourceId,
|
||||
curve.ObservationKey,
|
||||
points.Length,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
Clamp01(saturatedFraction),
|
||||
0.0,
|
||||
UsableForCalibration: false);
|
||||
}
|
||||
|
||||
var coverage = Clamp01(points.Length / (double)Math.Max(1, expectedLedCount));
|
||||
var spacing = SpacingCoherence(points);
|
||||
var smoothness = Smoothness(points);
|
||||
var pointConfidence = points.Average(static point => Clamp01(point.Confidence));
|
||||
var saturationPenalty = 1.0 - Clamp01(saturatedFraction);
|
||||
var exposure = Clamp01(exposureFitness);
|
||||
var score = coverage *
|
||||
(0.28 * spacing + 0.22 * smoothness + 0.25 * pointConfidence + 0.25 * exposure) *
|
||||
saturationPenalty;
|
||||
return new MimirLedSplineQualityReport(
|
||||
curve.SourceId,
|
||||
curve.ObservationKey,
|
||||
points.Length,
|
||||
coverage,
|
||||
spacing,
|
||||
smoothness,
|
||||
exposure,
|
||||
Clamp01(saturatedFraction),
|
||||
Clamp01(score),
|
||||
UsableForCalibration: points.Length >= 3 && coverage >= 0.60 && score >= 0.55);
|
||||
}
|
||||
|
||||
private static double SpacingCoherence(IReadOnlyList<MimirLedSplineObservationPoint> points)
|
||||
{
|
||||
if (points.Count < 3)
|
||||
{
|
||||
return points.Count >= 2 ? 0.75 : 0.0;
|
||||
}
|
||||
|
||||
var distances = new double[points.Count - 1];
|
||||
for (var index = 1; index < points.Count; index++)
|
||||
{
|
||||
var dx = points[index].ImageX - points[index - 1].ImageX;
|
||||
var dy = points[index].ImageY - points[index - 1].ImageY;
|
||||
distances[index - 1] = Math.Sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
var mean = distances.Average();
|
||||
if (mean <= 1.0e-9)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var variance = distances.Average(distance =>
|
||||
{
|
||||
var delta = distance - mean;
|
||||
return delta * delta;
|
||||
});
|
||||
var coefficient = Math.Sqrt(variance) / mean;
|
||||
return 1.0 / (1.0 + coefficient * 3.0);
|
||||
}
|
||||
|
||||
private static double Smoothness(IReadOnlyList<MimirLedSplineObservationPoint> points)
|
||||
{
|
||||
if (points.Count < 3)
|
||||
{
|
||||
return points.Count >= 2 ? 0.75 : 0.0;
|
||||
}
|
||||
|
||||
var turnSum = 0.0;
|
||||
var turns = 0;
|
||||
for (var index = 1; index < points.Count - 1; index++)
|
||||
{
|
||||
var ax = points[index].ImageX - points[index - 1].ImageX;
|
||||
var ay = points[index].ImageY - points[index - 1].ImageY;
|
||||
var bx = points[index + 1].ImageX - points[index].ImageX;
|
||||
var by = points[index + 1].ImageY - points[index].ImageY;
|
||||
var aLength = Math.Sqrt(ax * ax + ay * ay);
|
||||
var bLength = Math.Sqrt(bx * bx + by * by);
|
||||
if (aLength <= 1.0e-9 || bLength <= 1.0e-9)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dot = Math.Clamp((ax * bx + ay * by) / (aLength * bLength), -1.0, 1.0);
|
||||
turnSum += Math.Acos(dot);
|
||||
turns++;
|
||||
}
|
||||
|
||||
if (turns == 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var meanTurn = turnSum / turns;
|
||||
return 1.0 / (1.0 + meanTurn);
|
||||
}
|
||||
|
||||
private static double Clamp01(double value) =>
|
||||
double.IsFinite(value) ? Math.Clamp(value, 0.0, 1.0) : 0.0;
|
||||
}
|
||||
|
||||
public sealed class MimirLedSplineSweepSelector
|
||||
{
|
||||
public MimirLedSplineSweepResult? SelectBest(IEnumerable<MimirLedSplineSweepResult> results) =>
|
||||
results
|
||||
.Where(static result => result.Quality.UsableForCalibration)
|
||||
.OrderByDescending(static result => result.Quality.Score)
|
||||
.ThenBy(static result => result.Quality.SaturatedFraction)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public sealed class MimirVisualCalibrationResidualSolver
|
||||
{
|
||||
public MimirVisualCalibrationResidualFrame EvaluateLedSpline(MimirLedSplineFieldCandidate candidate)
|
||||
|
|
|
|||
Loading…
Reference in a new issue