多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

OpenCV水果识别实战:苹果、香蕉、梨子样本采集与SVM分类

OpenCV水果识别实战:苹果、香蕉、梨子样本采集与SVM分类 简介这份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_okTrue) 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) # 只统计 S40 且 V40 的像素过滤背景和阴影 mask (hsv[:, :, 1] 40) (hsv[:, :, 2] 40) h hsv[:, :, 0][mask] hist, _ np.histogram(h, bins18, 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, axis0) print(cls, np.round(mean_hist, 3))逻辑说明cv2.cvtColor把 BGR 转成 HSV注意 OpenCV 读进来默认是 BGR 不是 RGB直接按 RGB 处理会得到错误的色相。mask过滤掉低饱和和低明度像素避免灰色背景和阴影污染直方图。bins18是把 0 到 180 的色相分成 18 段每段 10 度对三类水果足够区分分得太细会让特征稀疏。参数说明S40和V40这两个阈值是经验值背景越干净可以调得越高。如果你的样本背景杂乱把 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, keycv2.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(kernelrbf, C10, gammascale, probabilityTrue) ) clf.fit(X, y) joblib.dump(clf, fruit_svm.pkl) print(train acc:, clf.score(X, y))逻辑说明make_pipeline把标准化和 SVM 绑在一起这是很多人忽略的一步。SVM 对特征尺度敏感颜色直方图的值在 0 到 1 之间长宽比可能到 3不标准化的话长宽比会主导距离计算。probabilityTrue让推理时能拿到置信度方便设阈值拒绝低置信样本。参数说明C10是正则强度的倒数越大越容易过拟合小样本下 1 到 10 之间试。gammascale是 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 fdataset/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, alpha1.0, beta20)) outs.append(cv2.convertScaleAbs(img, alpha1.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 度以内角度太大水果会转出画面边缘出现黑边反而引入噪声。参数说明beta20和beta-20是亮度增减的经验值对应大约 ±15% 的明度变化。如果你的样本本身光照差异就大这个扰动可以省掉避免和真实差异叠加。增强之后特征维度会显得冗余21 维里有些分量对分类贡献很小。用随机森林的feature_importances_筛一遍把重要性低于 0.02 的维度去掉再重训 SVM小样本下通常能涨 3 到 5 个百分点。这个筛选步骤我一般会跑两次第一次看排序第二次按阈值砍维度砍完再验证一次确认不是过拟合带来的假提升。最后说个习惯每次调完参数把训练集准确率、验证集准确率、验证集混淆矩阵三个数一起记下来只记一个准确率过两天你根本想不起来当时改了什么。我吃过这个亏后来固定用一个文本文件记省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表