leerform recipe subsystem created
This commit is contained in:
@@ -6,8 +6,9 @@ namespace CandyboxPlugin.Module;
|
||||
public class CandyboxRecipeCreationTool:IRecipeCreationTool
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public bool CreateRecipe(out string recipeName)
|
||||
public Task<string?> CreateRecipeAsync()
|
||||
{
|
||||
return MaterialInputBox.Prompt("Recipe name","", out recipeName)==DialogResult.OK;
|
||||
var result = MaterialInputBox.Prompt("Recipe name","", out var recipeName);
|
||||
return Task.FromResult(result == DialogResult.OK ? recipeName : null);
|
||||
}
|
||||
}
|
||||
26
Plugins/LindtLeerformPlugin/LeerformRecipeCreationTool.cs
Normal file
26
Plugins/LindtLeerformPlugin/LeerformRecipeCreationTool.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using LindtLeerformPlugin.Views;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
|
||||
namespace LindtLeerformPlugin;
|
||||
|
||||
public class LeerformRecipeCreationTool : IRecipeCreationTool
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public async Task<string?> CreateRecipeAsync()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return null;
|
||||
|
||||
var parent = desktop.MainWindow;
|
||||
if (parent == null)
|
||||
return null;
|
||||
|
||||
var dialog = new RecipeNameDialog();
|
||||
var result = await dialog.ShowDialog<string?>(parent);
|
||||
|
||||
return string.IsNullOrWhiteSpace(result) ? null : result;
|
||||
}
|
||||
}
|
||||
72
Plugins/LindtLeerformPlugin/LeerformRecognitionControl.cs
Normal file
72
Plugins/LindtLeerformPlugin/LeerformRecognitionControl.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Diagnostics;
|
||||
using LindtLeerformPlugin.Services;
|
||||
using OpenCvSharp;
|
||||
using Serilog;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||
|
||||
namespace LindtLeerformPlugin;
|
||||
|
||||
public class LeerformRecognitionControl : BaseRecognitionControl
|
||||
{
|
||||
private readonly IImageSource _imageSource;
|
||||
private Mat? _cameraMatrix;
|
||||
private Mat? _distCoeffs;
|
||||
|
||||
public LeerformRecognitionControl(
|
||||
LeerformRecognitionControlSettings settings,
|
||||
IImageSource imageSource,
|
||||
ILoadingService loadingService) : base(settings, loadingService)
|
||||
{
|
||||
_imageSource = imageSource;
|
||||
}
|
||||
|
||||
public override List<RecipeData> GetRecipesData()
|
||||
{
|
||||
return [new RecipeData { RecipeName = "Default" }];
|
||||
}
|
||||
|
||||
protected override void Initialize(RecipeData currentRecipe)
|
||||
{
|
||||
_cameraMatrix?.Dispose();
|
||||
_distCoeffs?.Dispose();
|
||||
_cameraMatrix = null;
|
||||
_distCoeffs = null;
|
||||
|
||||
var calibrationData = CalibrationDataStore.Load();
|
||||
if (calibrationData != null)
|
||||
{
|
||||
(_cameraMatrix, _distCoeffs) = CameraCalibrationService.LoadCalibrationMats(calibrationData);
|
||||
Log.Information("Calibration data loaded (RMS error: {RmsError:F3})", calibrationData.RmsError);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void WarmUp()
|
||||
{
|
||||
}
|
||||
|
||||
protected override (Mat originalImage, Mat analysisImage, TimeSpan processingTime, TimeSpan acquisitionTime, string[] errorNames)?
|
||||
ProcessImage(CancellationToken token)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var image = _imageSource.GetImage(token).Result;
|
||||
|
||||
|
||||
if (image == null)
|
||||
return null;
|
||||
|
||||
var acquisitionTime = sw.Elapsed;
|
||||
|
||||
if (_cameraMatrix != null && _distCoeffs != null)
|
||||
{
|
||||
|
||||
var undistorted = new Mat();
|
||||
Cv2.Undistort(image, undistorted, _cameraMatrix, _distCoeffs);
|
||||
sw.Stop();
|
||||
|
||||
return (image, undistorted, sw.Elapsed, acquisitionTime, []);
|
||||
}
|
||||
sw.Stop();
|
||||
return (image, image, TimeSpan.Zero, acquisitionTime, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Inspectron.Settings;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
|
||||
namespace LindtLeerformPlugin;
|
||||
|
||||
public class LeerformRecognitionControlSettings(string cameraName) : BaseRecognitionControlSettings(cameraName)
|
||||
{
|
||||
public override void RegisterSettings(InspectronSettings settings)
|
||||
{
|
||||
base.RegisterSettings(settings);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Inspectron.Settings;
|
||||
using LindtLeerformPlugin.Models;
|
||||
using VisionBuilder.UI.Common;
|
||||
|
||||
namespace LindtLeerformPlugin;
|
||||
@@ -15,51 +12,14 @@ public class LeerformSettings : ISettings
|
||||
CameraName = cameraName;
|
||||
}
|
||||
|
||||
public CalibrationData? CalibrationData { get; set; }
|
||||
public int CheckerboardRows { get; set; } = 6;
|
||||
public int CheckerboardCols { get; set; } = 9;
|
||||
public string CalibrationImageDirectory { get; set; } = "CalibrationImages";
|
||||
public string CalibrationImageDirectory { get; set; } = Path.Combine("..", "Data", "CalibrationImages");
|
||||
|
||||
public void RegisterSettings(InspectronSettings settings)
|
||||
{
|
||||
settings.RegisterSimple(this, () => CalibrationData!, $"{CameraName}/Leerform", nameof(CalibrationData));
|
||||
settings.RegisterSimple(this, () => CheckerboardRows, $"{CameraName}/Leerform", nameof(CheckerboardRows));
|
||||
settings.RegisterSimple(this, () => CheckerboardCols, $"{CameraName}/Leerform", nameof(CheckerboardCols));
|
||||
settings.RegisterSimple(this, () => CalibrationImageDirectory, $"{CameraName}/Leerform", nameof(CalibrationImageDirectory));
|
||||
}
|
||||
}
|
||||
|
||||
public class CalibrationDataConverter : ITypeConverter
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public object ConvertFrom(object value)
|
||||
{
|
||||
if (value is string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json) || json == "null")
|
||||
return null!;
|
||||
|
||||
return JsonSerializer.Deserialize<CalibrationData>(json, JsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize CalibrationData from JSON.");
|
||||
}
|
||||
throw new InvalidOperationException("Value must be a JSON string.");
|
||||
}
|
||||
|
||||
public object ConvertTo(object value, Type destinationType)
|
||||
{
|
||||
if (value is CalibrationData data)
|
||||
{
|
||||
return JsonSerializer.Serialize(data, JsonOptions);
|
||||
}
|
||||
if (value == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
throw new InvalidOperationException("Value must be a CalibrationData.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Inspectron.Settings;
|
||||
using LindtLeerformPlugin.Models;
|
||||
using Ninject;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Plugins;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
|
||||
namespace LindtLeerformPlugin;
|
||||
|
||||
@@ -10,12 +10,25 @@ public class Plugin : IPlugin
|
||||
{
|
||||
public void RegisterGlobalModules(IKernel kernel)
|
||||
{
|
||||
TypeConverterRegistry.Register<CalibrationData>(new CalibrationDataConverter());
|
||||
kernel.Rebind<IRecipeCreationTool>().To<LeerformRecipeCreationTool>().InSingletonScope();
|
||||
}
|
||||
|
||||
public void RegisterCameraModules(IKernel kernel, string cameraName)
|
||||
{
|
||||
kernel.Bind<LeerformSettings, ISettings>().ToConstant(new LeerformSettings(cameraName));
|
||||
|
||||
// Remove existing recognition settings ISettings binding before rebinding
|
||||
var existingRecognitionControlSettings = kernel.GetBindings(typeof(BaseRecognitionControlSettings)).First();
|
||||
var toRemove = kernel.GetBindings(typeof(ISettings))
|
||||
.First(x => x.ProviderCallback.Target == existingRecognitionControlSettings.ProviderCallback.Target);
|
||||
kernel.RemoveBinding(toRemove);
|
||||
|
||||
kernel.Bind<LeerformRecognitionControlSettings, BaseRecognitionControlSettings, ISettings>()
|
||||
.ToConstant(new LeerformRecognitionControlSettings(cameraName));
|
||||
kernel.Rebind<IRecognitionControl>()
|
||||
.To<LeerformRecognitionControl>()
|
||||
.InSingletonScope();
|
||||
|
||||
kernel.RegisterModule<LeerformModule>();
|
||||
}
|
||||
}
|
||||
|
||||
36
Plugins/LindtLeerformPlugin/Services/CalibrationDataStore.cs
Normal file
36
Plugins/LindtLeerformPlugin/Services/CalibrationDataStore.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using LindtLeerformPlugin.Models;
|
||||
|
||||
namespace LindtLeerformPlugin.Services;
|
||||
|
||||
public static class CalibrationDataStore
|
||||
{
|
||||
private static readonly string FilePath = Path.Combine("..", "Data", "Config", "calibration.json");
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public static CalibrationData? Load()
|
||||
{
|
||||
if (!File.Exists(FilePath))
|
||||
return null;
|
||||
|
||||
var json = File.ReadAllText(FilePath);
|
||||
return JsonSerializer.Deserialize<CalibrationData>(json, JsonOptions);
|
||||
}
|
||||
|
||||
public static void Save(CalibrationData data)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(FilePath);
|
||||
if (directory != null)
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var json = JsonSerializer.Serialize(data, JsonOptions);
|
||||
File.WriteAllText(FilePath, json);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LindtLeerformPlugin.Models;
|
||||
using LindtLeerformPlugin.Services;
|
||||
using OpenCvSharp;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
@@ -14,12 +13,15 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
private readonly LeerformSettings _settings;
|
||||
private CancellationTokenSource? _previewCts;
|
||||
private Mat? _lastFrame;
|
||||
private Mat? _calibCameraMatrix;
|
||||
private Mat? _calibDistCoeffs;
|
||||
|
||||
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
|
||||
[ObservableProperty] private string _statusText = "Ready";
|
||||
[ObservableProperty] private int _capturedImageCount;
|
||||
[ObservableProperty] private bool _isPreviewRunning;
|
||||
[ObservableProperty] private bool _isCalibrated;
|
||||
[ObservableProperty] private bool _applyCalibration;
|
||||
[ObservableProperty] private string _calibrationImageDirectory;
|
||||
[ObservableProperty] private double _rmsError;
|
||||
|
||||
@@ -29,8 +31,13 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
_settings = settings;
|
||||
|
||||
_calibrationImageDirectory = settings.CalibrationImageDirectory;
|
||||
_isCalibrated = settings.CalibrationData is { CameraMatrix.Length: > 0 };
|
||||
_rmsError = settings.CalibrationData?.RmsError ?? 0;
|
||||
|
||||
var calibrationData = CalibrationDataStore.Load();
|
||||
_isCalibrated = calibrationData is { CameraMatrix.Length: > 0 };
|
||||
_rmsError = calibrationData?.RmsError ?? 0;
|
||||
|
||||
if (_isCalibrated)
|
||||
LoadCalibrationMats(calibrationData!);
|
||||
|
||||
UpdateCapturedImageCount();
|
||||
}
|
||||
@@ -86,9 +93,10 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
var patternSize = new Size(_settings.CheckerboardCols, _settings.CheckerboardRows);
|
||||
var data = CameraCalibrationService.Calibrate(CalibrationImageDirectory, patternSize);
|
||||
|
||||
_settings.CalibrationData = data;
|
||||
CalibrationDataStore.Save(data);
|
||||
_settings.CalibrationImageDirectory = CalibrationImageDirectory;
|
||||
|
||||
LoadCalibrationMats(data);
|
||||
IsCalibrated = true;
|
||||
RmsError = data.RmsError;
|
||||
StatusText = $"Calibrated (RMS: {data.RmsError:F4})";
|
||||
@@ -122,7 +130,18 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
_lastFrame?.Dispose();
|
||||
_lastFrame = frame;
|
||||
|
||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(frame);
|
||||
var displayFrame = frame;
|
||||
if (ApplyCalibration && _calibCameraMatrix != null && _calibDistCoeffs != null)
|
||||
{
|
||||
displayFrame = new Mat();
|
||||
Cv2.Undistort(frame, displayFrame, _calibCameraMatrix, _calibDistCoeffs);
|
||||
}
|
||||
|
||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
|
||||
|
||||
if (displayFrame != frame)
|
||||
displayFrame.Dispose();
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var old = PreviewImage;
|
||||
@@ -143,9 +162,18 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCalibrationMats(Models.CalibrationData data)
|
||||
{
|
||||
_calibCameraMatrix?.Dispose();
|
||||
_calibDistCoeffs?.Dispose();
|
||||
(_calibCameraMatrix, _calibDistCoeffs) = CameraCalibrationService.LoadCalibrationMats(data);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StopPreview();
|
||||
_lastFrame?.Dispose();
|
||||
_calibCameraMatrix?.Dispose();
|
||||
_calibDistCoeffs?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,31 +29,30 @@
|
||||
</Border>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<Border DockPanel.Dock="Right" Width="220" Padding="12" Background="#2D2D2D">
|
||||
<Border DockPanel.Dock="Right" Width="220" Padding="12" Background="White">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Calibration Settings" FontWeight="Bold" FontSize="14" Foreground="White" />
|
||||
|
||||
<TextBlock Text="Image Directory" Foreground="LightGray" />
|
||||
<TextBox Text="{Binding CalibrationImageDirectory}" />
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
<TextBlock Text="Calibration Settings" FontWeight="Bold" FontSize="14" Foreground="Black" />
|
||||
|
||||
|
||||
<Button Content="Calibrate" Command="{Binding RunCalibrationCommand}" Margin="0,4"
|
||||
Background="#00C853" Foreground="White" FontWeight="Bold" />
|
||||
Background="Red" Foreground="White" FontWeight="Bold" />
|
||||
<Button Content="Clear Images" Command="{Binding ClearCalibrationImagesCommand}" Margin="0,4"
|
||||
Background="#FF6D00" Foreground="White" FontWeight="Bold" />
|
||||
Background="White" Foreground="Red" FontWeight="Bold"
|
||||
BorderBrush="Red" BorderThickness="1" />
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<TextBlock Text="Calibrated" Foreground="LimeGreen" IsVisible="{Binding IsCalibrated}" />
|
||||
<TextBlock Foreground="LightGray" IsVisible="{Binding IsCalibrated}">
|
||||
<TextBlock Foreground="Gray" IsVisible="{Binding IsCalibrated}">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="RMS Error: {0:F4}">
|
||||
<Binding Path="RmsError" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<CheckBox Content="Apply Calibration" IsChecked="{Binding ApplyCalibration}"
|
||||
IsEnabled="{Binding IsCalibrated}" Foreground="Black" Margin="0,4" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
33
Plugins/LindtLeerformPlugin/Views/RecipeNameDialog.axaml
Normal file
33
Plugins/LindtLeerformPlugin/Views/RecipeNameDialog.axaml
Normal file
@@ -0,0 +1,33 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LindtLeerformPlugin.Views.RecipeNameDialog"
|
||||
Title="New Recipe"
|
||||
Width="400" Height="180"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto" Margin="16">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Recipe name:"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<TextBox Grid.Row="1"
|
||||
x:Name="TxtRecipeName"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8">
|
||||
<Button x:Name="BtnOk"
|
||||
Content="OK"
|
||||
Width="80" Height="36"
|
||||
HorizontalContentAlignment="Center" />
|
||||
<Button x:Name="BtnCancel"
|
||||
Content="Cancel"
|
||||
Width="80" Height="36"
|
||||
HorizontalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
23
Plugins/LindtLeerformPlugin/Views/RecipeNameDialog.axaml.cs
Normal file
23
Plugins/LindtLeerformPlugin/Views/RecipeNameDialog.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace LindtLeerformPlugin.Views;
|
||||
|
||||
public partial class RecipeNameDialog : Window
|
||||
{
|
||||
public RecipeNameDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
BtnOk.Click += (_, _) => Close(TxtRecipeName.Text);
|
||||
BtnCancel.Click += (_, _) => Close(null);
|
||||
|
||||
TxtRecipeName.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
Close(TxtRecipeName.Text);
|
||||
else if (e.Key == Key.Escape)
|
||||
Close(null);
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user