ROS 로봇 자세 공분산 36칸 중 yaw 분산은 몇 번째 칸일까: 6×6 행우선 인덱스와 rad² 단위 계산하기
센서 사양에 yaw 표준편차 2°, x 위치 표준편차 0.05 m라고 적혀 있다면 ROS 로봇 자세 공분산 배열 float64[36] covariance의 35번 칸에는 약 0.0012184697을, 0번 칸에는 0.0025를 넣는다. 2나 0.05를 그대로 쓰는 것도, 2를 제곱한 4를 쓰는 것도 아니다. yaw 분산이 몇 번째 칸인지는 6×6 행우선 인덱스가 정하고, 그 칸에 들어갈 숫자는 표준편차를 rad² 분산으로 바꾸는 계산이 정한다.
메시지 주석에서 “row-major”라는 단어를 보면 행과 열을 뒤집어 읽을까 봐 먼저 걱정하게 된다. 그런데 제대로 채운 공분산 행렬에서는 그 혼동이 배열을 한 칸도 바꾸지 않는다. 숫자를 실제로 틀어지게 하는 쪽은 변수 순서를 잘못 기억하는 실수와, 도나 표준편차를 라디안·분산으로 바꾸지 않고 넣는 실수다. 이 글을 따라가면 원하는 칸 번호를 직접 계산하고, 배열을 채우는 코드의 테스트가 무엇을 잡아야 하는지 정할 수 있다. 0부터 세는 배열 인덱스와 도→라디안 곱셈(×π/180)은 안다고 보고, 표준편차·분산·공분산은 첫 절에서 짚는다.
36칸은 여섯 변수의 흔들림을 한 줄로 편 표다
표준편차(standard deviation)는 측정값이 참값 주변에서 보통 얼마나 흩어지는지를 원래 단위로 나타낸다. yaw 표준편차 2°는 “방향을 대략 ±2° 안쪽으로 안다”는 감각에 가깝다. 분산(variance)은 표준편차의 제곱이고, 단위도 함께 제곱된다. m는 m²가 되고 rad는 rad²가 된다.
공분산(covariance)은 두 변수가 평균에서 같은 방향으로 함께 치우치는지를 부호로 나타낸다. x가 평균보다 크게 나올 때 y도 평균보다 크게 나오는 경향이 있으면 양수, x가 클 때 y는 평균보다 작게 나오는 경향이면 음수다. 치우치는 방향 사이에 일관된 관계가 없으면 0에 가깝다. 두 오차의 크기가 함께 커지더라도 방향이 제각각이면 공분산은 0일 수 있다는 점이 흔히 헷갈리는 부분이다.
같은 변수끼리의 공분산이 곧 그 변수의 분산이다. 변수가 n개면 모든 짝을 모은 n×n 공분산 행렬(covariance matrix)이 생기고, 왼쪽 위에서 오른쪽 아래로 내려가는 대각 원소(diagonal element)에 분산이, 나머지 칸에 공분산이 놓인다. x–y 공분산과 y–x 공분산은 같은 값이므로 이 행렬은 i행 j열과 j행 i열이 같은 대칭 행렬이다.
ROS의 자세(pose)는 위치 x, y, z와 회전 세 개, 모두 여섯 변수로 불확실성을 표현한다. 그래서 6×6 = 36칸이다. REP-103의 Covariance Representation 절과 geometry_msgs/PoseWithCovariance.msg 주석은 같은 문구로 이 배열을 설명한다.
- “Row-major representation of the 6×6 covariance matrix”
- 변수 순서는 (x, y, z, rotation about X axis, rotation about Y axis, rotation about Z axis)
- 회전은 fixed-axis representation을 쓴다
고정축 회전(fixed-axis rotation)은 움직이지 않는 X, Y, Z축을 기준으로 각각 얼마나 돌았는지로 방향을 나타내는 방식이다. REP-103은 X, Y, Z축에 대한 고정축 회전을 각각 roll, pitch, yaw라고 부른다. 메시지 주석에는 roll·pitch·yaw라는 단어가 없으므로, “Z축 회전 = yaw”라는 대응은 이 REP-103 문장에서 읽어 낸 것이다.
이 배치는 TwistWithCovariance(속도)에도 그대로 쓰이고, 둘을 품고 있는 nav_msgs/Odometry도 같은 36칸 규약을 따른다. 2026년 9월 27일에 대조한 ROS 2 Jazzy·Rolling과 ROS 1 Noetic의 PoseWithCovariance.msg는 파일 내용이 같았다.
행 번호 × 6 + 열 번호가 칸 번호다
행우선(row-major)은 2차원 표를 1차원 배열로 옮길 때 0행을 왼쪽부터 끝까지 담고, 다음 행으로 넘어가는 순서다. 책을 한 줄씩 읽는 방식과 같다. 한 행이 6칸이니 r행 c열(둘 다 0부터)의 위치는 r × 6 + c가 된다. 반대로 한 열씩 위에서 아래로 담는 방식을 열우선(column-major)이라 하고, 그때 위치는 c × 6 + r이다.
책 비유는 칸을 찾는 순서까지만 맞는다. 책의 글자는 위아래가 뒤바뀌면 뜻이 달라지지만, 공분산 행렬은 대칭이라 “r행 c열”과 “c행 r열”이 같은 값을 갖는다. 행우선·열우선 혼동이 왜 대개 무해한지는 뒤에서 이 차이로 설명한다.
변수 순서 (x, y, z, roll, pitch, yaw)에 0~5를 붙이면 대각 여섯 칸의 번호가 나온다.
| 변수 (행 = 열) | 계산 | 인덱스 | 분산 단위 |
|---|---|---|---|
| x | 0×6+0 | 0 | m² |
| y | 1×6+1 | 7 | m² |
| z | 2×6+2 | 14 | m² |
| roll (X축 회전) | 3×6+3 | 21 | rad² |
| pitch (Y축 회전) | 4×6+4 | 28 | rad² |
| yaw (Z축 회전) | 5×6+5 | 35 | rad² |
대각 칸은 7씩 커진다. 한 행을 내려가면서(+6) 한 열 오른쪽으로 가기(+1) 때문이다. 비대각 칸은 대칭 쌍 두 곳을 함께 채운다. x–y 공분산이라면 0행 1열의 1번과 1행 0열의 6번에 같은 값을 넣는다.

