简介面向生物医学图像分析与MATLAB开发者的细菌菌落自动计数实践包解决培养皿平板上菌落目标识别、图像分割与数量统计的自动化问题相对人工计数更高效、可重复。包内共5个文件包含2个m格式MATLAB脚本、1张jpg原始样例图、1张png标记效果图及1份txt授权文本整体仅174KB轻量便于检验与改造。实现流程覆盖图像灰度化、噪声去除、对比度增强、Otsu阈值分割、形态学膨胀腐蚀、连通域标记、菌落区域特征提取以及基于规则的分类计数突出传统图像处理技术在小目标计数中的完整链路脚本与示例数据配套从原始图像到标记覆盖层可直接运行复现便于调试与二次开发。目前已有839人学习下载适合课程设计、算法验证也可作为进一步引入机器学习分类器的起始模板。1. 为什么我从手动数菌落转到了MATLAB方案做微生物实验的人应该都有过这种体验培养皿里密密麻麻长满了菌落导师丢给你一句“数一下”然后你就得对着灯光、拿着记号笔在培养皿背面一个点一个点地标记——数到一半忘了数到哪只能从头再来白色菌落碰上白色琼脂眼睛花到怀疑人生更要命的是不同人数出来的结果能差出20%以上谁也不知道该信谁。这就是我当初决定动手写一套MATLAB自动化计数程序的直接原因。MATLAB在图像处理上有天然的生态优势内置Image Processing Toolbox矩阵运算效率高不需要像OpenCV那样配一堆环境依赖写出来的代码还能直接在课题组内分享——毕竟大多数做生物信息或者实验科学的同行电脑里大概率都装过MATLAB。这套方案能解决什么简单说就三件事把菌落图片变成清晰的前景目标把重叠粘连的菌落逐个切开最后输出一个可信的计数结果。整个过程半小时能跑完几十张培养皿图像而且每张图的结果可以复现——做科研的人都知道“可复现”这三个字有多重要。本文面向的读者有两类一类是像我这样被手动计数折磨过的生物/医学方向研究者另一类是刚开始学MATLAB图像处理、想找一个真实场景练手的学生。两种人都能从这里拿走能直接用的代码思路和避坑经验。需要提前说明的是我用的示例代码基于MATLAB R2021a版本核心依赖Image Processing Toolbox如果你用的是更高版本也没问题API基本都保持兼容。整套流程说白了就是预处理→分割→校正→计数下面我按实际踩坑的顺序一步步拆开讲。2. 输入图像的质量控制坏图进坏结果出自动化计数看着是算法问题但我在实际测试中发现大概有六成以上的计数偏差根本不出在算法上而是卡在第一步——输入图像质量参差不齐。培养皿拍照的角度、光源方向、琼脂厚薄、培养基颜色每一处差异都会直接传导到分割结果里。2.1 我自己踩过的“光线不均匀”坑第一次跑程序时我拿了一组培养皿照片直接做灰度化加阈值分割结果边缘和中心区域完全对不上——中心区域菌落能分开边缘区域一团黑。查了半天才反应过来问题出在拍照时的光源上培养皿边缘由于曲面反射亮度比中心低了约30%。这组测试图用的是手机在超净台照明灯下拍的不是标准成像设备。后来我改用MATLAB的imflatfield函数做平坦场校正效果立竿见影。这个函数的思想很简单估计图像背景的亮度变化趋势然后做除法校正把不均匀的背景光拉平。% 读取原始图像 I imread(plate_sample.jpg); % 灰度化 Igray rgb2gray(I); % 平坦场校正sigma取30比较适中去估计低频背景变化 Iflat imflatfield(Igray, 30);用imflatfield之后再做二值化边缘区域的菌落终于能稳定地出来了。这里有个参数心得sigma控制背景估计的光滑程度我试过10到80之间的值对于常见的培养皿图片30到50是比较稳的区间——太小会把单个菌落内部的亮度差异也当成背景太大则校正不充分。2.2 尺度标定像素和毫米必须挂钩另一个早期被忽略的问题是尺度。不同手机、不同拍摄距离下同样的培养皿在图像里的像素尺寸完全不同。如果只计数不测量大小尺度的确无关紧要——但如果你想统计“菌落直径分布”或者分类计大菌落/小菌落就必须把像素转换成实际物理尺寸。我的做法是在拍照时固定拍摄距离并把培养皿的直径作为已知参考。标准90mm培养皿在图像中占了多少像素就能算出每毫米对应的像素数% 手动选取培养皿边缘直径两端点计算像素直径 [x1, y1] ginput(1); [x2, y2] ginput(1); pixelDiameter sqrt((x2-x1)^2 (y2-y1)^2); mmPerPixel 90 / pixelDiameter; % 标准90mm培养皿这步虽然简单但请务必保存mmPerPixel这个值。我有个习惯每批图片的拍摄参数固定后标定一次就够了但如果换了手机、换了拍摄距离必须重新标定。这一步不做好后面任何基于尺寸的过滤都不可靠。2.3 图像预处理顺序的调整经过反复试验我最终确定的预处理顺序是灰度化→平坦场校正→中值滤波→对比度增强→二值化。中值滤波我用的是3×3窗口主要目的是去掉CCD/CMOS传感器上的椒盐噪声尺寸再大就会模糊小菌落的边缘。对比度增强我用imadjust把灰度直方图的1%和99%分位映射到0到255。注意不要用满幅0-255映射因为培养皿图像里可能有少量高光噪点满幅映射会把背景噪声一起放大。% 中值滤波去噪 Ifilt medfilt2(Iflat, [3 3]); % 对比度增强 Iadj imadjust(Ifilt, stretchlim(Ifilt, [0.01 0.99]));这一套走完图像肉眼看起来会更“清爽”背景更均匀菌落边缘更锐利。预处理做完才轮到真正的分割环节。3. 菌落分割的三种主流思路从阈值到深度学习分割是整个流程的核心。菌落和背景的区分本质上是灰度图像里的前景目标提取问题。我用过三种方法各有适用场景这里把取舍逻辑讲清楚。3.1 全局阈值法快但只适合简单背景最简单粗暴的是imbinarize配合graythreshOtsu方法自动计算阈值。这种方法在“白色菌落深色培养基”或者“深色菌落白色培养基”这种高对比度场景下非常好用速度快、参数少一行代码搞定。bw imbinarize(Iadj, global); % 默认使用Otsu阈值但问题也很明显如果培养皿里有杂质琼脂碎块、气泡、残渣这些玩意儿和菌落一样是灰度上的前景目标Otsu会把它们一起分进来。我的测试数据里大约有10%-15%的图像存在明显杂质干扰直接全局阈值会导致虚计数偏高。3.2 自适应阈值法能处理光照不均但参数要调imbinarize的adaptive选项是按局部邻域计算阈值的能处理光照不均匀的问题尤其当你的图像比较边缘暗时效果比全局阈值好很多。bw imbinarize(Iadj, adaptive, Sensitivity, 0.5);Sensitivity默认是0.5调高会让更多像素被判为前景适合菌落颜色浅的情况调低则反之。我一般建议从0.4试到0.7每档都跑一遍肉眼对比分割结果是否“合理”——所谓合理就是菌落没有被切碎、背景杂质没有被大量带入。3.3 基于颜色分割被低估的菌落图像杀手锏绝大多数菌落图像是彩色的但很多教程一上来就直接转灰度白白丢掉了颜色信息。某些场景下菌落是白色/浅黄色的培养基是红色的比如MacConkey琼脂灰度化之后两者可能区分度反而不高但颜色空间里是天然分离的。可以用Color Thresholder这个App交互式地选颜色范围也可以直接用代码在LAB色彩空间里分割。我在处理带红色培养基的图像时用A通道绿色到品红轴分割效果远好于灰度图% 转换到LAB色彩空间 lab rgb2lab(I); % 提取A通道 aChannel lab(:,:,2); % 分割前景 bw aChannel 0; % 具体阈值需根据样品调整这个方法最大的优势是对光照变化不敏感因为LAB空间的色彩通道和亮度是解耦的。如果你用的培养基有比较鲜明的颜色强烈建议试试这条路。我后期固定下来的流程里有大约30%的图像走的是颜色分割路径。3.4 深度学习分割什么时候才值得用如果你去搜“深度学习 菌落计数”会看到各种U-Net、Mask R-CNN的论文。我个人的观点是除非你的图像来源极其复杂比如环境样本、土壤稀释液杂质形态千变万化传统方法实在压不住否则没必要上深度学习。原因很现实深度分割需要标注数据。标注几百个培养皿的菌落掩膜工作量不比手动计数轻松多少。MATLAB的Deep Learning Toolbox确实支持训练U-Net但从准备数据到调通训练流程一周时间跑不掉。当然如果你有现成的预训练模型或者课题组的图库比较统一可以试——但先把传统方法的效果压榨干净再说。下面给出传统方法选型的速查表可以对照自己的图像特征挑入口图像特征首选方案备选方案高对比度背景干净Otsu全局阈值—光照不均匀边缘偏暗自适应阈值平坦场校正全局阈值培养基有显著颜色LAB色彩空间分割RGB通道阈值杂质多形态杂乱颜色分割形态学过滤深度学习分割4. 粘连菌落切分与计数核心难点拆解分割之后得到的是一张二值图白色区域代表“候选菌落”。但细菌在培养基上生长时经常挤在一起形成粘连块——二值图里看起来就是一坨不规则的白色连通域。直接把连通域数量当成菌落数会严重低估。我用一组实际测试数据说明问题的严重性一皿大约300个菌落的培养皿图片如果只数连通域结果是187——差了几乎40%。这玩意儿不解决自动化计数毫无意义。4.1 为什么距离变换加分水岭有效粘连菌落切开的基本思路是先算二值图内部每个像素到最近背景的距离得到距离图距离图里每个“相对独立的菌落中心”会形成一个局部极大值区域这些极大值点就是分水岭算法的种子然后基于距离图做分水岭变换把粘连块按“山谷”分割开。这个过程我用一个生活类比来理解把每一坨粘连菌落想象成一片连绵的山脉每个菌落是一座山峰。距离图上山峰越高代表这个像素越靠近某个菌落的中心。从每个峰顶往山下“注水”水会在山脊处交汇——交汇线就是菌落之间的边界。% 二值图取反使菌落区域为前景 bw imclose(bw, strel(disk, 3)); % 先做一次闭运算填补内部空洞 % 距离变换 D -bwdist(~bw); % 用h-minima变换抑制噪声产生的假局部极小值 Dmod imhmin(D, 2); % 分水岭分割 L watershed(Dmod);这里最关键的一步是imhmin。如果不做这一步距离图上一堆由噪声或细小纹理造成的局部极小值点都会被当成菌落中心切开结果就是菌落被碎尸万段。imhmin的作用就是抑制深度小于指定值的局部极小——我测试下来2到5之间的值比较合适具体取决于菌落大小和粘连程度。4.2 标记控制分水岭的完整代码直接对距离图做分水岭结果往往还是会有过分割。更可控的做法是先明确找出“每个菌落的中心标记”再以这些标记为种子来分水岭。这叫标记控制分水岭Marker-Controlled Watershed是实际项目里最推荐的做法。% 找局部极大值作为标记 % 注意需要去除图像边界附近的假标记 maxs imextendedmax(D, 2); % 同样是阈值控制 % 强制将紧贴边界的标记去除 maxs imclearborder(maxs); % 关掉过小的标记噪声点 maxs bwareaopen(maxs, 5); % 使用这些标记强制修改距离图 Dmod imimposemin(D, maxs); % 分水岭 L watershed(Dmod);imextendedmax是找高度差超过2的局部极大值区域这个2和前面的imhmin对应。把标记算好后再用imimposemin强制设定分水岭的源头最后的分割结果比直接分水岭稳定得多。我自己实践下来这套流程对50%-70%重叠的菌落团都有不错的分割效果。超过这个重叠度比如菌落快长成一片了肉眼都很难分辨边界算法当然也力不从心——这时候正确做法是建议实验人员重新稀释涂板而不是继续在算法上死磕。4.3 分水岭之后必须先过滤再计数分水岭会得到一张标签图L每个分割区域就是一个候选菌落。但注意分水岭很“老实”——它会把所有能分的东西都分开杂质、气泡、边缘残影也会被当成一个区域。所以计数前必须做过滤处理面积过滤用regionprops提取每个区域的面积删除明显过大或过小的区域。小菌落的面积下限通常在20-50像素具体取决于你图片的尺度过大的可能是未分割开的菌落团也可能是一块杂质。圆度过滤菌落大体是圆形的。计算每个区域的圆度4π×面积/周长²删除圆度低于0.3的细长形杂质。这个方法对去除琼脂划痕特别有效。边缘过滤培养皿边缘的反光会被误识别成目标。使用imclearborder直接清除接触图像边缘的区域或者先做圆形掩膜只留培养皿内部区域。我把这些过滤写成一个独立的函数输入标签图输出过滤后的计数结果和每个菌落的属性表stats regionprops(L, Area, Perimeter, Centroid, Circularity, MajorAxisLength); % STRUCT版regionprops在R2023a后推荐改用table版 % filtered stats([stats.Area] minArea [stats.Circularity] 0.3); count numel(filtered);用MajorAxisLength还可以估算每个菌落的实际直径结合之前标定的mmPerPixel。这步让我额外得到一个有用的输出菌落的大小分布直方图这在做抑菌圈实验时效用很大。5. 实际运行中踩过的坑五个典型案例的完整排查链路跑这套程序半年多我遇到过各种奇奇怪怪的问题。这里挑五个坑把排查思路完整写出来——直接给结论的教程很多但我想让你看到“怎么一步步找到问题根源”才是最值钱的。案例一计数结果比手动数少了一半排查链路先看预处理后的灰度图正常再看二值图傻眼了——菌落颜色浅的部分直接被阈值切掉了整个菌落呈“空心甜甜圈”状。原因是我用了全局阈值而浅色菌落的灰度中心和深色背景靠得太近Otsu算出的阈值把它们划到了背景一侧。解决办法换成自适应阈值把Sensitivity调到0.55空心问题解决了。这个案例给我的教训是不要再盲目信任全局阈值多张图一起跑之前先挑一张形态最复杂的图验证分割效果。案例二计数结果比手动数多很多排查链路二值图里出现了密密麻麻的小噪点面积过滤的阈值没拦住。放大看这些小噪点很多是培养基表面的微小气泡或者干燥后产生的纹理。原因是我用颜色分割时阈值取得太宽把培养基表面纹理也收了进来。解决办法把面积下线从10像素提到30像素同时对二值图先做一次开运算腐蚀再膨胀把小噪点直接抹掉。案例三分水岭把一个大菌落切成了七八瓣排查链路看距离图发现这个菌落表面并不光滑——它的边缘有褶皱状纹理距离图上产生了多个局部极大值。每个极大值都变成种子自然被切碎了。解决办法把imextendedmax的阈值从2提到5小突起不再产生种子同时把imhmin参数提到3。这里不存在“万能参数”我的习惯是针对一批图做2-3档调参肉眼确认最佳值。案例四培养皿边缘反光被当成了菌落这个坑最隐蔽。培养皿的塑料边缘在打光角度不合适时会产生一道亮环二值化后是一圈弧形连通域面积很大遮都遮不掉。排查链路最开始以为是光照不均匀但平坦场校正后反光依然存在。后来才意识到要先把培养皿区域从图像里“抠出来”再处理。解决办法先用Hough变换找培养皿的圆形边界生成掩膜只留皿内区域。用imfindcircles要调Sensitivity和EdgeThreshold我的经验值分别是0.85和0.1能稳定扣出培养皿区域。[centers, radii] imfindcircles(Igray, [300 600], ObjectPolarity, bright, Sensitivity, 0.85); % 生成掩膜 mask false(size(Igray)); mask insertShape(mask, FilledCircle, [centers(1,1) centers(1,2) radii(1)], Color, white); mask mask(:, :, 1) 0;扣完之后做一次bw bw mask反光环和皿外干扰直接消失。案例五图像含有日期水印文字被当成菌落这个属于“一看图就懂一跑代码就忽略”的问题。很多成像系统会在图片角落加上日期时间水印这些字在二值图里也是白色区域。排查链路当时数完发现结果稳定偏多20个左右多出来的“菌落”位置每次都在图片左下角固定区域。比对原图后确认是水印文本。解决办法最省事的就是提前Crop掉水印区域或者直接做一个与培养皿位置相关的ROI掩膜把水印排除在外。别指望形态学过滤能分清“细菌”和“数字”——它们的形状差异并不稳定。6. 准确率验证与调优参考怎么信这套系统跑通程序只是第一步真正要用到实验数据里必须有验证环节。我自己的验证方式很简单随机从图像集中抽10%的图片手动点数作为标准参考值然后和程序输出对比计算误差率。6.1 误差构成实测下来误差主要来自两个方向分割时的过分割把一个菌落切成多块和欠分割多个菌落没被分开。这两个指标可以通过下面代码统计% 手动标记的菌落数量 manualCount % 程序输出的数量 autoCount errorRate abs(autoCount - manualCount) / manualCount * 100;我整理了最近一次测试的误差分布发现在菌落数量小于50时程序相对误差容易偏大绝对值误差3-5个就会导致6%-10%的相对偏差数量在50-300之间时误差控制在1%-5%之间效果最稳超过300个时粘连比例骤增误差又会回升到5%-10%。所以一个非常实用的建议是如果你的培养皿经常长超过300个菌落要么稀释重新涂板要么接受误差并把它在论文方法部分写清楚——这比假装误差为零要诚实得多。6.2 参数调优的优先顺序遇到结果不理想时我调参的顺序是先看二值图——分割目标是否干净不干净先调阈值/颜色空间再看标记图——菌落中心标记是否正确不正确的调imextendedmax阈值最后看过滤条件——面积和圆度过滤是否合适如果123都正常但结果仍然差回头查图像采集条件——光源、背景、培养皿位置是否统一。按这个顺序走90%的问题在半小时内能定位。如果你直接一上来就调分水岭参数大概率越调越乱。6.3 一个容易被忽略的细节批处理命名和结果导出跑了上百张图之后你会发现程序挂在那个地方不重要重要的是结果能不能追溯回去。我强烈建议保持一个习惯输入图像按“日期_样品编号_稀释倍数.jpg”命名输出结果用同名字加后缀保存同时把每张图的计数结果、过滤参数、生成时间写进一个CSV汇总表。% 批量保存结果到同名CSV T table({fileName}, count, errorRate, minArea, circularityThresh, VariableNames, {Image, Count, ErrorRate, MinArea, Circularity, RunTime}); writetable(T, count_results.csv, WriteMode, append);这一个小习惯在写论文时帮你省出大量返工时间——做科研的都懂实验记录要是追溯不到等于白干。7. 我对这套方案的整体评估与后续扩展做了大半年整套MATLAB自动化计数流程最终稳定在一个可接受的水平。以最近一批样本为例我用上述流程处理了87张培养皿图像与3位同学的手动计数结果对比程序输出的平均偏差为2.3%处理速度约每张1.5秒不包含人工调参时间。这个结果对于高密度菌落计数、抑菌圈实验、菌落大小分布统计等场景已经足够作为实验数据支撑。这套代码里我认为最值得复用的一块是把“预处理→分割→标记→分水岭→过滤”封装成了流水线函数换一批新图时只调参数不重写代码。对应的所有代码我已经整理好核心功能不足200行对Image Processing Toolbox版本依赖也不强即使是MATLAB初学者逐行弄懂之后也能改出自己的版本。后续我计划做两件事一是把深度学习分割接进来专门处理那些传统方法压不住的极端粘连场景二是把代码移植成App Designer界面方便课题组里完全不懂代码的同事直接用。这两件事都在试验中等有了可靠的实验结果再来分享。最后还是那句老话所有自动化的前提都是对原理的透彻理解。工具会更新但“图像质量决定上限参数调优决定下限”这个逻辑不会变。希望这篇文章能让你在自动计数的路上少走我走过的那些弯路。本文还有配套的精品资源点击获取