profile influences configuration
This commit is contained in:
EugeneTes
2026-05-15 11:54:36 +02:00
parent 2be13501fa
commit 8704d0b5ac
42 changed files with 2467 additions and 1023 deletions

View File

@@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<EnableDynamicLoading>true</EnableDynamicLoading>
<Nullable>enable</Nullable>
<OutDir>..\..\VisionBuilder.UI.Windows.Test\bin\Debug\Data\Plugins\CandyboxPlugin</OutDir>
<OutDir>..\..\VisionBuilder.UI.Windows.Uno\bin\Debug\Data\Plugins\CandyboxPlugin</OutDir>
</PropertyGroup>

View File

@@ -3,7 +3,13 @@ namespace LindtLeerformPlugin.Models;
public class CellResult
{
public int Index { get; set; }
/// <summary>True when no chocolate blobs were detected inside this cell (the form is empty as expected).</summary>
public bool IsGood { get; set; }
/// <summary>Largest (histogram bin value spline at that bin) across all bins. Negative if every bin is below the spline.</summary>
public double MaxExceedance { get; set; }
/// <summary>Number of contours inside the cell that passed the area filter.</summary>
public int BlobCount { get; set; }
/// <summary>Total pixel area classified as chocolate inside this cell (sum of accepted contour areas).</summary>
public int DetectedArea { get; set; }
}

View File

@@ -4,8 +4,18 @@ public class LeerformRecipe
{
public string RecipeName { get; set; } = string.Empty;
public CellPattern Pattern { get; set; } = new();
public int[] HistogramBins { get; set; } = [5, 5, 5];
public float[] AverageSpline { get; set; } = [];
public Dictionary<int, float[]> CellSplines { get; set; } = new();
/// <summary>LAB a* threshold for white chocolate (THRESH_BINARY_INV). Pixels with a* below this become foreground.</summary>
public int AThreshold { get; set; } = 140;
/// <summary>LAB L* threshold for dark chocolate (THRESH_BINARY_INV). Pixels with L* below this become foreground.</summary>
public int LThreshold { get; set; } = 100;
/// <summary>Minimum contour area (px) for a detected blob to count.</summary>
public int MinBlobArea { get; set; } = 100;
/// <summary>Maximum contour area (px) for a detected blob to count.</summary>
public int MaxBlobArea { get; set; } = 5000;
public string ThumbnailBase64 { get; set; } = string.Empty;
}

View File

