161 lines
6.0 KiB
C#
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;
|
|
}
|
|
}
|