This commit is contained in:
2025-10-20 14:47:17 +08:00
parent 2e46747ba9
commit 546b894e6b
16 changed files with 917 additions and 141 deletions

View File

@@ -24,34 +24,32 @@ namespace Check.Main.Camera
/// </summary>
public static class CameraManager
{
// 活动的相机实例字典,键为相机名称
//1、相机与UI管理----管理每台相机对象和对应的图像显示窗口。
// 活动的相机实例字典(键值对),键 (string):使用相机的名称,值 (HikvisionCamera):存储实际的相机实例,{ get; } 创建了一个只读的公共属性
public static Dictionary<string, HikvisionCamera> ActiveCameras { get; } = new Dictionary<string, HikvisionCamera>();
// 相机对应的图像显示窗口字典
//public static Dictionary<string, FormImageDisplay> CameraDisplays { get; } = new Dictionary<string, FormImageDisplay>();
public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();
public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();
public static Dictionary<string, FormImageDisplay> OriginalImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//原始图像窗口
public static Dictionary<string, FormImageDisplay> ResultImageDisplays { get; } = new Dictionary<string, FormImageDisplay>();//结果图像窗口
// --- 多相机同步逻辑 ---
// 产品检测队列
//2、多相机同步逻辑
// 【队列】一个产品需要多台相机拍完,才算完整。
private static readonly Queue<ProductResult> ProductQueue = new Queue<ProductResult>();
// 队列锁,保证线程安全
// 【(队列)锁】保证队列在多线程安全
private static readonly object QueueLock = new object();
// 当前启用的相机数量,用于判断产品是否检测完毕
private static int EnabledCameraCount = 0;
//public static event EventHandler<DetectionResultEventArgs> 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;
/// <summary>
/// 获取或设置模拟硬触发的间隔时间(毫秒)。
/// </summary>
@@ -221,12 +219,12 @@ namespace Check.Main.Camera
// }
//}
//********************初始化和启动流程*******************
/// <summary>
/// 准备所有相机硬件、UI窗口和后台处理器但不开始采集。
/// 这是“启动设备”的第一阶段。
/// </summary>
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
/// <summary>
/// 根据配置列表初始化或更新所有相机
/// 类似 PrepareAll但更侧重于更新配置会检查物理设备数量不足时报 warning。
/// </summary>
public static void Initialize(ProcessConfig config, FrmMain mainForm)
{
@@ -398,7 +397,7 @@ namespace Check.Main.Camera
}
/// <summary>
/// 定时器触发事件。
/// 定时器触发事件。(定时器 OnHardwareTriggerTimerElapsed 会遍历相机,对处于软件触发模式的相机执行 SoftwareTrigger()。)
/// </summary>
private static void OnHardwareTriggerTimerElapsed(object sender, ElapsedEventArgs e)
{
@@ -490,7 +489,9 @@ namespace Check.Main.Camera
//}
// Shutdown 方法也简化
/// <summary>
/// 停止所有相机 + 关闭窗口 + 清空字典 + 关闭检测协调器 + 销毁模型
/// </summary>
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
/// <summary>
/// 【将 SkiaSharp.SKImage 安全地转换为 System.Drawing.Bitmap。
/// </summary>
private static Bitmap ConvertSKImageToBitmap(SKImage skImage)
public static Bitmap ConvertSKImageToBitmap(SKImage skImage)
{
if (skImage == null) return null;

View File

@@ -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));
}
/// <summary>
/// 图像处理主循环
/// </summary>
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<string>
{
@"D:\HalconTemplateMatch\train2\logo1.bmp",
@"D:\HalconTemplateMatch\train2\logo2.bmp",
@"D:\HalconTemplateMatch\train2\logo3.bmp"
},
@"D:\HalconTemplateMatch\model_2");
//训练阶段相机39.25修改!!
trainer.TrainAndSaveTemplates(
new List<string>
{
@"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}");
}
}
}
/// <summary>
/// 将 System.Drawing.Bitmap 安全地转换为 SkiaSharp.SKImage。
/// </summary>

View File

@@ -30,7 +30,7 @@ namespace Check.Main.Camera
}
/// <summary>
/// 相机配置信息类用于PropertyGrid显示和编辑
/// 相机配置信息类CameraSettings用于PropertyGrid显示和编辑(****核心!****)
/// </summary>
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;

View File

