☰
超市秤盘水果检测数据集:4592张图14类,VOC+YOLO双格式训练指南
2026/9/27 23:09:41 网站建设 项目流程

简介:超市秤盘水果检测数据集是一份面向目标检测任务的高质量标注数据包,适合需要训练水果识别模型的深度学习开发者、零售场景研究者及目标检测初学者。素材拍摄于真实超市称重场景,共含4592张jpg原始图片及同数量标注,覆盖苹果、香蕉、黑莓、辣椒、葡萄、柠檬、树莓、番茄等14个常见品类,并细分有包装(wb)与无包装(wob)状态,便于模型学习外观差异。压缩包内共2000个文件,其中以XML标注文件为主,另含YOLO格式TXT标注及使用说明文档,整体约515.89MB,目录结构便于直接接入VOC或YOLO训练流程。已有291人学习浏览,适合作为校园实训、算法调参和称重结算场景落地的数据基础支撑。标注格式规范、类别体系明确,可显著减少数据准备时间,让研究者更聚焦于模型结构与性能优化。

1. 超市秤盘水果检测数据集是给谁用的:4592张图、14类水果和一份双格式标注

超市生鲜结算台前,收银员一天要重复几千次“看秤盘、认水果、选单价”的动作。把水果从秤盘上拿起来看一眼再放回去,看起来是肌肉记忆,实际上是非常典型的视觉分类任务——不同品种的苹果、橙子、梨堆在一起,光线来自头顶的荧光灯,秤盘是不锈钢反光面。这套「超市秤盘水果检测数据集VOC+YOLO格式4592张14类别.7z」就是为这个场景准备的:4592张实拍图,覆盖14个常见结算水果类别,同一批标注同时给了VOC和YOLO两套格式,压缩成一个7z包分发。它解决的是训练数据从哪来、标注格式怎么统一的问题,适合正在做智慧秤盘、自助收银、农产品分拣的算法工程师和学生。

这份数据集的价值不只是图片数量。做过检测训练的人都知道,VOC格式方便看、方便改、方便用LabelImg继续标注,YOLO格式直接喂给YOLOv5/YOLOv8不用写转换代码。同一份标注给你两套,省掉的恰恰是最容易翻车的格式转换环节。我下面会把这份数据从解压到训练完成的完整路径拆开讲,包括7z解压、格式检查、数据划分、YOLO训练参数和五六个我在类似数据集上踩过的坑。

2. 先把4592张图从7z里取出来:Linux与Windows的解压姿势和文件完整性检查

2.1 7z是比zip更省空间的选择,但先要把工具链装好

7z压缩率通常比zip高20%到40%,对图片这种重复度不高的二进制文件也有稳定收益。4592张图加标注文件压缩后体积会小不少,这是它用7z分发的原因。但7z在Linux服务器上默认没有解压工具,这是多数人拿到数据后的第一个坎。

Debian/Ubuntu系系统安装p7zip:

sudo apt update && sudo apt install -y p7zip-full 7z x 超市秤盘水果检测数据集VOC+YOLO格式4592张14类别.7z -o/data/fruit_scale/

这条命令里x表示解压并保留目录结构,-o指定输出目录,注意-o后面紧跟路径、中间不能有空格,否则7z会把空格当成路径的一部分。p7zip-full装完后提供7z和7za两个命令,日常用7z就够了。CentOS/RHEL系用yum install p7zip,如果官方源没有,就加EPEL源再装。

Windows上正规做法是装7-Zip,右键压缩包选“提取到当前文件夹”。这里有个容易被忽略的细节:7z包内的文件名如果是中文,Windows解压时偶尔会出现乱码,原因是压缩时用了UTF-8编码而系统默认GBK。乱码不影响文件内容,但会影响你写脚本遍历路径——保险的做法是解压后看一眼目录名,如果乱码,手动重命名成英文路径,后面所有训练脚本都走英文路径,避免Python在Windows下处理中文字符路径时报警告。

解压完成后一定要做文件数核对。标题说4592张图,你解压完数出来的数量必须一致:

