news 2026/10/14 14:59:44

YOLOv5+DeepSORT目标跟踪计数:从检测到统计的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv5+DeepSORT目标跟踪计数:从检测到统计的工程实践

简介:基于Python和YOLOv5、DeepSORT联合实现的行人与车辆跟踪计数系统,面向毕业设计、课程设计以及目标检测类项目开发者。资料内含完整可运行的源码、项目文档和演示视频,从环境配置、模型加载到检测追踪与计数输出均有说明,覆盖常用依赖与运行入口,便于在验证后继续二次延展。压缩包共122个文件,以Python源码、yaml模型配置、PyTorch权重、mp4演示视频和Markdown文档为主,整体约129.7MB,目录层级清楚,可快速定位所需模块。目前已有66人学习下载。结合配套文档与演示录像,可清晰理解YOLOv5检测与DeepSORT跟踪的协同流程,掌握目标ID分配、轨迹管理、区域计数等关键实现细节,同时可根据自带配置文件和测试视频复现运行效果,适合需要快速搭建同类系统或深入拆解算法逻辑的读者参考。

1. 一套能数清楚人流车流的系统:YOLOv5+DeepSORT 跟踪计数到底解决什么问题

商场入口想知道一天进出多少人,路口想统计早晚高峰的车流量。如果只做目标检测,每帧框出来的目标无法跨帧关联,同一辆车在不同帧会被当成好几个目标,计数必然虚高。把 YOLOv5 的检测结果交给 DeepSORT 做跨帧跟踪,每个目标拿到稳定 ID 后再计一次数,才能真正落到客流统计、车流统计这类实际场景。这套方案对毕业设计、课程设计或工程预研都很合适:YOLOv5 负责“看到”目标,DeepSORT 负责“认出”同一个目标,计数逻辑只关心 ID 是否越线或进出区域。上手门槛并不高,Python 环境下把检测和跟踪串起来,核心代码可以控制在几百行内。后面内容就是沿着这个链路逐步拆开讲,每一步都给出可复现的做法和参数依据。

2. 把检测先跑通:YOLOv5 环境搭建与模型推理的最小闭环

2.1 环境依赖怎么选:Python 版本、PyTorch 与推理后端

YOLOv5 对 Python 版本的要求不算苛刻,3.8 到 3.10 都能稳定跑。真正影响体验的是 PyTorch 和 CUDA 的搭配。手头有 NVIDIA 显卡,优先装 CUDA 版 PyTorch,推理速度能比 CPU 快一个数量级;没有显卡就老老实实装 CPU 版,用 yolov5s 权重跑单路视频也能勉强实时,只是分辨率要控制在 640 以内。

# 创建独立虚拟环境,避免污染系统 Python conda create -n track python=3.8 conda activate track # 安装 PyTorch,cu118 对应 CUDA 11.8,需要根据你的驱动版本调整 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 YOLOv5 依赖 pip install -r requirements.txt

逻辑说明:虚拟环境是第一步,YOLOv5 的依赖项里有特定版本的 numpy、opencv-python、matplotlib 等,直接装在系统环境里容易和已有项目冲突。PyTorch 的安装源选择取决于显卡驱动支持的 CUDA 版本,可以在命令行执行nvidia-smi查看驱动对应的 CUDA 版本,再决定 cu118 还是 cu121。

参数说明:from requirements.txt安装方式会按官方锁定版本安装,注意装完后检查一下 numpy 版本,如果后续 OpenCV 报错,通常是 numpy 2.x 和旧版 opencv-python 不兼容,pip install numpy==1.24.4可以解决。

2.2 用 YOLOv5 跑通第一帧检测:从权重加载到输出解析

环境就绪后,先用单张图片验证检测链路。这里用torch.hub加载官方预训练权重,第一次运行会自动下载 yolov5s.pt,约 14 MB,网络慢的话也可以手动放到项目目录下。

