
简介这份水果新鲜程度检测数据集面向计算机视觉初学者与目标检测实践者用于训练和验证水果新鲜度识别模型可服务于智能分拣、生鲜质检等场景。数据集包含apple、bad apple、banana、bad banana共4个类别标注文件同时提供txt与xml两种格式分别存放于独立文件夹便于适配YOLO与VOC等主流检测框架。压缩包共1192个文件其中jpg图像397张、txt标签398个、xml标注397个整体约15.19MB体量轻便适合快速开展模型训练与算法对比实验。目前已有1438人学习下载配套博文提供了数据集说明与检测结果参考读者可据此完成数据加载、类别映射、训练评估等完整流程并对照检测效果排查漏检与误检问题是入门目标检测与水果品质识别的一份实用素材。1. 水果新鲜程度检测数据集从烂果分拣线到模型训练集的落地真相做过水果分拣线的人都知道最头疼的不是机械臂抓不准而是摄像头底下那堆果子到底该判“新鲜”还是“次鲜”。水果新鲜程度检测数据集就是专门解决这个问题的图像数据集合——它把不同腐败阶段的水果拍成带标签的图片让模型学会区分“刚摘的”“放了两天的”“已经开始长霉的”。这不是一个玩具数据集它直接对应分拣线上的真实需求腐烂初期的果子肉眼难辨但一旦混进包装箱整箱货的货架期就会被拖垮。适合谁用做农产品视觉分拣的算法工程师、想切入智慧农业的CV团队、以及需要给质检环节加一道AI防线的水果加工厂技术负责人。数据集的质量直接决定你的模型是“能用”还是“玄学”。2. 水果新鲜程度检测数据集到底长什么样标签体系与采集逻辑2.1 新鲜度分级不是二分类先搞清楚你的业务要几档很多人第一次接触这个方向默认把它当成“新鲜 vs 腐烂”的二分类。真到产线上就翻车了。水果的腐败是连续过程分拣线需要的是可执行的档位。常见做法是分三到五档新鲜无瑕疵、次鲜轻微失水或表皮微皱、可加工局部软化但未霉变、腐烂明显霉斑或汁液渗出。档位越多标注一致性越难保证模型也越容易在边界样本上震荡。我一般建议从三档起步跑通闭环后再加档。标签体系一旦定下来整个数据集的采集、标注、训练、评估都围绕它转中途改档位等于重做。2.2 采集环节的四个硬约束光照、角度、背景、批次数据集的质量在按下快门那一刻就决定了七成。光照要覆盖产线常见的LED白光和自然光混合场景避免单一光源导致模型把“亮度”当成“新鲜度”的特征。拍摄角度至少包含顶视和侧视因为霉变往往从底部或侧面先出现。背景要干净但别太干净——纯白背景训出来的模型到产线上遇到传送带纹理就懵了。批次维度最容易被忽略同一批水果的成熟度天然接近如果训练集和验证集按随机划分模型可能记住的是“批次特征”而不是“腐败特征”。正确做法是按批次划分让验证集里的果子来自训练集没见过的批次。2.3 标注规范边界样本怎么判谁来判标注一致性是数据集的生命线。边界样本——比如表皮有一小块颜色异常但按压后质地正常——必须写进标注手册明确判为哪一档。常见做法是让两名标注员独立标同一批样本计算一致性指标低于阈值就重新培训。标注工具用LabelImg或CVAT都行关键是导出格式要统一。如果后续要用YOLO系列做检测直接存YOLO格式如果先做分类再过渡到检测分类文件夹结构更省事。标注文件里除了类别建议加一个“置信度”字段让标注员对拿不准的样本标低置信度训练时可以对这部分样本降权。3. 从零构建可训练的水果新鲜程度检测数据集采集、清洗、划分3.1 采集脚本与目录结构用OpenCV批量抓帧产线上直接录视频再抽帧比一张张拍效率高得多。下面这个脚本把一段产线视频按固定间隔抽帧并按日期和批次存到对应目录。注意抽帧间隔别太密否则相邻帧几乎一样等于变相重复采样。import cv2 import os from datetime import datetime def extract_frames(video_path, output_root, batch_id, interval_sec2): 从产线视频中按时间间隔抽帧 video_path: 输入视频路径 output_root: 输出根目录 batch_id: 批次编号用于后续按批次划分 interval_sec: 抽帧间隔秒 cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) frame_interval int(fps * interval_sec) save_dir os.path.join(output_root, batch_id) os.makedirs(save_dir, exist_okTrue) frame_count 0 saved_count 0 while True: ret, frame cap.read() if not ret: break if frame_count % frame_interval 0: # 用时间戳命名避免重名 timestamp datetime.now().strftime(%Y%m%d%H%M%S%f) filename f{batch_id}_{timestamp}.jpg cv2.imwrite(os.path.join(save_dir, filename), frame) saved_count 1 frame_count 1 cap.release() print(f批次 {batch_id} 共抽取 {saved_count} 帧保存至 {save_dir}) # 调用示例 extract_frames(line_video_01.mp4, ./raw_frames, batch_20240101, interval_sec3)逻辑说明interval_sec控制抽帧密度产线传送带速度越快间隔应越小。batch_id是后续按批次划分训练集的关键千万别用随机命名。保存的图片先不做任何裁剪和增强保留原始信息清洗阶段再处理。3.2 清洗去模糊、去重复、去无效样本抽完帧第一件事是清洗。模糊帧、曝光过度帧、空帧传送带上没果子都要去掉。用拉普拉斯方差检测模糊阈值一般设在100到200之间低于阈值的直接删。重复帧用感知哈希去重汉明距离小于5的视为重复。无效样本靠人工快速过一遍比任何自动方法都可靠。import cv2 import numpy as np import os from imutils import paths def is_blurry(image_path, threshold120): 拉普拉斯方差判断模糊值越小越模糊 img cv2.imread(image_path) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) variance cv2.Laplacian(gray, cv2.CV_64F).var() return variance threshold def clean_dataset(raw_dir, clean_dir, blur_threshold120): 清洗原始帧保留清晰样本 os.makedirs(clean_dir, exist_okTrue) removed 0 kept 0 for img_path in paths.list_images(raw_dir): if is_blurry(img_path, blur_threshold): removed 1 continue # 保留清晰样本文件名不变 filename os.path.basename(img_path) os.rename(img_path, os.path.join(clean_dir, filename)) kept 1 print(f清洗完成保留 {kept} 张剔除 {removed} 张模糊样本) clean_dataset(./raw_frames, ./clean_frames, blur_threshold120)参数说明blur_threshold需要根据你的相机分辨率和拍摄距离微调。产线相机通常分辨率较高阈值可以设到150以上。清洗后建议人工抽查一遍确认没有把“轻微失焦但新鲜度特征明显”的样本误删。3.3 按批次划分训练集、验证集、测试集这是最容易被忽视的一步。随机划分会让同一批次的相似样本同时出现在训练和验证集里模型评估结果虚高。正确做法是按batch_id划分比如8:1:1确保验证集和测试集里的批次在训练集中完全没出现过。import os import shutil import random def split_by_batch(clean_dir, output_dir, ratios(0.8, 0.1, 0.1)): 按批次划分数据集 clean_dir: 清洗后的图片目录文件名前缀为批次号 output_dir: 输出目录包含train/val/test三个子目录 ratios: 训练/验证/测试比例 batches {} for fname in os.listdir(clean_dir): batch_id fname.split(_)[0] # 假设文件名格式为 batchid_timestamp.jpg batches.setdefault(batch_id, []).append(fname) batch_list list(batches.keys()) random.shuffle(batch_list) n_train int(len(batch_list) * ratios[0]) n_val int(len(batch_list) * ratios[1]) train_batches batch_list[:n_train] val_batches batch_list[n_train:n_train n_val] test_batches batch_list[n_train n_val:] for split_name, split_batches in [(train, train_batches), (val, val_batches), (test, test_batches)]: split_dir os.path.join(output_dir, split_name) os.makedirs(split_dir, exist_okTrue) for batch_id in split_batches: for fname in batches[batch_id]: shutil.copy(os.path.join(clean_dir, fname), os.path.join(split_dir, fname)) print(f{split_name}: {len(split_batches)} 个批次 f{sum(len(batches[b]) for b in split_batches)} 张图片) split_by_batch(./clean_frames, ./dataset_split)逻辑说明批次数量少的时候比如少于20个批次按批次划分可能导致某一类完全缺失。这时候需要先检查每个批次的标签分布确保划分后各集合的类别比例大致均衡。如果批次实在太少退而求其次按“采集日期”划分但要在论文或报告中说明这个局限。4. 用水果新鲜程度检测数据集训练第一个模型分类与检测两条路4.1 先跑分类基线ResNet18微调的最小命令分类任务比检测简单适合快速验证数据集质量。用PyTorch加载预训练ResNet18替换最后一层全连接冻结前面层先训几轮再解冻微调。数据增强用随机水平翻转、颜色抖动、随机裁剪别用太激进的增强否则新鲜度相关的颜色特征会被破坏。import torch import torch.nn as nn from torchvision import models, transforms, datasets from torch.utils.data import DataLoader # 数据增强温和为主保护颜色特征 train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(./dataset_split/train, transformtrain_transform) val_dataset datasets.ImageFolder(./dataset_split/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) model models.resnet18(pretrainedTrue) num_features model.fc.in_features model.fc nn.Linear(num_features, len(train_dataset.classes)) # 类别数按你的档位来 # 先冻结主干只训分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 训练循环简化版完整训练需加验证和保存逻辑 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(5): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() print(fEpoch {epoch1}, Loss: {running_loss/len(train_loader):.4f})参数说明batch_size根据显存调整16到64之间。lr在冻结阶段用1e-3解冻后降到1e-4或更低。ColorJitter的幅度别超过0.3否则模型可能把颜色偏移当成新鲜度变化。训练5轮后解冻全部层再训10到20轮观察验证集准确率是否还在涨。4.2 检测任务YOLOv8的数据配置与训练命令分类只能告诉你“这张图里果子新不新鲜”检测要告诉你“哪个果子不新鲜”。YOLOv8是目前落地最顺手的检测框架。数据集目录按YOLO格式组织images和labels平行存放labels里每个txt文件对应一张图每行是class_id x_center y_center width height坐标归一化到0到1。# fruit_freshness.yaml path: ./dataset_split train: train/images val: val/images test: test/images names: 0: fresh 1: slightly_stale 2: rotten# 安装ultralytics并启动训练 pip install ultralytics yolo detect train \ datafruit_freshness.yaml \ modelyolov8n.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ patience20 \ projectruns/fruit_freshness \ nameexp1参数说明modelyolov8n.pt是最小模型适合快速验证精度不够换yolov8s.pt或yolov8m.pt。imgsz640是默认值如果果子在图中占比小可以提到1280但显存和速度会受影响。patience20表示验证集指标20轮不提升就早停防止过拟合。训练完在runs/fruit_freshness/exp1/weights/下拿best.pt做推理。4.3 评估指标怎么看别只看准确率分类任务看混淆矩阵重点看“次鲜”和“腐烂”之间的误判。这两个档位误判的代价完全不同把腐烂判成次鲜等于放行了一颗坏果把次鲜判成腐烂只是多扔了一颗还能用的果子。检测任务看mAP0.5和mAP0.5:0.95但更要看每个类别的AP。如果“腐烂”类的AP远低于其他类说明数据集中腐烂样本太少或标注质量差。我一般会单独统计“腐烂被漏检”的比例这个指标比整体mAP更能反映产线风险。5. 水果新鲜程度检测数据集避坑五个让模型翻车的真实原因5.1 现象验证集准确率95%产线上线后误判率超过30%原因训练集和验证集按随机划分同一批次的相似图片同时出现在两边。模型学到的是“批次特征”而不是“腐败特征”。产线上的新批次果子外观和训练集里的批次有细微差异模型直接懵了。解决按批次划分数据集验证集和测试集必须来自训练集没见过的批次。如果批次数量不够至少按采集日期划分并在报告中说明这个局限。5.2 现象模型把“有阴影”的果子全判成腐烂原因数据集中腐烂样本恰好多数在阴影下拍摄模型把“暗”当成了“腐烂”的特征。这是典型的虚假相关。解决采集时确保每个档位在不同光照条件下都有样本。训练时加入随机亮度调整但幅度别太大。更彻底的做法是检查数据集中每个类别的光照分布如果某个类别集中在特定光照下重新采样。5.3 现象标注文件里类别编号从1开始训练时全部报错原因YOLO格式要求类别编号从0开始很多标注工具默认从1开始。这个坑几乎每个新手都会踩一次。解决标注完成后写个脚本检查所有txt文件里的最小类别编号如果是1批量减1。下面这个脚本可以快速修正。import os def fix_class_id(labels_dir): 将类别编号从1-based转为0-based for txt_file in os.listdir(labels_dir): if not txt_file.endswith(.txt): continue path os.path.join(labels_dir, txt_file) with open(path, r) as f: lines f.readlines() new_lines [] for line in lines: parts line.strip().split() if parts: parts[0] str(int(parts[0]) - 1) new_lines.append( .join(parts) \n) with open(path, w) as f: f.writelines(new_lines) print(类别编号已修正为0-based) fix_class_id(./dataset_split/train/labels)5.4 现象训练loss震荡剧烈验证集指标忽高忽低原因学习率设太大或者batch size太小导致梯度噪声大。另一个常见原因是数据集中存在大量标注错误的样本模型在拟合噪声。解决先把学习率降到1e-4试几轮如果loss仍然震荡检查标注。用模型对训练集做推理把预测置信度低但标注为高置信度的样本挑出来人工复核。通常能发现5%到10%的标注错误。5.5 现象模型对“次鲜”和“新鲜”的边界完全分不清原因这两个档位在视觉上差异本来就小如果标注手册没有明确定义边界标注员自己都拿不准模型更学不会。解决回到标注环节把“次鲜”的定义量化。比如“表皮失水面积超过10%但无霉变”判为次鲜“失水面积小于10%”判为新鲜。量化标准写进标注手册重新标注边界样本。如果量化后仍然难以区分考虑合并这两个档位或者引入近红外等额外模态。6. 让水果新鲜程度检测数据集真正落地的两个进阶技巧第一个技巧是主动学习闭环。产线每天产生大量新图片全部标注成本太高。做法是先用当前模型对未标注图片做推理挑出置信度在0.4到0.6之间的“不确定样本”只标注这部分加入训练集重新训练。这个闭环跑上三四轮模型在边界样本上的表现会有明显提升。我一般每轮挑500到1000张标注成本可控效果比随机标注好得多。第二个技巧是时序信息融合。单帧图片判断新鲜度有天然上限但产线上同一个果子会被多台相机从不同角度拍到。把同一果子的多帧预测结果做投票或加权平均能显著降低误判。具体做法是给每个果子分配一个追踪ID把连续5帧的预测概率做滑动平均再判档位。这个后处理逻辑不复杂但产线实测能把误判率再压下去三到五个百分点。最后说个血泪教训别在数据集还没稳定的时候就急着上检测模型。我见过太多团队花两周标了检测框结果发现分类基线都还没跑通标注规范改了三次前面标的全废了。先用分类任务把标签体系和数据质量验证清楚再过渡到检测这条路稳得多。希望帮到你。本文还有配套的精品资源点击获取