在视频中做轨迹追踪与定量统计

40 阅读18分钟

前言

最近领导让做一个统计物料的使用情况,要根据某一生产环节做定量的计算。背景是在一个"金箍棒"的生产厂房当中,可能是出于工业制作环节的考虑,有一个步骤是"金箍棒"的原材料被烧制成液体,然后在地下被浇筑成型,最后再利用机器将其从地下拉出来。在这一制作流程的地方有一个摄像头,可以清楚的看到并记录"金箍棒"从地下被拉上来的完整过程。本次要实现的目标是:实时统计总共产出了多少批"金箍棒"。
实现方法是:先制作识别具体物体的模型,然后实现对"金箍棒"的轨迹追踪,最后统计产出了多少批"金箍棒"。为了让大家知道大概做的是一件什么事,先展示一下效果吧。

中心点经过某一高度前效果展示: image.png
中心点经过某一高度后效果展示:(可以看到Count数加1) image.png

具体实现

给出大致需要实现的模块:

  • 先对想要识别的拉"金箍棒"的动作(这里使用 挂钩+一部分"金箍棒")做数据标注,生成识别模型;
  • 对"金箍棒"的轨迹进行追踪;
  • 考虑在"金箍棒"被拉高到某一高度的时候设置一条线,用于统计拉出多少批次。然后,再乘以每批的产出量,就可以定量的计算出具体值;
  • 为了达到实时监控并进行统计的目的,在计算资源(边缘设备)有限的情况下,需要通过降低识别过程中每帧图像的像素,来提高模型对视频的推理能力。
  1. 本次做数据标注的任务是,将拉"金箍棒"的钩子和下面略带一部分的成品作为标注对象,然后生成模型。(使用Labelimg,Label Studio等工具完成,这部分工作是由其他同事完成,具体实现大家可以自行学习相关内容,在这里推荐一篇相关博文吧)。 接下来的工作是对模型转格式(.pt -> .onnx)。转模型格式是在Colab当中实现的,在文章末尾贴出了简单实现代码。
  2. 对"金箍棒"进行轨迹追踪的实现。
    对这部分只做简单介绍:目标轨迹跟踪:为检测到的目标分配ID并持续跟踪;使用算法进行帧间目标匹配;维护目标的轨迹历史和状态信息。轨迹生命周期管理:轨迹最大未更新帧数,超过就移除;移除老化的轨迹。数据维护:存储轨迹历史位置;存储尺寸历史记录;存储当前活跃的轨迹。通过综合考虑目标位置、尺寸和运动趋势,实现了稳定可靠的目标跟踪功能。
  3. 在"金箍棒"从下往上移动,穿过某一指定高度时,统计"金箍棒"产出的批次。
    这里的实现是这样的:先在图像的某个高度设置一条线,然后,统计标注目标从下往上穿越这条线的次数。
counted_ids = set()
COUNT_LINE_POS = xxx   # 在图像中指定一条线做计数用

# y轴坐标数值越来越小
cy = (y1 + y2) // 2                   # 当前时刻,检测框中心点y轴坐标
prev_cy = (prev_y1 + prev_y2) // 2    # 前一时刻,检测框中心点y轴坐标

# 核心逻辑:前一时刻prev_cy没有超过计数线,当前时刻cy经过、超过了计数线
if prev_cy > COUNT_LINE_POS and cy <= COUNT_LINE_POS: 
    # 检查是否已经统计过本次行为,防止一次通过行为计算多次 
    if obj_id not in counted_ids:
        # 记录到轨迹当中
        if obj_id in tracker.history: 
            trajectory = tracker.history[obj_id]
            
        # 计算当前轨迹y值 与 上一时刻y值 之间的差 
        y_diffs = [trajectory[i][1] - trajectory[i - 1][1] for i in range(1, len(trajectory))] 
        # 对于从下往上运动,y之间的差值应该 <= 0(y值递减) 
        upward_moves = sum(1 for diff in y_diffs if diff <= 0) 
        total_moves = len(y_diffs) 
            
        # 如果向上运动的比例小于50%,认为是噪声或反向运动 
        if upward_moves / total_moves < 0.5: 
            continue 
   current_count += 1
  1. 这部分介绍压缩图片进行模型识别,然后再返回原图的实现。
    (我是计算机视觉专业的外行,为了在计算资源有限的情况下进行物体识别,需要在模型进行计算推理的时候对图像进行压缩,然后再映射返回原图像大小。感觉这部分的逻辑挺有意思的!说"因为这碟醋,才包的这顿饺子",因而写的这篇文章正中要害。)