import torch import cv2 import numpy as np # 加载预训练模型,yolov5s 是轻量版,速度和精度平衡 model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True) # 推理时关闭梯度计算,减少显存占用 model.eval() model.conf = 0.25 # 置信度阈值,低于该值的目标被过滤 model.iou = 0.45 # NMS 的 IoU 阈值,控制重叠框的合并力度 # 读取图片并推理 img = cv2.imread('street.jpg') results = model(img) # 输出结果: results.pandas().xyxy[0] 包含框坐标、置信度、类别 detections = results.pandas().xyxy[0] person_boxes = detections[detections['name'] == 'person'][['xmin', 'ymin', 'xmax', 'ymax', 'confidence']].values print(f"检测到 {len(person_boxes)} 个行人")

逻辑说明:model.conf和model.iou可以在调用前直接修改,也可以作为参数传入model(img, conf=0.25, iou=0.45)。检测结果里xyxy格式是左上角和右下角坐标,后续送给 DeepSORT 之前通常要转成xywh格式,即中心点坐标加宽高。pandas().xyxy[0]这个接口能直接拿到结构化数据,省去自己解析张量的麻烦。

参数说明:置信度阈值 0.25 是官方默认值,实际项目中如果检测框频繁抖动,可以提高到 0.4 以上,代价是召回率下降。IoU 阈值 0.45 控制 NMS 合并,值越大越倾向于保留更多重叠框,值越小越容易合并掉同一目标的多余框。调试阶段建议把阈值调低观察漏检情况,上线前再调高减少误检。

2.3 检测结果的后处理:类别过滤与坐标转换

YOLOv5 的 COCO 预训练模型能识别 80 类目标,但计数场景通常只关心行人和车辆。类别过滤不仅是业务需求,还能显著降低后续跟踪器的计算压力,避免把路牌、树影之类的误检目标纳入统计。

# 只保留行人、汽车、公交车、卡车,对应 COCO 类别 id: 0, 2, 5, 7 CLASS_IDS = [0, 2, 5, 7] def filter_detections(detections, class_ids): """ 按类别过滤检测结果, 同时转换为 DeepSORT 需要的 xywh 格式 detections: pandas DataFrame, 来自 results.pandas().xyxy[0] """ filtered = detections[detections['class'].isin(class_ids)] if filtered.empty: return np.empty((0, 5)) # xyxy -> xywh: 中心点坐标 + 宽 + 高 boxes = filtered[['xmin', 'ymin', 'xmax', 'ymax']].values xywh = np.zeros_like(boxes) xywh[:, 0] = (boxes[:, 0] + boxes[:, 2]) / 2 # cx xywh[:, 1] = (boxes[:, 1] + boxes[:, 3]) / 2 # cy xywh[:, 2] = boxes[:, 2] - boxes[:, 0] # width xywh[:, 3] = boxes[:, 3] - boxes[:, 1] # height # 拼接置信度, 组成 [cx, cy, w, h, conf] 的数组 conf = filtered[['confidence']].values return np.hstack((xywh, conf))

逻辑说明:这段转换是检测环节和跟踪环节之间的桥梁。DeepSORT 的输入格式要求是[x, y, w, h, conf],其中 x、y 是目标中心点坐标,这样设计是为了配合卡尔曼滤波的状态表示。直接传 xyxy 格式会导致跟踪器内部状态初始化出错,这是新手最常见的报错点之一。

参数说明:class_ids的取值来自 COCO 数据集类别定义,0是 person,2是 car,5是 bus,7是 truck。如果场景里还有摩托车(id 3)和自行车(id 1),按需加进去即可。注意过滤时用class列而不是name列,数值比较更稳定,不受类别名大小写影响。

3. 从检测框到稳定轨迹:DeepSORT 跟踪原理与代码接入

3.1 DeepSORT 的核心机制:卡尔曼滤波与级联匹配的配合

DeepSORT 的全称是 Deep Cosine Metric Learning for SORT,它在 SORT 的基础上引入了外观特征(ReID 特征)来弥补单纯运动模型的不足。SORT 只靠卡尔曼滤波预测位置和 IoU 匹配关联目标,一旦目标快速运动或发生遮挡,ID 很容易丢失;DeepSORT 的做法是把目标的运动特征和外观特征同时用于匹配,跟踪鲁棒性明显提升。

