ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MATLAB害虫检测识别系统:从图像预处理到分类器选型的完整实现

MATLAB害虫检测识别系统:从图像预处理到分类器选型的完整实现

简介:这份MATLAB害虫检测识别系统面向植物保护、农业信息化方向的本科生与研究生,尤其适合作为毕业设计或课程设计参考,解决叶片虫害等级自动判别的问题。系统基于颜色特征实现四分类,可区分正常、轻度、中度与严重虫害,核心流程包括按等级分文件夹训练颜色分量、生成color.mat训练值,再对测试叶片进行亮度调节、色彩空间转换与颜色分量计算,最终匹配输出识别结果。压缩包共94个文件,以84张jpg样本图、7个m脚本、1个mat训练数据、1份doc说明及1张png运行图为主,整体约7.81MB,样本按灾害等级分目录组织,便于直接复现实验。目前已有1930人学习下载,读者可获得完整算法脚本、分等级图像数据集、训练与匹配代码及运行效果参考,适合在此基础上做特征改进或扩展识别类别。

1. 从一张叶片照片到虫情报告:MATLAB害虫检测识别系统到底在做什么

田里发现叶片被啃出孔洞,但不知道是哪种虫、要不要打药、打什么药——这是植保和农技推广里最日常的困境。MATLAB害虫检测识别系统要解决的就是这件事:用手机或普通相机拍一张叶片或诱捕盘的照片,在MATLAB里跑完预处理、分割、特征提取、分类四步,输出害虫种类和大致数量。它适合三类人:做农业智能装备的嵌入式工程师、需要快速验证识别算法的研究生、以及想把虫情监测从人工数虫升级成半自动的植保技术员。整套流程不依赖深度学习框架,用MATLAB自带的图像处理工具箱和分类器就能跑通,对没有GPU的现场设备特别友好。下面按“先跑通再调优”的顺序,把每个环节的参数和坑讲清楚。

2. 图像预处理与目标分割:把虫子从背景里抠出来

2.1 为什么先做颜色空间转换而不是直接灰度化

很多新手拿到叶片照片第一反应是rgb2gray,然后阈值分割。这在绿色叶片上会翻车:害虫和叶脉的灰度值可能只差十几个灰阶,阈值根本切不开。更稳的做法是转到HSV或Lab空间,利用害虫与叶片在色度通道上的差异。常见做法是取HSV的S通道或Lab的a通道做分割,因为这两个通道对绿色背景的抑制最明显。

% 读取原始图像并转换到Lab空间 img = imread('leaf_sample.jpg'); lab = rgb2lab(img); % a通道对绿色背景抑制强,害虫区域通常偏红/偏暗 a_channel = lab(:,:,2); % 用Otsu自动阈值分割,避免手动调阈值 level = graythresh(a_channel); bw = imbinarize(a_channel, level); % 形态学闭运算填补虫体内部空洞 se = strel('disk', 3); bw_closed = imclose(bw, se); % 去除面积过小的噪点(小于50像素视为杂质) bw_clean = bwareaopen(bw_closed, 50);

逻辑说明:rgb2lab把图像转到感知均匀的Lab空间,a通道负值偏绿、正值偏红,害虫体色通常比叶片更偏红或更暗,所以a通道能拉开对比。graythresh用Otsu方法自动找阈值,省去手动试参。imclose用半径3的圆盘结构元素闭运算,把虫体上因反光造成的断裂连起来。bwareaopen去掉面积小于50像素的连通域,这些多半是灰尘或叶面斑点。

参数说明:strel('disk',3)的半径3是经验值,虫子体长在图像里占30到80像素时合适;如果拍的是微小蚜虫,半径要降到1到2,否则会把相邻两只虫粘成一块。bwareaopen的50像素阈值对应约0.5毫米见方的杂质,高分辨率图像可以提到100到200。

2.2 粘连虫体的分水岭分割

当害虫聚集在诱捕盘上时,二值化后经常出现一大片粘连区域,直接做连通域计数会把十只虫算成一只。这时候要用分水岭算法基于距离变换做拆分。

% 对清洗后的二值图做距离变换 D = -bwdist(~bw_clean); % 强制最小值,避免过度分割 D(~bw_clean) = -Inf; % 分水岭分割 L = watershed(D); % 把背景标记为0,只保留虫体区域 L(~bw_clean) = 0; % 统计连通域数量 stats = regionprops(L, 'Area', 'Centroid'); num_pests = length(stats);

逻辑说明:bwdist计算每个前景像素到最近背景的距离,虫体中心距离最大。取负值后分水岭算法会把每个“山峰”当成独立区域。D(~bw_clean) = -Inf强制背景为最小值,防止分割线跑到背景里。watershed返回标签矩阵,每个标签对应一只虫。regionprops统计面积和质心,面积可用于后续筛选。

