Devin.KR

라디안과 회전행렬

150분 안팎

학습 목표

90도 회전과 역회전을 적용해 기준 벡터를 복원합니다.

개념

축이 달라지면 성분도 달라집니다

거리 센서 앞쪽의 점 (1,0)을 지도에서도 (1,0)이라고 쓰면 로봇이 회전할 때 장애물이 함께 잘못 움직입니다. 회전은 같은 길이를 유지하면서 성분이 가리키는 축을 맞추는 계산입니다. 이번에는 원점 이동 없이 2차원 회전만 작성합니다. 반시계 90도에서 전방 벡터가 (0,1)이 되는지 먼저 확인한 뒤 역회전으로 원래 벡터를 복원합니다. 이 두 검사는 다음 동차 변환의 회전 부분을 점검하는 기준이 됩니다.

각도를 입력 경계에서 구분합니다

한 바퀴는 360도이며 라디안으로는 2π입니다. 따라서 90도는 π/2라디안입니다. math.sin과 math.cos는 라디안을 받습니다. 도 단위 숫자 90을 그대로 넘기면 문법 오류 없이 전혀 다른 각도의 값을 계산합니다. 브라우저 입력은 사람이 읽기 편한 도 단위이지만 함수 rotate의 매개변수는 yaw_rad입니다. 호출부에서 math.radians로 한 번 변환하고 함수 내부는 라디안 계약으로 유지합니다.

양의 회전은 +z 방향 위에서 평면을 내려다봤을 때 반시계입니다. x축 벡터 (1,0)은 +90도에서 y축 벡터 (0,1)로 바뀝니다. -90도에서는 (0,-1)이 됩니다. 로봇의 왼쪽과 화면 좌우를 혼동하지 않도록 축을 먼저 그립니다. 화면 좌표가 아래쪽을 양의 y로 쓰는 경우 그림 표시 단계에 별도 변환이 필요하지만 여기의 수학 좌표를 화면 규칙에 맞춰 뒤집지는 않습니다.

행렬의 두 행을 계산합니다

열벡터에 적용하는 회전행렬 R은 첫 행이 (cosθ,-sinθ), 둘째 행이 (sinθ,cosθ)입니다. 결과 x는 cosθ*x-sinθ*y이며 결과 y는 sinθ*x+cosθ*y입니다. 행렬을 외우기보다 x축 단위벡터를 넣어 첫 번째 열, y축 단위벡터를 넣어 두 번째 열을 확인합니다. +90도에서 첫 열은 (0,1), 둘째 열은 (-1,0)에 가까워야 두 축이 올바르게 돌아간 것입니다.

행렬은 입력 성분을 결과 성분으로 조합하는 계수 표입니다. 첫 행을 입력 열벡터와 곱해 새 x를 구하고 둘째 행으로 새 y를 구합니다. 코드에서 새 x를 x 변수에 덮어쓴 뒤 새 y를 계산하면 원래 x가 사라집니다. nx와 ny를 먼저 계산하고 마지막에 함께 반환합니다. 튜플 반환을 사용하면 호출자가 두 성분을 받아 다음 변환에 넘길 수 있으며, 화면 출력은 계산 함수 밖에서 담당합니다.

회전행렬의 열은 서로 수직이고 길이가 1입니다. 그래서 회전은 길이를 바꾸지 않습니다. 입력 (3,4)의 길이 5가 회전 후에도 5인지 math.hypot으로 확인합니다. 성분의 합은 유지되지 않으며 각 성분도 바뀔 수 있습니다. 두 성분을 각각 cosθ로 곱하는 구현은 방향 변경이 아니라 축소처럼 동작할 수 있으므로 길이 보존 테스트가 도움이 됩니다. 단위벡터 시험과 일반 벡터 시험을 함께 사용합니다.

역회전은 어떤 연산인가

R의 역은 전치 R^T이며 같은 식에 -θ를 넣어도 됩니다. 전치는 두 행과 두 열의 위치를 바꾸는 연산입니다. 평면 회전에서는 cos(-θ)=cosθ, sin(-θ)=-sinθ이므로 사인 항의 부호가 뒤집힙니다. 원점을 옮기지 않은 회전만 되돌릴 때 이 관계를 사용합니다. 이동까지 포함한 자세의 역에서는 이동을 먼저 빼는 추가 처리가 필요하며 다음 레슨에서 구현합니다.

rotate(rotate(p,θ),-θ)가 p를 복원해야 합니다. 하지만 두 함수가 서로 짝을 이루는 같은 방향 오류를 가지고 있다면 왕복 검사는 통과할 수도 있습니다. 따라서 왕복만으로 정확성을 판단하지 않고 (1,0)을 +90도 돌린 알려진 기대점 (0,1)도 검사합니다. 테스트는 내부 구현과 독립적인 기준점을 포함해야 합니다. 방향 오류는 알려진 점에서, 누적 오차는 왕복과 길이 보존에서 읽습니다.

기준 변경과 물체 회전을 구분합니다

같은 회전행렬 식도 무엇을 표현하는지에 따라 각도의 부호가 다릅니다. 물체 벡터를 고정 축 안에서 반시계로 돌린다면 +θ를 적용합니다. 자식 프레임이 부모 기준으로 +θ만큼 돌아 있고 자식 성분을 부모 성분으로 표현할 때도 +θ를 적용합니다. 반대로 부모에서 표현된 벡터를 자식 축으로 읽으려면 -θ를 적용합니다. “로봇이 90도 돌았다”라는 말만으로 변환 방향을 결정하지 않습니다.