卡尔曼滤波负责预测目标在下一帧的位置,同时估计预测误差。级联匹配则优先匹配近期出现过的目标,避免丢失时间较长的轨迹抢占新目标的匹配机会。外观特征通过一个小型 CNN 网络提取,每帧生成一个 128 维向量,与历史轨迹的外观特征库计算余弦距离。匹配最终交给匈牙利算法求解,代价矩阵由马氏距离和余弦距离加权得到。

3.2 把检测结果接入 DeepSORT:tracker 初始化与 update 流程

DeepSORT 的工程实现有很多版本,我常用的是把检测框和置信度传给update方法,它内部会完成预测、匹配、更新三步,并返回当前帧所有活跃轨迹的 ID 和位置。

from deep_sort import DeepSort # 初始化跟踪器: model_path 指向训练好的 ReID 权重 deepsort = DeepSort( model_path='ckpt.t7', # ReID 特征提取模型权重 max_dist=0.2, # 余弦距离阈值, 超过则认为不是同一目标 min_confidence=0.3, # 检测置信度低于该值的框不进入跟踪 nms_max_overlap=0.5, # 跟踪框 NMS 阈值 max_iou_distance=0.7, # IoU 关联阈值, 用于短期匹配 max_age=70, # 轨迹丢失后最多保留的帧数 n_init=3, # 轨迹被确认前必须连续匹配的帧数 nn_budget=100 # 外观特征库容量, 超出后丢弃最早特征 ) # 每帧处理: detections 是上一节 filter_detections 的输出, shape=(N, 5) # 注意 DeepSORT 要求宽高比在合理范围, 过扁的框会被内部过滤 outputs = deepsort.update(detections, frame) # outputs 格式: [x1, y1, x2, y2, track_id], 可以直接画框 for x1, y1, x2, y2, track_id in outputs: cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, f'ID:{track_id}', (int(x1), int(y1)-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

逻辑说明:update方法接收的检测框是 xyxy 格式,注意不同开源实现的接口有差异,有的要求传入[x1, y1, x2, y2, conf],有的要求先转换。这里的outputs返回的是跟踪结果,包含跟踪框坐标和稳定的 track_id。track_id是计数的核心依据,同一个目标在连续帧中保持同一个 ID,才能避免重复计数。

参数说明:max_dist=0.2是外观匹配的核心阈值,值越小对目标外观变化越敏感,容易丢 ID;值越大越容易把不同目标串到一起。max_age=70控制目标丢失后的保留时间,值越大对遮挡越鲁棒,但也会让短暂离开视野的旧目标占用匹配资源。n_init=3表示新目标需要连续 3 帧成功匹配才被确认为正式轨迹,避免单帧误检导致虚假轨迹。

3.3 跟踪参数怎么调:从 ID 跳变现象反推参数方向

调参不能靠猜,要观察现象反推。最常见的两个现象是 ID 频繁跳变和目标跟踪断裂,它们的调参方向正好相反。

# 在调试代码里打印每个 track_id 的存活帧数, 观察稳定性 # 脚本片段: 统计每个 ID 出现的帧数 from collections import defaultdict id_frames = defaultdict(int) # ... 在跟踪循环中 ... for x1, y1, x2, y2, track_id in outputs: id_frames[track_id] += 1 # 结果类似: {1: 120, 2: 3, 3: 150, 4: 2} # 帧数极短的 ID 是跳变信号, 说明跟踪器的匹配策略需要调整

如果大量 ID 只存活了几帧,说明目标在不断丢失和重新初始化。此时应该调大max_age,让轨迹在目标短暂遮挡后还能重新关联;同时适当调大max_dist,提高外观匹配的容忍度。反过来如果不同目标共享同一 ID,出现两个目标串在一起的情况,那就是max_dist太大,需要调小,同时降低max_iou_distance让空间重叠的约束更严格。

实践里我一般先用默认参数跑通,记录 ID 跳变频率,再做单参数网格搜索。比如把max_dist分别设为 0.1、0.2、0.3 跑同一段视频,对比 ID 存活帧数的分布,比凭感觉调要靠谱得多。

4. 计数器落地的两种方案:虚拟线计数与区域进出计数

