HarmonyOS开发:多传感器融合与卡尔曼滤波

举报
Jack20 发表于 2026/06/21 16:07:46 2026/06/21
【摘要】 HarmonyOS开发:多传感器融合与卡尔曼滤波核心要点:本文系统讲解HarmonyOS多传感器融合的核心理论、卡尔曼滤波算法原理与实现、扩展卡尔曼滤波(EKF)在非线性系统中的应用,以及基于加速度计+陀螺仪+磁力计的9轴姿态估计完整方案,涵盖从数学推导到工程实践的完整技术链路。项目说明HarmonyOS版本5.0+(API 12+)开发语言ArkTS核心API@ohos.sensor (...

HarmonyOS开发:多传感器融合与卡尔曼滤波

核心要点:本文系统讲解HarmonyOS多传感器融合的核心理论、卡尔曼滤波算法原理与实现、扩展卡尔曼滤波(EKF)在非线性系统中的应用,以及基于加速度计+陀螺仪+磁力计的9轴姿态估计完整方案,涵盖从数学推导到工程实践的完整技术链路。

项目 说明
开发语言 ArkTS
核心API @ohos.sensor (accelerometer, gyroscope, magnetometer)
权限声明 ohos.permission.ACCELEROMETER, ohos.permission.GYROSCOPE

一、背景与动机

1.1 为什么需要传感器融合

单一传感器存在固有局限性,无法满足高精度应用需求:

传感器 优势 局限
加速度计 测量绝对重力方向,无漂移 受运动加速度干扰,无法区分重力与运动
陀螺仪 响应快,不受运动干扰 存在积分漂移,长时间累积误差大
磁力计 测量地磁方向,提供航向 受铁磁干扰,室内精度差
气压计 测量垂直高度 受天气和气流影响

传感器融合的核心思想:利用不同传感器的互补特性,通过数学算法将多源数据融合为比任何单一传感器都更准确、更稳定的估计值。

1.2 传感器融合的典型应用

  • 9轴姿态估计:加速度计+陀螺仪+磁力计融合,实现无漂移的3D姿态跟踪
  • 步行导航(PDR):加速度计检测步数,陀螺仪估计方向,气压计估计楼层
  • 运动识别:多传感器特征融合,识别走路/跑步/骑行/静止等行为
  • 手势识别:加速度+角速度融合,识别复杂手势
  • VR/AR头部追踪:低延迟高精度的头部姿态估计

1.3 传感器融合架构

图片.png


二、核心原理

2.1 卡尔曼滤波数学基础

卡尔曼滤波(Kalman Filter, KF)是一种最优递推估计算法,它基于系统的状态空间模型和噪声统计特性,通过预测-更新两步循环实现对系统状态的最小方差估计。

系统模型

状态方程(描述状态如何随时间演化):

x_k = A·x_{k-1} + B·u_k + w_k

观测方程(描述如何从状态得到观测值):

z_k = H·x_k + v_k

其中:

  • x_k:k时刻的状态向量(我们要估计的量)
  • A:状态转移矩阵(描述状态如何演化)
  • B:控制输入矩阵
  • u_k:控制输入向量
  • w_k:过程噪声,~N(0, Q)
  • z_k:k时刻的观测向量
  • H:观测矩阵(描述状态到观测的映射)
  • v_k:观测噪声,~N(0, R)
  • Q:过程噪声协方差矩阵
  • R:观测噪声协方差矩阵

卡尔曼滤波五步迭代

flowchart LR
    subgraph Predict["预测步 Prediction"]
        P1["状态预测<br/>x̂ₖ⁻ = A·x̂ₖ₋₁"]
        P2["协方差预测<br/>Pₖ⁻ = A·Pₖ₋₁·Aᵀ + Q"]
    end

    subgraph Update["更新步 Update"]
        U1["卡尔曼增益<br/>Kₖ = Pₖ⁻·Hᵀ·(H·Pₖ⁻·Hᵀ + R)⁻¹"]
        U2["状态更新<br/>x̂ₖ = x̂ₖ⁻ + Kₖ·(zₖ - H·x̂ₖ⁻)"]
        U3["协方差更新<br/>Pₖ = (I - Kₖ·H)·Pₖ⁻"]
    end

    P1 --> P2 --> U1 --> U2 --> U3
    U3 -->|下一时刻| P1

    classDef predictStyle fill:#1a1a2e,stroke:#e94560,color:#eee,stroke-width:2px
    classDef updateStyle fill:#0f3460,stroke:#533483,color:#eee,stroke-width:2px

    class P1,P2 predictStyle
    class U1,U2,U3 updateStyle

卡尔曼增益的物理意义

  • 当观测噪声R大时,K趋近0,更信任预测值
  • 当过程噪声Q大时,K趋近1,更信任观测值
  • K本质上是在预测和观测之间做最优加权

2.2 扩展卡尔曼滤波(EKF)

标准卡尔曼滤波要求系统是线性的。对于姿态估计等非线性系统,需要使用扩展卡尔曼滤波(Extended Kalman Filter, EKF)

核心思想:在当前状态估计点对非线性函数进行一阶泰勒展开,得到线性化模型,然后应用标准卡尔曼滤波。

非线性状态方程: x_k = f(x_{k-1}, u_k) + w_k
非线性观测方程: z_k = h(x_k) + v_k

线性化:
A_k = ∂f/∂x |_{x̂_{k-1}}   (雅可比矩阵)
H_k = ∂h/∂x |_{x̂_k⁻}       (雅可比矩阵)

EKF的代价是引入了线性化误差,但对于温和的非线性系统,EKF仍然是实用且高效的选择。

2.3 互补滤波

互补滤波是一种更简单的传感器融合方法,特别适合加速度计+陀螺仪的姿态融合:

θ_fused = α · (θ_prev + ω·dt) + (1-α) · θ_accel

其中:

  • α:互补滤波系数(通常0.95-0.98)
  • θ_prev + ω·dt:陀螺仪积分(高频可信)
  • θ_accel:加速度计计算的角度(低频可信)

互补滤波本质上是高通滤波陀螺仪 + 低通滤波加速度计,与卡尔曼滤波在简单场景下效果类似,但计算量更小。


三、代码实战

3.1 标量卡尔曼滤波器实现

// KalmanFilter.ets - 卡尔曼滤波器通用实现

/**
 * 矩阵工具类
 * 提供基本的矩阵运算,用于卡尔曼滤波
 */
export class Matrix {
  private data: number[][];
  readonly rows: number;
  readonly cols: number;

  constructor(rows: number, cols: number, initData?: number[][]) {
    this.rows = rows;
    this.cols = cols;
    if (initData) {
      this.data = initData;
    } else {
      this.data = Array.from({ length: rows }, () => new Array(cols).fill(0));
    }
  }

  get(i: number, j: number): number {
    return this.data[i][j];
  }

  set(i: number, j: number, value: number): void {
    this.data[i][j] = value;
  }

  /**
   * 矩阵加法
   */
  add(other: Matrix): Matrix {
    const result = new Matrix(this.rows, this.cols);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.cols; j++) {
        result.set(i, j, this.data[i][j] + other.get(i, j));
      }
    }
    return result;
  }

  /**
   * 矩阵减法
   */
  subtract(other: Matrix): Matrix {
    const result = new Matrix(this.rows, this.cols);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.cols; j++) {
        result.set(i, j, this.data[i][j] - other.get(i, j));
      }
    }
    return result;
  }

  /**
   * 矩阵乘法
   */
  multiply(other: Matrix): Matrix {
    const result = new Matrix(this.rows, other.cols);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < other.cols; j++) {
        let sum = 0;
        for (let k = 0; k < this.cols; k++) {
          sum += this.data[i][k] * other.get(k, j);
        }
        result.set(i, j, sum);
      }
    }
    return result;
  }

  /**
   * 矩阵转置
   */
  transpose(): Matrix {
    const result = new Matrix(this.cols, this.rows);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.cols; j++) {
        result.set(j, i, this.data[i][j]);
      }
    }
    return result;
  }

  /**
   * 矩阵求逆(仅支持小矩阵,使用伴随矩阵法)
   */
  inverse(): Matrix {
    if (this.rows === 1 && this.cols === 1) {
      return new Matrix(1, 1, [[1 / this.data[0][0]]]);
    }
    if (this.rows === 2 && this.cols === 2) {
      const det = this.data[0][0] * this.data[1][1] - this.data[0][1] * this.data[1][0];
      if (Math.abs(det) < 1e-10) {
        return this.identity();
      }
      return new Matrix(2, 2, [
        [this.data[1][1] / det, -this.data[0][1] / det],
        [-this.data[1][0] / det, this.data[0][0] / det]
      ]);
    }
    // 3x3及以上使用高斯消元法
    return this.gaussianInverse();
  }

  /**
   * 高斯消元法求逆
   */
  private gaussianInverse(): Matrix {
    const n = this.rows;
    // 构造增广矩阵 [A|I]
    const augmented: number[][] = [];
    for (let i = 0; i < n; i++) {
      augmented[i] = [...this.data[i], ...new Array(n).fill(0)];
      augmented[i][n + i] = 1;
    }

    // 前向消元
    for (let col = 0; col < n; col++) {
      let maxRow = col;
      for (let row = col + 1; row < n; row++) {
        if (Math.abs(augmented[row][col]) > Math.abs(augmented[maxRow][col])) {
          maxRow = row;
        }
      }
      [augmented[col], augmented[maxRow]] = [augmented[maxRow], augmented[col]];

      const pivot = augmented[col][col];
      if (Math.abs(pivot) < 1e-10) continue;

      for (let j = col; j < 2 * n; j++) {
        augmented[col][j] /= pivot;
      }

      for (let row = 0; row < n; row++) {
        if (row !== col) {
          const factor = augmented[row][col];
          for (let j = col; j < 2 * n; j++) {
            augmented[row][j] -= factor * augmented[col][j];
          }
        }
      }
    }

    // 提取逆矩阵
    const result = new Matrix(n, n);
    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        result.set(i, j, augmented[i][n + j]);
      }
    }
    return result;
  }

  /**
   * 单位矩阵
   */
  identity(): Matrix {
    const result = new Matrix(this.rows, this.cols);
    for (let i = 0; i < this.rows; i++) {
      result.set(i, i, 1);
    }
    return result;
  }

  /**
   * 缩放
   */
  scale(scalar: number): Matrix {
    const result = new Matrix(this.rows, this.cols);
    for (let i = 0; i < this.rows; i++) {
      for (let j = 0; j < this.cols; j++) {
        result.set(i, j, this.data[i][j] * scalar);
      }
    }
    return result;
  }

  /**
   * 转为一维数组
   */
  toFlatArray(): number[] {
    return this.data.flat();
  }

  /**
   * 从一维数组创建列向量
   */
  static fromColumnVector(vec: number[]): Matrix {
    const m = new Matrix(vec.length, 1);
    for (let i = 0; i < vec.length; i++) {
      m.set(i, 0, vec[i]);
    }
    return m;
  }
}

