news 2026/9/8 23:50:00

MATLAB图像处理实战:菌落自动计数与分割算法详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MATLAB图像处理实战:菌落自动计数与分割算法详解

简介:这是一份面向生物医学图像处理与机器学习初学者的MATLAB实用项目,目标是从培养皿平板琼脂图像中自动识别并统计细菌菌落数量,替代传统人工计数,提升实验效率并降低主观误差。资源包共含5个文件,总大小仅174KB,其中两个MATLAB脚本分别承担核心计数算法与演示流程,两张示例图像包括原始平板照片和掩膜标记叠加结果,便于直观对照,另附txt授权说明文件。已有839人浏览学习,适合微生物科研人员、高校实验课程师生以及图形界面开发初学者参考。解压后可查看授权信息,运行演示脚本即可在自带图像上复现预处理、分割、形态学处理、连通区域分析和分类计数的完整链路;核心代码支持调整阈值、形态学核尺寸,或进一步替换为CNN分类器,为定制自动化菌落计数系统提供了灵活起点。 先说说我做这个项目的契机。实验室里每天要数培养皿上的菌落,人工数到一百多个就眼花,不同人数的结果还不一样,一份数据统计下来,光数数就要耗掉大半天。后来我直接用MATLAB写了一套自动化计数的图像处理脚本,配合Image Processing Toolbox,从读图到输出数量全程跑下来,一个菌落密集的平板大概几秒钟就能出结果,和人工数的误差控制在百分之几以内。这篇文章就把这套方法的完整思路、核心代码逻辑和踩过的坑都整理出来,给同样被“数菌落”折磨的同行一个能直接上手的参考。

整体内容围绕三件事展开:怎么把彩色菌落图像处理成干净的二值图,怎么把粘连在一起的菌落正确分开,以及怎么让计数结果稳定可靠、不挑拍摄环境。无论你是要处理培养皿照片,还是显微镜下的细菌视野,这套思路都能迁移过去。

1. 整体设计思路:为什么选图像分割这条路

1.1 自动化计数的核心难点在哪

菌落计数的本质,是统计图像中有多少个独立的“目标区域”。听起来很简单,但实际操作里会遇到三个问题:背景不是纯色,培养基可能有气泡、划痕;菌落之间有粘连,一个区域里可能叠着好几个菌落;光照不均匀,有的菌落边缘亮、有的中间亮。我最早试过用颜色阈值分离菌落,但因为培养基颜色和菌落颜色太接近,效果一直不稳定。后来换成了“灰度化 + 形态学增强 + 自适应分割 + 距离变换分水岭”这条经典路线,才把问题理顺。

MATLAB在这一块的优势在于:图像处理的函数库非常全,从读取图像到形态学操作、连通域分析、分水岭分割,全部有现成函数。不需要自己逐像素实现算法,可以把精力集中在参数调优和特征筛选上。另外它自带的Image Segmenter App可以先做交互式探索,把阈值、形态学算子这些参数先试出来,再固化到脚本里,调试效率比纯写代码高很多。

1.2 两种典型场景用什么策略

根据“细菌群”的形态,我把场景分成两类:第一类是培养皿/平板上的菌落,特征是目标大、数量相对少、颜色与背景差异明显,但菌落之间容易粘连。这个场景用“全局阈值分割 + 分水岭”就能解决。第二类是显微镜下的细菌涂抹视野,特征是目标小、数量极多、细胞彼此挨得很近,甚至重叠,这个要更抠细节,先做去噪增强,再用距离变换找种子点,才能分得开。

我的项目以第一个场景为主要对象,但代码里保留了面向第二场景的处理模块,实际跑下来同样适用。本文重点讲第一场景的实现,第二场景的参数调整会在常见问题部分补一句。

2. 图像预处理与前景分割:一切从干净的图开始

2.1 灰度化、去噪与光照校正的具体操作

读入图像后第一件事不是直接二值化,而是先做三步预处理。第一步,把彩色图像转成灰度图,用rgb2gray。这一步会丢失颜色信息,所以更适合菌落灰度与背景有差异的情况,如果你的菌落是有颜色的(比如粉色的假单胞菌),可以考虑在HSV空间里做色调筛选,这个后文会提。第二步,用中值滤波或者高斯滤波去噪,代码就是:

img = imread('plate.jpg'); if size(img, 3) == 3 gray = rgb2gray(img); else gray = img; end % 中值滤波去噪,窗口取3或5,太大会模糊边缘 denoised = medfilt2(gray, [5 5]);

第三步很关键,就是背景不均匀问题的处理。培养皿边缘或底部往往有阴影,直接用全局阈值会把阴影误判成菌落。解决办法是顶帽变换:先做形态学开运算得到背景估计,再用原图减去背景。

