
简介面向工业视觉缺陷检测场景的完整项目包基于掩码区域卷积神经网络实现缺陷的定位与实例分割旨在解决工业生产中表面缺陷难以人工检出的问题适用于本科、硕士阶段的图像处理课程设计、毕业设计或科研复现。资源内提供可直接运行的Python源码、基于深度残差网络预训练模型权重、用C与C编写的加速评估模块、符合COCO格式的数据集演示与评估脚本以及大量示例图片便于从数据准备、模型推理到结果可视化全流程动手实践。压缩包共227个文件以97个Python脚本和103张JPG图像为主体另含在线交互式示例、模型参数文件和少量构建配置文件整体约49.66MB目录结构清晰能够帮助使用者快速定位代码、数据与模型。目前已有657人学习适合希望深入理解深度学习缺陷检测原理并对照代码进行复现的读者。1. Defect Eye缺陷检测方案为什么先用Python做基线而不是直接上深度学习轴承外圈划痕、螺栓螺纹缺牙、硅片表面颗粒污染——这类缺陷检测场景很多工厂今天依然靠人工目检漏检率和招聘成本都压得产线负责人头疼。Defect Eye就是这样一套把图像处理中的预处理、分割、特征提取和判级串起来的Python缺陷检测方案以OpenCV为核心代码可控、参数可见能直接在本地跑通再迁移到产线。它不是黑匣子式的深度学习平台而是一个能改、能解释、能快速验证的基线系统。对刚接手质检自动化或想给现有视觉系统做复盘的工程师来说这是最合适的切入点。2. 方案选型与流水线拆解Defect Eye的核心检测链路与适用边界2.1 传统图像处理与深度学习的选择先回答三个问题再动手我接触的缺陷检测项目里要不要上深度学习几乎每次都被第一个抛出来。我的回答通常不是直接推荐而是反手抛出三个问题让需求方自己先回答。第一个问题你现在有多少缺陷样本轴承缺陷检测、螺栓缺陷检测这类表面质检缺陷出现率常常不到千分之五产线一周未必能攒出50个标注好的缺陷图。而深度学习哪怕用迁移学习每个缺陷类别也至少要百张量级的有效样本否则训练过程就是撞运气收敛不收敛纯看命。传统图像处理不需要海量正负样本缺陷的灰度特征、几何特征可以靠算子直接刻画这是它在这个场景下最大的生存优势。第二个问题你的算力和改造预算在什么水位深度学习模型训练要GPU服务器推理要么上独立显卡要么上边缘推理盒子现场为了跑一个模型再配一套算力成本往往比算法本身还高。OpenCV传统方案在一台普通工控机的CPU上就能跑到毫秒级很多做过matlab图像处理大作业的人都知道形态学和阈值分割的运算量对CPU来说没什么压力。第三个问题产线复盘的逻辑是什么深度学习模型给一个0.87的置信度现场工艺工程师问你它为什么认为这里是缺陷你解释不了。传统方案里面积阈值、灰度差异、长宽比这些参数每一个都能对应到物理含义出了误检可以直接定位是哪个条件放得太松。这个可解释性在工业验收时非常关键。所以我的判断是当缺陷形态相对固定、样本量不足、现场又要求快速上线时优先做一套以OpenCV为基础的检测方案作为基线。Defect Eye这类方案的名字里就带着检测定位很清楚它是传统图像处理路线的落地实现先把漏检和过杀压到可接受范围再决定要不要引入更重的模型。2.2 Defect Eye的核心检测流水线五个环节分别解决什么问题一个典型的Defect Eye检测流程本质是一条五环节的链路图像输入、预处理、缺陷分割、特征提取、判级输出。下面把每个环节在干什么、坏掉时什么表现说清楚。图像输入是第一关。工业相机输出的可能是彩色BGR图也可能是灰度图甚至是不带色彩信息的raw数据。这里最常见的问题不是算法不行而是通道顺序搞错OpenCV默认BGR而很多人的习惯是RGB一旦混用灰度化后的亮度分布就完全不同后面全乱。我一般会在入口统一转灰度并固定一张测试图做像素级对比。预处理是整条流水线的地基。常见的组合是中值滤波加对比度增强中值滤波对付产线上的椒盐噪声和细小灰尘颗粒CLAHE自适应直方图均衡对付光照不均。这里的核心原则是保证缺陷与背景的灰度差被放大同时不把噪声放大成伪缺陷。很多新手一上来就用高斯滤波结果划痕边缘被模糊得和背景混在一起这就是预处理选型错了。缺陷分割是决定检出率的核心环节。工业表面多数是亮背景暗缺陷所以顶帽变换配Otsu自动阈值是使用频率最高的组合。顶帽变换先用大核形态学开运算估计背景再用原图减背景把低频光照变化滤掉只留下高频的缺陷响应。Otsu则根据灰度直方图自动找分割阈值避免固定阈值在亮度漂移时整体失效。特征提取是把像缺陷的东西变成确定是缺陷的关键。每个连通域都会算出一组统计量面积、外接矩形的长宽比、灰度均值、与周边背景的灰度差、圆形度等。这组特征是高维的判级时用多条件过滤而不是单一面积。金属拉丝纹理、焊渣、灰尘在单特征上和真实缺陷高度重叠只有把特征组合起来才能把误检压下来。判级输出是最后一关可以做一个简单的阈值决策也可以接一个随机森林或SVM做二分类。很多产线方案到这里就直接输出NG/OK加画框结果数据回流做统计看板。如果发现过杀率高不要急着改分类器回头检查分割环节的形态学参数往往更有效。2.3 拿到项目包后的环境准备最小依赖、目录约定和第一步验证拿到Defect Eye的Python代码后第一件事不是打开源码去读而是先把环境和目录搭好再跑一张样例图验证链路通不通。常见做法是给这个项目单独建一个虚拟环境避免和系统Python或其他项目的依赖互相污染。python -m venv defect_env source defect_env/bin/activate # Windows下执行 defect_env\Scripts\activate pip install opencv-python4.8.1.78 pip install numpy1.26.2 pip install matplotlib3.8.2说明三点。第一OpenCV的版本必须固定。不同版本之间形态学算子的边界处理、阈值函数的浮点行为有细微差异在A机器上调好的参数换到B机器可能完全变样固定版本是保住参数可复现的最省力办法。第二numpy版本要跟OpenCV匹配OpenCV 4.8.x配numpy 1.26.x是经过大量项目验证过的组合不要随手装最新的numpy 2.x很多旧代码在它上面会报ndarray相关的兼容性错误。第三matplotlib不是必须的但用于中间结果可视化很顺手建议保留。目录我也建议固定下来不要所有图片堆在一个文件夹里defect_eye_project/ ├── images/ # 待检样本 ├── output/ # 结果和调试图 ├── config.yaml # 阈值和形态学参数没有就自己建 └── main.py # 检测主程序拿到项目包后第一步验证的顺序是先跑一张干净无缺陷的良品图确认系统不会误报再跑一张有明显缺陷的坏品图确认缺陷能被标出来。如果这两步都通过说明图像读取、灰度化和主流程是通的接下来才谈调参。如果第一步就报错优先检查图片路径是否含中文、图片是jpg还是png以及OpenCV是否真的装进了当前虚拟环境。3. 用Python跑通Defect Eye最小检测预处理、分割与判级的核心代码本章把上一章说的流水线落到代码。我会按预处理、分割、判级三步给出可以直接复用的函数里面的参数都标了调试方向你可以按自己的样本图微调。这里的代码核心都来自OpenCV的经典图像处理算法和opencv形态学图像处理里的膨胀与腐蚀一脉相承。3.1 预处理灰度化、中值滤波与CLAHE增强第一个函数负责把相机采集图转成适合分割的灰度图同时做两件关键事抑制噪声和增强对比度。import cv2 import numpy as np def preprocess(img_path: str, denoise_ksize: int 5, clahe_clip: float 2.0): # IMREAD_GRAYSCALE 强制转灰度避免彩色通道对后续阈值分割的干扰 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f无法读取图片: {img_path}) # 中值滤波对椒盐噪声和灰尘颗粒效果好且比高斯滤波更能保留边缘 denoised cv2.medianBlur(img, denoise_ksize) # CLAHE 自适应直方图均衡光照不均时比全局均衡更稳 clahe cv2.createCLAHE(clipLimitclahe_clip, tileGridSize(8, 8)) enhanced clahe.apply(denoised) # 转回BGR用于后续画框可视化 vis cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR) return img, enhanced, vis逻辑说明cv2.imread用IMREAD_GRAYSCALE在读入阶段就完成灰度化比先读彩色再调cvtColor少一步内存拷贝。中值滤波把每个像素替换为邻域中值对孤立噪声点有天然抑制作用同时不会像均值滤波那样把边缘模糊掉。CLAHE 按tileGridSize把图像切成小块分别做直方图均衡再用插值消除块间边界保证了光照不均下的对比度一致性。参数说明denoise_ksize是滤波核大小必须是大于1的奇数。5x5是工业样本的通用起点当你的缺陷是1到2像素的点状缺陷时改成3否则缺陷会被中值滤波当成噪声抹掉。clahe_clip控制对比度限制值越大增强越强默认2.0比较稳调到4.0以上会把背景纹理也增强出来误检会明显上升。tileGridSize一般保持8x8只有当图像分辨率特别低比如小于200x200时才改成4x4。注意denoise_ksize必须是奇数OpenCV遇到偶数核会直接报错。这是新手最容易犯的错。3.2 缺陷分割顶帽变换、Otsu阈值与形态学闭运算分割这一步的目标是输出一张二值图让缺陷候选和背景彻底分开。我采用的组合是顶帽变换去背景、Otsu自动阈值、闭运算连接断裂区域最后用连通域拿到每个候选区域的统计信息。def segment_defects(enhanced: np.ndarray, bg_kernel_size: int 15, close_ksize: int 3): # 顶帽变换用大核开运算估计背景原图减背景后暗缺陷被增强 bg_kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (bg_kernel_size, bg_kernel_size)) top_hat cv2.morphologyEx(enhanced, cv2.MORPH_TOPHAT, bg_kernel) # Otsu 自动阈值根据直方图自动算分割阈值不依赖固定经验值 _, binary cv2.threshold(top_hat, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 闭运算先膨胀再腐蚀把断裂的划痕边缘连成一个整体 close_kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (close_ksize, close_ksize)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, close_kernel) # 连通域分析返回的 stats 每行是 [x, y, width, height, area]0是背景 num, labels, stats, _ cv2.connectedComponentsWithStats(closed, connectivity8) return top_hat, closed, num, labels, stats逻辑说明顶帽变换是表面缺陷检测里最常用的一招它假设表面背景是低频成分缺陷是高频成分。用15x15的椭圆核对原图做开运算得到的是没有缺陷时的背景估计原图减掉它之后均匀光照下的背景灰度被压到接近0而划痕、凹坑这些暗缺陷会被显著抬高这一步直接解决了固定阈值难以适应光照漂移的问题。Otsu是另一道保险它扫描全部灰度级找类内方差最小的分割点让阈值自适应每一张图的灰度分布。闭运算用膨胀把分离的缺陷小线段接上再用腐蚀恢复原来的尺寸参数核选3x3既能补断点又不容易把两个挨得近的独立缺陷合并成一个。参数说明bg_kernel_size是本函数最敏感的调参对象它决定背景被估计得多平滑。当缺陷尺寸在10像素左右时15够用当缺陷是大块污渍或大面积氧化时要把背景核加大到30到50否则开运算会把缺陷本身当成背景直接抹掉。close_ksize大多数情况下保持3宁缺勿滥过大的闭运算核会把噪声点连成大片伪缺陷。3.3 特征过滤与判级面积、长宽比、对比度三个条件守住误检拿到连通域之后不能把所有候选都直接判NG必须用特征做过滤。这一节我给出的过滤条件是面积范围、长宽比上限、缺陷与背景的灰度对比度这套组合能对付多数产线场景。def filter_defects(enhanced, num, labels, stats, min_area30, max_area10000, max_aspect10.0, min_contrast8.0): defects [] for i in range(1, num): # 背景连通域编号为0从1开始 x, y, w, h, area stats[i] aspect max(w, h) / max(1, min(w, h)) # 面积过滤灰尘是小面积大块污渍是大面积按物理尺寸划掉两端 if area min_area or area max_area: continue # 长宽比过滤细长划痕允许但过长的线性噪点要排除 if aspect max_aspect: continue region labels i defect_mean float(enhanced[region].mean()) # 用膨胀得到缺陷外圈背景算灰度对比度 region_u8 region.astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) region_dilated cv2.dilate(region_u8, kernel) bg_mask (region_dilated 0) (~region) if bg_mask.sum() 10: continue bg_mean float(enhanced[bg_mask].mean()) contrast abs(defect_mean - bg_mean) # 对比度过滤灰度差不够大说明只是轻微的灰度波动 if contrast min_contrast: continue defects.append({ bbox: (x, y, w, h), area: int(area), aspect: round(aspect, 2), contrast: round(contrast, 2) }) return defects逻辑说明这个函数的核心思想是多条件串联判断任何一个条件不满足就跳过只有三个条件同时满足才认定是缺陷。面积过滤按相机分辨率和缺陷物理尺寸换算灰尘一般是极小面积指纹或大片污染是大面积两端都要切。长宽比过滤主要对付刷痕、拉丝这类线性背景纹理真实划痕虽然也细长但长宽比很少超过10。对比度过滤是最后一道闸它把灰度只差三四个灰度级的微弱波动判为噪声这类波动在金属抛光面和透明薄膜上尤其常见。参数说明min_area要从实际缺陷换算。假设相机是500万像素、视野100mm乘100mm一个0.2mm的针孔缺陷换算成像素直径约17像素、面积约220像素这时min_area设30到50比较安全设低了会把灰尘噪点全部纳入。min_contrast是灰度级差值8.0对暗缺陷够用如果缺陷本身就浅比如透明薄膜上的浅划痕要降到4到5但误检率也会上升需要配合面积条件收回来。4. 让缺陷检测参数不再拍脑袋样本、阈值与评价指标的调优闭环4.1 样本采集视频抽帧、覆盖光照变化是最小可行方案调参之前先解决一个前置问题你手上有没有一组能代表产线真实情况的测试图很多人用三五张漂亮的标准图调参上线第一天就被现场的灯光晃到误检。我一般建议按下面的最小方案准备样本。用视频抽帧代替逐张拍照把产线相机接入电脑拍一段1到2分钟的视频覆盖产品在传送带上移动的全过程然后每隔几帧抽一张。这样一次能拿到几百张光照变化和运动模糊都被采进来。覆盖三种光照状态正常照明、反光最强角度、防尘罩半开导致的光照下降每一类至少留20张。缺陷样本之外良品样本要更多建议比例在1:3以上。这里有一个大部分新手都会忽略的细节良品图必须包含所谓可疑良品也就是人眼能看出轻微痕迹但功能合格的件。系统如果把这些件判成NG就是最常见的过杀来源。采集时遇到这种说不清算不算缺陷的样本单独放一个目录不要和干净良品混在一起。实际调参时这个目录最能检验你的阈值是否卡在合理位置。样本采集完马上做的一件事是粗标把每张图的缺陷位置用矩形框标出来不需要精确到像素因为连通域分析和特征提取用的是区域统计粗标就够。这一步决定后续所有阈值评估有没有标准答案。4.2 网格搜索调阈值用F1分数代替肉眼调参传统做法的通病是人工改一个阈值跑一次图看了结果再改调了一下午还不确定是不是最优。更好的做法是把判定阈值当成可搜索参数在标好数据的样本集上做网格搜索。import numpy as np def evaluate_threshold(features, labels, thresholds): # features 是每个候选区域的对比度值labels 是人工标注的0/1 best_f1, best_thr 0.0, 0 for thr in thresholds: pred np.array([1 if f thr else 0 for f in features]) tp np.sum((pred 1) (np.array(labels) 1)) fp np.sum((pred 1) (np.array(labels) 0)) fn np.sum((pred 0) (np.array(labels) 1)) precision tp / max(1, tp fp) # 过杀率相关 recall tp / max(1, tp fn) # 漏检率相关 f1 2 * precision * recall / max(1e-6, precision recall) if f1 best_f1: best_f1, best_thr f1, thr return best_thr, best_f1 # 用法示例在15到60的对比度区间内按步长1搜索 best_thr, best_f1 evaluate_threshold( [f[contrast] for f in defects], manual_labels, thresholdsrange(15, 61, 1) )逻辑说明这段代码把所有候选缺陷区域的对比度特征和人工标注的0/1标签对齐在候选区间里逐个阈值计算precision、recall和F1。precision代表报出来的缺陷里有多少是真缺陷和过杀率相对recall代表真缺陷里被报出来多少和漏检率相对。F1是两者的调和平均避免只压一个指标时另一个崩掉。参数说明thresholdsrange(15, 61, 1)表示只对比度在15到60之间的阈值步长1。先跑一次粗搜索确定大致区间再在最优值附近用步长0.5做一次精搜比一上来就全区间细搜省时间。对于面积和长宽比这类整数参数同样可以用双重循环做组合搜索但样本量不够时组合搜索容易过拟合每个参数的候选值控制在5个以内。提示网格搜索得到一个阈值后先不要直接上线留出10到20张未参与搜索的验证图做最终确认防止阈值过拟合到样本集上。4.3 指标取舍漏检率、过杀率、节拍按这个顺序排优先级网格搜索给的是数学最优产线要的是场景最优。下面这个表格是我给现场项目做验收指标时常用的对照指标计算方式生产含义通常要求准确率(TPTN)/(PN)整体对错比例参考值不作为验收指标漏检率FN/(TPFN)缺陷当成良品的比例一般要求低于0.5%过杀率FP/(TPFP)良品当成缺陷的比例一般要求低于3%F12PR/(PR)综合平衡分调参时使用不作为最终验收检测耗时ms/件节拍决定项按产线节拍反推为什么要按这个顺序排优先级因为F1最优的阈值往往让过杀率偏高而产线真正不能接受的是漏检。轴承缺陷检测的用户哪怕多报20个良品也不希望有一个坏品装进成品里出厂。所以实际落地步骤是先用网格搜索拿到F1最优值再以漏检率满足要求、过杀率尽量低为条件在最优阈值附近微调把阈值往保守方向推半格到一格。特别注意一点一个候选区域的最终判定依赖面积、长宽比、对比度三个条件网格搜索通常只搜对比度阈值面积和长宽比用物理换算值先钉死等对比度调完再回来微调面积下限。不要三个参数同时搜索参数空间爆炸不说调出来的组合在现场也解释不通。再补充一个和节拍相关的点产线对检测耗时的要求不是越快越好而是小于节拍并留出20%余量。如果节拍是10秒一件你跑到2秒一件已经很安全不值得为把2秒优化到1秒牺牲任何检出率。反过来如果跑到12秒优先检查是不是图像分辨率过高、形态学核过大而不是先怀疑算法复杂度。5. 避坑指南缺陷检测项目最容易翻车的5个现场与对策这一章写的都是我在类似项目里踩过或者帮别人排过的坑每条按现象、原因、解决的顺序展开。你可以把这章当成排错手册上线调试卡住时回来对号入座。重点是不要直接跳到算法层面先用中间结果图定位问题出在哪个环节。5.1 上午正常下午全屏误检光照漂移让阈值失去了锚点现象同一套代码、同一台相机上午跑良品图输出干净下午同样的良品图出现满屏缺陷框。原因产线环境光不是恒定的。太阳光从窗户照进来、相邻工位的灯管老化、防尘罩被挪动过都会让图像的灰度整体偏移。固定阈值是拿灰度绝对值做判决灰度一变整条分割的根基就断了。这几乎是固定阈值方案最常见的翻车原因。解决把固定阈值换成Otsu自适应阈值这是最低成本的修复。更稳的做法是在程序里记录每张图的灰度均值和方差当灰度均值偏离基准值超过15%时主动输出一条光照异常告警让现场赶紧检查光源。我通常把这个监控逻辑放在预处理函数入口不先检测缺陷先确保图能不能用来检测。如果Otsu后还是全屏误检再检查图里有没有过曝区域——过曝区域灰度饱和Otsu会把它们切成许多分散小区域先把过曝像素在原图上做mask排除再进Otsu。5.2 小缺陷被预处理抹掉滤波核和形态学核比缺陷还大现象针孔、麻点这类只有三五个像素的缺陷怎么调阈值都检不出来看起来像图像里完全没有过这些缺陷。原因中值滤波核选了5x5或7x7闭运算核选了3x3以上预处理时小缺陷的灰度就已经被周围的正常区域平均掉了。缺陷还没进分割环节就消失了后面再调阈值都是白费功夫。解决先确认缺陷像素尺寸再做预处理。如果缺陷就是3像素大小denoise_ksize降到3close_ksize保持1不做闭运算。想确认缺陷是否被预处理抹掉最笨也最有效的办法是把预处理前后的两版图并排存下来用图像查看器放大到像素级对比。另一个思路是干脆做多尺度并行小核路径专保留小缺陷大核路径专抓大片缺陷最后按面积条件融合这个方法我在第6章详细展开。5.3 金属拉丝和机加工纹理被判成缺陷纹理和缺陷的灰度响应分不开现象晶圆表面的周期电路、金属表面的拉丝、法兰盘的车刀纹被大量标记为缺陷过杀率直接飙到30%以上。原因纹理和真实缺陷在灰度幅值上都表现为局部突变单靠顶帽变换加阈值无法在灰度维度区分它们。本质是特征维度不够只有灰度特征没有纹理特征。解决两个方向。方向一是在预处理时用更大核的背景估计把纹理也当成背景滤掉但这对小缺陷不友好容易把真实小缺陷一起滤掉。方向二是给每个候选区域增加纹理特征用局部梯度方向直方图真实缺陷往往是各向异性的而拉丝纹理方向高度一致靠方向离散度能切掉大部分。具体实现上在filter_defects里对每个region用cv2.Laplacian计算梯度方向直方图再设一个方向离散度阈值实现成本不高但能显著降低这类过杀。5.4 检测速度跑不满节拍算法写得很Pythonic但没有给CPU留余地现象产线节拍要求3秒一件Python脚本跑单张图要7到8秒现场直接说方案不采用。原因常见有四个。图像用几千乘几千的原分辨率直接处理getStructuringElement在循环里反复调用对每个连通域都用Python切块做逐像素遍历形态学核设得过大导致运算量大。解决按顺序做四件事。第一用ROI裁剪把视野里永远不会出现缺陷的空白区域切掉只保留有效检测区域。第二把背景核、闭运算核这些结构元素提到函数外初始化一次全局复用。第三连通域分析用connectedComponentsWithStats它在C层实现不要在Python层自己遍历contour。第四图像还是太大就先cv2.resize按比例缩到宽度不超过1500像素很多缺陷在缩图后依然清晰速度能快好几倍。这个思路在做智能车图像处理时也是通用的摄像头连续帧不做ROI裁剪再强的CPU也撑不住。还有一个容易被忽略的点是内存分配在循环里反复创建numpy数组会拖慢速度把大数组放循环外复用效果立竿见影。5.5 换了机器跑结果不一致版本、路径和读取方式都在给你挖坑现象同一张测试图在开发笔记本上检出来了到产线工控机上检不出来或者结果完全不同。原因最常见的三个。OpenCV版本不同形态学和阈值行为有差异图片路径含中文或空格cv2.imread静默返回None然后代码可能跳过检测直接输出NG相机输出的是16bit灰度或raw被当8bit读导致灰度范围全部错乱。解决在项目里固定依赖版本并写成requirements.txt路径统一用Path对象处理读图后马上检查是不是None是None就抛错而不是继续跑相机取流的代码里显式声明数据类型和通道不要依赖OpenCV自动推断。另外到新机器上跑的第一步一定是打印读入图像的shape和dtype确认和开发环境完全一致再谈检测结果。6. 进阶技巧多尺度融合与中间结果验证把漏检率再降一个量级6.1 多尺度并行检测一套参数抓不住所有尺寸的缺陷第5章里提到小缺陷被抹掉的问题根治方案是多尺度并行。思路很简单分别用3、7、15的背景核对同一张图做顶帽变换加阈值再把三张二值图做或运算融合。小核路径只保留小缺陷大核路径只保留大缺陷互不干扰最后在连通域过滤阶段统一按面积范围分流。def multi_scale_segment(enhanced: np.ndarray, bg_kernels(3, 7, 15)): merged None for k in bg_kernels: kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) top_hat cv2.morphologyEx(enhanced, cv2.MORPH_TOPHAT, kernel) _, binary cv2.threshold(top_hat, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) merged binary if merged is None else cv2.bitwise_or(merged, binary) return merged逻辑说明这里不是简单换三个核而是每次顶帽变换都在估计不同平滑程度的背景。核越小背景估计越倾向于跟随局部灰度波动小缺陷能保留下来核越大背景越平滑只有幅度够大的缺陷才留下响应。三路结果取或不同尺度的缺陷都被保留进同一张二值图。参数说明bg_kernels(3, 7, 15)是针对1到30像素直径缺陷的常见配置。如果你的缺陷最小10像素、最大200像素就改成(11, 31, 71)原则是最小核比最小缺陷略小最大核比最大缺陷略大。6.2 中间结果批量导出调参不靠猜靠看多尺度之后还有一个更重要的习惯每次调参后批量导出中间图。我习惯在代码里加一个debugTrue的开关把预处理图、顶帽图、二值图、筛选后的标注图四张图按文件名前缀批量存到output/debug目录。debug_dir Path(output/debug) debug_dir.mkdir(parentsTrue, exist_okTrue) cv2.imwrite(str(debug_dir / f{stem}_01_enhanced.png), enhanced) cv2.imwrite(str(debug_dir / f{stem}_02_tophath.png), top_hat) cv2.imwrite(str(debug_dir / f{stem}_03_binary.png), closed) cv2.imwrite(str(debug_dir / f{stem}_04_result.png), result_vis)导出之后用看图软件逐张对比哪一步丢失缺陷、哪一步引入噪声一目了然。我做图像处理这些年最深的教训是调参效率低不是因为算法难而是因为你看不见中间过程只能靠猜。只要中间图在手大部分问题都能在五分钟内定位到具体环节。希望帮到你。本文还有配套的精品资源点击获取