@@ -179,7 +179,9 @@ namespace Check.Main.Camera
}
// 默认设置为连续模式
SetContinuousMode();
//SetContinuousMode();
// 设置为硬触发模式Line0
SetTriggerMode(false);
IsOpen = true;
OnCameraMessage("相机打开成功。", 0);

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
@@ -10,11 +10,14 @@
<PropertyGroup>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DockPanelSuite" Version="3.1.1" />
<PackageReference Include="DockPanelSuite.ThemeVS2015" Version="3.1.1" />
<PackageReference Include="HalconDotNet" Version="19.11.0" />
<PackageReference Include="HslCommunication" Version="7.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NPOI" Version="2.7.4" />
<PackageReference Include="OpenCvSharp4" Version="4.10.0.20241108" />
@@ -26,11 +29,17 @@
</ItemGroup>
<ItemGroup>
<Reference Include="halcondotnet">
<HintPath>C:\Program Files\MVTec\HALCON-10.0\bin\dotnet35\halcondotnet.dll</HintPath>
</Reference>
<Reference Include="hdevenginedotnet">
<HintPath>C:\Program Files\MVTec\HALCON-10.0\bin\dotnet35\hdevenginedotnet.dll</HintPath>
</Reference>
<Reference Include="HslCommunication">
<HintPath>..\..\..\HslCommunication-master\HslCommunication.dll</HintPath>
</Reference>
<Reference Include="MvCameraControl.Net">
<HintPath>C:\Program Files (x86)\MVS\Development\DotNet\win64\MvCameraControl.Net.dll</HintPath>
<HintPath>..\..\..\..\XKRS2025\XKRS2025\CheckDevice\Check.Main\bin\Debug\MvCameraControl.Net.dll</HintPath>
</Reference>
</ItemGroup>

View File

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

View File

@@ -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<HTuple> modelHandles = new List<HTuple>();
// /// <summary>
// /// 从文件加载多个模板
// /// </summary>
// 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}");
// }
// }
// /// <summary>
// /// 在测试图像中查找 Logo
// /// </summary>
// /// <returns>true = OKfalse = NG</returns>
// 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; // 没找到
// }
// /// <summary>
// /// 重载FindLogo函数double返回
// /// </summary>
// /// <param name="bmp"></param>
// /// <returns></returns>
// 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 = 没找到
// }
// /// <summary>
// /// Bitmap 转 Halcon HObject
// /// </summary>
// 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<HTuple> modelHandles = new List<HTuple>();
/// <summary>
/// 从指定目录加载所有 .shm 模板文件
/// </summary>
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}");
}
}
/// <summary>
/// 匹配并返回最高得分double返回
/// </summary>
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;
}
/// <summary>
/// Bitmap 转 Halcon HObject
/// </summary>
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);
}
}
}
}

View File

@@ -0,0 +1,62 @@
using HalconDotNet;
using System;
using System.Collections.Generic;
using System.IO;
namespace HalconTemplateMatch
{
public class LogoTemplateTrainer
{
/// <summary>
/// 从多张样本图像生成模板并保存到文件
/// </summary>
/// <param name="imagePaths">训练图片路径集合</param>
/// <param name="saveDir">模板保存目录</param>
public void TrainAndSaveTemplates(List<string> 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++;
}
}
}
}

View File

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

View File

@@ -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
{
// 如果删除的不是当前产品,我们仍然需要触发一次事件,

View File

@@ -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 = "生产统计" };

View File

@@ -127,5 +127,6 @@ namespace Check.Main.Infer
{
return this.MemberwiseClone(); // 浅克隆对于这个类足够了
}
}
}

150
Check.Main/Process_Img.cs Normal file
View File

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

View File

@@ -28,79 +28,54 @@
/// </summary>
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);
}

View File

