
做杨梅分级这件事比很多人想象中要麻烦得多。杨梅颜色深、表面全是颗粒状小突起磕碰伤和霉变一开始往往不明显光线一变拍出来的照片色调就完全不一样。最近我把一套基于Matlab GUI的杨梅质量检测与分级系统完整跑了一遍从图像分割、特征提取到面板交互全部打通代码里有完整的GUI界面能直接看到识别结果和分级输出。这篇文章就把整个实现过程、核心算法逻辑和调试时踩过的坑都整理出来给正在做水果识别、农产品分级相关课题或者刚接触Matlab图像处理的朋友做个参考。这套系统的核心价值在于不需要昂贵的硬件一台普通电脑加一个USB摄像头或者手机拍图就能对单颗杨梅做大小、色泽、表面缺陷的综合评价并自动划分等级。重点在GUI面板设计、分割算法和分级规则三个环节上。下面按照我的实际开发顺序来复盘。1. 系统整体设计与分级思路1.1 为什么选Matlab GUI做杨梅分级我在这个项目里选Matlab而不是OpenCV或者Python首要原因是开发效率。Matlab的Image Processing Toolbox把图像读取、颜色空间转换、形态学处理、连通域分析都封装成了现成函数写验证代码特别快。做算法原型时我可以在命令行一条条执行看清楚每一步mask长什么样再把它固化到GUI回调函数里。Python当然也能做但要处理环境依赖、界面库封装周期明显更长。第二个原因是GUI原生支持。Matlab的GUIDE和App Designer都能拖拽控件生成界面image处理结果可以丢到axes控件里刷新表格控件可以直接输出分级数据。对实验室场景和小型分选测试平台来说这种“带面板”的交互方式足够直观指导老师或产线工人不需要懂代码点按钮就能用。第三个原因是学术传承。很多高校的数字图像处理课程设计仍然以Matlab为主源码便于继续修改和复现。所以这套系统我刻意保持纯Matlab函数实现不引入外部依赖拿到源码直接能跑。1.2 分级标准与系统架构杨梅质量分级不能只看一个指标。从市场角度看大颗粒、颜色深紫红、表面无明显破损霉斑的杨梅价格最高小粒、发青或带伤口的只能走低价渠道。因此我把分级规则拆成三个维度大小用连通域最小外接椭圆的长轴长度换算成实际尺寸用于区分大果、中果、小果。色泽在HSV颜色空间里统计成熟区色调H和饱和度S判断是否达到紫红标准。表面缺陷通过与周边正常果皮颜色做差分检测裂果、霉斑和磕碰造成的褐变区域计算缺陷面积占比。系统整体架构分为五层图像输入、预处理、分割、特征提取、决策输出。GUI作为最上层把所有步骤串起来。采集端支持本地图片读取和摄像头实时抓帧我实际测试主要用本地图片因为摄像头标定和光照控制会引入额外变量先把算法调稳再上实时流是更稳妥的路线。2. 图像预处理与分割算法拆解2.1 颜色空间选择与背景去除杨梅检测第一步是把果体和背景分开。最初我直接对RGB做阈值分割效果非常糟糕——RGB三个通道和光照强度高度相关同样一颗杨梅在亮处和暗处的R、G、B值差异巨大阈值完全没法固定。后来全部换成HSV空间发现稳定得多。HSV中的H通道色调区分着色相对光照变化不敏感S通道饱和度描述颜色鲜艳程度V通道明度对光照直接回应。杨梅果体通常是暗红色或紫红在H通道里落在0.9到1.0之间也就是品红区间而常见背景比如白色托盘、绿色衬纸、黑色传送带色调和杨梅有明显差异。实际分割时我采用多条件组合hsv rgb2hsv(img); H hsv(:, :, 1); S hsv(:, :, 2); V hsv(:, :, 3); mask (H 0.85) (H 1.0) (S 0.25) (V 0.08);这里S大于0.25是为了过滤掉背景中偏灰白的区域V大于0.08是把阴影里几乎看不见的暗部去掉。如果背景是深黑或者深蓝H通道会有较大干扰建议在拍摄时使用统一的浅色背景板不然就要针对实际背景加一道颜色排除逻辑。2.2 阈值分割与形态学修正纯色度阈值分割的mask通常会有很多孔洞和毛刺。杨梅表面那些颗粒状突起在图像里会形成微小的高光和暗影导致果体内部出现零散的黑点。直接用这个mask去算面积结果会偏小且不稳定。我用形态学闭运算来修复。闭运算先用膨胀填补内部小孔再用腐蚀把边界恢复平滑。对杨梅这种近圆形果体圆形结构元素最合适半径取15像素左右这个值要根据图像分辨率调整。分辨率越高半径要适当加大否则单个颗粒间的缝隙填不掉。mask imclose(mask, strel(disk, 15)); mask imfill(mask, holes); mask bwareaopen(mask, 1000);imfill把封闭区域内部的空洞填充掉避免杨梅表面的暗斑被算成背景。bwareaopen删除面积小于1000像素的小连通块把背景里的噪点清理掉。顺序不能乱一定要先闭运算再填充最后再做面积筛选。先做面积筛选的话小碎块可能会把imclose的结构元素撑大效果变差。2.3 单颗杨梅轮廓提取与粘连分割如果一次只拍一颗杨梅上面的mask就是最终结果。但实际场景里经常出现多颗连在一起这时连通域会被识别成一个大块统计面积和尺寸就会严重失真。我处理粘连的思路比较传统距离变换加水域分割学名叫分水岭。dist -bwdist(~mask); dist(~mask) -Inf; L watershed(dis);这里bwdist计算每个背景像素到最近果体像素的距离加负号后果体中心区域值最大分水岭会在不同果体中心之间画出分界线。我通常是先对整体mask做一次距离变换找到局部极大值作为种子点再调用watershed。这个方法对圆润的杨梅效果不错但对形状畸变的果子容易出现过分割需要在分割后做面积合并。如果只是做课设展示建议干脆用“单颗拍摄”的简化流程把分割精力省下来放在颜色和缺陷检测上。我在源码里同时保留了两种模式通过面板中的下拉菜单切换单果识别和多果分割。3. 特征提取与质量分级实现3.1 大小与形状特征计算分割得到单颗杨梅的mask后我用regionprops直接提取几何特征。这里的核心参数有Area、Centroid、MajorAxisLength、MinorAxisLength、Eccentricity。其中MajorAxisLength是果体最长轴的长度单位是像素要换算成毫米需要提前标定相机。我实测的标定方法是在拍摄平面上放一枚直径25毫米的硬币计算硬币像素直径与物理直径的比值得到每毫米像素数scale然后对所有尺寸特征做除法。杨梅不是标准圆长轴比短轴更能代表实际大小。分级时我用长轴值作为尺寸判据。Eccentricity偏心率用来做形状判据畸形果的偏心率通常大于0.6可以单独筛出来。stats regionprops(mask, Area, Centroid, MajorAxisLength, ... MinorAxisLength, Eccentricity); for k 1:length(stats) diameterPixel stats(k).MajorAxisLength; diameterMm diameterPixel / scale; eccentricity stats(k).Eccentricity; end3.2 颜色与成熟度特征分析颜色特征不能只看整幅RGB均值因为杨梅成熟度不同色泽区间跨度很大。我把重点放在HSV空间的H和S分量上。完全成熟的杨梅呈深紫红色色调H集中在0.88到0.96之间饱和度S在0.45以上偏青的未熟果H则落在0.35到0.5区间。在mask区域内统计H的均值和方差能有效区分成熟度。实现时要注意H通道在0度到360度之间有环形边界0.98和0.02在数值上看起来差距很大实际颜色却很接近。计算均值前要把H分量映射到圆形空间做向量均值或者先判断是否有大量大于0.95的像素再决定是否做角度换算。我踩过这个坑最初直接用mean函数结果紫红果的H均值被算成0.6颜色判断完全乱了。huePixels H(mask); % 将色调转换为角度后再取均值 hueAngles huePixels * 360; meanHue atan2(mean(sin(deg2rad(hueAngles))), ... mean(cos(deg2rad(hueAngles)))) * 180/pi; if meanHue 0 meanHue meanHue 360; end3.3 表面缺陷检测策略杨梅的损伤分两类一类是明显裂口和霉斑灰白色或黑色和正常果皮的深红差异大另一类是捏伤、水渍造成的局部褐变颜色和正常果皮非常接近肉眼都难以辨认。我采用局部异常检测来做缺陷区域提取。思路是先在mask区域内计算灰度图的中值将每个像素的灰度与该中值比较。正常果皮区域灰度分布比较集中损伤区域会有明显偏移。再结合饱和度通道霉斑区域S通常偏低裂口处则会出现高饱和度暗边。把这两个条件合并得到缺陷区域。grayImg rgb2gray(img); foreground grayImg(mask); medGray median(foreground); defectMask (abs(grayImg - medGray) 35) S 0.5;缺陷面积占比就是defectMask像素数除以mask总像素数。这个35的阈值是我在室内光源下反复实验得到的经验值光线变强或变弱时要同步调整。另一个更稳的方法是针对单个杨梅取Otsu全局阈值做自适应差分但计算量稍大GUI下实时性会受影响。3.4 分级决策规则与结果输出特征全部算好后我采用规则评分的方式决定等级。规则的好处是逻辑透明便于课上讲解和调整。等级划分参考常见生鲜标准我做了简化等级长轴直径(mm)色调H均值(度)饱和度S均值缺陷面积占比偏心率特级≥ 25320~360≥ 0.45 5% 0.5一级20~25300~3600.35~0.455%~10% 0.6二级15~20270~3300.25~0.3510%~15% 0.7等外其余情况其余情况其余情况 15%≥ 0.7实际运行时我按顺序判尺寸在尺寸合格后再看颜色和缺陷是否达标任一指标过低就降级。评分结果用set函数写到GUI的Edit控件或表格里同时用颜色提示特级为绿色、等外为红色方便现场一眼分辨。4. GUI面板设计与功能实现4.1 面板控件布局与功能分区这个系统叫“带面板”核心就是把所有操作入口集中在一个界面上。我用GUIDE创建了.fig文件界面分成三大区域左侧图像显示区右侧结果展示区底部操作区。图像显示区放两个axes一个显示原图一个显示分割掩膜或检测结果叠加图结果展示区用静态文本、Edit控件和表格来显示等级、尺寸、颜色和缺陷占比底部放“打开图像”、“开始检测”、“批量处理”、“退出系统”四个按钮。布局时要注意axes之间不要叠放GUIDE里一旦重叠容易导致句柄混乱。我习惯在属性检查器里直接固定每个控件的Position不使用自动布局这样在窗口缩放时不会乱跑。4.2 回调函数编写与数据传递GUI的数据传递依赖handles结构体。每次打开图像后把工作路径和图像矩阵都存进handles再调用guidata(hObject, handles)刷新。检测按钮回调从handles里读取图像执行后把结果再存回去。打开图像的回调示例function pushbutton_open_Callback(hObject, eventdata, handles) [filename, pathname] uigetfile({*.jpg;*.png;*.bmp}, 选择杨梅图片); if isequal(filename, 0) return; end imgPath fullfile(pathname, filename); handles.img imread(imgPath); handles.imgPath imgPath; axes(handles.axes1); imshow(handles.img); title(原始图像); guidata(hObject, handles);检测按钮的回调则调用我封装好的处理函数function pushbutton_detect_Callback(hObject, eventdata, handles) img handles.img; scale handles.scale; [mask, defectMask] segmentAndDetect(img); [feature, grade] evaluateGrade(mask, defectMask, scale, img); set(handles.editGrade, String, grade); set(handles.editDiameter, String, sprintf(%.1f mm, feature.diameter)); set(handles.editHue, String, sprintf(%.1f deg, feature.hue)); set(handles.editDefect, String, sprintf(%.1f%%, feature.defectRatio * 100)); axes(handles.axes2); imshow(imoverlay(img, bwperim(mask), cyan)); title(检测结果); guidata(hObject, handles);回调函数名必须和控件Tag严格对应比如按钮Tag是pushbutton_open回调函数名就是pushbutton_open_Callback。GUIDE生成的自动命名可以改但改了Tag以后回调绑定关系需要重新在属性检查器里绑定不然点击没有任何反应。很多新手在这里卡住我建议直接沿用默认Tag只改显示文字。4.3 批量识别与Excel导出除了单张识别我加了批量文件夹识别。面板里放了一个“批量处理”按钮回调中用uigetdir选择文件夹遍历所有jpg图片把每张图的结果按行追加到表格最后用xlswrite写Excel。files dir(fullfile(folder, *.jpg)); data cell(length(files), 6); for i 1:length(files) img imread(fullfile(folder, files(i).name)); [mask, defectMask] segmentAndDetect(img); [feature, grade] evaluateGrade(mask, defectMask, scale, img); data(i, :) {files(i).name, grade, feature.diameter, ... feature.hue, feature.defectRatio, feature.eccentricity}; end xlswrite(fullfile(folder, result.xlsx), data);这里有个细节xlswrite在macOS和高版本Windows下偶尔会报错需要确认Excel COM组件正常。如果没有安装Excel建议直接写csv用fopen加fprintf保存稳定性更高。4.4 GUI界面优化与运行打包界面要给人看的不是只给代码跑。我在字体选择上全部用微软雅黑避免Matlab默认字体在中文界面下发虚。控件的Enable属性根据流程动态控制没有打开图像时检测按钮置灰检测完成后导出按钮才可用。这样能防止用户误操作导致空指针错误。如果要脱离Matlab运行可以用deploytool生成独立exe但需要安装MATLAB Compiler。需要注意的是独立程序打包后第一次启动比较慢图像处理工具箱必须包含在打包列表里。我在实测中发现如果不勾选Image Processing Toolbox的runtime组件生成exe打开图片时会直接报“function imread not found”。5. 实操过程从源码到完整系统跑通5.1 环境准备与源码结构我使用的运行环境是MATLAB R2022aWindows 10 64位。源码包打开后主文件是Yangmei_GUI.fig和Yangmei_GUI.m算法函数单独放在fun文件夹下。先运行Yangmei_GUI界面就能直接拉起来。如果用一个更干净的机器学习指标来打分这套系统的识别准确率取决于分割质量。准备测试集时最好把杨梅放在固定位置拍摄背景用白色或淡蓝色打光要均匀。我在室内用LED平板灯色温5500K用柔和散射来减少表面反光。强反光会让高光区域的饱和度掉到0.1以下分割mask中间容易缺一块。5.2 快速运行并调试参数打开GUI以后第一步点“打开图像”测试分割效果。如果出现背景大片白色被误分为果体先看静态图是偏绿背景还是偏红背景。偏绿背景需要把H通道下界从0.85改成0.95因为绿色H大约在0.3左右应该不会误判如果误判可能是底色太暗影响了S和V。建议直接在命令行查看一下任意像素的HSV值再回填到代码里。第二步调尺寸换算。在源码里有一个scale变量默认值是10表示每毫米10像素。找一个已知直径的实物或者标定尺拍一张图量出像素直径后更新scale。不更新的话长轴25毫米的果可能被判成50毫米等级会完全错乱。5.3 自定义分级阈值与训练数据规则写死在代码里如果要改阈值只需要在evaluateGrade函数顶部的参数区修改。我把所有阈值集中放在一个struct里避免散落在代码各处gradeParams struct(); gradeParams.diaLevel1 25; gradeParams.diaLevel2 20; gradeParams.diaLevel3 15; gradeParams.hueLevel1 320; gradeParams.hueLevel2 280; gradeParams.defectLevel1 0.05; gradeParams.defectLevel2 0.10;每次调整后建议跑一遍20张已经人工标注好等级的测试图统计混淆矩阵。我实际调参时发现缺陷面积占比阈值对结果影响最大经常因为光照变化导致缺陷误检所以“特级”分类要留出余量不要卡在5%边缘。6. 常见问题与排查技巧实录6.1 分割结果不干净背景噪点很多这个问题最常见的成因是S阈值太低导致浅色背景进入mask。建议把S阈值从0.25提升到0.35同时检查bwareaopen的尺寸参数背景碎块一般面积不大删除1000像素以下的连通域足够如果图片分辨率是1920×1080可以提高到3000。另一种情况是杨梅和背景颜色接近比如深红杨梅放在暗红桌面上。这属于拍摄方案问题算法再好也救不了必须换背景。我的原则是背景与杨梅色相距离至少差120度用亮蓝色或纯白分割阈值才稳定。6.2 光照不均导致半边杨梅被判定为缺陷因为缺陷检测用的是灰度局部差分如果光源只照到杨梅一侧另一侧被阴影覆盖灰度值会显著低于中值于是被误判成缺陷。解决办法有两个一是增加正面补光让整个果面受光均匀二是把缺陷检测从灰度差分改成HSV饱和度差分阴影区的饱和度只会轻微变化而真正霉斑区域的饱和度会大幅下降。我在实测里发现国内小型分选线的车间光照往往偏黄HSV的H均值会整体偏移。对此可以在分割前做一次白平衡校正用矫正后的图像再提取颜色特征。6.3 GUI按钮点击无反应显示函数未定义如果回调里调用自定义函数时提示未定义十有八九是fun文件夹没有加入搜索路径。在打开源代码路径后需要右键文件夹选择“添加到路径”或者在主InitFcn里加addpath(fullfile(fileparts(mfilename(fullpath)), fun));如果点击按钮完全没有反应先检查按钮的Callback属性是否绑定了正确函数。GUIDE里右键按钮可以查看回调跳转如果绑定丢失选择正确的那一行重新绑定。6.4 检测速度太慢界面像卡死纯Matlab图像处理做单张识别正常在一秒以内。如果卡顿明显多是因为regionprops遍历了太多mask或者是xlswrite在等待Excel进程。我建议在批量处理前用tic/toc统计每步耗时优先优化分割循环。对于多果场景避免对每个连通域都调用一次polyfit替代椭圆拟合直接用regionprops的椭圆属性速度能提升好几倍。6.5 中文字符在界面里变成乱码这个坑在Matlab高版本依然存在。直接用中文写控件字符串时如果.fig文件和脚本的编码不一致界面输入法会乱。我建议在代码初始化中用set(handles.text, String, 杨梅分级)统一设置中文而不是在GUIDE属性面板里直接键入中文。脚本文件保存时选择UTF-8再在文件开头加一句feature(DefaultCharacterSet, UTF-8)能解决大多数乱码情况。7. 现场演示与扩展经验说实话杨梅分级系统做到“演示能看、结果能讲”是非常适合课程设计的。我在答辩现场会额外准备一组对比图一张是光照均匀下的识别结果一张是故意侧光的失败案例这样既能展示算法能力也能说明图像采集的重要性。评委不只看代码能不能跑更关注你是否理解每个参数的意义。如果后续想把这个系统往实际产线推有几个改进方向值得考虑。一是把分割核心换成基于深度学习的语义分割比如U-Net对粘连果的分割精度会明显优于传统分水岭但需要标注一批杨梅数据二是加入机械臂或传送带控制通过串口把分级结果发送给PLC实现自动分拣三是把GUI迁移到App Designer界面更现代且对高DPI显示器支持更好。我个人更推荐先做第一个因为图像质量直接决定分级结果下限识别模型再强也救不了噪声极大的图。再分享一个实操小技巧测试时不要只挑品相好的杨梅多拿几颗带疤的、软的、有点发霉的样本把它们混在一组反复跑几轮。这样能逼着你去调缺陷检测参数而不是让系统只在“理想图片”上自嗨。我在调完这轮之后整个系统的鲁棒性提升非常明显也真正理解了为什么杨梅分级比苹果、橙子更棘手。