/**
 * 卡尔曼滤波器
 * 通用实现,支持任意维度的状态和观测
 */
export class KalmanFilter {
  // 状态向量
  private x: Matrix;
  // 状态协方差矩阵
  private P: Matrix;
  // 状态转移矩阵
  private A: Matrix;
  // 观测矩阵
  private H: Matrix;
  // 过程噪声协方差
  private Q: Matrix;
  // 观测噪声协方差
  private R: Matrix;
  // 控制输入矩阵
  private B: Matrix;

  /**
   * 构造卡尔曼滤波器
   * @param stateDim 状态维度
   * @param observeDim 观测维度
   * @param controlDim 控制输入维度
   */
  constructor(stateDim: number, observeDim: number, controlDim: number = 0) {
    this.x = new Matrix(stateDim, 1);
    this.P = new Matrix(stateDim, stateDim).identity().scale(1);
    this.A = new Matrix(stateDim, stateDim).identity();
    this.H = new Matrix(observeDim, stateDim);
    this.Q = new Matrix(stateDim, stateDim).identity().scale(0.01);
    this.R = new Matrix(observeDim, observeDim).identity().scale(1);
    this.B = new Matrix(stateDim, controlDim);
  }

  // Setter方法
  setState(x: Matrix): void { this.x = x; }
  setCovariance(P: Matrix): void { this.P = P; }
  setTransitionMatrix(A: Matrix): void { this.A = A; }
  setObservationMatrix(H: Matrix): void { this.H = H; }
  setProcessNoise(Q: Matrix): void { this.Q = Q; }
  setObservationNoise(R: Matrix): void { this.R = R; }
  setControlMatrix(B: Matrix): void { this.B = B; }

