# ============================================================ # imm_filter.py # Модуль 5: IMM-фильтр (Interacting Multiple Model) # # Три модели движения цели: # 1) CV — Constant Velocity (прямолинейное) # 2) CA — Constant Acceleration (разгон / торможение) # 3) CT — Coordinated Turn (вираж) # # IMM автоматически определяет, какую модель использовать, # через вероятности Маркова. Это критично для маневрирующей цели, # т.к. обычный Kalman (CV) запаздывает при манёврах. # ============================================================ import numpy as np from config import * from config_intercept import * from helpers import clamp, box_center, box_wh, clip_box class _KalmanCV: """ Constant Velocity модель. Состояние: [cx, cy, w, h, vx, vy, vw, vh] (8D) """ def __init__(self, q_pos, q_vel): self.dim_x = 8 self.dim_z = 4 self.x = np.zeros((8, 1), dtype=np.float64) self.P = np.eye(8, dtype=np.float64) * 50.0 self.H = np.zeros((4, 8), dtype=np.float64) for i in range(4): self.H[i, i] = 1.0 self.R = np.diag([IMM_R_POS, IMM_R_POS, IMM_R_SIZE, IMM_R_SIZE]).astype(np.float64) self.q_pos = float(q_pos) self.q_vel = float(q_vel) def F(self, dt): F = np.eye(8, dtype=np.float64) F[0, 4] = dt F[1, 5] = dt F[2, 6] = dt F[3, 7] = dt return F def Q(self, dt): q = np.zeros((8, 8), dtype=np.float64) dt2 = dt * dt q[0, 0] = self.q_pos * dt2 q[1, 1] = self.q_pos * dt2 q[2, 2] = self.q_pos * dt2 q[3, 3] = self.q_pos * dt2 q[4, 4] = self.q_vel * dt q[5, 5] = self.q_vel * dt q[6, 6] = self.q_vel * dt q[7, 7] = self.q_vel * dt return q def predict(self, dt): F = self.F(dt) self.x = F @ self.x self.P = F @ self.P @ F.T + self.Q(dt) def update(self, z): z = np.array(z, dtype=np.float64).reshape(4, 1) y = z - self.H @ self.x S = self.H @ self.P @ self.H.T + self.R K = self.P @ self.H.T @ np.linalg.inv(S) self.x = self.x + K @ y I = np.eye(8, dtype=np.float64) self.P = (I - K @ self.H) @ self.P return y, S def likelihood(self, z): """Правдоподобие измерения (для IMM mixing).""" z = np.array(z, dtype=np.float64).reshape(4, 1) y = z - self.H @ self.x S = self.H @ self.P @ self.H.T + self.R try: S_inv = np.linalg.inv(S) det_S = max(np.linalg.det(S), 1e-300) n = self.dim_z exp_val = float(-0.5 * (y.T @ S_inv @ y).item()) exp_val = max(exp_val, -500.0) # предотвращение underflow norm = (2.0 * np.pi) ** (-n / 2.0) * det_S ** (-0.5) return max(norm * np.exp(exp_val), 1e-300) except np.linalg.LinAlgError: return 1e-300 class _KalmanCA: """ Constant Acceleration модель. Состояние: [cx, cy, w, h, vx, vy, ax, ay, vw, vh] (10D) Измерение: [cx, cy, w, h] """ def __init__(self, q_pos, q_vel, q_acc): self.dim_x = 10 self.dim_z = 4 self.x = np.zeros((10, 1), dtype=np.float64) self.P = np.eye(10, dtype=np.float64) * 50.0 self.H = np.zeros((4, 10), dtype=np.float64) self.H[0, 0] = 1.0 # cx self.H[1, 1] = 1.0 # cy self.H[2, 2] = 1.0 # w self.H[3, 3] = 1.0 # h self.R = np.diag([IMM_R_POS, IMM_R_POS, IMM_R_SIZE, IMM_R_SIZE]).astype(np.float64) self.q_pos = float(q_pos) self.q_vel = float(q_vel) self.q_acc = float(q_acc) def F(self, dt): F = np.eye(10, dtype=np.float64) dt2 = 0.5 * dt * dt F[0, 4] = dt # cx += vx*dt F[0, 6] = dt2 # cx += 0.5*ax*dt² F[1, 5] = dt # cy += vy*dt F[1, 7] = dt2 # cy += 0.5*ay*dt² F[4, 6] = dt # vx += ax*dt F[5, 7] = dt # vy += ay*dt F[2, 8] = dt # w += vw*dt F[3, 9] = dt # h += vh*dt return F def Q(self, dt): q = np.zeros((10, 10), dtype=np.float64) dt2 = dt * dt q[0, 0] = self.q_pos * dt2 q[1, 1] = self.q_pos * dt2 q[2, 2] = self.q_pos * dt2 q[3, 3] = self.q_pos * dt2 q[4, 4] = self.q_vel * dt q[5, 5] = self.q_vel * dt q[6, 6] = self.q_acc * dt q[7, 7] = self.q_acc * dt q[8, 8] = self.q_vel * dt q[9, 9] = self.q_vel * dt return q def predict(self, dt): F = self.F(dt) self.x = F @ self.x self.P = F @ self.P @ F.T + self.Q(dt) def update(self, z): z = np.array(z, dtype=np.float64).reshape(4, 1) y = z - self.H @ self.x S = self.H @ self.P @ self.H.T + self.R K = self.P @ self.H.T @ np.linalg.inv(S) self.x = self.x + K @ y I = np.eye(10, dtype=np.float64) self.P = (I - K @ self.H) @ self.P return y, S def likelihood(self, z): z = np.array(z, dtype=np.float64).reshape(4, 1) y = z - self.H @ self.x S = self.H @ self.P @ self.H.T + self.R try: S_inv = np.linalg.inv(S) det_S = max(np.linalg.det(S), 1e-300) n = self.dim_z exp_val = float(-0.5 * (y.T @ S_inv @ y).item()) exp_val = max(exp_val, -500.0) norm = (2.0 * np.pi) ** (-n / 2.0) * det_S ** (-0.5) return max(norm * np.exp(exp_val), 1e-300) except np.linalg.LinAlgError: return 1e-300 class _KalmanCT: """ Coordinated Turn модель. Состояние: [cx, cy, w, h, vx, vy, omega, vw, vh] (9D) omega = угловая скорость виража (рад/с) """ def __init__(self, q_pos, q_vel, q_omega): self.dim_x = 9 self.dim_z = 4 self.x = np.zeros((9, 1), dtype=np.float64) self.P = np.eye(9, dtype=np.float64) * 50.0 self.H = np.zeros((4, 9), dtype=np.float64) self.H[0, 0] = 1.0 self.H[1, 1] = 1.0 self.H[2, 2] = 1.0 self.H[3, 3] = 1.0 self.R = np.diag([IMM_R_POS, IMM_R_POS, IMM_R_SIZE, IMM_R_SIZE]).astype(np.float64) self.q_pos = float(q_pos) self.q_vel = float(q_vel) self.q_omega = float(q_omega) def F(self, dt): omega = float(self.x[6, 0]) F = np.eye(9, dtype=np.float64) if abs(omega) < 1e-4: # При нулевой omega — линейная модель F[0, 4] = dt F[1, 5] = dt else: # Нелинейная CT-модель so = np.sin(omega * dt) co = np.cos(omega * dt) F[0, 4] = so / omega F[0, 5] = -(1.0 - co) / omega F[1, 4] = (1.0 - co) / omega F[1, 5] = so / omega F[4, 4] = co F[4, 5] = -so F[5, 4] = so F[5, 5] = co F[2, 7] = dt F[3, 8] = dt return F def Q(self, dt): q = np.zeros((9, 9), dtype=np.float64) dt2 = dt * dt q[0, 0] = self.q_pos * dt2 q[1, 1] = self.q_pos * dt2 q[2, 2] = self.q_pos * dt2 q[3, 3] = self.q_pos * dt2 q[4, 4] = self.q_vel * dt q[5, 5] = self.q_vel * dt q[6, 6] = self.q_omega * dt q[7, 7] = self.q_vel * dt q[8, 8] = self.q_vel * dt return q def predict(self, dt): F = self.F(dt) self.x = F @ self.x self.P = F @ self.P @ F.T + self.Q(dt) def update(self, z): z = np.array(z, dtype=np.float64).reshape(4, 1) y = z - self.H @ self.x S = self.H @ self.P @ self.H.T + self.R K = self.P @ self.H.T @ np.linalg.inv(S) self.x = self.x + K @ y I = np.eye(9, dtype=np.float64) self.P = (I - K @ self.H) @ self.P return y, S def likelihood(self, z): z = np.array(z, dtype=np.float64).reshape(4, 1) y = z - self.H @ self.x S = self.H @ self.P @ self.H.T + self.R try: S_inv = np.linalg.inv(S) det_S = max(np.linalg.det(S), 1e-300) n = self.dim_z exp_val = float(-0.5 * (y.T @ S_inv @ y).item()) exp_val = max(exp_val, -500.0) norm = (2.0 * np.pi) ** (-n / 2.0) * det_S ** (-0.5) return max(norm * np.exp(exp_val), 1e-300) except np.linalg.LinAlgError: return 1e-300 # ───────────────────────────────────────────────────────────── # IMM FILTER # ───────────────────────────────────────────────────────────── class IMMFilter: """ Interacting Multiple Model фильтр. Управляет тремя фильтрами Калмана с разными моделями движения и переключается между ними через байесовское взвешивание. Интерфейс совместим с Kalman8D из trackers.py: - init_from_box(box) - predict(dt) - update(z) где z = [cx, cy, w, h] - to_box() → [x1, y1, x2, y2] - uncertainty() → float """ def __init__(self): self.enabled = bool(IMM_ENABLE) # Три модели self.models = [ _KalmanCV(IMM_CV_Q_POS, IMM_CV_Q_VEL), _KalmanCA(IMM_CA_Q_POS, IMM_CA_Q_VEL, IMM_CA_Q_ACC), _KalmanCT(IMM_CT_Q_POS, IMM_CT_Q_VEL, IMM_CT_Q_OMEGA), ] self.n_models = len(self.models) self.model_names = ["CV", "CA", "CT"] # Вероятности моделей self.mu = np.array(IMM_INIT_PROBS, dtype=np.float64) self.mu /= self.mu.sum() # Матрица переходов Маркова self.TPM = np.array(IMM_TRANSITION_MATRIX, dtype=np.float64) # Нормализация строк for i in range(self.n_models): self.TPM[i] /= max(self.TPM[i].sum(), 1e-12) self.initialized = False # Кэш для API-совместимости с Kalman8D self._merged_x = np.zeros(8, dtype=np.float64) # [cx, cy, w, h, vx, vy, vw, vh] self._merged_P = np.eye(8, dtype=np.float64) * 50.0 def init_from_box(self, box): """Инициализация из bounding box.""" cx, cy = box_center(box) w, h = box_wh(box) for m in self.models: m.x[:] = 0 m.x[0, 0] = float(cx) m.x[1, 0] = float(cy) m.x[2, 0] = float(w) m.x[3, 0] = float(h) m.P = np.eye(m.dim_x, dtype=np.float64) * 50.0 self.mu = np.array(IMM_INIT_PROBS, dtype=np.float64) self.mu /= self.mu.sum() self.initialized = True self._update_merged() def predict(self, dt, q_scale=1.0): """ IMM Predict: interaction → predict каждой модели. Returns: np.array [cx, cy, w, h] — merged prediction """ if not self.initialized: return np.zeros(4, dtype=np.float32) dt = float(max(1e-3, dt)) # ─── 1. Interaction (mixing) ───────────────────────────── # Вычисляем mixing probabilities c_bar = self.TPM.T @ self.mu # predicted model probs c_bar = np.maximum(c_bar, 1e-12) mixing_probs = np.zeros((self.n_models, self.n_models), dtype=np.float64) for j in range(self.n_models): for i in range(self.n_models): mixing_probs[i, j] = self.TPM[i, j] * self.mu[i] / c_bar[j] # Mixed states for each model for j in range(self.n_models): mj = self.models[j] dim = mj.dim_x # Mixed state x_mixed = np.zeros((dim, 1), dtype=np.float64) for i in range(self.n_models): mi = self.models[i] # Проецируем состояние mi на размерность mj xi_proj = self._project_state(mi.x, mi.dim_x, dim) x_mixed += mixing_probs[i, j] * xi_proj # Mixed covariance P_mixed = np.zeros((dim, dim), dtype=np.float64) for i in range(self.n_models): mi = self.models[i] xi_proj = self._project_state(mi.x, mi.dim_x, dim) Pi_proj = self._project_cov(mi.P, mi.dim_x, dim) diff = xi_proj - x_mixed P_mixed += mixing_probs[i, j] * (Pi_proj + diff @ diff.T) mj.x = x_mixed mj.P = P_mixed # ─── 2. Predict each model ────────────────────────────── for m in self.models: m.predict(dt) self._update_merged() return self._merged_x[:4].astype(np.float32) def update(self, z): """ IMM Update: update каждой модели → пересчёт вероятностей. Args: z: [cx, cy, w, h] """ if not self.initialized: return z = np.array(z, dtype=np.float64).reshape(4) # ─── 1. Likelihood каждой модели ───────────────────────── likelihoods = np.array( [m.likelihood(z) for m in self.models], dtype=np.float64 ) # ─── 2. Update каждой модели ───────────────────────────── for m in self.models: m.update(z) # ─── 3. Обновление вероятностей моделей ────────────────── c_bar = self.TPM.T @ self.mu c_bar = np.maximum(c_bar, 1e-12) self.mu = c_bar * likelihoods total = self.mu.sum() if total > 1e-300: self.mu /= total else: self.mu = np.array(IMM_INIT_PROBS, dtype=np.float64) self.mu /= self.mu.sum() self._update_merged() def to_box(self): """Возвращает merged bounding box [x1, y1, x2, y2].""" cx = float(self._merged_x[0]) cy = float(self._merged_x[1]) w = float(max(2.0, self._merged_x[2])) h = float(max(2.0, self._merged_x[3])) return np.array([cx - w * 0.5, cy - h * 0.5, cx + w * 0.5, cy + h * 0.5], dtype=np.float32) def uncertainty(self): """Суммарная неопределённость (для совместимости с Kalman8D).""" return float( self._merged_P[0, 0] + self._merged_P[1, 1] + self._merged_P[2, 2] + self._merged_P[3, 3] ) def get_velocity(self): """Возвращает (vx, vy) — merged скорость в пикс/сек.""" return float(self._merged_x[4]), float(self._merged_x[5]) def get_acceleration(self): """Возвращает (ax, ay) — оценка ускорения из CA-модели.""" ca = self.models[1] # CA модель if ca.dim_x >= 8: return float(ca.x[6, 0]), float(ca.x[7, 0]) return 0.0, 0.0 def get_turn_rate(self): """Возвращает omega — угловая скорость из CT-модели.""" ct = self.models[2] # CT модель if ct.dim_x >= 7: return float(ct.x[6, 0]) return 0.0 def get_model_probs(self): """Возвращает вероятности моделей [p_CV, p_CA, p_CT].""" return self.mu.copy() def get_dominant_model(self): """Возвращает название наиболее вероятной модели.""" idx = int(np.argmax(self.mu)) return self.model_names[idx] @property def x(self): """Совместимость с Kalman8D.x — возвращает 8x1 вектор.""" return self._merged_x.reshape(8, 1).astype(np.float32) # ─── Private ───────────────────────────────────────────────── def _project_state(self, x, from_dim, to_dim): """Проецирует состояние между моделями разной размерности.""" out = np.zeros((to_dim, 1), dtype=np.float64) # Первые 4 компонента (cx, cy, w, h) всегда совпадают n_copy = min(4, from_dim, to_dim) out[:n_copy] = x[:n_copy] # Скорости vx, vy (индексы 4,5 в CV/CT, 4,5 в CA) if from_dim >= 6 and to_dim >= 6: out[4] = x[4] out[5] = x[5] # Скорости размера: зависит от модели # CV: vw=x[6], vh=x[7] # CA: vw=x[8], vh=x[9] # CT: vw=x[7], vh=x[8] # Для простоты: копируем что можем return out def _project_cov(self, P, from_dim, to_dim): """Проецирует ковариацию между моделями.""" out = np.eye(to_dim, dtype=np.float64) * 50.0 n = min(from_dim, to_dim) out[:n, :n] = P[:n, :n] return out def _update_merged(self): """Обновляет merged state как взвешенную сумму моделей.""" self._merged_x[:] = 0 self._merged_P[:] = 0 for i, m in enumerate(self.models): xi = self._project_state(m.x, m.dim_x, 8).flatten() self._merged_x += self.mu[i] * xi for i, m in enumerate(self.models): xi = self._project_state(m.x, m.dim_x, 8).flatten() Pi = self._project_cov(m.P, m.dim_x, 8) diff = (xi - self._merged_x).reshape(8, 1) self._merged_P += self.mu[i] * (Pi + diff @ diff.T) # ─── Status / Draw ─────────────────────────────────────────── def status_line(self): if not self.enabled: return "IMM filter disabled" return f"IMM filter ready: models={self.model_names}" def draw_overlay(self, frame_bgr): """Рисует вероятности моделей.""" if not self.enabled or not self.initialized or not DRAW_IMM_PROBS: return import cv2 h, w = frame_bgr.shape[:2] probs = self.get_model_probs() dominant = self.get_dominant_model() # Барграф вероятностей bar_x = w - 150 bar_y = 100 bar_w = 120 bar_h = 16 colors = [ (200, 200, 200), # CV — серый (0, 165, 255), # CA — оранжевый (0, 0, 255), # CT — красный ] for i, (name, prob) in enumerate(zip(self.model_names, probs)): y = bar_y + i * (bar_h + 6) # Фон cv2.rectangle(frame_bgr, (bar_x, y), (bar_x + bar_w, y + bar_h), (60, 60, 60), -1) # Заполнение fill_w = int(bar_w * prob) cv2.rectangle(frame_bgr, (bar_x, y), (bar_x + fill_w, y + bar_h), colors[i], -1) # Текст cv2.putText(frame_bgr, f"{name} {prob:.0%}", (bar_x + 4, y + bar_h - 3), cv2.FONT_HERSHEY_SIMPLEX, 0.40, (255, 255, 255), 1) cv2.putText(frame_bgr, f"IMM: {dominant}", (bar_x, bar_y - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.50, (0, 255, 255), 1)