se = strel('disk', 30); % 结构元素半径要大于菌落尺寸 background = imopen(denoised, se); corrected = imsubtract(denoised, background);

顶帽变换的核心逻辑,是用一个比菌落大的结构元素去估算背景光照,然后扣掉它。挑结构元素半径的原则是:比最大的单个菌落直径还要大,但不能大到把整个培养皿都当成背景。我常用的值在20到50像素之间。

预处理做完后,你会发现菌落和背景的对比度明显拉开了,这时候再做二值化,阈值会好选得多。这里有一个我早期踩过的坑:如果直接用全局阈值去处理没校正光照的图像,会出现培养皿边缘一圈全是“假菌落”,怎么调阈值都顾此失彼。

2.2 二值化阈值的选择:固定阈值不够用

MATLAB里二值化最常用的就是imbinarize,默认用的是Otsu方法,它会自动寻找一个阈值,让前景和背景的类间方差最大。不过Otsu也有失效场景——如果前景(菌落)面积占比很小,或者背景有大面积渐变,全局阈值就可能把暗背景整个归成前景。这种情况我会切换到自适应阈值:

bw = imbinarize(corrected, 'adaptive', 'Sensitivity', 0.45);

这里的Sensitivity参数控制分割灵敏度,越接近1,分出的前景越多,噪声也越多。我一般从0.4开始试,观察二值图里的菌落是否完整,如果菌落中间出现空洞就降低灵敏度,如果背景噪声点多就调高。

注意,二值化之后一定要看一下菌落内部是不是实心的。因为菌落经常是中间凸起、边缘薄,光照下中间会有一圈高光,二值化后反而出现“面包圈”现象,中间是空的。这种空心区域如果没有处理,后面计数会把一个菌落当两个。解决办法是在二值图基础上做“填充孔洞”:

bw_filled = imfill(bw, 'holes');

这个操作会把所有完全被前景包围的背景区域填满,非常适合处理高光造成的空心。但注意不要对前一步的背景区域使用,否则会把培养皿内部噪声全部连成大片。

3. 粘连菌落的分割:距离变换与分水岭的配合

3.1 为什么直接数连通域不靠谱

做完二值化后,很多人会直接用bwlabel统计连通域数量,觉得任务就完成了。但菌落长得根本不是规则的圆,两个菌落挨得近的时候,二值图上就是一个不规则的大块,bwlabel数出来是1,而真实菌落数量是2。我一开始也这么干过,结果拿一份密集平板的图去数,出来32个,实际人工数了44个,误差接近三成。

要解决这个问题,就得把粘连区域切开。对规则圆形目标,最成熟的方法是“距离变换 + 分水岭”。先说距离变换的原理:对二值图里的每个前景像素,计算它到最近背景像素的距离。如果一个区域里有多个圆形目标,距离变换后每个目标的中心位置会是局部最大值,这个局部最大值就是“种子点”。找到种子点之后,分水岭算法会从每个种子点开始向周围扩展,直到碰到其他区域扩展出来的边界,从而形成分割线。

3.2 用MATLAB实现距离变换和分水岭的完整步骤

代码核心流程是这样的:

% 1. 对二值填充图做距离变换 D = bwdist(~bw_filled); % 2. 对距离图做H-minima变换,抑制不必要的局部极小值 D_suppressed = imhmin(D, minDepth); % 3. 找局部极大值作为前景标记 mask = imextendedmax(D_suppressed, 2); % 4. 强制在标记位置设置极小值点 D_mod = imimposemin(-D, mask); % 5. 分水岭分割 L = watershed(D_mod);

这里最核心的参数是minDepth,也就是在H-minima变换里用来控制“多低的谷底才被算作一个独立目标”。如果设得太小,杂信号也会成为标记,导致过度分割,一个完整菌落被劈成三瓣;如果设得太大,真实粘连菌落合并成同一个标记,分不出来。我的经验是:minDepth取距离变换最大值的20%到30%左右比较稳,可以先用max(D(:))看看值域再定。

还有一个坑要注意:watershed处理的结果里,要排除背景区域(像素值为0的区域),因为背景也会被分水岭边界分割成很多小块。通常的做法是:

bw_labels = label2rgb(L, 'jet', [0 0 0]);

或者直接对L做掩膜,把背景区域的标签设为0,只统计前景区域内的标签数。

我实际跑下来测过,对100个左右菌落的平板,距离变换分水岭能正确切开约九成以上的粘连体。剩下切不开的,基本是那种完全重叠、连人眼都要仔细分辨的深度粘连菌落,这种场景下可以考虑更复杂的凹点检测方法,但日常实验里觉得精度已经够用了。

