
视频计数这件事做视觉的人应该都懂那种痛图片里数东西已经够烦了换成视频帧与帧之间目标忽隐忽现、遮挡、形变、尺度跳变全来了传统检测器逐帧跑一遍再人工去重费时费力还容易漏。最近圈子里讨论度很高的一个方向就是让模型在视频里对任意物体做自动计数——给几个示例框或者一句文本描述模型自己把整段视频里该类目标的数量数出来。这篇就围绕这个主题把这类方案的整体设计、核心细节、实操流程和踩坑经验完整拆一遍适合做视觉算法、数据标注工具、内容分析平台的同行参考也适合刚接触视频理解、想找一个能落地的练手项目的同学。1. 整体设计与思路拆解1.1 为什么视频计数不能简单等于逐帧检测加去重很多人第一反应是拿一个成熟的目标检测器逐帧推理把每帧的框收集起来再按 IoU 做跟踪去重数量不就出来了我一开始也这么想实测下来问题一大堆。首先是身份一致性目标在视频里会进出画面、被遮挡、发生形变逐帧检测的框在时间轴上并不连续简单 IoU 匹配在快速运动和密集场景下会频繁断链导致同一个目标被重复计数。其次是示例泛化用户想数的往往不是人车这种通用类别而是穿红衣服的工人货架上第三排的瓶子这种细粒度目标通用检测器的类别体系根本覆盖不了。所以这类方案的核心思路一定是把计数当成一个以示例为条件的匹配问题而不是单纯的检测问题。给定参考帧上的若干示例点、框或者文本模型要在后续所有帧里找到和示例属于同一类的目标并维持跨帧的身份关联。这个思路的好处是不需要预定义类别表用户给什么示例就数什么泛化性直接拉满。1.2 三块拼图示例编码、跨帧关联、数量聚合把整个系统拆开看无论具体实现怎么变基本都逃不出这三块示例编码把用户给的参考信息框、点、文本编码成一个查询向量这个向量要能表达我要找的东西长什么样。框示例通常走 ROI 特征提取文本示例走 CLIP 类的图文对齐编码两者可以融合。跨帧关联在时间轴上把同一目标的检测结果串起来形成轨迹。这一步决定了最终计数会不会重复或漏计是整个系统里最容易出问题的地方。数量聚合把轨迹折算成数量。注意不是简单数轨迹条数还要处理目标中途进入画面目标离开后又回来这类边界情况。我见过不少实现把重心全放在检测精度上结果关联一塌糊涂最后数量偏差比检测误差还大。经验之谈关联模块的投入产出比往往高于检测模块尤其是在密集、遮挡多的场景里。1.3 方案选型的几个关键取舍具体到工程落地有几个岔路口需要提前想清楚取舍点选项 A选项 B我的建议示例形式仅支持框示例框点文本多模态优先框文本点示例作为补充关联方式纯 IoU 匹配外观特征运动预测外观为主、运动为辅IoU 只做兜底计数粒度只给总数总数逐帧轨迹必须给轨迹否则无法排查误差推理策略全帧逐帧推理关键帧插值短片段逐帧长视频抽帧插值这里重点说下推理策略。全帧逐帧推理精度最高但算力吃不消一段几分钟的视频动辄上万帧。我的做法是先按固定间隔抽帧比如每 5 帧取 1 帧跑检测和关联再对轨迹做线性插值补全中间帧。实测在目标运动不太剧烈的情况下抽帧间隔 5 以内数量误差基本可控间隔拉到 10 以上就开始明显漏计快速移动的小目标了。提示抽帧间隔不是拍脑袋定的要结合目标在画面里的平均位移来估。粗略算法是目标平均像素速度 × 间隔帧数 目标自身尺寸的一半超过这个阈值就容易断链。2. 核心细节解析与实操要点2.1 示例编码让模型看懂你要数什么示例编码的质量直接决定了后续匹配的上限。以框示例为例常见做法是把参考帧上框内的区域送进骨干网络取出 ROI 特征做池化得到一个定长向量。这里有个容易被忽略的细节单个示例的特征方差很大如果用户只给了一个框模型很容易把这个框里的背景、光照也当成目标特征学进去。我的处理方式是鼓励用户给 3 到 5 个示例然后对多个示例特征做聚类或者取均值同时计算类内方差方差过大的示例对直接提示用户重新框选。文本示例这块走图文对齐编码是主流。把红色工装的人这类描述编码成文本向量再和图像区域的视觉特征做相似度匹配。实操中要注意文本描述的粒度太笼统比如物体匹配不到东西太细比如左数第三个戴蓝帽子的又会过拟合到单个实例。我的经验是引导用户用属性类别的组合描述比如蓝色安全帽的工人属性负责区分类别负责兜底。代码层面示例编码的核心逻辑大致是这样伪代码具体接口按你用的框架调整def encode_examples(ref_frame, boxes, texts, backbone, text_encoder): # 视觉示例编码 roi_feats [] for box in boxes: crop crop_by_box(ref_frame, box) feat backbone(crop) # 骨干特征 roi_feats.append(roi_pool(feat, box)) visual_query aggregate(roi_feats) # 多示例聚合均值或聚类中心 # 文本示例编码 text_query text_encoder(texts) if texts else None # 融合视觉为主文本做门控加权 if text_query is not None: gate sigmoid(mlp(cat([visual_query, text_query]))) query gate * visual_query (1 - gate) * text_query else: query visual_query return query这段里aggregate和gate是两个关键点。聚合方式我试过均值和 K-means 取中心后者在示例差异大时更稳门控融合则是为了避免文本描述不准时把视觉信号带偏实测比直接相加稳不少。2.2 跨帧关联计数准确率的真正命门关联模块我一般拆成检测框生成 → 特征提取 → 代价矩阵 → 匹配四步。检测框生成用任意成熟检测头都行重点是后面的特征和匹配。外观特征要选对。纯用检测头的分类特征往往区分度不够密集场景里同类目标长得太像。我的做法是额外接一个轻量的重识别头输出 128 或 256 维的嵌入训练时用对比损失拉开不同实例的距离。这个头不用训得太重几十万样本量级就够用训过头反而会在新场景上过拟合。代价矩阵的构造是核心。我通常用加权组合cost w1 * (1 - cosine_sim) w2 * iou_distance w3 * motion_distance权重怎么定实测下来外观占大头w1 取 0.6 左右IoU 做兜底w2 取 0.25运动预测卡尔曼滤波的预测框和检测框距离占小头w3 取 0.15。这个配比在行人、车辆、货品几类场景里都比较稳但如果是外观高度相似的目标比如流水线上一样的零件就得把运动权重往上提。匹配算法用匈牙利算法做全局最优分配是标配。这里有个坑新目标出现和旧目标消失的处理。我的做法是维护一个暂定轨迹池新检测如果连续 3 帧都能匹配上才转正为正式轨迹正式轨迹连续 5 帧匹配不上才判定消失。这两个阈值3 和 5是经验值帧率高、目标运动慢的场景可以调小反之调大。注意轨迹转正和消失的阈值不要设得太激进。我早期把转正阈值设成 1 帧结果画面里的噪点、误检全被当成新目标计进去了数量虚高一大截。2.3 数量聚合别让边界情况毁掉结果轨迹有了数量怎么算最朴素的是数正式轨迹条数但这样会漏掉目标在视频开始前就在画面里和目标在视频结束后还在画面里的情况。我的处理是轨迹的起始帧如果是视频第一帧且该轨迹在起始帧就位于画面边缘之外进入正常计数轨迹如果从第一帧就完整出现在画面内说明它可能在录制前就存在正常计数但打上起始存在标记轨迹如果在最后一帧仍未消失打上结束存在标记正常计数。这些标记主要是给下游排查用的不影响总数但能帮你快速定位为什么这段视频数出来比人工多/少。另外短暂遮挡导致的轨迹断裂要单独处理。目标被柱子挡了两秒又出来关联模块很可能断成两条轨迹数量就多算了一个。我的补救办法是轨迹消失后保留一个休眠期比如 30 帧休眠期内如果有新轨迹的起始位置和消失位置在空间上接近、外观相似度高就把两条轨迹合并。这个合并逻辑能挽回不少因遮挡导致的重复计数。3. 实操过程与核心环节实现3.1 环境与依赖准备这套流程对环境的依赖不算重核心就是深度学习框架加几个视觉库。我用的组合是 PyTorch OpenCV 一个跟踪匹配的工具库具体版本按你机器上的 CUDA 情况来。下面是我实际跑通的一套依赖清单版本号仅作参考不必严格对齐# 基础环境 python3.10 torch2.1.0 torchvision0.16.0 opencv-python4.8.1 numpy1.24.3 scipy1.11.2 # 匈牙利算法 filterpy1.4.5 # 卡尔曼滤波装完之后先跑一个最小验证读一段测试视频确认帧读取、尺寸、帧率都正常再往下走。这一步看着废话但我见过太多人卡在视频解码上——某些编码格式 OpenCV 读出来帧数是错的或者颜色通道顺序不对后面所有特征都跟着错。import cv2 cap cv2.VideoCapture(test.mp4) fps cap.get(cv2.CAP_PROP_FPS) total int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(ffps{fps}, frames{total}, duration{total/fps:.1f}s)如果total和你用播放器看到的时长对不上八成是编码问题建议先用 ffmpeg 转成标准 H.264 再处理。3.2 参考帧选取与示例标注参考帧选得好后面省一半事。我的原则是选目标清晰、遮挡少、数量适中3 到 8 个的帧。如果整段视频目标都很多就选一个目标相对分散的帧。参考帧不一定是第一帧用户可以指定任意帧。示例标注这块如果做工具给非技术用户用交互设计很关键。我做的版本支持三种方式拖框、点选、输入文本。拖框最直观点选适合小目标文本适合快速描述。标注完立刻在参考帧上回显匹配结果让用户确认模型理解的是不是我要数的东西这个即时反馈能大幅降低后续返工。3.3 逐帧推理与轨迹维护主循环主循环的逻辑我整理成下面这个骨架实际项目里会拆成多个模块但核心流程就是这样tracks [] # 正式轨迹 tentative [] # 暂定轨迹 dormant [] # 休眠轨迹 query encode_examples(ref_frame, boxes, texts, backbone, text_encoder) for idx, frame in enumerate(video_frames): if idx % sample_interval ! 0: continue dets detector(frame) # 检测 feats reid_head(frame, dets) # 重识别特征 # 与示例查询做相似度过滤只保留同类目标 dets, feats filter_by_query(dets, feats, query, thresh0.5) # 卡尔曼预测 predicted [kalman_predict(t) for t in tracks tentative] cost build_cost_matrix(predicted, dets, feats, tracks tentative) matches, unmatched_dets, unmatched_tracks hungarian(cost, max_cost0.8) update_tracks(matches, unmatched_dets, unmatched_tracks, tracks, tentative, dormant, idx)几个参数值得单独说。sample_interval前面提过按目标速度估thresh0.5是示例相似度过滤阈值这个值太低会把背景误检放进来太高会漏掉外观变化大的目标我一般从 0.45 起调max_cost0.8是匈牙利匹配的代价上限超过这个值就不强行匹配宁可让检测走新轨迹分支。3.4 轨迹合并与最终计数输出主循环跑完进入后处理。先把休眠轨迹和正式轨迹做一次合并前面说的遮挡补救再统计数量。输出我建议至少包含三样东西总数、每条轨迹的起止帧和轨迹 ID、以及一个可视化视频把轨迹画在画面上不同 ID 不同颜色。可视化视频是排查神器数量对不上时看一眼视频就知道是漏检、重复计数还是误检。final_count len(tracks) len(merged_from_dormant) report { total: final_count, tracks: [{id: t.id, start: t.start, end: t.end, frames: len(t.hits)} for t in tracks] }如果要做批量处理建议把每条视频的 report 存成 JSON方便后续做误差统计和回归测试。4. 常见问题与排查技巧实录4.1 数量虚高重复计数从哪来这是最高频的问题。我整理了一张排查表按出现频率排序现象可能原因排查方法解决手段同一目标被数了多次轨迹断裂后重新起轨看可视化视频找同色系不同 ID 的轨迹调大休眠期、降低合并阈值背景被当成目标示例相似度阈值过低检查误检框的相似度分数分布阈值从 0.45 往上调目标进出画面被重复计边缘区域匹配不稳看轨迹起止是否都在画面边缘边缘区域降低匹配权重快速运动目标断链抽帧间隔过大对比抽帧和全帧的轨迹数缩小抽帧间隔或加运动补偿我踩过最深的一个坑是边缘区域。目标从画面左边出去、右边进来比如环绕拍摄关联模块会当成两个目标。后来我在代价矩阵里对画面边缘的检测框降权并且对消失位置在左边缘、新出现位置在右边缘的轨迹对做特殊合并才把这个误差压下去。4.2 数量偏低漏计怎么查漏计通常比重复计数更难发现因为结果看起来合理。排查思路是反着来先看检测召回再看示例过滤最后看轨迹转正阈值。检测召回不足小目标、低对比度目标容易漏。可以临时把检测置信度阈值调低跑一遍看数量变化如果明显上升说明是检测问题。示例过滤过严相似度阈值太高会把外观变化的目标滤掉。把阈值降到 0.3 跑一遍对比。轨迹转正阈值太高目标只出现几帧就消失比如飞鸟掠过转正阈值设成 3 帧就永远转不了正。这种场景要把阈值降到 2。提示排查漏计时建议固定一段 30 秒到 1 分钟的短视频人工数一遍作为基准然后每次只改一个参数跑对比。一次改多个参数你永远不知道是哪个起了作用。4.3 性能与精度的平衡全帧推理精度好但慢抽帧快但可能漏。我的折中方案是自适应抽帧先用低分辨率、大间隔快速跑一遍统计目标平均位移再根据位移动态调整间隔。位移大的片段用间隔 2 到 3位移小的用间隔 5 到 8。实测在保持精度基本不变的前提下整体耗时能降 40% 左右。另外重识别头的推理是性能大头如果目标数量不多可以对每条轨迹只保留最近 10 帧的特征做滑动平均而不是每帧都算全量特征这样能省不少算力。4.4 跨场景泛化换一批视频就崩怎么办这是示例式计数方案的通病。在 A 场景调的阈值换到 B 场景就失灵。我的经验是把阈值做成场景自适应的跑之前先用参考帧上的示例做一次自校准统计示例之间的相似度分布取分布的某个分位数比如 20% 分位作为过滤阈值的基准再上下浮动。这样换场景时不用手动调参模型自己会找到一个相对合理的阈值。还有一个技巧是多参考帧。如果视频里光照变化大单参考帧的示例特征会偏可以允许用户标 2 到 3 个不同时间点的参考帧每个参考帧生成一组查询向量匹配时取最大相似度。这个改动不大但跨场景稳定性提升明显。5. 影响范围与延伸思考5.1 这套方案能落到哪些场景示例式视频计数听起来偏学术但落地场景其实很实在。我接触过的需求里排前三的是仓储盘点数货架上的箱子、托盘、人流统计特定着装或特定区域的人员、生产质检数流水线上特定缺陷件。这三个场景的共同点是目标类别不固定、需要按需计数正好是示例式方案的主场。再往外延内容分析里数视频中出现的特定物品、农业里数果实或牲畜、交通里数特定车型都是同一套逻辑换个示例而已。关键是把示例标注的交互做顺让非技术用户也能用起来。5.2 还能怎么改进从工程角度看我觉得有两个方向值得投入。一是把关联和检测做成端到端现在两阶段拆开误差会累积端到端训练理论上能缓解但对数据量要求高。二是引入时序一致性约束让模型在训练时就学会同一目标在相邻帧的特征应该接近而不是靠后处理硬凑。从产品角度看计数结果的可解释性比精度更重要。用户看到一个数字如果不信你得能让他点开看是哪几个目标、在哪几帧出现的。所以可视化轨迹这条链路我建议从一开始就做进产品而不是当成调试工具。我个人在实际操作中的体会是这类项目最耗时间的从来不是模型本身而是数据准备和误差排查。模型选型、参数调整这些跑几轮就有感觉了真正磨人的是拿到一段新视频数量对不上你得一层层往下剥——是检测漏了、关联断了、还是聚合算错了。所以我的建议是从第一天起就把日志和可视化做扎实每条轨迹的每一次匹配都留痕后面排查能省下大量时间。另外别迷信论文里的指标那些都是在标准数据集上调好的换到你的真实场景老老实实拿几十段视频人工标一遍做基准比什么都靠谱。