feat(pipeline): 添加逆行处理器

- 新增 DrawDirectionProcessor 用于绘制车辆行驶方向和轨迹
- 新增 DrawGraffitiProcessor 用于绘制热力图和网格方向箭头
- 新增 DrawObjectBoxProcessor 用于绘制检测框、ID、标签和置信度
- 新增 GraffitiVisualizer 用于涂鸦可视化处理
- 新增 GraffitiProcessor 用于计算车辆轨迹并更新车道涂鸦
This commit is contained in:
zimoyin
2026-01-10 09:41:18 +08:00
parent 963e858341
commit 23555e0cc9
12 changed files with 1454 additions and 0 deletions
+263
View File
@@ -0,0 +1,263 @@
import numpy as np
import cv2
import math
from collections import deque, defaultdict
from yolo_gs.pipeline.base_processor import BaseProcessor
from yolo_gs.pipeline.pipeline_data import PipelineData
class GraffitiProcessor(BaseProcessor):
"""涂鸦计算处理器:计算车辆轨迹并更新车道涂鸦"""
def __init__(self, name: str = "涂鸦计算处理器"):
super().__init__(name)
# 可调参数
self.MAX_HEAT = 10.0
self.INC_PER_PASS = 1.0
self.DEC_PER_OPPOSITE = 1.0
self.MIN_MOVE_DIST = 6.0
self.MIN_CUM_DIST = 20.0
self.HISTORY_LEN = 16
self.HEAT_DECAY_PER_FRAME = 0.002
self.VEHICLE_CLASSES = {2, 3, 5, 7}
self.FULL_DECAY_EVERY = 30 # 每30帧进行一次全图衰减
# 初始化数据结构(在process中根据帧尺寸初始化)
self.graffiti = None
self.frame_shape = None
self.track_hist = defaultdict(lambda: deque(maxlen=self.HISTORY_LEN))
self.track_cum_dist = defaultdict(float)
self.frame_idx = 0
def init_graffiti(self, h, w):
"""初始化涂鸦数据结构"""
if self.graffiti is None or self.frame_shape != (h, w):
self.graffiti = GraffitiLane(w, h)
self.frame_shape = (h, w)
def process(self, data: PipelineData) -> PipelineData:
"""处理每一帧,更新涂鸦数据"""
if data.current_result is None or data.frame is None:
return data
frame = data.frame
h, w = frame.shape[:2]
self.init_graffiti(h, w)
self.frame_idx = data.frame_idx
# 周期性对整图做完整衰减
if self.frame_idx % self.FULL_DECAY_EVERY == 0:
self.graffiti.full_decay(self.frame_idx)
# 获取检测框
boxes = getattr(data.current_result, "boxes", None)
if boxes is None:
return data
# 处理每个检测框
for box in boxes:
try:
# 解析检测框
xyxy = getattr(box, "xyxy", None)
if xyxy is None:
continue
x1, y1, x2, y2 = map(float, xyxy[0].tolist())
cls = int(getattr(box, "cls", -1))
track_id = int(getattr(box, "id", -1))
# 仅处理车辆类
if cls not in self.VEHICLE_CLASSES:
continue
# 更新轨迹和历史
self._update_track(track_id, x1, y1, x2, y2)
# 如果累计距离达到阈值,更新涂鸦
if (self.track_cum_dist[track_id] >= self.MIN_CUM_DIST and
len(self.track_hist[track_id]) >= 2):
self._update_graffiti(track_id, x2 - x1)
self.track_cum_dist[track_id] = 0.0
except Exception as e:
continue
# 清理长时间不活跃的track
self._cleanup_old_tracks()
# 存储涂鸦数据到PipelineDatadeque转换为list以便序列化)
data.put_data("graffiti", "heat_map", self.graffiti.heat)
data.put_data("graffiti", "dir_x_map", self.graffiti.dir_x)
data.put_data("graffiti", "dir_y_map", self.graffiti.dir_y)
data.put_data("graffiti", "track_hist", {
track_id: list(hist) for track_id, hist in self.track_hist.items()
})
data.put_data("graffiti", "track_cum_dist", dict(self.track_cum_dist))
return data
def _update_track(self, track_id, x1, y1, x2, y2):
"""更新车辆轨迹信息"""
cx, cy = (x1 + x2) / 2.0, (y1 + y2) / 2.0
prev_centers = self.track_hist[track_id]
if len(prev_centers) > 0:
last_cx, last_cy = prev_centers[-1]
move_dist = math.hypot(cx - last_cx, cy - last_cy)
else:
move_dist = 0.0
if len(prev_centers) > 0:
self.track_cum_dist[track_id] += move_dist
else:
self.track_cum_dist[track_id] = 0.0
if move_dist >= self.MIN_MOVE_DIST:
prev_centers.append((cx, cy))
elif len(prev_centers) == 0:
prev_centers.append((cx, cy))
def _update_graffiti(self, track_id, vehicle_width):
"""根据车辆轨迹更新涂鸦"""
hist = self.track_hist[track_id]
if len(hist) < 2:
return
cp_prev = hist[-2]
cp_cur = hist[-1]
# 创建多边形掩码
poly = self._polygon_from_segment(cp_prev, cp_cur, max(8.0, vehicle_width))
if poly is None:
return
# 计算ROI掩码
bbox_roi, mask_roi = self._polygon_mask_roi_from_pts(poly, self.frame_shape)
if mask_roi.size == 0:
return
# 计算方向向量
dir_vec = self._normalize_vec((cp_cur[0] - cp_prev[0], cp_cur[1] - cp_prev[1]))
dir_vec_scaled = dir_vec.astype(np.float32) * 1.0
# 更新涂鸦
self.graffiti.apply_paint_roi(
bbox_roi, mask_roi, dir_vec_scaled,
inc=self.INC_PER_PASS,
current_frame=self.frame_idx
)
def _cleanup_old_tracks(self):
"""清理长时间不活跃的track,防止内存泄漏"""
# 可以根据需要实现,比如超过一定帧数没有更新就清理
# 这里先不实现,根据实际情况调整
pass
def _polygon_from_segment(self, center_prev, center_cur, veh_width_px):
"""从两点构造矩形带"""
x0, y0 = center_prev
x1, y1 = center_cur
vx = x1 - x0
vy = y1 - y0
dist = math.hypot(vx, vy)
if dist == 0:
return None
ux, uy = vx / dist, vy / dist
px, py = -uy, ux
half_w = veh_width_px / 2.0
p0 = (int(round(x0 + px * half_w)), int(round(y0 + py * half_w)))
p1 = (int(round(x0 - px * half_w)), int(round(y0 - py * half_w)))
p2 = (int(round(x1 - px * half_w)), int(round(y1 - py * half_w)))
p3 = (int(round(x1 + px * half_w)), int(round(y1 + py * half_w)))
return np.array([p0, p1, p2, p3], dtype=np.int32)
def _polygon_mask_roi_from_pts(self, pts, img_shape):
"""在多边形最小包围矩形内生成局部掩码"""
if pts is None or len(pts) == 0:
return (0, 0, 0, 0), np.zeros((0, 0), dtype=np.uint8)
x, y, w, h = cv2.boundingRect(pts)
x2 = min(x + w, img_shape[1])
y2 = min(y + h, img_shape[0])
w = x2 - x
h = y2 - y
if w <= 0 or h <= 0:
return (0, 0, 0, 0), np.zeros((0, 0), dtype=np.uint8)
pts_roi = pts.copy()
pts_roi[:, 0] -= x
pts_roi[:, 1] -= y
mask = np.zeros((h, w), dtype=np.uint8)
cv2.fillPoly(mask, [pts_roi], 1)
return (x, y, w, h), mask
def _normalize_vec(self, v):
"""归一化向量"""
v = np.array(v, dtype=np.float32)
norm = np.linalg.norm(v)
if norm == 0:
return np.array([0.0, 0.0], dtype=np.float32)
return v / norm
class GraffitiLane:
"""涂鸦车道数据结构"""
def __init__(self, frame_w, frame_h):
self.w = frame_w
self.h = frame_h
self.heat = np.zeros((self.h, self.w), dtype=np.float32)
self.dir_x = np.zeros((self.h, self.w), dtype=np.float32)
self.dir_y = np.zeros((self.h, self.w), dtype=np.float32)
self.last_decay_frame = 0
def _decay_factor(self, frames):
if frames <= 0:
return 1.0
return (1.0 - 0.002) ** frames # HEAT_DECAY_PER_FRAME = 0.002
def lazy_decay_roi(self, bbox, current_frame):
x, y, w, h = bbox
if w <= 0 or h <= 0:
return
frames = current_frame - self.last_decay_frame
if frames <= 0:
return
factor = self._decay_factor(frames)
self.heat[y:y + h, x:x + w] *= factor
self.dir_x[y:y + h, x:x + w] *= factor
self.dir_y[y:y + h, x:x + w] *= factor
def full_decay(self, current_frame):
"""在整图上强制应用pending衰减(用于周期性清理)"""
frames = current_frame - self.last_decay_frame
if frames <= 0:
return
factor = self._decay_factor(frames)
if factor == 1.0:
self.last_decay_frame = current_frame
return
# 对整图应用因子
self.heat *= factor
self.dir_x *= factor
self.dir_y *= factor
self.last_decay_frame = current_frame
def apply_paint_roi(self, bbox, mask_roi, direction_vector, inc=1.0, current_frame=0):
self.lazy_decay_roi(bbox, current_frame)
x, y, w, h = bbox
add = inc * mask_roi.astype(np.float32)
self.heat[y:y + h, x:x + w] = np.minimum(10.0, self.heat[y:y + h, x:x + w] + add) # MAX_HEAT = 10.0
dx, dy = direction_vector
self.dir_x[y:y + h, x:x + w] += dx * add
self.dir_y[y:y + h, x:x + w] += dy * add