去年接到某五金厂的视觉检测改造项目,需求听起来很普通:冲压件既要测量几个关键尺寸,又要检查表面划伤。原有方案是3D轮廓仪加人工目检两条线,3D轮廓仪那一台连软件带调试差不多花了小二十万,检测节拍还一直压不下来。我在现场蹲了一天,决定把方案推倒重来:扔开3D轮廓仪,只保留一台工业相机,上位机用C#写,算法侧用YOLOv8做多任务并发,同一套硬件同时完成尺寸测量和瑕疵检测。项目上线后,整套检测设备的硬件加软件投入只相当于原来的三成左右,节拍还从1.5秒一件降到了0.9秒一件。这篇文章把整个决策过程和实现细节完整盘一遍,给正在琢磨用低成本视觉替代3D检测的同行做个参考。
1. 改造动机:3D轮廓仪的账,算明白了才敢扔
先说清楚一个前提:不是所有3D设备都能扔。我这个项目里的五金冲压件,检测项集中在平面尺寸和表面状态,比如外形长宽、孔径、孔距,以及划伤、凹坑、毛刺。旧方案之所以上3D轮廓仪,是因为供应商习惯性推荐“高配置”,并没有真去分析哪些检测项需要高度信息。现场检测数据拉出来一看,真正依赖Z值的三维共面度、平面度类项目几乎没有,大部分尺寸在二维投影平面上就能测准。
1.1 原检测流程的槽点:轮廓仪和人工目检各自为战
旧产线分两道工位,第一道用3D轮廓仪测尺寸,第二道靠人工目检看表面。每件产品先停在旋转平台上,轮廓仪扫描一圈生成点云,再软件拟合特征;一旦震动稍大或者来料表面油污不均,点云就出洞,重新扫描一次就要多花两三秒。人工目检就更难受,冲压件表面反光强,划伤要在特定角度才看得清,质检员得反复换姿势,一天下来眼睛酸得不行,漏检完全是概率问题。
我把两道工位的节拍和不良率统计了三天,发现两个硬伤:3D轮廓仪的单件检测时间超过1.5秒,已经成为产线瓶颈;人工目检漏检率在0.8%到1.2%之间波动,客户退回的批次里大部分是轻微划伤和边缘毛刺。这就是典型的“重资产、低产出”组合,必须换思路。
1.2 项目总体投入对照:成本直降70%是怎么算出来的
立项时我做了一张投入对照表,不是只比设备单机价格,而是把软件、安装、调试、后期维护都算进去。旧方案里的3D轮廓仪需要专门配一台工控机,还要买轮廓分析软件授权,现场调试调了整整一个月。新方案的核心是“一台工业相机加一个YOLOv8模型组”,软件完全自研,上位机用C#写,省下的全是授权费和集成费。
| 投入项 | 原3D轮廓仪方案 | 单相机+YOLOv8方案 |
|---|---|---|
| 检测设备 | 3D轮廓一体机约12万 | 500万像素工业相机+镜头+光源约1.6万 |
| 工控机 | 数据分析专用机约1.8万 | 普通工控机约1.2万 |
| 软件与算法 | 轮廓分析软件授权约3.5万 | 自研C#上位机+YOLOv8约3万折算 |
| 现场安装调试 | 约2.8万 | 约1万 |
| 总计 | 约18.6万元 | 约5.3万元 |
18.6万降到5.3万,投入大概只剩原来的28%,说“成本直降70%”一点也不虚。省掉的钱里最大头是第三方软件授权,其次是3D设备的安装调试周期。当然,算法自研需要前期的学习成本和部署试错,这部分我后面会展开,不是所有人拿来就能跑。
1.3 单相机方案能接住的需求边界与风险
在扔掉3D轮廓仪之前,必须划清楚边界。要是你的产品真的需要测高度、平面度、轮廓度,或者工件有大量叠层遮蔽导致二维投影无法还原尺寸,那我建议老实保留3D设备。单相机2D方案只适合固定姿态、固定高度、表面反射可控制的场合。五金冲压件刚好符合:底模定位后姿态固定,高度方向由模具保证,公差也集中在平面尺寸上。
风险也客观存在。金属反光和油污会影响成像,光源方案必须单独设计;YOLOv8分割得到的边缘精度有限,不能直接拿来做高精度尺寸,还要加一道亚像素细化;模型会“漂”,后续必须持续收集坏例再训练。这几条风险我在后面第4章、第7章都会给具体解法。
2. 系统架构设计:C#上位机如何编排“双检”任务
这套系统的核心不是某个模型多强,而是C#上位机把相机、两套YOLOv8推理、测量逻辑、PLC通信拧成了一股绳。整体分四层:设备驱动层负责相机SDK和串口通信;数据处理层运行两个推理任务;业务判定层做尺寸拟合和缺陷筛选;表现层用WinForms展示图像和结果。四层之间通过异步队列解耦,任何一层卡住,其他层不会被拖死。
2.1 为什么要用C#写上位机而不是到处拼脚本
用Python做算法原型很方便,但真要上产线,运维、界面、PLC联动都绕不开C#。首先是工业生态成熟,相机厂商的SDK大都提供C#示例,Modbus/TCP、串口那些通信库开箱即用;其次是多线程稳定,产线设备讲究“跑一个月不重启”,C#的内存管理和异常机制比脚本后端更适合做看门狗;最后是界面开发效率,WinForms拖控件五分钟就能出一个带实时图像和结果报表的操作面板。
我见过很多项目用Python脚本跑视觉算法,单独跑没问题,一加上PLC握手、数据库写入、断线重连就开始各种卡顿,最后还是要用C#重写外壳。所以我从一开始就决定:算法用YOLOv8训练导出成ONNX,跑在C#进程里,用OnnxRuntime引擎调用,代码统一管理,不搞两套环境。
2.2 一图看懂任务拆分:尺寸任务、瑕疵任务各用什么模型
这个项目的“多任务”用得很彻底。我没有硬塞给一个模型去同时输出尺寸和瑕疵,而是拆成两个子任务,共用一张图完成不同侧重点检测:
- 尺寸任务:用YOLOv8分割模型,只分割一个类别“workpiece”,拿到工件的完整掩膜,再基于掩膜轮廓做尺寸计算。背光照明下,工件在图像里是暗色剪影,分割非常稳定。
- 瑕疵任务:用YOLOv8检测模型,识别划伤、凹坑、毛刺、锈迹四种缺陷,输出边界框和置信度。前光照明下,表面缺陷被光源勾亮,检测模型能稳定锁定缺陷位置和范围。
两个模型互不干扰,却在同一个C#上位机进程里并发运行。分割模型给尺寸任务提供“工件在哪里”的粗定位,检测模型只负责找缺陷。两条管线的结果最终汇合到一个判定函数里:尺寸超差或存在缺陷都判NG,并把类别、坐标、测量值一起写进本地数据库和PLC报文。
2.3 并发流水线:两路检测同时跑,互不阻塞
真正的并发设计不是开两个线程就完事,而是要考虑图像产生速度和消费速度不匹配的问题。相机每秒采集帧率可以到30fps,但两个模型推理加起来要一百多毫秒,如果每个模型各自等采集完再算,节拍必然爆炸。我采用的方案是生产者消费者模式:采集线程抓一帧,放进一个有界队列,推理线程从队列取图后同时触发两个Task并行跑两个模型。
队列容量我设成4,满员时直接丢弃最旧的一帧并记录“丢帧计数”。这样即使相机短时间内暴发多帧,系统也能自动跳帧,保持输出节拍稳定。UI线程不参与任何推理计算,只轮询结果队列刷新界面,PLC通信则由独立任务负责,每500毫秒同步一次状态。多任务并发不是简单“同时做两件事”,而是要让每件事都有自己的生产消费节奏,谁都不能拖垮谁。
3. 相机、镜头和光源方案:单相机成像,一半功夫在硬件
很多做算法的同行喜欢把精力放在模型上,轻视硬件,结果模型在实验室跑得飞起,现场一上产线就废。单相机方案对手、光源、夹具极其敏感,打光稍微偏一点,分割掩膜就会多出阴影块,缺陷模型也会被反光斑点骗得团团转。这章把硬件选型的关键计算和现场细节讲透。
3.1 相机与镜头选型:先算视野和像素分辨率,再谈精度
我的检测视野定在80毫米×60毫米左右,要覆盖一个四寸左右的冲压件。最终选的是某国产500万像素全局快门工业相机,分辨率2448×2048,全局快门能有效避免运动模糊。为什么不用卷帘快门?因为产线上会有轻微震动,卷帘快门在高速运动下会“斜切”边缘,对测量来说就是灾难。
镜头我用的是0.5倍远心镜头。远心镜头的好处是视野内放大倍率一致,物体离焦也不会造成尺寸变化,这对尺寸测量特别重要。普通的FA镜头在边缘会有畸变,即便做了畸变标定,景深范围内放大倍率还是有微小变化,精度很难追到0.05毫米以内。像素分辨率可以简单算:视野宽度80毫米除以横向像素2448,约等于0.0327毫米每像素。配合后面会讲的亚像素细化,尺寸精度稳定做到±0.03毫米是可行的,完全覆盖五金件±0.1毫米的常见公差。
3.2 双照明模式:背光测轮廓,低角度光找瑕疵
金属件最怕的就是高光和不均匀反光。我设计了两组光源,对应两个检测任务,用同一个相机分时采集两帧图像:
- 第一帧用背光板:工件放在背光源前面,形成黑色剪影,轮廓分明,用来跑分割模型和尺寸任务。
- 第二帧用低角度条形光:光源从两侧以15度左右的角度斜射到金属表面,划伤、凹坑这类微小的表面起伏会形成明显明暗差,用来跑瑕疵检测模型。
这两帧的采集间隔大约100毫秒,由PLC控制光源频闪和相机硬触发。我把两帧图像合成一个结构体传入处理管线,尺寸任务只看背光帧,瑕疵任务只看前光帧。为什么这样分?因为背光下工件表面细节全被压成黑色,根本看不到划伤;前光下轮廓边缘又会有强烈眩光。拆成两个成像模式,各自获得最干净的输入,比指望一个万能光照解决所有问题现实得多。
3.3 安装、触发和固定细节
硬件安装的细节决定系统能不能长期稳定。第一,工件必须固定在同一个平面上,我特意把夹具基准面加工出来,工件放上去后高度误差控制在0.1毫米以内,否则标定比例会失真。第二,相机支架要设计成可微调的刚性结构,锁紧后不许动,拆装一次就得重新标定。第三,触发用硬件触发而不是软件触发,PLC在定位气缸到位后给出高电平给相机外触发口,杜绝软件轮询带来的2到5毫秒抖动。
还有一点,光源要有稳态亮度。我选的条形光源是恒流驱动,不是普通开关电源,避免产线电压波动导致成像亮度漂移。现场灰尘大,镜头前面加装防尘罩,每周用气枪吹一次,这些看着不起眼,恰恰是模型精度能维持的关键。
4. 数据准备与模型训练:怎么让YOLOv8同时当“尺子”和“质检员”
YOLOv8的训练不是拿现成模型直接压上去就完事。这个项目里,模型既要给出可供测量用的高精度掩膜,又要能准确识别微小缺陷,数据标注和训练策略必须严格区分。我把数据集分成两套:一套是背光条件下的工件分割数据,一套是前光条件下的缺陷检测数据。
4.1 标注规则:分割掩膜负责尺寸,检测框负责瑕疵
背光帧的标注非常简单,只标一个分割类“workpiece”,把整个工件轮廓描成多边形掩膜。注意要包含所有可见孔洞,因为孔径和孔距都要靠孔洞轮廓测量。我一开始只标了外形,导致模型对孔洞区域无感,后来重新标注,每个孔洞都必须明确画出来。
前光帧的标注则复杂得多。四个缺陷类别分别是“scratch”“dent”“burr”“rust”,其中划伤用多边形描最合适,凹坑用紧致框更稳定,毛刺小且细,必须放大图像仔细甄别。这里有一条经验:缺陷样本宁可少而准,也不要多而杂。我第一批数据里混了不少油渍和灰尘伪影,模型训练出来误检率奇高,后来清洗掉三分之一的坏标注,效果立刻好了很多。
4.2 训练与ONNX导出:稳定是第一位
训练配置我走了保守路线。分割模型用的是YOLOv8s-seg,输入分辨率640;缺陷检测模型用的是YOLOv8s,输入分辨率也是640。为什么不用更大的输入?因为产线推理平台只是一块入门级显卡,输入分辨率翻倍,显存占用和延时都会大幅上升,而缺陷尺寸大多在0.5毫米以上,640分辨率足够识别。
训练命令做成了脚本,方便后续增量训练:
# 分割模型,用于工件掩膜 yolo seg train data=workpiece.yaml model=yolov8s-seg.pt epochs=200 imgsz=640 batch=16 # 检测模型,用于表面缺陷 yolo detect train data=defect.yaml model=yolov8s.pt epochs=200 imgsz=640 batch=16训练完成后导出ONNX格式,这一步直接决定C#端能否稳定加载。我采用固定输入尺寸导出,不启用动态长宽比,减少推理时额外的前处理开销:
yolo seg export model=runs/train/exp/weights/best.pt format=onnx opset=12 simplify=True dynamic=False yolo detect export model=runs/train/exp2/weights/best.pt format=onnx opset=12 simplify=True dynamic=False训练阶段还有一个重要动作:把现场采集到的“坏例”增量并入训练集。产线跑了两周后,我发现夜班灯光电压波动导致图像偏暗,模型漏检明显。于是专门在低光照条件下采集了300张图重新训练,同时在训练时加了亮度扰动和噪声增强,模型对光照波动的鲁棒性提升非常明显。
4.3 像素标定与亚像素细化:YOLO给粗结果,OpenCV给细结果
YOLO分割掩膜虽然能框出工件,但它输出的掩膜是像素级边界,直接量尺寸误差至少有1到2个像素,换算过来可能超过公差范围。我的做法是“粗分割+亚像素细化”两步走:先让分割模型算出工件和孔洞的粗略掩膜,再用OpenCV在掩膜边界附近的小窗口内做灰度梯度分析,找到精确边缘。
具体来说,掩膜转成轮廓后,对外轮廓和孔洞轮廓分别取最小外接矩形或拟合椭圆,把轮廓附近的区域单独裁出来,在原始灰度图里用Canny算子提取边缘点,再做直线或圆拟合。这一步能把边缘定位到亚像素精度,实际测量效果比直接抄掩膜坐标高一个数量级。标定比例也在这套流程里起作用:用棋盘格标定板在当前工作距离下测出每像素对应的毫米数,半径和孔距全部换算成毫米后再跟公差比较。
5. C#推理与测量逻辑实现:核心代码拆解
这一章直接贴核心代码,工程化的重点全部在这里。我用的是OnnxRuntime的C#接口,先把分割模型和缺陷检测模型加载成两个全局的会话对象,然后并行调用。
5.1 加载ONNX模型与预处理
using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; var options = new SessionOptions(); options.AppendExecutionProvider_CUDA(0); // 优先用GPU options.AppendExecutionProvider_CPU(); var segSession = new InferenceSession("workpiece_seg.onnx", options); var defectSession = new InferenceSession("defect_det.onnx", options);预处理阶段我用的是Letterbox方式,把任意尺寸图像等比缩放到640×640,再填充灰度值。这一步必须跟训练时保持完全一致,否则模型输入分布被破坏,精度会下降。
public void Preprocess(Mat image, int targetSize, out Mat resized, out float scale, out int padX, out int padY) { float ratio = Math.Min((float)targetSize / image.Width, (float)targetSize / image.Height); scale = ratio; int newW = (int)Math.Round(image.Width * ratio); int newH = (int)Math.Round(image.Height * ratio); Cv2.Resize(image, resized, new Size(newW, newH)); int padW = targetSize - newW; int padH = targetSize - newH; padX = padW / 2; padY = padH / 2; Cv2.CopyMakeBorder(resized, resized, padY, padH - padY, padX, padW - padX, BorderTypes.Constant, new Scalar(114, 114, 114)); Cv2.CvtColor(resized, resized, ColorConversionCodes.BGR2RGB); }输入张量直接由Mat的像素数据转换,OneDnn和CUDA执行provider都支持这样直接喂数据,省掉一遍数组拷贝。BatchSize固定为1,实测这样最快。
5.2 YOLOv8分割输出的后处理
分割模型的输出分两部分:一部分是框和分类得分以及掩膜系数,另一部分是原型掩膜图。C#端拿到原始张量后要先做NMS,再用掩膜系数和原型掩膜合成每个实例的二值掩膜。关键步骤是NMS之后,只保留最高分的那个工件实例,因为视野里固定只放一个工件:
// 简化示意:分割输出解析 var outputTensor = session.Run(inputs)[0].ToArray(); // [1, 4+nc+32, numAnchors] int nc = 1; // 工件一个类别 int maskCoeffStart = 4 + nc; var prototypes = session.Run(inputs)[1].ToArray(); // [1, 32, 160, 160] // 先按置信度筛掉低分框 foreach (var anchor in anchors) { float score = anchor.classScores[0]; if (score > 0.35) { boxes.Add(BoundingBox.FromAnchor(anchor, stride)); maskCoeffs.Add(anchor.Slice(maskCoeffStart, 32)); } } // 合并重叠框(NMS IoU阈值0.45) List<int> keepIdx = Nms(boxes, 0.45); ... // 对保留下来的锚点,用原型掩膜和系数重建掩膜 Mat maskPrototype = ExtractPrototype(prototypes[0]); Mat instanceMask = MaskCoefficients * maskPrototype; // 矩阵加权合成 Cv2.Threshold(instanceMask, instanceMask, 0.5, 1, ThresholdTypes.Binary);缺陷模型的后处理就简单了,纯检测输出,只需NMS过滤重叠框,再把置信度低于0.25的框清除。这里我会保留独立阈值配置项,现场调参时直接改配置文件重启生效。
5.3 尺寸计算、缺陷判定与结果上报
拿到工件掩膜后,我先用轮廓检索函数提取全部外轮廓和孔洞轮廓:
Mat mask = GetInstanceMask(); Cv2.FindContours(mask, out Point[] contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxNone);每个轮廓转成RotatedRect,计算出外接矩形长宽、孔洞直径和孔心坐标。注意孔洞判定逻辑:外轮廓按面积最大处理,其余面积较大的轮廓统一视为孔洞,再对每个孔洞做圆拟合,得到直径和中心坐标。孔距的计算就是两个孔心坐标在毫米坐标系下的欧氏距离。
瑕疵判定则是拿缺陷模型的结果跟工件区域做空间滤波:只保留中心点落在工件掩膜范围内的缺陷框,背景区域和夹具边缘的误检全部丢弃。这一步非常关键,我调试时发现模型对夹具边缘的毛刺误检率不低,加了这个滤波后误检几乎降为零。
结果上报用Modbus TCP写回到PLC,同时把每件产品的测量值、判定结果、置信度保存在本地SQLite库里,便于追溯。报文帧设计得很简单:第1字节状态,后面依次是长、宽、孔距、各缺陷类别的检出标志,末尾加上CRC校验。
6. 多任务并发的稳定性与性能优化
系统并发跑得稳不稳,不是看初期Demo而是踩坑之后才知道。我在现场加了连续24小时压力测试,专门盯帧率、显存、结果乱序三件事。这一章把稳定化的关键决策和实测数据放出来。
6.1 用Channel搭生产者消费者流水线
.NET自带的Channel<T>是很好用的异步队列,我用来连接采集线程和推理线程。核心代码大概是这个结构:
Channel<VisionFrame> channel = Channel.CreateBounded<VisionFrame>( new BoundedChannelOptions(4) { FullMode = BoundedChannelFullMode.DropOldest }); async Task CaptureLoop(ChannelWriter<VisionFrame> writer) { while (true) { var frame = await camera.CaptureDoubleFrameAsync(); // 背光帧+前光帧 await writer.WriteAsync(frame); } } async Task InferenceLoop(ChannelReader<VisionFrame> reader) { await foreach (var frame in reader.ReadAllAsync()) { var segTask = Task.Run(() => segSession.Infer(frame.Backlit)); var defectTask = Task.Run(() => defectSession.Infer(frame.Frontlit)); Task.WaitAll(segTask, defectTask); // 汇总结果,做尺寸测量和缺陷判定 await reportChannel.Writer.WriteAsync(result); } }DropOldest模式保证队列不会因为采集过快而爆内存,只会主动丢最旧的帧。相机帧率在产线上有突发,比如机械手换料瞬间连续命中触发信号,如果不用有界队列,系统内存会逐渐涨高甚至假死;用这个模式后,系统只会丢弃来不及处理的旧帧,不会影响新来的帧。
6.2 实测节拍与瓶颈定位
上线后的实际时间分配如下,单位毫秒:
| 环节 | 耗时 | 说明 |
|---|---|---|
| 双帧采集 | 约80 | 两次硬触发,间隔100ms |
| 分割模型推理 | 约45 | YOLOv8s-seg,640×640 |
| 缺陷模型推理 | 约28 | YOLOv8s,640×640 |
| 尺寸测量与判定 | 约12 | 轮廓处理+亚像素拟合 |
| PLC报文与UI刷新 | 约5 | 异步发送,不阻塞主体 |
单件总耗时约170毫秒,加上工件到位和传动时间,实际节拍稳定在0.9秒。对比原来的3D方案1.5秒,相当于产能提升了40%。瓶颈在分割模型推理,如果想继续提速,可以把seg模型换成YOLOv8n-seg,损失一点边缘精度,换回大约20毫秒。对于非关键工位我会优先保精度,不盲目追求最快。
6.3 并发踩坑:丢帧、显存与推理结果错位
有三个坑必须单独说。第一个是显存分配,两个模型同时在GPU上推理,显存峰值不能简单相加,OnnxRuntime会为每个会话预留一部分workspace,我先把分割模型显存限制在3GB,缺陷模型限制在2GB,预留30%余量后系统才稳定。第二个是ONNX推理本身的线程安全,我一开始尝试在同一个会话对象上多线程并发Run,结果出现随机崩溃,后来改成每个Task使用独立的会话实例,问题彻底消失。第三个是结果错位,因为我有时会跳过旧帧,如果PLC发来的产品编号和图像帧没有绑在一起,就会出现测量值对不上工件的严重事故。解决办法是给每一帧加盖一个递增序列号,结果返回时同时返回序列号,PLC那边用序列号匹配每次触发请求。
7. 常见问题速查与现场避坑清单
项目交付后我又跟产线三个月,积累了各种现场问题,直接整理成速查清单,方便后面接手的人少走弯路。
7.1 金属反光导致漏检误检,怎么压
冲压件表面有油膜和冲压纹,低角度光很容易制造大面积反光。瑕疵模型把反光斑点误当成凹坑,分割模型也偶尔把反光区当成背景破洞。我最后用过三招:第一,给相机镜头加偏振片,同时光源前面也加偏振片,通过偏振方向交叉压制镜面反射;第二,把低角度条形光的角度从15度微调到10度,反光斑明显弱下去;第三,在推理判定里加一个最小面积阈值,小于0.2平方毫米的候选框直接忽略,滤掉细碎光斑。
这中间最容易被忽略的是油污影响。冲压件返修后残留油渍会形成类似瑕疵的暗斑,光靠模型很难区分,我在产线加了一道气吹工序,同时把现场光照亮度提高20%,并把这部分油污样本当成独立的“疑似”类别重新采集标注,让模型学会忽略它们。
7.2 标定漂移和模型退化怎么防
标定漂移是单相机测量最常见的坑。产线温度变化、支架螺栓松动、地板震动都会让像素比例产生微小偏移。我在系统里设了每日自动验证流程:每天早上开机后用标准量块在夹具上拍一张图,软件自动测标准块长度,偏差超过0.02毫米就弹出提醒并提示重新标定。同时把相机支架全部定位销锁死,杜绝拆卸后凭手感装回。
模型退化主要靠持续学习。我每周导出一批新的困难样本给模型增量训练,特别是新增瑕疵类型时不会直接改标注,而是单独建一个新类别训练,防止旧类别遗忘。模型更新后先在旁路模式运行一个星期,跟旧模型结果做A/B对比,确认不会引入新误检再切换正式部署。
7.3 PLC通断、掉线和重启恢复
上位机与PLC之间用的是Modbus TCP,现场偶发交换机端口闪断会导致通信假死。最开始我用简单的定时读写,断线后必须手动重启程序才能恢复,产线工程师叫苦不迭。后来做了自动重连机制:每500毫秒发一次心跳报文,连续三次超时后进入重连状态,断开期间检测结果缓存到本地队列,通信恢复后按顺序补发,并在界面上标出补发数据。再后来干脆加了看门狗程序,上位机异常退出时自动拉起进程,保证夜班无人值守时不掉链子。
7.4 最后一个小技巧:每天看一眼故障样本
再多自动化也比不上定期的现场巡检。我给自己定了一个习惯:每天早上到产线第一件事,翻一遍昨天的NG图片集,重点看那些模型置信度在0.4到0.6之间的“模糊样本”,这一区域内藏着新出现的缺陷变体。每发现一类新的假瑕疵或漏检模式,就补一张到训练集里,哪怕只有几十张,下一版模型效果都会有肉眼可见的改善。
像这种“单相机+双模型并发+亚像素细化”的架构,后续想扩展也很容易:换大口径镜头就能覆盖更大范围,把两个模型并到一个Graph里能省一次预处理开销,缺陷类别也可以随新工艺随时加训。项目的核心其实不在某个炫技的模型,而在于把成本、节拍、精度、稳定性全部摆到桌面上算清楚,再决定怎么组合技术方案。能稳定跑在产线上,才是真本事。