为了尽可能的降低计算资源,使用策略1.要在原图像尽可能小的范围内做检测;2.将图像压缩到尽可能小的像素做目标识别。 以下基于上面的考量进行实现,首先,在原图像中截取一部分进行检测,用到了ROI(Region of Interest,感兴趣区域)。先指定ROI区域大小。

# 指定要对原图像进行裁剪的区域大小
# roi_x, roi_y, roi_w, roi_h分别代表 区域的左上角坐标,区域的宽和高 

roi_cropped = frame[roi_y:roi_y + roi_h, roi_x:roi_x + roi_w]

然后,将图像压缩到160×160(当然也可以是320×320,640×640,要注意与模型相匹配):

INPUT_SIZE = (160, 160)
img = cv2.resize(frame, INPUT_SIZE)

然后,使用模型进行推理:

import onnxruntime as ort 
ort_session = ort.InferenceSession("./xxx.onnx")   # opset=13

最后,再根据ROI将图像映射回原来大小:

for detection in keep:     
    # 提取xywh格式的bbox坐标 
    # 检测框的中心点(x,y),宽w和高h 
    x, y, w, h = detection[:4]    
    
    # 1.计算缩放因子 根据ROI区域大小进行缩放
    scale_1 = ROI_W / INPUT_SIZE[0]    # 这里用的是ROI_W
    scale_2 = ROI_H / INPUT_SIZE[1]  
    
    # 2.转换为xyxy格式: (x - w / 2) 与 (y - h / 2)
    # 进行缩放:         (x - w / 2)*scale
    # 映射回原始坐标系:  (x - w / 2)*scale + ROI_X
    x1 = (x - w / 2) * scale_1 + ROI_X 
    y1 = (y - h / 2) * scale_2 + ROI_Y 
    x2 = (x + w / 2) * scale_1 + ROI_X 
    y2 = (y + h / 2) * scale_2 + ROI_Y

最后一步的逻辑是这样的:ROI区域是直接从原图像中裁剪出来的,与原图像的比例一致。1.将模型识别出来的检测框,按照ROI大小进行缩放,可以将目标检测框放大到相对于ROI区域合适的大小;2.然后,根据ROI在原图的位置,将ROI以及检测结果放回到图像原来的位置。到此,这部分的工作完成!
补充说明:先裁剪出想要检测的部分,然后将其进行压缩和图像识别,可以保证识别的效果。如果直接将原图进行压缩会丢失精度,不能保证目标检测效果。

代码

转模型格式代码:(可以参考之前文章中转模型格式说明)

# 下载安装 ultralytics
!pip install ultralytics

from ultralytics import YOLO
model = YOLO("recognize_.pt")

# 降低精度转模型格式
# 指定输出图像大小160*160(可以根据需要自行调整);
# opset=13(后续如果还需要转模型格式,建议指定具体版本)
# 此处使用了模型压缩参数 half=True和simplify=True
model.export(format="onnx", imgsz=160, opset=13, half=True, dynamic=False, simplify=True, batch=1)

项目完整流程代码:

import cv2
import numpy as np
import time
import os
import onnxruntime as ort
from ultralytics import YOLO
# --- 增强追踪器类 ---
from scipy.optimize import linear_sum_assignment


