这个项目最吸引初学者的点在于:它把“图像处理”和“机器学习分类”串成了一条完整链路,而不是只讲某个孤立函数。很多刚接触 Matlab 数字图像处理的同学,手里已经会了imread、rgb2gray、imshow这些基础操作,但真正要做一个“能自动识别水果种类”的小系统时,往往不知道特征怎么提、分类器怎么选、数据集怎么组织。本文就是围绕这套水果分类识别系统,把从图像预处理、特征提取、分类器训练到 GUI 界面运行的完整流程拆开讲清楚。无论你是数字图像处理课程的初学者,还是正在做 Matlab 课程设计、毕业设计,都可以照着本文一步步复现,并在此基础上扩展成自己的项目。
文章会先交代项目需求和技术选型,再说明环境准备和核心原理,然后给出完整可运行的源码与运行结果,最后补充高频问题和工程化建议。如果你在运行过程中遇到报错,本文最后一节也整理了常见排查思路,可以作为排错手册使用。
1. 项目背景与需求分析
1.1 为什么要做水果分类识别系统
水果分类是计算机视觉中一个比较典型的“小目标识别”案例。它不像人脸识别、自动驾驶那样需要大规模深度模型和海量显卡资源,但仍然覆盖了数字图像处理的主要环节:图像读取、数据预处理、特征提取、特征选择、模型训练和结果评估。换句话说,把水果分类系统做明白,图像分类的基本功就打通了。
从实际场景看,水果分类识别的应用价值也很直接:
- 超市自助结算:通过摄像头识别水果种类,自动计价。
- 农产品分拣:产线传送带上按大小、品种自动分拣。
- 智能冰箱:识别冰箱内存放的水果蔬菜种类,辅助食材管理。
- 教学实验:作为数字图像处理、模式识别课程的综合性大作业。
在 Matlab 环境下实现这套系统,最大的优势是开发效率高。图像处理工具箱(Image Processing Toolbox)提供了现成的滤波、二值化、形态学操作和区域属性提取函数,统计与机器学习工具箱(Statistics and Machine Learning Toolbox)又提供了fitcknn、fitcecoc、fitctree等开箱即用的分类器。我们不需要从底层实现图像算法,而是把重点放在“如何利用这些工具箱组合出一套可用的识别流程”。
1.2 需求拆解
一个最小可用的水果分类识别系统,要满足以下功能:
- 读取一张水果图片(jpg、png、bmp 等常见格式)。
- 对图像进行预处理,包括尺寸统一、去噪、背景处理。
- 从图像中提取可以区分不同水果的特征。
- 用训练数据训练一个分类模型。
- 对新的测试图片输出预测类别。
- 最好能提供一个简单的界面,让用户选择图片并查看结果。
为了把问题控制在一个合理范围,本文以苹果、香蕉、橙子三类水果为例。这三类水果在颜色上差异明显,在形状和纹理上也有可提取的判别信息,非常适合演示完整的图像分类流程。如果你后续要识别更多水果类别,只需要扩展数据集、重新提取特征,并评估是否要增加特征维度或更换分类器即可。
2. 运行环境与工具箱准备
Matlab 版本对本文代码的影响比较大,尤其是二值化函数和部分分类器接口。下面以常见配置为例,给出推荐环境。
2.1 环境版本建议
| 项目 | 建议 |
|---|---|
| 操作系统 | Windows 10 / 11、Ubuntu 20.04 及以上均可 |
| Matlab 版本 | R2016b 及以上,推荐 R2020a 以后版本 |
| 必要工具箱 | Image Processing Toolbox、Statistics and Machine Learning Toolbox |
| 可选工具箱 | Computer Vision Toolbox(本文不强制使用) |
需要特别说明的是,Matlab 版本更新较快,不同版本的 GUI 工具在布局上会有差异,但核心图像处理函数相对稳定。如果你使用的版本较老,比如 R2014a,那么部分新函数(如imbinarize)可能不可用,需要替换为im2bw。后面遇到具体报错时,我也会在常见问题里给出替代方案。
版本要求应结合你的实际环境调整,本文重点演示配置和编码思路,函数接口以主流写法为准。
2.2 推荐项目目录结构
为了避免脚本、数据和模型文件混在一起,建议建立如下目录结构:
fruit_classify/ ├── main_train.m % 训练脚本 ├── main_test.m % 单张图片测试脚本 ├── fruit_app.m % GUI 入口 ├── extractFeatures.m % 特征提取函数 ├── predictFruit.m % 分类预测函数 ├── data/ │ ├── train/ │ │ ├── apple/ │ │ ├── banana/ │ │ └── orange/ │ └── test/ │ ├── apple/ │ ├── banana/ │ └── orange/ └── model/ └── fruitModel.mat其中train目录下的子文件夹名就是类别标签,这也是 MatlabimageDatastore推荐的文件夹组织方式。如果你是自己拍摄水果照片,只需按照这个结构把图片分好类即可。
3. 数字图像处理核心原理
这一节是整个系统最关键的部分。很多同学直接看代码没问题,但一旦要换数据集或换识别对象,就不知道从哪里改。原因就是没有理解每一步在做什么。
3.1 图像预处理:为什么不能直接用原图训练
摄像头拍摄的水果照片往往大小不一致。有的图片是 3000×4000 像素,有的是 400×300 像素,如果直接提取特征,特征维度很难对齐,计算量也会相差很大。因此第一步通常是统一尺寸,把图像缩放到固定大小,比如 256×256。
其次,拍摄环境可能有噪声,尤其是光照不足或手机拍摄时,图像会带有随机噪声。中值滤波是一种常用的去噪方法,它能在去除椒盐噪声的同时较好地保留边缘信息。对比均值滤波,中值滤波不会把边缘“糊”得那么厉害。对于水果图像而言,边缘正是形状特征提取的重要依据,所以保留边缘很重要。
预处理代码可以简单写为:
function img = preprocessImage(img) % 统一尺寸 img = imresize(img, [256, 256]); % 如果是灰度图,转成RGB三通道,方便后续统一处理 if size(img, 3) == 1 img = repmat(img, [1 1 3]); end % RGB图像中值滤波去噪 img = medfilt3(img); end有人会问:为什么不是先转灰度图再去噪?因为后续颜色特征要在 HSV 空间提取,提前把 RGB 三通道保留下来,能在同一个函数里完成去噪和颜色空间转换。如果先转灰度,颜色信息就丢失了,后面提取颜色特征时还得重新读原图。
3.2 颜色特征:HSV 空间与颜色矩
颜色是水果分类最直观的特征。苹果偏红、香蕉偏黄、橙子偏橙,这些颜色差异在 RGB 空间里也可以看到,但 RGB 三个通道的相关性较高,也容易受光照强度影响。相比之下,HSV 色彩空间把色调(Hue)、饱和度(Saturation)、明度(Value)分开,H 通道对光照变化更鲁棒,更符合人类对颜色的主观感受。
提取颜色特征时,我们不需要把整幅图的直方图都堆进特征向量,那样维度太高且计算量大。更常用的做法是计算“颜色矩”,也就是每个通道的均值和标准差。
function colorFeature = extractColorFeature(img) hsv = rgb2hsv(img); colorFeature = zeros(1, 6); for k = 1:3 channel = hsv(:, :, k); colorFeature(2*k - 1) = mean(channel(:)); colorFeature(2*k) = std(channel(:)); end end可以看到,3 个通道各取均值和标准差,最终得到 6 维特征。均值描述图像整体的颜色倾向,标准差描述颜色分布的离散程度。比如红苹果的 H 通道均值会明显区别于香蕉的 H 通道均值,这就是区分依据。
3.3 形状特征:二值化与 regionprops
除了颜色,水果的形状也有区分价值。苹果接近圆形,香蕉是长条形,橙子也是偏圆形但可能在大小和轮廓圆整度上与苹果不同。
提取形状特征前,需要把水果从背景中分离出来。最简单的方法是使用 Otsu 全局阈值法,也就是graythresh计算阈值后再二值化。对于背景和前景颜色差异较大的图片,这种方法效果不错;如果背景复杂,则需要考虑更高级的分割方法,比如 K-means 分割。
二值化后,可以使用bwareaopen去掉小面积噪声区域,再用imfill补充内部空洞。随后调用regionprops获取区域的面积、周长、长轴长度、短轴长度等属性。
function shapeFeature = extractShapeFeature(gray) bw = imbinarize(gray, graythresh(gray)); bw = imfill(bw, 'holes'); bw = bwareaopen(bw, 1000); stats = regionprops(bw, {'Area', 'Perimeter', 'MajorAxisLength', 'MinorAxisLength'}); if isempty(stats) shapeFeature = zeros(1, 4); return; end area = stats(1).Area; perimeter = stats(1).Perimeter; major = stats(1).MajorAxisLength; minor = stats(1).MinorAxisLength; circularity = 4 * pi * area / (perimeter^2 + eps); eccentricity = sqrt(1 - (minor / (major + eps))^2); % 归一化面积和周长,避免图像尺寸大小影响 shapeFeature = [area / (256 * 256), circularity, perimeter / (256 * 4), eccentricity]; end其中:
circularity是圆度,越接近 1 表示越接近圆形。eccentricity是离心率,越接近 0 表示越接近正圆,越接近 1 表示越扁。- 面积和周长的归一化,是为了抵消统一缩放后仍可能存在的分辨率差异。
对于香蕉这样的长条形水果,离心率会远高于苹果和橙子,所以这 4 维特征对分类很有帮助。
3.4 纹理特征:灰度共生矩阵
纹理特征主要描述图像表面的粗糙度、对比度和规律性。水果表皮存在差异:苹果表面相对光滑,橙子表皮有颗粒感,香蕉表皮有纵向纹路。虽然单纯靠颜色和形状已经能区分这三类,但加入纹理特征能提高系统对复杂样本的鲁棒性。
灰度共生矩阵(GLCM)是计算纹理特征的经典方法。它统计图像中两个像素在指定方向和距离上灰度值共同出现的概率。Matlab 中可以用graycomatrix计算,用graycoprops提取对比度、相关性、能量和同质性。
function textureFeature = extractTextureFeature(gray) glcm = graycomatrix(gray, 'Offset', [0 1; -1 1; -1 0; -1 -1], 'Symmetric', true); statsGLCM = graycoprops(glcm, {'Contrast', 'Correlation', 'Energy', 'Homogeneity'}); contrast = mean(statsGLCM.Contrast); correlation = mean(statsGLCM.Correlation); energy = mean(statsGLCM.Energy); homogeneity = mean(statsGLCM.Homogeneity); textureFeature = [contrast, correlation, energy, homogeneity]; end这里使用了 4 个方向偏移量:0 度、45 度、90 度、135 度,然后取平均。这样可以使纹理特征对方向不敏感。能量越大表示纹理越均匀,对比度越大表示纹理越明显。
3.5 特征向量拼接与分类器选择
把颜色、形状、纹理特征拼接在一起,就得到一张图片的完整特征向量。本文的数据集规模不大,特征维度也只有 14 维(6 维颜色 + 4 维形状 + 4 维纹理),因此分类器不需要太复杂。
三种常见选择:
| 分类器 | 特点 | 适用场景 |
|---|---|---|
| KNN(K 近邻) | 原理简单,无需训练过程,预测时计算距离 | 小数据集、特征维度不高 |
| SVM(支持向量机) | 分类边界清晰,泛化能力较强 | 中等规模样本,两类或多类问题 |
| BP 神经网络 | 非线性拟合能力强,但训练时间较长 | 样本量较大、特征关系复杂 |
本文以 KNN 为主,因为它在小样本分类任务中表现稳定,而且参数少、结果容易解释。fitcknn是 Matlab 统计工具箱提供的 KNN 分类器训练函数,可以用NumNeighbors设置邻居个数,用Standardize开启特征标准化。
在工程中,KNN 的缺点是预测阶段需要计算测试样本与所有训练样本的距离,样本数量很大时速度会变慢。如果后续要扩展为大规模分拣系统,建议换成 SVM 或集成学习模型,但整体特征提取流程可以保持不变。
4. 完整实战案例:水果分类识别系统
下面进入代码实现。为了让读者能完整跑通,我会给出从特征提取到模型训练再到预测的全部代码,并模拟生成一份可用的训练数据。如果你手头已有苹果、香蕉、橙子三类图片,可以直接用真实数据替换模拟数据部分。
4.1 创建项目结构并准备数据
首先建立章节 2.2 中的目录结构。在data/train下分别创建apple、banana、orange三个文件夹,在data/test下同样创建三个文件夹。
如果有真实图片,请把每张图放入对应文件夹,并尽量保证:
- 图片背景不要太复杂。
- 每个类别至少 20~30 张训练图。
- 训练集和测试集不要有重复图片。
如果没有现成图片,可以用下面的脚本生成一组“模拟特征样本”,它可以帮助你先跑通整个训练流程,理解每段代码的作用。注意:模拟数据只能用于验证代码流程,不代表真实识别效果。
% 文件路径:generateSyntheticData.m function [features, labels] = generateSyntheticData(nSamples) % 生成三组模拟特征,用于演示分类流程 % 每组特征对应:颜色特征均值/方差、形状特征、纹理特征 rng(42); % 苹果:色调偏红 appleColor = [0.45 + 0.02*randn(nSamples, 2), 0.15 + 0.02*randn(nSamples, 2), 0.30 + 0.05*randn(nSamples, 2)]; appleShape = [0.45 + 0.03*randn(nSamples, 1), 0.85 + 0.03*randn(nSamples, 1), 0.60 + 0.02*randn(nSamples, 1), 0.30 + 0.05*randn(nSamples, 1)]; appleTexture = [0.20 + 0.02*randn(nSamples, 4)]; % 香蕉:色调偏黄,长条形 bananaColor = [0.22 + 0.02*randn(nSamples, 2), 0.38 + 0.02*randn(nSamples, 2), 0.25 + 0.05*randn(nSamples, 2)]; bananaShape = [0.35 + 0.03*randn(nSamples, 1), 0.50 + 0.05*randn(nSamples, 1), 0.55 + 0.02*randn(nSamples, 1), 0.75 + 0.05*randn(nSamples, 1)]; bananaTexture = [0.25 + 0.02*randn(nSamples, 4)]; % 橙子:色调偏橙,圆形,表皮有颗粒感 orangeColor = [0.35 + 0.02*randn(nSamples, 2), 0.30 + 0.02*randn(nSamples, 2), 0.22 + 0.05*randn(nSamples, 2)]; orangeShape = [0.45 + 0.03*randn(nSamples, 1), 0.88 + 0.03*randn(nSamples, 1), 0.62 + 0.02*randn(nSamples, 1), 0.25 + 0.05*randn(nSamples, 1)]; orangeTexture = [0.30 + 0.02*randn(nSamples, 4)]; apple = [appleColor, appleShape, appleTexture]; banana = [bananaColor, bananaShape, bananaTexture]; orange = [orangeColor, orangeShape, orangeTexture]; features = [apple; banana; orange]; labels = [repmat({'apple'}, nSamples, 1); repmat({'banana'}, nSamples, 1); repmat({'orange'}, nSamples, 1)]; end这段代码生成了三类水果的模拟特征矩阵。每一行是一个样本,前 6 列模拟颜色信息,中间 4 列模拟形状信息,最后 4 列模拟纹理信息。它的作用是让读者在没有真实图片的情况下,先把训练、评估、预测的代码流程跑通。
4.2 编写特征提取函数
特征提取函数extractFeatures.m整合了预处理、颜色特征、形状特征和纹理特征。这个函数既可以用于训练集批量提取,也可以用于预测新图片。
% 文件路径:extractFeatures.m function feature = extractFeatures(imgPath) % 读取图片 img = imread(imgPath); % 预处理:统一尺寸 + 中值滤波去噪 img = preprocessImage(img); % 颜色特征 colorFeature = extractColorFeature(img); % 灰度图,用于形状和纹理特征 gray = rgb2gray(img); % 形状特征 shapeFeature = extractShapeFeature(gray); % 纹理特征 textureFeature = extractTextureFeature(gray); % 拼接为完整特征向量 feature = [colorFeature, shapeFeature, textureFeature]; end function img = preprocessImage(img) img = imresize(img, [256, 256]); if size(img, 3) == 1 img = repmat(img, [1 1 3]); end img = medfilt3(img); end function colorFeature = extractColorFeature(img) hsv = rgb2hsv(img); colorFeature = zeros(1, 6); for k = 1:3 channel = hsv(:, :, k); colorFeature(2*k - 1) = mean(channel(:)); colorFeature(2*k) = std(channel(:)); end end function shapeFeature = extractShapeFeature(gray) bw = imbinarize(gray, graythresh(gray)); bw = imfill(bw, 'holes'); bw = bwareaopen(bw, 1000); stats = regionprops(bw, {'Area', 'Perimeter', 'MajorAxisLength', 'MinorAxisLength'}); if isempty(stats) shapeFeature = zeros(1, 4); return; end area = stats(1).Area; perimeter = stats(1).Perimeter; major = stats(1).MajorAxisLength; minor = stats(1).MinorAxisLength; circularity = 4 * pi * area / (perimeter^2 + eps); eccentricity = sqrt(1 - (minor / (major + eps))^2); shapeFeature = [area / (256 * 256), circularity, perimeter / (256 * 4), eccentricity]; end function textureFeature = extractTextureFeature(gray) glcm = graycomatrix(gray, 'Offset', [0 1; -1 1; -1 0; -1 -1], 'Symmetric', true); statsGLCM = graycoprops(glcm, {'Contrast', 'Correlation', 'Energy', 'Homogeneity'}); contrast = mean(statsGLCM.Contrast); correlation = mean(statsGLCM.Correlation); energy = mean(statsGLCM.Energy); homogeneity = mean(statsGLCM.Homogeneity); textureFeature = [contrast, correlation, energy, homogeneity]; end这个文件里定义了多个子函数,但对外只暴露extractFeatures(imgPath)一个接口。这样设计的好处是,后续如果你想改进预处理、改变形状特征的计算方式,只需要修改对应子函数,不需要改动主流程。
4.3 编写训练脚本
训练脚本main_train.m负责读取所有训练图片,提取特征,训练分类器,并保存模型文件。
% 文件路径:main_train.m clc; clear; close all; %% 1. 设置训练集路径和类别 dataDir = 'data/train'; categories = {'apple', 'banana', 'orange'}; %% 2. 批量提取特征 features = []; labels = {}; for i = 1:length(categories) dirPath = fullfile(dataDir, categories{i}); if ~exist(dirPath, 'dir') error(['文件夹不存在: ', dirPath]); end imds = imageDatastore(dirPath); fprintf('正在提取类别 %s 的 %d 张图片特征...\n', categories{i}, numel(imds.Files)); for j = 1:numel(imds.Files) f = extractFeatures(imds.Files{j}); features = [features; f]; labels = [labels; categories{i}]; end end fprintf('\n特征提取完成,共 %d 个样本,特征维度 %d。\n', size(features, 1), size(features, 2)); %% 3. 转换为 categorical 标签 labelsCategorical = categorical(labels); %% 4. 训练 KNN 分类器 k = 5; mdl = fitcknn(features, labelsCategorical, 'NumNeighbors', k, 'Standardize', true); fprintf('KNN 分类器训练完成,k = %d。\n', k); %% 5. 在训练集上进行回代评估 trainPred = predict(mdl, features); trainAcc = sum(trainPred == labelsCategorical) / numel(labelsCategorical); fprintf('训练集回代准确率:%.2f%%\n', trainAcc * 100); %% 6. 保存模型 if ~exist('model', 'dir') mkdir('model'); end save('model/fruitModel.mat', 'mdl'); fprintf('模型已保存到 model/fruitModel.mat\n');这段脚本用imageDatastore读取文件夹下所有图片。imageDatastore是 Matlab 读取图像数据集的高效方式,它会自动遍历文件夹,不需要手动拼接每一张图片的路径。训练完成后,会保存出一个fruitModel.mat文件,供预测脚本和 GUI 调用。
4.4 编写预测函数
预测函数predictFruit.m接收图片路径和模型对象,返回预测类别。
% 文件路径:predictFruit.m function predLabel = predictFruit(imgPath, mdl) f = extractFeatures(imgPath); predLabel = predict(mdl, f); end如果你希望输出每个类别的置信度,可以用predict的第二个返回值。
% 文件路径:predictFruitWithScore.m function [predLabel, score] = predictFruitWithScore(imgPath, mdl) f = extractFeatures(imgPath); [predLabel, score] = predict(mdl, f); end4.5 编写测试脚本
测试脚本main_test.m选择一张测试图片,加载模型,并显示原图和预测结果。
% 文件路径:main_test.m clc; clear; close all; %% 加载训练好的模型 load('model/fruitModel.mat', 'mdl'); %% 选择测试图片 [filename, pathname] = uigetfile({'*.jpg;*.png;*.bmp', '图片文件 (*.jpg, *.png, *.bmp)'}, '选择一张水果图片'); if isequal(filename, 0) disp('未选择图片,程序退出。'); return; end imgPath = fullfile(pathname, filename); img = imread(imgPath); %% 预测 [predLabel, score] = predictFruitWithScore(imgPath, mdl); %% 显示结果 figure; imshow(img); title(sprintf('识别结果:%s', char(predLabel))); predStr = char(predLabel); names = categories(mdl.ClassNames); disp('各类别得分:'); for i = 1:length(mdl.ClassNames) fprintf('%s: %.4f\n', mdl.ClassNames(i), score(i)); end需要注意的是,mlt.ClassNames的顺序可能与categories的输入顺序不完全一致,但predict返回的score与mdl.ClassNames是一一对应的,所以显示时使用mdl.ClassNames即可。
4.6 GUI 界面快速实现
如果不想每次都在命令行调用,可以写一个简单的 GUI 界面。Matlab 中推荐使用 App Designer 创建界面,但为了便于展示核心逻辑,这里先给出一个基于脚本文件对话框的轻量实现。它已经能完成“选择图片 -> 显示结果”的核心交互。
% 文件路径:fruit_app.m function fruit_app() % 加载模型 if exist('model/fruitModel.mat', 'file') load('model/fruitModel.mat', 'mdl'); else errordlg('未找到 model/fruitModel.mat,请先运行 main_train.m 训练模型。', '错误'); return; end % 选择图片 [filename, pathname] = uigetfile({'*.jpg;*.png;*.bmp', '图片文件 (*.jpg, *.png, *.bmp)'}, '选择一张水果图片'); if isequal(filename, 0) return; end imgPath = fullfile(pathname, filename); img = imread(imgPath); % 预测 [predLabel, score] = predictFruitWithScore(imgPath, mdl); % 显示 figure('Name', '水果分类识别系统', 'NumberTitle', 'off', 'Position', [300 200 600 500]); subplot(2, 1, 1); imshow(img); title('原始图片'); subplot(2, 1, 2); axis off; text(0.1, 0.6, ['识别结果:', char(predLabel)], 'FontSize', 20, 'Color', 'b'); names = mdl.ClassNames; for i = 1:length(names) text(0.1, 0.6 - 0.15 * i, sprintf('%s: %.2f%%', names(i), score(i) * 100), 'FontSize', 12); end end如果使用 App Designer,可以把回调函数里的核心代码替换为上述预测逻辑,并增加按钮和图像显示控件。这里不展开界面设计的每一个细节,因为本文重点在图像处理与分类链路,而不是控件拖拽。
5. 系统运行与验证
5.1 运行步骤
- 打开 Matlab,将当前文件夹切换到项目根目录。
- 运行
main_train.m,训练模型并保存。 - 运行
main_test.m,选择一张测试图片,查看识别结果。 - 运行
fruit_app.m,体验图片选择与结果展示流程。
如果你的 Matlab 没有统计与机器学习工具箱,fitcknn会直接报错。解决办法是安装对应工具箱,或者将分类器替换为更适合低依赖环境的模型。
5.2 预期输出
当main_train.m正常执行时,命令窗口会输出类似下面的信息:
正在提取类别 apple 的 30 张图片特征... 正在提取类别 banana 的 30 张图片特征... 正在提取类别 orange 的 30 张图片特征... 特征提取完成,共 90 个样本,特征维度 14。 KNN 分类器训练完成,k = 5。 训练集回代准确率:100.00% 模型已保存到 model/fruitModel.mat注意,训练集回代准确率很高是正常现象,因为模型见过这些样本。它只能说明模型在训练数据上没有明显欠拟合,并不能代表测试集上的真实性能。评估模型应该使用独立的测试集。
5.3 使用测试集评估模型
你可以在data/test目录下准备三类测试图片,然后运行下面的评估脚本,得到测试集准确率和混淆矩阵。
% 文件路径:evaluateTest.m clc; clear; close all; load('model/fruitModel.mat', 'mdl'); testDir = 'data/test'; categories = {'apple', 'banana', 'orange'}; trueLabels = []; predLabels = []; for i = 1:length(categories) dirPath = fullfile(testDir, categories{i}); imds = imageDatastore(dirPath); for j = 1:numel(imds.Files) pred = predictFruit(imds.Files{j}, mdl); trueLabels = [trueLabels; categorical(categories{i})]; predLabels = [predLabels; pred]; end end acc = sum(predLabels == trueLabels) / numel(trueLabels); fprintf('测试集准确率:%.2f%%\n', acc * 100); figure; confusionchart(trueLabels, predLabels);confusionchart会绘制一个混淆矩阵图,对角线上的数字越高说明分类效果越好。
6. 常见问题与排查思路
以下是我在类似项目中经常遇到的问题,整理成表格供快速查阅。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
运行main_train.m报错:Undefined function 'extractFeatures' | 当前路径下没有包含extractFeatures.m文件 | 检查文件是否在项目目录,或者用addpath添加路径 |
| 报错:Undefined function 'imbinarize' | Matlab 版本低于 R2016b | 将imbinarize(gray, graythresh(gray))改为im2bw(gray, graythresh(gray)) |
| 报错:Undefined function 'fitcknn' | 未安装 Statistics and Machine Learning Toolbox | 在 Matlab 中执行ver检查工具箱,或使用fitcknn替代方案 |
| 读取图片时路径含中文导致失败 | Matlab 对某些中文路径支持不好 | 将项目放在全英文路径下,图片文件名也用英文 |
| 特征提取很慢 | 图片尺寸过大,medfilt3处理时间长 | 降低imresize的目标尺寸,例如改为 128×128 |
| 训练集回代准确率很高,但测试集准确率低 | 数据量太少、过拟合,或训练集与测试集差异大 | 增加样本数量,做数据增强,尝试更简单模型 |
| 多类别预测时结果全偏向某一类 | 类别样本不平衡 | 增加少数类样本,或在训练时使用Prior参数修正先验概率 |
medfilt3对彩色图像报错 | 图像不是 uint8 或 double 类型 | 使用im2double或uint8转换后再调用 |
| GUI 启动后找不到模型文件 | 当前工作目录不对 | 在 GUI 中先用fileparts(mfilename('fullpath'))获取脚本所在目录,再拼接模型路径 |
其中“路径问题”是初学者最容易踩的坑。Matlab 的当前工作目录和你脚本所在的目录并不一定相同。最稳妥的做法是在脚本开头使用:
currentDir = fileparts(mfilename('fullpath')); cd(currentDir);这样可以确保脚本无论从哪里启动,都会先切换到自身所在目录,再访问相对路径下的数据和模型文件。
7. 工程化最佳实践
从“能运行”到“好用、可维护、可扩展”,还需要注意以下几个工程层面的问题。
7.1 数据集组织与版本管理
训练集、测试集、验证集一定要分开,且三者之间不能有重复图片。数据集的版本管理同样重要,建议在模型文件命名中加入日期或特征版本号,例如:
fruitModel_knn_v1_20260501.mat这样当你调整了特征提取方式或新增了类别后,旧的模型文件不会被直接覆盖,方便回溯和对比实验。
7.2 特征标准化
KNN 是基于距离的分类器,如果不同特征的量纲差异很大,距离计算会被量纲大的特征主导。比如面积接近 0.5,而纹理对比度可能只有 0.2,如果不做标准化,形状特征对距离的影响就会过大。
在fitcknn中开启'Standardize', true会自动做标准化,但如果你要切换到 SVM 或 BP 神经网络,也需要提前用zscore或mapminmax对特征做标准化。
7.3 数据增强
真实项目中,收集大量带标签水果图片的成本较高。常见的数据增强方式包括:
- 水平翻转。
- 小角度旋转。
- 轻微平移。
- 调整亮度、对比度。
- 加少量高斯噪声。
这些操作在 Matlab 中可以借助imrotate、imtranslate、imnoise、imadjust实现。数据增强不仅能扩大样本量,还能提升模型对拍摄角度、光照变化的鲁棒性。
7.4 模型选择与调参
KNN 的NumNeighbors是一个超参数。k 值太小容易过拟合,k 值太大又可能把不同类别样本混在一起。实际使用中,可以用交叉验证来选择 k 值。
% 5折交叉验证选择最优 k rng(42); ks = 1:2:15; cvError = zeros(length(ks), 1); for i = 1:length(ks) mdlTemp = fitcknn(features, labelsCategorical, 'NumNeighbors', ks(i), 'Standardize', true); cvError(i) = kfoldLoss(crossval(mdlTemp, 'KFold', 5)); end [~, bestIdx] = min(cvError); bestK = ks(bestIdx); fprintf('最优 k = %d\n', bestK);7.5 日志与可重复性
训练脚本里最好记录每次实验的关键信息:使用了哪些类别、每类多少样本、特征维度、分类器类型、运行时间。可以简单用fprintf输出到命令窗口,也可以使用diary命令将输出保存到日志文件。
diary('training_log.txt'); % 训练脚本... diary off;保证随机种子固定。使用rng(42)在生成模拟数据、划分训练集和测试集时可以复现结果。
7.6 安全限制与数据合规
如果项目要用于实际生产环境,需要注意图片数据的合规问题。尤其是涉及用户上传图片时,应该避免收集不必要的个人信息。在开发阶段,建议只使用公开的、可自由使用的水果图片数据集,或者自己拍摄的样本。
对于排序、删除、覆盖文件等操作,务必先备份,再做最小粒度的验证。比如删除错误分类图片时,优先移动到trash文件夹而不是直接delete。
8. 总结与学习路线
本文以“Matlab 基于数字图像处理的水果分类识别系统”为例,完整实现了一条从图像预处理到特征提取、从分类器训练到 GUI 展示的图像分类链路。核心掌握点可以总结为以下几条:
- 图像预处理不是可有可无的步骤,它对后续特征提取的稳定性和分类准确率影响很大。
- 颜色、形状、纹理是最常用的三类手工特征,使用 HSV 颜色矩、regionprops 区域属性和灰度共生矩阵分别提取,可以覆盖大多数简单物体分类场景。
- KNN 特别适合小样本、低维特征的分类入门,但不适合大规模实时应用。
- 训练集准确率高不代表模型好,必须用测试集或交叉验证评估。
- Matlab 项目中,路径管理、工具箱依赖和模型保存是三个最容易出错也最容易被忽视的点。
如果你已经可以独立跑通这套系统,下一步可以从三个方向继续深入。
第一,扩展类别与数据集。把三类水果扩展到五类、十类,观察准确率变化,思考哪些类别之间容易混淆,是否需要增加新的特征维度。
第二,从手工特征过渡到深度学习。使用 AlexNet、ResNet、GoogLeNet 等预训练模型进行迁移学习,可以大幅提升复杂场景下的识别能力。Matlab 中可以通过deepNetworkDesigner和trainNetwork实现,但需要 Deep Learning Toolbox。
第三,把算法集成到实时应用。如果你希望做成实时视频识别,需要把当前脚本改造成基于webcam或视频流的实时检测模式,并考虑跟踪算法,因为视频中同一个水果会在多帧连续出现,逐帧识别会浪费时间。
如果本文对你有帮助,可以收藏备用。实际运行中如果遇到代码报错,建议先在本文的基础上检查路径、工具箱和图片格式,如果仍无法解决,欢迎在评论区带上完整报错信息一起讨论。