
简介面向学习图像处理与机器视觉的读者这套 MATLAB 药片缺陷和有无检测资料围绕工业药品视觉检测场景展开重点解决噪声干扰、复杂背景以及目标有无与好坏判断等问题。资源共 15 个文件包含 5 个 m 脚本、7 张 jpg 示例图以及 doc/docx 说明文档压缩包约 1.78MB便于结合代码、样图与文字材料对照理解。方案中涉及小波去噪抑制高斯噪声、自适应中值滤波处理椒盐噪声并通过形态学重构去除复杂背景再利用边缘算子和二值化提取目标最后扫描匹配并判定药片缺陷与有无。已有 1578 人学习适合希望掌握 MATLAB 形态学与边缘算法落地流程、复现检测思路并积累课程设计或项目案例的读者参考。1. 从一条泡罩包装线说起药片的缺陷和有无检测到底在解决什么泡罩板封好铝箔、装盒之前那一瞬间是最容易出事的地方。某个坑位空着下游封口、装盒、装箱等抽检查出来一整批都要拆。药片的缺陷和有无检测这套 MATLAB 工程解决的就是这一个工位拍一张泡罩板图先判“这个坑位有没有片”再判“有片的话边缘是不是完整的、有没有缺口崩边划痕”。整条链路靠形态学做分割、靠边缘算法做缺陷复核没有网络、没有训练、没有 GPU只要有 Image Processing Toolbox 就能跑。它适合两类人。一类是手上有相机和 MATLAB 授权、想把视觉链路先跑通再谈上线的开发者因为从灰度化到判定的每一步都摊在源码里出问题能一行一行查不是黑匣子。另一类是拿它当形态学与边缘检测练习样例的人参数化做得比较干净改几个阈值就能观察到结果怎么变。下面按“怎么搭的、怎么跑的、哪里会翻车、怎么改成自己的”顺着拆。2. 技术选型与整体架构一条六段式图像处理管线怎么搭2.1 为什么这个场景先不急着上深度学习药片检测听起来像是很典型的“上个小网络就完了”的场景但真到工位上会发现三件事把这条路堵得挺死。第一是样本量产线上正常片要多少有多少缺陷片往往要人工挑几个月才凑够几百张还分崩边、缺口、划痕、双片、空位等好几类每类分下来样本薄得可怜训出来的模型对没见过的那类缺陷基本没有泛化能力。第二是可解释性判定 NG 之后工艺人员会问“凭什么”一个分数 0.73 没法回答但“这个坑位连通域面积 180 像素低于下限 900”能立刻定位到是空位还是分割失败。第三是现场调参换一批药片、换一种包材光源和对比度就变了传统链路的做法是改两个阈值重跑模型的做法是重新标注入重新训。所以这套工程的技术路线定得很明确整板定位用几何网格目标分割用形态学缺陷判定用边缘特征加形状特征深度学习方法留着当二期候选。这个决策背后其实是一句大白话——先把“能不能稳”跑出来再谈“准不准”。形态学里最吃重的三个算子是大津法二值化imbinarize、开闭运算imopen/imclose、连通域分析regionprops边缘部分用 Canny再用凸包减原图算出凹区面积衡量缺口。整套流程串起来是六段采集 → 预处理 → 分割 → 槽位定位 → 判定 → 输出可视化。这个顺序不能随便动。预处理没做干净就二值化后面形态学的结构元素尺寸怎么调都是玄学槽位定位放在分割之后而不是之前是为了不被光照不均带偏。2.2 六段管线各自的输入输出契约把每一段都写成独立的函数函数之间只通过矩阵和结构体传递是这套代码最先立下的规矩。这么做不是为了好看是因为调参时会反复单独跑某一段如果所有逻辑塞在一个脚本里改一个阈值就要从头跑到尾调一次两分钟一天下来调不了几次。各段的契约大致是这样阶段入口函数输入输出关键参数采集loadImage文件路径单通道 / 三通道矩阵无预处理preprocess原始图double 归一化灰度图背景半径、去噪窗口分割segmentChip预处理灰度二值 mask自适应灵敏度、最小面积槽位定位gridROI图像尺寸 行列数ROI 矩形数组行列数、边距比判定judgePresence/judgeDefectmask ROI状态字符串 特征结构体面积上下限、凸度阈值输出renderResult原图 判定表标注图 表格无契约一旦定下来后面所有“为什么这里要这样”的讨论都有了落点。比如preprocess返回的一定是 0 到 1 的 double不是 uint8因为imbinarize的自适应模式对 double 的灵敏度参数响应更线性uint8 下同一个灵敏度值在不同曝光下表现差别很大。这类细节不写死在契约里早晚会在批处理时翻车。2.3 目录结构与函数职责工程目录按职责分层main_inspect.m只做编排不写算法config/params.m集中所有可调参数改参数只改一个文件src/下按管线阶段拆函数。实际结构是这样pill_inspect/ ├── main_inspect.m % 单图入口负责调用链编排 ├── main_batch.m % 批量入口遍历目录写结果表 ├── config/ │ └── params.m % 所有阈值、尺寸、路径集中在这里 ├── src/ │ ├── loadImage.m % 读图 通道归一 │ ├── preprocess.m % 顶帽 去噪 对比度拉伸 │ ├── segmentChip.m % 二值化 形态学清理 │ ├── gridROI.m % 按行列均分生成槽位 ROI │ ├── judgePresence.m % 有无判定 │ ├── judgeDefect.m % 缺陷判定边缘 形状 │ ├── renderResult.m % 画框、打标签、存图 │ └── px2mm.m % 像素/毫米换算阈值全部走它 └── data/ ├── good/ % 良品样本 └── ng/ % 缺陷样本px2mm.m这个看起来最不起眼的文件其实最关键。所有结构元素半径、面积上下限、边缘带宽在配置里一律以毫米写进函数时统一换算成像素。理由是换相机换镜头是产线上最常发生的事分辨率一变原来调好的像素阈值全部失效而毫米阈值不受影响。我见过不止一个项目在换相机之后整条链路全崩最后发现只是结构元素半径从 15 像素变成了 15 个“新像素”。3. 图像预处理链路灰度化、光照不均校正与去噪的先后顺序3.1 通道选择别急着 rgb2gray拿到一张彩色图第一反应是rgb2gray这在大多数场景没问题但在透明泡罩加铝箔反光的组合下会吃掉关键对比度。rgb2gray内部是加权求和权重约 0.2989R 0.5870G 0.1140B绿色权重最大因为人眼对绿最敏感——可相机的 Bayer 排列和产线光源未必按人眼来。如果光源是白光 LED、包材偏蓝蓝色通道里药片和背景的灰度差可能比加权后的灰度图大得多。工程上的做法是先各通道看一眼对比度再决定配置文件里留一个ChannelMode开关function gray loadImage(imgPath, channelMode) %LOADIMAGE 读图并归一化为单通道 double0~1 % imgPath : 图像文件路径 % channelMode : gray | R | G | B | max | sat raw imread(imgPath); if ndims(raw) 3 switch channelMode case gray g rgb2gray(raw); case R g raw(:,:,1); case G g raw(:,:,2); case B g raw(:,:,3); case max g max(raw, [], 3); % 取最亮通道压反光 case sat hsv rgb2hsv(raw); g hsv(:,:,2); % 用饱和度区分药片与白色铝箔 end else g raw; % 已经是灰度图直接透传 end gray im2double(g); end这里ndims(raw) 3那个分支必须写。批处理目录里经常混着灰度图和彩色图早期版本直接写raw(:,:,3)跑到某一张就报数组越界整批任务中断前面跑的全白费。sat这个模式是后来加的糖衣片有颜色、铝箔是白的饱和度图上药片区域明显更亮比灰度图抗反光。参数上要注意max和sat都改变了灰度分布后面的自适应灵敏度要重新标定不能沿用灰度图的 0.45。3.2 光照不均校正顶帽变换怎么用才不糊环形光源打下来视场中心亮、边缘暗这是几乎躲不掉的。直接对全图做一次大津法阈值只能取一个中心区域药片被完整分割边缘区域整片糊成一块背景槽位全判空位。这就是典型的“预处理没做干净就去二值化”。顶帽变换是这里最省事的一招。原理不复杂开运算会把比结构元素小的亮目标抹掉只留背景那原图减开运算结果剩下的就是比背景亮的局部目标。结构元素半径的选择是这个函数唯一的调参点——它必须大于药片本身的最大半径否则药片被当成背景一起减掉同时又必须小于光照渐变的尺度否则背景没被抹干净减完还是不均。function ig flattenIllumination(gray, bgRadiusPx) %FLATTENILLUMINATION 顶帽变换压掉背景光斑与镜头暗角 % gray : 单通道 double0~1 % bgRadiusPx : 背景结构元素半径像素须大于药片最大外接半径 seBg strel(disk, bgRadiusPx); opened imopen(gray, seBg); % 开运算抹掉亮目标只留背景 ig imsubtract(gray, opened); % 原图减背景得到均匀化的目标 ig imadjust(ig); % 重新拉满动态范围 endimsubtract之后动态范围被压缩了最亮的目标也就零点几imadjust不拉一次后面imbinarize的灵敏度参数完全没有意义。这个顺序在不少教程里被省掉了结果就是同样一个灵敏度换一张图效果天差地别。半径怎么定我一般先量药片直径bgRadiusPx round(px2mm(药片直径mm) * 0.75)再用一张边缘区域的样图肉眼确认开运算后药片是否完全消失。如果药片还在说明半径不够如果背景里的浅色条纹也被一起抹掉说明半径太大需要往下调。提示顶帽变换只对“亮目标暗背景”有效。如果光源是背光、药片在亮背景上呈暗色要用底帽变换imbothat两者不能混用。3.3 去噪与对比度拉伸的先后顺序药片表面有压片麻点铝箔上偶尔有碎屑相机本身有传感器噪声。这些噪声对形态学的影响比对边缘算法还大——一个孤立噪点在二值化后会变成一个小连通域开运算能去掉但如果噪点恰好贴着药片边缘就会把药片轮廓咬出一个假缺口直接判 NG。去噪放在顶帽之后、二值化之前。顺序反过来也能跑但顶帽的结构元素会把噪声点也一起放大压在后面的滤波要把窗口开得很大边缘一起糊掉。中值滤波和高斯滤波二选一判断依据是噪声类型压片麻点、铝箔碎屑这类椒盐噪声用中值传感器噪声这类高斯噪声用高斯。function id denoiseChip(i, mode, winSize, sigma) %DENOISECHIP 去噪按噪声类型切换滤波器 % i : 单通道 double % mode : median 压片麻点/碎屑 | gauss 传感器噪声 % winSize : 中值滤波窗口边长奇数常用 3 或 5 % sigma : 高斯核标准差常用 1.0~1.5 switch mode case median id medfilt2(i, [winSize winSize], symmetric); case gauss id imgaussfilt(i, sigma, FilterSize, 2*ceil(3*sigma)1); otherwise error(未知去噪模式: %s, mode); end endmedfilt2的第三个参数symmetric是边界处理方式用对称填充替换默认的零填充。零填充会在图像四边造出一圈黑边如果泡罩板贴边拍摄边缘槽位会被这圈黑边影响判出莫名其妙的 NG。这个坑我在第一次跑满板图时就踩了板子右下角两个槽位永远报缺陷查了半天发现是补零边界。对比度拉伸放在去噪之后。如果先做imadjust再做中值滤波拉伸会把噪声对比度也放大中值窗口得跟着变大药片边缘跟着钝。imadjust之后如果还想再强一点可以用adapthisteq限制对比度自适应直方图均衡但它对噪声非常敏感必须严格放在去噪之后且ClipLimit不要超过 0.01。4. 形态学与边缘算法核心从二值化到缺陷判定的落地4.1 二值化与形态学开闭把药片从背景里抠出来二值化这一步全局大津法在光照均匀时够用但实际产线上顶帽变换之后仍然会有局部明暗残留全局阈值会漏掉一部分边缘槽位。用自适应模式更稳function [mask, bwRaw] segmentChip(gray, sens, minAreaPx, seOpenPx, seClosePx) %SEGMENTCHIP 自适应二值化 形态学清理 % gray : 预处理后的单通道 double % sens : 自适应灵敏度0~1越大越容易判为前景 % minAreaPx : 最小保留面积像素²滤掉碎屑 % seOpenPx : 开运算半径像素去毛刺 % seClosePx : 闭运算半径像素补缺口 bwRaw imbinarize(gray, adaptive, ... ForegroundPolarity, bright, ... Sensitivity, sens); % 开运算先腐蚀掉贴着轮廓的毛刺和细碎噪点 mask imopen(bwRaw, strel(disk, seOpenPx)); % 闭运算再膨胀回来填平药片内部的小孔洞 mask imclose(mask, strel(disk, seClosePx)); % 面积滤波小于最小面积的连通域直接丢弃 mask bwareaopen(mask, minAreaPx); % 填洞药片表面的刻字凹槽会在 mask 里留洞 mask imfill(mask, holes); end几个参数的含义值得说清楚。Sensitivity在自适应模式下不是阈值本身而是控制局部阈值相对局部均值的偏移量越大越倾向于把像素判成前景。药片反光强的时候这个值要往下调糖衣片偏暗的时候要往上调实测下来 0.4 到 0.6 之间能覆盖大部分情况。开运算和闭运算的顺序不能反。先开后闭是先去掉外轮廓的毛刺再补内部孔洞反过来先闭后开闭运算会把两个靠得近的药片连成一片开运算再也分不开。这个顺序问题在药片间距小的时候尤其明显。imfill(mask,holes)这一句是必须的。药片上的刻字、糖衣的凹坑在二值化后会变成内部孔洞不填掉的话regionprops算出来的面积偏小本来合格的药片会被判成面积不足。这个坑很隐蔽因为肉眼看 mask 图几乎看不出洞只有面积数值对不上才发现。4.2 有无检测网格 ROI 加面积区间判定有无检测和缺陷检测最好拆成两套逻辑因为它们的失败模式完全不同。有无检测要回答的是“这个坑位该不该有东西”缺陷检测要回答的是“这个东西本身完不完整”。合在一起写一个空位会被判成“100% 缺失的缺陷”报错信息就没法看了。槽位定位不做全图连通域用几何网格。理由是全图连通域在铝箔反光、泡罩破损时会粘连一粘就少一个槽位整板判定错位。几何网格的前提是相机固定、板子定位靠机械挡块实际产线上这个前提基本成立。function rois gridROI(imgSize, rows, cols, marginRatio, shrinkRatio) %GRIDROI 按行列均分生成槽位 ROI % imgSize : [H W] % rows, cols : 泡罩板行列数 % marginRatio : 四周留白比例避开板边 % shrinkRatio : 单槽收缩比例避免吃到相邻槽位 H imgSize(1); W imgSize(2); mh round(H * marginRatio); mw round(W * marginRatio); cellH (H - 2*mh) / rows; cellW (W - 2*mw) / cols; rois struct(Row, {}, Col, {}, Rect, {}); k 0; for r 1:rows for c 1:cols k k 1; x0 round(mw (c-1)*cellW cellW*(1-shrinkRatio)/2) 1; y0 round(mh (r-1)*cellH cellH*(1-shrinkRatio)/2) 1; w floor(cellW * shrinkRatio); h floor(cellH * shrinkRatio); rois(k).Row r; rois(k).Col c; rois(k).Rect [x0, y0, w, h]; % [x y w h]imcrop 约定 end end endRect用的是imcrop的[x y w h]约定不是 MATLAB 索引的[row col]这点一定要在注释里写死否则后面取 ROI 的时候行列颠倒切出来的全是横条排查起来很费时间。有了 ROI有无判定就是切出来数面积function [state, areaPx] judgePresence(mask, roi, areaLowPx, areaHighPx) %JUDGEPRESENCE 单槽位有无判定 % areaLowPx : 面积下限低于判空位 % areaHighPx : 面积上限高于判粘连/双片 sub imcrop(mask, roi); areaPx nnz(sub); if areaPx areaLowPx state MISSING; % 空位 elseif areaPx areaHighPx state MULTI; % 粘连或双片 else state PRESENT; % 正常有片进入缺陷判定 end end面积上下限从毫米换算正常片面积是 πr²下限取 0.6 倍上限取 1.5 倍。这两个系数看起来宽松但实际调的时候宁可宽一点——过窄会让稍微偏位的正常片被判空位那是最不能接受的误检。4.3 边缘算法判缺陷Canny 边缘加凸度、圆度双指标有片之后的缺陷判定靠三个特征投票圆度、凸度凸包减原图的凹区比例、Canny 边缘密度。单靠任何一个都不稳。圆度用4πA/P²手算正常圆片接近 1崩边之后周长变大、面积变小圆度明显下降。凸度用bwconvhull生成凸包减去原 mask 就是凹进去的部分缺口、崩边都落在这块区域里。Canny 边缘图主要用于两件事一是给出可视化的边缘叠加图给人工复核二是统计药片内部的边缘密度划痕会在内部产生额外的边缘。function [ok, feat] judgeDefect(grayChip, maskChip, p) %JUDGEDEFECT 单颗药片缺陷判定 % grayChip : 药片外接矩形内的灰度子图 % maskChip : 同尺寸的二值 mask % p : 参数结构体见 config/params.m feat struct(); feat.Area nnz(maskChip); % --- 圆度4*pi*A/P^2 --- st regionprops(maskChip, Perimeter); per st(1).Perimeter; feat.Circularity 4*pi*feat.Area / max(per^2, eps); % --- 凸度凹区凸包减原图拿凹进去的面积占比 --- hull bwconvhull(maskChip); concave hull ~maskChip; feat.ConcaveRatio nnz(concave) / max(feat.Area, 1); % --- Canny 边缘只保留药片内部排除外轮廓干扰 --- inner imerode(maskChip, strel(disk, p.EdgeBandPx)); e edge(grayChip, Canny, p.CannyThresh, p.CannySigma); e e inner; feat.EdgeDensity nnz(e) / max(nnz(inner), 1); % --- 三指标投票 --- badCircle feat.Circularity p.CircLow; badConcave feat.ConcaveRatio p.ConcaveHigh; badEdge feat.EdgeDensity p.EdgeHigh; ok ~(badCircle || badConcave || badEdge); endedge的 Canny 模式里第三个参数是阈值可以给标量也可以给[low high]双阈值第四个是高斯平滑标准差 sigma。sigma 越大越抗噪、越吃细边缘划痕这类缺陷本身就细sigma 超过 1.5 基本就没了。我一般从 1.0 起调阈值用[0.08 0.20]这种偏低的组合宁可多检出一些边缘靠后面的EdgeDensity阈值去卡。EdgeBandPx这个参数是腐蚀带宽作用是把药片外轮廓那一圈边缘剔掉。不剔的话外轮廓本身就是一圈强边缘EdgeDensity的基数被抬高划痕带来的增量被稀释掉阈值怎么调都不敏感。这个参数一般取 3 到 5 像素太小剔不干净太大把靠近边缘的崩边也一起剔了。三个阈值CircLow、ConcaveHigh、EdgeHigh没有通用值只能拿良品样本跑一遍把三个特征的分布打出来取下限或上限往外放一点。这部分工作没什么捷径属于血泪经验——凭感觉定阈值上线早晚要被一批没见过的包材教做人。5. 避坑与排查跑不通、误检、漏检的五条实战记录5.1 预处理与分割阶段的三个坑坑一换一批糖衣片后几乎每片都判 NG。现象很直观良品批跑出来满屏红框凹区比例全部超标。原因是糖衣片表面反光比素片强得多顶帽变换之后药片中心过曝边界向外扩了一圈但扩得不均匀凸包减原图算出来一大片假凹区。更麻烦的是过曝区域的灰度和背景拉不开自适应二值化把它吞掉了半个。解决分两步先降曝光或者换漫射光源把药片表面的高光压下去再在judgeDefect之前对 mask 做一次小半径开运算去毛刺strel(disk, 3)把外扩的锯齿边磨平。同时把Sensitivity从 0.45 提到 0.55让过曝区域也能被判进前景。这三处一起改之后糖衣片的误检基本压住了。坑二右下角两个槽位永远报缺陷换了图也一样。现象是位置固定跟药片内容无关。原因是中值滤波默认零填充在图像右下边缘造出一圈黑边黑边和药片之间的过渡带被 Canny 抓成强边缘EdgeDensity直接爆表。解决是把medfilt2的填充方式改成symmetric或者在所有滤波之前先padarray扩一圈再裁回来。前者更省事改一个参数就行。这个坑的隐蔽之处在于它只影响贴边区域如果你的泡罩板四周留白足够可能永远碰不到一旦改成满幅拍摄就立刻暴露。坑三两颗挨得近的药片被判成一片另一个槽位报空位。现象是相邻两个槽位一个面积超标判MULTI一个面积不足判MISSING。原因是铝箔反光把两个槽位的 mask 连成了一条细桥bwareaopen滤不掉因为桥连着的是一大块。解决方向是别依赖全图连通域按 ROI 切分之后再判面积桥被 ROI 边界切断两个槽位各算各的。如果 ROI 切完还是粘可以在 ROI 内先做一次距离变换bwdist加分水岭watershed把粘连目标分开。分水岭容易过分割只在 ROI 内小范围用别上全图。5.2 判定与工程化阶段的两个坑坑四细划痕在 Canny 边缘图上完全消失。现象是肉眼能看到一道浅划痕但EdgeDensity和良品没差别。原因是 Canny 的 sigma 用了 1.8划痕宽度只有两三个像素被高斯平滑直接抹平了。同时双阈值取的是[0.15 0.35]弱边缘全被抑制。解决是把 sigma 降到 0.8 到 1.2双阈值降到[0.06 0.18]把弱边缘尽量保留。但这样噪声也会进来所以EdgeDensity阈值要跟着往上提。划痕这类低对比度缺陷更稳的做法是先做底帽变换imbothat结构元素半径取比划痕宽度略大把暗细线从药片表面提出来再单独统计底帽图的响应强度不跟 Canny 混在一起判。坑五换了个分辨率更高的相机全部阈值失效。现象是结构元素半径还是按老分辨率的像素值写开运算直接把药片腐蚀没了mask 全空。这个坑在前面提过一次这里说具体的处理方式。解决是引入px2mm换算层配置里所有尺寸一律写毫米进函数时换算。同时加一个启动自检跑第一张图时输出“当前分辨率 xx px/mm药片等效直径 xx 像素结构元素半径 xx 像素”跟预期对不上就直接报错退出别让它默默跑完整批。这个自检大概十行代码省下的排查时间是以天计的。注意批量跑之前一定要先跑一张带自检的样图。批处理跑了一半崩掉前面写出的结果文件是不完整的如果下游直接拿这份表做统计错误会被带进下一道工序。6. 二次开发把脚本改成能落地的小工具拿到这套代码之后最该做的第一件事是把散在函数里的硬编码参数全部收进config/params.m用一个结构体传下去。原来的写法里自适应灵敏度、结构元素半径、面积上下限各写在各处调一次参要翻五个文件。收拢之后长这样function p params(pxPerMM) %PARAMS 全部可调参数尺寸类统一以毫米给出并按分辨率换算 p.PxPerMM pxPerMM; p.ChannelMode gray; p.BgRadiusPx round(3.2 * pxPerMM); % 药片最大半径约 3.2mm p.DenoiseMode median; p.DenoiseWin 5; p.Sens 0.45; p.OpenPx round(0.25 * pxPerMM); p.ClosePx round(0.30 * pxPerMM); p.MinAreaPx pi * (2.0*pxPerMM)^2; % 面积下限按等效半径换算 p.SlotAreaLow p.MinAreaPx * 0.60; p.SlotAreaHigh p.MinAreaPx * 1.50; p.CannyThresh [0.08 0.20]; p.CannySigma 1.0; p.EdgeBandPx round(0.35 * pxPerMM); p.CircLow 0.82; p.ConcaveHigh 0.08; p.EdgeHigh 0.16; end第二步是把批处理入口写扎实遍历data/下所有图逐张跑结果落成一张表。表里除了判定结果把三个特征值也一起写进去后面调阈值时直接看这列数据的分布比重新跑一遍快得多。function tbl main_batch(cfg) %MAIN_BATCH 批量检测输出每张图每个槽位的判定与特征 files [dir(fullfile(cfg.dataDir, *.png)); ... dir(fullfile(cfg.dataDir, *.jpg))]; rows {}; for i 1:numel(files) fp fullfile(files(i).folder, files(i).name); gray loadImage(fp, cfg.p.ChannelMode); pre preprocess(gray, cfg.p); mask segmentChip(pre, cfg.p.Sens, cfg.p.MinAreaPx, ... cfg.p.OpenPx, cfg.p.ClosePx); rois gridROI(size(mask), cfg.Rows, cfg.Cols, 0.04, 0.86); for k 1:numel(rois) [state, areaPx] judgePresence(mask, rois(k).Rect, ... cfg.p.SlotAreaLow, cfg.p.SlotAreaHigh); feat struct(Circularity, NaN, ConcaveRatio, NaN, ... EdgeDensity, NaN); if state PRESENT mChip imcrop(mask, rois(k).Rect); gChip imcrop(pre, rois(k).Rect); [~, feat] judgeDefect(gChip, mChip, cfg.p); end rows(end1,:) {files(i).name, rois(k).Row, rois(k).Col, ... char(state), areaPx, feat.Circularity, ... feat.ConcaveRatio, feat.EdgeDensity}; %#okAGROW end end tbl cell2table(rows, VariableNames, {File,Row,Col,State, ... AreaPx,Circularity,ConcaveRatio,EdgeDensity}); writetable(tbl, fullfile(cfg.outDir, result.csv)); endimcrop切mask和切pre必须用同一个Rect否则特征是在错位的图上算的。这种错位不会报错只会让阈值调到怀疑人生。第三步是验证。别拿跑通的图当验证集要拿一批标注好的数据算混淆矩阵重点看两类错误正常片被判 NG 的误检和缺陷片漏过去的漏检。前者影响产线节拍后者影响质量两个都得压。做法是把result.csv里State一列跟人工标注对一遍用confusionchart画出来误检集中在哪个特征上回去单独调那个阈值。最后说一个习惯。从那次右下角槽位永远报缺陷之后我每次改完参数都会先跑一张四周带留白的整板图、一张贴边拍摄的整板图、一张只有空位没有药片的图三张全过再放进批处理。这三张覆盖了边界、满幅、毫无前景这三种最容易翻车的情况跑一遍不到十秒。希望帮到你。本文还有配套的精品资源点击获取