☰
基于MATLAB形态学与边缘算法的药片缺陷及有无检测方案
2026/11/5 2:18:01 网站建设 项目流程

简介:面向图像处理初学者与MATLAB实践者的药片缺陷检测案例包,聚焦工业视觉中目标有无与好坏判定的典型流程。方案融合小波去噪与自适应中值滤波抑制高斯、椒盐噪声,借形态学重构剔除复杂背景,再以边缘算子提取轮廓并生成二值图像,最后通过扫描匹配标定目标位置并给出判别结果,对理解预处理、分割与匹配的串联关系较有帮助,且不依赖深度学习框架,便于在普通MATLAB环境运行。压缩包共15个文件、约1.78MB,含5个m脚本(主流程、边缘函数、帧差与自适应中值滤波等)、7张jpg示例图以及2份doc和1份docx说明文档,脚本与图示对应,便于逐步复现与调试。已有1578人学习,适合课程设计、实验练习或检测入门参考,可据此梳理算法链路、替换测试图像并观察各环节输出变化。

1. 需求切入

药片缺陷检测和有无检测,本质上属于两件性质完全不同的任务,却常常被安排到同一个工位上完成:有无检测只看泡罩里有没有那片药,答案是二值的;缺陷检测要判断这片药有没有崩边、缺角、表面黑点或者碎裂,答案是分类的,而且缺陷尺度常常只有零点几毫米。把这两件事放在一起做,最直接的做法是用 MATLAB 搭一条由形态学分割与边缘算法组成的流水线:先用形态学把“区域”拿稳,再用边缘把“轮廓”抠准,最后用一组几何量做判定。

这套方案适合三类人:正在做视觉检测方向毕业设计或课程设计的学生,需要一套结构完整、参数可解释、能跑出数据的方案;刚接手产线外观检测工位的工程师,需要一套不依赖大量标注样本就能快速上线的基线;以及手上有 MATLAB 授权、但不想为了一个单工位去搭深度学习训练环境的人。接下来会从需求拆解讲到总体架构、核心实现、常见问题排查,最后落到怎么验收和怎么答辩。

2. 需求拆解与算法选型:形态学与边缘算法为什么能支撑产线节拍

2.1 把“缺陷”翻译成可测量的几何量

做视觉检测最容易犯的错误,是拿着一句“检测药片缺陷”就开始写代码。这句话在算法层面是没有意义的,必须先把每种缺陷翻译成一个能算出来的数。通常的做法是先和工艺方确认缺陷清单和最小可接受尺度,再逐条映射成几何量或灰度量:

缺陷类型图像表现可测量的量常用手段
崩边、缺角轮廓出现内凹缺口凸包面积比、圆形度边缘提取 + regionprops
裂片、裂纹细长暗线穿过片面底帽响应、长宽比形态学底帽变换
黑点、油污局部暗斑底帽响应面积形态学底帽变换
亮斑、异物局部高亮顶帽响应面积形态学顶帽变换
空位(缺片)该有位号处无目标连通域数量、位号覆盖率二值化 + 连通域分析
碎片面积极小面积与标准面积之比连通域面积过滤
粘连双片单个连通域面积翻倍面积比、距离变换脊线分水岭分割

这张表是整个方案的地基。它决定了后面每一个结构元的尺寸、每一个阈值的量纲,也决定了答辩时你能不能解释清楚“为什么这个参数取 9 而不是 5”。

尺度换算要先做。假设相机视场覆盖 60 mm × 45 mm,传感器是 1280 × 960,那么空间分辨率约为 21 px/mm,也就是单个像素对应约 0.047 mm。药片直径按 8 mm 算,成像直径约 170 px;最小缺陷按 0.3 mm 算,约 6 px。这两个数字直接给出了后面所有参数的下界:结构元半径不能超过缺陷尺度的一半(否则缺陷被抹平),也不能小于噪声尺度(否则噪声去不掉)。6 px 的缺陷,结构元半径取 3 px 是上限,取 9 px 就一定检不出来。