参数说明:分水岭容易过度分割,如果发现一只虫被切成两半,可以在距离变换后做一次imhmax抑制浅谷,或者对regionprops返回的面积做二次过滤,把面积小于平均面积40%的区域合并回相邻区域。这个阈值我一般设0.4,具体看虫种体型一致性。

3. 特征提取与分类器选型:用什么特征区分蛾类和甲虫

3.1 形态学特征与纹理特征的组合策略

分割出虫体后,下一步是提取能区分种类的特征。单靠面积和周长不够,因为不同种类的蛾类体型可能重叠。我一般用三组特征:形态学特征(面积、周长、偏心率、凸包比)、颜色特征(Lab均值、HSV直方图)、纹理特征(灰度共生矩阵的对比度和能量)。这三组特征拼成一个特征向量,送进分类器。

% 提取形态学特征 stats = regionprops(L, 'Area', 'Perimeter', 'Eccentricity', 'Solidity'); % 提取颜色特征:Lab三通道均值 mean_a = mean(a_channel(bw_clean)); mean_b = mean(lab(:,:,3)(bw_clean)); % 提取纹理特征:灰度共生矩阵 gray = rgb2gray(img); glcm = graycomatrix(gray, 'NumLevels', 8, 'Offset', [0 1]); props = graycoprops(glcm, {'Contrast', 'Energy'}); % 拼接特征向量 feature_vec = [stats.Area, stats.Perimeter, stats.Eccentricity, ... stats.Solidity, mean_a, mean_b, props.Contrast, props.Energy];

逻辑说明:regionprops的Eccentricity反映虫体细长程度,蛾类通常比甲虫更细长;Solidity是面积与凸包面积之比,甲虫更接近1。Lab均值捕捉体色差异。graycomatrix的Offset设为[0 1]表示水平方向相邻像素对,NumLevels降到8是为了减少稀疏矩阵维度。graycoprops提取对比度和能量,对比度高说明纹理粗糙,能量高说明纹理均匀。

参数说明:NumLevels设8到16之间,太高会导致GLCM矩阵稀疏、计算变慢。Offset可以设多个方向(如[0 1]、[1 0]、[1 1])然后取平均,但特征维度会翻倍。如果分类器是SVM,特征维度超过20时建议先做PCA降维。

3.2 用fitcecoc做多类分类的配置要点

MATLAB里做多类分类最省事的是fitcecoc,它用纠错输出码把二分类SVM扩展到多类。对于害虫识别这种类别数在5到15之间的任务,比直接改神经网络省事得多。

% 假设featureTable是表格,最后一列是类别标签 % 划分训练集和测试集 cv = cvpartition(featureTable.Label, 'HoldOut', 0.3); trainData = featureTable(training(cv), :); testData = featureTable(test(cv), :); % 训练ECOC多类SVM template = templateSVM('KernelFunction', 'rbf', 'BoxConstraint', 1); mdl = fitcecoc(trainData(:, 1:end-1), trainData.Label, ... 'Learners', template, 'Coding', 'onevsone'); % 预测并计算准确率 pred = predict(mdl, testData(:, 1:end-1)); accuracy = sum(pred == testData.Label) / height(testData);

逻辑说明:cvpartition按类别分层抽样,保证训练集和测试集类别比例一致。templateSVM指定RBF核,BoxConstraint控制惩罚力度。fitcecoc的Coding设为onevsone,对每两个类别训练一个SVM,适合类别数不多的情况。predict返回预测标签,直接和真实标签比对算准确率。

参数说明:BoxConstraint默认是1,如果发现训练准确率高但测试准确率低(过拟合),降到0.1到0.5;如果两者都低(欠拟合),升到10。RBF核的KernelScale建议用'auto'让MATLAB自己选,手动设的话一般取特征矩阵平均范数的倒数。HoldOut的0.3是经验值,样本少于200时建议用交叉验证而不是单次划分。

4. 避坑与排查:虫情识别里最容易翻车的五个地方

4.1 现象:分割结果把叶脉当成虫子

原因:叶脉在a通道上和害虫体色接近,Otsu阈值切不开。解决:在分割前加一步形态学开运算,用线形结构元素去掉细长叶脉;或者把面积下限从50提到150,叶脉连通域通常比虫子大但更细长,用Eccentricity大于0.95过滤掉。

4.2 现象:分类器在训练集上准确率99%,测试集只有60%

原因:特征维度太高而样本太少,SVM过拟合。解决:先用sequentialfs做特征选择,或者用PCA降到5到8维;同时把BoxConstraint降到0.1。我一般还会检查是不是把类别标签泄漏进了特征矩阵,比如误把文件名里的类别编号当特征。

