Files
easyTrace/tests/TraceCad.Tests/SheetCalibratorTests.cs
2026-05-06 10:22:34 +02:00

161 lines
6.0 KiB
C#

using OpenCvSharp;
using OpenCvSharp.Aruco;
using TraceCad.Vision.Calibration;
using Xunit;
namespace TraceCad.Tests;
public sealed class SheetCalibratorTests
{
[Fact]
public void CalibrateDetectsSyntheticA4SheetMarkers()
{
const double pixelsPerMillimetre = 4.0;
var template = ReferenceSheetTemplate.DefaultA4();
using var sheet = RenderSyntheticSheet(template, pixelsPerMillimetre);
var tempDirectory = Directory.CreateTempSubdirectory("easytrace-calibration-test-").FullName;
try
{
var imagePath = Path.Combine(tempDirectory, "sheet.png");
Cv2.ImWrite(imagePath, sheet);
var result = new SheetCalibrator().Calibrate(imagePath, tempDirectory, template);
Assert.True(result.Success, result.Message);
Assert.Equal(8, result.MatchedMarkerCount);
Assert.Equal(0.0625, result.MmPerPixel, 9);
Assert.Equal("perspective", result.CorrectionMode);
Assert.Null(result.Quality);
Assert.True(File.Exists(result.CorrectedImagePath));
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}
[Fact]
public void CalibrateAppliesResidualWarpForSyntheticLensDistortion()
{
const double pixelsPerMillimetre = 8.0;
var template = ReferenceSheetTemplate.DefaultA4();
using var sheet = RenderSyntheticSheet(template, pixelsPerMillimetre);
using var distorted = ApplyRadialDistortion(sheet, -0.025);
var tempDirectory = Directory.CreateTempSubdirectory("easytrace-distortion-test-").FullName;
try
{
var imagePath = Path.Combine(tempDirectory, "distorted-sheet.png");
Cv2.ImWrite(imagePath, distorted);
var result = new SheetCalibrator().Calibrate(
imagePath,
tempDirectory,
template,
SheetCalibrationOptions.Default with
{
PixelsPerMillimetre = 8.0,
MinimumResidualImprovementMm = -100.0
});
Assert.True(result.Success, result.Message);
Assert.True(result.MatchedMarkerCount >= 4);
Assert.True(
result.CorrectionMode.StartsWith("perspective + cross-validated residual warp", StringComparison.Ordinal),
$"{result.Message} mode {result.CorrectionMode}");
Assert.True(File.Exists(result.CorrectedImagePath));
}
finally
{
Directory.Delete(tempDirectory, recursive: true);
}
}
private static Mat RenderSyntheticSheet(ReferenceSheetTemplate template, double pixelsPerMillimetre)
{
var sheet = new Mat(
(int)Math.Round(template.HeightMm * pixelsPerMillimetre),
(int)Math.Round(template.WidthMm * pixelsPerMillimetre),
MatType.CV_8UC3,
Scalar.White);
var dictionary = CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict5X5_1000);
foreach (var marker in template.Markers)
{
using var markerImage = new Mat();
dictionary.GenerateImageMarker(
marker.Id,
(int)Math.Round(marker.SizeMm * pixelsPerMillimetre),
markerImage,
1);
using var markerColor = new Mat();
Cv2.CvtColor(markerImage, markerColor, ColorConversionCodes.GRAY2BGR);
var target = new Rect(
(int)Math.Round(marker.TopLeftMm.X * pixelsPerMillimetre),
(int)Math.Round(marker.TopLeftMm.Y * pixelsPerMillimetre),
markerColor.Width,
markerColor.Height);
markerColor.CopyTo(new Mat(sheet, target));
}
DrawControlGeometry(sheet, template, pixelsPerMillimetre);
return sheet;
}
private static void DrawControlGeometry(
Mat sheet,
ReferenceSheetTemplate template,
double pixelsPerMillimetre)
{
var thickness = Math.Max(1, (int)Math.Round(0.2 * pixelsPerMillimetre));
Cv2.Line(
sheet,
ToPixel(template.ReferenceLineStartMm, pixelsPerMillimetre),
ToPixel(template.ReferenceLineEndMm, pixelsPerMillimetre),
Scalar.Black,
thickness);
var center = ToPixel(template.CrosshairMm, pixelsPerMillimetre);
var halfTick = (int)Math.Round(6.0 * pixelsPerMillimetre);
Cv2.Line(
sheet,
new Point(center.X, center.Y - halfTick),
new Point(center.X, center.Y + halfTick),
Scalar.Black,
thickness);
}
private static Point ToPixel(TraceCad.Core.Geometry.Point2 point, double pixelsPerMillimetre) =>
new(
(int)Math.Round(point.X * pixelsPerMillimetre),
(int)Math.Round(point.Y * pixelsPerMillimetre));
private static Mat ApplyRadialDistortion(Mat source, double coefficient)
{
using var mapX = new Mat(source.Height, source.Width, MatType.CV_32FC1);
using var mapY = new Mat(source.Height, source.Width, MatType.CV_32FC1);
var centerX = (source.Width - 1.0) / 2.0;
var centerY = (source.Height - 1.0) / 2.0;
var radius = Math.Min(centerX, centerY);
for (var y = 0; y < source.Height; y++)
{
for (var x = 0; x < source.Width; x++)
{
var normalizedX = (x - centerX) / radius;
var normalizedY = (y - centerY) / radius;
var r2 = (normalizedX * normalizedX) + (normalizedY * normalizedY);
var scale = 1.0 + (coefficient * r2);
mapX.Set(y, x, (float)(centerX + ((x - centerX) * scale)));
mapY.Set(y, x, (float)(centerY + ((y - centerY) * scale)));
}
}
var distorted = new Mat();
Cv2.Remap(source, distorted, mapX, mapY, InterpolationFlags.Linear, BorderTypes.Constant, Scalar.White);
return distorted;
}
}