2.2 形态学管区域,边缘算法管轮廓

形态学和边缘算法经常被当成二选一,实际上它们解决的是两个层次的问题。形态学的本质是用一个已知形状的结构元去探测图像中“能容纳这个结构元的位置”,它输出的是区域级的判断:这块区域还在不在、有没有被腐蚀掉、有没有被填上。开运算能去掉比结构元小的亮点,闭运算能填掉比结构元小的暗洞,顶帽变换提取的是“比背景亮且尺度小于结构元”的成分,底帽变换提取的是“比背景暗且尺度小于结构元”的成分。这套操作对黑点、亮斑、细小崩茬非常有效,而且完全可解释,改了哪个参数会影响哪类缺陷,一目了然。

边缘算法解决的是另一件事:目标与背景之间的梯度在哪里。Sobel 用一阶差分加平滑,抗噪好但定位粗;Canny 用双阈值加非极大值抑制,定位准但阈值敏感。药片的崩边、缺角这类缺陷,本质上是轮廓的局部形变,用边缘提取拿到闭合轮廓,再算圆形度、凸包面积比、轮廓到拟合圆的径向偏差,比纯区域方法灵敏得多。

两者组合的顺序通常是:形态学先做粗分割拿到药片区域,把背景和噪点清干净;再在这个区域的掩膜内做边缘提取,避免背景纹理产生大量伪边缘;最后把边缘算出来的形状指标和形态学算出来的区域指标一起送进判定规则。先区域后边缘,是这个方案里最重要的一条顺序约束,反过来做会得到一堆无法归属到具体药片的碎边缘。

2.3 与深度学习方案的边界在哪里

这里必须说清楚选型边界,否则方案写出来是站不住的。深度学习方案在缺陷形态多变、样本充足的场景下确实更强,但它有三个前提:每种缺陷至少几百张标注样本、一个能跑训练的显卡环境、以及一个能解释“为什么判这一片不合格”的交付方式。在很多单工位、小批量、缺陷样本极难收集的项目里,这三个前提一个都不满足——你可能一个月都攒不到 50 张真实崩边样本。

形态学加边缘的路线,优势在于零训练、可解释、参数有物理量纲、单帧耗时在毫秒级。劣势也很明确:对光照变化敏感、对未定义过的缺陷类型没有泛化能力、参数需要人工调。所以比较务实的做法是分层——用形态学做第一道全检,把所有“疑似”都拦下来;对拦下来的疑难样本落盘存图,积累到一定数量后再考虑是否引入分类模型做二次判定。这套结构在毕设和课设里也更好讲:每一层都有明确职责,每一个参数都能追溯到物理尺度。

注意:不要在方案里写“准确率 99%”这种没有测试集支撑的数字。验收章节会讲怎么用混淆矩阵把这个数算出来。

3. 总体架构设计:采集、校正、分割、判定四层与六个模块

3.1 四层流水线与数据流

整个处理链路按职责切成四层,层与层之间只传约定的数据结构,不互相越界。这样做的直接好处是:调参的时候你知道该改哪一层,出问题的时候你知道该看哪一层的中间图。

第一层是采集层,负责触发、曝光、图像入内存,输出原始灰度图和一帧时间戳。第二层是校正层,负责背景亮度校正、镜头畸变校正、必要时做 ROI 裁剪,输出动态范围归一化后的图像。第三层是分割层,负责二值化、形态学清理、连通域分析、粘连分离,输出药片实例列表(每个实例带掩膜和质心)。第四层是判定层,负责逐实例计算几何特征、比对规则表、输出 OK/NG 与缺陷码。

六个模块分别是:采集与标定模块、预处理模块、有无检测模块、粘连分离模块、缺陷特征模块、判定与输出模块。模块划分的原则是“一个模块只回答一个问题”,比如有无检测模块只回答“这一板够不够数”,它不关心某一片是不是崩了边;缺陷特征模块只输出特征向量,它不做最终裁决。裁决权统一收在判定模块,规则表放在一个独立的配置文件里,这样换产品型号时只改配置不改代码。

