简介:HIT-UAV红外小目标数据集是一份面向目标检测、红外图像分析与无人机视觉研究的高质量标注数据,适合计算机视觉学习者、科研人员及算法工程师用于小目标检测模型的训练与验证。数据源自无人机在不同场景(学校、停车场、道路、游乐场等)拍摄的红外热像,覆盖人员、自行车、汽车及其他车辆,并记录飞行高度(60至130米)、相机视角(30至90度)与昼夜光照条件,为多维度算法评测提供了坚实基础。资源包共2002个文件,压缩后约192.88MB,以1994个XML标注文件为主体,包含每幅图像的边界框与类别信息;另含5个TXT文件提供训练集、验证集和测试集的划分,以及1个voc2yolo.py脚本,可将VOC格式标注一键转换为YOLO格式,省去手动整理的繁琐流程。对于需要直接使用标准红外数据集开展实验的研究者,这套数据已覆盖不同高度、角度和光照的场景,便于评估模型泛化能力。目前已有1520人学习下载,适合具备一定目标检测基础、希望在小目标场景下快速验证或微调模型的开发者使用。
1. HIT-UAV 红外小目标数据集:无人机视角下的红外目标检测到底难在哪
第一次拿到 HIT-UAV 红外小目标数据集时,我犯过新人都会犯的错:把它当普通可见光检测任务,通道改成 3、分辨率 640 就开跑。跑了一天,mAP 卡在 0.3 上不去。问题不在模型,而在数据本身:无人机高度带来的低分辨率、热成像单通道灰度、背景里大量与目标温度相近的干扰物,让红外小目标检测成为一个独立算法分支。这个数据集正是为这个分支准备的公开基准,它逼你回答一个问题:低分辨率热成像下,目标可能只有 8×8 像素,你怎么把它找出来。做算法预研、论文对比实验、带新人练手,它都合适;想直接拿它训练业务系统去部署,得先看清标注覆盖面和场景单调性。下面从数据体检讲起,把最容易浪费时间的环节一个个拆开。
2. 先搞懂这批红外图像:数据特性与标注格式决定后续一切
2.1 无人机高度和热成像带来的三个数据特性
无人机视角的红外图像有三个共性,这三个特性不是背景知识,而是后续每一步参数选择的决策依据。
第一个是小。以 640 宽的图像为例,一个行人可能只占 12×8 像素,目标框面积不到 100 像素。主流检测器对输入做多次下采样,到最低层特征图上,这个目标只剩不到半个像素的响应,几乎等于被抹掉。这就是很多通用检测模型在 HIT-UAV 上 mAP 极低的直接原因,不是模型“笨”,而是目标的感知粒度在深层特征图上已经不存在了。
第二个是背景复杂。红外图里地面、树木、建筑边缘会形成明显的高温热梯度,这些区域的局部纹理和目标边缘非常接近。模型很容易把背景高频边缘当成目标,产生一堆虚警。我第一轮实验里统计过误检分布,大多集中在屋檐、道路白线和树冠边缘,真目标反而因为轮廓不够“锐利”被漏掉。
第三个是单通道。红外灰度图没有颜色信息,只有热辐射强度。可见光预训练模型在 RGB 数据上学到的通道统计完全不匹配,直接加载权重会有一段明显的冷启动期,前几十个 epoch 的 loss 下降看起来像没在训练。这一点决定了我们在 3.2 节怎么处理输入通道。
放到一起,这三个特性把 HIT-UAV 和一个普通可见光检测数据集彻底区分开:目标小到难以感知、背景难到难以区分、输入信息量还少一个维度。后面所有决策,包括切图、高分辨率训练、小目标子集评估,全从这三个特性推导出来,而不是从模型结构推导。
2.2 标注格式:拿到手先判断是 XML 还是 txt
不少人在拿到数据集后的第一反应是找训练脚本,这最容易翻车。不同渠道流通的版本里,标注格式、目录组织、类别编号都不一定一致,可能是 PASCAL VOC 的 XML,可能是 YOLO 的 txt,也可能只有 JSON。所以第一步不是训练,而是数据体检。下面这套命令我会先跑一遍:
# 数据集目录体检 tree -L 2 /data/hit-uav | head -50 echo "--- XML 标注数量 ---" find /data/hit-uav -name "*.xml" | wc -l echo "--- YOLO txt 标注数量 ---" find /data/hit-uav -name "*.txt" | wc -l echo "--- 图像数量 ---" find /data/hit-uav \( -name "*.jpg" -o -name "*.png" -o -name "*.bmp" \) | wc -l数量关系直接告诉你标注格式:XML 和图像数量持平,基本是 VOC;txt 和图像数量持平,基本是 YOLO;两者都不是,就去翻 json。数量对不上也不用慌,红外数据集常有缺标注情况,少几个文件不代表数据坏了,但要把缺标注的图像从训练目录里排除,而不是让加载器报错。
确定为 VOC 后,我还会打开一个 XML 看内容:
head -40 /data/hit-uav/annotations/00001.xml重点看三个字段:name的实际取值,这决定类别映射表;bndbox里的坐标是整数还是小数,这决定转换脚本用整数还是浮点;size节点是否存在,如果存在且准确,可以做归一化分母,如果缺失,必须像 3.1 节那样从原图读尺寸。这三个字段哪怕只有一个和预期不同,后续标签转换的输出都会错得无声无息。
2.3 数据划分:按序列切,别按帧随机切
HIT-UAV 这类数据的图像来自飞行视频抽帧,同一目标经常连续出现在几十帧里。按帧随机划分训练集和验证集,等价于把同一个目标同时放进训练和验证,验证指标会明显虚高,落到新场景视频上立刻原形毕露。我在这上面吃过亏,之后处理无人机数据一律按序列切分,不再按帧切。
先确认每个图像的序列归属,常见做法是用上级目录名作为序列 ID。序列长度往往很不均匀,有的序列只有十几帧,有的几百帧,如果按目录个数直接 8:2 切,验证集可能只有几百帧。所以我一般先统计每个序列的帧数:
from pathlib import Path data_root = Path("/data/hit-uav") seq_count = {} for img_path in data_root.rglob("*.jpg"): seq_id = img_path.parent.name seq_count[seq_id] = seq_count.get(seq_id, 0) + 1 for seq_id, n in sorted(seq_count.items(), key=lambda x: x[1], reverse=True): print(seq_id, n)统计完帧数后,按帧数累计分配:把序列按帧数从大到小排序,依次放入训练集直到达到总帧数的 80%,剩下给验证集。这样两头样本量都足。如果数据集里还带飞行轨迹或时间戳元信息,优先按“拍摄地点/飞行架次”切分,那比按目录名更严格,能进一步避免同一片区域在不同架次里被重复采集导致的跨集合泄露。
3. 把 HIT-UAV 跑进检测器:从标签转换到训练的最小可复现流程
3.1 标签转换:从 XML 到 YOLO 的适配脚本
确认标注为 VOC 格式后,第二步是把 XML 转成 YOLO 格式。YOLO 系列的标签行格式是 class_id x_center y_center width height,坐标全部归一化到 0~1;VOC 保存的是绝对像素坐标。转换脚本的难点不在解析,而在异常数据处理。下面这个版本我沿用很久,主要胜在有两层防御:
import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image xml_root = Path("/data/hit-uav/annotations") img_root = Path("/data/hit-uav/images") out_root = Path("/data/hit-uav/labels") class_map = {"person": 0} # 以 2.2 节核查到的 name 实际取值为准 def convert_one(xml_path: Path): tree = ET.parse(xml_path) root = tree.getroot() img_file = img_root / (xml_path.stem + ".jpg") with Image.open(img_file) as im: w, h = im.size # 用真实图尺寸,不依赖 XML 的 size 节点 lines = [] for obj in root.findall("object"): name = obj.findtext("name") if name not in class_map: continue bbox = obj.find("bndbox") x1, y1 = map(float, (bbox.findtext("xmin"), bbox.findtext("ymin"))) x2, y2 = map(float, (bbox.findtext("xmax"), bbox.findtext("ymax"))) box_w, box_h = x2 - x1, y2 - y1 if box_w < 1 or box_h < 1: # 丢弃宽或高不足 1 像素的退化框 continue xc = (x1 + x2) / 2 / w yc = (y1 + y2) / 2 / h bw = box_w / w bh = box_h / h if not (0 < xc < 1 and 0 < yc < 1 and 0 < bw < 1 and 0 < bh < 1): continue lines.append(f"{class_map[name]} {xc:.6f} {yc:.6f} {bw:.6f} {bh:.6f}") out_path = out_root / (xml_path.stem + ".txt") out_path.write_text("\n".join(lines)) for xml_path in xml_root.glob("*.xml"): convert_one(xml_path)两处防御必须保留。第一个是box_w < 1过滤:红外小目标数据里真的有宽或高只有 1~2 像素的标注,这类框进入损失计算会产生剧烈数值抖动,过滤后训练更稳定。第二个是归一化后的范围检查,坐标必须落在 (0,1) 区间,一旦出现 0 或 1 说明某个坐标算错,要立即排查而不是忽略。class_map的取值必须来自 2.2 节打开的 XML 真值,不要凭印象填写。
转换完成后做一次可视化自查:把生成的 txt 标签画回原图,肉眼确认框的位置和目标边缘对得上。这一步推荐用 OpenCV 的rectangle直接画,不要依赖标注软件,因为我们要检查的是转换结果,不是原始标注。
3.2 训练配置:单通道、小目标的参数调整
进入训练之前,有几组参数需要针对 HIT-UAV 专门调整。第一个是输入分辨率,我习惯 640 起步;第二个是锚框,小目标数据集的框面积分布非常集中,通用锚框策略在小目标上不高效,常见做法是先对训练集真实框做 k-means 聚类,用聚类结果替换默认锚框;第三个是单通道输入问题。
红外单通道和预训练权重的 RGB 输入不匹配。常见做法是读取后复制成三通道,能跑通,但会引入冗余计算,而且预训练权重里本来就没有红外知识。我更推荐在数据加载层声明单通道,在模型入口用一层 1->3 卷积完成通道扩展。YOLO 系框架里改数据配置和模型输入层就能做到,并不复杂。
下面是一份 baseline 阶段的最小数据集配置:
# dataset.yaml path: /data/hit-uav train: images/train val: images/val nc: 1 names: ["person"] imgsz: 640 batch: 16 epochs: 150 patience: 20 workers: 8nc和单类别对应。如果另一份版本里包含多类别,names要按 label 文件里的 id 顺序对齐,不要只看名称排序。imgsz=640是一个平衡点:分辨率提高对小目标检测立竿见影,但显存和训练时间同步上涨,从 640 换到 1280 可能提升 5 个点以上的 mAP,训练耗时也可能翻倍。所以 baseline 固定 640,后续用切图方式获取高分辨率收益。
模型结构方面,我在小目标任务上一般对比下面几个配置,这张表不是绝对结论,但它反映了一个经验排序:
| 配置 | 显存占用 | 训练速度 | 小目标表现 |
|---|---|---|---|
| yolov8n + 640 | 低 | 快 | 一般,漏检偏多 |
| yolov8s + 640 | 中 | 中 | 略好 |
| yolov8n + 切图 | 中 | 中 | 提升明显 |
| yolov8s + 切图 | 高 | 慢 | 通常最优 |
小目标场景里,先切图后升模型,性价比最高。如果资源只允许你动一个变量,选切图。常见的反面教材是把可见光任务里那一套“换大模型、加数据增强”直接搬过来,最后发现训练时间翻倍,mAP 只涨零点几个点,本质上是目标在特征图里根本站不稳,换再大的模型也没用。
3.3 训练启动与资源估算
配置就绪后,训练命令本身很简洁:
yolo train data=/data/hit-uav/dataset.yaml \ model=yolov8n.pt \ imgsz=640 batch=16 epochs=150 \ project=/data/exp name=hit_uav_baseline \ device=0batch=16能否跑起来取决于显存。640 分辨率、yolov8n 在 16GB 显存显卡上比较宽松;8GB 卡建议降到 8,同时把workers调低,避免数据加载成为瓶颈。单卡足够覆盖 HIT-UAV 这种规模的 baseline,不需要一上来就分布式。patience=20会触发早停,验证指标连续 20 个 epoch 无改善时自动停止,建议始终打开。
训练启动后不要只盯 loss。我在这个环节会固定训练随机种子,并把数据划分、分辨率、batch 写进实验名。HIT-UAV 这类小数据集对随机种子非常敏感,同一个配置不同种子跑两次,mAP 可能差 2~3 个点,不记录种子的实验几乎无法复现。观察曲线的重点也放在验证集 mAP 上而不是训练 loss,训练 loss 下降到平稳只能说明模型记住了训练集。
4. 在 HIT-UAV 上训练最容易碰到的五个坑与排查顺序
4.1 小目标在多次下采样后直接消失
现象:训练 loss 下降正常,验证集 mAP 却是 0,或者只有零星几个检测框。
原因:模型经过多次下采样后,最低层特征图上 8×8 的目标连一个点都占不满,特征在前向传播中被抹掉。HIT-UAV 里这类目标占比高,很多通用配置下训练流程看起来一切正常,检测结果却像坏了一样全无响应。
解决:先用分辨率干预,把imgsz提高到 1280,不改网络结构。目标像素被放大后能在深层特征图上留下足够响应,mAP 通常立刻回升。如果显存不够,先用第 5 章的切图方案。
4.2 归一化坐标算错导致训练不收敛
现象:训练 loss 一直很大且不降,验证集上完全没有框。
原因:标签转换脚本出错,常见两类。一是整数除法把宽高截断成 0;二是不小心把x_center写成了x_min。这类错误不报异常,模型只是安静地学习错误坐标,表现就像训练坏了但又看不出坏在哪。
解决:转换完成后,打印几行标签文件,手动反算回像素坐标,确认和原图标注一致再开训练。我通常把可视化自查做成转换脚本的一部分,每次转完顺手画一批图,几分钟检查能省下数小时无效训练。
4.3 单通道灰度图被强行复制成三通道的隐性代价
现象:训练冷启动特别慢,收敛后 mAP 也比同配置预期低一截。
原因:多数框架默认输入三通道,如果直接在加载时把灰度复制三次,网络前面几层花大量参数学习冗余表达,来自可见光预训练权重的通道统计完全派不上用场。
解决:在数据加载层声明使用单通道,并通过网络入口层完成通道扩展。没有时间改网络结构时,可以先按三通道跑通流程,但把“三通道复制”记成一个变量,调优阶段再换单通道方案对比收益。
4.4 验证集和训练集共享同一目标序列
现象:验证 mAP 高得反常,能到 0.85 以上,但拿到任何新场景视频上检测效果都明显打折扣。
原因:数据划分时没有按序列切分,同一个目标的多帧被随机分到训练和验证,模型事实上见过验证目标。
解决:回到 2.3 节的按序列划分做法,按帧数加权分配。这是底线要求,不是可选优化。凡是无人机视频抽帧构成的数据集,都应该先做序列划分再做随机增强。
4.5 背景高频边缘与目标特征相似导致虚警
现象:漏检不算多,但地面建筑边缘、树冠、道路标线附近冒出一堆小框。
原因:红外图里背景高频边缘和目标局部纹理高度相似,模型区分不开。小目标本身的语义信息太少,能用于判别的特征几乎只有亮度和与周围的对比度。
解决:评估时把虚警数量单独统计,不能只看 mAP。调优时着眼难例挖掘,比如修改损失对背景难分样本的权重,或者切图时把边缘丰富区域保留在训练集里。5.3 节的 PD/FA 统计在这种场景下比 mAP 更接近业务感受。
5. HIT-UAV 进阶打法:切图、P2 检测层和红外指标验证
5.1 切图:把 16×16 的目标变成 32×32
切图是红外小目标检测里最直接的提点手段。把 640 图像切成 320 子图,目标像素面积等于放大一倍。切图的关键是同步修正框坐标:子图里的框坐标必须减去左上角偏移。下面是一个带重叠的切图逻辑:
def crop_with_shift(img, box, block_size=320, stride=256): h, w = img.shape[:2] out = [] for y0 in range(0, h - block_size + 1, stride): for x0 in range(0, w - block_size + 1, stride): x1, y1, x2, y2 = box if x2 < x0 or x1 > x0 + block_size: continue if y2 < y0 or y1 > y0 + block_size: continue new_box = (x1 - x0, y1 - y0, x2 - x0, y2 - y0) out.append(((x0, y0), new_box)) return outstride=256让相邻切块有 64 像素重叠,防止目标恰好在切块边缘被截断。切图后务必再跑一次可视化自查,我见过有人忘记修正坐标,结果模型输出位置整体偏移。相比直接拉高训练分辨率,切图对显存友好得多。
5.2 P2 检测层:把特征图分辨率翻倍
切图解决输入粒度,P2 检测层解决网络自身的空间粒度。在检测头增加高分辨率特征图输出,对小目标是实打实的增益,代价是训练时间变长,而且高分辨率特征图会放大背景纹理,虚警可能上升。我的建议是:先在切图基础上加 P2,再对比 5.3 节的小目标 mAP 和虚警数。两个指标都变好才留下 P2,否则在精度和部署成本之间不值得。
5.3 验证:看整体 mAP 不如看小目标子集和 PD/FA
整体 mAP 经常被中大型目标拉高,掩盖小目标漏检。我现在习惯按面积切子集单独评价。用 COCO 工具可以这样:
from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval gt = COCO("/data/hit-uav/val_gt.json") dt = gt.loadRes("/data/hit-uav/val_pred.json") ev = COCOeval(gt, dt, "bbox") ev.params.imgIds = [i for i in gt.getImgIds() if any(a < 1024 for a in [o["area"] for o in gt.loadAnns(gt.getAnnIds(imgIds=i))])] ev.evaluate() ev.accumulate() ev.summarize()这段代码把验证限制在至少含有一个面积小于 1024 像素目标的图像上,得到的是小目标子集 mAP。红外任务还习惯同时统计 PD 和 FA:PD 是真目标被检出的比例,FA 是虚警数量,两者放一起比 mAP 更贴近实际部署感受。我现在拿到一个新红外数据集,固定动作永远是先体检、再按序列划分、做完标签可视化,这三步没做扎实之前不会让训练脚本占上显存。小目标检测的差距往往不在模型结构,而在这些基础环节的较真程度,希望帮到你。
本文还有配套的精品资源,点击获取