plugin system docs

custom buttons for plugins
calibration functions for Leeform
This commit is contained in:
EugeneTes
2026-03-30 16:15:47 +02:00
parent a1838cafeb
commit 4bd117af47
76 changed files with 4521 additions and 59 deletions

View File

@@ -0,0 +1,116 @@
using System.Text.Json;
using OpenCvSharp;
using VisionBuilder.UI.RaspberryAcquisition.Models;
namespace VisionBuilder.UI.RaspberryAcquisition.Services;
public static class CameraCalibrationService
{
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
public static CalibrationData Calibrate(string imageDirectory, Size patternSize)
{
var imageFiles = Directory.GetFiles(imageDirectory, "*.png");
if (imageFiles.Length == 0)
throw new InvalidOperationException($"No PNG images found in {imageDirectory}");
var objectPointsList = new List<Mat>();
var imagePointsList = new List<Mat>();
Size imageSize = default;
// Build the 3D object points for the checkerboard (z=0 plane)
int cornerCount = patternSize.Width * patternSize.Height;
var objPts = new Point3f[cornerCount];
for (int row = 0; row < patternSize.Height; row++)
for (int col = 0; col < patternSize.Width; col++)
objPts[row * patternSize.Width + col] = new Point3f(col, row, 0);
int found = 0;
foreach (var file in imageFiles)
{
using var image = Cv2.ImRead(file);
using var gray = new Mat();
Cv2.CvtColor(image, gray, ColorConversionCodes.BGR2GRAY);
imageSize = new Size(image.Width, image.Height);
if (Cv2.FindChessboardCorners(gray, patternSize, out var corners,
ChessboardFlags.AdaptiveThresh | ChessboardFlags.FastCheck))
{
Cv2.CornerSubPix(gray, corners,
new Size(11, 11), new Size(-1, -1),
new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 30, 0.001));
// Convert to Mat for CalibrateCamera
var objMat = new Mat(cornerCount, 1, MatType.CV_32FC3);
for (int i = 0; i < cornerCount; i++)
objMat.Set(i, 0, objPts[i]);
objectPointsList.Add(objMat);
var imgMat = new Mat(corners.Length, 1, MatType.CV_32FC2);
for (int i = 0; i < corners.Length; i++)
imgMat.Set(i, 0, corners[i]);
imagePointsList.Add(imgMat);
found++;
}
}
if (found == 0)
throw new InvalidOperationException("Checkerboard corners not found in any image");
var cameraMatrix = new Mat();
var distCoeffs = new Mat();
double rms = Cv2.CalibrateCamera(
objectPointsList, imagePointsList, imageSize,
cameraMatrix, distCoeffs,
out _, out _);
var data = new CalibrationData
{
CameraMatrix = MatToArray(cameraMatrix),
DistCoeffs = MatToArray(distCoeffs),
RmsError = rms,
ImageWidth = imageSize.Width,
ImageHeight = imageSize.Height
};
cameraMatrix.Dispose();
distCoeffs.Dispose();
foreach (var m in objectPointsList) m.Dispose();
foreach (var m in imagePointsList) m.Dispose();
return data;
}
public static void SaveCalibration(CalibrationData data, string path)
{
var json = JsonSerializer.Serialize(data, JsonOptions);
File.WriteAllText(path, json);
}
public static (Mat cameraMatrix, Mat distCoeffs) LoadCalibration(string path)
{
var json = File.ReadAllText(path);
var data = JsonSerializer.Deserialize<CalibrationData>(json)
?? throw new InvalidOperationException("Failed to deserialize calibration data");
var cameraMatrix = new Mat(3, 3, MatType.CV_64F);
for (int i = 0; i < 9; i++)
cameraMatrix.Set(i / 3, i % 3, data.CameraMatrix[i]);
var distCoeffs = new Mat(1, data.DistCoeffs.Length, MatType.CV_64F);
for (int i = 0; i < data.DistCoeffs.Length; i++)
distCoeffs.Set(0, i, data.DistCoeffs[i]);
return (cameraMatrix, distCoeffs);
}
private static double[] MatToArray(Mat mat)
{
var total = mat.Rows * mat.Cols;
var arr = new double[total];
for (int i = 0; i < total; i++)
arr[i] = mat.At<double>(i / mat.Cols, i % mat.Cols);
return arr;
}
}

View File