3.2 采集与标定模块的关键参数

采集环节的投入回报比远高于算法环节。血泪经验是:一个算法调了两天没进展的问题,换一个打光方式十分钟就解决了。药片通常是白色或浅色、表面有弧度、还可能带糖衣反光,最忌讳的是单侧直射光——它会在药片边缘形成一圈高亮弧,把崩边完全盖住。

比较稳的做法是环形漫射光加低角度补光组合:环形光提供均匀的正面照明,低角度光把边缘的凹陷和缺口打出阴影,让崩边在灰度上现形。相机用黑白工业相机即可,彩色信息对这个任务没有增益,反而增加通道间的不一致。曝光时间要压到运动模糊可以忽略的量级,如果产线是步进停顿拍照,曝光可以放宽到 5~10 ms;如果是飞拍,通常要压到 1 ms 以内并配脉冲光源。

标定模块要做两件事:像素当量标定和位号映射。前者用标准棋盘格或已知直径的量规球算出 mm/px,后者在首次上料时用一张满料图做模板,把每个泡罩位号的质心坐标记下来,后续每帧都和模板做位置匹配。有了位号映射,“有无检测”就从“数个数”升级成“报出具体缺第几号位”,这在现场排查时价值很大。

3.3 判定规则表与状态机

判定层不建议写成 if-else 堆叠,而是做成一张规则表,每行一条规则,包含特征名、比较符、阈值、超限后的缺陷码和严重等级。这样做的原因是:不同客户对缺陷的容忍度不同,有的允许单处 0.3 mm 以内的崩边,有的要求零容忍,规则表能把这种差异从代码里剥离出来。

规则 ID特征量判据缺陷码等级
R01实例面积 / 标准面积< 0.85碎片致命
R02实例面积 / 标准面积> 1.60粘连致命
R03圆形度 4πA/P²< 0.90崩边严重
R04凸包面积比 Solidity< 0.93缺角严重
R05底帽缺陷面积> 200 px²黑点轻微
R06顶帽缺陷面积> 300 px²亮斑异物轻微
R07位号覆盖数< 设计数缺片致命

状态机部分要处理一件事:单片连续多帧的判定结果怎么合并。如果产线是停顿拍照,一帧即一片,直接输出即可;如果是连续飞拍,同一片会出现在多帧里,需要用质心跟踪去重,只在药片离开视野时输出一次结论,避免同一次缺陷被重复计数。

4. 核心实现:从灰度校正到形态学缺陷提取的完整 MATLAB 代码

4.1 预处理与光照校正

原始图直接二值化,在环形光下几乎必然翻车——中间亮、四周暗,Otsu 算出来的全局阈值会把暗区的药片整个判成背景。所以预处理的第一步不是滤波,是背景亮度校正。

% 读入并统一到单通道灰度,避免不同批次图像的通道差异带来阈值漂移 I = imread('pill_01.png'); if size(I,3) == 3 G = rgb2gray(I); else G = I; end G = im2double(G); % 背景校正:用大尺度结构元做闭运算估计缓变背景,再逐像素相除 % 结构元半径必须远大于药片半径,否则背景估计会被药片本身污染 seBg = strel('disk', 60); bg = imclose(G, seBg); Gc = imdivide(G, bg + eps); % eps 防止除零 Gc = mat2gray(Gc); % 拉伸回 [0,1] % 轻度高斯滤波压制传感器噪声,sigma 取 1 左右,不要过大 Gf = imgaussfilt(Gc, 1.0);

