打场晒粮检测数据集:VOC转YOLO校验与单类别训练避坑
简介:面向智慧交通与城市道路场景的目标检测实践者,这份资源聚焦“打场晒粮”行为识别,提供一套可用于模型训练与验证的数据集说明材料。数据集含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| 文件类型 | 数量 | 内容 | 关键点 |
|---|---|---|---|
| jpg | 1065 | 原始道路图像 | 尺寸以实际读取为准,不假设统一分辨率 |
| xml | 1065 | VOC 标注,含 size、object、bndbox | 坐标是像素绝对值 |
| txt | 1065 | YOLO 标注,每行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(f"bbox异常={len(bad_box)} 示例={bad_box[:5]}") print(f"txt异常={len(bad_txt)} 示例={bad_txt[:5]}")逻辑上分三层。第一层只比 stem 集合,找出缺图、缺标注、缺 txt,这一步能挡住八成「训练到一半报 FileNotFound」的低级事故。第二层钻进 xml,检查size/width、size/height是否为正、bndbox是否满足xmax > xmin、是否落在图像范围之内——labelImg 在极端情况下会存出负数或超界坐标,尤其是标注员在缩放视图里拖框拖到画布外。第三层核对 txt,box_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_ok=True) 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应该接近 1245,drop理想情况是 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_ok=True) 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(f"train={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.yaml:
path: ./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 \ model=yolov8s.pt \ imgsz=640 \ epochs=120 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ mosaic=1.0 \ mixup=0.1 \ degrees=5.0 \ translate=0.1 \ scale=0.5 \ hsv_h=0.015 hsv_s=0.7 hsv_v=0.4 \ patience=30 \ seed=42 \ project=./runs name=shailiang_baseline逐个说为什么这么设。imgsz=640是通用起点,晒粮区域多为连片大面,不像小目标那样必须上 1024;但如果你的业务里目标在画面里只占很小一块(比如高点位卡口远景),把imgsz提到 960 再试,收益往往比换模型大。epochs=120配patience=30,是因为 852 张训练图在十几轮后就开始记忆,早停比硬跑 300 轮更划算。scale=0.5、translate=0.1、degrees=5属于中等强度几何增强,晒粮区域的形状不规则,轻微旋转缩放不会破坏语义。
hsv_v=0.4这个值值得单独强调:晒粮的颜色是黄褐色,和沥青路面、泥土路肩、水面反光在某些光照下非常接近,亮度和饱和度扰动是让模型别死记颜色、去学纹理和上下文的关键。mosaic=1.0在小数据集上能显著扩充样本组合,但也容易把本来就不大的目标拼得更小,如果发现验证集召回上不去,先把它降到 0.5 试试,这是常见的调节点。
4.3 单类别场景该盯哪几个指标
训练日志里指标一堆,实际有用的就几个:
| 指标 | 含义 | 这份数据集上的关注点 |
|---|---|---|
| mAP50 | IoU=0.5 时的平均精度 | 主要看它,但验证集只有百余框,波动±3 个点属正常 |
| mAP50-95 | IoU 0.5~0.95 平均 | 反映框贴合度,晒粮边界模糊,这个值通常明显低于 mAP50 |
| Precision | 预测为正里有多少真 | 误检多(水渍、阴影)时它先掉 |
| 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 \ imgsz=640 conf=0.15 iou=0.5 \ save_txt=True save_conf=True \ project=./runs name=infer_allconf=0.15是刻意压低到业务阈值以下的:目的是把「模型犹豫但可能对」的框也捞出来,宁可多看图,不要漏掉难例。save_txt=True会把每张图的预测框按 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_conf=True存下来的置信度在 0.5 以上的那些,它们才是真正的难例。补标漏检图同样要克制,只补那些肉眼确认是摊晒却被漏掉的,不要把模糊地带硬标进去。
最后说个习惯。从那以后我每次拿到这种双格式数据集,不管描述写得多清楚,都强制走一遍三件套对齐、坐标范围检查、新旧 txt 对比这三步,哪怕多花半小时。数据集这东西没有后悔药,标签错了训练十轮也看不出来,等上线才发现就晚了。希望帮到你。
本文还有配套的精品资源,点击获取