회전 변환 행렬 (2D, 3D)

회전 변환 행렬 (2D, 3D)

2020, Jan 02    

선형대수학 글 목차


  • 참조 : https://en.wikipedia.org/wiki/Rotation_matrix
  • 참조 : https://ko.wikipedia.org/wiki/회전변환행렬


  • 이번 글에서는 2D와 3D 상태에서의 좌표의 회전 변환하는 방법에 대하여 알아보도록 하겠습니다.



목차



2D에서의 회전 변환


  • 2D 좌표계에서 회전 변환을 할 때 사용하는 변환 행렬은 다음과 같습니다.


  • R(θ)=[cosθsinθsinθcosθ]R(θ)=[cosθsinθsinθcosθ]


  • 여기서 θθ는 각도에 해당합니다. 반시계 방향으로 회전하는 방향이 + 각도가 됩니다.
  • 위 회전 행렬을 이용하여 (x,y)(x,y) 좌표를 회전 변환을 하면 다음과 같습니다.


  • [cosθsinθsinθcosθ][xy]=[xcosθysinθxsinθ+ycosθ]=[xy][cosθsinθsinθcosθ][xy]=[xcosθysinθxsinθ+ycosθ]=[xy]


  • 위 식을 이용하여 회전 변환한 좌표를 구하면 다음과 같습니다.


Drawing


  • 자주 사용하는 회전인 90도 회전 / 180도 회전 / 270도 회전은 다음과 같습니다.


  • R(π2)=[0110]R(π2)=[0110]
  • R(π2)=[1001]R(π2)=[1001]
  • R(3π2)=[0110]R(3π2)=[0110]


회전 변환 행렬 유도


  • 회전 변환을 다루는 방법에 대해서는 위 글에서 다루었습니다. 그러면 왜 저런 형태의 행렬식이 유도되었는 지에 대하여 다루어 보겠습니다.


Drawing


  • 먼저 앞에서 다룬 회전 변환은 원점을 기준으로 회전을 하게 됩니다. 따라서 위 그림에서도 원점을 중심으로 PP'로 어떻게 변환되는 지 다루어 보도록 하겠습니다.
  • 아래 식에서 P,¯OP,cos(α),sin(α)P,¯¯¯¯¯¯¯¯OP,cos(α),sin(α)를 정의해 보겠습니다.


  • P=(x,y)P=(x,y)
  • ¯OP=l=(x0)2+(y0)2)=x2+y2¯¯¯¯¯¯¯¯OP=l=(x0)2+(y0)2)=x2+y2
  • cos(α)=x¯OP=xx2+y2cos(α)=x¯¯¯¯¯¯¯¯OP=xx2+y2
  • sin(α)=y¯OP=yx2+y2sin(α)=y¯¯¯¯¯¯¯¯OP=yx2+y2


  • 위 식을 그대로 이용하여 PP에 적용해 보도록 하겠습니다. P=(x,y)P=(x,y)P=(x,y)P=(x,y)+θ+θ 만큼 회전 시킨 것이므로 회전 각도 만큼 반영해여 식을 적어보겠습니다.


  • x=x2+y2cos(α+θ)x=x2+y2cos(α+θ)
  • y=x2+y2sin(α+θ)y=x2+y2sin(α+θ)


Drawing


  • 삼각함수의 덧셈 정리를 이용하여 식을 풀어보도록 하겠습니다.


  • x=x2+y2(cos(α)cos(θ)sin(α)sin(θ))=(x2+y2xx2+y2cos(θ)x2+y2yx2+y2sin(θ))=xcos(θ)ysin(θ)x=x2+y2(cos(α)cos(θ)sin(α)sin(θ))=(x2+y2xx2+y2cos(θ)x2+y2yx2+y2sin(θ))=xcos(θ)ysin(θ)


  • y=x2+y2sin(α+θ)=x2+y2(sin(α)cos(θ)+cos(α)sin(θ))=(x2+y2yx2+y2cos(θ)+x2+y2xx2+y2sin(θ))=ycos(θ)+xsin(θ)y=x2+y2sin(α+θ)=x2+y2(sin(α)cos(θ)+cos(α)sin(θ))=(x2+y2yx2+y2cos(θ)+x2+y2xx2+y2sin(θ))=ycos(θ)+xsin(θ)


  • 위에서 유도한 식을 정리하면 다음과 같습니다.


  • (xy)=(cosθsinθsinθcosθ)(xy)(xy)=(cosθsinθsinθcosθ)(xy)