strel('disk', 60)这个半径的取法有讲究:它决定了背景估计的“尺度”。半径太小,闭运算会把药片当成背景的一部分,校正后药片被削平;半径太大,计算变慢但对缓变光照的跟随变差。经验取值是药片成像半径的 3~5 倍,8 mm 药片在 21 px/mm 下半径约 84 px,60 其实偏小,实际工程里更稳的是 200 以上,或者干脆用imflatfield这类专用函数。这里保留 60 是为了说明原理,真实参数要按你的分辨率重算。

imgaussfilt的 sigma 同理:它是噪声尺度和缺陷尺度之间的折中。sigma 越大去噪越狠,但 0.3 mm 的小崩边也会被一起糊掉。

4.2 有无检测:二值化、连通域与计数

有无检测的目标非常朴素:数清楚这一板上有几片药,少了就报警。

% 全局阈值分割,Otsu 在背景校正之后才可靠 level = graythresh(Gf); BW = imbinarize(Gf, level); % 开运算去小噪点,闭运算填掉片面反光造成的小孔洞 BW = imopen(BW, strel('disk', 3)); BW = imclose(BW, strel('disk', 5)); BW = imfill(BW, 'holes'); % 面积过滤:小于最小药片面积 60% 的连通域视为碎片或噪声 pxPerMm = 21; % 由标定得到,单位 px/mm stdDiaMm = 8.0; % 标准药片直径 stdAreaPx = pi * (stdDiaMm/2)^2 * pxPerMm^2; BW = bwareaopen(BW, round(0.60 * stdAreaPx)); cc = bwconncomp(BW); nPill = cc.NumObjects; expect = 12; % 该板设计位号数 isFull = (nPill == expect);

这段的关键是bwareaopen的阈值。它必须和标定挂钩,而不是写死一个“50”。写死像素阈值的后果在第 5 章会专门讲——换个镜头,整套参数全部作废。另外imclose的半径 5 也有边界:如果崩边缺口宽度只有 6~8 px,半径 5 的闭运算会把它部分填平,导致后面的形状判定失效。这就是为什么开闭运算的半径必须小于最小缺陷尺度的一半。

4.3 粘连分离:距离变换加分水岭

两片药贴在一起时,bwconncomp只会给出一个连通域,面积是标准值的两倍左右,计数永远少一片。这时候必须做实例级分离。

% 距离变换:每个前景像素到最近背景的距离,粘连处形成鞍点脊线 D = bwdist(~BW); D = -D; % 取负,把脊线变成"谷" D(~BW) = -Inf; % 背景强制为极小值,保证分水岭不会跨背景扩散 D = imhmin(D, 3); % h-minima 抑制浅极小值,防止过分割 L = watershed(D); L(~BW) = 0; % 只保留前景内的标记 % 按标记统计实例,并过滤掉面积过小的过分割碎块 stats = regionprops(L, 'Area', 'Centroid'); valid = find([stats.Area] > 0.60 * stdAreaPx); nPill = numel(valid);

imhmin的那个 3 是这套流程里最需要试的参数。取 0 时,距离变换里每一个微小的局部极值都会变成一个分割点,一片完整的药会被切成七八块;取太大(比如 10),粘连的两片又分不开。经验做法是在一批已知粘连样本上从 1 开始逐个试,找到刚好能分开又不产生过分割的值,然后固定下来。

4.4 缺陷检测:顶帽底帽与边缘几何量

区域分割拿到实例之后,缺陷检测在实例掩膜内进行。亮缺陷用顶帽,暗缺陷用底帽,轮廓类缺陷用边缘。

% 结构元必须大于最大缺陷尺度、小于药片半径 seDefect = strel('disk', 9); topHat = imtophat(Gf, seDefect); % 亮缺陷:反光点、崩茬、异物 blackHat = imblackhat(Gf, seDefect); % 暗缺陷:黑点、油污、裂纹阴影 % 两个方向分别二值化后合并,阈值按对比度归一化后给 maskDefect = imbinarize(topHat, 0.12) | ... imbinarize(blackHat, 0.10); maskDefect = bwareaopen(maskDefect, 30); maskDefect = maskDefect & BW; % 只在药片区域内统计,排除背景干扰 % 轮廓类缺陷:先平滑再取边缘,避免噪声被放大成"裂纹" eSobel = edge(Gf, 'sobel', 0.08); eCanny = edge(Gf, 'Canny', [0.05 0.15]); eCanny = eCanny & imdilate(BW, strel('disk', 2)); % 限定在药片邻域

