You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

566 lines
20 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# ============================================================
# 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)