임의의 점을 중심으로 회전 변환


  • 앞에서 다룬 내용은 모두 원점을 기준으로 회전한 것입니다. 좀 더 일반적인 케이스를 적용하기 위해 기준이 원점이 아니라 특정 좌표를 기준으로 회전 시켜보도록 하겠습니다.


Drawing


  • 위 그림을 보면 원점을 기준으로 30도 회전한 것을 알 수 있습니다.


Drawing


  • 반면에 위 그림에서는 회전한 기준을 보면 (1, 0)임을 알 수 있습니다.


Drawing


  • 지금부터 해야할 작업은 위 그림 처럼 기준점에서 각 점 방향으로의 벡터를 회전하는 것입니다. (물론 반시계 방향 회전이 + 회전 각도 입니다.)


Drawing


  • ① 기준점을 v0=(xbase,ybase)v0=(xbase,ybase) 이라고 하면 각 점 방향으로의 벡터는 viv0=(xixbase,yiybase)viv0=(xixbase,yiybase)이 됩니다.
  • ② 이 벡터를 앞에서 알아본 변환 행렬을 이용하여 회전 시키면 됩니다.


  • (xy)=(cosθsinθsinθcosθ)(xxbaseyybase)(xy)=(cosθsinθsinθcosθ)(xxbaseyybase)


  • 여기 까지 계산 하면 ((0,0)(0,0)(xxbase,yybase)(xxbase,yybase)) 방향과 크기의 벡터가 θθ 만큼 회전하여 (x,y)(x,y)가 되었습니다.
  • ③ 벡터의 시작점을 회전 기준인 (xbase,ybase)(xbase,ybase)으로 옮겨줍니다.
  • ①, ②, ③ 과정을 식으로 옮기면 다음과 같습니다.


  • (xy)=(cosθsinθsinθcosθ)(xxbaseyybase)+(xbaseybase)(xy)=(cosθsinθsinθcosθ)(xxbaseyybase)+(xbaseybase)





3D에서의 회전 변환


  • 3D에서의 회전 변환은 2차원에서 사용한 회전 변환 행렬을 유사하게 사용합니다. 다만 이 때, 3차원에 맞춰서 행렬의 차원이 늘어나게 되고 각 차원별로 회전을 고려해 주어야 합니다.
  • 예를 들어서 Rx(θ)Rx(θ)는 x축을 중심으로 회전하는 행렬 변환이고 Ry(θ)Ry(θ)는 y축을 중심으로 Rz(θ)Rz(θ)는 z축을 중심으로 회전하는 행렬 변환입니다.


  • Rx(θ)=[1000cosθsinθ0sinθcosθ]Rx(θ)=1000cosθsinθ0sinθcosθ
  • Ry(θ)=[cosθ0sinθ010sinθ0cosθ]
  • Rz(θ)=[cosθsinθ0sinθcosθ0001]


  • 이 행렬을 정리해 보려고 하는데, 그 전에 roll, yaw, pitch에 대하여 알아보겠습니다.


Drawing


  • 위 회전축 기준으로 roll은 x축을 기준으로 회전한 양을 뜻하고 pitch는 y축을 기준으로 회전한 양 그리고 yaw는 z축을 기준으로 회전한 양을 뜻합니다. 위 그림처럼 생각하시면 됩니다.
    • 예를 들어 자동차가 좌회전 또는 우회전을 한다면 z축을 기준으로 회전을 하는 것이므로 yaw의 변화가 있게 됩니다.
  • 그러면 Rx(θ), Ry(θ) 그리고 Rz(θ) 각각 x축, y축, z축을 기준으로 회전하는 회전 변환 행렬이 됩니다.
  • x축을 기준으로 회전한 roll angleα, y축을 기준으로 회전한 pitch angleβ 마지막으로 z축을 기준으로 회전한 yaw angleγ로 두겠습니다.


  • R=Rz(γ)Ry(β)Rx(α)=[cosγsinγ0sinγcosγ0001][cosβ0sinβ010sinβ0cosβ][1000cosαsinα0sinαcosα]


  • 위 변환 행렬을 모두 곱하면 roll, pitch, yaw angle을 모두 고려한 회전을 나타낼 수 있습니다.
  • 위 식을 풀어서 나타내면 다음과 같습니다.


  • R=[cosγ cosβcosγ sinβ sinαsinγ cosαcosγ sinβ cosα+sinγ sinαsinγ cosβsinγ sinβ sinα+cosγ cosαsinγ sinβ cosαcosγ sinαsinβcosβ sinαcosβ cosα]


  • 위 식을 파이썬 코드로 나타내면 다음과 같습니다.


