一文彻底搞懂无人机中的各种坐标系:定义、关系与转换
做无人机开发,绕不开的第一道坎就是坐标系。GPS 给的是经纬度,IMU 给的是机体角速度,飞控要的是导航系速度,相机吐的是像素坐标,云台还有自己的一套……稍不留神符号一反、轴序一错,姿态就飞了。
这篇文章把无人机涉及的坐标系一次性梳理清楚,包含定义、图示、转换矩阵、代码实现,以及一堆我踩过的坑。
目录
- 一、为什么需要这么多坐标系
- 二、坐标系全景图
- 三、地球相关坐标系
- 四、导航坐标系(NED / ENU)
- 五、机体坐标系(Body Frame)
- 六、气流坐标系与航迹坐标系
- 七、传感器坐标系:IMU / 云台 / 相机 / 像素
- 八、姿态表示方法:欧拉角、DCM、四元数
- 九、完整转换链路与代码实现
- 十、十个常见的坑
一、为什么需要这么多坐标系
本质原因只有一个:不同的物理量,在不同的参考系下描述才最简单。
| 物理量 | 传感器 | 天然所在坐标系 |
|---|---|---|
| 角速度 ω \omega ω | 陀螺仪 | 机体系(传感器固连在机体上) |
| 比力 f f f | 加速度计 | 机体系 |
| 位置 | GNSS | 大地坐标系(经纬高) |
| 航向 | 磁力计 | 机体系测量,需转到导航系 |
| 气压高度 | 气压计 | 沿重力方向,导航系 z 轴 |
| 目标像素 | 相机 | 像素坐标系 |
| 攻角/侧滑角 | 空速管/风标 | 气流坐标系 |
飞控要做的事,就是把这些散落在各个坐标系的量,统一到同一个坐标系下做融合与控制。所以坐标系转换是导航算法的地基,地基不稳,上面写再漂亮的 EKF 也是白搭。
二、坐标系全景图
先给一张总览,后面逐个展开:
记号约定(全文统一):
- C b n C_b^n Cbn 表示从 b 系到 n 系的方向余弦矩阵(DCM),即 v n = C b n v b v^n = C_b^n v^b vn=Cbnvb
- 旋转矩阵是正交阵,因此 C n b = ( C b n ) T = ( C b n ) − 1 C_n^b = (C_b^n)^{\mathrm T} = (C_b^n)^{-1} Cnb=(Cbn)T=(Cbn)−1
- 链式法则: C b e = C n e C b n C_b^e = C_n^e C_b^n Cbe=CneCbn,上下标"消消乐",中间的抵消
这个记号极其好用,写公式时看上下标能不能对消,就知道有没有写反。
三、地球相关坐标系
3.1 地心惯性坐标系(ECI,i 系)
- 原点:地球质心
- Z i Z_i Zi 轴:沿地球自转轴指向北极
- X i X_i Xi 轴:指向春分点(J2000 历元)
- Y i Y_i Yi 轴:右手定则补齐
特点:不随地球自转,是牛顿力学意义上的近似惯性系。惯导的力学编排方程最原始的形式就写在 i 系里,因为只有在惯性系下 F = m a F = ma F=ma 才不需要引入附加的科氏力、离心力。
工程上,除非你做航天或者高精度惯导推导,日常无人机开发几乎不直接用 i 系,但它是理解科氏力修正项的来源。
3.2 地心地固坐标系(ECEF,e 系)
- 原点:地球质心
- Z e Z_e Ze 轴:指向北极(与 Z i Z_i Zi 重合)
- X e X_e Xe 轴:指向本初子午线与赤道的交点
- Y e Y_e Ye 轴:右手定则(指向东经 90°)
e 系随地球一起转,相对 i 系的角速度为:
ω i e = 7.292115 × 10 − 5 r a d / s \omega_{ie} = 7.292115 \times 10^{-5}\ \mathrm{rad/s} ωie=7.292115×10−5 rad/s
i 系到 e 系只差一个绕 Z 轴的转动:
C i e = [ cos ( ω i e t ) sin ( ω i e t ) 0 − sin ( ω i e t ) cos ( ω i e t ) 0 0 0 1 ] C_i^e = \begin{bmatrix} \cos(\omega_{ie}t) & \sin(\omega_{ie}t) & 0 \\ -\sin(\omega_{ie}t) & \cos(\omega_{ie}t) & 0 \\ 0 & 0 & 1 \end{bmatrix} Cie= cos(ωiet)−sin(ωiet)0sin(ωiet)cos(ωiet)0001
ECEF 是 GNSS 解算的原生坐标系——你的 RTK 板卡内部算的就是 XYZ,只是输出时帮你转成了经纬高。
3.3 大地坐标系(LLA / Geodetic)
用纬度 L L L、经度 λ \lambda λ、大地高 h h h 描述位置,基于 WGS-84 参考椭球:
| 参数 | 符号 | 数值 |
|---|---|---|
| 长半轴 | a a a | 6378137.0 m |
| 扁率 | f f f | 1/298.257223563 |
| 第一偏心率平方 | e 2 e^2 e2 | 2 f − f 2 ≈ 0.00669438 2f - f^2 \approx 0.00669438 2f−f2≈0.00669438 |
LLA → ECEF(有闭式解):
{ x e = ( R N + h ) cos L cos λ y e = ( R N + h ) cos L sin λ z e = [ R N ( 1 − e 2 ) + h ] sin L \begin{cases} x_e = (R_N + h)\cos L \cos \lambda \\ y_e = (R_N + h)\cos L \sin \lambda \\ z_e = \left[R_N(1 - e^2) + h\right]\sin L \end{cases} ⎩ ⎨ ⎧xe=(RN+h)cosLcosλye=(RN+h)cosLsinλze=[RN(1−e2)+h]sinL
其中卯酉圈曲率半径:
R N = a 1 − e 2 sin 2 L R_N = \frac{a}{\sqrt{1 - e^2 \sin^2 L}} RN=1−e2sin2La
ECEF → LLA 没有简洁的闭式解(因为 L L L 出现在 R N R_N RN 里),经度可以直接算:
λ = arctan 2 ( y e , x e ) \lambda = \arctan2(y_e,\ x_e) λ=arctan2(ye, xe)
纬度和高度需要迭代,或者用 Bowring / Ferrari 等近似闭式算法。迭代法通常 3~4 次就收敛到毫米级:
L k + 1 = arctan [ z e + e 2 R N ( L k ) sin L k x e 2 + y e 2 ] , h = x e 2 + y e 2 cos L − R N L_{k+1} = \arctan\left[\frac{z_e + e^2 R_N(L_k)\sin L_k}{\sqrt{x_e^2 + y_e^2}}\right],\quad h = \frac{\sqrt{x_e^2 + y_e^2}}{\cos L} - R_N Lk+1=arctan[xe2+ye2ze+e2RN(Lk)sinLk],h=cosLxe2+ye2−RN
⚠️ 大地高 ≠ 海拔高。GNSS 输出的 h h h 是相对 WGS-84 椭球面的椭球高,而地图上的海拔是相对大地水准面(Geoid)的正高。二者相差一个大地水准面差距 N N N,在中国大陆大约 -10 m 到 -40 m(华北约 -10 m,青藏高原可达 -40 m 以上)。做贴地飞行、地形跟随时,这个偏差足以撞山。
四、导航坐标系(NED / ENU)
导航系(n 系)也叫当地水平坐标系、地理坐标系,原点取在载体所在位置(或某个起飞参考点),坐标轴与当地水平面对齐。
有两种常见约定:
4.1 NED(North-East-Down,北东地)
- X n X_n Xn:指向地理北
- Y n Y_n Yn:指向东
- Z n Z_n Zn:指向地心(向下)
使用者:PX4 内部、传统惯导、航空航天教材、NASA/AIAA 惯例。
优点: Z Z Z 轴向下,重力是正的( g n = [ 0 , 0 , + 9.8 ] T g^n = [0, 0, +9.8]^{\mathrm T} gn=[0,0,+9.8]T),高度增加是负的 Z Z Z。配合前右下(FRD)机体系,构成右手系,且欧拉角定义最自然。
4.2 ENU(East-North-Up,东北天)
- X n X_n Xn:指向东
- Y n Y_n Yn:指向北
- Z n Z_n Zn:指向天(向上)
使用者:ROS/ROS2(REP-103 强制规定)、MAVROS、大多数 SLAM 框架、测绘领域。
优点:符合"高度往上加"的直觉,跟数学上的 x y xy xy 平面 + z z z 向上一致。
4.3 两者的转换
NED ↔ ENU 是一个对合变换(自己是自己的逆):
C NED ENU = C ENU NED = [ 0 1 0 1 0 0 0 0 − 1 ] C_{\text{NED}}^{\text{ENU}} = C_{\text{ENU}}^{\text{NED}} = \begin{bmatrix} 0 & 1 & 0 \\ 1 & 0 & 0 \\ 0 & 0 & -1 \end{bmatrix} CNEDENU=CENUNED= 01010000−1
即:交换前两个分量,第三个取负。注意这个矩阵的行列式是 -1,它不是旋转矩阵而是"旋转 + 反射"——所以不能简单地用欧拉角来表达 NED→ENU 的关系,必须成对地同时切换机体系约定(FRD ↔ FLU),才能保持右手性。
4.4 ECEF → NED
在参考点 ( L 0 , λ 0 ) (L_0, \lambda_0) (L0,λ0) 处:
C e n = [ − sin L 0 cos λ 0 − sin L 0 sin λ 0 cos L 0 − sin λ 0 cos λ 0 0 − cos L 0 cos λ 0 − cos L 0 sin λ 0 − sin L 0 ] C_e^n = \begin{bmatrix} -\sin L_0 \cos\lambda_0 & -\sin L_0 \sin\lambda_0 & \cos L_0 \\ -\sin\lambda_0 & \cos\lambda_0 & 0 \\ -\cos L_0 \cos\lambda_0 & -\cos L_0 \sin\lambda_0 & -\sin L_0 \end{bmatrix} Cen= −sinL0cosλ0−sinλ0−cosL0cosλ0−sinL0sinλ0cosλ0−cosL0sinλ0cosL00−sinL0
完整流程(局部切平面 / LTP):
[ N E D ] = C e n ( p e − p e , 0 ) \begin{bmatrix} N \\ E \\ D \end{bmatrix} = C_e^n \left( \mathbf{p}_e - \mathbf{p}_{e,0} \right) NED =Cen(pe−pe,0)
小范围简化:飞行半径几公里以内,可以直接用平面近似,误差在厘米级:
{ N ≈ ( L − L 0 ) ⋅ ( R M + h ) E ≈ ( λ − λ 0 ) ⋅ ( R N + h ) cos L 0 D ≈ − ( h − h 0 ) \begin{cases} N \approx (L - L_0) \cdot (R_M + h) \\ E \approx (\lambda - \lambda_0) \cdot (R_N + h)\cos L_0 \\ D \approx -(h - h_0) \end{cases} ⎩ ⎨ ⎧N≈(L−L0)⋅(RM+h)E≈(λ−λ0)⋅(RN+h)cosL0D≈−(h−h0)
其中子午圈曲率半径 R M = a ( 1 − e 2 ) ( 1 − e 2 sin 2 L ) 3 / 2 R_M = \dfrac{a(1-e^2)}{(1 - e^2\sin^2 L)^{3/2}} RM=(1−e2sin2L)3/2a(1−e2)。
粗略记:中国纬度下,1° 纬度 ≈ 111 km,1° 经度 ≈ 111 × cos(纬度) km。
五、机体坐标系(Body Frame)
原点在无人机重心,与机体固连,随飞机一起动。同样有两种约定:
5.1 FRD(Front-Right-Down,前右下)
- X b X_b Xb:机头方向
- Y b Y_b Yb:右翼方向
- Z b Z_b Zb:机腹方向(向下)
使用者:PX4 内部、航空航天惯例,与 NED 配对。
5.2 FLU(Front-Left-Up,前左上)
- X b X_b Xb:机头方向
- Y b Y_b Yb:左翼方向
- Z b Z_b Zb:机背方向(向上)
使用者:ROS(REP-103)、MAVROS 对外接口,与 ENU 配对。
FRD ↔ FLU 转换矩阵: d i a g ( 1 , − 1 , − 1 ) \mathrm{diag}(1, -1, -1) diag(1,−1,−1),即绕 X X X 轴转 180°,这个是真正的旋转(行列式 +1)。
5.3 机体系下的状态量
| 符号 | 含义 | 说明 |
|---|---|---|
| u , v , w u, v, w u,v,w | 机体系三轴速度 | 前向、右向、下向 |
| p , q , r p, q, r p,q,r | 机体系三轴角速度 | 滚转率、俯仰率、偏航率 |
| ϕ , θ , ψ \phi, \theta, \psi ϕ,θ,ψ | 欧拉角 | 滚转 Roll、俯仰 Pitch、偏航 Yaw |
角速度的正方向按右手定则:
- p > 0 p > 0 p>0:右翼下沉(右滚)
- q > 0 q > 0 q>0:机头上抬(抬头)
- r > 0 r > 0 r>0:机头右偏
5.4 NED → Body:欧拉角与旋转顺序
航空领域标准采用 Z-Y-X 顺序的内旋(intrinsic),也记作 3-2-1 顺序:
- 先绕 Z n Z_n Zn 转偏航角 ψ \psi ψ
- 再绕新的 Y Y Y 轴转俯仰角 θ \theta θ
- 最后绕新的 X X X 轴转滚转角 ϕ \phi ϕ
C n b = R x ( ϕ ) R y ( θ ) R z ( ψ ) C_n^b = R_x(\phi)\, R_y(\theta)\, R_z(\psi) Cnb=Rx(ϕ)Ry(θ)Rz(ψ)
其中(注意这是坐标系旋转/被动旋转的形式):
R z ( ψ ) = [ c ψ s ψ 0 − s ψ c ψ 0 0 0 1 ] , R y ( θ ) = [ c θ 0 − s θ 0 1 0 s θ 0 c θ ] , R x ( ϕ ) = [ 1 0 0 0 c ϕ s ϕ 0 − s ϕ c ϕ ] R_z(\psi) = \begin{bmatrix} c\psi & s\psi & 0 \\ -s\psi & c\psi & 0 \\ 0 & 0 & 1\end{bmatrix},\quad R_y(\theta) = \begin{bmatrix} c\theta & 0 & -s\theta \\ 0 & 1 & 0 \\ s\theta & 0 & c\theta\end{bmatrix},\quad R_x(\phi) = \begin{bmatrix} 1 & 0 & 0 \\ 0 & c\phi & s\phi \\ 0 & -s\phi & c\phi\end{bmatrix} Rz(ψ)= cψ−sψ0sψcψ0001 ,Ry(θ)= cθ0sθ010−sθ0cθ ,Rx(ϕ)= 1000cϕ−sϕ0sϕcϕ
展开得到完整的 DCM:
C n b = [ c θ c ψ c θ s ψ − s θ s ϕ s θ c ψ − c ϕ s ψ s ϕ s θ s ψ + c ϕ c ψ s ϕ c θ c ϕ s θ c ψ + s ϕ s ψ c ϕ s θ s ψ − s ϕ c ψ c ϕ c θ ] C_n^b = \begin{bmatrix} c\theta c\psi & c\theta s\psi & -s\theta \\ s\phi s\theta c\psi - c\phi s\psi & s\phi s\theta s\psi + c\phi c\psi & s\phi c\theta \\ c\phi s\theta c\psi + s\phi s\psi & c\phi s\theta s\psi - s\phi c\psi & c\phi c\theta \end{bmatrix} Cnb= cθcψsϕsθcψ−cϕsψcϕsθcψ+sϕsψcθsψsϕsθsψ+cϕcψcϕsθsψ−sϕcψ−sθsϕcθcϕcθ
反过来 C b n = ( C n b ) T C_b^n = (C_n^b)^{\mathrm T} Cbn=(Cnb)T。
从 DCM 反解欧拉角(记 C b n C_b^n Cbn 的元素为 c i j c_{ij} cij, i , j i,j i,j 从 1 开始):
θ = − arcsin ( c 31 ) , ϕ = arctan 2 ( c 32 , c 33 ) , ψ = arctan 2 ( c 21 , c 11 ) \theta = -\arcsin(c_{31}),\quad \phi = \arctan2(c_{32},\, c_{33}),\quad \psi = \arctan2(c_{21},\, c_{11}) θ=−arcsin(c31),ϕ=arctan2(c32,c33),ψ=arctan2(c21,c11)
注意:这里用的是 C b n C_b^n Cbn。如果你手上是 C n b C_n^b Cnb,公式里的下标要相应转置。务必先确认自己手上的矩阵是哪个方向的,这是最高频的 bug 来源之一。
角度定义域: ϕ ∈ ( − 180 ° , 180 ° ] \phi \in (-180°, 180°] ϕ∈(−180°,180°], θ ∈ [ − 90 ° , 90 ° ] \theta \in [-90°, 90°] θ∈[−90°,90°], ψ ∈ [ 0 ° , 360 ° ) \psi \in [0°, 360°) ψ∈[0°,360°) 或 ( − 180 ° , 180 ° ] (-180°, 180°] (−180°,180°]。
5.5 欧拉角速率 ≠ 机体角速度
这是新手最容易搞混的一点。陀螺仪测的 [ p , q , r ] [p, q, r] [p,q,r] 是机体系下的角速度矢量分量,而 [ ϕ ˙ , θ ˙ , ψ ˙ ] [\dot\phi, \dot\theta, \dot\psi] [ϕ˙,θ˙,ψ˙] 是三个欧拉角各自的变化率,它们分属三个不同的中间坐标系,不能直接画等号。
转换关系:
[ ϕ ˙ θ ˙ ψ ˙ ] = [ 1 sin ϕ tan θ cos ϕ tan θ 0 cos ϕ − sin ϕ 0 sin ϕ / cos θ cos ϕ / cos θ ] [ p q r ] \begin{bmatrix}\dot\phi \\ \dot\theta \\ \dot\psi\end{bmatrix} = \begin{bmatrix} 1 & \sin\phi\tan\theta & \cos\phi\tan\theta \\ 0 & \cos\phi & -\sin\phi \\ 0 & \sin\phi / \cos\theta & \cos\phi / \cos\theta \end{bmatrix} \begin{bmatrix} p \\ q \\ r \end{bmatrix} ϕ˙θ˙ψ˙ = 100sinϕtanθcosϕsinϕ/cosθcosϕtanθ−sinϕcosϕ/cosθ pqr
只有在小角度( ϕ ≈ θ ≈ 0 \phi \approx \theta \approx 0 ϕ≈θ≈0)时才近似有 ϕ ˙ ≈ p , θ ˙ ≈ q , ψ ˙ ≈ r \dot\phi \approx p,\ \dot\theta \approx q,\ \dot\psi \approx r ϕ˙≈p, θ˙≈q, ψ˙≈r。这也是为什么多旋翼小角度线性化控制器能工作、而做大机动时必须用四元数的原因。
看这个矩阵,当 θ → ± 90 ° \theta \to \pm 90° θ→±90° 时 tan θ → ∞ \tan\theta \to \infty tanθ→∞、 cos θ → 0 \cos\theta \to 0 cosθ→0,矩阵奇异——这就是万向节死锁(Gimbal Lock)。
六、气流坐标系与航迹坐标系
这两个坐标系主要用在固定翼上,多旋翼一般不太关心,但做抗风控制或空速融合时会用到。
6.1 气流坐标系(Wind / Air-path Frame,a 系)
- X a X_a Xa:沿空速矢量方向(飞机相对空气的运动方向)
- Z a Z_a Za:在飞机对称面内,垂直 X a X_a Xa 向下
- Y a Y_a Ya:右手定则
气动力天然定义在这个系里:阻力 D 沿 − X a -X_a −Xa,升力 L 沿 − Z a -Z_a −Za,侧力 Y 沿 Y a Y_a Ya。
攻角 α \alpha α 与侧滑角 β \beta β 定义了 a 系与 b 系的关系。设机体系下空速分量为 ( u , v , w ) (u, v, w) (u,v,w):
V = u 2 + v 2 + w 2 , α = arctan w u , β = arcsin v V V = \sqrt{u^2 + v^2 + w^2},\quad \alpha = \arctan\frac{w}{u},\quad \beta = \arcsin\frac{v}{V} V=u2+v2+w2,α=arctanuw,β=arcsinVv
反过来:
u = V cos α cos β , v = V sin β , w = V sin α cos β u = V\cos\alpha\cos\beta,\quad v = V\sin\beta,\quad w = V\sin\alpha\cos\beta u=Vcosαcosβ,v=Vsinβ,w=Vsinαcosβ
转换矩阵:
C a b = [ cos α cos β − cos α sin β − sin α sin β cos β 0 sin α cos β − sin α sin β cos α ] C_a^b = \begin{bmatrix} \cos\alpha\cos\beta & -\cos\alpha\sin\beta & -\sin\alpha \\ \sin\beta & \cos\beta & 0 \\ \sin\alpha\cos\beta & -\sin\alpha\sin\beta & \cos\alpha \end{bmatrix} Cab= cosαcosβsinβsinαcosβ−cosαsinβcosβ−sinαsinβ−sinα0cosα
6.2 航迹坐标系(Trajectory / Path Frame,k 系)
- X k X_k Xk:沿地速矢量方向
- Z k Z_k Zk:在包含 X k X_k Xk 的铅垂面内向下
- Y k Y_k Yk:右手定则
与导航系的关系由航迹倾角 γ \gamma γ(爬升角) 和 航迹方位角 χ \chi χ 描述:
γ = arcsin − v D ∣ v ∣ , χ = arctan 2 ( v E , v N ) \gamma = \arcsin\frac{-v_D}{|\mathbf v|},\quad \chi = \arctan2(v_E,\ v_N) γ=arcsin∣v∣−vD,χ=arctan2(vE, vN)
6.3 空速、地速与风速
三者是矢量关系(在同一坐标系下相加):
V 地速 = V 空速 + V 风速 \mathbf V_{\text{地速}} = \mathbf V_{\text{空速}} + \mathbf V_{\text{风速}} V地速=V空速+V风速
无风时,气流系与航迹系重合, γ = θ − α \gamma = \theta - \alpha γ=θ−α(无侧滑无滚转的情况)。有风时两者分离,这正是固定翼定点盘旋会画出"椭圆"、多旋翼悬停会有姿态倾角的原因。
七、传感器坐标系:IMU / 云台 / 相机 / 像素
7.1 IMU 坐标系
理论上 IMU 应与机体系重合,实际上:
- 安装位置偏移(杆臂 lever arm):IMU 不在重心,会引入向心加速度和角加速度切向分量:
f IMU = f CG + ω ˙ × r + ω × ( ω × r ) f_{\text{IMU}} = f_{\text{CG}} + \dot{\boldsymbol\omega} \times \mathbf r + \boldsymbol\omega \times (\boldsymbol\omega \times \mathbf r) fIMU=fCG+ω˙×r+ω×(ω×r)
高机动时这一项不可忽略, r = 5 c m \mathbf r = 5\ \mathrm{cm} r=5 cm、 ω = 200 ° / s \omega = 200°/\mathrm{s} ω=200°/s 时,向心项就有约 0.6 m / s 2 0.6\ \mathrm{m/s^2} 0.6 m/s2。
-
安装角误差:焊接、贴片总有 1~3° 偏差,需要通过标定得到固定的安装矩阵 C IMU b C_{\text{IMU}}^b CIMUb。
-
GNSS 天线杆臂:GNSS 天线也不在重心,位置需要补偿 p CG = p ant − C b n r ant b \mathbf p_{\text{CG}} = \mathbf p_{\text{ant}} - C_b^n \mathbf r_{\text{ant}}^b pCG=pant−Cbnrantb。松组合做得再好,杆臂不补偿,转弯时位置就会甩出去。
7.2 云台坐标系
三轴云台通过 Yaw-Pitch-Roll 三个电机隔离机体运动。注意云台角度有两种口径:
- 关节角(joint angle):相对机体的角度,电机编码器直接读出
- 姿态角(attitude angle):相对导航系的绝对角度,云台 IMU 输出
C g n = C b n ⋅ C g b C_g^n = C_b^n \cdot C_g^b Cgn=Cbn⋅Cgb
其中 C g b C_g^b Cgb 由三个关节角构成。做目标定位时一定要用绝对姿态角,否则机体一晃目标就跑了。
7.3 相机坐标系
OpenCV / 计算机视觉惯例(RDF):
- X c X_c Xc:图像右
- Y c Y_c Yc:图像下
- Z c Z_c Zc:光轴向前
注意这跟机体系 FRD 差了一个旋转:相机的"前"是 Z Z Z,机体的"前"是 X X X。典型的前视相机安装:
C c b = [ 0 0 1 1 0 0 0 1 0 ] C_c^b = \begin{bmatrix} 0 & 0 & 1 \\ 1 & 0 & 0 \\ 0 & 1 & 0 \end{bmatrix} Ccb= 010001100
(含义:相机 Z Z Z → 机体 X X X,相机 X X X → 机体 Y Y Y,相机 Y Y Y → 机体 Z Z Z)
7.4 图像与像素坐标系
- 图像坐标系:原点在光轴与像平面交点(主点),单位是毫米
- 像素坐标系:原点在图像左上角, u u u 向右、 v v v 向下,单位是像素
针孔模型:
s [ u v 1 ] = [ f x 0 c x 0 f y c y 0 0 1 ] ⏟ 内参 K [ X c Y c Z c ] s \begin{bmatrix} u \\ v \\ 1 \end{bmatrix} = \underbrace{\begin{bmatrix} f_x & 0 & c_x \\ 0 & f_y & c_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{内参 } K} \begin{bmatrix} X_c \\ Y_c \\ Z_c \end{bmatrix} s uv1 =内参 K fx000fy0cxcy1 XcYcZc
7.5 完整的目标定位链路
无人机看到地面一个目标点,算它的经纬度,要走完这一整条链:
像素 ( u , v ) → K − 1 相机系 → C c g 云台系 → C g b 机体系 → C b n NED → C n e ECEF → LLA \text{像素}(u,v) \xrightarrow{K^{-1}} \text{相机系} \xrightarrow{C_c^g} \text{云台系} \xrightarrow{C_g^b} \text{机体系} \xrightarrow{C_b^n} \text{NED} \xrightarrow{C_n^e} \text{ECEF} \rightarrow \text{LLA} 像素(u,v)K−1相机系Ccg云台系Cgb机体系CbnNEDCneECEF→LLA
由于单目缺少深度,最后一步通常用射线与地平面求交来定深:设相机在 NED 下位置为 ( N c , E c , D c ) (N_c, E_c, D_c) (Nc,Ec,Dc),射线方向单位矢量为 d n = [ d N , d E , d D ] T \mathbf d^n = [d_N, d_E, d_D]^{\mathrm T} dn=[dN,dE,dD]T,地面高度为 D g D_g Dg,则:
s = D g − D c d D , p target n = p c n + s d n s = \frac{D_g - D_c}{d_D},\quad \mathbf p_{\text{target}}^n = \mathbf p_c^n + s\, \mathbf d^n s=dDDg−Dc,ptargetn=pcn+sdn
要求 d D > 0 d_D > 0 dD>0(射线朝下),否则说明相机在看天,无解。
八、姿态表示方法:欧拉角、DCM、四元数
| 方法 | 参数量 | 优点 | 缺点 |
|---|---|---|---|
| 欧拉角 | 3 | 直观,物理意义明确 | 万向节死锁,三角函数开销大,插值非线性 |
| 方向余弦矩阵 | 9 | 无奇异,变换直接 | 冗余度高,需要正交化,存储/计算成本高 |
| 四元数 | 4 | 无奇异,计算高效,插值平滑 | 不直观,有双覆盖( q q q 与 − q -q −q 表示同一姿态) |
| 旋转矢量 | 3 | 无冗余,适合圆锥误差补偿 | 2 π 2\pi 2π 处奇异 |
工程实践:内部用四元数解算,对外显示用欧拉角,需要变换矢量时转成 DCM。
8.1 四元数与 DCM
定义 q = [ q 0 , q 1 , q 2 , q 3 ] T \mathbf q = [q_0, q_1, q_2, q_3]^{\mathrm T} q=[q0,q1,q2,q3]T(Hamilton 约定,标量在前), ∥ q ∥ = 1 \|\mathbf q\| = 1 ∥q∥=1:
C b n = [ q 0 2 + q 1 2 − q 2 2 − q 3 2 2 ( q 1 q 2 − q 0 q 3 ) 2 ( q 1 q 3 + q 0 q 2 ) 2 ( q 1 q 2 + q 0 q 3 ) q 0 2 − q 1 2 + q 2 2 − q 3 2 2 ( q 2 q 3 − q 0 q 1 ) 2 ( q 1 q 3 − q 0 q 2 ) 2 ( q 2 q 3 + q 0 q 1 ) q 0 2 − q 1 2 − q 2 2 + q 3 2 ] C_b^n = \begin{bmatrix} q_0^2 + q_1^2 - q_2^2 - q_3^2 & 2(q_1q_2 - q_0q_3) & 2(q_1q_3 + q_0q_2) \\ 2(q_1q_2 + q_0q_3) & q_0^2 - q_1^2 + q_2^2 - q_3^2 & 2(q_2q_3 - q_0q_1) \\ 2(q_1q_3 - q_0q_2) & 2(q_2q_3 + q_0q_1) & q_0^2 - q_1^2 - q_2^2 + q_3^2 \end{bmatrix} Cbn= q02+q12−q22−q322(q1q2+q0q3)2(q1q3−q0q2)2(q1q2−q0q3)q02−q12+q22−q322(q2q3+q0q1)2(q1q3+q0q2)2(q2q3−q0q1)q02−q12−q22+q32
8.2 四元数与欧拉角
欧拉角 → 四元数(Z-Y-X 顺序):
{ q 0 = c ϕ 2 c θ 2 c ψ 2 + s ϕ 2 s θ 2 s ψ 2 q 1 = s ϕ 2 c θ 2 c ψ 2 − c ϕ 2 s θ 2 s ψ 2 q 2 = c ϕ 2 s θ 2 c ψ 2 + s ϕ 2 c θ 2 s ψ 2 q 3 = c ϕ 2 c θ 2 s ψ 2 − s ϕ 2 s θ 2 c ψ 2 \begin{cases} q_0 = c\frac{\phi}{2}c\frac{\theta}{2}c\frac{\psi}{2} + s\frac{\phi}{2}s\frac{\theta}{2}s\frac{\psi}{2} \\ q_1 = s\frac{\phi}{2}c\frac{\theta}{2}c\frac{\psi}{2} - c\frac{\phi}{2}s\frac{\theta}{2}s\frac{\psi}{2} \\ q_2 = c\frac{\phi}{2}s\frac{\theta}{2}c\frac{\psi}{2} + s\frac{\phi}{2}c\frac{\theta}{2}s\frac{\psi}{2} \\ q_3 = c\frac{\phi}{2}c\frac{\theta}{2}s\frac{\psi}{2} - s\frac{\phi}{2}s\frac{\theta}{2}c\frac{\psi}{2} \end{cases} ⎩ ⎨ ⎧q0=c2ϕc2θc2ψ+s2ϕs2θs2ψq1=s2ϕc2θc2ψ−c2ϕs2θs2ψq2=c2ϕs2θc2ψ+s2ϕc2θs2ψq3=c2ϕc2θs2ψ−s2ϕs2θc2ψ
8.3 四元数运动学方程
姿态更新的核心方程:
q ˙ = 1 2 q ⊗ [ 0 ω n b b ] = 1 2 Ω ( ω ) q \dot{\mathbf q} = \frac{1}{2}\,\mathbf q \otimes \begin{bmatrix} 0 \\ \boldsymbol\omega_{nb}^b \end{bmatrix} = \frac{1}{2} \boldsymbol\Omega(\boldsymbol\omega)\, \mathbf q q˙=21q⊗[0ωnbb]=21Ω(ω)q
其中:
Ω ( ω ) = [ 0 − p − q − r p 0 r − q q − r 0 p r q − p 0 ] \boldsymbol\Omega(\boldsymbol\omega) = \begin{bmatrix} 0 & -p & -q & -r \\ p & 0 & r & -q \\ q & -r & 0 & p \\ r & q & -p & 0 \end{bmatrix} Ω(ω)= 0pqr−p0−rq−qr0−p−r−qp0
注意 ω n b b \boldsymbol\omega_{nb}^b ωnbb 是机体相对导航系的角速度,而陀螺测的是机体相对惯性系的 ω i b b \boldsymbol\omega_{ib}^b ωibb,二者相差地球自转和位置移动引起的牵连角速度:
ω n b b = ω i b b − C n b ( ω i e n + ω e n n ) \boldsymbol\omega_{nb}^b = \boldsymbol\omega_{ib}^b - C_n^b(\boldsymbol\omega_{ie}^n + \boldsymbol\omega_{en}^n) ωnbb=ωibb−Cnb(ωien+ωenn)
对于消费级 MEMS 陀螺(零偏漂移 > 10 ° / h > 10°/\mathrm{h} >10°/h),地球自转 15 ° / h 15°/\mathrm{h} 15°/h 完全淹没在噪声里,可以直接忽略;但对于战术级以上的光纤陀螺,这一项必须补偿。
九、完整转换链路与代码实现
下面是一份可直接使用的 Python 实现(NED / FRD 约定):
import numpy as np
# ---------- WGS-84 参数 ----------
A_WGS84 = 6378137.0
F_WGS84 = 1.0 / 298.257223563
E2_WGS84 = F_WGS84 * (2 - F_WGS84)
def lla2ecef(lat_deg, lon_deg, h):
"""大地坐标 -> ECEF,角度输入单位为度,高度为米(椭球高)"""
lat, lon = np.radians(lat_deg), np.radians(lon_deg)
sL, cL = np.sin(lat), np.cos(lat)
RN = A_WGS84 / np.sqrt(1 - E2_WGS84 * sL ** 2)
x = (RN + h) * cL * np.cos(lon)
y = (RN + h) * cL * np.sin(lon)
z = (RN * (1 - E2_WGS84) + h) * sL
return np.array([x, y, z])
def ecef2lla(p, tol=1e-12, max_iter=10):
"""ECEF -> 大地坐标,迭代法"""
x, y, z = p
lon = np.arctan2(y, x)
r = np.hypot(x, y)
lat = np.arctan2(z, r * (1 - E2_WGS84)) # 初值
for _ in range(max_iter):
sL = np.sin(lat)
RN = A_WGS84 / np.sqrt(1 - E2_WGS84 * sL ** 2)
h = r / np.cos(lat) - RN
lat_new = np.arctan2(z, r * (1 - E2_WGS84 * RN / (RN + h)))
if abs(lat_new - lat) < tol:
lat = lat_new
break
lat = lat_new
sL = np.sin(lat)
RN = A_WGS84 / np.sqrt(1 - E2_WGS84 * sL ** 2)
h = r / np.cos(lat) - RN
return np.degrees(lat), np.degrees(lon), h
def C_e2n(lat_deg, lon_deg):
"""ECEF -> NED 的方向余弦矩阵"""
lat, lon = np.radians(lat_deg), np.radians(lon_deg)
sL, cL, sl, cl = np.sin(lat), np.cos(lat), np.sin(lon), np.cos(lon)
return np.array([
[-sL * cl, -sL * sl, cL],
[-sl, cl, 0.0],
[-cL * cl, -cL * sl, -sL],
])
def lla2ned(lat, lon, h, lat0, lon0, h0):
"""相对参考点的 NED 局部坐标"""
return C_e2n(lat0, lon0) @ (lla2ecef(lat, lon, h) - lla2ecef(lat0, lon0, h0))
def euler2dcm(roll, pitch, yaw):
"""欧拉角(弧度) -> C_n^b,Z-Y-X 内旋顺序"""
sr, cr = np.sin(roll), np.cos(roll)
sp, cp = np.sin(pitch), np.cos(pitch)
sy, cy = np.sin(yaw), np.cos(yaw)
return np.array([
[cp * cy, cp * sy, -sp],
[sr * sp * cy - cr * sy, sr * sp * sy + cr * cy, sr * cp],
[cr * sp * cy + sr * sy, cr * sp * sy - sr * cy, cr * cp],
])
def dcm2euler(C_b2n):
"""C_b^n -> 欧拉角(弧度),含万向节死锁保护"""
if abs(C_b2n[2, 0]) > 0.99999: # |sin(pitch)| ≈ 1
pitch = np.sign(-C_b2n[2, 0]) * np.pi / 2
roll = 0.0 # 死锁时 roll/yaw 不可分,约定 roll=0
yaw = np.arctan2(-C_b2n[0, 1], C_b2n[1, 1])
return roll, pitch, yaw
pitch = -np.arcsin(C_b2n[2, 0])
roll = np.arctan2(C_b2n[2, 1], C_b2n[2, 2])
yaw = np.arctan2(C_b2n[1, 0], C_b2n[0, 0])
return roll, pitch, yaw
def euler2quat(roll, pitch, yaw):
"""欧拉角 -> 四元数 [w, x, y, z]"""
cr, sr = np.cos(roll / 2), np.sin(roll / 2)
cp, sp = np.cos(pitch / 2), np.sin(pitch / 2)
cy, sy = np.cos(yaw / 2), np.sin(yaw / 2)
return np.array([
cr * cp * cy + sr * sp * sy,
sr * cp * cy - cr * sp * sy,
cr * sp * cy + sr * cp * sy,
cr * cp * sy - sr * sp * cy,
])
def quat2dcm(q):
"""四元数 [w,x,y,z] -> C_b^n"""
w, x, y, z = q / np.linalg.norm(q)
return np.array([
[1 - 2 * (y*y + z*z), 2 * (x*y - w*z), 2 * (x*z + w*y)],
[2 * (x*y + w*z), 1 - 2 * (x*x + z*z), 2 * (y*z - w*x)],
[2 * (x*z - w*y), 2 * (y*z + w*x), 1 - 2 * (x*x + y*y)],
])
def quat_propagate(q, omega, dt):
"""四元数一阶积分更新,omega = [p,q,r] (rad/s)"""
p_, q_, r_ = omega
Omega = np.array([
[0, -p_, -q_, -r_],
[p_, 0, r_, -q_],
[q_, -r_, 0, p_],
[r_, q_, -p_, 0 ],
])
q_new = q + 0.5 * Omega @ q * dt
return q_new / np.linalg.norm(q_new) # 必须归一化!
NED2ENU = np.array([[0, 1, 0], [1, 0, 0], [0, 0, -1]]) # 自逆
FRD2FLU = np.diag([1.0, -1.0, -1.0]) # 自逆
# ---------- 端到端示例:像素 -> 地面目标经纬度 ----------
def pixel_to_ground(u, v, K, C_c2b, att_rpy, drone_lla, ref_lla, ground_h=0.0):
"""
单目 + 平地假设的目标定位
K: 相机内参 3x3
C_c2b: 相机系 -> 机体系 的安装矩阵
att_rpy: 无人机欧拉角 (roll, pitch, yaw),弧度
drone_lla/ref_lla: (lat, lon, h)
"""
# 1) 像素 -> 相机系归一化射线
d_c = np.linalg.inv(K) @ np.array([u, v, 1.0])
d_c /= np.linalg.norm(d_c)
# 2) 相机系 -> 机体系 -> NED
C_b2n = euler2dcm(*att_rpy).T
d_n = C_b2n @ C_c2b @ d_c
if d_n[2] <= 1e-6:
raise ValueError("射线未指向地面(相机在看天或平视)")
# 3) 与地平面求交
p_c_ned = lla2ned(*drone_lla, *ref_lla)
D_ground = -(ground_h - ref_lla[2])
s = (D_ground - p_c_ned[2]) / d_n[2]
p_tgt_ned = p_c_ned + s * d_n
# 4) NED -> ECEF -> LLA
p_tgt_ecef = lla2ecef(*ref_lla) + C_e2n(ref_lla[0], ref_lla[1]).T @ p_tgt_ned
return ecef2lla(p_tgt_ecef)
自检代码(写完转换函数一定要跑一遍这个):
# 往返一致性
lla = (39.9042, 116.4074, 50.0)
assert np.allclose(ecef2lla(lla2ecef(*lla)), lla, atol=1e-6)
# 欧拉角往返
rpy = (0.3, -0.2, 1.1)
assert np.allclose(dcm2euler(euler2dcm(*rpy).T), rpy, atol=1e-9)
# 四元数与 DCM 一致性
assert np.allclose(quat2dcm(euler2quat(*rpy)), euler2dcm(*rpy).T, atol=1e-9)
# 正交性
C = euler2dcm(*rpy)
assert np.allclose(C @ C.T, np.eye(3), atol=1e-12)
assert np.isclose(np.linalg.det(C), 1.0)
# 重力向量:水平静止时,机体系加速度计应读到 [0,0,g] (FRD/NED)
g_n = np.array([0, 0, 9.8])
assert np.allclose(euler2dcm(0, 0, 0) @ g_n, [0, 0, 9.8])
# 抬头 30° 时,重力在机体 x 轴有 -g*sin(30°) 分量
print(euler2dcm(0, np.radians(30), 0) @ g_n) # [-4.9, 0, 8.49]
最后一条特别有用:用重力向量做 sanity check,可以一眼看出符号是否搞反。
十、十个常见的坑
1️⃣ NED 和 ENU 混用
最经典的坑。PX4 内部 NED/FRD,MAVROS 对外 ENU/FLU,两边一对接就出问题。记住:坐标系约定必须成对切换(NED↔ENU 的同时 FRD↔FLU),否则右手系变左手系,姿态全乱。
调试口诀:让飞机原地向东平移,看输出的第一个分量。是正的 → ENU;接近 0 而第二个是正的 → NED。
2️⃣ C b n C_b^n Cbn 和 C n b C_n^b Cnb 搞反
写代码时把方向余弦矩阵变量名写全,比如 C_body_to_ned,而不是 C 或者 R。多打十几个字符,能省几小时调试。
3️⃣ 主动旋转 vs 被动旋转
- 主动旋转:坐标系不动,把矢量转过去
- 被动旋转:矢量不动,把坐标系转过去
两者互为转置。本文用的全是被动旋转(航空惯例),而 scipy.spatial.transform.Rotation 默认返回的是主动旋转矩阵。用 scipy 时注意:
from scipy.spatial.transform import Rotation as R
# scipy 的 'ZYX' + degrees,返回的 as_matrix() 是 C_b^n(主动)
C_b2n = R.from_euler('ZYX', [yaw, pitch, roll]).as_matrix()
C_n2b = C_b2n.T
4️⃣ 四元数存储顺序:wxyz vs xyzw
- Hamilton + wxyz:Eigen(构造函数)、MATLAB、PX4、多数惯导教材
- xyzw:ROS
geometry_msgs/Quaternion、scipy、Eigen 的内存布局(coeffs())
Eigen 尤其阴间:Quaterniond q(w, x, y, z) 构造是 wxyz,但 q.coeffs() 返回的是 xyzw。从裸内存 memcpy 的时候一定小心。
另外还有 JPL 约定(乘法顺序相反),航天领域文献常用,跟 Hamilton 混着看会精神分裂。
5️⃣ 万向节死锁
俯仰角 θ → ± 90 ° \theta \to \pm 90° θ→±90° 时,roll 和 yaw 无法区分。多旋翼平飞时到不了,但穿越机翻滚、固定翼失速、云台俯冲拍摄都会撞上。
解决方案:内部姿态解算一律用四元数;仅在最后显示给用户时转欧拉角,并对 θ = ± 90 ° \theta = \pm 90° θ=±90° 做特殊处理(见上面代码)。
6️⃣ 磁北 vs 真北
磁力计测的是磁北,导航系定义的是真北(地理北),二者相差磁偏角(Declination)。
中国境内磁偏角从东北的 -10° 左右变化到西部的 +2° 左右,而且逐年漂移。飞控通常用 WMM 模型根据经纬度查表补偿。带 RTK 双天线定向的机型可以直接测真北,精度和抗磁干扰能力都远好于磁罗盘。
7️⃣ 椭球高 vs 海拔高
前面提过:GNSS 的高度是椭球高,地图海拔是正高,差一个大地水准面差距。做地形跟随、贴地飞行、航测 DEM 配准时必须转换。
8️⃣ 杆臂效应
IMU 和 GNSS 天线都不在重心。IMU 杆臂在高机动时引入伪加速度,GNSS 杆臂在姿态变化时引入位置跳变。松/紧组合导航中不补偿杆臂,转弯时轨迹会呈现规律性外扩。
9️⃣ 四元数忘记归一化
数值积分会让 ∥ q ∥ \|\mathbf q\| ∥q∥ 慢慢偏离 1,导致等效的旋转矩阵不再正交,姿态缓慢发散。每次积分后必须归一化。同理,DCM 直接积分也需要周期性做正交化(Gram-Schmidt 或 SVD)。
🔟 角度单位和归一化
- 弧度还是角度?统一在接口层转换,内部一律用弧度。
- 偏航角跨越 ±180° 时的角度差计算:直接相减会得到 350° 而不是 -10°。正确做法:
def wrap_pi(a):
return (a + np.pi) % (2 * np.pi) - np.pi
err = wrap_pi(yaw_target - yaw_current) # 永远走短路径
这个 bug 会让飞机在正北附近突然反向旋转一整圈,非常吓人。
总结
| 坐标系 | 符号 | 原点 | 主要用途 |
|---|---|---|---|
| 地心惯性系 | i | 地心 | 惯导力学编排的理论基础 |
| 地心地固系 | e | 地心 | GNSS 解算、全球定位 |
| 大地坐标系 | — | 椭球面 | 位置表达(经纬高) |
| 导航系 | n | 载体/起飞点 | 速度、位置、控制的主战场 |
| 机体系 | b | 重心 | IMU 测量、力和力矩、控制分配 |
| 气流系 | a | 重心 | 气动力(升力/阻力) |
| 航迹系 | k | 重心 | 轨迹规划、爬升角控制 |
| 相机系 | c | 光心 | 视觉感知 |
记忆主线:
LLA ↔ ECEF ↔ NED ⏟ 位置,靠经纬度决定 ∣ NED ↔ Body ⏟ 姿态,靠欧拉角/四元数决定 ∣ Body ↔ Camera ⏟ 安装,靠标定决定 \underbrace{\text{LLA} \leftrightarrow \text{ECEF} \leftrightarrow \text{NED}}_{\text{位置,靠经纬度决定}} \quad\Big|\quad \underbrace{\text{NED} \leftrightarrow \text{Body}}_{\text{姿态,靠欧拉角/四元数决定}} \quad\Big|\quad \underbrace{\text{Body} \leftrightarrow \text{Camera}}_{\text{安装,靠标定决定}} 位置,靠经纬度决定 LLA↔ECEF↔NED 姿态,靠欧拉角/四元数决定 NED↔Body 安装,靠标定决定 Body↔Camera
三段各自的"驱动量"不同:第一段由位置驱动,第二段由姿态驱动,第三段由标定外参驱动。想清楚这一点,任何复杂的转换链路都能拆开。
最后送一条实践建议:把所有坐标转换封装成独立模块,配上完整单元测试(往返一致性、正交性、行列式为 1、重力向量检查)。这部分代码写一次可以用很多年,值得一开始就写扎实。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题欢迎评论区讨论。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)