前言
最近领导让做一个统计物料的使用情况,要根据某一生产环节做定量的计算。背景是在一个"金箍棒"的生产厂房当中,可能是出于工业制作环节的考虑,有一个步骤是"金箍棒"的原材料被烧制成液体,然后在地下被浇筑成型,最后再利用机器将其从地下拉出来。在这一制作流程的地方有一个摄像头,可以清楚的看到并记录"金箍棒"从地下被拉上来的完整过程。本次要实现的目标是:实时统计总共产出了多少批"金箍棒"。
实现方法是:先制作识别具体物体的模型,然后实现对"金箍棒"的轨迹追踪,最后统计产出了多少批"金箍棒"。为了让大家知道大概做的是一件什么事,先展示一下效果吧。
中心点经过某一高度前效果展示:
中心点经过某一高度后效果展示:(可以看到Count数加1)
具体实现
给出大致需要实现的模块:
- 先对想要识别的拉"金箍棒"的动作(这里使用 挂钩+一部分"金箍棒")做数据标注,生成识别模型;
- 对"金箍棒"的轨迹进行追踪;
- 考虑在"金箍棒"被拉高到某一高度的时候设置一条线,用于统计拉出多少批次。然后,再乘以每批的产出量,就可以定量的计算出具体值;
- 为了达到实时监控并进行统计的目的,在计算资源(边缘设备)有限的情况下,需要通过降低识别过程中每帧图像的像素,来提高模型对视频的推理能力。
- 本次做数据标注的任务是,将拉"金箍棒"的钩子和下面略带一部分的成品作为标注对象,然后生成模型。(使用Labelimg,Label Studio等工具完成,这部分工作是由其他同事完成,具体实现大家可以自行学习相关内容,在这里推荐一篇相关博文吧)。 接下来的工作是对模型转格式(.pt -> .onnx)。转模型格式是在Colab当中实现的,在文章末尾贴出了简单实现代码。
- 对"金箍棒"进行轨迹追踪的实现。
对这部分只做简单介绍:目标轨迹跟踪:为检测到的目标分配ID并持续跟踪;使用算法进行帧间目标匹配;维护目标的轨迹历史和状态信息。轨迹生命周期管理:轨迹最大未更新帧数,超过就移除;移除老化的轨迹。数据维护:存储轨迹历史位置;存储尺寸历史记录;存储当前活跃的轨迹。通过综合考虑目标位置、尺寸和运动趋势,实现了稳定可靠的目标跟踪功能。 - 在"金箍棒"从下往上移动,穿过某一指定高度时,统计"金箍棒"产出的批次。
这里的实现是这样的:先在图像的某个高度设置一条线,然后,统计标注目标从下往上穿越这条线的次数。
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.要在原图像尽可能小的范围内做检测;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}批次")
此代码为本人所编写,商用请联系本人
本文是我最近在工作中遇到问题,进行探索和研究的成果,做本篇文章只为记录以及学习讨论。希望感兴趣的小伙伴进行学习,共同进步!