YOLO11车辆检测实战:三种标签格式转换与跨平台一键训练脚本
发布时间:2026/9/30 10:28:03来源:尧图网络
简介车辆检测数据集配套说明文档以PDF形式提供了1000张真实场景车辆图片数据集的完整介绍与获取入口。数据覆盖城市道路、高速道路、农村道路及车辆遮挡/严重遮挡等丰富场景并划分Auto、Bus、Car、LCV、Motorcycle、Multi-Axle、Tractor、Truck共8个类别适合交通道路监控等实际项目中的车辆检测算法训练与数据集补充。文档详细说明了基于labelimg的高质量标注流程同时整理VOC(xml)、COCO(json)、YOLO(txt)三种常见格式标签可直接对接YOLO等主流检测框架随附的YOLO11一键训练脚本还提供GPU、CPU、Mac(M芯片)多平台运行方案并给出博主训练结果日志供参考。整份资源为单个PDF文件大小约7.19MB目前已有1332人学习下载适合需要快速获取车辆检测数据并开展训练实验的开发者。1. 1000 张车辆图真的够训 YOLO11 吗三种标签格式与一键脚本的真相做目标检测的人看到“车辆检测数据集-1000张图-对应VOC/COCO/YOLO三种格式标签支持GPU(GPUs)/CPU/Mac三平台YOLO11一键训练脚本”这个标题时第一反应通常是1000 张图能训出什么我的结论是1000 张车辆图配合预训练权重在场景相对固定的前提下完全够训出一个能用的检测器。真正卡住你的往往不是图少而是标签格式混乱、训练脚本绑死某一台机器、batch 和 epoch 全靠猜。这套资源的价值不在图片数量而在“三种格式标签 三平台脚本”这套交付形态。它解决的是从业者最常遇到的落地问题数据集格式不统一导致训练前疯狂写转换脚本换台机器又跑不起来。适合三类人想做车辆检测但不想从零标注的算法工程师接项目需要快速出 demo 的乙方以及想在本地环境完整跑通 YOLO11 训练流程的学生。接下来我按“数据整理 → 脚本设计 → 参数调优 → 避坑”的顺序把这条链路拆开讲。2. 把 1000 张车辆图整理成 VOC/COCO/YOLO 三格式标签转换脚本与正确性校验拿到数据集先别急着训练。第一步是盘清目录图片放哪、三种标签各自怎么组织、train/val 怎么划分。多数情况下这套数据不会是三份完全独立的标注而是同一批标注导出的三种格式。你要做的是选定一份作为源数据其余两份作为交付产物。2.1 三种标签格式的本质区别先决定维护哪一份源数据格式存储方式边界框表达典型标注工具训练前处理VOC每张图一个 XMLxmin/ymin/xmax/ymax像素LabelImg解析 XML 再归一化COCO一个 JSON 文件x/y/w/h像素labelme、coco-annotator按 image_id 关联YOLO每张图一个 txtclass_id x_center y_center w h归一化LabelImg / LabelU可直接喂 ultralytics我的做法是以 YOLO 格式作为源数据VOC 和 COCO 只在交付时生成。理由有三个YOLO 标签是纯文本diff 对比和脚本处理都方便ultralytics 直接消费这种格式省掉训练前的解析步骤坐标做了归一化不受原图分辨率影响。COCO JSON 是嵌套结构三个格式里维护成本最高每次改类别都要重算 id 映射不适合作为中间工作格式。“三种格式”不等于三个数据源。1000 张图的规模下格式转换脚本几秒就能跑完所以正确姿势是维护一份 YOLO 标签需要 VOC/COCO 时现转。下面两个转换脚本是我最常用的版本。2.2 VOC 转 YOLO坐标归一化与 class 顺序锁死import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image CLASS_NAMES [car, truck, bus] # 固定顺序转换期收集新类只追加不重排 def voc_to_yolo(xml_path: Path, img_path: Path) - str: root ET.parse(xml_path).getroot() img_w, img_h Image.open(img_path).size # 用实际图尺寸别信 XML 里的 size lines [] for obj in root.findall(object): name obj.find(name).text if name not in CLASS_NAMES: CLASS_NAMES.append(name) cls_id CLASS_NAMES.index(name) 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) x_center (xmin xmax) / 2 / img_w y_center (ymin ymax) / 2 / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h if w 1e-6 or h 1e-6: continue # 宽高为 0 的坏框直接丢 lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) return \n.join(lines)逻辑说明VOC 的 bndbox 是绝对像素坐标YOLO 需要的是相对原图的归一化坐标所以 x_center 是 xmin 和 xmax 的中点除以图宽w 是框宽除以图宽。代码里特意用Image.open(img_path).size读取真实尺寸而不是用 XML 里的 size 节点。很多数据集在网上下载后被压缩过XML 里写的 1920x1080 和实际图片尺寸对不上最后训练出来的框会整体偏移。参数说明CLASS_NAMES 的顺序就是训练时 data.yaml 的类别顺序一旦定下就不要重排。转换脚本跑完后建议把CLASS_NAMES导出成classes.txt后面生成 data.yaml 时直接读它避免“JSON 里叫 CarYOLO 文件里叫 car”这种大小写不一致的问题。归一化坐标保留 6 位小数足够再多也不会提升训练精度。2.3 COCO 转 YOLOcategory_id 映射与 crowd 标注处理import json from pathlib import Path def coco_to_yolo(json_path: Path, images_dir: Path, labels_dir: Path): coco json.loads(json_path.read_text()) # COCO 的 category_id 通常从 1 开始要映射成 YOLO 从 0 开始的连续索引 cat_id_to_idx {cat[id]: i for i, cat in enumerate(coco[categories])} img_id_to_file {img[id]: img[file_name] for img in coco[images]} img_id_to_size {img[id]: (img[width], img[height]) for img in coco[images]} anns_by_img {} for ann in coco[annotations]: if ann.get(iscrowd, 0) 1: continue # crowd 标注是一团轮廓车辆检测直接丢掉更稳 anns_by_img.setdefault(ann[image_id], []).append(ann) for image_id, anns in anns_by_img.items(): img_w, img_h img_id_to_size[image_id] lines [] for ann in anns: x, y, bw, bh ann[bbox] # COCO bbox 是 [x, y, width, height] x_center (x bw / 2) / img_w y_center (y bh / 2) / img_h cls_id cat_id_to_idx[ann[category_id]] lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {bw / img_w:.6f} {bh / img_h:.6f}) out_file labels_dir / (Path(img_id_to_file[image_id]).stem .txt) out_file.write_text(\n.join(lines))逻辑说明COCO 的 bbox 字段是[x, y, width, height]格式本身不难真正容易错的是 category_id。COCO JSON 的类别 id 通常从 1 开始而 YOLO 要求类别编号从 0 开始连续排列中间不能有空洞。cat_id_to_idx这个字典就是干这个的它把原始 id 映射成enumerate生成的连续索引。另一个坑是 iscrowd 字段值为 1 的标注表示一群目标混在一起只有轮廓没有单体框直接跳过对车辆检测更安全。参数说明img_id_to_size是为了避免每处理一个标注就读一次图片文件1000 张图不差这点时间但万级数据集就有明显差别。如果 COCO 的 file_name 带着子目录前缀比如train/0001.jpg建议把相对路径存下来而不是只取 stem否则不同子目录下重名的图片会互相覆盖。2.4 训练前的标签体检四类异常与划分一致性转换完成不等于数据没问题。训练前我会跑一遍审计脚本把标签里最常出现的四类异常揪出来图无标签、标签无图、class id 越界、坐标越界。from pathlib import Path def audit_labels(img_dir: Path, label_dir: Path, max_cls: int 3): img_files {p.stem: p for p in img_dir.glob(*.jpg)} label_files {p.stem: p for p in label_dir.glob(*.txt)} orphan_imgs sorted(set(img_files) - set(label_files)) orphan_labels sorted(set(label_files) - set(img_files)) bad_cls, bad_coord [], [] for stem, p in label_files.items(): for line in p.read_text().splitlines(): parts line.split() if len(parts) 5: continue cls_id int(parts[0]) if cls_id 0 or cls_id max_cls: bad_cls.append((str(p), line)) coords list(map(float, parts[1:5])) if any(c -1e-6 or c 1 1e-6 for c in coords): bad_coord.append((str(p), line)) return orphan_imgs, orphan_labels, bad_cls, bad_coord逻辑说明这个脚本返回四类问题清单orphan_imgs是有图没标签orphan_labels是有标签没图bad_cls是类别编号超出 data.yaml 范围bad_coord是归一化坐标不在 0 到 1 之间。注意阈值用的是1e-6而不是 0因为某些标注工具的浮点误差会让边界坐标变成 1.0000001这种框不算真的越界。除这四类之外还有一个必须人工确认的点三份标签的 train/val 划分是否一致。我实际遇到过 VOC 格式的验证集里混着 YOLO 训练集的图片导致验证指标虚高部署到新场景后 mAP 暴跌。统一做法是只维护一份train.txt和一份val.txt三种格式的目录结构都从这一份划分生成而不是各格式自带划分文件。3. 让一键脚本在 GPU/CPU/Mac 上都能跑设备探测、batch 分档与 data.yaml 生成一键脚本不是魔法它只是把三件事封装起来环境探测、数据准备、参数入口。依赖极少pip install ultralytics装好之后脚本里只需要from ultralytics import YOLO。剩下的工作就是让同一份代码在不同平台上自动做出正确选择。3.1 别再写死 deviceCUDA、MPS、CPU 三选一很多训练脚本喜欢写device0这在有 N 卡的机器上没问题换到 Mac 上直接报错。跨平台脚本的第一步就是写一个设备探测函数。import sys import torch def pick_device(prefer: str ) - str: if prefer: return prefer if torch.cuda.is_available() and torch.cuda.device_count() 0: return 0 # 单卡默认多卡用 --device 0,1 手动覆盖 if sys.platform darwin and getattr(torch.backends, mps, None) and torch.backends.mps.is_available(): return mps return cpu逻辑说明torch.cuda.is_available()是基础检查但要加上device_count() 0否则有可能出现驱动层可用、实际没有可用显卡的尴尬情况。Mac 的判断要额外处理某些 torch 版本没有编译 MPS 后端直接调用torch.backends.mps.is_available()会抛 AttributeError所以这里用getattr包了一层。选好设备后batch size 不应该写死。8G 显存卡和 24G 显存卡用同一个 batch 会有一个直接 OOM另一个占不满。我习惯按显存分档。def pick_batch(device: str, total_mem_gb: float) - int: if device in (cpu, mps): return 8 if total_mem_gb 24: return 32 if total_mem_gb 12: return 16 return 8参数说明这是经验值不是精确算法。YOLO11n 本身显存占用不高8G 显存跑 batch 8 比较稳12G 可以上 1624G 上 32。对 1000 张图的规模batch 32 以上收益很小反而可能让 BN 层统计不稳定所以设个上限就够了。3.2 data.yaml 自动生成names 的顺序就是模型输出头的顺序YOLO 训练必须有一个 data.yaml里面声明图片路径、训练集、验证集和类别名。新手最容易在这里翻车手写 names 顺序和标签文件里的 class id 对不上。from pathlib import Path import yaml def collect_class_ids(labels_dir: Path): ids set() for p in labels_dir.glob(*.txt): for line in p.read_text().splitlines(): if line.strip(): ids.add(int(line.split()[0])) return sorted(ids) def build_data_yaml(data_dir: Path, names: list[str]): train_txt train.txt # 用相对路径换机器不用改 val_txt val.txt data { path: str(data_dir.resolve()).replace(\\, /), train: train_txt, val: val_txt, names: {i: name for i, name in enumerate(names)}, } (data_dir / data.yaml).write_text(yaml.safe_dump(data, allow_unicodeTrue))逻辑说明collect_class_ids扫描所有标签文件收集出现过的类别编号。如果返回的列表不是从 0 开始连续递增比如[0, 1, 3]说明标签里有越界 class id需要回到 2.4 的审计脚本排查。build_data_yaml里的 names 字典会直接决定模型输出头的类别顺序所以这里必须复用 2.2 里锁定的 CLASS_NAMES不能重新排序。参数说明path字段用Path.resolve()转成绝对路径再把反斜杠替换成正斜杠这样 Windows 和 Linux 都能解析。train 和 val 用相对路径是刻意的因为path字段已经指到了数据集根目录如果 write 绝对路径换一台机器就要改文件。3.3 最小一键训练脚本从检查数据到启动训练的完整实现把上面的函数组合起来就是一个能直接用的训练入口。import argparse import sys from pathlib import Path import torch from ultralytics import YOLO def main(): ap argparse.ArgumentParser() ap.add_argument(--data, typePath, defaultPath(datasets/Vehicle1000)) ap.add_argument(--epochs, typeint, default150) ap.add_argument(--batch, typeint, default0) # 0 表示自动按显存分档 ap.add_argument(--imgsz, typeint, default640) ap.add_argument(--device, typestr, default) ap.add_argument(--model, typestr, defaultyolo11n.pt) args ap.parse_args() data_yaml args.data / data.yaml if not data_yaml.exists(): sys.exit(f[train] {data_yaml} 不存在请先运行 2.2 和 2.3 的转换脚本) device pick_device(args.device) if args.batch 0: if device 0: mem torch.cuda.get_device_properties(0).total_memory / 1024**3 else: mem 0 args.batch pick_batch(device, mem) print(f[train] device{device}, batch{args.batch}, epochs{args.epochs}) model YOLO(args.model) model.train( datastr(data_yaml), epochsargs.epochs, batchargs.batch, imgszargs.imgsz, devicedevice, projectruns/train, namevehicle1000, pretrainedTrue, cacheTrue, # 内存小于 16G 时改成 False见 5.5 ) if __name__ __main__: main()逻辑说明--batch 0是自动模式的关键设计。用户不指定 batch 时脚本根据探测到的设备类型和显存大小自动分档。sys.exit加在所有数据处理之后、训练启动之前避免数据没准备好就白跑一个 epoch。model.train里的project和name参数让每次训练输出到独立的目录重复跑不会覆盖上一次的 best.pt。参数说明model默认用yolo11n.pt。1000 张图配 nano 模型是稳妥选择模型小、收敛快、不容易过拟合。cacheTrue会把预处理后的图片缓存到内存第一次训练慢一点第二次开始明显提速但内存不足时它会成为 OOM 的元凶。4. 用 YOLO11 把 1000 张车图训到可用验收指标、超参表与最小命令数据整理完、脚本能跑通之后真正决定模型能不能用的是训练参数。这一章不聊玄学调参只给一套我在车辆检测场景下验证过多次的参数组合和判断标准。4.1 先定验收基线mAP50、mAP50-95 和混淆矩阵怎么读车辆检测里最常见的两个指标是 mAP50 和 mAP50-95。mAP50 只要求预测框和真实框的 IoU 超过 0.5 就算正确对“车辆”这种体积较大、边界不需要特别精确的目标这个指标很直观。mAP50-95 则是在 0.5 到 0.95 多个 IoU 阈值下取平均更严格更考验框的回归精度。1000 张训练集 yolo11n 150 epoch 的经验基线光照稳定的道路监控场景mAP50 做到 0.85 以上是常见结果mAP50-95 在 0.55 到 0.7 之间。如果 mAP50 连 0.6 都到不了先怀疑标签质量而不是继续加 epoch。注意同一个数据集连续跑两次mAP50 有 ±0.02 的随机波动属于正常不用为了一点波动反复调参。看混淆矩阵时也有一条常见疑问为什么总和不是 1因为 YOLO 的混淆矩阵按行做了归一化每一行代表一个真实类别的样本被预测到各类的比例行和为 1列不受约束。这不是 bug也不是矩阵算错了别拿它当模型好坏的评判依据。4.2 超参表哪些值得动哪些保持默认参数ultralytics 默认我的建议理由modelyolo11n.ptyolo11n.pt1000 张图用 nano 起步过拟合风险最小epochs100150小数据集要训到过拟合拐点靠早停兜底batch16按显存分档见 3.1batch 过大对 BN 不稳定imgsz640640车辆是大目标不需要 1280lr00.010.01迁移学习下这个值够用lrf0.010.01末段学习率降两个数量级optimizerautoSGD小数据 SGD 比 AdamW 稳健warmup_epochs3.05.01000 张图让学习率爬升更平缓cos_lrFalseTrue余弦退火在后半程更稳patience5030验证指标 30 轮不涨就停cacheFalseTrue内存够时1000 张图缓存后训练提速明显ampTrue平台默认Mac MPS 下建议 False见 5.2这张表的核心思路是能不动的不动要动的都服务于小数据集这个前提。很多教程喜欢贴一长串自定义参数其实对车辆检测这种目标尺度单一的任务默认配置已经够好。我一般只改 epochs、batch、cache 和 cos_lr其余保持 ultralytics 默认。4.3 预训练权重的作用yolo11n.pt 不是给你省时间的是给你省数据的yolo11n.pt 是在 COCO 80 类数据上预训练过的权重。把这套权重迁移到车辆检测任务最大的收益是模型已经学会了边缘、纹理、局部形状这些底层特征你只需要用 1000 张图微调检测头。如果从零训练同样的 epoch 数 mAP50 可能差 10 个点以上。权重下载是自动的第一次运行YOLO(yolo11n.pt)时ultralytics 会把它下载到本地缓存目录。离线环境下手动放权重也简单找一台联网机器先执行一次yolo predict modelyolo11n.pt然后把缓存目录里的文件拷过去放到相同位置即可。注意实现细节预训练模型的检测头是 80 类YOLO11 在训练时会根据 data.yaml 的 names 数量重建检测头所以类别顺序一旦变了预训练头对应的权重就会被丢弃只保留 backbone 的迁移收益。数据增强方面YOLO11 默认开 mosaic、随机仿射、HSV 增强这些对 1000 张图不是坏事。mosaic 能把四张图拼成一张等效扩大训练样本的上下文多样性。只有在小目标占比高的场景才建议关掉 mosaic车辆检测这种目标占据画面较大比例的任务保留默认更合适。4.4 训练中看什么results.csv 比终端进度条有用我不太建议一直盯着终端进度条。更可靠的观察对象是 ultralytics 每次训练自动写的 results.csv里面每个 epoch 的 loss 和验证指标都有记录。import csv from pathlib import Path def tail_results(run_dir: Path, n: int 5): rows list(csv.DictReader((run_dir / results.csv).open())) for row in rows[-n:]: print( fepoch{row[epoch]:3} fbox_loss{float(row[train/box_loss]):.4f} fmAP50{float(row[metrics/mAP50(B)]):.4f} fmAP50-95{float(row[metrics/mAP50-95(B)]):.4f} )逻辑说明results.csv的表头由 ultralytics 自动生成每次训练都会在runs/train/name目录下覆盖写一份。脚本读取最后 n 行打印 box_loss、mAP50 和 mAP50-95。判断标准很简单box_loss 还在降但 mAP50 连续 10 轮不动说明模型在记训练集噪声可以提前停mAP 还在涨但 loss 震荡说明学习率偏大。参数说明Windows 下如果 results.csv 正被 Excel 占用脚本读取会报 PermissionError把文件复制一份再读就行。字段名里的(B)表示框检测不需要在意。4.5 第一次跑通的最小命令python train.py --data datasets/Vehicle1000 --epochs 150 --batch 16 --imgsz 640 --device 0逻辑说明这是最直接的启动方式。--batch 16适合 12G 显存如果你的卡是 8G可以改成--batch 8或者干脆不传 batch让脚本自动分档。第一次跑不要直接上 150 epoch。先用--epochs 3 --batch 8跑一轮验证数据流确认没有“All labels empty”这类报错再启动完整训练。Mac 用户把--device改成mps纯 CPU 笔记本用--device cpu --batch 8 --workers 0。CPU 上一个 epoch 可能要几分钟到十几分钟第一次跑 3 个 epoch 验证流程完全够用。5. 一键训练避坑1000 张车辆图训练中 6 个真实排错现场这一章写的都是实际运行日志里出现过的坑不是玄学。每一条按“现象 → 原因 → 解决”展开你照着排查比自己翻文档快得多。5.1 CUDA out of memory8G / 12G / 24G 显存的 batch 分档现象训练刚启动就报torch.cuda.OutOfMemoryError或者跑到第 5 个 epoch 突然崩。原因大多数情况是 batch 写死 16而显卡只有 8G 显存另一个帮凶是 cacheTrue 把预处理图片缓存进了显存。解决按 3.1 的分档逻辑设置 batch8G 用 812G 用 1624G 用 32。显存够但内存吃紧时把 cache 改成 False。要注意的是 OOM 不一定发生在前向传播反向传播时梯度比激活值更吃显存所以“前面能跑后面崩”很常见。5.2 Mac MPS 上的 NaN loss两个必改参数现象Apple Silicon 的 Mac 上训练第一个 epoch loss 直接变成 nan或者 DataLoader 报 connection reset。原因MPS 后端对混合精度的支持不完整某些算子在 FP16 下会回退到 CPU 产生数值问题workers 大于 0 时 MPS 的多进程数据加载偶发崩溃。解决model.train(ampFalse, workers0)。这两个参数对训练速度的影响不大但在 Mac 上能避免九成问题。另外不要用torch.compileMPS 下的编译支持还很粗糙容易出现“训练能启动但 Loss 不下降”的假死状态。5.3 标签 class id 错位数据没问题但 mAP 是 0现象loss 在正常下降验证集 mAP50 却始终是 0训练结束也没有生成有效模型。原因标签文件里的类别编号和 data.yaml 的 names 顺序不一致。比如标签里 class 0 是卡车data.yaml 的 names[0] 却写着 car模型学到的特征和验证集的类别对不上GT 匹配全部失败。解决先用 2.4 的审计脚本扫描所有标签的 class id确认从 0 开始连续。再打印标签文件前 20 行的第一列和 data.yaml 的 names 逐项核对。类别顺序一旦变了预训练权重和标签都会错位这不是 mAP 高低的问题而是整个训练无效。5.4 Windows 路径分隔符data.yaml 路径解析失败现象Windows 上训练报[Errno 2] No such file or directory但文件确实存在。原因data.yaml 里写的是D:\datasets\Vehicle1000yaml 解析时会处理反斜杠转义\d变成不可见字符路径直接被截断。解决所有路径统一用正斜杠/或者在 build_data_yaml 里用Path.resolve()生成绝对路径后做一次replace(\\, /)。另一个隐蔽场景是训练和推理不在同一台机器训练在 Linux、推理在 Windows这时绝对路径失效建议 data.yaml 里path写根目录train/val 写相对路径。5.5 训练到一半进程被 Killed内存与 swap 不足现象训练跑了 20 个 epoch终端没有任何报错进程直接被 Killed。原因cacheTrue把 1000 张图全部缓存进内存加上模型权重和 DataLoader 的占用物理内存耗尽被 OOM killer 干掉。WSL2 用户更明显WSL2 默认只拿物理内存的一部分还经常不配 swap。解决内存小于 16G 的机器把 cache 设为 False。WSL2 用户在%UserProfile%\.wslconfig里加一段配置[wsl2] memory12GB swap8GB localhostForwardingtrue改完执行wsl --shutdown再重开 WSL 才生效。注意配置的 memory 值不能超过物理内存8G 物理内存的机器设 12G 会启动失败。5.6 预测阶段空框best.pt 加载后什么都检测不到现象训练指标不错但用 best.pt 对新图推理输出结果全为空。原因最常见的是推理时 conf 阈值设得过高车辆目标置信度在 0.3 到 0.5 之间时被过滤掉。另一个原因是推理时加载的模型 classes 和训练时不一致虽然模型不报错但输出类别索引无法对齐。解决先跑一次低阈值测试model.predict(sourcetest.jpg, conf0.05, iou0.5)如果低阈值下有框说明是阈值问题正常情况下车辆检测用 0.25 左右合理。如果低阈值还是空检查模型 predictor 的 names 是否和训练一致然后确认测试图本身确实包含车辆目标别拿一张背景图测模型然后怀疑训练翻车了。6. 训完别急着部署用 best.pt 反查训练集标注质量训练完成后第一件事不是部署而是做一次伪标签审计。1000 张图的数据集里最常出现的问题是框中心偏移十几像素、漏标被遮挡的车辆、类别标反。训练过程不会直接告诉你这些它只会在 mAP 上悄悄扣分。与其反复调参不如直接把错误标签揪出来修掉。做法很简单用刚训好的 best.pt 对训练集做一次预测然后把预测框和原始 YOLO 标签做 IoU 配对。如果某个标注框的最佳匹配 IoU 低于 0.5但预测置信度很高说明标注坐标大概率歪了如果一张图有明显的车辆漏标说明标注人员漏框了。下面这个脚本输出可疑样本清单from pathlib import Path import numpy as np from ultralytics import YOLO def compute_iou(box_a, box_b): # box 格式 [x_center, y_center, w, h] 归一化 ax1, ay1 box_a[0] - box_a[2] / 2, box_a[1] - box_a[3] / 2 ax2, ay2 box_a[0] box_a[2] / 2, box_a[1] box_a[3] / 2 bx1, by1 box_b[0] - box_b[2] / 2, box_b[1] - box_b[3] / 2 bx2, by2 box_b[0] box_b[2] / 2, box_b[1] box_b[3] / 2 ix1, iy1 max(ax1, bx1), max(ay1, by1) ix2, iy2 min(ax2, bx2), min(ay2, by2) inter max(0, ix2 - ix1) * max(0, iy2 - iy1) area_a box_a[2] * box_a[3] area_b box_b[2] * box_b[3] return inter / (area_a area_b - inter 1e-6) model YOLO(runs/train/vehicle1000/weights/best.pt) for label_path in Path(datasets/Vehicle1000/labels).glob(*.txt): # 预测 IoU 配对逻辑省略输出 conf0.7 且 iou0.5 的样本到 audit.csv pass逻辑说明这里的核心判断是“置信度与 IoU 不匹配”。高置信度说明模型认为这里确定有目标低 IoU 说明模型框的位置和标注框对不上两者矛盾就是标注有问题的证据。低置信度高 IoU 的样本则可能是难例这类不需要改标注留着训练即可。参数说明IoU 阈值 0.5 对应 mAP50 的判定标准conf 阈值 0.7 可以只筛选高置信度矛盾样本减少人工审查量。先取排序后前 20 个样本人工过一遍确认特征后再批量处理。我现在的习惯是正式训练之前先用 10 个 epoch 快速训一个小模型做一轮这种审计把标注错位修掉之后才开始正式训练。这个流程比任何调参都值mAP50 提升 3 到 8 个点是常有的事。没有可靠标签再好的参数组合也只是让模型更高效地记住错误。希望这个习惯能帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网