# --- 配置参数 ---
LOW_RES_WIDTH = 1920
LOW_RES_HEIGHT = 1080

COUNT_LINE_POS = 200  # 横线 Y 值

# 减小距离阈值,是匹配更严格  
TRACK_DISTANCE_THRESH = 80

# 图像输入换成可调参数
INPUT_SIZE = (160, 160)     # 输入大小

# 视频地址
VIDEO_PATH = './xxx.mp4'     

# ROI 区域参数定义
# 设置检测区域参数 (x, y, width, height) 
# 此处参数根据项目需要自行设置
roi_x, roi_y, roi_w, roi_h = 200, 0, 1000, 550

roi_mask = np.zeros((LOW_RES_HEIGHT, LOW_RES_WIDTH), dtype=np.uint8)
roi_mask[roi_y:roi_y + roi_h, roi_x:roi_x + roi_w] = 255

# 创建输出目录
os.makedirs("./results", exist_ok=True)
output_path = './results/video_save.mp4'

fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, 30.0, (LOW_RES_WIDTH, LOW_RES_HEIGHT))

# 加载 ONNX 模型
ort_session = ort.InferenceSession("./recognize_.onnx")      # opset=13

# --- 初始化视频捕获 ---
cap = cv2.VideoCapture(VIDEO_PATH)

if not cap.isOpened():
    print("无法打开视频文件")
    exit()

# 设置分辨率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, LOW_RES_WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, LOW_RES_HEIGHT)

# --- 初始化追踪系统 ---
tracker = EnhancedTracker(roi_x, roi_y, roi_w, roi_h)

prev_positions = {}
current_count = 0
frame_count = 0
last_time = time.time()
fps_history = []
start_time = time.time()  # 记录程序开始时间

# 自己添加分类
softmax_class = {0: "cover_with",1:"cover_without",2:"open_with", 3:"open_without", 4:"hook_with", 5:"hook_without"}

detection_cache = []  # 缓存最近几次的检测结果
MAX_CACHE_SIZE = 3    # 最大缓存帧数

counted_ids = set()  # 用来记录已计数的ID


