一、核心概念术语说明视觉引导通过视觉检测结果引导机械手/PLC 进行精确定位和操作九点标定使用 9 个点对建立像素坐标到世界坐标的映射关系旋转中心拟合通过 3 个旋转位置拟合圆心确定机械旋转中心偏移量计算视觉检测位置与基准位置的差值用于引导机械补偿VPP 工程文件VisionPro 的序列化配置文件包含工具链配置单例模式确保全局只有一个实例的设计模式用于 Vision/PLC/Config 类INI 配置文件使用 kernel32.dll P/Invoke 读写的参数配置文件CogCalibNPointToNPointN 点到 N 点标定工具用于坐标系统转换CogFitCircleTool拟合圆工具用于旋转中心计算二、常用操作1. 单例视觉管理类public class Vision { // LazyT 延迟初始化首次访问 .Value 时才创建实例自动线程安全 private static readonly LazyVision _instance new LazyVision(() new Vision()); public static Vision Instance { get { return _instance.Value; } } public const string IdentificationTbPath VPP/Identification_tb.vpp; // 识别工具块 public const string NPointCalibrationTbPath VPP/NPointCalibration_tb.vpp; // 标定工具块 public CogToolBlock IdentificationTb { get; private set; } public CogToolBlock NPointCalibrationTb { get; private set; } // 从 ToolBlock 工具列表中找到第一个 CogAcqFifoTool返回其采集接口 public ICogAcqFifo IdentificationFifo { get { return IdentificationTb.Tools.OfTypeCogAcqFifoTool().First().Operator; } } // 异步加载 VPP放到子线程避免阻塞 UI 线程 public Task LoadVpp() { return Task.Run(() { IdentificationTb CogSerializer.LoadObjectFromFile(IdentificationTbPath) as CogToolBlock; NPointCalibrationTb CogSerializer.LoadObjectFromFile(NPointCalibrationTbPath) as CogToolBlock; }); } // 保存配置 public void Save(CogToolBlock tb, string path) { CogSerializer.SaveObjectToFile(tb, path); } }2. 单例 PLC 操作类西门子public class PlcOperator { private static readonly LazyPlcOperator _instance new LazyPlcOperator(() new PlcOperator()); public static PlcOperator Instance { get { return _instance.Value; } } private Plc plc null; private Timer timer null; // PLC 地址常量 public const string PosXAddr DB50.DBD20; public const string PosYAddr DB50.DBD24; public const string MeasureNoAddr DB50.DBD4; public const string OffsetRAddr DB51.DBD0; public const string OffsetXAddr DB51.DBD4; public const string OffsetYAddr DB51.DBD8; // 事件测量拍照编号变化 public event Actionint MeasureNoChange; private int measureNo; public float PosX { get; private set; } public float PosY { get; private set; } private PlcOperator() { timer new System.Timers.Timer(240); // 240ms 轮询一次 PLC平衡实时性与 CPU 占用 timer.Elapsed Timer_Elapsed; } // 定时读取 PLC 数据运行在定时器线程更新 UI 时需 BeginInvoke private void Timer_Elapsed(object sender, ElapsedEventArgs e) { // DB50.DBD20: DB块号 50, DBDDouble Word20字节偏移 // PLC 将 float 按 IEEE754 存入 uintConvertToFloat() 为 S7.Net 的扩展方法 PosX ((uint)plc.Read(PosXAddr)).ConvertToFloat(); PosY ((uint)plc.Read(PosYAddr)).ConvertToFloat(); int no (int)(uint)plc.Read(MeasureNoAddr); if (no ! 0 no ! measureNo) { measureNo no; MeasureNoChange?.Invoke(no); } } // 异步连接 PLC public async Task Connect(CpuType cpu, string ip, short rack, short slot) { plc new Plc(cpu, ip, rack, slot); await plc.OpenAsync(); timer.Start(); } // 写入偏移量到 PLC public float OffsetR { set { plc.Write(OffsetRAddr, value); } } public float OffsetX { set { plc.Write(OffsetXAddr, value); } } public float OffsetY { set { plc.Write(OffsetYAddr, value); } } }3. 单例配置管理类public class GlobalConfig { private static GlobalConfig instance; private static readonly object obj new object(); // 双检锁单例Double-Check Locking线程安全且开销低于 LazyT public static GlobalConfig Instance { get { if (instance null) // 第一次检查减少加锁开销 { lock (obj) // 加锁确保安全 { if (instance null) { instance new GlobalConfig(); } // 第二次检查 } } return instance; } } const string INI_FILE_PATH Config.ini; Ini ini; public float BaseX { get; set; } public float BaseY { get; set; } public float BaseA { get; set; } public float RotateCenterX { get; set; } public float RotateCenterY { get; set; } public string PlcIp { get; set; } private GlobalConfig() { ini new Ini(Path.GetFullPath(INI_FILE_PATH)); // GetValue(键名, Section名, 默认值) —— 对应 INI [PointConfig] BaseX0 BaseX Convert.ToSingle(ini.GetValue(BaseX, PointConfig, 0)); BaseY Convert.ToSingle(ini.GetValue(BaseY, PointConfig, 0)); PlcIp ini.GetValue(PlcIp, PlcConfig, 0.0.0.0); } // 析构函数对象被 GC 回收前自动调用确保退出前配置写回磁盘 ~GlobalConfig() { ini.WriteValue(BaseX, PointConfig, BaseX.ToString()); ini.WriteValue(PlcIp, PlcConfig, PlcIp); ini.Save(); } }4. 九点标定实现public class NPointCalibrationFrm : Form { CogCalibNPointToNPoint calib; // 初始化从 ToolBlock 获取标定工具 public NPointCalibrationFrm() { calib (vision.IdentificationTb.Tools[CogCalibNPointToNPointTool1] as CogCalibNPointToNPointTool).Calibration; } // 订阅前先取消订阅防止多次调用导致重复执行 // plcOperator.NPointCalibrationNoChange - NPointCalibrationNoChange; // plcOperator.NPointCalibrationNoChange NPointCalibrationNoChange; // 订阅 PLC 九点到位事件 private async void NPointCalibrationNoChange(int no) { // 1. 等待机械到位稳定 await Task.Delay(1000); // 2. CogToolBlock.Run() 必须在 UI 线程上执行否则报线程安全异常 Invoke(new Action(() vision.NPointCalibrationTb.Run())); // 3. 获取像素坐标Run 完成后可在任意线程读取输出 double x (double)vision.NPointCalibrationTb.Outputs[X].Value; double y (double)vision.NPointCalibrationTb.Outputs[Y].Value; // 4. 添加点对像素坐标 机械坐标 calib.AddPointPair(x, y, plcOperator.PosX, plcOperator.PosY); // 5. 通知 PLC 继续下一步 plcOperator.NPointCalibrationCheckNo no; // 6. 达到 9 点后执行标定 if (no 9) { calib.Calibrate(); if (calib.Calibrated) { // ComputedRMSError均方根重投影误差越小越准确 // 建议阈值 30超出则说明标定点有误差需清除后重新标定 if (calib.ComputedRMSError 30) { MessageBox.Show($标定误差过大{calib.ComputedRMSError:0.00}建议重新标定); return; } vision.Save(vision.IdentificationTb, Vision.IdentificationTbPath); } } } // 清除所有标定点对 private void button1_Click(object sender, EventArgs e) { for (int i calib.NumPoints - 1; i -1; i--) { calib.DeletePointPair(i); } } }5. 旋转中心拟合public class FindRotateCenterFrm : Form { float x1, y1, x2, y2, x3, y3, cx, cy; // 订阅 PLC 旋转到位事件3 次 private void PLC_CenterCalibrationNoChange(int num) { vision.IdentificationTb.Run(); float x Convert.ToSingle(vision.IdentificationTb.Outputs[X].Value); float y Convert.ToSingle(vision.IdentificationTb.Outputs[Y].Value); if (num 1) { x1 x; y1 y; } else if (num 2) { x2 x; y2 y; } else if (num 3) { x3 x; y3 y; // 三点拟合圆心 calcCircleCenter(x1, y1, x2, y2, x3, y3, out cx, out cy); } // 通知 PLC 继续旋转 plcOperator.CenterCalibrationCheckNo num; } // 三点拟合圆心算法 private void calcCircleCenter(float x1, float y1, float x2, float y2, float x3, float y3, out float x, out float y) { // 三点共线时 (a*d - b*c)0会导致除零结果为 NaN三次旋转角度需足够大 double a x1 - x2; // 联立方程组系数 double b y1 - y2; double c x1 - x3; double d y1 - y3; double e ((x1 * x1 - x2 * x2) - (y2 * y2 - y1 * y1)) / 2; // 常数项 double f ((x1 * x1 - x3 * x3) - (y3 * y3 - y1 * y1)) / 2; // 克拉默法求解线性方程组得圈心坐标 x (float)((e * d - b * f) / (a * d - b * c)); y (float)((a * f - e * c) / (a * d - b * c)); } // 保存旋转中心到配置 private void button1_Click(object sender, EventArgs e) { globalConfig.RotateCenterX cx; globalConfig.RotateCenterY cy; } }6. 主窗体检测流程public partial class Form1 : Form { Vision vision Vision.Instance; PlcOperator plcOperator PlcOperator.Instance; GlobalConfig globalConfig GlobalConfig.Instance; private async void Form1_Load(object sender, EventArgs e) { await vision.LoadVpp(); await plcOperator.Connect(CpuType.S71200, globalConfig.PlcIp, 0, 1); // 监听测量到位事件 plcOperator.MeasureNoChange testing; } // 检测触发PLC 发信号 → 拍照 → 检测 → 回传偏移量 private void testing(int num) { vision.IdentificationTb.Run(); // SubRecords 键名格式为 工具名.InputImage/.OutputImage // 具体名称在 QuickBuild 中鼠标悬停 Record 时可查看 cogIdentificationDisplay.Record vision.IdentificationTb .CreateLastRunRecord().SubRecords[CogCalibNPointToNPointTool1.OutputImage]; if (num 1) { // 第一次拍照计算角度偏移 float r Convert.ToSingle(vision.IdentificationTb.Outputs[Angle].Value); r (float)(180 / Math.PI) * r; // 弧度转角度 float offsetR -(globalConfig.BaseA - r); plcOperator.OffsetR offsetR; } if (num 2) { // 第二次拍照计算位置偏移 float x Convert.ToSingle(vision.IdentificationTb.Outputs[X].Value); float y Convert.ToSingle(vision.IdentificationTb.Outputs[Y].Value); float offsetX globalConfig.BaseX - x; float offsetY globalConfig.BaseY - y; plcOperator.OffsetX offsetX; plcOperator.OffsetY offsetY; } // 通知 PLC 检测完成 plcOperator.MeasureCheckNo num; } // 实时显示 private void startLiveBtn_Click(object sender, EventArgs e) { cogLiveDIsplay.StartLiveDisplay(vision.IdentificationFifo); } // 释放相机 private void Form1_FormClosing(object sender, FormClosingEventArgs e) { vision.IdentificationFifo?.FrameGrabber?.Disconnect(false); } }7. 项目架构总览引导项目架构 ├── Common/ │ ├── Vision.cs # 视觉工具管理单例 │ ├── PlcOperator.cs # PLC 通信管理单例 │ ├── GlobalConfig.cs # 全局配置管理单例 │ └── Ini.cs # INI 文件读写 ├── VPP/ │ ├── Identification_tb.vpp # 识别 ToolBlock │ └── NPointCalibration_tb.vpp # 标定 ToolBlock ├── Frm/ │ ├── Form1.cs # 主窗体检测流程 │ ├── NPointCalibrationFrm.cs # 九点标定窗体 │ ├── FindRotateCenterFrm.cs # 旋转中心拟合窗体 │ ├── CogFifoEditFrm.cs # 相机编辑窗体 │ └── CogToolBlockEditFrm.cs # ToolBlock 编辑窗体 └── Config.ini # 配置文件三、问题排查错误1VPP 加载失败现象CogSerializer.LoadObjectFromFile()异常原因VPP 路径错误或版本不兼容解决确认 VPP 文件存在于bin/Debug/VPP/目录检查 VisionPro 版本是否与创建 VPP 时一致在 QuickBuild 中重新生成 VPP错误2九点标定 RMS 误差过大现象calib.ComputedRMSError 30原因标定点不准确或分布不合理解决确保 9 个点均匀分布在视野范围内检查机械坐标的精度重新标定清除旧点对错误3PLC 数据读取异常现象读取的坐标值为 0 或异常大原因地址格式错误或数据类型不匹配解决使用ConvertToFloat()扩展方法转换 uint 到 float确认 DB 块地址与 PLC 程序一致检查 PLC 的 DB 块是否已下载错误4跨线程 UI 访问异常现象InvalidOperationException: 线程间操作无效原因在 PLC 定时器线程中直接操作 UI 控件解决BeginInvoke(new Action(() { // UI 操作代码 }));错误5旋转中心拟合失败现象三点拟合圆心结果为 NaN原因三个点共线或距离太近解决确保三次旋转角度间隔足够大建议 120°检查识别结果是否准确增加await Task.Delay(1000)等待机械稳定四、相关资源官方文档Cognex VisionPro Documentation