import numpy as np

def euler_to_rotation_matrix(roll, pitch, yaw):
    # Convert angles to radians
    roll = np.radians(roll)
    pitch = np.radians(pitch)
    yaw = np.radians(yaw)

    # Define individual rotation matrices
    Rx = np.array([[1, 0, 0],
                   [0, np.cos(roll), -np.sin(roll)],
                   [0, np.sin(roll), np.cos(roll)]])
    
    Ry = np.array([[np.cos(pitch), 0, np.sin(pitch)],
                   [0, 1, 0],
                   [-np.sin(pitch), 0, np.cos(pitch)]])
    
    Rz = np.array([[np.cos(yaw), -np.sin(yaw), 0],
                   [np.sin(yaw), np.cos(yaw), 0],
                   [0, 0, 1]])

    # Combine the rotations
    R = np.dot(Rz, np.dot(Ry, Rx))
    return R


회전 변환 행렬의 구성 요소


  • 회전 변환 행렬의 경우 각 열의 성분각 축의 기저 벡터 (basis vector)가 회전 되었을 때의 벡터를 의미합니다. 3차원 행렬의 경우 X,Y,Z 순서로 축의 의미를 가진다면 회전 변환 행렬의 첫번째 열은 X 축의 기저 벡터를 회전 변환하였을 때의 벡터, 두번째 열은 Y 축의 기저 벡터를 회전 변환하였을 때의 벡터, 세번째 열은 Z 축의 기저 벡터를 회전 변환하였을 때의 벡터를 의미합니다.
  • 각 축의 기저 벡터를 회전하는 예시를 확인하면 그 뜻을 쉽게 알 수 있습니다.


  • [R11R12R13R21R22R23R31R32R33][100]=[R11R21R31]
  • [R11R12R13R21R22R23R31R32R33][010]=[R12R22R32]
  • [R11R12R13R21R22R23R31R32R33][001]=[R13R23R33]


  • 첫번째 예시인 X 축의 예시를 살펴보면 기저 벡터가 [1,0,0]T[R11,R21,R31]T 로 변환된 것을 알 수 있습니다. 두번째, 세번째 예시를 통하여 Y,Z 축이 어떻게 변하는 지 또한 확인할 수 있습니다.


  • 다음 예시를 통하여 간단하게 회전 변환의 역할에 대하여 알아보도록 하겠습니다.


  • [010001100]


  • 먼저 첫번째 열인 [0,0,1]T 을 통해 X 축의 기저 벡터에 회전 변환 행렬을 적용하면 [1,0,0][0,0,1]T 이 된 것을 알 수 있습니다. 즉, Z 축의 기저 벡터 처럼 변경 되었습니다.
  • 두번째 열인 [1,0,0]T 을 통해 Y 축의 기저 벡터에 회전 변환 행렬을 적용하면 [0,1,0][1,0,0]T 이 된 것을 알 수 있습니다. 즉, 음의 방향의 X 축 기저 벡터 처럼 변경 되었습니다.
  • 세번째 열인 [0,1,0]T 을 통해 Z 축의 기저 벡터에 회전 변환 행렬을 적용하면 [0,0,1][0,1,0]T 이 된 것을 알 수 있습니다. 즉, 음의 방향의 Y 축 기저 벡터 처럼 변경 되었습니다.
  • 따라서 R[:, 0], R[:, 1], R[:, 2] 성분을 이용하면 기저 벡터가 어떻게 회전 변환 되는 지 알 수 있습니다.


  • 임의의 회전 행렬을 적용하였을 때, 현재 좌표축 기준으로 변환된 좌표축에서의 기저 벡터를 구하고 싶다면 각 열에서 절대값이 가장 큰 축을 기저 벡터로 간주하여 기저 벡터를 구성하면 됩니다. 이 때, 부호 또한 반영하면 현재 기저 벡터 기준으로 축의 방향 또한 고려하여 확인할 수 있습니다.