변수 parent_from_child와 child_from_parent는 입력과 결과 기준을 드러냅니다. 자식 전방 벡터 (1,0), 부모에서 본 자식 yaw +90도라면 parent_from_child 결과는 (0,1)입니다. 부모의 (0,1)을 child_from_parent로 읽으면 (1,0)이 됩니다. 점을 실제로 옮겼다는 의미와 표현 기준을 바꿨다는 의미를 구분해 설명하고, 같은 물리적 방향을 두 프레임에서 다른 숫자로 표현할 수 있음을 확인합니다.

작은 소수는 계산 오류가 아닐 수 있습니다

컴퓨터의 cos(π/2)는 정확한 0 대신 매우 작은 값을 돌려줄 수 있습니다. 따라서 회전한 x 성분을 == 0으로 비교하면 올바른 구현도 실패할 수 있습니다. 허용 절대 오차 1e-9로 비교하며, 화면에서는 작은 값을 0.000으로 정리합니다. 허용 오차는 테스트 계약이고 실제 센서 오차의 추정치가 아닙니다. 90도 회전의 계산 오차를 제거하려고 회전행렬 자체를 정수로 바꾸지 않습니다.

중간 값을 소수점 세 자리로 반올림하면 회전을 여러 번 합성할 때 정밀도를 잃습니다. 함수는 실수값을 유지하고 마지막 print에만 형식을 적용합니다. f 문자열로 만든 값은 str이므로 다음 rotate 입력에 사용하지 않습니다. 프로그램의 수치 처리와 사용자에게 보여주는 형식을 나누면 음의 0 표시 문제를 해결하면서 역회전 검사도 정확하게 유지할 수 있습니다.

회전 과제를 구현하고 진단합니다

과제 입력은 x y angle_deg 세 숫자입니다. 출력은 회전 결과 x,y와 역회전으로 복원한 x,y 네 값이며 모두 소수점 아래 세 자리입니다. 0도, +90도, -90도, 180도와 영벡터가 채점에 들어갑니다. 영벡터는 어떤 각도로 돌려도 영벡터이므로 정상 경계 사례입니다. 360도도 같은 방향으로 돌아오지만 작은 계산 오차가 남을 수 있으므로 표시 정리와 수치 검증을 함께 적용합니다.

+90도 입력에서 (0,-1)이 나오면 sin 항의 부호나 역회전 적용 여부를 살펴봅니다. 결과 길이가 달라지면 원래 x를 덮어썼는지 두 성분의 식을 확인합니다. NameError: name math is not defined는 회전 규칙 문제가 아니라 import 누락입니다. TypeError: must be real number는 문자열 상태로 삼각함수에 전달했는지 보라는 신호입니다. 도와 라디안을 혼동한 경우 예외가 없다는 점도 기억합니다.

완료할 때는 각도 입력의 단위, 회전 양의 방향, 열벡터 사용, 역회전 순서를 한 문장씩 설명합니다. 코드와 함께 두 기준 벡터의 회전 결과를 남기면 리뷰어가 부호를 빠르게 점검할 수 있습니다. 여기서는 yaw 하나로 표현 가능한 평면만 다루며 3차원 자세를 yaw 하나로 처리하지 않습니다. 오일러각의 한계와 각도 정규화의 다른 응용은 더 읽기의 회전 장으로 연결합니다.

따라하기

도 입력을 라디안으로

삼각함수에 넣기 전에 입력 단위를 바꿉니다.

import math
angle_rad = math.radians(90)
print(f"rad={angle_rad:.6f}")
print(f"sin={math.sin(angle_rad):.3f}")

실행 결과

rad=1.570796
sin=1.000

두 기준 축의 회전

열벡터 회전의 두 열을 각각 확인합니다.

import math
def rotate(x, y, angle):
    c, s = math.cos(angle), math.sin(angle)
    return c*x-s*y, s*x+c*y
def show(value):
    return f"{0.0 if abs(value) < 0.0005 else value:.3f}"
for point in [(1,0),(0,1)]:
    result = rotate(*point, math.pi/2)
    print(" ".join(show(v) for v in result))

실행 결과

0.000 1.000
-1.000 0.000

복원과 길이 보존

임의 각도에서 역회전 오차와 길이 차이를 검사합니다.

import math
def rotate(x, y, angle):
    c, s = math.cos(angle), math.sin(angle)
    return c*x-s*y, s*x+c*y
p = (3.0,4.0)
q = rotate(*p, math.radians(37))
r = rotate(*q, math.radians(-37))
print("restored", max(abs(a-b) for a,b in zip(p,r)) <= 1e-9)
print("length", abs(math.hypot(*p)-math.hypot(*q)) <= 1e-9)

실행 결과

restored True
length True

확인 문제

실습

유한한 숫자 x y angle_deg를 읽어 회전 x y와 역회전으로 복원한 x y를 한 줄에 출력합니다. 좌표 단위 m, 각도 입력 도, 함수 입력 rad입니다. +z 위에서 내려다본 반시계가 양입니다. 좌표 절댓값 10000 이하, 각도 절댓값 360 이하입니다. 모두 소수점 아래 세 자리이며 작은 표시값은 제공 show로 정리합니다.

모범 답안
import sys, math
def rotate(x, y, angle):
    c, s = math.cos(angle), math.sin(angle)
    return c*x-s*y, s*x+c*y
def show(value):
    return f"{0.0 if abs(value) < 0.0005 else value:.3f}"
x,y,angle_deg = map(float,sys.stdin.read().split())
a=math.radians(angle_deg)
q=rotate(x,y,a)
r=rotate(*q,-a)
print(" ".join(show(v) for v in (*q,*r)))

더 읽기

면접 질문

  • 서로 다른 좌표계의 위치를 변환하는 과정을 설명해 주시면 됩니다.