1. 项目定位与需求拆解
1.1 为什么“数个数”这个需求比你想的更普遍
我之所以想写这个合集,是因为最近一年里,身边至少有四五个朋友找我咨询同一个类型的问题:能不能用Python写个程序,自动数一下图里有几个人、仓库里有多少箱货、养殖场里有多少只鸡、马路上停了多少辆车。
这类需求乍一听很零散,但拆开来看内核完全一致——都需要对图像或视频里的某一类目标进行计数。而传统的图像处理方案(比如肤色检测、边缘检测、背景差分)在环境复杂、目标密集、遮挡严重的场景下基本都会失效。真正能扛住真实场景的做法,是走深度学习目标检测路线。
这个合集想要解决的核心问题就是:当你面对“数某类目标个数”这个需求时,从算法选型、数据准备、模型训练到最终落地推理,一整条链路该怎么走。
1.2 技术路线的几个核心选项
“目标个数检测”听起来是个很笼统的词,但在技术实现上大概分三个层次:
| 技术层次 | 代表方法 | 适用场景 | 计数精度 | 开发成本 |
|---|---|---|---|---|
| 传统视觉 | 轮廓检测、模板匹配 | 背景单一、目标规整 | 低 | 极低 |
| 深度学习检测 | YOLO系列、SSD、Faster R-CNN | 复杂场景、多目标、遮挡 | 较高 | 中 |
| 深度学习密度估计 | CSRNet、SFANet | 极度密集人群/物体 | 高(区域级) | 高 |
做这个合集时,我把重点放在深度学习目标检测方向上,因为这是绝大多数落地需求的最优折中方案:检测精度够用、推理速度可控、开发资料丰富、踩坑案例也最多。
1.3 合集内容的设计逻辑
这个合集的筛选和排序逻辑,我按“由浅入深、从单一到复合”来编排:
- 先讲通用目标检测框架的核心原理和选型逻辑,比如两阶段和单阶段检测的区别。
- 再讲数据准备环节,包括标注格式转换、数据增强策略。
- 然后是一个完整的基线项目实操,用Python生态从零跑通一个检测计数的完整流程。
- 最后是各类场景化应用的扩展案例,比如车辆计数、人员统计、农产品分拣计数等。
这种结构对新手友好,因为每一篇都有可运行的代码和明确输出;对老手也有参考价值,因为扩展案例中包含了不同场景下的调优思路和踩坑记录。
2. 目标检测基座:选对模型才谈得上计数准确
2.1 两阶段检测与单阶段检测的核心差异
关于目标检测算法,市面上的教程已经多到泛滥,但大部分都停留在“给你跑通一个YOLO”的层面。我在这里想说得更直白一点,把两代主流模型家族的底细讲清楚。
两阶段检测的代表是Faster R-CNN。它先通过区域建议网络(RPN)在一张图里找出可能包含目标的候选框,再对每个候选框做二次分类和回归。这个方案的优点是小目标检测能力强、精度上限高,缺点是速度慢,参数量大,在实时计数场景里容易被卡脖子。
单阶段检测的代表是YOLO和SSD。它们直接在一张图上划网格、每个网格预置多个锚框,一次前向传播同时输出类别和位置。优点是速度快、端到端训练简单,缺点是极小目标和极端密集场景下容易出现漏检。
这个合集的实操项目中,我默认选择了YOLO系列模型,原因很简单:计数场景通常需要处理大量图片甚至视频流,速度是刚需;而YOLO在精度上的短板可以通过后续的模型调优和推理策略(比如多尺度推理)来弥补。如果你追求极致的密集人群计数精度,那就得考虑密度估计路线,不过那是另外一套完全不同的技术栈了。
2.2 锚框设计才是暗藏玄机的环节
很多人训练检测模型时,习惯直接把公开权重拿来在自己的数据集上继续跑,然后发现精度一直上不去,怎么调都白费力气。这种情况十有八九是锚框设置不合理。
YOLO系列默认的锚框尺寸是在COCO数据集上聚类得到的,覆盖的是行人、猫狗、车辆、杯子这类常见物体。假如你的任务是数养殖场里的鸡,而鸡在画面里通常只有二三十个像素大小,那么默认锚框根本没法匹配到鸡的尺度,模型训练时正样本数量极少,自然学不出靠谱的检测效果。
解决办法是重新聚类自己的数据集:
import numpy as np from sklearn.cluster import KMeans # 读取标注文件中的框尺寸,格式为(x1, y1, x2, y2) boxes = np.loadtxt("all_boxes.txt", dtype=float)[:, 2:] # 取宽高 wh = boxes[:, [2, 3]] - boxes[:, [0, 1]] # 宽高 = 右下角 - 左上角 kmeans = KMeans(n_clusters=9, random_state=42) kmeans.fit(wh) anchors = kmeans.cluster_centers_ anchors = sorted(anchors, key=lambda x: x[0] * x[1]) print(anchors)我用这个方式处理过一个车辆计数的项目,聚类后的锚框尺度从(12, 8)到(180, 120)不等,明显比默认锚框紧凑得多。替换锚框后mAP直接提升将近7个百分点,这个提升幅度在调优阶段已经是非常可观的了。
2.3 模型选型的通用判断标准
我给这合集定了一个实用选型矩阵,你直接用这个判断就行了:
- 目标尺寸普遍小于32x32像素 → 优先选用小目标优化过的检测器(如YOLO-Nano变体或带特征金字塔的模型),并配合多尺度训练。
- 场景中有大量遮挡 → 优先选择带注意力机制的检测器,或者在后处理阶段提高NMS的容错阈值。
- 需要实时处理视频流 → 优先选择单阶段检测器,并考虑剪枝和量化。
- 对计数绝对精度要求极高 → 可以混合检测+跟踪方案,用多帧投票消除单帧误差。
要清楚一件事:目标检测模型输出的是一堆矩形框和类别置信度,而你要的“个数”则是这堆框的数量。这中间还有一条“后处理”的黑路要走,下一部分细讲。
3. 数据准备与标注的坑,比模型本身更值得花时间
3.1 标注格式五花八门,堪称第一道拦路虎
做深度学习项目,数据标注是最耗时也最容易因为格式问题翻车的环节。最开始接触这个领域的人,往往在标注工具上反复辗转,一会儿用LabelImg,一会儿用labelme,一会儿又要转成YOLO的txt格式,折腾半天发现模型训练入口处还在报错。
我这合集的建议是:统一使用COCO格式作为中转,用脚本把任何标注格式都转换成COCO JSON,再由数据集配置文件动态加载。COCO格式数据组织清晰、生态支持最好,后续要转成YOLO格式、VOC格式或其它格式都只需要写一个小脚本。
一个典型的转换逻辑如下:
import json import os def voc_to_coco(voc_xml_dir, out_json): images = [] annotations = [] categories = [{"id": 1, "name": "person"}] image_id = 0 ann_id = 0 for xml_file in os.listdir(voc_xml_dir): if not xml_file.endswith(".xml"): continue # 解析XML获取标注信息,这里省略具体解析代码 # 生成images条目和annotations条目 image_id += 1 dataset = {"images": images, "annotations": annotations, "categories": categories} with open(out_json, "w") as f: json.dump(dataset, f)3.2 一个让训练效果翻倍的数据增强策略
很多人在做目标检测数据增强时,只做随机翻转和颜色抖动,其实这对于“计数”任务远远不够。计数任务最痛点的问题是:目标个数在不同图像中差异巨大,有的图里只有两三个目标,有的图里几十上百个目标。如果不刻意做“复制粘贴式增强”来平衡目标数量的分布,模型的召回率会在目标密集的场景上严重偏低。
我之前做过一个货架商品计数项目,训练集里商品数量超过50个的图片只占8%,导致模型在真实货架上漏检非常严重。后来我用了一个简单的拼接增强策略:把两张各包含二三十个目标的小图拼成一张大图,顺便复制粘贴目标框到空白区域。这样目标数量分布被拉均匀,最终模型在高密度区域的召回率提升了12%以上。
代码实现大致是:
import random import cv2 import numpy as np def mosaic_augment(img1, boxes1, img2, boxes2): h1, w1 = img1.shape[:2] h2, w2 = img2.shape[:2] canvas = np.zeros((max(h1, h2), w1 + w2, 3), dtype=np.uint8) canvas[:h1, :w1] = img1 canvas[:h2, w1:w1 + w2] = img2 boxes = boxes1 + [(x + w1, y, x + w1 + bw, y + bh) for (x, y, bw, bh) in boxes2] return canvas, boxes当然这只是个小示例,真正的Mosaic增强里还需要考虑缩放、透视畸变、标注边界裁剪等细节,但核心思想是一样的:让模型在训练时看到更丰富的目标空间分布。
3.3 数据质量比数据量更值钱
训练摇头晃脑的猫狗模型,你可以觉得偶尔标错一两个框无关紧要。但计数任务不一样——假若画面中有18个目标,模型只检测出17个,对于“有无异常”的业务判断来说可能就漏了一件大事。所以标注质检环节非常关键,建议至少做一次双人交叉复核,特别是对遮挡目标和边缘截断目标。
一个容易让人崩溃的细节是:靠图像边缘截断的目标类别该如何标注?有些工具默认只标注完整目标,这会导致模型对不完整目标完全无感。我的建议是凡是面积占比超过20%的边缘目标都必须标注,否则模型永远不会学到“目标可以不完整出现”这个规律。
另一方面,标注框的紧致程度也影响计数精度。框如果明显比目标大一圈,两个相邻目标就会因为框区域重叠而发生NMS合并,最终导致少计数。这种情况大家一般注意不到,排查时费劲得很,我建议在做标注前给标注工人明确一个规则:框必须贴合目标外接矩形,误差不超过3个像素。
4. 从零跑通一个目标计数项目的全流程实操
4.1 环境搭建与工程结构规划
这个合集里的每个项目都基于Python 3.9+,依赖PyTorch、OpenCV、NumPy和Ultralytics框架。关于YOLO这个系列,我个人建议直接使用Ultralytics封装好的包,原因不只是因为代码简洁,更因为它内置了训练、验证、导出、部署的全链路工具,对新人极其友好。
安装依赖:
pip install ultralytics opencv-python numpy torch torchvision工程目录建议这样规划,避免后期把自己绕晕:
project_root/ ├── data/ │ ├── images/ # 原始图片 │ ├── labels/ # 标注文本 │ └── dataset.yaml # 数据集配置 ├── scripts/ │ ├── convert_format.py │ ├── train.py │ ├── detect_and_count.py │ └── evaluate.py ├── models/ # 权重文件 └── runs/ # 训练日志与结果这种单目录工程结构在处理多个项目时尤其方便,每个项目独立一套环境,不会出现依赖冲突。运行训练任务前,建议先确认GPU显存和批大小之间的匹配关系,否则容易出现CUDA Out of Memory错误。
4.2 训练流程与关键参数调节
以Ultralytics为例,启用一个计数训练任务非常简单:
from ultralytics import YOLO model = YOLO("yolov8n.pt") results = model.train( data="data/dataset.yaml", epochs=100, imgsz=640, batch=16, workers=4, device=0, patience=15, optimizer="SGD", lr0=0.01, )但参数写下去之前,有几个点需要结合你的具体场景想清楚:
imgsz不要盲目跟着默认值走。如果目标整体较小,建议用imgsz=1280甚至更高。目标检测在低分辨率输入下容易产生大量漏检,而在高分辨率输入下虽然预测速度慢了,计数精度却往往提升明显。batch受显存限制。我建议不要缩减batch来满足大图训练,而是用梯度累积策略,保证有效批大小不变,这样才能稳定收敛。patience是早停机制,设置为15意味着如果连续15个epoch在验证集上没有更好表现就会停止训练。对于数据量较小的项目,这个值建议提升到30,给模型更多寻找最优点的时间。
训练完成后,验证指标里最值得关注的不是mAP而是每张图的平均漏检率和误检率。这两个指标更直接反映计数任务的业务效果。模型输出的预测框数量并不直接等于真实目标数,后处理才是决定成败的地方。
4.3 计数后处理:怎么把一堆框变成可信的“个数”
有一次我帮一位做商场客流统计的朋友调模型,模型mAP达到了86%,但是数出来的人数总是比真实值少5%左右。排查半天发现问题出在后处理阶段:置信度阈值设得太高了,导致大量低置信度但真实存在的目标被过滤掉。
目标计数场景里置信度阈值的选取,和普通目标检测场景相当不同。普通检测追求“宁缺毋滥”,防止把背景误判成目标;而计数场景追求“不漏不重”,因此应该:
- 设定较低的基础置信度阈值,比如0.25。
- 采用类别特定的自适应阈值,比如人这个类别置信度降一点,因为人员出现姿势变化大、遮挡多;车辆这种特征稳定的类别置信度可以适当提高。
- 利用多尺度推理或测试时增强(TTA)对同一张图预测多次,再对框进行融合,以此消除单帧预测的随机误差。
一个我屡试不爽的后处理流程如下:
def merge_predictions(pred1, pred2, pred3): from collections import defaultdict all_boxes = defaultdict(list) for pred in [pred1, pred2, pred3]: for box in pred.boxes: cls = int(box.cls) x1, y1, x2, y2 = box.xyxy[0].tolist() conf = float(box.conf) all_boxes[cls].append([x1, y1, x2, y2, conf]) final_count = {} for cls, boxes in all_boxes.items(): final_count[cls] = len(boxes) # 简单计数,实战中可用加权投票策略 return final_count如果视频计数场景中目标运动幅度不大,还可以用目标跟踪算法(如ByteTrack)取代多帧融合,因为跟踪算法天然会为每个目标分配一个唯一ID,统计独立ID数量即是目标个数。
5. 场景化扩展:从通用检测到分领域落地
5.1 人员计数:人群密度与遮挡是最大挑战
人员计数是最常见的需求。街拍图中人一多,互相遮挡就成了家常便饭。YOLO模型在这种场景下经常出现“一个人被检测成两个框”或“两个人重叠被合并成一个框”的情况。
我常用的一套组合拳是:
- 在训练阶段引入随机遮挡增强(Random Erasing),让模型习惯目标被局部遮盖的状态。
- 用置信度加权的NMS替换普通NMS,而不是纯粹依靠框重叠面积来合并。
- 结合头部检测。密集人群中头部遮挡往往比身体轻,所以额外训练一个头部检测器,并让头部框与人体框联合投票计算最终人数。
5.2 车辆计数:小目标与运动模糊的博弈
航拍视角下的车辆计数,车辆通常只有二三十像素,而且车身颜色与地面背景容易混淆,运动模糊让标注变得异常痛苦。
针对这个场景,我的经验是:
- 优先使用带注意力机制与特征金字塔的小目标检测变体,而不要直接用最大的YOLOv8x模型——参数多反而容易在特征提取中把小目标信息淹没。
- 将图像切块推理(如切成640x640的块,重叠区域50%),而不是整体缩放。整体缩放到640x640会让车辆缩成一团,几乎不可能检测出来。
- 训练数据中务必包含大量不同光照条件和天气条件下的图像,否则晴天训练出来的模型在阴天场景下漏检率会急剧上升。
5.3 农产品计数:从实验室走出来需要解决大量边界情况
做过农产品分拣计数项目的人都有体会,最大的问题不是算法本身不行,而是“传送带上的物理状态太不稳定”。比如西红柿会滚动、苹果会叠在一起、鸡蛋会因反光让检测器眼花。
这时,我会在YOLO检测之后加一个“位置校验层”:
- 根据目标框的中心点分布做聚类,同一簇内只计一次。
- 对相邻两帧的目标框做交并比计算,如果同一个框连续出现超过3帧,才确认是一个稳定目标。
- 对置信度偏低的目标做一个二次分类确认,避免把传送带的机械臂误判成农产品。
农产品计数项目里,我强烈建议多看几个失败案例图,因为我发现很多阈值参数不是靠调出来的,是靠看误检样本改出来的。
5.4 计数结果的可视化与业务对接
实际交付时直接输出一个数字,业务方往往没有安全感。所以我在每个项目里都会做结果可视化输出:在原始图像上画出检测框、给每个目标编上序号、标注置信度,同时生成一张CSV表格包含图片文件名、类别、目标个数、处理耗时等字段。
这种“可解释的计数结果”在很多场景里直接决定了项目能否通过验收,别小看这一步。视觉上的反馈可以帮助业务人员直观检查漏检和误检,也比单纯给个数字更能建立信任感。
6. 常见问题与排障手册
6.1 检测框数量不稳定的原因与对策
| 问题表现 | 可能原因 | 解决方案 |
|---|---|---|
| 同一张图预测两次结果数量不同 | 推理过程中未固定随机种子,或开启TTA导致随机性 | 设置torch.manual_seed(0),关闭随机采样 |
| 目标个数偏少 | 置信度阈值过高、小目标漏检 | 降低阈值到0.15,提高推理分辨率 |
| 目标个数偏多 | 存在严重误检,把背景纹理当目标 | 提升置信度阈值,并增加背景负样本训练 |
| 合并框导致个数减少 | NMS阈值过高 | 把NMS IoU阈值从0.5降到0.3 |
6.2 训练时损失不下降的经典坑
最常见的原因是学习率设置不合理和标签格式错乱。检查标签是否与图片对齐时,我建议写一行可视化脚本:
import cv2 from ultralytics.utils.plotting import Annotator image = cv2.imread("sample.jpg") annotator = Annotator(image) # 从对应label文件读取坐标,这里省略解析过程 for box in parsed_boxes: annotator.box_label(box, "target") image = annotator.result() cv2.imwrite("check_label.jpg", image)如果画出来的框位置与物体明显不匹配,那就是数据转换脚本里的坐标换算出了问题,最常见的是坐标归一化的分母弄反了或类别ID从1开始而框架内部从0开始,这类问题排查难度不大,但极其耗费时间。
6.3 推理速度不达标的调优方向
如果模型在GPU上推理很快但CPU上跑不动,可以用模型导出技术,把模型导出成TensorRT或OpenVINO格式。导出后再做一次INT8量化,速度可以提升3到5倍,精度损失通常控制在2%以内。
还有一招——如果项目允许通过抽帧而不是逐帧计数来处理视频,那就可以把推理负载直接降低64%以上。每秒处理两帧和每秒处理三十帧,在多数人员、车辆计数场景里的误差差别可能只有1%到2%,但计算成本差异巨大。
7. 最后分享一点自己的体会
折腾了这么多个计数项目之后,我最大的感受是:目标检测模型的精度提升空间是有限的,但工程侧的处理策略对最终业务效果起着决定性作用。光是“把图像从缩放变成了切块推理”这一个改动,就能让小目标场景的漏检率下降好几倍。
另外要强调的是,每类场景都要关注数据采集时的实际分布。我之前在养殖场计数项目上踩过一个坑,训练数据全是白天采集的,晚上灯光昏暗的时候推理结果惨不忍睹。后来补了一批夜间红外图像重新训练,效果马上上来了。说到底,模型只是工具,真正决定上线效果的是数据和后处理策略。
这个合集后续还会继续扩展,比如加实例分割计数的案例、给密集场景加入密度估计做混合方案的案例。有什么具体场景和问题,欢迎在评论区留言交流。我后续会针对高频问题再写更深入的专题。