@@ -0,0 +1,163 @@
using System.Diagnostics;
using System.Threading.Channels;
using OpenCvSharp;
using VisionBuilder.UI.Common.Processing;
using VisionBuilder.UI.RaspberryAcquisition.Models;
namespace VisionBuilder.UI.RaspberryAcquisition.Services;
public class ConfigurableLibcameraSource : IImageSource, IDisposable
{
private static readonly byte[] JpegHeader = [0xff, 0xd8];
private static readonly byte[] JpegFooter = [0xff, 0xd9];
private const int ChunkSize = 1024;
private readonly Channel<Mat> _imageChannel = Channel.CreateBounded<Mat>(
new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest });
private CaptureSettings _settings;
private CancellationTokenSource? _cts;
private Thread? _captureThread;
public int FrameCount { get; private set; }
public event Action<string>? StatusChanged;
public ConfigurableLibcameraSource(CaptureSettings settings)
{
_settings = settings;
}
public Task<Mat> GetImage(CancellationToken token)
{
return _imageChannel.Reader.ReadAsync(token).AsTask();
}
public void Start()
{
Stop();
FrameCount = 0;
_cts = new CancellationTokenSource();
_captureThread = new Thread(CaptureLoop) { IsBackground = true };
_captureThread.Start();
StatusChanged?.Invoke("Running");
}
public void Stop()
{
_cts?.Cancel();
_captureThread?.Join(2000);
_cts?.Dispose();
_cts = null;
_captureThread = null;
StatusChanged?.Invoke("Stopped");
}
public void UpdateSettings(CaptureSettings newSettings)
{
_settings = newSettings;
Start();
}
private void CaptureLoop()
{
var psi = new ProcessStartInfo
{
FileName = "rpicam-vid",
Arguments = _settings.BuildArguments(),
RedirectStandardOutput = true,
UseShellExecute = false
};
Process? process = null;
try
{
process = Process.Start(psi);
if (process == null)
{
StatusChanged?.Invoke("Error: failed to start rpicam-vid");
return;
}
using var br = new BinaryReader(process.StandardOutput.BaseStream);
var imageBuffer = new byte[1024 * 1024];
var buff = br.ReadBytes(ChunkSize);
while (!_cts!.Token.IsCancellationRequested)
{
var imageStart = Find(buff, JpegHeader);
if (imageStart == -1)
{
StatusChanged?.Invoke("Error: JPEG header not found");
break;
}
var size = buff.Length - imageStart;
Array.Copy(buff, imageStart, imageBuffer, 0, size);
while (!_cts.Token.IsCancellationRequested)
{
buff = br.ReadBytes(ChunkSize);
var imageEnd = Find(buff, JpegFooter);
if (imageEnd != -1)
{
imageEnd += JpegFooter.Length;
var frame = new byte[size + imageEnd];
Array.Copy(imageBuffer, frame, size);
Array.Copy(buff, 0, frame, size, imageEnd);
var decoded = Mat.ImDecode(frame);
FrameCount++;
_imageChannel.Writer.TryWrite(decoded);
var remaining = buff.Length - imageEnd;
var newBuff = new byte[ChunkSize];
Array.Copy(buff, imageEnd, newBuff, 0, remaining);
var temp = br.ReadBytes(imageEnd);
Array.Copy(temp, 0, newBuff, remaining, temp.Length);
buff = newBuff;
break;
}
Array.Copy(buff, 0, imageBuffer, size, buff.Length);
size += buff.Length;
}
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
StatusChanged?.Invoke($"Error: {ex.Message}");
}
finally
{
if (process is { HasExited: false })
{
process.Kill();
process.Dispose();
}
}
}
private static int Find(byte[] buff, byte[] search)
{
for (int start = 0; start <= buff.Length - search.Length; start++)
{
if (buff[start] == search[0])
{
int next;
for (next = 1; next < search.Length; next++)
{
if (buff[start + next] != search[next])
break;
}
if (next == search.Length)
return start;
}
}
return -1;
}
public void Dispose()
{
Stop();
}
}

View File

@@ -0,0 +1,13 @@
using OpenCvSharp;
namespace VisionBuilder.UI.RaspberryAcquisition.Services;
public static class ImageConverter
{
public static Avalonia.Media.Imaging.Bitmap MatToAvaloniaBitmap(Mat mat)
{
var encoded = mat.ImEncode(".bmp");
using var ms = new MemoryStream(encoded);
return new Avalonia.Media.Imaging.Bitmap(ms);
}
}