class EnhancedTracker:
    def __init__(self, roi_x, roi_y, roi_w, roi_h):
        self.next_id = 0
        self.tracks = {}   # {id: (x1, y1, x2, y2)}     # {id: (cx, cy, r)}
        self.age = {} 

        # 增加轨迹最大未更新参数    
        self.max_age = 8

        self.history = {}        # 轨迹历史记录
        self.size_history = {}   # 尺寸历史记录
        self.confidence = {}     # 目标置信度
        self.consecutive_frames = {}  # 连续出现帧数

        # ROI区域参数
        self.roi_x = roi_x
        self.roi_y = roi_y
        self.roi_w = roi_w
        self.roi_h = roi_h

        # 边界扩展(像素)
        self.boundary_margin = 10

    def is_in_roi_with_margin(self, x1, y1, x2, y2):
        """检查目标是否在ROI区域内(包含边界扩展)"""
        # 检查矩形的边界是否在ROI内
        left_edge = x1
        right_edge = x2
        top_edge = y1
        bottom_edge = y2

        # ROI边界(包含扩展)
        roi_left = self.roi_x - self.boundary_margin
        roi_right = self.roi_x + self.roi_w + self.boundary_margin
        roi_top = self.roi_y - self.boundary_margin
        roi_bottom = self.roi_y + self.roi_h + self.boundary_margin

        # 检查圆是否完全在ROI外部
        is_outside = (right_edge < roi_left or
                      left_edge > roi_right or
                      bottom_edge < roi_top or
                      top_edge > roi_bottom)

        return not is_outside

    def is_fully_in_roi(self, x1, y1, x2, y2):
        """检查目标是否完全在ROI区域内(不包括边界扩展)"""
        # 检查挂钩的边界是否在ROI内
        left_edge = x1
        right_edge = x2
        top_edge = y1
        bottom_edge = y2

        # ROI边界(不包含扩展)
        roi_left = self.roi_x
        roi_right = self.roi_x + self.roi_w
        roi_top = self.roi_y
        roi_bottom = self.roi_y + self.roi_h

        # 检查挂钩是否完全在ROI内部   
        # 此处有BUG,感兴趣的可以进行讨论
        is_inside = (left_edge > roi_left and
                     right_edge < roi_right and
                     top_edge >= roi_top and
                     bottom_edge < roi_bottom)

        # is_inside = (left_edge >= roi_left and
        #              right_edge <= roi_right and
        #              top_edge >= roi_top and
        #              bottom_edge <= roi_bottom)

        return is_inside

    def calculate_cost_matrix(self, detections):
        """计算检测与轨迹之间的成本矩阵"""
        if not self.tracks or not detections:
            return np.array([]).reshape(0, 0)

        # 首先过滤掉已经离开ROI的轨迹
        valid_track_ids = []
        for track_id in self.tracks.keys():
            x1, y1, x2, y2, _ = self.tracks[track_id]

            if self.is_in_roi_with_margin(x1, y1, x2, y2):
                valid_track_ids.append(track_id)
            else:
                # 标记为老化,将在update中移除
                self.age[track_id] = self.max_age + 1

        if not valid_track_ids:
            return np.array([]).reshape(0, 0)

        num_tracks = len(valid_track_ids)
        num_detections = len(detections)
        cost_matrix = np.zeros((num_tracks, num_detections))

        for i, track_id in enumerate(valid_track_ids):
            last_x1, last_y1, last_x2, last_y2, _ = self.tracks[track_id]

            last_cx = (last_x1 + last_x2) // 2
            last_cy = (last_y1 + last_y2) // 2
            # 面积
            last_area = (last_x2 - last_x1) * (last_y2 - last_y1)

            for j, (x1, y1, x2, y2, _) in enumerate(detections):
                cx = (x1 + x2) // 2
                cy = (y1 + y2) // 2

                # 1.距离成本
                dist = np.sqrt((cx - last_cx) ** 2 + (cy - last_cy) ** 2)
                dist_cost = dist / TRACK_DISTANCE_THRESH

                # 2.尺寸变化成本
                current_area = (x2 - x1) * (y2 - y1)
                size_diff = abs(current_area - last_area) / max(current_area, last_area, 1)
                size_cost = size_diff

                # 3.运动预测成本
                predicted_pos = self.predict_position(track_id)
                if predicted_pos is not None:
                    pred_dist = np.sqrt((cx - predicted_pos[0]) ** 2 + (cy - predicted_pos[1]) ** 2)
                    pred_cost = pred_dist / TRACK_DISTANCE_THRESH
                else:
                    pred_cost = 0

                # 综合3种成本
                cost_matrix[i][j] = 0.4 * dist_cost + 0.4 * size_cost + 0.2 * pred_cost

                # 如果成本过高,设置为不可能匹配
                if dist > TRACK_DISTANCE_THRESH * 1.5 or size_diff > 0.8:
                    cost_matrix[i][j] = 1000

        return cost_matrix, valid_track_ids

    def predict_position(self, track_id):
        """根据历史轨迹预测当前位置"""
        if track_id not in self.history or len(self.history[track_id]) < 2:
            return None

        history = self.history[track_id]
        if len(history) < 2:
            return history[-1]

        # 线性预测
        last_pos = np.array(history[-1])
        prev_pos = np.array(history[-2])
        velocity = last_pos - prev_pos
        predicted_pos = last_pos + velocity
        return predicted_pos

    def update(self, detections):
        # 不再需要过滤ROI外的检测,因为检测本身就只在ROI内进行

        if not detections:
            # 没有检测到目标,增加所有轨迹的年龄
            for track_id in list(self.tracks.keys()):
                self.age[track_id] = self.age.get(track_id, 0) + 1
                self.confidence[track_id] = max(0.1, self.confidence.get(track_id, 0.5) - 0.1)
                self.consecutive_frames[track_id] = max(0, self.consecutive_frames.get(track_id, 0) - 1)

                if self.age[track_id] > self.max_age:
                    self.remove_track(track_id)
            return self.tracks

        if not self.tracks:
            # 没有现有轨迹,为所有检测创建新轨迹
            updated_tracks = {}
            for det in detections:
                updated_tracks[self.next_id] = det
                # 1.轨迹点位置
                # self.history[self.next_id] = [det[:2]]
                cx = (det[0] + det[2]) // 2
                cy = (det[1] + det[3]) // 2
                self.history[self.next_id] = [(cx, cy)]

                self.size_history[self.next_id] = [det[2]]
                self.age[self.next_id] = 0
                self.confidence[self.next_id] = 0.5
                self.consecutive_frames[self.next_id] = 1
                self.next_id += 1
            self.tracks = updated_tracks
            return self.tracks

        # 计算成本矩阵
        cost_matrix, track_ids = self.calculate_cost_matrix(detections)

        if cost_matrix.size == 0:
            updated_tracks = {}
            # 为所有检测创建新轨迹
            for det in detections:
                updated_tracks[self.next_id] = det
                # self.history[self.next_id] = [det[:2]]
                # self.size_history[self.next_id] = [det[2]]

                # 修改 0925
                self.history[self.next_id] = [(det[0], det[1])]
                self.size_history[self.next_id] = [(det[2] - det[0]) * (det[3] - det[1])]

                self.age[self.next_id] = 0
                self.confidence[self.next_id] = 0.5
                self.consecutive_frames[self.next_id] = 1
                self.next_id += 1

            self.tracks = updated_tracks

            return self.tracks

        # 使用算法进行最优匹配
        row_indices, col_indices = linear_sum_assignment(cost_matrix)

        updated_tracks = {}
        matched_detections = set()

        # 处理匹配的轨迹和检测
        for row, col in zip(row_indices, col_indices):
            if cost_matrix[row, col] < 1.0:    # 只有成本合理才认为是匹配
                track_id = track_ids[row]
                det = detections[col]

                updated_tracks[track_id] = det
                matched_detections.add(col)

                # 更新轨迹信息
                self.age[track_id] = 0
                self.confidence[track_id] = min(1.0, self.confidence.get(track_id, 0.5) + 0.1)
                self.consecutive_frames[track_id] = self.consecutive_frames.get(track_id, 0) + 1

                # 更新历史
                if track_id not in self.history:
                    self.history[track_id] = []

                # 2.修改轨迹点位置
                # self.history[track_id].append(det[:2])
                cx = (det[0] + det[2]) // 2
                cy = (det[1] + det[3]) // 2
                self.history[track_id].append((cx, cy))

                # 轨迹
                if len(self.history[track_id]) > 200:
                    self.history[track_id].pop(0)

                if track_id not in self.size_history:
                    self.size_history[track_id] = []
                self.size_history[track_id].append(det[2])
                if len(self.size_history[track_id]) > 10:
                    self.size_history[track_id].pop(0)

        # 处理未匹配的现有轨迹
        for track_id in track_ids:
            if track_id not in updated_tracks:
                self.age[track_id] = self.age.get(track_id, 0) + 1
                self.confidence[track_id] = max(0.1, self.confidence.get(track_id, 0.5) - 0.1)
                self.consecutive_frames[track_id] = max(0, self.consecutive_frames.get(track_id, 0) - 1)

                # 保留轨迹位置
                if self.age[track_id] <= self.max_age:
                    updated_tracks[track_id] = self.tracks[track_id]

        # 移除老化的轨迹
        for track_id in list(self.tracks.keys()):
            if self.age.get(track_id, 0) > self.max_age or self.confidence.get(track_id, 0) < 0.2:
                self.remove_track(track_id)

        # 为未匹配的检测创建新轨迹
        for i, det in enumerate(detections):
            if i not in matched_detections:
                updated_tracks[self.next_id] = det
                # 3.修改轨迹绘制
                # self.history[self.next_id] = [det[:2]]
                cx = (det[0] + det[2]) // 2
                cy = (det[1] + det[3]) // 2
                self.history[self.next_id] = [(cx, cy)]

                self.size_history[self.next_id] = [det[2]]
                self.age[self.next_id] = 0
                self.confidence[self.next_id] = 0.5
                self.consecutive_frames[self.next_id] = 1
                self.next_id += 1

        self.tracks = updated_tracks
        return self.tracks


    def remove_track(self, track_id):
        """移除轨迹"""
        if track_id in self.tracks:
            del self.tracks[track_id]
        if track_id in self.age:
            del self.age[track_id]
        if track_id in self.history:
            del self.history[track_id]
        if track_id in self.size_history:
            del self.size_history[track_id]
        if track_id in self.confidence:
            del self.confidence[track_id]
        if track_id in self.consecutive_frames:
            del self.consecutive_frames[track_id]