@@ -5,9 +5,8 @@ namespace LindtLeerformPlugin.Services;
public class LeerformPatternRecognitionService
{
private static readonly Scalar GoodColor = new(0, 255, 0); // BGR Green
private static readonly Scalar BadColor = new(0, 0, 255); // BGR Red
private static readonly Scalar OverrideColor = new(0, 255, 255); // BGR Yellow
private static readonly Scalar GoodColor = new(0, 255, 0); // BGR Green
private static readonly Scalar BadColor = new(0, 0, 255); // BGR Red
public IReadOnlyList<CellRegion> ComputeCells(CellPattern pattern)
{
@@ -75,86 +74,98 @@ public class LeerformPatternRecognitionService
}
/// <summary>
/// Aggregate histogram pooled across every cell in the pattern. Used only at calibration
/// time to seed the default average spline; not persisted in the recipe. Note that pooling
/// weights cells by their pixel count rather than averaging per-cell histograms.
/// Build a single foreground mask covering both white and dark chocolate flecks on the pink mold,
/// using the LAB-channel thresholding approach from <c>jupiter/grid_test.ipynb</c>.
/// <list type="bullet">
/// <item><description><b>White chocolate</b> — pink mold has a* well above 128 (red), white sits near 128. Inverse-threshold a* so neutral pixels become foreground.</description></item>
/// <item><description><b>Dark chocolate</b> — pink mold is bright (high L*), dark chocolate is dark. Inverse-threshold L* so dark pixels become foreground.</description></item>
/// </list>
/// The two masks are OR-merged, then morphologically opened (3×3 ELLIPSE) and closed (5×5 ELLIPSE)
/// to drop single-pixel noise and merge speck fragments. Caller owns the returned <see cref="Mat"/>.
/// </summary>
public float[] ComputeReferenceHistogram(Mat bgrImage, CellPattern pattern, int[]? bins = null)
public static Mat DetectChocolateMask(Mat bgrImage, int aThreshold, int lThreshold)
{
bins ??= [5, 5, 5];
using var mask = BuildMask(pattern, new Size(bgrImage.Cols, bgrImage.Rows));
using var hist = CalcHistogram(bgrImage, mask, bins);
return HistogramToArray(hist);
using var lab = new Mat();
Cv2.CvtColor(bgrImage, lab, ColorConversionCodes.BGR2Lab);
var channels = Cv2.Split(lab);
try
{
var lChan = channels[0];
var aChan = channels[1];
using var whiteMask = new Mat();
Cv2.Threshold(aChan, whiteMask, aThreshold, 255, ThresholdTypes.BinaryInv);
using var darkMask = new Mat();
Cv2.Threshold(lChan, darkMask, lThreshold, 255, ThresholdTypes.BinaryInv);
var combined = new Mat();
Cv2.BitwiseOr(whiteMask, darkMask, combined);
using var openKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3));
using var closeKernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(5, 5));
Cv2.MorphologyEx(combined, combined, MorphTypes.Open, openKernel);
Cv2.MorphologyEx(combined, combined, MorphTypes.Close, closeKernel);
return combined;
}
finally
{
foreach (var ch in channels)
ch.Dispose();
}
}
public IReadOnlyList<CellResult> EvaluateCells(Mat bgrImage, LeerformRecipe recipe)
{
using var chocolateMask = DetectChocolateMask(bgrImage, recipe.AThreshold, recipe.LThreshold);
var cells = ComputeCells(recipe.Pattern);
var bins = recipe.HistogramBins is { Length: 3 } ? recipe.HistogramBins : [5, 5, 5];
var imageSize = new Size(bgrImage.Cols, bgrImage.Rows);
var results = new List<CellResult>(cells.Count);
foreach (var cell in cells)
{
float[]? spline = null;
if (recipe.CellSplines != null
&& recipe.CellSplines.TryGetValue(cell.Index, out var custom)
&& custom is { Length: SplineCurve.KnotCount })
using var cellMask = BuildMask(recipe.Pattern, imageSize, cell.Index);
using var perCell = new Mat();
Cv2.BitwiseAnd(chocolateMask, cellMask, perCell);
Cv2.FindContours(
perCell,
out var contours,
out _,
RetrievalModes.External,
ContourApproximationModes.ApproxSimple);
var blobs = 0;
var area = 0;
foreach (var contour in contours)
{
spline = custom;
}
else if (recipe.AverageSpline is { Length: SplineCurve.KnotCount })
{
spline = recipe.AverageSpline;
var contourArea = (int)Cv2.ContourArea(contour);
if (contourArea >= recipe.MinBlobArea && contourArea <= recipe.MaxBlobArea)
{
blobs++;
area += contourArea;
}
}
if (spline == null)
throw new InvalidOperationException(
$"Recipe has no spline for cell {cell.Index} — open Calibration and click 'Set Reference'.");
using var mask = BuildMask(recipe.Pattern, imageSize, cell.Index);
using var hist = CalcHistogram(bgrImage, mask, bins);
var arr = HistogramToArray(hist);
var (isGood, exceed) = EvaluateAgainstSpline(arr, spline);
results.Add(new CellResult
{
Index = cell.Index,
IsGood = isGood,
MaxExceedance = exceed
IsGood = blobs == 0,
BlobCount = blobs,
DetectedArea = area
});
}
return results;
}
/// <summary>
/// Cell is good when no histogram bin rises above the spline. <c>maxExceedance</c> is the
/// largest (bin spline) across all bins; non-positive means the cell passes.
/// </summary>
public static (bool isGood, double maxExceedance) EvaluateAgainstSpline(float[] histogram, float[] spline)
{
if (histogram == null || histogram.Length == 0)
return (true, 0);
var maxExceed = double.NegativeInfinity;
for (var i = 0; i < histogram.Length; i++)
{
var threshold = SplineCurve.EvaluateAtBin(spline, i, histogram.Length);
var diff = histogram[i] - threshold;
if (diff > maxExceed) maxExceed = diff;
}
return (maxExceed <= 0, maxExceed);
}
public Mat RenderOverlay(Mat bgrImage, CellPattern pattern,
IReadOnlyList<CellResult> results,
IReadOnlySet<int>? overriddenCells = null)
public Mat RenderOverlay(Mat bgrImage, CellPattern pattern, IReadOnlyList<CellResult> results)
{
var overlay = bgrImage.Clone();
var cells = ComputeCells(pattern);
var resultByIndex = results.ToDictionary(r => r.Index);
var imageRect = new Rect(0, 0, overlay.Cols, overlay.Rows);
const int thickness = 2;
foreach (var cell in cells)
@@ -168,9 +179,6 @@ public class LeerformPatternRecognitionService
Cv2.Rectangle(overlay, cell.BoundingBox, color, thickness);
else
Cv2.Circle(overlay, cell.Center, cell.Radius, color, thickness);
if (overriddenCells != null && overriddenCells.Contains(cell.Index))
DrawOverrideMarker(overlay, cell, pattern.Shape, imageRect);
}
return overlay;
}
@@ -187,39 +195,6 @@ public class LeerformPatternRecognitionService
}
}
public void DrawOverrideMarkers(Mat target, CellPattern pattern, IReadOnlySet<int> overriddenCells)
{
if (overriddenCells.Count == 0) return;
var cells = ComputeCells(pattern);
var imageRect = new Rect(0, 0, target.Cols, target.Rows);
foreach (var cell in cells)
{
if (!overriddenCells.Contains(cell.Index)) continue;
DrawOverrideMarker(target, cell, pattern.Shape, imageRect);
}
}
private static void DrawOverrideMarker(Mat target, CellRegion cell, CellShape shape, Rect imageRect)
{
Point center;
if (shape == CellShape.Square)
{
center = new Point(cell.BoundingBox.Right - 8, cell.BoundingBox.Top + 8);
}
else
{
var dx = (int)(cell.Radius * 0.7);
var dy = (int)(cell.Radius * 0.7);
center = new Point(cell.Center.X + dx, cell.Center.Y - dy);
}
if (!imageRect.Contains(center))
return;
Cv2.Circle(target, center, 4, OverrideColor, thickness: -1);
Cv2.Circle(target, center, 4, new Scalar(0, 0, 0), thickness: 1); // thin black outline for visibility
}
public int? FindCellAtPoint(int x, int y, CellPattern pattern)
{
var cells = ComputeCells(pattern);
@@ -240,78 +215,4 @@ public class LeerformPatternRecognitionService
}
return null;
}
public float[] ComputeCellHistogram(Mat bgrImage, CellPattern pattern, int cellIndex, int[]? bins = null)
{
bins ??= [5, 5, 5];
using var mask = BuildMask(pattern, new Size(bgrImage.Cols, bgrImage.Rows), cellIndex);
using var hist = CalcHistogram(bgrImage, mask, bins);
return HistogramToArray(hist);
}
public static Mat RenderHistogramChart(float[] histogram, int[] bins, int width = 600, int height = 220)
{
var chart = new Mat(height, width, MatType.CV_8UC3, new Scalar(30, 30, 30));
if (histogram == null || histogram.Length == 0)
return chart;
var totalBins = bins[0] * bins[1] * bins[2];
var max = 0f;
for (var i = 0; i < histogram.Length; i++)
if (histogram[i] > max) max = histogram[i];
if (max <= 0)
return chart;
const int leftPad = 10;
const int bottomPad = 15;
var barWidth = Math.Max(1, (width - 2 * leftPad) / totalBins);
var maxBarHeight = height - bottomPad - 10;
for (var i = 0; i < totalBins && i < histogram.Length; i++)
{
var binB = i / (bins[1] * bins[2]);
var binG = (i / bins[2]) % bins[1];
var binR = i % bins[2];
var b = (int)((binB + 0.5) * 256 / bins[0]);
var g = (int)((binG + 0.5) * 256 / bins[1]);
var r = (int)((binR + 0.5) * 256 / bins[2]);
var barHeight = (int)(histogram[i] / max * maxBarHeight);
var x = leftPad + i * barWidth;
var y = height - bottomPad - barHeight;
Cv2.Rectangle(chart, new Rect(x, y, barWidth, barHeight), new Scalar(b, g, r), -1);
}
return chart;
}
private static Mat CalcHistogram(Mat bgrImage, Mat mask, int[] bins)
{
var hist = new Mat();
Cv2.CalcHist(
images: new[] { bgrImage },
channels: new[] { 0, 1, 2 },
mask: mask,
hist: hist,
dims: 3,
histSize: bins,
ranges: new[] { new Rangef(0, 256), new Rangef(0, 256), new Rangef(0, 256) });
// L1 normalize so each bin is a probability in [0, 1] summing to 1.
Cv2.Normalize(hist, hist, 1.0, 0.0, NormTypes.L1);
var totalBins = bins[0] * bins[1] * bins[2];
var reshaped = hist.Reshape(1, totalBins).Clone();
hist.Dispose();
return reshaped;
}
private static float[] HistogramToArray(Mat hist)
{
var totalBins = hist.Rows * hist.Cols;
var arr = new float[totalBins];
for (var i = 0; i < totalBins; i++)
arr[i] = hist.At<float>(i, 0);
return arr;
}
}