  /**
   * 预测步
   * @param u 控制输入向量
   */
  predict(u?: Matrix): void {
    // 状态预测: x̂ₖ⁻ = A·x̂ₖ₋₁ + B·uₖ
    this.x = this.A.multiply(this.x);
    if (u) {
      this.x = this.x.add(this.B.multiply(u));
    }

    // 协方差预测: Pₖ⁻ = A·Pₖ₋₁·Aᵀ + Q
    this.P = this.A.multiply(this.P).multiply(this.A.transpose()).add(this.Q);
  }

  /**
   * 更新步
   * @param z 观测向量
   */
  update(z: Matrix): void {
    // 卡尔曼增益: Kₖ = Pₖ⁻·Hᵀ·(H·Pₖ⁻·Hᵀ + R)⁻¹
    const Ht = this.H.transpose();
    const S = this.H.multiply(this.P).multiply(Ht).add(this.R);
    const K = this.P.multiply(Ht).multiply(S.inverse());

    // 状态更新: x̂ₖ = x̂ₖ⁻ + Kₖ·(zₖ - H·x̂ₖ⁻)
    const innovation = z.subtract(this.H.multiply(this.x));
    this.x = this.x.add(K.multiply(innovation));

    // 协方差更新: Pₖ = (I - Kₖ·H)·Pₖ⁻
    const I = new Matrix(this.P.rows, this.P.cols).identity();
    this.P = I.subtract(K.multiply(this.H)).multiply(this.P);
  }

  /**
   * 获取当前状态估计
   */
  getState(): Matrix {
    return this.x;
  }

  /**
   * 获取当前协方差
   */
  getCovariance(): Matrix {
    return this.P;
  }

  /**
   * 获取状态向量的某个分量
   */
  getStateComponent(index: number): number {
    return this.x.get(index, 0);
  }
}

3.2 一维卡尔曼滤波应用:气压海拔平滑

// BarometerKalmanFilter.ets - 气压海拔一维卡尔曼滤波

/**
 * 一维卡尔曼滤波器
 * 简化版本,无需矩阵运算
 * 适用于单变量状态估计
 */
export class ScalarKalmanFilter {
  // 状态估计
  private x: number;
  // 估计误差协方差
  private P: number;
  // 过程噪声方差
  private Q: number;
  // 观测噪声方差
  private R: number;

  constructor(initialValue: number = 0, processNoise: number = 0.01, observationNoise: number = 1.0) {
    this.x = initialValue;
    this.P = 1.0;
    this.Q = processNoise;
    this.R = observationNoise;
  }

  /**
   * 预测步(无控制输入)
   */
  predict(): void {
    // 一维情况:x̂⁻ = x̂(状态不变)
    // P⁻ = P + Q
    this.P += this.Q;
  }

  /**
   * 更新步
   * @param z 观测值
   */
  update(z: number): number {
    // 卡尔曼增益: K = P⁻ / (P⁻ + R)
    const K = this.P / (this.P + this.R);

    // 状态更新: x̂ = x̂⁻ + K·(z - x̂⁻)
    this.x = this.x + K * (z - this.x);

    // 协方差更新: P = (1 - K)·P⁻
    this.P = (1 - K) * this.P;

    return this.x;
  }

  /**
   * 一步滤波(预测+更新)
   */
  filter(z: number): number {
    this.predict();
    return this.update(z);
  }

  /**
   * 获取当前估计值
   */
  getValue(): number {
    return this.x;
  }

  /**
   * 设置过程噪声
   */
  setProcessNoise(Q: number): void {
    this.Q = Math.max(0.0001, Q);
  }

  /**
   * 设置观测噪声
   */
  setObservationNoise(R: number): void {
    this.R = Math.max(0.0001, R);
  }
}

/**
 * 气压海拔卡尔曼滤波应用
 */
