简介:面向计算机视觉开发者与研究人员,基于YOLOv8n的小目标检测实战项目,旨在解决小目标因像素少、特征弱而难以被常规算法准确识别的痛点。项目在YOLOv8轻量级版本基础上,通过改进网络结构、特征融合策略与损失函数设计,在保持较高准确度的同时更适合计算资源受限的嵌入式环境。资源不仅提供完整可运行的Python源码,还配有从环境安装、数据集准备到模型训练评估的流程教程,便于初学者逐步上手,也有助于工程师快速复用。压缩包共2000个文件,以txt、yaml、md为主要类型,其中大量txt文件承担数据或标注信息存储,yaml用于模型与训练配置,md为项目说明文档,整体约336.37MB,目录结构清晰。目前已有80人学习,项目被标记为优质项目实战,具备很强的实践与参考价值,可作为深度学习和目标检测领域的可靠起步点。
1. 小目标检测为什么难:从“看不见”到“测不准”,YOLOv8n 能救哪一部分
做小目标检测的人都会经历一次心态崩塌:模型在公开数据集上 mAP 高得漂亮,一到自己的业务图上,几万个像素里那个目标就是测不出来。不是模型菜,而是小目标本身的物理条件太苛刻——一个 8×8 像素的目标,经过 YOLOv8n 的第一层卷积和 32 倍下采样之后,在特征图里只剩不到 1 个像素点,信息几乎被抹平。这时候再好的分类头也分不出它是什么。所以“小目标检测”从来不是单纯换模型的问题,而是数据、预处理、训练策略和推断方式四件事一起做对。这篇就是围绕 YOLOv8n 把这四件事讲透:怎么做数据集、怎么调参数、怎么排查翻车点,以及拿到一个标题里这种“源码+流程教程”项目包后怎么验收和改造。适合手里有具体小目标检测需求、想快速落地而不是只看原理的工程师。
2. YOLOv8n 的小目标能力边界:DFL 头、SPPF 与高分辨率输入的取舍
2.1 为什么小目标任务优先选 YOLOv8n 而不是大模型
直觉上,模型越大越强,小目标应该选 YOLOv8x,但真实落地里 n 版本反而更常用。原因是小目标检测的主要矛盾不在参数量,而在输入分辨率和训练策略。YOLOv8n 的 backbone 用 CSPDarknet 结构,FLOPs 只有 8.7G 左右,在 640 输入下单张前向非常快,这给了我们一个关键操作空间——把输入分辨率从 640 拉到 1024 甚至 1280,让目标在图上占据更多像素。如果用 YOLOv8x,显存先撑不住,batch 被迫降低,训练反而更不稳定。
另外一点容易被忽略:YOLOv8n 的检测头是 anchor-free 的 Decoupled Head,分类和回归分支分开,回归分支还带了 DFL(Distribution Focal Loss)。DFL 的作用是把边界框坐标建模成概率分布而不是直接回归一个值,这对小目标尤其友好。小目标的位置往往只占几个像素,标注框的抖动在小目标上占比很大,DFL 相当于给这种抖动加了一个概率约束,不会因为一两像素的标注噪声就跑偏。
小目标场景还有一个关键需求是“快”。很多小目标检测业务是实时的,比如无人机画面、机器人巡检、产线质检,模型需要在边端设备上跑。YOLOv8n 的轻量结构配合 TensorRT 或 OpenVINO 可以很容易推到百帧级别,而大模型即便精度高一点,部署成本也高一个量级。我一般会这样选型:先跑 YOLOv8n 拿一个 baseline,再考虑用 YOLOv8s 或者加 P2 层做精度提升,而不是一上来就上大模型。
2.2 模型结构上的两个硬伤:深下采样与感受野矛盾
YOLOv8 默认是 32 倍下采样,输出三个尺度的特征图分别是 8 倍、16 倍和 32 倍下采样。小目标检测的常见定义是目标尺寸小于 32×32 像素,也就是说最小的目标在 32 倍下采样的特征图上连一个格子都占不满。这意味着大部分小目标信息集中在前两层特征图上,而 P3(8 倍下采样)是 80×80 的特征图,每个格子对应的原图感受野约为 8×8 像素。
这里存在一个天然矛盾:小目标需要高分辨率特征图来保留空间细节,但高分辨率特征图的语义信息不够。浅层特征图经过的卷积层少,目标类别信息弱;深层特征图有语义,但空间分辨率已经被抹掉了。YOLOv8 的 neck 部分用 FPN+PAN 结构做了一次多尺度融合,理论上可以让深层语义信息回流到浅层,但实际上对小目标来说这种融合还不够。
常见做法是给模型加 P2 检测头,也就是 4 倍下采样输出。P2 层的特征图是 160×160,一个 8×8 的目标在 P2 上还能占 2×2 个格子,定位精度上了一个台阶。YOLOv8 官方没有提供带 P2 的预训练权重,需要自己改 yaml 结构,这会丢掉预训练的参数优势。所以我更推荐先用“高分辨率输入 + 切片推理”的组合方案,这样不需要改模型结构,完全复用官方权重,训练和部署的坑要少很多。
2.3 输入分辨率涨上去之后,哪些代价先找上门
把训练分辨率从 640 提到 1024,参数量不增加,但计算量和显存占用是平方增长。YOLOv8n 在 640 下 batch=16 大概要 6G 显存,到 1280 分辨率,同样的 batch 直接超过 24G。这里有几个应对手段:降低 batch 同时开梯度累积、开启 AMP 混合精度、把 mosaic 增强的拼图数从 4 降到 2 以减少显存峰值。
另外要小心的是,提升分辨率并不总是带来收益。如果你的数据集里大部分目标本来就是中大型物体,只是偶尔有几个小目标,那提升分辨率只会让训练变慢,模型对小目标的提升有限。正确做法是先统计数据集里目标尺寸的分布,确认小目标占比足够大,再去提分辨率。这一步我一般会在数据准备阶段用脚本扫一遍标注框,算一下每个目标相对原图的面积比,然后画个分布直方图,再做决定。
还有一点是数据增强策略要跟着分辨率走。高分辨率输入下的 random crop 很容易把一个小目标从上下文里切掉一半,导致模型学到残缺样本。我习惯在增强配置里把 scale 的范围收窄,比如 0.5~1.5,同时把 hsv_h、hsv_s 这些颜色增强参数调低,因为颜色变化对小目标的影响比大目标更敏感,目标本身像素就少,再一变色就彻底认不出来了。
3. 构建小目标数据集:红外小目标与可见光小目标的标注、切图与上下文
3.1 先给目标量尺寸:为什么 80% 的失败来自数据没准备好
所有小目标检测项目的第一个坑不是模型,而是数据。YOLO 的标注格式是归一化的中心点坐标加宽高,这本身对小目标就不友好——一个 5×5 像素的目标在 1920×1080 图上的宽高分别是 0.0026 和 0.0046,数值非常小,训练时哪怕标注偏差一个像素,归一化坐标的误差都会被放大。更麻烦的是,很多标注工具在缩小图片显示时根本看不清小目标,标注员漏标、错标是常态。
我先讲一个我常用的统计脚本,拿到数据集第一步就扫一遍标注框的尺寸分布。脚本逻辑很简单:读每张图的宽高,读对应 txt 标注的每个框,计算 w/width 和 h/height,再把结果按区间累加。这一步帮我在实际项目中筛掉过两个不靠谱的数据集——其中一个号称小目标数据集,扫完才发现超过 60% 的目标框面积占比在 10% 以上,根本是普通目标检测的难度。
import os from collections import defaultdict def stat_target_size(img_dir, label_dir): # 统计每个标注框相对原图的面积占比 size_buckets = defaultdict(int) total_boxes = 0 for img_file in os.listdir(img_dir): if not img_file.endswith(('.jpg', '.png')): continue img_w, img_h = get_image_size(os.path.join(img_dir, img_file)) label_file = os.path.join(label_dir, os.path.splitext(img_file)[0] + '.txt') if not os.path.exists(label_file): continue for line in open(label_file): parts = line.strip().split() if len(parts) < 5: continue _, cx, cy, bw, bh = parts[:5] w_pix = float(bw) * img_w h_pix = float(bh) * img_h area_ratio = (w_pix * h_pix) / (img_w * img_h) total_boxes += 1 if area_ratio < 0.003: # 约小于 0.3% 面积,视为小目标 size_buckets['small'] += 1 elif area_ratio < 0.01: size_buckets['medium'] += 1 else: size_buckets['large'] += 1 return dict(size_buckets), total_boxes这个脚本的作用不是精确评估,而是帮你快速判断“这个数据集值不值得花时间训练”。当一个数据集里小目标占比低于 20% 时,我建议先做采样或切图,而不是直接开训。切图是让小目标变成“相对大目标”的核心手段——把一张 1920×1080 的大图切成 640×640 的块,原本 16×16 像素的目标在切图后变成 5.3% 的相对尺寸,模型的检测难度就从“几乎不可能”变成了“有点难”。这也是红外小目标检测数据集最常见的预处理方式之一。
3.2 滑窗切图与标注坐标变换:切不好会把标注全切丢
切图这个操作看起来简单,但翻车点极多。最典型的问题是:目标刚好落在切片边界上,被一刀切成两半,导致两个残缺框都成为训练样本;或者切片之间的重叠率设计不合理,目标在训练集和验证集里出现重叠,造成数据泄漏。前者会让模型学出“半个人”这种错误特征,后者会让验证集 mAP 虚高,真实场景一测就现原形。
我采用的切图方案一般是这样:设置切片大小为 640×640,重叠率按 stride 计算,常用 stride 取 320 或 480。stride 越小,目标越不容易被切碎,但训练样本数量会大增。同时要在切图时保留坐标映射关系——原图上的目标框,减去切片左上角偏移量后得到新图上的坐标,如果该框被边界截断超过 30%,直接丢弃这半个框,不强行保留。
def slice_image_with_labels(img, labels, slice_size=640, stride=320): # img: PIL Image, labels: list of [cls, cx, cy, bw, bh] in absolute pixels h, w = img.size[1], img.size[0] patches = [] patch_labels = [] for y in range(0, h, stride): for x in range(0, w, stride): box = (x, y, min(x + slice_size, w), min(y + slice_size, h)) patch = img.crop(box) keep = [] for cls, cx, cy, bw, bh in labels: # 目标框中心点在切片内,且框主体在切片内 if x <= cx < x + slice_size and y <= cy < y + slice_size: nx, ny = cx - x, cy - y # 计算被截断的比例,超过阈值则丢弃 inter_w = min(cx + bw/2, x + slice_size) - max(cx - bw/2, x) inter_h = min(cy + bh/2, y + slice_size) - max(cy - bh/2, y) if inter_w >= bw * 0.7 and inter_h >= bh * 0.7: keep.append([cls, nx, ny, bw, bh]) if len(keep) > 0: patches.append(patch) patch_labels.append(keep) return patches, patch_labels这个函数有几个值得细看的参数。第一个是判断“目标是否保留”的方式:我选择“中心点必须在切片内”作为硬条件,这比“框与切片有交集就保留”更稳健,能避免一个目标同时出现在多个切片里却都是半截框的情况。第二个是截断阈值 0.7,也就是目标被切掉超过 30% 就丢弃。第三个是切片尺寸和步长,640×320 的 stride 意味着相邻切片有 320 像素重叠,一个目标最多出现在两个切片里,不会过度重复训练。切完后记得把标注从像素坐标换成归一化坐标,YOLO 格式要求除以切片宽高。
3.3 红外小目标数据集与上下文增强:小目标不能脱离背景学
红外小目标检测是最近很热的场景,机场跑道、森林防火、电力巡检里都有红外小目标需求。这类数据集和可见光小目标有个本质差异:目标没有颜色和纹理信息,只有 3~10 个像素的灰度突变,模型能依赖的只有目标与周围背景的对比度。直接把普通可见光的预训练权重拿来做红外小目标,效果往往很差,因为模型的浅层特征提取器学到的是 RGB 纹理,不是灰度对比。
对于这种场景,常见做法是先用目标的邻域裁剪上下文块,把目标周围一定范围(比如 32×32 或 64×64 邻域)的背景同时送入模型,让模型学习“什么背景、什么对比度下会出现目标”。这本质上是一种上下文增强。还有一种更直接的做法:把红外单通道图复制成三通道,再叠加一个高斯差分滤波后的边缘通道,等于在输入侧就帮模型把“灰突变”这个特征增强出来。
不管是红外还是可见光小目标,一个共同的增强原则是:不引入会破坏“目标-背景关系”的增强。比如 random erasing 这种把随机区域涂黑的增强,对大目标无伤大雅,对小目标可能就是灭顶之灾——目标本来就只有 10 个像素,再被涂掉 5 个,模型学到的是一个残缺目标。另一个要慎用的是上下翻转和 90 度旋转,如果目标本身有方向性(比如飞机头朝向),这类增强会创造不存在的样本,干扰分类器。我一般只保留轻微的色彩抖动和 10 度以内的随机旋转,这是小目标增强策略里的保守路线。
4. 训练与评估落地的完整操作:YOLOv8n 最小训练配置与参数调整
4.1 从“源码+流程教程”项目包里快速搭建项目目录
拿到一个标题里这种“YOLOv8n 小目标检测项目源码+流程教程”的压缩包,很多人第一件事是直接解压跑 training.py,这是个坏习惯。项目包质量参差不齐,我需要先检查它的结构是否完整,再决定怎么跑。一般一个规范的小目标项目包应该包含这几块:数据集目录(或数据集下载脚本)、模型配置文件(yaml)、训练入口脚本、评估脚本和推理脚本,外加一个 README 写明环境版本。
我一般按这套目录来梳理和改造:
project/ ├── data/ │ ├── images/train/ # 训练图像 │ ├── labels/train/ # 训练标注 │ ├── images/val/ # 验证图像 │ └── labels/val/ # 验证标注 ├── models/ │ ├── yolo11n_small.yaml # 自定义模型配置 │ └── yolo11n.pt # 预训练权重 ├── configs/ │ └── small_target.yaml # 数据与训练参数 ├── scripts/ │ ├── train.py # 训练入口 │ ├── val.py # 评估入口 │ └── infer.py # 推理与可视化 └── runs/ # 训练输出目录这套结构的核心是“数据和配置分离”。很多项目包把数据集路径写死在脚本里,换机器就要改代码,很浪费时间。我习惯把数据集路径统一放在一个 data.yaml 里让训练脚本读取,这样换数据集不需要动脚本。用 ultralytics 的 YOLO API 时,最标准的做法就是传一个 data.yaml 路径给模型,其余交给框架处理。
4.2 核心训练命令:配置项拆开讲清楚每个参数怎么定
用 YOLOv8n 训练小目标模型,我很少改动模型结构,而是把重点放在训练参数上。一个比较稳妥的起步配置是这样的:
yolo train \ model=yolo11n.pt \ data=small_target.yaml \ epochs=150 \ imgsz=1024 \ batch=8 \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01 \ mosaic=1.0 \ cos_lr=True \ patience=30 \ amp=True \ device=0,1逐个说下关键参数。imgsz=1024 是抬升空间分辨率的默认值,如果你的目标特别小(比如 10×10 以内),还可以尝试 1280;batch=8 在 1024 分辨率下约为 12G 显存,如果显存不够就减到 4 然后开 amp 和梯度累积。optimizer 选 AdamW 而不是默认的 SGD,这是小目标任务的一个习惯——AdamW 对稀疏梯度的处理更稳,小目标的梯度本来就稀疏,用 SGD 容易被噪声冲歪。lr0 设为 0.001 而非默认 0.01,因为小目标的损失面更崎岖,学习率大容易震荡。
cos_lr=True 的效果是把学习率按余弦曲线衰减,到后期逐步降低更新幅度,这对小目标稳定收敛有帮助。mosaic=1.0 表示开局一张图随机拼接 4 张图,能有效增加上下文多样性,但小目标本身像素少,拼接后可能被缩得更小。如果训练后发现小目标漏检严重,可以把 mosaic 降到 0.5 甚至关掉,对比一下效果再决定。amp=True 是混合精度训练,显存不够时必开。
4.3 评估阶段要看哪些指标:mAP50 与 mAP50-95 怎么取舍
小目标检测的评估有个特殊现象:模型在 mAP50 上表现尚可,在 mAP50-95 上惨不忍睹。原因是 mAP50-95 对预测框和真实框的 IoU 要求很高,从 0.5 一路算到 0.95 取平均,而小目标本身只有几十个像素,差两三个像素 IoU 就从 0.7 掉到 0.5,得分自然难看。所以小目标项目里,mAP50 才是主要参考指标,mAP50-95 只做辅助参考。
评估命令如下:
yolo val \ model=runs/train/exp/weights/best.pt \ data=small_target.yaml \ imgsz=1024 \ conf=0.001 \ iou=0.6 \ save_json=True这里把 conf 设为 0.001 是为了保留足够多的低置信度预测框。小目标在模型输出时置信度普遍偏低,如果把 conf 设为默认的 0.25,很多真目标被过滤掉,mAP 严重低估。iou=0.6 是 NMS 的 IoU 阈值,小目标重叠场景多,阈值设低一点能减少同类目标互相抑制。save_json=True 会把结果导出成 JSON 格式,方便后续按类别和尺寸分析漏检情况。
评估完还要看一个指标:Precision 和 Recall 的平衡点。小目标检测最常见的问题是 Recall 上不去,也就是漏检多。如果 Recall 明显低于 Precision,说明模型“宁缺毋滥”,可以从两个方向调:一是降低 conf 阈值,二是检查数据集里小目标样本是不是太少,需要回数据准备阶段重新做切图和采样。
4.4 训练曲线怎么看:三个关键信号与对应动作
训练过程里输出一堆日志和曲线图,但真正有判读价值的就三处:loss 曲线的下降形态、验证集 mAP 曲线的拐点、以及 Precision 和 Recall 的交叉点。
第一个信号:loss 曲线在 50 轮之后仍然剧烈震荡不下降。这通常是学习率偏高或者 batch 太小的问题,降低 lr0 到 0.0005 并把 batch 提高到 16 或以上,震荡会明显收敛。如果震荡伴随 loss 值特别高,还要检查数据标注里有没有大量空标注文件——YOLO 框架对空 txt 和缺失 txt 的处理方式不同,容易带来脏训练信号。
第二个信号:mAP 曲线在 100 轮左右还在缓慢上升但没有平台期趋势。这说明训练还没收敛,可以把 epochs 加到 300,或关闭早停。注意 patience=30 的意思是最多容忍 30 轮没有改进就停止,如果数据集较小,这个值可以放大到 50。
第三个信号:验证集 Precision 和 Recall 两条曲线交叉位置偏离。正常模型交叉点在 0.5 附近表示平衡;如果交叉点 Recall 只有 0.3,说明大量目标根本没被模型召回来。这时不要再调训练超参了,先回去看数据集的小目标占比,多数情况是样本不足或标注错误。
5. 小目标训练避坑:从项目实战里翻车次数最多的 5 个故障排查记录
5.1 翻车现场:loss 降得很好,验证集 mAP 却是 0
这是一个非常多见的诡异现象:训练时损失函数一路下降得很顺利,验证集 mAP 却是 0,模型看起来什么都没学会。排查下来发现原因往往不在训练参数,而在数据集与模型的匹配上——标注框尺寸小于模型下采样后特征图的最小粒度,网络根本没有机会学到这个目标。
检查方法很简单:用脚本统计训练集里目标框的最小尺寸,再对照模型的下采样倍数。YOLOv8n 最小输出层是 8 倍下采样,也就是 8×8 像素以内的目标在特征图上小于 1 个像素,这类目标基本是白标。解决路径是按 3.2 节的切图方案把小目标切出来放大,或者增加 P2 检测头让模型看到更细粒度的特征。
如果确认数据没问题,另一个可能是模型配置加载错了。项目包时常见的问题是把数据集路径写错,训练时无标签可读,loss 掉到很低是因为模型在学“输出空框”。这种要检查 data.yaml 里的路径是不是绝对路径,以及训练日志里 YOLO 框架打印出的图片数和标注框数是否合理。
5.2 红外小目标加载预训练权重效果差,是继续微调还是从头训
红外小目标数据集和 ImageNet 预训练权重之间的域差异太大,直接续训的效果常常不理想。现象是训练初期 loss 不降反升,之后虽然降下来,最终的 mAP 也不如预期。原因在于预训练模型的前几层卷积提取的是自然图像的边缘、颜色、纹理特征,而红外小目标只有低对比度的灰度梯度,两套特征分布完全不匹配。
我处理这个问题的顺序是先冻结模型的前 10 层,只训练 head 和 neck 部分,等 loss 稳定后再解冻全部层做完整热身。做法是在代码里用 model.model前面的参数设置 requires_grad=False,把一个阶段训练完后再设置回 True 继续训练。这样能避免预训练特征被红外样本粗暴冲刷,保留一部分基础卷积特征。另外输入侧做红外单通道转三通道时,建议加一个对比度拉伸,否则模型会把“几乎纯黑”的背景当成主要特征去拟合。
5.3 1024 分辨率训练 OOM,batch 调小后精度跟着崩
高分辨率输入是把双刃剑,OOM 是必经之路。常见应对是 batch 减半,但减半后精度也跟着掉。原因是 batch 太小导致 BN 层的统计量估计不准,小目标的梯度信号本来弱,再叠加不稳定的 BN,训练很容易亮红灯。
解决这个问题的顺序是:先开启 amp,再同时开梯度累积,而不是只减 batch。梯度累积把多个 step 的梯度累加后一次性更新,相当于等效 batch 不变。具体做法是 batch 调到 4,然后设置 accumulate=4,等效 batch 16。显存如果再不够,再把 mosaic 关闭或降到 0.5,因为 mosaic 拼图占用的显存峰值很高。如果还撑不住,最后才考虑把 imgsz 降到 896。
5.4 小目标漏检多,NMS 参数是背锅侠还是真凶
很多同学发现模型漏检小目标,第一反应是调低 confidence 阈值。这个操作没错,但只调 conf 不调 iou 的话,漏检改善有限。原因是小目标经常密集分布,两个目标挨得很近,NMS 会把置信度低的一个框当成重叠框抑制掉。也就是“漏检”其实是“被抑制”。
正确的调整顺序是先降低 iou 阈值,从默认 0.5 调到 0.3 或 0.25,让 NMS 更保守,不轻易合并相邻框;训练结束后再用验证集做一次 conf 扫描,找到 Recall 和 Precision 的交叉点,确定部署用的 conf 值。另一种做法是尝试给检测头加一个“针对小目标的第二路输出”,也就是 4.2 节 P2 头的思路,但那是结构改造,建议在 NMS 调参无效后再考虑。
5.5 训练到一半 mAP 突然掉头向下,是正常波动还是翻车拐点
训练到后期,验证集 mAP 出现一次明显下跌,很多人会认为这是噪声,继续跑完。实际上这往往是“灾难性遗忘”的先兆——后期学习率还偏高,模型在小目标上的特征被新样本冲掉。关键看 mAP 下跌后能否在 10 轮内回升,如果连续 20 轮不回升,说明最优权重已经出现在下跌点之前,训练参数需要回退。
此时不要手动中断,先用框架保存的 last.pt 和 best.pt 做对比验证,确认 best.pt 对应的 mAP 确实更高。然后重启一段超短训练,把 epochs 重置为原来的一半,lr0 降低到原来的 1/5,warmup_epochs 设成 3,模型加载 best.pt 继续跑,通常能在一个较小的学习率下再次拉升 mAP 并稳定到平台期。这是我在小目标训练中用过最频繁的“后悔药”操作。
6. 从验证到落地的一点进阶技巧:切片推断与可视化误差分析
小目标模型的训练告一段落后,真正决定能不能用的环节是推断策略。YOLOv8n 在 1024 输入下推理一张 4K 图,如果直接整图送入模型,小目标依然很难保住细节,因为 4K 图被缩小到 1024 时,目标可能小于 5 个像素。正确做法是切片推理:在推断阶段同样把大图切块,每个切片分别送入模型,再把检测框映射回原图坐标。只是切片的步长比训练时更大,通常取 1024, stride 取 512,因为推断时没有增强需求,重叠率可以低一些。
切片推理的代码核心是保持和训练时一致的坐标变换,只是方向反过来——模型输出的归一化框乘以切片宽高,再加上切片左上角坐标,就能还原回原图位置。切块之间的重叠区域允许同一个目标被重复检测,后处理时按标准 NMS 合并,用小目标场景的个人经验,重叠 80% 以上的同类框保留置信度高的那条即可。这部分逻辑不复杂,但实测量级是 4K 图上推断耗时翻 3 倍左右,如果你的业务对实时性有硬要求,就要在精度和速度之间做取舍。
另一个值得做的验证手段是可视化误差分析,把预测框、真实框和置信度画在同一张图上,按目标尺寸分组看漏检模式。你很快会发现一个规律:漏检的往往集中在某几个尺寸区间,而不是均匀分布。我一般用这段代码快速输出误检和漏检图:
from ultralytics import YOLO import cv2 model = YOLO('best.pt') img = cv2.imread('val_sample.jpg') results = model.predict(img, imgsz=1024, conf=0.1, iou=0.3) boxes = results[0].boxes.xyxy.cpu().numpy() clss = results[0].boxes.cls.cpu().numpy() # 在原图上画预测框,之后和标注框做逐像素比对 for box, cls in zip(boxes, clss): cv2.rectangle(img, (int(box[0]), int(box[1])), (int(box[2]), int(box[3])), (0, 255, 0), 2) cv2.putText(img, str(int(cls)), (int(box[0]), int(box[1]) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite('pred_vis.jpg', img)把预测框画出来后,拿 benchmark 工具按目标尺寸分组统计 Recall,基本就能定位是哪一类尺寸漏得最凶。如果是“极小目标整体漏检”,回数据准备阶段做切图放大;如果是“特定背景下的目标漏检”,回到数据增强,补那类背景的样本。这套误差分析流程是我每次小目标检测项目收尾时的固定动作,它比多看几篇论文和调参更能把效果往上拉一截。
做过的几个小目标项目里,让我印象最深的一次是项目上线前才发现训练时把 mosaic 开得太猛,大量小目标在拼图里被缩成了 1~2 个像素,模型压根没学过“完整的小目标”。后来直接把 mosaic 在最后 50 轮里降到 0,精度提升明显。所以如果你是第一次跑小目标检测,我的建议是:先把一个 baseline 完整跑通,确认标注和数据没问题,再逐项加增强和加分辨率,最后再谈调模型结构。小目标检测的难点不在算法有多深,而在每一步都要保住目标的“像素生命”。希望帮到你。
本文还有配套的精品资源,点击获取