4. 特征筛选与计数实现:别让杂质混进统计结果

4.1 用regionprops统计菌落数量和大小

经过前面几步,已经得到了一张分割好的标签图L,前景中每个独立的标签区域就代表一个菌落。接下来是计数和特征提取环节,核心函数是regionprops

stats = regionprops(L, 'Area', 'Centroid', 'MajorAxisLength', 'MinorAxisLength'); num_colonies = length(stats);

直接length(stats)就能得到菌落数。但这样不够严谨,因为二值图里可能混入培养基杂质、气泡边缘、掉落的碎屑。这些干扰物面积通常明显小于正常菌落,或者偏心度极高(长条形的划痕)。所以要做特征筛选。

4.2 面积、圆度和离心率筛选的经验值

我习惯设置三个筛选维度:面积下限、面积上限、离心率上限。面积下限是过滤小杂质的关键,一般取所有连通域面积中位数的五分之一。这个值不是拍脑袋定的,因为菌落尺寸分布相对均匀,真正的小杂质会远小于菌落,取中位数的比例能自适应地过滤掉它们。

离心率滤掉长条形划痕,公式是:

ecc = sqrt(1 - (MinorAxisLength / MajorAxisLength)^2);

完整逻辑:

areas = [stats.Area]; eccs = sqrt(1 - ([stats.MinorAxisLength] ./ [stats.MajorAxisLength]).^2); valid_idx = areas > minArea & areas < maxArea & eccs < 0.9; filtered_stats = stats(valid_idx); num_colonies_final = length(filtered_stats);

这里eccs < 0.9是一个比较宽松的值,因为圆形菌落的离心率接近0,长条划痕接近1。如果设置太严格,椭圆形的次级菌落会被误删,太宽松又滤不掉条状干扰。

还有一类特殊情况值得单独处理:如果菌落是彩色的,或者颜色差异明显,可以在RGB或HSV空间增加一个颜色特征。比如用mean(H)把某种色调的菌落单独挑出来,这在混合培养场景下很实用。代码上不复杂,就是每个连通域求色调均值,再加一个色调范围判断。

4.3 导出结果与可视化叠加

计数完成之后,建议把结果可视化出来,方便人工复核。画图方法很直接:

figure; imshow(img); hold on; for k = 1:num_colonies_final plot(filtered_stats(k).Centroid(1), filtered_stats(k).Centroid(2), 'ro'); text(filtered_stats(k).Centroid(1)+5, filtered_stats(k).Centroid(2), ... num2str(k), 'Color', 'yellow', 'FontSize', 8); end

这样标好号的图可以直接存档,后续审稿或者汇报的时候能展示“每个被计数的目标都被标记了”,比单纯给一个总数说服力强很多。有人会用insertMarker或者insertText来实现,效果类似,但我习惯用plot,因为可以自由调整样式。

另外要提醒一件事:regionprops统计的结果是按照标签图的索引顺序排列的,如果你前面做过特征筛选,索引会错位。所以如果要把筛选结果和原连通域对应起来,一定要保留stats的原始索引信息,或者用bwlabeln参数直接分组再统计。

5. 常见问题与排查技巧实录

5.1 培养皿边缘反光和阴影的干扰怎么处理

培养皿边缘那一圈弧形反光是我遇到最多的问题,二值化后经常会出现一个巨大的月牙形区域,面积比菌落大几十倍,完全干扰了regionprops的面积统计。解决办法有两个方向:一是在预处理阶段用imopen的背景估计法已经能去掉大部分均匀阴影,二是对边缘区域做掩膜扣除。后者的操作不复杂,提前圈出培养皿的有效区域(ROI),用poly2mask生成掩膜,在统计时只对掩膜内的连通域计数。我做了个简单版:

% 手动圈选ROI,也可以用自动找圆的方法 h = drawcircle(gca); mask = createMask(h); stats = regionprops(L .* mask, 'Area', 'Centroid');

这种半自动ROI做法快,缺点是换一张图就得重新圈一次。批量处理时可以用imfindcircles先自动找培养皿边缘圆,把圆心和半径提取出来,再生成圆形掩膜,这样就能全自动处理一批图片。

5.2 菌落太小和背景太杂导致误检

如果菌落本身很小,比如长满了整个平板、菌落直径只有十几像素,中值滤波窗口稍微调大一点就会把菌落边缘模糊掉。我的经验是这种场景不要用5x5的滤波窗口,改用3x3,或者干脆不滤波,直接做顶帽变换。噪声点的问题靠后面的面积筛选解决,不要试图在第一阶段就全部滤干净,否则容易误伤目标。

