
简介面向机器视觉入门者与工业缺陷检测工程实践者的专题PDF围绕光源、相机、镜头及图像采集卡的选型思路常用图像处理方法与算法库展开帮助读者建立从硬件搭建到缺陷识别的整体认知。压缩包内含1个PDF文件大小约1.26MB内容集中便于通读、检索和按主题查阅。已有1192人学习下载。整份文档结合工件尺寸测量、表面划痕与斑点孔洞检测、Logo印刷完整性判断等场景系统梳理可见光与不可见光光源、背向与前向照明及同轴光、低角度光等照明方式并涉及相机分辨率、帧率、色彩深度等参数与预处理、边缘检测、模板匹配等算法。读者可据此理解选型依据、打光思路和缺陷检测流程适合作为项目前期方案讨论与知识查漏的参考。1. 一条产线的误检率从 8% 降到 0.6%靠的不是换模型有个做连接器外观检测的团队检测网络在验证集上 mAP 做到 0.92上线一周就被产线判了「不可用」——日产两万件8% 的误检意味着每天有一千六百多件要拉去人工复检产线主管直接叫停。后来他们没有换网络结构只改了三件事把环形光的入射角压低 15 度、把标注规范从「框住缺陷」改成「按缺陷最小外接矩形标注并单独立类」、把固定判定阈值换成按批次自适应。误检率落到 0.6%。机器视觉工业缺陷检测这件事难点很少在模型本身。它是一套「成像—标注—建模—判定—复检」的闭环任何一环松了后面全是白做。这篇笔记按落地顺序拆开讲光怎么打、数据怎么攒、模型怎么选、产线上怎么判、翻车点埋在哪。适合正在做或准备做外观检测的一线工程师也适合被误检率反复折磨的现场负责人。2. 打光与成像缺陷检测的地基为什么在相机之外2.1 先定缺陷的物理特征再倒推光源类型新手最容易犯的顺序错误是先选相机、再选镜头、最后随手配个环形光。正确的顺序反过来先问「这个缺陷在光学上表现为哪种差异」再倒推光源。缺陷本质上是「局部物理量偏离正常区域」可能是反射率差、高度差、透光率差、颜色差也可能是偏振特性差。不同的差异对应完全不同的打光方案。我一般会让现场工程师先拿手持显微镜对着不良品看十分钟把缺陷拍成微距图判断它是「凸起/凹陷」还是「材质/颜色异常」还是「几何缺件」。这一步做完光源方案基本就定了后面选相机只是在算分辨率。下面这张表是我在实际项目中反复用到的对照关系可以直接拿去对缺陷表现光学特征推荐光源关键参数划痕、凹坑、压伤表面起伏漫反射方向突变低角度环形光暗场入射角 10°20°角度越低起伏越突出高反光金属曲面镜面反射淹没纹理穹顶光圆顶漫射内壁白色漫射涂层光源从顶部进印刷字符、标签、丝印表面平整但有颜色/反射差同轴光半反半透镜正对镜头方向出光缺件、轮廓缺口、针脚歪斜投影剪影差异背光平行背光平行度高边缘过渡小色差、污渍、油墨偏色光谱反射率差异白光 彩色相机或特定窄带波长与材料吸收峰匹配细微裂纹、透明件内部缺陷透射/散射差异低角度暗场 偏振片正交偏振压掉表面反光只留散射一个常被忽略的参数是波长。短波长比如 450nm 蓝光的衍射极限更小对微米级划痕的分辨能力明显更好长波长穿透性强适合表层下有结构的材料。我在一个透明注塑件的内应力检测里试过白光和蓝光对比蓝光下应力纹的对比度提升了将近一倍就是因为散射强度对波长敏感。这类东西没有普适公式只能靠实测但知道「波长是个可调参数」能省掉很多瞎试。提示光源的频闪同步一定要和相机曝光对齐否则会出现亮度条纹。产线变频器、伺服电机是常见的干扰源光源用独立直流供电比从产线取电稳得多。2.2 分辨率怎么算最小可检缺陷决定相机和镜头真正决定成像是否够用的不是相机的「多少万像素」这种营销参数而是「像素当量」——一个像素对应实物多长。算法线上有个经验值缺陷在图像上至少占 3×3 个像素边缘检测和分类才有稳定余量如果要做亚像素级的尺寸测量得做到缺陷横跨 58 个像素。换算关系很直接像素当量 (mm/px) 最小可检缺陷尺寸 (mm) / 需要的像素数 视场 FOV (mm) 像素当量 (mm/px) × 传感器长边像素数举个例子某类密封圈上最小的毛刺是 0.1mm按 3 像素算像素当量要 ≤ 0.033mm/px。选一台 2448×2048 的 500 万像素相机视场长边 0.033 × 2448 ≈ 81mm。也就是说这台相机只能覆盖 81mm 宽的视野。如果被测件有 150mm 宽要么换更高分辨率的相机要么用两个工位拼接要么改用线扫相机配合运动。除了像素当量还有三个参数必须一起核景深产线工件有上下浮动景深不够会出现边缘件模糊。景深随放大倍率下降高倍率下往往只有零点几毫米这时候要考虑加自动对焦或者用远心镜头。远心度测尺寸、测间隙这类任务普通镜头的透视误差会让不同高度的物体投影尺寸不同。远心镜头能把这个误差压到很小代价是视野小、价格高、工作距离固定。畸变广角镜头边缘畸变能到百分之几如果缺陷分布在视野边缘同一尺寸的缺陷在中心和边缘的像素数不一样会直接拉低召回。要么选低畸变镜头要么做一次标定校正。我把这套算法整理成一个可执行的检查清单选型评审时逐条过最小缺陷尺寸量出来不是猜单位 mm按任务类型定像素数分类 3px测量 58px算像素当量反推视场核对工件尺寸是否落在视场内超了就拆工位或换线扫核对景深是否覆盖工件高度浮动确认工作距离能装进设备空间最后才看相机接口、帧率、快门类型。顺序错了就会陷入「买了相机发现视野不够、换了镜头发现景深不够」的循环。2.3 一段最小采集脚本先把成像质量量出来选完硬件别急着标数据先写个脚本把图像质量量化出来。很多项目后期返工根源是成像阶段没有客观指标全靠「看着还行」。下面这段用开源视觉库做采集和三项质量评估跑一遍就知道当前成像能不能支撑后续标注。import cv2 import numpy as np # 采集参数分辨率、曝光、增益全部锁死禁止自动模式 cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 2448) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 2048) cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 手动曝光自动曝光会让批次间亮度漂移 cap.set(cv2.CAP_PROP_GAIN, 0) # 增益拉满会放大噪声宁可加长曝光 def image_metrics(gray): contrast float(gray.std()) # 全局对比度 sharpness float(cv2.Laplacian(gray, cv2.CV_64F).var()) # 拉普拉斯方差越大越清晰 # 信噪比用中值滤波结果近似「信号」残差近似「噪声」 denoised cv2.medianBlur(gray, 5) noise gray.astype(np.float32) - denoised.astype(np.float32) snr 20 * np.log10(float(denoised.std()) / (float(noise.std()) 1e-6)) return contrast, sharpness, snr ok, frame cap.read() if ok: gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) c, s, snr image_metrics(gray) print(f对比度{c:.2f} 清晰度{s:.1f} 信噪比{snr:.2f}dB) cv2.imwrite(sample_raw.png, frame) # 存原图别存压缩后的 jpg cap.release()这段代码的逻辑说明CAP_PROP_EXPOSURE在不同驱动下取值含义不同有的平台是绝对值、有的是对数刻度必须以现场实测亮度为准不能照抄。medianBlur(5)的核大小要和噪声尺度匹配核太大把真实细节也当噪声抹掉了信噪比会虚高。Laplacian方差对失焦非常敏感同一个工件在合焦和轻微失焦下这个值能差三到五倍是个好用的对焦辅助指标。参数上给三档经验阈值用来判断成像是否达到可标注水平指标偏弱可用良好处理方向对比度灰度 std 151535 35调光源角度/亮度换波段清晰度Laplacian var 5050200 200检查对焦、景深、运动模糊信噪比 20dB2030dB 30dB降增益、加平均、稳住供电采集时还有一个必做动作存图必须存无损格式。现场有人图省事存 jpg压缩块效应在高频纹理区会凭空造出「伪缺陷」标注员标了一批不存在的瑕疵模型学到的是压缩伪影。这个坑我踩过一次返工了两周。3. 缺陷数据集从采样、标注到增强的完整链路3.1 正样本好攒负样本才是要设计的部分缺陷检测的数据集结构和常规目标检测不一样。常规检测是「有什么标什么」缺陷检测是「正常品几乎无穷多不良品少得可怜」。一条产线良率 99.5%你想攒 500 张不良样本得等十万件产品过线按一秒一件也要一天多而且不良类型还不一定齐。我的做法是把数据分成三块分别管理正常样本池良品图像量大用来训练「正常」的分布边界也用来做对比学习或者异常检测。这类样本不需要标注攒起来成本极低。缺陷样本池按缺陷类型分类存储每一类至少攒到 50100 个实例低于这个量级模型学不到稳定的类内变化。边界样本池这是最值钱的一块——「疑似但不判」的样本比如轻微色差、极浅划痕、正常工艺纹路。这些样本决定了阈值定在哪里。现场判定标准往往是模糊的质量部门口头说的「轻微不影响」和实际客诉标准经常对不上只有把这些边界样本单独拿出来给质量部门逐张确认才能把判定标准固化下来。主动采样比被动等更有效。我会让产线在换料、换模、换批次的前 200 件里强制留图因为缺陷往往集中在切换后的稳定期之前。另外把历史客诉的图片全部收回来哪怕只有几十张也比随机采样一百张有用。一个容易被忽略的问题是样本泄漏。同一个工件拍了多张图或者连续帧里截出来的图被随机分到训练集和验证集两侧验证指标会虚高一大截。正确做法是按「工件 ID」或「时间批次」分组划分同一工件、同一批次只能出现在一侧。3.2 标注规范bbox、旋转框还是像素级 mask标注形式选错后面模型怎么调都补不回来。三类标注各有适用场景选型看缺陷的几何特性标注形式适用缺陷优点代价水平框 bbox缺陷大致呈块状、长宽比接近 1标注快模型成熟细长缺陷框内混入大量背景旋转框划痕、裂纹、长条形缺陷紧贴缺陷走向背景占比低标注成本高部分框架支持有限像素级 mask不规则轮廓、面积/形状要量化精度最高可算面积周长标注成本最高一个实例几分钟连接器、五金件上的划痕基本都是细长形用水平框标注时一个 200×8 像素的划痕会被框成 200×200框内 96% 是背景模型很容易学到背景纹理当特征。这种场景必须上旋转框或者直接用分割。我在一个金属外壳项目上做过对比同样的网络水平框的召回 0.71换成旋转框后到 0.89差距全在背景干扰上。标注规范里还必须写死几条「不许自由发挥」的规则否则不同标注员之间的一致性会崩一个缺陷被光照分成两段是否合并成一个实例建议合并按物理连通性判断缺陷贴着图像边缘被截断标不标建议标但打上 truncated 标记训练时降低权重面积小于多少像素的忽略建议给一个下限比如 5×5 像素低于此不标避免噪声当缺陷同类缺陷相邻但不连通是分开标还是合并建议分开除非物理上是一个整体规则写进标注文档后做一轮交叉校验抽 10% 的图让两个人独立标算 IoU 一致性低于 0.7 就说明规则还有歧义得回去改文档。这一步花两天能省掉后面两周的返工。3.3 小样本增强别只做翻转和亮度抖动数据增强在缺陷检测里的作用比常规检测更大因为样本本来就少。但常见的翻转、旋转、亮度抖动只覆盖了「几何和光度」的变化遗漏了两个对工业场景更关键的方向成像链路变化和缺陷形态变化。成像链路变化指的是相机噪声、曝光波动、镜头脏污、光源衰减。这些在产线上天天发生但训练集里一张都没有模型一遇到就翻车。对应的增强手段是加高斯噪声、随机伽马、局部模糊、随机遮挡小块。缺陷形态变化指的是同一类缺陷的不同表现比如划痕有长有短、有深有浅可以用弹性形变和局部仿射来模拟。下面这段用常见的图像增强库组合出这套策略import albumentations as A from albumentations.pytorch import ToTensorV2 train_tf A.Compose([ # 几何小角度旋转 轻微缩放模拟工件摆放偏差 A.ShiftScaleRotate(shift_limit0.03, scale_limit0.08, rotate_limit12, border_mode0, p0.6), A.HorizontalFlip(p0.5), # 光度模拟光源衰减与相机自动曝光波动 A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.5), A.RandomGamma(gamma_limit(80, 120), p0.3), # 成像链路噪声 局部模糊 传感器脏点 A.GaussNoise(var_limit(5.0, 25.0), p0.4), A.OneOf([A.MotionBlur(blur_limit3), A.GaussianBlur(blur_limit3)], p0.25), A.CoarseDropout(max_holes6, max_height16, max_width16, fill_value0, p0.2), ToTensorV2(), ])逻辑说明和参数边界ShiftScaleRotate的旋转角不要超过 15°超过后缺陷的物理形态就失真了模型会学到不存在的模式scale_limit控制在 0.1 以内因为像素当量是固定的大幅缩放等于模拟了不存在的相机。GaussNoise的方差上限要参考 2.3 节实测的信噪比噪声加得比真实噪声还大等于在训练模型对抗一个不存在的敌人。CoarseDropout模拟镜头脏点但遮挡面积不能大否则会把小缺陷整个盖掉导致漏检样本被当成正样本训练。还有一个针对小目标的技巧如果缺陷在原图上只占几十个像素训练时用 640 的输入尺寸会把缺陷缩到十几个像素特征几乎消失。做法是把原图按缺陷位置切块比如切成 512×512 的滑窗保证每块至少包含一个缺陷用切块训练推理时再滑窗拼接。切图会带来重复检测的问题需要一个 NMS 或基于位置的去重逻辑这个后面在避坑章节会讲。4. 模型选型与训练传统算子、检测网络与分割头的取舍4.1 传统算子什么时候还值得用现在一提缺陷检测就往深度模型上冲但有一类场景传统方法依然是更优解缺陷与背景的差异可以用一个明确的数学量描述且产线不允许出现不可解释的输出。典型例子是背光下的轮廓缺口面积、周长、圆度、规则纹理上的周期性异常频域峰值、高对比度场景下的尺寸超差边缘拟合。这类场景用传统算子有三个实际好处样本需求几乎为零、单帧耗时稳定在毫秒级、判定逻辑可以画成流程图给质量部门签字确认。深度模型给不出的东西恰恰是第三点——质量部门问「为什么这张判不良」你没法拿一个 0.87 的置信度回答。传统算子要写得好关键在预处理而不是算子本身。下面这段是「背光剪影 形态学 轮廓特征」的最小可跑流程适合缺件、毛刺、破洞这类场景import cv2 import numpy as np def inspect_silhouette(img_gray, min_area120, max_circularity0.85): # 1) 高斯去噪核大小按噪声尺度取奇数 blur cv2.GaussianBlur(img_gray, (5, 5), 0) # 2) Otsu 自动阈值适用于背景与目标对比度稳定的背光场景 _, binary cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 3) 开运算去掉毛刺噪点闭运算填补小孔洞 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) clean cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) clean cv2.morphologyEx(clean, cv2.MORPH_CLOSE, kernel) # 4) 轮廓分析 contours, _ cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) defects [] for cnt in contours: area cv2.contourArea(cnt) if area min_area: continue peri cv2.arcLength(cnt, True) circularity 4 * np.pi * area / (peri ** 2 1e-6) if circularity max_circularity: # 圆度低说明轮廓有缺口或毛刺 defects.append((area, circularity, cv2.boundingRect(cnt))) return defects参数说明min_area是面积下限直接对应「多小的缺陷不判」这个值必须和质量部门一起定不能工程师自己拍。max_circularity是圆度阈值1.0 是完美圆实际工件轮廓本身就不是圆所以阈值要按良品的圆度分布来定——拿一批良品跑一遍取圆度分布的 1% 分位作为阈值这叫「按良品标定」。Otsu阈值适合双峰直方图如果背景有渐变得换成自适应阈值或者先做背景减除。传统方法的局限要心里有数光照一变阈值就失效、纹理复杂的表面没法用、多类缺陷要写一堆 if-else。所以实际项目里最常见的架构是「传统算子做快速筛查 深度模型做疑难分类」前者拦掉 80% 的明显缺陷后者只处理剩下的模糊样本算力压力小很多。4.2 三类模型怎么选一张决策表到了深度模型这一层选型不是「哪个 SOTA 用哪个」而是看标注形式、节拍预算和可解释性要求三个约束的交集。模型类型适用场景标注需求单帧耗时中端 GPU可解释性单阶段检测器缺陷位置和类别都要节拍要求高水平框或旋转框820ms中有框和置信度语义/实例分割缺陷轮廓不规则需要算面积像素级 mask2060ms中高能看到分割结果异常检测无监督缺陷类型未知、样本极少只需良品515ms低只有异常分数选择逻辑我一般这样走缺陷有明确类别、每类样本过百、节拍在 30ms 以内 → 单阶段检测器走旋转框或分割头。缺陷形状不规则、要输出面积/周长、节拍宽松50ms→ 分割网络。缺陷类型说不清、良品多不良品少、项目还在探索期 → 先用异常检测跑一版把异常分数分布画出来再决定要不要转监督。节拍极紧5ms且缺陷特征明确 → 回到传统算子或者用极轻量的分类网络做「有/无」二分类。一个反直觉的经验很多项目一开始上分割后来发现检测器加旋转框就够了因为质量部门最终只关心「有没有、在哪、多大」并不需要像素级轮廓。反过来也有——做面积量化的项目用检测框算面积误差能到 30%这时候分割就是刚需。选型前先问清楚下游怎么用这个结果。4.3 训练脚本骨架与五个必调参数训练部分给一个最小可跑的骨架重点不在框架 API而在几个决定成败的参数。import torch from torch.utils.data import DataLoader def train_one_epoch(model, loader, optimizer, scaler, device, cls_weight): model.train() total_loss 0.0 for images, targets in loader: images images.to(device, non_blockingTrue) targets [{k: v.to(device) for k, v in t.items()} for t in targets] optimizer.zero_grad(set_to_noneTrue) with torch.cuda.amp.autocast(): # 混合精度省显存也提速 loss_dict model(images, targets) # 分类损失加权缺陷样本远少于背景不加权会被背景淹没 loss sum(v * cls_weight.get(k, 1.0) for k, v in loss_dict.items()) scaler.scale(loss).backward() scaler.unscale_(optimizer) torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) # 防梯度爆炸 scaler.step(optimizer) scaler.update() total_loss loss.item() return total_loss / len(loader)五个必须现场调的参数按重要性排输入分辨率。它不是越大越好。分辨率翻倍显存和耗时大约翻四倍而小缺陷的召回提升往往只有几个点。正确做法是按 2.2 节算出的像素当量来定输入侧一个像素对应的实物尺寸不要比成像侧大太多否则等于主动降采样。正负样本比例与分类损失权重。缺陷像素占比常常低于 0.5%不加权模型会直接学会「全判正常」也能拿到 99.5% 的准确率。权重不是拍脑袋按类别频率的倒数开方来给再用验证集上的召回微调。学习率与 warmup。小数据集上大学习率会直接训崩。我一般用 1e-4 起步前 500 步线性 warmup后面余弦退火。如果 loss 在前 200 步就震荡先降一个数量级。锚框尺寸或分割的采样策略。缺陷普遍是小目标默认锚框尺寸往往偏大。用训练集真实框做一次 k-means 聚类把锚框尺寸换成聚出来的值召回通常能涨 35 个点。早停与验证指标。验证指标不要用 mAP用「在固定误检率下的召回」。产线真正关心的是「每千件误判几件的前提下能抓到多少不良」这个指标才和业务对齐。训练集和验证集的划分再强调一次按批次分组不要随机。我见过验证集 mAP 0.95、上线召回 0.6 的项目回头一查训练集里同一个工件的连续五帧被分到了两侧模型其实是在「认工件」而不是「认缺陷」。4.4 导出与推理侧前处理对齐模型训完导出成通用中间格式最容易翻车的不是导出本身而是前后处理不一致。训练时的归一化均值方差、resize 的插值方式、RGB/BGR 通道顺序任何一项对不上推理精度都会莫名其妙掉一截而且不掉到零只是稳定地掉 510 个点特别难查。import torch model.eval() dummy torch.randn(1, 3, 640, 640).to(device) torch.onnx.export( model, dummy, defect.onnx, input_names[images], output_names[preds], opset_version12, # 12 兼容性较好别盲目追新 dynamic_axes{images: {0: 1}}, # batch 固定为 1产线逐帧推理 do_constant_foldingTrue, ) # 关键导出前后跑同一张图比对输出差异 with torch.no_grad(): torch_out model(dummy) import onnxruntime as ort sess ort.InferenceSession(defect.onnx) ort_out sess.run(None, {images: dummy.cpu().numpy()}) # 差异应在 1e-4 量级以内超过就说明有算子没对上这段的重点是最后那步对比验证。导出不是「跑通就完事」必须拿同一张输入图分别过原始模型和导出模型逐元素比对输出。差异超过 1e-4 就说明某个算子被替换成了近似实现或者常量折叠改变了数值路径。这个比对脚本要固化成导出流程的一部分每次导出都跑。推理侧还要注意两件事一是 letterbox 填充的值要和训练一致常见是 114 灰填充值不匹配会让边缘区域的缺陷特征偏移二是后处理的 NMS 阈值要按切图策略调整切图训练、滑窗推理时同一缺陷会出现在相邻窗口里NMS 的 IoU 阈值要调低一些才能去重干净。5. 产线落地避坑误检、节拍、复检的 5 条血泪记录5.1 现象白天误检率正常夜班翻倍现象同一套模型白班误检率 2%夜班涨到 6%8%第二天早上又恢复。原因产线夜班电压波动更大光源亮度随之下漂图像整体对比度下降缺陷与背景的分离度变差。而模型是在白天采集的数据上训的没见过低对比度的分布。有人会怀疑是模型「玄学」实际上是一个纯物理问题。解决三步走。第一光源改成独立稳压供电并在光源前加一块漫射板降低对电流波动的敏感度第二采集端加一个亮度监控每帧算平均灰度偏离基准超过阈值就报警而不是硬跑第三训练集里按亮度分档采样把不同亮度下的图像都覆盖到。做完这三步夜班误检率回到 2.5% 以内。5.2 现象换一批来料就大面积误判现象模型上线两周稳定换了供应商的一批料之后误检率突然从 1.5% 飙到 12%图像肉眼看着没差别。原因不同供应商的材料表面粗糙度、涂层厚度、甚至是同一种颜色的色号都有细微差异这些差异在图像上表现为整体纹理统计量的偏移。模型把「供应商 A 的纹理」当成了正常分布的一部分遇到供应商 B 就全判异常。解决短期靠「按批次自适应阈值」——统计当前批次前 100 件的异常分数分布用分位数重新定阈值而不是用训练时的固定值。长期靠数据治理每引入一个新供应商先采集 200 件良品做一次小样本微调只训最后一层20 分钟能跑完。这件事必须写进换料流程否则每次都要重新踩一遍。5.3 现象训练集 0.98产线只有 0.7现象验证集指标漂亮上线后召回明显偏低把产线漏检的图拿回来一看缺陷在图像上确实存在但模型没框出来。原因八成的概率是数据泄漏——同一工件的连续帧被随机分到了训练集和验证集。模型记住了工件本身的特征划痕位置、背景纹理而不是缺陷特征。验证集里那些「长得像训练集」的样本自然全对。另外两成是标注质量问题比如小缺陷漏标模型把它当成了负样本。解决第一重新按工件 ID 或时间批次分组划分数据集重训一遍指标会掉下来但这个才是真实水平。第二抽 200 张验证图人工复核标注特别是面积小于 20×20 像素的小目标漏标率往往超过 15%。第三在训练里加一个「难例挖掘」环节把验证集上漏检的图按 3 倍权重加回训练集。5.4 现象单帧推理 30ms整线节拍还是不达标现象模型单帧耗时实测 30ms产线要求 100ms 一件理论上绰绰有余实际却经常超时报警。原因单帧耗时只算了模型前向。实际链路是「触发—采集—传输—预处理—推理—后处理—通信—PLC 动作」任何一环都可能成为瓶颈。最常见的是采集端的软触发延迟和后处理的 NMS 在大分辨率下耗时暴涨还有一个隐蔽的黑匣子是图像从相机到内存的拷贝用错 API 会多走一次全图复制。解决给链路每一段打时间戳找出真正的瓶颈。经验上预处理里的 resize 用硬件加速能省 58ms后处理 NMS 换成排序后贪心能省掉一半。另一个常被忽略的点是推理批大小产线逐帧推理时 batch1GPU 利用率其实很低如果有多个工位可以把同一时刻的多工位图像拼成一个 batch 一起推吞吐能提升两三倍。最后留 20% 的时间余量别把节拍卡死在理论值上。5.5 现象模型迭代后老缺陷突然漏检现象为了修一类新缺陷重训了模型新缺陷召回上去了但原来已经解决的老缺陷又开始漏。原因新数据加入后训练集的类别分布变了模型容量有限时会「牺牲」旧类去拟合新类这叫灾难性遗忘。在小数据集上尤其明显因为旧类的样本量本来就不多。解决建一个固定不变的「回归测试集」包含历史上所有出现过的缺陷类型每次模型更新必须在这个集合上跑一遍任何一类召回下降超过 2 个点就回滚。同时训练时把旧数据按比例混入不要只训新数据。工程上更稳的做法是模型集成——给每类缺陷单独训一个二分类器新增缺陷只训新模型老模型不动代价是推理耗时线性增长需要按节拍权衡。6. 上线之后阈值自适应与模型漂移的监控手法模型上线那天不是终点是漂移的起点。产线环境在变光源老化、镜头积灰、来料波动模型不会主动告诉你它不行了它只会安静地多判几个不良然后产线开始抱怨「最近误检又多了」。我现在的习惯是给每个检测工位配一套轻量的漂移监控核心是三张图输入分布图、输出分数分布图、判定结果趋势图。输入分布看平均灰度和图像清晰度的日趋势任何一项连续三天单向漂移就说明成像链路在老化输出分数看每天异常分数的中位数和分位数间距正常品的中位分数如果开始往上爬就是漂移的早期信号判定结果趋势看每日判废率的移动平均突增或突降都要查。阈值这件事固定值是省事但脆弱的方案。我一般用基于批次分位数的自适应阈值思路是假设当前批次的绝大多数样本是良品用它们的分数分布来定「什么算异常」import numpy as np def adaptive_threshold(scores, base0.5, k3.0, floor0.2, ceil0.9): scores: 当前批次所有样本的异常分数假设 95% 为良品 base: 兜底基准值样本量不足时使用 k: 稳健标准差倍数越大越宽松 scores np.asarray(scores, dtypenp.float32) if scores.size 50: return base # 样本太少别乱自适应 med np.median(scores) mad np.median(np.abs(scores - med)) 1e-6 # 中位绝对偏差抗离群点 th med k * 1.4826 * mad # 1.4826 是把 MAD 折算成正态标准差的系数 return float(np.clip(th, floor, ceil))这段的关键在1.4826这个系数它让 MAD 在正态假设下等价于标准差但比标准差抗离群点——产线批次里偶尔混进几个真不良用标准差会被拉偏用 MAD 不会。k3.0意味着阈值落在良品分布的 99.7% 分位附近实际调这个值就是在调误检率和漏检率的平衡点质量部门要求严就调小产线嫌复检多就调大。floor和ceil是安全边界防止某一批次数据异常比如整批都是不良导致阈值被拉到极端值。监控之外还有一个具体技巧值得单独说用复检结果反哺模型。产线人工复检时判「误检」的图和判「漏检」补标的图是两种完全不同价值的数据。误检图告诉你模型的决策边界太松漏检图告诉你训练分布有盲区。我一般要求复检系统把这两类图自动归档每周导出一次误检图只做阈值调整的参考不重训因为会引入分布偏移漏检图经人工确认后加入训练集。这个闭环跑三个月模型的实际召回通常能再涨 5 到 8 个点。最后说个我自己的教训早期做项目时我总想着一次把模型做到最好再上线结果拖了四个月产线等不及先上了人工全检。后来我改成「先上 70 分的版本把复检闭环跑起来靠真实数据迭代」反而三个月就到了 90 分。缺陷检测这件事产线上的真实数据比实验室里的调参重要得多早上线一天就多一天的数据。希望帮到你。本文还有配套的精品资源点击获取