ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

红外目标检测数据集:格式转换与YOLO训练避坑指南

红外目标检测数据集:格式转换与YOLO训练避坑指南 简介YOLO红外多目标检测数据集面向目标检测学习与算法验证场景提供1000张真实场景高质量红外图片经LabelImg标注后附带voc(xml)、coco(json)、yolo(txt)三种格式标签分别存放于独立文件夹并确保标注框质量较高可直接接入YOLO系列模型训练。压缩包共2000个文件以xml和txt标签数据为主另含3个数据集划分脚本、YOLO环境搭建与训练案例教程Windows/Linux版及yaml配置文件整体仅96.39MB便于下载与迁移。附带的划分脚本可根据需求自动生成训练集、验证集、测试集配套教程覆盖从环境安装到自定义数据训练全流程能够帮助初学者快速跑通项目、减少格式转换与手动划分工作。目前已有456人学习下载适合需要红外多目标数据集进行算法实验或课程设计的开发者参考使用。1. 红外多目标检测数据集能做什么别把这份打包资源只当图片库凌晨两点的厂区周界雾天的高速卡口无人机夜巡时的林区火点这些场景里可见光摄像头基本是废的红外热像仪才是主力。但红外图像天生就是单通道灰度、目标边缘糊、背景噪声大跑常规 YOLO 模型经常出现「白天能检测、晚上就翻车」的尴尬。这份「YOLO红外多目标检测数据集」之所以值得花时间研究不只是因为里面那 1000 张图片而是它同时给了 VOC、COCO、YOLO 三套现成标签还带划分脚本和训练教程——等于把「数据准备」这个最枯燥的环节一次性做完了。适合谁手里有红外设备但没标注数据的人被三种标签格式来回折腾的人以及想用 YOLO 系列在红外场景落地却总被预处理拖后腿的人。先说个反直觉的结论拿到这个资源包第一天最该做的不是跑训练而是把三套格式的对应关系吃透。2. 红外场景为什么让检测模型集体翻车先读懂数据集的“脾气”在动手训练之前有必要先弄清楚红外图像和可见光图像在算法眼里到底差在哪。很多人拿到数据集直接套预训练权重跑出来的 mAP 却惨不忍睹然后开始怀疑参数设置——其实问题往往出在“数据本身的分布”和“模型预设的分布”根本不匹配。这一章把底层逻辑讲清楚后面调参才有方向。2.1 红外图像的三个底层特性单通道、低信噪比、边缘过渡带第一个特性是单通道。红外热像仪输出的本质是一张温度分布图保存成图像后通常是 8 位或 16 位灰度图。YOLO 系列模型的标准输入是三通道 RGB很多人直接把灰度图复制成三份喂进去模型确实能跑但 preprocess 阶段会丢失一部分温度梯度信息相当于把一个连续的温度场量化成 0~255 的离散灰度目标与背景的温差如果只有几个灰度级肉眼都难分辨模型就更难学了。第二个特性是低信噪比。红外探测器本身有热噪声加上大气衰减和背景辐射图像里会出现大量随机闪烁的亮点和条纹。这些噪声在可见光里不常见但在红外图像里它们是“常态”。模型如果训练数据里噪声模式和真实使用场景不一致部署时就会出现大量误检——把噪点当成人、把散热器当车辆这种翻车案例属于血泪经验。第三个特性是边缘过渡带。可见光图像里目标轮廓通常是锐利的人和背景之间有明显的颜色跳变。红外图像不是这样目标边缘往往有几像素宽的渐变带因为热量传导是连续的。这就导致标注框贴不齐目标要么框大了把背景框进来要么框小了切掉目标的边缘热辐射区。理解了这三个特性再回头看这份数据集的价值就清楚了它提供的红外图片是按真实热像仪输出整理的噪声分布、边缘特征、灰度范围都是“红外味”的不是拿可见光图像做灰度化来糊弄的。你在这种数据上训练出来的模型迁移到实际红外设备上效果才靠得住。2.2 三种标注格式的颗粒度差异为什么同一批图片要存三份标签VOC、COCO、YOLO 三种格式看着都是“框住目标”但它们在数据结构上差异很大直接决定了你能用什么框架训练、能不能做实例分割、要不要写转换脚本。先看存储方式。VOC 格式把每张图片的标注存成一个独立的 XML 文件文件名和图片名一一对应XML 里有文件夹名、文件名、图片尺寸、目标类别和 bounding box 的像素坐标xmin, ymin, xmax, ymax。COCO 格式则把所有图片的标注汇总到一个 JSON 文件里分成 images、annotations、categories 三个大数组框的坐标存成 [x, y, width, height] 的像素形式。YOLO 格式最简单粗暴每张图片对应一个同名 TXT 文件每行一个目标格式是“类别序号 x_center y_center width height”四个数值全部是相对图片宽高的归一化比例。这三者能共存靠的是同一个坐标换算关系。像素框转 YOLO 归一化框就是做除法x_center (xmin xmax) / 2 / width。反过来,YOLO 转 VOC 就是乘法加四舍五入。COCO 的 [x, y, w, h] 与 VOC 的 [xmin, ymin, xmax, ymax] 之间则需要换算xmin xymin yxmax x wymax y h。作为一线工程视角我建议你拿到数据后先做一件事随便取一张图片把三种格式的标注都画出来对比一下。用 OpenCV 画框三个来源画出来的框应该完全重合。如果发现某个框偏了 1~2 个像素通常是标注软件取整造成的可以接受如果偏了 10 个像素以上说明转换脚本有 bug这个数据集要用就得先修。3. 三种标签格式互转VOC、COCO、YOLO 之间的坐标桥接脚本很多人的工作流是这样的先拿到 VOC 格式的 XML但要训练 YOLO 就需要 TXT后来想跑 mmdetection 又需要 COCO 的 JSON。如果每次都手工转换或者找在线工具一来效率低二来容易出错。标题里直接给了三种格式说明打包者已经替你把转换做了但你不应该只满足于“用现成的”——万一你要往里加自己的数据呢到时候还得自己写转换。这一章把坐标桥接的脚本讲透你既可以用它来校验现有数据也可以扩展成自己的转换工具。3.1 坐标体系对照一张表格看清三种格式的差异格式文件形式坐标基准坐标字段典型用途VOCXML每图一个像素绝对值xmin, ymin, xmax, ymaxPASCAL VOC 评测、mmdetectionCOCOJSON全部汇总像素绝对值x, y, width, heightDetectron2、mmdetection、COCO APIYOLOTXT每图一个归一化0~1x_center, y_center, width, heightYOLOv5/v8、Ultralytics 生态这里面最容易被坑的是 YOLO 格式的坐标它的 x_center 是框中心点相对图片宽的比例不是左上角坐标。新手常犯的错是把 VOC 的 xmin 直接除以图片宽度当成 x_center结果框全部偏到左边去了。还有一个坑是 COCO 的 bbox 从左上角开始算而很多人在转成 YOLO 时忘记先加 half width导致中心点算错。另一个容易被忽略的点是类别编号的稳定性。YOLO 的 TXT 文件里存的是整数序号不是类别名。如果数据集里类别的顺序是 person0, car1, 而你在训练配置里把顺序写成了 car0, person1那么所有标签全部错位模型会把猫当成狗来训。所以我拿到任何一种格式第一件事是提取类别列表写进一个 classes.txt后面所有转换和训练都以这个文件为准。3.2 一个往返转换脚本的骨架XML 到 JSON 再到 TXT以下是我惯用的转换脚本骨架基于 Python 标准库和 xml.etree不需要安装额外依赖就能跑。这个脚本能完成 VOC→COCO→YOLO 的转换反过来也一样。import os import xml.etree.ElementTree as ET import json from glob import glob # ---------- VOC XML - COCO JSON ---------- def voc_to_coco(xml_dir, img_dir, output_json): categories {} images [] annotations [] ann_id 1 for xml_path in sorted(glob(os.path.join(xml_dir, *.xml))): tree ET.parse(xml_path) root tree.getroot() # 图片基本信息取自 XML 里的 size 节点 size root.find(size) width int(size.find(width).text) height int(size.find(height).text) filename root.find(filename).text image_id len(images) 1 images.append({ id: image_id, file_name: filename, width: width, height: height }) # 解析每个 object 节点 for obj in root.iter(object): name obj.find(name).text # 动态建立类别表保证编号连续且可回溯 if name not in categories: categories[name] len(categories) 1 cat_id categories[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) # COCO 的 bbox 是 [x, y, w, h]从 VOC 换算即做减法 w xmax - xmin h ymax - ymin annotations.append({ id: ann_id, image_id: image_id, category_id: cat_id, bbox: [xmin, ymin, w, h], area: w * h, iscrowd: 0 }) ann_id 1 coco_data { images: images, annotations: annotations, categories: [{id: cid, name: name} for name, cid in categories.items()] } with open(output_json, w) as f: json.dump(coco_data, f, indent2) print(f[INFO] COCO JSON saved: {output_json}) # ---------- COCO JSON - YOLO TXT ---------- def coco_to_yolo(coco_json, img_dir, out_label_dir): with open(coco_json) as f: data json.load(f) # 建立 image_id - 图片信息的映射方便反查宽高 img_map {img[id]: img for img in data[images]} # 建立 category_id - 序号从0开始的映射YOLO 要求类别从0编号 cat_map {cat[id]: idx for idx, cat in enumerate(data[categories])} os.makedirs(out_label_dir, exist_okTrue) for ann in data[annotations]: img img_map[ann[image_id]] img_w img[width] img_h img[height] x, y, w, h ann[bbox] # 像素转归一化注意中心点要加半宽半高 x_center (x w / 2) / img_w y_center (y h / 2) / img_h w_norm w / img_w h_norm h / img_h cat_id cat_map[ann[category_id]] label_path os.path.join( out_label_dir, os.path.splitext(img[file_name])[0] .txt ) with open(label_path, a) as lf: lf.write(f{cat_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}\n) print(f[INFO] YOLO labels saved to: {out_label_dir})这段代码的逻辑分三段先读 XML 的 size 和 object 节点拼出 COCO 的 images 和 annotations 数组再从 COCO JSON 反查图片宽高和类别映射把像素框换算成归一化框写进 TXT。其中第 19 行图像 id 递增和第 47 行类别从头编号是两个最容易出错的地方图像 id 必须从 1 开始且连续类别编号必须从 0 开始且和 data.yaml 里 classes 顺序一致。使用这套脚本时还有两个参数要注意。一是输出小数位数用:.6f足够保证还原精度写太少会丢像素。二是glob排序用了sorted保证不同机器上输出顺序一致方便后面做差分校验——把转换完的 YOLO 框画回原图再和 VOC 原始标注叠图对比若像素偏差 ≤2 就说明脚本没问题。4. 划分脚本为什么值得单独打包训练集、验证集、测试集的比例博弈很多公开数据集只给图片和标签划分的事让用户自己做。但实际做过的都知道划分这一步写不好会在训练时埋雷比如训练集和验证集出现同一张图片的增强版本验证指标虚高或者某个类别在训练集里有 80 张在验证集里只有 3 张导致那个类别 mAP 波动极大。这份资源把划分脚本单独打包说明作者清楚“数据泄漏”和“类别分布不均匀”是训练效果的两大隐形杀手。4.1 一份合格的划分脚本必须做到的 4 件事第一件事是固定随机种子让划分结果可复现。不设随机种子的话每次跑脚本产出的文件列表都不一样今天训完明天想复现实验结果就难了。一般用random.seed(42)或者numpy.random.seed(0)就可以。第二件事是支持分层抽样。所谓分层就是按类别比例来抽而不是全局随机抽。假设数据集里行人 900 个框、车辆 100 个框全局随机抽 20% 做验证集运气不好可能把 100 个车辆框抽走了 30 个验证集里车辆占比变成 30%——而训练集里车辆只剩 70 个。这样一来模型对车辆的拟合就不充分验证时车辆类别的 AP 会很难看。常见做法是先把所有图片按“包含哪些类别”分组再在组内按比例抽。第三件事是路径回写。划分脚本通常输出三个 txt 文件分别记录训练、验证、测试图片的绝对路径或相对路径。训练框架读取这些 txt 来组装数据集。路径写法的坑在于在 Windows 上跑通的脚本换到 Linux 服务器上因为分隔符不同就炸了。我习惯在脚本里统一用os.path.join拼接输出前用.replace(\\, /)强制转成正斜杠。第四件事是划分比例可调。最常用的配比是训练:验证:测试 8:1:1 或 7:2:1。1000 张图片其实不算多我建议验证集至少留 100 张测试集留 100 张否则验证 loss 曲线毛刺会大到没法判断模型是否收敛。脚本里最好把比例做成参数比如train_ratio0.8, val_ratio0.1而不是写死在代码里。4.2 分层抽样划分代码固定种子下的可复现划分下面是按“图片内类别集合”做分层的划分脚本适用于 VOC 和 YOLO 两种标签结构输出三类文件清单。import os import random from glob import glob from collections import defaultdict random.seed(42) # 配置区改成你的实际路径 IMG_DIR images # 所有图片 YOLO_LABEL_DIR labels # 所有 YOLO 格式 txt 标签 OUTPUT_DIR splits TRAIN_RATIO 0.8 # 训练集比例 VAL_RATIO 0.1 # 验证集比例测试集自动取剩余 os.makedirs(OUTPUT_DIR, exist_okTrue) img_paths sorted(glob(os.path.join(IMG_DIR, *.jpg))) # 建立 类别组合 - 图片列表 的映射 # 例如一张图同时包含 person 和 car它会被归入 (person,car) 这个组 group_map defaultdict(list) for img_path in img_paths: label_path os.path.join( YOLO_LABEL_DIR, os.path.splitext(os.path.basename(img_path))[0] .txt ) class_set set() if os.path.exists(label_path): with open(label_path) as lf: for line in lf: cls_id line.split()[0] # 第一个字段是类别序号 class_set.add(cls_id) # 用归一化的类别组合作为分组键保证同组图片的类别分布相似 group_key tuple(sorted(class_set)) group_map[group_key].append(img_path) train_list, val_list, test_list [], [], [] # 在每个分组内独立按比例划分避免某一类别被整体抽走 for key, paths in group_map.items(): random.shuffle(paths) n_train int(len(paths) * TRAIN_RATIO) n_val int(len(paths) * VAL_RATIO) train_list.extend(paths[:n_train]) val_list.extend(paths[n_train:n_train n_val]) test_list.extend(paths[n_train n_val:]) # 写文件时统一转成 / 分隔符兼容 Linux 服务器训练环境 def write_split(path_list, out_name): with open(os.path.join(OUTPUT_DIR, out_name), w) as f: for p in path_list: f.write(p.replace(\\, /) \n) write_split(train_list, train.txt) write_split(val_list, val.txt) write_split(test_list, test.txt) print(ftrain: {len(train_list)}, val: {len(val_list)}, test: {len(test_list)})这段代码的核心是group_map先把所有图片按“包含的类别集合”分组再在每组内洗牌划分。这样做的效果是即使某个类别只出现在 30 张图里它也能按比例分配到训练、验证、测试三份里不会出现验证集某个类别一个样本都没有的极端情况。要注意的关键参数是TRAIN_RATIO 0.8和VAL_RATIO 0.1。当图片总数少的时候int()取整会带来偏差比如 9 张图的组8:1:1 的比例各自取整后可能变成 702极端的组甚至可能验证集为空。遇到这种情况我建议把划分脚本跑完后检查一下 val.txt 的行数如果少于图片总数的 5%就要考虑增大验证集比例或者换一种划分策略。还有一种做法是直接用 sklearn 的train_test_split配合stratify参数但上面的纯标准库版本更容易让新手看懂逻辑。5. 红外数据集的 4 个常见踩坑点现象、原因、解决办法花了整章讲完格式转换和划分脚本接下来这部分是我最想写的——真正跑训练时容易翻车的地方。很多坑不是模型结构的问题而是数据使用方式的问题。以下 4 条都是用这份红外数据集训练时最容易撞上的按严重程度排序每一条都给出对应的解决方法。5.1 标注框边缘贴不住目标训练时损失不降现象是训练了二三十个 epochloss 曲线一直在一个高位震荡降不下去。用验证集跑出来的框和真实标注对比发现框的位置总是偏的偏的方向还不统一有时左边切掉目标有时右边多出一大块。原因出在红外图像的边缘过渡带。标注员在画框时如果有的人贴着目标高亮区画有的人把边缘渐变区也算进去就会导致同一个物体的标注框宽窄不一。模型要同时拟合两种不同宽度的框自然难以收敛。解决方法是先做一次标注质量审计。把训练集的标注框宽度和高度画成直方图看是不是双峰分布如果是就是有人框松有人框紧。更稳妥的办法是在预处理阶段做边缘锐化——对灰度图做 CLAHE 对比度增强或高斯差分滤波把过渡带压窄再喂给模型。我用的参数一般是 CLAHE 的 clipLimit2.0, tileGridSize(8,8)不会过度增强噪声。5.2 小目标漏检严重mAP 好看但实际部署时逐帧丢检现象是训练指标看起来还行mAP0.5 能到 0.7 以上但拿到实际红外视频流里一跑远处的人和车辆经常漏检尤其是图像里占不到 32×32 像素的目标。原因是红外场景的目标尺度分布和常规数据集差异大。1000 张图里如果大量目标是小目标而 YOLO 默认的 anchor 尺寸是往 COCO 数据集的尺度分布上设计的两者不匹配小目标分支就学不好。解决路径有三条按见效快慢排第一训练时把输入分辨率从默认 640 提到 960 或 1280小目标在特征图上的像素占比变大但要显存够用第二调整 anchor 参数用utils/autoanchor.pyYOLOv5 自带基于训练集的框重新聚类第三开启 mosaic 增强YOLOv8 中默认开启让模型在训练时见到更多小目标。我自己偏好先改分辨率再开 autoanchor改动小、收益立竿见影。需要留意的是分辨率提高会显著增加训练时间1000 张图在单卡上跑 960 分辨率大约会慢 30% 左右。5.3 红外噪声被模型当成特征热噪声引发大量误检现象是训练和验证的 loss 都正常但部署到实机后模型对着空旷地面疯狂报警把热辐射噪点、阳光照射的金属板、散热器排风都当成检测目标。原因是红外图像的噪声分布和可见光完全不同。如果训练数据里的噪声模式比较固定比如设备型号固定、天气条件单一模型会走捷径——“看到这种噪声纹理就输出目标”而不是真正学习目标的形状特征。这种问题在验证集上常常暴露不出来因为验证集是同一批噪声模式的数据。解决思路是在数据增强阶段引入针对红外噪声的扰动。常见做法是做随机噪声注入和背景替换对图像加高斯噪声、椒盐噪声或者用随机裁剪的背景图做 mosaic 拼接破坏噪声和目标之间的固定关联。还有一个我用过的技巧是训练前对输入做中值滤波预处理kernel size 3×3把热噪声压一压再进模型代价是会稍微模糊边缘但对于 YOLO 的 anchor-based 检测影响不大。5.4 类别不平衡让损失函数“玄学”少样本类别训不出来现象是训练时 loss 整体下降正常但查看每个类别的 AP发现样本多的类别 AP 很高0.8样本少的类别 AP 在 0.1 以下徘徊。更迷惑的是调整学习率后少样本类别的指标变得更差有种“玄学”难以调参的感觉。原因是 YOLO 默认的损失函数对各类别一视同仁样本多的类别在总损失里占比大梯度更新方向被它们主导。少样本类别不是没学而是梯度被淹没了。解决方法是组合拳先看数据分布如果某个类别框数量少于总数的 5%考虑做复制粘贴增强把小目标贴到背景图上不行就改用 focal loss让损失函数更关注难分类样本再不行就降学习率、加长训练轮数比如从 100 epoch 加到 200 epoch配合余弦退火让模型后期有更长的精调时间。切忌一上来就改网络结构或换大模型1000 张数据撑不起大模型的训练换个更大的 backbone 往往只会加速过拟合。6. 用这份数据集跑通 YOLO 训练的全流程从 data.yaml 到验证功能前面讲了格式、划分和避坑最后落到“怎么用这份数据集真正训出一个能用的模型”。这里以 YOLO 生态的常见做法为例Ultralytics 系版本不同但流程一致给出一条最小可复现的训练路径。6.1 组织目录结构并编写 data.yaml拿到数据集后第一步是先把文件整理成训练框架能识别的目录结构再写 data.yaml。常见做法是把图片和标签分开放最终形成一个这样的布局dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml把前面划分脚本输出的 train.txt、val.txt 按对应关系把图片和标签复制到上述目录中。然后写 data.yaml# data.yaml path: /absolute/path/to/dataset # 数据集根目录建议写绝对路径 train: images/train val: images/val test: images/test nc: 3 # 类别数量改成你的实际值 names: [person, car, animal] # 改成数据集 classes.txt 里的顺序这里最常犯的错是names的顺序和你转换脚本里的类别序号对不上。YOLO 标签里的 0、1、2 是序号data.yaml里的names列表按索引对应。如果你的 TXT 里0代表person那names第一项必须是person写反了整个模型就废了。6.2 启动训练最小命令与关键参数解读python train.py \ --data data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache \ --project runs/train \ --name infrared_exp参数含义分别说明。--weights yolov5s.pt是加载 COCO 预训练权重虽然前面说红外分布和可见光不同但预训练权重里的低层特征边缘、纹理仍然有用不要从零开始训。如果显存是 8G 左右--batch 16配--img 640刚好要上 960 分辨率就把 batch 降到 8。--cache表示把图片提前加载到内存1000 张图占用大概 2~3G 内存能显著加速训练但如果图片分辨率很高、内存不够就关掉。训练时打开 TensorBoard 观察 loss 曲线——出现 loss 不降或验证集 mAP 来回抖动时回头检查 5.1 到 5.4 提到的问题。6.3 query 验证与导出推理训练完用 val.py 验证python val.py \ --data data.yaml \ --weights runs/train/infrared_exp/weights/best.pt \ --img 640 \ --save-json这一步会输出各类别的 AP 和混淆矩阵。特别提醒不要只看 mAP 综合值红外场景要按类别看 AP尤其是小目标类别的召回率。验证通过后如果想部署常见做法是导出 ONNX 模型方便后续推理集成或跨平台部署。导出的命令很简单python export.py --weights runs/train/infrared_exp/weights/best.pt --include onnx --img 640我踩过最大的一个坑是训练时把--img设为 960导出 ONNX 时却用默认 640结果部署后推理尺寸不匹配检测框全部偏移。从那以后我养成了习惯训练、验证、导出三个阶段的输入尺寸必须保持一致并在模型输入层打印 shape 做二次确认。这条习惯让我少加了不知多少班也希望能帮到你少走这段弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进