R = np.array([
    0.00463, -0.99998, 0.00385, 
    -0.01405, -0.00391, -0.99989, 
    0.99989, 0.00457, -0.01407]
).reshape(3, 3)

basis_rotation = np.zeros((3, 3), dtype=np.float32)
for i in range(3):
    axis = np.argmax(np.abs(R[:, i]))
    if R[axis, i] > 0:
        basis_rotation[axis, i] = 1.0
    elif R[axis, i] < 0:
        basis_rotation[axis, i] = -1.0
    else:
        basis_rotation[axis, i] = 0.0

print(basis_rotation)
# [[ 0. -1.  0.]
#  [ 0.  0. -1.]
#  [ 1.  0.  0.]]


  • basis_rotation이 앞에서 해석한 내용과 동일한 값을 가집니다. 이와 같은 방법으로 현재 좌표계 기준의 기저 벡터가 회전 변환을 거쳤을 때, 어떻게 바뀌는 지 확인할 수 있습니다.


회전 변환 행렬의 직교성


  • 지금까지 살펴본 rotation 행렬은 orthogonal 행렬이며 다음과 같은 성질을 따릅니다.


  • RT=R1
  • RTR=I


  • orthogonal 또는 orthonormal인 행렬 Q 가 있을 때, QQT=QTQ=I 임은 필요충분 조건임이 알려져 있습니다.
  • 앞에서 살펴본 2D, 3D 회전 변환 행렬의 경우도 RRT=RTR=I 를 만족하며 일반적으로 orthogonal 형태이므로 orthogonal 하다고 말할 수 있습니다.
  • 또한 orthogonal한 경우 determinant가 1을 만족하는데 이 조건에도 만족하게 됩니다.


Drawing


  • 위 계산 결과와 같이 2D 회전 변환 행렬 RRRT=I 임을 확인할 수 있습니다.


  • 3D 회전 변환 행렬의 경우 간단히 det(R) = I 임을 통해 orthogonal임을 확인해 보겠습니다.
  • 아래 식과 같이

  • R=Rz(α)Ry(β)Rx(γ)
  • RTR=(RzRyRx)T(RzRyRx)=RTxRTyRTzRzRyRx
  • RTxRX=I
  • RTyRy=I
  • RTzRz=I
  • RTR=I
  • plus, det(R)=det(Rz)det(Ry)det(Rx)=1×1×1=1


  • 아래와 같이 3D 회전 변환 행렬의 각 방향의 Rx,Ry,Rz 의 determinant는 1임을 확인할 수 있습니다.


Drawing