4.3 现象:分水岭分割后虫子数量比实际多一倍

原因:距离变换的局部极大值太多,一只虫被切成多个区域。解决:在bwdist后加imhmax(D, 2)抑制深度小于2的谷,或者对regionprops返回的面积做合并,把面积小于平均面积40%的区域合并到最近的相邻区域。

4.4 现象:换一批照片后识别率骤降

原因:训练集和测试集的光照条件、拍摄距离不一致,颜色特征漂移。解决:在预处理阶段加白平衡校正,用illumwhite或简单的灰度世界算法;或者把颜色特征从绝对Lab值改成相对值,比如虫体a通道均值减去叶片背景a通道均值。

4.5 现象:MATLAB报错“Out of memory”处理大图时

原因:高分辨率图像做GLCM或分水岭时内存占用大。解决:先用imresize把长边降到1024像素再处理;GLCM的NumLevels从8降到4;分水岭前用bwareaopen去掉小区域减少标签数。如果还不行,把图像切成512乘512的块分块处理再合并结果。

5. 从单张识别到批量虫情统计:一个可复用的脚本框架

把前面几步串起来,写成一个函数,输入是图像文件夹路径,输出是每张图的虫种计数和总计数。这个框架我用了两年多,换虫种时只需要改特征提取里的参数和重新训练分类器。

function pest_report = batch_pest_detect(imgFolder, model) % imgFolder: 图像文件夹路径 % model: 训练好的分类器 % 返回表格:文件名、各虫种数量、总数 files = dir(fullfile(imgFolder, '*.jpg')); pest_report = table(); for i = 1:length(files) img = imread(fullfile(imgFolder, files(i).name)); % 预处理与分割 lab = rgb2lab(img); a_channel = lab(:,:,2); level = graythresh(a_channel); bw = imbinarize(a_channel, level); bw = imclose(bw, strel('disk', 3)); bw = bwareaopen(bw, 50); % 分水岭拆分粘连 D = -bwdist(~bw); D(~bw) = -Inf; L = watershed(D); L(~bw) = 0; % 逐区域提取特征并分类 stats = regionprops(L, 'Area', 'Perimeter', 'Eccentricity', 'Solidity'); counts = containers.Map(); for j = 1:length(stats) mask = (L == j); mean_a = mean(a_channel(mask)); mean_b = mean(lab(:,:,3)(mask)); gray = rgb2gray(img); glcm = graycomatrix(gray, 'NumLevels', 8, 'Offset', [0 1]); props = graycoprops(glcm, {'Contrast', 'Energy'}); fv = [stats(j).Area, stats(j).Perimeter, stats(j).Eccentricity, ... stats(j).Solidity, mean_a, mean_b, props.Contrast, props.Energy]; label = predict(model, fv); if isKey(counts, label) counts(label) = counts(label) + 1; else counts(label) = 1; end end % 汇总到表格 row = table({files(i).name}, {counts}, sum(cell2mat(values(counts))), ... 'VariableNames', {'FileName', 'SpeciesCounts', 'Total'}); pest_report = [pest_report; row]; end end

逻辑说明:外层循环遍历文件夹里所有jpg图像。每张图走一遍预处理、分割、分水岭、特征提取、分类的流程。containers.Map用来累计每个虫种的数量,键是分类器输出的标签。最后把文件名、虫种计数字典和总数拼成一行表格。这个函数可以直接在MATLAB命令行调用,比如report = batch_pest_detect('field_photos', trainedModel)。

参数说明:graycomatrix在循环里对整张图算GLCM再取区域均值,效率不高但代码简单;如果图像数量大,可以改成先算整图GLCM再按标签区域取均值。predict对单个样本调用会有开销,批量预测时可以把所有区域的特征向量拼成矩阵一次性预测,速度能快5到10倍。containers.Map的键必须是字符或字符串,如果分类器返回的是categorical类型,要先转成char。

进阶用法:如果现场设备没有MATLAB许可证,可以用MATLAB Coder把训练好的分类器和预处理流程生成C代码,部署到树莓派或Jetson Nano上。生成代码时注意graycomatrix和watershed都支持代码生成,但containers.Map不支持,需要改成固定长度的数组来计数。另一个方向是用迁移学习替换手工特征,把预训练的SqueezeNet或MobileNetV2拿来微调,MATLAB的Deep Learning Toolbox支持直接导入ONNX模型,识别率通常能比手工特征高10到15个百分点,代价是需要更多标注样本和训练时间。

我自己的习惯是每换一个虫种或换一个拍摄场景,先拿20张图跑一遍分割,把中间结果bw_clean和L用imshowpair叠在原图上肉眼检查,确认分割没问题再往下走特征和分类。这一步花十分钟,能省掉后面调分类器参数的两小时。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进