find /data/fruit_scale -type f | wc -l find /data/fruit_scale -name "*.jpg" | wc -l

如果图片数量和标注数量对不上,说明压缩包在传输或解压过程中出了问题,或者你拿到的包本身不完整。文件数量核对是后悔药,晚吃不如早吃,等训练到一半发现图片打不开再排查就费劲了。

2.2 解压后先别急着训练:目录结构、图片尺寸、标注文件逐项体检

数据集解压后,先看一眼根目录布局。常见的数据集包会分成VOC和YOLO两个子目录,VOC那边是JPEGImages放图片、Annotations放XML,YOLO这边是images放图片、labels放TXT,外加一个classes.txt或data.yaml记录类别名。你手上这份以解压后的实际结构为准,但体检逻辑是通用的。

第一步检查图片本身能不能打开,不是看后缀名是.jpg就当它是合法图片:

import os from PIL import Image img_dir = "/data/fruit_scale/images" bad_images = [] for f in os.listdir(img_dir): if not f.lower().endswith((".jpg", ".jpeg", ".png")): continue path = os.path.join(img_dir, f) try: img = Image.open(path) img.load() except Exception as e: bad_images.append((path, str(e))) print(f"总图片数: {len([f for f in os.listdir(img_dir) if f.lower().endswith(('.jpg','.jpeg','.png'))])}") print(f"损坏图片数: {len(bad_images)}") for path, err in bad_images[:10]: print(path, err)

这段代码用PIL逐张打开图片并真正加载像素数据。img.load()是关键,只Image.open()不会真正读文件内容,它是惰性的,损坏文件要到load()才报错。如果你发现损坏图片,直接从数据集里剔除,同时删掉对应的XML和TXT标注,不要留半张图配一个空标注。

第二步检查标注文件和图片的对应关系。VOC格式要求每个JPEGImages里的图都有一个同名XML在Annotations里,YOLO格式要求每个images里的图都有一个同名TXT在labels里。写一个对账脚本:

import os img_dir = "/data/fruit_scale/images" label_dir = "/data/fruit_scale/labels" img_names = set() for f in os.listdir(img_dir): if f.lower().endswith((".jpg", ".jpeg", ".png")): img_names.add(os.path.splitext(f)[0]) label_names = set() for f in os.listdir(label_dir): if f.endswith(".txt"): label_names.add(os.path.splitext(f)[0]) print("图片无标注:", len(img_names - label_names)) print("标注无图片:", len(label_names - img_names))

这一步会暴露两类问题:一是解压时丢文件,二是数据包本身就有漏标。如果是后者,建议把没有标注的图片从训练集里剔掉,YOLO训练时如果labels目录里凑不出对应的TXT,训练脚本会跳过或者直接报错,具体表现取决于你用的框架版本。

第三步看类别文件。打开classes.txt或data.yaml,确认14个类别名是你预期的。这个文件后面训练时要用,类别顺序直接决定标注里的class_id对不对,所以现在看清楚,省得训练到一半发现模型把苹果学成了梨。

3. VOC与YOLO两套标注格式:XML和TXT的字段对应关系与转换逻辑

3.1 同一条标注在VOC和YOLO里长什么样

VOC格式本质是XML文件,文件名跟图片名一致,里面用<object>标签描述每一个目标框。一份标准的VOC XML长这样:

<annotation> <folder>JPEGImages</folder> <filename>IMG_0213.jpg</filename> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>apple</name> <difficult>0</difficult> <bndbox> <xmin>312</xmin> <ymin>208</ymin> <xmax>684</xmax> <ymax>521</ymax> </bndbox> </object> </annotation>

YOLO格式是纯文本,每行一条标注,五个字段用空格分隔:

0 0.2594 0.3375 0.1938 0.2898

第一个数字是类别索引,后面四个是归一化坐标:中心点x、中心点y、框宽、框高,全部除以图片宽高缩放到0到1之间。所以同一张IMG_0213.jpg里的那个苹果框,转到YOLO格式就是上面这行数——xmin=312在1920宽的图上归一化后中心点x是(312+684)/2/1920=0.2594,宽度是(684-312)/1920=0.1938。