@@ -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();
}
// 开始统计

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
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
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>
@@ -26,36 +26,36 @@
<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
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
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
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
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
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
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
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -117,4 +117,137 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="mscorlib" name="mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="uiTableLayoutPanel1.ColumnCount" type="System.Int32, mscorlib">
<value>2</value>
</data>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="btnStartDevice.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="btnStartDevice.Font" type="System.Drawing.Font, System.Drawing">
<value>宋体, 12pt</value>
</data>
<data name="btnStartDevice.Location" type="System.Drawing.Point, System.Drawing">
<value>6, 51</value>
</data>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="btnStartDevice.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>6, 6, 6, 6</value>
</data>
<data name="btnStartDevice.MinimumSize" type="System.Drawing.Size, System.Drawing">
<value>2, 2</value>
</data>
<data name="btnStartDevice.Size" type="System.Drawing.Size, System.Drawing">
<value>410, 128</value>
</data>
<data name="btnStartDevice.TabIndex" type="System.Int32, mscorlib">
<value>0</value>
</data>
<data name="btnStartDevice.Text" xml:space="preserve">
<value>启动设备</value>
</data>
<data name="&gt;&gt;btnStartDevice.Name" xml:space="preserve">
<value>btnStartDevice</value>
</data>
<data name="&gt;&gt;btnStartDevice.Type" xml:space="preserve">
<value>Sunny.UI.UIButton, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb</value>
</data>
<data name="&gt;&gt;btnStartDevice.Parent" xml:space="preserve">
<value>uiTableLayoutPanel1</value>
</data>
<data name="&gt;&gt;btnStartDevice.ZOrder" xml:space="preserve">
<value>0</value>
</data>
<data name="btnStartCheck.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="btnStartCheck.Font" type="System.Drawing.Font, System.Drawing">
<value>宋体, 12pt</value>
</data>
<data name="btnStartCheck.Location" type="System.Drawing.Point, System.Drawing">
<value>6, 221</value>
</data>
<data name="btnStartCheck.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>6, 6, 6, 6</value>
</data>
<data name="btnStartCheck.MinimumSize" type="System.Drawing.Size, System.Drawing">
<value>2, 2</value>
</data>
<data name="btnStartCheck.Size" type="System.Drawing.Size, System.Drawing">
<value>410, 122</value>
</data>
<data name="btnStartCheck.TabIndex" type="System.Int32, mscorlib">
<value>1</value>
</data>
<data name="btnStartCheck.Text" xml:space="preserve">
<value>开始检测</value>
</data>
<data name="&gt;&gt;btnStartCheck.Name" xml:space="preserve">
<value>btnStartCheck</value>
</data>
<data name="&gt;&gt;btnStartCheck.Type" xml:space="preserve">
<value>Sunny.UI.UIButton, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb</value>
</data>
<data name="&gt;&gt;btnStartCheck.Parent" xml:space="preserve">
<value>uiTableLayoutPanel1</value>
</data>
<data name="&gt;&gt;btnStartCheck.ZOrder" xml:space="preserve">
<value>1</value>
</data>
<data name="uiTableLayoutPanel1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms">
<value>Fill</value>
</data>
<data name="uiTableLayoutPanel1.Location" type="System.Drawing.Point, System.Drawing">
<value>0, 0</value>
</data>
<data name="uiTableLayoutPanel1.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>6, 6, 6, 6</value>
</data>
<data name="uiTableLayoutPanel1.RowCount" type="System.Int32, mscorlib">
<value>5</value>
</data>
<data name="uiTableLayoutPanel1.Size" type="System.Drawing.Size, System.Drawing">
<value>422, 396</value>
</data>
<data name="uiTableLayoutPanel1.TabIndex" type="System.Int32, mscorlib">
<value>0</value>
</data>
<data name="&gt;&gt;uiTableLayoutPanel1.Name" xml:space="preserve">
<value>uiTableLayoutPanel1</value>
</data>
<data name="&gt;&gt;uiTableLayoutPanel1.Type" xml:space="preserve">
<value>Sunny.UI.UITableLayoutPanel, SunnyUI, Culture=neutral, PublicKeyToken=27d7d2e821d97aeb</value>
</data>
<data name="&gt;&gt;uiTableLayoutPanel1.Parent" xml:space="preserve">
<value>$this</value>
</data>
<data name="&gt;&gt;uiTableLayoutPanel1.ZOrder" xml:space="preserve">
<value>0</value>
</data>
<data name="uiTableLayoutPanel1.LayoutSettings" type="System.Windows.Forms.TableLayoutSettings, System.Windows.Forms">
<value>&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;TableLayoutSettings&gt;&lt;Controls&gt;&lt;Control Name="btnStartDevice" Row="1" RowSpan="1" Column="0" ColumnSpan="2" /&gt;&lt;Control Name="btnStartCheck" Row="3" RowSpan="1" Column="0" ColumnSpan="2" /&gt;&lt;/Controls&gt;&lt;Columns Styles="Percent,50,Percent,50" /&gt;&lt;Rows Styles="Percent,11.38585,Percent,35.35353,Percent,7.575758,Percent,33.83838,Percent,11.38585" /&gt;&lt;/TableLayoutSettings&gt;</value>
</data>
<metadata name="$this.Localizable" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<data name="$this.AutoScaleDimensions" type="System.Drawing.SizeF, System.Drawing">
<value>11, 24</value>
</data>
<data name="$this.ClientSize" type="System.Drawing.Size, System.Drawing">
<value>422, 396</value>
</data>
<data name="$this.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms">
<value>6, 6, 6, 6</value>
</data>
<data name="$this.Text" xml:space="preserve">
<value>启动管理</value>
</data>
<data name="&gt;&gt;$this.Name" xml:space="preserve">
<value>FormControlPanel</value>
</data>
<data name="&gt;&gt;$this.Type" xml:space="preserve">
<value>WeifenLuo.WinFormsUI.Docking.DockContent, WeifenLuo.WinFormsUI.Docking, Culture=neutral, PublicKeyToken=5cded1a1a0a7b481</value>
</data>
</root>