회전을 표현하는 방법 중 하나인 Quaternion에 대해 다뤄보겠습니다.
회전을 표현하는 다른 방법인 Rotation Matrix(이전글)와 비교했을 때, Quaternion은 회전 계산량이 작으며 메모리 효율적이고, Gimbal Lock(짐벌락) 현상이 발생하지 않는 장점을 갖고 있습니다.
다만 직관적으로 이해하거나 시각화기가 어렵다는 단점을 갖고 있습니다.
Quaternion이란?
기본적으로 Quaternion(사원수)는 4개의 실수 값을 3개의 복소수로 표현한 형태를 말합니다.
$$ q=w+xi+yj+zk $$
$w$ 스칼라 부분이고 $x,y,z$는 벡터부분 $i,j,k$는 허수 단위입니다. 허수(imaginary number) 는 실수만으로는 해결 할 수 없는 문제(대표적으로 음수의 제곱근문제)를 풀기 위해 만들어졌습니다. 허수 개념을 리마인드하고자 아래 테이블을 갖고 와봤습니다.(그림출처 : link)
이 3개의 복소수를 사용하는 Quaternion을 그래픽스, 로봇, 항공 등 분야에선 물체를 회전하는데 사용합니다.
Quaternion의 회전 표현
회전 축 $\mathbf{u} = \left(u_x, u_y, u_z\right)$와 회전 각도 $ {\theta} $를 통해 정의됩니다.
$$ q = \cos\left(\frac{\theta}{2}\right) + \sin\left(\frac{\theta}{2}\right)\left(u_x i + u_y j + u_z k\right) $$ cos부분은 $w$와 대응되고, sin부분은 $xi+yj+zk$ 대응되는 것을 알 수 있습니다.
다른 회전 표현법인 Rotation Matrix(이전글) 에서도 cos, sin을 사용하는게 비슷한 형태임을 알 수 있습니다. 복소수가 들어가기 때문에 머리속으로 생각하기가 좀 까다로워집니다. 때문에 (1,0,0) 인 벡터를 z축으로 Quaternion으로 10도씩 회전하는 예시를 먼저 가져와 보았습니다.
회전 되지않은 0도에서는 scalar부분이 1인데 vector부분이 0이었다가 회전을 시작하면서 scalar값이 바뀌고 $u_z$값이 변경되는 것을 볼 수 있습니다. 0도에서 시작 vector가 (1,0,0)일 때 scalar가 1, $u_z$는 0이었다가 180도에서 종료 vector가 (-1,0,0)일 때 scalar값은 0, $u_z$는 1이 되는 것을 볼 수 있습니다. 앞서 Quaternion이 직관적으로 이해하기 어렵다고 했었는데 역시나 값을 출력해봐도 정확히 와닿지는 않습니다.
Quaternion의 회전
회전하는 절차를 단계적으로 살펴보겠습니다.
1) 각 회전축에 대해 정규화를 합니다. 크기가 1인 단위벡터를 만들어주기 위함입니다.
$$ f{u}_{\text{normalized}} = \frac{\mathbf{u}}{\|\mathbf{u}\|} $$ $$ \|\mathbf{u}\| = \sqrt{u_x^2 + u_y^2 + u_z^2} $$
2) 회전 쿼터니언 q를 만들어줍니다.
우선 degree -180~180 범위가 아닌 radian -$\pi$ ~$\pi$ 범위로 바꿔줍니다. $$\text{radian} = \text{degree} \times \frac{\pi}{180}$$ 그리고 앞서 언급한 수식을 적용해줍니다. $$ q = \cos\left(\frac{\theta}{2}\right) + \sin\left(\frac{\theta}{2}\right)\left(u_x i + u_y j + u_z k\right) $$ 코드를 적어보면 다음과 같습니다. 코드에서 angle_radians는 길이가 3인 1차원 배열입니다.
angle_radians = np.radians(angle_degrees)
w = np.cos(angle_radians / 2)
x, y, z = rotation_axis * np.sin(angle_radians / 2)
q = np.array([w, x, y, z])
3) 입력 벡터 $v=(v_x, v_y, v_z)$ 를 쿼터니언 형태 $q_v = [0, v_x, v_y, v_z]$ 로 변환합니다.
q_v = np.array([0] + list(point))
4) 쿼터니언을 계산합니다. $$ q_{\text{rotated}} = q \cdot q_v \cdot q^* $$ 여기서 q*는 쿼터니언의 켤레($ q^* = [w, -x, -y, -z] $)를 나타냅니다. 켤레를 곱하는 이유는 수학적인 내용을 정확히 이해를 못했지만 3차원 공간상의 $q_v$에 $q$만 곱하면 4차원에 위치하게 되는데 $q*$를 다시 곱해줌으로써 3차원에 위치하도록 만들어 주는 역할을 한다고 합니다.
쿼터니언 곱셈 식은 아래와 같습니다. $$ q_1 \cdot q_2 = \begin{bmatrix} w_1w_2 - x_1x_2 - y_1y_2 - z_1z_2 \\ w_1x_2 + x_1w_2 + y_1z_2 - z_1y_2 \\ w_1y_2 - x_1z_2 + y_1w_2 + z_1x_2 \\ w_1z_2 + x_1y_2 - y_1x_2 + z_1w_2 \end{bmatrix}$$ 쿼터니언 곱셈 규칙이라 생각해주시면 됩니다.
해당 수식을 적용한 코드입니다.
# 쿼터니언 곱셈 함수
def quaternion_multiply(q1, q2):
w1, x1, y1, z1 = q1
w2, x2, y2, z2 = q2
return np.array([
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
])
q_conjugate = np.array([w, -x, -y, -z])
rotated_quaternion = quaternion_multiply(quaternion_multiply(q, q_v), q_conjugate)
위에서 언급한 전체 코드입니다.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
# 쿼터니언 곱셈 함수
def quaternion_multiply(q1, q2):
w1, x1, y1, z1 = q1
w2, x2, y2, z2 = q2
return np.array([
w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2,
w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2,
w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2,
w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2
])
# 쿼터니언을 이용한 3D 회전 함수
def rotate_3d_quaternion(point, angle_degrees, axis='x'):
# 회전 축 설정
if axis == 'x':
rotation_axis = np.array([1, 0, 0])
elif axis == 'y':
rotation_axis = np.array([0, 1, 0])
elif axis == 'z':
rotation_axis = np.array([0, 0, 1])
rotation_axis = rotation_axis / np.linalg.norm(rotation_axis) # 축 정규화
# 회전 쿼터니언 계산
angle_radians = np.radians(angle_degrees)
w = np.cos(angle_radians / 2)
x, y, z = rotation_axis * np.sin(angle_radians / 2)
q = np.array([w, x, y, z])
# 순수 쿼터니언 (벡터 부분만 있는 쿼터니언)
q_v = np.array([0] + list(point))
# 회전: q * q_v * q_conjugate
q_conjugate = np.array([w, -x, -y, -z])
rotated_quaternion = quaternion_multiply(quaternion_multiply(q, q_v), q_conjugate)
# 결과 벡터 반환
return rotated_quaternion[1:], q, q_v, q_conjugate # 첫 번째 항목은 스칼라 부분이므로 제외
# 초기 설정
point = np.array([1, 0, 0]) # 초기 점
angles = np.arange(0, 181, 10) # 0도부터 180도까지 10도 단위
axis = 'z' # 회전 축 설정 ('x', 'y', 'z' 중 선택)
for angle in angles:
rotated, q, q_v, q_conjugate = rotate_3d_quaternion(point, angle, axis=axis)
Quaternion to Degree
마지막으로 읽기 어려운 쿼터니언 표기법을 읽기 편한 회전축과 각도로 바꿔보겠습니다. 쿼터니언의 기본 형식을 다시 보겠습니다. $$ q=w+xi+yj+zk $$ 여기서 w를 획득하는 수식은 다음과 같습니다. $$\theta = 2 \cdot \arccos(w)$$ 회전축은 쿼터니언의 벡터 성분 (x,y,z)로 계산합니다. $$\mathbf{u} = \frac{\left(x, y, z\right)}{\sqrt{x^2 + y^2 + z^2}}, \quad \text{if } \sqrt{x^2 + y^2 + z^2} \neq 0$$ 회전각도 $\theta = 0$일 때 회전축은 정의 할 수 없습니다.
이렇게 계산된 $\theta$는 radian -$\pi$ ~$\pi$이므로 degree -180~180범위로 바꿔줍니다. $$\text{degree} = \text{radian} \times \frac{180}{\pi}$$ 이를 정리한 코드입니다.
def quaternion_to_angle_axis(q):
w, x, y, z = q
theta = 2 * np.arccos(w)
norm = np.sqrt(x**2 + y**2 + z**2)
if norm < 1e-6: # 회전 각도가 0인 경우, 축은 정의되지 않음
return theta, 0, 0, 0
else:
u_x, u_y, u_z = x / norm, y / norm, z / norm
return np.degrees(theta), u_x, u_y, u_z
'Camera Model' 카테고리의 다른 글
[개념 정리] SfM(Structure from Motion) of COLMAP - 3부 (0) | 2025.01.18 |
---|---|
[개념 정리] SfM(Structure from Motion) of COLMAP - 2부 (4) | 2025.01.05 |
[개념 정리] Rotation Matrix (회전 행렬) (0) | 2024.12.21 |
[논문 리뷰] MASt3R (arXiv2024) : 3D기반 Image Matching 기법 (0) | 2024.06.30 |
[개념 정리] Fronto Parallel Plane (0) | 2024.05.26 |
댓글