2.0.15
profile influences configuration
This commit is contained in:
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
<Nullable>enable</Nullable>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ namespace LindtLeerformPlugin.Models;
|
|||||||
public class CellResult
|
public class CellResult
|
||||||
{
|
{
|
||||||
public int Index { get; set; }
|
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; }
|
public bool IsGood { get; set; }
|
||||||
/// <summary>Largest (histogram bin value − spline at that bin) across all bins. Negative if every bin is below the spline.</summary>
|
|
||||||
public double MaxExceedance { get; set; }
|
/// <summary>Number of contours inside the cell that passed the area filter.</summary>
|
||||||
|
public int BlobCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Total pixel area classified as chocolate inside this cell (sum of accepted contour areas).</summary>
|
||||||
|
public int DetectedArea { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,18 @@ public class LeerformRecipe
|
|||||||
{
|
{
|
||||||
public string RecipeName { get; set; } = string.Empty;
|
public string RecipeName { get; set; } = string.Empty;
|
||||||
public CellPattern Pattern { get; set; } = new();
|
public CellPattern Pattern { get; set; } = new();
|
||||||
public int[] HistogramBins { get; set; } = [5, 5, 5];
|
|
||||||
public float[] AverageSpline { get; set; } = [];
|
/// <summary>LAB a* threshold for white chocolate (THRESH_BINARY_INV). Pixels with a* below this become foreground.</summary>
|
||||||
public Dictionary<int, float[]> CellSplines { get; set; } = new();
|
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;
|
public string ThumbnailBase64 { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ public class LeerformPatternRecognitionService
|
|||||||
{
|
{
|
||||||
private static readonly Scalar GoodColor = new(0, 255, 0); // BGR Green
|
private static readonly Scalar GoodColor = new(0, 255, 0); // BGR Green
|
||||||
private static readonly Scalar BadColor = new(0, 0, 255); // BGR Red
|
private static readonly Scalar BadColor = new(0, 0, 255); // BGR Red
|
||||||
private static readonly Scalar OverrideColor = new(0, 255, 255); // BGR Yellow
|
|
||||||
|
|
||||||
public IReadOnlyList<CellRegion> ComputeCells(CellPattern pattern)
|
public IReadOnlyList<CellRegion> ComputeCells(CellPattern pattern)
|
||||||
{
|
{
|
||||||
@@ -75,86 +74,98 @@ public class LeerformPatternRecognitionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Aggregate histogram pooled across every cell in the pattern. Used only at calibration
|
/// Build a single foreground mask covering both white and dark chocolate flecks on the pink mold,
|
||||||
/// time to seed the default average spline; not persisted in the recipe. Note that pooling
|
/// using the LAB-channel thresholding approach from <c>jupiter/grid_test.ipynb</c>.
|
||||||
/// weights cells by their pixel count rather than averaging per-cell histograms.
|
/// <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>
|
/// </summary>
|
||||||
public float[] ComputeReferenceHistogram(Mat bgrImage, CellPattern pattern, int[]? bins = null)
|
public static Mat DetectChocolateMask(Mat bgrImage, int aThreshold, int lThreshold)
|
||||||
{
|
{
|
||||||
bins ??= [5, 5, 5];
|
using var lab = new Mat();
|
||||||
using var mask = BuildMask(pattern, new Size(bgrImage.Cols, bgrImage.Rows));
|
Cv2.CvtColor(bgrImage, lab, ColorConversionCodes.BGR2Lab);
|
||||||
using var hist = CalcHistogram(bgrImage, mask, bins);
|
var channels = Cv2.Split(lab);
|
||||||
return HistogramToArray(hist);
|
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)
|
public IReadOnlyList<CellResult> EvaluateCells(Mat bgrImage, LeerformRecipe recipe)
|
||||||
{
|
{
|
||||||
|
using var chocolateMask = DetectChocolateMask(bgrImage, recipe.AThreshold, recipe.LThreshold);
|
||||||
|
|
||||||
var cells = ComputeCells(recipe.Pattern);
|
var cells = ComputeCells(recipe.Pattern);
|
||||||
var bins = recipe.HistogramBins is { Length: 3 } ? recipe.HistogramBins : [5, 5, 5];
|
|
||||||
var imageSize = new Size(bgrImage.Cols, bgrImage.Rows);
|
var imageSize = new Size(bgrImage.Cols, bgrImage.Rows);
|
||||||
var results = new List<CellResult>(cells.Count);
|
var results = new List<CellResult>(cells.Count);
|
||||||
|
|
||||||
foreach (var cell in cells)
|
foreach (var cell in cells)
|
||||||
{
|
{
|
||||||
float[]? spline = null;
|
using var cellMask = BuildMask(recipe.Pattern, imageSize, cell.Index);
|
||||||
if (recipe.CellSplines != null
|
using var perCell = new Mat();
|
||||||
&& recipe.CellSplines.TryGetValue(cell.Index, out var custom)
|
Cv2.BitwiseAnd(chocolateMask, cellMask, perCell);
|
||||||
&& custom is { Length: SplineCurve.KnotCount })
|
|
||||||
|
Cv2.FindContours(
|
||||||
|
perCell,
|
||||||
|
out var contours,
|
||||||
|
out _,
|
||||||
|
RetrievalModes.External,
|
||||||
|
ContourApproximationModes.ApproxSimple);
|
||||||
|
|
||||||
|
var blobs = 0;
|
||||||
|
var area = 0;
|
||||||
|
foreach (var contour in contours)
|
||||||
{
|
{
|
||||||
spline = custom;
|
var contourArea = (int)Cv2.ContourArea(contour);
|
||||||
|
if (contourArea >= recipe.MinBlobArea && contourArea <= recipe.MaxBlobArea)
|
||||||
|
{
|
||||||
|
blobs++;
|
||||||
|
area += contourArea;
|
||||||
}
|
}
|
||||||
else if (recipe.AverageSpline is { Length: SplineCurve.KnotCount })
|
|
||||||
{
|
|
||||||
spline = recipe.AverageSpline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (spline == null)
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Recipe has no spline for cell {cell.Index} — open Calibration and click 'Set Reference'.");
|
|
||||||
|
|
||||||
using var mask = BuildMask(recipe.Pattern, imageSize, cell.Index);
|
|
||||||
using var hist = CalcHistogram(bgrImage, mask, bins);
|
|
||||||
var arr = HistogramToArray(hist);
|
|
||||||
|
|
||||||
var (isGood, exceed) = EvaluateAgainstSpline(arr, spline);
|
|
||||||
results.Add(new CellResult
|
results.Add(new CellResult
|
||||||
{
|
{
|
||||||
Index = cell.Index,
|
Index = cell.Index,
|
||||||
IsGood = isGood,
|
IsGood = blobs == 0,
|
||||||
MaxExceedance = exceed
|
BlobCount = blobs,
|
||||||
|
DetectedArea = area
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Mat RenderOverlay(Mat bgrImage, CellPattern pattern, IReadOnlyList<CellResult> results)
|
||||||
/// Cell is good when no histogram bin rises above the spline. <c>maxExceedance</c> is the
|
|
||||||
/// largest (bin − spline) across all bins; non-positive means the cell passes.
|
|
||||||
/// </summary>
|
|
||||||
public static (bool isGood, double maxExceedance) EvaluateAgainstSpline(float[] histogram, float[] spline)
|
|
||||||
{
|
|
||||||
if (histogram == null || histogram.Length == 0)
|
|
||||||
return (true, 0);
|
|
||||||
|
|
||||||
var maxExceed = double.NegativeInfinity;
|
|
||||||
for (var i = 0; i < histogram.Length; i++)
|
|
||||||
{
|
|
||||||
var threshold = SplineCurve.EvaluateAtBin(spline, i, histogram.Length);
|
|
||||||
var diff = histogram[i] - threshold;
|
|
||||||
if (diff > maxExceed) maxExceed = diff;
|
|
||||||
}
|
|
||||||
return (maxExceed <= 0, maxExceed);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Mat RenderOverlay(Mat bgrImage, CellPattern pattern,
|
|
||||||
IReadOnlyList<CellResult> results,
|
|
||||||
IReadOnlySet<int>? overriddenCells = null)
|
|
||||||
{
|
{
|
||||||
var overlay = bgrImage.Clone();
|
var overlay = bgrImage.Clone();
|
||||||
var cells = ComputeCells(pattern);
|
var cells = ComputeCells(pattern);
|
||||||
var resultByIndex = results.ToDictionary(r => r.Index);
|
var resultByIndex = results.ToDictionary(r => r.Index);
|
||||||
var imageRect = new Rect(0, 0, overlay.Cols, overlay.Rows);
|
|
||||||
const int thickness = 2;
|
const int thickness = 2;
|
||||||
|
|
||||||
foreach (var cell in cells)
|
foreach (var cell in cells)
|
||||||
@@ -168,9 +179,6 @@ public class LeerformPatternRecognitionService
|
|||||||
Cv2.Rectangle(overlay, cell.BoundingBox, color, thickness);
|
Cv2.Rectangle(overlay, cell.BoundingBox, color, thickness);
|
||||||
else
|
else
|
||||||
Cv2.Circle(overlay, cell.Center, cell.Radius, color, thickness);
|
Cv2.Circle(overlay, cell.Center, cell.Radius, color, thickness);
|
||||||
|
|
||||||
if (overriddenCells != null && overriddenCells.Contains(cell.Index))
|
|
||||||
DrawOverrideMarker(overlay, cell, pattern.Shape, imageRect);
|
|
||||||
}
|
}
|
||||||
return overlay;
|
return overlay;
|
||||||
}
|
}
|
||||||
@@ -187,39 +195,6 @@ public class LeerformPatternRecognitionService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DrawOverrideMarkers(Mat target, CellPattern pattern, IReadOnlySet<int> overriddenCells)
|
|
||||||
{
|
|
||||||
if (overriddenCells.Count == 0) return;
|
|
||||||
var cells = ComputeCells(pattern);
|
|
||||||
var imageRect = new Rect(0, 0, target.Cols, target.Rows);
|
|
||||||
foreach (var cell in cells)
|
|
||||||
{
|
|
||||||
if (!overriddenCells.Contains(cell.Index)) continue;
|
|
||||||
DrawOverrideMarker(target, cell, pattern.Shape, imageRect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DrawOverrideMarker(Mat target, CellRegion cell, CellShape shape, Rect imageRect)
|
|
||||||
{
|
|
||||||
Point center;
|
|
||||||
if (shape == CellShape.Square)
|
|
||||||
{
|
|
||||||
center = new Point(cell.BoundingBox.Right - 8, cell.BoundingBox.Top + 8);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var dx = (int)(cell.Radius * 0.7);
|
|
||||||
var dy = (int)(cell.Radius * 0.7);
|
|
||||||
center = new Point(cell.Center.X + dx, cell.Center.Y - dy);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageRect.Contains(center))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Cv2.Circle(target, center, 4, OverrideColor, thickness: -1);
|
|
||||||
Cv2.Circle(target, center, 4, new Scalar(0, 0, 0), thickness: 1); // thin black outline for visibility
|
|
||||||
}
|
|
||||||
|
|
||||||
public int? FindCellAtPoint(int x, int y, CellPattern pattern)
|
public int? FindCellAtPoint(int x, int y, CellPattern pattern)
|
||||||
{
|
{
|
||||||
var cells = ComputeCells(pattern);
|
var cells = ComputeCells(pattern);
|
||||||
@@ -240,78 +215,4 @@ public class LeerformPatternRecognitionService
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float[] ComputeCellHistogram(Mat bgrImage, CellPattern pattern, int cellIndex, int[]? bins = null)
|
|
||||||
{
|
|
||||||
bins ??= [5, 5, 5];
|
|
||||||
using var mask = BuildMask(pattern, new Size(bgrImage.Cols, bgrImage.Rows), cellIndex);
|
|
||||||
using var hist = CalcHistogram(bgrImage, mask, bins);
|
|
||||||
return HistogramToArray(hist);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Mat RenderHistogramChart(float[] histogram, int[] bins, int width = 600, int height = 220)
|
|
||||||
{
|
|
||||||
var chart = new Mat(height, width, MatType.CV_8UC3, new Scalar(30, 30, 30));
|
|
||||||
if (histogram == null || histogram.Length == 0)
|
|
||||||
return chart;
|
|
||||||
|
|
||||||
var totalBins = bins[0] * bins[1] * bins[2];
|
|
||||||
var max = 0f;
|
|
||||||
for (var i = 0; i < histogram.Length; i++)
|
|
||||||
if (histogram[i] > max) max = histogram[i];
|
|
||||||
if (max <= 0)
|
|
||||||
return chart;
|
|
||||||
|
|
||||||
const int leftPad = 10;
|
|
||||||
const int bottomPad = 15;
|
|
||||||
var barWidth = Math.Max(1, (width - 2 * leftPad) / totalBins);
|
|
||||||
var maxBarHeight = height - bottomPad - 10;
|
|
||||||
|
|
||||||
for (var i = 0; i < totalBins && i < histogram.Length; i++)
|
|
||||||
{
|
|
||||||
var binB = i / (bins[1] * bins[2]);
|
|
||||||
var binG = (i / bins[2]) % bins[1];
|
|
||||||
var binR = i % bins[2];
|
|
||||||
|
|
||||||
var b = (int)((binB + 0.5) * 256 / bins[0]);
|
|
||||||
var g = (int)((binG + 0.5) * 256 / bins[1]);
|
|
||||||
var r = (int)((binR + 0.5) * 256 / bins[2]);
|
|
||||||
|
|
||||||
var barHeight = (int)(histogram[i] / max * maxBarHeight);
|
|
||||||
var x = leftPad + i * barWidth;
|
|
||||||
var y = height - bottomPad - barHeight;
|
|
||||||
Cv2.Rectangle(chart, new Rect(x, y, barWidth, barHeight), new Scalar(b, g, r), -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return chart;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Mat CalcHistogram(Mat bgrImage, Mat mask, int[] bins)
|
|
||||||
{
|
|
||||||
var hist = new Mat();
|
|
||||||
Cv2.CalcHist(
|
|
||||||
images: new[] { bgrImage },
|
|
||||||
channels: new[] { 0, 1, 2 },
|
|
||||||
mask: mask,
|
|
||||||
hist: hist,
|
|
||||||
dims: 3,
|
|
||||||
histSize: bins,
|
|
||||||
ranges: new[] { new Rangef(0, 256), new Rangef(0, 256), new Rangef(0, 256) });
|
|
||||||
// L1 normalize so each bin is a probability in [0, 1] summing to 1.
|
|
||||||
Cv2.Normalize(hist, hist, 1.0, 0.0, NormTypes.L1);
|
|
||||||
|
|
||||||
var totalBins = bins[0] * bins[1] * bins[2];
|
|
||||||
var reshaped = hist.Reshape(1, totalBins).Clone();
|
|
||||||
hist.Dispose();
|
|
||||||
return reshaped;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float[] HistogramToArray(Mat hist)
|
|
||||||
{
|
|
||||||
var totalBins = hist.Rows * hist.Cols;
|
|
||||||
var arr = new float[totalBins];
|
|
||||||
for (var i = 0; i < totalBins; i++)
|
|
||||||
arr[i] = hist.At<float>(i, 0);
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
namespace LindtLeerformPlugin.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Monotone cubic Hermite spline (Fritsch–Carlson). Five control points with
|
|
||||||
/// fixed, evenly spaced X positions; only Y is editable. Output never overshoots
|
|
||||||
/// the input range, so envelope curves stay inside [0, 1] when knots are.
|
|
||||||
/// </summary>
|
|
||||||
public static class SplineCurve
|
|
||||||
{
|
|
||||||
public const int KnotCount = 5;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Evaluate the spline at parameter <paramref name="t"/> in [0, 1].
|
|
||||||
/// Knots are positioned at t = i / (knots.Length - 1).
|
|
||||||
/// </summary>
|
|
||||||
public static float Evaluate(float[]? knots, double t)
|
|
||||||
{
|
|
||||||
if (knots == null || knots.Length == 0)
|
|
||||||
return 0f;
|
|
||||||
if (knots.Length == 1)
|
|
||||||
return knots[0];
|
|
||||||
|
|
||||||
if (t <= 0) return knots[0];
|
|
||||||
if (t >= 1) return knots[^1];
|
|
||||||
|
|
||||||
var n = knots.Length - 1;
|
|
||||||
var pos = t * n;
|
|
||||||
var i = (int)System.Math.Floor(pos);
|
|
||||||
if (i >= n) return knots[n];
|
|
||||||
var localT = pos - i;
|
|
||||||
|
|
||||||
// Compute secant slopes and Fritsch-Carlson tangents at the two surrounding knots only.
|
|
||||||
var dxLeft = i > 0 ? (double)(knots[i] - knots[i - 1]) : 0;
|
|
||||||
var dxMid = (double)(knots[i + 1] - knots[i]);
|
|
||||||
var dxRight = i + 2 <= n ? (double)(knots[i + 2] - knots[i + 1]) : 0;
|
|
||||||
|
|
||||||
var m0 = MonotoneTangent(dxLeft, dxMid, hasLeft: i > 0, hasRight: true);
|
|
||||||
var m1 = MonotoneTangent(dxMid, dxRight, hasLeft: true, hasRight: i + 2 <= n);
|
|
||||||
|
|
||||||
var t2 = localT * localT;
|
|
||||||
var t3 = t2 * localT;
|
|
||||||
var h00 = 2 * t3 - 3 * t2 + 1;
|
|
||||||
var h10 = t3 - 2 * t2 + localT;
|
|
||||||
var h01 = -2 * t3 + 3 * t2;
|
|
||||||
var h11 = t3 - t2;
|
|
||||||
|
|
||||||
var y = h00 * knots[i] + h10 * m0 + h01 * knots[i + 1] + h11 * m1;
|
|
||||||
return (float)y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convenience: evaluate the spline at the position of bin <paramref name="binIndex"/>
|
|
||||||
/// of a histogram with <paramref name="totalBins"/> bins.
|
|
||||||
/// </summary>
|
|
||||||
public static float EvaluateAtBin(float[]? knots, int binIndex, int totalBins)
|
|
||||||
{
|
|
||||||
if (totalBins <= 1)
|
|
||||||
return Evaluate(knots, 0);
|
|
||||||
var t = (double)binIndex / (totalBins - 1);
|
|
||||||
return Evaluate(knots, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Build a default envelope spline from a reference histogram. For each knot
|
|
||||||
/// the segment max around the knot position is taken, multiplied by 1.2 with a
|
|
||||||
/// small floor added, and clamped to [0, 1].
|
|
||||||
/// </summary>
|
|
||||||
public static float[] CreateDefault(float[]? referenceHistogram, int knotCount = KnotCount)
|
|
||||||
{
|
|
||||||
var knots = new float[knotCount];
|
|
||||||
if (referenceHistogram == null || referenceHistogram.Length == 0)
|
|
||||||
{
|
|
||||||
for (var k = 0; k < knotCount; k++) knots[k] = 0.05f;
|
|
||||||
return knots;
|
|
||||||
}
|
|
||||||
|
|
||||||
var totalBins = referenceHistogram.Length;
|
|
||||||
var span = totalBins - 1;
|
|
||||||
// Half-width of the window we look at around each knot.
|
|
||||||
var segHalf = System.Math.Max(1, span / (2 * (knotCount - 1)));
|
|
||||||
|
|
||||||
for (var k = 0; k < knotCount; k++)
|
|
||||||
{
|
|
||||||
var center = knotCount == 1 ? 0 : k * span / (knotCount - 1);
|
|
||||||
var lo = System.Math.Max(0, center - segHalf);
|
|
||||||
var hi = System.Math.Min(totalBins - 1, center + segHalf);
|
|
||||||
var max = 0f;
|
|
||||||
for (var i = lo; i <= hi; i++)
|
|
||||||
if (referenceHistogram[i] > max) max = referenceHistogram[i];
|
|
||||||
|
|
||||||
var y = max * 1.2f + 0.01f;
|
|
||||||
if (y < 0f) y = 0f;
|
|
||||||
if (y > 1f) y = 1f;
|
|
||||||
knots[k] = y;
|
|
||||||
}
|
|
||||||
return knots;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double MonotoneTangent(double secLeft, double secRight, bool hasLeft, bool hasRight)
|
|
||||||
{
|
|
||||||
if (!hasLeft) return secRight;
|
|
||||||
if (!hasRight) return secLeft;
|
|
||||||
// If the signs differ (or either is zero), the knot is an extremum: tangent must be zero
|
|
||||||
// to preserve monotonicity locally.
|
|
||||||
if (secLeft == 0 || secRight == 0 || System.Math.Sign(secLeft) != System.Math.Sign(secRight))
|
|
||||||
return 0;
|
|
||||||
// Average — Fritsch-Carlson would also clamp by 3*min(|secLeft|,|secRight|), but for our
|
|
||||||
// small (5 knot) curves the simple average plus zero-at-extrema rule already prevents
|
|
||||||
// overshoot in practice.
|
|
||||||
var avg = 0.5 * (secLeft + secRight);
|
|
||||||
var limit = 3.0 * System.Math.Min(System.Math.Abs(secLeft), System.Math.Abs(secRight));
|
|
||||||
if (System.Math.Abs(avg) > limit)
|
|
||||||
avg = System.Math.Sign(avg) * limit;
|
|
||||||
return avg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using LindtLeerformPlugin.Models;
|
using LindtLeerformPlugin.Models;
|
||||||
using LindtLeerformPlugin.Services;
|
using LindtLeerformPlugin.Services;
|
||||||
using LindtLeerformPlugin.Views;
|
|
||||||
using OpenCvSharp;
|
using OpenCvSharp;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using VisionBuilder.UI.Common;
|
using VisionBuilder.UI.Common;
|
||||||
@@ -26,16 +25,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
private Mat? _calibDistCoeffs;
|
private Mat? _calibDistCoeffs;
|
||||||
private Mat? _referenceFrame;
|
private Mat? _referenceFrame;
|
||||||
|
|
||||||
// Non-modal cell histogram tool window state. While open, clicks on cells in the
|
|
||||||
// calibration preview update its content instead of opening a new window.
|
|
||||||
private CellHistogramWindow? _cellDialog;
|
|
||||||
private CellHistogramViewModel? _cellDialogVm;
|
|
||||||
|
|
||||||
// Transient seed for the average spline; never persisted in the recipe.
|
|
||||||
private float[]? _averageReferenceHistogram;
|
|
||||||
private float[]? _averageSpline;
|
|
||||||
private readonly Dictionary<int, float[]> _cellSplines = new();
|
|
||||||
|
|
||||||
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
|
[ObservableProperty] private Avalonia.Media.Imaging.Bitmap? _previewImage;
|
||||||
[ObservableProperty] private string _statusText = "Ready";
|
[ObservableProperty] private string _statusText = "Ready";
|
||||||
[ObservableProperty] private int _capturedImageCount;
|
[ObservableProperty] private int _capturedImageCount;
|
||||||
@@ -57,8 +46,11 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
[ObservableProperty] private decimal? _roiWidth = 0m;
|
[ObservableProperty] private decimal? _roiWidth = 0m;
|
||||||
[ObservableProperty] private decimal? _roiHeight = 0m;
|
[ObservableProperty] private decimal? _roiHeight = 0m;
|
||||||
|
|
||||||
// Detection
|
// Detection thresholds (LAB-based, see LeerformPatternRecognitionService.DetectChocolateMask)
|
||||||
[ObservableProperty] private bool _hasAverageSpline;
|
[ObservableProperty] private decimal? _aThreshold = 140m;
|
||||||
|
[ObservableProperty] private decimal? _lThreshold = 100m;
|
||||||
|
[ObservableProperty] private decimal? _minBlobArea = 100m;
|
||||||
|
[ObservableProperty] private decimal? _maxBlobArea = 5000m;
|
||||||
|
|
||||||
// Recipe
|
// Recipe
|
||||||
[ObservableProperty] private string _currentRecipeName = "default";
|
[ObservableProperty] private string _currentRecipeName = "default";
|
||||||
@@ -185,37 +177,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
StatusText = "Calibration images cleared";
|
StatusText = "Calibration images cleared";
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void SetReferenceHistogram()
|
|
||||||
{
|
|
||||||
using var frame = GetAnalysisFrame();
|
|
||||||
if (frame == null)
|
|
||||||
{
|
|
||||||
StatusText = "No active frame to use as reference";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var pattern = BuildCurrentPattern();
|
|
||||||
_averageReferenceHistogram = _patternService.ComputeReferenceHistogram(frame, pattern);
|
|
||||||
_averageSpline = SplineCurve.CreateDefault(_averageReferenceHistogram);
|
|
||||||
_cellSplines.Clear();
|
|
||||||
|
|
||||||
_referenceFrame?.Dispose();
|
|
||||||
_referenceFrame = frame.Clone();
|
|
||||||
|
|
||||||
HasAverageSpline = true;
|
|
||||||
StatusText = "Average spline seeded; per-cell overrides cleared.";
|
|
||||||
RefreshPreview();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Failed to seed average spline");
|
|
||||||
StatusText = $"Reference error: {ex.Message}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void TestPattern() => RunTestPattern();
|
private void TestPattern() => RunTestPattern();
|
||||||
|
|
||||||
@@ -227,24 +188,21 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
StatusText = "No active frame to test";
|
StatusText = "No active frame to test";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_averageSpline == null)
|
|
||||||
{
|
|
||||||
StatusText = "Set reference first";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var recipe = BuildCurrentRecipe();
|
var recipe = BuildCurrentRecipe();
|
||||||
var results = _patternService.EvaluateCells(frame, recipe);
|
var results = _patternService.EvaluateCells(frame, recipe);
|
||||||
var overrides = new HashSet<int>(_cellSplines.Keys);
|
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results);
|
||||||
using var overlay = _patternService.RenderOverlay(frame, recipe.Pattern, results, overrides);
|
|
||||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(overlay);
|
var bitmap = ImageConverter.MatToAvaloniaBitmap(overlay);
|
||||||
|
|
||||||
var old = PreviewImage;
|
var old = PreviewImage;
|
||||||
PreviewImage = bitmap;
|
PreviewImage = bitmap;
|
||||||
old?.Dispose();
|
old?.Dispose();
|
||||||
|
|
||||||
|
_referenceFrame?.Dispose();
|
||||||
|
_referenceFrame = frame.Clone();
|
||||||
|
|
||||||
var bad = results.Count(r => !r.IsGood);
|
var bad = results.Count(r => !r.IsGood);
|
||||||
StatusText = $"Test: {results.Count - bad} good / {bad} bad";
|
StatusText = $"Test: {results.Count - bad} good / {bad} bad";
|
||||||
}
|
}
|
||||||
@@ -269,105 +227,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? HitTestCell(int imageX, int imageY)
|
|
||||||
{
|
|
||||||
return _patternService.FindCellAtPoint(imageX, imageY, BuildCurrentPattern());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ShowCellHistogramAsync(int imageX, int imageY, Avalonia.Controls.Window owner)
|
|
||||||
{
|
|
||||||
using var frame = GetAnalysisFrame();
|
|
||||||
if (frame == null) return Task.CompletedTask;
|
|
||||||
|
|
||||||
var pattern = BuildCurrentPattern();
|
|
||||||
var cellIndex = _patternService.FindCellAtPoint(imageX, imageY, pattern);
|
|
||||||
if (cellIndex == null) return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (_averageSpline is not { Length: SplineCurve.KnotCount })
|
|
||||||
{
|
|
||||||
StatusText = "Set reference first";
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Already open: commit the previous cell's edits and switch to the new cell.
|
|
||||||
if (_cellDialog != null && _cellDialogVm != null)
|
|
||||||
{
|
|
||||||
CommitDialogStateToCell(_cellDialogVm);
|
|
||||||
LoadCellIntoDialog(_cellDialogVm, frame, pattern, cellIndex.Value);
|
|
||||||
_cellDialog.Activate();
|
|
||||||
RunTestPattern();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dialogVm = new CellHistogramViewModel { Bins = new[] { 5, 5, 5 } };
|
|
||||||
LoadCellIntoDialog(dialogVm, frame, pattern, cellIndex.Value);
|
|
||||||
|
|
||||||
// Re-test against the current frame on every spline drag release so the
|
|
||||||
// calibration window's preview updates live behind the open dialog.
|
|
||||||
dialogVm.DragCompleted = (avg, cell) =>
|
|
||||||
{
|
|
||||||
if (avg is { Length: SplineCurve.KnotCount })
|
|
||||||
_averageSpline = avg;
|
|
||||||
if (cell is { Length: SplineCurve.KnotCount })
|
|
||||||
_cellSplines[dialogVm.CellIndex] = cell;
|
|
||||||
else
|
|
||||||
_cellSplines.Remove(dialogVm.CellIndex);
|
|
||||||
RunTestPattern();
|
|
||||||
};
|
|
||||||
|
|
||||||
_cellDialogVm = dialogVm;
|
|
||||||
_cellDialog = new CellHistogramWindow { DataContext = dialogVm };
|
|
||||||
_cellDialog.Closed += OnCellDialogClosed;
|
|
||||||
_cellDialog.Show(owner);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error(ex, "Failed to show cell histogram");
|
|
||||||
StatusText = $"Histogram error: {ex.Message}";
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadCellIntoDialog(CellHistogramViewModel vm, Mat frame, CellPattern pattern, int cellIndex)
|
|
||||||
{
|
|
||||||
var bins = vm.Bins is { Length: 3 } ? vm.Bins : new[] { 5, 5, 5 };
|
|
||||||
var cellHist = _patternService.ComputeCellHistogram(frame, pattern, cellIndex, bins);
|
|
||||||
var avgSplineCopy = (float[])(_averageSpline ?? new float[SplineCurve.KnotCount]).Clone();
|
|
||||||
var cellSplineCopy = _cellSplines.TryGetValue(cellIndex, out var existing) && existing is { Length: SplineCurve.KnotCount }
|
|
||||||
? (float[])existing.Clone()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
vm.CellIndex = cellIndex;
|
|
||||||
vm.Title = $"Cell #{cellIndex}";
|
|
||||||
vm.CellHistogram = cellHist;
|
|
||||||
vm.AverageSpline = avgSplineCopy;
|
|
||||||
vm.CellSpline = cellSplineCopy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CommitDialogStateToCell(CellHistogramViewModel vm)
|
|
||||||
{
|
|
||||||
if (vm.AverageSpline is { Length: SplineCurve.KnotCount })
|
|
||||||
_averageSpline = vm.AverageSpline;
|
|
||||||
|
|
||||||
if (vm.CellSpline is { Length: SplineCurve.KnotCount })
|
|
||||||
_cellSplines[vm.CellIndex] = vm.CellSpline;
|
|
||||||
else
|
|
||||||
_cellSplines.Remove(vm.CellIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCellDialogClosed(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_cellDialogVm != null)
|
|
||||||
CommitDialogStateToCell(_cellDialogVm);
|
|
||||||
if (_cellDialog != null)
|
|
||||||
_cellDialog.Closed -= OnCellDialogClosed;
|
|
||||||
_cellDialog = null;
|
|
||||||
_cellDialogVm = null;
|
|
||||||
RunTestPattern();
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void SaveRecipe()
|
private void SaveRecipe()
|
||||||
{
|
{
|
||||||
@@ -411,29 +270,15 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
RoiWidth = recipe.Pattern.RoiWidth;
|
RoiWidth = recipe.Pattern.RoiWidth;
|
||||||
RoiHeight = recipe.Pattern.RoiHeight;
|
RoiHeight = recipe.Pattern.RoiHeight;
|
||||||
|
|
||||||
_averageReferenceHistogram = null;
|
AThreshold = recipe.AThreshold;
|
||||||
_averageSpline = recipe.AverageSpline is { Length: SplineCurve.KnotCount }
|
LThreshold = recipe.LThreshold;
|
||||||
? recipe.AverageSpline
|
MinBlobArea = recipe.MinBlobArea;
|
||||||
: null;
|
MaxBlobArea = recipe.MaxBlobArea;
|
||||||
|
|
||||||
_cellSplines.Clear();
|
|
||||||
if (recipe.CellSplines != null)
|
|
||||||
{
|
|
||||||
foreach (var kv in recipe.CellSplines)
|
|
||||||
{
|
|
||||||
if (kv.Value is { Length: SplineCurve.KnotCount })
|
|
||||||
_cellSplines[kv.Key] = kv.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HasAverageSpline = _averageSpline != null;
|
|
||||||
|
|
||||||
_referenceFrame?.Dispose();
|
_referenceFrame?.Dispose();
|
||||||
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
|
_referenceFrame = LeerformRecipeStore.DecodeThumbnail(recipe);
|
||||||
|
|
||||||
StatusText = HasAverageSpline
|
StatusText = $"Loaded recipe '{recipeName}'";
|
||||||
? $"Loaded recipe '{recipeName}'"
|
|
||||||
: $"Loaded recipe '{recipeName}' — no spline; click 'Set Reference'.";
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -457,9 +302,10 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
{
|
{
|
||||||
RecipeName = CurrentRecipeName,
|
RecipeName = CurrentRecipeName,
|
||||||
Pattern = BuildCurrentPattern(),
|
Pattern = BuildCurrentPattern(),
|
||||||
HistogramBins = [5, 5, 5],
|
AThreshold = (int)(AThreshold ?? 140m),
|
||||||
AverageSpline = _averageSpline ?? [],
|
LThreshold = (int)(LThreshold ?? 100m),
|
||||||
CellSplines = new Dictionary<int, float[]>(_cellSplines)
|
MinBlobArea = (int)(MinBlobArea ?? 100m),
|
||||||
|
MaxBlobArea = (int)(MaxBlobArea ?? 5000m)
|
||||||
};
|
};
|
||||||
|
|
||||||
private async Task PreviewLoopAsync(CancellationToken ct)
|
private async Task PreviewLoopAsync(CancellationToken ct)
|
||||||
@@ -531,11 +377,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
owned = true;
|
owned = true;
|
||||||
}
|
}
|
||||||
_patternService.DrawPattern(displayFrame, pattern, GridColor);
|
_patternService.DrawPattern(displayFrame, pattern, GridColor);
|
||||||
if (_cellSplines.Count > 0)
|
|
||||||
{
|
|
||||||
var overrides = new HashSet<int>(_cellSplines.Keys);
|
|
||||||
_patternService.DrawOverrideMarkers(displayFrame, pattern, overrides);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
|
var bitmap = ImageConverter.MatToAvaloniaBitmap(displayFrame);
|
||||||
@@ -563,32 +404,19 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pattern geometry changed — cell indices are no longer valid, so any per-cell
|
|
||||||
/// overrides and the seeded average spline must be discarded. The user has to
|
|
||||||
/// click 'Set Reference' again before testing.
|
|
||||||
/// </summary>
|
|
||||||
private void InvalidateSplinesForPatternChange()
|
|
||||||
{
|
|
||||||
if (_cellSplines.Count == 0 && _averageSpline == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_cellSplines.Clear();
|
|
||||||
_averageSpline = null;
|
|
||||||
_averageReferenceHistogram = null;
|
|
||||||
HasAverageSpline = false;
|
|
||||||
StatusText = "Pattern changed — splines cleared. Click 'Set Reference'.";
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnApplyCalibrationChanged(bool value) => RefreshPreview();
|
partial void OnApplyCalibrationChanged(bool value) => RefreshPreview();
|
||||||
partial void OnRowsChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
partial void OnRowsChanged(decimal? value) => RefreshPreview();
|
||||||
partial void OnColsChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
partial void OnColsChanged(decimal? value) => RefreshPreview();
|
||||||
partial void OnSelectedCellShapeChanged(CellShape value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
partial void OnSelectedCellShapeChanged(CellShape value) => RefreshPreview();
|
||||||
partial void OnPaddingChanged(decimal? value) { InvalidateSplinesForPatternChange(); RefreshPreview(); }
|
partial void OnPaddingChanged(decimal? value) => RefreshPreview();
|
||||||
partial void OnRoiXChanged(decimal? value) => RefreshPreview();
|
partial void OnRoiXChanged(decimal? value) => RefreshPreview();
|
||||||
partial void OnRoiYChanged(decimal? value) => RefreshPreview();
|
partial void OnRoiYChanged(decimal? value) => RefreshPreview();
|
||||||
partial void OnRoiWidthChanged(decimal? value) => RefreshPreview();
|
partial void OnRoiWidthChanged(decimal? value) => RefreshPreview();
|
||||||
partial void OnRoiHeightChanged(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)
|
private void LoadCalibrationMats(Models.CalibrationData data)
|
||||||
{
|
{
|
||||||
@@ -600,13 +428,6 @@ public partial class CalibrationWindowViewModel : ObservableObject, IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_singleCameraVm.PropertyChanged -= OnSingleCameraVmPropertyChanged;
|
_singleCameraVm.PropertyChanged -= OnSingleCameraVmPropertyChanged;
|
||||||
if (_cellDialog != null)
|
|
||||||
{
|
|
||||||
_cellDialog.Closed -= OnCellDialogClosed;
|
|
||||||
_cellDialog.Close();
|
|
||||||
_cellDialog = null;
|
|
||||||
_cellDialogVm = null;
|
|
||||||
}
|
|
||||||
StopPreview();
|
StopPreview();
|
||||||
_lastFrame?.Dispose();
|
_lastFrame?.Dispose();
|
||||||
_referenceFrame?.Dispose();
|
_referenceFrame?.Dispose();
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using LindtLeerformPlugin.Services;
|
|
||||||
|
|
||||||
namespace LindtLeerformPlugin.ViewModels;
|
|
||||||
|
|
||||||
public partial class CellHistogramViewModel : ObservableObject
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _title = "Cell Histogram";
|
|
||||||
public int CellIndex { get; set; }
|
|
||||||
public int[] Bins { get; init; } = [5, 5, 5];
|
|
||||||
|
|
||||||
[ObservableProperty] private float[]? _cellHistogram;
|
|
||||||
[ObservableProperty] private float[]? _cellSpline;
|
|
||||||
[ObservableProperty] private float[] _averageSpline = new float[SplineCurve.KnotCount];
|
|
||||||
[ObservableProperty] private string _verdictText = string.Empty;
|
|
||||||
|
|
||||||
public bool HasOverride => CellSpline is { Length: SplineCurve.KnotCount };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invoked by the view when the user releases the mouse after dragging a spline handle.
|
|
||||||
/// Lets the calibration window re-test and refresh its preview while the dialog stays open.
|
|
||||||
/// </summary>
|
|
||||||
public Action<float[], float[]?>? DragCompleted { get; set; }
|
|
||||||
|
|
||||||
public void RaiseDragCompleted() => DragCompleted?.Invoke(AverageSpline, CellSpline);
|
|
||||||
|
|
||||||
partial void OnCellSplineChanged(float[]? value)
|
|
||||||
{
|
|
||||||
OnPropertyChanged(nameof(HasOverride));
|
|
||||||
UpdateVerdict();
|
|
||||||
}
|
|
||||||
|
|
||||||
partial void OnAverageSplineChanged(float[] value) => UpdateVerdict();
|
|
||||||
partial void OnCellHistogramChanged(float[]? value) => UpdateVerdict();
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void CreateOverride()
|
|
||||||
{
|
|
||||||
// Seed from THIS cell's histogram so the user gets a useful starting envelope.
|
|
||||||
CellSpline = SplineCurve.CreateDefault(CellHistogram);
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void RemoveOverride()
|
|
||||||
{
|
|
||||||
CellSpline = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateVerdict()
|
|
||||||
{
|
|
||||||
var hist = CellHistogram;
|
|
||||||
if (hist == null || hist.Length == 0)
|
|
||||||
{
|
|
||||||
VerdictText = string.Empty;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var spline = CellSpline is { Length: SplineCurve.KnotCount } ? CellSpline : AverageSpline;
|
|
||||||
if (spline is not { Length: SplineCurve.KnotCount })
|
|
||||||
{
|
|
||||||
VerdictText = string.Empty;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var (isGood, exceed) = LindtLeerformPlugin.Services.LeerformPatternRecognitionService
|
|
||||||
.EvaluateAgainstSpline(hist, spline);
|
|
||||||
var label = isGood ? "GOOD" : "BAD";
|
|
||||||
var splineLabel = HasOverride ? "cell spline" : "average spline";
|
|
||||||
VerdictText = $"Verdict: {label} Max exceedance: {exceed:F4} Using: {splineLabel}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -102,11 +102,21 @@
|
|||||||
|
|
||||||
<Separator Margin="0,8" />
|
<Separator Margin="0,8" />
|
||||||
|
|
||||||
<Button Content="SET REFERENCE (RESETS SPLINES)" Command="{Binding SetReferenceHistogramCommand}" Margin="0,4"
|
<TextBlock Text="Detection Thresholds" FontWeight="Bold" FontSize="14" Foreground="Black" />
|
||||||
HorizontalAlignment="Stretch" />
|
|
||||||
<TextBlock Text="✓ Average spline set" Foreground="LimeGreen" IsVisible="{Binding HasAverageSpline}" />
|
|
||||||
|
|
||||||
<Button Content="TEST PATTERN" Command="{Binding TestPatternCommand}" Margin="0,4"
|
<TextBlock Text="A* (white chocolate, lower = stricter)" Foreground="Black" />
|
||||||
|
<NumericUpDown Value="{Binding AThreshold}" Minimum="0" Maximum="255" Increment="1" FormatString="0" />
|
||||||
|
|
||||||
|
<TextBlock Text="L* (dark chocolate, lower = stricter)" Foreground="Black" />
|
||||||
|
<NumericUpDown Value="{Binding LThreshold}" Minimum="0" Maximum="255" Increment="1" FormatString="0" />
|
||||||
|
|
||||||
|
<TextBlock Text="Min blob area (px)" Foreground="Black" />
|
||||||
|
<NumericUpDown Value="{Binding MinBlobArea}" Minimum="0" Maximum="100000" Increment="10" FormatString="0" />
|
||||||
|
|
||||||
|
<TextBlock Text="Max blob area (px)" Foreground="Black" />
|
||||||
|
<NumericUpDown Value="{Binding MaxBlobArea}" Minimum="0" Maximum="1000000" Increment="100" FormatString="0" />
|
||||||
|
|
||||||
|
<Button Content="TEST PATTERN" Command="{Binding TestPatternCommand}" Margin="0,8,0,4"
|
||||||
HorizontalAlignment="Stretch" />
|
HorizontalAlignment="Stretch" />
|
||||||
|
|
||||||
<Separator Margin="0,8" />
|
<Separator Margin="0,8" />
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Media.Imaging;
|
|
||||||
using LindtLeerformPlugin.ViewModels;
|
using LindtLeerformPlugin.ViewModels;
|
||||||
|
|
||||||
namespace LindtLeerformPlugin.Views;
|
namespace LindtLeerformPlugin.Views;
|
||||||
|
|
||||||
public partial class CalibrationWindow : Window
|
public partial class CalibrationWindow : Window
|
||||||
{
|
{
|
||||||
private static readonly Cursor HandCursor = new(StandardCursorType.Hand);
|
|
||||||
private static readonly Cursor DefaultCursor = new(StandardCursorType.Arrow);
|
|
||||||
|
|
||||||
public CalibrationWindow()
|
public CalibrationWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
PreviewImageControl.PointerMoved += OnPreviewPointerMoved;
|
|
||||||
PreviewImageControl.PointerPressed += OnPreviewPointerPressed;
|
|
||||||
PreviewImageControl.PointerExited += OnPreviewPointerExited;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnClosing(WindowClosingEventArgs e)
|
protected override void OnClosing(WindowClosingEventArgs e)
|
||||||
@@ -24,64 +15,4 @@ public partial class CalibrationWindow : Window
|
|||||||
(DataContext as CalibrationWindowViewModel)?.Dispose();
|
(DataContext as CalibrationWindowViewModel)?.Dispose();
|
||||||
base.OnClosing(e);
|
base.OnClosing(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (int x, int y)? ToImageCoordinates(Avalonia.Point pointerPos)
|
|
||||||
{
|
|
||||||
if (PreviewImageControl.Source is not Bitmap bitmap)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var ctrlSize = PreviewImageControl.Bounds.Size;
|
|
||||||
var imgSize = bitmap.Size;
|
|
||||||
if (ctrlSize.Width <= 0 || ctrlSize.Height <= 0 || imgSize.Width <= 0 || imgSize.Height <= 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var scale = System.Math.Min(ctrlSize.Width / imgSize.Width, ctrlSize.Height / imgSize.Height);
|
|
||||||
var displayW = imgSize.Width * scale;
|
|
||||||
var displayH = imgSize.Height * scale;
|
|
||||||
var offsetX = (ctrlSize.Width - displayW) / 2;
|
|
||||||
var offsetY = (ctrlSize.Height - displayH) / 2;
|
|
||||||
|
|
||||||
var imgX = (pointerPos.X - offsetX) / scale;
|
|
||||||
var imgY = (pointerPos.Y - offsetY) / scale;
|
|
||||||
|
|
||||||
if (imgX < 0 || imgY < 0 || imgX >= imgSize.Width || imgY >= imgSize.Height)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return ((int)imgX, (int)imgY);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPreviewPointerMoved(object? sender, PointerEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not CalibrationWindowViewModel vm)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var coords = ToImageCoordinates(e.GetPosition(PreviewImageControl));
|
|
||||||
if (coords == null)
|
|
||||||
{
|
|
||||||
PreviewImageControl.Cursor = DefaultCursor;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cellIndex = vm.HitTestCell(coords.Value.x, coords.Value.y);
|
|
||||||
PreviewImageControl.Cursor = cellIndex.HasValue ? HandCursor : DefaultCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPreviewPointerExited(object? sender, PointerEventArgs e)
|
|
||||||
{
|
|
||||||
PreviewImageControl.Cursor = DefaultCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnPreviewPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not CalibrationWindowViewModel vm)
|
|
||||||
return;
|
|
||||||
if (!e.GetCurrentPoint(PreviewImageControl).Properties.IsLeftButtonPressed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var coords = ToImageCoordinates(e.GetPosition(PreviewImageControl));
|
|
||||||
if (coords == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await vm.ShowCellHistogramAsync(coords.Value.x, coords.Value.y, this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:vm="using:LindtLeerformPlugin.ViewModels"
|
|
||||||
xmlns:v="using:LindtLeerformPlugin.Views"
|
|
||||||
x:Class="LindtLeerformPlugin.Views.CellHistogramWindow"
|
|
||||||
x:DataType="vm:CellHistogramViewModel"
|
|
||||||
Title="{Binding Title}"
|
|
||||||
Width="820" Height="560"
|
|
||||||
Background="#1A1A1A"
|
|
||||||
WindowStartupLocation="CenterOwner"
|
|
||||||
ShowInTaskbar="False"
|
|
||||||
CanResize="True">
|
|
||||||
|
|
||||||
<DockPanel Margin="12">
|
|
||||||
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0,0,0,8">
|
|
||||||
<TextBlock Text="Drag the gray dashed handles to edit the AVERAGE spline. Use 'Override Cell' to give this cell its own spline."
|
|
||||||
Foreground="LightGray" FontSize="12" TextWrapping="Wrap" />
|
|
||||||
<TextBlock Text="A cell is BAD when any histogram bar rises above its active spline."
|
|
||||||
Foreground="#888888" FontSize="11" TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8" Margin="0,12,0,0">
|
|
||||||
<Button Content="Override Cell"
|
|
||||||
Command="{Binding CreateOverrideCommand}"
|
|
||||||
IsVisible="{Binding !HasOverride}"
|
|
||||||
Background="#FF8C00" Foreground="Black" FontWeight="Bold" />
|
|
||||||
<Button Content="Remove Override"
|
|
||||||
Command="{Binding RemoveOverrideCommand}"
|
|
||||||
IsVisible="{Binding HasOverride}"
|
|
||||||
Background="#8B0000" Foreground="White" FontWeight="Bold" />
|
|
||||||
<TextBlock Text="{Binding VerdictText}" Foreground="LightGray"
|
|
||||||
VerticalAlignment="Center" FontSize="13" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Border Background="#0A0A0A" Padding="2">
|
|
||||||
<v:SplineHistogramEditor x:Name="Editor"
|
|
||||||
Histogram="{Binding CellHistogram}"
|
|
||||||
CellSpline="{Binding CellSpline, Mode=TwoWay}"
|
|
||||||
AverageSpline="{Binding AverageSpline, Mode=TwoWay}"
|
|
||||||
Bins="{Binding Bins}" />
|
|
||||||
</Border>
|
|
||||||
</DockPanel>
|
|
||||||
</Window>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using LindtLeerformPlugin.ViewModels;
|
|
||||||
|
|
||||||
namespace LindtLeerformPlugin.Views;
|
|
||||||
|
|
||||||
public partial class CellHistogramWindow : Window
|
|
||||||
{
|
|
||||||
public CellHistogramWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
Editor.SplineDragCompleted += (_, _) =>
|
|
||||||
(DataContext as CellHistogramViewModel)?.RaiseDragCompleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Input;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using LindtLeerformPlugin.Services;
|
|
||||||
|
|
||||||
namespace LindtLeerformPlugin.Views;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Custom Avalonia control that draws a histogram as colored bars and overlays an editable
|
|
||||||
/// average spline (faint dashed gray) plus an optional editable cell spline (bright orange).
|
|
||||||
/// Five control points per spline; X positions are fixed and only Y is dragged.
|
|
||||||
/// </summary>
|
|
||||||
public class SplineHistogramEditor : Control
|
|
||||||
{
|
|
||||||
public static readonly StyledProperty<float[]?> HistogramProperty =
|
|
||||||
AvaloniaProperty.Register<SplineHistogramEditor, float[]?>(nameof(Histogram));
|
|
||||||
|
|
||||||
public static readonly StyledProperty<float[]?> CellSplineProperty =
|
|
||||||
AvaloniaProperty.Register<SplineHistogramEditor, float[]?>(
|
|
||||||
nameof(CellSpline), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<float[]?> AverageSplineProperty =
|
|
||||||
AvaloniaProperty.Register<SplineHistogramEditor, float[]?>(
|
|
||||||
nameof(AverageSpline), defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
|
|
||||||
|
|
||||||
public static readonly StyledProperty<int[]?> BinsProperty =
|
|
||||||
AvaloniaProperty.Register<SplineHistogramEditor, int[]?>(nameof(Bins));
|
|
||||||
|
|
||||||
public float[]? Histogram
|
|
||||||
{
|
|
||||||
get => GetValue(HistogramProperty);
|
|
||||||
set => SetValue(HistogramProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float[]? CellSpline
|
|
||||||
{
|
|
||||||
get => GetValue(CellSplineProperty);
|
|
||||||
set => SetValue(CellSplineProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float[]? AverageSpline
|
|
||||||
{
|
|
||||||
get => GetValue(AverageSplineProperty);
|
|
||||||
set => SetValue(AverageSplineProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int[]? Bins
|
|
||||||
{
|
|
||||||
get => GetValue(BinsProperty);
|
|
||||||
set => SetValue(BinsProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private const double LeftPad = 12;
|
|
||||||
private const double RightPad = 12;
|
|
||||||
private const double TopPad = 12;
|
|
||||||
private const double BottomPad = 18;
|
|
||||||
private const double HitRadius = 12;
|
|
||||||
private const double HandleRadius = 6;
|
|
||||||
|
|
||||||
private static readonly IBrush BackgroundBrush = new SolidColorBrush(Color.FromRgb(20, 20, 20));
|
|
||||||
private static readonly IPen AxisPen = new Pen(new SolidColorBrush(Color.FromRgb(80, 80, 80)), 1);
|
|
||||||
private static readonly IPen AverageSplinePen = new Pen(
|
|
||||||
new SolidColorBrush(Color.FromRgb(170, 170, 170)), 2,
|
|
||||||
dashStyle: new DashStyle(new double[] { 4, 4 }, 0));
|
|
||||||
private static readonly IBrush AverageHandleFill = new SolidColorBrush(Color.FromRgb(220, 220, 220));
|
|
||||||
private static readonly IPen AverageHandleStroke = new Pen(new SolidColorBrush(Color.FromRgb(80, 80, 80)), 1);
|
|
||||||
private static readonly IPen CellSplinePen = new Pen(new SolidColorBrush(Color.FromRgb(255, 140, 0)), 2.5);
|
|
||||||
private static readonly IBrush CellHandleFill = new SolidColorBrush(Color.FromRgb(255, 140, 0));
|
|
||||||
private static readonly IPen CellHandleStroke = new Pen(Brushes.Black, 1);
|
|
||||||
|
|
||||||
static SplineHistogramEditor()
|
|
||||||
{
|
|
||||||
AffectsRender<SplineHistogramEditor>(
|
|
||||||
HistogramProperty, CellSplineProperty, AverageSplineProperty, BinsProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SplineHistogramEditor()
|
|
||||||
{
|
|
||||||
// Render() fills the bounds, and Control hit-tests its full Bounds rectangle by
|
|
||||||
// default, so we just need to be focusable for pointer capture during drag.
|
|
||||||
Focusable = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cached handle pixel positions, refreshed at every Render(). Indexed [0..4].
|
|
||||||
private readonly Point[] _avgHandlePixels = new Point[SplineCurve.KnotCount];
|
|
||||||
private readonly Point[] _cellHandlePixels = new Point[SplineCurve.KnotCount];
|
|
||||||
|
|
||||||
private SplineKind _draggingKind = SplineKind.None;
|
|
||||||
private int _draggingIndex = -1;
|
|
||||||
|
|
||||||
/// <summary>Raised on pointer release after a spline handle drag has completed.</summary>
|
|
||||||
public event EventHandler? SplineDragCompleted;
|
|
||||||
|
|
||||||
private enum SplineKind { None, Average, Cell }
|
|
||||||
|
|
||||||
public override void Render(DrawingContext ctx)
|
|
||||||
{
|
|
||||||
var bounds = new Rect(Bounds.Size);
|
|
||||||
ctx.FillRectangle(BackgroundBrush, bounds);
|
|
||||||
|
|
||||||
var plotRect = new Rect(
|
|
||||||
LeftPad, TopPad,
|
|
||||||
Math.Max(1, Bounds.Width - LeftPad - RightPad),
|
|
||||||
Math.Max(1, Bounds.Height - TopPad - BottomPad));
|
|
||||||
|
|
||||||
DrawAxes(ctx, plotRect);
|
|
||||||
|
|
||||||
var hist = Histogram;
|
|
||||||
var bins = Bins;
|
|
||||||
var binCount = (hist?.Length) ?? (bins is { Length: 3 } ? bins[0] * bins[1] * bins[2] : 0);
|
|
||||||
|
|
||||||
// Fixed [0, 1] Y axis — no auto-zoom. Bins and splines are L1-normalised so 1.0
|
|
||||||
// is the natural ceiling and the user always sees an absolute scale.
|
|
||||||
const double displayMax = 1.0;
|
|
||||||
|
|
||||||
if (hist != null && hist.Length > 0 && bins is { Length: 3 })
|
|
||||||
DrawBars(ctx, plotRect, hist, bins, displayMax);
|
|
||||||
|
|
||||||
if (binCount <= 0)
|
|
||||||
binCount = 125; // safe default for spline rendering when histogram is missing
|
|
||||||
|
|
||||||
DrawSpline(ctx, plotRect, AverageSpline, AverageSplinePen, displayMax, binCount,
|
|
||||||
_avgHandlePixels, AverageHandleFill, AverageHandleStroke, drawHandles: AverageSpline is { Length: SplineCurve.KnotCount });
|
|
||||||
DrawSpline(ctx, plotRect, CellSpline, CellSplinePen, displayMax, binCount,
|
|
||||||
_cellHandlePixels, CellHandleFill, CellHandleStroke, drawHandles: CellSpline is { Length: SplineCurve.KnotCount });
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DrawAxes(DrawingContext ctx, Rect plot)
|
|
||||||
{
|
|
||||||
ctx.DrawLine(AxisPen, new Point(plot.Left, plot.Bottom), new Point(plot.Right, plot.Bottom));
|
|
||||||
ctx.DrawLine(AxisPen, new Point(plot.Left, plot.Top), new Point(plot.Left, plot.Bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DrawBars(DrawingContext ctx, Rect plot, float[] hist, int[] bins, double displayMax)
|
|
||||||
{
|
|
||||||
var totalBins = bins[0] * bins[1] * bins[2];
|
|
||||||
if (totalBins <= 0) return;
|
|
||||||
|
|
||||||
var barWidth = plot.Width / totalBins;
|
|
||||||
for (var i = 0; i < totalBins && i < hist.Length; i++)
|
|
||||||
{
|
|
||||||
var binB = i / (bins[1] * bins[2]);
|
|
||||||
var binG = (i / bins[2]) % bins[1];
|
|
||||||
var binR = i % bins[2];
|
|
||||||
|
|
||||||
var b = (byte)Math.Min(255, (int)((binB + 0.5) * 256 / bins[0]));
|
|
||||||
var g = (byte)Math.Min(255, (int)((binG + 0.5) * 256 / bins[1]));
|
|
||||||
var r = (byte)Math.Min(255, (int)((binR + 0.5) * 256 / bins[2]));
|
|
||||||
var brush = new SolidColorBrush(Color.FromRgb(r, g, b));
|
|
||||||
|
|
||||||
var v = hist[i];
|
|
||||||
if (v < 0) v = 0;
|
|
||||||
var h = v / displayMax * plot.Height;
|
|
||||||
if (h < 0) h = 0;
|
|
||||||
if (h > plot.Height) h = plot.Height;
|
|
||||||
|
|
||||||
var x = plot.Left + i * barWidth;
|
|
||||||
var y = plot.Bottom - h;
|
|
||||||
ctx.FillRectangle(brush, new Rect(x, y, Math.Max(1, barWidth), h));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSpline(DrawingContext ctx, Rect plot, float[]? knots, IPen curvePen,
|
|
||||||
double displayMax, int binCount, Point[] handleCache, IBrush handleFill, IPen handleStroke,
|
|
||||||
bool drawHandles)
|
|
||||||
{
|
|
||||||
if (knots == null || knots.Length < 2 || displayMax <= 0)
|
|
||||||
{
|
|
||||||
for (var k = 0; k < handleCache.Length; k++) handleCache[k] = new Point(double.NaN, double.NaN);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample the spline along the plot width and stroke a polyline.
|
|
||||||
var steps = Math.Max(64, (int)plot.Width);
|
|
||||||
var geometry = new StreamGeometry();
|
|
||||||
using (var sgc = geometry.Open())
|
|
||||||
{
|
|
||||||
for (var s = 0; s <= steps; s++)
|
|
||||||
{
|
|
||||||
var t = (double)s / steps;
|
|
||||||
var y = SplineCurve.Evaluate(knots, t);
|
|
||||||
var px = plot.Left + t * plot.Width;
|
|
||||||
var py = plot.Bottom - Math.Clamp(y / displayMax, 0, 1) * plot.Height;
|
|
||||||
var p = new Point(px, py);
|
|
||||||
if (s == 0) sgc.BeginFigure(p, false);
|
|
||||||
else sgc.LineTo(p);
|
|
||||||
}
|
|
||||||
sgc.EndFigure(false);
|
|
||||||
}
|
|
||||||
ctx.DrawGeometry(null, curvePen, geometry);
|
|
||||||
|
|
||||||
// Cache and draw handles at the knot positions.
|
|
||||||
if (knots.Length <= handleCache.Length)
|
|
||||||
{
|
|
||||||
for (var k = 0; k < knots.Length; k++)
|
|
||||||
{
|
|
||||||
var t = knots.Length == 1 ? 0.0 : (double)k / (knots.Length - 1);
|
|
||||||
var px = plot.Left + t * plot.Width;
|
|
||||||
var py = plot.Bottom - Math.Clamp(knots[k] / displayMax, 0, 1) * plot.Height;
|
|
||||||
handleCache[k] = new Point(px, py);
|
|
||||||
|
|
||||||
if (drawHandles)
|
|
||||||
{
|
|
||||||
ctx.DrawEllipse(handleFill, handleStroke, handleCache[k], HandleRadius, HandleRadius);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnPointerPressed(e);
|
|
||||||
var props = e.GetCurrentPoint(this).Properties;
|
|
||||||
if (!props.IsLeftButtonPressed) return;
|
|
||||||
|
|
||||||
var pos = e.GetPosition(this);
|
|
||||||
|
|
||||||
// Cell handles take priority over average handles when both overlap.
|
|
||||||
var hit = HitTest(pos, _cellHandlePixels, requireCellSpline: true);
|
|
||||||
if (hit >= 0)
|
|
||||||
{
|
|
||||||
_draggingKind = SplineKind.Cell;
|
|
||||||
_draggingIndex = hit;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
hit = HitTest(pos, _avgHandlePixels, requireCellSpline: false);
|
|
||||||
if (hit < 0) return;
|
|
||||||
_draggingKind = SplineKind.Average;
|
|
||||||
_draggingIndex = hit;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.Pointer.Capture(this);
|
|
||||||
ApplyDrag(pos);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnPointerMoved(PointerEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnPointerMoved(e);
|
|
||||||
if (_draggingKind == SplineKind.None) return;
|
|
||||||
var pos = e.GetPosition(this);
|
|
||||||
ApplyDrag(pos);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
|
||||||
{
|
|
||||||
base.OnPointerReleased(e);
|
|
||||||
if (_draggingKind == SplineKind.None) return;
|
|
||||||
_draggingKind = SplineKind.None;
|
|
||||||
_draggingIndex = -1;
|
|
||||||
if (e.Pointer.Captured == this)
|
|
||||||
e.Pointer.Capture(null);
|
|
||||||
e.Handled = true;
|
|
||||||
SplineDragCompleted?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int HitTest(Point pos, Point[] handles, bool requireCellSpline)
|
|
||||||
{
|
|
||||||
if (requireCellSpline && CellSpline is not { Length: SplineCurve.KnotCount }) return -1;
|
|
||||||
|
|
||||||
for (var i = 0; i < handles.Length; i++)
|
|
||||||
{
|
|
||||||
var h = handles[i];
|
|
||||||
if (double.IsNaN(h.X)) continue;
|
|
||||||
var dx = pos.X - h.X;
|
|
||||||
var dy = pos.Y - h.Y;
|
|
||||||
if (dx * dx + dy * dy <= HitRadius * HitRadius)
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyDrag(Point pos)
|
|
||||||
{
|
|
||||||
if (_draggingIndex < 0) return;
|
|
||||||
|
|
||||||
var plotRect = new Rect(
|
|
||||||
LeftPad, TopPad,
|
|
||||||
Math.Max(1, Bounds.Width - LeftPad - RightPad),
|
|
||||||
Math.Max(1, Bounds.Height - TopPad - BottomPad));
|
|
||||||
|
|
||||||
var rel = (plotRect.Bottom - pos.Y) / plotRect.Height;
|
|
||||||
var newY = (float)Math.Clamp(rel, 0.0, 1.0);
|
|
||||||
|
|
||||||
if (_draggingKind == SplineKind.Average)
|
|
||||||
{
|
|
||||||
var current = AverageSpline;
|
|
||||||
if (current is not { Length: SplineCurve.KnotCount }) return;
|
|
||||||
var copy = (float[])current.Clone();
|
|
||||||
copy[_draggingIndex] = newY;
|
|
||||||
SetCurrentValue(AverageSplineProperty, copy);
|
|
||||||
}
|
|
||||||
else if (_draggingKind == SplineKind.Cell)
|
|
||||||
{
|
|
||||||
var current = CellSpline;
|
|
||||||
if (current is not { Length: SplineCurve.KnotCount }) return;
|
|
||||||
var copy = (float[])current.Clone();
|
|
||||||
copy[_draggingIndex] = newY;
|
|
||||||
SetCurrentValue(CellSplineProperty, copy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
<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>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\framework\Inspectron.Settings\Inspectron.Settings.csproj">
|
<ProjectReference Include="..\..\..\framework\Inspectron.Settings\Inspectron.Settings.csproj">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
<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>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,12 @@ public class App : Application
|
|||||||
var args = desktop.Args ?? [];
|
var args = desktop.Args ?? [];
|
||||||
var commandLineParser = new CommandLineParser(args);
|
var commandLineParser = new CommandLineParser(args);
|
||||||
|
|
||||||
var settings = new InspectronSettings("../Data/Config");
|
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||||
|
|
||||||
|
var settings = new InspectronSettings("../Data/Config", profile);
|
||||||
|
|
||||||
var mainKernel = Common.VisionBuilder.CreateMainKernel(settings);
|
var mainKernel = Common.VisionBuilder.CreateMainKernel(settings);
|
||||||
|
|
||||||
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
|
||||||
mainKernel.UsePlugins(profile);
|
mainKernel.UsePlugins(profile);
|
||||||
|
|
||||||
mainKernel
|
mainKernel
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ builder.Services.AddMudServices();
|
|||||||
// Parse command line args
|
// Parse command line args
|
||||||
CommandLineParser commandLineParser = new CommandLineParser(args);
|
CommandLineParser commandLineParser = new CommandLineParser(args);
|
||||||
|
|
||||||
|
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||||
|
|
||||||
// Setup settings
|
// Setup settings
|
||||||
InspectronSettings settings = new InspectronSettings(Path.Combine("..", "Data", "Config"));
|
InspectronSettings settings = new InspectronSettings(Path.Combine("..", "Data", "Config"), profile);
|
||||||
|
|
||||||
var mainKernel = VisionBuilder.UI.Common.VisionBuilder.CreateMainKernel(settings);
|
var mainKernel = VisionBuilder.UI.Common.VisionBuilder.CreateMainKernel(settings);
|
||||||
|
|
||||||
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
|
||||||
mainKernel.UsePlugins(profile);
|
mainKernel.UsePlugins(profile);
|
||||||
|
|
||||||
mainKernel
|
mainKernel
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ internal static class Program
|
|||||||
MaterialSkin.MaterialSkinManager.ConfigureForInspectron();
|
MaterialSkin.MaterialSkinManager.ConfigureForInspectron();
|
||||||
CommandLineParser commandLineParser = new CommandLineParser(args);
|
CommandLineParser commandLineParser = new CommandLineParser(args);
|
||||||
|
|
||||||
InspectronSettings settings = new InspectronSettings(Path.Combine("..", "Data", "Config"));
|
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||||
|
|
||||||
|
InspectronSettings settings = new InspectronSettings(Path.Combine("..", "Data", "Config"), profile);
|
||||||
|
|
||||||
var mainKernel = VisionBuilder.UI.Common.VisionBuilder.CreateMainKernel(settings);
|
var mainKernel = VisionBuilder.UI.Common.VisionBuilder.CreateMainKernel(settings);
|
||||||
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
|
||||||
mainKernel.UsePlugins(profile);
|
mainKernel.UsePlugins(profile);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ApplicationIcon>Inspectron icon-512.ico</ApplicationIcon>
|
<ApplicationIcon>Inspectron icon-512.ico</ApplicationIcon>
|
||||||
<Deterministic>false</Deterministic>
|
<Deterministic>false</Deterministic>
|
||||||
<Version>2.0.14</Version>
|
<Version>2.0.15</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
@@ -49,13 +49,12 @@ namespace VisionBuilder.UI.Windows.Test
|
|||||||
MaterialSkin.MaterialSkinManager.ConfigureForInspectron();
|
MaterialSkin.MaterialSkinManager.ConfigureForInspectron();
|
||||||
CommandLineParser commandLineParser = new CommandLineParser(args);
|
CommandLineParser commandLineParser = new CommandLineParser(args);
|
||||||
|
|
||||||
|
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
||||||
|
|
||||||
InspectronSettings settings = new InspectronSettings("..\\Data\\Config");
|
InspectronSettings settings = new InspectronSettings("..\\Data\\Config", profile);
|
||||||
|
|
||||||
var mainKernel = Common.VisionBuilder.CreateMainKernel(settings);
|
var mainKernel = Common.VisionBuilder.CreateMainKernel(settings);
|
||||||
|
|
||||||
var profile = commandLineParser.GetStringArgument("profile", 'p');
|
|
||||||
|
|
||||||
mainKernel.UsePlugins(profile);
|
mainKernel.UsePlugins(profile);
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<ApplicationIcon>Inspectron icon-512.ico</ApplicationIcon>
|
<ApplicationIcon>Inspectron icon-512.ico</ApplicationIcon>
|
||||||
<Deterministic>false</Deterministic>
|
<Deterministic>false</Deterministic>
|
||||||
<Version>2.0.14</Version>
|
<Version>2.0.15</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Windows",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialSkin.Core", "framework\MaterialSkin.Core\MaterialSkin.Core.csproj", "{D5FD2E9D-DA4F-1343-47E0-FBC473A149BC}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MaterialSkin.Core", "framework\MaterialSkin.Core\MaterialSkin.Core.csproj", "{D5FD2E9D-DA4F-1343-47E0-FBC473A149BC}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Windows.Uno", "VisionBuilder.UI.Windows.Test\VisionBuilder.UI.Windows.Uno.csproj", "{7697A266-A721-4D74-9C5C-0D4F2F6BBF68}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Windows.Uno", "VisionBuilder.UI.Windows.Uno\VisionBuilder.UI.Windows.Uno.csproj", "{7697A266-A721-4D74-9C5C-0D4F2F6BBF68}"
|
||||||
ProjectSection(ProjectDependencies) = postProject
|
ProjectSection(ProjectDependencies) = postProject
|
||||||
{19E9B1A2-FBC6-2668-EDDF-1B3A6828184A} = {19E9B1A2-FBC6-2668-EDDF-1B3A6828184A}
|
{19E9B1A2-FBC6-2668-EDDF-1B3A6828184A} = {19E9B1A2-FBC6-2668-EDDF-1B3A6828184A}
|
||||||
{31C47198-DEC2-4073-A0C2-220549502CD1} = {31C47198-DEC2-4073-A0C2-220549502CD1}
|
{31C47198-DEC2-4073-A0C2-220549502CD1} = {31C47198-DEC2-4073-A0C2-220549502CD1}
|
||||||
@@ -46,6 +46,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.So
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.RecipeConverter", "Hawkeye.VisionBuilder.UI.RecipeConverter\Hawkeye.VisionBuilder.UI.RecipeConverter.csproj", "{97AF69DC-5061-2643-3CC1-99B51976B05B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.RecipeConverter", "Hawkeye.VisionBuilder.UI.RecipeConverter\Hawkeye.VisionBuilder.UI.RecipeConverter.csproj", "{97AF69DC-5061-2643-3CC1-99B51976B05B}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hawkeye.VisionBuilder.UI.RecipeYoloPatcher", "Hawkeye.VisionBuilder.UI.RecipeYoloPatcher\Hawkeye.VisionBuilder.UI.RecipeYoloPatcher.csproj", "{6B456C8A-92D8-43D0-AC96-756997F5B643}"
|
||||||
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Recipes.Scripted.Tests", "VisionBuilder.UI.Recipes.Scripted.Tests\VisionBuilder.UI.Recipes.Scripted.Tests.csproj", "{376797BE-145E-4C48-BD7D-8BF8822A6BC5}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Recipes.Scripted.Tests", "VisionBuilder.UI.Recipes.Scripted.Tests\VisionBuilder.UI.Recipes.Scripted.Tests.csproj", "{376797BE-145E-4C48-BD7D-8BF8822A6BC5}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Statistics", "VisionBuilder.UI.Statistics\VisionBuilder.UI.Statistics.csproj", "{DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Statistics", "VisionBuilder.UI.Statistics\VisionBuilder.UI.Statistics.csproj", "{DBAE2469-ADC5-49CB-B6F5-4F440AF2E32B}"
|
||||||
@@ -311,6 +313,18 @@ Global
|
|||||||
{97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|x64.Build.0 = Release|Any CPU
|
{97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|x86.ActiveCfg = Release|Any CPU
|
{97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|x86.Build.0 = Release|Any CPU
|
{97AF69DC-5061-2643-3CC1-99B51976B05B}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{6B456C8A-92D8-43D0-AC96-756997F5B643}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{376797BE-145E-4C48-BD7D-8BF8822A6BC5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
@@ -662,6 +676,8 @@ Global
|
|||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{5AFF312A-EF5F-49ED-BF54-5AC69F7BEC03} = {FA674B5A-3394-926C-2B1E-70E5B00E4A5C}
|
||||||
|
{98781EAC-F3B2-4DEF-AFED-B0FEB8297025} = {FA674B5A-3394-926C-2B1E-70E5B00E4A5C}
|
||||||
{D5FD2E9D-DA4F-1343-47E0-FBC473A149BC} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
|
{D5FD2E9D-DA4F-1343-47E0-FBC473A149BC} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
|
||||||
{7697A266-A721-4D74-9C5C-0D4F2F6BBF68} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
{7697A266-A721-4D74-9C5C-0D4F2F6BBF68} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
||||||
{E286CE4C-B68A-94B6-F477-0DBF42358009} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
|
{E286CE4C-B68A-94B6-F477-0DBF42358009} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
|
||||||
@@ -699,6 +715,7 @@ Global
|
|||||||
{149E2AFC-A714-4645-883A-2EE685E63FB9} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
{149E2AFC-A714-4645-883A-2EE685E63FB9} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
||||||
{F70920C6-EF98-42B7-9F21-6E94781E9900} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
{F70920C6-EF98-42B7-9F21-6E94781E9900} = {F3414823-B70E-435E-B4EA-80ABF4371449}
|
||||||
{5A1D9E37-353C-4E30-A302-48487DC3A9E8} = {583A77DF-A293-4F3E-AB96-7310BC495822}
|
{5A1D9E37-353C-4E30-A302-48487DC3A9E8} = {583A77DF-A293-4F3E-AB96-7310BC495822}
|
||||||
|
{0549063E-5B22-4332-AD54-0D799B6DB4D2} = {FA674B5A-3394-926C-2B1E-70E5B00E4A5C}
|
||||||
{85063197-582A-4335-8298-431CF5CB95E1} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
|
{85063197-582A-4335-8298-431CF5CB95E1} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace Inspectron.Settings
|
|||||||
public class InspectronSettings
|
public class InspectronSettings
|
||||||
{
|
{
|
||||||
|
|
||||||
public InspectronSettings(string path=null)
|
public InspectronSettings(string path = null, string profile = null)
|
||||||
{
|
{
|
||||||
Assembly assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
|
Assembly assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
|
||||||
|
|
||||||
@@ -29,20 +29,19 @@ namespace Inspectron.Settings
|
|||||||
string startupPath = Path.GetDirectoryName(new Uri(assemblyName.CodeBase).LocalPath);
|
string startupPath = Path.GetDirectoryName(new Uri(assemblyName.CodeBase).LocalPath);
|
||||||
_defaultSettingsPath = Path.Combine(startupPath, "DefaultSettings.xml");
|
_defaultSettingsPath = Path.Combine(startupPath, "DefaultSettings.xml");
|
||||||
|
|
||||||
|
string settingsFileName = string.IsNullOrEmpty(profile)
|
||||||
|
? "AppSettings.xml"
|
||||||
|
: "AppSettings_" + profile + ".xml";
|
||||||
|
|
||||||
if (path == null)
|
if (path == null)
|
||||||
{
|
{
|
||||||
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
_settingsPath = string.Format("{0}\\{1}\\{2}\\AppSettings.xml", appDataPath, _applicationName, _versionString);
|
_settingsPath = string.Format("{0}\\{1}\\{2}\\{3}", appDataPath, _applicationName, _versionString, settingsFileName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_settingsPath = Path.Combine(path,"AppSettings.xml");
|
_settingsPath = Path.Combine(path, settingsFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
internal Path<object> GetSettingsPath(string pathName)
|
internal Path<object> GetSettingsPath(string pathName)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user