两者对比可以放进一张表里:

对比项VOC XMLYOLO TXT
坐标形式绝对像素,xmin/ymin/xmax/ymax归一化浮点,cx/cy/w/h
类别标识字符串,如apple整数索引,如0
每文件标注个数多个<object>嵌套每行一个目标
可读性人眼易读,标注工具通用机器直接消费,省IO

VOC格式对人友好,你拿LabelImg打开XML能直接看到框叠在图上;YOLO格式对训练框架友好,Dataloader读TXT时零解析成本。数据集同时提供两套,本质上就是帮你省掉了那一步转换。

3.2 格式转换最容易出错的两个点:归一化坐标和类别索引

如果这份数据集只给了VOC格式,你自己转YOLO时最容易在两点上翻车。第一是归一化时把宽高写反。cx = (xmin + xmax) / 2 / width这个公式不难,但有人会顺手写成(xmax - xmin) / 2,那是半径不是中心点,结果就是框全部偏到左上角。第二是类别索引跟类别名的映射对不上。VOC里用的是<name>apple</name>字符串,YOLO里用的是整数,这个整数必须在classes.txt里的行号一一对应,一旦你把data.yaml里的类别顺序调换了,之前标好的索引全部作废。

一个把VOC转成YOLO格式的Python脚本,按数据集的Annotations目录批量处理:

import os import xml.etree.ElementTree as ET voc_dir = "/data/fruit_scale/VOC/Annotations" yolo_dir = "/data/fruit_scale/VOC/labels_converted" class_names = ["apple", "banana", "orange", "pear", "kiwi", "grape", "strawberry", "peach", "mango", "watermelon", "lemon", "plum", "tomato", "cantaloupe"] os.makedirs(yolo_dir, exist_ok=True) class_to_id = {name: idx for idx, name in enumerate(class_names)} for xml_file in os.listdir(voc_dir): if not xml_file.endswith(".xml"): continue tree = ET.parse(os.path.join(voc_dir, xml_file)) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) img_name = root.find("filename").text out_lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_to_id: print(f"警告: {xml_file} 里出现未定义类别 {name}") continue box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) cx = ((xmin + xmax) / 2) / img_w cy = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h out_lines.append(f"{class_to_id[name]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") txt_name = os.path.splitext(xml_file)[0] + ".txt" with open(os.path.join(yolo_dir, txt_name), "w") as f: f.write("\n".join(out_lines) + "\n") print(f"转换完成,输出到 {yolo_dir}")

脚本的要点在于用class_to_id字典统一管理类别名到索引的映射。skip掉未定义类别是一个安全策略,宁可日志里打警告,也不要让错误类别名偷偷变成一个错的索引写进训练数据。

坐标归一化之后保留6位小数。训练框架读的时候会转成float32,6位小数在1920x1080的图上对应大约0.002像素的误差,完全够用。如果你在别的数据集上看到有人用10位小数,那纯粹是强迫症,对训练结果没有任何影响。

这里还要提一个VOC格式特有的字段:<difficult>。它在VOC XML里用来标记那些“连人都很难看清”的目标,比如被挡了大半的水果。很多转换脚本直接忽略这个字段,把difficult目标也转成一个普通标注。如果这个数据集里的XML存在difficult=1的目标,建议转换时把它们过滤掉,因为它们会给训练带来大量噪声——模型学到的不是水果特征,而是“半遮半掩的轮廓也算目标”。

4. 用YOLOv8训练这份数据集:数据划分、YAML配置与训练命令

4.1 数据划分脚本:保证验证集每类都有样本

训练检测模型前要把数据划分成训练集和验证集,一般按8:2或9:1。简单的random.shuffle然后按比例切,是最常见的做法,但对14个类别的数据来说有个隐患:某个冷门水果在整体数据里占比小,随机划分后可能全部落进训练集,验证集里一个都没有,验证mAP对那个类别就是空的——你根本不知道模型到底学没学会。

一个按类别分层划分的脚本:

