资讯动态

打场晒粮检测数据集:VOC转YOLO校验与单类别训练避坑

发布时间:2026/10/25 14:35:33 来源:尧图企业网站定制
简介面向智慧交通与城市道路场景的目标检测实践者这份资源聚焦“打场晒粮”行为识别提供一套可用于模型训练与验证的数据集说明材料。数据集含1065张jpg图像并配套等量Pascal VOC格式xml标注与YOLO格式txt标注标注类别为shailiang共1245个矩形框标注工具为labelImg适合道路异常占用、城市治理巡检等任务的数据准备与算法复现。资源包共1个文件主体为docx文档大小约2.32MB便于查阅数据组织方式、标注规则与类别信息。页面显示已有64人学习或浏览具备一定参考热度。读者可据此了解VOC与YOLO双格式的对应关系、类别与框数分布并开展数据校验、格式转换和检测模型训练前的准备工作需注意数据集不对模型或权重精度作保证使用时应结合自身流程复核标注质量。1. 1065 张图、1245 个框这份打场晒粮数据集能直接干什么道路中间摊着一片金黄车压上去轮胎打滑、刹车距离翻倍——这是县乡道路和城郊结合部常见的场景。这份打场晒粮检测数据集把这类画面整理成了可直接进训练脚本的形态1065 张 jpg 原图配套 1065 个 VOC 格式 xml 与 1065 个 YOLO 格式 txt单类别shailiang标注框总数 1245 个全部由 labelImg 拉矩形框完成。换算一下平均每张图 1.17 个框说明绝大多数画面里只有一块连片的晒粮区域少量图有两处分散摊晒。它适合做智慧交通里的道路占用巡检、路政巡查辅助、车载或无人机视角的异常事件检测也适合拿来做单类别检测的基线实验。不 suitable 的地方也要说清它只有一类没有分割掩码没有现成权重也不带 ImageSets 划分文件指望开箱得到一个能上路的模型会失望。2. 拆包先对齐三件套VOC 与 YOLO 目录的映射关系拿到压缩包别急着解压到训练目录先花十分钟把结构摸清。这份数据集的三种文件是严格一一对应的每张 jpg 有一份同名 xml也有一份同名 txt文件名主干stem完全相同只有扩展名不同。这个「同名不同缀」的约定是所有后续操作的基石一旦某个环节重命名破坏了 stem 对齐后面排查要花几倍时间。2.1 目录树与文件命名约定常见的组织方式是这样解压后先确认层级是否与预期一致dataset/ ├── JPEGImages/ # 1065 张 jpg 原图 ├── Annotations/ # 1065 个 VOC xml └── labels/ # 1065 个 YOLO txt文件类型数量内容关键点jpg1065原始道路图像尺寸以实际读取为准不假设统一分辨率xml1065VOC 标注含 size、object、bndbox坐标是像素绝对值txt1065YOLO 标注每行cls cx cy w h坐标是归一化到 0~1 的相对值有两点需要提前说明。第一数据集说明里写的「不包含分割路径的 txt 文件」实际就是指没有ImageSets/Main/train.txt、val.txt这类划分清单训练集与验证集得自己切。第二xml 里理论上可以有segmented、polygon字段但这份数据集只有矩形框别照着别的 VOC 数据集的解析代码去读分割节点会读出一堆空列表然后怀疑人生。提示解压后先做一次全目录md5sum或文件数统计把「1065」这个数字钉死。后面任何一个环节数字对不上立刻回头查不要带着缺图继续跑训练。2.2 用一段脚本把三件套和坐标范围一次校验干净与其等训练报错不如先做静态体检。下面这段脚本做五件事stem 对齐、空文件检测、xml 结构完整、bbox 越界检查、txt 归一化范围检查。import os import glob from xml.etree import ElementTree as ET ROOT ./dataset IMG_DIR, XML_DIR, TXT_DIR f{ROOT}/JPEGImages, f{ROOT}/Annotations, f{ROOT}/labels CLASSES [shailiang] # 类别白名单顺序即 class id EPS 1e-3 # 归一化坐标容差 def stems(d, ext): return {os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f{d}/*{ext})} img_s, xml_s, txt_s stems(IMG_DIR, .jpg), stems(XML_DIR, .xml), stems(TXT_DIR, .txt) # 1) 三件套对齐任何一边缺失都打印出来 for name, s in [(jpg, img_s - xml_s), (xml, xml_s - img_s), (txt缺失, xml_s - txt_s)]: if s: print(f[{name}] 数量{len(s)} 示例{sorted(s)[:5]}) bad_box, bad_txt, box_total [], [], 0 for st in sorted(xml_s txt_s): # 2) 解析 xml检查 size 与 bndbox 合法性 tree ET.parse(f{XML_DIR}/{st}.xml) root tree.getroot() w int(root.findtext(size/width)); h int(root.findtext(size/height)) objs root.findall(object) for obj in objs: cls obj.findtext(name).strip() if cls not in CLASSES: bad_box.append((st, 未知类别, cls)) x1 float(obj.findtext(bndbox/xmin)); y1 float(obj.findtext(bndbox/ymin)) x2 float(obj.findtext(bndbox/xmax)); y2 float(obj.findtext(bndbox/ymax)) if x2 x1 or y2 y1: bad_box.append((st, 宽或高为0, (x1, y1, x2, y2))) if not (0 x1 and 0 y1 and x2 w and y2 h): bad_box.append((st, 越界, (x1, y1, x2, y2, w, h))) # 3) txt 行数应与 xml 的 object 数一致坐标应落在 0~1 lines [l for l in open(f{TXT_DIR}/{st}.txt).read().splitlines() if l.strip()] if len(lines) ! len(objs): bad_txt.append((st, 行数不符, len(lines), len(objs))) for l in lines: c, cx, cy, bw, bh l.split() c, cx, cy, bw, bh int(c), float(cx), float(cy), float(bw), float(bh) box_total 1 if c len(CLASSES) or not all(-EPS v 1 EPS for v in (cx, cy, bw, bh)): bad_txt.append((st, 类别id或坐标异常, l)) if bw 0 or bh 0: bad_txt.append((st, 归一化宽高非正, l)) print(f图片{len(img_s)} xml{len(xml_s)} txt{len(txt_s)} 框总数{box_total}) print(fbbox异常{len(bad_box)} 示例{bad_box[:5]}) print(ftxt异常{len(bad_txt)} 示例{bad_txt[:5]})逻辑上分三层。第一层只比 stem 集合找出缺图、缺标注、缺 txt这一步能挡住八成「训练到一半报 FileNotFound」的低级事故。第二层钻进 xml检查size/width、size/height是否为正、bndbox是否满足xmax xmin、是否落在图像范围之内——labelImg 在极端情况下会存出负数或超界坐标尤其是标注员在缩放视图里拖框拖到画布外。第三层核对 txtbox_total应该等于题目里给的 1245如果算出来不是这个数说明有 txt 被重写过或存在空行残留。参数上重点是CLASSES和EPS。CLASSES的顺序直接决定 txt 里 class id 的含义必须和训练配置里的names完全一致顺序错一位就是「标签全对但 mAP 归零」的经典翻车。EPS只影响浮点容差给1e-3足够不必调。3. 从 VOC 到 YOLO转换脚本、归一化与越界框裁剪有人会问既然压缩包里已经给了 txt为什么还要自己转一遍血泪经验是数据集附带的 txt 可能是别的流水线生成的类别 id 顺序、是否裁剪越界框、是否丢弃极小框全都不受你控制。你要么信它要么自己从 xml 重新生成一份然后拿两边的结果做 diff。我一般选择后者成本是几分钟收益是后面所有实验都可复现。3.1 一份带裁剪和过滤的转换脚本VOC 的bndbox是(xmin, ymin, xmax, ymax)绝对像素YOLO 要的是(cx, cy, w, h)相对值中心点归一化。中间最容易错的两步一是中心点算成左上角二是除以了错误的宽高比如用了固定 640 而原图是别的尺寸。import os, glob from xml.etree import ElementTree as ET ROOT ./dataset CLASSES [shailiang] CLS2ID {c: i for i, c in enumerate(CLASSES)} MIN_SIDE 2 # 宽或高小于 2px 的框直接丢多为误拖 OUT_DIR f{ROOT}/labels_regenerated os.makedirs(OUT_DIR, exist_okTrue) def voc_to_yolo(x1, y1, x2, y2, w, h): # 先裁剪到图像边界内防止负数导致归一化后越界 x1, x2 max(0.0, x1), min(float(w), x2) y1, y2 max(0.0, y1), min(float(h), y2) bw, bh x2 - x1, y2 - y1 if bw MIN_SIDE or bh MIN_SIDE: return None cx, cy (x1 x2) / 2.0 / w, (y1 y2) / 2.0 / h return cx, cy, bw / w, bh / h stat {write: 0, drop: 0, file: 0} for xml_path in sorted(glob.glob(f{ROOT}/Annotations/*.xml)): stem os.path.splitext(os.path.basename(xml_path))[0] root ET.parse(xml_path).getroot() w float(root.findtext(size/width)); h float(root.findtext(size/height)) rows [] for obj in root.findall(object): cls obj.findtext(name).strip() if cls not in CLS2ID: # 白名单外类别不写避免 id 错位 continue x1 float(obj.findtext(bndbox/xmin)); y1 float(obj.findtext(bndbox/ymin)) x2 float(obj.findtext(bndbox/xmax)); y2 float(obj.findtext(bndbox/ymax)) res voc_to_yolo(x1, y1, x2, y2, w, h) if res is None: stat[drop] 1 continue cx, cy, bw, bh res rows.append(f{CLS2ID[cls]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) stat[write] 1 # 即使没有框也写空文件保留“有图无目标”的负样本信息 with open(f{OUT_DIR}/{stem}.txt, w) as f: f.write(\n.join(rows)) stat[file] 1 print(stat)脚本的核心在voc_to_yolo这个函数。裁剪放在归一化之前是因为标注工具存出的xmin偶尔是负数在缩放视图里拖框越过左边界不裁就会算出小于 0 的cx某些训练框架会静默丢掉这一行某些则直接断言失败。MIN_SIDE设成 2 像素是为了过滤那种肉眼看不见的误拖小框——这种框进训练集只会污染正样本分布。统计输出的write应该接近 1245drop理想情况是 0如果drop有几十个说明原始 xml 里确实存在大量退化框值得回头抽几张图看看标注质量。file必须等于 xml 总数 1065它验证的是遍历没漏。3.2 转换后必须做的一步新旧 txt 对比既然原包自带 txt就拿它和新生成的做一次集合对比。下面这段不追求逐位相等而是统计「框级别」的差异方便判断原 txt 是不是可信。import os, glob def load(txt_dir, stem): p f{txt_dir}/{stem}.txt if not os.path.exists(p): return [] out [] for l in open(p).read().splitlines(): if l.strip(): c, cx, cy, w, h l.split() out.append((int(c), round(float(cx), 3), round(float(cy), 3), round(float(w), 3), round(float(h), 3))) return sorted(out) diff_cnt, stems 0, [os.path.splitext(os.path.basename(p))[0] for p in glob.glob(./dataset/Annotations/*.xml)] for st in stems: a, b load(./dataset/labels, st), load(./dataset/labels_regenerated, st) if a ! b: diff_cnt 1 if diff_cnt 5: print(st, \n 原始:, a, \n 新生:, b) print(f存在差异的图片数{diff_cnt}/{len(stems)})差异通常来自三处小数位截断原始 txt 保留 6 位如果你 round 到 3 位会出现大量假差异所以对比时两边精度要统一、越界框被裁剪、极小框被过滤。如果差异图片数在个位数以内基本可以认为原始标注一致直接用自生成版本如果差异过半说明原 txt 的生成逻辑和你想的不一样必须以 xml 为准。注意不要用「训练能跑起来」当作标注正确的证据。YOLO 系列对越界坐标是宽容的越界框会被裁剪进图内loss 照样下降模型只是悄悄学到了偏移的框。这种问题只有静态检查能提前发现。4. 训练配置单类别 1245 个框怎么切分与调参1065 张图、1245 个框、1 个类别这三个数字决定了训练策略数据量偏小、类别单一、框密度低。它不属于「大数据集跑大模型」的场景反而更容易在验证集上出现过拟合与指标虚高。所以切分方式和增强强度比模型选型更值得花时间。4.1 划分数据集并生成 data.yaml随机切 8:1:1 是最省事的做法但在这个场景下有个隐患同一段路、同一时刻的连续帧如果被分到训练和验证两边验证集几乎是训练集的复制品mAP 会高得离谱。稳妥做法是先按图片的视觉相似度聚一下简单点用文件名的采集批次前缀再在组内按比例抽样。import os, glob, random, shutil from collections import defaultdict random.seed(42) # 固定种子保证可复现 ROOT, DST ./dataset, ./split TRAIN_R, VAL_R 0.8, 0.1 # 剩余 0.1 作测试集 stems [os.path.splitext(os.path.basename(p))[0] for p in glob.glob(f{ROOT}/JPEGImages/*.jpg)] # 按文件名前缀分组模拟“同批次/同路段”的近似重复约束 groups defaultdict(list) for s in stems: groups[s.split(_)[0]].append(s) for sub in [images/train, images/val, images/test, labels/train, labels/val, labels/test]: os.makedirs(f{DST}/{sub}, exist_okTrue) for g, items in groups.items(): random.shuffle(items) n len(items); n_tr, n_va int(n * TRAIN_R), int(n * VAL_R) for i, s in enumerate(items): part train if i n_tr else (val if i n_tr n_va else test) shutil.copy(f{ROOT}/JPEGImages/{s}.jpg, f{DST}/images/{part}/{s}.jpg) shutil.copy(f{ROOT}/labels_regenerated/{s}.txt, f{DST}/labels/{part}/{s}.txt) tot len(stems) print(ftrain{int(tot*TRAIN_R)} val≈{int(tot*VAL_R)} test≈{tot-int(tot*TRAIN_R)-int(tot*VAL_R)})划分脚本的关键参数只有两个seed和TRAIN_R/VAL_R。种子固定是为了让别人复现你的结果也让自己的两次实验可比。比例上1065 张按 8:1:1 大约是 852/106/107验证集只有一百来张、按 1.17 框/图算也就一百二三十个框指标波动会很大所以后面看指标时不能只看单次结果。对应的data.yamlpath: ./split train: images/train val: images/val test: images/test names: 0: shailiang nc: 1names里0: shailiang的 id 必须和 txt 第一列完全一致。拼写也别马虎shailiang不是英文单词是最容易被打成shailiangg、shai_liang、Shailiang的地方而框架不做大小写归一。4.2 训练命令与超参选择以常见的 YOLO 系列检测框架为例命令结构如下不同小版本参数名略有差异按自己装的版本--help对齐yolo detect train \ data./split/data.yaml \ modelyolov8s.pt \ imgsz640 \ epochs120 \ batch16 \ lr00.01 \ lrf0.01 \ mosaic1.0 \ mixup0.1 \ degrees5.0 \ translate0.1 \ scale0.5 \ hsv_h0.015 hsv_s0.7 hsv_v0.4 \ patience30 \ seed42 \ project./runs nameshailiang_baseline逐个说为什么这么设。imgsz640是通用起点晒粮区域多为连片大面不像小目标那样必须上 1024但如果你的业务里目标在画面里只占很小一块比如高点位卡口远景把imgsz提到 960 再试收益往往比换模型大。epochs120配patience30是因为 852 张训练图在十几轮后就开始记忆早停比硬跑 300 轮更划算。scale0.5、translate0.1、degrees5属于中等强度几何增强晒粮区域的形状不规则轻微旋转缩放不会破坏语义。hsv_v0.4这个值值得单独强调晒粮的颜色是黄褐色和沥青路面、泥土路肩、水面反光在某些光照下非常接近亮度和饱和度扰动是让模型别死记颜色、去学纹理和上下文的关键。mosaic1.0在小数据集上能显著扩充样本组合但也容易把本来就不大的目标拼得更小如果发现验证集召回上不去先把它降到 0.5 试试这是常见的调节点。4.3 单类别场景该盯哪几个指标训练日志里指标一堆实际有用的就几个指标含义这份数据集上的关注点mAP50IoU0.5 时的平均精度主要看它但验证集只有百余框波动±3 个点属正常mAP50-95IoU 0.5~0.95 平均反映框贴合度晒粮边界模糊这个值通常明显低于 mAP50Precision预测为正里有多少真误检多水渍、阴影时它先掉Recall真目标里检出多少漏检多粮食与路面颜色接近时它先掉混淆矩阵背景列背景被误判为目标的比例单类别最该看的一格直接对应误检单类别还有个特殊性类别间混淆不存在所有误差都集中在「目标 vs 背景」这一条边界上。所以看到 mAP 不动别急着加数据先看混淆矩阵里背景那一列——如果大量背景被预测成shailiang问题在负样本不足不是正样本不够。提示验证集小的时候单次训练结果不足以支撑结论。至少换两个随机种子各跑一遍或者做 5 折交叉验证对 1065 张的规模来说成本完全可控。5. 避坑与排查五条踩过的坑现象、原因、解决5.1 数据与标注层面的三个坑坑一训练 loss 正常下降但 mAP 长期贴着 0。现象是分类损失和框回归损失都在降验证集指标却纹丝不动甚至预测框位置看起来还算合理。原因八成在类别 id 与names的顺序对不上或者names里的键写成了字符串shailiang而 txt 第一列是0。框架在做标签匹配时找不到对应类别把全部预测当负样本处理。解决方式是打印一批 txt 的第一列取值分布与data.yaml的names键逐一对照一条awk {print $1} labels/train/*.txt | sort | uniq -c就能看清。坑二框整体偏移全部挤在图像左上角或某个角落。这是最经典的一类。现象是可视化预测结果时框大小尚可位置系统性偏左上。原因通常是把 VOC 的绝对像素坐标直接当成了 YOLO 格式写进 txt或者归一化时除的是固定尺寸比如 640而不是原图实际宽高。解决方式是用第 2 章的校验脚本把归一化范围卡到 0~1 并交叉验证随机抽 5 张图把 txt 反算回像素坐标与 xml 的bndbox逐一对齐误差应当在像素级。坑三部分图明明摊着粮食但 txt 是空的。空 txt 在 YOLO 里等价于「这张图全是背景」模型会从中学到「这种路面不是目标」。如果空文件是标注遗漏造成的等于往训练集里灌了一批错误负样本。原因可能是标注员分批作业时漏标也可能是原始 txt 生成时把极小框全过滤了却没同步说明。解决方式是先统计空 txt 数量再人工抽查对应图片确实是空背景纯路面、无摊晒就保留是漏标就补实在拿不准就把这批图从训练集剔除。1065 张的规模人工过一遍一百来张空标注完全做得完。5.2 训练与推理层面的两个坑坑四验证集 mAP 高得反常上线后一塌糊涂。现象是训练日志里 mAP50 早早冲到 0.9 以上换一段新路的视频一跑误检漏检一堆。原因是划分时按帧随机切同一段连续视频的近重复帧同时进了训练和验证。解决方式是划分前先按采集批次或文件名前缀分组组为最小单位整体分配第 4 章的划分脚本里groups那段就是干这个的。代价是验证集比例可能略有浮动换来的是指标可信。坑五推理时把水渍、阴影、路面修补识别成晒粮。现象是模型在验证集上表现正常实际部署时误检集中在潮湿路面、树影、新铺的浅色沥青这几类画面。原因是这些区域在颜色和纹理上与摊晒的粮食高度相似而训练集里这类负样本占比不足。解决分三步先把误检图收集起来作为纯背景图配空 txt加入训练集再提高hsv_v与hsv_s的扰动幅度削弱颜色捷径最后调推理置信度阈值从 0.25 起逐步往上扫画出误检率与召回率的权衡曲线按业务能接受的漏检水平定阈值而不是拍脑袋用默认值。注意这五条里有三条属于「数据静态检查能提前发现」的类型。先跑检查脚本再开训练能省掉大量对着 loss 曲线猜原因的时间。6. 进阶把 1245 个框榨干的难例回流流程单类别、千余张图的数据集最容易陷入的循环是「指标卡住 → 加数据 → 还是卡住」。真正有效的做法不是加量而是把现有数据里被浪费掉的信息挖出来。我常用的一套流程是用基线模型对全量 1065 张包括训练集本身跑一遍推理把误检和漏检挑出来分两路回流——误检图当纯背景负样本漏检图补标后当正样本。# 1) 全量推理输出带框的可视化图和逐图统计 yolo detect predict \ model./runs/shailiang_baseline/weights/best.pt \ source./dataset/JPEGImages \ imgsz640 conf0.15 iou0.5 \ save_txtTrue save_confTrue \ project./runs nameinfer_allconf0.15是刻意压低到业务阈值以下的目的是把「模型犹豫但可能对」的框也捞出来宁可多看图不要漏掉难例。save_txtTrue会把每张图的预测框按 YOLO 格式存下来路径通常在runs/infer_all/labels/这就给了后面做程序化对比的基础。import glob, os def read(p): if not os.path.exists(p): return [] return [tuple(map(float, l.split()[:5])) for l in open(p).read().splitlines() if l.strip()] def iou(a, b): ax1, ay1, ax2, ay2 a[1]-a[3]/2, a[2]-a[4]/2, a[1]a[3]/2, a[2]a[4]/2 bx1, by1, bx2, by2 b[1]-b[3]/2, b[2]-b[4]/2, b[1]b[3]/2, b[2]b[4]/2 ix max(0, min(ax2, bx2) - max(ax1, bx1)); iy max(0, min(ay2, by2) - max(ay1, by1)) inter ix * iy union a[3]*a[4] b[3]*b[4] - inter return inter / union if union 0 else 0.0 fp_list, fn_list [], [] for p in glob.glob(./dataset/labels_regenerated/*.txt): st os.path.splitext(os.path.basename(p))[0] gt read(p); pred read(f./runs/infer_all/labels/{st}.txt) matched_gt set() for pr in pred: hit [i for i, g in enumerate(gt) if iou(pr, g) 0.5] if hit: matched_gt.update(hit) else: fp_list.append(st); break # 有任意误检即记入 FP 清单 if len(matched_gt) len(gt): fn_list.append(st) # 有漏检即记入 FN 清单 print(f误检图 {len(set(fp_list))} 张漏检图 {len(set(fn_list))} 张) open(fp.txt, w).write(\n.join(sorted(set(fp_list)))) open(fn.txt, w).write(\n.join(sorted(set(fn_list))))这段代码做的事很朴素逐图把预测框和真值框按 IoU 0.5 做贪心匹配匹配不上的预测算误检没被匹配的真值算漏检。iou函数里框的表示是(cls, cx, cy, w, h)中心点加宽高的换算要注意别把w/h当成角点。回流时有个度要把握误检图加入负样本后如果数量超过总图数的 10%正负样本比例会明显失衡模型开始倾向于「什么都不预测」。我的经验是负样本控制在 5%~8%且优先挑选那些「模型高置信度误检」的图也就是save_confTrue存下来的置信度在 0.5 以上的那些它们才是真正的难例。补标漏检图同样要克制只补那些肉眼确认是摊晒却被漏掉的不要把模糊地带硬标进去。最后说个习惯。从那以后我每次拿到这种双格式数据集不管描述写得多清楚都强制走一遍三件套对齐、坐标范围检查、新旧 txt 对比这三步哪怕多花半小时。数据集这东西没有后悔药标签错了训练十轮也看不出来等上线才发现就晚了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