参数说明:顶帽阈值 0.12 和底帽阈值 0.10 不是对称的,因为药片本身是浅色,暗缺陷的对比度通常比亮缺陷更明显,阈值可以给得更低一些;bwareaopen的 30 px² 对应约 0.07 mm²,是按“小于这个面积的斑点不判缺陷”的工艺口径换算来的。这两个数字都必须跟着你的标定走。

特征计算用regionprops一次性拿齐,注意圆形度不是内置属性,要自己算:

% 逐实例计算形状特征 props = regionprops(L, 'Area', 'Perimeter', 'Solidity', ... 'MajorAxisLength', 'MinorAxisLength', 'Centroid'); for k = 1:numel(props) A = props(k).Area; P = props(k).Perimeter; circularity = 4 * pi * A / (P^2); % 理想圆为 1 areaRatio = A / stdAreaPx; % 与标准面积之比 aspect = props(k).MajorAxisLength / max(props(k).MinorAxisLength, eps); % 判定:三条硬规则,任一触发即为 NG if areaRatio < 0.85 || areaRatio > 1.60 || circularity < 0.90 verdict = "NG"; else verdict = "OK"; end end

Perimeter在 MATLAB 里是沿像素边界估算的,对噪声比较敏感,通常要先对掩膜做一次轻度开运算再算周长,否则圆形度会系统性偏低。Solidity是区域面积与凸包面积之比,对缺角特别灵敏,但对崩边的响应不如圆形度——这也是为什么两条规则要同时保留,它们各自覆盖不同的缺陷形态。

5. 避坑与常见问题排查:五个让误检率翻倍的细节

5.1 全局阈值在光照不均下翻车

现象:同一批图里,靠中心的药片判 OK,靠边缘的整片被判成背景,缺片报警连续不断。

原因:Otsu 假设直方图是双峰的,而环形光造成的亮度梯度把背景拉成了一条宽坡,双峰假设不成立,算出来的阈值偏向亮区。

解决:先做背景校正再二值化,或者直接改用局部自适应阈值imbinarize(Gf, 'adaptive', 'ForegroundPolarity', 'bright', 'Sensitivity', 0.45)。自适应阈值的Sensitivity在 0.4~0.5 之间通常够用,调高会引入背景噪点。我在项目里一般两张图对比着看:校正后的图和原图的直方图放一起,如果双峰明显了,才敢用全局阈值。

5.2 结构元尺寸凭感觉定,崩边被磨平

现象:肉眼能看到的缺口,算法判 OK;把开运算半径从 3 调到 5,检出率直接掉一半。

原因:形态学是“按结构元形状筛子过一遍”,凡是能被结构元容纳的凹陷都会被填平。结构元半径一旦超过缺陷尺度的一半,缺陷在数学上就不存在了。

解决:把结构元半径和最小缺陷尺度绑死,写进配置:seRadius = floor(0.5 * minDefectPx),并在代码里加断言assert(seRadius < minDefectPx/2)。另外把“清理”和“保留”用的结构元分开——去噪用 3,填孔用 5,两者不要混成一个参数。

5.3 边缘算子直接作用在原始图上,噪声变成裂纹

现象:好好的药片被判成裂片,NG 图上显示一条细长的“裂纹”,放大一看是传感器噪点。

原因:Sobel 和 Canny 都是基于梯度的,梯度对噪声的放大是一阶的。原始图的噪声标准差如果有 3 个灰度级,边缘响应里就会冒出大量伪边缘,而裂纹的判据恰恰是“细长”,伪边缘完美命中。

