简介:YOLO红外飞机小目标检测数据集面向目标检测入门与进阶学习者,也适合需要开展红外弱小目标识别研究的工程师和学生,提供1000张来自真实场景的高质量红外飞机图片,覆盖多种背景与目标尺度,可有效支撑小目标检测算法研究与模型验证。数据使用LabelImg标注,标注框质量可靠,同时提供VOC(xml)、COCO(json)与YOLO(txt)三种格式标签,分别存放,可直接接入YOLO系列训练流程,免去自行格式转换的麻烦。压缩包共2000个文件,包含1000个xml标注文件、990个txt文本文件(主要为YOLO格式标签),以及6个html图文教程、3个py划分脚本和1个yaml配置文件,整体约13.29MB。教程覆盖Linux与Windows两种环境下的YOLO环境搭建,并结合案例演示如何修改配置训练自己的数据集;划分脚本可灵活生成训练集、验证集和测试集,显著降低上手门槛。目前已有302人学习下载,适合需要快速获取高质量红外飞机小目标数据集并完成环境配置、模型训练与验证的读者。
1. 红外飞机小目标检测数据集:1000张红外图能不能喂出一个能用的YOLO模型
夜视、云层遮挡、远距离监视,这几个场景一叠加,红外飞机小目标检测就成了目标检测里最磨人的一块。你拿YOLO跑常规数据集顺风顺水,一到红外图就原形毕露:飞机在画面里只有十几二十个像素,没有颜色纹理,背景又是大片大片的暗区,检测器要么漏检,要么把云层噪声当目标框出来。这个题为"YOLO红外飞机小目标检测数据集"的压缩包,解决的正是这个痛点——它把1000张红外飞机图像整理成了可以直接喂给YOLO的工程目录,附上voc、coco、yolo三套格式的标签、划分脚本和训练教程,省掉你自己标注、自己转换、自己踩坑的时间。适合谁?刚接触小目标检测、想在红外场景下用YOLO出结果的研究生和工程师,以及需要一份标准数据来验证检测算法改进效果的开发者。一个反直觉的结论先放这儿:1000张图对YOLO来说不算多,但只要类别单一、标注规整、训练参数调对,单类红外飞机检测完全能跑到可用水平,关键在数据组织和参数设置。
2. 拆解数据集:红外小目标的三套标注体系与划分逻辑
2.1 红外图里的"小目标"到底多小:尺寸、信噪比与检测难点
小目标检测和常规目标检测的第一道分水岭是目标尺寸。COCO数据集里把面积小于32×32像素的目标定义为小目标,而红外飞机场景里,很多目标连这个门槛都够呛。1000张红外图里,如果飞机出现在远景,它的包围框可能只有10×10到25×25像素,占整张图的面积比例往往不到0.1%。目标面积小意味着网络下采样几次之后,目标在特征图里只剩一两个像素点,检测头根本拿不到有效特征。
更麻烦的是红外图像本身的特性。可见光图像有丰富的边缘、纹理、颜色信息,而红外图像里目标和背景的灰度差往往很小,目标只是一个稍微亮一点的斑点,没有轮廓细节。加上红外传感器本身的噪声,云层边缘、地物热辐射都可能形成和目标灰度相近的干扰。所以在处理这类数据集时,不能只看标签格式对不对,还要关注两个隐含信息:目标尺寸分布范围和图像背景的灰度分布。拿到压缩包后,第一步应该做的不是直接训练,而是统计标签里所有包围框的宽高分布,确定目标到底是"小"还是"极小",这直接影响后文的anchor策略和推理时的输入分辨率。
2.2 voc、coco、yolo三种标签格式:同一张图三种坐标写法怎么换算
这个压缩包的最大价值在于同一批图片给了三套标签。为什么需要三套?因为不同训练框架吃不同的格式:voc格式是目标检测老牌数据集Pascal VOC的XML标签,Darknet系的YOLOv5/YOLOv8原生用txt格式,而coco格式是通用目标检测数据集COCO的JSON标注,很多检测框架和评估工具都以COCO为基准。
voc格式里,一个目标用一个XML节点描述,核心字段是bndbox里的xmin、ymin、xmax、ymax,单位是像素,表示包围框左上角和右下角坐标。coco格式则是一个大JSON文件,每张图对应一个annotation条目,bbox字段是[x, y, width, height],同样以像素为单位,同时还有area、segmentation、iscrowd等字段。yolo格式最不一样:每行一个目标,五个数字,依次是class_id、x_center、y_center、width、height,前两个是包围框中心点坐标,后两个是宽高,四个值全部除以图片宽高做了归一化,所以取值范围是0到1之间的小数。
三者换算的核心公式是:
xmin = (x_center - width / 2) * img_w ymin = (y_center - height / 2) * img_h xmax = (x_center + width / 2) * img_w ymax = (y_center + height / 2) * img_h反向换算时同理,用像素坐标减去半宽半高得到左上角,再除以图像宽高得到归一化值。看起来简单,但实际动手时踩坑最多的地方有两个:一是voc转yolo时忘了除以图像宽高,导致train时坐标全部大于1被判为无效框;二是coco的bbox和voc的xmin不一致——voc用的是左上右下,coco用的是左上+宽高,不少人转格式时空着area字段不填,后面跑评估脚本时报KeyError。下面给出一段可复用的voc转yolo核心函数,读取XML并输出YOLO格式的txt文件。
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_txt_path, class_names): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) lines = [] for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in class_names: continue cls_id = class_names.index(cls_name) bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h width = (xmax - xmin) / img_w height = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") with open(out_txt_path, 'w') as f: f.write('\n'.join(lines))这段代码先从XML的size节点读出原图真实宽高,再遍历所有object节点,取出类别名和包围框坐标,按公式做归一化后写入txt。注意两个细节:类别名必须预先定义好class_names列表,且顺序不能变,因为YOLO的类别编号按列表顺序生成,训练时dataset.yaml里的names顺序必须和这里完全一致,否则会出类别错位的玄学问题;另外边界框坐标超出图像范围时,有人直接clip到[0,1]没问题,但最好先检查标注本身是否越界,而不是盲目裁剪。
2.3 1000张图的划分逻辑:比例只是表象,防泄漏才是关键
有了图片和标签,下一步是划分train/val/test。大多数人随手按8:1:1随机切分,但红外飞机数据集有个容易被忽视的坑——数据可能存在时间相关性。如果这1000张图是连续视频帧抽出来的,相邻帧里的飞机位置、姿态高度相似,随机划分很可能让同一段连续帧既出现在训练集又出现在验证集,val指标虚高,模型实际部署效果大打折扣,这就是典型的数据泄漏。
正确做法是划分前先看图片文件的命名或拍摄时间信息。如果文件名带时间戳,或者推断出是视频抽帧而来,应该按时间顺序把整个视频片段切分成三段,再分别分入train、val、test,保证验证集和训练集里的飞机来自不同片段。如果文件名没有时间信息,退一步也要把同一批连拍的图片手动归组,再按组划分。训练起来之后如果val的mAP高得离谱而测试时效果很差,大概率就是验证集泄漏了,这时回头改划分脚本比加数据更有效。
划分脚本本身要保证图片和对应的三套标签同步移动,不能只搬图不搬标签。常见的做法是建立文件名映射关系,以不带后缀的图片名为key,分别查找同名.xml和同名.txt以及记录在coco.json里的image_id,移动到对应目录。这一步放到第3章的划分脚本里一并处理。
3. 把压缩包装成工程:目录规范、格式转换脚本与划分脚本的改造
3.1 解压后先干三件事:建目录、画框检查、核对类别
压缩包解压出来,先别急着配环境。我拿到任何数据集的第一件事永远是画框检查,因为标注质量决定了训练效果的上限。用OpenCV把XML里的包围框画回原图,肉眼扫一遍,重点看三类问题:框有没有明显偏移目标、有没有漏标的目标、有没有类别标错的。红外图里目标和背景对比度低,标注时很容易框大一圈或框到背景噪声上,这种错标在训练时会被模型当成"正确答案"学进去,后期再调参也救不回来。
目录建议按YOLO工程的标准结构组织,方便后面直接用ultralytics训练:
dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── annotations/ │ ├── train.json │ └── val.json └── dataset.yamlimages和labels目录是YOLO训练的直接输入,annotations目录放coco格式的JSON,供需要coco格式的框架或评估工具使用。如果你手头的压缩包结构不是这样,就用脚本整理成这个标准结构。类别核对也在这里做:打印所有标签里的类别名集合,确认是否只有一类"airplane"或类似命名,类别数量要小于10,否则后文训练配置和评估都会变得更复杂。
3.2 转换脚本改造:给voc转yolo脚本加上coco输出
第2.2节给的是单张XML转txt的核心函数,实际工程里要把它包装成批量处理,并同时产出coco格式的JSON。常见做法是分两步:先批量把XML转成yolo的txt,再从同一批XML生成coco JSON。这里给出批量转换加coco导出的完整脚本骨架。
import os import json import xml.etree.ElementTree as ET from glob import glob def voc_to_yolo_batch(xml_dir, label_out_dir, class_names): os.makedirs(label_out_dir, exist_ok=True) for xml_path in glob(os.path.join(xml_dir, '*.xml')): name = os.path.splitext(os.path.basename(xml_path))[0] voc_to_yolo(xml_path, os.path.join(label_out_dir, name + '.txt'), class_names) def voc_to_coco(xml_dir, json_out_path, class_names): images, annotations = [], [] ann_id = 1 for img_id, xml_path in enumerate(glob(os.path.join(xml_dir, '*.xml'))): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find('size/width').text) img_h = int(root.find('size/height').text) file_name = root.find('filename').text images.append({ 'id': img_id, 'file_name': file_name, 'width': img_w, 'height': img_h }) for obj in root.iter('object'): cls_name = obj.find('name').text if cls_name not in class_names: continue bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) w, h = xmax - xmin, ymax - ymin annotations.append({ 'id': ann_id, 'image_id': img_id, 'category_id': class_names.index(cls_name), 'bbox': [xmin, ymin, w, h], 'area': w * h, 'iscrowd': 0, 'segmentation': [[xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax]] }) ann_id += 1 coco = { 'images': images, 'annotations': annotations, 'categories': [{'id': i, 'name': n} for i, n in enumerate(class_names)] } with open(json_out_path, 'w') as f: json.dump(coco, f)这个脚本里有几个参数值得解释。class_names列表是全局约定,voc_to_yolo和voc_to_coco必须传入同一个列表,才能保证txt里的类别编号和JSON里的category_id对应。segmentation我按多边形框填了四个顶点,如果后续你要训练实例分割模型,这个字段就能直接复用;不用的框架往往只读bbox,也不会报错。area字段务必填上,很多coco评估工具会在算AP时读取area做尺寸分组,缺失会直接跳过该目标导致指标偏低。
3.3 划分脚本:图片和三套标签同步移动,并做孤儿文件检查
划分脚本是整个流程里最容易出低级错误的一步,常见翻车现场是:图片分了train,标签没跟上,训练时每张图都找不到标签文件,ultralytics直接跳过这些图,你还不容易察觉。我习惯把划分脚本写成"按图片主名同步搬移"的方式,再跑一遍一致性检查。
import os import shutil import random from glob import glob random.seed(42) img_dir = 'dataset/images_all' xml_dir = 'dataset/annotations_xml' yolo_label_dir = 'dataset/labels_all' split_ratio = {'train': 0.8, 'val': 0.1, 'test': 0.1} imgs = glob(os.path.join(img_dir, '*.jpg')) names = [os.path.splitext(os.path.basename(p))[0] for p in imgs] random.shuffle(names) # 按比例切分,先按完整视频片段分组再shuffle效果更好 cut_train = int(len(names) * split_ratio['train']) cut_val = int(len(names) * (split_ratio['train'] + split_ratio['val'])) splits = { 'train': names[:cut_train], 'val': names[cut_train:cut_val], 'test': names[cut_val:] } for split, name_list in splits.items(): for img_out in ['dataset/images', 'dataset/labels', 'dataset/annotations']: os.makedirs(os.path.join(img_out, split), exist_ok=True) for name in name_list: for ext in ['.jpg', '.png']: src_img = os.path.join(img_dir, name + ext) if os.path.exists(src_img): shutil.move(src_img, os.path.join('dataset/images', split, name + ext)) break src_xml = os.path.join(xml_dir, name + '.xml') if os.path.exists(src_xml): shutil.move(src_xml, os.path.join('dataset/annotations', split, name + '.xml')) src_txt = os.path.join(yolo_label_dir, name + '.txt') if os.path.exists(src_txt): shutil.move(src_txt, os.path.join('dataset/labels', split, name + '.txt')) # 孤儿文件检查:labels目录里有txt但没有对应图片的,一律删掉 for split in splits: label_paths = glob(os.path.join('dataset/labels', split, '*.txt')) orphan = [] for lp in label_paths: base = os.path.splitext(os.path.basename(lp))[0] img_exists = os.path.exists(os.path.join('dataset/images', split, base + '.jpg')) or \ os.path.exists(os.path.join('dataset/images', split, base + '.png')) if not img_exists: orphan.append(lp) for lp in orphan: os.remove(lp) print(f'removed orphan label: {lp}')脚本逻辑分三段。第一段用random.seed固定随机种子,保证每次运行划分结果一致,这很重要——你调参后要复现实验,随机种子不同会导致训练集不同,指标没法对比。第二段按8:1:1切片,切片前shuffle打乱,这里我注释了"按完整视频片段分组再shuffle效果更好",如果你的图来自连拍片段,改成先按片段名分组再对组shuffle更稳。第三段是孤儿文件检查,清理掉有标签无图片的txt,防止训练时读入无效标签。
提示:划分完成后建议再跑一遍数量统计,打印train/val/test各自的图片数和标签数,确认三个集合里都有数据且比例正确。val集为空时ultralytics会直接跳过验证环节,很多人没注意照样训练完,最后拿不出可信的mAP。
4. 跑通YOLOv8训练红外飞机小目标:环境、数据配置与5个必调参数
4.1 环境安装与预训练模型选择:v8权重怎么拉、版本怎么锁
训练红外小目标检测,我推荐直接上YOLOv8的ultralytics库,理由很简单:配置最省事、验证指标齐全、数据格式兼容上面做好的目录。环境安装用pip一把梭,Python版本建议3.9到3.11之间,太新的Python版本有些CUDA轮子还没跟上,装torch时会踩坑。
pip install ultralytics预训练模型的下载是新手最困惑的一步。yolo预训练模型下载其实不需要手动找链接,ultralytics会在你第一次指定模型名时自动从官方GitHub Releases拉权重文件,比如yolov8n.pt约6MB、yolov8s.pt约22MB。如果有人告诉你yolo预训练模型下载必须去某些第三方站,那多半是误导。我一般直接用yolov8n或yolov8s做红外小目标的基础权重,因为模型越小,训练越快,小数据集上过拟合风险也越低。
yolo detect train data=dataset/dataset.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=1280 \ batch=8上面这行命令就是完整的训练入口。model=yolov8n.pt表示加载COCO预训练权重,如果网络环境不好导致权重下载失败,可以手动下载后放到当前目录,ultralytics检测到本地文件就不会再拉取。训练时如果batch过小导致显存溢出,优先降imgsz而不是关掉训练重来。
4.2 数据配置文件dataset.yaml:类别顺序必须和标签一致
数据集配置文件是连接目录结构和训练器的桥梁。单类红外飞机检测的yaml很简单,但这里出过最多的坑:names顺序和标签文件里的class_id不一致。前面第3.2节强调class_names列表的顺序不能变,就是在为这一步埋伏笔。写yaml时直接沿用转换脚本里的class_names顺序:
path: dataset train: images/train val: images/val test: images/test names: 0: airplanepath是相对当前工作目录的路径,train和val指向图片目录,ultralytics会到对应目录找图片,再去同级父目录下的labels目录找同名txt标签。这个依赖关系是框架约定的,如果你的图片在images/train,那么标签必须放在labels/train,且文件名第一段完全一致。所以第3章划分脚本里我把labels目录同步建好,就是为了满足这个约定,不用在yaml里再写一遍label路径。
注意:类别名建议统一用英文小写,不要带空格和特殊字符。红外数据集的类别通常只有airplane一类,但如果压缩包里还有其他类别(比如helicopter、drone),names列表必须按类别编号升序排列,和转换脚本里的class_names严格一致。
4.3 5个必调参数:imgsz、batch、epochs、optimizer与mosaic
训练红外小目标检测和训练常规数据集,参数策略有明显差异,这里列出五个最关键的调参项。
第一个是imgsz,输入分辨率。默认640在常规数据集上够用,但红外飞机只有十几二十个像素,640下采样8倍后目标只剩两三个像素,检测头几乎学不到特征。我的经验是把imgsz提到1280甚至1536,目标在输入图里能占十几个像素,检测率会有明显提升。代价是显存和训练时间翻倍,显存不够时先降batch。
第二个是batch,批大小。红外图像背景大都是暗区,图像内容相似度很高,batch太小会让梯度估计噪声大,训练不稳定。在显存允许的前提下batch尽量不小于8。如果你只有一个GPU且显存只有12G,imgsz=1280时batch=4是底线。
第三个是epochs。1000张图单类别,epochs设100足够,设太大容易过拟合。具体判断看训练日志里的val精度曲线,连续20个epoch没有上升就该停了,ultralytics支持patience参数做早停。
第四个是optimizer。红外小目标数据集上我一般用SGD带动量,动量为0.937,初始学习率0.01。AdamW收敛快但容易在小数据集上过早饱和,SGD的泛化在小样本场景下更好。
第五个是mosaic,数据增强里影响最大的一项。mosaic增强会把四张图拼在一起训练,问题在于拼接时小目标可能在裁剪边缘被切断,目标消失或变成半截框。红外小目标本身像素少,mosaic的破坏效果比常规目标更严重。我通常把mosaic关闭或降到0.3以下,同时保留其他的翻转、缩放增强。
yolo detect train data=dataset/dataset.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=1280 \ batch=8 \ optimizer='SGD' \ lr0=0.01 \ mosaic=0.3 \ patience=204.4 训练过程看什么:loss曲线、验证指标与yolo损失函数的行为
训练跑起来之后,别盯着进度条发呆。ultralytics日志里会同步输出box_loss、cls_loss、dfl_loss三条loss曲线,红外小目标场景下box_loss会比较高,因为目标小、像素少,回归精度本身就受限。正常的loss曲线应该整体缓慢下降,如果box_loss震荡剧烈或者出现NaN,多半是学习率过大或输入图像有异常,这时候直接停掉,先查数据再调lr。
每个epoch结束后的验证环节会输出mAP50、mAP50-95、precision、recall四组指标。红外小目标检测的mAP50-95通常低于常规检测,因为小目标对IoU的变化极其敏感,这是任务本身的客观难度,不用太焦虑。更值得关注的是precision和recall的平衡:如果recall低,说明漏检多,优先提分辨率或增强数据;如果precision低,说明误检多,优先调置信度阈值或检查背景噪声标注。yolo损失函数里的dfl_loss控制边界框回归的分布,小目标场景下dfl_loss的绝对值会比大目标高,这正常,不必因为这个单独调权重。
5. 红外小目标训练最常见的5个翻车点:现象、原因与处理
5.1 训练中loss变成NaN或者bn崩溃:yolo训练中bn崩溃的全链路排查
现象:训练进行到十几个epoch,loss突然变成nan,或者验证mAP剧烈震荡,最终全红。多人遇到这类问题第一反应是调低学习率,但根本原因往往在数据侧。红外图像里存在大量纯黑或近纯黑的背景区域,经过网络卷积后这些区域的响应值极低,批归一化层的均值和方差被这些暗区拉偏,统计量不稳定导致梯度爆炸。
原因要分几层排查:一是数据里有没有全黑或全灰的坏图,检查方法是统计每张图的灰度均值,找出均值低于某个阈值的图直接剔除;二是输入图像里目标太靠近图像边界,增强时把目标切没了一半,标签框越界或面积趋近于零,导致回归loss异常;三是初始学习率过大,红外小目标数据集的梯度尺度更大,0.01的初始学习率对yolov8n在小数据集上可能偏高。
解决按顺序三步走:先清洗数据,剔除异常图;再把mosaic降到0或0.3,减少目标被裁剪破坏的概率;最后把lr0降到0.005或0.001。改完这三步,bn崩溃的问题基本不会再出现。如果依然有loss抖动,检查标签txt里有没有归一化坐标大于1或小于0的值,转换脚本里漏了clip操作也会造成这个问题。
5.2 验证集指标高但实际检测很差:数据泄漏和划分脚本的锅
现象:训练日志里val mAP50到了0.9以上,测试集效果却差得离谱,漏检率很高。第一反应是模型过拟合,但把训练轮数调低后依然如此,这时候要怀疑验证集和训练集太"像"了。
原因大概率是第2.3节说的数据泄漏。红外飞机图如果是视频连拍抽帧,随机划分会让同一段飞行动作的相邻帧同时进入train和val,验证集成了"开卷考试",模型记住的是帧间相似性而不是目标的通用特征。另一个常见原因是划分脚本bug,图片移动了标签没跟着移动,导致val集实际为空,ultralytics在验证时静默跳过,日志里的mAP是最后一次train batch的伪指标。
解决方法是重写划分脚本,按视频片段分组后再划分,保证同一片段只进一个集合。同时加一道校验:val集里随机抽10张图,手动数一下有没有标签,并用训练好的模型跑一遍预测,确认能正常画出框。没有验证集的训练日志等于没有指标,这种翻车最隐蔽,也最伤时间。
5.3 混淆矩阵总和不是1:yolo混淆矩阵总合不唯一的真凶
现象:训练结束后画混淆矩阵,发现所有数字加起来不等于样本总数,或者和预期的目标数对不上。这不算模型错误,而是理解偏差。
原因是混淆矩阵统计的是预测框和真实框在IoU匹配阈值下的匹配关系,一个真实框最多匹配一个预测框,但背景类别会统计所有未匹配的预测框,所以矩阵里面除了airplane类,还有一个background列或行,把所有格子的数加总自然不等于目标总数,而是等于"真实目标数+误检框数"。yolo混淆矩阵总合不唯一这个说法,往往是把背景类别漏算了。
解决方法是不要追求总和等于GT数,而是看对角线上的数值占比,尤其关注airplane这一行的漏检率是多少,background那一行里有多少误检框。如果你用的是coco格式评估工具,还要确认类别顺序和id是否对齐,coco的category_id从1开始,yolo的class_id从0开始,错一位整个矩阵就乱了。
5.4 检测框整体偏移半张图:转换脚本里除错了宽高
现象:训练出来的模型能检测到目标,但画出的框整体偏移,框的位置在目标的上方或左上方偏移几十上百像素,目标越大偏移越明显。
原因不是模型问题,是标签坐标错了。最常见的错误是voc转yolo时,把x_center除以了错误的尺寸,比如原图实际是1920×1080,脚本里读到的却是某次resize后的640×640;或者转换时把xmin和ymin直接当作中心坐标,没有加上半宽半高。另一类错误是coco格式的bbox是左上角坐标加宽高,转yolo时直接拿bbox[0]当x_center来算,忘了加w/2。
解决方法是给一张已知的图写个验证脚本:用opencv读原图,根据yolo标签的坐标还原像素坐标并画框,和voc原标注叠加对比,人工确认是否重合。这个验证步骤每转换一个数据集都应该做一次,别嫌麻烦,它能在训练前拦住70%的标签错误。检测框偏移这种问题,等训练完再发现,返工成本就高了。
5.5 小目标mAP始终为0:anchor设置和输入分辨率的锅
现象:训练完mAP50有数值,但mAP50-95很低,或者按COCO尺寸分组后,小目标那一组的AP是0。这在小目标检测里非常典型,刚入坑的人容易怀疑模型能力不够。
原因是模型的下采样倍率和默认anchor尺寸不匹配。YOLOv8默认anchor锚定在8、16、32倍下采样特征层,如果输入是640,最小特征层每个格子对应原图8×8像素。一个16×16像素的飞机,在8倍下采样层上只占2×2个格子,特征少得可怜。更关键的是anchor初始化偏向COCO数据集的常规目标尺寸,红外小目标的宽高往往只有十几像素,和预设anchor相差太远。
解决方法有两个方向:一是把imgsz提到1280以上,目标在特征图上占据更多像素,本质上是把"小目标"放大成"中目标";二是采用切片推理方案,把大图切成无重叠的块分别检测,再合并结果。训练侧还可以把anchor的宽高范围手动向小尺寸方向收缩,YOLOv8支持自动anchor进化,在小数据集上可以关掉自动统计,显式指定anchor尺寸。先做imgsz提升,成本最低,见效也最快。
6. 验证与进阶:用混淆矩阵和置信度曲线把漏检率再压一截
模型训练完,别急着部署,先用val集做一轮细致的验证分析。YOLOv8自带混淆矩阵和F1-confidence曲线,这两个工具能把模型的问题暴露得很清楚。
yolo detect val model=runs/detect/train/weights/best.pt \ data=dataset/dataset.yaml \ conf=0.25 \ imgsz=1280上面的命令会输出混淆矩阵图和f1_curve.png、pr_curve.png等文件。打开混淆矩阵,重点关注airplane类所在行:如果对角线的值占了80%以上,说明主类别识别稳定;如果background那一行的值不小,说明误检集中在背景噪声上,这时你可以把conf阈值往上调,用f1_curve.png找F1最大值对应的置信度阈值,把这个值写进推理参数里,能明显降低误检框数量。
进一步进阶可以用切片推理。红外飞机图分辨率通常很高,整张图直接放大到1280会丢失目标细节,但如图分辨率只有640×512,直接推理即可。如果是1920×1080的大图,我一般把图切成512×512的块,检测完按坐标拼回去,过滤掉重叠框。这个思路在电力红外检测、遥感小目标场景里都适用,属于不换模型就能提点的手段。
最后说一个我养成的习惯:每次训练完,随机抽30张测试图,把预测框画出来人工看一遍,不看指标只看框。红外小目标检测的指标曲线再漂亮,都不如眼睛确认几个典型场景来得踏实。新一轮训练前先复盘上一轮的漏检图是漏在远距离还是漏在强背景下,再决定下一次调参方向。这个"先看框、再调参"的流程,帮我省掉了很多在参数里瞎试的时间,也希望帮到你。
本文还有配套的精品资源,点击获取