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

View File

@@ -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>

View 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;
}
}

View 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);
}
}

View 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>

View 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());
}
}

View File

@@ -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>

View File

@@ -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; }
} }

View File

@@ -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;
} }

View File

@@ -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;
}
} }

View File

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

View File

@@ -4,7 +4,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using 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();

View File

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

View File

@@ -102,11 +102,21 @@
<Separator Margin="0,8" /> <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" />

View File

@@ -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);
}
} }

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings> <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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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>

View File

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -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);

View File

@@ -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>

View File

@@ -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

View File

@@ -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)
{ {