有时候用户问的问题越简单动手做的时候坑越多。前阵子我在迭代一版检测方案模型用到的就是队里一直沿用的YOLO26内部版本功能上已经能稳定输出边界框和类别了。结果下游同学过来问了一句你能不能直接告诉我这批测试视频里一共有多少辆车、多少人、多少只鸟我下意识觉得这不就是把框的数量加一下吗等真去实现“YOLO26自动计数自动统计各个类别数量”的时候才发现检测和计数之间隔着一个状态管理问题远不是一行sum能解决的。这篇把整个方案的思考过程、模块设计、核心代码、实验指标和踩坑记录完整写出来。如果你正在做检测类的项目或者想给论文/报告里的“工作量”加一个有说服力的扩展模块可以直接按这套思路复现。它不需要改模型结构也不需要重新训练属于把检测结果变成业务统计结果的“最后一公里”。1. 检测和计数之间隔着一个“状态管理”很多刚接触检测任务的读者会以为“模型能输出框那数量自然就出来了”。这个直觉对单张静态图成立放到视频或长序列里就立刻破功。1.1 从一张图到一段视频YOLO26原生的输出缺了什么YOLO26单帧推理拿到的一般是[N, 6]这样的数组每一行对应一个检测框包括框的坐标、置信度、类别编号。它解决的是“这张图里有没有目标、目标在哪儿、目标属于哪一类”这个问题。它不解决“这段视频里一共有多少个独立目标”这个问题。差别在哪模型天生没有跨帧状态。视频第10帧出现一辆车第11帧同一辆车还停在那里模型会再次输出一个检测框。如果不做任何处理按帧累加就会把同一辆车数两遍。更别说一辆车在画面里停留30秒按25帧每秒算它可能被数了750次。这就是典型的“框有价值直接加框没价值”。所以要做自动计数必须把“检测结果”和“统计口径”分开看。你的业务到底要的是“某一帧画面里同时出现的最大数量”“一个视频里出现过多少独立目标”还是“固定区域里进出了多少人”统计逻辑完全不同。1.2 先把“计数”拆成三类逐帧统计、去重总数、区域断面计数我建议在写代码之前先确认需求否则后面返工的概率非常大。结合实际场景最常见的计数需求其实是下面三种计数类型统计口径典型场景最关键依赖逐帧统计每一帧图片中各类别目标的数量考场人数统计、货架商品瞬时数量只需要检测结果去重总数整段视频中互不重复的目标总数量停车场车辆统计、养殖场动物盘点需要跟踪ID或特征匹配区域断面计数越过某条线/进入某块区域的目标数量出入口客流统计、高速卡口车流统计需要跟踪ID 空间判断逐帧统计最简单拿检测结果按类别做聚合就行。去重总数就复杂了你需要知道“第10帧的这辆车”和“第15帧的这辆车”是不是同一辆。区域断面计数还要在这个基础上加上轨迹方向判断。我在这个项目里的需求比较复合既要统计视频中各类别出现的去重总数也要输出每一帧的分布情况。所以我最终做的是一个可插拔的计数模块既不碰YOLO26的训练流程也不改它的权重所有逻辑都挂在推理结果的后面。2. 自动计数模块的总体设计与选型这个模块我起名叫 AutoCounter设计原则很简单把计数从模型里剥离出来做成一个独立组件。好处是模型升级了计数逻辑不用重写业务口径变了模型也不需要重新训练。2.1 不修改模型结构也能挂接计数功能有些同学一看到“改进YOLO”就以为必须往网络里加分支、加注意力、加损失函数。其实大部分计数需求完全不需要动模型。检测模型负责感知计数模块负责记忆和汇总两者解耦之后反而更容易排查问题。我们当时考虑过三种做法第一种是在模型输出层加一个回归头直接预测目标数量。这个方案对单一类别且画面目标数量固定的场景或许有效但可解释性差也不方便扩展到多类别而且需要重新训练。第二种是训练一个单独的计数网络比如密度图估计模型。这类模型适合人群密度估计但照样需要自己准备数据、训练、调参周期很长。第三种就是本文说的后处理计数方案。检测器直接复用现成的YOLO26能力后面挂计数器通过跟踪ID完成去重。这种方案实现最快也最容易写清楚逻辑非常适合作为工程落地或论文中的“附加模块”。从结果看第三种确实是最划算的。它把检测精度和计数精度解耦检测不准锅归检测计数逻辑有Bug单独修计数逻辑就行。实验时还能分别报mAP和计数指标论文的工作量也丰富不少。2.2 类别字典、跟踪ID和ROI规则三者如何协作自动统计各个类别数量前提是有一个“类别字典”。这个字典直接来自训练用的类别配置文件不要在代码里硬编码[cat, dog]这种列表。因为数据集一旦增加类别硬编码列表就会漏统计而且排查起来非常隐蔽。跟踪ID是整个模块最核心的字段。它解决的是“同一目标出现多次如何只算一次”的问题。如果推理时没有跟踪ID也可以先用交并比匹配相邻两帧的框自己维护一套简易ID。不过自己写匹配逻辑容易在目标密集或遮挡时把ID弄丢更省心的方式是直接开启检测器配套的跟踪链路。ROI规则属于可选项。如果业务不需要按区域计数完全可以不接。它起的作用是空间过滤只统计画面中某个多边形区域内部的目标。比如只统计货架区域内的人忽略走廊上走动的人。这三样东西协作起来后整个计数流程才真正闭环先过滤出ROI内的检测框再按类别聚合最后用跟踪ID去重。3. 核心代码把检测结果变成可复用的统计流下面这段代码是我实际项目里简化后的版本把复杂的工程细节去掉保留核心逻辑。它分为三部分检测结果包装、计数器主体、调用示例。3.1 推理结果先转成统一的检测对象YOLO26推理返回的原始数组直接用起来不方便我习惯先包一层TrackedDet对象把类别、坐标、置信度、跟踪ID都放在一个结构里后续所有模块都用这个结构传数据。class TrackedDet: __slots__ (cls, score, bbox, track_id) def __init__(self, cls, score, bbox, track_idNone): self.cls cls self.score score self.bbox bbox # [x1, y1, x2, y2] self.track_id track_id property def center(self): return ( (self.bbox[0] self.bbox[2]) / 2, (self.bbox[1] self.bbox[3]) / 2 )包装这一步的价值在于后面无论换什么检测器只需要把新模型的输出适配成TrackedDet计数器代码完全不用改。以后YOLO26再升级或者换成另一套检测框架迁移成本都极低。3.2 按类别累计与去重逻辑计数器主体维护两个状态total是当前累计的各类别总数_seen是已经计过数的跟踪ID集合。每次新进来一批检测框如果带跟踪ID就先去重如果不带跟踪ID就按逐帧累加处理。class AutoCounter: def __init__(self, class_names): self.class_names class_names self.total {name: 0 for name in class_names} self._seen set() self.frame_idx 0 self.frame_stats [] def reset(self): self.total {name: 0 for name in self.class_names} self._seen.clear() self.frame_idx 0 self.frame_stats [] def update(self, dets, dedupTrue): self.frame_idx 1 per_frame {name: 0 for name in self.class_names} for det in dets: if det.cls not in self.total: continue per_frame[det.cls] 1 if not dedup: self.total[det.cls] 1 continue if det.track_id is None: self.total[det.cls] 1 continue if det.track_id in self._seen: continue self._seen.add(det.track_id) self.total[det.cls] 1 self.frame_stats.append({ frame: self.frame_idx, per_frame: per_frame }) return self.total.copy()这里有个细节值得展开当dedupTrue时ID去重后的总数只会在目标第一次出现时加一次之后目标被持续检测到total不再增加。但per_frame反映的是当前帧的实际可见数量两个口径用途不同。比如统计停车场总车辆数要用去重总数监控画面里同时排队的人数要用逐帧统计千万别混用。3.3 实时调用和周期输出实际跑视频时只需要循环读帧、推理、更新计数器即可。下面这段是调用端的示例counter AutoCounter([cat, dog, bird, car]) for frame in video_stream: raw_results yolo26_infer(frame) dets [TrackedDet( clsraw[class_name], scoreraw[score], bboxraw[bbox], track_idraw.get(track_id) ) for raw in raw_results] counter.update(dets, dedupTrue) if counter.frame_idx % 100 0: print(f第 {counter.frame_idx} 帧: {counter.total})每一百帧打印一次累计结果方便人工核对。最后一帧处理完后counter.total就是整段视频的去重后类别数量。如果你还要统计ROI区域只需要在counter.update之前过滤一次def is_inside(bbox, roi): cx (bbox[0] bbox[2]) / 2 cy (bbox[1] bbox[3]) / 2 return roi.contains(cx, cy) def filter_inside(dets, roi): return [det for det in dets if is_inside(det.bbox, roi)]中心点落在ROI多边形内就保留否则丢掉。要注意的是这个过滤逻辑只能做区域内部目标统计不能据此判断目标是否“进入区域”。想统计进出流量还得结合跟踪ID和相邻帧中心点的位移方向。4. 用实验设计和图表把“工作量”做实很多做检测改进的同学会把精力全放在mAP上做完发现论文里只有一张结果表显得很单薄。挂上计数模块之后实验维度一下子变多了而且都是真实可复现的指标不是凭空造数据。4.1 计数类任务建议增加的指标检测模型能力之外计数模块本身可以单独出指标。比较常用的是三个指标计算公式含义MAEmean(预测计数 - 真实计数MAPEmean(预测计数 - 真实计数计数准确率1 - MAE / 真实计数计数任务中最直观的准确率计算方式不复杂但需要说明的是“真实计数”怎么定义。如果统计的是去重总数那人工标注也要按同一个口径来。否则会出现模型数了8辆车、人工数了3次进出同一辆车然后标了3辆车的尴尬情况指标无意义。我当时的做法是单独标了一组测试序列每个序列给一个“期望正确总数”标签再跑计数模块统计 MAE 和 MAPE。这个指标可以和原始YOLO26的检测指标放在一张大表里展示改进模块不会牺牲检测精度的同时还额外输出了业务价值。4.2 可视化方案时间线、堆叠柱状图、热力分布除了数字指标一张好的图能让人一眼看清计数模块在干什么。第一张图建议画“每帧各类别数量折线图”。横轴是帧序号纵轴是目标数量每条曲线对应一个类别。它能反映一天中或一段视频中各类别出现的波动趋势。第二张图是“类别堆叠柱状图”。把每个时间段内的去重总数按类别堆起来适合做整段时间的类别结构分析。第三张图是“ROI热力图”。把检测框中心点落在画面中的位置统计成密度热力图能直观看出哪些区域目标最集中。这三张图不用刻意美化用常见的绘图库就能画得很好看。对论文来说“框体可视化”只是展示模型看到了什么而“统计图表”展示的是模型输出如何被转化成了决策信息。两者配合工作量看起来完整得多。4.3 论文怎么写这部分才不突兀计数模块在论文里的正确位置是“方法”章节里作为后处理分支出现或者在实验章节作为扩展分析。不要把它包装成检测器主干创新那会很牵强审稿人一眼就能看穿。一个比较自然的结构是先描述YOLO26作为基础检测器再提出“现有检测结果无法直接满足业务统计需求”的问题然后给出一个后处理计数分支包含跟踪ID去重和类别累计逻辑。实验部分增加计数指标结论部分强调“不仅提精度还输出结构化统计结果对下游决策更友好”。相比无中生有地加一堆模块这种“检测计数”的组合是真实需求驱动的每一步都有明确的目的论文也更好写。5. 踩坑复盘重复计数、类别失衡与阈值设置这个模块写起来不难真正让我花时间的是各种隐藏场景下的错误计数。下面几个坑是我实际踩过并且反复确认过的直接写出来帮你省时间。5.1 重复计数比漏计更可怕漏计往往是因为目标被挡住或检测置信度低相对容易被发现。重复计数则隐蔽得多尤其当跟踪ID频繁切换时。目标被短暂遮挡后重新出现跟踪模块可能认为它是个新目标分配一个新ID计数就会从1变成2。我在项目中遇到过最夸张的情况一辆车因为树荫遮挡导致ID切换了三次总数直接多了2。解决思路有两个。一是提高跟踪模块的关联稳定性允许目标在遮挡期间保留ID更久不过这会引入计算开销。二是在统计层做“ID合并”补偿如果两个ID的框位置高度重合且类别相同就视为同一个目标。实际操作中我选择的方案是前者在进入计数模块之前先对短时间中断的目标做轨迹插值。也就是目标消失了三五帧就延续它原来的ID继续保持。只有超过阈值才认为它真正离开。5.2 类别不均衡会造成计数虚高如果数据集里某个类别数量极少模型对该类别的置信度往往不稳定。比如“施工现场机械”只有几十个标注框模型很容易把其他类别误检成机械。计数模块是把所有超过阈值的框都算进去的一旦误检错误就会被累计到总数里。这时候不要急着调计数逻辑先回头看看检测器在这个类别上的precision和recall。分类别统计的precision低误检框自然多计数自然虚高。可以按类别单独设置置信度阈值比如普通类别用0.25稀有类别提高到0.45牺牲一点召回率换取计数可信度。5.3 置信度阈值和IOU阈值之间的平衡阈值这件事没有一个通用值关键看你的业务容忍度。如果计数是为了做总量统计稍微漏检几个目标影响不大那么置信度阈值可以适当降低把更多目标框进来。要是计数是为了精确核对库存或人员漏检一个都会被追责那就要把阈值调高再配合跟踪模块保障重框目标只计一次。检测的IOU阈值影响的是NMS之后保留的框数量。阈值设太低同一目标会被拆成两个框阈值设太高密集场景下两个紧挨的目标会被合并成一个框。这两个方向都会让计数失真。我的经验是先按常见值 0.45 跑一遍再看具体场景里的计数误差曲线误差突然变大的帧数往往就是阈值敏感区间。6. 一点个人体会计数模块还能往哪延伸从“YOLO26自动计数”这个点延伸出去我后来还做了两个比较有用的变体。一个是时间片统计按小时或按天聚合计数结果得到一段趋势曲线能够辅助判断高峰期。另一个是ROI密度预警把ROI内目标数量超过阈值的时刻标记出来做成异常告警。这两个变体都没有重新训练模型纯粹是在计数模块之外再加一层规则。过程让我意识到模型性能之外的“封装能力”往往被严重低估。把每一帧的检测结果变成结构化统计再把统计结果变成业务语言很多问题不需要重新训练也能解决。以后你再听到“自动计数”这个需求先别急着往模型里塞新模块仔细问清楚统计口径后面能少走不少弯路。 SEO 优化官网定制响应式建站教育培训建站