Roll, Pitch, Yaw와 Rotation 행렬의 변환



  • 지금까지 살펴본 방식은 Roll, Pitch, Yaw를 이용하여 Rotation 행렬을 구하는 방법에 대하여 살펴보았습니다.
  • 이번에는 임의의 Rotation 행렬이 주어졌을 때, 이 행렬을 Roll, Pitch, Yaw로 분해하는 방법을 알아보도록 하겠습니다. 3차원 좌표축의 기준은 앞에서 다룬바와 같이 X 축이 전방, Y 축이 왼쪽, Z 축이 위로 향하는 Forward-Left-Up 순서의 좌표축을 의미합니다. 만약 다른 좌표축 기준에서 Roll, Pitch, Yaw 성분을 분해하려면 아래 내용을 이해한 다음에 분해 순서 및 방식을 일부 수정하여 적용해야 합니다.


  • 앞에서 Roll, Pitch, Yaw 각각은 다음과 같은 형태로 표현할 수 있었습니다. 아래 식에서 ψx 축의 회전인 Roll 의 각도를 의미하며 θy 축의 회전인 Pitch의 각도를 의미하고 마지막으로 ϕz 축의 회전인 Yaw의 각도를 의미합니다.


  • Rx(ψ)=[1000cosψsinψ0sinψcosψ]
  • Ry(θ)=[cosθ0sinθ010sinθ0cosθ]
  • Rz(ϕ)=[cosϕsinϕ0sinϕcosϕ0001]


  • 앞에서 다룬 바와 같이 Rotation 행렬은 다음과 같이 Roll, Pitch, Yaw를 이용하여 표현할 수 있었습니다. 회전 순서는 Rx,Ry,Rz 순서로 곱해지기 때문에 Roll, Pitch, Yaw 순서로 회전 됩니다.

  • R=Rz(ϕ)Ry(θ)Rx(ψ)=[cosθcosϕsinψsinθcosϕcosψsinϕcosψsinθcosϕ+sinψsinϕcosθsinϕsinψsinθsinϕ+cosψcosϕcosψsinθsinϕsinψcosϕsinθsinψcosθcosψcosθ]=[R11R12R13R21R22R23R31R32R33]


  • 현재 알고 싶은 정보는 위 Rotation 행렬 R 을 이용하여 ψ,θ,ϕ 를 구하고 싶은 것입니다.


  • 2개의 θ 구하기


  • 앞에서 R 을 구하였을 때, R31 은 다음과 같습니다.


  • R31=sin(θ)


  • 따라서 θ 를 쉽게 구할 수 있습니다.


  • θ=sin1(R31)=sin1(R31)


  • 추가적으로 sin(πθ)=sinθ 를 만족하므로 다음과 같이 2개의 식을 만족하는 θ 를 구할 수 있습니다. 단, R31±1 인 경우를 가정하며 이유는 글의 뒷부분에서 이어서 설명하도록 하겠습니다.


  • θ1=sin1(R31)
  • θ2=πθ1=π+sin1(R31)


  • 위 식의 전개와 같이 θ 가 2 종류가 나오기 때문에 최종 해 또한 2 종류로 도출됩니다.


  • 대응되는 ψ 구하기


  • 위 식에서 R32,R33 을 이용하면 ψ 를 구할 수 있습니다.


  • R32R33=sin(ψ)cos(θ)cos(ψ)cos(θ)=sin(ψ)cos(ψ)=tan(ψ)
  • ψ=atan2(R32,R33)


  • 위 식의 atan2 는 수치적 안정성을 위해 사용합니다. 일반적인 atan 은 1사분면과 4사분면만 표현할 수 있습니다. 즉, 분자, 분모가 모두 양수이거나 모두 음수인 경우만 atan 을 연산할 수 있습니다. 반면에 4개의 사분면에서 모두 atan 을 적용하기 위해 함수화한 것이 atan2 라고 말할 수 있습니다. 상세 내용은 아래 링크를 참조하시기 바랍니다.


  • 따라서 atan2 를 적용할 때에는 부호를 잘 고려해서 함수에 적용하면 위 링크에서 예외 처리한 내용을 고민하지 않고 계산할 수 있습니다.
  • 따라서 앞선 식 R32R33=sin(ψ)cos(θ)cos(ψ)cos(θ)=sin(ψ)cos(ψ) 에서 단순히 cos(θ) 를 소거하지 않고 다음과 같이 연산해 줍니다. 다음과 같이 연산하면 cos(θ)0 일 때에 연산이 모두 유효합니다.


  • ψ=atan2(R32cos(θ),R33cos(θ))


  • 위 식에서 cos(θ)θ 의 값이 1, 4 사분면에 있으면 양수이고 2, 3 사분면에 있으면 음수이기 때문에 위 식과 같이 부호를 고려한다면 θ 의 값에 따라 ψ 를 구할 수 있습니다.
  • 앞선 식에서 θ 의 후보로 θ1,θ2 가 있었기 때문에 다음과 같이 2개의 ψ1,ψ2 를 구할 수 있습니다. cos(θ)=0 인 경우는 글 뒷부분에서 한번에 예외처리 하도록 하겠습니다.


  • ψ1=atan2(R32cos(θ1),R33cos(θ1))
  • ψ2=atan2(R32cos(θ2),R33cos(θ2))


  • 대응되는 ϕ 구하기


  • 앞에서 ψ1,ψ2 를 구할 때에는 R32,R33 을 이용하였습니다. ϕ 를 구할 때에는 R21,R11 을 이용해 보도록 하겠습니다. 방식은 완전히 동일합니다.


  • ϕ=atan2(R21cos(θ),R11cos(θ))


  • 이 경우에도 cos(θ)0 일 때, 계산이 모두 유효하며 아래와 같이 2개의 ϕ1,ϕ2 를 구할 수 있습니다.


  • ϕ1=atan2(R21cos(θ1),R11cos(θ1))
  • ϕ2=atan2(R21cos(θ2),R11cos(θ2))


  • cos(θ)0 일 때의 Euler Angles


  • 지금까지 살펴본 방법으로 cos(θ)0 일 때, 다음과 같이 2가지 Euler Angles를 구할 수 있습니다.


  • (ψ1,θ1,ϕ1)=(Roll1,Pitch1,Yaw1)
  • (ψ2,θ2,ϕ2)=(Roll2,Pitch2,Yaw2)


  • cos(θ)=0 일 때의 Euler Angles


  • 다음과 같이 cos(θ)=0 인 경우는 θ=π2 또는 θ=π2 입니다. 그리고 R31=sin(θ)=±1 인 경우를 의미하기도 합니다.
  • 이 경우에도 앞에서 살펴본 방식과 동일하게 수식을 전개한다면 R11,R12,R32,R33 이 0이기 때문에 다음과 같은 문제가 발생합니다.


  • ψ=atan2(00,00)
  • ϕ=atan2(00,00)


  • 즉, ψ,ϕ 의 값을 정할 수 없게 됩니다. 이와 같은 현상을 Gimbal Lock이라고 하며 다음 링크에서 그 현상의 기하학적인 의미를 살펴볼 수 있습니다.
    • 링크 : https://gaussian37.github.io/vision-concept-axis_angle_rotation/
  • 이러한 문제를 회피하여 Euler Angles를 구하기 위해 다음과 같이 식을 전개해 보도록 하겠습니다.


  • 먼저 θ=π2 인 경우 부터 살펴보도록 하겠습니다. cos(θ)0 일 때에는 R32,R33 을 이용하여 ψ 를 구하고 R11,R21 을 이용하여 ϕ 를 구하였습니다. 반면 cos(θ)=0 와 같은 예외 상황을 처리하기 위해서는 R12,R13,R22,R23 을 이용합니다.


  • 삼각함수의 덧셈 공식을 이용하면 다음과 같이 식을 전개할 수 있습니다.


