简介这份资源是面向零售智能化与计算机视觉方向的超市货架空置缺货检测数据集适用于目标检测模型训练、货架陈列分析及补货预警等场景适合具备一定深度学习基础的研究者与算法工程师使用。数据集共4470张jpg图片每张均配有对应的VOC格式xml标注与YOLO格式txt标注标注类别为Empty-Space与Reduced两类分别对应货架空置与商品减少状态总标注框数达23775个其中Reduced框16205个、Empty-Space框7570个采用labelImg工具按矩形框规则完成标注。压缩包内共2000个文件以1999个xml标注文件和1个说明txt为主整体约251.17MB目录结构便于按图片与标注对应检索。目前已有380人学习下载可为货架缺货识别、商品陈列状态分类等任务提供可直接投入训练的标注数据帮助读者省去从零采集与标注的成本快速搭建检测流程并验证模型效果。1. 货架空置检测4470 张双格式数据集到底能跑出什么结果超市货架缺货这件事靠人巡场永远慢半拍。一个理货员管几十组货架等发现某个 SKU 空了可能已经空了两小时。所以越来越多团队想用目标检测来做自动巡检——摄像头定时拍货架模型判断哪些位置是空的。但真正动手时第一个卡住的地方往往不是模型选型而是数据公开的货架缺货数据集极少自己标又贵又慢。这个标题里的 4470 张、2 类标签、VOCYOLO 双格式恰好是能直接开跑的最小可用规模。它解决的不是“能不能检测”的问题而是“能不能低成本先验证一版”的问题。适合两类人一是想快速搭缺货检测原型的算法工程师二是手里有货架图但不知道怎么组织标注和训练流程的落地开发者。下面按数据检查、格式转换、训练配置、避坑、进阶验证的顺序讲透。2. 拿到数据集先别急着训4470 张的分布与标签质量怎么查2.1 两类标签的实际含义与标注边界标题写的是 2 类标签但“2 类”具体是什么直接决定模型学什么。货架缺货检测里最常见的两类划分是empty_shelf空位/缺货区域和occupied有商品区域也有团队用gap商品间空隙和out_of_stock明确缺货位。不管命名如何核心是模型要区分“正常陈列”和“异常空缺”。这里有个容易翻车的地方标注员对“空位”的理解不一致。一个货架格位里商品被拿走后剩下一指宽的缝算不算缺货如果标注规范没写死4470 张里会混入大量边界模糊的框训练时 loss 震荡、mAP 上不去你还以为是模型问题。我一般会先抽 100 张看框的分布重点看两类框的面积比和位置关系。import os import xml.etree.ElementTree as ET from collections import Counter # 统计 VOC 标注中两类框的数量和面积分布 ann_dir annotations # VOC xml 目录 class_counter Counter() area_buckets {small: 0, medium: 0, large: 0} for xml_file in os.listdir(ann_dir): if not xml_file.endswith(.xml): continue tree ET.parse(os.path.join(ann_dir, xml_file)) root tree.getroot() for obj in root.findall(object): name obj.find(name).text class_counter[name] 1 bbox obj.find(bndbox) w float(bbox.find(xmax).text) - float(bbox.find(xmin).text) h float(bbox.find(ymax).text) - float(bbox.find(ymin).text) area w * h # 按面积粗分桶阈值根据实际分辨率调整 if area 32 * 32: area_buckets[small] 1 elif area 96 * 96: area_buckets[medium] 1 else: area_buckets[large] 1 print(类别分布:, class_counter) print(面积分布:, area_buckets)这段代码做两件事统计每类框的总数判断是否存在严重类别不平衡按面积分桶判断小目标占比。货架缺货检测里空位往往是小目标——一个格位在整张货架图里可能只占几十像素。如果 small 桶占比超过 40%后面训练时 anchor 尺度和输入分辨率都要针对性调否则小空位全漏检。参数说明面积阈值 32×32 和 96×96 是 COCO 的通用划分但货架场景分辨率差异大建议先打印几张图的宽高按实际像素重新设阈值。类别名要和后续 YOLO 的names顺序一致否则训练时标签错位模型学出来的东西完全对不上。2.2 用一条命令确认 VOC 与 YOLO 是否一一对应标题说 VOCYOLO 双格式常见做法是同一批图配两套标注VOC 的 xml 和 YOLO 的 txt。但实际拿到的包经常出现“图对不上标注”“txt 行数和 xml 框数不一致”的情况。别信目录结构用脚本对一遍。# 检查图片、VOC xml、YOLO txt 三者文件名是否对齐 img_dirimages voc_dirannotations yolo_dirlabels # 图片文件名去扩展名排序 ls $img_dir | sed s/\.[^.]*$// | sort /tmp/img_names.txt ls $voc_dir | sed s/\.xml$// | sort /tmp/voc_names.txt ls $yolo_dir | sed s/\.txt$// | sort /tmp/yolo_names.txt echo 图片数: $(wc -l /tmp/img_names.txt) echo VOC数: $(wc -l /tmp/voc_names.txt) echo YOLO数: $(wc -l /tmp/yolo_names.txt) # 找差集 echo --- 图片有但VOC没有 --- comm -23 /tmp/img_names.txt /tmp/voc_names.txt echo --- VOC有但图片没有 --- comm -13 /tmp/img_names.txt /tmp/voc_names.txt逻辑说明comm -23输出只在第一个文件里出现的行comm -13反之。跑完如果差集为空说明文件名层面是对齐的。但文件名对齐不代表框对齐还要抽几张图把 xml 的框画出来和 YOLO txt 还原的框叠一起看。YOLO txt 每行是class_id cx cy w h都是归一化到 0~1 的值还原时乘以图片宽高即可。这一步花十分钟能省掉后面几小时的“为什么 loss 不降”的排查。提示如果发现 YOLO txt 里有class_id超出 0 和 1 的值说明类别索引写错了直接改 txt 比重新导出快。3. 从 VOC 到 YOLO转换脚本与四个边界坑3.1 转换脚本的核心逻辑与参数虽然标题说双格式但很多团队实际只维护一套另一套靠脚本转。VOC 转 YOLO 是最常见的需求核心就三步读 xml 拿宽高和框坐标归一化按类别映射写 txt。import os import xml.etree.ElementTree as ET # 类别映射顺序必须和训练时 names 一致 class_map {empty_shelf: 0, occupied: 1} def voc_to_yolo(xml_path, img_w, img_h, out_path): tree ET.parse(xml_path) root tree.getroot() lines [] for obj in root.findall(object): name obj.find(name).text.strip() if name not in class_map: continue # 跳过未定义类别避免索引错乱 cls_id class_map[name] 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) # 归一化中心点和宽高 cx (xmin xmax) / 2.0 / img_w cy (ymin ymax) / 2.0 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h # 裁剪到 0~1防止越界 cx min(max(cx, 0.0), 1.0) cy min(max(cy, 0.0), 1.0) w min(max(w, 0.0), 1.0) h min(max(h, 0.0), 1.0) lines.append(f{cls_id} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}) with open(out_path, w) as f: f.write(\n.join(lines))参数说明class_map的键必须和 xml 里的name完全一致包括大小写和下划线。归一化保留 6 位小数足够YOLO 训练时不会因为精度丢框。裁剪到 0~1 是防止标注时框超出图片边界导致训练报错。3.2 四个最容易踩的边界坑第一个坑图片宽高从 xml 的size里读但有些 xml 的size和实际图片不一致。解决方法是转换时用 PIL 或 OpenCV 重新读一次图片真实宽高以图片为准。第二个坑框的xmin xmax或ymin ymax。标注工具偶尔会写出这种反框归一化后 w 或 h 为负YOLO 直接报错。转换时加一句判断反框就交换坐标。第三个坑空 xml。有些图没有目标xml 里没有object节点转换后 txt 是空文件。YOLO 训练时空 txt 是合法的负样本但如果你用某些第三方脚本可能会被当成损坏文件跳过。确认你的训练框架支持空标签。第四个坑类别名带空格或特殊字符。比如empty shelf中间有空格class_map里写empty_shelf就对不上所有框被跳过txt 全空。转换后一定抽查几个 txt 的行数和 xml 的object数量对比。# 抽查统计每个 txt 的行数找出异常空文件 for f in labels/*.txt; do lines$(wc -l $f) if [ $lines -eq 0 ]; then echo 空标签: $f fi done | head -20如果空标签数量远少于预期比如 4470 张里只有几十张空图说明大部分空 txt 是转换失败不是真正的负样本。这时候回去查class_map和 xml 的name是否匹配。4. 训练配置输入分辨率、anchor 与类别权重的取舍4.1 输入分辨率怎么定小空位检测的关键参数货架缺货检测的难点是小目标。一张 1920×1080 的货架图一个空位可能只有 40×60 像素。如果训练时把输入缩到 640空位只剩十几像素特征图上一两个格子模型根本学不到。所以输入分辨率不能照搬 COCO 的 640。常见做法是先统计标注框的宽高分布取中位数偏小的值反推需要的最小输入。比如空位框宽度中位数是 50 像素原图宽 1920那么输入至少 960 才能让空位在特征图上有 25 像素。我一般会设 960 或 1280 起步显存不够再降但不要低于 800。# 统计标注框宽高的中位数辅助定输入分辨率 import os import xml.etree.ElementTree as ET import statistics widths, heights [], [] for xml_file in os.listdir(annotations): if not xml_file.endswith(.xml): continue root ET.parse(os.path.join(annotations, xml_file)).getroot() for obj in root.findall(object): bbox obj.find(bndbox) w float(bbox.find(xmax).text) - float(bbox.find(xmin).text) h float(bbox.find(ymax).text) - float(bbox.find(ymin).text) widths.append(w) heights.append(h) print(框宽中位数:, statistics.median(widths)) print(框高中位数:, statistics.median(heights)) print(框宽10分位:, sorted(widths)[len(widths)//10])参数说明看中位数和 10 分位。如果 10 分位宽度只有 20 像素说明有大量极小目标输入分辨率要往 1280 走同时考虑用 P2 层特征更高分辨率特征图来检测小目标。代价是显存和推理时间上升需要权衡。4.2 anchor 尺度调整与类别权重YOLO 系列默认 anchor 是基于 COCO 聚类的对货架空位这种细长或小方块目标不一定合适。如果训练几个 epoch 后 recall 一直低先别怀疑模型用 k-means 在自己的标注框上重新聚一遍 anchor。# 用 k-means 在自家标注框上聚 anchor简化版需安装 numpy import numpy as np from sklearn.cluster import KMeans # 假设 boxes 是 N×2 的数组每行是归一化后的宽高 boxes np.array([(w, h) for w, h in zip(widths, heights)]) k 6 # anchor 数量按模型要求设 kmeans KMeans(n_clustersk, random_state0).fit(boxes) anchors kmeans.cluster_centers_ print(聚类 anchor宽, 高:) for a in anchors: print(f {a[0]:.1f}, {a[1]:.1f})逻辑说明聚出来的 anchor 宽高是像素值写进模型配置时注意有些框架要求归一化有些要求绝对像素看文档。类别权重方面如果empty_shelf和occupied数量比超过 1:5给少的那类加权重或者在 loss 里用 focal loss 缓解。但货架场景里两类通常不会太失衡先跑一版看混淆矩阵再决定。注意改 anchor 后要重新从头训不要在旧权重上微调否则 anchor 和学到的特征不匹配效果反而更差。5. 避坑与排查训练不收敛、漏检、误检的常见原因5.1 现象loss 震荡不降mAP 卡在 0.1 以下原因最常见的是标签格式错。YOLO txt 里混入了 VOC 的绝对坐标或者class_id从 1 开始而不是 0。另一个原因是图片和标签没对齐比如images/里是a.jpglabels/里是a.txt但训练脚本按a.jpg去找a.txt时路径拼错读到的全是空标签。解决先跑一遍第 2.2 节的对齐脚本确认文件名一致。再抽一个 txt手动还原框画到图上看位置对不对。如果框位置整体偏移检查归一化时用的宽高是不是图片真实宽高。5.2 现象小空位大量漏检大空位正常原因输入分辨率太低或者 anchor 尺度偏大。小目标在深层特征图上已经消失模型看不到。解决提高输入分辨率到 960 或 1280在模型里启用更高分辨率的特征层如 YOLOv5 的 P2用第 4.2 节的脚本重新聚 anchor增加小尺度 anchor。如果显存不够用梯度累积模拟大 batch不要降分辨率。5.3 现象把商品间缝隙误检成空位原因标注规范里“空位”和“缝隙”边界不清模型学到了错误的模式。或者训练集里这类负样本太少模型没见过。解决回看标注把明显是缝隙的框改成occupied或删掉。在训练集里补充一些只有缝隙没有空位的图作为负样本。推理时调高置信度阈值比如从 0.25 提到 0.4先保准确率。5.4 现象验证集 mAP 正常但实际货架图效果差原因训练集和实际场景分布不一致。比如训练集都是正面平拍实际摄像头是斜上方俯拍或者训练集光照均匀实际有强反光。解决抽一批实际场景图人工标几十张混进训练集微调。如果没法标至少做数据增强随机透视变换模拟角度变化随机亮度对比度模拟光照。增强参数不要开太大否则小目标变形后更难学。5.5 现象推理速度太慢达不到实时原因输入分辨率高模型大。960 输入的 YOLOv5s 在普通 GPU 上可能只有十几 FPS。解决先确认业务需不需要实时。货架巡检通常是定时拍照几秒一张不需要 30 FPS。如果确实要实时换更小的模型YOLOv5n或者用 TensorRT 加速。降分辨率是最直接的手段但要重新评估小目标漏检。6. 进阶验证用混淆矩阵和 PR 曲线判断这版模型能不能上线训练完看 mAP 只是第一步真正决定能不能上线的是混淆矩阵和 PR 曲线。混淆矩阵告诉你两类之间有没有互相误判PR 曲线告诉你调不同置信度阈值时准确率和召回率怎么变。# 用 sklearn 画混淆矩阵假设已有真实标签和预测结果 from sklearn.metrics import confusion_matrix, classification_report import numpy as np # y_true, y_pred 是 0/1 标签列表按框匹配后得到 y_true [0, 1, 0, 1, 0, 1, 0, 0, 1, 1] y_pred [0, 1, 0, 0, 0, 1, 1, 0, 1, 1] cm confusion_matrix(y_true, y_pred) print(混淆矩阵:) print(cm) print(classification_report(y_true, y_pred, target_names[empty_shelf, occupied]))逻辑说明混淆矩阵对角线是正确分类非对角线是误判。如果empty_shelf被大量判成occupied说明漏检严重要召回优先调低置信度阈值。反过来如果occupied被大量判成empty_shelf说明误检多调高阈值。PR 曲线更直观横轴召回率纵轴准确率。曲线越靠右上越好。实际选阈值时看业务能接受多少误报。货架巡检如果误报太多理货员会烦阈值就设高一点宁可漏报几个如果缺货代价高就设低一点宁可多跑几趟。我自己的习惯是先跑一版 baseline看混淆矩阵里哪类错得多针对性补数据或调 anchor再跑第二版。两版对比 PR 曲线如果曲线整体右移说明改动有效。不要一次改太多参数否则出了问题不知道是哪个引起的。最后说个血泪经验4470 张听起来不少但如果两类分布不均或者小目标占比高实际能用的有效样本可能只有一半。别急着上大模型先用小模型把流程跑通确认数据没问题再换大模型提精度。数据质量永远比模型结构重要。希望帮到你。本文还有配套的精品资源点击获取