4.1 方案一:虚拟线越线计数,一条线段完成单向统计

越线计数的原理很简单:在视频画面中定义一条线段,跟踪到的目标中心点从前一帧到当前帧跨越了这条线,就认为完成了一次穿越。配合 track_id 去重,每个 ID 只在第一次越线时计数。

def is_crossing(prev_point, curr_point, line_start, line_end): """ 判断目标中心点是否跨越了线段 prev_point: 上一帧中心点 (x, y) curr_point: 当前帧中心点 (x, y) line_start: 线段起点 (x1, y1) line_end: 线段终点 (x2, y2) 返回值: True 表示跨越, False 表示未跨越 """ def side(p): return (line_end[0] - line_start[0]) * (p[1] - line_start[1]) - \ (line_end[1] - line_start[1]) * (p[0] - line_start[0]) prev_side = side(prev_point) curr_side = side(curr_point) # 两点在直线异侧即跨越 return prev_side * curr_side < 0 # 在跟踪循环中使用 crossed_ids = set() # 记录已计数的 ID count = 0 for x1, y1, x2, y2, track_id in outputs: cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 # 用字典保存每个 ID 的上一帧中心点 if track_id not in prev_centers: prev_centers[track_id] = (cx, cy) continue prev = prev_centers[track_id] if is_crossing(prev, (cx, cy), line_start, line_end): if track_id not in crossed_ids: count += 1 crossed_ids.add(track_id) # 关键: 同一 ID 只计一次 print(f"ID {track_id} 越线, 当前计数: {count}") prev_centers[track_id] = (cx, cy)

逻辑说明:is_crossing函数用的是向量叉积的符号变化来判断点是否跨线,不需要计算交点坐标,效率高且数值稳定。crossed_ids集合是防重复计数的关键,一个目标从线的一侧走到另一侧只算一次,即使它在线附近来回晃动也不会重复累加。

参数说明:line_start和line_end是手动标定的两个画面坐标点,一般选在目标必经的通道位置。线不要放太靠近画面边缘,否则目标刚进入画面就被计数,跟踪状态还没稳定下来,容易引入误判。线的方向决定了计数方向,比如从左到右画一条线,只有从左往右跨越才计数,反向跨越则不计。

4.2 方案二:区域进出计数,适合闸机口与复杂出入口

越线计数在目标运动方向单一的场景下很好用,但出入口存在徘徊、折返的情况,比如有人在门口犹豫了一下又走回去,此时越线计数会误统计。区域进出计数更适合这种场景:定义一个多边形区域,目标从区域外进入认为是一次“进入”,从区域内出去则认为是一次“离开”。

import cv2 def point_in_polygon(point, polygon): """ 判断点是否在多边形内部 point: (x, y) polygon: 顶点列表 [(x1,y1), (x2,y2), ...] """ x, y = point n = len(polygon) inside = False p1x, p1y = polygon[0] for i in range(1, n + 1): p2x, p2y = polygon[i % n] if (y > min(p1y, p2y)): if (y <= max(p1y, p2y)): if (x <= max(p1x, p2x)): if p1y != p2y: xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x if p1x == p2x or x <= xinters: inside = not inside p1x, p1y = p2x, p2y return inside # 维护每个 ID 在区域内外状态 inside_status = {} # track_id -> True/False enter_count = 0 exit_count = 0 for x1, y1, x2, y2, track_id in outputs: cx, cy = (x1 + x2) / 2, (y1 + y2) / 2 # 只取宽高比合理的框, 过滤掉极端形状的误检 w, h = x2 - x1, y2 - y1 if w <= 0 or h <= 0 or w / h > 5: continue is_inside = point_in_polygon((cx, cy), zone_polygon) if track_id not in inside_status: # 新 ID: 记录初始状态, 不计数, 避免目标已在区域内时被误判 inside_status[track_id] = is_inside else: # 状态变化: 从外到内是进入, 从内到外是离开 if not inside_status[track_id] and is_inside: enter_count += 1 print(f"ID {track_id} 进入, 累计进入: {enter_count}") elif inside_status[track_id] and not is_inside: exit_count += 1 print(f"ID {track_id} 离开, 累计离开: {exit_count}") # 状态更新 inside_status[track_id] = is_inside