import os import random from collections import defaultdict images_dir = "/data/fruit_scale/images" labels_dir = "/data/fruit_scale/labels" train_dir = "/data/fruit_scale/split/train" val_dir = "/data/fruit_scale/split/val" os.makedirs(train_dir, exist_ok=True) os.makedirs(val_dir, exist_ok=True) img_names = [f for f in os.listdir(images_dir) if f.endswith(".jpg")] label_to_imgs = defaultdict(list) for img_name in img_names: stem = os.path.splitext(img_name)[0] label_path = os.path.join(labels_dir, stem + ".txt") if not os.path.exists(label_path): continue with open(label_path) as f: first_line = f.readline().strip() if first_line: class_id = first_line.split()[0] label_to_imgs[class_id].append(stem) train_set = set() val_set = set() for class_id, stems in label_to_imgs.items(): random.seed(42) random.shuffle(stems) val_count = max(1, int(len(stems) * 0.2)) val_set.update(stems[:val_count]) train_set.update(stems[val_count:]) print("训练集:", len(train_set), "验证集:", len(val_set))

这里用defaultdict把每个类别对应的图片先收集起来,然后对每个类别单独按20%切分,最后合并。这样能保证验证集里每个类至少有一条样本,冷门类不会被随机性吃掉。random.seed(42)固定随机种子,让每次划分结果一致——这对复现实验结果很重要,不然你换一次划分训练出的模型就不一样了,后面想定位是数据问题还是模型问题都难。

4.2 训练用的YAML和命令行参数:batch、epoch、imgsz怎么定

数据划分好之后,YOLOv8需要一个YAML文件告诉它数据在哪、类别有几类。文件放在数据集的根目录下:

path: /data/fruit_scale train: split/train val: split/val names: 0: apple 1: banana 2: orange 3: pear 4: kiwi 5: grape 6: strawberry 7: peach 8: mango 9: watermelon 10: lemon 11: plum 12: tomato 13: cantaloupe

这里有两个强制约束:train和val路径必须是图片目录的路径,里面放.jpg文件,YOLO会自动去同级找labels目录里对应的TXT;names字典里的类别索引必须和标注文件里每行的第一个数字保持一致。如果写成names: {apple: 0}这种反向映射,训练不会报错,但类别全乱了。

训练命令:

cd /data/fruit_scale yolo detect train \ data=fruit.yaml \ model=yolov8s.pt \ epochs=200 \ batch=16 \ imgsz=640 \ device=0

参数选择有讲究。batch大小的上限取决于GPU显存:一张12G显存的卡跑imgsz=640、yolov8s架构,batch能到16;如果是8G显存,降到8;显存不够又不想降batch,就把imgsz降到480,但注意imgsz会影响小目标的检测效果——秤盘上的葡萄、草莓这类小尺寸水果,imgsz=640是底线,再低小目标就容易漏。

epochs=200不是拍脑袋。4592张图对YOLO这个量级的模型来说数据量偏少,模型通常在150个epoch左右才能收敛,200个epoch是留出余量。训练时盯着box_loss和cls_loss曲线,如果到180个epoch还在震荡,加patience=30让训练自动早停:

yolo detect train data=fruit.yaml model=yolov8s.pt epochs=300 batch=16 imgsz=640 patience=30

patience表示连续30个epoch验证集指标没有提升就提前结束,省时间。

如果loss曲线早期出现一个直上直下的尖峰然后恢复,那通常是batch里混入了空标注图片或者标注框超出图片边界的样本。YOLOv8训练时对这类问题有一定容忍度,但loss会抖。检查一下有没有TXT里的坐标大于1或小于0的,有的话直接删掉对应样本,别让模型花精力去拟合坏数据。

5. 训练超市秤盘水果检测模型常见的坑:7z解压、标注错位与损失异常

5.1 7z解压报“密码正确但文件头错”——不是压缩包坏了

现象:解压4592张这份数据集时,7z提示“密码正确但文件头错误”或者直接报CRC校验失败,卡在某个文件上过不去。很多人第一反应是压缩包传坏了,重新下载一遍,发现还是同一个位置报错。