View File

@@ -1,116 +0,0 @@
namespace LindtLeerformPlugin.Services;
/// <summary>
/// Monotone cubic Hermite spline (FritschCarlson). Five control points with
/// fixed, evenly spaced X positions; only Y is editable. Output never overshoots
/// the input range, so envelope curves stay inside [0, 1] when knots are.
/// </summary>
public static class SplineCurve
{
public const int KnotCount = 5;
/// <summary>
/// Evaluate the spline at parameter <paramref name="t"/> in [0, 1].
/// Knots are positioned at t = i / (knots.Length - 1).
/// </summary>
public static float Evaluate(float[]? knots, double t)
{
if (knots == null || knots.Length == 0)
return 0f;
if (knots.Length == 1)
return knots[0];
if (t <= 0) return knots[0];
if (t >= 1) return knots[^1];
var n = knots.Length - 1;
var pos = t * n;
var i = (int)System.Math.Floor(pos);
if (i >= n) return knots[n];
var localT = pos - i;
// Compute secant slopes and Fritsch-Carlson tangents at the two surrounding knots only.
var dxLeft = i > 0 ? (double)(knots[i] - knots[i - 1]) : 0;
var dxMid = (double)(knots[i + 1] - knots[i]);
var dxRight = i + 2 <= n ? (double)(knots[i + 2] - knots[i + 1]) : 0;
var m0 = MonotoneTangent(dxLeft, dxMid, hasLeft: i > 0, hasRight: true);
var m1 = MonotoneTangent(dxMid, dxRight, hasLeft: true, hasRight: i + 2 <= n);
var t2 = localT * localT;
var t3 = t2 * localT;
var h00 = 2 * t3 - 3 * t2 + 1;
var h10 = t3 - 2 * t2 + localT;
var h01 = -2 * t3 + 3 * t2;
var h11 = t3 - t2;
var y = h00 * knots[i] + h10 * m0 + h01 * knots[i + 1] + h11 * m1;
return (float)y;
}
/// <summary>
/// Convenience: evaluate the spline at the position of bin <paramref name="binIndex"/>
/// of a histogram with <paramref name="totalBins"/> bins.
/// </summary>
public static float EvaluateAtBin(float[]? knots, int binIndex, int totalBins)
{
if (totalBins <= 1)
return Evaluate(knots, 0);
var t = (double)binIndex / (totalBins - 1);
return Evaluate(knots, t);
}
/// <summary>
/// Build a default envelope spline from a reference histogram. For each knot
/// the segment max around the knot position is taken, multiplied by 1.2 with a
/// small floor added, and clamped to [0, 1].
/// </summary>
public static float[] CreateDefault(float[]? referenceHistogram, int knotCount = KnotCount)
{
var knots = new float[knotCount];
if (referenceHistogram == null || referenceHistogram.Length == 0)
{
for (var k = 0; k < knotCount; k++) knots[k] = 0.05f;
return knots;
}
var totalBins = referenceHistogram.Length;
var span = totalBins - 1;
// Half-width of the window we look at around each knot.
var segHalf = System.Math.Max(1, span / (2 * (knotCount - 1)));
for (var k = 0; k < knotCount; k++)
{
var center = knotCount == 1 ? 0 : k * span / (knotCount - 1);
var lo = System.Math.Max(0, center - segHalf);
var hi = System.Math.Min(totalBins - 1, center + segHalf);
var max = 0f;
for (var i = lo; i <= hi; i++)
if (referenceHistogram[i] > max) max = referenceHistogram[i];
var y = max * 1.2f + 0.01f;
if (y < 0f) y = 0f;
if (y > 1f) y = 1f;
knots[k] = y;
}
return knots;
}
private static double MonotoneTangent(double secLeft, double secRight, bool hasLeft, bool hasRight)
{
if (!hasLeft) return secRight;
if (!hasRight) return secLeft;
// If the signs differ (or either is zero), the knot is an extremum: tangent must be zero
// to preserve monotonicity locally.
if (secLeft == 0 || secRight == 0 || System.Math.Sign(secLeft) != System.Math.Sign(secRight))
return 0;
// Average — Fritsch-Carlson would also clamp by 3*min(|secLeft|,|secRight|), but for our
// small (5 knot) curves the simple average plus zero-at-extrema rule already prevents
// overshoot in practice.
var avg = 0.5 * (secLeft + secRight);
var limit = 3.0 * System.Math.Min(System.Math.Abs(secLeft), System.Math.Abs(secRight));
if (System.Math.Abs(avg) > limit)
avg = System.Math.Sign(avg) * limit;
return avg;
}
}

View File

