简介红外场景下的多目标检测数据集面向目标检测入门者与算法开发者专门解决公开红外数据集稀少、标注格式不统一等问题。包内包含1000张真实场景高质量图片使用LabelImg标注标注框质量可靠标签同时提供VOC(xml)、COCO(json)、YOLO(txt)三种格式存放于不同文件夹可直接喂给YOLO系列模型训练。压缩包共2000个文件以xml、txt为主另有Python划分脚本、yaml配置和HTML教程文档整体96.39MB。随包附赠YOLO环境搭建Windows/Linux与训练案例教程以及三种数据集划分脚本支持按需生成训练集、验证集、测试集。目前已有455人浏览学习适合需要真实红外数据做实验、快速搭建YOLO训练流程的师生和工程师使用。1. 红外多目标检测数据集为什么 1000 张图配上三种标签就够你起步了红外场景下的多目标检测一直比可见光麻烦数据少、目标小、背景热噪声多能直接拿来训练的公开数据集更少。我拿到这份 YOLO红外多目标检测数据集时第一反应不是立刻训练而是先看标签组织方式——1000张红外图片附带voc、coco、yolo三种格式的标签还有划分脚本和训练教程。这意味着你不用再写一套格式转换也不用为了数据划分熬夜写正则。它适合两类人一是刚跑通YOLO、手上没有红外数据的新手二是已经在做电力巡检或安防监控想快速给现有模型补红外场景的工程师。接下来我按“格式校验—划分—训练—避坑”这条路讲清楚。2. 拿到 rar 先别急着训练把 VOC、COCO、YOLO 三种标签先对齐rar 包解压后第一件事不是双击 train.py而是花十分钟看清目录结构。常见的数据组织方式是这样images 里放原始红外图labels 里放 YOLO 格式的 txtannotations 里可能既有 VOC 的 xml也有 COCO 的 json。如果你拿到的包只给了其中一种格式也别慌下面的转换脚本可以直接用。真正会坑人的不是格式本身而是三种格式之间的坐标基准和索引顺序在没对齐之前任何训练结果都不可信。2.1 三种标签格式的差异为什么同一个框要存三份同一张红外图、同一个目标用不同框架训练时需要不同喂法。VOC 系的 xml 给的是像素坐标xmin, ymin, xmax, ymax人眼能看懂但 mmdetection、Detectron2 这类框架要自己转COCO 的 json 给的是[x, y, width, height]左上角为原点还要补 segmentation 和 areaYOLO 的 txt 给的是归一化后的中心点和宽高且类别编号从 0 开始。同一个框存三份不是冗余而是为了让你在切换训练框架时不用重写转换逻辑。格式典型文件坐标表示主要使用场景VOC.xmlx_min, y_min, x_max, y_max像素坐标检测框架通用中间格式COCO.jsonbbox: [x, y, width, height]像素坐标segmentation 多边形Detectron2、MMDetection、YOLO 的 coco 模式YOLO.txtx_center, y_center, width, height均归一化到 0~1YOLOv5、YOLOv8 原生训练这里最容易翻车的是坐标归一化时的尺寸写死。很多红外图是 640×512 或者 1024×768如果你写死成 640×640所有框都会偏移。所以转换脚本里img_w和img_h必须从 XML 或图像里动态读取。2.2 从 VOC 的 XML 出发坐标归一化与类别映射的转换脚本拿到一个 VOC 标注目录后最常见的需求是转成 YOLO 格式。先整理一份names_list顺序就是 YOLO 训练时的类别编号。比如数据里有 person、car、heater 三类names_list [person, car, heater]person 就是 0car 是 1heater 是 2。import os import xml.etree.ElementTree as ET from pathlib import Path VOC_NAMES [person, car, heater] # 顺序必须与之后 data.yaml 的 names 完全一致 def voc_xml_to_yolo_txt(xml_path, out_txt_path, names_list): 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): name obj.find(name).text if name not in names_list: continue # 不在目标列表里的类别直接跳过避免污染标签 cat_id names_list.index(name) bndbox obj.find(bndbox) x_min float(bndbox.find(xmin).text) y_min float(bndbox.find(ymin).text) x_max float(bndbox.find(xmax).text) y_max float(bndbox.find(ymax).text) # 某些标注工具可能把 xmin/xmax 写反先排个序 x_min, x_max sorted((x_min, x_max)) y_min, y_max sorted((y_min, y_max)) x_center (x_min x_max) / 2 / img_w y_center (y_min y_max) / 2 / img_h width (x_max - x_min) / img_w height (y_max - y_min) / img_h # 归一化后可能出现 0 或超过 1 的异常值截断并修正 x_center min(max(x_center, 0), 1) y_center min(max(y_center, 0), 1) width min(width, 1 - x_center) height min(height, 1 - y_center) lines.append(f{cat_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}) Path(out_txt_path).write_text(\n.join(lines), encodingutf-8) # 批量处理 examples xml_dir Path(annotations/voc_xmls) label_dir Path(labels) label_dir.mkdir(exist_okTrue) for xml_file in xml_dir.glob(*.xml): out_txt label_dir / (xml_file.stem .txt) voc_xml_to_yolo_txt(xml_file, out_txt, VOC_NAMES)这段脚本的逻辑核心是两个系数1/img_w和1/img_h。YOLO 标签不存像素宽高只存相对原图的比例所以原图尺寸必须和 xml 里的 size 一致。红外图如果被预处理成灰度后保存尺寸通常不变但如果你做过裁剪、resize、填充转换就必须在预处理之后做否则标签和图像对不上。2.3 转 COCO 的 JSON注意 segmentation、area 和 iscrowd 这三个字段COCO 格式比 VOC 更严格。除了 bbox还要提供segmentation、area和iscrowd。很多标注工具导出的 COCO 只有 bbox训练时虽然能用但用 Detectron2 或 YOLO 的 COCO eval 时缺少 segmentation 会让部分评估代码报错。import json from pathlib import Path import xml.etree.ElementTree as ET def voc_to_coco(xml_dir, images_dir, out_json, names_list): categories [{id: i, name: name, supercategory: none} for i, name in enumerate(names_list)] images, annotations [], [] ann_id 0 for idx, xml_path in enumerate(sorted(Path(xml_dir).glob(*.xml))): tree ET.parse(xml_path) root tree.getroot() filename root.find(filename).text img_w int(root.find(size/width).text) img_h int(root.find(size/height).text) images.append({ id: idx, file_name: filename, width: img_w, height: img_h }) for obj in root.iter(object): name obj.find(name).text if name not in names_list: continue cat_id names_list.index(name) bndbox obj.find(bndbox) x1 float(bndbox.find(xmin).text) y1 float(bndbox.find(ymin).text) x2 float(bndbox.find(xmax).text) y2 float(bndbox.find(ymax).text) x1, x2 sorted((x1, x2)) y1, y2 sorted((y1, y2)) w x2 - x1 h y2 - y1 # 用矩形四个角组成一个简单多边形满足 COCO 对 segmentation 的基本要求 poly [x1, y1, x2, y1, x2, y2, x1, y2] annotations.append({ id: ann_id, image_id: idx, category_id: cat_id, bbox: [x1, y1, w, h], area: w * h, iscrowd: 0, segmentation: [poly] }) ann_id 1 json.dump({images: images, annotations: annotations, categories: categories}, open(out_json, w), ensure_asciiFalse) voc_to_coco(annotations/voc_xmls, images, annotations/coco.json, [person, car, heater])这里三个字段容易写错area对矩形框来说就是w * h如果你用多边形顶点计算面积结果也一样iscrowd对普通检测目标必须写 0写成 1 表示该目标是一群目标训练时会被特殊处理segmentation必须是“顶点列表的列表”哪怕你只给一个矩形也要写成[[x1,y1,x2,y1,x2,y2,x1,y2]]直接给一维数组会被解析成多个分离区域。2.4 YOLO 的 txt 格式每个文件对应一个 txt类别编号从 0 开始YOLO 标签看起来最简单一行一个目标五个数字。但它的隐含约定比 VOC 更严txt 文件名必须和图片名完全一致不含扩展名类别编号从 0 开始不是从 1 开始坐标是归一化浮点数0 和 1 都合法但负数和大于 1 的框会让损失函数变奇怪。很多从 LabelImg 导出的 VOC用户建类别时从 1 开始编号转 YOLO 后如果不减 1整个模型的类别都错位。拿到 txt 后我建议先可视化检查一遍别直接开训。下面这段脚本会把 YOLO 标签画回原图确认框的位置和类别编号。import cv2 from pathlib import Path img_path images/infrared_001.jpg label_path labels/infrared_001.txt names [person, car, heater] img cv2.imread(str(img_path)) h, w img.shape[:2] for line in Path(label_path).read_text().strip().splitlines(): parts line.split() if len(parts) 5: continue cat_id, x_center, y_center, bw, bh map(float, parts[:5]) x1 int((x_center - bw / 2) * w) y1 int((y_center - bh / 2) * h) x2 int((x_center bw / 2) * w) y2 int((y_center bh / 2) * h) x1, y1 max(x1, 0), max(y1, 0) x2, y2 min(x2, w - 1), min(y2, h - 1) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, names[int(cat_id)], (x1, max(y1 - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) cv2.imwrite(check_infrared_001.jpg, img)这里要留意红外图像本身可能是单通道灰度但 OpenCV 读进来会自动变成三通道所以画框不受影响。真正要注意的是h, w img.shape[:2]不能改成(w, h)否则横竖坐标全反。可视化这一步能发现 90% 的标签错位花五分钟比后面训三天再返工划算得多。3. 划分脚本怎么用先按场景分层再按比例拆 train/val/test标签格式对齐之后第二个关键动作是划分。很多新手直接random.shuffle(images)按单张图分 8:1:1结果训练集 mAP 很好看一到真实场景就下滑。红外数据不是一张张独立的自然图片它通常来自连续视频帧或同一巡检点的多次拍摄按单张随机划分会造成严重的数据泄露。3.1 为什么不能随机打乱红外设备与人员场景里的类别分布红外多目标检测的应用场景很固定电力设备巡检、园区安防、夜间交通。同一个场景里热像仪的角度、温度范围、背景物体都高度相似连续帧之间的差异可能只有目标的轻微移动。如果按单张随机划分同一个巡检点的前后帧会被同时分进 train 和 val验证集评测结果虚高模型实际泛化能力没被反映。更合理的做法是先按场景分组再把场景列表按 7:1:2 或 8:1:1 划分。一个文件名里通常带有场景前缀比如station_001_023.jpg里的station或者目录名就是场景名。我在处理 firc-dataset 这类电力红外数据集时习惯直接用文件夹名做 scene_id省去解析文件名的麻烦。1000 张红外图如果来自 20 个场景那就把 20 个场景分成 14 个训练场景、2 个验证场景、4 个测试场景而不是把 1000 张图打乱。3.2 用 Python 脚本按文件粒度划分保持图片和标签一一对应这里我采用的是“生成 filelist”而不是直接移动文件。理由很简单移动文件会破坏原始目录一旦划分比例不满意还得从 rar 里重新解压。生成三个 txt每行写图片路径训练时通过 data.yaml 引用既保留原始数据又能随时重新划分。import random from pathlib import Path from collections import defaultdict random.seed(42) src_images Path(images) src_labels Path(labels) # 先筛掉没有对应标签的图片避免训练时出现 “empty label” 警告 valid_images [] for img_path in src_images.glob(*.jpg): label_path src_labels / (img_path.stem .txt) if label_path.exists(): valid_images.append(img_path) else: print(missing label:, img_path) # 用文件名第一个下划线前的部分作为场景 ID def scene_of(p: Path): return p.stem.split(_)[0] scene_to_images defaultdict(list) for img_path in valid_images: scene_to_images[scene_of(img_path)].append(img_path) scene_names list(scene_to_images.keys()) random.shuffle(scene_names) n len(scene_names) n_train round(n * 0.7) n_val round(n * 0.15) with open(train.txt, w) as ft, open(val.txt, w) as fv, open(test.txt, w) as fte: for i, scene in enumerate(scene_names): for img_path in scene_to_images[scene]: rel_img str(img_path) if i n_train: ft.write(rel_img \n) elif i n_train n_val: fv.write(rel_img \n) else: fte.write(rel_img \n) print(ftrain scenes: {n_train}, val scenes: {n_val}, test scenes: {n - n_train - n_val}) print(ftotal images in train/val/test: ...)这段代码最关键的是random.seed(42)固定随机种子保证每次重新划分结果一致。n_train round(n * 0.7)是对场景数做比例不是对图片数做比例所以每个子集里的图片数可能不是精确 7:1:2但只要场景内图片数量均匀结果接近。如果你希望 train 图片数也尽量接近 70%可以再加上图片数加权抽样但对 1000 张这类小数据集按场景分更稳。3.3 划分完必须做的事检查类别是否在三个子集里都出现划分完成后不要急着写 data.yaml先跑一个统计脚本。红外数据类别通常不均衡比如“heater”可能只在少数场景出现如果恰好全落到 test模型训练时没见过这个类评估结果没有意义。from collections import Counter from pathlib import Path def count_classes(filelist, label_dir): counter Counter() for line in Path(filelist).read_text().strip().splitlines(): img_path Path(line.strip()) label_path label_dir / (img_path.stem .txt) if not label_path.exists(): continue for line in label_path.read_text().strip().splitlines(): if line.strip(): cat_id line.split()[0] counter[int(cat_id)] 1 return counter for split in [train, val, test]: counter count_classes(f{split}.txt, Path(labels)) total sum(counter.values()) print(split, total boxes:, total, dict(counter))输出类似train total boxes: 3800 {0: 2100, 1: 900, 2: 800}。如果某个子集的类别数量为 0有两个选择一是把这个类别所在的场景重新划进对应子集二是接受模型对该类别泛化能力弱的事实。我倾向于调整场景划分比例让每个子集至少出现一次每个类别。3.4 给训练用的 filelist路径、类别 names 文件与 data.yaml 怎么写YOLO 训练不认三个 txt而是通过一个 data.yaml 把路径和类别信息灌进去。YOLOv8 的 data.yaml 支持直接写相对路径或绝对路径但要注意train和val字段既可以指向目录也可以指向一个图片列表 txt。我个人推荐指向图片目录因为目录操作比 txt 更健壮。path: /home/user/infrared_dataset # 改成你的实际路径 train: images/train val: images/val test: images/test nc: 3 names: [person, car, heater]如果你用前面的 filelist 方式也可以把 train/val/test 直接指向 txt 文件路径但要注意 txt 内每行必须是图像文件的绝对路径。nc必须和 names 长度一致names 的顺序必须和 YOLO txt 里的整数编号一致。常见错误是 names 排序和 VOC_NAMES 不一样比如 VOC_NAMES 是[person,car,heater]data.yaml 里写成[heater,car,person]模型训练时不会报错但跑出来的混淆矩阵和 mAP 全部错位而且很难发现。4. 用 YOLO 跑通红外多目标检测的最小训练流程数据准备好了接下来就是把训练流程跑通。我见过不少人在环境配置阶段就卡了两天其实红外多目标检测对模型版本没有特殊要求YOLOv5 和 YOLOv8 都行。关键在于预训练权重、输入分辨率和数据增强这三个变量。4.1 yolo 环境配置推荐 YOLOv8 还是 YOLOv5如果是新项目我会推荐 YOLOv8。理由不是精度一定比 YOLOv5 高而是 ultralytics 的命令行统一、配置文件清晰对红外这类需要频繁调参的数据集更友好。YOLOv5 的优势在于部署生态成熟如果你的最终目标是 TensorRT 或 RKNNYOLOv5 的转换资料更多。环境配置用 conda 最省心conda create -n infrared python3.10 -y conda activate infrared pip install ultralytics torch2.0.0 torchvision0.15.0注意 torch 版本要和你机器的 CUDA 匹配。GPU 驱动是 12.x 的直接装默认 torch 版本显卡比较老就装 cu118 或 cu121 对应的轮子。装完后命令行输yolo能看到命令帮助说明环境正常。4.2 从一个最简训练命令开始预训练权重与数据配置环境正常后先用一条最简命令把基线跑起来。不要一上来就调一堆超参数那会让问题变复杂。yolo detect train \ datainfrared.yaml \ modelyolov8s.pt \ imgsz640 \ batch16 \ epochs100 \ device0 \ workers4 \ seed42modelyolov8s.pt会自动下载 COCO 预训练权重或者你提前使用 yolo 预训练模型下载好的 .pt 文件放到当前目录训练时会优先使用本地权重。不要在 1000 张红外图上从头训练预训练权重能大幅缩短收敛时间。这里batch16是 640×640 输入下 16G 显存的大致水位如果显存不足降到 8epochs100对这个小数据集足够再多也容易过拟合。seed42保证每次实验可比。4.3 红外场景下必调的三个参数imgsz、batch、mosaic红外小目标多这三个参数的影响比模型结构大得多。imgsz决定输入分辨率。红外图常见 640×512用 640 作为输入时一个 32×32 的热源目标只占 50×50 像素还能识别但更小的目标可能只有 8×8占 10×10 像素不到。如果你的验证集里AP_small明显低于其他类别先把imgsz提到 1280 试一轮。代价是显存占用和训练时间都翻倍所以我的习惯是先用 640 跑通基线再决定是否提分辨率。batch要根据显存调。参数表供参考显卡显存imgsz640 推荐 batchimgsz1280 推荐 batch8G8216G16424G24640G328mosaic是 YOLOv8 默认开启的增强策略把四张图缩放到中间拼成一张。它对可见光中大目标有效但对红外小目标不友好目标缩小后更接近噪点还可能被裁剪到边缘。如果小目标 AP 一直不涨我建议先关掉 mosaic 跑一轮yolo detect train \ datainfrared.yaml \ modelyolov8s.pt \ imgsz640 \ batch16 \ epochs100 \ mosaic0.0 \ mixup0.0关闭 mosaic 后模型会更快收敛但类别不均衡时容易过拟合。比较稳妥的做法是先用mosaic0.0建立一个无增强或弱增强的 baseline确认模型能学会再逐步把 mosaic 加回 0.5看小目标 AP 是否受影响。4.4 训练日志里的关键指标不要只看 loss训练过程中终端会打印box_loss、cls_loss、dfl_loss以及precision、recall、mAP50、mAP50-95。很多新手只盯 loss看到 loss 降到 0.050 就觉得快收敛了。实际上 loss 受标签质量和输入分辨率影响很大红外图像噪声高loss 往往降不到可见光那么低。我一般只看最后的验证集指标尤其是mAP50和mAP50-95的差距。如果mAP50高但mAP50-95低说明模型对大目标定位可以但小目标定位不够精准常见对策是提高输入分辨率或调整 IoU 阈值。训练过程中的recall如果一直低于 0.5优先怀疑标签漏标而不是模型能力。你会发现 val 集有些红外目标画框太小标注时人眼已经看不清边界模型的回归上限也被数据质量卡住了。5. 避坑红外多目标数据集从解压到训练的 5 个高频问题这一节是血泪经验。红外数据集本身的陷阱加上 rar 包交付方式带来的混乱几乎是每个跑这个方向的人都会踩一遍。5.1 文件名对不上导致训练集一张图都没读到现象训练命令刚跑起来就输出警告No labels found in image set或者直接提示某张图片的 label 不存在训练实际用的图片数量远小于 1000。原因rar 包里的 images 和 labels 文件命名不统一常见三种情况大小写不一致比如IMG_001.jpg对应img_001.txt扩展名不一致图片是.png但脚本按.jpg去 glob文件名带了额外空格或窗口系统的非法字符。解决先做一个文件名一致性检查把所有图片和标签统一成小写扩展名再用脚本找出没有对应标签的图片。for f in images/*.png images/*.jpg; do s$(basename $f) s${s%.*} ls labels/${s}.txt /dev/null 21 || echo missing label for: $f done把输出的缺失列表手动补标签或者直接删掉这些图片。不要指望训练时exist_ok自动跳过空标签会污染 batch 统计。5.2 红外单通道图被读成三通道尺寸对不上和颜色诡异现象图片明明是一张灰度红外图用 OpenCV 读出来却是(H, W, 3)看起来是灰色的但按单通道处理时报错维度不对。原因YOLO 的预处理默认接受三通道图像。红外相机保存的 8 位灰度图会被 OpenCV 自动扩展为三通道这本不是问题。真正的问题是有些红外图是 16 位 PNG直接读出来是(H, W)单通道且像素值范围是 0~65535YOLO 的归一化会把大部分值压成 0整个画面几乎是黑的。解决在训练前统一把图像转成 8 位三通道 JPEG。转换时做一次直方图拉伸否则过暗或过亮的图像会让模型学到温度偏移而不是目标形状。import cv2 import numpy as np img cv2.imread(infrared_16bit.png, cv2.IMREAD_UNCHANGED) img8 cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) img8 cv2.cvtColor(img8, cv2.COLOR_GRAY2BGR) cv2.imwrite(infrared_8bit.jpg, img8)这里注意cv2.normalize对 16 位图做的是全局拉伸如果画面中有一个高亮热源其他目标会变暗。我一般先按 2% 和 98% 的百分位做 clip再拉伸效果更稳。5.3 COCO 的 category_id 与 YOLO 的类别索引错位现象训练时 loss 正常下降但验证集输出的混淆矩阵里person 的框落在了 car 那行或者用 COCO eval 时 mAP 几乎为 0。原因COCO json 的 categories 列表里的id没有从 0 开始而是从 1 开始YOLO txt 的类别编号却是从 0 开始。转换脚本如果直接enumerate(names_list)生成 COCO id而 names_list 里包含 0 号类别COCO 就会把 0 号当成背景所有类别偏移一个位置。另一个原因是 categories 的排序和 data.yaml 不一致。解决在转换脚本里明确约定 COCOcategory_id index同时记录一份 names 顺序。训练前打印一下 json 里 categories 和 data.yaml 的 names人工核对一次。下面这段代码可以快速检查import json from pathlib import Path data json.load(open(annotations/coco.json)) for c in data[categories]: print(c[id], c[name]) print(Path(infrared.yaml).read_text())只要输出顺序能和 yaml 的 names 对应上问题就排除。这个坑最好在还没训练时就排除等训练完再看损失的是时间。5.4 mosaic 增强把红外小目标切没了AP 反而下降现象开 default 的 mosaic 训练loss 看着很低但 val 的 recall 一直上不去尤其是小目标类别。关掉 mosaic 后recall 立刻回升。原因mosaic 会把 4 张图 resize 后拼成 1 张目标面积直接缩小一半以上。红外小目标本身只有 20×20 甚至更小缩放后变成几个像素模型基本学不到。更隐蔽的问题是 mosaic 拼接时目标可能被裁剪如果目标正好在拼接缝附近标签还在但像素信息切没了模型被迫把半截目标学成完整目标。解决训练前先用mosaic0.0跑一轮确认 baseline 的 AP 能到合理范围再逐步加 mosaic。很多电力红外场景下最后我甚至保持mosaic0.0和mixup0.0靠 Fl_Augment 和 HSV 抖动来增加多样性。如果一定要用 mosaic把imgsz提到 1280 也能缓解目标被缩没的问题但显存压力随之而来。5.5 混淆矩阵总和不为 1没搞懂归一化方向就调阈值现象训练完打开confusion_matrix.png发现矩阵里所有数值加起来不是 1或者某一行之和超过 1于是怀疑配置有问题。原因YOLO 的混淆矩阵默认按行归一化每一行表示某一真实类别的目标被预测到其他类别的比例所以每行之和为 1但整个矩阵所有元素之和不是 1。如果你自己用 sklearn 只统计框级计数矩阵那元素之和就等于样本数也不是 1。这里没有 bug是“归一化方向”和“计数单位”的区别。解决先确认你要的是行归一化还是全局占比。看 YOLO 官方生成的图每行最大的值应该在对角线上代表该类别被正确识别的比例。如果某个非对角线值很高比如 person 那一行里 heater 有 0.4说明模型大量把红外行人误判成发热设备这不是调阈值能解决的要回到数据层面增加 personheater 的难例。比纠结矩阵总和更重要的是看你知道该往哪个方向补数据。6. 训练完先做这两件事分尺度看 mAP 和用混淆矩阵定位漏检训练结束模型保存到了runs/detect/trainN/weights/best.pt。先别急着部署也别急着吹指标先做两件事。6.1 分尺度看 mAP红外目标别只看 mAP50打开results.png或验证日志里面有metrics/mAP50(B)、metrics/mAP50-95(B)其实还有一组按目标尺度拆分的指标small_P、small_R、small_mAP。红外场景里很大一部分目标属于 small 尺度。如果整体 mAP50 有 0.85但 small_mAP 只有 0.2说明模型对大目标拟合得很好小目标基本漏检。这时候优先考虑提升输入分辨率或改用切片推理而不是继续加大 epoch。6.2 用混淆矩阵定位“红外背景被误判”的类别对看confusion_matrix.png的最后一列和最后一行。最后一列是真实目标被误判成背景的比例如果很高说明该类别召回不足最后一行是背景被误判成各个目标的比例如果某个类别在背景误判中特别高说明模型容易在冷背景里把热噪声当成目标。这两个方向的处理方法完全相反前者要补该类的样本或者调低置信度阈值后者要增加负样本或调高置信度阈值。6.3 一个可复用的验证循环用 val.txt 批量推理并输出热力图验证阶段我一般会对 val 每个场景做一次批量推理保留置信度和裁剪图方便肉眼确认漏检模式。yolo detect predict \ modelruns/detect/trainN/weights/best.pt \ sourceval.txt \ imgsz640 \ conf0.25 \ save_txtTrue \ save_confTrue \ save_cropTrue跑完后再写一个几十行的小脚本把预测 txt 和真实 txt 做比对统计每个类别的漏检数和误检数。如果误检集中在特定场景比如白天逆光的红外图就针对这个场景补数据。我现在每次拿到新红外数据集第一件事永远是先统一图像位深和文件名再可视化标签画一遍框确认没问题才敢训练。这个习惯帮我省掉了大量临时返工的时间。希望帮到你。本文还有配套的精品资源点击获取