红外多目标检测实战:VOC/COCO/YOLO格式转换与YOLOv8训练全流程
简介面向目标检测学习与实战的YOLO红外多目标检测数据集收录5000张真实场景高质量红外图片数据场景丰富且使用LabelImg逐张精细标注适用于行人、车辆等多类目标的红外检测任务。标签提供VOCxml、COCOjson和YOLOtxt三种格式分别存放在不同文件夹可直接接入YOLOv5、YOLOv8等系列模型训练。包内共2000个文件除1986个xml标注文件外另有6个html环境搭建与训练教程覆盖Linux和Windows双系统下的YOLO环境配置、数据集修改与训练流程3个Python脚本可按需切分训练集、验证集与测试集并附训练列表txt压缩包整体约203MB。已有208人学习。从环境搭建、数据划分到模型训练均有配套材料适合需要快速验证YOLO训练流程或从事红外视觉研究的学生与开发者。1. 红外多目标检测5000 张三格式数据集到底该怎么用红外场景的目标检测和可见光完全是两套写法图像多为单通道灰度目标对比度低人、车在画面里常常只有几十个像素还叠着热噪声和遮挡。很多人第一次拿红外数据跑 YOLO 卡住的不是网络结构而是标签格式——标注工具导出 VOC训练框架要 YOLO评测要 COCO三套格式互不兼容光写转换脚本就得一天。这套含 5000 张图片的 YOLO 红外多目标检测数据集重点不在数量而在同时给出了 VOC、COCO、YOLO 三种格式标签、划分脚本和训练教程。解压后按「看懂格式 → 重新划分 → 配置训练 → 验证归因」四步走一个下午能跑通第一版模型。适合刚入门 YOLO 想练数据管线的学生也适合做工业巡检、安防监控、夜间辅助驾驶的工程师快速验证方案。每一步都给出可复制的命令和参数并把红外场景的典型坑标出来。2. 三种标签格式的字段差异与互转脚本VOC、COCO、YOLO 怎么对齐解压 .rar 后先别急着开训把目录结构当作业规范看。三种格式共存的好处是不同工具链都能直接吃坏处是同一份标注在三个地方各写一遍字段口径稍有偏差后面训练就是灾难。三套格式的核心差异先握在心里转换时才不会踩边界。2.1 VOC 格式XML 里的字符串类别与绝对像素坐标VOCPascal VOC格式历史最久、标注工具兼容性最好。JPEGImages 放原图Annotations 放同名 XML每个目标一个object节点类别是name字符串框是bndbox里的 xmin/ymin/xmax/ymax 四角像素坐标annotation size width640/width height512/height /size object nameperson/name bndbox xmin120/xmin ymin80/ymin xmax180/xmax ymax210/ymax /bndbox /object /annotation这里有两个高频坑。第一name是字符串person 和 Person 会在转换时被当成两个类先统一类别表再转第二坐标是原始分辨率下的整数像素做缩放增强前必须先换算成归一化值。红外图像很多标注工具会顺手把 16 bit 深度图转成 8 bit 再导出XML 里记录的宽高可能和图片实际尺寸不一致转换脚本里我会用代码自动读图宽高不手写死值。2.2 COCO 格式单个 JSON 串起 images 与 annotationsCOCO 格式把整个数据集压缩成一个 JSON五段结构里真正干活的是 images、annotations、categories 三段。images 数组里每张图一个条目id 是全局唯一整数annotations 里每个框带 image_id、category_id、bbox 和 area。bbox 是 [x, y, width, height] 的左上角加宽高不是四角这是和 VOC 最容易混的一点。KITTI 标注转 YOLO 也是同样的思路只是字段名和坐标系不同原理完全一样。红外数据类别通常不多COCO 的 categories 里 id 从 1 开始编号而 YOLO 的类别编号从 0 开始互转时category_id - 1这个偏移是最高频的 bug 来源。另外 annotations 里如果带了 segmentation 多边形转 YOLO 时只取 bbox 字段直接用x w/2算中心点更稳不要用多边形顶点去拟合框。2.3 YOLO 格式每张图一个同名 txt坐标全部归一化YOLO 格式最简单也最容易出错。labels 目录下每张图一个同名 txt每行五个字段类别编号、中心点 x、中心点 y、宽、高全部是除以图片宽高后的 0 到 1 浮点数。以上面 XML 的框为例图片 640x512中心点是 (150, 145)归一化后约 0.234、0.283。提示训练前把 labels 和图片的文件名统一成小写Windows 下解压的包经常出现Img_001.JPG与img_001.txt不匹配的问题框架按精确文件名查找大小写不一致直接读不到标签。三种格式的差异可以收敛成一张对照表维度VOCCOCOYOLO存储形态每图一个 XML全部装进一个 JSON每图一个 txt类别表示字符串 name整数 category_id整数编号坐标定义xmin/ymin/xmax/ymax像素[x, y, w, h]像素[cx, cy, w, h]归一化最高频错误类名大小写不一致category_id 从 1 开始忘记归一化2.4 一个能直接跑的 VOC 转 YOLO 脚本下面脚本读取 VOC 的 Annotations 目录输出 YOLO 的 labels 目录类别表由你手工维护顺序图片尺寸从对应图片文件读取而不是信 XMLimport os import xml.etree.ElementTree as ET import cv2 classes [person, car, bike, truck] # 顺序即 YOLO 类别编号 def voc_to_yolo(xml_path, img_dir, label_dir): name os.path.splitext(os.path.basename(xml_path))[0] img cv2.imread(os.path.join(img_dir, name .jpg)) if img is None: return False h, w img.shape[:2] tree ET.parse(xml_path) lines [] for obj in tree.getroot().iter(object): cls obj.find(name).text if cls not in classes: continue 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) cx ((xmin xmax) / 2) / w cy ((ymin ymax) / 2) / h bw (xmax - xmin) / w bh (ymax - ymin) / h lines.append(f{classes.index(cls)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(label_dir, name .txt), w) as f: f.write(\n.join(lines)) return True参数说明classes列表的顺序决定输出编号必须和后续data.yaml的 names 严格一致用cv2.imread拿宽高是为了绕过 XML 里size字段与图片实际尺寸不一致的脏数据cx 用 (xminxmax)/2 而不是 xmin w/2两者数学等价但前者在遇到 0 宽度异常框时更鲁棒。跑完抽查三个 txt确认所有值都在 0 到 1 之间数值大于 1 就是除错了分辨率。3. 划分脚本的正确写法按类别覆盖切分 train/val/test 而不是随机打散3.1 为什么红外数据不能直接 random.shuffle随机划分在常规数据集上够用但红外场景有两个特性会让它失真。第一红外视频抽帧得来的数据集里相邻帧高度相似纯随机会让同一目标的不同帧同时出现在 train 和 val验证指标虚高模型真实泛化能力被高估第二红外目标类别天然不均匀行人、车辆是大类摩托车这类小类可能只有几十张纯随机下小类有概率在验证集里一张不出现mAP 会呈现出假性的类别不平衡。常见做法是先按场景或视频片段分组在组层面做划分如果压缩包没有提供场景 ID退而求其次用类别覆盖约束来切分保证每个类别在 train 和 val 里都至少出现一次。3.2 按类别覆盖约束的划分脚本与参数下面脚本按类别分组小类先挑验证集大类只能补位不能抢占从根上保证类别覆盖import os import random from collections import defaultdict random.seed(42) val_ratio 0.1 labels [f for f in os.listdir(labels) if f.endswith(.txt)] cls_map defaultdict(list) for lb in labels: with open(os.path.join(labels, lb)) as f: cls_set {line.split()[0] for line in f if line.strip()} for c in cls_set: cls_map[c].append(lb) assign {} # img - train / val # 小类先处理避免 val 名额被大类占用 for c in sorted(cls_map, keylambda k: len(cls_map[k])): imgs cls_map[c] random.shuffle(imgs) if len(imgs) 1: assign[imgs[0]] train print(f[warn] class {c} 只有 1 张放入 train) continue k min(max(1, int(len(imgs) * val_ratio)), len(imgs) - 1) for i, img in enumerate(imgs): cur assign.get(img) if cur is None: assign[img] val if i k else train elif cur train and i k: assign[img] val val [i for i, a in assign.items() if a val] train [i for i, a in assign.items() if a train] test list(set(labels) - set(train) - set(val)) # 无标签/多出来的进 test逻辑说明按类别样本量升序处理样本最少的类别先锁定自己的 val 名额后面的大类遇到已被分到 val 的图片不会覆盖只能把自己组内剩余图片补进 val 或 train。k min(max(1, int(len(imgs) * val_ratio)), len(imgs) - 1)保证每个类别在 val 里至少 1 张、在 train 里至少留 1 张。单张图片属于多个类别时以最先处理它的那个类别的决策为准。参数含义如下参数默认值说明random.seed42固定后划分可复现换 seed 前先记录旧划分val_ratio0.1验证集占比小类会强制至少 1 张进 vallen(imgs) - 1动态防止某个类别全部进 val 导致训练集缺类3.3 划分后必须做的三项校验3.3.1 图片与标签一一对应划分完先把 images 和 labels 两个目录的文件数对一遍再用下面的命令列出对不上的文件comm -3 (ls images/train | sed s/\.jpg$// | sort) (ls labels/train | sed s/\.txt$// | sort)左边的comm参数是训练集图片名右边是标签名输出里的每一行都是孤儿文件。空标签图片因为有同名 txt 不会出现在这里但它属于另一类问题见 3.3.3。3.3.2 类别分布检查对 train、val、test 各统计一次每个类别的框数量把它和原始数据集的分布做对比。红外场景里小类别的占比如果被切成 0 或者翻了 10 倍说明划分失效回去改随机种子或改成按场景分组划分。统计脚本很简单遍历 txt 文件把每行的第一个字段累加即可不必引入额外依赖。3.3.3 空标签与超界框红外数据里全黑帧、过曝帧经常没被标注工具删干净对应 txt 是 0 字节。训练时 YOLO 遇到空标签图片会直接跳过但 val 里的空标签会拉低 mAP建议统一挪到 test 或直接删除。超界框xmax 大于图片宽在 VOC 转 YOLO 时会被归一化成大于 1 的值这类框要修坐标而不是删掉因为修完类别语义还在删了等于丢弃一个正样本。4. 用 YOLOv8 训练红外多目标数据集完整参数与命令4.1 data.yaml 的写法与红外单通道图像处理YOLO 训练教程里第一步永远是先写对数据描述文件。data.yaml 把路径、类别数、类别名一次性喂给训练器path: ./yolo_infrared train: images/train val: images/val test: images/test nc: 4 names: 0: person 1: car 2: bike 3: truckpath是相对 data.yaml 的根目录train/val 是相对 path 的路径nc必须和 names 的条数一致不一致时框架报错还算友好就怕 nc 写大、names 写少模型输出维度对不上直接显存报错。红外图像通常是单通道而 YOLO 输入层固定三通道常见做法是cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)把同一个灰度值复制到三个通道。不要用 PIL 的 convert(RGB)两者结果一样但 OpenCV 路径在批量数据管线里少一次 ndarray 转换。红外直方图大多集中在低亮度区转三通道后顺手做一次 CLAHE 对比度增强再存盘mAP 往往能涨 2 到 3 个点这是红外比可见光更明显的预处理收益。4.2 训练命令与关键参数表推荐直接用 YOLOv8 官方 CLI命令如下yolo detect train \ datadata.yaml \ modelyolov8n.pt \ epochs150 \ imgsz640 \ batch16 \ device0 \ workers4 \ patience30 \ project./runs \ nameinfrared_run命令说明modelyolov8n.pt表示加载 COCO 预训练权重再微调5000 张的量级从头训练容易欠拟合迁移学习是标准路线这也是网上大多数 yolov5 训练教程一致的做法只是 YOLOv8 把权重文件格式换成了 ptpatience30表示 val 的 mAP 连续 30 轮不涨就早停红外数据收敛慢这个值不要设太小。关键参数见下表参数推荐值理由modelyolov8n / yolov8s5000 张从 n 起步n 精度不够再换 simgsz6401280目标面积小于 32x32 时优先提 imgszepochs120200红外目标特征弱收敛比可见光慢batch显存上限的一半批次太小组内统计噪声大loss 抖动mosaic1.0最后 10 轮关前中期提升泛化后期干扰收敛lr00.0050.01微调预训练权重时不宜用默认 0.01 以上patience30数据量小早停能省一半时间4.3 红外小目标的损失函数与后处理调整YOLOv8 是 anchor-free 结构没有传统 anchor 宽高先验网上一些 yolov5 训练教程里讲 k-means 聚类 anchor 的步骤在 v8 里已经不需要了但红外小目标不友好是结构固有特性有实际效果的调整是三个方向。第一把 imgsz 提到 1280小目标在特征图上的像素占比直接变大这是最有效的手段代价是显存和推理时间第二调高分类损失权重红外场景类别少、类间差异大分类错误比定位错误更伤 mAPYOLOv8 训练参数里 cls 可以从默认值往上加第三推理时把 conf 从 0.25 降到 0.1红外目标对比度低模型输出的置信度普遍偏低阈值太高会过滤掉大量真目标代价是误检变多需要配合 iou0.45 的 NMS 一起调。环境上如果只有 AMD 显卡先用devicecpu跑通整个流程再根据显存换 ROCm 或 DirectML 后端yolo 环境配置这一步最忌讳一开始就追求跑满性能。5. 训练完先做这三件事可视化、混淆矩阵归因与格式回灌5.1 推理可视化先看 val 集而不是盲测新图训练完第一件事不是拿没见过的图试效果而是先看验证集上的预测叠加图因为 val 的 ground truth 是已知的能直接对照框得准不准yolo predict modelruns/infrared_run/weights/best.pt sourceimages/val conf0.1 save_txtTrue重点看三类图全黑区域被框出目标的热噪声误检、只框出半个目标的回归不完整、重叠目标被漏掉的NMS 抑制过度。5.2 混淆矩阵归因区分漏检和误检谁占大头用 val 集跑一次官方验证拿到 confusion matrix 图。红外场景有两个典型模式背景被大量预测成前景说明 conf 阈值过低或背景特征和目标的灰度分布太接近先调阈值再考虑补负样本同类目标互相漏检例如车挡住人说明训练数据的遮挡样本不足要回数据层面补样本而不是继续调参。这一步能明确告诉你「接下来该调参还是该补数据」比盲目加 epoch 有用得多。5.3 格式回灌预测 txt 反算回 COCO 对齐评测链YOLO 的预测结果只有 txt但项目评测常用 pycocotools或者下游系统只认 VOC XML。回灌时把归一化坐标反算回像素即可核心就一段def txt_to_coco(txt_path, img_w, img_h): out [] with open(txt_path) as f: for line in f: c, cx, cy, w, h map(float, line.split()) out.append({ category_id: int(c) 1, # YOLO 从 0 起COCO 从 1 起 bbox: [(cx - w / 2) * img_w, (cy - h / 2) * img_h, w * img_w, h * img_h], }) return out整个脚本里最容易错的就是注释标出的int(c) 1偏移漏掉它评测出来的 mAP 会断崖式下跌且逐类指标全部错位。回灌完成后用 pycocotools 算一次 mAP0.5 和 mAP0.5:0.95把数字连同划分 seed 一起记下来后续所有改动都要拿新指标和这个基准线比而不是对着终端里的 loss 曲线自我感觉良好。本文还有配套的精品资源点击获取