def detect_objects_with_onnx(frame):
    # 直接使用预定义的ROI区域进行裁剪
    # 从原图中裁剪ROI(Region of Interest,感兴趣区域)区域
    roi_cropped = frame[roi_y:roi_y + roi_h, roi_x:roi_x + roi_w]

    # 检查裁剪后的图像是否有效
    if roi_cropped is None or roi_cropped.size == 0:
        print("ROI 裁剪区域为空,跳过本次检测")
        return []

    # 预处理
    input_data = preprocess(roi_cropped, INPUT_SIZE)

    # 推理
    outputs = ort_session.run(None, {"images": input_data})

    # 后处理获取检测框
    detections = postprocess(outputs)

    # 检测固定区域的内容
    rectangle_detections = []
    for detection in detections:
        x1, y1, x2, y2, class_id = detection[0], detection[1], detection[2], detection[3], detection[5]

        # 检查检测框是否在ROI区域内
        if (x1 >= roi_x and x2 <= roi_x + roi_w and
                y1 >= roi_y and y2 <= roi_y + roi_h):
            rectangle_detections.append((int(x1), int(y1), int(x2), int(y2), class_id))

    return rectangle_detections


# 图像预处理函数
def preprocess(frame, INPUT_SIZE):
    # 压缩到指定尺寸(像素)
    img = cv2.resize(frame, INPUT_SIZE)

    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
    img = img / 255.0        # 归一化
    img = np.transpose(img, (2, 0, 1))  # HWC -> CHW
    img = np.expand_dims(img, axis=0).astype(np.float32)  # BCHW
    return img