@@ -4,7 +4,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LindtLeerformPlugin.Models;
using LindtLeerformPlugin.Services;
using LindtLeerformPlugin.Views;
using OpenCvSharp;
using Serilog;
using VisionBuilder.UI.Common;
@@ -26,16 +25,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
private Mat? _calibDistCoeffs;
private Mat? _referenceFrame;
// Non-modal cell histogram tool window state. While open, clicks on cells in the
// calibration preview update its content instead of opening a new window.
private CellHistogramWindow? _cellDialog;
private CellHistogramViewModel? _cellDialogVm;
// Transient seed for the average spline; never persisted in the recipe.
private float[]? _averageReferenceHistogram;
private float[]? _averageSpline;
private readonly Dictionary<int, float[]> _cellSplines = new();
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
[ObservableProperty] private string _statusText = "Ready";
[ObservableProperty] private int _capturedImageCount;
@@ -57,8 +46,11 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
[ObservableProperty] private decimal? _roiWidth = 0m;
[ObservableProperty] private decimal? _roiHeight = 0m;
// Detection
[ObservableProperty] private bool _hasAverageSpline;
// Detection thresholds (LAB-based, see LeerformPatternRecognitionService.DetectChocolateMask)
[ObservableProperty] private decimal? _aThreshold = 140m;
[ObservableProperty] private decimal? _lThreshold = 100m;
[ObservableProperty] private decimal? _minBlobArea = 100m;
[ObservableProperty] private decimal? _maxBlobArea = 5000m;
// Recipe
[ObservableProperty] private string _currentRecipeName = "default";
@@ -185,37 +177,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
StatusText = "Calibration images cleared";
}
[RelayCommand]
private void SetReferenceHistogram()
{
using var frame = GetAnalysisFrame();
if (frame == null)
{
StatusText = "No active frame to use as reference";
return;
}
try
{
var pattern = BuildCurrentPattern();
_averageReferenceHistogram = _patternService.ComputeReferenceHistogram(frame, pattern);
_averageSpline = SplineCurve.CreateDefault(_averageReferenceHistogram);
_cellSplines.Clear();
_referenceFrame?.Dispose();
_referenceFrame = frame.Clone();
HasAverageSpline = true;
StatusText = "Average spline seeded; per-cell overrides cleared.";
RefreshPreview();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to seed average spline");
StatusText = $"Reference error: {ex.Message}";
}
}
[RelayCommand]
private void TestPattern() => RunTestPattern();
@@ -227,24 +188,21 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
StatusText = "No active frame to test";
return;
}
if (_averageSpline == null)
{
StatusText = "Set reference first";
return;
}
try
{
var recipe = BuildCurrentRecipe();
var results = _patternService.EvaluateCells(frame, recipe);
var overrides = new HashSet<int>(_cellSplines.Keys);
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results, overrides);
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results);
var bitmap = ImageConverter.MatToAvaloniaBitmap(overlay);
var old = PreviewImage;
PreviewImage = bitmap;
old?.Dispose();
_referenceFrame?.Dispose();
_referenceFrame = frame.Clone();
var bad = results.Count(r => !r.IsGood);
StatusText = $"Test: {results.Count - bad} good / {bad} bad";
}
@@ -269,105 +227,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
return frame;
}
public int? HitTestCell(int imageX, int imageY)
{
return _patternService.FindCellAtPoint(imageX, imageY, BuildCurrentPattern());
}
public Task ShowCellHistogramAsync(int imageX, int imageY, Avalonia.Controls.Window owner)
{
using var frame = GetAnalysisFrame();
if (frame == null) return Task.CompletedTask;
var pattern = BuildCurrentPattern();
var cellIndex = _patternService.FindCellAtPoint(imageX, imageY, pattern);
if (cellIndex == null) return Task.CompletedTask;
if (_averageSpline is not { Length: SplineCurve.KnotCount })
{
StatusText = "Set reference first";
return Task.CompletedTask;
}
try
{
// Already open: commit the previous cell's edits and switch to the new cell.
if (_cellDialog != null && _cellDialogVm != null)
{
CommitDialogStateToCell(_cellDialogVm);
LoadCellIntoDialog(_cellDialogVm, frame, pattern, cellIndex.Value);
_cellDialog.Activate();
RunTestPattern();
return Task.CompletedTask;
}
var dialogVm = new CellHistogramViewModel { Bins = new[] { 5, 5, 5 } };
LoadCellIntoDialog(dialogVm, frame, pattern, cellIndex.Value);
// Re-test against the current frame on every spline drag release so the
// calibration window's preview updates live behind the open dialog.
dialogVm.DragCompleted = (avg, cell) =>
{
if (avg is { Length: SplineCurve.KnotCount })
_averageSpline = avg;
if (cell is { Length: SplineCurve.KnotCount })
_cellSplines[dialogVm.CellIndex] = cell;
else
_cellSplines.Remove(dialogVm.CellIndex);
RunTestPattern();
};
_cellDialogVm = dialogVm;
_cellDialog = new CellHistogramWindow { DataContext = dialogVm };
_cellDialog.Closed += OnCellDialogClosed;
_cellDialog.Show(owner);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to show cell histogram");
StatusText = $"Histogram error: {ex.Message}";
}
return Task.CompletedTask;
}
private void LoadCellIntoDialog(CellHistogramViewModel vm, Mat frame, CellPattern pattern, int cellIndex)
{
var bins = vm.Bins is { Length: 3 } ? vm.Bins : new[] { 5, 5, 5 };
var cellHist = _patternService.ComputeCellHistogram(frame, pattern, cellIndex, bins);
var avgSplineCopy = (float[])(_averageSpline ?? new float[SplineCurve.KnotCount]).Clone();
var cellSplineCopy = _cellSplines.TryGetValue(cellIndex, out var existing) && existing is { Length: SplineCurve.KnotCount }
? (float[])existing.Clone()
: null;
vm.CellIndex = cellIndex;
vm.Title = $"Cell #{cellIndex}";
vm.CellHistogram = cellHist;
vm.AverageSpline = avgSplineCopy;
vm.CellSpline = cellSplineCopy;
}
private void CommitDialogStateToCell(CellHistogramViewModel vm)
{
if (vm.AverageSpline is { Length: SplineCurve.KnotCount })
_averageSpline = vm.AverageSpline;
if (vm.CellSpline is { Length: SplineCurve.KnotCount })
_cellSplines[vm.CellIndex] = vm.CellSpline;
else
_cellSplines.Remove(vm.CellIndex);
}
private void OnCellDialogClosed(object? sender, EventArgs e)
{
if (_cellDialogVm != null)
CommitDialogStateToCell(_cellDialogVm);
if (_cellDialog != null)
_cellDialog.Closed -= OnCellDialogClosed;
_cellDialog = null;
_cellDialogVm = null;
RunTestPattern();
}
[RelayCommand]
private void SaveRecipe()
{
@@ -411,29 +270,15 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
RoiWidth = recipe.Pattern.RoiWidth;
RoiHeight = recipe.Pattern.RoiHeight;
_averageReferenceHistogram = null;
_averageSpline = recipe.AverageSpline is { Length: SplineCurve.KnotCount }
? recipe.AverageSpline
: null;
_cellSplines.Clear();
if (recipe.CellSplines != null)
{
foreach (var kv in recipe.CellSplines)
{
if (kv.Value is { Length: SplineCurve.KnotCount })
_cellSplines[kv.Key] = kv.Value;
}
}
HasAverageSpline = _averageSpline != null;
AThreshold = recipe.AThreshold;
LThreshold = recipe.LThreshold;
MinBlobArea = recipe.MinBlobArea;
MaxBlobArea = recipe.MaxBlobArea;
_referenceFrame?.Dispose();
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
StatusText = HasAverageSpline
? $"Loaded recipe '{recipeName}'"
: $"Loaded recipe '{recipeName}' — no spline; click 'Set Reference'.";
StatusText = $"Loaded recipe '{recipeName}'";
}
catch (Exception ex)
{
@@ -457,9 +302,10 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
{
RecipeName = CurrentRecipeName,
Pattern = BuildCurrentPattern(),
HistogramBins = [5, 5, 5],
AverageSpline = _averageSpline ?? [],
CellSplines = new Dictionary<int, float[]>(_cellSplines)
AThreshold = (int)(AThreshold ?? 140m),
LThreshold = (int)(LThreshold ?? 100m),
MinBlobArea = (int)(MinBlobArea ?? 100m),
MaxBlobArea = (int)(MaxBlobArea ?? 5000m)
};
private async Task PreviewLoopAsync(CancellationToken ct)
@@ -531,11 +377,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
owned = true;
}
_patternService.DrawPattern(displayFrame, pattern, GridColor);
if (_cellSplines.Count > 0)
{
var overrides = new HashSet<int>(_cellSplines.Keys);
_patternService.DrawOverrideMarkers(displayFrame, pattern, overrides);
}
}
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
@@ -563,32 +404,19 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
}
}
/// <summary>
/// Pattern geometry changed — cell indices are no longer valid, so any per-cell
/// overrides and the seeded average spline must be discarded. The user has to
/// click 'Set Reference' again before testing.
/// </summary>
private void InvalidateSplinesForPatternChange()
{
if (_cellSplines.Count == 0 && _averageSpline == null)
return;
_cellSplines.Clear();
_averageSpline = null;
_averageReferenceHistogram = null;
HasAverageSpline = false;
StatusText = "Pattern changed — splines cleared. Click 'Set Reference'.";
}
partial void OnApplyCalibrationChanged(bool value) => RefreshPreview();
partial void OnRowsChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
partial void OnColsChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
partial void OnSelectedCellShapeChanged(CellShape value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
partial void OnPaddingChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
partial void OnRowsChanged(decimal? value) => RefreshPreview();
partial void OnColsChanged(decimal? value) => RefreshPreview();
partial void OnSelectedCellShapeChanged(CellShape value) => RefreshPreview();
partial void OnPaddingChanged(decimal? value) => RefreshPreview();
partial void OnRoiXChanged(decimal? value) => RefreshPreview();
partial void OnRoiYChanged(decimal? value) => RefreshPreview();
partial void OnRoiWidthChanged(decimal? value) => RefreshPreview();
partial void OnRoiHeightChanged(decimal? value) => RefreshPreview();
partial void OnAThresholdChanged(decimal? value) => RefreshPreview();
partial void OnLThresholdChanged(decimal? value) => RefreshPreview();
partial void OnMinBlobAreaChanged(decimal? value) => RefreshPreview();
partial void OnMaxBlobAreaChanged(decimal? value) => RefreshPreview();
private void LoadCalibrationMats(Models.CalibrationData data)
{
@@ -600,13 +428,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
public void Dispose()
{
_singleCameraVm.PropertyChanged -= OnSingleCameraVmPropertyChanged;
if (_cellDialog != null)
{
_cellDialog.Closed -= OnCellDialogClosed;
_cellDialog.Close();
_cellDialog = null;
_cellDialogVm = null;
}
StopPreview();
_lastFrame?.Dispose();
_referenceFrame?.Dispose();

View File

@@ -1,72 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LindtLeerformPlugin.Services;
namespace LindtLeerformPlugin.ViewModels;
public partial class CellHistogramViewModel : ObservableObject
{
[ObservableProperty] private string _title = "Cell Histogram";
public int CellIndex { get; set; }
public int[] Bins { get; init; } = [5, 5, 5];
[ObservableProperty] private float[]? _cellHistogram;
[ObservableProperty] private float[]? _cellSpline;
[ObservableProperty] private float[] _averageSpline = new float[SplineCurve.KnotCount];
[ObservableProperty] private string _verdictText = string.Empty;
public bool HasOverride => CellSpline is { Length: SplineCurve.KnotCount };
/// <summary>
/// Invoked by the view when the user releases the mouse after dragging a spline handle.
/// Lets the calibration window re-test and refresh its preview while the dialog stays open.
/// </summary>
public Action<float[], float[]?>? DragCompleted { get; set; }
public void RaiseDragCompleted() => DragCompleted?.Invoke(AverageSpline, CellSpline);
partial void OnCellSplineChanged(float[]? value)
{
OnPropertyChanged(nameof(HasOverride));
UpdateVerdict();
}
partial void OnAverageSplineChanged(float[] value) => UpdateVerdict();
partial void OnCellHistogramChanged(float[]? value) => UpdateVerdict();
[RelayCommand]
private void CreateOverride()
{
// Seed from THIS cell's histogram so the user gets a useful starting envelope.
CellSpline = SplineCurve.CreateDefault(CellHistogram);
}
[RelayCommand]
private void RemoveOverride()
{
CellSpline = null;
}
private void UpdateVerdict()
{
var hist = CellHistogram;
if (hist == null || hist.Length == 0)
{
VerdictText = string.Empty;
return;
}
var spline = CellSpline is { Length: SplineCurve.KnotCount } ? CellSpline : AverageSpline;
if (spline is not { Length: SplineCurve.KnotCount })
{
VerdictText = string.Empty;
return;
}
var (isGood, exceed) = LindtLeerformPlugin.Services.LeerformPatternRecognitionService
.EvaluateAgainstSpline(hist, spline);
var label = isGood ? "GOOD" : "BAD";
var splineLabel = HasOverride ? "cell spline" : "average spline";
VerdictText = $"Verdict: {label} Max exceedance: {exceed:F4} Using: {splineLabel}";
}
}

View File

@@ -102,11 +102,21 @@
<Separator Margin="0,8" />
<Button Content="SET REFERENCE (RESETS SPLINES)" Command="{Binding SetReferenceHistogramCommand}" Margin="0,4"
HorizontalAlignment="Stretch" />
<TextBlock Text="✓ Average spline set" Foreground="LimeGreen" IsVisible="{Binding HasAverageSpline}" />
<TextBlock Text="Detection Thresholds" FontWeight="Bold" FontSize="14" Foreground="Black" />
<Button Content="TEST PATTERN" Command="{Binding TestPatternCommand}" Margin="0,4"
<TextBlock Text="A* (white chocolate, lower = stricter)" Foreground="Black" />
<NumericUpDown Value="{Binding AThreshold}" Minimum="0" Maximum="255" Increment="1" FormatString="0" />
<TextBlock Text="L* (dark chocolate, lower = stricter)" Foreground="Black" />
<NumericUpDown Value="{Binding LThreshold}" Minimum="0" Maximum="255" Increment="1" FormatString="0" />
<TextBlock Text="Min blob area (px)" Foreground="Black" />
<NumericUpDown Value="{Binding MinBlobArea}" Minimum="0" Maximum="100000" Increment="10" FormatString="0" />
<TextBlock Text="Max blob area (px)" Foreground="Black" />
<NumericUpDown Value="{Binding MaxBlobArea}" Minimum="0" Maximum="1000000" Increment="100" FormatString="0" />
<Button Content="TEST PATTERN" Command="{Binding TestPatternCommand}" Margin="0,8,0,4"
HorizontalAlignment="Stretch" />
<Separator Margin="0,8" />

View File

@@ -1,22 +1,13 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using LindtLeerformPlugin.ViewModels;
namespace LindtLeerformPlugin.Views;
public partial class CalibrationWindow : Window
{
private static readonly Cursor HandCursor = new(StandardCursorType.Hand);
private static readonly Cursor DefaultCursor = new(StandardCursorType.Arrow);
public CalibrationWindow()
{
InitializeComponent();
PreviewImageControl.PointerMoved += OnPreviewPointerMoved;
PreviewImageControl.PointerPressed += OnPreviewPointerPressed;
PreviewImageControl.PointerExited += OnPreviewPointerExited;
}
protected override void OnClosing(WindowClosingEventArgs e)
@@ -24,64 +15,4 @@ public partial class CalibrationWindow : Window
(DataContext as CalibrationWindowViewModel)?.Dispose();
base.OnClosing(e);
}
private (int x, int y)? ToImageCoordinates(Avalonia.Point pointerPos)
{
if (PreviewImageControl.Source is not Bitmap bitmap)
return null;
var ctrlSize = PreviewImageControl.Bounds.Size;
var imgSize = bitmap.Size;
if (ctrlSize.Width <= 0 || ctrlSize.Height <= 0 || imgSize.Width <= 0 || imgSize.Height <= 0)
return null;
var scale = System.Math.Min(ctrlSize.Width / imgSize.Width, ctrlSize.Height / imgSize.Height);
var displayW = imgSize.Width * scale;
var displayH = imgSize.Height * scale;
var offsetX = (ctrlSize.Width - displayW) / 2;
var offsetY = (ctrlSize.Height - displayH) / 2;
var imgX = (pointerPos.X - offsetX) / scale;
var imgY = (pointerPos.Y - offsetY) / scale;
if (imgX < 0 || imgY < 0 || imgX >= imgSize.Width || imgY >= imgSize.Height)
return null;
return ((int)imgX, (int)imgY);
}
private void OnPreviewPointerMoved(object? sender, PointerEventArgs e)
{
if (DataContext is not CalibrationWindowViewModel vm)
return;
var coords = ToImageCoordinates(e.GetPosition(PreviewImageControl));
if (coords == null)
{
PreviewImageControl.Cursor = DefaultCursor;
return;
}
var cellIndex = vm.HitTestCell(coords.Value.x, coords.Value.y);
PreviewImageControl.Cursor = cellIndex.HasValue ? HandCursor : DefaultCursor;
}
private void OnPreviewPointerExited(object? sender, PointerEventArgs e)
{
PreviewImageControl.Cursor = DefaultCursor;
}
private async void OnPreviewPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (DataContext is not CalibrationWindowViewModel vm)
return;
if (!e.GetCurrentPoint(PreviewImageControl).Properties.IsLeftButtonPressed)
return;
var coords = ToImageCoordinates(e.GetPosition(PreviewImageControl));
if (coords == null)
return;
await vm.ShowCellHistogramAsync(coords.Value.x, coords.Value.y, this);
}
}

View File

@@ -1,43 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LindtLeerformPlugin.ViewModels"
xmlns:v="using:LindtLeerformPlugin.Views"
x:Class="LindtLeerformPlugin.Views.CellHistogramWindow"
x:DataType="vm:CellHistogramViewModel"
Title="{Binding Title}"
Width="820" Height="560"
Background="#1A1A1A"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
CanResize="True">
<DockPanel Margin="12">
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0,0,0,8">
<TextBlock Text="Drag the gray dashed handles to edit the AVERAGE spline. Use 'Override Cell' to give this cell its own spline."
Foreground="LightGray" FontSize="12" TextWrapping="Wrap" />
<TextBlock Text="A cell is BAD when any histogram bar rises above its active spline."
Foreground="#888888" FontSize="11" TextWrapping="Wrap" />
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8" Margin="0,12,0,0">
<Button Content="Override Cell"
Command="{Binding CreateOverrideCommand}"
IsVisible="{Binding !HasOverride}"
Background="#FF8C00" Foreground="Black" FontWeight="Bold" />
<Button Content="Remove Override"
Command="{Binding RemoveOverrideCommand}"
IsVisible="{Binding HasOverride}"
Background="#8B0000" Foreground="White" FontWeight="Bold" />
<TextBlock Text="{Binding VerdictText}" Foreground="LightGray"
VerticalAlignment="Center" FontSize="13" />
</StackPanel>
<Border Background="#0A0A0A" Padding="2">
<v:SplineHistogramEditor x:Name="Editor"
Histogram="{Binding CellHistogram}"
CellSpline="{Binding CellSpline, Mode=TwoWay}"
AverageSpline="{Binding AverageSpline, Mode=TwoWay}"
Bins="{Binding Bins}" />
</Border>
</DockPanel>
</Window>

View File

@@ -1,14 +0,0 @@
using Avalonia.Controls;
using LindtLeerformPlugin.ViewModels;
namespace LindtLeerformPlugin.Views;
public partial class CellHistogramWindow : Window
{
public CellHistogramWindow()
{
InitializeComponent();
Editor.SplineDragCompleted += (_, _) =>
(DataContext as CellHistogramViewModel)?.RaiseDragCompleted();
}
}

View File

@@ -1,305 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using LindtLeerformPlugin.Services;
namespace LindtLeerformPlugin.Views;
/// <summary>
/// Custom Avalonia control that draws a histogram as colored bars and overlays an editable
/// average spline (faint dashed gray) plus an optional editable cell spline (bright orange).
/// Five control points per spline; X positions are fixed and only Y is dragged.
/// </summary>
public class SplineHistogramEditor : Control
{
public static readonly StyledProperty<float[]?> HistogramProperty =
AvaloniaProperty.Register<SplineHistogramEditor, float[]?>(nameof(Histogram));
public static readonly StyledProperty<float[]?> CellSplineProperty =
AvaloniaProperty.Register<SplineHistogramEditor, float[]?>(
nameof(CellSpline), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
public static readonly StyledProperty<float[]?> AverageSplineProperty =
AvaloniaProperty.Register<SplineHistogramEditor, float[]?>(
nameof(AverageSpline), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
public static readonly StyledProperty<int[]?> BinsProperty =
AvaloniaProperty.Register<SplineHistogramEditor, int[]?>(nameof(Bins));
public float[]? Histogram
{
get => GetValue(HistogramProperty);
set => SetValue(HistogramProperty, value);
}
public float[]? CellSpline
{
get => GetValue(CellSplineProperty);
set => SetValue(CellSplineProperty, value);
}
public float[]? AverageSpline
{
get => GetValue(AverageSplineProperty);
set => SetValue(AverageSplineProperty, value);
}
public int[]? Bins
{
get => GetValue(BinsProperty);
set => SetValue(BinsProperty, value);
}
private const double LeftPad = 12;
private const double RightPad = 12;
private const double TopPad = 12;
private const double BottomPad = 18;
private const double HitRadius = 12;
private const double HandleRadius = 6;
private static readonly IBrush BackgroundBrush = new SolidColorBrush(Color.FromRgb(20, 20, 20));
private static readonly IPen AxisPen = new Pen(new SolidColorBrush(Color.FromRgb(80, 80, 80)), 1);
private static readonly IPen AverageSplinePen = new Pen(
new SolidColorBrush(Color.FromRgb(170, 170, 170)), 2,
dashStyle: new DashStyle(new double[] { 4, 4 }, 0));
private static readonly IBrush AverageHandleFill = new SolidColorBrush(Color.FromRgb(220, 220, 220));
private static readonly IPen AverageHandleStroke = new Pen(new SolidColorBrush(Color.FromRgb(80, 80, 80)), 1);
private static readonly IPen CellSplinePen = new Pen(new SolidColorBrush(Color.FromRgb(255, 140, 0)), 2.5);
private static readonly IBrush CellHandleFill = new SolidColorBrush(Color.FromRgb(255, 140, 0));
private static readonly IPen CellHandleStroke = new Pen(Brushes.Black, 1);
static SplineHistogramEditor()
{
AffectsRender<SplineHistogramEditor>(
HistogramProperty, CellSplineProperty, AverageSplineProperty, BinsProperty);
}
public SplineHistogramEditor()
{
// Render() fills the bounds, and Control hit-tests its full Bounds rectangle by
// default, so we just need to be focusable for pointer capture during drag.
Focusable = true;
}
// Cached handle pixel positions, refreshed at every Render(). Indexed [0..4].
private readonly Point[] _avgHandlePixels = new Point[SplineCurve.KnotCount];
private readonly Point[] _cellHandlePixels = new Point[SplineCurve.KnotCount];
private SplineKind _draggingKind = SplineKind.None;
private int _draggingIndex = -1;
/// <summary>Raised on pointer release after a spline handle drag has completed.</summary>
public event EventHandler? SplineDragCompleted;
private enum SplineKind { None, Average, Cell }
public override void Render(DrawingContext ctx)
{
var bounds = new Rect(Bounds.Size);
ctx.FillRectangle(BackgroundBrush, bounds);
var plotRect = new Rect(
LeftPad, TopPad,
Math.Max(1, Bounds.Width - LeftPad - RightPad),
Math.Max(1, Bounds.Height - TopPad - BottomPad));
DrawAxes(ctx, plotRect);
var hist = Histogram;
var bins = Bins;
var binCount = (hist?.Length) ?? (bins is { Length: 3 } ? bins[0] * bins[1] * bins[2] : 0);
// Fixed [0, 1] Y axis — no auto-zoom. Bins and splines are L1-normalised so 1.0
// is the natural ceiling and the user always sees an absolute scale.
const double displayMax = 1.0;
if (hist != null && hist.Length > 0 && bins is { Length: 3 })
DrawBars(ctx, plotRect, hist, bins, displayMax);
if (binCount <= 0)
binCount = 125; // safe default for spline rendering when histogram is missing
DrawSpline(ctx, plotRect, AverageSpline, AverageSplinePen, displayMax, binCount,
_avgHandlePixels, AverageHandleFill, AverageHandleStroke, drawHandles: AverageSpline is { Length: SplineCurve.KnotCount });
DrawSpline(ctx, plotRect, CellSpline, CellSplinePen, displayMax, binCount,
_cellHandlePixels, CellHandleFill, CellHandleStroke, drawHandles: CellSpline is { Length: SplineCurve.KnotCount });
}
private static void DrawAxes(DrawingContext ctx, Rect plot)
{
ctx.DrawLine(AxisPen, new Point(plot.Left, plot.Bottom), new Point(plot.Right, plot.Bottom));
ctx.DrawLine(AxisPen, new Point(plot.Left, plot.Top), new Point(plot.Left, plot.Bottom));
}
private static void DrawBars(DrawingContext ctx, Rect plot, float[] hist, int[] bins, double displayMax)
{
var totalBins = bins[0] * bins[1] * bins[2];
if (totalBins <= 0) return;
var barWidth = plot.Width / totalBins;
for (var i = 0; i < totalBins && i < hist.Length; i++)
{
var binB = i / (bins[1] * bins[2]);
var binG = (i / bins[2]) % bins[1];
var binR = i % bins[2];
var b = (byte)Math.Min(255, (int)((binB + 0.5) * 256 / bins[0]));
var g = (byte)Math.Min(255, (int)((binG + 0.5) * 256 / bins[1]));
var r = (byte)Math.Min(255, (int)((binR + 0.5) * 256 / bins[2]));
var brush = new SolidColorBrush(Color.FromRgb(r, g, b));
var v = hist[i];
if (v < 0) v = 0;
var h = v / displayMax * plot.Height;
if (h < 0) h = 0;
if (h > plot.Height) h = plot.Height;
var x = plot.Left + i * barWidth;
var y = plot.Bottom - h;
ctx.FillRectangle(brush, new Rect(x, y, Math.Max(1, barWidth), h));
}
}
private void DrawSpline(DrawingContext ctx, Rect plot, float[]? knots, IPen curvePen,
double displayMax, int binCount, Point[] handleCache, IBrush handleFill, IPen handleStroke,
bool drawHandles)
{
if (knots == null || knots.Length < 2 || displayMax <= 0)
{
for (var k = 0; k < handleCache.Length; k++) handleCache[k] = new Point(double.NaN, double.NaN);
return;
}
// Sample the spline along the plot width and stroke a polyline.
var steps = Math.Max(64, (int)plot.Width);
var geometry = new StreamGeometry();
using (var sgc = geometry.Open())
{
for (var s = 0; s <= steps; s++)
{
var t = (double)s / steps;
var y = SplineCurve.Evaluate(knots, t);
var px = plot.Left + t * plot.Width;
var py = plot.Bottom - Math.Clamp(y / displayMax, 0, 1) * plot.Height;
var p = new Point(px, py);
if (s == 0) sgc.BeginFigure(p, false);
else sgc.LineTo(p);
}
sgc.EndFigure(false);
}
ctx.DrawGeometry(null, curvePen, geometry);
// Cache and draw handles at the knot positions.
if (knots.Length <= handleCache.Length)
{
for (var k = 0; k < knots.Length; k++)
{
var t = knots.Length == 1 ? 0.0 : (double)k / (knots.Length - 1);
var px = plot.Left + t * plot.Width;
var py = plot.Bottom - Math.Clamp(knots[k] / displayMax, 0, 1) * plot.Height;
handleCache[k] = new Point(px, py);
if (drawHandles)
{
ctx.DrawEllipse(handleFill, handleStroke, handleCache[k], HandleRadius, HandleRadius);
}
}
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
var props = e.GetCurrentPoint(this).Properties;
if (!props.IsLeftButtonPressed) return;
var pos = e.GetPosition(this);
// Cell handles take priority over average handles when both overlap.
var hit = HitTest(pos, _cellHandlePixels, requireCellSpline: true);
if (hit >= 0)
{
_draggingKind = SplineKind.Cell;
_draggingIndex = hit;
}
else
{
hit = HitTest(pos, _avgHandlePixels, requireCellSpline: false);
if (hit < 0) return;
_draggingKind = SplineKind.Average;
_draggingIndex = hit;
}
e.Pointer.Capture(this);
ApplyDrag(pos);
e.Handled = true;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (_draggingKind == SplineKind.None) return;
var pos = e.GetPosition(this);
ApplyDrag(pos);
e.Handled = true;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (_draggingKind == SplineKind.None) return;
_draggingKind = SplineKind.None;
_draggingIndex = -1;
if (e.Pointer.Captured == this)
e.Pointer.Capture(null);
e.Handled = true;
SplineDragCompleted?.Invoke(this, EventArgs.Empty);
}
private int HitTest(Point pos, Point[] handles, bool requireCellSpline)
{
if (requireCellSpline && CellSpline is not { Length: SplineCurve.KnotCount }) return -1;
for (var i = 0; i < handles.Length; i++)
{
var h = handles[i];
if (double.IsNaN(h.X)) continue;
var dx = pos.X - h.X;
var dy = pos.Y - h.Y;
if (dx * dx + dy * dy <= HitRadius * HitRadius)
return i;
}
return -1;
}
private void ApplyDrag(Point pos)
{
if (_draggingIndex < 0) return;
var plotRect = new Rect(
LeftPad, TopPad,
Math.Max(1, Bounds.Width - LeftPad - RightPad),
Math.Max(1, Bounds.Height - TopPad - BottomPad));
var rel = (plotRect.Bottom - pos.Y) / plotRect.Height;
var newY = (float)Math.Clamp(rel, 0.0, 1.0);
if (_draggingKind == SplineKind.Average)
{
var current = AverageSpline;
if (current is not { Length: SplineCurve.KnotCount }) return;
var copy = (float[])current.Clone();
copy[_draggingIndex] = newY;
SetCurrentValue(AverageSplineProperty, copy);
}
else if (_draggingKind == SplineKind.Cell)
{
var current = CellSpline;
if (current is not { Length: SplineCurve.KnotCount }) return;
var copy = (float[])current.Clone();
copy[_draggingIndex] = newY;
SetCurrentValue(CellSplineProperty, copy);
}
}
}

View File

@@ -0,0 +1,23 @@
{
"CameraMatrix": [
2520.5848610442777,
0,
903.5347706837581,
0,
2522.5455493789846,
714.9728204617296,
0,
0,
1
],
"DistCoeffs": [
-0.17606445822785338,
0.8803211909256239,
3.4191194540339253E-05,
0.003768837119214187,
-3.0648022036508693
],
"RmsError": 0.9078398532362246,
"ImageWidth": 2028,
"ImageHeight": 1520
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
<OutDir>..\..\..\VisionBuilder.UI.Windows.Test\bin\Debug\Data\Plugins\PackstrasseBarcodeReader</OutDir>
<OutDir>..\..\..\VisionBuilder.UI.Windows.Uno\bin\Debug\Data\Plugins\PackstrasseBarcodeReader</OutDir>
</PropertyGroup>
<ItemGroup>

View File

@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
<OutDir>D:\Inspectron\Hawkeye\code\VisionBuilder5\VisionBuilder.UI\VisionBuilder.UI.Windows.Test\bin\Debug\Data\Plugins\PralinenPLC</OutDir>
<OutDir>D:\Inspectron\Hawkeye\code\VisionBuilder5\VisionBuilder.UI\VisionBuilder.UI.Windows.Uno\bin\Debug\Data\Plugins\PralinenPLC</OutDir>
</PropertyGroup>
<ItemGroup>

View File

@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
<OutDir>..\..\..\VisionBuilder.UI.Windows.Test\bin\Debug\Data\Plugins\B24SiemensPlugin</OutDir>
<OutDir>..\..\..\VisionBuilder.UI.Windows.Uno\bin\Debug\Data\Plugins\B24SiemensPlugin</OutDir>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\framework\Inspectron.Settings\Inspectron.Settings.csproj">

View File

@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnableDynamicLoading>true</EnableDynamicLoading>
<OutDir>D:\Inspectron\Hawkeye\code\VisionBuilder5\VisionBuilder.UI\VisionBuilder.UI.Windows.Test\bin\Debug\Data\Plugins\TestPlugin</OutDir>
<OutDir>D:\Inspectron\Hawkeye\code\VisionBuilder5\VisionBuilder.UI\VisionBuilder.UI.Windows.Uno\bin\Debug\Data\Plugins\TestPlugin</OutDir>
</PropertyGroup>