逻辑说明:区域计数的精髓在状态机——每个 ID 维护一个布尔状态,只有当状态发生翻转时才计数。新出现的 ID 直接记录初始状态但不计数,这样可以规避目标一开始就在区域内部的情况。point_in_polygon用的是射线法,对任意凸多边形和凹多边形都有效。

注意区域边界的处理:目标中心点正好落在多边形边上时,point_in_polygon的结果不稳定,实际项目中可以在多边形上做一个内缩的小缓冲区,或者直接忽略距离边界太近的检测框,数出来的数据会更平滑。

4.3 双向计数:用中心点位移判断行进方向

很多场景需要区分方向,比如商场入口要分别统计进和出的人数,道路要区分上行和下行。方向判断可以结合越线计数实现:记录目标跨越线时的速度方向,即上一帧位置指向当前帧位置的向量。

def get_direction(prev_point, curr_point): """ 计算目标运动方向角度, 以水平向右为 0 度, 逆时针为正 返回值: 方向角 (弧度), 范围 [-pi, pi] """ dx = curr_point[0] - prev_point[0] dy = curr_point[1] - prev_point[1] return math.atan2(dy, dx) # 方向角到文字描述 DIRECTION_LABELS = { 'right': (math.pi * -0.25, math.pi * 0.25), 'down': (math.pi * 0.25, math.pi * 0.75), 'left': (math.pi * 0.75, math.pi * 1.0), 'up': (math.pi * -1.0, math.pi * -0.75), } def direction_label(angle): """把方向角映射到四个主方向之一""" # 处理角度区间重叠的简化实现, 实际可改为查表 for label, (low, high) in DIRECTION_LABELS.items(): if low <= angle <= high: return label return 'unknown' # 在越线计数时结合方向 if is_crossing(prev, (cx, cy), line_start, line_end): angle = get_direction(prev, (cx, cy)) label = direction_label(angle) if label == 'right': right_count += 1 elif label == 'left': left_count += 1

逻辑说明:get_direction使用中心点前后两帧的位移来计算方向角,比利用检测框宽高变化判断方向更直接。注意这里有一个陷阱:单帧位移可能会受检测框抖动影响,方向角会跳跃,解决方法是把位移向量平滑一下,用最近 3 帧的平均位移来代替单帧位移。

参数说明:方向区间用 90 度象限划分是简化做法,实际场景中目标的运动方向很少完全水平或垂直,建议做成可配置的扇形区间,比如把 45 度范围内的位移都归为水平方向。

5. 避坑指南:ID 跳变、重复计数与性能瓶颈的排查记录

5.1 检测框抖动导致目标 ID 反复切换

现象:视频中行人走路时身体晃动,检测框大小有时 120 像素有时 80 像素,导致同一目标的框与前几帧 IoU 过低,匹配失败,ID 每隔十几帧就换一个。

原因:YOLOv5 的检测框是目标外接矩形,行人摆臂、姿态变化都会让框的尺寸和位置明显波动,进而影响跟踪器的匹配结果。IoU 匹配对框的稳定性极其敏感,而 DeepSORT 的级联匹配如果连续几帧失败,就会把轨迹标记为丢失并赋予新 ID。

解决:把 YOLOv5 的置信度阈值从 0.25 提高到 0.4,过滤掉低质量的检测框,框的抖动幅度会明显减小。同时把 DeepSORT 的max_age从默认 70 提高到 100 甚至更高,即使中间有几帧匹配失败,也有余量等目标重新关联回来。如果还有抖动,考虑用 EMA 对检测框做一次平滑:

# 简单指数平滑, 对同一 ID 的检测框做时间维平滑 smoothed_boxes = {} alpha = 0.6 # 平滑系数, 越大越依赖历史值 for x1, y1, x2, y2, track_id in outputs: box = np.array([x1, y1, x2, y2], dtype=np.float32) if track_id in smoothed_boxes: smoothed_boxes[track_id] = alpha * smoothed_boxes[track_id] + (1 - alpha) * box else: smoothed_boxes[track_id] = box

