
拿到IMU原始数据那一刻不少人的第一反应是为什么静止时加速度计的Z轴还稳在9.8附近是不是传感器坏了其实没坏那是重力分量结结实实地叠在测量值里。做机器人、SLAM、自动驾驶、姿态解算的朋友迟早都要面对“加速度计重力消除”这一步——它是位姿估计、运动加速度提取、多传感器融合的共同前提。这篇文章我从加速度计测量原理讲起对比欧拉角、旋转矩阵、四元数三种重力提取路径再给出一套完整可复现的Python处理流程最后重点聊聊那些原理书上不会写的工程坑包括滤波器收敛、滤波时序、陀螺零偏、时间戳对齐这类真实场景里最容易翻车的地方。1. 加速度计测量原理为什么永远带着一个9.81.1 比力方程加速度计测的到底是什么很多刚接触惯导的人会把加速度计当成“运动加速度计”觉得加速度计输出直接就是物体动的快慢变化。这是个非常顽固的误解。加速度计本质上测的不是运动加速度而是比力specific force也就是单位质量感受到的惯性力与引力之和按常用约定可以写成 f a - g或者从测量角度说输出中天然包含了重力场的影响。拿生活中的例子来类比你把手机平放在桌面上打开加速度计App能看到Z轴读数是9.8左右。按“运动加速度”的理解手机没动应该读0但为什么是9.8因为桌面给了手机一个向上的支撑力支撑力让手机内部的质量块产生微小位移加速度计测量并输出了这个等效加速度。换句话说加速度计感受到的是“除了重力之外还有谁在推它”。在自由落体状态下加速度计反而会输出近似0就是因为失去了支撑质量块处于失重状态。从这个角度看“静止时加速度计0”才是真正奇怪的事。工程上我们要做重力消除其实就是在比力方程中把重力项分离出去把加速度计测得的总比力减去重力在传感器坐标系下的投影剩下的才是由运动引起的线性加速度。1.2 坐标系约定NED、ENU与正负号的三角关系重力消除之所以容易翻车有一半的坑都出在坐标系约定上。IMU数据手册里一般会画坐标轴指向常见的两个约定是NED北东地和ENU东北天。NED里重力向量是 (0, 0, 9.8)ENU里重力向量是 (0, 0, -9.8)。别小看这一个正负号它会让你的重力消除结果差出一个2g的直流分量。实际处理中我最常做的一件事就是在写重力消除代码前先用一句注释固定住约定。比如世界坐标系ENU重力向量 g_W [0, 0, -9.80665]IMU坐标系与机体固连X前Y左Z上或X右Y前Z上具体看数据手册姿态旋转矩阵 R表示从世界系到机体系的旋转当你发现静止时加速度计输出是 (0, 0, -9.8)说明Z轴朝上世界系用的就是ENU如果静止时输出是 (0, 0, 9.8)大概率是NED或Z轴朝下。先花两分钟确认这个约定能省下后面一整天的排查时间。1.3 重力消除的本质目标说穿了重力消除就做一件事把 a_meas加速度计原始输出中的重力投影 g_body 减掉得到运动加速度 a_motiona_motion a_meas - g_body这里的 g_body 是重力向量在世界系中的表示经过姿态旋转之后在IMU坐标系中的投影。如果姿态准确那么静止时 a_motion 应该接近 [0, 0, 0]只剩传感器噪声运动时 a_motion 是纯运动加速度可以用做二次积分、步态识别、急加速检测、振动特征提取等。这个操作看起来简单真正做起来牵涉姿态估计、坐标变换、滤波时序和零偏处理任何一个环节出问题最后得到的“运动加速度”都会带着奇怪的残差。2. 重力分量提取三种路径与各自暗藏的坑2.1 欧拉角法公式不难但别在姿态剧烈变化时硬用重力分量提取最直观的方式就是先求姿态角roll、pitch再把世界系重力投影到机体系。静止或缓慢运动时可以用加速度计本身来估计初始的roll和pitchroll atan2(a_y, a_z) pitch atan2(-a_x, sqrt(a_y^2 a_z^2))有了roll和pitch就可以按Z-Y-X或别的旋转顺序构造旋转矩阵进而把 [0, 0, g]或 [0, 0, -g]转到机体系。这个方案有两个“软肋”第一动态运动时加速度计的输出包含了运动加速度直接用这个输出去求roll和pitch得到的姿态本身就有误差。用有误差的姿态去做重力投影等于在抖动的地基上盖房子减完重力的数据里会残留一部分与运动相关的错误分量。第二欧拉角存在万向锁问题。pitch接近90°时roll和yaw退化成同一个自由度atan2计算不稳定重力投影也跟着失真。所以欧拉角法只适合姿态变化平缓、低动态的场景或者只用来做初始对准。对于绝大多数需要实时解算的项目来说它不是最优选。2.2 旋转矩阵法工程落地时我最推荐的选择姿态解算输出的旋转矩阵 R 把世界坐标系映射到IMU坐标系或反过来取决于你的定义。用旋转矩阵做重力投影非常直接g_body R * g_W这个式子不受万向锁影响适用于任意姿态数值稳定性好而且在很多IMU库如Madgwick、Mahony内部已经把姿态表示成了四元数转成旋转矩阵就是一次矩阵运算方便得很。当重力向量在世界系中定义为 g_W [0, 0, -9.80665]ENU时机体系重力投影的具体展开式是g_body_x -9.80665 * (2*(q.xq.z q.wq.y)) g_body_y -9.80665 * (2*(q.yq.z - q.wq.x)) g_body_z -9.80665 * (1 - 2*(q.xq.x q.yq.y))这里的 q 是单位四元数姿态从世界系转到机体系。如果你定义的是NED正负号整体反过来即可。用旋转矩阵法的好处是你不需要单独去算roll和pitch姿态滤波输出的四元数直接拿来用就行代码也简单不容易出错。我接触过的VINS-Mono、MSCKF这类开源方案预积分公式里用的也是类似思路直接拿旋转矩阵或四元数把重力从世界系变换到IMU系再叠加进预积分项。2.3 四元数法计算省事但最容易在共轭和归一化上翻车四元数法本质上是旋转矩阵法的代数简写。重力投影可以用四元数旋转公式实现g_body q ⊗ g_W ⊗ q*其中 q* 是单位四元数的共轭。这里要特别小心两个问题。第一个问题q* 的方向。四元数旋转的左右顺序非常容易搞混是把世界向量转到机体系还是把机体系向量转到世界系公式里的 q 和 q* 位置完全不同。不同开源库对“姿态四元数”的定义又不一样有的表示世界到机体有的表示机体到世界照抄公式时特别容易方向反掉。反掉的结果就是重力消除后静止数据不再接近0而会出现一个大小约2g、随时间缓慢变化的残差。第二个问题四元数必须归一化。四元数只有单位模长时q ⊗ v ⊗ q* 才等价于一个纯旋转不会缩放向量长度。如果你拿到的是滤波器输出但忘了归一化或者滤波器数值稳定性差导致四元数模长漂移重力投影的模长就不再等于9.8减完重力后会留下一个与姿态相关的缩放残差。这个残差在静止时看着像噪声动态时会被放大非常迷惑。我见过不少调试者在一个问题上卡了好几天重力消除后静止数据在某一轴上总有一个稳定的小偏置怎么加零偏校正都去不掉。最后发现是四元数模长从1漂到了1.03重力投影变成了10.1减完自然剩个大约0.3的偏置。把 q 归一化之后立竿见影。2.4 三种方案对比速查方案适用场景主要风险计算量欧拉角法低动态、初始对准万向锁、动态下姿态误差大低旋转矩阵法通用工程场景推荐需要确认矩阵方向中四元数法嵌入式、实时计算共轭方向、归一化问题多低从技术选型角度我统一推荐旋转矩阵法作为主方案四元数法作为嵌入式优化时的备选欧拉角法只用来做人眼观察或初始值估计。理由很简单旋转矩阵法在代码可读性、调试难度、鲁棒性上综合最优。3. 实战流程用Python做一次完整的加速度计重力消除3.1 第一步零偏校正与数据熟悉拿到了IMU原始数据先别急着写重力消除。第一步应该是画图、看数据、评估零偏。把设备静止放在桌面上采30秒对加速度计三个轴求均值。水平放置时Z轴应该有约±9.8X、Y轴接近0。但真实传感器总有点零偏比如X轴可能稳定输出0.03而不是0Z轴可能是9.79而不是9.81。这些零偏如果不处理后面重力消除后的运动加速度里会带直流分量。零偏处理的常规做法静止段求平均在后续每个样本里减掉这个均值。注意这里减零偏是在重力消除之前做还是之后做严格来说差距不大但工程上我习惯先减零偏再做重力消除因为零偏是传感器自身误差不是运动或重力引起的先去掉以后后续姿态解算的输入更干净。如果零偏随温度变化明显最好做温度补偿或者至少让设备预热之后再采集。很多消费级IMU冷启动和热机半小时后的零偏能差出0.1g以上这对重力消除结果来说是很大的误差源。3.2 第二步姿态估计——不能直接用加速度计算角度想要把重力投影到机体系首先要有机体系相对世界系的姿态。这个姿态可以通过两类信息融合获得陀螺仪的角速度积分和加速度计/磁力计的姿态观测。如果你直接用加速度计算出的roll、pitch来做重力消除会遇到一个逻辑悖论加速度计里混着运动加速度你用混着运动加速度的测量值去求姿态再用这个姿态去分离重力分离结果自然不干净。所以正确的做法是用陀螺仪积分姿态再用加速度计对roll、pitch做慢速修正——这正是互补滤波和Kalman滤波在做的事。这里我贴一段简化版的Mahony姿态解算核心逻辑适合第一次搭IMU处理的同学参考import numpy as np def mahony_update(q, gyro, accel, dt, kp0.5, ki0.0): # q: 姿态四元数 [w, x, y, z]世界系到机体系 # gyro: 角速度 rad/s # accel: 加速度计原始测量 m/s^2已减零偏 if np.linalg.norm(accel) 0.1: # 加速度模长太小接近自由落体不做修正 return q # 归一化加速度 accel accel / np.linalg.norm(accel) # 根据当前姿态预测重力方向在机体系下的分量 g_body np.array([ 2.0 * (q[1]*q[3] - q[0]*q[2]), 2.0 * (q[0]*q[1] q[2]*q[3]), q[0]*q[0] - q[1]*q[1] - q[2]*q[2] q[3]*q[3] ]) # 测量得到的重力方向与实际预测方向做叉积得到误差 error np.cross(accel, g_body) # 陀螺仪修正 gyro_corrected gyro kp * error # 四元数微分方程更新 q_dot 0.5 * np.array([ [-q[1], -q[2], -q[3]], [ q[0], -q[3], q[2]], [ q[3], q[0], -q[1]], [-q[2], q[1], q[0]] ]).dot(gyro_corrected) q_new q q_dot * dt q_new q_new / np.linalg.norm(q_new) return q_new这段代码里加速度计的作用是修正陀螺仪积分带来的慢漂而不是直接被当作姿态角来源。kp调大一点姿态会更信任加速度计但动态运动会引入更多误差调小一点姿态更平滑但静态漂移会大一些。具体数值要靠实验试。3.3 第三步用四元数做重力投影与逐样本消除姿态拿到了重力消除就是一步矩阵运算的事。我常用的处理函数长这样GRAVITY 9.80665 # 按ENU重力在世界系为 [0, 0, -GRAVITY] def remove_gravity(accel, quat): accel: 原始加速度计测量形状 (N, 3)单位 m/s^2 quat: 姿态四元数形状 (N, 4)世界系到机体系 accel_out np.zeros_like(accel) for i in range(len(accel)): q quat[i] q q / np.linalg.norm(q) # 重力在机体系下的投影 g_i np.array([ 2.0*(q[1]*q[3] - q[0]*q[2]), 2.0*(q[0]*q[1] q[2]*q[3]), q[0]*q[0] - q[1]*q[1] - q[2]*q[2] q[3]*q[3] ]) * (-GRAVITY) # 消除重力 accel_out[i] accel[i] - g_i return accel_out这里最关键的是 g_body 的展开式它本质上是前面提到的四元数旋转公式展开。它和Mahony更新里的 g_body 形式几乎一样唯一的区别是上一步里用了归一化的方向这一步要乘上重力加速度大小。实际处理大量数据时这个Python循环会有点慢。可以用numpy写矢量化版本效果完全一样def remove_gravity_vectorized(accel, quat): qw, qx, qy, qz quat[:, 0], quat[:, 1], quat[:, 2], quat[:, 3] norm np.linalg.norm(quat, axis1, keepdimsTrue) qw, qx, qy, qz qw/norm[:, 0], qx/norm[:, 0], qy/norm[:, 0], qz/norm[:, 0] g_body np.stack([ 2.0*(qx*qz - qw*qy), 2.0*(qy*qz qw*qx), qw*qw - qx*qx - qy*qy qz*qz ], axis1) * (-GRAVITY) return accel - g_body把向量化版本跑在大规模数据上速度能快几十倍。如果你处理的是长时间车载或穿戴式数据这一步节省的时间很可观。3.4 第四步结果验证——怎么判断重力消除做对了重力消除做没做对不能靠肉眼扫一眼曲线就下结论。我建议按照下面三个检查项逐条验证第一静态验证。把设备静止放置跑完整条处理链。理想情况下消除重力后的三轴加速度应该贴在0附近波动幅度由传感器噪声决定典型范围是±0.01到±0.05 m/s²。如果静态段消除后出现明显偏置优先检查零偏、姿态滤波器收敛情况和四元数归一化。第二旋转验证。手持设备在静止状态下快速旋转90度或180度旋转过程里姿态变化很快消除重力后的加速度曲线不应该出现一个明显的9.8幅值的脉冲。如果出现说明姿态估计滞后或者旋转矩阵方向反了。第三运动验证。做一次已知运动模式的测试比如沿着某个轴平移加速、急停。消除重力后对应轴的加速度应该合理解释运动特征。这一步要和外部参考比如编码器、视觉里程计、高速相机交叉验证如果只有IMU数据至少要检查运动段的总加速度模长|a_motion|是否在合理范围不应长时间显著偏大。这些验证步骤是我自己调试时反复使用的“黄金三重检”。任何一步不通过都不要急着把数据喂给下游的位姿解算或融合算法。4. 工程避坑这些场景下重力消除会“看起来正常但实际是错的”4.1 滤波器收敛前的几百毫秒姿态还没站稳重力投影也是错的姿态滤波器和很多迭代算法一样需要时间收敛。开机瞬间陀螺仪的积分从初始姿态开始如果初始姿态给得不准比如默认四元数全是0滤波器要先靠加速度计把roll和pitch拉回来这个过程可能要几百毫秒甚至更久。在这段收敛时间里重力投影的方向是慢慢“歪”向正确位置的减完重力后的数据会呈现一个明显的斜坡或一段偏置。很多人看到启动段数据异常以为是传感器坏了或代码写错了其实是滤波器还没醒过来。解决方法有两个一是初始化时先用加速度计估算初始roll和pitch构造四元数作为滤波器初值二是直接丢弃启动后前0.5秒到1秒的数据不参与后续分析。做离线处理时第二种方法简单粗暴且有效。4.2 滤波顺序先消除重力再滤波还是先滤波再消除重力很多人在做重力消除前喜欢先对加速度计数据做低通滤波觉得先把高频噪声滤掉再做后续处理会干净一些。这个操作本身没毛病但顺序有讲究。重力分量是低频信号姿态变化不剧烈时几乎就是直流低通滤波后它几乎原样保留所以“先滤波再消除重力”对重力项影响不大。但运动加速度往往包含中高频成分低通滤波器会引入时间延迟让运动加速度的峰值在时间上滞后。你消掉的是滞后后的加速度测量值里的重力分量但姿态航向即重力投影方向没有跟着滞后结果在快速机动或急转弯的时刻消除重力后的数据里会出现正负交替的脉冲——这不是真实运动而是滤波器相位失配的产物。我的实践顺序是先用陀螺仪加速度计做原始姿态解算不滤波或仅轻度滤波随后立即用原始姿态做重力消除最后再对消除重力后的运动加速度做低通/带通滤波。这样滤波器引入的延迟只作用于最终的运动加速度而不会干扰重力投影与姿态之间的时序一致性。4.3 陀螺零偏漂移重力消除后出现慢变的“幽灵加速度”陀螺仪的零偏如果不标定或不补偿姿态解算里的roll和pitch会随时间缓慢漂移。姿态一旦漂了重力投影方向也跟着慢慢偏重力消除后的运动加速度里就会多出一个变化缓慢的“幽灵分量”。这个分量和真实运动无关完全由陀螺零偏引起但对下游积分来说非常致命——积一次位置里就多一段虚假轨迹。这里要特别说一句roll和pitch的漂移会被加速度计修正拉住一般不会发散太远所以很多人反而忽略它。但就算只漂1度重力投影就会产生约9.8×sin(1°)≈0.17 m/s²的横向分量这个量级在步态分析和车辆急加速检测里已经完全不可忽略了。所以做重力消除之前最好先做一次陀螺零偏标定采集静止数据求平均即可得到粗略零偏更精确的可以用Allan方差分析法。4.4 安装倾斜与外参残差重力消除后残留的固定分量IMU传感器安装到设备上时如果安装面本身有倾斜或者设计上就有一个安装角度那么“机体系”和“载体参考系”之间还存在一个固连外参旋转。如果代码里没有加载这个外参姿态滤波器解算出来的是IMU姿态而重力消除又按这个姿态投影那么重力在“真实载体坐标系”里就没有被彻底消除而是残留了一个由安装角决定的分量。这个问题的典型特征是在某个固定姿态下重力消除后的数据在某一轴上有一个稳定的偏置而且这个偏置随设备朝向变化你不管怎么调零偏都去不掉。解决思路是显式建模外参。先用水平仪或机械工装量出IMU相对载体的安装角或者做一次静止多姿态标定把安装角拟合出来然后在姿态链路里额外乘上一个外参旋转。部分IMU标定工具比如imu_utils配合Kalibr也能直接输出安装矩阵可以接入这个流程。4.5 时间戳乱序与多传感器对齐重力消除再准数据进不了融合也是白搭这个话题在纯IMU数据处理里容易被忽略但只要你做的是多传感器融合比如标题热搜词里常见的vinsfusion、相机imu联合标定、imu雷达外参标定时间对齐就是绕不开的一环。如果你的IMU数据时间戳本身就乱或者和相机、激光雷达、轮式里程计的时间基准不对齐那么无论重力消除做得多精确在融合框架里数据的物理意义都是错位的。轻则融合精度下降重则估计器直接发散。工程上建议在采集阶段就保持统一时钟源或者至少记录每个传感器的硬件时间戳并在后处理里对齐。如果拿到的数据已经没法重采也可以用相关性分析估计传感器间的时间偏移再把IMU重力消除后的数据重采样到统一时间网格上。这个环节虽然不属于重力消除本身的数学推导但它是重力消除成果能真正产生的必要条件。5. 重力消除之后数据的下一步和两个容易混淆的概念5.1 运动加速度能做什么从步态分割到急加速检测重力消除后的加速度信号最直接的应用就是运动特征提取。举例来说步态识别场景中人在走路时垂直轴的运动加速度会呈现周期性波动消除重力后这个波动更干净可以直接用窗口方差来做步态周期的分割。很多手环和手机计步算法干的就是这么一件事。车辆急加速/急刹车检测中纵向轴的运动加速度超过某个阈值就触发一次事件。如果没有消除重力一个上坡场景就会被误判为持续加速一个下坡场景会被误判为持续刹车妥妥的灾难。这些应用在实现层面其实都靠一个简单的窗口统计逻辑对消除重力后的三轴加速度取窗口均值或窗口方差。如果消除得干净阈值只需要设置一次如果消除得不干净不同姿态下的数据统计特性会飘阈值怎么调都不舒服。5.2 IMU预积分与重力消除的关系融合里的标准套路到了视觉惯性融合或者激光惯性融合层面“重力消除”通常不是用上面这种逐样本减法实现的而是在预积分里完成的。VINS-Mono这类方案在预积分时会使用IMU原始测量把重力作为待估计变量放进状态向量通过重力向量和姿态旋转把重力分量从预积分项中扣除。这两者的关系可以这样理解逐样本重力消除是“先处理数据再看应用”预积分则是“把重力作为模型一部分在估计过程中处理”。后者更适合紧耦合融合场景因为重力和零点偏置可以在优化框架里同时估计比离线的两步走更精确。但不管哪种方案如果你要做相机imu联合标定、imu雷达外参标定第一步依然要保证加速度计和陀螺仪的原始数据质量以及时间戳对齐干净。标定工具能把外参估计出来但前提是输入的数据里没有迟滞、没有乱序、没有未补偿的零偏。5.3 一个需要澄清的概念重力消除做对了yaw还是会慢漂很多初学IMU的人做完重力消除后发现一个现象加速度数据变干净了但基于IMU的位姿解算中yaw角依然慢慢漂。这其实不是重力消除的问题而是惯导系统的固有特性带来的。重力消除只利用加速度计陀螺仪约束了roll和pitch对于yaw方向加速度计无法提供任何长期参考磁力计可以提供但磁力计又容易受环境干扰。纯粹靠陀螺仪积分yaw零偏漂移会一直在缓慢累积所以“yaw仍会慢漂”是正常物理约束下的结果不是bug。解决yaw漂移的路径只有两条一是引入绝对航向参考磁力计、视觉、GNSS航向二是设计多传感器融合系统用外部信息周期性修正yaw。重力消除在这个问题里能做的是保证roll和pitch不漂为后续融合提供一个稳定的姿态基准这就已经发挥价值了。最后再分享一个小技巧。调试重力消除时我习惯在代码里加一个开关让重力消除模块可以临时输出“原始加速度”和“重力投影”两个中间量。当你发现最终消除结果不对劲时把这两个中间量拉出来对比一眼就能看出是姿态方向错、重力幅值错还是零偏错。比起盯着最终曲线猜原因这个办法能省掉大半排查时间。真实项目里重力消除的问题很少是数学不会大多是把符号约定、滤波器时序和传感器的固有问题搅在了一起。先把数据处理链路拆开一块一块验证再复杂的IMU数据也能捋顺。