실제 로봇이나 실행 화면이 아니라, REP-103의 변수 순서와 행우선 규칙을 옮긴 개념 도식이다.
단위 쪽 규칙도 REP-103에 있다. ROS는 SI 단위를 쓰며 각도의 단위는 radian이다. 그러니 회전 분산 칸에는 rad²가 들어간다. 속도 공분산(TwistWithCovariance)이라면 대각 원소는 (m/s)²와 (rad/s)²로 읽는 것이 SI 규약에서 따라 나오는 해석이다. 메시지 주석이 속도 공분산의 단위를 직접 적어 두지는 않았다.
2°와 0.05 m를 각 칸의 숫자로 바꾸기
x 칸부터 채운다. 표준편차 0.05 m를 제곱하면 0.05² = 0.0025 m²이고, 이 값이 0번 칸에 들어간다.
yaw는 한 단계가 더 있다. 도로 된 표준편차를 먼저 라디안으로 바꾼 뒤 제곱해야 한다.
- 2° × π/180 = 0.03490658503988659 rad
- (0.03490658503988659 rad)² = 0.0012184696791468343 rad² ≈ 0.00122 rad²
이 값이 35번 칸에 들어간다. 도와 라디안 변환 자체, 그리고 yaw의 0점과 회전 방향이 헷갈린다면 나침반 방위를 ROS ENU yaw로 바꾸는 글에서 각도 규약을 먼저 맞추는 편이 낫다. 이 글이 다루는 것은 yaw 값이 아니라 yaw 불확실성이 들어갈 칸과 단위다.
나머지 34칸을 0으로 두면 “서로 다른 변수의 오차끼리는 상관이 없다”고 가정한 셈이 된다. 수학적으로는 그런 뜻이고, 그 가정이 쓰려는 센서에 맞는지는 센서 쪽 자료로 따로 판단해야 한다.
아래 코드는 두 값을 채운 뒤 대각 여섯 칸을 출력한다. 표준 라이브러리만 쓰므로 ROS 패키지는 필요 없다. 2026년 9월 27일 Linux의 Python 3.12.3에서 python3 -I -B로 실행해 종료 코드 0과 아래 출력을 확인했다. 이 글의 숫자는 모두 이런 산술 검산에서 나왔고, ROS 노드나 필터에 값을 넣어 본 결과는 아니다.
import math
ORDER = ["x", "y", "z", "roll", "pitch", "yaw"] # REP-103 순서
N = 6
def idx(row, col):
"""6x6 행우선 배열에서 row행 col열의 인덱스(0부터)."""
return row * N + col
cov = [0.0] * 36
cov[idx(0, 0)] = 0.05 ** 2 # x 표준편차 0.05 m -> m^2
cov[idx(5, 5)] = math.radians(2.0) ** 2 # yaw 표준편차 2도 -> rad^2
for i, name in enumerate(ORDER):
print(f"{name:5s} index={idx(i, i):2d} value={cov[idx(i, i)]!r}")
assert all(cov[idx(i, j)] == cov[idx(j, i)] for i in range(N) for j in range(N))
print("deg^2 4.0 / correct:", 4.0 / cov[idx(5, 5)])
x index= 0 value=0.0025000000000000005
y index= 7 value=0.0
z index=14 value=0.0
roll index=21 value=0.0
pitch index=28 value=0.0
yaw index=35 value=0.0012184696791468343
deg^2 4.0 / correct: 3282.806350011744
0번 칸이 0.0025가 아니라 0.0025000000000000005로 찍히는 것은 부동소수점 표현 때문이며 계산이 틀린 것이 아니다. 마지막 줄은 다음 절에서 다룰 “도²를 넣었을 때의 배율”이다. 같은 계산을 더 많은 사례로 묶은 HuntLab의 공개 산술 예제 example.py와 그 검증 기록 verification.json도 공개돼 있다. 검증 기록에는 Python 3.12.3, 종료 코드 0, 아홉 사례의 기대값과 실제값 일치가 남아 있다.
행우선과 열우선을 바꿔 읽어도 배열이 그대로인 조건
행우선 배열의 i×6+j번 칸을 누군가 열우선으로 읽으면, 그 칸은 i행 j열이 아니라 j행 i열로 해석된다. 공분산 행렬은 대칭이라 i행 j열과 j행 i열에 같은 값이 들어 있다. 대각 칸은 i = j라서 번호부터 같다. 결국 올바르게 채운 공분산 행렬이라면 행우선으로 편 36칸과 열우선으로 편 36칸은 완전히 같은 배열이다.
이 결론에는 조건이 붙는다. x 분산 0.0025, yaw 분산 0.0012184696791468343, x–y 공분산 0.001을 대칭으로 채운 6×6 행렬을 두 방식으로 펴서 비교하면 두 배열은 같다. 같은 행렬에서 1행 0열만 비우고 0행 1열에만 0.001을 넣으면 두 배열이 달라진다. 행우선 배열의 1번 칸은 0.001, 6번 칸은 0.0이다. 이 비교는 공개 예제에는 없는 추가 산술이며 같은 날 같은 Python 환경에서 확인했다.
그렇다고 이 경우의 진짜 문제가 행/열 우선 혼동은 아니다. 한쪽 삼각형만 채운 순간 이미 대칭이 아니므로 공분산 행렬로서 잘못된 입력이다. 그래서 “행/열 우선을 헷갈려도 괜찮다”는 말은 “대칭 쌍을 모두 채웠다면”이라는 전제와 함께 써야 한다. 비대각 칸을 채우는 코드라면 행/열 우선 검사보다 cov[i*6+j] == cov[j*6+i] 대칭 검사가 더 의미 있다. 위 코드의 assert가 그 검사다.
값을 실제로 틀어지게 하는 것은 변수 순서와 단위다
회전을 앞에 두는 순서로 착각할 때
변수 순서를 (roll, pitch, yaw, x, y, z)로 잘못 기억했다고 해 보자. 이것은 설명을 위해 만든 반례이며, 특정 라이브러리가 이 순서를 쓴다는 뜻은 아니다. 이 착각에서는 yaw가 2번 변수가 되어 2×6+2 = 14번 칸에 들어간다. ROS 규약에서 14번은 z 분산 칸이다. x는 3번 변수가 되어 3×6+3 = 21번, 즉 roll 분산 칸에 들어간다. 위치 분산(m²)이 각도 분산(rad²) 자리에 앉아 단위까지 섞인다.
이 실수는 배열 길이도, 대칭성도 망가뜨리지 않는다. 그래서 행렬 모양만 보는 검사로는 드러나지 않는다.
표준편차나 도 단위를 그대로 넣을 때
단위 실수는 숫자의 크기를 바꾼다. 아래 배율과 해석되는 표준편차는 모두 식으로 직접 계산할 수 있다. 도² 배율은 공개 예제와 위 코드 출력에 있고, 나머지 행은 같은 환경에서 추가로 계산한 값이다.
| 넣은 값 | 들어간 칸 | 올바른 값 | 배율 | 배열이 뜻하게 되는 표준편차 |
|---|---|---|---|---|
| 도 단위 분산 4 (= 2²) | 35 | 0.0012184697 rad² | 4 / 0.0012184697 ≈ 3282.8배 | √4 = 2 rad ≈ 114.6° |
| 표준편차 0.05 (제곱 안 함) | 0 | 0.0025 m² | 0.05 / 0.0025 = 20배 | √0.05 ≈ 0.224 m |
| yaw 표준편차 0.0349 rad (제곱 안 함) | 35 | 0.0012184697 rad² | 약 28.65배 | 약 10.70° |
첫 행의 3282.8은 우연한 숫자가 아니다. 같은 각도의 분산을 도²로 적은 값과 rad²로 적은 값의 비는 언제나 (180/π)² = 3282.806350011744이다. 입력한 표준편차가 2°든 5°든 도²를 rad² 칸에 넣으면 분산은 이만큼 커진다. 해석으로 옮기면 “yaw를 ±2° 정도로 안다”고 전하려던 값이 “±115° 정도로 모른다”는 값이 된다.
이렇게 부풀린 값을 받은 필터가 측정을 사실상 무시하는지, 수렴이 늦어지는지는 이 계산으로 알 수 없다. 계산이 말해 주는 것은 배열 속 분산이 몇 배가 되고, 그 분산이 어떤 표준편차를 뜻하게 되는지까지다.
채운 배열은 대각 여섯 칸을 서로 다른 크기로 점검한다
배열을 채우는 코드를 테스트한다면 대각 0, 7, 14, 21, 28, 35가 각각 x, y, z, roll, pitch, yaw의 분산인지, 앞 세 칸은 m² 크기이고 뒤 세 칸은 rad² 크기인지 한 줄씩 확인한다. 이때 테스트 입력의 여섯 분산을 서로 다른 값으로 넣어야 한다. 모든 대각에 같은 값을 넣으면 순서가 뒤섞여도 결과 배열이 같아서 순서 착각을 잡을 수 없다.
규칙이 손에 붙었는지는 다음 세 질문으로 가늠할 수 있다. 답을 보기 전에 먼저 계산해 본다.
- y와 yaw 사이의 공분산을 채우려면 어느 칸에 넣어야 할까?
답: 11번과 31번 두 칸이다. y는 1번 변수, yaw는 5번 변수이므로 1행 5열은1×6+5 = 11, 대칭 쌍인 5행 1열은5×6+1 = 31이다. 한 칸만 채우면 대칭이 깨진다. sensor_msgs/Imu의orientation_covariance는 9칸이다. 마지막 대각 칸은 몇 번일까?
답: 8번이다.Imu.msg의orientation_covariance선언은float64[9]이고, 같은 줄 주석이 “Row major about x, y, z axes”라고 적는다. 9칸이니 3×3이고 한 행이 3칸이므로 2행 2열은2×3+2 = 8이다. 곱하는 수가 행렬의 한 변 길이로 바뀔 뿐 규칙은 같다.- 테스트에서 여섯 대각에 모두 0.01을 넣었더니 통과했다. 순서 착각이 없다고 말할 수 있을까?
답: 말할 수 없다. 대각 값이 모두 같으면 어느 변수가 어느 칸에 갔는지 구별되지 않는다. 위치와 각도의 자릿수가 다르도록 서로 다른 값을 넣어야 한다.
두 번째 질문의 IMU 메시지에는 자세 공분산에 없는 규약이 하나 더 있다. Imu.msg 주석은 공분산이 전부 0이면 “covariance unknown”으로 해석하고, 추정치가 없는 항목은 해당 공분산의 0번 원소를 -1로 두라고 한다. PoseWithCovariance.msg에는 같은 문구가 없으므로, 자세 공분산에서도 같은 뜻으로 통한다고 가정하지 않는다.
칸 번호와 단위가 맞아도 남는 질문
이 계산은 “원하는 불확실성이 규약에 맞는 칸과 단위로 들어갔다”까지만 보장한다. 그 숫자가 센서의 실제 불확실성을 잘 반영하는지, 행렬이 양의 준정부호(positive semi-definite) 조건을 만족하는지, 비대각 공분산을 어떻게 추정하는지는 다루지 않았다. robot_localization이나 Nav2 AMCL 같은 패키지가 이 값을 어떻게 쓰는지도 이 글 밖의 질문이다. 메시지의 자세는 쿼터니언으로 담기지만 공분산은 고정축 회전 기준이라는 점도 적어 둘 만하다. 둘 사이의 변환 역시 범위 밖이다.
Odometry.msg 주석은 pose가 header.frame_id, twist가 child_frame_id 좌표계로 지정된다고만 말한다. 공분산이 어느 프레임 기준인지 따로 밝힌 문구는 찾지 못했으므로 여기서 단정하지 않는다.
마지막으로 REP-103은 모든 ROS 소프트웨어가 이 규약을 따르도록 강제할 수는 없다고 스스로 밝힌다. 직접 공분산을 채운다면 위 인덱스와 단위를 그대로 쓰면 된다. 다른 사람이 만든 드라이버의 공분산을 받아 쓴다면, 그 드라이버 문서가 순서와 단위를 REP-103대로 적었는지부터 확인하는 것이 먼저다. 단위를 경계에서 검사하는 방법은 ROS 속도 단위 경계 테스트 글에서 다른 예로 이어 볼 수 있다. 이 글에는 제휴·협찬이 없다.
참고 링크
- REP 103: Standard Units of Measure and Coordinate Conventions (고정 커밋 11ca24a) — 공분산 표현, 각도 단위, roll·pitch·yaw 정의. 2026-09-27 확인.
- ros2/common_interfaces (Jazzy, a941f14)
PoseWithCovariance.msg— 6×6 행우선, 변수 순서,float64[36]. Rolling과 ROS 1 Noetic도 같은 문구. 2026-09-27 확인. - ros2/common_interfaces (Jazzy)
TwistWithCovariance.msg— 속도 공분산의 같은 배치. 2026-09-27 확인. - ros2/common_interfaces (Jazzy)
Imu.msg— L18orientation_covariance의float64[9]행우선(x, y, z) 선언, L7–L13 전부 0·0번 원소 -1 규약. 2026-09-27 확인.