简介:Visdrone2021-CC人群计数数据集是VisDrone系列2021年版本中的专项资源,面向无人机视觉监控及计算机视觉研究人员,旨在解决高密度、复杂背景下的人群数量精确估计问题。压缩包共包含10854个文件,总大小约820MB,其中有7236张JPEG图像(涵盖RGB与热成像)、3616个XML标注文件及2个Python辅助脚本,图像覆盖不同光照、天气和人群密度场景,XML标注可直接用于训练检测或密度回归模型。资源附带的数据处理脚本支持图像增强与评估指标计算,可帮助研究者快速构建从预处理到模型验证的实验流程,结合热成像数据还能提升低光照或遮挡场景下的鲁棒性。目前已有1536人学习下载,适合作为无人机视觉人群计数算法研究、课题实验以及智能监控系统开发的参考数据。
1. VisDrone2021_CrowdCounting.rar:解压后先别急着跑,这个任务的本质是密度回归
解压 VisDrone2021_CrowdCounting.rar 之后,大部分人第一反应是找训练好的权重跑一遍测试集,然后卡在数据加载上。原因是 VisDrone2021 这套数据集的标注格式是目标检测的边界框,而 CrowdCounting 任务真正需要的是密度图:画面里每个可见人头对应一个高斯峰。无人机俯视视角下,密集人群的头部在图上互相重叠,检测框严重时会大面积漏检,算检测框个数或者直接统计检测目标都是不可靠的路线。这个压缩包的方向,是在 VisDrone2021 的俯视视频序列上训练一个端到端的密度回归模型,把每帧图像转成一张密度图,再对密度图求和得到人数。它适合两类人:一是做过检测但没接触过密度回归的工程师,二是刚拿到数据却找不到标注转换入口的开发者。真正决定项目成败的不是模型结构,而是把检测框标注转换成人头点、再变成可训练密度图的那一步,这篇文章就把这条链路完整拆开。
2. 人群计数为什么选密度图回归:从 VisDrone 的标注里挑出计数信息
2.1 密度图回归 vs 检测框计数:俯视密集场景的第一性原理
地面监控的人群计数,检测框方案还能勉强工作,因为人头之间有足够间距。但无人机挂高之后,视角变成俯视,人的头顶在成像面上是一个个小圆斑,密集时互相吞没。检测模型里的 anchor 匹配、NMS 合并、小目标阈值,每一步都在丢失人头。你在 VisDrone 上做检测会发现预测框数量比实际人头数少很多,这不是模型能力问题,是表征方式错了。
密度图回归换了一个思路:不判断“这里有一个人”,而是对每个像素输出一个人头密度值。训练时把每个人头中心点变成一个高斯核,真实地面图就是把所有人头的高斯核叠加起来;预测时对整张密度图求和,得到的就是人数。它的好处从根上避开了遮挡问题,两个人头在图像上重叠,高斯峰也重叠,但积分总量还是两个人,模型不需要分辨谁是谁。
数学上,密度图可以写成:
D(p) = Σ_i G_σ(p - p_i)
其中 p_i 是第 i 个人头中心点坐标,G_σ 是标准差为 σ 的高斯核。给定一张密度图,人数就是:
C = Σ_p D(p)
这套公式是后面所有代码的基础,VisDrone 的标注里没有 p_i,需要先从 bbox 里提取出来。
2.2 解析 VisDrone 标注文件:把 bbox 中心转成人头点标注
VisDrone 的标注是 txt 文件,每一行对应一个目标,字段用逗号分隔。包含帧号、目标 ID、bbox 的左上角坐标和宽高、score、类别、截断程度、遮挡程度、ignore 标志。做人群计数,最关心的是 frame、bbox 中心、类别和 ignore。
常见的 VisDrone 标注行:
1,1,639,402,80,120,1,1,0,0,0第一个字段是帧号,第二是目标 ID,第三到第六是 bbox 的 left、top、width、height,第八个是类别。类别里 pedestrian 是 1,people 是 2,两者都要纳入计数。people 类在标注里表示“一群人”,框往往很大,直接取框中心当人头点会出错,这在后面会专门处理。
第一步先写一个解析脚本,把 bbox 中心转成“帧号 + 像素坐标”的点列表:
""" visdrone_to_points.py 把 VisDrone 的 bbox 标注转成人头点列表。 只保留 pedestrian(1) 和 people(2) 两类; ignore=1 的框直接丢弃,避免把低置信度目标当训练点。 输出格式: frame x y """ import os import glob from collections import defaultdict COUNTING_CLASSES = {1, 2} # pedestrian、people def convert_annotation(txt_path): points_by_frame = defaultdict(list) with open(txt_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split(",") if len(parts) < 11: continue frame = int(parts[0]) obj_id = int(parts[7]) x1 = float(parts[2]) y1 = float(parts[3]) w = float(parts[4]) h = float(parts[5]) ignore = int(parts[10]) if obj_id not in COUNTING_CLASSES: continue if ignore == 1: continue if h < 5: continue # people 类是大框,只有小框可以当作单个人头 if obj_id == 2 and w > 48: continue cx = x1 + w / 2.0 cy = y1 + h / 2.0 points_by_frame[frame].append((cx, cy)) return points_by_frame def main(data_root, out_dir): os.makedirs(out_dir, exist_ok=True) for txt in sorted(glob.glob(os.path.join(data_root, "**", "*.txt"), recursive=True)): seq = os.path.basename(os.path.dirname(txt)) points = convert_annotation(txt) out_path = os.path.join(out_dir, f"{seq}.txt") with open(out_path, "w", encoding="utf-8") as f: for frame in sorted(points.keys()): for x, y in points[frame]: f.write(f"{frame} {x:.2f} {y:.2f}\n") print(f"{seq}: {sum(len(v) for v in points.values())} points") if __name__ == "__main__": main("./VisDrone2021/train/", "./points/train/")这里几个过滤条件的逻辑要说明一下。h < 5 的框去掉,是因为一个人在图像里只有五六个像素高时,高斯核再小也会把相邻目标糊成一片,这类点对密度回归没有正面作用。ignore == 1 的框是标注方标记的低置信度或严重遮挡目标,不该作为训练监督信号。people 大框的过滤是要单独解释的:people 类本意是“给一群人一个大范围框”,一个框里可能有三五个人头,取中心当一个人头会让真实人数偏小;宽度超过 48 像素的 people 框直接丢弃,小于这个值的通常是单人靠近镜头的特殊情况,框中心基本等于人头中心。这个阈值会根据你的图像分辨率变化,用原图跑的话 48 可以作为起点。
解析脚本输出的点列表是接下来的数据基础,不要再回到原始标注去取框了,所有密度图都从点列表生成。
3. 把 VisDrone 的框标注变成密度图:两套生成脚本与参数选择
3.1 固定高斯核与自适应高斯核的选择
拿到人头点列表后,下一步是生成密度图。最简单的做法是给每个点固定一个 σ,比如 σ = 15,然后叠加高斯核。这个方案在人群稀疏、人头大小均匀的地面监控视频里够用,但放到 VisDrone 上很快会出问题。无人机画面里,近处的人头在图上占四五十像素,远处的人头只有六七个像素,固定 σ 会把远处的小头糊成一个平缓的大包,多个小头的高斯核叠在一起直接变成一个没有区分度的区域,密度图的总量尽管还在,空间分布已经完全错位,训练出来的模型只会“估个数”,不会“找人”。
解决方法是自适应高斯核,核心思想是人头越密、核越小:
σ_i = β · mean(d_1, d_2, d_3)
其中 d_1 到 d_3 是第 i 个人头到最近三个邻居的欧氏距离,β 是缩放系数。这个公式最早是人群密度估计的常见做法,用在 VisDrone 上的关键是要把 β 和上下限设对。
""" compute_adaptive_sigma.py 对每个点计算自适应高斯核的 sigma。 """ import numpy as np def adaptive_sigma(points, beta=0.3, k=3, min_sigma=2.0, max_sigma=20.0): pts = np.array(points, dtype=np.float32) n = len(pts) sigmas = np.ones(n, dtype=np.float32) * min_sigma for i in range(n): diff = pts - pts[i] # (n, 2) 所有点到当前点的偏移 dist = np.sqrt((diff ** 2).sum(axis=1)) dist[i] = np.inf # 自己不算邻居 nearest = np.sort(dist)[:k] sigma = beta * nearest.mean() sigmas[i] = np.clip(sigma, min_sigma, max_sigma) return sigmas参数选择有实际依据。β 取 0.3 是大多数密集人群场景的默认值,因为近距离邻居的平均距离大概能反映人头之间的实际间隔,取三成作为高斯核标准差,出来的核既不会糊掉邻居,也不会变成孤立的尖点。当一帧超过 80 人、人头极度密集时,β 调到 0.2 会更稳;画面空旷、每帧只有二三十人时 0.5 反而更好。min_sigma 设为 2 是因为小于 2 像素的高斯核退化成单个像素点,模型几乎学不到有效梯度。max_sigma 设为 20 是因为无人机俯视场景里人头投影尺寸有限,σ 超过 20 后核的覆盖范围太大,会把稀疏场景里几个相邻的人头融成一个峰,密度图求和虽然还是那些人,但空间监督信号已经失真。
3.2 密度图生成脚本与归一化细节
有了每个点的 σ,就可以把点列表转成和原图一样大小的密度图。这个脚本建议在数据预处理阶段跑一遍,把所有帧的密度图存成 npy 文件,训练时直接读,省去 DataLoader 里重复计算的开销。磁盘占用会大一些,但能避免训练中断时重新算密度图的等待。
""" generate_density_map.py 按帧生成自适应高斯核密度图。 密度图和原图分辨率一致,保存为 npy。 """ import numpy as np import cv2 def render_density(image_h, image_w, points, sigmas): density = np.zeros((image_h, image_w), dtype=np.float32) if not points: return density pts = np.array(points, dtype=np.float32) for (x, y), sigma in zip(pts, sigmas): # 核尺寸取 6 倍标准差,并保证是奇数 size = int(sigma * 6) if size % 2 == 0: size += 1 kernel = cv2.getGaussianKernel(size, sigma) kernel2d = kernel @ kernel.T kernel2d /= kernel2d.sum() # 让核的积分等于 1 x0 = int(round(x - size // 2)) y0 = int(round(y - size // 2)) x1 = x0 + size y1 = y0 + size # 裁剪超出图像边界的部分 left = max(0, -x0) top = max(0, -y0) right = max(0, x1 - image_w) bottom = max(0, y1 - image_h) cut = kernel2d[top:size - bottom, left:size - right] x0 = max(0, x0) y0 = max(0, y0) x1 = min(image_w, x1) y1 = min(image_h, y1) density[y0:y1, x0:x1] += cut return density这里有一个细节值得单独说:高斯核除以自身总和之后,单个核的积分是 1,多个核叠加后密度图的积分正好等于这一帧的总人数。这个性质是密度图方法正确性的基石,训练时损失函数拿预测密度图的积分去和真实人数比对才有意义。如果你把核归一化这一步漏了,每个核的实际面积是网格点数而不是 1,密度图积分会比真实人数大好几倍,MSE 一上来就下不去。
图像进模型之前通常要缩放到统一尺寸,密度图也必须同步缩放。最简单的处理是等比缩放短边到 960、长边不超过 1920,缩放时记住 scale 和原尺寸,对密度图用 cv2.resize 的 INTER_AREA 插值,同时把点坐标也乘上 scale。等比缩放后的密度图积分理论上不变,但插值会引入微小误差,严格的做法是缩放后重新对密度图做归一化:每张密度图除以自身总和、再乘以原图人数。这一步没那么玄,但能避免损失值在训练过程中出现意料之外的小幅波动。
4. 训练配置与损失函数:从零跑通一个无人机人群计数模型
4.1 骨干网络与关键训练参数:照抄能起跑的最小配置
密度图模型的主流选择还是 CSRNet 这条路线:用 VGG16 的前半部分做骨干,后面接空洞卷积扩大感受野,最后上采样回原图尺寸。原因很简单,VGG16 是经典特征提取网络,预训练权重容易拿到,人群计数的重点不在骨干的创新,而在密度图监督。无人机俯视画面里人头的纹理和自然图像的纹理共享边缘、颜色、梯度这些低级特征,直接复用 ImageNet 预训练权重,比从零开始训练少花至少一半时间。
""" crowd_counter.py CSRNet 简化版:VGG16 前段骨干 + 空洞卷积分支 + 双线性上采样。 """ import torch import torch.nn as nn from torchvision.models import vgg16 class CrowdCounter(nn.Module): def __init__(self): super().__init__() features = list(vgg16(pretrained=True).features.children()) self.backbone = nn.Sequential(*features[:23]) # 输出 512 通道,1/8 分辨率 self.head = nn.Sequential( nn.Conv2d(512, 256, 3, padding=2, dilation=2), nn.ReLU(inplace=True), nn.Conv2d(256, 128, 3, padding=2, dilation=2), nn.ReLU(inplace=True), nn.Conv2d(128, 64, 3, padding=1), nn.ReLU(inplace=True), nn.Conv2d(64, 1, 1), ) self.upsample = nn.Upsample(scale_factor=8, mode="bilinear", align_corners=False) def forward(self, x): x = self.backbone(x) x = self.head(x) x = self.upsample(x) return xfeatures[:23] 取的是 VGG16 前四个卷积块,输出 512 通道的特征图,分辨率降为输入的 1/8。后面的空洞卷积 dilation=2 在保持 1/8 分辨率的同时扩大感受野,为的是让每个像素能看到更大范围的人群分布,这对无人机大场景尤其重要,人群往往是成片出现的。最后用双线性上采样把密度图拉回原图尺寸,是工程上的简化做法,效果足够;要更高精度就把上采样换成两层转置卷积,但参数量和训练时间都会上涨。
训练参数直接给一份能跑通的默认配置。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 输入短边 | 960 | 等比缩放,长边不超过 1920 |
| batch size | 4 | 3090 级别显卡可以开到 8 |
| 优化器 | AdamW | weight decay 设为 5e-4 |
| backbone 学习率 | 1e-5 | 预训练权重,只做微调 |
| head 学习率 | 1e-4 | 新初始化的层,学得快一些 |
| 总 epoch | 120 | 第 60 轮之后 lr 乘 0.1 |
| 损失函数 | MSE + L1 | 比例 1 : 0.5 |
| 随机裁剪 | 512x768 | 从缩放后的图像里裁剪,等比对应密度图 |
学习率分开设置是这里最值得注意的一点。backbone 已经包含成熟特征,lr 太大会把预训练权重冲掉,尤其是无人机视角图像和 ImageNet 自然图有分布差异,冲过头就看不到任何特征了。head 是新初始化的层,lr 大一个量级能加快收敛。我自己习惯把 backbone 的 dropout 也关闭,密度图任务不需要它对特征做随机置零。
4.2 损失函数:只盯 MSE 会让密度图变得模糊
经典的 CSRNet 只用 MSE,回归密度图的每个像素。MSE 的优点是收敛快,对极端误差敏感,但它有个副作用:模型发现把密度图整个压平、把所有峰值分给一片区域,MSE 也很小,因为积分总量对了。结果是密度图一片模糊,人数估算还行,真拿去做人群空间分布分析就废了。
实际落地时我更推荐 MSE + L1 的组合:
""" mixed_loss.py MSE 管总量,L1 管逐像素稀疏度,两者权重可调。 """ import torch.nn.functional as F def mixed_loss(pred, gt, w_l1=0.5): gt = gt.float() pred = pred.float() # 确保两个张量尺寸一致后再算损失 if pred.shape != gt.shape: pred = F.interpolate(pred, size=gt.shape[-2:], mode="bilinear", align_corners=False) mse = F.mse_loss(pred, gt) l1 = F.l1_loss(pred, gt) return mse + w_l1 * l1MSE 对大误差敏感,L1 对每个像素的偏差都给出恒定梯度,不会像 MSE 那样对小数像素误差几乎无感。w_l1 取 0.5 起步,如果你的密度图出现明显模糊,把 w_l1 提到 1.0;如果出现密集区域过冲、稀疏区域被抑制,就降回 0.2。这个权重不需要调太多轮,对最终 MAE 的影响远小于数据过滤规则。
如果你还想让密度峰位置更准,可以在这个基础上叠加 SSIM 结构损失,但要注意密度图的数值范围不固定,叠加 SSIM 前需要先对 pred 和 gt 做 min-max 归一化,否则大值区域会主导整个梯度。第一版不建议加,把 MSE+L1 跑通再考虑。
4.3 从预训练权重起步:为什么不建议随机初始化
从头训练一个密度图模型在 VisDrone 上几乎不可能收敛。一方面数据量不够,无人机视角下的人群变化比地面视频更复杂,头部朝向、遮挡模式、尺度跨度都更大;另一方面密度图监督本身没有目标检测那种强定位信号,全靠大量数据堆出来。用 VGG16 预训练权重做初始化,相当于模型已经知道怎么提取边缘、纹理、形状,只需要学会把人头特征映射到密度。
如果 PyTorch 版本新到 pretrained 参数报错,就换成 weights="IMAGENET1K_V1"。加载过程要注意不要把你的 head 层参数算进去,只加载 backbone 的前缀。一个常见的坑是加载权重时报 size mismatch,原因是 head 里有几个 3x3 卷积的参数和 VGG 分类头同名,加载时对不上。用 strict=False 再手动检查 backbone 是否真的加载成功,别让加载错误被静默吞掉。
5. VisDrone2021_CrowdCounting 排查手册:5 个必须绕开的坑
5.1 ignore 区域被当成负样本
现象:模型预测的密度图在车辆、树荫、建筑边缘出现大片假峰,而且这些假峰不在任何标注人头附近。查训练数据发现这些区域像素很暗、纹理规则,模型把它们当成了“没有人”的区域来学习。
原因:VisDrone 的 ignore=1 表示该区域目标过小或不可辨认,不代表这里没人。数据里还有大量完全没有任何标注的背景区域,模型天然会把它们当负样本。ignore 区域既有可能是人、又没有任何正样本标注,模型从中学到的规则是“这个地方没有头”,这个规则本身是错的。
解决:解析标注时利用 ignore 框生成一个 valid mask,和密度图同尺寸,ignore 区域置 0、其他区域置 1,损失计算时只对 valid mask 内的像素算,等于把这块监督信号挖掉。如果你偷懒不做,至少训练几个 epoch 后把 ignore 区域的预测值单独打印出来,如果均值明显大于 0.01,就得回去补第一步。
5.2 固定 sigma 让远处小头直接蒸发
现象:训练损失下降正常,可视化密度图发现远处的人头几乎看不到,只有近处几个人头有响应。整体人数误差不小,而且集中在远景区域。
原因:固定 sigma 时,远处的 6 像素小头被高斯核摊到几十个像素范围内,峰值只有 0.01 量级,和背景噪声差不多。MSE 损失在这些点上贡献趋近于零,模型学到的是“远景没人”。
解决:改用自适应 sigma,并确认 min_sigma 设到 2.0 附近。生成密度图后抽查远处小目标:取一个人头点位置,看密度图对应坐标的峰值是否和相邻背景有至少 5 倍差距。如果没有,说明 sigma 还是偏大或者该点被过滤掉了。这个抽查脚本值得保留,后续每次换数据都要跑一遍。
5.3 people 类被按一个点算,人数低估
现象:测试集上预测人数系统性低于真实人数,误差在人群密集的视频序列里尤其明显,而稀疏序列误差很小。
原因:标注里的 people 类是给“一群人”打的框,一个框覆盖几个人甚至十几个人。只用 bbox 中心当作一个点,模型会认为这个位置只有一个人。无人机俯视角度下,这类框数量不少,累计起来误差就大了。
解决:处理标注时分三档。宽度小于 40 像素的 people 框保留,当作远处单个行人;宽度在 40 到 80 之间,按框宽高比例估算 2 到 3 个点,均匀放在框内;超过 80 像素的框直接丢弃,因为它通常是密集人群的大范围框,无法可靠还原人头位置。如果你做严格的人头计数,最稳妥的是只保留 pedestrian 类,但要注意这样会丢掉一部分真实人头,需要自行权衡。
5.4 按帧切分训练/验证导致数据泄漏
现象:验证集 MAE 很低,模型表现非常好,但拿到另外一批无人机视频上测试直接崩掉,误差翻倍。
原因:VisDrone 以视频序列组织数据,同一个序列里相邻帧的背景几乎一样,人和车的位置变化也很小。如果你随机按帧切分,同一时刻附近的帧会同时出现在训练和验证集里,模型等于背下了这片背景。这不是泛化能力,是数据泄漏。
解决:切分必须按序列进行,一个视频序列的所有帧要么全在训练集,要么全在验证集。VisDrone 官方给的训练/验证划分本身就是按序列做的,直接沿用官方划分,不要自己再按帧重新切分。这个原则同样适用于测试集:测试序列的无人机飞行轨迹、拍摄地点如果和训练集接近,分数只能说明模型在这个数据分布下的表现,不代表它能应付完全陌生的场景。
5.5 图像 resize 之后密度图与标签错位
现象:训练时 loss 正常下降,可视化预测叠加到原图上,发现密度峰整体偏移了十几个像素。越靠近图像边缘偏移越明显。
原因:图像缩放用了 cv2.resize,密度图 resize 时用了最近邻,或者缩放后直接改图像尺寸、密度图还保持原图大小,导致监督信号和输入图像空间不对齐。前期不明显,随着损失收敛到很小的值,未对齐像素的误差就暴露出来了。
解决:图像和密度图必须使用同一套坐标变换参数。等比缩放时先算出 scale,图像用 INTER_LINEAR,密度图用 INTER_AREA,缩放后核对两者的 shape 是否完全一致。裁剪同样要小心:random crop 的坐标直接应用到密度图,不能单独对密度图再做一次 resize。把这个封装进 Dataset 类里,杜绝在训练循环里手写缩放逻辑。
6. 模型训完怎么验证:在 VisDrone 测试集上算 MAE/MSE,再迁移到地面视角
6.1 评估脚本:密度图求和后和真值比对
人群计数公认的两个指标是 MAE 和 MSE。MAE 表达平均绝对误差,MSE 对大误差更敏感,一个模型两项都不错才说明它稳定。评估时只需要对每帧求预测密度图的积分和真值密度图的积分,再比对即可。
""" evaluate.py 在测试序列上逐帧计算 MAE / MSE。 """ import numpy as np import glob def evaluate(pred_dir, gt_dir): mae_list = [] mse_list = [] pred_files = sorted(glob.glob(pred_dir + "/*.npy")) for p_path in pred_files: name = p_path.split("/")[-1].replace("_pred.npy", "") g_path = f"{gt_dir}/{name}_gt.npy" pred_map = np.load(p_path).astype(np.float32) gt_map = np.load(g_path).astype(np.float32) pred_count = pred_map.sum() gt_count = gt_map.sum() err = pred_count - gt_count mae_list.append(abs(err)) mse_list.append(err ** 2) mae = float(np.mean(mae_list)) rmse = float(np.sqrt(np.mean(mse_list))) print(f"MAE: {mae:.3f} RMSE: {rmse:.3f}") return mae, rmse这套评估逻辑要在训练过程中就挂上,每 5 个 epoch 跑一次验证集,记录 MAE。如果 MAE 在第 30 轮之后开始回升或者抖动剧烈,优先怀疑学习率太高或者数据里混入了异常帧,而不是模型结构的问题。
6.2 自检:把密度图可视化成人头位置
只看 MAE 数字远远不够,密度图模型可能总人数估得准,但人头位置全错。数值指标需要配合可视化一起判断。用 maximum_filter 找密度图的局部极大值,这些峰值就是模型认为的人头位置。
""" visualize_peaks.py 从预测密度图中提取局部峰值,叠加到原图上检查。 """ from scipy.ndimage import maximum_filter def extract_peaks(density, min_thresh=0.15): local_max = maximum_filter(density, size=11) == density thresh = min_thresh * density.max() ys, xs = np.where(local_max & (density > thresh)) return xs, yssize=11 对应高斯核约 9 到 11 像素的峰值间距,小于这个间隔的相邻峰会被合并。min_thresh 取 0.15 是经验值,如果密度图整体都低,就把阈值降到 0.05。把提取出来的峰值画到原图上,对比标注点,如果预测峰出现在人群边界而非人群中心,说明自适应 sigma 的 min_sigma 偏大,回头调参。
6.3 从无人机到地面监控的迁移边界
VisDrone 上训好的密度模型直接拿到地面监控视角,效果会下降,但不至于完全失效。地面画面的人头尺度和无人机中远景的人头尺度接近,特征上可以迁移,视角从俯视变成斜视之后,人头的遮挡方式和背景复杂度都变了,直接推理的误差会明显上升。我的习惯是先用原模型做零样本推理,统计误差分布,挑出误差最大的几条视频做微调,通常几百帧数据、微调 20 个 epoch 就能把 MAE 拉回可用水平。密度图模型属于场景敏感型模型,迁移能力弱于检测模型,这是密度回归方法的固有代价,不要指望一个在 VisDrone 上训完的模型能通吃所有视角。
最后分享一个我自己的检查习惯:任何一次新数据、新预处理流程跑通之后,我先随机抽三帧做可视化,一帧密集一帧稀疏一帧中等密度,确认密度峰的位置和人头重合,再开始调损失函数和超参数。如果这三帧都正常,后面出现玄学问题,大概率出在数据加载顺序或者在线增强上,而不是模型本身。这个习惯帮我排掉过高斯核归一化、resize 错位、ignore 过滤失效这几个最常见的坑,希望帮到你。
本文还有配套的精品资源,点击获取