接手这个项目之前,我一直觉得水果分选不是什么高科技:无非是传送带上拍张照片,把颜色不对、形状不对的果子踢出去。真等动手做起来才发现,问题多得很。同一批苹果,早上采的和下午采的底色都不一样;运输出来的轻微碰伤前期根本看不出颜色变化,等到了客户手里才发黑;还有果皮上的蜡质高光,反光点能直接把相机参数带偏。单纯用阈值分割,调一组参数只能在一堆果子里活下去,换一批样本就崩。后来我把方案改成“图像特征提取 + 机器学习分类器”,全程用Matlab做原型验证,总算是把缺陷水果分选这件事从玄学变成了工程。这套流程做下来,我觉得值得整理成一篇完整的记录。
1. 先厘清边界:分选问题到底要解决什么
很多人一听到“缺陷检测”就想着用深度学习把病害区域框出来。但在实际产线上,尤其是中小规模的加工厂,问题往往更简单也更直接:他们只需要把果子分成“能卖的”和“不能卖的”,最多再加一档“可以降级处理”。所以我在项目一开始做的事,不是让模型去数有几个虫眼,而是和需求方把标准和优先级定下来。
1.1 缺陷类型和检测难点的初步拆解
常见的水果缺陷大体分几类:机械损伤(磕碰、挤压、划痕)、病理缺陷(腐烂、霉变、果锈)、虫害(虫眼、蛀洞)、还有形态异常(畸形、裂果)。每一类在图像上的表现完全不同。磕碰伤早期在颜色上变化很小,可能只是轻微凹陷,光线一变就看不见;腐烂区域则是从一个小点开始向外蔓延,颜色不均匀,边缘模糊;划痕是线状结构,形状细长;果锈这类表面纹理变化,颜色和正常果皮差别不大,但质感不一样。
这就意味着,单一特征很难覆盖所有缺陷。颜色特征对腐烂和褪色敏感,但对早期碰伤基本无效;纹理特征能抓住果皮表面粗糙度变化,却容易受果实品种影响;形状特征又能排除一些畸形果,但对斑点类缺陷无能为力。所以特征融合是必须的,而不是找到某一个“神奇特征”然后一劳永逸。
检测难点的第二个维度是环境。产线不是实验室,自然光和日光灯混在一起,传送带背景颜色可能跟果皮接近,果子还会滚动旋转,同一个缺陷在不同姿态下的形状会有很大变化。假如只采集“摆正了拍”的样本,训练出来的模型到了现场几乎没法用。项目一开始我吃过大亏就是在这里:用固定角度拍摄的图做测试准确率有98%,换成传送带随机角度一测,直接掉到82%。后来才把空间翻转和旋转增强加进去。
1.2 为什么传统阈值分割在这里不够用
传统机器视觉做人机分选,最经典的做法是在某个颜色空间里做阈值分割。比如把RGB转换到HSV或者Lab,然后在色调通道上设一个区间,把超出区间的像素标记为缺陷。这在单一背景、恒定光源下很有效,也是很多教科书里的入门案例。但真实水果分选要面对的信息不是一块简单的色块,而是一颗果实的局部区域。
原因有几层。第一,果皮的颜色不是均匀色,不同品种、不同成熟度、甚至同一颗果子的阴阳面都存在连续的渐变,阈值只能画一条硬线,很容易把正常的深色区域归为缺陷。第二,高光导致的过曝区域在HSI空间里会拉低饱和度,而这些区域的色调值不稳定,用固定阈值没法稳定识别。第三,阈值分割没有形状和纹理的概念。一块碰伤可能颜色只深了一点点,面积也不大,但是在固定阈值下它可能和果萼阴影混在一起,只有加入形态学先验才能分开。
机器学习方法不一样的地方在于,它用多个特征共同构造一个分类边界,而不是靠一两个通道的绝对数值。同样是碰伤,颜色特征给一个弱的信号,纹理特征给一个弱的信号,形状特征再给一个提示,三个弱信号合并起来,分类器就能学出一个比“颜色等于某个值”稳健得多的判断边界。这是它在缺陷水果分选里能站住脚的根本原因。
2. 数据准备与标注:整个项目最容易翻车的地方
模型训练花的时间其实不长,真正把进度拖慢的,是数据。我得说实话,如果你打算在Matlab里跑通这条流程,请把七成精力放在数据采集和标注上。模型好不好,一开始就由数据决定了。很多刚接触机器学习的开发者想不明白,觉得算法才是核心,结果数据标的乱七八糟,后面怎么调都救不回来。
2.1 图像采集阶段就要考虑产线约束
相机选型不用刻意追求超高分辨率,关键是要让最小的目标缺陷在图像上至少占据几个像素。比如你要求检测1毫米的瑕疵,视野宽度是100毫米,相机分辨率是1000像素,那么1像素对应0.1毫米,理论上够了,但要考虑运动模糊和镜头畸变,实际最好让最小缺陷占5个像素以上,也就是分辨率再大两到三倍。
光源是另一个高优先级问题。水果表面多半有蜡质或绒毛,容易产生镜面反射。刚开始我用了环形LED直射光,拍出来的苹果每个都有两个刺眼的白点,那个区域的颜色信息完全丢失,触发了大量误报。后来参考果品行业常见做法,改用低角度漫射光并加偏振镜,才把高光压下去。如果是在传送带场景,还必须注意曝光时间要短,否则运动模糊会抹掉细小的划痕。一个比较稳的参数组合是:曝光时间控制在1/1000秒以内,相机离果面30-40厘米,光圈调小一点保证景深够用,传送带速度不要超过每秒0.5米。
数据集划分也要讲究。同一颗果实多次拍摄的图像,应该全部放进同一个集合,要么都在训练集,要么都在测试集。否则模型在训练阶段“见过”某一颗果子的不同角度,测试时又碰到同一颗果子的另一个角度,造成数据泄露,测试准确率虚高。很多项目报告里写的99%准确率就是这么灌水灌出来的,一到现场就露馅。
2.2 标注规范、样本均衡和数据增强
标注本身不复杂,但要有统一规范。Matlab自带的Image Labeler可以用来框选缺陷区域,也可以直接用文件夹方式:把正常果实和缺陷果实的整图分别放在train/normal、train/defect里,用图像级标签做分类。对于分选需求,整图级二分类往往已经足够,不需要做像素级分割。但是要明确一条标注规则:一张图只要存在任意一处明显缺陷,就标记为缺陷类,不要因为缺陷小就犹豫。标准不一致,标签噪声会严重影响模型效果。
类别不平衡是这个项目里几乎必然遇到的问题。正常果实数量多,缺陷果实数量少,比例可能到10比1。如果直接训练,分类器会倾向于把所有样本都判成正常,因为这样正确率也高。解决思路有三种:一是对缺陷样本做重采样,把少数类复制几份;二是对多数类做欠采样,但会丢失信息;三是做离线数据增强,对缺陷样本进行旋转、平移、缩放、亮度抖动,然后在训练中过采样。实测下来,先用数据增强把缺陷样本数量补到正常样本的六到七成,再配合SVM的代价敏感学习,效果最好。
数据增强在Matlab里可以自己写,也可以直接用图像增强工具函数。旋转角度最好覆盖±30度,因为传送带上的果实姿态随机;亮度抖动范围设在±15%左右,模拟不同光照;水平翻转和轻微缩放也可以加。要注意的是,增强只能用于训练集,测试集必须保持现场采集的原始分布,否则评估结果不真实。我习惯在增强之后人工抽样看几十张图,确认增强没有把缺陷特征给扭曲掉。
3. 特征工程:从像素到可学习向量的核心环节
机器学习分类器输入的不是图像本身,而是一个固定长度的特征向量。特征工程的任务,就是想办法把图像里的物理差异量化成数值。这一步做得好不好,直接决定分类器的上限。传统机器学习领域有句话叫“Garbage in, garbage out”,特征就是那个输入。
3.1 颜色特征:从RGB转换到HSV和Lab
RGB是最原始的颜色表示,但它三个通道之间相关性强,而且受光照强度影响很大。同样是红色苹果,暗处拍出来的R值可能和亮处的G值差不多。所以更合适的做法是把图像转换到HSV或Lab色彩空间。
HSV中H表示色相,跟亮度关系小,适合用来区分果皮颜色异常;S表示饱和度,腐烂区域往往伴随饱和度降低;V表示明度,高光和暗影都反映在V上。Lab色彩空间的a分量在绿色和红色之间变化,对于苹果这类红色水果红,很容易判断成熟度和底色偏差。我常用的流程是:
% 读取图像并转换到HSV空间 img = imread('sample.jpg'); hsvImg = rgb2hsv(img); H = hsvImg(:,:,1); S = hsvImg(:,:,2); V = hsvImg(:,:,3); % 统计H通道基础指标 hMean = mean(H(:)); hStd = std(H(:)); hP95 = prctile(H(:), 95); hP05 = prctile(H(:), 5);这里之所以用分位数而不是最大最小值,是因为单个像素噪声很容易让最大值失真。取5%到95%的分位数,能描述主要像素的分布,过滤掉极端反光点。对S和V通道也同样统计均值、标准差和分位数,这样一个果实就能得到一组颜色描述子。缺陷区域在颜色上通常表现为色相偏移、饱和度异常或明度突变,这三类信息必须一起喂给分类器。
3.2 纹理与形状特征:捕捉颜色看不出的问题
早期碰伤和果锈这种缺陷,颜色变化很弱,但表面纹理会有明显的粗糙度变化。灰度共生矩阵是一种经典的纹理描述方式。Matlab里用graycomatrix和graycoprops可以快速计算对比度、能量、相关性和同质性。对比度反映纹理的深浅差异,能量反映纹理的均匀性,相关性反映纹理方向性。对果实缺陷来说,正常果皮纹理相对均匀,能量较高;碰伤区和腐烂区纹理杂乱,对比度上升,能量下降。
局部二值模式同样常用。它的核心思想是给每个像素点周边做二进制编码,统计整个图像中不同编码模式的频率直方图。Matlab从某个版本开始提供了extractLBPFeatures,可以直接得到LBP特征向量。如果版本较低,也可以自己写一个函数,网上类似实现很多,但要注意边界像素处理。
% 灰度图像上的GLCM特征 grayImg = rgb2gray(img); offset = [0 1; -1 1; -1 0; -1 -1]; % 四个方向 glcm = graycomatrix(grayImg, 'Offset', offset, 'Symmetric', true); textureStats = graycoprops(glcm, {'Contrast','Correlation','Energy','Homogeneity'});形状特征也是不可忽略的一环。在特征提取之前,需要先把果实区域从背景中分割出来,最简单的做法是用Otsu阈值分割或者K-means把图像分成前景和背景,然后取面积最大的连通域作为果实。随后用regionprops计算面积、周长、离心率、致密性等几何属性。圆形度可以这样算:circularity = 4*pi*Area/(Perimeter^2)。正常果实的圆形度接近1,畸形果或严重裂纹果的圆形度会明显下降。把这些形状指标和颜色、纹理特征拼接在一起,就得到了每张图像的特征向量。
3.3 特征标准化和降维
不同特征量纲差别很大:面积可能是几千,色相均值是0到1,对比度可能是几万。这种差异会让SVM在高维空间里被量纲大的特征主导。所以训练前必须做Z-score标准化,让每个特征均值为0、标准差为1。Matlab里一句[X, mu, sigma] = zscore(X)就搞定。训练时保存mu和sigma,预测时用同样参数对测试数据做标准化。
特征数量要控制。如果只有几十个特征,通常不需要降维。但如果把LBP的全部直方图bin都纳入,维度可能到几百,这时候可以做PCA或者用relieff选择有效的特征子集。我实际走下来,颜色统计量大约五六个、GLCM四个、LBP二三十个、形状三四个,总共不超过五十维,配合样本量几百张,已经足够训练出稳定的模型。特征越多越要小心过拟合,模型在训练集上表现好不代表在明天新采的果子上表现好。
4. 分类器训练与评估:选择合适的机器学习算法
有了特征向量和标签,接下来就是选分类器。Matlab的Statistics and Machine Learning Toolbox里提供了SVM、随机森林(TreeBagger)、决策树、KNN、朴素贝叶斯、判别分析等多种选择。我没有一开始就全试一遍,而是先定了两个主力候选:SVM和随机森林。
4.1 SVM和随机森林怎么选
SVM的优势在于小样本下泛化能力强,尤其在高维空间里,它只依赖支持向量来构建边界,不容易被远处无关样本干扰。它的核心参数是核函数、BoxConstraint和KernelScale。核函数我用的是RBF高斯核,因为它能刻画非线性边界,对特征之间的关系没有强线性假设。BoxConstraint控制误分类惩罚,越大越容易过拟合;KernelScale控制核的宽度,越小越能拟合复杂边界但也越容易过拟合。
随机森林的思路正好相反,它是一堆决策树的集成。每棵树在数据子集和特征子集上随机训练,最后对输出做投票。它对特征相关性和异常值没有那么敏感,还能量化每个特征的重要性。在样本不均衡的时候,随机森林通常比普通SVM表现得稳。但它的模型文件偏大,预测速度稍慢,不过在单张图片级别完全够用。
下面是一个简单的对比表,我在选型时主要参考这几个维度:
| 模型 | 非线性能力 | 调参难度 | 训练速度 | 预测速度 | 可解释性 | 小样本表现 |
|---|---|---|---|---|---|---|
| SVM(RBF核) | 强 | 中 | 中 | 快 | 弱 | 好 |
| 随机森林 | 强 | 低 | 快 | 中 | 较强 | 较好 |
| 决策树 | 中 | 低 | 快 | 快 | 强 | 容易过拟合 |
| KNN | 取决于距离 | 低 | 无训练 | 慢 | 弱 | 样本多才好 |
我在实际项目里的选择是:先用SVM做主线,因为它在Matlab里的接口清晰、超参搜索方便,而且模型小,容易部署;随机森林作为交叉验证的对比项,用来确认SVM的性能没有异常。如果换一个数据规模更大的场景,我会更倾向于随机森林,或者直接考虑深度学习。
4.2 Matlab训练流程、交叉验证和超参搜索
SVM训练代码很简单。假设特征矩阵是X(每一行是一张图的特征向量),标签是Y(0表示正常,1表示缺陷),可以这样训练:
% 拟合并做5折交叉验证 rng(42); SVMModel = fitcsvm(X, Y, ... 'KernelFunction', 'rbf', ... 'BoxConstraint', 1, ... 'KernelScale', 'auto', ... 'Standardize', true, ... 'ClassNames', [0 1]); CVSVMModel = crossval(SVMModel, 'KFold', 5); loss = kfoldLoss(CVSVMModel); fprintf('5折交叉验证错误率: %.2f%%\n', loss * 100);如果第一版效果不够好,可以用贝叶斯优化自动搜索超参数。Matlab的fitcsvm支持OptimizeHyperparameters选项,但要注意搜索过程会反复训练模型,样本量大的时候会等很久。为了节省时间,我会固定核函数为RBF,只优化BoxConstraint和KernelScale两个参数,目标函数是交叉验证损失。
optimizedSVMModel = fitcsvm(X, Y, ... 'KernelFunction', 'rbf', ... 'OptimizeHyperparameters', {'BoxConstraint','KernelScale'}, ... 'HyperparameterOptimizationOptions', struct(... 'AcquisitionFunctionName', 'expected-improvement-plus', ... 'MaxObjectiveEvaluations', 30, ... 'Kfold', 5));评估阶段不要只看准确率。在分选场景里,漏掉一个坏果流出到市场,比误杀一个好果的代价大得多。所以我更关心召回率,也就是所有真实缺陷样本中有多少被正确识别出来。用confusionchart可以清楚展示混淆矩阵,然后手工计算精确率、召回率和F1分数。精确率是机器判定为缺陷的样本中有多少真的是缺陷;召回率是确实有缺陷的样本中有多少被判出来。分选系统的目标通常是高召回率,即使牺牲一点精确率,让误杀率稍微高一点,也比漏检亡羊补牢强。如果需要调整阈值,SVM默认阈值是0.5,可以通过fitcsvm的ScoreTransform和predict输出来调整。
5. 实际部署与现场调试:从正确率到稳定性的距离
模型在实验台上跑出95%的准确率,只代表故事刚开了个头。现场环境的变化会让精度掉得你怀疑人生。我在这部分要写的是如何把一个看上去还行的模型,变成一个真正能在产线上连续工作的分选逻辑。
5.1 打包部署方案与实时性预算
Matlab模型部署有几个可选路线。如果工厂的工控机装了Matlab运行环境,可以通过Application Compiler打包成独立程序,界面做成简单的实时检测窗口。如果后续要嵌入到小型设备里,可以用MATLAB Coder将特征提取和SVM预测的代码转换成C/C++,部署在ARM板卡或工业计算机上。我建议从一开始就把特征提取和分类器封装成独立函数,输入是图像路径或图像矩阵,输出是标签和置信度,这样后续不管走哪条部署路线都方便。
实时性要提前算好。比如传送带速度是每秒0.5米,相机视野宽度是30厘米,那么每张图有效时间大约是0.6秒。图像采集、预处理、特征提取和分类加起来必须小于这个时间。传统特征提取加SVM通常在几十毫秒内完成,压力不大。如果用深度学习做端到端检测,单张图在普通CPU上可能到几百毫秒,必须上GPU,否则跟不上产线。这也是我坚持用传统机器学习的一个重要原因:成本低,部署简单。
5.2 现场最常见的两类翻车问题和修复方法
第一个翻车点是高光。水果表面有一层天然蜡质,进入分选现场后,光源角度和输送带振动会让高光位置不断变化。高光区域的像素几乎饱和,颜色特征完全失真,SVM可能把这些区域当成异常。应对办法有两种:一种是在光源设计阶段加偏振片,物理上消除镜面反射;另一种是在算法上检测高光区域,如果高光占比过高,就把该区域从特征统计中剔除,或者单独把高光比例作为一个特征加入模型,让模型学会处理它。
第二个翻车点是品种和产地的底色变化。同一批模型的测试数据是某个产地的红富士,下一次来的是山东栖霞产的,颜色更红更深,模型的误报率立刻就上去了。这说明模型并不是学会了“缺陷是什么”,而是记住了“当前数据里缺陷长什么样”。解决办法不是重新开发,而是维护一个增量样本库。现场每次人工抽检发现误报或漏检,就把对应图片归档,每周重新训练一次模型,测试集里保留一批固定样本来验证模型是否回退。这比一次性交付模型然后甩手不管要实用得多。
5.3 一条更稳妥的迭代路线
如果现在让我重新做一个水果分选项目,我不会急着堆特征或者直接套深度网络。我会先搭一个最小闭环:用最基础的颜色和形状特征训练一个SVM,放到几段真实视频里跑一跑,看看失败案例集中在哪。是光线问题,还是标注问题,还是特征不足以区分?根据失败案例再决定加纹理特征、调超参数、或者换模型。每一个调整都用历史回归测试集验证,避免修好一个错引出另一个错。
最后分享一个习惯:我在每轮实验后都会保留一份模型配置文件,里面记录特征列表、采集日期、图像预处理参数、分类器参数、测试集组成和混淆矩阵结果。这样过了两个月再打开项目,还能快速复现当时的实验,而不是面对着十几个不同版本的脚本发呆。做工程不是写论文,可复现性和鲁棒性才是真正让分选项目从实验室走向产线的关键。