Files
EugeneTes 8704d0b5ac 2.0.15
profile influences configuration
2026-05-15 11:54:36 +02:00

313 lines
9.7 KiB
C#

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