简介:这份资源面向生物医学图像处理方向的学习者与科研人员,提供一套基于MATLAB的细菌菌落自动计数实现方案,用于替代培养皿平板琼脂图像中耗时且易出错的人工计数。包内共5个文件,以2个m脚本为核心,另含1张jpg示例图、1张png掩膜叠加效果图与1个txt授权说明,压缩包约174KB,体积轻便,便于直接运行与二次修改。脚本覆盖图像预处理、菌落分割、形态学操作、连通组件分析、特征提取与分类计数等完整环节,示例图与掩膜叠加图可用于直观验证分割与标记效果。目前已有839人学习下载,适合具备一定MATLAB基础、希望快速上手菌落计数流程或将其迁移到自身实验数据中的读者参考借鉴。
1. 从一张培养皿照片到菌落数:这套 MATLAB 自动化计数方案到底能省多少事
如果你做过微生物实验,大概率经历过这样的场景:培养箱里拿出几十个平板,对着菌落计数器一个个点,眼睛盯到发酸,手一抖多点一个还得重来。更麻烦的是不同人判读标准不一致,同一张平板两个人能数出差十几个菌落。这套基于 MATLAB 开发的自动化计数细菌群方案,核心就是用图像处理算法替代人工目视计数,把一张培养皿照片变成可复现的数字结果。它适合三类人:需要批量处理平板图像的实验人员、想用 MATLAB 做图像分割练手的开发者、以及被重复计数折磨到想写脚本的从业者。整套逻辑不依赖深度学习框架,纯靠经典图像处理链路就能跑通,对硬件要求低,一台普通笔记本就能复现。
2. 图像预处理与菌落分割:从 RGB 到二值掩膜的完整链路
2.1 为什么先做灰度化和对比度增强
培养皿照片直接拿来做分割,最大的问题是背景光照不均和培养基颜色干扰。常见做法是先把 RGB 图转灰度,再用自适应直方图均衡化把局部对比度拉起来。这里有个容易翻车的地方:如果直接用imadjust做全局拉伸,皿边缘的反光会被一起放大,后面阈值分割时边缘会冒出一圈假菌落。我一般用adapthisteq配合'ClipLimit'参数控制在 0.01 到 0.03 之间,超过 0.05 会把噪声也当信号增强。
% 读取原始培养皿图像 img = imread('plate_sample.jpg'); % 转灰度,减少通道冗余 gray = rgb2gray(img); % 自适应直方图均衡化,ClipLimit 控制对比度增强上限 % 值越大增强越猛,但噪声也会被放大,建议 0.01~0.03 enhanced = adapthisteq(gray, 'ClipLimit', 0.02, 'NumTiles', [8 8]); % 中值滤波去椒盐噪声,窗口 3x3 足够 % 窗口开太大菌落边缘会被抹平,5x5 以上慎用 denoised = medfilt2(enhanced, [3 3]); figure; subplot(1,3,1); imshow(gray); title('原始灰度'); subplot(1,3,2); imshow(enhanced); title('对比度增强'); subplot(1,3,3); imshow(denoised); title('去噪后');这段代码的逻辑很直白:灰度化去掉颜色通道的干扰,adapthisteq把暗处的菌落提亮,medfilt2把传感器噪点抹掉。参数上NumTiles决定分块数量,8×8 适合 1000×1000 左右的图,图更大就调到 16×16。ClipLimit是玄学参数,不同品牌的培养皿底反射率不一样,建议先跑一张看效果再批量处理。
2.2 阈值分割:Otsu 不是万能药
很多人一上来就用graythresh做 Otsu 全局阈值,结果遇到菌落大小差异大的平板就崩了——大菌落被完整分割,小菌落直接消失。原因是 Otsu 找的是全局最优类间方差,当小菌落像素占比低于 5% 时,它会被归到背景类里。解决办法是改用局部自适应阈值imbinarize的'adaptive'模式,或者先做形态学顶帽变换把微小菌落凸显出来。
% 方法一:Otsu 全局阈值,适合菌落大小均匀的图 level = graythresh(denoised); bw_global = imbinarize(denoised, level); % 方法二:自适应阈值,适合菌落大小差异大的图 % Sensitivity 参数控制阈值松紧,0.4~0.6 之间调节 bw_adaptive = imbinarize(denoised, 'adaptive', ... 'Sensitivity', 0.5, 'ForegroundPolarity', 'dark'); % 形态学开运算去掉小噪点,圆形结构元素半径 2 se = strel('disk', 2); bw_clean = imopen(bw_adaptive, se); % 填充菌落内部空洞 bw_filled = imfill(bw_clean, 'holes'); figure; subplot(1,3,1); imshow(bw_global); title('Otsu 全局阈值'); subplot(1,3,2); imshow(bw_adaptive); title('自适应阈值'); subplot(1,3,3); imshow(bw_filled); title('形态学清理后');ForegroundPolarity设成'dark'是因为菌落通常比培养基暗,如果你的图像是暗背景亮菌落,改成'bright'。Sensitivity这个参数值得多说一句:它本质上是局部窗口内阈值的偏移量,调高会把更多暗像素判为前景,调低则相反。我一般从 0.5 开始,如果发现菌落被切碎了就降到 0.4,如果背景噪点太多就升到 0.6。形态学开运算的结构元素半径不要超过 3,否则小菌落会被直接腐蚀掉。
2.3 分水岭分割:解决菌落粘连的关键一步
当两个菌落挨在一起时,二值化后它们会连成一个连通域,直接做连通域计数就会漏数。这时候需要分水岭算法把粘连区域切开。思路是先算距离变换,找到每个菌落中心的局部极大值作为种子点,再从种子点向外扩展分水岭脊线。
% 距离变换:每个前景像素到最近背景像素的距离 D = -bwdist(~bw_filled); % 强制最小值,避免过分割 D2 = imimposemin(D, ~bw_filled | (D > -2)); % 分水岭分割 L = watershed(D2); % 把分水岭脊线(值为 0 的像素)设为背景 bw_final = bw_filled; bw_final(L == 0) = 0; % 去掉面积过小的区域,阈值 50 像素 bw_final = bwareaopen(bw_final, 50); figure; subplot(1,3,1); imshow(bw_filled); title('分割前'); subplot(1,3,2); imshow(label2rgb(L)); title('分水岭标签'); subplot(1,3,3); imshow(bw_final); title('分割后');imimposemin那一步是防止过分割的核心,D > -2这个条件是在距离变换的负值空间里保留足够深的谷底作为种子。bwareaopen的面积阈值要根据实际分辨率调,假设菌落直径最小 5 像素,面积就是约 20 像素,设 50 是留了安全余量。如果发现菌落被切得太碎,把-2改成-3或-4,让种子点更少。
3. 菌落计数与结果输出:连通域标记、面积过滤与批量导出
3.1 连通域标记与计数逻辑
分割完成后,用bwlabel给每个连通域打标签,标签的最大值就是菌落数量。但这里有个坑:如果图像边缘有被截断的菌落,它们会被当成独立连通域计入,导致结果偏大。常见做法是先把边缘一圈像素设为背景,或者用imclearborder直接清除接触边界的连通域。
% 清除接触图像边界的连通域,避免截断菌落被误计 bw_noborder = imclearborder(bw_final); % 连通域标记,8 邻域连接 [labeled, num_colonies] = bwlabel(bw_noborder, 8); % 获取每个连通域的属性 stats = regionprops(labeled, 'Area', 'Centroid', 'Perimeter', 'Eccentricity'); % 按面积过滤:小于 30 像素的视为噪声,大于 5000 的视为杂质 valid_idx = find([stats.Area] > 30 & [stats.Area] < 5000); num_valid = length(valid_idx); fprintf('原始连通域数量: %d\n', num_colonies); fprintf('面积过滤后菌落数量: %d\n', num_valid); % 可视化:在原图上标注菌落中心和编号 figure; imshow(img); hold on; for i = 1:num_valid c = stats(valid_idx(i)).Centroid; text(c(1), c(2), num2str(i), 'Color', 'r', 'FontSize', 8); plot(c(1), c(2), 'g+', 'MarkerSize', 10); end hold off; title(['检测到 ' num2str(num_valid) ' 个菌落']);bwlabel的第二个参数 8 表示 8 邻域连接,对于圆形菌落来说 8 邻域比 4 邻域更合适,因为对角线方向的像素也应该算连通。regionprops返回的结构体数组里,Area是最常用的过滤依据,Eccentricity可以用来排除细长形的杂质(比如划痕),一般设阈值 0.9 以上视为非菌落。面积上下限的设定需要根据你的图像分辨率来定,建议先跑一张已知数量的平板做标定。
3.2 批量处理与结果导出
单张图跑通之后,真正的效率提升来自批量处理。把待处理的图片放在一个文件夹里,用dir遍历,每张图走一遍完整链路,最后把结果写进 CSV。这里要注意内存管理:如果图片数量超过几百张,每张都imshow会拖慢速度,批量模式下应该关掉可视化。
% 批量处理文件夹下所有 jpg 图片 input_dir = 'plates/'; output_csv = 'colony_counts.csv'; files = dir(fullfile(input_dir, '*.jpg')); % 打开 CSV 文件准备写入 fid = fopen(output_csv, 'w'); fprintf(fid, 'filename,colony_count,avg_area\n'); for k = 1:length(files) % 读取图像 filepath = fullfile(input_dir, files(k).name); img = imread(filepath); % 完整处理链路(封装成函数更整洁) gray = rgb2gray(img); enhanced = adapthisteq(gray, 'ClipLimit', 0.02); denoised = medfilt2(enhanced, [3 3]); bw = imbinarize(denoised, 'adaptive', 'Sensitivity', 0.5); bw = imopen(bw, strel('disk', 2)); bw = imfill(bw, 'holes'); % 分水岭分割 D = -bwdist(~bw); D2 = imimposemin(D, ~bw | (D > -2)); L = watershed(D2); bw(L == 0) = 0; bw = bwareaopen(bw, 50); bw = imclearborder(bw); % 计数与统计 [labeled, n] = bwlabel(bw, 8); stats = regionprops(labeled, 'Area'); areas = [stats.Area]; valid = areas(areas > 30 & areas < 5000); % 写入 CSV fprintf(fid, '%s,%d,%.1f\n', files(k).name, length(valid), mean(valid)); % 每处理 10 张打印一次进度 if mod(k, 10) == 0 fprintf('已处理 %d / %d\n', k, length(files)); end end fclose(fid); fprintf('批量处理完成,结果已写入 %s\n', output_csv);这段脚本的关键点在于把处理链路内联到循环里,避免反复调用函数带来的开销。fprintf写 CSV 时用%.1f保留一位小数,避免面积均值出现一长串浮点数。进度打印每 10 张一次,既能知道跑到哪了,又不会刷屏。如果图片格式不统一,把*.jpg改成*.*然后在循环里判断扩展名。
3.3 参数标定:用已知平板反推阈值
自动化计数最怕的是“看起来能跑但数不准”。我的习惯是拿 3 到 5 张人工计数过的平板做标定,把自动计数结果和人工结果对比,误差超过 10% 就回去调参数。标定顺序建议是:先调Sensitivity让分割结果和肉眼判断一致,再调面积过滤上下限去掉明显杂质,最后调分水岭的imimposemin阈值处理粘连。
| 参数 | 作用 | 推荐范围 | 调参方向 |
|---|---|---|---|
| ClipLimit | 对比度增强上限 | 0.01~0.03 | 菌落暗就调高,噪点多就调低 |
| Sensitivity | 自适应阈值松紧 | 0.4~0.6 | 漏检多就调高,误检多就调低 |
| 面积下限 | 过滤噪声 | 20~50 像素 | 小菌落多就调低 |
| 面积上限 | 过滤杂质 | 3000~8000 像素 | 大菌落多就调高 |
| imimposemin 阈值 | 分水岭种子深度 | -2~-4 | 过分割就调低绝对值 |
标定完成后把参数写进一个结构体或配置文件,批量处理时直接读取,避免每次手动改脚本。
4. 避坑与排查:那些让计数结果翻车的细节
4.1 培养皿边缘反光被当成菌落
现象:计数结果比人工多出 20% 到 30%,可视化图上菌落集中在皿边缘一圈。原因是培养皿塑料边缘在闪光灯下产生高亮反光,灰度化后形成亮环,自适应阈值把它判成了前景。解决办法是在预处理阶段加一个圆形掩膜,把皿外区域直接置零。用imfindcircles检测皿边界,或者手动指定圆心和半径生成掩膜。
% 生成圆形掩膜,只保留皿内区域 [rows, cols] = size(gray); [center_x, center_y, radius] = imfindcircles(gray, [200 500]); mask = zeros(rows, cols); [X, Y] = meshgrid(1:cols, 1:rows); mask((X - center_x).^2 + (Y - center_y).^2 <= radius^2) = 1; gray_masked = gray .* uint8(mask);4.2 培养基颜色偏深导致菌落对比度不足
现象:分割结果里菌落和背景糊在一起,二值图上大片连通。原因是某些培养基(比如血平板)本身颜色深,菌落只比背景暗一点点,灰度差不到 10 个灰阶。解决办法是改用颜色通道分离,在 RGB 空间里找菌落和背景差异最大的通道。常见做法是看绿色通道或蓝色通道,因为菌落中的血红蛋白会吸收这些波段。
4.3 分水岭过分割把单个菌落切成多块
现象:一个明显的大菌落被标成了 3 到 4 个计数。原因是距离变换的局部极大值太多,每个小凸起都成了一个种子点。解决办法是调imimposemin的阈值,或者先对距离变换图做高斯平滑再找种子。我一般用imgaussfilt对距离图做 sigma=2 的平滑,能把毛刺抹掉。
4.4 批量处理时内存溢出
现象:跑到第 200 张左右 MATLAB 报内存不足。原因是每张图的中间变量没有及时清理,工作区堆积了几百个矩阵。解决办法是在循环末尾加clear清理临时变量,或者把处理逻辑封装成函数,函数返回后局部变量自动释放。另外imshow在批量模式下应该关掉,它占用的图形句柄很吃内存。
4.5 不同批次平板需要重新标定
现象:昨天调好的参数今天换了一批平板就不准了。原因是培养基批次、培养时间、拍照光照条件都变了,灰度分布跟着变。这不是 bug,是图像处理方法的固有边界。我的做法是每批平板先抽 2 张跑一下,看计数结果和目视是否吻合,偏差大就微调Sensitivity,不要指望一套参数打天下。
5. 进阶技巧:把计数结果做成可追溯的数据链
跑通基础链路之后,真正拉开效率差距的是数据管理。我现在的习惯是每张平板除了输出计数,还保存分割后的二值图、标注图和原始图的缩略图,三者用同一个文件名前缀。这样后面如果对某个结果有疑问,可以直接翻出当时的中间图看是哪一步出了问题,不用重新跑一遍。这个习惯是被一次返工逼出来的——某批平板计数结果异常,但原始图已经覆盖,只能全部重拍重跑,血泪经验。
% 为每张图建立独立的结果文件夹 [~, basename, ~] = fileparts(files(k).name); result_dir = fullfile('results', basename); if ~exist(result_dir, 'dir') mkdir(result_dir); end % 保存原始图缩略图 imwrite(imresize(img, 0.5), fullfile(result_dir, 'original_thumb.jpg')); % 保存二值分割图 imwrite(bw, fullfile(result_dir, 'binary_mask.png')); % 保存标注图 fig = figure('Visible', 'off'); imshow(img); hold on; for i = 1:num_valid c = stats(valid_idx(i)).Centroid; text(c(1), c(2), num2str(i), 'Color', 'r', 'FontSize', 8); end hold off; saveas(fig, fullfile(result_dir, 'annotated.png')); close(fig); % 保存参数快照,方便追溯 params = struct('ClipLimit', 0.02, 'Sensitivity', 0.5, ... 'AreaMin', 30, 'AreaMax', 5000, 'WatershedThresh', -2); save(fullfile(result_dir, 'params.mat'), 'params');这段代码把每张图的结果隔离到独立文件夹,参数快照用.mat存下来。后面如果发现某张图计数有问题,打开对应文件夹就能看到当时用的所有参数和中间结果,定位问题从“猜”变成“看”。figure('Visible', 'off')是关键,它让绘图在后台完成,不弹窗口,批量跑几百张也不会卡界面。
另一个进阶方向是把计数结果和实验记录关联起来。比如 CSV 里除了文件名和计数,再加一列实验条件(稀释度、培养时间、温度),后面做统计分析时可以直接用。这个用readtable和writetable配合就能实现,不需要额外工具。
最后说一个验证技巧:拿一张已知菌落数的标准平板(比如商业化的质控菌株平板),跑 10 次自动计数,看结果的变异系数。如果 CV 超过 5%,说明参数不稳定,需要回去检查光照或阈值设置。我现在的习惯是每次换新批次培养基,都先拿质控平板走一遍这个验证,确认参数没漂再批量跑。希望帮到你。
本文还有配套的精品资源,点击获取