From 546b894e6bdab1e1eb5139797c962b2513ce199c Mon Sep 17 00:00:00 2001 From: 820689062 <820689062@qq.com> Date: Mon, 20 Oct 2025 14:47:17 +0800 Subject: [PATCH] 111 --- Check.Main/Camera/CameraManager.cs | 53 ++-- Check.Main/Camera/CameraProcessor.cs | 147 ++++++++- Check.Main/Camera/CameraSettings.cs | 6 +- Check.Main/Camera/HikvisionCamera.cs | 4 +- Check.Main/Check.Main.csproj | 13 +- Check.Main/Common/EasyE5Options.cs | 2 + Check.Main/Common/LogoMatcher.cs | 290 ++++++++++++++++++ Check.Main/Common/LogoTemplateTrainer.cs | 62 ++++ .../Common/ProcessingCompletedEventArgs.cs | 4 +- Check.Main/Dispatch/ProductManager.cs | 3 +- Check.Main/FrmMain.cs | 23 +- Check.Main/Infer/ModelSettings.cs | 1 + Check.Main/Process_Img.cs | 150 +++++++++ Check.Main/UI/FormControlPanel.Designer.cs | 97 +++--- Check.Main/UI/FormControlPanel.cs | 16 +- Check.Main/UI/FormControlPanel.resx | 187 +++++++++-- 16 files changed, 917 insertions(+), 141 deletions(-) create mode 100644 Check.Main/Common/LogoMatcher.cs create mode 100644 Check.Main/Common/LogoTemplateTrainer.cs create mode 100644 Check.Main/Process_Img.cs diff --git a/Check.Main/Camera/CameraManager.cs b/Check.Main/Camera/CameraManager.cs index 3762048..080acef 100644 --- a/Check.Main/Camera/CameraManager.cs +++ b/Check.Main/Camera/CameraManager.cs @@ -24,34 +24,32 @@ namespace Check.Main.Camera /// public static class CameraManager { - // 活动的相机实例字典,键为相机名称 + //1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。 + // 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性 public static Dictionary ActiveCameras { get; } = new Dictionary(); // 相机对应的图像显示窗口字典 //public static Dictionary CameraDisplays { get; } = new Dictionary(); - public static Dictionary OriginalImageDisplays { get; } = new Dictionary(); - public static Dictionary ResultImageDisplays { get; } = new Dictionary(); + public static Dictionary OriginalImageDisplays { get; } = new Dictionary();//原始图像窗口 + public static Dictionary ResultImageDisplays { get; } = new Dictionary();//结果图像窗口 - // --- 多相机同步逻辑 --- - // 产品检测队列 + //2、多相机同步逻辑 + // 【队列】一个产品需要多台相机拍完,才算完整。 private static readonly Queue ProductQueue = new Queue(); - // 队列锁,保证线程安全 + // 【(队列)锁】保证队列在多线程下安全 private static readonly object QueueLock = new object(); // 当前启用的相机数量,用于判断产品是否检测完毕 private static int EnabledCameraCount = 0; - //public static event EventHandler OnDetectionCompleted; // public static bool IsDetectionRunning { get; private set; } = false; - - // 产品ID计数器 + // 产品ID【计数器】 private static long _productCounter = 0; // 用于同步计数器访问的锁,虽然long的自增是原子操作,但为清晰和未来扩展,使用锁是好习惯 private static readonly object _counterLock = new object(); - // --- 新增:硬触发模拟器 --- + // 3、--- 新增:硬触发模拟器 --- private static readonly System.Timers.Timer _hardwareTriggerSimulator; - /// /// 获取或设置模拟硬触发的间隔时间(毫秒)。 /// @@ -221,12 +219,12 @@ namespace Check.Main.Camera // } //} - + //********************初始化和启动流程******************* /// /// 准备所有相机硬件、UI窗口和后台处理器,但不开始采集。 /// 这是“启动设备”的第一阶段。 /// - public static void PrepareAll(ProcessConfig config, FrmMain mainForm) + public static void PrepareAll(ProcessConfig config, FrmMain mainForm)//①准备所有相机和模型 { // 1. 清理旧资源和UI mainForm.ClearStatusStrip(); @@ -238,7 +236,7 @@ namespace Check.Main.Camera YoloModelManager.Initialize(config.ModelSettings); DetectionCoordinator.Initialize(config.CameraSettings, config.ModelSettings); - // 3. 创建相机硬件实例和UI窗口 + // 3. 创建相机硬件实例和UI窗口------ var deviceList = new HikvisionCamera().FindDevices(); if (deviceList.Count == 0) { @@ -246,7 +244,7 @@ namespace Check.Main.Camera return; } - foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled)) + foreach (var setting in config.CameraSettings.Where(s => s.IsEnabled))//打开相机 → 设置触发模式 → 绑定事件(图像采集、检测完成)。 { var cam = new HikvisionCamera { Name = setting.Name, CameraIndex = setting.CameraIndex }; cam.TriggerMode = setting.TriggerMode; @@ -293,6 +291,7 @@ namespace Check.Main.Camera /// /// 根据配置列表初始化或更新所有相机 + /// 类似 PrepareAll,但更侧重于更新配置,会检查物理设备数量,不足时报 warning。 /// public static void Initialize(ProcessConfig config, FrmMain mainForm) { @@ -398,7 +397,7 @@ namespace Check.Main.Camera } /// - /// 定时器触发事件。 + /// 定时器触发事件。(定时器 OnHardwareTriggerTimerElapsed 会遍历相机,对处于软件触发模式的相机执行 SoftwareTrigger()。) /// private static void OnHardwareTriggerTimerElapsed(object sender, ElapsedEventArgs e) { @@ -490,7 +489,9 @@ namespace Check.Main.Camera //} - // Shutdown 方法也简化 + /// + /// 停止所有相机 + 关闭窗口 + 清空字典 + 关闭检测协调器 + 销毁模型 + /// public static void Shutdown() { // 1. 停止硬件和模拟器 @@ -618,7 +619,7 @@ namespace Check.Main.Camera // } //} - // 图像回调方法现在极其简单 + //【相机回调】 private static void OnCameraImageAcquired(HikvisionCamera sender, Bitmap bmp) { Bitmap bmpForDisplay = null; @@ -626,16 +627,16 @@ namespace Check.Main.Camera try { - // 1. 为“显示”和“处理”创建两个完全独立的深克隆副本 + // 1. 深度克隆 Bitmap → 分成 显示副本 和 处理副本。 bmpForDisplay = DeepCloneBitmap(bmp, "Display"); bmpForProcessing = DeepCloneBitmap(bmp, "Processing"); } finally { // 2.无论克隆成功与否,都必须立即释放事件传递过来的原始bmp,防止泄漏。 - bmp?.Dispose(); + bmp?.Dispose();//空条件运算符 ?.-----在访问对象成员前检查对象是否为 null,如果 bmp 不是 null,则正常调用 Dispose() 方法,替代传统的 if (bmp != null) 检查 } - // 分支 A: 将用于显示的副本发送到对应的UI窗口 + // 分支 A: 显示副本 → 交给 FormImageDisplay.UpdateImage()。 if (bmpForDisplay != null && OriginalImageDisplays.TryGetValue(sender.Name, out var displayWindow)) { // displayWindow.UpdateImage 会处理线程安全问题 @@ -646,7 +647,7 @@ namespace Check.Main.Camera // 如果没有对应的显示窗口,或克隆失败,必须释放为显示创建的副本 bmpForDisplay?.Dispose(); } - // 分支 B: 将用于处理的副本发送到检测协调器的后台队列 + // 分支 B: 处理副本 → 交给 DetectionCoordinator.EnqueueImage() 进行检测。 if (bmpForProcessing != null) { // bmpForProcessing 的所有权在这里被转移给了协调器 @@ -673,11 +674,11 @@ namespace Check.Main.Camera // 2. 找到此相机的结果显示窗口 if (ResultImageDisplays.TryGetValue(cameraEntry.Key, out var resultDisplay)) { - var bmp = ConvertSKImageToBitmap(e.ResultImage); - if (bmp != null) + //var bmp = ConvertSKImageToBitmap(e.ResultImage); + if (e.ResultImage != null) { // UpdateImage 会负责克隆并显示,所以这里传递 bmp 即可 - resultDisplay.UpdateImage(bmp); + resultDisplay.UpdateImage(e.ResultImage); } } else @@ -691,7 +692,7 @@ namespace Check.Main.Camera /// /// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。 /// - private static Bitmap ConvertSKImageToBitmap(SKImage skImage) + public static Bitmap ConvertSKImageToBitmap(SKImage skImage) { if (skImage == null) return null; diff --git a/Check.Main/Camera/CameraProcessor.cs b/Check.Main/Camera/CameraProcessor.cs index 2386223..2770ca0 100644 --- a/Check.Main/Camera/CameraProcessor.cs +++ b/Check.Main/Camera/CameraProcessor.cs @@ -1,5 +1,6 @@ using Check.Main.Common; using Check.Main.Infer; +using HalconTemplateMatch; using SkiaSharp; using System; using System.Collections.Concurrent; @@ -10,7 +11,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using YoloDotNet.Extensions; - +using OpenCvSharp; namespace Check.Main.Camera { public class CameraProcessor : IDisposable @@ -52,6 +53,9 @@ namespace Check.Main.Camera _imageQueue.Add(new ImageData(_imageCounter, _cameraIndex, bmp)); } + /// + /// 图像处理主循环 + /// private void ProcessQueue() { // 从模型管理器获取此线程专属的YOLO模型 @@ -61,6 +65,36 @@ namespace Check.Main.Camera ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 无法获取对应的YOLO模型,处理线程已中止。"); return; // 如果没有模型,此线程无法工作 } + + //训练阶段(相机2) + var trainer = new LogoTemplateTrainer(); + + trainer.TrainAndSaveTemplates( + new List + { + @"D:\HalconTemplateMatch\train2\logo1.bmp", + @"D:\HalconTemplateMatch\train2\logo2.bmp", + @"D:\HalconTemplateMatch\train2\logo3.bmp" + }, + @"D:\HalconTemplateMatch\model_2"); + + //训练阶段(相机3)9.25修改!! + trainer.TrainAndSaveTemplates( + new List + { + @"D:\HalconTemplateMatch\train3\3C_1.bmp", + @"D:\HalconTemplateMatch\train3\3C_2.bmp", + @"D:\HalconTemplateMatch\train3\3C_3.bmp" + }, + @"D:\HalconTemplateMatch\model_3"); + + if (trainer == null) + { + ThreadSafeLogger.Log($"[错误] 相机 #{_modeId} 未加载模板,处理线程已中止。"); + return; + } + + while (_isRunning) { try @@ -69,19 +103,15 @@ namespace Check.Main.Camera ImageData data = _imageQueue.Take(); using (data) { - //SKImage resultSkImage = null; // 用于存储最终绘制好结果的图像 - + string result = ""; using (var skImage = ConvertBitmapToSKImage(data.Image)) // 转换图像格式并管理其生命周期 { if (skImage == null) continue; var predictions = yoloModel.RunObjectDetection(skImage); - // 模拟模型处理 - //Thread.Sleep(50); // 模拟耗时 - //bool isOk = new Random().NextDouble() > 0.1; - //string result = isOk ? "OK" : "NG"; + result = predictions.Any() ? "NG" : "OK"; - string result = predictions.Any() ? "NG" : "OK"; ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},检测到 {predictions.Count} 个目标,结果: {result}"); + // 将处理结果交给协调器进行组装 DetectionCoordinator.AssembleProduct(data, result); @@ -90,16 +120,111 @@ namespace Check.Main.Camera using (var resultSkImage = skImage.Draw(predictions)) { // 4. 触发事件,将绘制好的 resultSkImage 传递出去 + + Bitmap bitmap = CameraManager.ConvertSKImageToBitmap(resultSkImage); // 所有权在这里被转移 OnProcessingCompleted?.Invoke(this, new ProcessingCompletedEventArgs( _cameraIndex, data.ProductId, - resultSkImage + bitmap )); } } } + //***********************************使用Halcon模板匹配进行检测**************************************************** + if (data.Image == null) continue; + // 统一定义预测结果 + var matcher = new LogoMatcher(); + + //9.25(增加一根据不同的相机编号调用不同的模型!) + string filepath = ""; + if (_cameraIndex == 2) + { + matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_2"); + filepath = "D:\\HalconTemplateMatch\\train2"; + } + else if (_cameraIndex == 3) + { + matcher.LoadTemplates(@"D:\HalconTemplateMatch\model_3"); + filepath = "D:\\HalconTemplateMatch\\train3"; + } + + ////原bool返回的处理 + //bool found = matcher.FindLogo(data.Image); + //string result = found ? "OK":"NG"; + + //double返回的处理 + double score = matcher.FindLogo(data.Image); + + + //Mat cam = ProcessImg.BitmapToMat(data.Image); + + //score= ProcessImg.ProcessImagesInFolder(filepath,cam); + + //string result = (score > 0.5) ? "OK" : "NG"; + + ThreadSafeLogger.Log($"相机 #{_cameraIndex} 处理产品 #{data.ProductId},结果: {result},得分: {score}"); + + // 将处理结果交给协调器进行组装 + DetectionCoordinator.AssembleProduct(data, result); + + + //给PLC的M90、M91写值(10.10) + if (FrmMain.PlcClient != null && FrmMain.PlcClient.IsConnected) + { + if (result == "OK") + { + //吹气到合格框 + FrmMain.PlcClient.WriteAsync("M90", 1).Wait(); // 写入M90为1 + //Thread.Sleep(100); + FrmMain.PlcClient.WriteAsync("M90", 0).Wait(); // 写入M90为1 + + //var a = FrmMain.PlcClient.ReadAsync("M90"); + //Console.WriteLine(a); + + //FrmMain.PlcClient.WriteAsync("M91", 0).Wait(); + + // 延时复位 + Task.Run(async () => + { + //await Task.Delay(300); // 延时300毫秒,可根据实际气动时间调整 + //await FrmMain.PlcClient.WriteAsync("M90", 0); + }); + } + else + { + //吹气到不合格框 + //FrmMain.PlcClient.WriteAsync("M90", 0).Wait(); + FrmMain.PlcClient.WriteAsync("M91", 1).Wait();// 写入M91为1 + FrmMain.PlcClient.WriteAsync("M91", 0).Wait(); // 写入M90为1 + + //var a = FrmMain.PlcClient.ReadAsync("M90"); + //Console.WriteLine(a); + // 延时复位 + Task.Run(async () => + { + //await Task.Delay(300); + //await FrmMain.PlcClient.WriteAsync("M91", 0); + }); + } + } + else + { + ThreadSafeLogger.Log("[PLC] 未连接,跳过写入。"); + } + + + // ③ 外部订阅事件 + OnProcessingCompleted?.Invoke( + this, + new ProcessingCompletedEventArgs + ( + _cameraIndex, + data.ProductId, + data.Image // 原图传出去 + ) + ); } } catch (InvalidOperationException) @@ -107,12 +232,14 @@ namespace Check.Main.Camera // 当调用 Stop 时,会 CompleteAdding 队列,Take 会抛出此异常,是正常退出流程 break; } - catch (Exception ex) + catch (Exception ex) { ThreadSafeLogger.Log($"[ERROR] 相机 #{_cameraIndex} 处理线程异常: {ex.Message}"); } } } + + /// /// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。 /// diff --git a/Check.Main/Camera/CameraSettings.cs b/Check.Main/Camera/CameraSettings.cs index 054a4f0..0a5cce3 100644 --- a/Check.Main/Camera/CameraSettings.cs +++ b/Check.Main/Camera/CameraSettings.cs @@ -30,7 +30,7 @@ namespace Check.Main.Camera } /// - /// 相机配置信息类,用于PropertyGrid显示和编辑 + /// 相机配置信息类CameraSettings,用于PropertyGrid显示和编辑(****核心!****) /// public class CameraSettings : INotifyPropertyChanged, ICloneable { @@ -39,8 +39,8 @@ namespace Check.Main.Camera private int _cameraIndex = 0; private string _name = "Camera-1"; - private string _ipAddress = "192.168.1.100"; - private string _ipDeviceAddress = "192.168.1.101"; + private string _ipAddress = "169.254.51.253"; + private string _ipDeviceAddress = "169.254.51.45"; private TriggerModeType _triggerMode = TriggerModeType.Continuous; private bool _isEnabled = true; private CheckType _checkType = CheckType.DeepLearning; diff --git a/Check.Main/Camera/HikvisionCamera.cs b/Check.Main/Camera/HikvisionCamera.cs index 79e7779..e44c78c 100644 --- a/Check.Main/Camera/HikvisionCamera.cs +++ b/Check.Main/Camera/HikvisionCamera.cs @@ -179,7 +179,9 @@ namespace Check.Main.Camera } // 默认设置为连续模式 - SetContinuousMode(); + //SetContinuousMode(); + // 设置为硬触发模式(Line0) + SetTriggerMode(false); IsOpen = true; OnCameraMessage("相机打开成功。", 0); diff --git a/Check.Main/Check.Main.csproj b/Check.Main/Check.Main.csproj index d210470..36b996e 100644 --- a/Check.Main/Check.Main.csproj +++ b/Check.Main/Check.Main.csproj @@ -1,7 +1,7 @@  - WinExe + Exe net8.0-windows enable true @@ -10,11 +10,14 @@ false false + True + + @@ -26,11 +29,17 @@ + + C:\Program Files\MVTec\HALCON-10.0\bin\dotnet35\halcondotnet.dll + + + C:\Program Files\MVTec\HALCON-10.0\bin\dotnet35\hdevenginedotnet.dll + ..\..\..\HslCommunication-master\HslCommunication.dll - C:\Program Files (x86)\MVS\Development\DotNet\win64\MvCameraControl.Net.dll + ..\..\..\..\XKRS2025\XKRS2025\CheckDevice\Check.Main\bin\Debug\MvCameraControl.Net.dll diff --git a/Check.Main/Common/EasyE5Options.cs b/Check.Main/Common/EasyE5Options.cs index 59fb256..911cbb5 100644 --- a/Check.Main/Common/EasyE5Options.cs +++ b/Check.Main/Common/EasyE5Options.cs @@ -33,6 +33,8 @@ namespace Check.Main.Common _slaveId = slaveId; } + public bool IsConnected => _tcpClient != null && _tcpClient.Connected;//10.11添加 + public async Task ConnectAsync() { await _tcpClient.ConnectAsync(_ip, _port); diff --git a/Check.Main/Common/LogoMatcher.cs b/Check.Main/Common/LogoMatcher.cs new file mode 100644 index 0000000..61422ca --- /dev/null +++ b/Check.Main/Common/LogoMatcher.cs @@ -0,0 +1,290 @@ +//using HalconDotNet; +//using System; +//using System.Collections.Generic; +//using System.Drawing.Imaging; +//using System.IO; + +//namespace HalconTemplateMatch +//{ +// public class LogoMatcher +// { +// private List modelHandles = new List(); + +// /// +// /// 从文件加载多个模板 +// /// +// public void LoadTemplates(string dir) +// { +// foreach (var file in Directory.GetFiles(dir, "*.shm")) +// { +// HTuple modelID; +// HOperatorSet.ReadShapeModel(file, out modelID); +// modelHandles.Add(modelID); +// Console.WriteLine($"加载模板: {file}"); +// } +// } + +// /// +// /// 在测试图像中查找 Logo +// /// +// /// true = OK,false = NG +// public bool FindLogo(string testImagePath) +// { +// HObject ho_TestImage; +// HOperatorSet.ReadImage(out ho_TestImage, testImagePath); +// HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); + +// foreach (var modelID in modelHandles) +// { +// HOperatorSet.FindShapeModel( +// ho_TestImage, +// modelID, +// new HTuple(0).TupleRad(), +// new HTuple(360).TupleRad(), +// 0.5, // 最低分数 +// 1, // 最大匹配数 +// 0.5, // 重叠度 +// "least_squares", +// 0, +// 0.9, +// out HTuple row, +// out HTuple col, +// out HTuple angle, +// out HTuple score); + +// if (score.Length > 0 && score[0].D > 0.5) +// { +// Console.WriteLine($"找到 Logo: Row={row[0]}, Col={col[0]}, Score={score[0]}"); +// return true; // 找到即返回成功 +// } +// } + +// return false; // 没找到 +// } + + + +// /// +// /// 重载FindLogo函数(double返回) +// /// +// /// +// /// +// public double FindLogo(Bitmap bmp) +// { +// // Bitmap 转 HObject +// HObject ho_TestImage; +// Bitmap2HObject(bmp, out ho_TestImage); + +// HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); + +// double bestScore = -1; + +// foreach (var modelID in modelHandles) +// { +// HOperatorSet.FindShapeModel( +// ho_TestImage, +// modelID, +// new HTuple(0).TupleRad(), +// new HTuple(360).TupleRad(), +// 0.5, // 最低分数 +// 1, // 最大匹配数 +// 0.5, // 重叠度 +// "least_squares", +// 0, +// 0.9, +// out HTuple row, +// out HTuple col, +// out HTuple angle, +// out HTuple score); + +// if (score.Length > 0 && score[0].D > bestScore) +// { +// bestScore = score[0].D; +// } +// } + +// ho_TestImage.Dispose(); +// return bestScore; // -1 = 没找到 +// } + +// /// +// /// Bitmap 转 Halcon HObject +// /// +// private void Bitmap2HObject(Bitmap bmp, out HObject hobj) +// { +// HOperatorSet.GenEmptyObj(out hobj); +// Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); +// BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); + +// try +// { +// HOperatorSet.GenImageInterleaved( +// out hobj, +// bmpData.Scan0, +// "bgr", // Bitmap 默认是 BGR +// bmp.Width, +// bmp.Height, +// 0, +// "byte", +// bmp.Width, +// bmp.Height, +// 0, +// 0, +// -1, +// 0 +// ); +// } +// finally +// { +// bmp.UnlockBits(bmpData); +// } +// } +// } +//} + + + +using HalconDotNet; +using NPOI.OpenXmlFormats.Vml; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; + +namespace HalconTemplateMatch +{ + public class LogoMatcher + { + private readonly List modelHandles = new List(); + + /// + /// 从指定目录加载所有 .shm 模板文件 + /// + public void LoadTemplates(string dir) + { + try + { + string fullPath = Path.GetFullPath(dir); + + if (!Directory.Exists(fullPath)) + { + Console.WriteLine($"[警告] 模型目录不存在: {fullPath}"); + return; + } + + string[] modelFiles = Directory.GetFiles(fullPath, "*.shm", SearchOption.TopDirectoryOnly); + + if (modelFiles.Length == 0) + { + Console.WriteLine($"[警告] 模型目录中没有任何 .shm 文件: {fullPath}"); + return; + } + + foreach (var file in modelFiles) + { + try + { + HTuple modelID; + HOperatorSet.ReadShapeModel(file, out modelID); + modelHandles.Add(modelID); + Console.WriteLine($"[加载成功] 模板: {file}"); + } + catch (HOperatorException ex) + { + Console.WriteLine($"[错误] 无法加载模板 {file}: {ex.Message}"); + } + } + + if (modelHandles.Count == 0) + { + Console.WriteLine($"[警告] 没有成功加载任何模板文件。"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[异常] 加载模板目录出错: {ex.Message}"); + } + } + + /// + /// 匹配并返回最高得分(double返回) + /// + public double FindLogo(Bitmap bmp) + { + if (modelHandles.Count == 0) + { + Console.WriteLine("[警告] 尚未加载任何模板。"); + return -1; + } + + // Bitmap 转 Halcon 对象 + HObject ho_TestImage; + Bitmap2HObject(bmp, out ho_TestImage); + HOperatorSet.Rgb1ToGray(ho_TestImage, out ho_TestImage); + + double bestScore = -1; + + foreach (var modelID in modelHandles) + { + try + { + HOperatorSet.FindScaledShapeModel( + ho_TestImage, + modelID, + new HTuple(0).TupleRad(), + new HTuple(360).TupleRad(), + 0.8, 1.2, + 0.5, 1, 0.5, + "least_squares_high", + 0, 0.9, + out HTuple row, out HTuple col, out HTuple angle, out HTuple scale, out HTuple score +); + + + if (score.Length > 0 && score[0].D > bestScore) + bestScore = score[0].D; + } + catch (HOperatorException ex) + { + Console.WriteLine($"[错误] 模板匹配失败: {ex.Message}"); + } + } + + ho_TestImage.Dispose(); + return bestScore; + } + + /// + /// Bitmap 转 Halcon HObject + /// + private void Bitmap2HObject(Bitmap bmp, out HObject hobj) + { + HOperatorSet.GenEmptyObj(out hobj); + Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height); + BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); + + try + { + HOperatorSet.GenImageInterleaved( + out hobj, + bmpData.Scan0, + "bgr", + bmp.Width, + bmp.Height, + 0, + "byte", + bmp.Width, + bmp.Height, + 0, + 0, + -1, + 0); + } + finally + { + bmp.UnlockBits(bmpData); + } + } + } +} diff --git a/Check.Main/Common/LogoTemplateTrainer.cs b/Check.Main/Common/LogoTemplateTrainer.cs new file mode 100644 index 0000000..2860784 --- /dev/null +++ b/Check.Main/Common/LogoTemplateTrainer.cs @@ -0,0 +1,62 @@ +using HalconDotNet; +using System; +using System.Collections.Generic; +using System.IO; + +namespace HalconTemplateMatch +{ + public class LogoTemplateTrainer + { + /// + /// 从多张样本图像生成模板并保存到文件 + /// + /// 训练图片路径集合 + /// 模板保存目录 + public void TrainAndSaveTemplates(List imagePaths, string saveDir) + { + if (!Directory.Exists(saveDir)) + Directory.CreateDirectory(saveDir); + + int index = 0; + foreach (var path in imagePaths) + { + HObject ho_Image; + HOperatorSet.ReadImage(out ho_Image, path); + + // 转灰度 + HOperatorSet.Rgb1ToGray(ho_Image, out HObject ho_Gray); + + // 二值化 + HOperatorSet.Threshold(ho_Gray, out HObject ho_Region, 128, 255); + + // 提取连通域 + HOperatorSet.Connection(ho_Region, out HObject ho_Connected); + HOperatorSet.SelectShapeStd(ho_Connected, out HObject ho_Selected, "max_area", 70); + + // ROI 约束 + HOperatorSet.ReduceDomain(ho_Gray, ho_Selected, out HObject ho_ROI); + + // 创建形状模板 + HTuple modelID; + HOperatorSet.CreateShapeModel( + ho_ROI, + "auto", + new HTuple(0).TupleRad(), + new HTuple(360).TupleRad(), + "auto", + "auto", + "use_polarity", + "auto", + "auto", + out modelID); + + // 保存模板到文件 + string modelFile = Path.Combine(saveDir, $"logo_model_{index}.shm"); + HOperatorSet.WriteShapeModel(modelID, modelFile); + + Console.WriteLine($"训练完成并保存模板: {modelFile}"); + index++; + } + } + } +} diff --git a/Check.Main/Common/ProcessingCompletedEventArgs.cs b/Check.Main/Common/ProcessingCompletedEventArgs.cs index ddae0d8..e7f70da 100644 --- a/Check.Main/Common/ProcessingCompletedEventArgs.cs +++ b/Check.Main/Common/ProcessingCompletedEventArgs.cs @@ -11,9 +11,9 @@ namespace Check.Main.Common { public int CameraIndex { get; } public long ProductId { get; } - public SKImage ResultImage { get; } // 【修改】携带已绘制好结果的SKImage + public Bitmap ResultImage { get; } // 原来是SKImage ResultImage - public ProcessingCompletedEventArgs(int cameraIndex, long productId, SKImage resultImage) + public ProcessingCompletedEventArgs(int cameraIndex, long productId, Bitmap resultImage)//原来是SKImage resultImage { CameraIndex = cameraIndex; ProductId = productId; diff --git a/Check.Main/Dispatch/ProductManager.cs b/Check.Main/Dispatch/ProductManager.cs index 262094b..8869498 100644 --- a/Check.Main/Dispatch/ProductManager.cs +++ b/Check.Main/Dispatch/ProductManager.cs @@ -52,6 +52,7 @@ namespace Check.Main.Dispatch config = new ProcessConfig(); } + CurrentProductName = productName; CurrentConfig = config; @@ -120,7 +121,7 @@ namespace Check.Main.Dispatch // 切换到列表中的第一个产品,或者如果列表为空,则创建一个默认产品 string nextProduct = ProductList.FirstOrDefault() ?? "DefaultProduct"; SwitchToProduct(nextProduct); - } + } else { // 如果删除的不是当前产品,我们仍然需要触发一次事件, diff --git a/Check.Main/FrmMain.cs b/Check.Main/FrmMain.cs index 8a5f002..bd41f25 100644 --- a/Check.Main/FrmMain.cs +++ b/Check.Main/FrmMain.cs @@ -19,6 +19,8 @@ namespace Check.Main public partial class FrmMain : Form { // private FrmCamConfig _formCameraConfig; + public static EasyPlcClient PlcClient;//定义全局PLC对象 --- 10.10添加① + private FrmConfig _frmConfig; private FrmLog _formLog; private ThemeBase _theme = new VS2015BlueTheme(); // 外观主题 @@ -40,11 +42,28 @@ namespace Check.Main _deserializeDockContent = new DeserializeDockContent(GetContentFromPersistString); } - private void FrmMain_Load(object sender, EventArgs e) + //添加 PLC 初始化方法 --- 10.10添加② + private async Task InitPlcConnection() { + try + { + PlcClient = new EasyPlcClient("192.168.1.88", 502); + await PlcClient.ConnectAsync(); + ThreadSafeLogger.Log("[PLC] 已成功连接到汇川PLC"); + } + catch (Exception ex) + { + ThreadSafeLogger.Log($"[PLC] 连接失败: {ex.Message}"); + } + } + + private async void FrmMain_Load(object sender, EventArgs e)//添加了一个async,原来是private void FrmMain_Load-----10.10修改 + { + // 初始化PLC连接-----10.10添加 + await InitPlcConnection(); + EasyPlcClient easyPlcClient = new EasyPlcClient("127.0.0.1", 502, 1); easyPlcClient.ConnectAsync(); - _frmConfig = new FrmConfig { Text = "主程序配置" }; _formLog = new FrmLog { Text = "运行日志" }; _formStatistics = new FormStatistics { Text = "生产统计" }; diff --git a/Check.Main/Infer/ModelSettings.cs b/Check.Main/Infer/ModelSettings.cs index cf05f07..0aaedc8 100644 --- a/Check.Main/Infer/ModelSettings.cs +++ b/Check.Main/Infer/ModelSettings.cs @@ -127,5 +127,6 @@ namespace Check.Main.Infer { return this.MemberwiseClone(); // 浅克隆对于这个类足够了 } + } } diff --git a/Check.Main/Process_Img.cs b/Check.Main/Process_Img.cs new file mode 100644 index 0000000..7271ad8 --- /dev/null +++ b/Check.Main/Process_Img.cs @@ -0,0 +1,150 @@ +using OpenCvSharp; +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Drawing.Imaging; +using OpenCvSharp.Extensions; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Diagnostics; +using System.Threading; + +public class ProcessImg +{ + // 对单个图像进行模板匹配 + + public static (double score, Rect? coords) MatchTemplate(Mat img, string templatePath) + { + // 确保图像和模板文件存在 + + + // 读取图像和模板 + + var template = Cv2.ImRead(templatePath, ImreadModes.Color); + + // 创建一个模板匹配的结果矩阵 + var result = new Mat(); + Cv2.MatchTemplate(img, template, result, TemplateMatchModes.CCoeffNormed); + + // 查找最大匹配值 + Cv2.MinMaxLoc(result, out _, out var maxVal, out _, out var maxLoc); + + // 如果找到的最大匹配值大于阈值 + double threshold = 0.3; // 可以根据需要调整阈值 + if (maxVal >= threshold) + { + // 计算匹配的坐标 + var topLeft = maxLoc; + var rect = new Rect(topLeft.X, topLeft.Y, template.Width, template.Height); + + return (maxVal, rect); + } + + return (0, null); + } + + // 遍历文件夹中的所有图像文件进行模板匹配,并找到最佳得分图像 + public static Double ProcessImagesInFolder(string folderPath, Mat img) + { + // 获取所有图像文件 + var imageFiles = Directory.GetFiles(folderPath, "*.*", SearchOption.TopDirectoryOnly) + .Where(file => file.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || + file.EndsWith(".png", StringComparison.OrdinalIgnoreCase) || + file.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + + + // 确保输出文件夹存在 + + + // 线程数量 + int numThreads = 5; + + // 用于存储每个图像的得分和坐标 + var bestMatch = new ConcurrentBag<(string imagePath, double score, Rect? coords)>(); + DateTime startTime = DateTime.Now; + Stopwatch sw = new Stopwatch(); + sw.Start(); + + Parallel.ForEach(imageFiles, new ParallelOptions { MaxDegreeOfParallelism = numThreads }, picPath => + { + DateTime threadStartTime = DateTime.Now; + + var (score, coords) = MatchTemplate(img, picPath); + + + + bestMatch.Add((picPath, score, coords)); + + + DateTime threadEndTime = DateTime.Now; + TimeSpan threadElapsed = threadEndTime - threadStartTime; + Console.WriteLine($"线程处理 {picPath} 耗时: {threadElapsed.TotalMilliseconds}ms"); + }); + + sw.Stop(); + TimeSpan totalElapsed = sw.Elapsed; + Console.WriteLine($"处理完成,耗时: {totalElapsed.TotalSeconds}秒"); + // 查找最佳得分 + var best = bestMatch.OrderByDescending(m => m.score).FirstOrDefault(); + return best.score; + //if (best.coords.HasValue) + //{ + // Rect rect = best.coords.Value; + // return rect; + //} + + //else + //{ + // return new Rect(0, 0, 0, 0); + //} + + + } + public static Mat BitmapToMat(Bitmap bitmap) + { + if (bitmap == null) + { + throw new ArgumentException("Bitmap is null"); + } + + // 根据 Bitmap 的宽度、高度和像素格式创建一个与其相对应的 Mat + Mat mat = new Mat(bitmap.Height, bitmap.Width, MatType.CV_8UC3); // 假设 Bitmap 是 24-bit RGB + + // 锁定 Bitmap 的内存区域以直接访问它的内存 + BitmapData bitmapData = bitmap.LockBits( + new Rectangle(0, 0, bitmap.Width, bitmap.Height), + ImageLockMode.ReadOnly, + bitmap.PixelFormat + ); + + try + { + // 使用直接内存拷贝将 Bitmap 的数据拷贝到 Mat + unsafe + { + byte* srcData = (byte*)bitmapData.Scan0; + byte* dstData = (byte*)mat.DataPointer; + + int stride = bitmapData.Stride; + int width = bitmap.Width * 3; // 24bpp 3个字节一个像素 + int height = bitmap.Height; + + for (int y = 0; y < height; y++) + { + Buffer.MemoryCopy(srcData + y * stride, dstData + y * mat.Step(), width, width); + } + } + } + finally + { + bitmap.UnlockBits(bitmapData); + } + + return mat; + } + + +} diff --git a/Check.Main/UI/FormControlPanel.Designer.cs b/Check.Main/UI/FormControlPanel.Designer.cs index 3e0283e..b6f3d46 100644 --- a/Check.Main/UI/FormControlPanel.Designer.cs +++ b/Check.Main/UI/FormControlPanel.Designer.cs @@ -28,79 +28,54 @@ /// private void InitializeComponent() { - this.uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel(); - this.btnStartDevice = new Sunny.UI.UIButton(); - this.btnStartCheck = new Sunny.UI.UIButton(); - this.uiTableLayoutPanel1.SuspendLayout(); - this.SuspendLayout(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FormControlPanel)); + uiTableLayoutPanel1 = new Sunny.UI.UITableLayoutPanel(); + btnStartDevice = new Sunny.UI.UIButton(); + btnStartCheck = new Sunny.UI.UIButton(); + uiTableLayoutPanel1.SuspendLayout(); + SuspendLayout(); // // uiTableLayoutPanel1 // - this.uiTableLayoutPanel1.ColumnCount = 2; - this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.uiTableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.uiTableLayoutPanel1.Controls.Add(this.btnStartDevice, 0, 1); - this.uiTableLayoutPanel1.Controls.Add(this.btnStartCheck, 0, 3); - this.uiTableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; - this.uiTableLayoutPanel1.Location = new System.Drawing.Point(0, 0); - this.uiTableLayoutPanel1.Name = "uiTableLayoutPanel1"; - this.uiTableLayoutPanel1.RowCount = 5; - this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F)); - this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 35.35353F)); - this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 7.575758F)); - this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 33.83838F)); - this.uiTableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 11.38585F)); - this.uiTableLayoutPanel1.Size = new System.Drawing.Size(230, 198); - this.uiTableLayoutPanel1.TabIndex = 0; - this.uiTableLayoutPanel1.TagString = null; + resources.ApplyResources(uiTableLayoutPanel1, "uiTableLayoutPanel1"); + uiTableLayoutPanel1.Controls.Add(btnStartDevice, 0, 1); + uiTableLayoutPanel1.Controls.Add(btnStartCheck, 0, 3); + uiTableLayoutPanel1.Name = "uiTableLayoutPanel1"; + uiTableLayoutPanel1.TagString = null; // // btnStartDevice // - this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartDevice, 2); - this.btnStartDevice.Cursor = System.Windows.Forms.Cursors.Hand; - this.btnStartDevice.Dock = System.Windows.Forms.DockStyle.Fill; - this.btnStartDevice.FillPressColor = System.Drawing.Color.LimeGreen; - this.btnStartDevice.FillSelectedColor = System.Drawing.Color.LimeGreen; - this.btnStartDevice.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); - this.btnStartDevice.Location = new System.Drawing.Point(3, 25); - this.btnStartDevice.MinimumSize = new System.Drawing.Size(1, 1); - this.btnStartDevice.Name = "btnStartDevice"; - this.btnStartDevice.Size = new System.Drawing.Size(224, 64); - this.btnStartDevice.TabIndex = 0; - this.btnStartDevice.Text = "启动设备"; - this.btnStartDevice.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); - this.btnStartDevice.Click += new System.EventHandler(this.btnStartDevice_Click); + uiTableLayoutPanel1.SetColumnSpan(btnStartDevice, 2); + btnStartDevice.Cursor = Cursors.Hand; + resources.ApplyResources(btnStartDevice, "btnStartDevice"); + btnStartDevice.FillPressColor = Color.LimeGreen; + btnStartDevice.FillSelectedColor = Color.LimeGreen; + btnStartDevice.Name = "btnStartDevice"; + btnStartDevice.TipsFont = new Font("宋体", 9F, FontStyle.Regular, GraphicsUnit.Point, 134); + btnStartDevice.Click += btnStartDevice_Click; // // btnStartCheck // - this.uiTableLayoutPanel1.SetColumnSpan(this.btnStartCheck, 2); - this.btnStartCheck.Cursor = System.Windows.Forms.Cursors.Hand; - this.btnStartCheck.Dock = System.Windows.Forms.DockStyle.Fill; - this.btnStartCheck.FillPressColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0))))); - this.btnStartCheck.FillSelectedColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(192)))), ((int)(((byte)(0))))); - this.btnStartCheck.Font = new System.Drawing.Font("宋体", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); - this.btnStartCheck.Location = new System.Drawing.Point(3, 110); - this.btnStartCheck.MinimumSize = new System.Drawing.Size(1, 1); - this.btnStartCheck.Name = "btnStartCheck"; - this.btnStartCheck.Size = new System.Drawing.Size(224, 61); - this.btnStartCheck.TabIndex = 1; - this.btnStartCheck.Text = "开始检测"; - this.btnStartCheck.TipsFont = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134))); - this.btnStartCheck.Click += new System.EventHandler(this.btnStartCheck_Click); + uiTableLayoutPanel1.SetColumnSpan(btnStartCheck, 2); + btnStartCheck.Cursor = Cursors.Hand; + resources.ApplyResources(btnStartCheck, "btnStartCheck"); + btnStartCheck.FillPressColor = Color.FromArgb(0, 192, 0); + btnStartCheck.FillSelectedColor = Color.FromArgb(0, 192, 0); + btnStartCheck.Name = "btnStartCheck"; + btnStartCheck.TipsFont = new Font("宋体", 9F, FontStyle.Regular, GraphicsUnit.Point, 134); + btnStartCheck.Click += btnStartCheck_Click; // // FormControlPanel // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(230, 198); - this.ControlBox = false; - this.Controls.Add(this.uiTableLayoutPanel1); - this.MaximizeBox = false; - this.MinimizeBox = false; - this.Name = "FormControlPanel"; - this.Text = "启动管理"; - this.uiTableLayoutPanel1.ResumeLayout(false); - this.ResumeLayout(false); + resources.ApplyResources(this, "$this"); + AutoScaleMode = AutoScaleMode.Font; + ControlBox = false; + Controls.Add(uiTableLayoutPanel1); + MaximizeBox = false; + MinimizeBox = false; + Name = "FormControlPanel"; + uiTableLayoutPanel1.ResumeLayout(false); + ResumeLayout(false); } diff --git a/Check.Main/UI/FormControlPanel.cs b/Check.Main/UI/FormControlPanel.cs index 578b166..8dc4759 100644 --- a/Check.Main/UI/FormControlPanel.cs +++ b/Check.Main/UI/FormControlPanel.cs @@ -25,7 +25,7 @@ namespace Check.Main.UI public FormControlPanel() { InitializeComponent(); - + ConfigurationManager.OnConfigurationChanged += HandleConfigurationChanged; UpdateUI(); @@ -50,6 +50,8 @@ namespace Check.Main.UI })); } } + + //点击“ 启动设备” 按钮 private void btnStartDevice_Click(object sender, EventArgs e) { if (_isDeviceReady)//_isDeviceRunning @@ -86,7 +88,7 @@ namespace Check.Main.UI ThreadSafeLogger.Log("用户点击“启动设备”,开始新的启动流程..."); // 1. 从单一数据源获取完整的配置对象 - var config = ConfigurationManager.GetCurrentConfig(); + var config = ConfigurationManager.GetCurrentConfig(); // 2. 验证相机配置的有效性 if (config.CameraSettings == null || !config.CameraSettings.Any(c => c.IsEnabled)) { @@ -124,6 +126,7 @@ namespace Check.Main.UI UpdateUI();//UpdateDeviceButtonUI(); } + //点击 “开始检测” 按钮 private void btnStartCheck_Click(object sender, EventArgs e) { if (_isDetecting) @@ -131,7 +134,7 @@ namespace Check.Main.UI // --- 停止检测 --- ThreadSafeLogger.Log("用户点击“停止检测”,暂停数据流..."); // 停止硬触发模拟器 - CameraManager.StopHardwareTriggerSimulator(); + //CameraManager.StopHardwareTriggerSimulator(); // 停止相机采集 CameraManager.StopAll(); @@ -150,10 +153,11 @@ namespace Check.Main.UI // 启动硬触发模拟器(如果需要) var config = ConfigurationManager.GetCurrentConfig(); - if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Software)) + if (config.CameraSettings.Any(c => c.IsEnabled && c.TriggerMode == TriggerModeType.Hardware)) { - CameraManager.TriggerInterval = 100; - CameraManager.StartHardwareTriggerSimulator(); + ThreadSafeLogger.Log("相机设置为硬件触发模式,将由 PLC 输出脉冲信号控制拍照。"); + //CameraManager.TriggerInterval = 100; + // CameraManager.StartHardwareTriggerSimulator(); } // 开始统计 diff --git a/Check.Main/UI/FormControlPanel.resx b/Check.Main/UI/FormControlPanel.resx index 1af7de1..4fd7bf1 100644 --- a/Check.Main/UI/FormControlPanel.resx +++ b/Check.Main/UI/FormControlPanel.resx @@ -1,17 +1,17 @@  - @@ -117,4 +117,137 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 2 + + + + Fill + + + + 宋体, 12pt + + + 6, 51 + + + + 6, 6, 6, 6 + + + 2, 2 + + + 410, 128 + + + 0 + + + 启动设备 + + + btnStartDevice + + + Sunny.UI.UIButton, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb + + + uiTableLayoutPanel1 + + + 0 + + + Fill + + + 宋体, 12pt + + + 6, 221 + + + 6, 6, 6, 6 + + + 2, 2 + + + 410, 122 + + + 1 + + + 开始检测 + + + btnStartCheck + + + Sunny.UI.UIButton, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb + + + uiTableLayoutPanel1 + + + 1 + + + Fill + + + 0, 0 + + + 6, 6, 6, 6 + + + 5 + + + 422, 396 + + + 0 + + + uiTableLayoutPanel1 + + + Sunny.UI.UITableLayoutPanel, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb + + + $this + + + 0 + + + <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="btnStartDevice" Row="1" RowSpan="1" Column="0" ColumnSpan="2" /><Control Name="btnStartCheck" Row="3" RowSpan="1" Column="0" ColumnSpan="2" /></Controls><Columns Styles="Percent,50,Percent,50" /><Rows Styles="Percent,11.38585,Percent,35.35353,Percent,7.575758,Percent,33.83838,Percent,11.38585" /></TableLayoutSettings> + + + True + + + 11, 24 + + + 422, 396 + + + 6, 6, 6, 6 + + + 启动管理 + + + FormControlPanel + + + WeifenLuo.WinFormsUI.Docking.DockContent, WeifenLuo.WinFormsUI.Docking, Culture=neutral, PublicKeyToken=5cded1a1a0a7b481 + \ No newline at end of file