@Entry
@Component
struct BarometerKalmanPage {
  // 原始气压
  @State rawPressure: number = 1013.25;
  // 滤波后气压
  @State filteredPressure: number = 1013.25;
  // 原始海拔
  @State rawAltitude: number = 0;
  // 滤波后海拔
  @State filteredAltitude: number = 0;
  // 卡尔曼增益显示
  @State kalmanGain: string = '';
  // 过程噪声参数
  @State processNoise: number = 0.01;
  // 观测噪声参数
  @State observationNoise: number = 1.0;

  // 卡尔曼滤波器实例
  private pressureKF: ScalarKalmanFilter = new ScalarKalmanFilter(1013.25, 0.01, 1.0);
  private altitudeKF: ScalarKalmanFilter = new ScalarKalmanFilter(0, 0.1, 5.0);

  /**
   * 处理气压数据
   */
  processBarometerData(pressure: number): void {
    this.rawPressure = Math.round(pressure * 100) / 100;

    // 气压卡尔曼滤波
    this.filteredPressure = Math.round(this.pressureKF.filter(pressure) * 100) / 100;

    // 计算原始海拔
    this.rawAltitude = this.calculateAltitude(pressure);

    // 海拔卡尔曼滤波
    this.filteredAltitude = Math.round(this.altitudeKF.filter(this.rawAltitude) * 10) / 10;
  }

  /**
   * 计算海拔
   */
  private calculateAltitude(pressure: number): number {
    const T0 = 288.15;
    const L = 0.0065;
    const exponent = 0.190263;
    return Math.round((T0 / L) * (1 - Math.pow(pressure / 1013.25, exponent)) * 10) / 10;
  }

  build() {
    Column() {
      Text('气压卡尔曼滤波')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#e0e0e0')
        .margin({ bottom: 24 })

      // 原始 vs 滤波对比
      Row() {
        Column() {
          Text('原始海拔')
            .fontSize(12)
            .fontColor('#888888')
          Text(`${this.rawAltitude}`)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#ff5722')
          Text('m')
            .fontSize(12)
            .fontColor('#888888')
        }
        .layoutWeight(1)

        Column() {
          Text('滤波海拔')
            .fontSize(12)
            .fontColor('#888888')
          Text(`${this.filteredAltitude}`)
            .fontSize(28)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4caf50')
          Text('m')
            .fontSize(12)
            .fontColor('#888888')
        }
        .layoutWeight(1)
      }
      .width('90%')
      .padding(20)
      .borderRadius(16)
      .backgroundColor('#1a1a2e')
      .margin({ bottom: 16 })

      // 参数调节
      Column() {
        Text('滤波参数')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#e0e0e0')
          .margin({ bottom: 12 })

        Row() {
          Text('过程噪声 Q:')
            .fontSize(13)
            .fontColor('#888888')
            .width(100)
          Slider({ value: this.processNoise * 1000, min: 1, max: 100, step: 1 })
            .width('50%')
            .onChange((value: number) => {
              this.processNoise = value / 1000;
              this.altitudeKF.setProcessNoise(this.processNoise);
            })
          Text(`${this.processNoise.toFixed(3)}`)
            .fontSize(13)
            .fontColor('#e0e0e0')
            .width(60)
        }
        .margin({ bottom: 8 })

        Row() {
          Text('观测噪声 R:')
            .fontSize(13)
            .fontColor('#888888')
            .width(100)
          Slider({ value: this.observationNoise, min: 0.1, max: 20, step: 0.1 })
            .width('50%')
            .onChange((value: number) => {
              this.observationNoise = value;
              this.altitudeKF.setObservationNoise(this.observationNoise);
            })
          Text(`${this.observationNoise.toFixed(1)}`)
            .fontSize(13)
            .fontColor('#e0e0e0')
            .width(60)
        }
      }
      .width('90%')
      .padding(20)
      .borderRadius(16)
      .backgroundColor('#1a1a2e')
      .margin({ bottom: 16 })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#0a0a1a')
  }
}

3.3 9轴姿态估计:加速度计+陀螺仪+磁力计融合

// AttitudeEstimator.ets - 9轴姿态估计器
import sensor from '@ohos.sensor';
import { BusinessError } from '@ohos.base';

// 传感器数据接口
interface AccelData { x: number; y: number; z: number; timestamp: number; }
interface GyroData { x: number; y: number; z: number; timestamp: number; }
interface MagData { x: number; y: number; z: number; timestamp: number; }

// 姿态角(欧拉角表示)
interface EulerAngles {
  roll: number;    // 横滚角(-180° ~ 180°)
  pitch: number;   // 俯仰角(-90° ~ 90°)
  yaw: number;     // 航向角(0° ~ 360°)
}

/**
 * 9轴姿态估计器
 * 使用互补滤波融合加速度计、陀螺仪和磁力计
 */
export class AttitudeEstimator {
  // 当前姿态角(度)
  private roll: number = 0;
  private pitch: number = 0;
  private yaw: number = 0;

  // 互补滤波系数
  private alpha: number = 0.98; // 陀螺仪权重

  // 陀螺仪偏置(零偏校准值)
  private gyroBiasX: number = 0;
  private gyroBiasY: number = 0;
  private gyroBiasZ: number = 0;

  // 上次更新时间
  private lastTimestamp: number = 0;

  // 是否已初始化
  private initialized: boolean = false;

  // 磁力计偏置(硬铁校准)
  private magBiasX: number = 0;
  private magBiasY: number = 0;
  private magBiasZ: number = 0;