Case 1: θ=π2


  • R12=sin(ψ)cos(ϕ)cos(ψ)sin(ϕ)=sin(ψϕ)
  • R13=cos(ψ)cos(ϕ)+sin(ψ)sin(ϕ)=cos(ψϕ)
  • R22=sin(ψ)sin(ϕ)cos(ψ)cos(ϕ)=cos(ψϕ)=R13
  • R23=cos(ψ)sin(ϕ)sin(ψ)cos(ϕ)=sin(ψϕ)=R12


  • R12R13=sin(ψϕ)cos(ψϕ)=tan(ψϕ)
  • (ψϕ)=atan2(R12,R13)
  • ψ=ϕ+atan2(R12,R13)


Case 2: θ=π2


  • R12=sin(ψ)cos(ϕ)cos(ψ)sin(ϕ)=sin(ψ+ϕ)
  • R13=cos(ψ)cos(ϕ)+sin(ψ)sin(ϕ)=cos(ψ+ϕ)
  • R22=sin(ψ)sin(ϕ)+cos(ψ)cos(ϕ)=cos(ψ+ϕ)=R13
  • R23=cos(ψ)sin(ϕ)sin(ψ)cos(ϕ)=sin(ψ+ϕ)=R12


  • R12R13=sin(ψ+ϕ)cos(ψ+ϕ)=tan(ψ+ϕ)
  • (ψ+ϕ)=atan2(R12,R13)
  • ψ=ϕ+atan2(R12,R13)


  • 지금까지 내용을 살펴보면 θ=±π2Gimbal Lock 상황에서는 ψϕ 가 서로 연결되어 있어 특정 해를 구할 수 없습니다. 따라서 특정 해를 지정하고 싶으면 ϕ=0 과 같이 임의의 값을 지정해 주어야 합니다.


  • 이 조건들을 모두 이용하면 다음과 같이 의사 코드를 작성할 수 있습니다.


Drawing


  • 첫 조건문의 R31±1θ±π2 을 의미합니다.
  • 첫 조건문에서 else로 분기되면 ϕ 는 어떤 값이 되어도 되므로 임의로 0으로 지정할 수 있습니다.


  • 정리하면 임의의 Rotation 행렬을 Euler AngleRoll, Pitch, Yaw로 분해할 수 있습니다.
  • 이 때, 특정 한 축의 회전각이 ±π2 인 경우가 아니라면 2개의 해를 가지게 되고 회전 각도의 범위를 고려하여 1개의 해를 선택하여 사용할 수 있습니다. 반면 한 축의 회전각이 ±π2 인 경우에는 무한히 많은 해를 가지게 되며 Gimbal Lock 현상이 발생합니다.


