《计算机视觉:模型、学习和推理》第 19 章-时序模型

目录

前言

一、时序估计框架(19.1)

核心概念

[19.1.1 推理](#19.1.1 推理)

[19.1.2 学习](#19.1.2 学习)

二、卡尔曼滤波器(19.2)

核心概念

[19.2.1 推理(卡尔曼滤波核心步骤)](#19.2.1 推理(卡尔曼滤波核心步骤))

[19.2.2 改写测量合并阶段](#19.2.2 改写测量合并阶段)

[19.2.3 推理总结](#19.2.3 推理总结)

[19.2.4/5 示例(完整代码 + 可视化对比)](#19.2.4/5 示例(完整代码 + 可视化对比))

[完整 Python 代码(可直接运行,Mac 适配)](#完整 Python 代码(可直接运行,Mac 适配))

代码解释

运行效果

[19.2.6 滤波](#19.2.6 滤波)

[19.2.7 时序和测量模型](#19.2.7 时序和测量模型)

[19.2.8 卡尔曼滤波器的问题](#19.2.8 卡尔曼滤波器的问题)

三、扩展卡尔曼滤波器(19.3)

核心概念

[完整 Python 代码(非线性运动跟踪示例)](#完整 Python 代码(非线性运动跟踪示例))

代码解释

四、无损卡尔曼滤波器(19.4)

核心概念

[19.4.1 状态演化](#19.4.1 状态演化)

[19.4.2 测量合并过程](#19.4.2 测量合并过程)

[完整 Python 代码(UKF 实现)](#完整 Python 代码(UKF 实现))

运行效果

五、粒子滤波(19.5,补充完整)

核心概念

[完整 Python 代码(粒子滤波实现)](#完整 Python 代码(粒子滤波实现))

总结

核心知识点回顾

代码使用说明


前言

在计算机视觉领域,时序模型是处理动态视觉问题的核心(比如视频目标跟踪、运动物体姿态估计、无人车视觉导航等)。《计算机视觉:模型、学习和推理》第 19 章的时序模型,核心是解决 "如何从随时间变化的观测数据中,估计出物体的真实状态"。

本文会用通俗易懂的语言拆解本章核心知识点,搭配可直接运行的 Python 完整代码可视化对比图,让你彻底搞懂卡尔曼滤波器、扩展卡尔曼滤波器等核心内容。

一、时序估计框架(19.1)

核心概念

时序估计的本质是 "追根溯源":我们能拿到的是带噪声的观测数据 (比如摄像头每一帧看到的物体位置),但想知道的是物体的真实状态(比如物体实际的位置、速度)。

这个框架分为两大核心环节:

19.1.1 推理

推理是 "根据已有信息猜真实状态":就像你看到一个人在画面里的位置(观测),结合他上一帧的运动状态(历史),猜他此刻的真实位置(状态)。

19.1.2 学习

学习是 "优化猜的规则":如果每次猜的结果和真实值有偏差,就调整规则(比如状态转移的参数),让下次猜得更准。

二、卡尔曼滤波器(19.2)

核心概念

卡尔曼滤波器是时序估计的 "基础神器",专门解决线性系统 + 高斯噪声的状态估计问题。可以把它比喻成:

一个 "智能猜数器":每次先根据上一次的猜测结果 "预测" 当前状态,再用新的观测数据 "修正" 这个预测,最终得到更准的结果。

19.2.1 推理(卡尔曼滤波核心步骤)

卡尔曼滤波的推理过程分两大步:

1.预测阶段:用系统的状态转移模型,从 k-1 时刻的状态,预测 k 时刻的先验状态;

2.更新阶段:用 k 时刻的观测数据,修正先验状态,得到 k 时刻的后验状态(最终的最优估计)。

19.2.2 改写测量合并阶段

测量合并阶段的核心是 "权衡":观测数据有噪声、预测结果也有误差,卡尔曼滤波器会计算一个 "卡尔曼增益",来决定 "更相信预测" 还是 "更相信观测"。

19.2.3 推理总结

卡尔曼滤波的核心公式(用通俗语言翻译):

预测:当前先验状态 = 状态转移矩阵 × 上一时刻状态 + 控制输入

卡尔曼增益:增益 = 先验协方差 × 观测矩阵^T / (观测矩阵×先验协方差×观测矩阵^T + 观测噪声)

更新:最终状态 = 先验状态 + 增益 × (观测值 - 观测矩阵×先验状态)

19.2.4/5 示例(完整代码 + 可视化对比)

下面用一个 "跟踪匀速运动的小球" 的例子,实现卡尔曼滤波器,并对比 "原始观测(带噪声)" 和 "卡尔曼滤波后结果" 的差异。

完整 Python 代码(可直接运行,Mac 适配)
复制代码
import numpy as np
import matplotlib.pyplot as plt

# ==================== Mac系统Matplotlib中文显示配置 ====================
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

class KalmanFilter:
    """卡尔曼滤波器实现类"""
    def __init__(self):
        # 1. 初始化状态:[位置x, 速度vx]
        self.x = np.array([[0.0], [0.0]])  # 初始状态(假设小球从原点出发,初始速度0)
        
        # 2. 状态转移矩阵F:x_k = F * x_{k-1}(匀速运动模型)
        # [x_k; vx_k] = [1, dt; 0, 1] * [x_{k-1}; vx_{k-1}]
        self.dt = 0.1  # 时间步长
        self.F = np.array([[1, self.dt],
                           [0, 1]])
        
        # 3. 观测矩阵H:观测值z = H * x(我们只能观测到位置,观测不到速度)
        self.H = np.array([[1, 0]])
        
        # 4. 过程噪声协方差Q:描述系统模型的不确定性
        self.Q = np.array([[0.001, 0],
                           [0, 0.001]])
        
        # 5. 观测噪声协方差R:描述观测数据的噪声大小
        self.R = np.array([[0.1]])  # 观测位置的噪声
        
        # 6. 状态协方差矩阵P:描述状态估计的不确定性
        self.P = np.eye(2)  # 初始协方差
    
    def predict(self):
        """预测阶段:计算先验状态和先验协方差"""
        # 预测状态
        self.x = np.dot(self.F, self.x)
        # 预测协方差
        self.P = np.dot(np.dot(self.F, self.P), self.F.T) + self.Q
        return self.x
    
    def update(self, z):
        """更新阶段:用观测值修正预测结果"""
        # 计算卡尔曼增益K
        S = np.dot(np.dot(self.H, self.P), self.H.T) + self.R  # 观测残差协方差
        K = np.dot(np.dot(self.P, self.H.T), np.linalg.inv(S))  # 卡尔曼增益
        
        # 修正状态
        self.x = self.x + np.dot(K, (z - np.dot(self.H, self.x)))
        
        # 修正协方差
        I = np.eye(self.x.shape[0])  # 单位矩阵
        self.P = np.dot((I - np.dot(K, self.H)), self.P)
        return self.x

# ==================== 生成模拟数据 ====================
# 生成真实状态:小球以匀速2 m/s运动,共100个时间步
t = np.arange(0, 10, 0.1)  # 0到10秒,步长0.1
true_x = 2 * t  # 真实位置
true_vx = np.ones_like(t) * 2  # 真实速度(匀速)

# 生成带噪声的观测数据(模拟摄像头观测的位置)
np.random.seed(42)  # 固定随机种子,保证结果可复现
obs_x = true_x + np.random.normal(0, np.sqrt(0.1), size=len(true_x))  # 加高斯噪声

# ==================== 运行卡尔曼滤波 ====================
kf = KalmanFilter()
filtered_x = []  # 存储滤波后的位置
filtered_vx = []  # 存储滤波后的速度

for z in obs_x:
    kf.predict()
    x_est = kf.update(z)
    filtered_x.append(x_est[0, 0])
    filtered_vx.append(x_est[1, 0])

# ==================== 可视化对比 ====================
plt.figure(figsize=(12, 6))

# 绘制位置对比
plt.subplot(1, 2, 1)
plt.plot(t, true_x, 'g-', label='真实位置', linewidth=2)
plt.plot(t, obs_x, 'r.', label='带噪声观测位置', alpha=0.5)
plt.plot(t, filtered_x, 'b-', label='卡尔曼滤波后位置', linewidth=2)
plt.xlabel('时间 (s)')
plt.ylabel('位置 (m)')
plt.title('卡尔曼滤波:位置估计对比')
plt.legend()
plt.grid(True, alpha=0.3)

# 绘制速度对比
plt.subplot(1, 2, 2)
plt.plot(t, true_vx, 'g-', label='真实速度', linewidth=2)
plt.plot(t, filtered_vx, 'b-', label='卡尔曼滤波后速度', linewidth=2)
plt.xlabel('时间 (s)')
plt.ylabel('速度 (m/s)')
plt.title('卡尔曼滤波:速度估计对比')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
代码解释

1.KalmanFilter 类 :封装了卡尔曼滤波的核心逻辑,包含predict(预测)和update(更新)两个核心方法;

2.模拟数据生成:生成小球匀速运动的真实位置,再给观测值加高斯噪声(模拟真实场景的观测误差);

3.可视化:左侧对比 "真实位置、带噪声观测位置、滤波后位置",右侧对比 "真实速度、滤波后速度"。

运行效果
  • 红色散点(带噪声观测)会围绕绿色实线(真实位置)上下波动;
  • 蓝色实线(滤波后结果)几乎和绿色实线重合,完美过滤了噪声;
  • 速度估计也能稳定收敛到真实值 2 m/s。

19.2.6 滤波

卡尔曼滤波的 "滤波" 本质是 "从噪声中提取信号":就像给观测数据 "开了美颜",去掉噪声的瑕疵,还原真实状态。

19.2.7 时序和测量模型

时序模型(状态转移模型) :描述物体状态随时间的变化规律(比如匀速、匀加速运动),对应代码中的F矩阵;

测量模型 :描述 "真实状态" 和 "观测数据" 之间的关系,对应代码中的H矩阵。

19.2.8 卡尔曼滤波器的问题

卡尔曼滤波器有个 "致命缺点":只适用于线性系统。如果物体的运动是非线性的(比如圆周运动、加速运动),卡尔曼滤波的效果会急剧下降。

三、扩展卡尔曼滤波器(19.3)

核心概念

扩展卡尔曼滤波器(EKF)是卡尔曼滤波的 "非线性版",核心思路是:

把非线性系统 "局部线性化"(用泰勒展开的一阶近似),然后再用卡尔曼滤波的逻辑处理。

可以比喻成:

非线性的山路,我们把它切成无数小段,每一小段都近似看成直线,然后用走直线的方法(卡尔曼滤波)走完全程。

完整 Python 代码(非线性运动跟踪示例)

复制代码
import numpy as np
import matplotlib.pyplot as plt

# ==================== Mac系统Matplotlib中文显示配置 ====================
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

class ExtendedKalmanFilter:
    """扩展卡尔曼滤波器(EKF)实现类:处理非线性运动(圆周运动)"""
    def __init__(self):
        # 初始化状态:[x, y, 角度theta, 角速度omega]
        self.x = np.array([[1.0], [0.0], [0.0], [0.1]])  # 初始在(1,0),初始角速度0.1 rad/s
        
        # 状态协方差矩阵P
        self.P = np.eye(4) * 0.1
        
        # 过程噪声协方差Q
        self.Q = np.eye(4) * 0.001
        
        # 观测噪声协方差R(观测x和y的噪声)
        self.R = np.eye(2) * 0.01
        
        self.dt = 0.1  # 时间步长
    
    def state_transition(self, x):
        """非线性状态转移函数(圆周运动):x_k = f(x_{k-1})"""
        x_pos, y_pos, theta, omega = x[:, 0]
        # 圆周运动:角度随时间增加,位置随角度变化
        new_theta = theta + omega * self.dt
        new_x = np.cos(new_theta)
        new_y = np.sin(new_theta)
        new_omega = omega  # 角速度不变
        return np.array([[new_x], [new_y], [new_theta], [new_omega]])
    
    def jacobian_F(self, x):
        """状态转移函数的雅可比矩阵(线性化核心)"""
        theta = x[2, 0]
        omega = x[3, 0]
        # 雅可比矩阵J_F = df/dx
        J = np.array([
            [0, 0, -np.sin(theta + omega*self.dt)*omega, -np.sin(theta + omega*self.dt)*self.dt*omega + np.cos(theta + omega*self.dt)*self.dt],
            [0, 0, np.cos(theta + omega*self.dt)*omega, np.cos(theta + omega*self.dt)*self.dt*omega + np.sin(theta + omega*self.dt)*self.dt],
            [0, 0, 1, self.dt],
            [0, 0, 0, 1]
        ])
        return J
    
    def observation_model(self, x):
        """观测模型:z = h(x) = [x_pos; y_pos]"""
        return np.array([[x[0, 0]], [x[1, 0]]])
    
    def jacobian_H(self):
        """观测模型的雅可比矩阵"""
        return np.array([
            [1, 0, 0, 0],
            [0, 1, 0, 0]
        ])
    
    def predict(self):
        """预测阶段:非线性预测 + 线性化协方差更新"""
        # 1. 非线性预测状态
        self.x = self.state_transition(self.x)
        
        # 2. 计算雅可比矩阵
        J_F = self.jacobian_F(self.x)
        
        # 3. 更新协方差
        self.P = np.dot(np.dot(J_F, self.P), J_F.T) + self.Q
        return self.x
    
    def update(self, z):
        """更新阶段:用观测值修正预测结果"""
        # 1. 计算雅可比矩阵H
        J_H = self.jacobian_H()
        
        # 2. 计算卡尔曼增益
        S = np.dot(np.dot(J_H, self.P), J_H.T) + self.R
        K = np.dot(np.dot(self.P, J_H.T), np.linalg.inv(S))
        
        # 3. 修正状态
        z_pred = self.observation_model(self.x)
        self.x = self.x + np.dot(K, (z - z_pred))
        
        # 4. 修正协方差
        I = np.eye(self.x.shape[0])
        self.P = np.dot((I - np.dot(K, J_H)), self.P)
        return self.x

# ==================== 生成模拟数据 ====================
np.random.seed(42)
t = np.arange(0, 20, 0.1)  # 0到20秒,步长0.1
n_steps = len(t)

# 真实状态:圆周运动(半径1,角速度0.1 rad/s)
true_theta = 0.1 * t
true_x = np.cos(true_theta)
true_y = np.sin(true_theta)

# 带噪声的观测数据
obs_x = true_x + np.random.normal(0, 0.1, n_steps)
obs_y = true_y + np.random.normal(0, 0.1, n_steps)

# ==================== 运行EKF ====================
ekf = ExtendedKalmanFilter()
ekf_x = []
ekf_y = []

for z_x, z_y in zip(obs_x, obs_y):
    ekf.predict()
    x_est = ekf.update(np.array([[z_x], [z_y]]))
    ekf_x.append(x_est[0, 0])
    ekf_y.append(x_est[1, 0])

# ==================== 可视化对比 ====================
plt.figure(figsize=(10, 10))
plt.plot(true_x, true_y, 'g-', label='真实轨迹(圆周)', linewidth=3)
plt.scatter(obs_x, obs_y, c='r', s=10, label='带噪声观测点', alpha=0.5)
plt.plot(ekf_x, ekf_y, 'b-', label='EKF估计轨迹', linewidth=2)
plt.axis('equal')
plt.xlabel('X位置 (m)')
plt.ylabel('Y位置 (m)')
plt.title('扩展卡尔曼滤波器(EKF):圆周运动跟踪对比')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
代码解释

1.非线性状态转移:用圆周运动模型替代线性模型,体现非线性系统的特点;

2.雅可比矩阵:EKF 的核心,用于对非线性函数做一阶泰勒展开,实现线性化;

3.可视化:对比 "真实圆周轨迹、带噪声观测点、EKF 估计轨迹",能看到 EKF 完美拟合真实轨迹。

四、无损卡尔曼滤波器(19.4)

核心概念

无损卡尔曼滤波器(UKF)是比 EKF 更优的非线性滤波方法,核心思路是:

不做线性化,而是通过 "采样 sigma 点" 来近似状态的概率分布,再用这些点来计算预测和更新。

可以比喻成:

EKF 是 "用一根直线近似曲线",而 UKF 是 "用多个点拟合曲线",拟合效果更准。

19.4.1 状态演化

UKF 的状态演化是通过 sigma 点的传播实现的:先根据当前状态的均值和协方差生成 sigma 点,再让这些点通过非线性的状态转移函数,得到新的 sigma 点,最后用这些新点计算先验状态的均值和协方差。

19.4.2 测量合并过程

测量合并阶段和 EKF 类似,但也是通过 sigma 点的传播来计算观测的均值和协方差,再计算卡尔曼增益,最终修正状态。

完整 Python 代码(UKF 实现)

python 复制代码
import numpy as np
import matplotlib.pyplot as plt

# ==================== Mac系统Matplotlib中文显示配置 ====================
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

# ==================== 补充EKF类定义(关键修复) ====================
class ExtendedKalmanFilter:
    """扩展卡尔曼滤波器(EKF)实现类:处理非线性运动(圆周运动)"""
    def __init__(self):
        # 初始化状态:[x, y, 角度theta, 角速度omega]
        self.x = np.array([[1.0], [0.0], [0.0], [0.1]])  # 初始在(1,0),初始角速度0.1 rad/s
        
        # 状态协方差矩阵P
        self.P = np.eye(4) * 0.1
        
        # 过程噪声协方差Q
        self.Q = np.eye(4) * 0.001
        
        # 观测噪声协方差R(观测x和y的噪声)
        self.R = np.eye(2) * 0.01
        
        self.dt = 0.1  # 时间步长
    
    def state_transition(self, x):
        """非线性状态转移函数(圆周运动):x_k = f(x_{k-1})"""
        x_pos, y_pos, theta, omega = x[:, 0]
        # 圆周运动:角度随时间增加,位置随角度变化
        new_theta = theta + omega * self.dt
        new_x = np.cos(new_theta)
        new_y = np.sin(new_theta)
        new_omega = omega  # 角速度不变
        return np.array([[new_x], [new_y], [new_theta], [new_omega]])
    
    def jacobian_F(self, x):
        """状态转移函数的雅可比矩阵(线性化核心)"""
        theta = x[2, 0]
        omega = x[3, 0]
        # 雅可比矩阵J_F = df/dx
        J = np.array([
            [0, 0, -np.sin(theta + omega*self.dt)*omega, -np.sin(theta + omega*self.dt)*self.dt*omega + np.cos(theta + omega*self.dt)*self.dt],
            [0, 0, np.cos(theta + omega*self.dt)*omega, np.cos(theta + omega*self.dt)*self.dt*omega + np.sin(theta + omega*self.dt)*self.dt],
            [0, 0, 1, self.dt],
            [0, 0, 0, 1]
        ])
        return J
    
    def observation_model(self, x):
        """观测模型:z = h(x) = [x_pos; y_pos]"""
        return np.array([[x[0, 0]], [x[1, 0]]])
    
    def jacobian_H(self):
        """观测模型的雅可比矩阵"""
        return np.array([
            [1, 0, 0, 0],
            [0, 1, 0, 0]
        ])
    
    def predict(self):
        """预测阶段:非线性预测 + 线性化协方差更新"""
        # 1. 非线性预测状态
        self.x = self.state_transition(self.x)
        
        # 2. 计算雅可比矩阵
        J_F = self.jacobian_F(self.x)
        
        # 3. 更新协方差
        self.P = np.dot(np.dot(J_F, self.P), J_F.T) + self.Q
        return self.x
    
    def update(self, z):
        """更新阶段:用观测值修正预测结果"""
        # 1. 计算雅可比矩阵H
        J_H = self.jacobian_H()
        
        # 2. 计算卡尔曼增益
        S = np.dot(np.dot(J_H, self.P), J_H.T) + self.R
        K = np.dot(np.dot(self.P, J_H.T), np.linalg.inv(S))
        
        # 3. 修正状态
        z_pred = self.observation_model(self.x)
        self.x = self.x + np.dot(K, (z - z_pred))
        
        # 4. 修正协方差
        I = np.eye(self.x.shape[0])
        self.P = np.dot((I - np.dot(K, J_H)), self.P)
        return self.x

# ==================== UKF类定义(原有代码) ====================
class UnscentedKalmanFilter:
    """无损卡尔曼滤波器(UKF)实现类:圆周运动跟踪"""

    def __init__(self):
        # 状态维度
        self.n_x = 4  # [x, y, theta, omega]
        # 观测维度
        self.n_z = 2  # [x, y]

        # 初始化状态
        self.x = np.array([[1.0], [0.0], [0.0], [0.1]])
        # 状态协方差
        self.P = np.eye(self.n_x) * 0.1

        # UKF参数
        self.alpha = 0.01  # 控制sigma点的分布
        self.beta = 2.0  # 对高斯分布的最优选择
        self.kappa = 3 - self.n_x  # 缩放参数

        # 计算权重
        self.lambda_ = self.alpha ** 2 * (self.n_x + self.kappa) - self.n_x
        self.Wm = np.zeros(2 * self.n_x + 1)  # 均值权重
        self.Wc = np.zeros(2 * self.n_x + 1)  # 协方差权重
        self.Wm[0] = self.lambda_ / (self.n_x + self.lambda_)
        self.Wc[0] = self.Wm[0] + (1 - self.alpha ** 2 + self.beta)
        for i in range(1, 2 * self.n_x + 1):
            self.Wm[i] = 1 / (2 * (self.n_x + self.lambda_))
            self.Wc[i] = self.Wm[i]

        # 噪声协方差
        self.Q = np.eye(self.n_x) * 0.001  # 过程噪声
        self.R = np.eye(self.n_z) * 0.01  # 观测噪声

        self.dt = 0.1  # 时间步长

    def generate_sigma_points(self):
        """生成sigma点"""
        sigma = np.zeros((self.n_x, 2 * self.n_x + 1))
        sigma[:, 0] = self.x[:, 0]  # 中心sigma点

        sqrt_P = np.linalg.cholesky((self.n_x + self.lambda_) * self.P)
        for i in range(self.n_x):
            sigma[:, i + 1] = self.x[:, 0] + sqrt_P[:, i]
            sigma[:, i + 1 + self.n_x] = self.x[:, 0] - sqrt_P[:, i]
        return sigma

    def state_transition(self, sigma):
        """非线性状态转移:sigma点传播"""
        sigma_pred = np.zeros_like(sigma)
        for i in range(sigma.shape[1]):
            x_pos, y_pos, theta, omega = sigma[:, i]
            new_theta = theta + omega * self.dt
            new_x = np.cos(new_theta)
            new_y = np.sin(new_theta)
            new_omega = omega
            sigma_pred[:, i] = [new_x, new_y, new_theta, new_omega]
        return sigma_pred

    def predict(self):
        """预测阶段"""
        # 1. 生成sigma点
        sigma = self.generate_sigma_points()

        # 2. 传播sigma点
        sigma_pred = self.state_transition(sigma)

        # 3. 计算先验状态均值
        self.x = np.dot(sigma_pred, self.Wm.reshape(-1, 1))

        # 4. 计算先验协方差
        self.P = self.Q.copy()
        for i in range(sigma_pred.shape[1]):
            diff = sigma_pred[:, i].reshape(-1, 1) - self.x
            self.P += self.Wc[i] * np.dot(diff, diff.T)

        return self.x, sigma_pred

    def observation_model(self, sigma):
        """观测模型:sigma点的观测值"""
        sigma_z = np.zeros((self.n_z, sigma.shape[1]))
        for i in range(sigma.shape[1]):
            sigma_z[:, i] = [sigma[0, i], sigma[1, i]]
        return sigma_z

    def update(self, z, sigma_pred):
        """更新阶段"""
        # 1. 计算观测的sigma点
        sigma_z = self.observation_model(sigma_pred)

        # 2. 计算观测均值
        z_pred = np.dot(sigma_z, self.Wm.reshape(-1, 1))

        # 3. 计算观测协方差和互协方差
        Pzz = self.R.copy()
        Pxz = np.zeros((self.n_x, self.n_z))
        for i in range(sigma_z.shape[1]):
            diff_z = sigma_z[:, i].reshape(-1, 1) - z_pred
            diff_x = sigma_pred[:, i].reshape(-1, 1) - self.x
            Pzz += self.Wc[i] * np.dot(diff_z, diff_z.T)
            Pxz += self.Wc[i] * np.dot(diff_x, diff_z.T)

        # 4. 计算卡尔曼增益
        K = np.dot(Pxz, np.linalg.inv(Pzz))

        # 5. 修正状态和协方差
        self.x = self.x + np.dot(K, (z - z_pred))
        self.P = self.P - np.dot(np.dot(K, Pzz), K.T)

        return self.x


# ==================== 生成模拟数据(和EKF示例相同) ====================
np.random.seed(42)
t = np.arange(0, 20, 0.1)
n_steps = len(t)

true_theta = 0.1 * t
true_x = np.cos(true_theta)
true_y = np.sin(true_theta)

obs_x = true_x + np.random.normal(0, 0.1, n_steps)
obs_y = true_y + np.random.normal(0, 0.1, n_steps)

# ==================== 运行UKF ====================
ukf = UnscentedKalmanFilter()
ukf_x = []
ukf_y = []

for z_x, z_y in zip(obs_x, obs_y):
    x_pred, sigma_pred = ukf.predict()
    x_est = ukf.update(np.array([[z_x], [z_y]]), sigma_pred)
    ukf_x.append(x_est[0, 0])
    ukf_y.append(x_est[1, 0])

# ==================== 可视化对比(EKF vs UKF) ====================
# 先运行EKF获取结果
ekf = ExtendedKalmanFilter()
ekf_x = []
ekf_y = []
for z_x, z_y in zip(obs_x, obs_y):
    ekf.predict()
    x_est = ekf.update(np.array([[z_x], [z_y]]))
    ekf_x.append(x_est[0, 0])
    ekf_y.append(x_est[1, 0])

# 绘图
plt.figure(figsize=(12, 6))

# 左图:EKF结果
plt.subplot(1, 2, 1)
plt.plot(true_x, true_y, 'g-', label='真实轨迹', linewidth=3)
plt.scatter(obs_x, obs_y, c='r', s=10, label='观测点', alpha=0.5)
plt.plot(ekf_x, ekf_y, 'b-', label='EKF估计', linewidth=2)
plt.axis('equal')
plt.title('扩展卡尔曼滤波器(EKF)')
plt.legend()
plt.grid(True, alpha=0.3)

# 右图:UKF结果
plt.subplot(1, 2, 2)
plt.plot(true_x, true_y, 'g-', label='真实轨迹', linewidth=3)
plt.scatter(obs_x, obs_y, c='r', s=10, label='观测点', alpha=0.5)
plt.plot(ukf_x, ukf_y, 'orange', label='UKF估计', linewidth=2)
plt.axis('equal')
plt.title('无损卡尔曼滤波器(UKF)')
plt.legend()
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
运行效果
  • UKF 的估计轨迹比 EKF 更贴近真实圆周轨迹,尤其是在噪声较大时;
  • 对比图能清晰看到:EKF 有轻微的偏差,而 UKF 几乎和真实轨迹重合。

五、粒子滤波(19.5,补充完整)

核心概念

粒子滤波是 "万能的非线性滤波方法",核心思路是:

用一堆 "粒子"(每个粒子代表一个可能的状态)来近似状态的概率分布,通过 "重采样" 让粒子向真实状态聚集。

可以比喻成:

找一个藏在草丛里的兔子,先撒一堆兔子玩偶(粒子)在草丛里,然后根据兔子的脚印(观测数据),把离脚印远的玩偶扔掉,离脚印近的玩偶多复制几个,最终所有玩偶都会聚集在兔子真实位置周围。

完整 Python 代码(粒子滤波实现)

复制代码
import numpy as np
import matplotlib.pyplot as plt

# ==================== Mac系统Matplotlib中文显示配置 ====================
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.family'] = 'Arial Unicode MS'
plt.rcParams['axes.facecolor'] = 'white'

class ParticleFilter:
    """粒子滤波器实现类:圆周运动跟踪"""
    def __init__(self, n_particles=1000):
        self.n_particles = n_particles  # 粒子数量
        self.particles = np.zeros((4, n_particles))  # 粒子状态:[x, y, theta, omega]
        # 初始化粒子(围绕初始位置随机分布)
        self.particles[0, :] = 1.0 + np.random.normal(0, 0.1, n_particles)
        self.particles[1, :] = 0.0 + np.random.normal(0, 0.1, n_particles)
        self.particles[2, :] = 0.0 + np.random.normal(0, 0.1, n_particles)
        self.particles[3, :] = 0.1 + np.random.normal(0, 0.01, n_particles)
        self.weights = np.ones(n_particles) / n_particles  # 粒子权重
        
        self.dt = 0.1  # 时间步长
        self.Q = np.diag([0.001, 0.001, 0.001, 0.001])  # 过程噪声
        self.R = np.diag([0.01, 0.01])  # 观测噪声
    
    def predict(self):
        """预测阶段:粒子传播(加过程噪声)"""
        for i in range(self.n_particles):
            # 非线性状态转移(圆周运动)
            x_pos, y_pos, theta, omega = self.particles[:, i]
            new_theta = theta + omega * self.dt
            new_x = np.cos(new_theta)
            new_y = np.sin(new_theta)
            new_omega = omega
            
            # 加过程噪声
            new_x += np.random.normal(0, np.sqrt(self.Q[0, 0]))
            new_y += np.random.normal(0, np.sqrt(self.Q[1, 1]))
            new_theta += np.random.normal(0, np.sqrt(self.Q[2, 2]))
            new_omega += np.random.normal(0, np.sqrt(self.Q[3, 3]))
            
            self.particles[:, i] = [new_x, new_y, new_theta, new_omega]
    
    def update(self, z):
        """更新阶段:计算权重 + 重采样"""
        # 1. 计算每个粒子的权重(观测似然)
        for i in range(self.n_particles):
            z_pred = np.array([self.particles[0, i], self.particles[1, i]])
            # 高斯似然
            diff = z - z_pred
            self.weights[i] = np.exp(-0.5 * np.dot(diff.T, np.dot(np.linalg.inv(self.R), diff)))
        
        # 2. 归一化权重
        self.weights += 1e-300  # 避免权重为0
        self.weights /= np.sum(self.weights)
        
        # 3. 重采样(轮盘赌法)
        indices = np.random.choice(self.n_particles, size=self.n_particles, p=self.weights)
        self.particles = self.particles[:, indices]
        self.weights = np.ones(self.n_particles) / self.n_particles
    
    def get_state(self):
        """计算粒子的均值作为估计状态"""
        return np.average(self.particles, axis=1, weights=self.weights)

# ==================== 生成模拟数据 ====================
np.random.seed(42)
t = np.arange(0, 20, 0.1)
n_steps = len(t)

true_theta = 0.1 * t
true_x = np.cos(true_theta)
true_y = np.sin(true_theta)

obs_x = true_x + np.random.normal(0, 0.1, n_steps)
obs_y = true_y + np.random.normal(0, 0.1, n_steps)

# ==================== 运行粒子滤波 ====================
pf = ParticleFilter(n_particles=1000)
pf_x = []
pf_y = []

for z_x, z_y in zip(obs_x, obs_y):
    pf.predict()
    pf.update(np.array([z_x, z_y]))
    state = pf.get_state()
    pf_x.append(state[0])
    pf_y.append(state[1])

# ==================== 可视化对比 ====================
plt.figure(figsize=(10, 10))
plt.plot(true_x, true_y, 'g-', label='真实轨迹', linewidth=3)
plt.scatter(obs_x, obs_y, c='r', s=10, label='观测点', alpha=0.5)
plt.plot(pf_x, pf_y, 'purple', label='粒子滤波估计', linewidth=2)
plt.axis('equal')
plt.xlabel('X位置 (m)')
plt.ylabel('Y位置 (m)')
plt.title('粒子滤波器:圆周运动跟踪对比')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

总结

核心知识点回顾

1.卡尔曼滤波器 :仅适用于线性系统 + 高斯噪声,核心是 "预测 - 更新" 两步,通过卡尔曼增益权衡预测和观测的权重;

2.扩展卡尔曼滤波器(EKF) :通过雅可比矩阵线性化处理非线性系统,是卡尔曼滤波的非线性扩展,但一阶近似会引入误差;

3.无损卡尔曼滤波器(UKF) :通过sigma 点采样替代线性化,非线性估计精度比 EKF 更高;

4.粒子滤波 :基于蒙特卡洛采样,适用于任意非线性、非高斯系统,是最通用的时序估计方法(但计算量更大)。

代码使用说明

1.所有代码均已适配 Mac 系统的 Matplotlib 中文显示,可直接复制运行;

2.核心类(KalmanFilter/EKF/UKF/ParticleFilter)可复用,只需修改状态转移模型 / 观测模型即可适配不同场景;

3.可视化部分通过对比图直观展示了各算法的效果,便于理解不同滤波器的优缺点。

希望这篇解析能帮你彻底搞懂《计算机视觉:模型、学习和推理》第 19 章的时序模型!如果有问题,欢迎在评论区交流~

相关推荐
格林威2 小时前
工业相机图像高速存储(C++版):先存内存,后批量转存方法,附海康相机实战代码!
开发语言·c++·人工智能·数码相机·计算机视觉·工业相机·堡盟相机
蓝净云2 小时前
python包管理工具uv
python·uv
Errorbot2 小时前
GPS学习(二)使用树莓派5和GPS PPS 实现微秒级精度的时间同步
学习·ubuntu·gps
Mintopia2 小时前
如何看待大模型发展瓶颈:从算力、数据到对齐与系统工程的再评估
前端·人工智能
Lxt12138_2 小时前
2026深耕学术,智启创作——论文创作如何正确使用新兴科技
人工智能·科技
x-cmd2 小时前
[260311] x-cmd v0.8.8:新增一键卸载 OpenClaw 命令,AI 命令补全回归,内网服务器一键部署 x-cmd
运维·服务器·人工智能·ai·ssh·x-cmd·openclaw
云梦谭2 小时前
AI如何重塑通信行业:从VoIP到智能语音平台
人工智能
tryCbest2 小时前
Django 基础入门教程(第三篇):Admin后台与ORM进阶(单表、多表、聚合查询)
python·django
翼龙云_cloud2 小时前
阿里云代理商:如何用百炼自动生成商品解说视频?
人工智能·阿里云·云计算