  /**
   * 更新姿态(融合三个传感器)
   * @param accel 加速度计数据
   * @param gyro 陀螺仪数据
   * @param mag 磁力计数据
   * @returns 当前姿态角
   */
  update(accel: AccelData, gyro: GyroData, mag: MagData): EulerAngles {
    const now = accel.timestamp;
    let dt = (now - this.lastTimestamp) / 1e9; // 纳秒转秒
    this.lastTimestamp = now;

    // 限制dt范围,防止首帧或异常值
    if (dt <= 0 || dt > 0.5) dt = 0.01;

    // 从加速度计计算倾斜角(低频参考)
    const accelRoll = Math.atan2(accel.y, accel.z) * (180 / Math.PI);
    const accelPitch = Math.atan2(-accel.x,
      Math.sqrt(accel.y * accel.y + accel.z * accel.z)) * (180 / Math.PI);

    // 从磁力计计算航向角(低频参考)
    // 先将磁力计数据旋转到水平面
    const magX = mag.x - this.magBiasX;
    const magY = mag.y - this.magBiasY;
    const magZ = mag.z - this.magBiasZ;

    const rollRad = this.roll * (Math.PI / 180);
    const pitchRad = this.pitch * (Math.PI / 180);

    // 磁力计倾斜补偿
    const magHorizontalX = magX * Math.cos(pitchRad) +
      magY * Math.sin(rollRad) * Math.sin(pitchRad) +
      magZ * Math.cos(rollRad) * Math.sin(pitchRad);
    const magHorizontalY = magY * Math.cos(rollRad) -
      magZ * Math.sin(rollRad);

    const magYaw = Math.atan2(-magHorizontalY, magHorizontalX) * (180 / Math.PI);
    // 转换为0-360度
    const magYaw360 = magYaw < 0 ? magYaw + 360 : magYaw;

    // 陀螺仪积分(高频参考)
    const gyroX = (gyro.x - this.gyroBiasX) * (180 / Math.PI); // rad/s -> deg/s
    const gyroY = (gyro.y - this.gyroBiasY) * (180 / Math.PI);
    const gyroZ = (gyro.z - this.gyroBiasZ) * (180 / Math.PI);

    if (!this.initialized) {
      // 首次使用加速度计和磁力计初始化
      this.roll = accelRoll;
      this.pitch = accelPitch;
      this.yaw = magYaw360;
      this.initialized = true;
      return { roll: this.roll, pitch: this.pitch, yaw: this.yaw };
    }

    // 互补滤波融合
    // Roll: 陀螺仪积分 + 加速度计修正
    this.roll = this.alpha * (this.roll + gyroX * dt) +
      (1 - this.alpha) * accelRoll;

    // Pitch: 陀螺仪积分 + 加速度计修正
    this.pitch = this.alpha * (this.pitch + gyroY * dt) +
      (1 - this.alpha) * accelPitch;

    // Yaw: 陀螺仪积分 + 磁力计修正
    // 航向角需要特殊处理(角度环绕)
    let gyroYaw = this.yaw + gyroZ * dt;
    // 确保在0-360范围内
    gyroYaw = ((gyroYaw % 360) + 360) % 360;

    // 航向角互补滤波(处理角度环绕)
    let yawDiff = magYaw360 - gyroYaw;
    if (yawDiff > 180) yawDiff -= 360;
    if (yawDiff < -180) yawDiff += 360;

    this.yaw = gyroYaw + (1 - this.alpha) * yawDiff;
    this.yaw = ((this.yaw % 360) + 360) % 360;

    return {
      roll: Math.round(this.roll * 10) / 10,
      pitch: Math.round(this.pitch * 10) / 10,
      yaw: Math.round(this.yaw * 10) / 10
    };
  }

  /**
   * 陀螺仪零偏校准
   * 在设备静止时调用,采集N个样本取平均
   * @param samples 校准样本数组
   */
  calibrateGyro(samples: GyroData[]): void {
    if (samples.length === 0) return;

    let sumX = 0, sumY = 0, sumZ = 0;
    for (const s of samples) {
      sumX += s.x;
      sumY += s.y;
      sumZ += s.z;
    }

    this.gyroBiasX = sumX / samples.length;
    this.gyroBiasY = sumY / samples.length;
    this.gyroBiasZ = sumZ / samples.length;
  }

  /**
   * 磁力计硬铁校准
   * @param biasX X轴偏置
   * @param biasY Y轴偏置
   * @param biasZ Z轴偏置
   */
  calibrateMagnetometer(biasX: number, biasY: number, biasZ: number): void {
    this.magBiasX = biasX;
    this.magBiasY = biasY;
    this.magBiasZ = biasZ;
  }

  /**
   * 设置互补滤波系数
   * @param alpha 陀螺仪权重(0-1),越大越信任陀螺仪
   */
  setAlpha(alpha: number): void {
    this.alpha = Math.max(0.5, Math.min(0.999, alpha));
  }

  /**
   * 获取当前姿态
   */
  getAttitude(): EulerAngles {
    return {
      roll: Math.round(this.roll * 10) / 10,
      pitch: Math.round(this.pitch * 10) / 10,
      yaw: Math.round(this.yaw * 10) / 10
    };
  }

  /**
   * 重置姿态估计器
   */
  reset(): void {
    this.roll = 0;
    this.pitch = 0;
    this.yaw = 0;
    this.initialized = false;
    this.lastTimestamp = 0;
  }
}

/**
 * 9轴姿态估计页面
 */
@Entry
@Component
struct AttitudeEstimationPage {
  // 姿态角
  @State roll: number = 0;
  @State pitch: number = 0;
  @State yaw: number = 0;
  // 传感器原始数据
  @State accelText: string = '等待数据...';
  @State gyroText: string = '等待数据...';
  @State magText: string = '等待数据...';
  // 状态
  @State isRunning: boolean = false;
  @State statusText: string = '未启动';