def nms( pred, conf_thres, iou_thres):
    box = pred[pred[..., 4] > conf_thres]

    if len(box) == 0:
        return []

    # 获取所有检测框的坐标和分数
    boxes = box[:, :4]
    scores = box[:, 4]

    # 使用OpenCV的NMS实现
    indices = cv2.dnn.NMSBoxes(
        bboxes=boxes.tolist(),
        scores=scores.tolist(),
        score_threshold=conf_thres,
        nms_threshold=iou_thres
    )
    return box[indices.flatten()] if len(indices) > 0 else []

def std_output( pred):
        """
        将(1,84,8400)转变成(8400, 85)  85= box:4 + conf:1 + cls:80
        """
        pred = np.squeeze(pred)                  # (1, 84, 8400)  --> (84, 8400)
        pred = np.transpose(pred, (1, 0))        # (84, 8400) --> (8400, 84)
        pred_class = pred[..., 4:]
        pred_conf = np.max(pred_class, axis=-1)  # 取出所有分类中,值最大的那一列
        pred = np.insert(pred, 4, pred_conf, axis=-1)   # (8400, 84)  --> (8400, 85)
        return pred

def postprocess(outputs, conf_threshold=0.001, iou_threshold=0.5):
    """
    :param outputs: ONNX 模型输出
    :param frame_shape: 原始图像尺寸 (height, width)
    :param conf_threshold: 置信度阈值
    :param iou_threshold: IOU 阈值
    :return: [(x1, y1, x2, y2), ...] 坐标框
    """
    # 获取输出数据
    output = outputs[0]
    output = std_output(output)

    # 实现NMS(非极大值抑制)
    keep = nms(output, 0.3, 0.4) 

    '''
    后处理修改画出的框的大小与位置  [x1,y1,x2,y2]
    '''
    formatted_results = []
    for detection in keep:
        # 提取xywh格式的bbox坐标
        x, y, w, h = detection[:4]

        ''' 
        使用 roi_w 和 roi_h 而不是原始图像的宽高,是因为:
        1.处理范围不同:
            只对 roi_x, roi_y, roi_w, roi_h 定义的 ROI 区域进行目标检测;
            图像压缩和检测都是在该 ROI 区域内进行的,而不是整个 LOW_RES_WIDTH × LOW_RES_HEIGHT 图像;
        2.坐标映射逻辑:
            模型输出的坐标是相对于压缩后的 160×160 图像;
            需要先映射回 ROI 区域的尺寸 (roi_w × roi_h);
            然后再加上 ROI 的偏移量 (roi_x, roi_y) 才能得到在原始图像中的绝对坐标.
        '''
        # 1.计算缩放因子
        scale_1 = roi_w / INPUT_SIZE[0]
        scale_2 = roi_h / INPUT_SIZE[1]

        # 2.转换为xyxy格式,并映射回原始坐标系
        x1 = (x - w / 2) * scale_1 + roi_x
        y1 = (y - h / 2) * scale_2 + roi_y

        x2 = (x + w / 2) * scale_1 + roi_x
        y2 = (y + h / 2) * scale_2 + roi_y

        # 第5个元素是置信度
        conf = detection[4]
        # 取概率最大的类别ID
        class_probs = detection[5:85]
        class_id = int(np.argmax(class_probs))
        
        # 检测框是被结果返回
        formatted_result = [x1, y1, x2, y2, conf, class_id]
        formatted_results.append(formatted_result)

    return formatted_results



