ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

红外多目标检测数据集:YOLO训练从VOC/COCO标注到标签转换全流程

红外多目标检测数据集:YOLO训练从VOC/COCO标注到标签转换全流程 简介面向YOLO红外多目标检测任务而整理的数据集配套资源适合目标检测初学进阶开发者及算法工程师使用。内容源自真实红外场景图片的标注结果提供VOC、COCO、YOLO三种格式标签并分文件夹存放可直接替换到YOLO系列训练管线中免去手工格式转换的麻烦。附带的跨平台教程覆盖Linux与Windows环境搭建、基于实例修改的YOLO训练案例以及三个Python划分脚本可分别用于切分训练集验证集测试集、生成ImageSets下的txt清单方便按实际数据量灵活调整。压缩包共2000个文件以1986个xml标注文件为主辅以html图文教程、txt说明和py脚本整体约203.34MB目录层级清晰。目前已有208人学习下载适合需要高质量红外标注数据并希望从数据准备到模型训练一次性跑通的目标检测开发者。1. 红外多目标检测数据集为什么是YOLO落地的一道坎做红外目标检测的人大多会卡在同一个位置上模型选型不难YOLO系列闭着眼挑一个就行难的是手里根本没有一份能直接喂给训练脚本的数据。拿公共可见光数据集跑出来的权重一到夜间监控、电力巡检、工业测温这些真实红外场景里mAP掉得惨不忍睹。这份标题里的 YOLO红外多目标检测数据集含5000张图片配VOC、COCO、YOLO三种格式标签再加划分脚本和训练教程恰好补上这个缺口。适合谁适合手里有红外设备但没时间标注、想快速把YOLO跑通在自己业务数据上的工程师也适合想搞清楚三种标注格式之间怎么互转的初学者。这篇就按数据集长什么样、三种格式怎么读、怎么转换和划分、训练参数怎么配、坑踩在哪这条线把整套流程拆给你看。2. 5000张红外图的标签格式VOC、COCO、YOLO三种格式到底差在哪2.1 VOC的XML里藏着的几个字段folder、size和segmentedVOC格式是三种格式里最啰嗦但最好读的一种每张图片对应一个同名XML文件。从Pascal VOC 2012继承下来的结构里真正决定检测框的是object节点下的bndbox里面四个值xmin、ymin、xmax、ymax全是整数像素坐标左上角为原点单位是像素。理解这个原点很重要因为后面转COCO和YOLO坐标时所有换算都从这里出发。一个容易被忽略的字段是size它记录的是原始图片的width和height。有些红外数据集做过压缩预处理XML里的图片尺寸可能和你实际读到的cv2.imread()结果不一致这种情况下直接做归一化会系统性偏移。另一个字段segmented在目标检测场景下基本是摆设恒为0但解析时不能因为它就没写就报错很多爬下来的红外数据集里这个节点会被省略。我自己一般直接用xml.etree.ElementTree解析不引入重型的标注库。读XML的脚本核心逻辑很固定定位annotation根节点遍历所有object取出name和bndbox子节点。红外数据集里类别名可能是person、car、dog这类常规类也可能是hotspot、equipment这种业务类标签里是什么就原样保留不要自作主张做映射。import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) width int(size.find(width).text) height int(size.find(height).text) objects [] for obj in root.iter(object): name obj.find(name).text 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) objects.append({ name: name, bbox: [xmin, ymin, xmax, ymax] # 像素坐标单位px }) return {width: width, height: height, objects: objects}这个函数是后续所有转换脚本的地基。注意xmin、ymin这里从int改成了float——实践中发现部分红外手标工具会标出带小数的坐标比如热像仪标定时出现0.5像素偏移直接用int()截断会在小目标上产生不可忽视的偏差。返回的width和height必须和实际图片核对我的习惯是解析完XML后顺手用cv2.imread().shape校验一次不一致时以图片为准。2.2 COCO的JSON结构images、annotations和categories的对应关系COCO格式是三个格式里最绕的因为它把所有图片的标注信息塞进一个JSON文件用id关联。顶层有五个键检测任务实际只用其中三个images是图片列表每条记录有id、file_name、width、heightannotations是标注列表每条记录有id、image_id、category_id、bbox、area、iscrowdcategories是类别表每条记录有id和name。这里最容易埋坑的是bbox的格式。COCO的bbox是[x, y, width, height]不是[xmin, ymin, xmax, ymax]。很多从VOC转过来的人直接把四个坐标原样搬进去训练脚本一跑loss一开始就像过山车因为框的右下角被当成了宽高。另一个坑是category_id必须从1开始有些工具从0开始YOLO的类别id从0开始COCO从1开始id和category_id不能混。红外数据集转COCO时我建议保留iscrowd: 0字段不要省略因为后端的pycocotools评估mAP时会读取它做过滤area则可以用w * h直接计算如果原标注是个多边形area应该用多边形面积但对于矩形框w * h就够了。转换脚本里需要维护两个字典一个把图片文件名映射到image_id一个把类别名映射到category_id。前者在JSON里是file_name键后者在训练配置里会和YOLO的类别顺序对齐。import json def voc_to_coco(parsed_list, output_json): images [] annotations [] categories [] cat_map {} ann_id 1 for img_id, item in enumerate(parsed_list, start1): images.append({ id: img_id, file_name: item[file_name], width: item[width], height: item[height] }) for obj in item[objects]: name obj[name] if name not in cat_map: cat_map[name] len(cat_map) 1 # COCO类别id从1开始 categories.append({id: cat_map[name], name: name}) xmin, ymin, xmax, ymax obj[bbox] w xmax - xmin h ymax - ymin annotations.append({ id: ann_id, image_id: img_id, category_id: cat_map[name], bbox: [xmin, ymin, w, h], # COCO用x,y,w,h area: w * h, iscrowd: 0 }) ann_id 1 coco { images: images, annotations: annotations, categories: categories } with open(output_json, w) as f: json.dump(coco, f, indent1)这段代码的parsed_list就是上一节parse_voc跑完所有XML后的汇总列表每条额外带上file_name字段。indent1是刻意选的COCO的原始标注文件用indent1的缩进风格后续如果要用labelme或coco-annotator回读兼容性更好。如果数据集类别是固定的建议把cat_map写成硬编码字典而不是自动分配避免不同批次的标注数据因为类别顺序不一致导致训练标签错位。2.3 YOLO的TXT才是训练时真正吃进去的格式归一化坐标与类别idYOLO格式最简洁每张图一个TXT文件每行一个目标格式是class_id x_center y_center width height四个坐标全部是归一化浮点数除以图片宽高后的值在0到1之间。类别id从0开始计数和COCO的从1开始正好错开一位。训练时YOLO系列框架Darknet、ultralytics的YOLOv5/v8读的就是这种TXTVOC和COCO只是作为转换的中间态存在。写YOLO格式的训练脚本时最大的翻车点来自对归一化三个字的理解。x_center是xmin加上框宽的一半再除以图片宽度不是把xmin直接除以宽度同理width是框宽除以图片宽度不是归一化后的框面积。很多人拿着VOC转YOLO框的位置几乎对但全部偏右下方就是这个除以2的操作写漏了。def voc_to_yolo(parsed_item, output_txt): width parsed_item[width] height parsed_item[height] lines [] for obj in parsed_item[objects]: class_id obj[class_id] xmin, ymin, xmax, ymax obj[bbox] box_w xmax - xmin box_h ymax - ymin x_center (xmin box_w / 2.0) / width y_center (ymin box_h / 2.0) / height w_norm box_w / width h_norm box_h / height lines.append(f{class_id} {x_center:.6f} {y_center:.6f} {w_norm:.6f} {h_norm:.6f}) with open(output_txt, w) as f: f.write(\n.join(lines))class_id需要由外部传入脚本本身不做类别映射。这就是为什么压缩包里一定要有类别清单YOLO训练要求一个data.yaml文件里写到类别名称列表顺序就是id顺序如果这里和转换时用的class_id对不上模型训练出来的结果会是一个错位的黑匣子loss能掉但预测框对应的类别永远是错的。小数位保留6位是ultralytics的标准做法5位也够用但习惯了6位后和其他工具交换不容易出边界问题。3. 用Python脚本把VOC转成COCO和YOLO一张图看清坐标系的转变3.1 读取XML并解析bounding box注意坐标是整数还是浮点在实际转换整个数据集之前先解决一个脚本健壮性问题红外图片的EXIF信息里常带温度矩阵某些看图软件会把温度值写进XML的扩展节点导致标准XML解析器读到不认识的结构直接抛异常。我的做法是在parse_voc里用try...except包裹ET.parse对单个文件解析失败时跳过并打印文件名而不是让整个转换流程中断。数据集的标注质量参差不齐手标工具会时不时漏写一个bndbox闭合标签这种脏数据不值得为它终止全部处理。批量转换时文件数量是5000张单线程跑Python的XML解析大概要一两分钟这个耗时可以接受。如果数量到5万张以上才会考虑用lxml替代标准库lxml的C加速能带来5倍左右的提升。普通场景下不要因为性能问题引入额外依赖标准库的可移植性本身就是价值。坐标取整的处理我一般会在解析时保留float原始值转换到一半时再决定要不要取整。VOC转COCO时保留小数没问题COCO的bbox允许浮点VOC转YOLO时也保留小数因为归一化之后本来就是要更多位数才精确。只有在直接读XML核对标注时才把坐标转成int来可视化画框时cv2.rectangle要求整数坐标浮点传入会直接报类型错误。3.2 VOC转COCO从文件清单拼成JSON字典VOC转COCO的脚本核心不是换算坐标而是维护三个列表之间的id一致性。images列表的id从1开始递增annotations列表通过image_id指向对应的图categories列表的id从1开始给每个类别编号。这三者的关系如果在拼接时错位COCO格式的JSON就算写出来训练框架加载时也会报KeyError: image_id之类的错误而且这个错误往往在训练到一半才暴露。我习惯先把所有图片文件过一遍生成images列表同时建立file_name - image_id的映射再遍历所有XML生成annotations。两个循环分开写逻辑更清晰。生成JSON后必做的自检是统计images数量是否等于图片文件数统计annotations数量是否等于XML里object节点的总数两个数量对不上说明有解析遗漏。import glob import os def batch_voc_to_coco(voc_dir, img_dir, output_json): xml_list sorted(glob.glob(os.path.join(voc_dir, *.xml))) parsed_list [] file_name_map {} for img_id, xml_path in enumerate(xml_list, start1): item parse_voc(xml_path) base os.path.splitext(os.path.basename(xml_path))[0] img_file os.path.join(img_dir, base .jpg) if not os.path.exists(img_file): print(fmissing image: {img_file}) continue item[file_name] os.path.basename(img_file) item[image_id] img_id file_name_map[item[file_name]] img_id parsed_list.append(item) # 自检打印统计信息 total_objs sum(len(item[objects]) for item in parsed_list) print(fparsed images: {len(parsed_list)}, total objects: {total_objs}) voc_to_coco(parsed_list, output_json)这段代码里img_file的扩展名写死了.jpg如果数据集里混有.png的红外伪彩图这里就会漏掉一批。稳妥的做法是用glob搜一遍同目录下所有常见扩展名或者读XML里的filename节点拿原始文件名。红外数据集的图片格式比可见光数据集更混乱有的设备输出的是16位TIFF有的输出的是8位PNG统一成JPG或PNG再进训练路径能少踩很多坑。3.3 VOC转YOLO归一化公式与小数位保留VOC转YOLO是三种转换里唯一涉及数值精度问题的一环。归一化坐标是浮点数但如果框特别小——红外场景下人远距离时可能只有十几个像素——归一化后的数值会小到0.008左右。这时候保留6位小数是底线0.008333和0.008300之间的差异在原始像素上能差出好几个像素对目标中心点定位是致命的。另一个坑是完全没见过的某些转换工具把宽度归一化时误用了图片的高度做分母或者反之。红外图像如果是非标准尺寸比如640×512这种热像仪常见分辨率宽高不一致时这种混淆尤其隐蔽——因为你画出来看框的位置好像是对的但框的长宽比是错的。调试方法很简单转换完随机抽一张图把TXT里的坐标还原成像素坐标画在原图上肉眼对比一下标注位置。def batch_voc_to_yolo(voc_dir, img_dir, output_dir, class_names): class_map {name: idx for idx, name in enumerate(class_names)} xml_list sorted(glob.glob(os.path.join(voc_dir, *.xml))) for xml_path in xml_list: item parse_voc(xml_path) base os.path.splitext(os.path.basename(xml_path))[0] for obj in item[objects]: obj[class_id] class_map[obj[name]] out_txt os.path.join(output_dir, base .txt) voc_to_yolo(item, out_txt)class_names就是前面说的类别清单列表顺序决定了id。一个常见失误是转换完所有TXT后又改了class_names的顺序导致整个数据集的标签全部错位且无法追溯。我的习惯是在输出目录里放一份classes.txt快照和TXT文件放一起这样换机器、换环境时不会出现我记得当时用的顺序是...这种靠记忆维护的尴尬局面。4. 划分脚本的三种策略与训练教程里的关键参数4.1 train/val/test按比例划分保证同一视频帧不串集红外多目标检测的数据有一个特殊的分布问题连续帧之间内容高度相似。如果划分数据集时只是简单随机把文件列表打乱再按7:2:1切分那同一段视频的相邻帧很可能一半落在训练集一半落在验证集。验证集是从没见过的视频片段变成几乎见过的相邻帧mAP虚高得离谱部署后一上真实视频流性能立刻现出原形。划分脚本里必须加一层按数据来源分组的逻辑。如果文件名里带传感器ID或采集时间戳直接按前缀分组如果没有就按文件名中的序列号间隔切分比如每10帧取2帧归入验证集。一个简单的办法是在划分前先把所有图片按视频序列号排序再按序列号整体划分而不是按单张图片划分。import random import os from collections import defaultdict def split_dataset(img_dir, ratio(0.7, 0.2, 0.1), seed42): images sorted(os.listdir(img_dir)) groups defaultdict(list) for img in images: seq_id _.join(img.split(_)[:-1]) # 假设文件名形如 seq01_frame001.jpg groups[seq_id].append(img) seq_ids list(groups.keys()) random.seed(seed) random.shuffle(seq_ids) n_train int(len(seq_ids) * ratio[0]) n_val int(len(seq_ids) * ratio[1]) splits {train: [], val: [], test: []} for i, seq in enumerate(seq_ids): if i n_train: splits[train].extend(groups[seq]) elif i n_train n_val: splits[val].extend(groups[seq]) else: splits[test].extend(groups[seq]) for split_name, img_list in splits.items(): with open(f{split_name}.txt, w) as f: f.write(\n.join(img_list)) print(f{split_name}: {len(img_list)} images)按序列切分后统计一下三个集合的样本量就知道有没有严重失衡。ratio(0.7, 0.2, 0.1)里的seed42不是随便选的固定随机种子才能保证下次重新划分时结果可复现——否则你调了一晚上参数第二天重新跑划分脚本训练集和验证集的构成全变了之前的实验结果全部作废。4.2 红外场景下的训练配置anchors、imgsz与loss函数的取舍红外图像的分辨率普遍不像可见光那么高640×512、384×288是常见的传感器规格。这带来一个直接约束输入网络的imgsz不要盲目设成YOLOv5默认的640。输入尺寸大于原始分辨率时网络会对图像做上采样红外图像的细节本来就少上采样只会平滑掉目标边缘的微弱对比度。我一般建议imgsz和原始分辨率对齐640×512的红外图直接设imgsz640384×288的设imgsz384。Anchors方面YOLOv5/v8的自动anchor机制会在训练前用k-means重新聚类整个训练集的框尺寸。红外场景下目标框的分布和COCO数据集差很多远处的行人可能只有20×40像素近处的车辆可能占据半幅图像。让框架自动聚类出来的anchor会适配当前数据所以不要手工指定默认anchor除非你完全清楚自己在干什么。Loss函数的选择上YOLOv8默认的box_loss是CIoU这个在红外场景下表现正常。如果遇到了小目标漏检严重的情况可以把box_loss换成SIoU它对小目标的收敛更平滑代价是训练时间大约增加5%到10%。分类损失用默认的BCE即可红外多目标检测里的类别不均衡问题主要靠数据层面解决而不是改loss——这个后面避坑章节会展开。# data.yaml 示例 train: ./train.txt val: ./val.txt nc: 3 names: [person, car, hotspot]这个YAML里每个字段都对应一个可能的坑。train和val指向的是文本文档文档里每行是图片路径不是图片目录nc必须和names列表长度一致多一个少一个都会在训练启动时报维度不匹配names的顺序必须和TXT标签里的class_id对应顺序错位时训练不会报错只是模型的预测结果看起来张冠李戴。4.3 把预训练权重搬进红外模型冻结backbone微调的具体做法红外数据集虽然有5000张但相比COCO的几十万张还是小巫见大巫从零训练YOLO很容易陷入过拟合。正确的打开方式是加载COCO预训练权重冻结backbone层只训练head层跑二十个epoch后再解冻整个网络微调。这里的原理是COCO学到的底层特征边缘、纹理、颜色渐变在红外图像里依然有效——因为红外灰度图同样有边缘和梯度信息——真正要重新学的是目标尺寸分布和类别语义。python train.py --data data.yaml \ --weights yolov8n.pt \ --epochs 50 \ --imgsz 640 \ --batch 16 \ --freeze 10 \ --patience 10--freeze 10表示冻结前10层对YOLOv8n来说约等于冻结了整个backbone。--patience 10的意思是连续10个epoch验证集mAP没有提升就提前停止这是防止训练后期过拟合最直接的手段。--weights yolov8n.pt会自动从官方仓库下载如果下载失败多半是被墙换成手动下载后放到本地路径传入即可。这里注意不要从来路不明的第三方源下载预训练权重——权重文件里可以被植入后门修改网络层权重让模型在特定触发图像上输出错误结果。5. 红外多目标检测的避坑指南从数据翻车到训练失败5.1 现象loss降到0.05但mAP极低——原因在标签类别id错位损失函数降到0.05看起来模型收敛得很漂亮但验证集mAP只有0.2不到。这种低loss低mAP的组合最迷惑人。检查后发现标签文件里的class_id和data.yaml里的names顺序对不上TXT里写的0在names里对应person但class_map转换时用的是旧的类别清单0对应car。模型输出的框位置是准的学习的类别语义是错的。解决的办法很笨但有效转换标签时在TXT文件里附带一个classes.txt快照训练前先对比快照和data.yaml的names是否一致。不要相信任何我记得顺序肯定没问题的记忆文件系统里真实存在的快照才是唯一可信依据。5.2 现象训练集正常但验证集mAP为0——划分时同源视频帧串集训练集loss正常下降训练集mAP也正常但验证集mAP一直是0且不随epoch变化。排除了标签问题和训练参数后最终定位到数据划分脚本上随机划分没按视频序列分组验证集里出现的帧和训练集来自同一段视频。更糟糕的是由于红外视频帧间差异极小模型在训练集里背下来的特征在验证集里完全适用验证集上表现极好但mAP为0说明验证集和训练集分布差异大到模型完全无法泛化。这个现象的原理是训练集和验证集太相似时模型过拟合了训练集的噪声分布换到真正独立的验证集上立刻失效。解决方法是按视频序列划分并且在代码里记录划分结果文件每次训练前对比划分结果是否变更。5.3 现象红外小目标打不到——标签坐标归一化时少乘了width红外场景最典型的问题是远处的人目标极小只有十几个像素。网络在特征图下采样32倍后原图640×512分辨率下小目标的特征点还不到1个像素检测头根本看不到它。排查后发现我们的标签归一化时把w和h都除以了图片宽度而不是w除以宽度、h除以高度——对640×512的热像仪图来说这是12%的系统性误差直接导致预测框的长宽比畸变。解决方法是严格按照box_w / width和box_h / height分开归一化并在转换后抽样画框验证。小目标本身的召回率低不能全赖标签但归一化错误会让本就吃力的检测雪上加霜。5.4 现象BN层在红外数据上崩溃——batch size太小且没有冻结统计量训练到第20个epoch时loss突然NaN或者loss开始剧烈震荡。常见原因不是学习率太大而是batch size设成了4甚至2BN层的均值方差统计在这么小的batch上极不稳定尤其在红外图像这种整体低对比度的输入上BN统计量会在几乎没有纹理和局部高亮两种极端状态之间疯狂横跳。解决方法是把batch size调到至少8显存不够就降imgsz而不是降batch。如果只能用batch 4就要在YOLOv5里用--sync-bn开启跨卡同步BN或者在模型定义里把BN改为GroupNorm。另外预训练权重里的BN统计量是COCO数据的分布冻结backbone训练时BN统计量不会更新解冻后再训练时BN重新适配红外数据分布这期间loss有个短暂上升是正常的。5.5 现象训练中途显存溢出——缓存数据集与batch策略训练开始几十个epoch后OOM但刚开始训练时显存占用并不高。原因多数是ultralytics默认的cache策略在训练过程中把整个数据集缓存到了GPU内存——红外数据集如果包含大量未压缩的TIFF或PNG缓存占用的显存远超预期。OOM后再重新从硬盘读图训练速度骤降GPU利用率掉到30%以下。解决方法是训练命令里显式限制缓存策略--cache disk或--cache ram按需选择不要用默认的False边训练边缓存。批量大小按显存峰值模型权重激活值缓存图像来估算630×512的输入下YOLOv8n的激活值大约占1.5GB留出余量再分配batch。6. 训练完怎么验证混淆矩阵、PR曲线和一条自检命令模型train完不要只看results.png里那个train/val loss曲线就急着部署。第一步跑验证集拿混淆矩阵python val.py --data data.yaml --weights best.pt --conf-thres 0.25 --iou-thres 0.5。混淆矩阵会直接暴露哪两类目标最容易互相误检——红外场景里最常见的是把car和hotspot发热的汽车引擎盖混淆因为两者的温度轮廓高度相似这时候不是调阈值能解决的得回去补样本。第二步看三个类别的PR曲线关注每个类别的AP0.5和AP0.5:0.95。红外小目标类别的AP0.5:0.95通常比可见光低5到10个百分点这是传感器分辨率的天花板。如果person类别的AP明显低于其他类检查验证集里小尺寸person的占比用小目标增强的trick处理。第三个自检是随机抽几百张验证集图片跑一次批量推理把检测框画在伪彩图上直接肉眼看错检的情况这一步比任何指标都更能发现问题。from ultralytics import YOLO model YOLO(best.pt) results model.predict(sourceval_images, conf0.25, saveTrue, imgsz640) for r in results: boxes r.boxes.xyxy.cpu().numpy() labels r.boxes.cls.cpu().numpy() # 打印当前帧的所有检测框坐标和类别id print(f{r.path}: {list(zip(boxes.tolist(), labels.tolist()))})conf0.25是我做红外检测的默认起点。红外图像对比度低模型输出的置信度普遍比可见光低一截用0.5的阈值会把很多真实目标滤掉——先跑一遍0.25看输出再根据漏检率和误检率决定往0.15还是0.35调。imgsz必须和训练时保持一致不一致时模型推理结果会劣化这是YOLO的固有特性不算bug。部署时的教训是不要直接拿训练时的best.pt上生产。我会先导出成onnx格式再用onnxruntime做一次精度对齐测试输入一张验证集图片对比pytorch和onnxruntime的输出差异差异超过1e-4就要检查opset版本或动态轴配置。这套流程看起来繁琐但它能挡住一批训练没问题、部署就乱来的坑。希望这些踩坑记录能帮你在红外数据集上少走几趟夜路。本文还有配套的精品资源点击获取
返回列表