原因:这类错误在7z包里最常见的原因是压缩包本身用了分卷或者加密头,但解压工具版本太旧,解析不了新版LZMA2的某些特性。第二个常见原因是下载工具把文件截断了——浏览器断点续传偶尔会在文件末尾缺几KB,7z检查文件头时没问题,解压到某个文件时发现数据不完整。

解决:先核对下载文件的字节数跟发布页给的是否一致,不一致就重新下载。字节数一致就升级解压工具:Linux上apt upgrade p7zip-full,Windows上把7-Zip升级到19.0以上版本。还有一个很多人不知道的操作——用7z t命令先测试压缩包完整性,只测试不解压:

7z t 超市秤盘水果检测数据集VOC+YOLO格式4592张14类别.7z

t参数会遍历压缩包内所有文件做CRC校验,几秒钟就能告诉你压缩包本体有没有损坏。这一步是解压前的后悔药,但大多数人是报了错才想起来用。

如果你确实搞丢了压缩包密码,7z没有后门,暴力破解的时间成本取决于密码长度,8位以上纯数字也够跑几天。密码忘记这个坑只能靠养成良好的归档习惯来避免——下载时把密码写进一个README.txt和压缩包放在一起,而不是记在聊天记录里。

5.2 图片能打开但标注对不上:VOC里的difficult和truncated被忽略

现象:训练时loss能降,但验证集的PR曲线在某个类别上明显偏低,画出来看发现有部分正样本的框没有命中预测。打开图片人工检查,发现不少目标被严重遮挡或者只露出一小部分,标注框标了,但人眼都很难认出来。

原因:VOC格式里有两个字段——difficult标记这个目标是不是难以辨认,truncated标记目标是不是被图片边缘截断。数据集的原始采集者可能把这些信息写进了XML,但转YOLO格式时被丢弃了,结果这些高噪声标注混进了正样本集。

解决:转格式时显式跳过difficult和truncated为1的目标。在3.2节的转换脚本里加一个判断:

if obj.find("difficult") is not None and obj.find("difficult").text == "1": continue if obj.find("truncated") is not None and obj.find("truncated").text == "1": continue

这是我在称重水果数据上最深刻的一条血泪经验。秤盘场景本来就容易互相遮挡——葡萄堆在一起、橘子叠罗汉是常态,如果数据集里大量目标都是这种状态,模型会把“遮挡”本身学成特征,换到真实场景一测,单独放一个苹果反而检测不出来。

5.3 训练loss不降或mAP为0:先查类别顺序,再查标注坐标

现象:训练跑了几十个epoch,cls_loss一直在2.0以上下不来,验证集mAP是0,一个目标都检测不出来。日志没有报错,训练流程看起来完全正常。

原因:类别顺序错位是头号嫌疑。如果data.yaml里names的顺序和标注TXT里class_id的顺序不是同一套,模型会把索引0当苹果学,但标注里索引0实际指向香蕉——类别名和框内容对不上,模型学到的映射关系是乱的,loss自然降不下去。第二个常见原因是标注坐标没归一化,或者归一化用的图片尺寸跟实际尺寸不一致。

解决:解压后先抽样看标注。选一张图,打开它对应的TXT,用脚本把框画回图上:

