简介:这是一份面向机器视觉初学者与工业检测工程师的技术梳理资料,系统讲解视觉检测系统的组成与硬件选型思路,涵盖光源类型(含LED、萤光灯、卤素灯等)、相机参数、镜头选择等关键环节,并介绍常用图像处理算法及主流算法库。资源为单份PDF文件,大小仅1.26MB,内容精炼便于快速查阅,目前已有1188人学习。文档结合划痕检测、标识识别等真实工业场景,对照明方式(如背向照明、低角度照射)与硬件搭配给出实用建议,并梳理边缘检测、模板匹配、灰度变换等常用算法,有助于理解从图像采集、预处理到缺陷判别的完整流程。无论是用于系统知识速览、项目选型参考,还是内部技术分享,都能快速建立清晰框架。相比零散教程,它将硬件与应用算法串联起来,能有效缩短上手周期,是性价比较高的工业视觉入门参考资料。
1. 机器视觉工业缺陷检测:一条产线上,算法只占一半
同一个金属件划痕,实验室里检出率 99%,搬到产线上变成 92%,换一批来料又掉到 88%。做过机器视觉工业缺陷检测的工程师多半都经历过这种“模型没动,现场变了”的窘境。像素精度漂移、光源衰减、曝光参数被误调、产品定位偏差,任何一个环节动了,缺陷成像就会变化,算法表现自然跟着波动。这篇内容把工业缺陷检测从成像、标定、算法到落地验收的完整链路拆开讲,重点落在“哪些参数会改变检测结果”“怎么定位变化源头”“怎么做换线验收”上。适合正在做视觉检测项目、被现场问题追着跑的工程师,也适合准备机器视觉岗位面试、想把项目讲透的候选人。这里没有放之四海而皆准的万能模型,只有一条可靠的技术路径。
2. 成像链路是缺陷检测的第一道变量:打光、相机与曝光参数
2.1 打光方案:先于算法的第一道坎
机器视觉工业缺陷检测的对象,表面形态差异极大。金属拉丝面的划痕、PCB 上的凹陷、薄膜上的气泡、透明件内部杂质,物理特性不同,适合的光路完全不同。常见做法是先根据缺陷特征确定打光方式,再谈相机选型,这步错了,后面算法再强也救不回来。
| 打光方式 | 适用缺陷类型 | 说明 |
|---|---|---|
| 背光(平行/漫射) | 轮廓缺陷、透光缺陷、尺寸检测 | 被测物挡住光形成剪影,缺陷表现为轮廓变化 |
| 同轴光 | 反光平面上的划痕、压伤 | 垂直入射垂直反射,平面越平整反射越均匀 |
| 低角度环形光 | 凹凸、压痕、刻印 | 光从侧面掠射,凹坑两侧形成明暗对比 |
| 结构光/频闪光 | 高速运动件、立体高度差 | 需要与编码器或 IO 触发联动 |
打光调整的核心指标是“缺陷与背景的灰度差”。用相机软件抓一帧图,画一条穿过缺陷的灰度剖面线,如果缺陷处灰度变化小于 15 到 20 个灰阶(8bit 图),误检率会明显上升。这时不应该着急训模型,而是先换角度、换波长、换偏振片。
2.2 相机与镜头:像素精度从这里“定死”
相机分辨率、靶面尺寸和镜头焦距之间的配合,直接决定了图像上每个像素对应的物理尺寸。选型公式不复杂,但现场出问题往往出在“忘了留视野余量”。
视场长边 / 相机长边像素数 = 每像素物理尺寸 例:视野 50mm,相机 2448 像素 → 50 / 2448 ≈ 0.0204 mm/pixel划痕宽度 0.05mm,至少需要显示在 3 到 5 个像素上才能被稳定检出,所以这里像素精度 0.02mm 够用。如果算出来像素精度大于缺陷目标尺寸的一半,就得换更高分辨率的相机,或者缩短工作距离。镜头安装还要注意靶面匹配,工业相机常见 2/3 英寸、1/1.8 英寸靶面,镜头靶面必须大于等于相机靶面,否则四角出现暗角,边缘区域的灰度判断就会失真。
2.3 曝光与增益:影响的不是亮度,是成像稳定性
2.3.1 曝光时间与光源亮度的配合
曝光调整原理在机器视觉里是个反复踩坑的点。曝光时间变长,图像变亮,运动模糊也会增大——如果产线速度是 200mm/s,曝光 2ms,单帧运动量是 0.4mm,相当于 20 个像素(按 0.02mm/pixel 算),边缘全糊掉。对应的做法是:运动场景用短曝光加亮光源,静止场景才考虑延长曝光。光源亮度不够时,正确的调整顺序是:先加光源功率,再开大光圈,最后才动曝光时间,增益放到最后考虑。
2.3.2 用 SDK 参数锁定成像状态
工业相机软件上常见的调节接口,在 SDK 里对应这样一组参数:
# 以常见的工业相机 SDK 为示例 import camera_sdk cam = camera_sdk.open_device(camera_id=0) # 关闭自动曝光和自动增益,全部切手动 cam.set_enum("ExposureAuto", "Off") cam.set_enum("GainAuto", "Off") # 曝光时间单位是微秒(us),先按运动模糊核算 # 线速度 v=200mm/s, 允许模糊 1 个像素,pixel_size=0.02mm # 曝光时间上限 = 0.02 / 200 = 0.0001s = 100us cam.set_float("ExposureTime", 80.0) # 留 20% 余量 cam.set_float("Gain", 0.0) # 增益尽量为 0,噪点干扰最小 # 关闭自动白平衡,避免颜色通道波动 cam.set_enum("BalanceWhiteAuto", "Off") # 抓帧并保存分析 frame = cam.trigger_and_get() frame.save("sample_good.bmp")这段代码逻辑上锁定了三类关键变量:曝光时间决定亮度与运动模糊量的平衡,增益决定噪声基底,白平衡决定彩色通道一致性。手动模式的意义是让图像保持一致,前后两天拍出的图灰度分布接近,算法阈值才不需要反复改。自动模式在缺陷检测中基本禁用。
提示:现场如果发现缺陷图片“一会儿亮一会儿暗”,优先排查是否有人把相机的自动曝光打开了,其次是光源控制器功率是否稳定,这是最常见的“算法失灵”假象。
3. 像素精度、标定与那些能让精度漂移的坑
3.1 像素精度的完整公式:不是只有分辨率
机器视觉 3D 入门教程和面试题里经常出现像素精度的计算,但它不只是一个除法。完整公式是:
像素精度(mm/pixel) = 视野长度(mm) / 像素数量(pixel)但实际影响检测精度的还有定位精度和标定误差。由于镜头畸变的存在,视野中心和边缘的像素精度不同。宽视野、短焦距、大靶面相机的边缘畸变更明显,直接用单一比值去算边缘尺寸,误差可达 1% 到 3%。对于缺陷检测来说,如果只判“有/无”,畸变影响不大,但涉及缺陷尺寸分级、位置判定,就必须做标定。
3.2 用标定板把像素精度变成空间变换
平面检测的标准做法是标定板标定,用 OpenCV 的 findChessboardCorners 和 calibrateCamera 完成相机内参和畸变系数的求解,再把像素坐标映射到真实物理坐标。
import cv2 import numpy as np # 棋盘格:内角点数量(列,行),方格实际边长(单位mm) pattern = (11, 8) square_size = 2.0 # 2mm 的棋盘格 # 3D 物理坐标点,z=0 平面 objp = np.zeros((pattern[0]*pattern[1], 3), np.float32) objp[:, :2] = np.mgrid[0:pattern[0], 0:pattern[1]].T.reshape(-1, 2) objp *= square_size objpoints, imgpoints = [], [] img = cv2.imread("calib.bmp") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) found, corners = cv2.findChessboardCorners(gray, pattern, None) if found: # 亚像素细化,比直接用整数像素坐标标定精度更高 criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria) objpoints.append(objp) imgpoints.append(corners) # 得到内参矩阵、畸变系数 ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None ) # 反向验证:把物理坐标投影回图像坐标,看重投影误差 mean_error = 0.0 for i in range(len(objpoints)): imgpoints2, _ = cv2.projectPoints(objpoints[i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[i], imgpoints2, cv2.NORM_L2) mean_error += error print("重投影误差(pixel):", mean_error / len(objpoints))重投影误差是标定质量的直接指标,一般控制在 0.05 像素以内。如果超过 0.1 像素,畸变模型和棋盘格数据没对齐,需要重新采集标定图。采集时让棋盘格出现在视野的九个区域(四角、四边中点、中心),只拍正中的标定结果对边缘修正没用。
3.3 现场哪些动作会让像素精度发生变化
机器视觉动了什么会导致像素精度变化,这是运行维护阶段的核心问题。常见原因分成三类:
| 变化来源 | 具体表现 | 排查方法 |
|---|---|---|
| 相机高度变化 | 整个像素精度等比例缩放 | 在图像上量一个已知工件尺寸进行对比 |
| 镜头调焦环松动 | 图像由清晰变模糊、轻微缩放 | 用清晰度评价函数(如 Laplacian 方差)看趋势 |
| 相机传感器移位 | 局部区域出现截断或比例不均 | 固定工件拍照,叠加重建残差图 |
一个实用的检查方法:加工件上找一个标准圆孔或标准方块,用图像处理量出像素宽度,除以设计尺寸得到当前像素精度,记录在 Excel 表里,每天开机测一次。精度的趋势曲线一出来,机械松动、镜头松动这类问题能在漏检出现前提前预警。
4. 算法路线怎么选:从传统特征到卷积神经网络
4.1 传统特征法:可解释性强的缺陷检测基线
工业缺陷检测的算法路线,在深度学习普及前以传统图像处理为主。到现在,表面纹理均匀、缺陷与背景对比明显的场景,传统方法的稳定性依然不差,而且不需要 GPU,推理速度快。常见做法是固定 ROI、滤波、阈值分割、连通域筛选。
import cv2 import numpy as np img = cv2.imread("metal_part.bmp", cv2.IMREAD_GRAYSCALE) # 1. 高斯滤波去噪,核大小略大于噪声像素尺寸 blur = cv2.GaussianBlur(img, (5, 5), 0) # 2. 固定阈值分割,适合背光或同轴光下对比度稳定的场景 _, mask = cv2.threshold(blur, 120, 255, cv2.THRESH_BINARY_INV) # 3. 形态学开运算去掉孤立噪点,闭运算连接断裂缺陷 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations=1) # 4. 连通域分析,过滤小于最小缺陷面积的区域 num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(mask, 8) min_area = 50 # 像素面积 for i in range(1, num_labels): area = stats[i, cv2.CC_STAT_AREA] if area < min_area: mask[labels == i] = 0 # 5. 在原始图上画检测结果框 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: x, y, w, h = cv2.boundingRect(cnt) cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)这套流程里的关键参数是阈值、核大小、最小面积,每一个参数都要有依据。阈值取决于成像灰度分布的稳定性;核大小取决于缺陷的最小宽度和图像噪声特征;最小面积取决于缺陷检测标准中定义的最小缺陷尺寸。把这三个参数暴露在配置文件中,方便现场调参,不要写死在代码里。
4.2 深度学习:分割优于分类的工业落地判断
当缺陷形态多变(划痕深浅不一、形状不规则)、背景纹理复杂时,传统阈值法会频繁误检,“不规则纹理上的缺陷”是典型的深度学习方法覆盖场景。工业上优先采用语义分割模型而不是图像分类模型。原因是:产线上需要知道“缺陷在哪里”,分类模型只能给出一个整体的判断,无法定位;分割模型输出的 mask 可以直接算面积、长宽比、位置,与原有规则体系兼容。
一个适合小样本的落地框架是 U-Net 或轻量分割模型的迁移学习。训练数据不用求多,几百张带标注的缺陷图就可以开始,重点在于数据增强策略:缺陷检测的增强顺序是平移、旋转、小角度扭曲、亮度抖动、对比度变化,最后才是高斯噪声,翻转需要根据产品形状决定,圆形件可以随意翻转,方向性磨痕件不能上下翻转。
import torch import torch.nn as nn class SimpleUNet(nn.Module): """工业小缺陷分割用轻量 U-Net,输入输出都是单通道灰度图""" def __init__(self): super().__init__() # 编码器:逐步下采样,扩大感受野 self.enc1 = self._block(1, 32) self.enc2 = self._block(32, 64) self.enc3 = self._block(64, 128) self.pool = nn.MaxPool2d(2) # 解码器:逐级上采样恢复细节 self.up3 = nn.ConvTranspose2d(128, 64, 2, stride=2) self.dec2 = self._block(128, 64) self.up2 = nn.ConvTranspose2d(64, 32, 2, stride=2) self.dec1 = self._block(64, 32) self.head = nn.Conv2d(32, 1, 1) def _block(self, in_ch, out_ch): return nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), nn.Conv2d(out_ch, out_ch, 3, padding=1), nn.BatchNorm2d(out_ch), nn.ReLU(inplace=True), ) def forward(self, x): e1 = self.enc1(x) e2 = self.enc2(self.pool(e1)) e3 = self.enc3(self.pool(e2)) d3 = self.up3(e3) d3 = torch.cat([d3, e2], dim=1) d2 = self.dec2(d3) d2 = torch.cat([self.up2(d2), e1], dim=1) d1 = self.dec1(d2) return torch.sigmoid(self.head(d1))训练时注意类别不平衡问题:缺陷像素往往只占整图的 1% 以下,直接用 BCE Loss 会让模型收敛到“全部预测为背景”。推荐用 Dice Loss 或 Focal Loss。如果缺陷过小,还可以在 Loss 里加一个像素权重项,给缺陷区域更高的权重。
class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super().__init__() self.smooth = smooth def forward(self, pred, target): # pred 和 target 形状为 (B, 1, H, W) pred = pred.contiguous().view(-1) target = target.contiguous().view(-1) intersection = (pred * target).sum() dice = (2.0 * intersection + self.smooth) / (pred.sum() + target.sum() + self.smooth) return 1.0 - diceDice Loss 对前景过小的问题相对友好,收敛后一般能稳定检出,边界轮廓比 BCE 训练出的结果更贴近标注。
4.3 缺陷检测项目推进顺序:学习路线与实践路径
结合机器视觉学习路线中常见的误区,工业缺陷检测项目正确的顺序是:先成像再算法,先规则再模型,先离线再在线。具体拆开是:
- 采集 50 张代表性缺陷图和 50 张好图,做灰度直方图分析,确认缺陷与背景可分。
- 先用传统方法做一版基线,输出检测率和误检率。
- 传统方法达不到要求时,再上分割模型,把传统方法的输出作为 pre-processing 可以减少模型输入中的无关干扰。
- 模型产出的 mask 不做直接判级,而是接传统的面积、灰度、形态学规则进行综合分析。
机器学习面试题里常问“缺陷检测项目怎么开展”,按这个顺序讲,面试官会认为你有实际项目经验,而不是只会跑开源模型。
5. 部署阶段的调试方法:阈值、指标与稳定性验证
5.1 漏检率和误检率的平衡:阈值不是拍脑袋定的
任何缺陷检测系统上线前都要标定一个判级阈值,这个阈值决定了最终产线的漏检率和误检率。当检测指标是“缺陷面积大于 0.5mm² 判 NG”时,阈值就作用在连通域面积上。调整面积阈值是一个搜索问题,用离线数据批量跑不同阈值,画出漏检率和误检率随阈值变化的曲线。
import numpy as np gt_area = [] # 人工标注的缺陷面积,单位mm² pred_area = [] # 算法输出的缺陷面积,单位mm² is_defect = [] # 人工判断是否为缺陷 (0/1) # 遍历面积阈值,计算不同阈值下的漏检和误检 thresholds = np.arange(0.05, 1.0, 0.05) for thresh in thresholds: # 算法判定为缺陷:面积 >= 阈值 pred_label = [1 if a >= thresh else 0 for a in pred_area] # 漏检:实际是缺陷但没有检出来 false_negative = sum(1 for p, g in zip(pred_label, is_defect) if p == 0 and g == 1) # 误检:不是缺陷但判成了缺陷 false_positive = sum(1 for p, g in zip(pred_label, is_defect) if p == 1 and g == 0) miss_rate = false_negative / max(sum(is_defect), 1) false_alarm_rate = false_positive / max(len(is_defect) - sum(is_defect), 1) print(f"面积阈值={thresh:.2f}mm² 漏检率={miss_rate:.3f} 误检率={false_alarm_rate:.3f}")产线上通常对漏检率有硬性要求,比如“不得高于 0.1%”,这时候不能只选择一个让漏检率最低的阈值,否则误检率可能飙高,导致产线频繁停线。要结合产线的人工复检能力选择一个平衡点。没有绝对最优,只有可接受的漏检/误检组合。
5.2 不透明测试集与灰度发布
工业缺陷检测系统的过拟合不仅发生在神经网络里,阈值参数、ROI 位置、滤波强度都会过拟合。常用的对策是保留一份不参与开发的测试集:从现场随机采集 1000 张连续图片,其中包含好品、已知缺陷、未知异常,人工标注后封存。每次改动参数或模型后用这套数据跑一遍回归,卡住误检率和漏检率不恶化。
提示:现场最常见的问题是产出符合统计指标,但换批次来料后漏检率上升。在采样时要刻意跨批次、跨班次、跨设备采集,不要只在一卷料、一个时间段内收集样本。
交接给现场时,还要明确一个异常处理逻辑——当检测算法连续多帧输出“图像质量问题”或置信度低于某个阈值时,系统应该自动做“拒检”而不是放行。这个动作可以为算法边界兜底,避免把不可靠的检测结果直接当成合格判定。
6. 换线验收与复试验证:一套可以照搬的测试动作
6.1 用“三段位样本”验证模型鲁棒性
做验收时不要只准备“好品、坏品”两类图。更实际的样本结构是三分法:标准好品(亮度正常、无脏污)、极限好品(亮度偏暗、有轻微油污但不影响功能)、缺陷品(不同严重程度)。极限好品是误检的主要来源,也是验收最容易漏掉的类别。
具体的验证脚本可以这样:把样本分为三组,分别输入检测系统,统计每组图像的误检率。
good_standard = load_images("dataset/good_standard/") good_edge = load_images("dataset/good_edge/") defective = load_images("dataset/defective/") # 输出每个样本的检测结果和置信度 for img in good_standard: print(f"标准好品 -> {predict(img)}") for img in good_edge: print(f"极限好品 -> {predict(img)}") for img in defective: print(f"缺陷品 -> {predict(img)}")若极限好品中的误检率大于 1%,优先检查打光和阈值,而不是增加模型的复杂度。误检集中在边缘区域时,考虑调整受检区域 ROI,把非受检区如倒角、螺丝孔排除掉。
6.2 光照漂移测试:提前发现产线上的“隐性变化”
换线验收时很多问题源于光源亮度和环境光的缓慢变化。一个简单有效的做法是连续 10 分钟拍同一工件,每分钟记录整张图的平均灰度,做出灰度时间曲线。正常状态下曲线呈水平线;如果曲线有斜坡或周期性波动,查光源控制器、频闪灯触发信号、环境光的日晒变化。机器视觉缺陷检测中最常见的“上午稳定下午失灵”就是光照漂移引起的,系统性做一次光照扫描比事后加算法更靠谱,也能提前规避现场条件变化对像素精度和灰度直方图的连环影响。验收记录里留下一张灰度趋势图,比贴几十张缺陷识别截图更有说服力。
本文还有配套的精品资源,点击获取