不用深度学习能检测工业缺陷吗?纯 OpenCV 数齿方案实战:HSV 掩码 + FFT 周期估计 + 动态规划
不用深度学习能检测工业缺陷吗?纯 OpenCV 数齿方案实战:HSV 掩码 + FFT 周期估计 + 动态规划
场景:产线要检测木料"缺齿"缺陷(应有的齿掉了/残了)。上深度学习之前,我们先试了一套纯传统视觉方案:零训练数据、零 GPU、每步可解释。本文完整还原这套方案的思路和实现,以及最后为什么还是转向了 YOLO。
为什么先试传统算法
深度学习不是免费的第一选择,它有明确的前置成本:攒缺陷图、标注、训练、迭代。而传统算法:
- 零数据:不需要任何训练集,规则写完当天就能跑;
- 可解释:每个判定都能追溯到具体像素依据,和客户解释"为什么判 NG"时理直气壮;
- 零依赖:只要 OpenCV + numpy,工控机上即拷即用。
如果缺陷特征足够规则(比如"齿应该等间距排列"这种强先验),传统算法完全有机会。我们的目标缺陷恰好就是这种:木料上一排等间距的齿,缺一个就破坏了周期性。"周期性被破坏"这件事,用频域方法检测再合适不过。
整体流水线
原图 → HSV+纹理掩码定位木料 ROI → FFT 判断齿排列方向(必要时旋转90°)
→ FFT+自相关估计齿距周期 T → DP 选出等间距的 7 个槽位
→ 三条规则判缺(间距异常 / 覆盖率过低 / 齿面暗斑)→ OK/NG
依赖只有三个:opencv-python、numpy、scipy(只用 find_peaks)。
第一步:定位木料 ROI
木头有两个稳定特征:颜色(黄褐色系)和纹理(木纹带来的局部灰度波动)。两个都做掩码,相与:
def _wood_mask(image):
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# 木色 HSV 范围(两段:主色调 + 偏红补偿)
m1 = cv2.inRange(hsv, np.array([10, 30, 40]), np.array([35, 255, 240]))
m2 = cv2.inRange(hsv, np.array([0, 20, 60]), np.array([15, 255, 255]))
mask = cv2.bitwise_or(m1, m2)
# 纹理掩码:5x5 邻域的灰度标准差,木纹区域 std 大、背景 std 小
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
mean2 = cv2.blur(gray.astype(np.float32) ** 2, (5, 5))
mean = cv2.blur(gray.astype(np.float32), (5, 5))
std = np.sqrt(np.maximum(mean2 - mean ** 2, 0)).astype(np.uint8)
_, tex = cv2.threshold(std, 8, 255, cv2.THRESH_BINARY)
mask = cv2.bitwise_and(mask, tex)
return cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((5, 5), np.uint8))
然后对掩码做行/列投影,取投影超过峰值 8% 的连续区间,就是木料的包围盒。纹理标准差这步是关键——单靠颜色,传送带上同色系杂物会混进来;加上"必须有木纹纹理"这个条件,假 ROI 基本绝迹。
第二步:FFT 估计齿距周期
齿等间距排列 → 沿排列方向的灰度轮廓是周期信号 → FFT 的频谱峰值对应的就是齿距:
def _estimate_period(vec, tmin=15, tmax=100, hint=None):
vec = np.asarray(vec, dtype=np.float64)
n = len(vec)
demean = vec - vec.mean()
window = demean * np.hanning(n) # 加窗压频谱泄漏
freqs = np.fft.rfftfreq(n)
nonzero = freqs > 0
periods = np.where(nonzero, 1.0 / np.maximum(freqs, 1e-12), 0)
valid = nonzero & (periods >= tmin) & (periods <= tmax) # 限定合理齿距范围
power = np.abs(np.fft.rfft(window)) ** 2
idx = np.where(valid)[0][np.argmax(power[valid])]
T = int(round(periods[idx]))
# 自相关在 T 附近精修(FFT 频率分辨率粗,自相关能补到像素级)
f = np.fft.rfft(demean, n=2 * n)
ac = np.fft.irfft(f * np.conjugate(f), n=2 * n)[:n]
ac = ac / (ac[0] + 1e-9)
peaks, _ = find_peaks(ac[int(T*0.7):int(T*1.3)], height=0.05)
if len(peaks):
T = peaks[0] + int(T * 0.7)
return T
两个细节值得说:
- FFT 定粗、自相关定精。FFT 的频率分辨率受信号长度限制(n 点信号只能分辨 1/n 的频率间隔),直接取峰值周期可能差 1~2 像素;自相关在 T±30% 范围内找第一个峰,把精度补回来;
- 限定周期范围(15~100px)很重要,不限的话 FFT 经常锁到木纹的高频伪周期上。
这个周期 T 同时回答了"齿该多密"和"齿该多宽",后面所有规则都拿它当尺子。
第三步:动态规划选槽位
有了周期 T,还不能直接"每隔 T 放一个框"——相位不知道,缺齿位置的谷干脆不存在。做法:
- 在灰度轮廓上找所有局部极小值(谷 = 槽),作为候选;
- 动态规划:从候选谷里选出恰好 7 个,使相邻间距尽量接近 T,且谷越深得分越高。
# dp[k][i] = 已选 k 个槽、最后一个槽是第 i 个候选谷时的最高分
for k in range(2, N + 1):
for i in range(M):
for j in range(i):
diff = abs((valleys[i] - valleys[j]) - T)
if diff > T * 0.45: # 间距偏离周期超过 45% 的组合不允许
continue
val = dp[k-1][j] + cand_scores[i] - 0.5 * (diff / T)
...
DP 的价值在于全局最优:贪心"找最深的 7 个谷"会被木纹伪谷骗,DP 用"等间距"这个结构先验把伪谷自然淘汰——缺齿位置没有谷,没关系,它会在下一步的间距检查里暴露。
第四步:三条规则判缺
槽位定下来后,三种缺齿模式各对应一条规则:
规则 1:间距异常(整个齿没了)
相邻槽中心间距 > 1.65 倍周期 → 中间缺了齿,缺几个用 round(gap / T) - 1 算。边缘的大间距跳过(木料两端本来就长)。
规则 2:边缘覆盖率过低(齿残缺)
每个槽带内跑 Canny 边缘,统计横向覆盖率——完整的齿边缘连续,残缺齿的边缘断了,覆盖率 < 8% 就报警。
规则 3:齿面暗斑(齿面缺损变色)
相邻齿互为参照:每个齿区域的亮度中位数和左右邻居的中位数比,暗于邻居 72% 直接判 NG;暗 12%~28% 之间的,再在区域内找暗斑连通域,面积占比 >5% 才确认。用邻居做基准而不是绝对阈值,这是对抗光照不均的关键。
效果
在留存的 22 张 OK 标准样上实测:22/22 全部通过,零误检。
耗时方面,1280×1024 原图在开发机上实测平均约 22ms,工控机上约 100~120ms——比 YOLO(512 输入)在同机型上的 300ms 快约 2~3 倍。速度上传统算法确实有优势,但后面的故事会说明:这个优势没能挽救它。
NG 检出率没有留下可靠数字,原因很现实:当时 NG 样本是靠手动放置拍摄的,位置稍微偏一点,算法本身和上料误差造成的错误就混在一起,统计不出干净的结果。这个问题本身也正是弃用的首要原因,见下节。
为什么最终还是转向了 YOLO
首要原因:手动上料,定位误差扛不住。 我们的被检木料只能手动放置,没有工装夹具固定——位置偏一点、角度歪一点,整套链路就开始出错:ROI 切偏 → 灰度轮廓变形 → 周期估计漂移 → DP 的等间距约束对不上 → 误检/漏检。传统算法的每条规则都建立在"目标位置和姿态大致固定"的假设上,而深度学习对这种位置变化的容忍度天然高得多(训练数据里本来就包含各种偏移)。
其余原因在维护成本:
- 参数太多,每个都是现场调出来的。全文粗数有十几个魔法数:HSV 范围、纹理阈值 8、间距阈值 1.65、暗度比 0.72/0.88、覆盖率 8%……换一批木料、换个光照,就得重新调一遍,而调参的人必须懂这套算法;
- 规则之间会互相影响。改一个阈值修好了 A 场景,可能碰坏 B 场景,没有 mAP 这种统一指标告诉你整体是变好还是变坏;
- 新缺陷类型 = 重写规则。后来要加"缺胶"检测,传统方案意味着再写一套规则;而 YOLO 方案只是数据集里加个类别、重训一次;
- 强先验是把双刃剑。"等间距 7 个齿"这个假设一旦不成立(产品换规格),整套 DP 逻辑失效;深度学习只需要换数据重训。
一句话:传统算法把"知识"硬编码在参数和规则里,深度学习把知识存在数据和权重里——后者的维护便宜得多。
什么时候仍该选传统算法
公平地说,以下场景传统算法仍是首选:
- 有工装/机构保证定位:被检物位置、角度基本固定(这是大前提,手动上料的场景慎入);
- 缺陷特征有强几何先验(等间距、对称、固定尺寸),且产品规格长期不变;
- 完全没有缺陷样本,深度学习冷启动不了;
- 需要向客户/审核方解释每一个判定的像素级依据。
我们的结论是:用传统方案快速验证可行性、攒第一批 NG 图(这些图后来正好成了 YOLO 的训练集),然后平滑过渡到深度学习。两条路线不是对立,是接力。
小结
- 周期性缺陷 → FFT/自相关估计周期 + DP 结构约束,是传统视觉里非常漂亮的一类打法;
- "相邻齿互为亮度基准"这种相对判据,比绝对阈值抗光照;
- 但传统算法对定位精度的依赖(手动上料慎入)、参数维护和缺陷扩展的成本,决定了产线长期方案还是深度学习;
- 传统方案最大的遗产:它攒下的 NG 图,成了深度学习冷启动的第一批数据。
系列文章:
- 《保姆级教程:零基础训练你的第一个 YOLOv8 缺陷检测模型(附 5 个产线真实踩坑)》
- 《YOLOv8 工业缺陷检测实战(上):48 张缺陷图起步,8 个版本迭代到上线的完整记录》
- 《YOLOv8 工业缺陷检测实战(下):小样本训练怎么把误检/漏检率压到产线标准》
- 《YOLOv8 部署到无 GPU 工控机实战:ONNX 导出 + OpenVINO 推理 + 手写后处理》
更多推荐


所有评论(0)