简介:基于OpenCV的水果识别样本集,收录苹果、香蕉、梨子三类共1600余张JPG图片,适合计算机视觉初学者、高校学生及开发者作为图像分类项目的训练与测试数据,解决水果样本难获取、标注数据不足的痛点。压缩包内文件总数1624个,除大量水果图像外,还含少量XML标注文件和项目配置文件,便于在分类器训练中参考或复用;资源整体仅17.82MB,下载和整理成本低。已有超过12800人学习下载,作为入门级水果识别数据集具有较高参考价值。利用这些样本,可以完成图像预处理、特征提取、分类器训练与评估等典型流程,支持复现颜色直方图、HOG特征配合SVM的水果分类实验,也可结合深度学习模型进一步扩展识别能力。资源描述中提及样本可能区分不同色泽或成熟状态,有助于测试模型在光照、角度变化下的鲁棒性,是理解计算机视觉基础流程的实用素材。 做果蔬分拣项目之前,我一直觉得“识别水果”这种需求应该直接上深度学习模型,直到朋友把一个传送带项目甩到我面前:苹果、香蕉、梨子,三种水果,摄像头俯拍,要求实时归类。我冷静想了想,就三个类别,颜色和形状差异肉眼可见,用YOLO反而要标注几百张图、调一堆超参数,纯属杀鸡用牛刀。最后我用OpenCV传统图像处理路线,固定光源下识别率做到95%以上,单帧处理十几毫秒,整个过程从样本采集到调参上线不到两天。这篇文章就把这套方案的思路、代码和踩坑记录完整分享出来,给正在做水果识别、样品分类或者入门OpenCV项目的人一个可以直接抄作业的参考。
1. 三种水果的视觉差异决定了算法选型
1.1 为什么不用深度学习而用传统图像处理
先说结论:传统OpenCV方案适合“类别少、特征明显、环境可控”的场景。深度学习擅长处理的是“类别多、背景杂乱、姿态千变万化”的问题,代价是样本标注成本和推理硬件需求。
我这个项目里,识别对象只有苹果、香蕉、梨子三种,它们之间有三个肉眼可见的显著特征差异:颜色完全不同或差别较大、整体形状一个偏圆、一个偏长条、一个介于两者之间。这类特征用HSV颜色分割加轮廓几何分析就能稳定提取,完全没必要引入神经网络。
另外还有个现实考量:项目要跑在树莓派或者工控机这类低算力设备上。同样的场景,OpenCV传统算法一帧只要十几毫秒,而一个小型CNN模型在树莓派上跑一次前向推理轻松上百毫秒,还要额外装推理框架。成本上完全不划算。
1.2 苹果、香蕉、梨子在图像上的三大差异维度
我在设计识别逻辑之前,先把三种水果在图像里的视觉特征列了个表,这个表直接决定了后面所有代码怎么写:
| 水果 | 主颜色 | 整体形状 | 长宽比 | 轮廓圆形度 |
|---|---|---|---|---|
| 苹果 | 红色(也有青苹果,这里按红苹果处理) | 近圆形 | 0.9~1.2 | 0.78以上 |
| 香蕉 | 黄色 | 细长弯月形 | 1.8~4.0 | 0.3~0.6 |
| 梨子 | 黄绿色 | 上窄下宽的椭圆 | 1.1~1.5 | 0.65~0.78 |
长宽比就是轮廓外接矩形的宽除以高,圆形度用公式(4\pi \times area / perimeter^2)计算,圆越正,值越接近1。这三个维度配合颜色mask,足够把三种水果分开。
这里要特别说明:苹果如果是青苹果,颜色会和梨子撞车,这时候就要靠形状特征做二次区分,苹果的圆形度更高,梨子的外接矩形长宽比更大且轮廓有“脖子”的凹陷。后面我会讲这个形状判别的具体实现。
2. 样本准备与HSV色彩空间分割
2.1 样本采集环境对识别率的影响
很多初学者把精力全花在调阈值上,忽略了样本采集环境,这是本末倒置。我在做样本时踩过一个坑:直接在办公室日光灯下随手拍了几十个水果照片当测试集,结果换到项目现场才发现,现场光源偏暖,同样的黄色香蕉在HSV里的H通道整体偏移了快10个值,之前调的阈值全部失灵。
所以样本采集阶段就要规定好环境:光源色温尽量固定,建议用白色LED灯或自然光;背景建议用纯色板,我这套方案用的是深蓝色背景板,因为蓝色和红、黄、绿色水果的HSV区间完全不冲突,分割时天然省事。拍摄角度固定为俯拍,垂直高度控制在40到60厘米之间,保证水果占画面比例一致。
每次采集样本时,顺手记录当时的实际光照条件,方便后期排查阈值漂移问题。你甚至可以做一个标定步骤:放一张标准色卡在画面角落,程序启动时自动检测色卡颜色,动态调整HSV阈值。这个功能我后续加上了,效果显著。
2.2 HSV阈值设计:苹果的红色区间需要分段处理
HSV颜色空间的H分量是色相,用一个角度值表示颜色。红、黄、绿色分别落在这个色环的不同区域。用HSV而不是BGR做颜色分割,最大的好处就是颜色对光照亮度不那么敏感。V通道代表亮度,S通道代表饱和度,这两个波动大,但H相位相对稳定。
苹果的红色在HSV里是个特例。红色的H值以0为起点,同时跨越了0和179两个边界——某一种红可能落在H=5的地方,另一种偏紫的红可能落在H=172的地方。所以苹果的红色mask必须用两段区间合并:
lower_red1 = np.array([0, 60, 60]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([160, 60, 60]) upper_red2 = np.array([179, 255, 255]) mask_apple = cv2.inRange(hsv, lower_red1, upper_red1) | cv2.inRange(hsv, lower_red2, upper_red2)两段区间的H范围分别是0到10和160到179,S下限设60是为了过滤灰暗的背景噪声,V下限设60是为了滤掉过暗的阴影区域。一个常见误区是只用一段0到180的全范围,那样会把橙色、粉色甚至部分环境光里的暖色都划进苹果区域,误检率飙升。
2.3 香蕉和梨子的HSV区间对比
香蕉和梨子的颜色相对好处理,但要注意一个细节:它们都可能呈现黄绿色,尤其没熟透的梨子更像绿色,熟透的香蕉边缘也可能泛绿。我最终用的区间是:
# 香蕉:纯黄 lower_banana = np.array([20, 80, 80]) upper_banana = np.array([35, 255, 255]) # 梨子:黄绿到浅绿 lower_pear = np.array([35, 40, 60]) upper_pear = np.array([80, 200, 255])香蕉的H区间取20到35,梨子的H区间取35到80。两者在H=35的位置有临界重叠,所以不能只靠颜色一刀切,必须配合形状特征。实际操作中发现,S通道在香蕉和梨子的区分上也有作用:香蕉的饱和度普遍更高,梨子偏灰绿。所以梨子的S上限我设成200,饱和度太高的像素(通常是香蕉的黄色)进不来;香蕉的S下限设80,太黯淡的绿色像素过不去。
这几个阈值不是拍脑袋定的,是我拿三四十张不同成熟度的水果照片反复测试,统计像素分布后得出的。建议你也这么做,拿一批自己的样本,用cv2.imshow配合cv2.getTrackbar调出实时调试窗口,一边拖动S和H的滑条一边看mask效果,比一次一次跑脚本效率高得多。
3. 轮廓提取与几何特征计算
3.1 从mask到质心:findContours的参数选择
颜色分割完成后,拿到的是黑白二值mask。接下来要把mask里的水果主体提取出来,核心工具是cv2.findContours。它的参数选择有讲究,我直接用最常见的组合:
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)RETR_EXTERNAL只取最外层轮廓,适合分离单个目标;如果你画面里可能有水果堆叠,就得考虑RETR_CCOMP加hierarchy层级分析。CHAIN_APPROX_SIMPLE会对轮廓做压缩,只保留关键点,减少后续计算量。
找完轮廓后要立刻做一步“面积过滤”:轮廓面积小于某个阈值的,直接丢弃。这些小块通常是背景反光、果梗影子或者噪声。我的项目里画面400x400像素,最小有效面积设500像素,效果不错。这个阈值可以根据你的相机分辨率等比调整。
3.2 长宽比与圆形度:区分香蕉和梨子的关键指标
拿到轮廓后,下一步是算两个几何特征:长宽比和圆形度。
x, y, w, h = cv2.boundingRect(contour) # 外接矩形 aspect_ratio = w / float(h) # 长宽比 perimeter = cv2.arcLength(contour, True) circularity = 4 * np.pi * area / (perimeter ** 2) # 圆形度为什么长宽比能区分香蕉和梨子?因为香蕉是明显的细长弯月形,外接矩形的长边和短边差距巨大;梨子虽然也是竖长的,但整体更接近椭圆,长宽比一般不超过1.5。我用aspect_ratio > 1.5作为香蕉的判据,实测准确率很高。
圆形度是另一个强大但不常被新手注意的特征。它衡量一个轮廓有多接近完美圆形。苹果的圆形度通常在0.78以上,梨子的轮廓因为上窄下宽,圆形度掉到0.78以下。有一次我拿了一个偏红的黄苹果测试,颜色区间和黄色香蕉撞了,结果正是靠高圆形度把它从香蕉堆里拉回来,归类成苹果。
3.3 特征对比表与分类决策树
把颜色和几何特征组合起来,我搭了一个三层的分类决策逻辑:
- 第一层:分别计算三种水果的颜色mask,得到三个候选区域。
- 第二层:如果某个mask的轮廓面积过小(比如红苹果的红色区域在整个画面里占比不足),视为该类别未出现。
- 第三层:当多个mask同时命中时(比如梨子的黄绿色和香蕉的黄色重叠),用几何特征做最终仲裁。
仲裁规则我写成了一张决策表:
| 命中情况 | 仲裁逻辑 |
|---|---|
| 只有苹果mask命中 | 直接判为苹果 |
| 只有香蕉mask命中 | 直接判为香蕉 |
| 只有梨子mask命中 | 直接判为梨子 |
| 香蕉mask和梨子mask都命中 | 看长宽比,>1.5判香蕉,否则判梨子 |
| 苹果mask和梨子mask都命中 | 看圆形度,>0.72判苹果,否则判梨子 |
| 三个mask都命中 | 取面积最大的轮廓对应的类别 |
这套决策树写起来简单,逻辑清晰,比直接输出“概率最大类别”更可控。遇到异常情况你还能定位是哪一层判断出了岔子。
4. 代码实现:完整的识别流程
4.1 预处理函数:尺寸归一化与去噪
先把整个流程的预处理部分搭好。所有代码基于Python和OpenCV 4.x,安装过程很简单,pip install opencv-python numpy即可,不需要额外装别的库。
import cv2 import numpy as np def preprocess(image_path, target_size=(400, 400)): img = cv2.imread(image_path) if img is None: print(f"[ERROR] 无法读取图像: {image_path}") return None, None, None img = cv2.resize(img, target_size) # 统一尺寸,方便后续阈值比较 blurred = cv2.GaussianBlur(img, (5, 5), 0) # 高斯滤波去噪 hsv = cv2.cvtColor(blurred, cv2.COLOR_BGR2HSV) return img, blurred, hsv尺寸归一化这一步很多人不做,但实际项目里摄像头采集到的画面尺寸可能不固定,不统一的话,面积过滤阈值没法设成固定值。高斯滤波的核大小我建议5x5,太小去不掉传感器噪点,太大把水果边缘磨模糊了,轮廓会不精准。
4.2 颜色分割mask的生成
接着是生成三种水果的颜色mask,并把形态学开运算加上,用来去掉mask里的小孔洞和毛刺:
def build_masks(hsv): # 苹果:红色分两段区间 lower_red1 = np.array([0, 60, 60]) upper_red1 = np.array([10, 255, 255]) lower_red2 = np.array([160, 60, 60]) upper_red2 = np.array([179, 255, 255]) mask_apple = cv2.inRange(hsv, lower_red1, upper_red1) | \ cv2.inRange(hsv, lower_red2, upper_red2) # 香蕉:黄色 lower_banana = np.array([20, 80, 80]) upper_banana = np.array([35, 255, 255]) mask_banana = cv2.inRange(hsv, lower_banana, upper_banana) # 梨子:黄绿色 lower_pear = np.array([35, 40, 60]) upper_pear = np.array([80, 200, 255]) mask_pear = cv2.inRange(hsv, lower_pear, upper_pear) kernel = np.ones((5, 5), np.uint8) mask_apple = cv2.morphologyEx(mask_apple, cv2.MORPH_OPEN, kernel) mask_banana = cv2.morphologyEx(mask_banana, cv2.MORPH_OPEN, kernel) mask_pear = cv2.morphologyEx(mask_pear, cv2.MORPH_OPEN, kernel) return mask_apple, mask_banana, mask_pear形态学开运算是先腐蚀再膨胀,效果是去掉白色区域里的细小噪点和孤立小点。如果你的mask边缘有毛刺,可以在开运算后再加一次闭运算(先膨胀再腐蚀),把水果内部的孔洞填上。
4.3 轮廓筛选与最终分类逻辑
最后是核心的轮廓提取和分类函数:
def get_features(mask, min_area=500): contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) features = [] for c in contours: area = cv2.contourArea(c) if area < min_area: continue x, y, w, h = cv2.boundingRect(c) perimeter = cv2.arcLength(c, True) if perimeter == 0: continue aspect_ratio = w / float(h) circularity = 4 * np.pi * area / (perimeter ** 2) features.append({ 'contour': c, 'box': (x, y, w, h), 'area': area, 'aspect_ratio': aspect_ratio, 'circularity': circularity }) return features def classify_fruit(features): if not features: return 'unknown' main = max(features, key=lambda f: f['area']) if main['aspect_ratio'] > 1.5: return 'banana' if main['circularity'] > 0.72: return 'apple' return 'pear' def recognize(image_path): img, blurred, hsv = preprocess(image_path) if hsv is None: return None mask_apple, mask_banana, mask_pear = build_masks(hsv) feat_apple = get_features(mask_apple) feat_banana = get_features(mask_banana) feat_pear = get_features(mask_pear) # 判断哪些类别“命中” candidates = [] if feat_apple: candidates.append(('apple', feat_apple)) if feat_banana: candidates.append(('banana', feat_banana)) if feat_pear: candidates.append(('pear', feat_pear)) if not candidates: return 'unknown' # 只有一个候选时直接返回 if len(candidates) == 1: return candidates[0][0] # 多个类别都命中,用几何特征仲裁 main_candidates = [] for label, feats in candidates: main_feat = max(feats, key=lambda f: f['area']) main_candidates.append((label, main_feat)) # 香蕉和任何类别冲突时,优先看长宽比 if len(main_candidates) >= 2: max_area_feat = max(main_candidates, key=lambda item: item[1]['area']) if max_area_feat[1]['aspect_ratio'] > 1.5: return 'banana' for label, feat in main_candidates: if label == 'apple' and feat['circularity'] > 0.72: return 'apple' if max_area_feat[1]['circularity'] > 0.72: return 'apple' return 'pear' return 'unknown'这套代码跑起来,单张400x400图片的完整识别耗时在15毫秒左右。需要接摄像头的场景,把cv2.VideoCapture(0)的每一帧喂给recognize函数即可。如果目标设备性能更弱,可以先降采样到320x240,识别速度能再翻一倍,精度损失不大。
5. 实测调参中的坑与经验
5.1 高光区域导致误检
第一次实拍测试就翻车了:苹果表面有高光反射,高光区域在HSV里表现为V值特别高、S值特别低,接近白色。结果这个白色反射斑块在mask里形成了空洞,苹果轮廓变得残缺,圆形度大幅下降,被误判成了梨子。
解决办法有两个,我最终两个都上了。第一是在HSV阈值里对V通道做上限截断,把高光区域划进非目标区域:
lower_red1 = np.array([0, 60, 40]) upper_red1 = np.array([10, 255, 200])第二在形态学处理时加大闭运算的内核尺寸,把高光空洞填上。实测下来,把kernel从5x5调到7x7,配合V通道上限200,苹果的mask完整性恢复得挺好,圆形度重新回到0.8以上。
5.2 相邻水果粘连问题
第二个坑是多个水果靠在一起时,颜色分割后轮廓粘连成一个连通域,面积翻倍、外接矩形变宽,长宽比直接超标,香蕉和苹果挨着放时,一个苹果能被误判成香蕉。
我的处理思路是在轮廓阶段加一道“分离假设”:如果某个轮廓的max外接矩形面积占比过大(比如超过画面面积的60%),就用形态学腐蚀把轮廓分隔开,再做一次findContours。这里建议你用cv2.distanceTransform加水漫分水岭算法来处理粘连目标,实际效果比单纯腐蚀稳定。
不过最省事的方案还是从源头避免:项目实际部署时,传送带上用隔板限位,保证每个水果在画面里的位置相对独立。算法能兜底当然好,但工程上能用机械结构解决的,就别过度依赖算法。
5.3 背景与光照的设置建议
所有调参经验里,最值钱的是一条:把背景和光照固定下来,你的阈值才能有意义。我的测试板上有一块痕迹,本来影响不大,但在傍晚自然光变暖后,那块背景的HSV值漂进了梨子的颜色区间,导致每隔几秒就误报一次“梨子”。
后来我在代码里加了一个动态背景补偿逻辑:程序启动时先拍一帧空背景,把背景的HSV分布记录下来;每帧识别时,如果背景区域内有像素落在水果颜色区间,就做减法去除。其实更直接的办法是换深蓝色背景板,蓝色在HSV里和红、黄、绿都隔得很远,背景干扰基本归零。
光照方面,光源位置不要正对镜头,让光从侧面打过来,能大幅减少水果表面的高光反射。我在传送带两侧各放了一根白色LED灯管,亮度调到中等,色温稳定在5000K左右。固定好这些条件之后,阈值基本不用再动。
从我个人的实际经历来说,这类识别项目真正花时间的不是算法本身,而是对环境和样本的把控。如果你正准备做类似的水果识别项目,建议按这个顺序来:先固定环境和背景,再采集样本,接着在HSV调试窗口里调整阈值,最后才是写分类逻辑。顺序反了,后面就是无穷无尽的修bug。另外我做的这套颜色加形状的识别方案,稍微改改阈值和几何判据,完全可以迁移到西红柿、黄瓜、柠檬这些果蔬的识别上,框架是通用的,换产品只需要换参数和决策表,维护成本很低。
本文还有配套的精品资源,点击获取