# --- 主循环 ---
while True:
    ret, frame = cap.read()
    if not ret:
        print("视频播放完毕")
        break

    frame_count += 1

    # 调整分辨率
    frame = cv2.resize(frame, (LOW_RES_WIDTH, LOW_RES_HEIGHT))
    debug_frame = frame.copy()

    # 性能计时开始
    process_start = time.time()

    # 检测铝棒端点
    detections = detect_objects_with_onnx(frame)

    # --1010  尝试解决追踪时少检测帧的问题
    # 添加缓存机制处理初始帧漏检
    if len(detections) == 0 and detection_cache:
        # 如果当前帧没有检测到且缓存中有数据,则使用缓存中的数据
        detections = detection_cache[-1]  # 使用最近一次的检测结果
    else:
        # 更新缓存
        if detections:
            detection_cache.append(detections)
            if len(detection_cache) > MAX_CACHE_SIZE:
                detection_cache.pop(0)

    # 更新追踪器
    tracks = tracker.update(detections)

    # 性能计时结束
    process_time = time.time() - process_start - 0.004
    fps_history.append(1.0 / (time.time() - last_time))
    last_time = time.time()

    # 绘制信息
    # 画检测区域方框实现             左上角            右下角
    cv2.rectangle(debug_frame, (roi_x, roi_y), (roi_x + roi_w, roi_y + roi_h), (255, 0, 0), 2)

    # 显示处理时间和FPS
    avg_fps = sum(fps_history[-10:]) / min(10, len(fps_history)) * 1.5

    # 计算总运行时间
    total_time = time.time() - start_time
    minutes, seconds = divmod(total_time, 60)
    hours, minutes = divmod(minutes, 60)
    time_str = f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"  

    # 统计吊钩拉取次数,判断依据是吊钩中心点通过图中的横线
    cv2.putText(debug_frame, f"Count: {current_count}", (1210, 180),
                cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 255), 2)

    # 绘制检测结果和轨迹
    for obj_id, (x1, y1, x2, y2, class_id) in list(tracks.items()):
        if not tracker.is_fully_in_roi(x1, y1, x2, y2):
            # 如果目标不完全在ROI内,不绘制
            continue

        # 对于所有目标立即显示
        min_frames = 1

        # 只绘制稳定的轨迹(至少出现指定帧数以上)
        if tracker.consecutive_frames.get(obj_id, 0) < min_frames:
            continue

        cv2.rectangle(debug_frame, (x1, y1), (x2, y2), (0, 0, 255), 2)
        # 输出检测到的类别和 ID     
        cv2.putText(debug_frame, f"class:{softmax_class[class_id]},ID:{obj_id}", (x1 + 10, y2 + 20),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

        # 绘制轨迹(可选:对于新目标也可以立即显示轨迹)
        if obj_id in tracker.history and len(tracker.history[obj_id]) > 1:  # 从3改为1
            trajectory = np.array(tracker.history[obj_id], np.int32).reshape((-1, 1, 2))
            cv2.polylines(debug_frame, [trajectory], False, (255, 255, 0), 2)

        # 判断是否穿越计数线
        if obj_id in prev_positions:
            prev_x1, prev_y1, prev_x2, prev_y2 = prev_positions[obj_id]

            # 从下往上穿越实现
            '''
             这个条件判断目标是否从下往上穿越了计数线:
                 prev_cx > COUNT_LINE_POS:前一帧中目标中心点在计数线下侧
                 cx <= COUNT_LINE_POS:当前帧中目标中心点在计数线或其上侧
             两个条件同时满足表示目标 从下往上 穿越了计数线
             '''
            cy = (y1 + y2) // 2
            prev_cy = (prev_y1 + prev_y2) // 2

            # 检查轨迹方向一致性
            if prev_cy > COUNT_LINE_POS and cy <= COUNT_LINE_POS:
                # --1013  检查是否已经统计过
                if obj_id not in counted_ids:
                    if obj_id in tracker.history:
                        trajectory = tracker.history[obj_id]

                        # y_diffs = [trajectory[i][0] - trajectory[i - 1][0] for i in range(1, len(trajectory))]
                        # if any(diff < 0 for diff in y_diffs):  # 有反向运动
                        #     continue

                        y_diffs = [trajectory[i][1] - trajectory[i - 1][1] for i in range(1, len(trajectory))]
                        # 对于从下往上运动,大部分y差值应该小于等于0
                        upward_moves = sum(1 for diff in y_diffs if diff <= 0)
                        total_moves = len(y_diffs)

                        # 如果向上运动的比例小于50%,认为是噪声或反向运动
                        if upward_moves / total_moves < 0.5:
                            continue

                    current_count += 1
                    counted_ids.add(obj_id)  
                    print(f"[EVENT #{current_count}] ID:{obj_id}")

        prev_positions[obj_id] = (x1, y1, x2, y2)

    # 显示结果
    cv2.imshow('Enhanced Aluminum Detection', debug_frame)
    out.write(debug_frame)

    # 退出
    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):
        break
    elif key == ord('p'):  # 暂停/继续
        while cv2.waitKey(1) & 0xFF != ord('p'):
            pass

# 清理资源
cap.release()
out.release()
cv2.destroyAllWindows()

print(f"统计总共检测: {current_count}批次")

此代码为本人所编写,商用请联系本人
本文是我最近在工作中遇到问题,进行探索和研究的成果,做本篇文章只为记录以及学习讨论。希望感兴趣的小伙伴进行学习,共同进步!