资讯动态

基于Matlab的颗粒自动计数系统:算法链路与GUI设计实战

发布时间:2026/10/20 17:15:20 来源:尧图企业网站定制
做了这么多年图像处理我最怕听到的需求就是帮我把这堆颗粒数一下。培养皿里的菌落、材料截面上的颗粒、药片包装里的胶囊肉眼数完一遍眼睛发花拿笔点着数又容易漏一个下午就这么耗没了。后来我把这套基于Matlab的颗粒自动计数程序整理成了带GUI的完整工具图片导入、点一下按钮、几秒钟出总数顺带把每个颗粒的位置、面积、圆形度全部标出来。这里就把这套图像计数系统的算法链路、GUI设计思路和实测过程完整拆开聊聊包括源码里最值得注意的几处实现细节以及我在跑真实样品时踩过的坑。1. 颗粒计数需求从哪来实验室和生产现场的原始痛点1.1 数颗粒这件事为什么值得专门写一套程序先说个场景。某次帮一位做材料的同学处理SEM照片图上几百个大小不一的颗粒他要统计粒径分布。最开始他打算用图像软件手动圈圈了五十个就放弃了——太慢而且人的判断不稳定上午圈的和下午圈的完全不是一套标准。这其实就是颗粒计数最典型的困境样本量大、颗粒边界模糊、形态不规则人工操作既费时又不可复现。自动计数的核心价值不在于快这么简单在于标准统一。同一张图用同一套算法、同一组参数跑十次结果完全一致。这个特性在科研实验里非常重要因为数据要有可对比性在生产质检里更关键因为判断标准不能随操作员状态波动。所以说这套系统的本质是用确定性的计算逻辑替代人眼的主观判断。1.2 这套系统到底能数什么、不能数什么基于图像处理的颗粒计数适用对象需要满足一个基本前提颗粒与背景之间存在可区分的灰度或颜色差异。常见的适用场景包括应用场景颗粒类型成像特点计数难度微生物实验菌落培养皿底部圆形凸起低到中材料科学粉末颗粒、晶粒显微镜照片边界清晰中医药质检片剂、胶囊传送带上俯拍低农业选种种子浅色背景下深色种子低到中环境监测微塑料滤膜上分布不均匀中到高如果颗粒透明、与背景几乎同色或者大面积粘连成团传统图像处理算法会吃力这时候需要把预处理做扎实严重的情况就得考虑深度学习语义分割。但别急着上深度模型——我后面会聊到很多看似严重的问题通过合理的预处理和参数调优传统方法完全能扛下来。这套GUI源码正是基于传统图像处理流程搭建的胜在可解释、可调参、不吃硬件。2. 核心算法链路拆解从灰度化到连通域标记2.1 预处理环节灰度化、滤波与背景校正原始图像进来第一件事是灰度化。如果输入是RGB彩色图直接用rgb2gray按亮度加权合成单通道灰度图。这里有个容易被忽略的细节如果是彩色分割更有利的场景比如红色颗粒在绿色背景上可以先做颜色通道分离选对比度最高的通道作为灰度输入比单纯转灰度效果好得多。I_rgb imread(sample.png); I_gray rgb2gray(I_rgb); % 可选如果颗粒颜色与背景颜色不同用通道分离替代 rgb2gray % R I_rgb(:,:,1); G I_rgb(:,:,2); B I_rgb(:,:,3); % I_gray imsubtract(R, G); % 红色颗粒加强然后是滤波去噪。颗粒图像最常见的噪声是椒盐噪声和微弱的高斯噪声我实测下来中值滤波最稳。它能在去噪的同时保持颗粒边缘的锐利度这是均值滤波做不到的。medfilt2(I_gray, [3 3])一行代码搞定窗口大小一般取3到5窗口太大会把细小颗粒直接抹掉。背景校正常常被人忽略。如果拍摄时光照不均匀或者样品本身有底色渐变直接做阈值分割会出大问题——亮区域的背景被当成颗粒暗区域的真实颗粒被漏掉。一个非常实用的操作是顶帽变换Top-hat用形态学开运算估计背景再用原图减去背景得到去掉光照梯度的前景图。se strel(disk, 30); % 结构元素半径要大于最大颗粒半径 I_tophat imtophat(I_gray, se);注意se的半径选择很关键太小会把大颗粒误判为背景结构太大会丢失背景校正效果。通常取图中最大颗粒直径的1.5到2倍即可。2.2 阈值分割选型全局阈值、大津法与自适应阈值预处理完成之后图像变成前景亮、背景暗的理想状态接下来需要把它转成二值图。这一步是计数准确率的绝对核心分割偏了后面怎么补救都有限。imbinarize配合graythresh是最常用的组合。graythresh计算的是大津法Otsu阈值它的原理是穷举所有灰度值找到让前景和背景两类像素的类间方差最大的那个阈值。简单理解就是找一个灰度分界线使分界线两侧的像素组内部尽可能一致、组间差异尽可能大。level graythresh(I_tophat); bw imbinarize(I_tophat, level);如果图像存在局部光照不均全局阈值扛不住改用自适应阈值bw imbinarize(I_tophat, adaptive, Sensitivity, 0.5, ForegroundPolarity, bright);Sensitivity这个参数我建议新手从0.4开始调调大则更容易把背景判为前景颗粒区域变大调小则相反。自适应阈值的原理是对每个像素基于其邻域统计一个局部阈值所以能应对渐变的背景。代价是计算量上去了而且对参数更敏感。这里有个实践经验不要把阈值写死在代码里。不同批次的图片亮度不可能完全一致固定阈值换到下一批图就直接翻车。自动阈值即使不是最优也至少能在一个合理范围内后续再通过形态学操作修正。2.3 形态学处理开运算去噪、闭运算补洞二值图出来之后很少是干净的。常见问题有三类背景上的零散噪点、颗粒内部的小孔、颗粒之间细弱的伪连接。这三个问题分别对应三个形态学操作。开运算先腐蚀后膨胀去掉细小的噪点和颗粒边缘的毛刺。开运算对面积小于结构元素的亮区直接抹除所以噪点半径小于结构元素半径就会被清掉。代码bw imopen(bw, strel(disk, 2));闭运算先膨胀后腐蚀填补颗粒内部的小孔以及颗粒边缘上的凹陷。比如颗粒表面有高光点分割后中心会出现黑洞闭运算可以把黑洞补上。代码bw imclose(bw, strel(disk, 3));结构元素半径的选择逻辑去噪用的小半径通常在1到3之间补洞用的半径要参考孔洞大小。我的经验是先用小半径开运算去噪再看结果决定要不要闭运算不要一上来两个都做——过度形态学操作会把细小的真实颗粒直接抹掉或者把两个独立的颗粒融成一个。如果颗粒之间有轻微粘连腐蚀操作可以先断开。但腐蚀会让颗粒整体缩小一圈影响面积统计精度所以腐蚀之后一般要配合条件膨胀恢复尺寸。这个操作比较进阶我放在第4章的粘连颗粒处理里细讲。2.4 连通域标记与计数bwlabel、regionprops 的核心用法二值图准备好了计数就变成了一件很笨的事把连在一起的白色像素团找出来每个团就是一个颗粒。bwlabel就是干这个的[L, n] bwlabel(bw, 8);第二个参数8表示8连通即一个像素的上下左右加四个对角都算相连。对于颗粒计数8连通是常规选择4连通会把对角线接触的颗粒拆成两个容易多计。L是一张标签图每个连通区域被赋予一个从1开始的整数编号。n就是连通区域的总数。但直接输出的n通常不等于最终答案因为里面混着噪声和异常大团块需要根据区域属性过滤。stats regionprops(L, Area, Centroid, BoundingBox, Perimeter); areas [stats.Area]; % 面积过滤去掉小于 minArea 的噪点去掉大于 maxArea 的黏连团块 idx_valid find(areas minArea areas maxArea); num_valid length(idx_valid);regionprops能输出的属性非常多除了面积、质心、外接矩形之外下面这几个在颗粒分析里极其常用属性含义用途Eccentricity离心率0为圆1为直线过滤条状杂质Solidity区域面积占凸包面积比例过滤不规则聚团Perimeter区域周长计算圆形度EquivDiameter等效直径粒径统计圆形度的计算方式是4*pi*Area / Perimeter^2完美的圆为1形状越不规则值越小。针对球形颗粒设定圆形度下限0.8就能把大部分杂质滤掉。这一步过滤做完num_valid才是真实的颗粒数。到这里核心计数已经跑通了。很多人以为做到这一步项目就结束了但实际开发GUI的时候才发现算法只是地基交互和鲁棒性才是真正花时间的地方。3. GUI设计思路把算法封装成可操作的桌面工具3.1 界面布局让不懂代码的人也能两分钟上手算法脚本写得好只能自己用加上GUI才能交付给别人用。这个项目的GUI我采用的是程序化创建方式基于uifigure而非老的figure因为uifigure的控件风格更现代而且能直接嵌入uitable表格展示区域属性。界面布局可以拆成三个区域左侧操作区选图按钮、计数按钮、参数输入框最小面积、最大面积、圆形度下限、导出结果按钮右上显示区原图预览右下显示区处理结果图用彩色框和高亮标记识别出的每个颗粒。实际写代码时控件排布用GridLayout来管理比绝对定位要省心得多fig uifigure(Name, 颗粒自动计数工具); grid uigridlayout(fig, [3, 2]); grid.RowHeight {fit, 1x, 1x}; grid.ColumnWidth {220, 1x}; % 第1行操作按钮 btn_open uibutton(grid, Text, 选择图像, ButtonPushedFcn, (src,event) selectImage()); btn_run uibutton(grid, Text, 开始计数, ButtonPushedFcn, (src,event) runCounting());布局的核心思路是最常用的动作放最大、最顺手的位置。选图是第一步计数是第二步导出是第三步让操作顺序从上到下自然流动不需要任何培训。3.2 回调函数与数据流避开全局变量地狱GUI最烦的问题就是数据怎么在回调之间共享。初学者最容易写成全局变量满天飞代码跑起来没问题但改起来想死。推荐的做法是用handles结构体配合guidata来存储和传递数据。function selectImage(app) [file, path] uigetfile({*.png;*.jpg;*.bmp;*.tif, 图像文件}); if isequal(file, 0) return; end app.I imread(fullfile(path, file)); % 原始图像存到 app 对象 app.I_gray rgb2gray(app.I); % 灰度图缓存避免重复计算 imshow(app.I, Parent, app.UIAxes1); end同一套思路runCounting回调从app对象读取图像数据处理完成后把结果写回app对象再刷新显示界面。这条数据流清晰不会出现两个按钮回调各持一份数据、互相覆盖的混乱局面。关于GUI里按钮回调的防错设计有一点要特别提醒未选图直接点计数按钮是必然发生的。回调函数里第一行就检查isempty(app.I)如果是真就弹出提示窗口并return。别觉得这是多余这种低级错误谁用都会犯。3.3 结果展示与导出把数据真正用起来算法只是得出一个数字这不够。实际使用场景里用户还需要知道每个颗粒的具体信息以及怎么把结果带走。这套GUI做两件事第一在结果图上把每个颗粒的位置标出来。viscircle可以在坐标区上画圆框配合regionprops输出的质心和等效直径每个颗粒圈一个圆同时用text标注编号。hold(app.UIAxes2, on); for k 1:length(idx_valid) c stats(idx_valid(k)).Centroid; d stats(idx_valid(k)).EquivDiameter; viscircles(app.UIAxes2, c, d/2, Color, r, LineWidth, 0.5); text(app.UIAxes2, c(1), c(2), num2str(k), Color, y, FontSize, 8); end hold(app.UIAxes2, off);第二导出结果到Excel或CSV。regionprops得到的是结构体数组直接struct2table转成表格再writetable写文件这样每个颗粒的编号、质心坐标、面积、等效直径、圆形度就都留下来了T struct2table(stats(idx_valid)); writetable(T, 颗粒统计结果.xlsx);有了坐标和面积数据用户后续还能自己画粒径分布直方图、算平均粒径、做空间分布分析。计数只是第一步数据化才是这套工具真正的价值所在。4. 实测中的计数误差与边界情况粘连、偏光、干扰全解析4.1 最头疼的粘连颗粒连通域合并的像素级处理颗粒计数翻车最严重的情况永远是颗粒互相粘连。两个颗粒紧挨着二值化后变成一个连通域bwlabel就把它当成一个颗粒数了。更头疼的是三个五个连成一串面积过滤根本救不回来。粘连问题的经典解法是分水岭算法。思路是把二值图转成距离图每个颗粒的中心区域距离值最大相当于一个个山包颗粒之间的粘连处距离值最小相当于山谷。然后用分水岭算法让水从山谷灌入淹没山包在山脊处画分界线。D bwdist(~bw); % 距离变换每个前景像素到最近背景的距离 D -D; % 取负让颗粒中心变成谷底 D(~bw) -Inf; % 背景区域不参与分水岭 L_ws watershed(D); bw_final bw; bw_final(L_ws 0) 0; % 分水岭脊线置为背景断开粘连直接对全图跑分水岭容易矫枉过正把单个颗粒也切开。更稳妥的做法是先按面积筛选出可疑聚团只对面积超出单颗粒面积阈值1.5倍以上的连通域做局部watershed正常颗粒不动。分水岭的效果很大程度上取决于距离变换前的bw质量。如果两个颗粒只是擦边接触距离图在接触点处只有一个很窄的谷分水岭能切如果两个颗粒重叠面积很大原始图像里就不存在可分的灰度信息分水岭也无能为力。这种情况必须回到成像端解决或者考虑深度学习方案。4.2 边界颗粒的取舍统计口径一定要提前定第二类常见误差来自图像边界。一个颗粒一半在画面内、一半在画面外bwlabel会把它当成一个完整颗粒统计但如果颗粒主体在画面外面积严重失真算出来的粒径分布会被干扰。我的处理方案是设置ROI统计区域。GUI里留一个选择统计区域的功能让用户手动框选要统计的范围代码只对ROI内部的连通域计数。ROI外的颗粒不参与统计与ROI边界相交的颗粒单独标记出来由用户决定是否计入。更严格的方案是边界触碰剔除连通域的BoundingBox如果与ROI边界相交直接剔除。x stats(idx_valid(k)).BoundingBox(1); y stats(idx_valid(k)).BoundingBox(2); w stats(idx_valid(k)).BoundingBox(3); h stats(idx_valid(k)).BoundingBox(4); if x 1 || y 1 || (x w) size(bw, 2) || (y h) size(bw, 1) % 该颗粒触碰边界按需求剔除或单独标记 end这里想提醒一句统计口径比算法精度更重要。哪怕计数算法很准如果边界处理规则不一致两批实验数据就无法对比。前期宁可多花十分钟把规则定清楚也不要后期在数据处理阶段纠结。4.3 光照不均与灰度漂移阈值怎么跟着图走前文提到过顶帽变换能校正渐变光照但实测中的光照问题往往更刁钻。比如某次拍摄时的反光在图像角落形成亮斑顶帽变换后亮斑区域依然残留高灰度背景阈值分割会把亮斑边缘也判成前景。这种情况下我一般用局部自适应阈值 高通预处理再顶帽变换的组合拳。具体流程原图转灰度顶帽变换去掉渐变的低频背景对顶帽结果再做一次高通滤波原图减中值滤波结果压制局部亮斑自适应阈值分割。每一层只解决一类问题整个链路下来鲁棒性会明显提升。但代价是参数变多这就要求GUI里的参数不是写死的而是做成可调节输入框用户面对反光严重的批次可以现场微调。4.4 参数调优的批量验证法别拿单张图调参最后聊一下参数的标定方法。很多人拿到代码就找一张效果最好的图调参数调得完美无缺换第二张图直接崩。这套思路是本末倒置——算法的目标是在一批图像上都稳定不是在某一张图上完美。我的做法是先取10到15张有代表性的图像覆盖亮、暗、颗粒密、颗粒疏等不同情况然后写一个小循环脚本遍历几组候选参数跑完自动把结果输出成表格和人工标定结果对比挑出错误率最低、波动最小的那组参数。thresholdRange [0.3, 0.4, 0.5, 0.6]; areaMinRange [10, 20, 30]; for t thresholdRange for a areaMinRange % 跑算法记录计数误差 end end这种网格化搜索虽然笨但非常有效。颗粒计数算法的参数空间通常不大网格搜索完全跑得动。调好一组参数后再回GUI里把默认值改掉后续使用就稳定多了。5. 从课程Demo到可用工具的进阶经验5.1 性能优化大图批量处理卡顿怎么办如果只是单张图片点一下按钮Matlab的处理速度完全够用。但实际场景里经常需要批量处理几十上百张图每张图还可能是几千万像素的高分辨率显微照片。这时有两个瓶颈第一个瓶颈是regionprops的计算量。当连通域数量上万时regionprops的全部属性输出会很慢。优化办法是只计算需要的属性比如只要Area, Centroid不要一次输出所有属性。第二个瓶颈是GUI界面刷新。每处理完一张图就imshow一次界面会一直卡顿。解决办法是批量处理时关闭实时刷新等所有图处理完了再一次性刷新结果。或者把批处理做成独立函数不在GUI线程里跑配合进度条提示。还有一个容易被忽略的问题Matlab默认用单线程大图滤波和分水岭在CPU上跑确实慢。如果场地有条件可以考虑用并行计算工具箱对一批图像用parfor并行处理速度接近线性提升。5.2 不只会数数尺寸筛选、形态分类与分布统计计数只是这套系统的地基。实际做材料分析的人拿到数据第一件事往往是画粒径分布直方图。区域属性里的EquivDiameter就是为此准备的。基于它可以直接给GUI加一个尺寸分布按钮输出直方图和累计分布曲线。更进一步根据圆形度、面积、长宽比做简单的形态分类规则圆形颗粒是一类不规则碎片是一类条状杂质是一类。这个功能在不纯样品分析里价值巨大。分类本质就是给区域属性设定一组条件和阈值不需要额外引入复杂算法。5.3 传统算法与深度学习方法的选择思考市面上现在很流行用深度学习做颗粒分割U-Net、Mask R-CNN这类模型在高粘连、强噪声场景下确实能打出漂亮的数据。但我做这个项目时依然坚持传统图像处理路线原因有三一是标注成本。深度学习需要逐像素标注的颗粒掩膜图标注几百张图的时间够传统算法方案从开发到验收好几个来回。二是可解释性。传统算法每一步都有明确几何含义——阈值分了什么、形态学消除了什么、面积阈值过滤了什么出了问题能追查、能解释。深度学习模型是个黑盒出了错很难说清楚是训练数据的问题还是模型结构的问题。三是部署成本。传统算法在普通电脑上跑得飞快不依赖GPU不依赖深度学习框架。对于实验室内自用的小工具这才是最实在的。当然如果颗粒粘连程度严重到人眼都难以分辨分水岭后依然错误率很高那确实应该考虑U-Net路线。我自己对这类场景的态度是先拿50张图试试传统算法的上限如果错误率能压到5%以内就不要再折腾深度学习。能用简单的方案解决就不必引入复杂的方案。最后再分享两个实操小技巧第一给GUI加一个恢复默认参数按钮。调参现场经常会调乱有个一键恢复能让使用者减少很多焦虑。第二批处理输出结果时命名里带上图像文件名一条数据一行记录后续做数据透视会特别方便。这套工具我前后迭代了三个版本最大的体会就是图像计数系统的精度上限不只在算法还在于交互设计能不能把算法能力稳定地发挥出来。把参数开放给用户、把结果沉淀成表格比追求某张图上的极致精确更有价值。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