解决:边缘提取前必须先滤波,imgaussfilt(Gf, 1.0)是最低要求;同时给边缘结果加面积和长度约束,bwareaopen(eCanny, 20)滤掉零星点,再用regionprops过滤掉长宽比不合理的连通域。血泪经验是:Canny 的双阈值不要用默认的[0.0625 0.1563],一定按你的图重新给,低阈值给高了漏检,给低了满屏噪点。

5.4 粘连两片当成一片,计数永远少一片

现象:满料板应该 12 片,算法报 11 片,但看图明明是满的。

原因:两片药边缘接触,二值化后合并成一个连通域,NumObjects少 1。面积规则 R02 本来能拦住,但如果只做了计数没做面积校验,就会漏。

解决:计数环节和面积校验必须同时存在,任一条命中都要报警。分水岭参数imhmin的 h 值要在已知粘连样本上标定;另外可以在分割后加一步“面积合理性检查”:如果一个连通域面积超过 1.6 倍标准值,无论分水岭有没有分开,都直接判粘连,不做二次猜测。

5.5 阈值用像素写死,换镜头全部推倒

现象:实验室调好的参数,搬到产线换了个镜头,全部失效,误检率 30%。

原因:所有面积阈值、结构元半径、bwareaopen参数都是以像素为单位写死的,而像素的物理含义随分辨率、工作距离、镜头焦距变化。

解决:把所有像素量纲的参数改成由pxPerMm推导,配置里只写物理量(mm、mm²),代码启动时统一换算。同时把标定结果和配置文件绑定成一份“产品档案”,换型时切换档案而不是改代码。那条后悔药就是:任何以像素为单位的常数,都不允许出现在业务代码里。

6. 验收与答辩:用一组可复现的指标证明方案站得住

验收不是“跑几张图看着都对”,而是要在固定测试集上给出一组可复现的数字。我一般会准备三类样本:良品 200 片以上、每类缺陷各 30 片以上、以及一批“边界样本”(刚好在公差边缘的崩边、轻微反光的亮斑)。边界样本最能暴露阈值设得过松还是过紧。

指标就四个,一张表说清楚:

指标计算方式目标值说明
漏检率漏检 NG 数 / 实际 NG 总数< 1%致命缺陷必须为 0
误检率误判 NG 的良品数 / 良品总数< 3%直接决定产线停线次数
重复性同一片重复拍 20 次,判定一致的比例> 98%检验阈值是否卡在临界点
单帧耗时从读图到输出判定的平均时间< 节拍要求含所有形态学与边缘运算

重复性这一项最容易被忽略,但它恰恰是答辩时最能加分的地方。做法很简单:固定一片良品和一片带轻微崩边的 NG 片,各连拍 20 次,看判定结果有没有跳变。如果有跳变,说明阈值正好压在特征的分布中心上,必须把阈值往安全侧挪,或者增加一条辅助规则做交叉验证。

答辩陈述建议按“问题—方案—证据”三段走。先讲清楚缺陷清单和尺度换算(这一步能证明你懂物理量纲),再讲四层流水线和规则表(证明你有工程结构),最后把混淆矩阵和重复性曲线放出来(证明方案被验证过)。不要花时间讲形态学的数学定义,评委更想听的是“你为什么用半径 9 而不是 5”。

还有一个具体技巧:把所有中间结果图存成一张 montage,原图、校正图、二值图、距离变换、分割标记、缺陷掩膜、边缘图排成一行。答辩现场任何一张图出问题,你能在三秒内定位到是哪一层。这套可视化我在每个项目里都会保留,因为它省下的排查时间远超写它的时间。

从那以后我每次做检测类方案,都强制走一遍“标定—尺度换算—参数配置化—重复性测试”这四步,哪怕项目再小也不跳过,因为跳过的那一步一定会在验收当天变成翻车现场。这份 MATLAB 实现的完整工程(主流程脚本、参数配置、示例图与中间结果可视化)可以直接拿去跑通再改成你自己的产品型号,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询