5.2 同一目标被重复计数:track_id 去重逻辑失效

现象:一个行人从左边走到右边,计数从 10 直接跳到了 12,对比视频发现是同一个目标被算了两次。

原因:目标在行进过程中,中途有一段时间被其他行人遮挡,DeepSORT 丢失了原轨迹,目标重新出现后被赋予了新 ID。计数逻辑只认 ID 不认人,新 ID 自然又触发了一次越线计数。

解决:单纯靠 ID 去重解决不了遮挡导致的 ID 切换,需要引入位置约束:如果在计数线附近出现一个新 ID,且新 ID 的起始位置与旧 ID 最后一次出现的位置距离小于某个阈值(比如 30 像素),并且旧 ID 的消失时间不超过 1 秒,就认为两者是同一个目标,做一次 ID 合并。这个逻辑也叫轨迹融合,实现时可以用相邻 ID 的起点和终点做配对。

5.3 目标被遮挡后跟踪轨迹断裂

现象:一辆车被路过的卡车挡住 2 秒,卡车开走后,被挡的车重新出现在画面里,但 ID 已经从 3 变成了 19,后续的计数也被打乱。

原因:DeepSORT 的max_age默认值是 70 帧,假设视频是 25 FPS,70 帧只有 2.8 秒。目标被遮挡超过这个时间,轨迹就被彻底删除,重新检测到后只能初始化新轨迹。

解决:把max_age调大到 120~150,同时确认nn_budget不要太小。nn_budget是外观特征库的容量上限,如果目标被遮挡期间特征库里的旧特征被清掉了,重新出现时外观匹配仍然可能失败。nn_budget=100在长遮挡场景下不够,建议调到 200 以上。

5.4 夜间场景行人检测率骤降

现象:晚上 7 点后,画面里的行人明显少了,对比人工目测,检测率大概只有白天的一半还不到。

原因:YOLOv5s 的夜间泛化能力有限,黑暗环境下的目标对比度低,特征提取难度大。加上摄像头自动增益导致噪声增加,置信度普遍偏低,大量真实目标被 0.25 的置信度阈值过滤掉了。

解决:备选项中效果最好的做法是换成 yolov5m 甚至 yolov5l 权重,大模型对低质量图像的特征表达能力更强。没有算力升级的条件,可以在推理前做一次 CLAHE 对比度增强预处理:

# 对低光照帧做 CLAHE 增强, 提升检测召回 clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8)) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) enhanced = clahe.apply(gray) frame = cv2.cvtColor(enhanced, cv2.COLOR_GRAY2BGR)

5.5 视频推理卡顿,显存溢出

现象:同时开两个测试视频,程序跑了几分钟后提示 CUDA out of memory,画面帧率只有几 FPS。

原因:YOLOv5 默认使用 FP32 精度推理,显存占用较大。DeepSORT 的 ReID 模型也需要额外显存。两个模型叠加,1080p 视频输入会轻松超过 4GB 显存。

解决:给 YOLOv5 开启半精度推理,显存占用减少近一半。同时把输入分辨率限制在 640x640,检测前把长边缩放,短边等比缩放。推理频率也可以人为限制,每 3 帧检测一次,中间帧用跟踪结果直接画框,对计数场景影响不大。

# YOLOv5 开启半精度 model.half() # 限制输入尺寸, 长边不超过 640 def resize_for_inference(img, max_size=640): h, w = img.shape[:2] scale = max_size / max(h, w) if scale < 1: img = cv2.resize(img, (int(w * scale), int(h * scale))) return img

6. 让系统更实用的三个进阶做法:从跑通到能交付

6.1 每隔几帧检测一次,用跟踪结果补全中间帧

检测是整条链路里最耗时的环节,而跟踪的卡尔曼预测几乎不花时间。做法:每 3 帧只做一次 YOLOv5 检测,中间 2 帧直接用 DeepSORT 的预测结果画框。帧率能从 8 FPS 提到 20 FPS 以上,计数精度几乎不受影响,因为计数的依据是跟踪 ID 而不是检测框本身。

