简介这是一份基于Matlab实现的图像斑点检测源码包集成了完整源码、测试图像与配套运行说明面向计算机、电子信息工程、数学等专业学生用于课程设计、期末大作业或毕业设计中的算法学习与功能参考。压缩包内共6个文件包括3个脚本、2张测试图片与1个文本说明脚本对应核心算法、结果可视化与主流程测试图片展示向日葵与巡逻场景体现不同纹理背景下的检测效果文本说明提供环境配置与操作指引。整个压缩包仅157KB体量小巧已有291人学习下载。借助代码与图像读者可复现斑点检测流程理解高斯拉普拉斯LoG算子在图像特征提取中的应用并通过调参体会检测尺度与阈值的影响为课程任务或后续算法扩展提供参照。需要注意的是源码为参考资料使用者需具备一定Matlab编程基础自行调试与适配。1. 斑点检测不是调高灵敏度而是先选对尺度做工业视觉这几年让我印象最深的一个反直觉结论是用MATLAB做斑点检测把Sensitivity从0.85调到0.9得到的一般不是召回率提升而是一堆被边缘碎片拼成的假圆。真正的圆形目标早就被尺度窗口拒绝了。斑点检测的价值并不在于“识别出目标”而在于先稳定地把目标从背景、噪声、光照变化里分离出来之后不管你接的是计数、测量还是深度学习的二次分类都会简单很多。这里要讲的完整链路集中在MATLAB的Image Processing Toolbox和Computer Vision Toolbox里从尺度空间原理讲到imfindcircles和detectMSERFeatures的选型再给出可直接运行的脚本、参数表和验证方法最后落到货物空缺检测这类真实任务上。2. 从灰度图像到尺度空间LoG与DoG如何确定斑点半径2.1 拉普拉斯算子的噪声放大问题直接对图像求二阶差分会放大高频噪声使响应图变成一锅盐。所以在实际实现中总是先做高斯平滑再求拉普拉斯这个操作被记作高斯拉普拉斯LoG。对图像f(x,y)尺度为σ时其定义为L(x,y;σ) σ² [∂²(G(x,y;σ)⊗f)/∂x² ∂²(G(x,y;σ)⊗f)/∂y²]前面的σ²是尺度归一化系数用于保证不同σ下响应幅度可比。LoG核具有中心正、四周负的形状因此当σ与斑点半径匹配时在斑点中心会产生最大响应的负极值bright斑点或正极值dark斑点。这里不做公式推导但有一个常用经验关系对于理想圆形亮斑其半径r与σ的匹配关系近似 r √2σ。这个关系是后续设置imfindcircles中R范围的基础。2.2 差分高斯DoG为什么成为实际实现中的主流LoG的计算量偏大好在从一个高斯尺度到下一个尺度的差异可以近似归一化的LoG。将两个相邻尺度的高斯响应相减得到DoG响应D(x,y;σ) G(x,y;kσ)⊗f − G(x,y;σ)⊗fDoG与LoG的差别只是一个与σ无关的比例因子(k−1)。当k→1时两者在数学上等价。实际使用中k取1.6或2时极值位置基本不变但计算适合用积分图或快速高斯滤波实现。这也是SIFT检测特征点使用DoG的原因。在MATLAB中虽然imfindcircles没有直接暴露DoG参数但它的PhaseCode方法内部也包含类似的多尺度差分响应策略。如果你需要自定义无法用圆形假设表达的斑点可以手动构造DoG金字塔再对响应图做三维非极大值抑制。下面给一个标定半径用的最小脚本它可以帮你为特定图像选择σ区间。% 构造已知半径为 r 的合成圆盘观察归一化响应峰值 [xx, yy] meshgrid(-25:25, -25:25); r 7; blobImg double(xx.^2 yy.^2 r.^2); sigmaList 1:0.25:14; peakResp zeros(size(sigmaList)); for i 1:numel(sigmaList) sigma sigmaList(i); % sigma^2 是对 LoG 的尺度归一化避免小尺度响应被放大 h sigma^2 * fspecial(log, ceil(6*sigma) 1, sigma); resp imfilter(blobImg, h, replicate, conv); peakResp(i) max(abs(resp(:))); % 取最强响应绝对值 end [~, idx] max(peakResp); estRadius sqrt(2) * sigmaList(idx); fprintf(峰值sigma%.2f估计半径%.2f\n, sigmaList(idx), estRadius);这段代码的思路是给定一个半径已知的二值圆盘用不同σ的归一化LoG滤波器与它卷积记录响应峰值。响应最强烈的σ乘上√2就是对当前目标半径的无偏估计。运行后你就能知道新图像上某个直径的颗粒应该把R范围设在哪里。如果真实目标带有软边缘峰值会比理论值偏高需要把R范围上下浮动20%。2.3 LoG响应阈值、局部抑制和R范围的关系斑点检测的准确率由三个因素决定尺度窗口、响应阈值、空间邻域抑制。尺度窗口就是R范围它决定了只允许半径在[minR, maxR]内的候选通过所以缩窄窗口永远是抑制误检的第一策略。响应阈值由Sensitivity控制它并不是一个物理阈值而是内部把LoG响应归一化到[0,1]之后所采取的候选保留比例。将Sensitivity从0.80调到0.85相当于让更多弱响应进入候选列表调到0.90后很多细长边缘的角点也会呈现出“伪圆形”的响应。这是初学阶段最常见的误区。另一个参数EdgeThreshold与边缘检测阈值有关它只作用于梯度幅值较大的像素。将其调高会滤除靠近强边缘的候选调低则会保留这些边缘附近的弱斑点。实际使用中如果你发现检出的圆形集中在物体轮廓上优先调高EdgeThreshold而不是降低Sensitivity。下面是σ与目标半径的对应关系参考适合用来建立直觉。目标半径 r (像素)匹配σ ≈ r/√2适合的R范围设置21.4[1 4]53.5[3 8]85.7[5 12]1510.6[10 22]3021.2[20 40]这里有一个和坐标相关的细节图像的行方向对应y列方向对应x所以在后面计算掩码时centers(i,1)是x列坐标centers(i,2)是y行坐标。不要把这个顺序弄反否则画出来的圆会镜像到另一个位置。3. 用imfindcircles在MATLAB中跑通圆形斑点检测3.1 预处理顺序会让检测结果直接翻盘斑点检测最容易被忽略的是输入图像的质量。以灰度化开始如果输入是RGB建议使用rgb2gray而不是直接取一个彩色通道因为工业相机在特殊光线下可能会出现红色通道饱和而绿色通道正常的现象保留RGB分量最大的通道也可能导致不同批次色偏不一致。去噪推荐medfilt2核大小3×3或5×5即可过大的中值滤波会抹掉小斑点边缘。然后是背景光照校正。在货物空缺检测或拍照有暗角的环境中直接调用imfindcircles会把背景渐变误认为半暗斑。常见的做法是先做形态学开运算得到背景估计再用原图减背景最后把亮度平移到中间范围。代码片段如下img imread(capsule_detection.jpg); if size(img, 3) 3 gray rgb2gray(img); else gray img; end gray medfilt2(gray, [3 3]); background imopen(gray, strel(disk, 15)); flatGray gray - background 128; flatGray imadjust(flatGray, stretchlim(flatGray, [0.02 0.98]));这里strel(disk, 15)的半径要大于最大目标半径否则open运算会把目标本身纳入背景估计。stretchlim做2%到98%的拉伸可以避免极端高光点拉高全局对比度。预处理完成后再进入检测器误检数量通常会下降一半以上。3.2imfindcircles返回的三个量和Sensitivity语义调用形式是[centers, radii, metrics] imfindcircles(flatGray, [6 12], ... ObjectPolarity, bright, ... Sensitivity, 0.83, ... EdgeThreshold, 0.13, ... Method, PhaseCode);centers是N×2矩阵radii返回检测半径metrics是每个候选圆在内部算法中的“圆强度”。注意radii的顺序不是按图像从左到右排序的输出顺序与内部检测顺序有关。如果你需要和人工标注做匹配必须先按坐标排序。两个核心参数这里重新强调Sensitivity控制弱响应候选是否进入最终检测集合EdgeThreshold控制在边缘附近的高梯度像素处是否继续搜索。一个常见的调节法则是先给定一个宽松的R范围把Sensitivity设为0.8观察漏检目标如果没有遗漏再把Sensitivity逐步提高0.02直到刚好出现第一个假圆然后回退0.02。这个过程可以用下面的脚本自动完成bestCandidates []; for sens 0.80:0.02:0.94 [c, r, m] imfindcircles(flatGray, [6 12], ... ObjectPolarity, bright, ... Sensitivity, sens, ... EdgeThreshold, 0.13); if isempty(c) continue; end % 用metric做简单的假圆过滤保留强度大于0.4的候选 strong m 0.4; if sum(strong) size(bestCandidates, 1) bestCandidates [c(strong, :), r(strong)]; end end这个循环把灵敏度从0.80扫到0.94并用metric0.4作为后置过滤返回数量最多的一组候选。对于批量标定来说这比重手动画ROI更重复。得到结果后再把Sensitivity固定为至少产生最多有效候选的那个值。下面的参数表列出了常用参数的经验初始值适合作为第一轮试验起点。参数作用经验初始值优先级R尺度窗口按2.3表格换算高ObjectPolarity斑点极性bright中Sensitivity弱响应保留比例0.82~0.86高EdgeThreshold边缘附近候选抑制0.1~0.2中Method检测算法PhaseCode低3.3 从圆检测到掩码后处理才是计数和测量的关键仅仅画出圆不够很多任务需要把斑点的像素级掩码交出去。一个稳的办法是用ndgrid生成圆掩码避免依赖可视化函数在批量脚本中的额外开销。代码如下[rows, cols] ndgrid(1:size(flatGray, 1), 1:size(flatGray, 2)); mask false(size(flatGray)); for i 1:size(centers, 1) mask mask | ((rows - centers(i, 2)).^2 (cols - centers(i, 1)).^2 radii(i)^2); end解释一下centers(i,2)是行坐标ycenters(i,1)是列坐标xndgrid生成的第一个维度是行所以这里用centers(i,2)对应行索引。半径radii(i)的单位是像素掩码直接按照圆的解析方程生成。这样得到的mask可以直接交给regionprops做面积、圆形度、直径统计也可以作为深度学习分割任务的弱标签。4. 不规则斑点用detectMSERFeatures补充检测4.1 MSER为什么能处理灰度不均匀的斑点imfindcircles依赖圆形模板遇到形状不均的细胞、裂纹区域或液体滴痕时半径估计会偏且边界不贴合。MSER最大稳定极值区域采用不同灰度阈值对图像做连通区域提取找到在较大阈值范围内保持面积稳定的连通域因此不需要任何形状假设。它对灰度渐变和局部光照变化非常稳定这是它在文本检测和宽基图像匹配中都被广泛使用的原因。在MATLAB中的接口是[regions, msers] detectMSERFeatures(flatGray, ... ThresholdDelta, 1.5, ... MinArea, 30, ... MaxArea, 1500, ... MaxVariation, 0.3);regions是MSERRegions对象msers是胞数组包含每个区域的具体像素列表。ThresholdDelta是相邻阈值步长值越小候选区域越多MinArea/MaxArea直接限制像素面积MaxVariation是区域稳定程度的阈值越大则越不稳定的区域也能被接受。对于目标半径在5像素左右的斑点面积范围应该是π×25≈78像素所以MinArea取30能挡住一半残破的候选。如果是斑点内部有纹理的细胞团建议把MaxVariation从默认的0.25调到0.5让更多弱稳定区域进入候选。4.2 用几何特征过滤MSER长条形候选MSER输出的区域里常混杂条状的亮度渐变或弯曲的纹理它们虽然“稳定”但不是我们要的斑点。这一步用regionprops提取圆形度、偏心率做区分。圆形度E定义为4πA/P²圆为1越细长的区域越小偏心率越大代表区域越接近直线。过滤代码props regionprops(regions, Area, Perimeter, Eccentricity, Centroid); circleScore 4 * pi * [props.Area] ./ ([props.Perimeter].^2 eps); ecc [props.Eccentricity]; validIdx find(circleScore 0.55 ecc 0.9 [props.Area] 30); centersMser reshape([props(validIdx).Centroid], 2, []);circleScore 0.55意味着允许近似圆形状如果我们检测的是半透明的药物颗粒可以把阈值放到0.45。ecc 0.9用来排除条状气泡但对于圆斑很少干扰。注意regionprops的Perimeter对于极不规整的边界可能会高估所以这个过滤只作为初筛不要单独完全依赖圆形度。4.3 两种检测器的结果如何合并去重仅用MSER检测小斑点时常见的问题是输出区域互相包含一个斑点被拆成多个同心区域。原因在于MSER对于同一稳定区域在不同阈值的连续范围内都可能输出对应的区域。所以合并时要按中心距离和面积重叠比例去重。一个实用思路是先给MSER区域计算出等效半径然后与imfindcircles结果一起按中心距离去重。allCenters [centersMser; centers]; % MSER结果在上imfindcircles结果在下 allRadii [sqrt([props(validIdx).Area] / pi), radii]; % 等效圆半径 keepFlag true(size(allCenters, 1), 1); for i 1:size(allCenters, 1) if ~keepFlag(i), continue; end for j i1:size(allCenters, 1) d norm(allCenters(i, :) - allCenters(j, :)); if d 0.6 * (allRadii(i) allRadii(j)) keepFlag(j) false; end end end finalCenters allCenters(keepFlag, :); finalRadii allRadii(keepFlag);0.6这个系数是一个经验值当两个区域重叠较大时保留先放在后面的检测器结果。通常我们更信任imfindcircles的半径估计所以把它放在后面这样MSER被去重掉。如果场景中圆形斑点占主导可以只用imfindcirclesMSER结果反而会引入相邻粘连区域。4.4 两种方法对比选择表判断维度imfindcirclesdetectMSERFeatures形状假设圆无尺度控制R范围明确MinArea/MaxArea像素面积亮度均匀性要求较高较低参数敏感度敏感一般运行速度快中等典型用途货架商品、铆钉、细胞核斑点不均、液体飞溅、纹理缺陷实际项目中我一般先用imfindcircles跑基线如果漏检目标带有明显的不规则边界再叠加MSER。两者同时用时要格外注意区域重叠否则统计数量会重复。5. 用合成图验证调参结果再落到货物空缺检测5.1 用已知位置生成合成图计算precision/recall在换用新相机或新产线时我最怕的是从肉眼判断检测效果因为人眼对“差不多”很宽容而工程验收需要可量化的数字。做法是生成一张包含已知位置和半径的合成图片然后用预处理和检测器跑一遍把输出和被标记的真值做匹配。代码synthetic zeros(256, 256); gtCenters []; for k 1:15 r randi([6 10]); cx randi([20 236]); cy randi([20 236]); [X, Y] meshgrid(1:256); blobMask (X - cx).^2 (Y - cy).^2 r^2; synthetic(blobMask) 180; gtCenters [gtCenters; cx cy r]; end synthetic imnoise(uint8(synthetic), gaussian, 0, 0.005);随后调用检测脚本按中心点距离判断匹配如果检测中心到某一真值中心的距离不大于该真值半径加3像素计为正确检测。计算precision和recall时precision 正确检测数 / 检测总数recall 正确检测数 / 真值总数。注意这里meshgrid生成的坐标是列在前行在后而检测器输出centers也是列在前行在后所以直接做欧氏距离时要用同一个坐标系。5.2 光照不均面积很大时先上顶帽变换而不是超分辨率增强遇到整幅图左侧亮右侧暗的场景很多新手会先做对比度拉伸结果暗区的真实斑点被压到阈值以下。正确的处理顺序是先顶帽变换去掉背景光照再做亮度和对比度校正。“图像超分辨率重建”通常是在分辨率不足时才需要而不是为了应付光照。如果目标直径只有3像素而相机分辨率摊到不到2倍像素此时直接放大图像会引入边界伪边缘让Sensitivity不得不调高反而增加误检。我的经验是先检测发现漏检是因为目标太小再做超分超分后的R范围要按放大倍数重新标定σ也应该对应变大。实际可用imresize的cubic插值放大2倍然后用5.1的合成图标定脚本重新估算R范围。5.3 货物空缺图像检测的参数固化和输出数量统计货物空缺检测是斑点检测在零售和仓储行业比较常见的落地场景。固定摄像头拍摄货架每格货物在图像中是一个圆形或近似圆形轮廓。由于相机角度固定同一货物的半径波动很小算法参数应当固化而不是每次让系统自动扫全参数。先采集50张代表性图像人工标注货物中心点和半径然后用前面的循环扫描法确定最佳Sensitivity和EdgeThreshold把这两个值写到配置文件里。检测完成后用掩码统计面积mask false(size(flatGray)); for i 1:size(centers, 1) mask mask | ((rows - centers(i, 2)).^2 (cols - centers(i, 1)).^2 radii(i)^2); end stats regionprops(mask, Area); areaPixels [stats.Area]; diameterPixels 2 * sqrt(areaPixels / pi); validGoods diameterPixels 0.8 * median(diameterPixels) diameterPixels 1.25 * median(diameterPixels); goodsCount sum(validGoods);regionprops计算每个连通域的等效直径再以中位数为参考过滤过小或过大的干扰。当goodsCount小于货位数量时就触发“货物空缺”告警。这里的关键不是把每个圆画得多准而是让面积统计稳定把预处理的背景半径、检测的R范围、Sensitivity三个值固定下来后货物数量统计的重复性可以控制在正负1%。最后把R范围、EdgeThreshold写成配置文件每次换产品只重跑一轮合成图标定脚本这套流程就不会在量产时失控。本文还有配套的精品资源点击获取