C#与HALCON联合开发机器视觉检测系统:架构、模块与验收实践
简介:围绕C#与HALCON构建的机器视觉检测系统,是一套面向机器视觉初学者与中高级开发者的完整工程源码,聚焦工件表面质量检测场景。系统按图像采集、预处理、特征提取、决策反馈和图形界面等模块组织,展示相机取图、去噪增强、形状与模板匹配、缺陷判定及报警联动的实现路径,适合用于课程实践、项目原型验证和二次开发参考。压缩包共245个文件、约45.48MB,以80个C#源码文件为核心,配有35张BMP测试图像、29个DLL与23个PDB依赖调试文件,以及10个resx/resources界面资源、6个csproj工程文件、3个exe和sln解决方案等,目录结构较完整,便于直接打开工程梳理调用关系。已有7066人学习下载。读者可据此理解C#调用HALCON算子、视觉流程编排、检测结果展示与工程部署方式,并借助测试图复现实验、定位问题,积累可迁移的机器视觉系统开发经验。
1. 一条外观检测线的真实取舍:C# 与 HALCON 到底谁管哪一段
某产线要做零件外观检测,最初的想法很朴素:用 C# 把相机采到的图读成 Bitmap,自己写阈值、找轮廓、算尺寸。真跑到现场就卡住了——换一批来料光照差两档,阈值全废;亚像素定位没有,测量值跳动比公差还大;模板匹配要从头造轮子。后来改成 C# 写上位机、HALCON 做算法内核,机器视觉里最费劲的那部分交给成熟算子库,视觉检测系统里界面、流程、数据、报警、配方这些工程活留在 C# 里。这份 C# 与 HALCON 联合开发的机器视觉软件系统源代码,就是按这个分工搭起来的完整视觉检测系统:相机采集、模板定位、尺寸测量、缺陷判定、结果落盘、配方切换,一条链走完。它适合正在做机器视觉方向毕设或课程设计的学生,也适合要把一套能跑的 C# 视觉软件直接搬到自己设备上改的工程师。
2. 需求拆解与选型:把「能看见缺陷」翻译成节拍、精度和软件栈
2.1 检测项怎么落成可量化的指标
需求沟通里最容易翻车的一句话是「要能检出划伤」。这句话没法验收,也没法写代码。负责的做法是先把它拆成四列:测什么、用什么成像方式测、精度要到多少、判定阈值是多少。检测项一般分三类——尺寸类(长度、直径、间距、角度)、位置类(装配偏移、缺件、错件)、表面类(划伤、脏污、毛刺、压痕)。三类在算法上的实现路径完全不同,尺寸类靠测量工具,位置类靠模板匹配加区域比对,表面类多半要靠 Blob 分析、频域滤波或者差异图。
精度这一栏要用像素当量倒推,不能拍脑袋。视场 40mm、相机 2448×2048,像素当量约 0.016mm/pixel;HALCON 的亚像素边缘通常能做到 1/4 像素以内,折算下来重复性在 0.004mm 量级。所以一个标称 ±0.02mm 的尺寸项,在这个配置下是有余量的;要是公差要求 ±0.005mm,那就该换远心镜头、提高分辨率或者上背光,而不是指望算法硬扛。我一般会先算这笔账再谈选型,省得后面相机都装好了才发现精度不够。
| 检测项 | 成像方式 | 精度要求 | 判定方式 |
|---|---|---|---|
| 外形长宽 | 背光、远心镜头 | ±0.02mm | 卡尺工具测边缘对距离 |
| 孔位偏移 | 同轴光 | ±0.03mm | 模板匹配定位后测圆心 |
| 表面划伤 | 低角度环形光 | 最小 0.1mm×2mm | Blob 面积+长宽比筛选 |
| 缺件/错件 | 正面均匀光 | 有无判断 | 区域灰度均值与模板比对 |
| 装配间隙 | 侧向条形光 | ±0.05mm | 测量矩形内双边缘间距 |
节拍是另一条硬约束。单件 300ms 的产线,一次曝光加传输大概吃掉 60~80ms,算法预算只剩 200ms 出头,还得留出通讯和界面刷新的余量。所以算法流程要做成「先粗后精」:先用降采样图像做快速定位,把 ROI 缩到工件附近,再做全分辨率的测量和判定。这套系统的流程编排就是按这个思路排的,每个工具只在自己关心的 ROI 里跑,不整图扫。
2.2 C# 做上位机、HALCON 做算法核的分层依据
有人会问,HDevelop 也能拖界面,为什么还要 C#?答案是工程化。产线上的软件不只是跑算法,它要连 PLC、要存追溯数据、要做用户权限、要支持十几个料号的配方切换、要能在客户现场远程看日志。这些事 HDevelop 做得吃力,.NET 生态里是现成的。反过来,纯 C# 加 OpenCVSharp 也能做视觉,但形状模板匹配的抗遮挡能力、亚像素边缘的稳定性、标定工具链的完整度,调起来的隐性成本很高。
所以分工线画在这里:C# 负责「壳」——界面、流程调度、多线程、数据库、通讯协议、配置管理、日志与报警;HALCON 负责「眼」——图像预处理、模板匹配、边缘提取、测量、Blob 分析、标定换算。两边通过 HALCON 的 .NET 接口(HObject / HTuple)交接,边界清晰。这条线画对了,后面加检测项就是加一个工具类,不用动主流程。
分层还有个现实好处:算法参数可以外置成配方,工艺工程师改参数不用重新编译。这套系统里所有阈值、ROI 偏移、匹配分数下限、灰度上下限都放在配方文件里,程序启动时加载,切换料号时热更新。
2.3 算子与相机选型的对照取舍
选型时最常纠结的几组,我把自己会怎么选列出来:
| 场景 | 常见选择 A | 常见选择 B | 我一般选 |
|---|---|---|---|
| 工件有旋转、部分遮挡 | NCC 灰度匹配 | 形状模板匹配 | B,对光照变化和遮挡更稳 |
| 边缘位置测量 | 手写梯度找极值 | measure_pos 测量工具 | B,亚像素和抗噪现成 |
| 表面缺陷提取 | 固定阈值 Blob | 动态阈值 + 形态学 | B,适应光照漂移 |
| 大幅面高速采集 | USB3 面阵 | GigE 面阵 | 看线长,超过 5m 用 GigE |
| 精密尺寸 | 普通 FA 镜头 | 双远心镜头 | B,视场内的放大倍率一致性更好 |
相机接口这块,GigE 的优点是线缆长、多相机组网方便,缺点是延迟比 USB3 略高;USB3 带宽足、延迟低,但线长限制在 3~5m,工业现场走线要提前规划。这套系统里采集层做了接口抽象,换相机只改一个配置项,算法层完全不用动——这也是分层带来的直接收益。
提示:选型阶段就把「精度、节拍、视场、景深」四个数写进需求文档,后面所有争论都回到这四个数上,比争论用哪个算子有效得多。
3. 总体架构设计:三层结构与六个模块的职责边界
3.1 表现层、业务层、算法层的划分
整套软件按三层切。表现层是 WinForm 或 WPF 的主界面,负责实时图像显示、结果列表、参数编辑、报警提示,它不碰任何 HALCON 算子,只消费业务层给出来的结果对象。业务层是流程引擎和调度中心,管着「取图 → 定位 → 测量 → 判定 → 存档 → 通讯」这条流水线,同时管配方加载、权限校验、异常上报。算法层是 HALCON 的封装,每个检测能力包装成一个工具类,对外只暴露「输入图像 + 参数 → 输出测量值」这样的签名。
这样切的好处是测试好做。算法层的每个工具可以单独拿一批离线图片跑回归,业务层可以用假的图像源做流程测试,界面层改动不影响算法正确性。做毕设答辩时这一点特别加分——老师问「你怎么验证算法是对的」,你能拿出分层测试的记录,比说「我跑了一遍看着没问题」有说服力得多。
六个模块分别是:设备接入(相机、光源、PLC/IO)、图像预处理、定位与测量、判定与分类、数据与追溯、配方与权限。每个模块内部的类只依赖下一层的接口,不允许跨层直接调。比如判定模块不能自己去读相机,它只接收测量模块的结果数组。
3.2 图像从相机到算法再到数据库的流转通路
高节拍下最容易出问题的是图像流转。做法是采集与处理分离:相机回调线程只做一件事——把 HObject 塞进一个有界队列,然后立刻返回,绝不在回调里跑算法。算法线程从队列里取图处理,处理完把结果对象推到结果队列,界面线程和存盘线程各自消费。队列要有界,满了就丢最旧的一帧并记一条丢帧日志,避免内存无限堆积。
// 采集线程与算法线程之间的有界缓冲,容量按节拍和算法耗时估算 private readonly BlockingCollection<HObject> _imageQueue = new BlockingCollection<HObject>(8); // 相机回调:只负责入队,禁止在这里做任何耗时运算 private void OnImageGrabbed(HObject image) { HObject copy; HOperatorSet.CopyImage(image, out copy); // 回调缓冲会被复用,必须拷贝 if (!_imageQueue.TryAdd(copy)) { // 队列满说明算法跟不上节拍,丢弃并计数,事后看日志定位瓶颈 copy.Dispose(); Interlocked.Increment(ref _dropCount); } } // 算法线程主循环 private void ProcessLoop() { foreach (var img in _imageQueue.GetConsumingEnumerable(_cts.Token)) { using (img) { var result = _pipeline.Run(img); // 内部依次调用各工具 _resultQueue.Add(result); } } }这里的关键点是CopyImage。很多相机 SDK 的回调缓冲区是复用的,你把引用直接传出去,下一帧到来时图像内容就被覆盖了,现象是「偶尔检测到上一件的缺陷」,这种问题查起来极其折磨。拷贝一次的代价远小于排查成本。队列容量 8 是按「算法单帧 80ms、节拍 300ms」估的,实际项目里按实测的算法耗时乘 2~3 倍来定,太大反而掩盖性能问题。
3.3 模块之间的接口约定与异常传递
工具层统一接口,是为了让流程配置化。每个检测工具实现同一个契约,流程引擎按配方里配置的顺序依次执行:
public interface IVisionTool { string Name { get; } // 工具标识,配方里按名字引用 ToolResult Execute(HObject image, JObject param); // param 来自配方文件的节点 } public class ToolResult { public bool Success { get; set; } // 算法是否正常执行完 public int ErrorCode { get; set; } // 非 0 表示执行失败,界面按码查提示 public double[] Values { get; set; } // 测量值数组,单位统一为毫米 public bool IsOk { get; set; } // 判定结论,与 Success 分开 public string DefectCode { get; set; } // NG 时的缺陷编码,用于统计 }Success和IsOk必须分开。算法没跑通(找不到模板、图像过曝、边缘数量不对)是执行异常,产品合格与否是判定结论,两者混在一起会导致「算法失败被当成 NG 品流出去」,这在追溯时是灾难。执行异常要走独立的报警通道,让设备停下来等人处理,而不是默默计数。
结果落盘用一张主表加一张明细表就够了,主表存单件结论,明细存每个测量项的值,方便后面按项做趋势分析:
CREATE TABLE InspectionRecord ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Sn TEXT NOT NULL, -- 产品条码/流水号 RecipeCode TEXT NOT NULL, -- 料号,用于分料号统计 Result INTEGER NOT NULL, -- 1=OK, 0=NG DefectCode TEXT, -- NG 缺陷编码,OK 时为空 ImagePath TEXT, -- NG 图像存档路径 CreateTime TEXT NOT NULL ); CREATE INDEX idx_record_sn ON InspectionRecord(Sn); CREATE INDEX idx_record_time ON InspectionRecord(CreateTime); CREATE TABLE MeasureDetail ( Id INTEGER PRIMARY KEY AUTOINCREMENT, RecordId INTEGER NOT NULL, ItemName TEXT NOT NULL, -- 测量项名称,与配方节点对应 Value REAL NOT NULL, LowerLimit REAL, UpperLimit REAL, IsOk INTEGER NOT NULL );索引这两条不是凑数。按 Sn 查是为了追溯,按时间查是为了做日报和趋势图,产线上这两个查询是每天都跑的。明细表拆出来是因为一个工件可能有十几二十个测量项,塞进主表的 JSON 字段里查起来费劲,做 SPC 分析时更难受。
4. 核心模块实现:采集、定位、测量、判级四段代码落地
4.1 相机接入与 HObject 的生命周期
相机接入层要做三件事:打开设备、配置参数、稳定取图。HALCON 用open_framegrabber统一封装各家接口,参数随接口类型不同而不同,具体字段要按相机厂商的接口文档对齐,不能照抄。
public class CameraDevice : IDisposable { private HTuple _acqHandle = null; private readonly object _lock = new object(); public void Open(string interfaceName, string deviceId) { // 参数顺序固定,含义依次为:接口名、水平/垂直分辨率、起始位置、 // 图像宽高、场序、位深、色彩空间、泛型参数、外部触发、相机类型、设备标识 HOperatorSet.OpenFramegrabber( interfaceName, 0, 0, 0, 0, 0, 0, "progressive", -1, "default", -1, "false", "default", deviceId, -1, -1, out _acqHandle); // 关掉自动曝光和自动增益,产线上这两个必须锁死,否则测量值会漂 HOperatorSet.SetFramegrabberParam(_acqHandle, "ExposureTime", 8000); HOperatorSet.SetFramegrabberParam(_acqHandle, "Gain", 0); } public HObject GrabOnce(int timeoutMs) { lock (_lock) // 同一句柄不能并发调用,加锁是最省事的做法 { HOperatorSet.GrabImageAsync(out HObject image, _acqHandle, timeoutMs); return image; } } public void Dispose() { if (_acqHandle != null) { HOperatorSet.CloseFramegrabber(_acqHandle); _acqHandle = null; } } }自动曝光这一条值得单独说。调试时开着自动曝光画面好看,上线后光源老化、环境光变化,相机会自己调增益,灰度整体漂移,阈值型检测项集体失效。血泪经验是:调试阶段就用固定曝光固定增益,把光源亮度调到能拿到稳定灰度的位置,之后不再动它。真要适应不同料号的反光差异,靠配方里的曝光参数切换,不靠相机自动。
HObject 是托管包装的非托管资源,必须显式释放。用using或者 try/finally 里的Dispose(),不能指望 GC。这一点在第 5 章展开。
4.2 形状模板匹配与亚像素定位
定位是整个流程的地基,定位偏了后面所有测量都偏。做法分两步:建模阶段从一张标准件图像里抠出特征明显的区域生成形状模板;运行阶段用模板在当前图里找位姿。
// 建模:在一张合格品图像上框出特征区,生成形状模板 public HTuple CreateTemplate(HObject image, double row1, double col1, double row2, double col2) { HOperatorSet.GenRectangle1(out HObject roi, row1, col1, row2, col2); HOperatorSet.ReduceDomain(image, roi, out HObject domain); roi.Dispose(); // 角度范围 -0.39~0.39 rad(约 ±22.5°),覆盖来料旋转; // 金字塔层数、对比度、最小对比度都交给 auto,让算子按图像自选 HOperatorSet.CreateShapeModel( domain, "auto", -0.39, 0.79, "auto", "auto", "use_polarity", "auto", "auto", out HTuple modelId); domain.Dispose(); return modelId; } // 运行:在当前图中查找,返回行、列、角度、得分 public bool Find(HTuple modelId, HObject image, out double row, out double col, out double angle) { HOperatorSet.FindShapeModel( image, modelId, -0.39, 0.79, 0.7, // 最小得分,低于它认为没找到 1, // 最多找几个匹配,单件检测取 1 0.5, // 重叠度 "least_squares", // 亚像素模式,精度最高的那一档 0, // 金字塔层数,0 表示用建模时的设置 0.9, // 贪婪度,越大越快越可能漏 out HTuple r, out HTuple c, out HTuple a, out HTuple score); row = r.Length > 0 ? r.D.ToArray()[0] : 0; col = c.Length > 0 ? c.D.ToArray()[0] : 0; angle = a.Length > 0 ? a.D.ToArray()[0] : 0; return score.Length > 0 && score.D[0] >= 0.7; }参数里最需要现场调的是MinScore和Greediness。MinScore 定太低(0.5 以下)会把相似结构误认成目标,定太高(0.85 以上)则在来料表面有轻微脏污时找不到。经验值是先设 0.7,拿一批包含边界样本的图跑,看得分分布,取一个能把 OK 品和误匹配分开的阈值。Greediness 默认 0.9,如果发现偶尔漏检,先往 0.8 降一档试试,速度会慢一些但更稳。
建模区域的选择也有讲究:不要选大面积的均匀区域,那种区域没有梯度信息,模板会退化成靠边缘支撑,抗噪能力差;要选带角点、圆孔、缺口这类强特征的位置。模板图像尽量用实际产线的相机和光源拍,别拿实验室的样品图凑合,光学条件不一样,模板的对比度参数就不对了。
4.3 测量与判级:从像素值到 OK/NG 结论
有了位姿,测量就好办了。思路是把模板坐标系里定义好的测量位置,通过刚性变换映射到当前图像的实际位置,再用测量工具在映射后的位置取边缘。这样来料有平移旋转也不影响。
// 模板坐标系下的基准点与测量矩形参数(建模时在标准件上确定) private const double TplRow = 512.0; private const double TplCol = 640.0; private const double TplAngle = 0.0; public double MeasureGap(HObject image, double mRow, double mCol, double mAngle, JObject param) { // 1. 用匹配位姿构造"模板坐标 -> 当前图像坐标"的刚性变换 HOperatorSet.VectorAngleToRigid(TplRow, TplCol, TplAngle, mRow, mCol, mAngle, out HTuple homMat2D); // 2. 把模板里定义好的测量中心映射过去,测量框跟着工件一起转 HOperatorSet.AffineTransPoint2d(homMat2D, TplRow, TplCol - 80, out HTuple row, out HTuple col); HTuple phi = TplAngle + mAngle; // 3. 生成测量矩形:长 60 宽 12,方向沿工件边,插值选双线性 HOperatorSet.GenMeasureRectangle2(row, col, phi, 30, 6, 60, 12, "bilinear", out HTuple handle); // 4. 在矩形内找边缘对,sigma=1.0 抑制噪声,threshold=30 控制灵敏度 HOperatorSet.MeasurePos(image, handle, 1.0, 30, "all", "all", out HTuple edgeRow1, out HTuple edgeCol1, out HTuple amp1, out HTuple edgeRow2, out HTuple edgeCol2, out HTuple amp2, out HTuple intraDist, out HTuple interDist); HOperatorSet.CloseMeasure(handle); if (intraDist.Length < 1) throw new VisionException(1001, "测量框内未找到成对边缘,检查光照或测量框位置"); // interDist 是相邻边缘间距(像素),乘像素当量换成毫米 double pixelScale = param.Value<double>("PixelScale"); // 由标定得到,如 0.0163 return interDist.D[0] * pixelScale; }Sigma和Threshold这两个参数是测量的手感所在。Sigma 是高斯平滑的系数,来料表面粗糙、图像噪点大就调大到 1.5~2.0,代价是边缘位置会被轻微模糊,精度略降;图像干净就保持 1.0 甚至 0.8。Threshold 是边缘幅度阈值,它决定什么算作边缘——太低会把噪点当边缘,太高会漏掉对比度弱的真实边缘。稳妥的做法是打开边缘幅度输出,看一批图的幅度分布,把阈值定在噪声幅度和真实边缘幅度之间。
判级就是拿测量值和配方里的上下限比:
public (bool ok, string defectCode) Judge(double value, JObject param) { double lower = param.Value<double>("LowerLimit"); double upper = param.Value<double>("UpperLimit"); double tol = param.Value<double>("JudgeTolerance"); // 判定死区,默认 0 // 死区的作用:刚好卡在边界上的值做二次确认,避免 OK/NG 反复横跳 if (value < lower - tol) return (false, param.Value<string>("DefectCodeLow")); if (value > upper + tol) return (false, param.Value<string>("DefectCodeHigh")); return (true, null); }判定死区这个小设计在批量生产时很关键。没有它的时候,一个值在上下限附近抖,同一个工件可能这次 OK 下次 NG,操作员会怀疑设备有问题。加了死区之后,边界附近的值统一往宽松方向靠,配合趋势图看,反而更容易发现工艺漂移。
NG 图像一定要存。存图策略按「日期 / 料号 / 时间戳_缺陷码」的目录结构落盘,保留最近 30 天,超期自动清理。这张图在后续分析缺陷成因、和供应商扯皮的时候,是唯一的证据链。
4.4 配方管理:换料号不改代码
配方是这套系统里最能体现工程成熟度的部分。十几个料号,每个料号的 ROI、阈值、模板、上下限都不同,如果写死在代码里,每换一次料就要改代码重新发版,现场根本没法用。
配方的组织方式是一棵 JSON 树,根节点是料号,下面是工具链数组,每个工具节点带自己的参数:
{ "RecipeCode": "P-1024", "PixelScale": 0.0163, "Tools": [ { "Name": "Locate", "Type": "ShapeMatch", "TemplateFile": "tpl_1024.shm", "MinScore": 0.72, "AngleStart": -0.39, "AngleExtent": 0.79 }, { "Name": "GapA", "Type": "MeasurePair", "RefRow": 512, "RefCol": 560, "LowerLimit": 1.98, "UpperLimit": 2.06, "JudgeTolerance": 0.005, "DefectCodeLow": "GAP_SMALL", "DefectCodeHigh": "GAP_LARGE" }, { "Name": "Scratch", "Type": "BlobDefect", "MinArea": 120, "MaxAspect": 8.0, "GrayLow": 40, "DefectCode": "SCRATCH" } ] }流程引擎按Tools数组顺序执行,遇到Locate就把位姿写进上下文,后面的测量工具从上下文里取。新增一个检测项,只是在数组里加一个节点,代码零改动。配方的加载和热切换要注意两点:一是切换时必须在流程空闲的间隙做,不能在一件工件处理到一半时换参数;二是每次切换记一条日志,包含料号、操作人、时间,出问题时能对上是哪次切换引入的偏差。
注意:配方文件建议加版本号和校验值,现场出现过配置文件被人手工改坏、程序启动直接崩的情况,加个校验能提前拦住。
5. 避坑与常见问题排查:内存、线程、标定三处最容易翻车的地方
5.1 内存只涨不落
现象:程序跑两三个小时内存从 200MB 涨到 2GB,最后卡死或者被系统杀掉。原因几乎都是 HObject 没释放。HALCON 的图像和区域对象底层是非托管内存,C# 的 GC 只回收包装对象,不会主动去释放底层句柄,峰值节拍下对象创建速度远超终结器回收速度,句柄就堆起来了。解决方法是所有 HObject 都走using,或者在方法末尾显式Dispose();尤其注意异常分支——try 块里创建了对象,catch 里直接 return,那个对象就漏了。
// 反面写法:异常一抛,img 和 region 都不会被释放 HObject img; HOperatorSet.ReadImage(out img, path); HObject region; HOperatorSet.Threshold(img, out region, 0, 128); DoSomething(region); // 若这里抛异常,上面两个对象泄漏 // 正确写法:用 using 包住,无论走哪条分支都释放 using (HObject img = new HObject()) using (HObject region = new HObject()) { HOperatorSet.ReadImage(out img, path); HOperatorSet.Threshold(img, out region, 0, 128); DoSomething(region); }排查手段:任务管理器看句柄数比看内存更准,句柄只涨不落就是泄漏。也可以在每个流程节点前后打一条get_system("handle_count")的日志,几个循环就能定位到是哪个工具漏了。
5.2 多线程下的偶发崩溃
现象:单线程跑一天没事,改成多线程后随机崩,报的是访问冲突或者算子内部错误,日志里看不出规律。原因是 HALCON 的 HTuple 和 HObject 不是线程安全的,两个线程同时操作同一个对象,或者在 A 线程创建、B 线程释放,都可能出问题。解法是「谁创建谁释放,跨线程只传值不传引用」:算法线程处理完把测量值(double 数组)和结果码传出去,不要把 HObject 传来传去;确实要传图像,就在目标线程里CopyImage一份。另外 HALCON 的算子在内部已经做了并行,外层再开一堆线程抢 CPU 反而是负优化,算法线程一般开 1~2 个就够。
5.3 标定与像素当量换算翻车
现象:测量值整体偏大或偏小百分之几,重复性很好但准确度不对。原因多半在像素当量上。像素当量不是拿标称视场除分辨率算出来的,那个值忽略了镜头畸变和实际放大倍率偏差。正确做法是拿一块标准量块或者标定板,在同一工作距离下拍图,量出已知长度对应的像素数,两者相除。更严谨的做法是做多点标定,把镜头畸变一起拟合掉,视场边缘的测量值才不会系统性偏大。
还有个容易忽略的点:更换镜头、调整工作距离、甚至相机被撞了一下,像素当量都要重新标。我见过一个项目调试时标定过,后来调焦换了镜头没重标,整批产品尺寸超差却判 OK,流到客户那边才发现。现在我的习惯是每次动过光学系统就在配方里改标定日期,并在开机自检里加一条「标定有效期提醒」。
5.4 触发时序错位与丢帧
现象:检测结果和产品对不上号,或者偶尔漏检一件。这类问题多半在触发链路上。相机是硬触发还是软触发、PLC 给信号到相机曝光的延迟多少、编码器分频对不对、光源频闪和曝光是否同步,任何一环错位都会导致「拍到了相邻的一件」。排查方法是拿一张带标记的样件,连续跑 100 件,把每件的图像和条码一一对应看,错位会立刻暴露。修复上,硬触发优先于软触发,触发信号走光耦隔离,别用普通 IO 直连;曝光时间和光源延时要在配方里成组配置,不能分开调。
5.5 部署环境缺运行库
现象:开发机上跑得好好的,拷到产线电脑上双击就报错,或者一打开相机就崩。原因是目标机缺 HALCON 运行时或者 .NET 运行时版本不匹配,也可能是相机厂商的驱动没装、网卡巨帧没开。部署前要列一张环境清单:.NET 运行时版本、HALCON 运行库位数(32/64 位必须和主程序一致)、相机驱动版本、网卡巨帧和中断亲和性设置、系统电源计划(改成高性能,别让 CPU 降频)。这张清单每次部署都过一遍,比现场抓瞎强。
6. 验收与答辩:用一组可复现的数据证明系统「测得准、跑得稳」
验收这一步,很多人栽在「我说它准」上。合格的做法是拿数据说话,而且数据要可复现。我一般准备三组测试:准确性测试、重复性测试、稳定性测试。
准确性用标准件。拿 10 件已知尺寸的标准件(或者三坐标测过的样件),每件测 10 次,算每个测量项的平均值和标准值的偏差。偏差要小于公差的三分之一,超了就得回去查标定或者算法。重复性用同一件产品连续测 30 次,算标准差,这个数反映的是系统自身的抖动,一般要求小于公差的十分之一。稳定性是连续跑 4~8 小时,统计总检数、误检数、漏检数、算法异常次数,同时记录内存和句柄曲线。
| 验收项 | 指标 | 测试方法 | 记录内容 |
|---|---|---|---|
| 测量准确性 | 偏差 ≤ 公差/3 | 10 件标准件 × 10 次 | 均值、偏差、最大值 |
| 重复性 | 标准差 ≤ 公差/10 | 同件连续 30 次 | 标准差、极差 |
| 检出率 | ≥ 99.5% | 200 件含已知缺陷样本 | 漏检数、漏检类型 |
| 误检率 | ≤ 0.5% | 200 件合格品 | 误检数、误检原因 |
| 节拍 | ≤ 设计值 | 连续 500 件计时 | 平均、P95、最大值 |
| 长时间稳定性 | 8 小时无异常 | 连续运行 | 内存曲线、异常次数 |
节拍统计最好在程序里内建,别拿秒表掐:
// 每个流程节点前后打点,落库后直接出 P95 报表 var sw = Stopwatch.StartNew(); var result = _pipeline.Run(image); sw.Stop(); _repo.SaveTiming(result.Sn, result.RecipeCode, sw.Elapsed.TotalMilliseconds);答辩时的讲法,我的建议是倒过来讲:先讲误检漏检的数据和你是怎么把这两个数压下去的,再讲架构和代码。老师最想听的不是「我用了什么技术」,而是「你怎么知道它是对的」。你可以拿出那张 200 件样本的混淆矩阵,讲某一类缺陷一开始漏检率 8%,查下来是 Blob 面积下限设太大,把下限从 200 调到 120 之后降到 0.5%——这种带数字的调试过程,比任何架构图都有说服力。
还有一个常被问到的点:如果换个料号怎么办。这时候现场演示配方切换,30 秒内完成,切换后连续跑 20 件全部正确,这一下就把「课程作业」和「能交付的系统」区分开了。
从那以后我每次做完一套视觉系统,验收前都强制走一遍「标准件 + 重复性 + 8 小时连续跑」这三关,哪怕时间再紧也不跳。跳过一次,后面在现场补的功夫是省下来那点时间的好几倍。希望这套结构和记录方法能帮到你,需要对照完整实现的话,仓库里的源代码可以直接拿去做参考。
本文还有配套的精品资源,点击获取