import cv2 img = cv2.imread("/data/fruit_scale/images/IMG_0213.jpg") h, w = img.shape[:2] with open("/data/fruit_scale/labels/IMG_0213.txt") as f: for line in f: parts = line.strip().split() cls_id, cx, cy, bw, bh = map(float, parts) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, str(int(cls_id)), (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imwrite("/tmp/check_bbox.jpg", img)

框如果全部偏移或者框的大小明显不对,就是归一化坐标出了问题。框如果位置都对但类别标号跟data.yaml的names对应不上,调data.yaml的顺序。这两种问题都要在训练前查完,等到训练跑到一半再来查,浪费的不只是GPU时间。

5.4 数据集划分后验证集里某些类一个都没有

现象:训练过程正常,但训练结束后看每个类别的验证结果,发现某个类别没有PR曲线,或者mAP那一栏显示nan。需要专门去翻results.csv才能发现。

原因:和第4.1节说的一样,普通随机划分没有按类别分层,冷门类别数量太少,全部进了训练集,验证集里一条都没有。模型在训练集里见过这个类,但验证集无从评估。

解决:前面4.1节的脚本已经处理了这个问题。如果你已经用随机划分跑完了训练,补救办法是重新划分并重训,没有别的捷径。所以从一开始就用分层划分,别等训练完再后悔。

5.5 混淆矩阵总合不唯一:NMS和IoU阈值影响了大半结果

现象:训练完看混淆矩阵,把每一行的数值加起来,发现不等于该类别的真实样本数。对角线上的数字怎么看都对不上。

原因:混淆矩阵里每一行代表该类别真实目标中,被分到各类别的比例,这个比例跟置信度阈值、NMS的IoU阈值都有关系。阈值不同,同一个目标可能被算作正确检测、错误检测或者漏检,混淆矩阵的数值就会变。

解决:混淆矩阵是用来分析错误模式的,不是用来对账的。看混淆矩阵时关注两类错误:一是对角线占该行比例低,说明这个类本身学得差;二是某个非对角线格子突出,说明这个类跟另一个类经常混淆——比如青苹果和梨在秤盘上确实容易被认错。分析完之后,针对混淆严重的类别,最直接的手段是回去看训练集里这两个类的图片数量是否均衡,数量少的类别考虑数据增强。

还有一个跟BN相关的点:如果训练时用了device=0单卡,batch=16跑得稳;但如果你换成了多卡训练,batch会按卡数倍增,BN的统计量会变,loss曲线可能比单卡更抖。严格说这不是数据集的坑,但很多人会在训练YOLO时遇到BN崩溃——表现为loss突然变成NaN。遇到这种情况先降学习率,从默认的0.01降到0.001,再不行就关掉cache=True,很多时候是数据加载的缓存问题,不是模型问题。

6. 训练完先别信mAP:在真实秤盘场景下做一次鲁棒性验证

训练结束后val指标好看,不意味着模型到了真实超市就能直接用。秤盘场景有三个实验室见不到的特殊干扰:不锈钢秤盘的反光会把水果颜色带偏,塑料保鲜膜的镜面反射会形成假高光,还有一堆水果堆叠导致的密集遮挡。验证模型的办法是拿手机对着家里的水果拍一段视频,或者去超市生鲜区录一段结算秤盘的画面,然后用训练好的权重逐帧推理:

yolo detect predict \ model=runs/detect/train/weights/best.pt \ source=/data/fruit_scale/test_video.mp4 \ conf=0.25 \ iou=0.45 \ save=True

把save=True输出的视频逐帧看一遍,重点关注两个指标:一是漏检率,真实场景里秤盘上有三五个水果,模型只框出来两个,这就是漏;二是误检率,保鲜膜的反光被错认成水果,这是另一个常见翻车点。如果漏检集中在某个特定类别,回去看这个类别的训练样本是不是大多为正对镜头的角度——秤盘上的水果是俯视视角,如果训练集里侧面照太多,俯视推理效果就会差。

conf和iou两个参数值得调一下。conf=0.25是YOLOv8默认值,够用;但秤盘场景里如果目标相互遮挡严重,可以试试把iou从0.45降到0.3,让重叠的框更容易被保留,代价是误检率上升。这两个参数没有固定最优值,我的习惯是先跑一遍默认值,统计漏检和误检数量,然后单独调其中一个参数再跑一遍对比,哪个方向的错误减少了就朝那个方向继续调。模型推理是一个黑匣子,但输入输出之间的参数调节是有迹可循的,别指望一次调到位。

这套从解压、体检、格式理解到训练验证的路径走通之后,你手里的4592张图才真正变成能跑的检测模型。我自己的教训是:不管数据集描述得多么规整,解压后先花半小时做2.2节的体检,再花十分钟抽样画框,永远是性价比最高的操作。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询