简介:面向OpenCV图像识别初学者与进阶者,这份水果样本包聚焦苹果、香蕉、梨子的分类应用,覆盖从图像预处理、特征提取到分类器训练与评估的典型实验流程。包体共1624个文件,以1618张JPG格式的水果图像为核心,另有5个XML标注/配置文件与1个IML工程文件,压缩包仅17.82MB,轻量易用,适合快速加载与迭代。图像样本来源于多种光照、角度和背景环境,并包含了不同成熟度与摆放姿态的实例,可支撑色彩直方图、GLCM纹理、HOG形状描述等特征提取实验,也能作为SVM、随机森林或卷积神经网络训练与验证的数据集。文件结构清晰,图像按类别组织,稍作整理即可用于监督学习任务,省去自行采集和清洗标注的时间。目前已有12814人学习下载,对于希望在真实样本上完整走通水果识别全流程、对比传统机器学习与深度学习效果的读者,是一份高性价比的参考数据包。
1. opencv水果识别样本(苹果、香蕉、梨子):先让样本能打,再谈模型能打
很多初学者拿到 opencv 水果识别样本(苹果、香蕉、梨子)的第一反应是赶紧把分类器跑起来,结果往往卡在同一个地方:训练集里测得好好的模型,换到另一批照片上,手电筒一亮、桌面一换就全线翻车。按我的踩坑经验,这个问题多数不是分类器选错了,而是你手里的「样本」压根没准备好——你的苹果只有一种光线、一种角度,机器学到的其实不是苹果,而是那条特定的光照和背景。下面这套路径从样本组织、特征提取、分类器参数到五个高频翻车点,给出一条能直接复现的水果分类路线。想用 OpenCV 做小样本图像识别、或者给后续深度学习做基线对比的读者,这套方法能帮你把试错成本压到最低。
2. 准备一套能过夜的水果样本集:目录结构、采集规范与数据增强
模型能跑多远,样本说了算。打算给 opencv 水果识别样本(苹果、香蕉、梨子)做训练的,先别急着写识别代码,把数据目录和采集规范定下来,后面所有调试都会顺很多。这一章的做法适用于个人研究、课堂作业和公司原型验证,按下面这套来,至少不会在数据组织上返工。
2.1 目录结构先定好:train / test 分家,交叉验证才不会变成记账本
我一般把数据按下面这种目录组织。train 和 test 分开放是底线,千万不要把所有照片放一个文件夹里再手动挑,那样调参时很容易把测试集的信息泄漏到训练里。
dataset/ ├── train/ │ ├── apple/ # 苹果样本 │ ├── banana/ # 香蕉样本 │ └── pear/ # 梨子样本 └── test/ ├── apple/ ├── banana/ └── pear/每个文件名建议统一成「类别_编号.jpg」,例如apple_001.jpg、banana_012.jpg。命名规范的目的有两个:一是防止同类文件在不同目录里重名,二是后续统计样本量、排查脏数据时能快速定位是哪一张。
原始照片和最终数据集分开。假设你刚拍完一批照片放在raw_photos/下,用下面的脚本一键完成划分:
# split_dataset.py from pathlib import Path import random import shutil random.seed(2024) # 固定种子,保证每次复现同一份划分 raw_dir = Path("raw_photos") # 原始照片目录,子目录为 Apple/Banana/Pear out_dir = Path("dataset") # 输出数据集目录 test_ratio = 0.2 # 测试集占比,样本量少于 100 时 0.2 比较稳 for fruit in ["apple", "banana", "pear"]: src = raw_dir / fruit images = sorted(src.glob("*.jpg")) # 只处理 jpg,png 同理 random.shuffle(images) test_count = int(len(images) * test_ratio) test_images = images[:test_count] # 前 20% 进测试集 train_images = images[test_count:] # 后 80% 进训练集 for split_name, imgs in [("train", train_images), ("test", test_images)]: dst = out_dir / split_name / fruit dst.mkdir(parents=True, exist_ok=True) for img in imgs: shutil.copy2(img, dst / img.name) # copy2 保留 EXIF 等元信息逻辑说明:这里按类别分别 shuffle,再按比例切分,保证训练集和测试集里三类水果的比例都一样。random.seed(2024)很关键,小样本实验里验证集划分经常是「评估玄学」的一部分,固定种子才能让你改完特征后确定成绩变化来自特征还是来自运气。test_ratio=0.2对应 100 张样本切 80/20,样本量再少可以升到 0.25,但不要低于 0.15,否则测试集只有十几张,评估波动太大。
完成后建议统计一下每个子目录的文件数。常见做法是find dataset -type f | sed 's|/[^/]*$||' | uniq -c,如果某个类别明显偏少,直接补拍,别等训练完才发现数据集是个跷跷板。
2.2 采集规范:白平衡、光源与画幅,决定了颜色特征的上限
三种水果里,苹果靠红色、香蕉靠黄色、梨子靠黄绿色,分类的首要特征是颜色。颜色特征最怕的不是分类器不准,而是拍摄条件失控。下面这张表是我在处理这类小样本时固定的拍摄规范,新手可以直接照抄:
| 拍摄条件 | 建议做法 | 容易出现的问题 |
|---|---|---|
| 光源 | 室内白光 LED 灯固定角度,或窗边自然光 + 避开直射 | 白炽灯偏黄、日光灯偏绿,颜色整体漂移 |
| 白平衡 | 拍摄前用白纸或灰卡对准镜头手动白平衡 | 自动白平衡在不同角度会跳,同一批照片颜色不一致 |
| 手机相机 | 关闭 HDR、AI 场景增强、滤镜 | 手机对高饱和物体常自动提升饱和度,会影响 S 通道特征 |
| 背景 | 纯色纸或磨砂板,颜色与水果差异大 | 红色桌布会把苹果和背景混在一起,掩膜分割时灾难 |
| 画幅 | 水果占画面 70% 以上,背景留得越少越好 | 背景占比过大时,特征向量里混入大量无关颜色 |
采集数量方面,每类至少 80~100 张是这类小项目的保底线。少了也能跑,但交叉验证的方差会很大,同一个模型换一次划分,准确率能差十几个百分点。三类一共 300 张左右,处理时间基本都在分钟级,数据集再翻一倍也完全扛得住。
还有一点容易被忽略:拍摄时尽量让水果处在同一高度、同一姿态范围。香蕉平放和立起来形状差异很大,如果训练集里基本都是平放,测试时立起来的都会被形状特征带偏。我一般只保留「平放、侧视 30°~60°」这个姿态区间,让形状特征保持稳定。
2.3 数据增强:旋转、缩放、亮度扰动,但别动颜色值
样本不够时,增强是最便宜的后悔药。OpenCV 自带几何变换接口,不用引入额外库。注意一个原则:靠颜色分类的任务,不要对色相做增强,否则红色苹果变个色就成了不存在的类别。
# augment.py import cv2 import numpy as np def rotate(img, angle=15): h, w = img.shape[:2] matrix = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) # 用边缘填充代替黑色填充,避免黑边干扰后续 HSV 掩膜 return cv2.warpAffine(img, matrix, (w, h), borderMode=cv2.BORDER_REPLICATE) def brightness(img, beta=20): # alpha=1.0 保持对比度不变,beta 控制亮度偏移 return cv2.convertScaleAbs(img, alpha=1.0, beta=beta) def scale(img, factor=0.9): h, w = img.shape[:2] return cv2.resize(img, (int(w * factor), int(h * factor)))逻辑说明:cv2.warpAffine的borderMode默认是黑色填充,黑边在做 HSV 掩膜时会被当成低饱和度背景,虽然能被inRange过滤,但边界处会引入一圈过渡像素。BORDER_REPLICATE复制边缘像素填充,效果更干净。cv2.convertScaleAbs的alpha控制对比度、beta控制亮度增量,水果识别里只加beta控制在 ±20 以内的亮度扰动即可,太大就违背了「同一批样本来自同种光照」的前提。
增强后的样本写回硬盘,而不是在训练实时生成,这是小样本场景的习惯做法。每种水果由 100 张原始图扩到 300 张左右,特征提取和训练时间增加有限,但分类器对旋转和轻微光照变化的鲁棒性会明显改善。注意增强的「度」:旋转超过 45° 会让香蕉的长轴比失真,缩放低于 0.8 会让轮廓细节丢失,这两个参数别贪心。
3. 提取特征:颜色直方图是主力,形状特征做辅助
分类器只是把特征向量映射到类别,特征本身决定上限。opencv 水果识别样本(苹果、香蕉、梨子)这个场景,特征设计有个主次关系:颜色是第一区分度,形状是第二区分度,纹理这类特征对小样本来说收益不大,不必强加。
3.1 为什么选 HSV 而不是 RGB:把亮度从颜色里拆出来
RGB 三个通道和光照强相关,同一颗红苹果在亮光和阴影下,三个通道一起变化,特征向量在特征空间里位移很大。HSV 把亮度(V)独立出来,色相(H)描述到底是什么颜色,饱和度(S)描述颜色有多纯,这样颜色特征就对亮度变化更稳定。OpenCV 里的注意点:8 位图像的 H 范围是 0~179,不是 0~360,S 和 V 都是 0~255,网上很多资料按 0~360 写,直接抄就翻车。
# color_hist.py import cv2 import numpy as np def color_hist(roi): hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV) # S 和 V 下限取 40,过滤低饱和度的阴影和反光区 mask = cv2.inRange(hsv, (0, 40, 40), (179, 255, 255)) hist = cv2.calcHist([hsv], [0], mask, [32], [0, 180]) total = hist.sum() if total == 0: return np.zeros(32) return (hist / total).flatten()逻辑说明:cv2.calcHist的第一个参数传的是图像列表,第二个参数[0]表示统计第 0 通道也就是 H 通道;掩膜mask只统计满足饱和度、亮度条件的像素,把高光和阴影剔除掉。bins=32把 180 个色相值压缩成 32 档,既能区分红黄绿,又不会因为档数太多导致每个桶里样本数太少。最后除以总和得到分布直方图,这样画面大小不同、水果大小不同时,特征仍然可比。
(0, 40, 40)这个下限是按典型室内光照来的。如果你的场景偏暗,V 的下限降到 30 能保住更多暗部细节;场景里反光厉害,S 下限提到 50 也能稳定不少。每次改动这个阈值,训练和测试都要用同一份掩膜,否则特征分布对不上。
直方图特征还可以补两个数:H 分量的均值和方差。以我的经验,香蕉的 H 峰值集中在 25~35,梨子集中在 40~60,而红苹果的像素会落在 0 附近和 175~179 两端,三者峰值位置差异明显,均值和方差能反映颜色分布集中度,和直方图一起进特征向量就行。
3.2 形状特征:圆形度、长短轴比与 Hu 矩的组合
苹果接近圆形,香蕉细长,梨子轮廓介于两者之间但一端明显收窄。只靠形状一件不够,但和颜色直方图组合后,能解决「红色圆球 vs 红色苹果」这类颜色近似问题。形状特征从轮廓提取,计算量很小:
# shape_feat.py def shape_features(gray): _, thresh = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY) cnts, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not cnts: return None cnt = max(cnts, key=cv2.contourArea) # 只保留最大轮廓,去掉噪声点 area = cv2.contourArea(cnt) peri = cv2.arcLength(cnt, True) circularity = 4 * np.pi * area / (peri * peri + 1e-6) # 圆形度 0~1 rect = cv2.minAreaRect(cnt) side1, side2 = rect[1] aspect = max(side1, side2) / (min(side1, side2) + 1e-6) # 长短轴比 hu = cv2.HuMoments(cv2.moments(cnt)).flatten() hu_log = [np.sign(x) * np.log10(abs(x) + 1e-10) for x in hu[:3]] # 转 log 尺度 return [circularity, aspect] + hu_log逻辑说明:cv2.RETR_EXTERNAL只取外轮廓,水果内部如果有斑点或纹理不会产生干扰轮廓;max(cnts, key=cv2.contourArea)在背景分割不完美时,能过滤掉孤立的小噪声块,但前提是水果本身是画面里最大的连通域。圆形度的范围是 0~1,苹果通常在 0.7 以上,香蕉常低于 0.4;长短轴比用minAreaRect的最小外接矩形计算,平放的香蕉可到 2.5 以上。
Hu 矩共 7 个,为什么我只取前 3 个?因为 7 个里后面的分量数值极小、噪声相对大,在小样本场景中带来的主要是过拟合。np.sign(x) * np.log10(abs(x) + 1e-10)是为了压尺度,Hu 矩的数值跨度能达到几十个数量级,不平滑直接丢给分类器,距离函数会被前几个大数主导。另外,cv2.threshold固定用 100 的分割阈值,适合训练集亮度接近的情况;如果光照变化大,改成大津法cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)会更稳。
3.3 把特征拼起来:拼接、标准化与训练测试同参数
最终特征向量是 32 维颜色直方图 + 1 维圆形度 + 1 维长短轴比 + 3 维权 Hu 矩,共 37 维。维度不高,但对 KNN 这类距离型分类器来说,量纲问题必须处理——直方图的值是 0~1,而 Hu 矩哪怕取了 log,数值范围仍然可能到 ±几,两者同权参与距离计算时,颜色特征会被形状特征的数值波动盖掉。
# build_features.py import cv2 import numpy as np from pathlib import Path def center_crop(img, ratio=0.85): h, w = img.shape[:2] side = int(min(h, w) * ratio) x = (w - side) // 2 y = (h - side) // 2 return img[y:y + side, x:x + side] def load_features(root): feats, labels = [], [] for label, fruit in enumerate(["apple", "banana", "pear"]): for img_path in sorted((Path(root) / fruit).glob("*.jpg")): img = cv2.imread(str(img_path)) if img is None: continue img = center_crop(img, 0.85) # 中心裁剪,去掉手工框的边缘偏差 img = cv2.resize(img, (224, 224)) # 统一正方形尺寸 hist = color_hist(img) shape = shape_features(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) if shape is None: continue feats.append(np.concatenate([hist, shape])) labels.append(label) return np.array(feats, dtype=np.float32), np.array(labels) feat_train, label_train = load_features("dataset/train") feat_test, label_test = load_features("dataset/test") mean = feat_train.mean(axis=0) std = feat_train.std(axis=0) + 1e-6 # 加 1e-6 防止方差为 0 feat_train = (feat_train - mean) / std feat_test = (feat_test - mean) / std # 测试集用同一个 mean/std逻辑说明:center_crop固定裁剪比例 0.85,避免手工框不准导致同一颗水果在不同图里占比差异过大;resize到 224×224 是常见习惯,正方形画幅能让轮廓特征在对比时不受原始照片宽高比影响。load_features里标签用 0、1、2 编码,训练时再转成列向量给 OpenCV 的分类器。标准化仅用训练集的 mean/std 计算,测试集不参与统计——如果测试也参与,评估值会偏乐观,这在调参时会给你一个假的「我很准」信号。
37 维在这个数据规模下不算大,不建议先 PCA。小样本里 PCA 减少的维数有限,却多了一步调参和一处可出错的环节。先把原始特征喂给分类器,如果后面发现训练时间和内存吃紧,再回头做 PCA 也不迟。
4. 训练分类器:KNN 与 SVM 的选型、参数与交叉验证
特征向量固定后,分类器从 KNN 起步,再升级到 SVM,是这个小样本任务里最顺滑的路线。两类算法的取舍其实很简单:KNN 无参数、能马上看到特征质量,SVM 在小样本上泛化能力更强、但需要调 C 和 gamma。先把 KNN 跑通,是确认特征没做错的最高效方式。
4.1 先用 KNN 跑通:k 值玄学与距离度量
KNN 的思路是「看离测试样本最近的 K 个训练样本投票」。它不需要训练阶段做复杂估计,所以特征有问题时,KNN 的准确率会直接反映出特征的可分性。OpenCV 内置了 KNN 接口,训练预测一段代码搞定:
# knn_train.py import cv2 import numpy as np knn = cv2.ml.KNearest_create() knn.setDefaultK(5) # 三类均衡时,奇数 k 避免平票 knn.train(feat_train.astype(np.float32), cv2.ml.ROW_SAMPLE, label_train.reshape(-1, 1).astype(np.float32)) ret, preds, neighbours, dist = knn.findNearest(feat_test.astype(np.float32), 5) acc = (preds.ravel() == label_test).mean() print("KNN acc:", round(acc, 3))逻辑说明:reshape(-1, 1)是 OpenCV 机器学习模块的老规矩,样本标签必须组织成列向量,而且 OpenCV 的 ml 模块不管是训练数据还是标签都要求 float32,所以这里统一做了转换;findNearest返回四个值,第一个是返回码,第二个是预测标签,neighbours是每个样本最近的 K 个邻居的标签,dist是距离。这里把preds.ravel()展平后和测试标签对比计算准确率。
k 值的选择在这个场景有点玄学:k=1 时过拟合最严重,测试样本只要有一张异常图就会带偏;k=3 或 k=5 起步一般最稳。k 太高(比如 15)会把很多边缘样本拉进投票,三个类别边界模糊时反而变差。固定 k=5,如果准确率在特征调整后反而下降,再回头看 k。距离度量方面,OpenCV 的 KNN 用欧氏距离,特征标准化做好后这个默认值就够用。
KNN 的本质是把分类决策推迟到预测时刻,所以训练集本身也是模型的一部分。训练集里混入模糊、失焦的图,即使只有三五张,也会在最邻近检索时污染投票结果。换句话说,KNN 调试不理想时,先检查数据,再怀疑 k 值。
4.2 换成 SVM:RBF 核的 C 和 gamma,找到范围比精确调参更值钱
样本量在几十到几百时,SVM 在小样本上的泛化通常优于 KNN。OpenCV 的 SVM 支持 C-SVC 类型和 RBF 核,RBF 核的两个关键参数 C 和 gamma,翻车主要发生在参数范围完全不对的情况。
# svm_train.py svm = cv2.ml.SVM_create() svm.setType(cv2.ml.SVM_C_SVC) svm.setKernel(cv2.ml.SVM_RBF) svm.setC(10.0) # 错分惩罚,越大越严格 svm.setGamma(0.01) # RBF 核宽度,越小决策边界越平滑 svm.train(feat_train.astype(np.float32), cv2.ml.ROW_SAMPLE, label_train.reshape(-1, 1).astype(np.float32)) _, preds = svm.predict(feat_test.astype(np.float32)) acc = (preds.ravel() == label_test).mean() print("SVM acc:", round(acc, 3))逻辑说明:setType(cv2.ml.SVM_C_SVC)是多分类的 C-SVC,适合三分类任务;setKernel(cv2.ml.SVM_RBF)选 RBF 核,理由是小样本和线性不可分特征下,RBF 可以映射到更高维空间。C=10、gamma=0.01 是针对 37 维特征和百张级样本的合理起点,但这两个值不是拍脑袋定的,靠小范围网格搜索找更好。下面这个两层循环足够用,feat_val / label_val是从训练集按 8:2 再切出的验证集,划分逻辑见 4.3:
# svm_grid.py from itertools import product C_choices = [1, 10, 50] gamma_choices = [0.001, 0.01, 0.1] for C, gamma in product(C_choices, gamma_choices): svm = cv2.ml.SVM_create() svm.setType(cv2.ml.SVM_C_SVC) svm.setKernel(cv2.ml.SVM_RBF) svm.setC(C) svm.setGamma(gamma) svm.train(feat_train.astype(np.float32), cv2.ml.ROW_SAMPLE, label_train.reshape(-1, 1).astype(np.float32)) _, preds = svm.predict(feat_val.astype(np.float32)) acc = (preds.ravel() == label_val).mean() print(f"C={C}, gamma={gamma}, acc={acc:.3f}")逻辑说明:这个循环用验证集选参,选出结果后再用完整训练集训练一次、测试集评估,这是小样本调参的标准顺序。gamma 从 0.001 到 0.1 按 10 倍步长搜一轮,C 按 1、10、50 搜一轮,基本能覆盖常见可行区。gamma 过大会让每个样本都变成自己的小区域,训练准确率虚高、测试崩盘;C 过大则把异常样本强制划对,决策边界被个别脏样本拉着走。SVM 不是黑匣子,如果你搜完一圈准确率始终上不去,问题大概率在前面两步:特征没分开,或者样本里混了太多噪声。
4.3 小样本交叉验证:用 5 折代替一拍脑袋的划分
固定划分评估有个隐患:90 张训练样本里,某一张异常图落在训练集还是验证集,能直接影响好几个百分点的评估值。交叉验证把数据切成 K 份,轮流拿一份做验证,K 次平均,把「运气成分」摊平。样本总量过百后,5 折是速度与稳定性的平衡点。
# kfold.py import numpy as np from itertools import product feat_all = np.concatenate([feat_train, feat_val]) label_all = np.concatenate([label_train, label_val]) K = 5 idx = np.arange(len(feat_all)) np.random.seed(0) np.random.shuffle(idx) folds = np.array_split(idx, K) for C, gamma in product([1, 10, 50], [0.001, 0.01, 0.1]): scores = [] for k in range(K): val_idx = folds[k] tr_idx = np.concatenate([folds[i] for i in range(K) if i != k]) svm = cv2.ml.SVM_create() svm.setType(cv2.ml.SVM_C_SVC) svm.setKernel(cv2.ml.SVM_RBF) svm.setC(C) svm.setGamma(gamma) svm.train(feat_all[tr_idx].astype(np.float32), cv2.ml.ROW_SAMPLE, label_all[tr_idx].reshape(-1, 1).astype(np.float32)) _, preds = svm.predict(feat_all[val_idx].astype(np.float32)) scores.append((preds.ravel() == label_all[val_idx]).mean()) print(f"C={C}, gamma={gamma}, mean={np.mean(scores):.3f} " f"std={np.std(scores):.3f}")逻辑说明:np.array_split(idx, K)把打乱后的索引切成 5 份,每轮取一份做验证、其余做训练,这种实现不依赖额外库。注意每次循环都要新建SVM_create,不能复用上一个实例,因为train会清空旧模型。看结果别只看 mean,std 太大说明某一份划分里混入了难样本,数据质量比参数更值得怀疑。5 折跑 9 组参数共 45 次训练,对百张级样本是秒级的事,完全不必为省时间缩减参数组合。
5. 避坑:opencv 水果识别样本最常见的 5 个翻车点
这一章写的是我处理这类小样本时见过最多的翻车姿势。每条按现象、原因、解决展开,你可以对照自己的日志和特征分布自查。很多问题到了最后都不在分类器参数上,而在数据和预处理环节。
5.1 背景分割把红色桌布当成苹果
现象:离线评估准确率很高,但拿到新场景一测,只要画面里有红色物体,就被判成苹果。
原因:训练时用的inRange掩膜阈值太宽,把背景里红色系的像素也算进特征。红色桌布和红苹果在 H 值上接近,颜色直方图里背景贡献的那一坨,让分类器把「画面里红色占比高」当成了苹果的判据。
解决:掩膜阈值别只调下限,把inRange改成只覆盖目标水果的 ROI,保证水果占画面主体。拍摄规范里背景用灰白纯色,从源头减少背景颜色进入直方图。如果条件不允许换背景,就把掩膜形态学腐蚀膨胀做一轮,把边缘过渡像素清掉,再统计 H 直方图。先看一帧的掩膜可视化结果再进批量处理,这一步能省半天排查时间。
5.2 白平衡偏移让黄色香蕉变成绿色
现象:室内白炽灯下拍的香蕉,特征和梨子拉不开距离,测试时黄色香蕉被判成梨子。
原因:白炽灯色温偏低,整体画面偏暖,黄色区域往橙色甚至绿色方向漂移;自动白平衡在取景角度变化时会跳动,同一颗香蕉在不同帧里的 H 值分布完全不同,颜色特征不稳定。
解决:拍摄时用白纸或灰卡手动白平衡,固定光源角度,别依赖自动白平衡。已经拍完的图,可以用灰度世界假设做一次性校正:把三通道均值拉到同一水平,能明显缓解偏色。注意校正参数要从同一批照片估计,不要逐张独立校正,否则还会引入新的不一致。这一步处理完,香蕉和梨子的色相分布会重新分开。
5.3 样本不平衡:每类数量差 3 倍以上,模型开始「偷懒」
现象:训练时总准确率不低,但看每类准确率,梨子只有 40%,苹果接近 95%。
原因:苹果拍了 300 张,梨子只有 80 张。分类器把边界往样本多的类别偏,代价函数里多数类的累积误差占比大,最优解就是把梨子区域让给苹果。KNN 和 SVM 对类别不平衡都很敏感。
解决:最有效的办法是补拍,三类数量尽量接近。补不了就用第 2 章的增强把少类补到和最多类同一量级,注意少类增强的旋转角度不要贪大,几何失真反而制造难样本。评估时也不止看总准确率,把每类的准确率单独打印一份,梨子那行数字突然掉下来,先怀疑样本量,再怀疑特征。
5.4 ROI 框定不严:同一个样本切出三种「水果」
现象:同一张图只是裁剪范围稍微变了下,预测结果就在三类之间来回跳。
原因:训练时手工框的 ROI 大小不一,resize 后水果在画面里的占比 40% 到 90% 不等,特征里混入的背景比例完全不同,样本在特征空间里形成了几团互相重叠的点。
解决:统一预处理管线,先按水果中心做正方形中心裁剪,再 resize 到固定尺寸。第 3.3 里的center_crop固定比例 0.85,即裁剪框占原图短边的 85%,可以覆盖大多数手工框的偏差。实现上把裁剪、resize、掩膜封装成一个函数,训练和测试只调用它,从流程上杜绝两边预处理不一致。
5.5 训练和测试预处理不一致:离线 95 分,摄像头前掉到 60 分
现象:脚本离线测试能到 95%,接上摄像头实时流后准确率暴跌,且稳定复现。
原因:离线测试走的是和训练相同的图像读取、resize 路径,实时流直接抓帧就送进分类器,颜色空间、尺寸甚至掩膜阈值都不同。特征在训练时做好的标准化参数mean、std也只在离线脚本里用了,实时推理那段代码没带上。
解决:把预处理、特征提取、标准化这三个步骤固化成独立的函数模块,推理脚本只 import 过来用。mean、std在训练完保存成 np 数组文件,预测时加载,不要在推理脚本里重新计算。我的做法是写一个predict.py,单测里喂一张固定的样本图验证输出,再接摄像头,这样问题就限定在采集端,不会和特征端混在一起排查。
6. 落地验证与进阶:混淆矩阵、滑动窗口多目标与光照归一化
6.1 用混淆矩阵看三类到底在哪里互相认错
准确率只是一个数字,混淆矩阵才告诉你错在哪儿。手写一个 3×3 矩阵,逐行打印:
from collections import Counter cm = Counter() for pred, true in zip(preds.ravel(), label_test): cm[(int(true), int(pred))] += 1 names = ["apple", "banana", "pear"] print("true \\ pred", *(f"{n:>6}" for n in names)) for i in range(3): row = [cm[(i, j)] for j in range(3)] print(f"{names[i]:>8}", *(f"{v:>6}" for v in row))常见结果是 apple 和 pear 偶有互串,banana 基本独立。看到这类模式,说明两类之间的颜色或形状边界确实近,下一步比较它们的 H 直方图峰值位置,找到具体分界,再决定要不要补特征或调掩膜。
6.2 从单图分类到整图多目标:滑动窗口 + 简单聚合
固定台面单目标场景跑通后,想识别画面里多个水果,最快的路线是滑动窗口:窗口大小 224、步长 112 扫过全图,每个 patch 过一遍分类器,对连续多个窗口给出同类预测的区域,按窗口中心聚类合并得到最终包围框。这个方案速度慢,但对角度变化不敏感,作为原型验证很合适。再进一步就用背景差分出前景连通域,每个连通域单独过分类器,速度能快一个量级。
6.3 值得花时间的两个扩展:光照归一化与 H-S 二维直方图
灰度世界白平衡适合统一不同光源下的样本;把一维 H 直方图升级成 32×8 的 H-S 二维直方图,可以区分「鲜红」和「暗红」,对苹果这种表面光泽强的水果有实际收益。这两步做完,特征维度小幅上升,但分类器的鲁棒性提升比调参数明显得多。
我自己的习惯是先把样本和特征跑顺,再回头调分类器。以前做这种水果样本也爱在 SVM 参数上钻牛角尖,后来发现把数据整理干净、特征两套组合,KNN 已经能打到不错的效果,SVM 只是锦上添花。希望帮到你。
本文还有配套的精品资源,点击获取