背景太杂通常是培养基有颗粒物,或者拍摄时分辨率太高引入了纹理细节。这种情况下自适应阈值的Sensitivity要调小一点,让分割更保守,宁可少误检几个小的杂质,也不要把背景大块区域分割进来。还有一招是分水岭之前对二值图做bwareaopen,直接删掉面积小于某个阈值的连通域,能减少大量小噪声对分水岭的干扰。

5.3 光照不均导致菌落中间出现空洞

前面提过的高光空洞问题,在菌落表面反光明显时特别突出。我有一次处理表面湿润的平板,几乎所有菌落中间都有一个白色高光区,二值化后成了空心的环。填充空洞用的是imfill,但如果高光区域特别大,填充后菌落中间会残留一圈黑色边界痕迹,分水岭阶段又会把它当成边缘切开。

这种情况我的处理顺序是先做形态学闭运算,把小的裂缝补上:

se_close = strel('disk', 3); bw_closed = imclose(bw_filled, se_close);

再对距离变换图做中值滤波平滑,减少内部局部极小值点。这套组合拳下来,空心导致的过分割基本可以消除。

5.4 处理显微镜下密集菌落时的参数调整

如果是显微镜照片或者涂抹视野,菌落目标非常密集,距离变换分水岭的参数要主动调宽。minDepth的相对比例要提高一些,imextendedmax里的阈值参数(上面代码里的2)要按距离图的最大值比例动态设置。我的代码里一般这样写:

maxDist = max(D(:)); minDepth = maxDist * 0.25; mask = imextendedmax(D, round(maxDist * 0.1));

用动态比例而不是固定数值,是应对不同分辨率图像最省心的办法。另外,密集场景下regionpropsMinorAxisLength普遍偏小,面积筛选的下限要相应降低,否则会漏掉大量真实菌落。

最后再分享一个实用经验

整个项目做完,我自己最大的体会是:自动化计数的精度瓶颈往往不在算法,而在图像采集端。如果你拍照的时候光照均匀、培养皿放在纯色背景下、镜头垂直拍摄,后面算法的参数随便调调都能得到比较好的结果;反之,如果照片拍得东倒西歪、反光严重,那再优秀的算法也要靠参数硬扛。所以我建议有条件的实验室,先固定一个拍摄装置,哪怕是用手机支架加一个小补光灯,都能让后续所有图像处理工作省心一半。另外,分水岭的minDepth参数建议做个“参数扫描”脚本,自动尝试多个候选值,把每个值对应的计数结果输出到表格里,你对照人工计数选一个最佳值,这样既省时间又能找到最优参数。这也是我在后期批量处理上百张图片时,总结出的最实用的一招。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:49:32

软件控制时钟调制扩展(SC-CME)检测机制详解与实战验证

做系统级电磁兼容测试时候&#xff0c;最怕的就是辐射发射&#xff08;Radiated Emission&#xff09;测试在半程突然冒出一个窄带尖峰&#xff0c;频率不高不低&#xff0c;恰好在民用标准限值附近挣扎。排查到最后&#xff0c;往往不是电源滤波的问题&#xff0c;也不是结构屏…

作者头像 李华
网站建设 2026/9/8 23:49:28

APM飞控源码怎么读?一文掌握ArduPilot源码地图与二次开发

简介&#xff1a;APM飞控源码是针对多旋翼无人机控制系统的开源学习资料&#xff0c;基于ArduCopter 3.2.1版本进行特定裁剪&#xff0c;去掉冗余模块&#xff0c;聚焦APM/Pixhawk飞控核心功能&#xff0c;适合无人机开发者、嵌入式爱好者及航模玩家从源码层面掌握飞行控制原理…

作者头像 李华
网站建设 2026/9/8 23:48:38

类型转换的四种面孔:从显式到隐式的安全实践

1. 这不是玄学&#xff0c;是类型转换在暗处拉扯代码的缰绳 “排查了一周的灵异 Bug&#xff0c;真相是一个不起眼的类型转换”——这句话刚在技术群刷出来&#xff0c;我就下意识摸了摸后颈。不是因为害怕&#xff0c;而是肌肉记忆&#xff1a;那种头皮发紧、咖啡因失效、日志…

作者头像 李华
网站建设 2026/9/8 23:46:33

用CNN检测恶意软件:二进制转图像与深度学习工程实战

简介&#xff1a;在恶意软件检测领域&#xff0c;深度学习技术正逐步替代传统特征匹配方法。这套基于Python的卷积神经网络&#xff08;CNN&#xff09;项目&#xff0c;面向安全领域开发者、数据科学学习者以及安全运维人员&#xff0c;提供了一套从数据预处理到模型训练、评估…

作者头像 李华