python code


  • 먼저 앞에서 설명한 기호를 이용하여 코드를 작성하면 다음과 같습니다.


import numpy as np

# Define the conversion function
def rotation_matrix_to_euler_angles(R):
    assert(R.shape == (3, 3))

    if R[2, 0] != 1 and R[2, 0] != -1:
        theta1 = -np.arcsin(R[2, 0])
        theta2 = np.pi - theta1
        psi1 = np.arctan2(R[2, 1] / np.cos(theta1), R[2, 2] / np.cos(theta1))
        psi2 = np.arctan2(R[2, 1] / np.cos(theta2), R[2, 2] / np.cos(theta2))
        phi1 = np.arctan2(R[1, 0] / np.cos(theta1), R[0, 0] / np.cos(theta1))
        phi2 = np.arctan2(R[1, 0] / np.cos(theta2), R[0, 0] / np.cos(theta2))
        return (psi1, theta1, phi1), (psi2, theta2, phi2)
    else:
        phi = 0  # can set to anything, it's the gimbal lock case
        if R[2, 0] == -1:
            theta = np.pi / 2
            psi = phi + np.arctan2(R[0, 1], R[0, 2])
        else:
            theta = -np.pi / 2
            psi = -phi + np.arctan2(-R[0, 1], -R[0, 2])
        return (psi, theta, phi), (None, None, None)

# Example rotation matrix
R_example = np.array([
    [0.5, -0.1464, 0.8536],
    [0.5, 0.8536, -0.1464],
    [-0.7071, 0.5, 0.5]
])


# Get the Euler angles
euler_angles_set_1, euler_angles_set_2 = rotation_matrix_to_euler_angles(R_example)

print("euler_angles_set_1:")
print(f"psi: {euler_angles_set_1[0]} radian.({euler_angles_set_1[0] * 180 / np.pi} deg).")
print(f"theta: {euler_angles_set_1[1]} radian.({euler_angles_set_1[1] * 180 / np.pi} deg).")
print(f"phi: {euler_angles_set_1[2]} radian.({euler_angles_set_1[2] * 180 / np.pi} deg).")

print("\neuler_angles_set_2:")
print(f"psi: {euler_angles_set_2[0]} radian.({euler_angles_set_2[0] * 180 / np.pi} deg).")
print(f"theta: {euler_angles_set_2[1]} radian.({euler_angles_set_2[1] * 180 / np.pi} deg).")
print(f"phi: {euler_angles_set_2[2]} radian.({euler_angles_set_2[2] * 180 / np.pi} deg).")

# euler_angles_set_1:
# psi: 0.7853981633974483 radian.(45.0 deg).
# theta: 0.7853885733974476 radian.(44.99945053347443 deg).
# phi: 0.7853981633974483 radian.(45.0 deg).

# euler_angles_set_2:
# psi: -2.356194490192345 radian.(-135.0 deg).
# theta: 2.3562040801923456 radian.(135.00054946652557 deg).
# phi: -2.356194490192345 radian.(-135.0 deg).


  • 먼저 첫번째 셋의 결과는 다음과 같습니다.
    • psi = 0.7853981633974483 radian.(45.0 deg).
    • theta = 0.7853885733974476 radian.(44.99945053347443 deg).
    • phi = 0.7853981633974483 radian.(45.0 deg).
  • 두번째 셋의 결과는 다음과 같습니다.
    • psi = -2.356194490192345 radian.(-135.0 deg).
    • theta = 2.3562040801923456 radian.(135.00054946652557 deg).
    • phi = -2.356194490192345 radian.(-135.0 deg).
  • 일반적으로 첫번째 셋의 결과를 사용하면 됩니다.


  • 다음으로 많이 사용하는 roll, pitch, yaw로 바꾸어서 표현하면 다음과 같습니다. 구현 방법 및 결과는 동일합니다.


import numpy as np

