简介:这份OpenCV水果识别样本面向计算机视觉入门与进阶学习者,聚焦苹果、香蕉、梨子三类水果的图像分类任务,可用于练习图像预处理、特征提取、分类器训练与测试的完整流程。包内共1624个文件,以1618张jpg水果图像为主体,辅以5个xml标注文件和1个iml工程配置,压缩包约17.82MB,样本覆盖不同角度与背景,便于直接用于训练与验证。资源围绕色彩直方图、纹理特征、形状描述子等特征提取方法,以及SVM、随机森林、CNN等分类器展开,可帮助读者理解光照变化、角度差异等实际场景下的泛化问题,并尝试数据增强等优化手段。目前已有12811人学习下载,适合希望动手实践图像识别、搭建水果分类demo或完成课程设计的学习者参考。
1. 水果识别样本这件事:苹果、香蕉、梨子为什么值得先跑通
很多人第一次接触 opencv 图像处理项目,都是从识别水果开始的。原因很直接:苹果、香蕉、梨子这三类目标在颜色和形状上差异明显,采集样本不需要专业设备,手机拍几十张就能开工,而且整个链路——采集、标注、训练、推理——能在半天内跑完一轮。但真正动手后你会发现,翻车点往往不在模型本身,而在样本组织方式和颜色空间的选择上。同一张苹果图,RGB 通道下和 HSV 通道下的可分性差出一大截;背景里多一块黄色桌布,香蕉的识别框就会飘。这篇笔记就围绕「opencv水果识别样本(苹果、香蕉、梨子)」这个方向,把样本怎么攒、特征怎么提、分类器怎么选、参数怎么调讲清楚,适合想用 opencv 做物体识别和计数、又不想一上来就啃深度学习框架的从业者。
2. 样本采集与目录组织:三类水果的最小可用数据集怎么攒
2.1 每类需要多少张、怎么拍才不白干
先给结论:苹果、香蕉、梨子各 80 到 120 张,就足够跑通一个传统视觉分类器。低于 50 张,验证集波动会大到让你怀疑代码写错了;高于 200 张,在传统方法下收益递减,除非你要做细粒度品种区分。
拍摄时控制三个变量。第一是背景,尽量用纯色桌面或白纸,避免出现和水果同色系的杂物——黄色香蕉旁边放一个黄色杯子,是血泪经验里最常见的翻车场景。第二是光照,自然散射光优先,避免单侧强光造成的高光斑,高光会让 HSV 里的 S 通道出现空洞。第三是角度,每个水果至少覆盖正面、侧面、俯视三个视角,因为形状特征(比如梨子的上窄下宽)只有在特定角度才明显。
文件命名建议带类别前缀和序号,例如apple_001.jpg、banana_001.jpg、pear_001.jpg。不要用中文名,也不要用空格,后面写脚本遍历目录时能省掉一堆编码麻烦。
2.2 目录结构与批量重命名的落地脚本
我一般会按下面的结构组织,训练脚本只认这个层级:
dataset/ train/ apple/ banana/ pear/ val/ apple/ banana/ pear/train 和 val 按 8:2 切分。切分不要手动拖文件,用脚本做,保证可复现:
import os import random import shutil SRC_DIR = "raw" # 原始图片,按类别放好 DST_DIR = "dataset" # 输出目录 SPLIT_RATIO = 0.8 # 训练集比例 random.seed(42) # 固定随机种子,保证每次切分一致 for cls in os.listdir(SRC_DIR): cls_path = os.path.join(SRC_DIR, cls) if not os.path.isdir(cls_path): continue imgs = [f for f in os.listdir(cls_path) if f.lower().endswith((".jpg", ".jpeg", ".png"))] random.shuffle(imgs) cut = int(len(imgs) * SPLIT_RATIO) for phase, subset in (("train", imgs[:cut]), ("val", imgs[cut:])): out_dir = os.path.join(DST_DIR, phase, cls) os.makedirs(out_dir, exist_ok=True) for name in subset: shutil.copy(os.path.join(cls_path, name), os.path.join(out_dir, name)) print(cls, "train:", cut, "val:", len(imgs) - cut)逻辑说明:脚本遍历raw下每个类别目录,打乱后按比例复制到dataset/train和dataset/val。random.seed(42)是关键,没有它每次切分结果不同,调参时你无法判断指标变化是模型带来的还是数据划分带来的。SPLIT_RATIO设 0.8 是传统视觉任务的常用值,样本量小的时候可以调到 0.7,给验证集多留一点。
参数说明:SRC_DIR指向你手工整理好的原始图,类别名就是最终标签名,所以目录名直接用apple、banana、pear,不要写「苹果」这种中文,OpenCV 读路径时对非 ASCII 支持不稳定,容易触发modulenotfounderror之外的路径报错。
提示:切分完成后,先肉眼翻一遍 val 目录,确认没有同一张图同时出现在 train 和 val 里。重复样本是验证指标虚高的头号原因。
3. 特征提取:HSV 颜色直方图加形状矩,为什么比直接喂 RGB 稳
3.1 颜色空间选择与通道阈值
RGB 三个通道对光照强度高度耦合,同一只苹果在阴影下和阳光下,R 值能差 60 以上。HSV 把色相(H)、饱和度(S)、明度(V)拆开,色相基本不受明暗影响,这就是水果识别里优先用 HSV 的原因。
苹果的 H 值集中在 0 到 10 和 170 到 180 两段(红色在色相环首尾),香蕉集中在 20 到 35,梨子偏黄绿,大致在 35 到 60。这些区间不是拍脑袋,是拿几十张样本统计出来的,你可以用下面的脚本自己验证一遍:
import cv2 import numpy as np import os def hsv_hist(path): img = cv2.imread(path) img = cv2.resize(img, (256, 256)) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 只统计 S>40 且 V>40 的像素,过滤背景和阴影 mask = (hsv[:, :, 1] > 40) & (hsv[:, :, 2] > 40) h = hsv[:, :, 0][mask] hist, _ = np.histogram(h, bins=18, range=(0, 180)) return hist / (hist.sum() + 1e-6) for cls in ["apple", "banana", "pear"]: folder = os.path.join("dataset/train", cls) feats = [hsv_hist(os.path.join(folder, f)) for f in os.listdir(folder)[:20]] mean_hist = np.mean(feats, axis=0) print(cls, np.round(mean_hist, 3))逻辑说明:cv2.cvtColor把 BGR 转成 HSV,注意 OpenCV 读进来默认是 BGR 不是 RGB,直接按 RGB 处理会得到错误的色相。mask过滤掉低饱和和低明度像素,避免灰色背景和阴影污染直方图。bins=18是把 0 到 180 的色相分成 18 段,每段 10 度,对三类水果足够区分,分得太细会让特征稀疏。
参数说明:S>40和V>40这两个阈值是经验值,背景越干净可以调得越高。如果你的样本背景杂乱,把 S 阈值提到 60 以上,代价是水果边缘的过渡像素被丢掉,直方图会变尖。
3.2 形状特征:轮廓矩与长宽比
颜色能分开苹果和香蕉,但苹果和梨子都是偏圆的红黄系,光靠颜色容易混。这时候补形状特征。用cv2.findContours拿到最大轮廓,算两个量:长宽比和 Hu 矩的第一、第二分量。
def shape_feat(path): img = cv2.imread(path) img = cv2.resize(img, (256, 256)) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # OTSU 自动阈值,比固定阈值适应性强 _, th = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) cnts, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not cnts: return None c = max(cnts, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(c) aspect = w / float(h) # 长宽比 M = cv2.moments(c) hu = cv2.HuMoments(M).flatten() return np.array([aspect, hu[0], hu[1]])逻辑说明:cv2.THRESH_OTSU自动找阈值,省去手动调thresh的麻烦,在光照均匀的样本上表现稳定。cv2.RETR_EXTERNAL只取外轮廓,避免水果表面的纹理被当成子轮廓。cv2.HuMoments返回 7 个矩,这里只取前两个,因为后几个对噪声敏感,小样本下反而拖后腿。
参数说明:aspect对香蕉区分度最高,正常摆放的香蕉长宽比在 2.5 以上,苹果和梨子都在 1.0 到 1.3 之间。梨子的hu[1]通常比苹果略大,因为上窄下宽的轮廓不对称性更强,但这个差异不大,实际分类时颜色特征权重应该更高。
注意:形状特征依赖分割质量。如果 OTSU 把背景也算进前景,长宽比会直接失真。跑完脚本先打印几张图的
aspect,确认数值在合理范围再往下走。
4. 分类器训练与推理:SVM 参数怎么设、模型怎么存
4.1 特征拼接与 SVM 训练脚本
把颜色直方图和形状特征拼成一个向量,喂给 SVM。特征维度是 18 + 3 = 21 维,样本量几百,SVM 完全够用,训练时间以秒计。
import cv2 import numpy as np import os import joblib from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline def extract(path): hist = hsv_hist(path) # 18 维 shape = shape_feat(path) # 3 维 if shape is None: return None return np.concatenate([hist, shape]) X, y = [], [] classes = ["apple", "banana", "pear"] for label, cls in enumerate(classes): folder = os.path.join("dataset/train", cls) for f in os.listdir(folder): feat = extract(os.path.join(folder, f)) if feat is not None: X.append(feat) y.append(label) X = np.array(X) y = np.array(y) # 标准化 + SVM 串成 pipeline,避免推理时忘记做同样的缩放 clf = make_pipeline( StandardScaler(), SVC(kernel="rbf", C=10, gamma="scale", probability=True) ) clf.fit(X, y) joblib.dump(clf, "fruit_svm.pkl") print("train acc:", clf.score(X, y))逻辑说明:make_pipeline把标准化和 SVM 绑在一起,这是很多人忽略的一步。SVM 对特征尺度敏感,颜色直方图的值在 0 到 1 之间,长宽比可能到 3,不标准化的话长宽比会主导距离计算。probability=True让推理时能拿到置信度,方便设阈值拒绝低置信样本。
参数说明:C=10是正则强度的倒数,越大越容易过拟合,小样本下 1 到 10 之间试。gamma="scale"是 sklearn 的默认自适应值,等于1/(n_features * X.var()),比手动设固定值省心。如果验证集准确率明显低于训练集,先把 C 降到 1 再看。
4.2 推理与批量计数
训练完要落到实际识别和计数上,下面这段是单张图推理加简单计数的写法:
import cv2 import joblib import numpy as np clf = joblib.load("fruit_svm.pkl") classes = ["apple", "banana", "pear"] def predict(path): feat = extract(path) if feat is None: return "unknown", 0.0 proba = clf.predict_proba([feat])[0] idx = int(np.argmax(proba)) return classes[idx], float(proba[idx]) # 批量统计 val 目录 from collections import Counter counter = Counter() for cls in classes: folder = f"dataset/val/{cls}" for f in os.listdir(folder): pred, conf = predict(f"{folder}/{f}") if conf > 0.6: # 置信度阈值,低于此值不计入 counter[pred] += 1 print(counter)逻辑说明:predict_proba返回三个类别的概率,取最大值对应的类别。conf > 0.6是拒绝阈值,低于这个值说明模型没把握,宁可判为未知也不要硬分。批量统计时用Counter累加,能快速看出哪一类被误判得最多。
参数说明:置信度阈值 0.6 是起点,如果你的场景对误报容忍度低,提到 0.75;对漏报容忍度低,降到 0.5。这个值没有标准答案,取决于你的业务更怕哪种错误。
5. 避坑与排查:水果识别样本最容易翻车的 4 个地方
5.1 现象:验证集准确率 95%,换一张新图就乱判
原因:训练样本和验证样本来自同一批拍摄,背景、光照高度一致,模型学到的是背景特征而不是水果特征。解决:验证集必须包含不同背景、不同光照的图,哪怕只有 10 张。采集时故意换一次桌面、换一个时间段拍,比多拍 50 张同场景图有用。
5.2 现象:香蕉识别框总是偏大或偏小
原因:香蕉细长,cv2.boundingRect返回的是轴对齐矩形,倾斜摆放时矩形面积远大于实际轮廓。解决:改用cv2.minAreaRect拿旋转矩形,或者直接用轮廓面积和周长比作为形状特征,绕开矩形框的偏差。
5.3 现象:HSV 直方图在阴天样本上完全失效
原因:阴天光照色温偏冷,整体 V 值下降,如果 mask 里 V 阈值设得高,大量像素被过滤,直方图样本量不足导致分布畸变。解决:把 V 阈值从 40 降到 25,或者在采集阶段就统一用室内补光灯,从源头控制色温。
5.4 现象:训练时报ModuleNotFoundError: No module named 'cv2'
原因:装的是opencv-python之外的包名,或者虚拟环境没激活。解决:确认pip install opencv-python执行成功,然后在 Python 里import cv2; print(cv2.__version__)验证。如果用的是 conda,注意 conda 源里的包名是opencv,和 pip 的opencv-python不是一回事,混装容易冲突。
6. 把样本量压到 30 张还能用的一个技巧:数据增强加特征筛选
前面说的 80 到 120 张是稳妥值。如果你手头只有 30 张,还有一个办法能救:在线数据增强加特征维度筛选。
增强不要用复杂的 GAN,传统视觉任务里翻转、轻微旋转、亮度扰动就够了。关键是增强只对训练集做,验证集保持原样,否则指标又虚高。
import cv2 import numpy as np def augment(img): outs = [img] outs.append(cv2.flip(img, 1)) # 水平翻转 # 亮度扰动,alpha 控制对比度,beta 控制亮度 outs.append(cv2.convertScaleAbs(img, alpha=1.0, beta=20)) outs.append(cv2.convertScaleAbs(img, alpha=1.0, beta=-20)) # 小角度旋转,保持尺寸不变 h, w = img.shape[:2] M = cv2.getRotationMatrix2D((w / 2, h / 2), 10, 1.0) outs.append(cv2.warpAffine(img, M, (w, h))) return outs逻辑说明:cv2.flip的第二个参数 1 表示水平翻转,水果左右翻转不改变类别,是安全的增强。convertScaleAbs做亮度和对比度扰动,模拟不同光照。旋转用warpAffine,角度控制在 10 度以内,角度太大水果会转出画面,边缘出现黑边反而引入噪声。
参数说明:beta=20和beta=-20是亮度增减的经验值,对应大约 ±15% 的明度变化。如果你的样本本身光照差异就大,这个扰动可以省掉,避免和真实差异叠加。
增强之后特征维度会显得冗余,21 维里有些分量对分类贡献很小。用随机森林的feature_importances_筛一遍,把重要性低于 0.02 的维度去掉,再重训 SVM,小样本下通常能涨 3 到 5 个百分点。这个筛选步骤我一般会跑两次,第一次看排序,第二次按阈值砍维度,砍完再验证一次,确认不是过拟合带来的假提升。
最后说个习惯:每次调完参数,把训练集准确率、验证集准确率、验证集混淆矩阵三个数一起记下来,只记一个准确率,过两天你根本想不起来当时改了什么。我吃过这个亏,后来固定用一个文本文件记,省了很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取