简介:这是一份面向yolo系列算法学习者和目标检测开发者的热红外无人机目标检测数据集,共360张带标签图像,适用于yolov5、yolov7、yolov8、yolov9、yolov10及yolo11等主流模型训练与验证。压缩包内含1081个文件,包括360张jpg原图、360个yolo格式txt标注、360个voc格式xml标注,以及1个已配置好的data.yaml文件。txt与xml两种标注格式分目录存放,txt采用class x_center y_center width height的归一化坐标格式,可直接用于模型训练;xml则便于在LabelImg等工具中二次查看或转换。整个资源包仅6.72MB,小巧易下载,且数据集已预先划分好训练集与验证集,配合data.yaml即可快速启动训练流程,无需额外预处理。目前已有80人学习使用,适合需要在热红外无人机场景下开展目标识别实验、算法对比或毕设课题的初中级开发者。
1. 别急着开train:360张热红外无人机带标数据,先回答三个问题
前阵子和同事聊无人机巡检任务,他手里正好有一份yolo算法-热红外无人机数据集-360张图像带标签的压缩包,问能不能直接拿去训练YOLOv8。我的第一反应是:能用,但别急着把train命令敲下去。360张在目标检测里属于明显偏小的量级,无人机俯视角又让目标普遍只有几十个像素,这个组合决定了标签格式、数据划分、增强策略比模型选型更决定成败。这篇就按我处理这类数据的顺序来写:先盘包验标,再划分增强,然后跑通训练并讲清三个关键参数,最后补上我踩过的坑和一个专门的验证手段。适合手里拿着同类热红外无人机数据、正准备训练目标检测模型的从业者,无论你之前用YOLOv5还是YOLOv8,这套流程都成立。
2. 先盘清zip包:目录结构与YOLO标签格式的一次性校验
拿到压缩包先做三件事:确认目录结构、确认标签格式、确认坐标和类别编号合法。这三件事做完,数据能不能直接进训练才有结论,否则后面跑出来的mAP无从解释。
2.1 解压后按images/labels核对数量:两条命令摸清家底
我一般把压缩包解压到一个固定目录,避免中文路径和空格路径带来的各种奇怪报错。解压后先用find看目录层级,不用ls,因为find能一眼看出有没有目录嵌套。
mkdir -p thermal_uav && unzip "yolo算法-热红外无人机数据集-360张图像带标签.zip" -d thermal_uav cd thermal_uav find . -maxdepth 2 -type d | sort-d thermal_uav指定解压目录,带引号是为防止文件名里的特殊字符出问题。解压完常见结构是images/和labels/平级,但很多打包者会顺手把图片放进train/或JPEGImages/这类目录,find一下就能看清真实层级,别凭猜想写路径。
接下来核对图片和标签数量。360张图像,标签文件理论上也应该是360份,对应关系靠同名不同扩展名匹配。
echo "图片数: $(find images -type f \( -name '*.jpg' -o -name '*.png' -o -name '*.jpeg' \) | wc -l)" echo "标签数: $(find labels -type f -name '*.txt' | wc -l)"注意find里\( ... \)的转义,这是为了让多个扩展名条件作为一个整体参与逻辑运算,少了括号,find的-o判断会乱,统计出的数量就不准。数量对不上时,优先找空标签和无标签图片,这两类在YOLO里会造成样本不一致。如果目录里混着.txt和.xml,说明数据被多次加工过,先做格式统一,别直接混训。
提示:如果标签目录里同时存在txt和xml,以数量多的那种为主,另一种单独转格式,不要期待YOLO能同时读取两种标注。
2.2 读一条标签:YOLO归一化坐标和VOC XML怎么判断
YOLO系列标签是纯文本,每行一个目标,格式固定为class_id cx cy w h。cx、cy是目标中心点坐标,w、h是宽高,四个值全部是相对图片宽度和高度的归一化结果,取值在0到1之间。先抽一条看看:
cat labels/$(ls images | head -1 | sed 's/\.[^.]*$/.txt/')这条命令把第一张图片的同名txt打出来,输出类似这样就是标准YOLO格式:
0 0.435914 0.682774 0.121624 0.082204 1 0.715623 0.498121 0.091786 0.123048第一列是类别编号,必须从0开始连续,比如两类就是0和1,不能是0和2。后面四列是归一化坐标和尺寸。如果读出来是高像素值,比如0 623.4 1024.8 180.2 240.5,说明坐标没有归一化,直接训练会得到一堆无法解释的坏框,这类数据必须处理后再用。
如果解压出来是XML目录,那是VOC格式,结构里是<object><name>person</name><bndbox>这类标签。VOC对YOLO不能直接用,常见做法是转成txt。转换关键点有三个:类别字符串映射成整数编号、像素坐标除以单张图宽高、处理多目标。用ElementTree就能写:
import xml.etree.ElementTree as ET from pathlib import Path annotations = Path("Annotations") out_dir = Path("labels") out_dir.mkdir(exist_ok=True) class_names = [] # 顺序即类别编号,后续写data.yaml要用 for xml_file in annotations.glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: class_names.append(name) xmin = float(obj.find("bndbox/xmin").text) ymin = float(obj.find("bndbox/ymin").text) xmax = float(obj.find("bndbox/xmax").text) ymax = float(obj.find("bndbox/ymax").text) cx = (xmin + xmax) / 2 / img_w cy = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 防止负宽高和越界坐标,越界严重的框还是要回原标注修 cx, cy = min(max(cx, 0), 1), min(max(cy, 0), 1) w, h = min(w, 1), min(h, 1) lines.append(f"{class_names.index(name)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") (out_dir / f"{xml_file.stem}.txt").write_text("\n".join(lines))这段逻辑很直接:从XML取图片宽高,逐目标计算归一化中心点和宽高。class_names列表的顺序就是类别编号顺序,之后写data.yaml会直接用到,别随手打乱顺序。末尾的clip是兜底手段,能挡住轻微越界,但如果框架本身和图片尺寸对不上,clip只是把错误藏起来,该修标注还是得修。
2.3 批量校验坐标与类别编号:连续class_id是硬要求
转完格式不等于能训练。一次把所有标签扫一遍,比等训练跑一半再排查省事得多。我习惯写个几十行的校验脚本,把三类问题一次查出来:坐标越界、宽高非正、类别编号不连续。
from pathlib import Path labels_dir = Path("labels") all_ids = set() bad_lines = [] for txt_file in labels_dir.glob("*.txt"): for line in txt_file.read_text().strip().splitlines(): parts = line.split() if len(parts) != 5: bad_lines.append((txt_file.name, "字段数不等于5", line)) continue cls_id = int(parts[0]) cx, cy, w, h = map(float, parts[1:]) all_ids.add(cls_id) if not (0 <= cx <= 1 and 0 <= cy <= 1): bad_lines.append((txt_file.name, "中心坐标越界", line)) if w <= 0 or h <= 0: bad_lines.append((txt_file.name, "宽高非正", line)) if w > 1 or h > 1: bad_lines.append((txt_file.name, "宽高超过图片", line)) print("类别编号:", sorted(all_ids)) for err in bad_lines[:20]: print(err)这里有个边界需要说透:归一化后cx和cy理论上严格在0到1之间,但标注时手一抖就可能算出1.0001,一般不影响训练,重算一次就行。真正致命的是w和h大于1,那说明某个框比整张图还大,大概率是标签和图片没对齐,这种标签对应的图要单独拎出来看。类别编号如果出现0和2而没有1,训练不会报错,但推理结果会张冠李戴,类别名和编号错位。
到这个步骤结束,数据能不能用、哪些条目要修,心里就有数了。
3. 把360张切成train/val再增强:固定种子划分与热红外专用增广
小数据集训练,划分的科学性和增强的针对性,决定模型是在学“目标特征”还是背图。360张不多,每一步都必须稳。
3.1 按文件名前缀分组划分:避免同一场景帧泄漏到val
最常见的错误是直接对全量文件做随机打乱再按比例切。无人机数据里大量图片来自同一段航拍视频抽帧,相邻帧之间背景和目标位置几乎一样。如果train里有一帧,val里正好有它的下一帧,val精度会虚高,你以为模型学到了,其实是认出了背景。这不是玄学,是数据泄漏。
我现在会先看文件名命名规律。很多抽帧数据文件名带帧序号,比如frame_000123.jpg。按每100帧一个场景桶分组,再在桶的层面做随机划分,这样同一段航线的相邻帧不会一个进train一个进val。
import random import shutil from pathlib import Path random.seed(42) img_dir = Path("images") label_dir = Path("labels") dst = Path("datasets/thermal_uav") def prefix_of(stem): try: seq = int(stem.split("_")[-1]) # frame_000123 -> 123 return stem.rsplit("_", 1)[0] + f"_{seq // 100:03d}" except ValueError: return stem # 无序号时按整个stem一组 groups = {} for img_path in img_dir.glob("*.jpg"): groups.setdefault(prefix_of(img_path.stem), []).append(img_path.stem) all_stems = [stem for group in groups.values() for stem in group] random.shuffle(all_stems) n = len(all_stems) train_stems = set(all_stems[:int(n * 0.8)]) val_stems = set(all_stems[int(n * 0.8):int(n * 0.9)]) test_stems = set(all_stems[int(n * 0.9):]) for split, stems in [("train", train_stems), ("val", val_stems), ("test", test_stems)]: (dst / split / "images").mkdir(parents=True, exist_ok=True) (dst / split / "labels").mkdir(parents=True, exist_ok=True) for stem in stems: shutil.copy(img_dir / f"{stem}.jpg", dst / split / "images") shutil.copy(label_dir / f"{stem}.txt", dst / split / "labels") print(len(train_stems), len(val_stems), len(test_stems))划分比例这里,360张我设成8:1:1。train约288张,val约36张,test约36张。test是最后评估用的,调参全程只看val,能不碰就不碰。固定seed=42比随机数发生器本身更重要,以后改增强参数对比效果,seed不一致,模型看到的数据顺序全变了,没法归因。
四个边界要注意:一是文件名不带序号时,整个stem变成一组,这种退化成近似随机切分,可接受;二是分组后每组图片数量差异大,小分组直接整组放进train,别硬塞进val;三是val/test尽量覆盖多个场景桶,不要按顺序截取前20%,那等于用一个场景当验证集;四是如果zip里已经按train/val分好文件夹,不用重切,但仍要确认划分是不是跨场景的。
3.2 热红外增强的3个有效选择:翻转、旋转、灰度抖动
360张训练图不管怎么划分都是典型小样本,增强不是可选项,是训练的一部分。但热红外和可见光的增强策略差别很大。彩色图常用的HSV色彩扰动,对热红外要拆开看:单通道灰度图做HSV没有意义;伪彩图做HSV会把温度到颜色的映射关系直接搅乱,训练出来的模型换个调色板就失灵。
我常用的三件套是水平翻转、小角度旋转、灰度对比度抖动。水平翻转最安全,目标语义不变;上下翻转在很多场景会破坏“目标都在地面”的先验,默认不开;旋转角度控制在正负15度,超过这个范围,目标框的宽高比会失真。用albumentations写最干净:
import albumentations as A from pathlib import Path import cv2 aug = A.Compose([ A.HorizontalFlip(p=0.5), A.Rotate(limit=15, p=0.5, border_mode=cv2.BORDER_CONSTANT), A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.5), ], bbox_params=A.BboxParams(format="yolo", label_fields=["class_labels"])) src_img_dir = Path("datasets/thermal_uav/train/images") src_lbl_dir = Path("datasets/thermal_uav/train/labels") dst_img_dir = Path("datasets/thermal_uav_aug/train/images") dst_lbl_dir = Path("datasets/thermal_uav_aug/train/labels") for txt_file in src_lbl_dir.glob("*.txt"): img = cv2.imread(str(src_img_dir / f"{txt_file.stem}.jpg")) boxes, labels = [], [] for line in txt_file.read_text().strip().splitlines(): cls, cx, cy, bw, bh = map(float, line.split()) boxes.append([cx, cy, bw, bh]) labels.append(int(cls)) for i in range(3): # 每张原图扩3张 out = aug(image=img, bboxes=boxes, class_labels=labels) cv2.imwrite(str(dst_img_dir / f"{txt_file.stem}_aug{i}.jpg"), out["image"]) lines = [] for bbox, label in zip(out["bboxes"], out["class_labels"]): x1, y1, x2, y2 = bbox[:4] # albumentations返回归一化xyxy,要换算回yolo格式 nx = (x1 + x2) / 2 ny = (y1 + y2) / 2 nw = max(x2 - x1, 0) nh = max(y2 - y1, 0) if nw > 1e-4 and nh > 1e-4: lines.append(f"{label} {nx:.6f} {ny:.6f} {nw:.6f} {nh:.6f}") (dst_lbl_dir / f"{txt_file.stem}_aug{i}.txt").write_text("\n".join(lines))这里最容易出错的是坐标格式转换。albumentations的BboxParams(format="yolo")要求输入[cx,cy,w,h]归一化,但输出是归一化xyxy,必须重新换算回YOLO需要的cx,cy,w,h。旋转出图边界时,新坐标可能为负,脚本里对nw/nh的过滤不能省。如果增强把目标切到只剩一小块,可以再加min_visibility过滤,训练时这种半个框的目标会干扰边界回归。
提示:这个增强只做了3倍,288张train变成约860张。不要一步做成10倍20倍,边际收益衰减很快,后面细说。
3.3 增强倍数不是越大越好:360张该翻到多少合适
增强的边际收益衰减非常快。我做过对比:同一份数据翻4倍和翻10倍,val的mAP差距基本在1个点以内,但训练时间翻了两倍以上。几何增强翻3到5倍足够,再多会生成大量高度重复的样本,模型学到的分布被增强分布带偏。288张train翻4倍就是1152张等效样本,配合训练轮次,已能把这批数据的表达能力压榨出来。
省下的时间不如花在imgsz和epochs上。YOLOv8自带的mosaic和fliplr在训练时还会在线增强,离线增强没做好,在线增强也会把错误分布放大。所以离线增强只要保证图足够多样、标签正确就够了,后续mosaic的参数还要单独调,这是第5章的一个坑。
4. 跑通第一个YOLO模型:data.yaml、训练命令与三个决定成败的参数
数据准备好就进入训练。这一章的命令是最小可运行版本,但参数必须按“小样本热红外+无人机视角”去调,不能照搬通用目标检测教程。
4.1 写data.yaml:路径、类别数和设备参数的坑
YOLOv8用data.yaml描述数据集,里面要写path、train、val、nc、names。最容易踩的坑是path写相对路径,训练命令在别的目录下执行,相对路径一错train目录就找不到,直接报错。我习惯把yaml放在数据集根目录下,用相对路径组织train/val。
# thermal_uav.yaml path: /path/to/datasets/thermal_uav_aug train: train/images val: val/images test: test/images nc: 2 names: 0: person 1: carnames的顺序必须和第2章校验脚本看到的class_id一一对应。nc声明错误是最隐蔽的:声明2类但标签里有3类,训练不报错,推理时类别错位,等到评估mAP才发现一团乱。写yaml前,把第2章脚本print出来的sorted(all_ids)再对一遍。
设备参数也有坑:多人共用服务器时,device=0可能撞车。多卡机器上先跑一下nvidia-smi看占用,再决定用哪张卡。
4.2 最小训练命令与参数表:epochs、batch、imgsz怎么定
用ultralytics训练,一条命令能跑,但我建议写成Python脚本,参数多时改起来方便,还能用git diff追踪改动。
yolo detect train \ data=thermal_uav.yaml \ model=yolov8s.yaml \ pretrained=true \ epochs=200 \ batch=8 \ imgsz=640 \ mosaic=0.5 \ close_mosaic=10 \ patience=30 \ cache=True \ device=0命令行版的好处是直接,但model=yolov8s.yaml是网络结构定义,预训练权重另用yolov8s.pt加载。只给model=yolov8s.pt也行,但小数据集上我习惯显式分开写,避免加载路径模糊。下面这组参数是我跑同类数据时固定用的:
| 参数 | 我的设置 | 理由 |
|---|---|---|
| imgsz | 640 | 360张小数据用640起步合理,1024对小目标更友好但显存和过拟合风险同步上升 |
| mosaic | 0.5 | 默认1.0在小目标数据上切碎目标,降到0.5保留上下文 |
| close_mosaic | 10 | 最后10轮关mosaic,让模型在接近真实分布的数据上收敛 |
| patience | 30 | 小数据val波动大,30轮能兜住过拟合点 |
| batch | 8 | 显存不够就4,batch在小数据集上比大目标数据更敏感 |
batch这个参数容易被忽略。360张数据的batch开32,一个epoch几步就走完,BN统计量都不稳定。8在这个规模上是起步值,显存只有6G就跑batch=4,不要硬冲。
Python方式等价:
from ultralytics import YOLO model = YOLO("yolov8s.yaml").load("yolov8s.pt") results = model.train( data="thermal_uav.yaml", epochs=200, batch=8, imgsz=640, mosaic=0.5, close_mosaic=10, patience=30, cache=True, device=0, )load("yolov8s.pt")是加载COCO预训练权重做迁移学习。小数据集这一步几乎是必须的,从零训练360张根本学不出泛化特征。cache=True把图像缓存进内存,小数据集显存换速度很划算。
4.3 看loss曲线判断过拟合:别等best.pt自己骗你
训练完看runs/detect/train/weights/里的best.pt和last.pt,但这只是开始。真正要读的是results.png里的两条loss曲线。小数据集最典型的图是train_loss持续下降,val_loss先降后升,在某个epoch开始分道扬镳。那个拐点就是模型开始背图的时刻。
YOLOv8的patience=30会触发早停,但你要明白best.pt是在val指标最高点保存的,不是loss最低点。如果val_loss一直低位摆动,说明模型没吃饱,epochs可以再加。如果val_loss从一开始就乱跳,优先怀疑数据问题,别急着调学习率。
值得留意的一个黑匣子:热红外小目标数据的val_loss波动通常比可见光大得多,单次val精度高不代表模型好。我训练完不急着部署,先跑一次test集推理,或者直接拿val做人工看图,确认框的位置符合直觉。如果val的mAP50有0.8但图片上一堆框偏在人脚下,说明标注有系统偏移,这类问题靠增强和调参都解决不了,只能回标注修。
5. 避坑:小样本热红外训练最常见的5个翻车点
这一章是我做过项目里踩得最扎实的5个坑,每一条都按现象、原因、解决的顺序写。前两条属于数据问题,中间两条属于热红外特有的域问题,最后一条是训练配置问题。
5.1 标签坐标越界但训练一切正常,mAP却起不来
现象:训练loss正常下降,val的mAP50始终在0.2以下徘徊,把预测画出来,框全在图片边缘或者飞在图片外面。
原因:标注文件里的坐标没有归一化。很多标注工具导出时保留的是像素坐标,直接塞进YOLO训练。YOLO对越界框不报错,它只会把越界值按原样学进去,模型输出的坐标也跟着越界。
解决:回到2.3的校验脚本,把坐标字段全量看一遍。如果发现像素级坐标,按2.2的转换逻辑重算:cx除以单张图宽度,cy除以单张图高度,w和h同理。注意热红外图像有些是640×512,有些是3840×2160的大图,批量重算前先确认每张图的宽高取的是当前图自己的尺寸,不能用整个数据集的统一值去算,否则小图大框的错误会换一种形式存在。
5.2 train/val划分泄漏:训练精度虚高,验证精度跳水
现象:train loss正常、val loss也正常,但把训练好的模型放到完全没见过的场景里,召回率惨不忍睹。回头检查train和val,发现里面有几乎一样的图。
原因:连续帧泄漏。抽帧数据如果不按场景分组划分,train和val会拿到同一航次相邻两帧,val精度看起来很高,实际是模型记住了背景纹理。
解决:按3.1的分组逻辑重切。文件名看不出规律时,看EXIF拍摄时间戳聚类后再切。另一个细节是test集要刻意选模型没见过的场景,哪怕test的mAP低一点,这也是真实水平。val可以看成调参的仪表盘,test才是交付时的成绩单,两者用途不要混。
5.3 高空小目标漏检:640输入就注定看不清
现象:训练和val精度都还可以,部署到整张无人机大图上,小目标几乎全部漏掉,只检到大目标。
原因:无人机飞行高度高时,目标在原始大图里可能只有20×20像素。imgsz=640意味着原始图被缩到640,20像素的小目标缩放后只剩几个像素,特征直接消失。这是分辨率问题,不是模型能力问题,换更大的模型也一样。
解决:两个方向。一是imgsz提到1024,显存不够就减小batch;二是把大图切块再送进模型,也就是第6章要写的SAHI切片推理。对360张这种规模,我更推荐后者,改动小、见效快。判断小目标的标准很简单:目标像素少于32×32,就按小目标流程处理,不要指望默认参数覆盖。
5.4 伪彩热红外被当成三通道:HSV增强把温度映射搅乱
现象:增强后的训练图颜色变得很奇怪,原来红色的人变成绿色,训练出来的模型在现场热红外观测图上完全失灵。
原因:热红外相机输出的大多是伪彩图,白热、黑热、铁红等调色板映射。颜色本身携带的是温度信息,不是物体真实颜色。对伪彩图做HSV扰动,等于把温度信息随机篡改,模型学到的颜色“语义”和采集设备对不上。
解决:先确认图像是单通道灰度还是伪彩三通道。单通道只做灰度级的亮度、对比度扰动;伪彩图关闭所有Hue/Saturation相关增强,只保留空间几何增强。用albumentations时,把RandomBrightnessContrast保留,HueSaturationValue这类transform全部删掉。判断方法很简单:用OpenCV读一张图,如果三通道数值几乎相同就是灰度映射,如果明显不同就是伪彩。
5.5 mosaic把目标切碎:最后十轮记得关掉
现象:训练时loss曲线很漂亮,部署后小目标漏检比val结果差一截。
原因:YOLOv8默认mosaic=1.0,训练时把4张图拼在一起随机缩放,拼接边界会直接切开目标。小目标本来像素就少,被切一半后,模型在训练时压根没见过完整的它,自然检不出来。
解决:mosaic=0.5并把close_mosaic=10,最后10个epoch关掉mosaic,让模型在正常完整目标上做最终收敛。这两个参数在4.2的表格里给过,这里再强调一次,因为默认值最容易直接带进生产环境。如果你用SAHI切片推理,mosaic切碎的问题会被放大,切片边界再切一次,目标被切断两次,漏检是必然的。
6. 进阶:用SAHI切片推理验证best.pt,别拿640小图直接量产
模型训练完,验证要放在真实大图上,而不是只信val的mAP。无人机单张原始图动辄几千万像素,直接缩到640输入会丢掉小目标。SAHI是专门解决这个问题的切片推理库,把大图切成有重叠的小块,逐块推理,再合并回原图坐标。切块之后,小目标在切片里的像素占比变大,检测能力直接上一个台阶。
from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model = AutoDetectionModel.from_pretrained( model_type="ultralytics", model_path="runs/detect/train/weights/best.pt", confidence_threshold=0.3, image_size=640, device="cuda:0", ) result = get_sliced_prediction( "big_uav_image.jpg", detection_model, slice_height=640, slice_width=640, overlap_height_ratio=0.2, overlap_width_ratio=0.2, ) result.export_visuals(export_dir="sahi_output/", file_name="big_uav_image")slice_height和slice_width设成640,和训练时的imgsz保持一致;重叠率0.2是为了让切在边界上的目标至少在一张切片里保持完整。confidence_threshold=0.3是验证阶段的值,部署时按业务容忍度在0.25到0.5之间调。现在我的习惯是拿SAHI跑完原始大图,和整图推理做对比,把漏检目标数量化,再决定是补数据还是调阈值。
这套流程我固定用了快两年,最深的教训是数据没验干净之前,后面所有调参都是在给错误地基加固。你拿到这份360张的热红外无人机数据,第一步永远是打开zip,把标签和划分看清楚,再谈训练。希望帮到你。
本文还有配套的精品资源,点击获取