6.2 画面上画出计数线和实时统计,方便现场验证

交付时不能只给别人一个终端输出的数字。把计数线、区域边界、每个目标的 ID 和当前计数叠加到视频画面上,直接输出 MP4 或者推流。这样验收方看到的效果和最终数字是一致的,不用对着日志文件想象。涉及多目标时可以用不同色系区分不同 ID,越线瞬间在画面上闪一下提示,验证和展示的效果都更直观。

6.3 把计数结果结构化输出,用 CSV 记录时间序列

相对完善的做法是把每次越线或进出事件的时间戳、目标 ID、方向写入 CSV。后续做时段分析直接读文件,不用重新跑视频。逻辑上在计数事件触发时追加一行记录,字段简单清晰,推荐把这个功能放在开发早期就接入,后续做数据分析顺便加字段比较麻烦。最后一句想用自己的教训来收尾:整套系统的核心链路就三步——检测、跟踪、计数。检测决定上限,跟踪决定稳定性,计数决定业务价值。开发时先按这个顺序打通主链路,再回头优化参数,你会发现大多数问题并不在算法本身,而在你的参数和业务场景匹配得够不够好。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 14:59:43

科研绘图与文献管理工具怎么选?5款AI科研工具实测对比

摘要:本文把沁言学术、Elicit、GraphPad、Paperpal、Zotero 放在同一张桌上,围绕科研绘图与数据呈现这条线,比较各自的定位、适用人群与语种特点,并给出一套按研究阶段挑工具的思路,帮读者判断哪类工具适合放进自己的流程。科研绘图看着只是把数据画成图,真做起来却牵出检索、统…

作者头像 李华
网站建设 2026/10/14 14:59:38

第八代TPU深度拆解:训练与推理实战指南

最近芯片圈子里聊得最多的&#xff0c;除了GPU就是Google的第八代TPU。很多训练团队不是没看过那几张泄露截图&#xff0c;真正的问题是&#xff1a;这些参数到了自己手上到底意味着什么&#xff0c;能跑多大的模型&#xff0c;能不能接进现在的PyTorch/JAX管线。这篇文章就是把…

作者头像 李华
网站建设 2026/10/14 14:58:47

归并排序从原理到Java实现:分治思想、复杂度与稳定性全解析

相信很多朋友在准备面试或做算法练习时&#xff0c;都绕不过归并排序&#xff08;Merge Sort&#xff09;这道坎。它不像冒泡、选择排序那样“一眼就能看懂”&#xff0c;但却是真正值得花时间吃透的算法之一。这篇文章不打算泛泛而谈&#xff0c;而是基于我实际写代码、调 bug…

作者头像 李华
网站建设 2026/10/14 14:58:45

C语言循环结构:原理、优化与工程实践

1. 循环结构在C语言中的核心地位作为一门诞生于1972年的经典编程语言&#xff0c;C语言至今仍保持着旺盛的生命力。根据2023年TIOBE编程语言排行榜显示&#xff0c;C语言以11.8%的市场占有率稳居第二位。这种持久的影响力很大程度上源于其简洁而强大的控制结构设计&#xff0c;…

作者头像 李华
网站建设 2026/10/14 14:57:40

半自动量化工作流:从数据管道到因子挖掘的alpha实践

做量化研究的人&#xff0c;最羡慕的状态往往不是天天盯盘&#xff0c;而是把重复劳动压缩到最低之后&#xff0c;只剩下高杠杆的判断和决策。这个系列我一直在聊“躺平挖 alpha”这件事&#xff0c;标题里的“躺平”当然不是字面意义上的躺着不动&#xff0c;而是用工作流优化…

作者头像 李华
网站建设 2026/10/14 14:57:26

OpenSMILE音频特征提取实战:从原理到Python调用指南

1. 先说清楚OpenSMILE是什么&#xff0c;以及为什么大家都用它做音频相关工作的人&#xff0c;早晚会遇到“提取特征”这回事。不管是语音情感识别、说话人确认、环境音分类&#xff0c;还是一台设备根据音量自动调节播放策略&#xff0c;第一步通常都是把一段声音变成一串能算…

作者头像 李华