Compare commits
10 Commits
ac0b298644
...
8704d0b5ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8704d0b5ac | ||
|
|
2be13501fa | ||
|
|
931de47164 | ||
|
|
371436d0c3 | ||
|
|
bb861834fd | ||
|
|
1032a5a28c | ||
|
|
1c7a4abd5c | ||
|
|
4bd117af47 | ||
|
|
a1838cafeb | ||
|
|
34b74ecdcd |
@@ -3,7 +3,9 @@
|
||||
"allow": [
|
||||
"Bash(find:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(dotnet sln:*)"
|
||||
"Bash(dotnet sln:*)",
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(python3)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ Plugins implement `IPlugin` with two registration points:
|
||||
|
||||
Modules implement `IVisionBuilderModule.InitializeModule()` for deferred initialization.
|
||||
|
||||
For detailed plugin architecture documentation and step-by-step implementation guide, see [`docs/PLUGIN_SYSTEM.md`](docs/PLUGIN_SYSTEM.md).
|
||||
|
||||
### Recognition Control
|
||||
`IRecognitionControl` manages the per-camera image processing lifecycle (start/stop/pause, image loop, events). `BaseRecognitionControl` provides the threading, loop, and event infrastructure — subclasses implement `Initialize`, `WarmUp`, `ProcessImage`, and `GetRecipesData`. Each camera gets its own singleton instance via the child kernel. For full implementation guide, settings reference, and existing implementations, see [`docs/RECOGNITION_CONTROL.md`](docs/RECOGNITION_CONTROL.md).
|
||||
|
||||
### DI Container
|
||||
Ninject `StandardKernel` is the root container. Child kernels (`Ninject.Extensions.ChildKernel`) scope per-camera services. All service resolution flows through the kernel — avoid `new` for services.
|
||||
|
||||
@@ -87,7 +92,7 @@ Ninject `StandardKernel` is the root container. Child kernels (`Ninject.Extensio
|
||||
|
||||
## Conventions
|
||||
|
||||
- Target platform is **x64** Windows.
|
||||
- Target platform is **x64** Windows, but new code should be cross-platform — use Avalonia for UI, avoid WinForms-only or Windows-specific APIs.
|
||||
- Culture is forced to `en-US` at startup.
|
||||
- Nullable reference types are enabled across most projects.
|
||||
- Operation attributes: `[Category("name")]` for UI grouping, `[NotForTool]` to exclude properties from serialization, `[IgnoreOperation]` to hide from discovery.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Hawkeye.VisionBuilder.UI.RecipeYoloPatcher</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.6.0.20220608" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)'=='CPU'">
|
||||
<PackageReference Include="YoloV8" Version="4.1.5" />
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Configuration)'!='CPU'">
|
||||
<PackageReference Include="YoloV8.Gpu" Version="4.1.7" />
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu" Version="1.18.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Hawkeye.VisionBuilder.Workflow\Hawkeye.VisionBuilder.Workflow.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
399
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/MainForm.Designer.cs
generated
Normal file
399
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/MainForm.Designer.cs
generated
Normal file
@@ -0,0 +1,399 @@
|
||||
#nullable disable
|
||||
|
||||
namespace Hawkeye.VisionBuilder.UI.RecipeYoloPatcher
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.lblModel = new System.Windows.Forms.Label();
|
||||
this.txtModelPath = new System.Windows.Forms.TextBox();
|
||||
this.btnBrowseModel = new System.Windows.Forms.Button();
|
||||
this.btnLoadClasses = new System.Windows.Forms.Button();
|
||||
this.lblClasses = new System.Windows.Forms.Label();
|
||||
this.lstClasses = new System.Windows.Forms.CheckedListBox();
|
||||
this.lblFiles = new System.Windows.Forms.Label();
|
||||
this.lstFiles = new System.Windows.Forms.ListBox();
|
||||
this.btnAddFiles = new System.Windows.Forms.Button();
|
||||
this.btnRemoveFiles = new System.Windows.Forms.Button();
|
||||
this.btnClearFiles = new System.Windows.Forms.Button();
|
||||
this.grpBounds = new System.Windows.Forms.GroupBox();
|
||||
this.lblMinArea = new System.Windows.Forms.Label();
|
||||
this.numMinArea = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblMaxArea = new System.Windows.Forms.Label();
|
||||
this.numMaxArea = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblMinWidth = new System.Windows.Forms.Label();
|
||||
this.numMinWidth = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblMaxWidth = new System.Windows.Forms.Label();
|
||||
this.numMaxWidth = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblMinHeight = new System.Windows.Forms.Label();
|
||||
this.numMinHeight = new System.Windows.Forms.NumericUpDown();
|
||||
this.lblMaxHeight = new System.Windows.Forms.Label();
|
||||
this.numMaxHeight = new System.Windows.Forms.NumericUpDown();
|
||||
this.btnApply = new System.Windows.Forms.Button();
|
||||
this.btnStandardize = new System.Windows.Forms.Button();
|
||||
this.txtLog = new System.Windows.Forms.TextBox();
|
||||
this.grpBounds.SuspendLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMinArea)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMaxArea)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMinWidth)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMaxWidth)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMinHeight)).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMaxHeight)).BeginInit();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lblModel
|
||||
//
|
||||
this.lblModel.AutoSize = true;
|
||||
this.lblModel.Location = new System.Drawing.Point(10, 15);
|
||||
this.lblModel.Name = "lblModel";
|
||||
this.lblModel.Size = new System.Drawing.Size(46, 15);
|
||||
this.lblModel.TabIndex = 0;
|
||||
this.lblModel.Text = "Model:";
|
||||
//
|
||||
// txtModelPath
|
||||
//
|
||||
this.txtModelPath.Location = new System.Drawing.Point(70, 12);
|
||||
this.txtModelPath.Name = "txtModelPath";
|
||||
this.txtModelPath.ReadOnly = true;
|
||||
this.txtModelPath.Size = new System.Drawing.Size(580, 23);
|
||||
this.txtModelPath.TabIndex = 1;
|
||||
//
|
||||
// btnBrowseModel
|
||||
//
|
||||
this.btnBrowseModel.Location = new System.Drawing.Point(660, 10);
|
||||
this.btnBrowseModel.Name = "btnBrowseModel";
|
||||
this.btnBrowseModel.Size = new System.Drawing.Size(110, 25);
|
||||
this.btnBrowseModel.TabIndex = 2;
|
||||
this.btnBrowseModel.Text = "Browse...";
|
||||
this.btnBrowseModel.UseVisualStyleBackColor = true;
|
||||
this.btnBrowseModel.Click += new System.EventHandler(this.BtnBrowseModel_Click);
|
||||
//
|
||||
// btnLoadClasses
|
||||
//
|
||||
this.btnLoadClasses.Enabled = false;
|
||||
this.btnLoadClasses.Location = new System.Drawing.Point(10, 45);
|
||||
this.btnLoadClasses.Name = "btnLoadClasses";
|
||||
this.btnLoadClasses.Size = new System.Drawing.Size(130, 25);
|
||||
this.btnLoadClasses.TabIndex = 3;
|
||||
this.btnLoadClasses.Text = "Load classes";
|
||||
this.btnLoadClasses.UseVisualStyleBackColor = true;
|
||||
this.btnLoadClasses.Click += new System.EventHandler(this.BtnLoadClasses_Click);
|
||||
//
|
||||
// lblClasses
|
||||
//
|
||||
this.lblClasses.AutoSize = true;
|
||||
this.lblClasses.Location = new System.Drawing.Point(10, 85);
|
||||
this.lblClasses.Name = "lblClasses";
|
||||
this.lblClasses.Size = new System.Drawing.Size(228, 15);
|
||||
this.lblClasses.TabIndex = 4;
|
||||
this.lblClasses.Text = "Classes (check the ones to add/update):";
|
||||
//
|
||||
// lstClasses
|
||||
//
|
||||
this.lstClasses.CheckOnClick = true;
|
||||
this.lstClasses.FormattingEnabled = true;
|
||||
this.lstClasses.IntegralHeight = false;
|
||||
this.lstClasses.Location = new System.Drawing.Point(10, 105);
|
||||
this.lstClasses.Name = "lstClasses";
|
||||
this.lstClasses.Size = new System.Drawing.Size(760, 180);
|
||||
this.lstClasses.TabIndex = 5;
|
||||
//
|
||||
// lblFiles
|
||||
//
|
||||
this.lblFiles.AutoSize = true;
|
||||
this.lblFiles.Location = new System.Drawing.Point(10, 295);
|
||||
this.lblFiles.Name = "lblFiles";
|
||||
this.lblFiles.Size = new System.Drawing.Size(173, 15);
|
||||
this.lblFiles.TabIndex = 6;
|
||||
this.lblFiles.Text = "Recipe files (*.hrcp, *.jhrcp):";
|
||||
//
|
||||
// lstFiles
|
||||
//
|
||||
this.lstFiles.FormattingEnabled = true;
|
||||
this.lstFiles.HorizontalScrollbar = true;
|
||||
this.lstFiles.IntegralHeight = false;
|
||||
this.lstFiles.ItemHeight = 15;
|
||||
this.lstFiles.Location = new System.Drawing.Point(10, 315);
|
||||
this.lstFiles.Name = "lstFiles";
|
||||
this.lstFiles.SelectionMode = System.Windows.Forms.SelectionMode.MultiExtended;
|
||||
this.lstFiles.Size = new System.Drawing.Size(660, 130);
|
||||
this.lstFiles.TabIndex = 7;
|
||||
//
|
||||
// btnAddFiles
|
||||
//
|
||||
this.btnAddFiles.Location = new System.Drawing.Point(680, 315);
|
||||
this.btnAddFiles.Name = "btnAddFiles";
|
||||
this.btnAddFiles.Size = new System.Drawing.Size(90, 25);
|
||||
this.btnAddFiles.TabIndex = 8;
|
||||
this.btnAddFiles.Text = "Add files...";
|
||||
this.btnAddFiles.UseVisualStyleBackColor = true;
|
||||
this.btnAddFiles.Click += new System.EventHandler(this.BtnAddFiles_Click);
|
||||
//
|
||||
// btnRemoveFiles
|
||||
//
|
||||
this.btnRemoveFiles.Location = new System.Drawing.Point(680, 345);
|
||||
this.btnRemoveFiles.Name = "btnRemoveFiles";
|
||||
this.btnRemoveFiles.Size = new System.Drawing.Size(90, 25);
|
||||
this.btnRemoveFiles.TabIndex = 9;
|
||||
this.btnRemoveFiles.Text = "Remove";
|
||||
this.btnRemoveFiles.UseVisualStyleBackColor = true;
|
||||
this.btnRemoveFiles.Click += new System.EventHandler(this.BtnRemoveFiles_Click);
|
||||
//
|
||||
// btnClearFiles
|
||||
//
|
||||
this.btnClearFiles.Location = new System.Drawing.Point(680, 375);
|
||||
this.btnClearFiles.Name = "btnClearFiles";
|
||||
this.btnClearFiles.Size = new System.Drawing.Size(90, 25);
|
||||
this.btnClearFiles.TabIndex = 10;
|
||||
this.btnClearFiles.Text = "Clear";
|
||||
this.btnClearFiles.UseVisualStyleBackColor = true;
|
||||
this.btnClearFiles.Click += new System.EventHandler(this.BtnClearFiles_Click);
|
||||
//
|
||||
// grpBounds
|
||||
//
|
||||
this.grpBounds.Controls.Add(this.lblMinArea);
|
||||
this.grpBounds.Controls.Add(this.numMinArea);
|
||||
this.grpBounds.Controls.Add(this.lblMaxArea);
|
||||
this.grpBounds.Controls.Add(this.numMaxArea);
|
||||
this.grpBounds.Controls.Add(this.lblMinWidth);
|
||||
this.grpBounds.Controls.Add(this.numMinWidth);
|
||||
this.grpBounds.Controls.Add(this.lblMaxWidth);
|
||||
this.grpBounds.Controls.Add(this.numMaxWidth);
|
||||
this.grpBounds.Controls.Add(this.lblMinHeight);
|
||||
this.grpBounds.Controls.Add(this.numMinHeight);
|
||||
this.grpBounds.Controls.Add(this.lblMaxHeight);
|
||||
this.grpBounds.Controls.Add(this.numMaxHeight);
|
||||
this.grpBounds.Location = new System.Drawing.Point(10, 455);
|
||||
this.grpBounds.Name = "grpBounds";
|
||||
this.grpBounds.Size = new System.Drawing.Size(760, 90);
|
||||
this.grpBounds.TabIndex = 11;
|
||||
this.grpBounds.TabStop = false;
|
||||
this.grpBounds.Text = "Bounds (applied to selected classes)";
|
||||
//
|
||||
// lblMinArea
|
||||
//
|
||||
this.lblMinArea.AutoSize = true;
|
||||
this.lblMinArea.Location = new System.Drawing.Point(10, 20);
|
||||
this.lblMinArea.Name = "lblMinArea";
|
||||
this.lblMinArea.Size = new System.Drawing.Size(56, 15);
|
||||
this.lblMinArea.TabIndex = 0;
|
||||
this.lblMinArea.Text = "Min Area";
|
||||
//
|
||||
// numMinArea
|
||||
//
|
||||
this.numMinArea.Location = new System.Drawing.Point(10, 40);
|
||||
this.numMinArea.Maximum = new decimal(new int[] { 9999999, 0, 0, 0 });
|
||||
this.numMinArea.Name = "numMinArea";
|
||||
this.numMinArea.Size = new System.Drawing.Size(110, 23);
|
||||
this.numMinArea.TabIndex = 1;
|
||||
//
|
||||
// lblMaxArea
|
||||
//
|
||||
this.lblMaxArea.AutoSize = true;
|
||||
this.lblMaxArea.Location = new System.Drawing.Point(130, 20);
|
||||
this.lblMaxArea.Name = "lblMaxArea";
|
||||
this.lblMaxArea.Size = new System.Drawing.Size(59, 15);
|
||||
this.lblMaxArea.TabIndex = 2;
|
||||
this.lblMaxArea.Text = "Max Area";
|
||||
//
|
||||
// numMaxArea
|
||||
//
|
||||
this.numMaxArea.Location = new System.Drawing.Point(130, 40);
|
||||
this.numMaxArea.Maximum = new decimal(new int[] { 9999999, 0, 0, 0 });
|
||||
this.numMaxArea.Name = "numMaxArea";
|
||||
this.numMaxArea.Size = new System.Drawing.Size(110, 23);
|
||||
this.numMaxArea.TabIndex = 3;
|
||||
this.numMaxArea.Value = new decimal(new int[] { 99999, 0, 0, 0 });
|
||||
//
|
||||
// lblMinWidth
|
||||
//
|
||||
this.lblMinWidth.AutoSize = true;
|
||||
this.lblMinWidth.Location = new System.Drawing.Point(260, 20);
|
||||
this.lblMinWidth.Name = "lblMinWidth";
|
||||
this.lblMinWidth.Size = new System.Drawing.Size(64, 15);
|
||||
this.lblMinWidth.TabIndex = 4;
|
||||
this.lblMinWidth.Text = "Min Width";
|
||||
//
|
||||
// numMinWidth
|
||||
//
|
||||
this.numMinWidth.Location = new System.Drawing.Point(260, 40);
|
||||
this.numMinWidth.Maximum = new decimal(new int[] { 9999999, 0, 0, 0 });
|
||||
this.numMinWidth.Name = "numMinWidth";
|
||||
this.numMinWidth.Size = new System.Drawing.Size(110, 23);
|
||||
this.numMinWidth.TabIndex = 5;
|
||||
//
|
||||
// lblMaxWidth
|
||||
//
|
||||
this.lblMaxWidth.AutoSize = true;
|
||||
this.lblMaxWidth.Location = new System.Drawing.Point(390, 20);
|
||||
this.lblMaxWidth.Name = "lblMaxWidth";
|
||||
this.lblMaxWidth.Size = new System.Drawing.Size(67, 15);
|
||||
this.lblMaxWidth.TabIndex = 6;
|
||||
this.lblMaxWidth.Text = "Max Width";
|
||||
//
|
||||
// numMaxWidth
|
||||
//
|
||||
this.numMaxWidth.Location = new System.Drawing.Point(390, 40);
|
||||
this.numMaxWidth.Maximum = new decimal(new int[] { 9999999, 0, 0, 0 });
|
||||
this.numMaxWidth.Name = "numMaxWidth";
|
||||
this.numMaxWidth.Size = new System.Drawing.Size(110, 23);
|
||||
this.numMaxWidth.TabIndex = 7;
|
||||
this.numMaxWidth.Value = new decimal(new int[] { 99999, 0, 0, 0 });
|
||||
//
|
||||
// lblMinHeight
|
||||
//
|
||||
this.lblMinHeight.AutoSize = true;
|
||||
this.lblMinHeight.Location = new System.Drawing.Point(520, 20);
|
||||
this.lblMinHeight.Name = "lblMinHeight";
|
||||
this.lblMinHeight.Size = new System.Drawing.Size(67, 15);
|
||||
this.lblMinHeight.TabIndex = 8;
|
||||
this.lblMinHeight.Text = "Min Height";
|
||||
//
|
||||
// numMinHeight
|
||||
//
|
||||
this.numMinHeight.Location = new System.Drawing.Point(520, 40);
|
||||
this.numMinHeight.Maximum = new decimal(new int[] { 9999999, 0, 0, 0 });
|
||||
this.numMinHeight.Name = "numMinHeight";
|
||||
this.numMinHeight.Size = new System.Drawing.Size(110, 23);
|
||||
this.numMinHeight.TabIndex = 9;
|
||||
//
|
||||
// lblMaxHeight
|
||||
//
|
||||
this.lblMaxHeight.AutoSize = true;
|
||||
this.lblMaxHeight.Location = new System.Drawing.Point(640, 20);
|
||||
this.lblMaxHeight.Name = "lblMaxHeight";
|
||||
this.lblMaxHeight.Size = new System.Drawing.Size(70, 15);
|
||||
this.lblMaxHeight.TabIndex = 10;
|
||||
this.lblMaxHeight.Text = "Max Height";
|
||||
//
|
||||
// numMaxHeight
|
||||
//
|
||||
this.numMaxHeight.Location = new System.Drawing.Point(640, 40);
|
||||
this.numMaxHeight.Maximum = new decimal(new int[] { 9999999, 0, 0, 0 });
|
||||
this.numMaxHeight.Name = "numMaxHeight";
|
||||
this.numMaxHeight.Size = new System.Drawing.Size(110, 23);
|
||||
this.numMaxHeight.TabIndex = 11;
|
||||
this.numMaxHeight.Value = new decimal(new int[] { 99999, 0, 0, 0 });
|
||||
//
|
||||
// btnApply
|
||||
//
|
||||
this.btnApply.Location = new System.Drawing.Point(10, 555);
|
||||
this.btnApply.Name = "btnApply";
|
||||
this.btnApply.Size = new System.Drawing.Size(375, 32);
|
||||
this.btnApply.TabIndex = 12;
|
||||
this.btnApply.Text = "Apply";
|
||||
this.btnApply.UseVisualStyleBackColor = true;
|
||||
this.btnApply.Click += new System.EventHandler(this.BtnApply_Click);
|
||||
//
|
||||
// btnStandardize
|
||||
//
|
||||
this.btnStandardize.Location = new System.Drawing.Point(395, 555);
|
||||
this.btnStandardize.Name = "btnStandardize";
|
||||
this.btnStandardize.Size = new System.Drawing.Size(375, 32);
|
||||
this.btnStandardize.TabIndex = 13;
|
||||
this.btnStandardize.Text = "Standardize names";
|
||||
this.btnStandardize.UseVisualStyleBackColor = true;
|
||||
this.btnStandardize.Click += new System.EventHandler(this.BtnStandardize_Click);
|
||||
//
|
||||
// txtLog
|
||||
//
|
||||
this.txtLog.Location = new System.Drawing.Point(10, 595);
|
||||
this.txtLog.Multiline = true;
|
||||
this.txtLog.Name = "txtLog";
|
||||
this.txtLog.ReadOnly = true;
|
||||
this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
||||
this.txtLog.Size = new System.Drawing.Size(760, 80);
|
||||
this.txtLog.TabIndex = 14;
|
||||
this.txtLog.WordWrap = false;
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(784, 690);
|
||||
this.Controls.Add(this.txtLog);
|
||||
this.Controls.Add(this.btnStandardize);
|
||||
this.Controls.Add(this.btnApply);
|
||||
this.Controls.Add(this.grpBounds);
|
||||
this.Controls.Add(this.btnClearFiles);
|
||||
this.Controls.Add(this.btnRemoveFiles);
|
||||
this.Controls.Add(this.btnAddFiles);
|
||||
this.Controls.Add(this.lstFiles);
|
||||
this.Controls.Add(this.lblFiles);
|
||||
this.Controls.Add(this.lstClasses);
|
||||
this.Controls.Add(this.lblClasses);
|
||||
this.Controls.Add(this.btnLoadClasses);
|
||||
this.Controls.Add(this.btnBrowseModel);
|
||||
this.Controls.Add(this.txtModelPath);
|
||||
this.Controls.Add(this.lblModel);
|
||||
this.Name = "MainForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "Recipe Yolo Patcher";
|
||||
this.grpBounds.ResumeLayout(false);
|
||||
this.grpBounds.PerformLayout();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMinArea)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMaxArea)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMinWidth)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMaxWidth)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMinHeight)).EndInit();
|
||||
((System.ComponentModel.ISupportInitialize)(this.numMaxHeight)).EndInit();
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label lblModel;
|
||||
private System.Windows.Forms.TextBox txtModelPath;
|
||||
private System.Windows.Forms.Button btnBrowseModel;
|
||||
private System.Windows.Forms.Button btnLoadClasses;
|
||||
private System.Windows.Forms.Label lblClasses;
|
||||
private System.Windows.Forms.CheckedListBox lstClasses;
|
||||
private System.Windows.Forms.Label lblFiles;
|
||||
private System.Windows.Forms.ListBox lstFiles;
|
||||
private System.Windows.Forms.Button btnAddFiles;
|
||||
private System.Windows.Forms.Button btnRemoveFiles;
|
||||
private System.Windows.Forms.Button btnClearFiles;
|
||||
private System.Windows.Forms.GroupBox grpBounds;
|
||||
private System.Windows.Forms.Label lblMinArea;
|
||||
private System.Windows.Forms.NumericUpDown numMinArea;
|
||||
private System.Windows.Forms.Label lblMaxArea;
|
||||
private System.Windows.Forms.NumericUpDown numMaxArea;
|
||||
private System.Windows.Forms.Label lblMinWidth;
|
||||
private System.Windows.Forms.NumericUpDown numMinWidth;
|
||||
private System.Windows.Forms.Label lblMaxWidth;
|
||||
private System.Windows.Forms.NumericUpDown numMaxWidth;
|
||||
private System.Windows.Forms.Label lblMinHeight;
|
||||
private System.Windows.Forms.NumericUpDown numMinHeight;
|
||||
private System.Windows.Forms.Label lblMaxHeight;
|
||||
private System.Windows.Forms.NumericUpDown numMaxHeight;
|
||||
private System.Windows.Forms.Button btnApply;
|
||||
private System.Windows.Forms.Button btnStandardize;
|
||||
private System.Windows.Forms.TextBox txtLog;
|
||||
}
|
||||
}
|
||||
312
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/MainForm.cs
Normal file
312
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/MainForm.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Windows.Forms;
|
||||
using Compunet.YoloV8;
|
||||
using Hawkeye.VisionBuilder.Workflow;
|
||||
using Hawkeye.VisionBuilder.Workflow.Operations.AI;
|
||||
|
||||
namespace Hawkeye.VisionBuilder.UI.RecipeYoloPatcher;
|
||||
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private sealed class ClassEntry
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Name { get; init; } = "";
|
||||
public override string ToString() => $"{Id}: {Name}";
|
||||
}
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void BtnBrowseModel_Click(object? sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "ONNX models (*.onnx)|*.onnx",
|
||||
Title = "Select Yolo model"
|
||||
};
|
||||
var defaultDir = Path.GetFullPath(@"..\Data\Models");
|
||||
if (Directory.Exists(defaultDir)) dialog.InitialDirectory = defaultDir;
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
txtModelPath.Text = dialog.FileName;
|
||||
btnLoadClasses.Enabled = true;
|
||||
lstClasses.Items.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnLoadClasses_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(txtModelPath.Text) || !File.Exists(txtModelPath.Text))
|
||||
{
|
||||
MessageBox.Show(this, "Pick a valid .onnx model first.", "Recipe Yolo Patcher",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
lstClasses.Items.Clear();
|
||||
Cursor = Cursors.WaitCursor;
|
||||
try
|
||||
{
|
||||
using var predictor = YoloV8Predictor.Create(txtModelPath.Text);
|
||||
foreach (var cls in predictor.Metadata.Names)
|
||||
{
|
||||
lstClasses.Items.Add(new ClassEntry { Id = cls.Id, Name = cls.Name });
|
||||
}
|
||||
Log($"Loaded {lstClasses.Items.Count} class(es) from {Path.GetFileName(txtModelPath.Text)}.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, $"Failed to load model:\r\n{ex.Message}", "Recipe Yolo Patcher",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cursor = Cursors.Default;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnAddFiles_Click(object? sender, EventArgs e)
|
||||
{
|
||||
using var dialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "Recipes (*.hrcp;*.jhrcp)|*.hrcp;*.jhrcp",
|
||||
Multiselect = true,
|
||||
Title = "Select recipe files"
|
||||
};
|
||||
var defaultDir = Path.GetFullPath(@"..\Data\Recipes");
|
||||
if (Directory.Exists(defaultDir)) dialog.InitialDirectory = defaultDir;
|
||||
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||
{
|
||||
foreach (var f in dialog.FileNames)
|
||||
{
|
||||
if (!lstFiles.Items.Contains(f)) lstFiles.Items.Add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnRemoveFiles_Click(object? sender, EventArgs e)
|
||||
{
|
||||
var selected = lstFiles.SelectedItems.Cast<object>().ToList();
|
||||
foreach (var item in selected) lstFiles.Items.Remove(item);
|
||||
}
|
||||
|
||||
private void BtnClearFiles_Click(object? sender, EventArgs e) => lstFiles.Items.Clear();
|
||||
|
||||
private void BtnApply_Click(object? sender, EventArgs e)
|
||||
{
|
||||
var checkedClasses = lstClasses.CheckedItems.Cast<ClassEntry>().ToList();
|
||||
if (checkedClasses.Count == 0)
|
||||
{
|
||||
MessageBox.Show(this, "Check at least one class to apply.", "Recipe Yolo Patcher",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
var files = lstFiles.Items.Cast<string>().ToList();
|
||||
if (files.Count == 0)
|
||||
{
|
||||
MessageBox.Show(this, "Add at least one recipe file.", "Recipe Yolo Patcher",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var bounds = new PatchBounds(
|
||||
(int)numMinArea.Value, (int)numMaxArea.Value,
|
||||
(int)numMinWidth.Value, (int)numMaxWidth.Value,
|
||||
(int)numMinHeight.Value, (int)numMaxHeight.Value);
|
||||
|
||||
Cursor = Cursors.WaitCursor;
|
||||
btnApply.Enabled = false;
|
||||
try
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var (added, updated) = PatchRecipe(file, checkedClasses, bounds);
|
||||
Log($"OK: {Path.GetFileName(file)} — added {added}, updated {updated}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"FAIL: {Path.GetFileName(file)} — {ex.Message}");
|
||||
}
|
||||
}
|
||||
Log("Done.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cursor = Cursors.Default;
|
||||
btnApply.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnStandardize_Click(object? sender, EventArgs e)
|
||||
{
|
||||
var files = lstFiles.Items.Cast<string>().ToList();
|
||||
if (files.Count == 0)
|
||||
{
|
||||
MessageBox.Show(this, "Add at least one recipe file.", "Recipe Yolo Patcher",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
Cursor = Cursors.WaitCursor;
|
||||
btnApply.Enabled = false;
|
||||
btnStandardize.Enabled = false;
|
||||
try
|
||||
{
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var renamed = StandardizeLabels(file);
|
||||
Log($"OK: {Path.GetFileName(file)} — renamed {renamed} label(s)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"FAIL: {Path.GetFileName(file)} — {ex.Message}");
|
||||
}
|
||||
}
|
||||
Log("Done.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cursor = Cursors.Default;
|
||||
btnApply.Enabled = true;
|
||||
btnStandardize.Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static int StandardizeLabels(string path)
|
||||
{
|
||||
var (wf, ext) = LoadRecipe(path);
|
||||
|
||||
int renamed = 0;
|
||||
foreach (var op in wf.Operations.OfType<YoloPickDetected>())
|
||||
{
|
||||
var newLabel = ToPascalCase(op.Label);
|
||||
if (newLabel != op.Label)
|
||||
{
|
||||
op.Label = newLabel;
|
||||
renamed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (renamed > 0) SaveRecipe(wf, path, ext);
|
||||
return renamed;
|
||||
}
|
||||
|
||||
private static string ToPascalCase(string? label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(label)) return label ?? string.Empty;
|
||||
var parts = label.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||
var sb = new StringBuilder(label.Length);
|
||||
foreach (var p in parts)
|
||||
{
|
||||
sb.Append(char.ToUpperInvariant(p[0]));
|
||||
if (p.Length > 1) sb.Append(p, 1, p.Length - 1);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static (WorkflowList wf, string ext) LoadRecipe(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
WorkflowList wf;
|
||||
if (ext == ".hrcp")
|
||||
{
|
||||
wf = new WorkflowList();
|
||||
using var fs = File.OpenRead(path);
|
||||
using var br = new BinaryReader(fs);
|
||||
wf.Load(br, new OperationDiscoveryService(wf));
|
||||
}
|
||||
else if (ext == ".jhrcp")
|
||||
{
|
||||
wf = WorkflowList.LoadJSONFromFile(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported extension '{ext}'.");
|
||||
}
|
||||
return (wf, ext);
|
||||
}
|
||||
|
||||
private static void SaveRecipe(WorkflowList wf, string path, string ext)
|
||||
{
|
||||
if (ext == ".hrcp")
|
||||
{
|
||||
using var fs = File.Create(path);
|
||||
using var bw = new BinaryWriter(fs);
|
||||
wf.Save(bw);
|
||||
}
|
||||
else
|
||||
{
|
||||
wf.SaveJSON(path);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct PatchBounds(int MinArea, int MaxArea, int MinWidth, int MaxWidth, int MinHeight, int MaxHeight);
|
||||
|
||||
private static (int added, int updated) PatchRecipe(string path, IReadOnlyList<ClassEntry> classes, PatchBounds b)
|
||||
{
|
||||
var (wf, ext) = LoadRecipe(path);
|
||||
|
||||
int anchor = -1;
|
||||
for (int i = 0; i < wf.Operations.Count; i++)
|
||||
{
|
||||
if (wf.Operations[i] is YoloPickDetected) anchor = i;
|
||||
}
|
||||
if (anchor < 0) anchor = wf.Operations.Count - 1;
|
||||
|
||||
var nameMap = wf.Operations.OfType<YoloPickDetected>()
|
||||
.Where(op => !string.IsNullOrEmpty(op.Label))
|
||||
.GroupBy(op => op.Label)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
|
||||
int added = 0, updated = 0;
|
||||
foreach (var cls in classes)
|
||||
{
|
||||
if (nameMap.TryGetValue(cls.Name, out var existing))
|
||||
{
|
||||
existing.MinArea = b.MinArea;
|
||||
existing.MaxArea = b.MaxArea;
|
||||
existing.MinWidth = b.MinWidth;
|
||||
existing.MaxWidth = b.MaxWidth;
|
||||
existing.MinHeight = b.MinHeight;
|
||||
existing.MaxHeight = b.MaxHeight;
|
||||
updated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var op = new YoloPickDetected
|
||||
{
|
||||
Label = cls.Name,
|
||||
TypeId = cls.Id + 1,
|
||||
SlotName = "Image",
|
||||
MinArea = b.MinArea,
|
||||
MaxArea = b.MaxArea,
|
||||
MinWidth = b.MinWidth,
|
||||
MaxWidth = b.MaxWidth,
|
||||
MinHeight = b.MinHeight,
|
||||
MaxHeight = b.MaxHeight
|
||||
};
|
||||
wf.Operations.Insert(++anchor, op);
|
||||
nameMap[cls.Name] = op;
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
SaveRecipe(wf, path, ext);
|
||||
return (added, updated);
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
txtLog.AppendText(msg + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
120
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/MainForm.resx
Normal file
120
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/MainForm.resx
Normal file
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
14
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/Program.cs
Normal file
14
Hawkeye.VisionBuilder.UI.RecipeYoloPatcher/Program.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Hawkeye.VisionBuilder.UI.RecipeYoloPatcher;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new MainForm());
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Emulation
|
||||
try
|
||||
{
|
||||
_currentFolder = imagesFolder;
|
||||
var path = Path.Combine(@"..\Data\Emulation", imagesFolder);
|
||||
var path = Path.Combine("..", "Data", "Emulation", imagesFolder);
|
||||
_selectedFiles = Directory.GetFiles(path, "*.bmp")
|
||||
.Concat(Directory.GetFiles(path, "*.png"))
|
||||
.Concat(Directory.GetFiles(path, "*.jpg"))
|
||||
|
||||
@@ -105,11 +105,13 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Hawkeye
|
||||
var pointer = pinnedArray.AddrOfPinnedObject();
|
||||
|
||||
|
||||
Mat image = new Mat(_cameraSettings.ImageSettings.Lines, _cameraSettings.ImageSettings.SensorWidth,
|
||||
|
||||
Mat image = Mat.FromPixelData(_cameraSettings.ImageSettings.Lines, _cameraSettings.ImageSettings.SensorWidth,
|
||||
MatType.CV_8UC1, pointer);
|
||||
|
||||
|
||||
|
||||
|
||||
var xCrop = _cameraSettings.ImageSettings.SensorWidth - _cameraSettings.ImageWidth -
|
||||
_cameraSettings.OffsetX;
|
||||
|
||||
|
||||
@@ -100,9 +100,9 @@ public class OpenCVBayerProcessor
|
||||
Mat greenCorrected = new Mat();
|
||||
Mat redCorrected = new Mat();
|
||||
|
||||
Mat blueLutMat = new Mat(1, 256, MatType.CV_8U, blueLut);
|
||||
Mat greenLutMat = new Mat(1, 256, MatType.CV_8U, greenLut);
|
||||
Mat redLutMat = new Mat(1, 256, MatType.CV_8U, redLut);
|
||||
Mat blueLutMat = Mat.FromPixelData(1, 256, MatType.CV_8U, blueLut);
|
||||
Mat greenLutMat = Mat.FromPixelData(1, 256, MatType.CV_8U, greenLut);
|
||||
Mat redLutMat = Mat.FromPixelData(1, 256, MatType.CV_8U, redLut);
|
||||
|
||||
Cv2.LUT(channels[0], blueLutMat, blueCorrected);
|
||||
Cv2.LUT(channels[1], greenLutMat, greenCorrected);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,38 @@
|
||||
using Inspectron.Settings;
|
||||
using VisionBuilder.UI.Common;
|
||||
|
||||
namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera;
|
||||
|
||||
public class LibcameraCaptureSettings : ISettings
|
||||
{
|
||||
public string CameraName { get; }
|
||||
|
||||
public LibcameraCaptureSettings(string cameraName)
|
||||
{
|
||||
CameraName = cameraName;
|
||||
}
|
||||
|
||||
public int Width { get; set; } = 2028;
|
||||
public int Height { get; set; } = 1520;
|
||||
public int ShutterSpeed { get; set; } = 10000;
|
||||
public int Framerate { get; set; } = 2;
|
||||
public double AwbGainRed { get; set; } = 3.86;
|
||||
public double AwbGainBlue { get; set; } = 1.46;
|
||||
|
||||
public string BuildArguments()
|
||||
{
|
||||
return $"--codec mjpeg -t0 --width {Width} --height {Height} " +
|
||||
$"--shutter {ShutterSpeed} --framerate {Framerate} " +
|
||||
$"--awbgains {AwbGainRed:F2},{AwbGainBlue:F2} --nopreview -o -";
|
||||
}
|
||||
|
||||
public void RegisterSettings(InspectronSettings settings)
|
||||
{
|
||||
settings.RegisterSimple(this, () => Width, CameraName + "/Sources/Libcamera", nameof(Width));
|
||||
settings.RegisterSimple(this, () => Height, CameraName + "/Sources/Libcamera", nameof(Height));
|
||||
settings.RegisterSimple(this, () => ShutterSpeed, CameraName + "/Sources/Libcamera", nameof(ShutterSpeed));
|
||||
settings.RegisterSimple(this, () => Framerate, CameraName + "/Sources/Libcamera", nameof(Framerate));
|
||||
settings.RegisterSimple(this, () => AwbGainRed, CameraName + "/Sources/Libcamera", nameof(AwbGainRed));
|
||||
settings.RegisterSimple(this, () => AwbGainBlue, CameraName + "/Sources/Libcamera", nameof(AwbGainBlue));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using OpenCvSharp;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Channels;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
|
||||
namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
|
||||
{
|
||||
public class LibcameraImageSource : IImageSource, IVisionBuilderModule
|
||||
{
|
||||
private LibcameraCaptureSettings _settings;
|
||||
|
||||
public LibcameraImageSource(LibcameraCaptureSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public async Task<Mat> GetImage(CancellationToken token)
|
||||
{
|
||||
return await _imageChannel.Reader.ReadAsync(token);
|
||||
}
|
||||
|
||||
public void InitializeModule()
|
||||
{
|
||||
Start();
|
||||
}
|
||||
|
||||
byte[] JpegHeader = new byte[] { 0xff, 0xd8 };
|
||||
byte[] JpegFooter = new byte[] { 0xff, 0xd9 };
|
||||
int ChunkSize = 1024;
|
||||
|
||||
Channel<Mat> _imageChannel = Channel.CreateBounded<Mat>(new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropOldest });
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Thread th = new Thread(StartLoop);
|
||||
th.IsBackground = true;
|
||||
th.Start();
|
||||
|
||||
}
|
||||
|
||||
private void StartLoop()
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "rpicam-vid",
|
||||
Arguments = _settings.BuildArguments(),
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
using (Process process = Process.Start(psi))
|
||||
using (BinaryReader br = new BinaryReader(process.StandardOutput.BaseStream))
|
||||
{
|
||||
|
||||
byte[] imageBuffer = new byte[1024 * 1024];
|
||||
|
||||
|
||||
// decode MJPEG buffer
|
||||
var buff = br.ReadBytes(ChunkSize);
|
||||
int frameId = 0;
|
||||
while (true)
|
||||
{
|
||||
|
||||
|
||||
var imageStart = Find(buff, JpegHeader);
|
||||
if (imageStart != -1)
|
||||
{
|
||||
var size = buff.Length - imageStart;
|
||||
|
||||
Array.Copy(buff, imageStart, imageBuffer, 0, size);
|
||||
while (true)
|
||||
{
|
||||
buff = br.ReadBytes(ChunkSize);
|
||||
var imageEnd = Find(buff, JpegFooter);
|
||||
if (imageEnd != -1)
|
||||
{
|
||||
var frame = new byte[size + imageEnd];
|
||||
Array.Copy(imageBuffer, frame, size);
|
||||
Array.Copy(buff, 0, frame, size, imageEnd);
|
||||
// process frame
|
||||
frameId++;
|
||||
var decoded = Mat.ImDecode(frame);
|
||||
|
||||
_imageChannel.Writer.WriteAsync(decoded, CancellationToken.None);
|
||||
|
||||
// copy the leftover data to the start
|
||||
Array.Copy(buff, imageEnd, buff, 0, buff.Length - imageEnd);
|
||||
// fill the remainder of the buffer with new data and start over
|
||||
var temp = br.ReadBytes(imageEnd);
|
||||
Array.Copy(temp, 0, buff, buff.Length - imageEnd, temp.Length);
|
||||
break;
|
||||
}
|
||||
|
||||
// copy all of the data to the imageBuffer
|
||||
Array.Copy(buff, 0, imageBuffer, size, buff.Length);
|
||||
size += buff.Length;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("JPEG header not found.");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
process.Kill();
|
||||
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Serilog.Log.Error($"Error starting libcamera: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
public static int Find(byte[] buff, byte[] search)
|
||||
{
|
||||
// enumerate the buffer but don't overstep the bounds
|
||||
for (int start = 0; start < buff.Length - search.Length; start++)
|
||||
{
|
||||
// we found the first character
|
||||
if (buff[start] == search[0])
|
||||
{
|
||||
int next;
|
||||
|
||||
// traverse the rest of the bytes
|
||||
for (next = 1; next < search.Length; next++)
|
||||
{
|
||||
// if we don't match, bail
|
||||
if (buff[start + next] != search[next])
|
||||
break;
|
||||
}
|
||||
|
||||
if (next == search.Length)
|
||||
return start;
|
||||
}
|
||||
}
|
||||
// not found
|
||||
return -1;
|
||||
}
|
||||
|
||||
public async Task<Mat> GetImageAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await _imageChannel.Reader.ReadAsync(cancellationToken);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
<Configurations>Debug;Release;CPU</Configurations>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -32,8 +31,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.6.0.20220608" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.6.0.20220608" />
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.10.0.20241108" />
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ public class AnomalyAI: BaseOperation
|
||||
|
||||
|
||||
// black and white mask
|
||||
var mask = new Mat(30, 48, MatType.CV_8UC1, resultColor);
|
||||
var mask = new Mat(30, 48, MatType.CV_8UC1);
|
||||
mask.SetArray(resultColor);
|
||||
|
||||
var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height));
|
||||
|
||||
|
||||
@@ -60,7 +60,8 @@ public class BackgroundSeparationModelOperation:BaseOperation,IHaveOrigin
|
||||
|
||||
var bytes= MatToBytes(img);
|
||||
var responseBytes = _transfer.TransferData(ModelName, 3, bytes);
|
||||
var response = new Mat(img.Rows, img.Cols, MatType.CV_8UC1, responseBytes);
|
||||
var response = new Mat(img.Rows, img.Cols, MatType.CV_8UC1);
|
||||
response.SetArray(responseBytes);
|
||||
|
||||
var contours = response.Threshold(128, 255, ThresholdTypes.Binary).FindContoursAsArray(RetrievalModes.List, ContourApproximationModes.ApproxSimple);
|
||||
|
||||
|
||||
@@ -78,7 +78,8 @@ public class Color128HalfOperation:BaseOperation
|
||||
.ToArray();
|
||||
|
||||
// black and white mask
|
||||
var mask = new Mat(_imageHeight, _imageWidth, MatType.CV_8UC1, resultColor);
|
||||
var mask = new Mat(_imageHeight, _imageWidth, MatType.CV_8UC1);
|
||||
mask.SetArray(resultColor);
|
||||
|
||||
var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height));
|
||||
|
||||
|
||||
@@ -111,7 +111,8 @@ public abstract class ColorAIOperation:BaseOperation
|
||||
|
||||
|
||||
// black and white mask
|
||||
var mask = new Mat(_imageHeight, _imageWidth, MatType.CV_8UC1, resultColor);
|
||||
var mask = new Mat(_imageHeight, _imageWidth, MatType.CV_8UC1);
|
||||
mask.SetArray(resultColor);
|
||||
|
||||
var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height));
|
||||
|
||||
|
||||
@@ -129,7 +129,8 @@ public class ModelAIOperation: BaseOperation
|
||||
|
||||
|
||||
// black and white mask
|
||||
var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1, resultColor);
|
||||
var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1);
|
||||
mask.SetArray(resultColor);
|
||||
|
||||
var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height));
|
||||
|
||||
|
||||
@@ -105,7 +105,8 @@ public class RawModelAIOperation: BaseOperation
|
||||
|
||||
|
||||
var outputSizeArray = outputSize.Select(Convert.ToInt32).ToArray();
|
||||
var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1, resultColor);
|
||||
var mask = new Mat(outputSizeArray[0], outputSizeArray[1], MatType.CV_8UC1);
|
||||
mask.SetArray(resultColor);
|
||||
|
||||
var maskResized = mask.Resize(new Size(currentImage.ImageData.Width, currentImage.ImageData.Height));
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public class SkeletonOperation:BaseOperation
|
||||
if(!CheckGrayscale(context)) return;
|
||||
|
||||
|
||||
var skeleton = new Mat(context.ActiveImage.ImageData.Size(), MatType.CV_8UC1, 0);
|
||||
var skeleton = new Mat(context.ActiveImage.ImageData.Size(), MatType.CV_8UC1, Scalar.All(0));
|
||||
var temp = context.ActiveImage.ImageData.Clone();
|
||||
var element = Cv2.GetStructuringElement(MorphShapes.Cross, new OpenCvSharp.Size(3, 3));
|
||||
var image = context.ActiveImage.ImageData.Clone();
|
||||
|
||||
@@ -49,13 +49,13 @@ public class GaborFilterOperation : BaseOperation
|
||||
}
|
||||
|
||||
var image = context.ActiveImage.ImageData;
|
||||
var newimage = new Mat(image.Size(), image.Type(), 0);
|
||||
var newimage = new Mat(image.Size(), image.Type(), Scalar.All(0));
|
||||
|
||||
foreach (double theta in steps)
|
||||
{
|
||||
var kernel = Cv2.GetGaborKernel(new Size(KSize, KSize), Sigma, theta, Lambda, Gamma, 0, MatType.CV_64F);
|
||||
kernel /= 1 * kernel.Sum().Val0;
|
||||
var filtered = new Mat(image.Size(), image.Type(), 0);
|
||||
var filtered = new Mat(image.Size(), image.Type(), Scalar.All(0));
|
||||
Cv2.Filter2D(image, filtered, -1, kernel);
|
||||
Cv2.Max(newimage, filtered, newimage);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ImageSizeProportionOperation:BaseOperation
|
||||
|
||||
context.ActiveImage = new HawkeyeImage()
|
||||
{
|
||||
ImageData = context.ActiveImage!.ImageData.Resize(Size.Zero, Width, Height)
|
||||
ImageData = context.ActiveImage!.ImageData.Resize(new Size(0,0), Width, Height)
|
||||
};
|
||||
|
||||
Status = $"Image resized to {context.ActiveImage.ImageData.Width}x{context.ActiveImage.ImageData.Height}";
|
||||
|
||||
@@ -18,7 +18,7 @@ public class InvertOperation:BaseOperation
|
||||
if (!CheckImageExists(context)) return;
|
||||
|
||||
|
||||
var res = new Mat(context.ActiveImage.ImageData.Size(), context.ActiveImage.ImageData.Type(), 0);
|
||||
var res = new Mat(context.ActiveImage.ImageData.Size(), context.ActiveImage.ImageData.Type(), Scalar.All(0));
|
||||
Cv2.BitwiseNot(context.ActiveImage.ImageData, res);
|
||||
context.ActiveImage = new HawkeyeImage()
|
||||
{
|
||||
|
||||
@@ -31,7 +31,7 @@ public class TakeBiggestOperation : BaseOperation
|
||||
var largestContour = contours.OrderByDescending(c => Cv2.ContourArea(c)).First();
|
||||
var mask = Mat.Zeros(image.Size(), MatType.CV_8UC1).ToMat();
|
||||
|
||||
Cv2.DrawContours(mask, new[] {largestContour}, -1, 255, -1);
|
||||
Cv2.DrawContours(mask, new[] {largestContour}, -1, Scalar.All(255), -1);
|
||||
|
||||
context.ActiveImage = new HawkeyeImage()
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ public class DistanceTransformOperation:BaseOperation
|
||||
|
||||
Cv2.MinMaxLoc(distance, out double minVal, out var maxVal);
|
||||
// clamp the distance transform to 0-255
|
||||
Cv2.Normalize(distance, res, 0, maxVal, NormTypes.MinMax, MatType.CV_8UC1);
|
||||
Cv2.Normalize(distance, res, 0, maxVal, NormTypes.MinMax, (int)MatType.CV_8UC1);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoCompleteMenu-ScintillaNET" Version="2.1.0" />
|
||||
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.6.0.20220608" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.6.0.20220608" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.6.0.20220608" />
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="Scintilla.NET" Version="5.3.2.7" />
|
||||
<PackageReference Include="WindowsAPICodePack" Version="8.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace CandyboxPlugin.Detectors
|
||||
// get this dll path
|
||||
var path=Assembly.GetCallingAssembly().Location;
|
||||
var dir=System.IO.Path.GetDirectoryName(path);
|
||||
var modelPath = Path.Combine(dir,"models\\" + MODEL_NAME);
|
||||
var modelPath = Path.Combine(dir,"models" , MODEL_NAME);
|
||||
_AIsession = new InferenceSession(modelPath, opts);
|
||||
_inputName = _AIsession.InputMetadata.First().Key;
|
||||
_dimensions = _AIsession.InputMetadata.First().Value.Dimensions;
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace CandyboxPlugin.Detectors
|
||||
// get this dll path
|
||||
var path = Assembly.GetCallingAssembly().Location;
|
||||
var dir = System.IO.Path.GetDirectoryName(path);
|
||||
var modelPath = Path.Combine(dir, "models\\" + MODEL_NAME);
|
||||
var modelPath = Path.Combine(dir, "models" , MODEL_NAME);
|
||||
_AIsession = new InferenceSession(modelPath, opts);
|
||||
_inputName = _AIsession.InputMetadata.First().Key;
|
||||
_dimensions = _AIsession.InputMetadata.First().Value.Dimensions;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
71
Plugins/LindtLeerformPlugin/LeerformModule.cs
Normal file
71
Plugins/LindtLeerformPlugin/LeerformModule.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using LindtLeerformPlugin.Services;
|
||||
using LindtLeerformPlugin.ViewModels;
|
||||
using LindtLeerformPlugin.Views;
|
||||
using Serilog;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||
|
||||
namespace LindtLeerformPlugin;
|
||||
|
||||
public class LeerformModule : IVisionBuilderModule
|
||||
{
|
||||
private readonly SingleCameraVM _singleCameraVm;
|
||||
private readonly LeerformSettings _settings;
|
||||
private readonly IImageSource _imageSource;
|
||||
private readonly LeerformPatternRecognitionService _patternService;
|
||||
private readonly LeerformRecipeStore _recipeStore;
|
||||
|
||||
public LeerformModule(
|
||||
SingleCameraVM singleCameraVm,
|
||||
LeerformSettings settings,
|
||||
IImageSource imageSource,
|
||||
LeerformPatternRecognitionService patternService,
|
||||
LeerformRecipeStore recipeStore)
|
||||
{
|
||||
_singleCameraVm = singleCameraVm;
|
||||
_settings = settings;
|
||||
_imageSource = imageSource;
|
||||
_patternService = patternService;
|
||||
_recipeStore = recipeStore;
|
||||
}
|
||||
|
||||
public void InitializeModule()
|
||||
{
|
||||
var calibrateButton = new ButtonDefinition("leerform_calibrate", "Calibrate", OnCalibrateClicked);
|
||||
_singleCameraVm.AddButton(calibrateButton);
|
||||
}
|
||||
|
||||
private void OnCalibrateClicked()
|
||||
{
|
||||
_ = ShowCalibrationWindowAsync();
|
||||
}
|
||||
|
||||
private async Task ShowCalibrationWindowAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
Log.Warning("Avalonia desktop lifetime not available, cannot show calibration window.");
|
||||
return;
|
||||
}
|
||||
|
||||
var parent = desktop.MainWindow;
|
||||
var vm = new CalibrationWindowViewModel(_imageSource, _settings, _singleCameraVm, _patternService, _recipeStore);
|
||||
var window = new CalibrationWindow { DataContext = vm };
|
||||
|
||||
if (parent != null)
|
||||
await window.ShowDialog(parent);
|
||||
else
|
||||
window.Show();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error showing calibration window");
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
92
Plugins/LindtLeerformPlugin/LeerformRecognitionControl.cs
Normal file
92
Plugins/LindtLeerformPlugin/LeerformRecognitionControl.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
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 readonly LeerformPatternRecognitionService _patternService;
|
||||
private readonly LeerformRecipeStore _recipeStore;
|
||||
private Mat? _cameraMatrix;
|
||||
private Mat? _distCoeffs;
|
||||
|
||||
public LeerformRecognitionControl(
|
||||
LeerformRecognitionControlSettings settings,
|
||||
IImageSource imageSource,
|
||||
ILoadingService loadingService,
|
||||
LeerformPatternRecognitionService patternService,
|
||||
LeerformRecipeStore recipeStore) : base(settings, loadingService)
|
||||
{
|
||||
_imageSource = imageSource;
|
||||
_patternService = patternService;
|
||||
_recipeStore = recipeStore;
|
||||
}
|
||||
|
||||
public override List<RecipeData> GetRecipesData()
|
||||
{
|
||||
var names = _recipeStore.ListRecipeNames();
|
||||
if (names.Count == 0)
|
||||
return [new RecipeData { RecipeName = "Default" }];
|
||||
|
||||
var recipes = new List<RecipeData>(names.Count);
|
||||
foreach (var name in names)
|
||||
{
|
||||
var recipe = _recipeStore.Load(name);
|
||||
recipes.Add(new RecipeData
|
||||
{
|
||||
RecipeName = name,
|
||||
Image = recipe != null ? LeerformRecipeStore.DecodeThumbnail(recipe) : null
|
||||
});
|
||||
}
|
||||
return recipes;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
25
Plugins/LindtLeerformPlugin/LeerformSettings.cs
Normal file
25
Plugins/LindtLeerformPlugin/LeerformSettings.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Inspectron.Settings;
|
||||
using VisionBuilder.UI.Common;
|
||||
|
||||
namespace LindtLeerformPlugin;
|
||||
|
||||
public class LeerformSettings : ISettings
|
||||
{
|
||||
public string CameraName { get; }
|
||||
|
||||
public LeerformSettings(string cameraName)
|
||||
{
|
||||
CameraName = cameraName;
|
||||
}
|
||||
|
||||
public int CheckerboardRows { get; set; } = 6;
|
||||
public int CheckerboardCols { get; set; } = 9;
|
||||
public string CalibrationImageDirectory { get; set; } = Path.Combine("..", "Data", "CalibrationImages");
|
||||
|
||||
public void RegisterSettings(InspectronSettings settings)
|
||||
{
|
||||
settings.RegisterSimple(this, () => CheckerboardRows, $"{CameraName}/Leerform", nameof(CheckerboardRows));
|
||||
settings.RegisterSimple(this, () => CheckerboardCols, $"{CameraName}/Leerform", nameof(CheckerboardCols));
|
||||
settings.RegisterSimple(this, () => CalibrationImageDirectory, $"{CameraName}/Leerform", nameof(CalibrationImageDirectory));
|
||||
}
|
||||
}
|
||||
45
Plugins/LindtLeerformPlugin/LindtLeerformPlugin.csproj
Normal file
45
Plugins/LindtLeerformPlugin/LindtLeerformPlugin.csproj
Normal file
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<OutDir>..\..\VisionBuilder.UI.Avalonia.Uno\bin\Debug\Data\Plugins\LindtLeerformPlugin</OutDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\framework\Inspectron.Settings\Inspectron.Settings.csproj">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.2.3">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.3">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108">
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime</ExcludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
10
Plugins/LindtLeerformPlugin/Models/CalibrationData.cs
Normal file
10
Plugins/LindtLeerformPlugin/Models/CalibrationData.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace LindtLeerformPlugin.Models;
|
||||
|
||||
public class CalibrationData
|
||||
{
|
||||
public double[] CameraMatrix { get; set; } = [];
|
||||
public double[] DistCoeffs { get; set; } = [];
|
||||
public double RmsError { get; set; }
|
||||
public int ImageWidth { get; set; }
|
||||
public int ImageHeight { get; set; }
|
||||
}
|
||||
13
Plugins/LindtLeerformPlugin/Models/CellPattern.cs
Normal file
13
Plugins/LindtLeerformPlugin/Models/CellPattern.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace LindtLeerformPlugin.Models;
|
||||
|
||||
public class CellPattern
|
||||
{
|
||||
public int Rows { get; set; } = 4;
|
||||
public int Cols { get; set; } = 5;
|
||||
public CellShape Shape { get; set; } = CellShape.Round;
|
||||
public int Padding { get; set; } = 5;
|
||||
public int RoiX { get; set; }
|
||||
public int RoiY { get; set; }
|
||||
public int RoiWidth { get; set; }
|
||||
public int RoiHeight { get; set; }
|
||||
}
|
||||
13
Plugins/LindtLeerformPlugin/Models/CellRegion.cs
Normal file
13
Plugins/LindtLeerformPlugin/Models/CellRegion.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace LindtLeerformPlugin.Models;
|
||||
|
||||
public class CellRegion
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public int Row { get; set; }
|
||||
public int Col { get; set; }
|
||||
public Rect BoundingBox { get; set; }
|
||||
public Point Center { get; set; }
|
||||
public int Radius { get; set; }
|
||||
}
|
||||
15
Plugins/LindtLeerformPlugin/Models/CellResult.cs
Normal file
15
Plugins/LindtLeerformPlugin/Models/CellResult.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
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>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; }
|
||||
}
|
||||
7
Plugins/LindtLeerformPlugin/Models/CellShape.cs
Normal file
7
Plugins/LindtLeerformPlugin/Models/CellShape.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace LindtLeerformPlugin.Models;
|
||||
|
||||
public enum CellShape
|
||||
{
|
||||
Square,
|
||||
Round
|
||||
}
|
||||
21
Plugins/LindtLeerformPlugin/Models/LeerformRecipe.cs
Normal file
21
Plugins/LindtLeerformPlugin/Models/LeerformRecipe.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace LindtLeerformPlugin.Models;
|
||||
|
||||
public class LeerformRecipe
|
||||
{
|
||||
public string RecipeName { get; set; } = string.Empty;
|
||||
public CellPattern Pattern { 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;
|
||||
}
|
||||
39
Plugins/LindtLeerformPlugin/Plugin.cs
Normal file
39
Plugins/LindtLeerformPlugin/Plugin.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using LindtLeerformPlugin.Services;
|
||||
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;
|
||||
|
||||
public class Plugin : IPlugin
|
||||
{
|
||||
public void RegisterGlobalModules(IKernel kernel)
|
||||
{
|
||||
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.Bind<LeerformPatternRecognitionService>().ToSelf().InSingletonScope();
|
||||
kernel.Bind<LeerformRecipeStore>().ToSelf().InSingletonScope();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
101
Plugins/LindtLeerformPlugin/Services/CameraCalibrationService.cs
Normal file
101
Plugins/LindtLeerformPlugin/Services/CameraCalibrationService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using LindtLeerformPlugin.Models;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace LindtLeerformPlugin.Services;
|
||||
|
||||
public static class CameraCalibrationService
|
||||
{
|
||||
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;
|
||||
|
||||
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));
|
||||
|
||||
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 (Mat cameraMatrix, Mat distCoeffs) LoadCalibrationMats(CalibrationData 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;
|
||||
}
|
||||
}
|
||||
13
Plugins/LindtLeerformPlugin/Services/ImageConverter.cs
Normal file
13
Plugins/LindtLeerformPlugin/Services/ImageConverter.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace LindtLeerformPlugin.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using LindtLeerformPlugin.Models;
|
||||
using OpenCvSharp;
|
||||
|
||||
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
|
||||
|
||||
public IReadOnlyList<CellRegion> ComputeCells(CellPattern pattern)
|
||||
{
|
||||
var cells = new List<CellRegion>();
|
||||
if (pattern.Rows <= 0 || pattern.Cols <= 0 || pattern.RoiWidth <= 0 || pattern.RoiHeight <= 0)
|
||||
return cells;
|
||||
|
||||
var cellW = pattern.RoiWidth / pattern.Cols;
|
||||
var cellH = pattern.RoiHeight / pattern.Rows;
|
||||
var padding = Math.Max(0, pattern.Padding);
|
||||
|
||||
var index = 0;
|
||||
for (var r = 0; r < pattern.Rows; r++)
|
||||
{
|
||||
for (var c = 0; c < pattern.Cols; c++)
|
||||
{
|
||||
var x = pattern.RoiX + c * cellW + padding;
|
||||
var y = pattern.RoiY + r * cellH + padding;
|
||||
var w = Math.Max(1, cellW - 2 * padding);
|
||||
var h = Math.Max(1, cellH - 2 * padding);
|
||||
var rect = new Rect(x, y, w, h);
|
||||
var center = new Point(x + w / 2, y + h / 2);
|
||||
var radius = Math.Max(1, Math.Min(w, h) / 2);
|
||||
|
||||
cells.Add(new CellRegion
|
||||
{
|
||||
Index = index++,
|
||||
Row = r,
|
||||
Col = c,
|
||||
BoundingBox = rect,
|
||||
Center = center,
|
||||
Radius = radius
|
||||
});
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
|
||||
public Mat BuildMask(CellPattern pattern, Size imageSize, int? cellIndex = null)
|
||||
{
|
||||
var mask = new Mat(imageSize, MatType.CV_8UC1, Scalar.All(0));
|
||||
var cells = ComputeCells(pattern);
|
||||
var color = Scalar.All(255);
|
||||
var imageRect = new Rect(0, 0, imageSize.Width, imageSize.Height);
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (cellIndex.HasValue && cellIndex.Value != cell.Index)
|
||||
continue;
|
||||
|
||||
var clipped = cell.BoundingBox & imageRect;
|
||||
if (clipped.Width <= 0 || clipped.Height <= 0)
|
||||
continue;
|
||||
|
||||
if (pattern.Shape == CellShape.Square)
|
||||
{
|
||||
Cv2.Rectangle(mask, clipped, color, thickness: -1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Cv2.Circle(mask, cell.Center, cell.Radius, color, thickness: -1);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 static Mat DetectChocolateMask(Mat bgrImage, int aThreshold, int lThreshold)
|
||||
{
|
||||
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 imageSize = new Size(bgrImage.Cols, bgrImage.Rows);
|
||||
var results = new List<CellResult>(cells.Count);
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var contourArea = (int)Cv2.ContourArea(contour);
|
||||
if (contourArea >= recipe.MinBlobArea && contourArea <= recipe.MaxBlobArea)
|
||||
{
|
||||
blobs++;
|
||||
area += contourArea;
|
||||
}
|
||||
}
|
||||
|
||||
results.Add(new CellResult
|
||||
{
|
||||
Index = cell.Index,
|
||||
IsGood = blobs == 0,
|
||||
BlobCount = blobs,
|
||||
DetectedArea = area
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
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);
|
||||
const int thickness = 2;
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (!resultByIndex.TryGetValue(cell.Index, out var result))
|
||||
continue;
|
||||
|
||||
var color = result.IsGood ? GoodColor : BadColor;
|
||||
|
||||
if (pattern.Shape == CellShape.Square)
|
||||
Cv2.Rectangle(overlay, cell.BoundingBox, color, thickness);
|
||||
else
|
||||
Cv2.Circle(overlay, cell.Center, cell.Radius, color, thickness);
|
||||
}
|
||||
return overlay;
|
||||
}
|
||||
|
||||
public void DrawPattern(Mat target, CellPattern pattern, Scalar color, int thickness = 2)
|
||||
{
|
||||
var cells = ComputeCells(pattern);
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (pattern.Shape == CellShape.Square)
|
||||
Cv2.Rectangle(target, cell.BoundingBox, color, thickness);
|
||||
else
|
||||
Cv2.Circle(target, cell.Center, cell.Radius, color, thickness);
|
||||
}
|
||||
}
|
||||
|
||||
public int? FindCellAtPoint(int x, int y, CellPattern pattern)
|
||||
{
|
||||
var cells = ComputeCells(pattern);
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (pattern.Shape == CellShape.Square)
|
||||
{
|
||||
if (cell.BoundingBox.Contains(x, y))
|
||||
return cell.Index;
|
||||
}
|
||||
else
|
||||
{
|
||||
var dx = x - cell.Center.X;
|
||||
var dy = y - cell.Center.Y;
|
||||
if (dx * dx + dy * dy <= cell.Radius * cell.Radius)
|
||||
return cell.Index;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
77
Plugins/LindtLeerformPlugin/Services/LeerformRecipeStore.cs
Normal file
77
Plugins/LindtLeerformPlugin/Services/LeerformRecipeStore.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using LindtLeerformPlugin.Models;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace LindtLeerformPlugin.Services;
|
||||
|
||||
public class LeerformRecipeStore
|
||||
{
|
||||
private const string RecipeExtension = ".jleerform";
|
||||
|
||||
public string RecipesDirectory { get; } = Path.Combine("..", "Data", "Recipes");
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public IReadOnlyList<string> ListRecipeNames()
|
||||
{
|
||||
if (!Directory.Exists(RecipesDirectory))
|
||||
return Array.Empty<string>();
|
||||
|
||||
return Directory.GetFiles(RecipesDirectory, "*" + RecipeExtension)
|
||||
.Select(Path.GetFileNameWithoutExtension)
|
||||
.Where(name => !string.IsNullOrEmpty(name))
|
||||
.Select(name => name!)
|
||||
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public LeerformRecipe? Load(string recipeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(recipeName))
|
||||
return null;
|
||||
|
||||
var path = GetRecipePath(recipeName);
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
return JsonSerializer.Deserialize<LeerformRecipe>(json, JsonOptions);
|
||||
}
|
||||
|
||||
public void Save(LeerformRecipe recipe)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(recipe.RecipeName))
|
||||
throw new ArgumentException("Recipe must have a name", nameof(recipe));
|
||||
|
||||
Directory.CreateDirectory(RecipesDirectory);
|
||||
var path = GetRecipePath(recipe.RecipeName);
|
||||
var json = JsonSerializer.Serialize(recipe, JsonOptions);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
public static Mat? DecodeThumbnail(LeerformRecipe recipe)
|
||||
{
|
||||
if (string.IsNullOrEmpty(recipe.ThumbnailBase64))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(recipe.ThumbnailBase64);
|
||||
var mat = Cv2.ImDecode(bytes, ImreadModes.Color);
|
||||
return mat.Empty() ? null : mat;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRecipePath(string recipeName) =>
|
||||
Path.Combine(RecipesDirectory, recipeName + RecipeExtension);
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LindtLeerformPlugin.Models;
|
||||
using LindtLeerformPlugin.Services;
|
||||
using OpenCvSharp;
|
||||
using Serilog;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
|
||||
namespace LindtLeerformPlugin.ViewModels;
|
||||
|
||||
public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IImageSource _imageSource;
|
||||
private readonly LeerformSettings _settings;
|
||||
private readonly SingleCameraVM _singleCameraVm;
|
||||
private readonly LeerformPatternRecognitionService _patternService;
|
||||
private readonly LeerformRecipeStore _recipeStore;
|
||||
|
||||
private CancellationTokenSource? _previewCts;
|
||||
private Mat? _lastFrame;
|
||||
private Mat? _calibCameraMatrix;
|
||||
private Mat? _calibDistCoeffs;
|
||||
private Mat? _referenceFrame;
|
||||
|
||||
[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;
|
||||
|
||||
// Pattern
|
||||
[ObservableProperty] private decimal? _rows = 4m;
|
||||
[ObservableProperty] private decimal? _cols = 5m;
|
||||
[ObservableProperty] private CellShape _selectedCellShape = CellShape.Round;
|
||||
[ObservableProperty] private decimal? _padding = 5m;
|
||||
|
||||
// ROI
|
||||
[ObservableProperty] private decimal? _roiX = 0m;
|
||||
[ObservableProperty] private decimal? _roiY = 0m;
|
||||
[ObservableProperty] private decimal? _roiWidth = 0m;
|
||||
[ObservableProperty] private decimal? _roiHeight = 0m;
|
||||
|
||||
// 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";
|
||||
|
||||
public CellShape[] AvailableCellShapes { get; } = Enum.GetValues<CellShape>();
|
||||
|
||||
public CalibrationWindowViewModel(
|
||||
IImageSource imageSource,
|
||||
LeerformSettings settings,
|
||||
SingleCameraVM singleCameraVm,
|
||||
LeerformPatternRecognitionService patternService,
|
||||
LeerformRecipeStore recipeStore)
|
||||
{
|
||||
_imageSource = imageSource;
|
||||
_settings = settings;
|
||||
_singleCameraVm = singleCameraVm;
|
||||
_patternService = patternService;
|
||||
_recipeStore = recipeStore;
|
||||
|
||||
_calibrationImageDirectory = settings.CalibrationImageDirectory;
|
||||
|
||||
var calibrationData = CalibrationDataStore.Load();
|
||||
_isCalibrated = calibrationData is { CameraMatrix.Length: > 0 };
|
||||
_rmsError = calibrationData?.RmsError ?? 0;
|
||||
|
||||
if (_isCalibrated)
|
||||
LoadCalibrationMats(calibrationData!);
|
||||
|
||||
UpdateCapturedImageCount();
|
||||
|
||||
_currentRecipeName = _singleCameraVm.SelectedRecipe?.RecipeName ?? "default";
|
||||
_singleCameraVm.PropertyChanged += OnSingleCameraVmPropertyChanged;
|
||||
|
||||
TryLoadRecipe(_currentRecipeName);
|
||||
}
|
||||
|
||||
private void OnSingleCameraVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName != nameof(SingleCameraVM.SelectedRecipe))
|
||||
return;
|
||||
|
||||
var name = _singleCameraVm.SelectedRecipe?.RecipeName ?? "default";
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
CurrentRecipeName = name;
|
||||
TryLoadRecipe(name);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateCapturedImageCount()
|
||||
{
|
||||
if (Directory.Exists(CalibrationImageDirectory))
|
||||
CapturedImageCount = Directory.GetFiles(CalibrationImageDirectory, "*.png").Length;
|
||||
else
|
||||
CapturedImageCount = 0;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void StartPreview()
|
||||
{
|
||||
if (IsPreviewRunning) return;
|
||||
|
||||
IsPreviewRunning = true;
|
||||
_previewCts = new CancellationTokenSource();
|
||||
_ = PreviewLoopAsync(_previewCts.Token);
|
||||
StatusText = "Preview running";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void StopPreview()
|
||||
{
|
||||
if (!IsPreviewRunning) return;
|
||||
|
||||
_previewCts?.Cancel();
|
||||
IsPreviewRunning = false;
|
||||
StatusText = "Preview stopped";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CaptureImage()
|
||||
{
|
||||
if (_lastFrame == null || _lastFrame.Empty()) return;
|
||||
|
||||
Directory.CreateDirectory(CalibrationImageDirectory);
|
||||
var filename = $"calib_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png";
|
||||
var path = Path.Combine(CalibrationImageDirectory, filename);
|
||||
Cv2.ImWrite(path, _lastFrame);
|
||||
UpdateCapturedImageCount();
|
||||
StatusText = $"Captured: {filename}";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RunCalibration()
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "Calibrating...";
|
||||
var patternSize = new Size(_settings.CheckerboardCols, _settings.CheckerboardRows);
|
||||
var data = CameraCalibrationService.Calibrate(CalibrationImageDirectory, patternSize);
|
||||
|
||||
CalibrationDataStore.Save(data);
|
||||
_settings.CalibrationImageDirectory = CalibrationImageDirectory;
|
||||
|
||||
LoadCalibrationMats(data);
|
||||
IsCalibrated = true;
|
||||
RmsError = data.RmsError;
|
||||
StatusText = $"Calibrated (RMS: {data.RmsError:F4})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Calibration error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearCalibrationImages()
|
||||
{
|
||||
if (!Directory.Exists(CalibrationImageDirectory)) return;
|
||||
|
||||
foreach (var file in Directory.GetFiles(CalibrationImageDirectory, "*.png"))
|
||||
File.Delete(file);
|
||||
|
||||
UpdateCapturedImageCount();
|
||||
StatusText = "Calibration images cleared";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void TestPattern() => RunTestPattern();
|
||||
|
||||
private void RunTestPattern()
|
||||
{
|
||||
using var frame = GetAnalysisFrame();
|
||||
if (frame == null)
|
||||
{
|
||||
StatusText = "No active frame to test";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var recipe = BuildCurrentRecipe();
|
||||
var results = _patternService.EvaluateCells(frame, recipe);
|
||||
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";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to test pattern");
|
||||
StatusText = $"Test error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private Mat? GetAnalysisFrame()
|
||||
{
|
||||
if (_lastFrame == null || _lastFrame.Empty())
|
||||
return null;
|
||||
|
||||
var frame = new Mat();
|
||||
if (_calibCameraMatrix != null && _calibDistCoeffs != null)
|
||||
Cv2.Undistort(_lastFrame, frame, _calibCameraMatrix, _calibDistCoeffs);
|
||||
else
|
||||
_lastFrame.CopyTo(frame);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveRecipe()
|
||||
{
|
||||
try
|
||||
{
|
||||
var recipe = BuildCurrentRecipe();
|
||||
recipe.RecipeName = CurrentRecipeName;
|
||||
|
||||
if (_referenceFrame != null && !_referenceFrame.Empty())
|
||||
{
|
||||
using var thumb = new Mat();
|
||||
Cv2.Resize(_referenceFrame, thumb, new Size(128, 128), 0, 0, InterpolationFlags.Area);
|
||||
var bytes = thumb.ImEncode(".png");
|
||||
recipe.ThumbnailBase64 = Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
_recipeStore.Save(recipe);
|
||||
StatusText = $"Saved recipe '{recipe.RecipeName}'";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to save recipe");
|
||||
StatusText = $"Save error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void TryLoadRecipe(string recipeName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var recipe = _recipeStore.Load(recipeName);
|
||||
if (recipe == null)
|
||||
return;
|
||||
|
||||
Rows = recipe.Pattern.Rows;
|
||||
Cols = recipe.Pattern.Cols;
|
||||
SelectedCellShape = recipe.Pattern.Shape;
|
||||
Padding = recipe.Pattern.Padding;
|
||||
RoiX = recipe.Pattern.RoiX;
|
||||
RoiY = recipe.Pattern.RoiY;
|
||||
RoiWidth = recipe.Pattern.RoiWidth;
|
||||
RoiHeight = recipe.Pattern.RoiHeight;
|
||||
|
||||
AThreshold = recipe.AThreshold;
|
||||
LThreshold = recipe.LThreshold;
|
||||
MinBlobArea = recipe.MinBlobArea;
|
||||
MaxBlobArea = recipe.MaxBlobArea;
|
||||
|
||||
_referenceFrame?.Dispose();
|
||||
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
|
||||
|
||||
StatusText = $"Loaded recipe '{recipeName}'";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to load recipe '{Recipe}'", recipeName);
|
||||
}
|
||||
}
|
||||
|
||||
private CellPattern BuildCurrentPattern() => new()
|
||||
{
|
||||
Rows = (int)(Rows ?? 1m),
|
||||
Cols = (int)(Cols ?? 1m),
|
||||
Shape = SelectedCellShape,
|
||||
Padding = (int)(Padding ?? 0m),
|
||||
RoiX = (int)(RoiX ?? 0m),
|
||||
RoiY = (int)(RoiY ?? 0m),
|
||||
RoiWidth = (int)(RoiWidth ?? 0m),
|
||||
RoiHeight = (int)(RoiHeight ?? 0m)
|
||||
};
|
||||
|
||||
private LeerformRecipe BuildCurrentRecipe() => new()
|
||||
{
|
||||
RecipeName = CurrentRecipeName,
|
||||
Pattern = BuildCurrentPattern(),
|
||||
AThreshold = (int)(AThreshold ?? 140m),
|
||||
LThreshold = (int)(LThreshold ?? 100m),
|
||||
MinBlobArea = (int)(MinBlobArea ?? 100m),
|
||||
MaxBlobArea = (int)(MaxBlobArea ?? 5000m)
|
||||
};
|
||||
|
||||
private async Task PreviewLoopAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var frame = await _imageSource.GetImage(ct);
|
||||
|
||||
_lastFrame?.Dispose();
|
||||
_lastFrame = frame;
|
||||
|
||||
if ((RoiWidth ?? 0m) <= 0m || (RoiHeight ?? 0m) <= 0m)
|
||||
{
|
||||
var w = frame.Cols;
|
||||
var h = frame.Rows;
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
RoiX = 0;
|
||||
RoiY = 0;
|
||||
RoiWidth = w;
|
||||
RoiHeight = h;
|
||||
});
|
||||
}
|
||||
|
||||
var bitmap = RenderFrame(frame);
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
var old = PreviewImage;
|
||||
PreviewImage = bitmap;
|
||||
old?.Dispose();
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
StatusText = $"Preview error: {ex.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Scalar GridColor = new(0, 255, 255); // BGR yellow
|
||||
|
||||
private Avalonia.Media.Imaging.Bitmap RenderFrame(Mat frame)
|
||||
{
|
||||
var displayFrame = frame;
|
||||
var owned = false;
|
||||
|
||||
if (ApplyCalibration && _calibCameraMatrix != null && _calibDistCoeffs != null)
|
||||
{
|
||||
displayFrame = new Mat();
|
||||
Cv2.Undistort(frame, displayFrame, _calibCameraMatrix, _calibDistCoeffs);
|
||||
owned = true;
|
||||
}
|
||||
|
||||
var pattern = BuildCurrentPattern();
|
||||
if (pattern.Rows > 0 && pattern.Cols > 0 && pattern.RoiWidth > 0 && pattern.RoiHeight > 0)
|
||||
{
|
||||
if (!owned)
|
||||
{
|
||||
displayFrame = frame.Clone();
|
||||
owned = true;
|
||||
}
|
||||
_patternService.DrawPattern(displayFrame, pattern, GridColor);
|
||||
}
|
||||
|
||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
|
||||
|
||||
if (owned)
|
||||
displayFrame.Dispose();
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void RefreshPreview()
|
||||
{
|
||||
if (_lastFrame == null || _lastFrame.Empty()) return;
|
||||
|
||||
try
|
||||
{
|
||||
var bitmap = RenderFrame(_lastFrame);
|
||||
var old = PreviewImage;
|
||||
PreviewImage = bitmap;
|
||||
old?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Preview error: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnApplyCalibrationChanged(bool value) => 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)
|
||||
{
|
||||
_calibCameraMatrix?.Dispose();
|
||||
_calibDistCoeffs?.Dispose();
|
||||
(_calibCameraMatrix, _calibDistCoeffs) = CameraCalibrationService.LoadCalibrationMats(data);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_singleCameraVm.PropertyChanged -= OnSingleCameraVmPropertyChanged;
|
||||
StopPreview();
|
||||
_lastFrame?.Dispose();
|
||||
_referenceFrame?.Dispose();
|
||||
_calibCameraMatrix?.Dispose();
|
||||
_calibDistCoeffs?.Dispose();
|
||||
}
|
||||
}
|
||||
146
Plugins/LindtLeerformPlugin/Views/CalibrationWindow.axaml
Normal file
146
Plugins/LindtLeerformPlugin/Views/CalibrationWindow.axaml
Normal file
@@ -0,0 +1,146 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LindtLeerformPlugin.ViewModels"
|
||||
x:Class="LindtLeerformPlugin.Views.CalibrationWindow"
|
||||
x:DataType="vm:CalibrationWindowViewModel"
|
||||
Title="Leerform Calibration"
|
||||
Width="1024" Height="800">
|
||||
|
||||
<Window.Styles>
|
||||
<Style Selector="Button">
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Foreground" Value="Red" />
|
||||
<Setter Property="BorderBrush" Value="Red" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
</Style>
|
||||
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="#FFEBEE" />
|
||||
<Setter Property="BorderBrush" Value="Red" />
|
||||
<Setter Property="TextBlock.Foreground" Value="Red" />
|
||||
</Style>
|
||||
<Style Selector="Button:pressed /template/ ContentPresenter">
|
||||
<Setter Property="Background" Value="#FFCDD2" />
|
||||
<Setter Property="BorderBrush" Value="Red" />
|
||||
<Setter Property="TextBlock.Foreground" Value="Red" />
|
||||
</Style>
|
||||
</Window.Styles>
|
||||
|
||||
<DockPanel>
|
||||
<!-- Toolbar -->
|
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="8">
|
||||
<Button Content="START PREVIEW" Command="{Binding StartPreviewCommand}" IsEnabled="{Binding !IsPreviewRunning}" />
|
||||
<Button Content="STOP PREVIEW" Command="{Binding StopPreviewCommand}" IsEnabled="{Binding IsPreviewRunning}" />
|
||||
<Button Content="CAPTURE IMAGE" Command="{Binding CaptureImageCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<Border DockPanel.Dock="Bottom" Background="#1E1E1E" Padding="8,4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<TextBlock Text="{Binding StatusText}" Foreground="LightGray" />
|
||||
<TextBlock Foreground="LightGray">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="Captured: {0}">
|
||||
<Binding Path="CapturedImageCount" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Settings Panel -->
|
||||
<Border DockPanel.Dock="Right" Width="290" Padding="12" Background="White">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Spacing="8" Margin="0,0,20,0">
|
||||
<TextBlock Text="Calibration Settings" FontWeight="Bold" FontSize="14" Foreground="Black" />
|
||||
|
||||
|
||||
<Button Content="CALIBRATE" Command="{Binding RunCalibrationCommand}" Margin="0,4" />
|
||||
<Button Content="CLEAR IMAGES" Command="{Binding ClearCalibrationImagesCommand}" Margin="0,4" />
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<TextBlock Text="Calibrated" Foreground="LimeGreen" 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" />
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<TextBlock Text="Pattern" FontWeight="Bold" FontSize="14" Foreground="Black" />
|
||||
|
||||
<TextBlock Text="Rows" Foreground="Black" />
|
||||
<NumericUpDown Value="{Binding Rows}" Minimum="1" Maximum="200" Increment="1" FormatString="0" />
|
||||
|
||||
<TextBlock Text="Cols" Foreground="Black" />
|
||||
<NumericUpDown Value="{Binding Cols}" Minimum="1" Maximum="200" Increment="1" FormatString="0" />
|
||||
|
||||
<TextBlock Text="Shape" Foreground="Black" />
|
||||
<ComboBox ItemsSource="{Binding AvailableCellShapes}" SelectedItem="{Binding SelectedCellShape}" HorizontalAlignment="Stretch" />
|
||||
|
||||
<TextBlock Text="Padding (px)" Foreground="Black" />
|
||||
<NumericUpDown Value="{Binding Padding}" Minimum="0" Maximum="500" Increment="1" FormatString="0" />
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<TextBlock Text="ROI (px)" FontWeight="Bold" FontSize="14" Foreground="Black" />
|
||||
|
||||
<TextBlock Text="X" Foreground="Black" />
|
||||
<NumericUpDown Value="{Binding RoiX}" Minimum="0" Maximum="100000" Increment="1" FormatString="0" />
|
||||
<TextBlock Text="Y" Foreground="Black" />
|
||||
<NumericUpDown Value="{Binding RoiY}" Minimum="0" Maximum="100000" Increment="1" FormatString="0" />
|
||||
<TextBlock Text="Width" Foreground="Black" />
|
||||
<NumericUpDown Value="{Binding RoiWidth}" Minimum="0" Maximum="100000" Increment="1" FormatString="0" />
|
||||
<TextBlock Text="Height" Foreground="Black" />
|
||||
<NumericUpDown Value="{Binding RoiHeight}" Minimum="0" Maximum="100000" Increment="1" FormatString="0" />
|
||||
|
||||
<Separator Margin="0,8" />
|
||||
|
||||
<TextBlock Text="Detection Thresholds" FontWeight="Bold" FontSize="14" Foreground="Black" />
|
||||
|
||||
<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" />
|
||||
|
||||
<TextBlock Text="Recipe" FontWeight="Bold" FontSize="14" Foreground="Black" />
|
||||
<TextBlock Foreground="Black">
|
||||
<TextBlock.Text>
|
||||
<MultiBinding StringFormat="Current: {0}">
|
||||
<Binding Path="CurrentRecipeName" />
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
<Button Content="SAVE RECIPE" Command="{Binding SaveRecipeCommand}" Margin="0,4"
|
||||
HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Live Preview -->
|
||||
<Border Background="#1A1A1A" Margin="4">
|
||||
<Image x:Name="PreviewImageControl"
|
||||
Source="{Binding PreviewImage}"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
18
Plugins/LindtLeerformPlugin/Views/CalibrationWindow.axaml.cs
Normal file
18
Plugins/LindtLeerformPlugin/Views/CalibrationWindow.axaml.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Avalonia.Controls;
|
||||
using LindtLeerformPlugin.ViewModels;
|
||||
|
||||
namespace LindtLeerformPlugin.Views;
|
||||
|
||||
public partial class CalibrationWindow : Window
|
||||
{
|
||||
public CalibrationWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnClosing(WindowClosingEventArgs e)
|
||||
{
|
||||
(DataContext as CalibrationWindowViewModel)?.Dispose();
|
||||
base.OnClosing(e);
|
||||
}
|
||||
}
|
||||
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);
|
||||
};
|
||||
}
|
||||
}
|
||||
23
Plugins/LindtLeerformPlugin/jupiter/calibration.json
Normal file
23
Plugins/LindtLeerformPlugin/jupiter/calibration.json
Normal 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
|
||||
}
|
||||
470
Plugins/LindtLeerformPlugin/jupiter/grid_align.ipynb
Normal file
470
Plugins/LindtLeerformPlugin/jupiter/grid_align.ipynb
Normal file
File diff suppressed because one or more lines are too long
376
Plugins/LindtLeerformPlugin/jupiter/grid_corners.ipynb
Normal file
376
Plugins/LindtLeerformPlugin/jupiter/grid_corners.ipynb
Normal file
File diff suppressed because one or more lines are too long
554
Plugins/LindtLeerformPlugin/jupiter/grid_test.ipynb
Normal file
554
Plugins/LindtLeerformPlugin/jupiter/grid_test.ipynb
Normal file
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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
7
VisionBuilder.UI.Avalonia.Uno/App.axaml
Normal file
7
VisionBuilder.UI.Avalonia.Uno/App.axaml
Normal file
@@ -0,0 +1,7 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Uno.App">
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
95
VisionBuilder.UI.Avalonia.Uno/App.axaml.cs
Normal file
95
VisionBuilder.UI.Avalonia.Uno/App.axaml.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Inspectron.Settings;
|
||||
using Ninject;
|
||||
using Ninject.Extensions.ChildKernel;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Plugins;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
using VisionBuilder.UI.Common.Services;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.IOCommander;
|
||||
using VisionBuilder.UI.Recipes.HawkeyeRecipe;
|
||||
using VisionBuilder.UI.Ringbuffer;
|
||||
using VisionBuilder.UI.Statistics;
|
||||
using VisionBuilder.UI.Avalonia;
|
||||
using VisionBuilder.UI.Avalonia.Uno.Views;
|
||||
using Lindt.Colorballs.Duo.Utils;
|
||||
using VisionBuilder.UI.Camera;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Uno;
|
||||
|
||||
public class App : Application
|
||||
{
|
||||
private const string CAMERA1 = "Camera 1";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var args = desktop.Args ?? [];
|
||||
var commandLineParser = new CommandLineParser(args);
|
||||
|
||||
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||
|
||||
var settings = new InspectronSettings("../Data/Config", profile);
|
||||
|
||||
var mainKernel = Common.VisionBuilder.CreateMainKernel(settings);
|
||||
|
||||
mainKernel.UsePlugins(profile);
|
||||
|
||||
mainKernel
|
||||
.UseIOCommander([CAMERA1])
|
||||
.UseAvaloniaServices()
|
||||
.RegisterGlobalPlugins();
|
||||
|
||||
mainKernel.Get<ILoadingService>().StartLoading("Camera initialization");
|
||||
|
||||
// CAMERA 1
|
||||
ChildKernel kernelCamera1 = new ChildKernel(mainKernel);
|
||||
|
||||
kernelCamera1
|
||||
.RegisterCamera(CAMERA1)
|
||||
.UseStatistics(CAMERA1)
|
||||
.UseRingbuffer(CAMERA1)
|
||||
.UseHawkeyeRecipes(CAMERA1)
|
||||
.UseCameraIOCommander(CAMERA1)
|
||||
.RegisterCameraPlugins(CAMERA1);
|
||||
|
||||
// LOAD SETTINGS
|
||||
mainKernel.RegisterSettings();
|
||||
kernelCamera1.RegisterSettings();
|
||||
settings.LoadSettings();
|
||||
|
||||
// Create camera and bind it (must be after settings load)
|
||||
kernelCamera1.UseCamera();
|
||||
|
||||
// Initialize modules
|
||||
mainKernel.InitializeModules();
|
||||
kernelCamera1.InitializeModules();
|
||||
|
||||
mainKernel.Get<ILoadingService>().StopLoading("Camera initialization");
|
||||
|
||||
var mainWindowVm = mainKernel.Get<MainWindowVM>();
|
||||
mainWindowVm.SingleCameraVms = [
|
||||
kernelCamera1.Get<SingleCameraVM>()
|
||||
];
|
||||
|
||||
var avaloniaUiSettings = mainKernel.Get<AvaloniaUISettings>();
|
||||
var uiConfiguration = mainKernel.Get<UIConfiguration>();
|
||||
var passwordInputService = mainKernel.Get<IPasswordInputService>();
|
||||
|
||||
settings.LoadSettings();
|
||||
|
||||
desktop.MainWindow = new MainWindow(mainWindowVm, avaloniaUiSettings, uiConfiguration, passwordInputService);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
27
VisionBuilder.UI.Avalonia.Uno/Program.cs
Normal file
27
VisionBuilder.UI.Avalonia.Uno/Program.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Avalonia;
|
||||
using Avalonia.LinuxFramebuffer;
|
||||
using Serilog;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Uno;
|
||||
|
||||
public class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
if (args.Contains("--drm"))
|
||||
BuildAvaloniaApp().StartLinuxDrm(args, card: null, scaling: 1.0);
|
||||
else
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.LogToTrace();
|
||||
}
|
||||
64
VisionBuilder.UI.Avalonia.Uno/Views/MainWindow.axaml
Normal file
64
VisionBuilder.UI.Avalonia.Uno/Views/MainWindow.axaml
Normal file
@@ -0,0 +1,64 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:views="using:VisionBuilder.UI.Avalonia.Views"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.MainWindow"
|
||||
Title="VisionBuilder UNO"
|
||||
Width="1920" Height="1080"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="#FFFFFF">
|
||||
|
||||
<Grid RowDefinitions="*,1,Auto">
|
||||
<!-- Main camera view -->
|
||||
<views:SingleCameraView x:Name="SingleCameraView" Grid.Row="0" />
|
||||
|
||||
<!-- Divider -->
|
||||
<Border Grid.Row="1" Background="#D32F2F" />
|
||||
|
||||
<!-- Bottom bar -->
|
||||
<Grid Grid.Row="2" Height="70" Background="White">
|
||||
<StackPanel x:Name="BottomBarPanel" Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<Button x:Name="BtnSettings"
|
||||
Content="SETTINGS"
|
||||
Width="176" Height="48"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnMinimize"
|
||||
Content="MINIMIZE"
|
||||
Width="176" Height="48"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnExit"
|
||||
Content="EXIT"
|
||||
Width="176" Height="48"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="LblVersion"
|
||||
Text="v0.0.0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,8,4"
|
||||
FontSize="12"
|
||||
Foreground="#888888" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
217
VisionBuilder.UI.Avalonia.Uno/Views/MainWindow.axaml.cs
Normal file
217
VisionBuilder.UI.Avalonia.Uno/Views/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using VisionBuilder.UI.Avalonia;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Services;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainWindowVM _mainWindowVm;
|
||||
private readonly AvaloniaUISettings _avaloniaUiSettings;
|
||||
private readonly UIConfiguration _uiConfiguration;
|
||||
private readonly IPasswordInputService _passwordInputService;
|
||||
private readonly Dictionary<string, Control> _dynamicButtonControls = new();
|
||||
|
||||
public MainWindow(MainWindowVM mainWindowVm, AvaloniaUISettings avaloniaUiSettings,
|
||||
UIConfiguration uiConfiguration, IPasswordInputService passwordInputService)
|
||||
{
|
||||
_mainWindowVm = mainWindowVm;
|
||||
_avaloniaUiSettings = avaloniaUiSettings;
|
||||
_uiConfiguration = uiConfiguration;
|
||||
_passwordInputService = passwordInputService;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
DataContext = _mainWindowVm;
|
||||
|
||||
SingleCameraView.SetViewModel(_mainWindowVm.SingleCameraVms[0]);
|
||||
|
||||
BtnSettings.Click += BtnSettings_Click;
|
||||
BtnMinimize.Click += BtnMinimize_Click;
|
||||
BtnExit.Click += BtnExit_Click;
|
||||
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
LblVersion.Text = version != null ? $"v{version.Major}.{version.Minor}.{version.Build}" : "v0.0.0";
|
||||
|
||||
_mainWindowVm.PropertyChanged += MainWindowVm_PropertyChanged;
|
||||
|
||||
_mainWindowVm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged;
|
||||
foreach (var btn in _mainWindowVm.DynamicButtons)
|
||||
AddDynamicButton(btn);
|
||||
}
|
||||
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
_mainWindowVm.OnProgramStarted();
|
||||
|
||||
if (_avaloniaUiSettings.FullScreen)
|
||||
{
|
||||
WindowState = WindowState.Maximized;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
base.OnClosed(e);
|
||||
_mainWindowVm.OnProgramClosed();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private void MainWindowVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(_mainWindowVm.TestMode))
|
||||
{
|
||||
// Toggle accent color for test mode visual indication
|
||||
// Green background when test mode is active
|
||||
if (_mainWindowVm.TestMode)
|
||||
{
|
||||
Background = new global::Avalonia.Media.SolidColorBrush(
|
||||
global::Avalonia.Media.Color.FromRgb(0x2E, 0x7D, 0x32)); // Green800
|
||||
}
|
||||
else
|
||||
{
|
||||
Background = new global::Avalonia.Media.SolidColorBrush(
|
||||
global::Avalonia.Media.Colors.White);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void BtnSettings_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_uiConfiguration.ProtectSettingsWithPassword)
|
||||
{
|
||||
var passwordOk = await _passwordInputService.GetPasswordAsync() == _uiConfiguration.AdminPassword;
|
||||
if (!passwordOk)
|
||||
return;
|
||||
}
|
||||
_mainWindowVm.SettingsCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void BtnMinimize_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
WindowState = WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void BtnExit_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_mainWindowVm.OnProgramClosed();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (ButtonDefinition btn in e.NewItems!)
|
||||
AddDynamicButton(btn);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (ButtonDefinition btn in e.OldItems!)
|
||||
RemoveDynamicButton(btn);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
foreach (var ctrl in _dynamicButtonControls.Values)
|
||||
BottomBarPanel.Children.Remove(ctrl);
|
||||
_dynamicButtonControls.Clear();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void AddDynamicButton(ButtonDefinition buttonDef)
|
||||
{
|
||||
Control control;
|
||||
if (buttonDef.Type == EButtonType.Toggle)
|
||||
{
|
||||
var toggleBtn = new ToggleButton
|
||||
{
|
||||
Content = buttonDef.Title.ToUpperInvariant(),
|
||||
Width = 176,
|
||||
Height = 48,
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||
IsChecked = buttonDef.IsToggled,
|
||||
IsVisible = buttonDef.IsVisible,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||
BorderThickness = new Thickness(2),
|
||||
Background = Brushes.White,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||
};
|
||||
toggleBtn.Click += (_, _) => buttonDef.Execute();
|
||||
buttonDef.PropertyChanged += (_, args) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||
toggleBtn.IsVisible = buttonDef.IsVisible;
|
||||
else if (args.PropertyName == nameof(ButtonDefinition.IsToggled))
|
||||
toggleBtn.IsChecked = buttonDef.IsToggled;
|
||||
});
|
||||
};
|
||||
control = toggleBtn;
|
||||
}
|
||||
else
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Content = buttonDef.Title.ToUpperInvariant(),
|
||||
Width = 176,
|
||||
Height = 48,
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||
IsVisible = buttonDef.IsVisible,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||
BorderThickness = new Thickness(2),
|
||||
Background = Brushes.White,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||
};
|
||||
btn.Click += (_, _) => buttonDef.Execute();
|
||||
buttonDef.PropertyChanged += (_, args) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||
btn.IsVisible = buttonDef.IsVisible;
|
||||
});
|
||||
};
|
||||
control = btn;
|
||||
}
|
||||
|
||||
// Insert before BtnMinimize
|
||||
var minimizeIndex = BottomBarPanel.Children.IndexOf(BtnMinimize);
|
||||
if (minimizeIndex >= 0)
|
||||
BottomBarPanel.Children.Insert(minimizeIndex, control);
|
||||
else
|
||||
BottomBarPanel.Children.Add(control);
|
||||
|
||||
_dynamicButtonControls[buttonDef.Key] = control;
|
||||
}
|
||||
|
||||
private void RemoveDynamicButton(ButtonDefinition buttonDef)
|
||||
{
|
||||
if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl))
|
||||
{
|
||||
BottomBarPanel.Children.Remove(ctrl);
|
||||
_dynamicButtonControls.Remove(buttonDef.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<!--<RuntimeIdentifier>linux-arm64</RuntimeIdentifier>-->
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Deterministic>false</Deterministic>
|
||||
<Version>2.0.14</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.3" />
|
||||
<PackageReference Include="Avalonia.LinuxFramebuffer" Version="11.2.3" />
|
||||
<PackageReference Include="Ninject.Extensions.ChildKernel" Version="3.3.0" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="swety.OpenCvSharp4.runtime.linux-arm64" Version="4.10.0.20241108" />
|
||||
<!-- Override transitive GPU OnnxRuntime with CPU-only (GPU has no linux-arm64 natives) -->
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu" Version="1.18.0" ExcludeAssets="all" />
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Windows" Version="1.18.0" ExcludeAssets="all" />
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Linux" Version="1.18.0" ExcludeAssets="all" />
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Avalonia\VisionBuilder.UI.Avalonia.csproj" />
|
||||
<ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj" />
|
||||
<ProjectReference Include="..\Hawkeye.VisionBuilder.UI.Sources.Emulation\Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Camera\VisionBuilder.UI.Camera.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.IOCommander\VisionBuilder.UI.IOCommander.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Recipes.HawkeyeRecipe\VisionBuilder.UI.Recipes.HawkeyeRecipe.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Ringbuffer\VisionBuilder.UI.Ringbuffer.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Statistics\VisionBuilder.UI.Statistics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
14
VisionBuilder.UI.Avalonia/AvaloniaUISettings.cs
Normal file
14
VisionBuilder.UI.Avalonia/AvaloniaUISettings.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Inspectron.Settings;
|
||||
using VisionBuilder.UI.Common;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia;
|
||||
|
||||
public class AvaloniaUISettings : ISettings
|
||||
{
|
||||
public bool FullScreen { get; set; } = false;
|
||||
|
||||
public void RegisterSettings(InspectronSettings settings)
|
||||
{
|
||||
settings.RegisterSimple(this, () => this.FullScreen, "System", nameof(FullScreen));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Converters;
|
||||
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
return value is true;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
23
VisionBuilder.UI.Avalonia/Converters/MatToBitmapConverter.cs
Normal file
23
VisionBuilder.UI.Avalonia/Converters/MatToBitmapConverter.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using OpenCvSharp;
|
||||
using VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Converters;
|
||||
|
||||
public class MatToBitmapConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is Mat mat && !mat.Empty())
|
||||
{
|
||||
return ImageConverter.MatToAvaloniaBitmap(mat);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
22
VisionBuilder.UI.Avalonia/ModuleExtensions.cs
Normal file
22
VisionBuilder.UI.Avalonia/ModuleExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Ninject;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
using VisionBuilder.UI.Common.Services;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
using VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia;
|
||||
|
||||
public static class ModuleExtensions
|
||||
{
|
||||
public static IKernel UseAvaloniaServices(this IKernel self)
|
||||
{
|
||||
self.Bind<ISettingsService>().To<AvaloniaSettingsService>().InSingletonScope();
|
||||
self.Bind<ILoadingService>().To<AvaloniaLoadingService>().InSingletonScope();
|
||||
self.Bind<IRecipeSelectionDialogService>().To<AvaloniaRecipeSelectionDialogService>().InSingletonScope();
|
||||
self.Bind<IImagePreviewService>().To<AvaloniaImagePreviewService>().InSingletonScope();
|
||||
self.Bind<IPasswordInputService>().To<AvaloniaPasswordInputService>().InSingletonScope();
|
||||
self.Bind<AvaloniaUISettings, ISettings>().To<AvaloniaUISettings>().InSingletonScope();
|
||||
return self;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
using VisionBuilder.UI.Avalonia.Views;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
public class AvaloniaImagePreviewService : IImagePreviewService
|
||||
{
|
||||
public async Task ShowImagePreviewAsync(ErrorPreviewVM errorPreviewVm)
|
||||
{
|
||||
var dialog = new ImagePreviewDialog(errorPreviewVm);
|
||||
var parent = GetMainWindow();
|
||||
if (parent != null)
|
||||
await dialog.ShowDialog(parent);
|
||||
}
|
||||
|
||||
private static Window? GetMainWindow()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return desktop.MainWindow;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
17
VisionBuilder.UI.Avalonia/Services/AvaloniaLoadingService.cs
Normal file
17
VisionBuilder.UI.Avalonia/Services/AvaloniaLoadingService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Serilog;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
public class AvaloniaLoadingService : ILoadingService
|
||||
{
|
||||
public void StartLoading(string title)
|
||||
{
|
||||
Log.Information("Loading: {Title}", title);
|
||||
}
|
||||
|
||||
public void StopLoading(string title)
|
||||
{
|
||||
Log.Information("Loaded: {Title}", title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using VisionBuilder.UI.Common.Services;
|
||||
using VisionBuilder.UI.Avalonia.Views;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
public class AvaloniaPasswordInputService : IPasswordInputService
|
||||
{
|
||||
public async Task<string?> GetPasswordAsync()
|
||||
{
|
||||
var dialog = new PasswordDialog();
|
||||
var parent = GetMainWindow();
|
||||
if (parent != null)
|
||||
return await dialog.ShowDialog<string?>(parent);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Window? GetMainWindow()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return desktop.MainWindow;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
using VisionBuilder.UI.Avalonia.Views;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
public class AvaloniaRecipeSelectionDialogService : IRecipeSelectionDialogService
|
||||
{
|
||||
private readonly IRecipeCreationTool _recipeCreationTool;
|
||||
|
||||
public AvaloniaRecipeSelectionDialogService(IRecipeCreationTool recipeCreationTool)
|
||||
{
|
||||
_recipeCreationTool = recipeCreationTool;
|
||||
}
|
||||
|
||||
public async Task<bool> SelectRecipeAsync(RecipeSelectionVM recipeSelectionVm)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
var dialog = new RecipeSelectionDialog(recipeSelectionVm, _recipeCreationTool, tcs);
|
||||
var parent = GetMainWindow();
|
||||
if (parent != null)
|
||||
{
|
||||
await dialog.ShowDialog(parent);
|
||||
return tcs.Task.IsCompleted && tcs.Task.Result;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Window? GetMainWindow()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return desktop.MainWindow;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Inspectron.Settings;
|
||||
using Inspectron.Settings.Avalonia.Configuration;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
public class AvaloniaSettingsService : ISettingsService
|
||||
{
|
||||
private readonly InspectronSettings _settings;
|
||||
|
||||
public AvaloniaSettingsService(InspectronSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public async Task ShowSettingsDialogAsync()
|
||||
{
|
||||
var dialog = new OptionsWindow(new DefaultControlFactory());
|
||||
dialog.LoadFromSettings(_settings);
|
||||
var parent = GetMainWindow();
|
||||
if (parent != null)
|
||||
{
|
||||
var result = await dialog.ShowDialog<bool>(parent);
|
||||
if (result)
|
||||
_settings.SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private static Window? GetMainWindow()
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
return desktop.MainWindow;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
13
VisionBuilder.UI.Avalonia/Services/ImageConverter.cs
Normal file
13
VisionBuilder.UI.Avalonia/Services/ImageConverter.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Services;
|
||||
|
||||
public static class ImageConverter
|
||||
{
|
||||
public static global::Avalonia.Media.Imaging.Bitmap MatToAvaloniaBitmap(Mat mat)
|
||||
{
|
||||
var encoded = mat.ImEncode(".bmp");
|
||||
using var ms = new MemoryStream(encoded);
|
||||
return new global::Avalonia.Media.Imaging.Bitmap(ms);
|
||||
}
|
||||
}
|
||||
87
VisionBuilder.UI.Avalonia/Views/ImagePreviewDialog.axaml
Normal file
87
VisionBuilder.UI.Avalonia/Views/ImagePreviewDialog.axaml
Normal file
@@ -0,0 +1,87 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Views.ImagePreviewDialog"
|
||||
Title="Image Preview"
|
||||
WindowState="Maximized"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="White"
|
||||
CanResize="True">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*" Margin="16">
|
||||
<!-- Row 0: Error name label -->
|
||||
<TextBlock x:Name="LblImageName"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="16"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
<!-- Row 1: Toolbar buttons -->
|
||||
<Grid Grid.Row="1" Margin="0,8,0,8">
|
||||
<!-- Left-aligned buttons -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
Spacing="8">
|
||||
<Button x:Name="BtnPrev"
|
||||
Content="PREVIOUS IMAGE"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnSwitchView"
|
||||
Content="SHOW ORIGINAL"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right-aligned buttons -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8">
|
||||
<Button x:Name="BtnLearn"
|
||||
Content="LEARN THIS"
|
||||
Width="168" Height="64"
|
||||
IsVisible="False"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnNext"
|
||||
Content="NEXT IMAGE"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnClose"
|
||||
Content="CLOSE"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="#D32F2F"
|
||||
Foreground="White" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Row 2: Image -->
|
||||
<Image x:Name="PreviewImage"
|
||||
Grid.Row="2"
|
||||
Stretch="Uniform" />
|
||||
</Grid>
|
||||
</Window>
|
||||
74
VisionBuilder.UI.Avalonia/Views/ImagePreviewDialog.axaml.cs
Normal file
74
VisionBuilder.UI.Avalonia/Views/ImagePreviewDialog.axaml.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Controls;
|
||||
using VisionBuilder.UI.Avalonia.Services;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Views;
|
||||
|
||||
public partial class ImagePreviewDialog : Window
|
||||
{
|
||||
private readonly ErrorPreviewVM _previewVm;
|
||||
|
||||
public ImagePreviewDialog(ErrorPreviewVM errorPreviewVm)
|
||||
{
|
||||
_previewVm = errorPreviewVm;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
_previewVm.PropertyChanged += PreviewVm_PropertyChanged;
|
||||
|
||||
// Initial state
|
||||
UpdateImage();
|
||||
BtnLearn.IsVisible = _previewVm.LearnButtonVisible;
|
||||
BtnPrev.IsEnabled = _previewVm.IsPreviousImageEnabled;
|
||||
BtnNext.IsEnabled = _previewVm.IsNextImageEnabled;
|
||||
LblImageName.Text = _previewVm.ImageName;
|
||||
|
||||
// Wire button events (direct method calls like WinForms)
|
||||
BtnPrev.Click += (_, _) => _previewVm.PreviousImage();
|
||||
BtnNext.Click += (_, _) => _previewVm.NextImage();
|
||||
BtnSwitchView.Click += (_, _) => _previewVm.SwitchView();
|
||||
BtnLearn.Click += (_, _) => _previewVm.Learn();
|
||||
BtnClose.Click += (_, _) => Close();
|
||||
}
|
||||
|
||||
private void PreviewVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(ErrorPreviewVM.ImageAnalysis):
|
||||
UpdateImage();
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.LearnButtonVisible):
|
||||
BtnLearn.IsVisible = _previewVm.LearnButtonVisible;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.IsPreviousImageEnabled):
|
||||
BtnPrev.IsEnabled = _previewVm.IsPreviousImageEnabled;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.IsNextImageEnabled):
|
||||
BtnNext.IsEnabled = _previewVm.IsNextImageEnabled;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.ImageName):
|
||||
LblImageName.Text = _previewVm.ImageName;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.OriginalView):
|
||||
BtnSwitchView.Content = _previewVm.OriginalView ? "SHOW ANALYSIS" : "SHOW ORIGINAL";
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateImage()
|
||||
{
|
||||
if (_previewVm.ImageAnalysis != null && !_previewVm.ImageAnalysis.Empty())
|
||||
{
|
||||
PreviewImage.Source = ImageConverter.MatToAvaloniaBitmap(_previewVm.ImageAnalysis);
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewImage.Source = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
VisionBuilder.UI.Avalonia/Views/PasswordDialog.axaml
Normal file
34
VisionBuilder.UI.Avalonia/Views/PasswordDialog.axaml
Normal file
@@ -0,0 +1,34 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Views.PasswordDialog"
|
||||
Title="Password Required"
|
||||
Width="400" Height="180"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto" Margin="16">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Enter password:"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<TextBox Grid.Row="1"
|
||||
x:Name="TxtPassword"
|
||||
PasswordChar="*"
|
||||
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
VisionBuilder.UI.Avalonia/Views/PasswordDialog.axaml.cs
Normal file
23
VisionBuilder.UI.Avalonia/Views/PasswordDialog.axaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Views;
|
||||
|
||||
public partial class PasswordDialog : Window
|
||||
{
|
||||
public PasswordDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
BtnOk.Click += (_, _) => Close(TxtPassword.Text);
|
||||
BtnCancel.Click += (_, _) => Close(null);
|
||||
|
||||
TxtPassword.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
Close(TxtPassword.Text);
|
||||
else if (e.Key == Key.Escape)
|
||||
Close(null);
|
||||
};
|
||||
}
|
||||
}
|
||||
49
VisionBuilder.UI.Avalonia/Views/RecipeSelectionDialog.axaml
Normal file
49
VisionBuilder.UI.Avalonia/Views/RecipeSelectionDialog.axaml
Normal file
@@ -0,0 +1,49 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Views.RecipeSelectionDialog"
|
||||
Title="Select Recipe"
|
||||
Width="800" Height="600"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="White">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<ListBox x:Name="RecipeList"
|
||||
Grid.Row="0"
|
||||
Margin="8"
|
||||
Background="White"
|
||||
SelectionMode="Single">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="8" Spacing="8">
|
||||
<Button x:Name="BtnCreateNew"
|
||||
Content="CREATE NEW RECIPE"
|
||||
IsVisible="False"
|
||||
Height="48" Padding="16,0"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnCancel"
|
||||
Content="CANCEL"
|
||||
Height="48" Width="176" Padding="16,0"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
130
VisionBuilder.UI.Avalonia/Views/RecipeSelectionDialog.axaml.cs
Normal file
130
VisionBuilder.UI.Avalonia/Views/RecipeSelectionDialog.axaml.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using VisionBuilder.UI.Avalonia.Services;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Views;
|
||||
|
||||
public partial class RecipeSelectionDialog : global::Avalonia.Controls.Window
|
||||
{
|
||||
private readonly RecipeSelectionVM _recipeSelectionVm;
|
||||
private readonly IRecipeCreationTool _recipeCreationTool;
|
||||
private readonly TaskCompletionSource<bool> _tcs;
|
||||
|
||||
public RecipeSelectionDialog(RecipeSelectionVM recipeSelectionVm, IRecipeCreationTool recipeCreationTool, TaskCompletionSource<bool> tcs)
|
||||
{
|
||||
_recipeSelectionVm = recipeSelectionVm;
|
||||
_recipeCreationTool = recipeCreationTool;
|
||||
_tcs = tcs;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
if (_recipeCreationTool.Enabled)
|
||||
{
|
||||
BtnCreateNew.IsVisible = true;
|
||||
}
|
||||
|
||||
BtnCancel.Click += (_, _) =>
|
||||
{
|
||||
_tcs.TrySetResult(false);
|
||||
Close();
|
||||
};
|
||||
BtnCreateNew.Click += BtnCreateNew_Click;
|
||||
|
||||
Closed += (_, _) => _tcs.TrySetResult(false);
|
||||
|
||||
LoadRecipes();
|
||||
}
|
||||
|
||||
private void LoadRecipes()
|
||||
{
|
||||
var recipes = _recipeSelectionVm.Recipes.OrderBy(x => x.RecipeName).ToList();
|
||||
var items = new List<RecipeItem>();
|
||||
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
Bitmap? thumbnail = null;
|
||||
try
|
||||
{
|
||||
if (recipe.Image != null && !recipe.Image.Empty())
|
||||
{
|
||||
thumbnail = ImageConverter.MatToAvaloniaBitmap(
|
||||
recipe.Image.Resize(new OpenCvSharp.Size(128, 128)));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore conversion errors
|
||||
}
|
||||
|
||||
items.Add(new RecipeItem(recipe, thumbnail));
|
||||
}
|
||||
|
||||
RecipeList.ItemsSource = items;
|
||||
|
||||
RecipeList.ItemTemplate = new global::Avalonia.Controls.Templates.FuncDataTemplate<RecipeItem>((item, _) =>
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Width = 140,
|
||||
Margin = new global::Avalonia.Thickness(4),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
var image = new Image
|
||||
{
|
||||
Source = item.Thumbnail,
|
||||
Width = 128,
|
||||
Height = 128,
|
||||
Stretch = Stretch.Uniform
|
||||
};
|
||||
|
||||
if (item.Thumbnail == null)
|
||||
{
|
||||
image.Source = null;
|
||||
}
|
||||
|
||||
var text = new TextBlock
|
||||
{
|
||||
Text = item.Recipe.RecipeName,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
TextTrimming = global::Avalonia.Media.TextTrimming.CharacterEllipsis,
|
||||
FontSize = 12,
|
||||
Margin = new global::Avalonia.Thickness(0, 4, 0, 0)
|
||||
};
|
||||
|
||||
panel.Children.Add(image);
|
||||
panel.Children.Add(text);
|
||||
return panel;
|
||||
});
|
||||
|
||||
RecipeList.SelectionChanged += RecipeList_SelectionChanged;
|
||||
}
|
||||
|
||||
private void RecipeList_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (RecipeList.SelectedItem is RecipeItem item)
|
||||
{
|
||||
_recipeSelectionVm.SelectedRecipe = item.Recipe;
|
||||
_tcs.TrySetResult(true);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private async void BtnCreateNew_Click(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
var recipeName = await _recipeCreationTool.CreateRecipeAsync();
|
||||
if (recipeName != null)
|
||||
{
|
||||
_recipeSelectionVm.SelectedRecipe = new RecipeData { RecipeName = recipeName };
|
||||
_tcs.TrySetResult(true);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private record RecipeItem(RecipeData Recipe, Bitmap? Thumbnail);
|
||||
}
|
||||
126
VisionBuilder.UI.Avalonia/Views/SingleCameraView.axaml
Normal file
126
VisionBuilder.UI.Avalonia/Views/SingleCameraView.axaml
Normal file
@@ -0,0 +1,126 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:VisionBuilder.UI.Avalonia.Converters"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Views.SingleCameraView"
|
||||
Background="White">
|
||||
|
||||
<UserControl.Resources>
|
||||
<converters:MatToBitmapConverter x:Key="MatToBitmapConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVis" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid ColumnDefinitions="5*,1,2*,1,Auto,Auto">
|
||||
<!-- Column 0: Preview -->
|
||||
<Grid Grid.Column="0" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0"
|
||||
x:Name="LblCameraName"
|
||||
Text="Camera"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,8,0,4" />
|
||||
|
||||
<Border Grid.Row="1" Margin="4"
|
||||
x:Name="PreviewBorder"
|
||||
BorderThickness="0"
|
||||
BorderBrush="Red">
|
||||
<Image x:Name="PreviewImage"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Column 1: Divider -->
|
||||
<Border Grid.Column="1" Background="#D32F2F" />
|
||||
|
||||
<!-- Column 2: Errors -->
|
||||
<Grid Grid.Column="2" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Errors"
|
||||
FontSize="16"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,8,0,4" />
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl x:Name="ErrorsList">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- Column 3: Divider -->
|
||||
<Border Grid.Column="3" Background="#D32F2F" />
|
||||
|
||||
<!-- Column 4: Stats -->
|
||||
<ScrollViewer Grid.Column="4" Width="220" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="StatsPanel" Margin="8,8,8,0" Spacing="2" />
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Column 5: Action buttons (right side) -->
|
||||
<StackPanel x:Name="ButtonsPanel" Grid.Column="5" Width="180" Margin="8" Spacing="6"
|
||||
VerticalAlignment="Top">
|
||||
<Button x:Name="BtnSelectRecipe"
|
||||
Content="SELECT RECIPE"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnStart"
|
||||
Content="START"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
Background="#D32F2F" Foreground="White" />
|
||||
<Button x:Name="BtnStop"
|
||||
Content="STOP"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
Background="#D32F2F" Foreground="White" />
|
||||
<Button x:Name="BtnPause"
|
||||
Content="PAUSE"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnResume"
|
||||
Content="RESUME"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnHotReload"
|
||||
Content="HOT RELOAD"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
415
VisionBuilder.UI.Avalonia/Views/SingleCameraView.axaml.cs
Normal file
415
VisionBuilder.UI.Avalonia/Views/SingleCameraView.axaml.cs
Normal file
@@ -0,0 +1,415 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using OpenCvSharp;
|
||||
using VisionBuilder.UI.Avalonia.Services;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Views;
|
||||
|
||||
public partial class SingleCameraView : UserControl
|
||||
{
|
||||
private SingleCameraVM? _vm;
|
||||
private ObservableCollection<ErrorData>? _subscribedErrors;
|
||||
private readonly Dictionary<string, (TextBlock label, TextBlock value)> _statsFields = new();
|
||||
private readonly Dictionary<string, Control> _dynamicButtonControls = new();
|
||||
|
||||
private const string StartedAtLabel = "Started at";
|
||||
private const string ProcessingTimeLabel = "Processing time";
|
||||
private const string BadLabel = "Bad";
|
||||
private const string TotalLabel = "Total";
|
||||
private const string ErrorRateLabel = "Error rate";
|
||||
private const string DetailsLabel = "Details";
|
||||
|
||||
public SingleCameraView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void SetViewModel(SingleCameraVM singleCameraVm)
|
||||
{
|
||||
_vm = singleCameraVm;
|
||||
_vm.SynchronizationContext = SynchronizationContext.Current;
|
||||
|
||||
DataContext = _vm;
|
||||
|
||||
// Bind commands
|
||||
BtnSelectRecipe.Click += async (_, _) => await _vm.SelectRecipe();
|
||||
BtnStart.Click += (_, _) => _vm.StartCommand.Execute(null);
|
||||
BtnStop.Click += (_, _) => _vm.StopCommand.Execute(null);
|
||||
BtnPause.Click += (_, _) => _vm.PauseCommand.Execute(null);
|
||||
BtnResume.Click += (_, _) => _vm.ResumeCommand.Execute(null);
|
||||
BtnHotReload.Click += (_, _) => _vm.HotReloadCommand.Execute(null);
|
||||
|
||||
// Subscribe to VM property changes
|
||||
_vm.PropertyChanged += Vm_PropertyChanged;
|
||||
_vm.PreviewVm.PropertyChanged += PreviewVm_PropertyChanged;
|
||||
_vm.StatisticsVm.PropertyChanged += StatisticsVm_PropertyChanged;
|
||||
|
||||
// Subscribe to errors collection
|
||||
_subscribedErrors = _vm.ErrorsVm.Errors;
|
||||
_subscribedErrors.CollectionChanged += Errors_CollectionChanged;
|
||||
|
||||
// Initial UI state
|
||||
UpdateButtonVisibility();
|
||||
UpdateCameraLabel();
|
||||
InitializeStatsFields();
|
||||
|
||||
// Subscribe to dynamic buttons
|
||||
_vm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged;
|
||||
foreach (var btn in _vm.DynamicButtons)
|
||||
AddDynamicButton(btn);
|
||||
}
|
||||
|
||||
private void Vm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(SingleCameraVM.CameraLabelAndStatus):
|
||||
UpdateCameraLabel();
|
||||
break;
|
||||
case nameof(SingleCameraVM.CurrentRecipeName):
|
||||
BtnSelectRecipe.Content = _vm!.CurrentRecipeName.ToUpperInvariant();
|
||||
break;
|
||||
case nameof(SingleCameraVM.CanStart):
|
||||
case nameof(SingleCameraVM.CanStop):
|
||||
case nameof(SingleCameraVM.CanPause):
|
||||
case nameof(SingleCameraVM.CanResume):
|
||||
case nameof(SingleCameraVM.CanHotReload):
|
||||
case nameof(SingleCameraVM.RecipeNotSelected):
|
||||
UpdateButtonVisibility();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateCameraLabel()
|
||||
{
|
||||
LblCameraName.Text = _vm?.CameraLabelAndStatus ?? "Camera";
|
||||
}
|
||||
|
||||
private void UpdateButtonVisibility()
|
||||
{
|
||||
if (_vm == null) return;
|
||||
BtnStart.IsVisible = _vm.CanStart;
|
||||
BtnStop.IsVisible = _vm.CanStop;
|
||||
BtnPause.IsVisible = _vm.CanPause;
|
||||
BtnResume.IsVisible = _vm.CanResume;
|
||||
BtnHotReload.IsVisible = _vm.CanHotReload;
|
||||
}
|
||||
|
||||
private void PreviewVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(PreviewVM.ImagePreview))
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var mat = _vm?.PreviewVm.ImagePreview;
|
||||
if (mat != null && !mat.Empty())
|
||||
{
|
||||
PreviewImage.Source = ImageConverter.MatToAvaloniaBitmap(mat);
|
||||
}
|
||||
|
||||
var isError = _vm?.PreviewVm.IsError ?? false;
|
||||
PreviewBorder.BorderThickness = isError ? new Thickness(4) : new Thickness(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#region Statistics
|
||||
|
||||
private void InitializeStatsFields()
|
||||
{
|
||||
StatsPanel.Children.Clear();
|
||||
_statsFields.Clear();
|
||||
UpdateStat(StartedAtLabel, "");
|
||||
UpdateStat(ProcessingTimeLabel, "");
|
||||
UpdateStat(TotalLabel, "0");
|
||||
UpdateStat(BadLabel, "0");
|
||||
UpdateStat(ErrorRateLabel, "0");
|
||||
UpdateStat(DetailsLabel, "");
|
||||
}
|
||||
|
||||
private void StatisticsVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (_vm == null) return;
|
||||
var stats = _vm.StatisticsVm;
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(StatisticsVM.SessionStarted):
|
||||
InitializeStatsFields();
|
||||
UpdateStat(StartedAtLabel, stats.SessionStarted);
|
||||
break;
|
||||
case nameof(StatisticsVM.ProcessingTime):
|
||||
UpdateStat(ProcessingTimeLabel, stats.ProcessingTime);
|
||||
break;
|
||||
case nameof(StatisticsVM.Bad):
|
||||
UpdateStat(BadLabel, stats.Bad.ToString());
|
||||
break;
|
||||
case nameof(StatisticsVM.Total):
|
||||
UpdateStat(TotalLabel, stats.Total.ToString());
|
||||
break;
|
||||
case nameof(StatisticsVM.ErrorRate):
|
||||
UpdateStat(ErrorRateLabel, stats.ErrorRate);
|
||||
break;
|
||||
case nameof(StatisticsVM.StatisticsDetails):
|
||||
UpdateStat(DetailsLabel, stats.StatisticsDetails);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateStat(string name, string text)
|
||||
{
|
||||
if (!_statsFields.ContainsKey(name))
|
||||
{
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = name + ":",
|
||||
FontWeight = FontWeight.Bold,
|
||||
FontSize = 12,
|
||||
Margin = new Thickness(0, 6, 0, 0)
|
||||
};
|
||||
var value = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0)
|
||||
};
|
||||
StatsPanel.Children.Add(label);
|
||||
StatsPanel.Children.Add(value);
|
||||
_statsFields[name] = (label, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_statsFields[name].value.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Errors
|
||||
|
||||
private void Errors_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
foreach (ErrorData error in e.NewItems)
|
||||
AddErrorThumbnail(error);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
foreach (ErrorData error in e.OldItems)
|
||||
RemoveErrorThumbnail(error);
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
ErrorsList.ItemsSource = null;
|
||||
_errorControls.Clear();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private readonly Dictionary<ErrorData, Control> _errorControls = new();
|
||||
|
||||
private void AddErrorThumbnail(ErrorData errorData)
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Width = 183,
|
||||
Margin = new Thickness(4),
|
||||
Cursor = new Cursor(StandardCursorType.Hand)
|
||||
};
|
||||
|
||||
Bitmap? thumbnail = null;
|
||||
try
|
||||
{
|
||||
if (errorData.ImageAnalysis != null && !errorData.ImageAnalysis.Empty())
|
||||
{
|
||||
thumbnail = ImageConverter.MatToAvaloniaBitmap(
|
||||
errorData.ImageAnalysis.Resize(new OpenCvSharp.Size(183, 138)));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore conversion errors
|
||||
}
|
||||
|
||||
var image = new Image
|
||||
{
|
||||
Source = thumbnail,
|
||||
Height = 138,
|
||||
Stretch = Stretch.Uniform
|
||||
};
|
||||
|
||||
var text = new TextBlock
|
||||
{
|
||||
Text = errorData.Title,
|
||||
FontSize = 11,
|
||||
HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
};
|
||||
|
||||
panel.Children.Add(image);
|
||||
panel.Children.Add(text);
|
||||
|
||||
panel.PointerPressed += (_, _) =>
|
||||
{
|
||||
_vm?.ErrorsVm.ShowImage(errorData);
|
||||
};
|
||||
|
||||
_errorControls[errorData] = panel;
|
||||
|
||||
// Insert at the beginning (newest first)
|
||||
if (ErrorsList.ItemsSource == null)
|
||||
{
|
||||
var items = new ObservableCollection<Control>();
|
||||
ErrorsList.ItemsSource = items;
|
||||
}
|
||||
|
||||
if (ErrorsList.ItemsSource is ObservableCollection<Control> collection)
|
||||
{
|
||||
collection.Insert(0, panel);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveErrorThumbnail(ErrorData errorData)
|
||||
{
|
||||
if (_errorControls.TryGetValue(errorData, out var control))
|
||||
{
|
||||
if (ErrorsList.ItemsSource is ObservableCollection<Control> collection)
|
||||
{
|
||||
collection.Remove(control);
|
||||
}
|
||||
_errorControls.Remove(errorData);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dynamic Buttons
|
||||
|
||||
private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (ButtonDefinition btn in e.NewItems!)
|
||||
AddDynamicButton(btn);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (ButtonDefinition btn in e.OldItems!)
|
||||
RemoveDynamicButton(btn);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
foreach (var ctrl in _dynamicButtonControls.Values)
|
||||
ButtonsPanel.Children.Remove(ctrl);
|
||||
_dynamicButtonControls.Clear();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void AddDynamicButton(ButtonDefinition buttonDef)
|
||||
{
|
||||
Control control;
|
||||
if (buttonDef.Type == EButtonType.Toggle)
|
||||
{
|
||||
var toggleBtn = new ToggleButton
|
||||
{
|
||||
Content = buttonDef.Title.ToUpperInvariant(),
|
||||
Height = 60,
|
||||
HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Stretch,
|
||||
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.Bold,
|
||||
IsChecked = buttonDef.IsToggled,
|
||||
IsVisible = buttonDef.IsVisible,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||
BorderThickness = new Thickness(2),
|
||||
Background = Brushes.White,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||
};
|
||||
toggleBtn.Click += (_, _) => buttonDef.Execute();
|
||||
buttonDef.PropertyChanged += (_, args) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||
toggleBtn.IsVisible = buttonDef.IsVisible;
|
||||
else if (args.PropertyName == nameof(ButtonDefinition.IsToggled))
|
||||
toggleBtn.IsChecked = buttonDef.IsToggled;
|
||||
});
|
||||
};
|
||||
control = toggleBtn;
|
||||
}
|
||||
else
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Content = buttonDef.Title.ToUpperInvariant(),
|
||||
Height = 60,
|
||||
HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Stretch,
|
||||
HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.Bold,
|
||||
IsVisible = buttonDef.IsVisible,
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)),
|
||||
BorderThickness = new Thickness(2),
|
||||
Background = Brushes.White,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F))
|
||||
};
|
||||
btn.Click += (_, _) => buttonDef.Execute();
|
||||
buttonDef.PropertyChanged += (_, args) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (args.PropertyName == nameof(ButtonDefinition.IsVisible))
|
||||
btn.IsVisible = buttonDef.IsVisible;
|
||||
});
|
||||
};
|
||||
control = btn;
|
||||
}
|
||||
|
||||
ButtonsPanel.Children.Add(control);
|
||||
_dynamicButtonControls[buttonDef.Key] = control;
|
||||
}
|
||||
|
||||
private void RemoveDynamicButton(ButtonDefinition buttonDef)
|
||||
{
|
||||
if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl))
|
||||
{
|
||||
ButtonsPanel.Children.Remove(ctrl);
|
||||
_dynamicButtonControls.Remove(buttonDef.Key);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
21
VisionBuilder.UI.Avalonia/VisionBuilder.UI.Avalonia.csproj
Normal file
21
VisionBuilder.UI.Avalonia/VisionBuilder.UI.Avalonia.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.2.3" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.3" />
|
||||
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj" />
|
||||
<ProjectReference Include="..\framework\Inspectron.Settings.Avalonia\Inspectron.Settings.Avalonia.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -47,6 +47,7 @@ public partial class MainLayout : IDisposable
|
||||
};
|
||||
|
||||
private MudTheme _currentTheme = null!;
|
||||
private bool _disposed;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -71,6 +72,7 @@ public partial class MainLayout : IDisposable
|
||||
|
||||
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (e.PropertyName == nameof(MainWindowVM.TestMode))
|
||||
{
|
||||
_testMode = MainWindowVm.TestMode;
|
||||
@@ -98,6 +100,7 @@ public partial class MainLayout : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
MainWindowVm.PropertyChanged -= OnPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using MudBlazor.Services;
|
||||
using Ninject;
|
||||
using Ninject.Extensions.ChildKernel;
|
||||
using VisionBuilder.UI.Blazor;
|
||||
using VisionBuilder.UI.Camera;
|
||||
using VisionBuilder.UI.Common;
|
||||
using VisionBuilder.UI.Common.Plugins;
|
||||
using VisionBuilder.UI.Common.Processing;
|
||||
@@ -25,12 +26,13 @@ builder.Services.AddMudServices();
|
||||
// Parse command line args
|
||||
CommandLineParser commandLineParser = new CommandLineParser(args);
|
||||
|
||||
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||
|
||||
// Setup settings
|
||||
InspectronSettings settings = new InspectronSettings("..\\Data\\Config");
|
||||
InspectronSettings settings = new InspectronSettings(Path.Combine("..", "Data", "Config"), profile);
|
||||
|
||||
var mainKernel = VisionBuilder.UI.Common.VisionBuilder.CreateMainKernel(settings);
|
||||
|
||||
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||
mainKernel.UsePlugins(profile);
|
||||
|
||||
mainKernel
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -16,12 +16,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MudBlazor" Version="8.0.0" />
|
||||
<PackageReference Include="Ninject.Extensions.ChildKernel" Version="3.3.0" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.6.0.20220608" />
|
||||
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.10.0.20241108" />
|
||||
<PackageReference Include="swety.OpenCvSharp4.runtime.linux-arm64" Version="4.10.0.20241108" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj" />
|
||||
<ProjectReference Include="..\Hawkeye.VisionBuilder.UI.Sources.Emulation\Hawkeye.VisionBuilder.UI.Sources.Emulation.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Camera\VisionBuilder.UI.Camera.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
|
||||
<ProjectReference Include="..\VisionBuilder.UI.Console\VisionBuilder.UI.Console.csproj" />
|
||||
|
||||
13
VisionBuilder.UI.Blazor.Uno/dotnet-tools.json
Normal file
13
VisionBuilder.UI.Blazor.Uno/dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.5",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public partial class ErrorPreview : ComponentBase, IDisposable
|
||||
|
||||
private List<ErrorData> _errors = new();
|
||||
private ErrorsVM? _subscribedVm;
|
||||
private bool _disposed;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
@@ -41,6 +42,7 @@ public partial class ErrorPreview : ComponentBase, IDisposable
|
||||
|
||||
private async void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (_disposed) return;
|
||||
if (ViewModel != null)
|
||||
_errors = new List<ErrorData>(ViewModel.Errors);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
@@ -54,6 +56,7 @@ public partial class ErrorPreview : ComponentBase, IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
Unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public partial class PreviewWindow : ComponentBase, IDisposable
|
||||
private DateTime _lastRenderTime = DateTime.MinValue;
|
||||
private Timer? _pendingTimer;
|
||||
private PreviewVM? _subscribedVm;
|
||||
private bool _disposed;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
@@ -69,6 +70,8 @@ public partial class PreviewWindow : ComponentBase, IDisposable
|
||||
|
||||
private async Task RenderCurrentFrame()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
_lastRenderTime = DateTime.UtcNow;
|
||||
|
||||
var mat = ViewModel?.ImagePreview;
|
||||
@@ -88,6 +91,7 @@ public partial class PreviewWindow : ComponentBase, IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_pendingTimer?.Dispose();
|
||||
Unsubscribe();
|
||||
}
|
||||
|
||||
@@ -33,46 +33,41 @@
|
||||
OnClick="@OnSelectRecipe">
|
||||
@_currentRecipeName
|
||||
</MudButton>
|
||||
@if (_canStart)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||
Class="camera-btn"
|
||||
Style="@(_canStart ? null : "display:none")"
|
||||
Disabled="@(!_canStart)"
|
||||
OnClick="@OnStart">
|
||||
Start
|
||||
</MudButton>
|
||||
}
|
||||
@if (_canStop)
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
|
||||
Class="camera-btn"
|
||||
Style="@(_canStop ? null : "display:none")"
|
||||
Disabled="@(!_canStop)"
|
||||
OnClick="@OnStop">
|
||||
Stop
|
||||
</MudButton>
|
||||
}
|
||||
@if (_showPauseButton && _canPause)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||
Class="camera-btn"
|
||||
Style="@(_showPauseButton && _canPause ? null : "display:none")"
|
||||
Disabled="@(!_canPause)"
|
||||
OnClick="@OnPause">
|
||||
Pause
|
||||
</MudButton>
|
||||
}
|
||||
@if (_showPauseButton && _canResume)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||
Class="camera-btn"
|
||||
Style="@(_showPauseButton && _canResume ? null : "display:none")"
|
||||
Disabled="@(!_canResume)"
|
||||
OnClick="@OnResume">
|
||||
Resume
|
||||
</MudButton>
|
||||
}
|
||||
@if (_canHotReload)
|
||||
{
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" FullWidth="true"
|
||||
Class="camera-btn"
|
||||
Style="@(_canHotReload ? null : "display:none")"
|
||||
Disabled="@(!_canHotReload)"
|
||||
OnClick="@OnHotReload">
|
||||
Hot Reload
|
||||
</MudButton>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@ public partial class SingleCameraControl : ComponentBase, IDisposable
|
||||
private bool _canHotReload;
|
||||
private bool _showPauseButton;
|
||||
private SingleCameraVM? _subscribedVm;
|
||||
private bool _disposed;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
@@ -61,6 +62,7 @@ public partial class SingleCameraControl : ComponentBase, IDisposable
|
||||
|
||||
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_disposed) return;
|
||||
UpdateState();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
@@ -103,6 +105,7 @@ public partial class SingleCameraControl : ComponentBase, IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
Unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public partial class Stats : ComponentBase, IDisposable
|
||||
private string _errorRate = "";
|
||||
private string _statisticsDetails = "";
|
||||
private StatisticsVM? _subscribedVm;
|
||||
private bool _disposed;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
@@ -43,6 +44,8 @@ public partial class Stats : ComponentBase, IDisposable
|
||||
|
||||
private async void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
_sessionStarted = ViewModel?.SessionStarted ?? "";
|
||||
_recipeName = ViewModel?.RecipeName ?? "";
|
||||
_processingTime = ViewModel?.ProcessingTime ?? "";
|
||||
@@ -57,6 +60,7 @@ public partial class Stats : ComponentBase, IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
Unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,24 @@
|
||||
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
[Parameter] public ErrorPreviewVM? ViewModel { get; set; }
|
||||
|
||||
private ErrorPreviewVM? _subscribedVm;
|
||||
private bool _disposed;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (_subscribedVm != ViewModel)
|
||||
{
|
||||
if (_subscribedVm != null)
|
||||
_subscribedVm.PropertyChanged -= OnPropertyChanged;
|
||||
_subscribedVm = ViewModel;
|
||||
if (ViewModel != null)
|
||||
ViewModel.PropertyChanged += OnPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_disposed) return;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -72,7 +82,8 @@
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (ViewModel != null)
|
||||
ViewModel.PropertyChanged -= OnPropertyChanged;
|
||||
_disposed = true;
|
||||
if (_subscribedVm != null)
|
||||
_subscribedVm.PropertyChanged -= OnPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user