# Redefine the conversion function using roll, pitch, and yaw after the code state reset
def rotation_matrix_to_euler_angles(R):
    assert(R.shape == (3, 3))

    if R[2, 0] != 1 and R[2, 0] != -1:
        pitch1 = -np.arcsin(R[2, 0])
        pitch2 = np.pi - pitch1
        roll1 = np.arctan2(R[2, 1] / np.cos(pitch1), R[2, 2] / np.cos(pitch1))
        roll2 = np.arctan2(R[2, 1] / np.cos(pitch2), R[2, 2] / np.cos(pitch2))
        yaw1 = np.arctan2(R[1, 0] / np.cos(pitch1), R[0, 0] / np.cos(pitch1))
        yaw2 = np.arctan2(R[1, 0] / np.cos(pitch2), R[0, 0] / np.cos(pitch2))
        return (roll1, pitch1, yaw1), (roll2, pitch2, yaw2)
    else:
        yaw = 0  # can set to anything, it's the gimbal lock case
        if R[2, 0] == -1:
            pitch = np.pi / 2
            roll = yaw + np.arctan2(R[0, 1], R[0, 2])
        else:
            pitch = -np.pi / 2
            roll = -yaw + np.arctan2(-R[0, 1], -R[0, 2])
        return (roll, pitch, yaw), (None, None, None)

# Example rotation matrix
R_example = np.array([
    [0.5, -0.1464, 0.8536],
    [0.5, 0.8536, -0.1464],
    [-0.7071, 0.5, 0.5]
])


# Get the Euler angles
euler_angles_set_1, euler_angles_set_2 = rotation_matrix_to_euler_angles(R_example)

print("euler_angles_set_1:")
print(f"Roll: {euler_angles_set_1[0]} radian.({euler_angles_set_1[0] * 180 / np.pi} deg).")
print(f"Pitch: {euler_angles_set_1[1]} radian.({euler_angles_set_1[1] * 180 / np.pi} deg).")
print(f"Yaw: {euler_angles_set_1[2]} radian.({euler_angles_set_1[2] * 180 / np.pi} deg).")

print("\neuler_angles_set_2:")
print(f"Roll: {euler_angles_set_2[0]} radian.({euler_angles_set_2[0] * 180 / np.pi} deg).")
print(f"Pitch: {euler_angles_set_2[1]} radian.({euler_angles_set_2[1] * 180 / np.pi} deg).")
print(f"Yaw: {euler_angles_set_2[2]} radian.({euler_angles_set_2[2] * 180 / np.pi} deg).")
# euler_angles_set_1:
# Roll: 0.7853981633974483 radian.(45.0 deg).
# Pitch: 0.7853885733974476 radian.(44.99945053347443 deg).
# Yaw: 0.7853981633974483 radian.(45.0 deg).

# euler_angles_set_2:
# Roll: -2.356194490192345 radian.(-135.0 deg).
# Pitch: 2.3562040801923456 radian.(135.00054946652557 deg).
# Yaw: -2.356194490192345 radian.(-135.0 deg).


  • 위 코드의 결과를 수식을 통해 계산한 결과와 비교해 보도록 하겠습니다.


  • R=[0.50.14640.85360.50.85360.14640.70710.50.5]
  • θ1=sin(0.7071)=π4
  • θ2=πθ1=ππ4=3π4
  • ψ1=atan2(0.5cos(π/4),0.5cos(π/4))=π4
  • ψ2=atan2(0.5cos(3π/4),0.5cos(3π/4))=3π4
  • ϕ1=atan2(0.5cos(π/4),0.5cos(π/4))=π4
  • ψ2=atan2(0.5cos(3π/4),0.5cos(3π/4))=3π4


  • (ψ1(Roll),θ1(Pitch),ϕ1(Yaw))=(π4,π4,π4)
  • (ψ2(Roll),θ2(Pitch),ϕ2(Yaw))=(3π4,3π4,3π4)


  • 만약 Gimbal Lock과 같은 상황을 고려하지 않고 간단하게 Roll, Pitch, Yaw를 구하고 싶다면 다음과 같이 간단하게 사용할 수 있습니다.


def rotation_matrix_to_euler_angles(R):
    assert(R.shape == (3, 3))
    theta = -np.arcsin(R[2, 0])
    psi = np.arctan2(R[2, 1] / np.cos(theta), R[2, 2] / np.cos(theta))
    phi = np.arctan2(R[1, 0] / np.cos(theta), R[0, 0] / np.cos(theta))
    return np.array([psi, theta, phi]) # Roll, Pitch, Yaw


선형대수학 글 목차