  // 姿态估计器
  private estimator: AttitudeEstimator = new AttitudeEstimator();
  // 传感器订阅ID
  private accelSubId: number = -1;
  private gyroSubId: number = -1;
  private magSubId: number = -1;
  // 传感器数据缓存
  private latestAccel: AccelData | null = null;
  private latestGyro: GyroData | null = null;
  private latestMag: MagData | null = null;
  // 陀螺仪校准样本
  private gyroCalibrationSamples: GyroData[] = [];
  private isCalibrating: boolean = false;

  aboutToAppear() {
    this.startSensors();
  }

  aboutToDisappear() {
    this.stopSensors();
  }

  /**
   * 启动传感器
   */
  private startSensors(): void {
    try {
      // 加速度计
      if (sensor.isSensorAvailable(sensor.SensorType.ACCELEROMETER)) {
        this.accelSubId = sensor.on(sensor.SensorType.ACCELEROMETER,
          (data: AccelData) => {
            this.latestAccel = data;
            this.accelText = `X:${data.x.toFixed(2)} Y:${data.y.toFixed(2)} Z:${data.z.toFixed(2)}`;
            this.tryUpdateAttitude();
          }, { interval: 10000000 }); // 10ms
      }

      // 陀螺仪
      if (sensor.isSensorAvailable(sensor.SensorType.GYROSCOPE)) {
        this.gyroSubId = sensor.on(sensor.SensorType.GYROSCOPE,
          (data: GyroData) => {
            this.latestGyro = data;
            this.gyroText = `X:${data.x.toFixed(2)} Y:${data.y.toFixed(2)} Z:${data.z.toFixed(2)}`;
            if (this.isCalibrating) {
              this.gyroCalibrationSamples.push(data);
            }
            this.tryUpdateAttitude();
          }, { interval: 10000000 });
      }

      // 磁力计
      if (sensor.isSensorAvailable(sensor.SensorType.MAGNETOMETER)) {
        this.magSubId = sensor.on(sensor.SensorType.MAGNETOMETER,
          (data: MagData) => {
            this.latestMag = data;
            this.magText = `X:${data.x.toFixed(2)} Y:${data.y.toFixed(2)} Z:${data.z.toFixed(2)}`;
            this.tryUpdateAttitude();
          }, { interval: 20000000 }); // 20ms
      }

      this.isRunning = true;
      this.statusText = '运行中';
    } catch (error) {
      this.statusText = `启动失败: ${(error as BusinessError).message}`;
    }
  }

  private stopSensors(): void {
    if (this.accelSubId !== -1) {
      try { sensor.off(sensor.SensorType.ACCELEROMETER, this.accelSubId); } catch (e) {}
    }
    if (this.gyroSubId !== -1) {
      try { sensor.off(sensor.SensorType.GYROSCOPE, this.gyroSubId); } catch (e) {}
    }
    if (this.magSubId !== -1) {
      try { sensor.off(sensor.SensorType.MAGNETOMETER, this.magSubId); } catch (e) {}
    }
    this.isRunning = false;
    this.statusText = '已停止';
  }

  /**
   * 尝试更新姿态(需要三个传感器都有数据)
   */
  private tryUpdateAttitude(): void {
    if (!this.latestAccel || !this.latestGyro || !this.latestMag) {
      return;
    }

    const attitude = this.estimator.update(
      this.latestAccel, this.latestGyro, this.latestMag
    );

    this.roll = attitude.roll;
    this.pitch = attitude.pitch;
    this.yaw = attitude.yaw;
  }

  /**
   * 开始陀螺仪校准
   */
  private startGyroCalibration(): void {
    this.isCalibrating = true;
    this.gyroCalibrationSamples = [];
    this.statusText = '校准中...请保持设备静止';

    // 3秒后自动完成校准
    setTimeout(() => {
      this.estimator.calibrateGyro(this.gyroCalibrationSamples);
      this.isCalibrating = false;
      this.statusText = `校准完成(${this.gyroCalibrationSamples.length}个样本)`;
      this.gyroCalibrationSamples = [];
    }, 3000);
  }

  /**
   * 获取方向描述
   */
  private getDirectionText(yaw: number): string {
    if (yaw >= 337.5 || yaw < 22.5) return '北';
    if (yaw >= 22.5 && yaw < 67.5) return '东北';
    if (yaw >= 67.5 && yaw < 112.5) return '东';
    if (yaw >= 112.5 && yaw < 157.5) return '东南';
    if (yaw >= 157.5 && yaw < 202.5) return '南';
    if (yaw >= 202.5 && yaw < 247.5) return '西南';
    if (yaw >= 247.5 && yaw < 292.5) return '西';
    return '西北';
  }

