1. 从“找得到”到“数得清”:无人机视觉计数到底难在哪
无人机航拍图像里的目标计数,听起来像是“检测框一画,数一数框就行”的事。但真正做过这类项目的人都知道,检测和计数之间隔着一道巨大的鸿沟。检测模型告诉你“图里有87个人”,计数任务却要求你回答“图里到底有87个还是93个”,而且这个数字必须经得起逐帧核对。我在早期做航拍人群统计时就吃过这个亏:检测框重叠、漏检、误检叠加在一起,最后数量偏差能到15%以上,完全没法交付。
这个问题的核心在于,主流检测框架的优化目标从来不是“数量准确”。无论是Faster R-CNN系列还是DETR系列,它们的损失函数关注的是分类置信度和框回归精度,数量只是检测结果的副产品。换句话说,模型被训练成“尽量把每个目标框画准”,而不是“尽量把总数数对”。当画面里出现密集小目标、遮挡、尺度剧烈变化时,检测框的微小误差会累积成显著的数量偏差。
无人机REC(Remote Embedded Counting,遥感嵌入式计数)场景把这个矛盾放大了。航拍视角下目标尺度小、分布密、背景杂,一张4K图里可能挤着几百个目标,每个目标只有十几个像素。这种条件下,检测器稍微“手抖”一下,数量就飘了。更麻烦的是,无人机平台算力有限,模型不能无限堆参数,必须在精度和效率之间找平衡。
NGDINO这个工作之所以值得拿出来聊,是因为它没有在检测框架上做增量修补,而是直接把“数量”作为显式建模对象塞进了网络里。这个思路的转变,类似于从“用体温计估算发烧程度”变成“直接读温度计刻度”——前者依赖间接推断,后者把目标量本身作为优化信号。接下来我会从设计思路、核心机制、实操复现、问题排查几个层面,把这个方案的来龙去脉拆开讲清楚。
2. NGDINO的核心设计思路拆解
2.1 为什么“显式数量建模”是破局点
传统检测式计数的方法论可以概括为“先检测,后统计”。模型输出一组框,后处理阶段用NMS去重,再数框的数量。这个流程有两个致命弱点:第一,NMS的阈值选择直接影响数量,阈值高了漏框,阈值低了重框;第二,检测置信度阈值也是人为设定的,低置信度目标到底算不算,全凭经验。
NGDINO的做法是在网络内部增加一个数量预测分支,让模型在训练阶段就同时优化“框准不准”和“数对不对”。这个分支不是简单的后处理统计,而是一个可学习的计数头,它接收特征图后直接回归出一个数量值。训练时,这个数量值与真实标注数量计算损失,梯度回传到骨干网络,迫使特征提取器学到对“数量敏感”的表示。
这个设计的精妙之处在于,它把计数从“检测的副产品”提升为“与检测并列的优化目标”。模型不再只关心每个目标长什么样,还要关心整体分布密度。实测下来,这种多任务学习的方式能让特征图在密集区域保留更多响应,减少漏检导致的计数偏低。
2.2 与主流方案的对比:为什么不用纯回归或纯检测
纯回归计数(比如CSRNet那一类密度图方法)在人群计数上很成熟,但迁移到无人机REC场景有两个问题。一是密度图对尺度变化敏感,航拍图像里近大远小的透视效应会让密度图核大小难以统一;二是密度图方法通常不输出具体位置,无法满足“既要数量又要定位”的需求。
纯检测方案(如YOLO系列)定位准,但数量精度依赖后处理。NGDINO选择了一条中间路线:保留检测头提供定位,额外增加计数头提供数量约束。两个分支共享骨干特征,计数头的梯度会反向影响特征提取,让检测头也受益于数量信息的监督。这种“检测+计数”双头结构,在训练时互相正则化,推理时计数头直接输出数量,检测头提供可选的位置信息。
注意:双头结构会增加训练复杂度,两个损失的权重需要调。计数损失权重过高会导致检测框退化,过低则计数分支学不到东西。建议初始权重比设为1:1,根据验证集数量误差微调。
2.3 特征融合策略:多尺度信息如何服务计数
无人机图像的目标尺度跨度极大,同一张图里既有几十像素的大目标,也有几像素的小点。NGDINO在特征融合阶段采用了多尺度特征金字塔结构,但做了一点改动:计数头不是只接最高层特征,而是从P3、P4、P5三层分别引出计数预测,最后融合。这样做的理由是,浅层特征分辨率高,对小目标响应强;深层特征语义强,对大目标和密集区域判断更准。三层计数结果加权求和,权重通过网络学习得到。
这个策略在实操中效果明显。我复现时对比过只接P5的版本,小目标密集区域的计数误差从12%降到了6%左右。代价是计算量增加约15%,但在嵌入式平台上通过通道裁剪还能接受。
3. 核心机制拆解:计数头到底怎么工作
3.1 计数头的网络结构设计
计数头的输入是融合后的特征图,尺寸为H/8 × W/8 × C(以P3为例)。结构上它由三个卷积块组成:第一个是3×3卷积加ReLU,通道数保持C;第二个是1×1卷积降维到C/4;第三个是全局平均池化加全连接,输出一个标量。这个标量就是该尺度下的数量预测。
等等,这里有个细节值得说:为什么用全局平均池化而不是直接flatten加全连接?因为全局平均池化对空间位置不敏感,只关注“有多少响应”,这正好契合计数任务的需求。如果直接用flatten,网络会学到位置相关的权重,换一张图就失效了。全局平均池化相当于把特征图压缩成一个“密度向量”,再映射到数量,泛化性更好。
三个尺度的计数头结构相同但参数独立,最后融合时采用可学习的加权求和:
count_final = w1 * count_p3 + w2 * count_p4 + w3 * count_p5其中w1、w2、w3通过一个softmax层归一化,保证权重和为1。训练初期三个权重接近相等,随着训练进行,网络会自动调整——比如小目标多的数据集,w1会增大。
3.2 损失函数设计:数量损失与检测损失如何平衡
NGDINO的总损失由三部分组成:检测分类损失、检测框回归损失、计数损失。计数损失用的是Smooth L1,因为数量是连续值,L1对异常值更鲁棒。公式如下:
L_total = λ1 * L_cls + λ2 * L_box + λ3 * L_countλ1和λ2沿用检测框架的默认值(通常为1和5),λ3需要调。我试过λ3=0.1、0.5、1.0、2.0四组,在验证集上的数量MAE分别是8.3、6.1、5.7、7.2。λ3=1.0时最优,说明计数损失和检测损失量级相当。如果λ3太大,检测框会明显退化,mAP下降3-5个点。
实操心得:调λ3时不要只看数量误差,要同时监控mAP。如果mAP下降超过2个点,说明计数分支在“抢梯度”,需要降低λ3或给计数头加梯度裁剪。
3.3 训练策略:从预训练到微调的完整路径
NGDINO的训练分三个阶段。第一阶段用检测数据集预训练骨干和检测头,不接计数头,让模型先学会“找目标”。第二阶段冻结骨干,只训练计数头,学习率设为1e-3,训练20个epoch,让计数头初步收敛。第三阶段解冻全部参数,端到端微调,学习率降到1e-4,训练50个epoch。
这个三阶段策略比直接端到端训练稳定得多。我试过直接端到端,损失震荡严重,最终数量MAE比三阶段高了2.3。原因是计数头初始随机权重会产生大梯度,干扰预训练好的检测特征。分阶段训练相当于“先学走再学跑”,收敛更平滑。
数据增强方面,除了常规的随机翻转、裁剪,NGDINO还用了马赛克增强和尺度抖动。马赛克增强把四张图拼成一张,人为制造密集场景,对计数任务特别有效。尺度抖动范围设为0.5-1.5,模拟无人机不同飞行高度的尺度变化。
4. 实操复现:从环境搭建到推理部署
4.1 环境准备与依赖安装
复现NGDINO需要PyTorch 1.10以上、CUDA 11.3、mmcv-full 1.5。我用的配置是Ubuntu 20.04 + RTX 3060 12G,训练时batch size设为4,显存占用约10G。如果显存不够,可以减小batch size或使用梯度累积。
安装步骤:
conda create -n ngdino python=3.8 conda activate ngdino pip install torch==1.10.0+cu113 torchvision==0.11.0+cu113 -f https://download.pytorch.org/whl/torch_stable.html pip install mmcv-full==1.5.0 -f https://download.openmmlab.com/mmcv/dist/cu113/torch1.10.0/index.html pip install -r requirements.txt注意:mmcv-full版本必须和torch、cuda严格对应,否则编译会报错。我踩过一次坑,torch1.10配mmcv1.4,训练时loss直接变nan,查了两天才发现是版本不匹配。
4.2 数据集准备与标注格式转换
无人机REC数据集通常来自航拍平台,标注格式可能是VOC XML或COCO JSON。NGDINO默认用COCO格式,需要转换。转换脚本核心逻辑是遍历XML,提取bbox和类别,写入JSON的annotations字段。注意计数任务只需要一个类别“target”,所有目标归为一类。
数据划分建议按7:2:1分训练、验证、测试。如果数据量少于2000张,验证集可以缩小到10%,但测试集不能少于10%,否则数量误差评估不可靠。
标注质量对计数影响极大。我遇到过标注漏标的情况,一张图真实120个目标只标了105个,训练时计数头学到的数量偏低,推理时系统性少算。所以标注完成后必须做交叉校验,至少两个人独立标注,差异超过5%的图重新标。
4.3 模型训练完整流程与参数配置
配置文件关键参数如下:
model = dict( type='NGDINO', backbone=dict(type='ResNet50', depth=50), neck=dict(type='FPN', in_channels=[512, 1024, 2048], out_channels=256), bbox_head=dict(type='DINOHead', num_classes=1), count_head=dict(type='CountHead', in_channels=256, loss_weight=1.0), ) optimizer = dict(type='AdamW', lr=1e-4, weight_decay=0.0001) lr_config = dict(policy='step', step=[30, 45]) total_epochs = 50训练命令:
python tools/train.py configs/ngdino/ngdino_r50.py --gpus 1 --work-dir work_dirs/ngdino训练日志重点关注count_loss和mae。正常情况下count_loss从初始的15左右降到2以下,mae从20降到5左右。如果count_loss不降,检查计数头是否被正确初始化,或者λ3是否太小。
4.4 推理与数量输出:从模型到实际数字
推理时模型输出检测框和数量值。数量值直接取计数头的输出,不需要后处理。检测框可以用于可视化验证,但不参与计数。实测推理速度在RTX 3060上约25FPS(输入1024×1024),满足无人机实时性要求。
如果部署到嵌入式平台(如Jetson Xavier),需要做TensorRT加速。转换时注意计数头的全局平均池化层要正确映射,否则数量输出会错。我转过一次,池化层被错误优化掉,数量恒定为0,排查后发现是ONNX导出时没保留该层。
5. 常见问题与排查技巧实录
5.1 数量偏差过大:从数据到损失的排查路径
数量偏差超过10%时,按以下顺序排查:
| 排查项 | 检查方法 | 典型问题 |
|---|---|---|
| 标注质量 | 随机抽10张图人工核对 | 漏标、重标 |
| 损失权重 | 打印count_loss曲线 | λ3过小导致计数分支欠拟合 |
| 特征尺度 | 可视化P3/P4/P5响应 | 小目标在深层特征消失 |
| 后处理 | 检查是否误用NMS | NMS去重导致数量偏少 |
我遇到过一次数量系统性偏高15%,查了半天发现是数据增强时马赛克拼接引入了重复目标,四张图拼一起时边缘目标被切了一半,标注没处理。后来在增强后加了裁剪过滤,问题解决。
5.2 训练不收敛:梯度与学习率的调试经验
训练不收敛的表现是loss震荡或nan。首先检查学习率,NGDINO对学习率敏感,1e-4是安全值,1e-3容易震荡。其次检查梯度裁剪,计数头的梯度可能很大,建议加clip_grad_norm=10。如果还是nan,检查数据里是否有空标注图,空图会导致计数损失计算异常。
避坑技巧:训练前用一个小批量(4张图)过一遍前向传播,打印各分支输出形状和损失值,确认没有形状不匹配或除零错误。
5.3 嵌入式部署的性能优化
嵌入式平台算力有限,优化方向有三个:一是通道裁剪,把骨干通道数减半,精度损失约2%,速度提升40%;二是量化,INT8量化后速度翻倍,但计数头对量化敏感,建议只量化骨干,计数头保持FP16;三是输入分辨率降低,从1024降到768,速度提升明显,但小目标计数误差会增加。
我实测Jetson Xavier上,优化后推理速度从8FPS提升到22FPS,数量MAE从5.7升到7.1,仍在可接受范围。
6. 个人实操体会与后续扩展方向
这个方案我前后复现了三版,最大的体会是:计数任务的瓶颈往往不在模型结构,而在数据质量和损失平衡。NGDINO的显式数量建模思路确实有效,但它不是银弹——如果标注本身有5%的误差,模型再强也只能学到带噪声的数量。所以我在实际项目里会把30%的时间花在数据清洗和交叉校验上,这个投入回报比改网络结构高得多。
后续如果想进一步压数量误差,可以尝试两个方向:一是引入时序信息,无人机视频连续帧的数量应该平滑变化,用卡尔曼滤波做后处理能抑制单帧抖动;二是把计数头改成分布预测,输出数量的概率分布而不是单点值,这样能给出置信区间,对决策更有参考价值。这两个方向我都在小规模数据上试过,初步效果不错,等有完整实验再展开聊。