简介面向超市商品缺货检测的目标检测数据集据资源描述包含4470张货架场景图片标注类别为“空置 / 缺货”两类分别对应Empty-Space与Reduced适合零售补货、货架健康度分析等场景下的模型训练。数据同时提供Pascal VOC与YOLO两种标注格式每个样本有对应xml与txt文件便于直接导入YOLO系列训练流程。资源包共2000个文件以xml标注文件为主并附说明txt压缩后大小约251MB全部标注框合计23775个其中空置类7570个、缺货类16205个边界框由labelImg工具绘制标注规则统一。已有380人浏览学习可快速获得成套的VOC/YOLO格式数据与标注清单免去自行采集标注的繁琐过程同时按类别统计了矩形框数量便于评估样本均衡性与训练侧重。数据集源自真实货架场景空置与部分缺货两种状态贴近实际业务可用于训练缺货检测、补货提醒与货架健康评估等任务为零售自动化巡检提供可靠的标注参考。1. 超市货架空置缺货检测数据集4470张图、2类标签把“补货”变成可量化的检测任务超市货架空置缺货检测数据集4470张真实货架图、2类目标标签、VOC与YOLO双格式打包是我在智能零售补货项目里最想要的那种“开箱即用”资源。货架缺货检测的难点不在算法多复杂而在于“空置”是一个空间概念一整层货架被拿空和某条货道露出背板需要的是不同的补货动作。这套数据把目标标注成框让模型能告诉你是哪个区域空了而不是一句“该补货了”。适合正在做超市视觉巡检、门店智能化改造的一线工程师也适合想用真实业务数据跑通目标检测全流程的初学者。先看懂它的标签结构再进训练能省下大量标数据的预算。2. VOC与YOLO双格式拆解先搞清楚4470张图里到底标了什么2.1 目录结构与两类标签的命名规律解压后先别急着把图片一股脑塞进训练脚本。这种VOCYOLO双格式数据集通常包含三块内容JPEGImages或images存放原图Annotations或VOC/Annotations存放XML标签labels存放YOLO格式的TXT标签。有些压缩包还会带train.txt、val.txt这类官方划分文件但我不建议直接信它后面会讲为什么。2类标签在货架空置场景里按这类数据的普遍命名习惯通常是“空置/缺货区域”和“正常货架区域”类别名可能是empty与normal也可能是missing与full甚至直接是编号0和1。拿到压缩包第一步不是训练而是把两类标签的类别名和框数量统计出来确认与你业务定义一致。两种格式的差异如下表对比项VOC格式XMLYOLO格式TXT文件位置Annotations或VOC/Annotationslabels核心标签object/name、bndbox每行class_id x_center y_center width height坐标形式像素绝对坐标xmin/xmax/ymin/ymax相对图片宽高的归一化坐标可读性高适合人工检查低适合直接训练类别编号字符串名称如“empty”整数从0开始从表里能看出来VOC格式相当于“给人看的”YOLO格式是“给模型吃的”。很多数据集同时给两种是因为历史工具链里有的只认XML而现在的YOLOv8训练流程只认TXT。如果你手里只有VOC也要能自己转后面3.2和避坑章会讲到转换时最容易踩的雷。2.2 写脚本校验标签训练前先给数据做个体检我拿到任何目标检测数据集第一件事不是训练而是写个脚本扫一遍标签。原因很实际标注工具导出的XML和TXT反复转格式时经常出现坐标越界、框宽高为负、类别编号对不上等脏数据。这些问题不会在训练启动时报错而是让模型在不知道的情况下学坏等到mAP上不去才回头查非常浪费时间。下面这段脚本用来扫VOC的XML统计类别分布和非法框import os import xml.etree.ElementTree as ET voc_dir Annotations def scan_voc(xml_dir): cls_counter {} bad_box 0 total_box 0 total_file 0 for fname in os.listdir(xml_dir): if not fname.endswith(.xml): continue total_file 1 tree ET.parse(os.path.join(xml_dir, fname)) root tree.getroot() # 读取图片实际尺寸后面检查越界要用 img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) for obj in root.findall(object): cls_name obj.find(name).text cls_counter[cls_name] cls_counter.get(cls_name, 0) 1 total_box 1 bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) ymin float(bbox.find(ymin).text) xmax float(bbox.find(xmax).text) ymax float(bbox.find(ymax).text) # 检查坐标关系和越界 if xmax xmin or ymax ymin: bad_box 1 print(f{fname}: invalid bbox {xmin},{ymin},{xmax},{ymax}) if xmin 0 or ymin 0 or xmax img_w or ymax img_h: bad_box 1 print(f{fname}: bbox out of range) return cls_counter, bad_box, total_file, total_box cls_stat, bad, num_file, num_box scan_voc(voc_dir) print(类别统计:, cls_stat) print(非法框数量:, bad, 文件数:, num_file, 总框数:, num_box)这段脚本用xml.etree.ElementTree解析XML读取size节点里的图片宽高再遍历每个object节点。校验逻辑是xmax必须大于xminymax必须大于ymin且四个坐标都在图片范围内。只要出现一条不符说明这个XML要么是标注工具生成的脏数据要么是某张图在导出时尺寸对不上对应的框在转成YOLO格式后会变成负数或大于1的归一化坐标训练时全是垃圾输入。再看YOLO格式的TXT怎么验。YOLO标签每行只有5个数类别id、中心点x、中心点y、宽度w、高度h前两个和宽高都是相对图片宽高的比例值正常范围是0到1。import os label_dir labels def scan_yolo(txt_dir): cls_counter {} out_of_range 0 bad_line 0 total_line 0 for fname in os.listdir(txt_dir): if not fname.endswith(.txt): continue with open(os.path.join(txt_dir, fname)) as fp: for line_idx, line in enumerate(fp): line line.strip() if not line: continue total_line 1 parts line.split() if len(parts) ! 5: bad_line 1 print(f{fname}:{line_idx 1} bad line: {line}) continue try: cid, cx, cy, w, h parts cls_counter[int(cid)] cls_counter.get(int(cid), 0) 1 cx, cy, w, h float(cx), float(cy), float(w), float(h) except ValueError: bad_line 1 print(f{fname}:{line_idx 1} not number: {line}) continue if not (0.0 cx 1.0 and 0.0 cy 1.0): out_of_range 1 print(f{fname}:{line_idx 1} cx/cy out of range) if w 0.0 or h 0.0 or w 1.0 or h 1.0: out_of_range 1 print(f{fname}:{line_idx 1} w/h invalid) return cls_counter, out_of_range, bad_line, total_line cls_stat, oor, bl, tl scan_yolo(label_dir) print(类别统计:, cls_stat, 越界行数:, oor, 坏行:, bl, 总数:, tl)这段脚本会逐行检查TXT统计类别id的出现次数并检查归一化坐标是否越界。这里要特别说明类别id是整数且VOC里的类别名在转换时已经排好序映射成0和1所以扫出来的0和1的框数应该能和XML统计里empty和normal的框数对得上。对不上说明jpg、xml、txt三者有一组文件名没配对这是后面训练时“找不到标签”的常见来源。做完这两步如果统计出来的两类框数量差距在一个数量级以上就意味着类别不平衡之后训练要在数据增强上补课。最后再用OpenCV把框画回原图人工抽查20张确认框贴的是空置区域而不是人、购物车或者地板。一个简单的可视化脚本即可核心是cv2.rectangle画框、cv2.putText写字这里不再展开。视觉检查看起来土但能滤掉脚本发现不了的长尾问题比如框选偏了、该并框的没并框。3. 从4470张图到YOLOv8权重数据划分、目录组织与训练参数3.1 按场景分组划分别让验证集“作弊”数据划分是目标检测最容易翻车的一步。很多人的习惯是随机打乱后按8:1:1分成train/val/test但货架场景数据往往来自一段段连续视频同一层货架在不同帧里的差别只是购物车位置和商品被拿掉了几件。如果同一段视频里的帧同时出现在train和val模型在验证时其实是“见过”这些货架的mAP会虚高得离谱等部署到没见过的新门店立刻现原形。我一般会先按文件名前缀或拍摄批次把图片分组。比如图片名是shelf_012_0001.jpg这种shelf_012就是同一次拍摄的同一组货架就以它作为最小分组单元整组整组地划入train、val或test保证同一货架不会同时出现在两个集合中。import os import random from collections import defaultdict src_img images dst_root split_dataset train_ratio, val_ratio 0.8, 0.1 os.makedirs(dst_root, exist_okTrue) imgs [f for f in os.listdir(src_img) if f.endswith(.jpg)] # 按文件名第一个下划线前的字符串分组 groups defaultdict(list) for f in imgs: prefix f.split(_)[0] groups[prefix].append(f) g_names list(groups.keys()) random.seed(42) random.shuffle(g_names) tr_names g_names[:int(len(g_names) * train_ratio)] va_names g_names[int(len(g_names) * train_ratio):int(len(g_names) * (train_ratio val_ratio))] te_names g_names[int(len(g_names) * (train_ratio val_ratio)):] print(分组数 train/val/test:, len(tr_names), len(va_names), len(te_names))这个脚本按前缀分组然后把组名切到三个集合里。random.seed(42)让每次跑出来的划分一致方便复现train_ratio和val_ratio可以调如果样本少可以改成0.9/0.05/0.05。因为4700多张图不算少我用8:1:1。注意三个集合的图片数量是整数分组切片后不是严格的4470*0.8但组级别的一致性比数量精确更重要。3.2 组织YOLO目录与dataset.yamlYOLOv8训练时不会自动去找原来labels目录里的TXT它要的是train/images、train/labels这种成对目录。所以你需要把图片和标签按划分结果搬过去。我这里直接写一个完整的组织脚本把images里对应图片复制到目标目录同时把labels里同名TXT复制过去import os import shutil # 读取上一步划分好的组名这里简化为按分好的组名直接复制图片和标签 sets {} sets[train] tr_names sets[val] va_names sets[test] te_names for split, names in sets.items(): img_dst os.path.join(dst_root, split, images) lab_dst os.path.join(dst_root, split, labels) os.makedirs(img_dst, exist_okTrue) os.makedirs(lab_dst, exist_okTrue) for g in names: for f in groups[g]: shutil.copy(os.path.join(src_img, f), os.path.join(img_dst, f)) base os.path.splitext(f)[0] .txt lab_src os.path.join(label_dir, base) if os.path.exists(lab_src): shutil.copy(lab_src, os.path.join(lab_dst, base)) else: print(missing label:, base)这段脚本把train/val/test的图片和TXT复制到对应目录。需要特别留意最后一行的missing提示如果某张图片没有对应的TXT说明原始数据集里就有漏标的图。漏标图留在训练集里会被当作背景负样本偶尔几张影响不大但几十张以上会让该类别的召回率明显下降遇到这种情况我建议直接删掉漏标的图片而不是硬留。在你的数据集根目录创建dataset.yamlpath: /home/user/datasets/supermarket_shelf train: train/images val: val/images test: test/images nc: 2 names: 0: empty 1: normal这里path写的是绝对路径最稳。names里0对应empty、1对应normal要和你转换TXT时设定的类别顺序完全一致。如果项目里实际类别名不同例如0是missing、1是full就按实际标签文件里的id顺序改。test可以省略但写上之后训练完可以直接用同一份yaml跑val和test避免再维护一份配置文件。3.3 训练命令与关键参数目录就绪后先用YOLOv8最小的模型把全流程跑通再决定要不要换大模型。只要显卡显存够下面这条命令可以直接跑yolo detect train \ datadataset.yaml \ modelyolov8n.pt \ epochs100 \ imgsz960 \ batch16 \ device0参数说明modelyolov8n.pt是官方预训练权重会基于COCO权重做迁移学习比从零训练收敛快很多epochs100在4470张图上大概需要一到两小时具体看显卡如果loss还在明显下降可以加到150imgsz960是货架场景比较推荐的值因为空置区域在画面里往往占很小比例640下小目标特征很容易丢batch16取决于显存8G卡建议降到8device0表示使用第一张GPU没有GPU就把devicecpu跑但速度会慢很多。训练结束后权重在runs/detect/train/weights/best.pt和last.pt。best.pt是验证集上mAP最高的一个epoch训练和后续推理都用它。训练日志里主要看两个指标box_loss和cls_loss稳定下降mAP50在后期缓慢爬到0.8以上。如果loss一直不降先别怀疑模型回去看2.2的校验结果大概率是标签有问题。4. 评估与可视化用val和test分清“真本事”和“背答案”4.1 先跑官方评估mAP与混淆矩阵怎么看训练完不要直接拿预测图发给业务方。先跑一遍官方评估用数据说话。best.pt在训练集上见过大部分样本必须用val和test两套数据分别验证才能避开“背答案”的假象。yolo detect val \ modelruns/detect/train/weights/best.pt \ datadataset.yaml \ splitval \ conf0.25 \ iou0.45这条命令在val集上跑评估输出precision、recall、mAP50、mAP50-95。conf0.25是置信度阈值低于这个值的预测框会被滤掉iou0.45是NMS的IoU阈值两个框重叠超过45%会合并成一个。用手持拍摄的货架图时NMS阈值不用调太低因为同一个目标不会出现多个高度重叠的框如果推理时发现一个空置区域被重复框成两块把iou降到0.4试试。在货架缺货检测里我更看重recall而不是mAP50-95。原因是业务损失不对称漏报一个空置区域意味着补货员白跑一趟误报一个正常区域补货员走过去确认一下就行。所以评估时如果发现recall低于0.8宁可接受precision稍微下降也要先保recall。评估完看两个文件results.csv是每个类别的precision、recall和APconfusion_matrix.png能看到empty和normal相互误检的情况。货架场景最典型的误检是normal被预测成empty说明模型把“商品颜色和背板相近”的区域当成了空置这是训练数据里正负样本边界不清造成的回到5.2里讲的标注规范去修。4.2 用test集做“开卷考试”区分模型泛化能力val集是训练过程中用来挑best.pt的参考集但如果你在3.1里按分组做了划分val和train之间不会出现同一货架的帧。test集则是完全没参与训练和选模型的数据最能反映模型在新门店货架上的表现。这两个指标放在一起看才有意义。yolo detect val \ modelruns/detect/train/weights/best.pt \ datadataset.yaml \ splittest \ conf0.25 \ iou0.45注意这里只改了splittest其他参数保持一致。对比val和test的mAP50两者接近说明模型泛化正常val高test低说明划分时仍有泄漏或者test里存在训练时没见过的新光照、新角度这时候需要回到3.1重看分组逻辑。如果两个指标都比较低可以打开results.csv逐类看哪个类别的AP在拖后腿。货架数据里最常出现的是empty类AP低因为空置区域形态多变有的是整层空有的只是几盒商品被拿走露了个缝。形态跨度过大时模型很难用一个框统一描述“缺货”这种情况的解法不是加模型复杂度而是回到标注环节把类别定义收紧或者增加更多空置形态的样本。4.3 可视化与置信度阈值把指标翻译成坐标框指标只能告诉你“整体好不好”不能告诉你“框画得准不准”。我习惯在test集上跑一次预测把带框的图存下来按置信度从低到高翻一遍找“翻车点”。yolo predict \ modelruns/detect/train/weights/best.pt \ sourcesplit_dataset/test/images \ saveTrue \ conf0.3 \ imgsz960source指向test集的图片目录saveTrue会生成带预测框的jpg。conf这里我设0.3比评估时的0.25略高因为业务现场摄像头角度更复杂太低的置信度会产生大量无效框。翻图时重点看三类一是空置区域是否被整个框住还是只框了一半二是货架背板的反光有没有被识别成empty三是两个紧挨的商品之间有没有产生无意义的窄框。这些现象在mAP上可能只差零点几个点但直接影响补货工单的准确度。阈值怎么选才不靠玄学我一般会在val集上先跑一次生成F1_curve.png它展示不同置信度下F1值的变化峰值对应的conf就是理论最优值。然后在test集上把conf设成该值附近观察precision和recall的实际变化。注意F1曲线是从val集算出来的最终还是要用test确认一次否则又落回“用验证集调参、拿验证集打分”的自娱自乐。5. 避坑目标检测数据集从转换到落地6条踩坑记录这一章把我在类似数据集上踩过的坑集中列出来每一条都是“现象→原因→解决”的格式给正在跑货架检测的人一个对照表。遇到类似症状时先看现象匹配哪一条能省下不少排查时间。5.1 划分与标注规范坑坑一val集mAP很高一到现场就漏检。现象训练时val mAP50到了0.9部署到另一个门店的新货架图空置区域大量漏检补货员反馈“该断货的没提示”。原因数据划分时把同一段视频里连续帧随机散进了train和val模型在val上“见过”同一组货架等于开卷考试考了原题mAP自然虚高。解决严格执行3.1的按场景分组划分test集里的货架组在训练时绝对不能碰。血泪经验是如果标注数据来源是一个门店后续至少要让标注方提供门店ID或拍摄批次否则泛化能力无从保证。坑二同一张图里的两类框相互打架。现象一张图上empty框和normal框重叠面积很大两个框都在同一个区域被预测出来。原因标注规范里没写清楚“露出背板多少算空置”标注员各标各的同一个位置在不同图里有时标empty有时标normal模型学到的类别边界自相矛盾。解决把标注规范量化我一般这样定义商品被拿走后露出的货架背板宽度超过该货道的30%就算空置区域不足30%不标。别小看这种“标多少算多少”的规则它直接决定模型能不能收敛。5.2 格式转换与标签质量坑坑三VOC转YOLO后坐标全乱训练loss不降。现象用脚本把XML转TXT后训练前几轮box_loss降不下去甚至越训越高画出的预测框位置错乱。原因转换脚本把xmin/xmax/ymin/ymax除以图片宽高时用的是标注工具写入XML的size但某几张图的JPG被压缩过实际像素尺寸和XML里记录的不一致得到大于1的归一化坐标。解决转换脚本不要信XML的size节点直接用cv2.imread读取原图尺寸做除法。同时用2.2的TXT校验脚本再筛一遍确保cx、cy、w、h都在0到1之间。坑四训练日志里报“image not found”或OpenCV读图返回None。现象训练跑到一半一堆图片打不开程序崩溃或对应图片被静默跳过。原因解压路径或文件名里有中文、空格、特殊字符Linux下YOLO的缓存机制遇到中文路径经常出问题。解决把整个数据集放到纯英文路径下文件名也做一次合规化数字、字母、下划线之外统统替换。这步做完能消掉大量和模型本身无关的报错。5.3 训练与推理部署坑坑五正常类框太多把空置类“淹没”了。现象训练结束后normal类的AP很高empty类的AP只有0.4左右预测时模型宁可不报也不报空置。原因两类框数量可能差了5:1甚至更多。YOLO默认按实例数分配正样本多数类会占据训练主导少数类的梯度被稀释。解决先用2.2的统计脚本确认比例。如果差距大于3:1给空置类的框做copy-paste增强把标注框内的空置区域抠出来随机贴到其他货架的背景区域。另外可以调整cls_loss对少数类的权重但copy-paste增强在数据层面更直接。坑六训练用640推理用1280性能反而倒退。现象训练时图省事用imgsz640上线为了保小目标把推理分辨率提到1280结果小目标没变准整图预测多了很多误检框。原因模型在640分辨率下学到的特征尺寸和1280下的目标尺度不一致强行放大输入只会让模型看到太多从没见过的细节。解决训练时直接定死部署分辨率。如果现场摄像头是固定1080P训练imgsz就设960或1280不要为了训练速度牺牲推理时的高分辨率。6. 进阶用检测结果生成补货单一个从坐标到业务动作的小工具评估通过之后模型输出的还是一个坐标框业务方要的是“哪排货架、哪一层、少了什么”。我习惯在模型后面加一层后处理把empty框聚合成补货建议。思路是统计每张图中empty区域的框数和框面积占比超过阈值就输出一条补货提示对连续多帧做平滑避免单帧误检触发无效工单。def empty_frame_score(pred_boxes, pred_ss, frame_area): # pred_boxes: [x1, y1, x2, y2]格式的空置框列表 # pred_ss: 每个框对应的置信度 score 0.0 empty_area 0.0 for box, s in zip(pred_boxes, pred_ss): if s 0.3: continue x1, y1, x2, y2 box empty_area (x2 - x1) * (y2 - y1) score empty_area / frame_area return score这段后处理把置信度0.3以上的空置框面积加起来除以整帧面积得到一个0到1的“空置率”。我一般设0.08作为补货触发阈值超过这个值就认为该货架需要人工确认。这个阈值不是理论最优而是根据单店的日均缺货记录反推出来的既不会让补货员被零星空置频繁打扰也不会把明显断货放过。参数需要按实际门店微调但逻辑足够简单。最后补一个验证习惯从test集里抽10张模型没见过的图让门店店员人工标一次空置区域和模型输出做对比统计框级别的交并比。这比任何mAP指标都更能说服业务方因为它直接对应“模型给我的补货清单准不准”。我吃过亏曾经拿mAP向店长汇报对方觉得数字好看但到了货架前还是半信半疑后来改成抽几张图现场演示才把信任建立起来。从那以后我下任何数据集的第一件事都是先跑通最小训练、再回头做标注体检这套流程帮我避开了很多浪费时间的翻车。希望帮到你。本文还有配套的精品资源点击获取