  build() {
    Scroll() {
      Column() {
        // 标题
        Text('9轴姿态估计')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#e0e0e0')
          .margin({ bottom: 24 })

        // 姿态角显示
        Column() {
          Text('姿态角')
            .fontSize(14)
            .fontColor('#888888')

          Row() {
            Column() {
              Text('Roll')
                .fontSize(12)
                .fontColor('#888888')
              Text(`${this.roll}°`)
                .fontSize(28)
                .fontWeight(FontWeight.Bold)
                .fontColor('#e94560')
            }
            .layoutWeight(1)

            Column() {
              Text('Pitch')
                .fontSize(12)
                .fontColor('#888888')
              Text(`${this.pitch}°`)
                .fontSize(28)
                .fontWeight(FontWeight.Bold)
                .fontColor('#533483')
            }
            .layoutWeight(1)

            Column() {
              Text('Yaw')
                .fontSize(12)
                .fontColor('#888888')
              Text(`${this.yaw}°`)
                .fontSize(28)
                .fontWeight(FontWeight.Bold)
                .fontColor('#0f3460')
            }
            .layoutWeight(1)
          }

          // 方向指示
          Text(`方向: ${this.getDirectionText(this.yaw)}`)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4caf50')
            .margin({ top: 8 })
        }
        .width('90%')
        .padding(24)
        .borderRadius(16)
        .backgroundColor('#1a1a2e')
        .margin({ bottom: 16 })

        // 传感器原始数据
        Column() {
          Text('传感器原始数据')
            .fontSize(14)
            .fontColor('#888888')
            .margin({ bottom: 8 })

          Text(`加速度: ${this.accelText}`)
            .fontSize(12)
            .fontColor('#e94560')
            .width('100%')
            .margin({ bottom: 4 })
          Text(`陀螺仪: ${this.gyroText}`)
            .fontSize(12)
            .fontColor('#533483')
            .width('100%')
            .margin({ bottom: 4 })
          Text(`磁力计: ${this.magText}`)
            .fontSize(12)
            .fontColor('#0f3460')
            .width('100%')
        }
        .width('90%')
        .padding(16)
        .borderRadius(12)
        .backgroundColor('#1a1a2e')
        .alignItems(HorizontalAlign.Start)
        .margin({ bottom: 16 })

        // 校准按钮
        Button(this.isCalibrating ? '校准中...' : '陀螺仪校准')
          .fontSize(14)
          .fontColor('#e0e0e0')
          .backgroundColor(this.isCalibrating ? '#333333' : '#0f3460')
          .borderRadius(8)
          .enabled(!this.isCalibrating)
          .onClick(() => this.startGyroCalibration())
          .margin({ bottom: 16 })

        // 状态
        Text(this.statusText)
          .fontSize(12)
          .fontColor('#888888')
      }
      .width('100%')
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#0a0a1a')
  }
}

四、踩坑与注意事项

4.1 传感器融合常见问题

问题 原因 解决方案
姿态角漂移 陀螺仪零偏未校准 静止时进行零偏校准,运行中持续在线校准
航向角跳变 磁力计受铁磁干扰 检测磁异常并临时降低磁力计权重
万向节锁 欧拉角在Pitch=±90°时退化 使用四元数表示姿态,避免欧拉角奇异
传感器时间不对齐 不同传感器采样率不同 使用最近时刻的数据,或进行时间插值
融合结果震荡 互补滤波系数不当 调整α值,通常0.95-0.98效果较好

4.2 四元数vs欧拉角

// 四元数表示(避免万向节锁)
interface Quaternion {
  w: number;  // 标量部分
  x: number;  // 向量部分
  y: number;
  z: number;
}

/**
 * 四元数乘法
 * 用于组合旋转
 */
function quaternionMultiply(q1: Quaternion, q2: Quaternion): Quaternion {
  return {
    w: q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z,
    x: q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y,
    y: q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x,
    z: q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w
  };
}

/**
 * 四元数归一化
 * 防止数值误差累积导致四元数长度偏离1
 */
function quaternionNormalize(q: Quaternion): Quaternion {
  const norm = Math.sqrt(q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z);
  if (norm < 1e-10) return { w: 1, x: 0, y: 0, z: 0 };
  return {
    w: q.w / norm,
    x: q.x / norm,
    y: q.y / norm,
    z: q.z / norm
  };
}

/**
 * 四元数转欧拉角
 */
function quaternionToEuler(q: Quaternion): EulerAngles {
  // Roll
  const sinr_cosp = 2 * (q.w * q.x + q.y * q.z);
  const cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y);
  const roll = Math.atan2(sinr_cosp, cosr_cosp) * (180 / Math.PI);

  // Pitch
  const sinp = 2 * (q.w * q.y - q.z * q.x);
  const pitch = Math.abs(sinp) >= 1 ?
    Math.sign(sinp) * 90 :
    Math.asin(sinp) * (180 / Math.PI);

  // Yaw
  const siny_cosp = 2 * (q.w * q.z + q.x * q.y);
  const cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z);
  const yaw = Math.atan2(siny_cosp, cosy_cosp) * (180 / Math.PI);

  return { roll, pitch, yaw: ((yaw % 360) + 360) % 360 };
}

4.3 卡尔曼滤波参数调优

过程噪声Q

  • Q越大,滤波器响应越快,但平滑效果越差
  • Q越小,滤波器越平滑,但响应越慢
  • 气压海拔:Q = 0.01~0.1(海拔变化缓慢)
  • 姿态估计:Q = 0.1~1.0(姿态变化较快)

观测噪声R

  • R越大,越不信任观测值,更依赖预测
  • R越小,越信任观测值,但噪声抑制能力差
  • 气压海拔:R = 1.0~5.0(传感器精度约±0.5hPa)
  • 加速度计角度:R = 5.0~20.0(受运动加速度干扰大)

调优经验

  1. 先固定R,调整Q使响应速度合适
  2. 再固定Q,调整R使平滑效果满意
  3. Q/R的比值决定了滤波器的等效时间常数

4.4 性能优化建议

  • 矩阵运算优化:对于固定维度的卡尔曼滤波,硬编码矩阵运算比通用矩阵类更高效
  • 使用Float32Array:传感器数据使用TypedArray存储,减少GC压力
  • 降低融合频率:姿态估计50Hz足够,不需要与传感器采样率一致
  • Worker线程:复杂的EKF计算移至Worker线程,避免阻塞UI
  • 增量更新:只更新变化的矩阵元素,避免全矩阵运算

五、HarmonyOS 6适配

5.1 新版传感器API变化

