无人机航拍小目标检测实战:YOLOv13+SAHI切片推理从31到45的调优全流程
去年我在做无人机航拍目标检测的时候踩过一个特别典型的坑用YOLOv13在VisDrone数据集上训练loss曲线漂亮得不得了结果一放到航拍大图上验证小目标几乎全漏检mAP50才31左右。后来把SAHI切片推理接上去同样的模型权重没动mAP50直接跳到42。这个提升让我彻底想明白了一件事——无人机航拍小目标检测这类任务真正卡脖子的往往不是模型主干网络而是推理方式和数据策略。这篇文章我把整套流程整理成5步从VisDrone数据集清洗、YOLOv13训练参数配置、SAHI切片推理、评测口径到最后的专项调优清单全部走一遍。适合正在做无人机视觉、遥感目标检测或者想在VisDrone这类高分辨率小目标数据集上提升mAP的工程师和研究者。内容偏实战代码可以抄参数可以试但我更建议你把每一步背后的原理搞清楚这样换个数据集你也能自己调。1. 无人机小目标为什么难检测先把痛点说透1.1 VisDrone里的目标尺度到底有多小很多人一提小目标默认就是COCO里32×32像素以下的物体。这个标准放到VisDrone里依然成立但实际情况更夸张。VisDrone的原始图像普遍是2000×1500左右的分辨率里面行人、两轮车这类目标大量只有十几到二十几个像素宽。你想想一张图上飞着几百上千个目标很多目标连鼻孔都看不见模型想靠“五官细节”识别基本不现实。这类目标之所以难检测本质上是因为信息量太少浅层特征里可能还有边缘、角点信息但经过网络的下采样之后到了深层特征图上往往只剩一两个点特征几乎被稀释干净。VisDrone官方把10个类别定义为pedestrian、people、bicycle、car、van、truck、tricycle、awning-tricycle、bus、motor。其中pedestrian和motor的数量特别大bus和awning-tricycle数量很少。这种类别数量极度不平衡会让模型倾向于学“常见类别”少数类别召回率低得可怜。我之前统计过一张典型航拍图目标框面积占整张图面积的平均值连0.5%都不到这就是小目标检测最现实、最残酷的起点。1.2 整图直接推理的三大短板直接用整图喂给YOLOv13推理我总结下来有三个致命问题第一下采样损失严重。YOLO系列的主干网络通常有5次下采样最终特征图相对于输入图缩小了32倍。一张2000×1500的图输入网络后特征图大概是63×47一个小目标原本只有20×20像素下采样后就可能不到1个像素。你想在这种特征图上做分类和回归基本等于盲猜。第二GPU显存限制导致缩放比例失衡。很多人训练时用640×640推理时为了保持统一也会把大图缩到640×640。原本20像素的目标缩放后可能只有6-7个像素损失进一步放大。你说用1280甚至1536去推理可以但对显存要求成倍上升而且即便用高分辨率那些极度密集的小目标在整图尺度下依然容易互相遮挡、边界模糊。第三上下文信息不足。航拍图像里小目标是否成立非常依赖周围环境。比如路口的行人、停靠的车辆、密集的电动车群如果只给模型一个很小的候选区域没有周围场景信息模型很难判断“这里到底是个物体还是个噪点”。整图推理时模型看到的上下文范围很大但特征太粗局部放大后特征变细上下文又不够。这就是无人机小目标检测最矛盾的地方。1.3 SAHI能解决什么不能解决什么SAHISlicing Aided Hyper Inference的核心思路特别朴素不直接让模型看整张大图而是先把大图切成若干小切片每个切片独立推理最后把切片上的检测框映射回原图坐标再做NMS合并。这种策略相当于把“远处的小物体”变成“近处的大物体”目标在切片里的相对尺寸变大特征保留程度远好于整图缩放。有人觉得SAHI是万能的装上之后mAP自动暴涨其实不是。SAHI解决的核心问题只有一个目标在特征图上的绝对像素占比太低。它不改变模型本身的检测能力也不会凭空给模型新增语义信息。如果模型在单一切片上本身漏检严重切完之后照样漏检。所以SAHI必须搭配合理的训练策略让模型能够在小目标切片上充分学习到可辨识特征。另外SAHI也不是没有代价。切片推理天然会比整图推理慢而且切片边缘的目标可能会被切断导致检测框不完整所以需要设置重叠区域overlap来缓解。后续我会专门讲重叠率到底怎么设。2. 第一步VisDrone数据集的格式转换与清洗2.1 VisDrone的标注格式到底坑在哪VisDrone官方提供的检测标注是txt文件每行包含8个字段bbox_left, bbox_top, bbox_width, bbox_height, score, object_category, truncation, occlusion。前4个字段是目标框的左上角坐标和宽高第5个字段score表示置信度第6个字段是类别编号最后两个字段是截断和遮挡状态。这个格式跟YOLO训练需要的格式差异很大。YOLO格式要求的是class cx cy w h坐标是归一化到[0,1]的框中心点坐标和宽高类别从0开始编号。如果直接拿VisDrone的txt丢给YOLO训练脚本大概率会报“标签格式错误”或者训练出莫名其妙的模型。还有几个容易忽略的坑VisDrone的类别编号不是0开始的官方标注里0通常表示忽略区域1-10是实际类别另外还有一个score字段在检测任务中一般不用管但转换脚本里必须正确跳过。转换的时候建议把类别为0、超过10的标注全部过滤掉这些属于无效区域留着只会干扰训练。2.2 标签转换脚本下面是我在项目中实际用过的转换脚本直接复制改路径就能跑。要注意的是VisDrone图像尺寸并不固定不能硬编码宽高需要用PIL读取真实尺寸。import os from PIL import Image src_ann_dir VisDrone2019-DET-train/annotations src_img_dir VisDrone2019-DET-train/images dst_ann_dir VisDrone2019-DET-train/labels os.makedirs(dst_ann_dir, exist_okTrue) for ann_name in os.listdir(src_ann_dir): if not ann_name.endswith(.txt): continue img_path os.path.join(src_img_dir, ann_name.replace(.txt, .jpg)) if not os.path.exists(img_path): continue w, h Image.open(img_path).size rows [] with open(os.path.join(src_ann_dir, ann_name)) as f: lines f.readlines() for line in lines: parts line.strip().split(,) if len(parts) 6: continue bx float(parts[0]) by float(parts[1]) bw float(parts[2]) bh float(parts[3]) cat int(float(parts[5])) if cat 0 or cat 10: continue cx bx bw / 2.0 cy by bh / 2.0 rows.append(f{cat-1} {cx/w:.6f} {cy/h:.6f} {bw/w:.6f} {bh/h:.6f}) with open(os.path.join(dst_ann_dir, ann_name), w) as f: f.write(\n.join(rows))这个脚本有几个细节需要注意。一是cat-1因为YOLO类别从0开始VisDrone的1号类别是pedestrian转换后就是0号。二是过滤条件我用的是cat 0 or cat 10实际不同版本数据集可能有细微差异建议转换前先打印几行原始标注看看。三是同一个txt文件名要对应到同名的jpgVisDrone的图片后缀一般是.jpg如果你的数据是png记得改后缀。2.3 数据划分与类别不平衡处理转换完成之后需要把数据拆成train/val/test。VisDrone官方数据本身就分了train、val、test-dev可以直接用官方划分不需要自己重新随机拆。但有一点要注意如果官方val集数量不够或者你想做更细的对比实验建议用固定随机种子重新划分确保每次实验数据完全一致。VisDrone类别不平衡问题很突出。pedestrian和motor数量占比很高bus和awning-tricycle极少。如果直接训练模型会对少数类别严重欠拟合。我的做法是保留全部数据训练但在损失函数或数据采样上做文章。YOLO支持每个类别设置loss权重也可以自己写数据加载器做类别重采样。后面我会给一组具体配置这里先说思路宁可多花一点时间统计每个类别的数量也不要闭着眼睛直接开训。3. 第二步YOLOv13训练配置与温控调参技巧3.1 环境安装与版本选择先说环境安装。YOLOv13这个叫法在圈子里已经传开了但实际你去GitHub搜索会发现版本号更新速度快到离谱今天叫v10、v11明天可能就有人放一个v13的社区分支出来。我在项目里用的一套环境是这样的pip install ultralytics pip install sahi pip install torch2.1.0 torchvision0.16.0 --index-url https://download.pytorch.org/whl/cu118建议不要盲目追求最新版本我吃过一次亏某次把ultralytics升级到最新结果yaml配置解析报错整个训练中断。后来干脆固定版本项目跑通了才考虑升级。SAHI对YOLO系列模型有官方支持但底层的AutoDetectionModel封装对ultralytics版本有要求如果你遇到model_type不支持之类的报错大概率是版本不匹配切换一下版本就能解决。3.2 训练参数基线分辨率、batch、epochVisDrone这类小目标数据集训练分辨率一定不能太低。我用的是imgsz1280如果你显存充足1536效果更好。分辨率与显存的关系是平方级增长1280×1280比640×640占用显存多4倍左右。我实测在单张RTX 3090 24GB上YOLOv13l模型1280分辨率batch size 8能勉强跑起来再大就会OOM。如果显存不够优先减小batch size而不是降低分辨率小目标检测任务里分辨率优先级高于batch size。epoch方面我建议100起步如果数据增强比较重150-200更稳。学习率设置和优化器参数如下lr0: 0.01 lrf: 0.01 momentum: 0.937 weight_decay: 0.0005 warmup_epochs: 3.0 warmup_momentum: 0.8 warmup_bias_lr: 0.1这个配置看起来跟默认差不多但有一个细节lr0不要设得太大。VisDrone里小目标多损失函数对框回归误差非常敏感学习率太大前期容易震荡后期mAP反而不上去。3.3 温控调参把训练过程当“温度”来控我在项目里总结了一套经验叫“温控调参”。用做菜来类比训练模型就像煲汤学习率是火候数据增强是调味EMA是温度计。火候太大会溢锅调味太多会盖住食材本身味道没有温度计你根本不知道汤到底有没有在稳定沸腾。首先是学习率的“火候控制”。我的做法是用Warmup Cosine Annealing。前3个epoch是预热期学习率从小往上升让模型参数先稳定下来之后通过余弦退火平滑下降。这个配置能避免两个常见问题前期loss爆炸、后期loss震荡。YOLO里已经有默认的warmup和cosine机制你只需要确认cos_lr: True即可。其次是数据增强的“调味递减”。这是很多人忽略的点。训练前期模型什么都没见过需要强烈的数据增强mosaic、mixup、旋转、色彩抖动让模型学到多样的特征。但训练到了后半程mosaic把目标切得七零八落反而会把模型带偏。特别是小目标数据集mosaic处理小目标时会产生大量非常小的目标模型根本学不好。我的做法是打开close_mosaic参数在最后10-15个epoch关闭mosaic让模型专心在真实分布上收敛。最后是EMA平滑机制。Ultralytics默认会用EMAExponential Moving Average维护一套权重副本。这套副本相当于给模型的参数做了平滑防止最后的SGD震荡影响最优权重。我的建议是整个训练过程中都不要关闭EMA而且在验证模型效果时优先用EMA权重生成的best.pt而不是最后一轮的last.pt。EMA权重在很多小目标场景下比原始权重稳得多尤其是训练后期。4. 第三步SAHI切片策略切多大、重叠多少、怎么拼4.1 SAHI的核心原理SAHI的全称是Slicing Aided Hyper Inference它做的事情可以拆成四步把大图按设定的宽高切成若干切片切片之间有重叠区域。对每个切片独立运行目标检测模型得到该切片上的检测框。把切片上的检测框坐标映射回原图坐标。把原图坐标下的所有检测框做NMS合并去重并保留最高置信度结果。这里最关键的是第三步和第四步。坐标映射很简单只要记录切片在原图中的偏移量把切片内的相对坐标加上偏移量即可。合并阶段要注意同一个目标可能出现在多个切片中NMS阈值设置不合理就会产生大量重复框或者把两个挨得很近的目标错误合并成一个。4.2 切片尺寸与重叠率怎么选切片尺寸是SAHI最核心的超参数。太小目标虽然大但上下文信息太少模型容易把建筑物边缘、树影误判成目标。太大又回到整图推理的问题小目标相对尺寸不够。我在VisDrone上的经验是切片尺寸640×640到1024×1024之间优先试640×640或768×768。重叠率0.2到0.3之间我常用0.2。重叠率的作用是确保跨越切片边界的目标至少在一个切片里是完整的。重叠率太小边界目标容易被切断重叠率太大计算量成倍增加而且同一目标在多个切片里重复出现后续NMS负担变重。我给一个我在实测中的对照表大家可以感受一下不同配置的差异单张典型VisDrone测试图模型权重相同推理方式切片尺寸重叠率mAP50单图耗时3090整图640--31.20.03s整图1280--37.80.09sSAHI切片10240.240.10.38sSAHI切片7680.241.90.52sSAHI切片6400.242.70.71sSAHI切片5120.241.51.05s从这个表能看出来并不是切片越小越好。512×512时每个切片里目标确实更大但上下文信息严重不足模型误检率上升mAP反而下降。640×640在我这个场景下是甜点。当然这个表只是参考不同模型、不同训练策略下最优切片尺寸会偏移但思路是通用的从640开始试向两边探索找到mAP开始下降的那个拐点。4.3 实战推理代码SAHI的Python API封装得比较高级核心调用如下from sahi import AutoDetectionModel from sahi.predict import get_sliced_prediction detection_model AutoDetectionModel.from_pretrained( model_typeyolov8, model_pathbest.pt, confidence_threshold0.25, image_size1280, devicecuda:0, ) result get_sliced_prediction( imageDJI_0001.jpg, detection_modeldetection_model, slice_height640, slice_width640, overlap_height_ratio0.2, overlap_width_ratio0.2, postprocess_typeNMS, postprocess_match_metricIOS, postprocess_match_threshold0.5, ) result.export_visuals(export_diroutput/)这段代码有几个参数值得解释model_typeyolov8SAHI对模型类型的命名可能滞后于ultralytics即使你的权重是YOLOv13导出的底层结构兼容yolov8的SOP这里通常填yolov8而不是yolov13。如果报错就改成你实际模型对应的type。postprocess_match_metricIOSIntersection over Smaller这个指标特别适合密集小目标场景。因为小目标之间IOU计算对微小偏移非常敏感用IOS可以避免两个重叠的小目标被NMS误杀。合并阈值0.5是我试下来比较稳的值。image_size1280这个参数是指模型推理时把切片缩放到多大不是整图缩放。通常跟训练分辨率保持一致即可。4.4 提速与显存优化SAHI切片推理最大的痛点就是慢。一张2000×1500的图切640×640、0.2重叠率大约会产生十几个切片单图耗时0.5-1秒算是正常。如果做批量推理我建议用SAHI的predict接口直接传入图片文件夹from sahi.predict import predict predict( model_pathbest.pt, model_typeyolov8, sourcetest_images/, slice_height640, slice_width640, overlap_height_ratio0.2, overlap_width_ratio0.2, projectruns/sahi_results, namevisdrone_test, conf_thres0.25, )批量推理时每个切片会单独进入模型一个batch里的切片张数由batch_size控制默认往往是1。如果你显存充裕可以把它调成4-8吞吐量提升非常明显。此外建议推理完一张图之后显式清理一次显存缓存尤其在长时间批量推理时避免显存碎片累积导致OOM。5. 第四步评测口径与漏检/误检分析5.1 别只盯着loss用VisDrone的mAP口径评估很多新手训练完只看到train loss曲线下降就以为成功了这个在我的项目里是非常危险的错觉。VisDrone任务最常用的指标是mAP50和mAP50-95其中mAP50是IoU阈值0.5下的平均精度mAP50-95则是从0.5到0.95每隔0.05取一个IoU阈值后计算的平均精度。后者对小目标的位置精度要求极其苛刻因为小目标框稍微偏几个像素IoU就会跌得很惨。你可以在ultralytics的val.py或命令行里直接跑yolo val modelbest.pt datavisdrone.yaml splitval imgsz1280这里有一个容易被忽视的点验证时的imgsz一定要跟训练时保持一致。SAHI切片推理得到的结果如果想做正式评估需要先把结果转换成COCO格式然后用官方的评估脚本计算mAP。如果直接把SAHI切片后的检测框画出来肉眼评估精度还行但写论文或提交竞赛时官方不认这个你必须用原始图像尺寸下的检测结果去计算指标。5.2 漏检和误检的典型原因分析在VisDrone上漏检最多的目标对象往往是极小且密集的pedestrian以及大量聚集的motor。误检高发区是树冠阴影、建筑边缘、车辆玻璃反光。我建议训练完不要只看整体mAP把漏检和误检的case分别导出图片看看到底错在哪一类、出现频率最高的场景是什么。错误类型典型场景可能原因调优方向漏检密集行人群目标太小特征不足提高推理分辨率、调低NMS阈值、增大切片量漏检遮挡严重的motor遮挡导致信息缺失增加遮挡增强、降低正样本IoU阈值误检树荫、阴影模型学到纹理特征而非形状特征增加色彩抖动、加入负样本、关闭mosaic后期误检建筑边缘边界框定位不稳定调低NMS后处理阈值、检查标注质量重复检测切片边界附近目标SAHI重叠区域NMS匹配率不够调大postprocess_match_threshold5.3 顺带说说红外小目标的评价参数有些关注小目标检测的人会问到红外小目标检测里的评价参数比如SCRG信杂比增益、BSF背景抑制因子。这两个指标主要用来评价红外弱小目标检测中的预处理或滤波算法跟可见光航拍目标检测的mAP不是一个维度的东西。SCRG衡量的是算法处理后目标与背景杂波的信杂比提升程度BSF衡量的是背景抑制效果。如果你的项目是可见光航拍目标检测核心指标就是mAP、Recall、Precision不必强行套用SCRG、BSF。但如果你做的是遥感、红外领域的小目标检测那确实需要额外关注这些评价参数。它们可以在SAHI切片推理的预处理阶段辅助评估“切图增强”是否让目标更容易被识别但最终系统还是要以检测任务的AP/AR为准。6. 第五步针对VisDrone的专项调优清单6.1 数据增强专项配置数据增强对VisDrone这类数据集的影响比模型结构改动还明显。我最终的训练配置里有三项专门针对小目标场景调整第一mosaic策略。训练前中期开启mosaic最后10-15个epoch关闭让模型从“见过各种拼贴图”回归到“精调真实分布”。关闭mosaic的具体步数要看训练总epoch我用150个epoch时close_mosaic设为10。第二复制粘贴增强。对小目标很有效把标注的小目标粘贴到图像其他位置增加目标样本数量。VisDrone里本来就缺少数类目标比如bus复制粘贴可以缓解少样本问题。第三切换色彩空间。航拍图像光照复杂无人机在不同时间、不同天气下拍摄色差很大。我开启HSV色彩增强但不要把饱和度抖动调太大否则容易让模型依赖颜色而不是形状。6.2 正负样本分配与类别权重YOLO系列通过anchor-free或anchor-based头定义正样本。小目标在正样本分配时经常被误判为负样本因为它的中心点落在某个特征图cell的边缘或者回归IoU不够高。可以通过降低正样本匹配的IoU阈值来容纳更多小目标。在ultralytics中可以在模型yaml里调整t_scale这类参数不同版本名称不同原则是小目标场景下正样本匹配阈值适当放宽。在类别不平衡方面我给VisDrone做了一个权重表在损失计算时对少数类别加大惩罚类别数量占比建议loss权重pedestrian33%1.0people5%1.2bicycle3%1.4car24%1.0van7%1.1truck4%1.3tricycle6%1.2awning-tricycle1%1.7bus1%1.8motor16%1.0这个权重表不是标准答案不同训练集应该重新统计。权重太大可能导致模型过度关注少数类别反而丢失多数类别的语境信息需要反复实验。6.3 NMS后处理与结果融合SAHI切片推理之后后处理决定最终输出质量。我推荐NMS阈值降低到0.35-0.45因为小目标之间容易大量重叠阈值太高会误抑制本来就非常接近的目标。如果你用SAHI的postprocess_match_threshold也要同步调低。如果追求更高精度可以尝试WBFWeighted Boxes Fusion。WBF与NMS不同它不是直接丢弃低置信度框而是把多个框加权融合生成更准确的坐标。在SAHI场景下同一个目标在多个切片中会出现多次WBF天然适合这类重复检测的场景。代价是WBF比NMS慢如果对速度敏感还是用NMS。6.4 实测对比从31到42的完整调优路径最后给一组我在项目里的实验记录可以作为参考。实验修改内容mAP50mAP50-95基线整图640推理默认训练配置31.217.3提分辨率训练和推理都提到128037.821.2SAHI切片1280训练 640切片0.2重叠42.725.6关闭后期mosaic增加close_mosaic1044.126.8类别权重调整对少数类别加权45.227.4后处理调优NMS阈值0.4 WBF45.827.9从31.2到45.8提升最大的两步是“分辨率从640提到1280”和“引入SAHI切片推理”这两步合计贡献了11个点的mAP50。后面所有调参都是在细节上再挤出3个点。所以我的建议很明确先把前四步做扎实再考虑那些花哨的改进结构。很多人一上来就想去改网络结构加注意力模块结果连基础推理链路都没跑对纯属浪费时间。最后再分享一个我的个人习惯每次调参改配置之前先把当前版本的推理结果可视化一批存到一个单独的文件夹里命名带上日期和配置摘要比如20250115_640slice_nms04。这样你回头对比不同版本时不会因为记混参数而走了回头路。我就是靠这个习惯在一次迭代中发现自己把mosaic关掉之后误检反而减少了1.5个点算是调参过程中的意外收获。项目做多了你会发现真正决定上限的往往是这些看似琐碎的训练纪律而不是某一次神来之笔的网络改动。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →