首页 / 资讯中心 / 文章详情

基于Matlab的缺陷水果分选:从特征工程到机器视觉产线落地

基于Matlab的缺陷水果分选:从特征工程到机器视觉产线落地 ★ FEATURED ARTICLE
去年有段时间我在帮一条果品分选线做视觉方案的前期评估。那天下午蹲在人工分拣台边上看了大半个钟头分拣员手脚很快但传送带稍微快一点带碰压伤的苹果就混过去了病斑果偶尔也会漏。这不是手速问题是人的注意力和标准一致性天然有上限——同一个果上午班和下午班可能给出不同结论。所以后来我把精力投到了用机器学习做缺陷水果分选上完整走了一遍基于Matlab的实现流程。这篇文章不打算堆学术名词就按我自己从数据采集、特征设计、模型训练到产线评估的实际路径来写把其中的取舍、坑和为什么这样做讲清楚。适合两类人看一类是在做水果质量检测相关课题的同学另一类是打算把机器视觉引入分选产线的工程师。1. 缺陷水果分选这件事难点并不在机器而在标准1.1 先搞清楚你要检测哪些缺陷很多人以为分选任务就是一个二分类好果和坏果。真做了才知道这个坏字背后是一堆视觉表征完全不同的东西。以苹果为例我整理过一份常见的缺陷类型清单碰压伤早期可能只是轻微水渍状颜色变化很浅普通可见光下肉眼都难辨认腐烂往往伴随褐变、皱缩和软化但边缘是渐变的容易和果蒂阴影混在一起病斑是局部块状变色面积小但色差明显虫眼就是个孔洞反而好检测果锈则是表面粗糙的黄色斑块。还有畸形果轮廓明显不圆但颜色纹理完全正常。如果把这些全部扔进一个异常类别里做训练样本标签会乱模型内部也会纠结——因为病斑和腐烂在特征空间里根本不是一回事。我的建议是第一步先按缺陷类别建立分类表明确每个类别的视觉表现和检测思路再做标签设计。给个苹果的参考表缺陷类型典型视觉表现检测难度常用特征碰压伤表面凹陷、轻微褐变或水渍状高早期不明显局部纹理变化、近红外吸收差异腐烂褐变、软化、皱缩、菌丝中易与果蒂阴影混淆HSV/Lab颜色、纹理粗糙度病斑点状或块状变色表面凸起中颜色区域、边缘梯度虫眼圆形小孔洞低边缘检测、孔洞面积畸形轮廓不规则中离心率、周长面积比果锈果面粗糙黄色斑块低纹理、颜色饱和度这张表的价值在于它决定了你后面选什么特征、用什么相机光源。比如碰压伤在可见光下很难搞如果你要检测它需要认真考虑打光方式普通白光是不够的。1.2 分选决策的本质是分层问题产线分选从来不是一个模型搞定所有事。尺寸重量分选、外观缺陷检测、内部品质评估是不同层级的问题。外观缺陷检测内部又分两段先判断有没有缺陷再对缺陷的面积、数量和严重程度做分级。直接训练一个端到端的模型输出优等果/一等果/二等果/淘汰果在样本量不足时特别容易翻车因为相邻等级之间的差异非常微妙。更稳的路线是先把缺陷有无做出来这个二分类模型做扎实了再叠加一个缺陷分级器。分层做的另外一个好处是可解释性产线工人能看懂为什么某个果被踢出来不是因为一个黑盒模型说不合格而是因为它在腐烂类别上的输出超过阈值。2. 数据先行样本采集、标注与类别平衡的实战思路2.1 采集阶段就要奔着产线环境去数据采集是决定整个项目上限的环节。实验室里搭个摄影棚慢慢拍和产线上用真实传送带、真实光源条件下拍结果天差地别。工业产线的常见配置是暗箱环境加LED环形无影光源相机从顶部俯拍有时配背光让轮廓更清晰。你要是不信可以试试在自然光下采一套数据训练再搬到产线的固定光源下跑准确率掉10个百分点都很正常。采集角度也要想清楚。单个俯视视角能覆盖大部分缺陷但碰压伤和果蒂附近的腐烂侧面视角会看得更清楚。所以正规一点的方案是多相机多视角每颗果经过时抓拍三到五张图。这时候有个容易被忽略的问题同一颗果的多张图片在训练和测试集划分时必须分到同一边不能一张在训练集、一张在测试集否则数据泄漏会让评估成绩虚高。我见过不止一个项目测试准确率99%产线一跑就崩一半原因都在这里。训练测试分割应该按果实分组做而不是按图片做。2.2 标注规范不是拍脑袋定出来的标注是脏活累活但也是最不能省的部分。这里的关键是标注规范要具体到可以重复执行。举个例子腐烂不能定义为看起来坏了而要写成表面变色区域面积超过整果面积的2%或出现明显皱缩、软化。标准不明确两个人标同一批图标签一致性可能只有七成模型训练出来自然左右摇摆。实操中可以用一个简单的办法提升一致性一份样本由两个人独立标注算标注一致性对不同意见的样本进行复核。我自己的习惯是把不确定单独放在一边不要硬塞进某个类别最后统一讨论讨论后仍然模糊的样本直接丢掉。缺陷样本本来就稀缺丢图有点心疼但模糊样本进模型只会拉低精度得不偿失。2.3 类别不平衡不要被好果占多数骗了真实的产线批次里好果比例极高可能90%以上都是合格果。如果你直接拿原始比例训练模型只需要学会说好果就能拿90%以上的准确率但它对缺陷几乎无感。这不是模型笨是目标函数被数据比例带偏了。处理办法无非几个方向对稀少缺陷类做数据增强包括旋转、水平翻转、小角度裁剪、亮度扰动、加高斯噪声对好果做欠采样把好果数量控制到缺陷样本的两到三倍以内或者给模型设置类别权重。注意增强不能改变缺陷的本质。旋转对苹果是天然合理的因为产线上苹果姿态随机但拉伸变形就不是好主意它会改变真实的缺陷形态。样本量的底线我也说下每类缺陷至少要有300个独立果实个体起步少于这个数模型泛化性基本没法保证。这里强调的是个体一个果实多角度拍出来五张图在增强和训练里可以当作五张样本但它本质上是同一个实例对泛化的贡献有限。3. 特征工程的取舍为什么直接从像素学不总是最优解3.1 颜色空间的选择直接决定光照鲁棒性做水果视觉检测第一个绕不开的决策就是用什么颜色空间。RGB是相机原始输出但R、G、B三个通道相关性非常高而且对光照强度极其敏感——光照稍变三通道一起变模型就容易产生误判。HSV把亮度信息放到V通道色调H和饱和度S相对独立光照变化主要落在V通道上对H和S的影响会小很多。Lab空间的a、b通道更接近人类感知的色差对果蔬红色和绿色的细微变化特别敏感是很多农业视觉项目的首选。Matlab里只需要一行转换函数rgb2hsv和rgb2lab。我的实际体会是把RGB换成HSV或Lab往往比换模型带来的提升还大。因为它不动算法的复杂度只是把输入数据表示成更合适的形式。尤其是检测腐烂和病斑这类颜色异常Lab空间的a通道和b通道经常能做出清晰区分。3.2 纹理与形状特征弥补颜色特征的盲区颜色特征解决颜色不对的问题但果锈和正常果皮的颜色差异有时很微妙腐烂边缘的纹理变化也很难用颜色完全刻画。这时候要上纹理特征。灰度共生矩阵是我用得比较多的工具它统计的是图像中特定灰度值组合在相邻位置出现的频率。Matlab里graycomatrix直接计算然后graycoprops提取对比度、能量、同质性、相关性四个指标。腐烂和果锈区域的纹理通常比正常果皮粗糙同质性会明显降低这个信号是颜色特征给不了的。形状特征主要服务畸形果检测。区域属性里常用的几个量面积、周长、离心率、圆形度4π乘以面积除以周长的平方。正常苹果的圆形度通常在一个稳定区间畸形果会显著偏离。这里提醒一句形状特征对前景分割质量非常敏感背景分割的掩码边缘有一点毛刺周长就会明显变大圆形度立刻失真所以后面预处理步骤要认真做。3.3 特征工程和深度学习靠数据集规模说话现在一讲到视觉检测很多人第一反应是上卷积神经网络。但我的建议是别急着跳。如果你的样本量在几百到一两千这个量级手工特征加传统机器学习SVM、随机森林是更稳的路线因为特征维度可控、不过拟合、每一环都可以解释和调优。当样本量到上万或者场景背景极其复杂、缺陷形态高度多变再考虑迁移学习用预训练网络做特征提取器或者微调一个轻量CNN。Matlab同样支持深度学习迁移学习甚至有图形化界面。但从工程落地角度第一版我非常推荐先把传统特征加机器学习跑通建立baseline这个baseline不仅是准确率的锚点更是你理解数据的工具——你可以看看哪些样本被分错了反推是特征没提取到还是标注有问题。4. Matlab实现路线从图像预处理到模型训练的可复现代码4.1 图像预处理与前景分割整个流程的第一步是把果实区域从背景里分离出来。以下代码假设传送带是暗色背景这是很多分选线采用的无影暗箱配置。imgPath apple_sample_01.jpg; I imread(imgPath); I im2double(I); % 高斯平滑去噪声窗口不要太大否则边缘糊掉 I imgaussfilt(I, 1.5); % 转到HSV空间做背景分割 hsvI rgb2hsv(I); bgMask hsvI(:,:,2) 0.08 hsvI(:,:,3) 0.25; % 暗色低饱和背景 fgMask imdilate(~bgMask, strel(disk, 5)); fgMask imfill(fgMask, holes); fgMask bwareaopen(fgMask, 1000); % 去掉小块噪点这里的关键是背景分割的阈值不是一个通用值它由你的光源和传送带颜色决定。项目启动第一天就应该对着实际现场图调整参数而不是拿着示例代码的阈值直接用。前景分割是后面所有特征提取的基础这里出了问题后面全是错的。如果现场背景复杂阈值分割搞不定就得考虑用分割网络但成熟度会下降成本也上去通常暗箱环境能解决大半问题。4.2 特征提取函数化是工程化第一步我强烈建议把特征提取封装成一个函数让每张图走完全相同的处理管线最后输出一行维度固定的特征向量。这是后期快速构建特征数据集的关键。function fvec extractFeatures(imgPath) I im2double(imread(imgPath)); I imgaussfilt(I, 1.5); hsvI rgb2hsv(I); bgMask hsvI(:,:,2) 0.08 hsvI(:,:,3) 0.25; fgMask imdilate(~bgMask, strel(disk, 5)); fgMask imfill(fgMask, holes); fgMask bwareaopen(fgMask, 1000); fvec []; % 颜色特征HSV三通道在果实区域内的均值和标准差 roiHSV hsvI .* fgMask; for ch 1:3 vals roiHSV(:,:,ch); fvec [fvec, mean(vals(fgMask)), std(vals(fgMask))]; end % Lab空间代表两通道均值对果蔬颜色更敏感 labI rgb2lab(I); roiLab labI .* fgMask; for ch 2:3 % a和b通道 vals roiLab(:,:,ch); fvec [fvec, mean(vals(fgMask))]; end % 纹理特征灰度共生矩阵 g rgb2gray(I) .* fgMask; glcm graycomatrix(g, Offset, [2 0; 0 2], Symmetric, true); stats graycoprops(glcm, {Contrast, Energy, Homogeneity, Correlation}); fvec [fvec, stats.Contrast, stats.Energy, stats.Homogeneity, stats.Correlation]; % 形状特征 reg regionprops(fgMask, Area, Perimeter, Eccentricity); if numel(reg) 1 area reg.Area; peri reg.Perimeter; circularity 4 * pi * area / peri^2; fvec [fvec, area, peri, reg.Eccentricity, circularity]; else fvec [fvec, 0, 0, 0, 0]; end end所有特征必须基于同一个前景掩码来计算这是写特征提取函数时最容易犯的低级错误——一会儿用fgMask一会儿忘记乘掩码直接用了全图统计特征含义就全变了。别问我怎么知道的这个错我犯过。整个数据集的特征表构建就很简单了。把每张样本图的标签和特征向量写进一张表里导出为CSV后面训练用这张表就够了。fileList dir(dataset/*.jpg); numSamples numel(fileList); features zeros(numSamples, 16); labels strings(numSamples, 1); for i 1:numSamples features(i,:) extractFeatures(fullfile(fileList(i).folder, fileList(i).name)); [~, name, ~] fileparts(fileList(i).name); parts split(name, _); labels(i) parts{1}; % 文件名前缀作为标签例如 defect_001.jpg end tbl array2table(features); tbl.label categorical(labels); writetable(tbl, features.csv);4.3 模型训练SVM做扎实了再谈集成方法拿到特征表之后训练模型就很直接了。我一般先训练一个带径向基核的SVM特征标准化必须打开因为颜色、纹理和形状特征的量纲完全不同不标准化的话SVM的边界会被数值大的特征主导。data readtable(features.csv); X data{:, 1:end-1}; y data.label; rng(42); cv cvpartition(y, HoldOut, 0.2); XTrain X(training(cv), :); yTrain y(training(cv), :); XTest X(test(cv), :); yTest y(test(cv), :); svmModel fitcsvm(XTrain, yTrain, ... KernelFunction, rbf, ... Standardize, true, ... ClassNames, {defect, good}); % 基础评估 [pred, score] predict(svmModel, XTest); acc mean(pred yTest); fprintf(SVM accuracy: %.2f%%\n, acc * 100);如果类别数超过两类用fitcecoc做一对一的多分类SVM。随机森林方面Matlab里用fitcensemble指定Bag方法即可它对特征标准化的要求低而且能输出特征重要性方便你回头审视哪些特征在起作用。rfModel fitcensemble(XTrain, yTrain, ... Method, Bag, ... NumLearningCycles, 100); [rfPred] predict(rfModel, XTest);SVM和随机森林都在小样本下表现稳定我自己的对比经验是特征做得好时两者差距不大SVM的决策边界更光滑随机森林对异常特征值更鲁棒但它更容易在特征维度偏高时过拟合。先用默认参数跑一轮再考虑调参不要一上来就网格搜索。调参这块Matlab的fitcsvm提供了OptimizeHyperparameters选项可以自动搜C和gamma。但小样本下自动调参容易过拟合我通常的做法是手动在验证集上做10折交叉验证看均值方差然后小范围网格搜索。这里必须强调调参只能用训练集内部交叉验证来选不能拿测试集反复试否则测试集就失去了评估意义。4.4 超参优化要克制很多初学者一上来就喜欢自动调优。自动调优不是不能用但要注意两点一是候选模型数量大小样本上容易选到那个运气好的配置二是时间成本高。我的习惯是第一版用手动配置跑通记下baseline如果效果确实不理想再做小范围的定向搜索比如固定核函数为RBF只搜框约束C和核尺度两个参数。5. 评估体系设计准确率好看为什么上了产线还是翻车5.1 准确率陷阱和两类代价前面提到过缺陷分选场景里好果和缺陷果比例极不平衡这时候准确率是个骗子。假设测试集里980个是好果、20个是缺陷果你的模型不管三七二十一全部判为好果准确率98%看起来挺漂亮但20个缺陷果一个都没有被挑出来这个模型上线等于白装。分选场景真正关心的是两个方向的错误漏检即缺陷果流向下游直接投诉品牌、拉低市场口碑误检即好果被当成缺陷淘汰换来的是农产品本身的损耗积少成多也是一笔不小的经济损失。这两类错误的权重通常不一样。漏检的代价往往更高所以实际操作中宁可稍微提高误检率也要把漏检压下来。5.2 用混淆矩阵和概率阈值做决策SVM默认以输出概率0.5作为分类阈值但这个阈值并不是为你的业务场景优化的。Matlab里可以先用fitSVMPosterior得到后验概率然后在验证集上扫描概率阈值挑一个让漏检率满足要求的阈值。svmModel fitSVMPosterior(svmModel); [~, score] predict(svmModel, XVal); thresholds 0.20:0.05:0.75; for t thresholds pred score(:,2) t; fnr sum(pred 0 yVal defect) / sum(yVal defect); fpr sum(pred 1 yVal good) / sum(yVal good); fprintf(thr%.2f 漏检率%.2f%% 误检率%.2f%%\n, t, fnr*100, fpr*100); end输出这张表之后让业务方来拍板他们能接受多高的漏检率对应付出多大的误检代价。这个决策过程本身就是你把算法模型变为产线工具的关键一步。5.3 指标清单别只盯一个数分选问题里值得看的指标我列一下。指标定义在分选场景里的意义召回率缺陷果被检出的比例直接对应漏检率召回越高漏检越少精确率被检出的缺陷里真缺陷的比例对应误检率精确越高好果浪费越少F1分数召回和精确的调和平均兼顾两类错误用于模型选型比较特异性好果被正确放行的比例衡量好果损失和召回率互补PR曲线不同阈值下精确率和召回率的走势帮你选择合适的业务阈值同时再强调一次数据泄漏测试集必须按果实分组隔离。同一颗果子的多视角照片不能一部分进训练、一部分进测试。否则模型可能只是认识这个果而不是学会了区分缺陷。这是实验室准确率高、产线一跑就崩的头号原因之一。5.4 用独立批次做干跑模型训练完之后我会留一批完全没参与训练验证的后续批次数据做成一个模拟产线的干跑序列。所谓干跑就是让算法对这批图像按时间顺序一张张处理统计漏检和误检。这一步能发现很多离线随机划分发现不了的问题比如某个时段的光照异常导致整段图像颜色偏移比如某个品种的果皮底色和之前数据差异大。干跑结果符合预期我才会考虑部署。6. 从实验室到分选产线实时性与硬件约束6.1 每条果子的计算预算是多少产线的节拍决定了你留给算法的绝对时间。比如一条产线每分钟过180个果就是每秒3个果每个果的处理时间预算大约是330毫秒。这个时间要分给图像采集、传输、算法处理和机械机构执行。机构执行如果占掉150毫秒留给算法的就只有180毫秒左右。这个账必须提前算不能等算法写完了再考虑。实测里SVM或随机森林的单次推理通常在1毫秒以内根本不是瓶颈。真正吃时间的是特征提取里的灰度共生矩阵和区域属性计算。我跑过一段真实数据特征提取一次大约80到150毫秒和硬件有关。优化思路有几个方向降低图像分辨率到刚好够用的水平把GLCM的灰度级从256降到32或64减少offset方向数量固定ROI避免整幅图都跑regionprops。一旦特征提取压进50毫秒以内整体算法耗时就完全可控了。6.2 部署方式两种务实路线Matlab里训练好的模型要上产线通常有两条路。一是用MATLAB Coder把特征提取和预测函数生成C/C代码集成到工控机程序里。这条路的问题在于并非所有图像处理函数都支持代码生成比如graycomatrix我记得坑就不少需要先查支持清单或者手工把GLCM计算用C重写。二是用MATLAB Compiler SDK打成动态库在C#或Python主程序里调用开发速度快但运行时依赖重适合产量不大的原型线。更轻量的做法是对于线性SVM或者决策树这类模型导出的权重就是一组数值直接在C里用几十行代码手写推理逻辑完全甩掉Matlab运行时。特征提取部分则用OpenCV重写。我的实际建议是先跑通Matlab联机的产线原型验证算法和生产节拍匹配再决定要不要做工程化移植。一上来就追求脱离Matlab往往会在特征提取的细节上浪费大量时间。6.3 光照一致性和批次漂移最后说一个老生常谈但总被忽视的坑光照一致性。实验室用的光源、角度、色温和产线上最终安装的光源不可能完全一致。哪怕都是白光LED色温和显色指数不同拍出来的果皮底色都不一样。所以我现在的习惯是数据采集尽量直接在产线的原型光源环境里做让模型从一开始就适应真实光照。即便做到了也不能一劳永逸。水果是农产品成熟度、产地、季节都会改变它的颜色分布。同一套模型在夏季数据的表现可能不错到了秋季换了一批底色更红的果误检率可能升高。所以建议定期做特征分布的漂移检测每更换一批次原材料或每隔一个月随机抽100个果跑一遍现有模型统计预测置信度的分布和训练时的分布做对比。偏差大了就要考虑用新批次数据微调分类阈值必要时做短周期重训练。我的经验是一个认真维护的缺陷分选模型必须当成一条需要持续保养的产线设备来对待而不是一次训练完就永久管用。定期回归测试、定期校准阈值这比一开始追求99%的准确率重要得多。从我这几轮做下来的体感来说这类项目的成败往往不是哪个算法多先进而是数据是否贴近现场、评估是否诚实、阈值是否按业务调过。尤其是后两点几乎决定了一个模型是被人当工具用还是被当成摆设。最后分享一个习惯在每个缺陷类别里留20张左右难分的样本比如浅色碰压伤、被果蒂阴影挡住的腐烂斑。每次模型更新后先人工过一遍这些困难样本预测结果的变化。这个操作比盯着单一准确率参数涨跌有用得多。模型往好的方向调整时困难样本的表现会慢慢变好如果正常样本准确率上去了但困难样本反而变差通常意味着过拟合或者特征选择出了问题。
阅读完成 · 觉得有帮助?
咨询建站