变化项 HarmonyOS 5 HarmonyOS 6
传感器订阅 sensor.on(type, callback) sensor.subscribe(type, callback, options)
数据结构 {x, y, z, timestamp} {x, y, z, timestamp, accuracy}(新增精度字段)
旋转向量传感器 不支持 SensorType.ROTATION_VECTOR(系统级融合姿态)
游戏旋转向量 不支持 SensorType.GAME_ROTATION_VECTOR(无磁力计融合)
批量订阅 不支持 sensor.subscribeMultiple(types[], callback)

5.2 使用系统级旋转向量传感器

HarmonyOS 6提供了系统级融合的旋转向量传感器,可以直接获取融合后的姿态四元数,无需自行实现融合算法:

// HarmonyOS 6 旋转向量传感器(系统级融合)
import sensor from '@ohos.sensor';

// 旋转向量数据
interface RotationVectorData {
  x: number;       // 四元数向量部分x
  y: number;       // 四元数向量部分y
  z: number;       // 四元数向量部分z
  w: number;       // 四元数标量部分w
  headingAccuracy: number; // 航向精度(弧度)
  timestamp: number;
}

class SystemFusedAttitude {
  private subscriberId: number = -1;

  /**
   * 订阅系统级融合姿态
   * 直接获取加速度计+陀螺仪+磁力计融合后的四元数
   */
  subscribeAttitude(callback: (q: Quaternion, euler: EulerAngles) => void): void {
    try {
      // 优先使用旋转向量传感器(含磁力计)
      if (sensor.isSensorAvailable(sensor.SensorType.ROTATION_VECTOR)) {
        this.subscriberId = sensor.on(
          sensor.SensorType.ROTATION_VECTOR,
          (data: RotationVectorData) => {
            const q: Quaternion = {
              w: data.w, x: data.x, y: data.y, z: data.z
            };
            const euler = quaternionToEuler(q);
            callback(q, euler);
          },
          { interval: 20000000 } // 20ms = 50Hz
        );
      }
      // 降级到游戏旋转向量(不含磁力计,无绝对航向)
      else if (sensor.isSensorAvailable(sensor.SensorType.GAME_ROTATION_VECTOR)) {
        this.subscriberId = sensor.on(
          sensor.SensorType.GAME_ROTATION_VECTOR,
          (data: RotationVectorData) => {
            const q: Quaternion = {
              w: data.w, x: data.x, y: data.y, z: data.z
            };
            const euler = quaternionToEuler(q);
            callback(q, euler);
          },
          { interval: 20000000 }
        );
      } else {
        // 最终降级:自行实现融合
        console.warn('旋转向量传感器不可用,请使用自实现融合算法');
      }
    } catch (error) {
      console.error('姿态传感器订阅失败');
    }
  }
}

5.3 HarmonyOS 6传感器批量订阅

// HarmonyOS 6 批量传感器订阅
import sensor from '@ohos.sensor';

class BatchSensorSubscription {
  /**
   * 批量订阅多个传感器
   * 所有传感器数据通过统一回调返回,自动时间对齐
   */
  subscribeMultiple(): void {
    try {
      if (typeof sensor.subscribeMultiple === 'function') {
        sensor.subscribeMultiple(
          [
            sensor.SensorType.ACCELEROMETER,
            sensor.SensorType.GYROSCOPE,
            sensor.SensorType.MAGNETOMETER
          ],
          (data: Record<string, Object>) => {
            // data包含所有传感器的最新数据
            const accel = data['ACCELEROMETER'] as AccelData;
            const gyro = data['GYROSCOPE'] as GyroData;
            const mag = data['MAGNETOMETER'] as MagData;

            // 直接融合,无需手动对齐时间戳
            if (accel && gyro && mag) {
              // 调用融合算法...
            }
          },
          { samplingPeriod: 10000000 } // 10ms
        );
      }
    } catch (error) {
      console.error('批量订阅失败');
    }
  }
}

六、总结

6.1 核心技术要点回顾

技术要点 关键实现
卡尔曼滤波 预测步(x̂⁻=Ax̂, P⁻=APAᵀ+Q) + 更新步(K=PHᵀ(HPHᵀ+R)⁻¹, x̂=x̂⁻+K(z-Hx̂⁻))
扩展卡尔曼滤波 非线性函数雅可比化 + 标准KF迭代
互补滤波 α·陀螺仪积分 + (1-α)·加速度计角度
9轴姿态估计 加速度计(倾斜) + 陀螺仪(角速度) + 磁力计(航向)
四元数 避免万向节锁,q=(w,x,y,z)表示3D旋转
楼层判定 气压差→高度差→楼层偏移

6.2 最佳实践清单

  1. ✅ 优先使用系统级旋转向量传感器,减少自行融合的开发量
  2. ✅ 自行融合时使用互补滤波作为基础,卡尔曼滤波作为进阶
  3. ✅ 使用四元数表示姿态,避免欧拉角万向节锁问题
  4. ✅ 陀螺仪使用前必须进行零偏校准
  5. ✅ 磁力计使用前进行硬铁/软铁校准
  6. ✅ 卡尔曼滤波参数Q/R根据应用场景调优
  7. ✅ 传感器数据时间对齐是融合的前提
  8. ✅ 适配HarmonyOS 6旋转向量传感器和批量订阅

6.3 扩展方向

  • 无迹卡尔曼滤波(UKF):处理强非线性系统,无需计算雅可比矩阵
  • 粒子滤波:处理非高斯噪声和非线性系统
  • 深度学习融合:使用神经网络学习最优融合权重
  • 分布式传感器融合:多设备传感器协同,提高估计精度
  • VIO(视觉惯性里程计):相机+IMU融合,实现6DoF位姿估计
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。