Skip to main content
Summary

로봇에게 행렬은 계산표가 아니라 변신 장치다

앞에서 우리는 로봇의 위치를 x, y로 표현했고, 목표를 향하는 방향을 벡터로 계산했다. 로봇이 목표를 따라 움직이려면 "어디에 있는가", "어느 방향으로 가야 하는가", "얼마나 회전해야 하는가"를 숫자로 다룰 수 있어야 했다.

이제 한 단계 더 나아가 보자. 로봇의 몸체는 점 하나가 아니다. 앞쪽을 나타내는 삼각형일 수도 있고, 바퀴가 달린 사각형일 수도 있고, 카메라나 LiDAR 같은 센서가 붙어 있을 수도 있다. 로봇이 회전하면 몸체의 모든 점이 함께 회전해야 한다. 로봇이 이동하면 센서 위치도 함께 이동해야 한다.

이런 일을 점마다 따로 계산하면 금방 복잡해진다. 그래서 행렬이 등장한다.

행렬은 단순히 숫자를 사각형으로 늘어놓은 표가 아니다. 로봇공학에서 행렬은 좌표와 벡터를 다른 모양, 다른 방향, 다른 위치로 바꾸는 변환 도구다. 좌표를 넣으면 새 좌표가 나온다. 벡터를 넣으면 방향과 크기가 바뀐 벡터가 나온다. 마치 로봇의 좌표를 통과시키는 변신 기계처럼 생각하면 된다.

이번 장의 목표는 행렬식을 외우는 것이 아니다. 행렬을 보면 "아, 이건 점들을 이렇게 움직이는 도구구나" 하고 느끼는 것이다.

행렬은 변환 도구다

행렬 곱은 x와 y를 새롭게 섞는다

2차원 점 하나를 벡터로 표현해 보자.

import numpy as np

p = np.array([2.0, 1.0])

이 점은 원점에서 오른쪽으로 2, 위로 1만큼 떨어진 위치다. 이제 이 점에 다음 행렬을 곱해 보자.

A = np.array([
    [2.0, 0.0],
    [0.0, 1.0]
])

p_new = A @ p
print(p_new)

결과는 다음과 같다.

[4. 1.]

x좌표가 2배가 되었고, y좌표는 그대로다. 이 행렬은 점을 x방향으로 늘리는 변환이다.

조금 더 일반적인 행렬을 보자.

| a  b |
| c  d |

이 행렬을 점 [x, y]에 곱하면 다음처럼 계산된다.

x' = a*x + b*y
y' = c*x + d*y

여기서 중요한 감각은 이것이다. 새 x좌표 x'는 예전 x와 예전 y를 섞어서 만들어진다. 새 y좌표 y'도 예전 x와 예전 y를 섞어서 만들어진다.

행렬 곱은 "숫자끼리 복잡하게 곱하는 일"이 아니라, 기존 좌표의 x성분과 y성분을 새로운 방식으로 섞어 새 좌표를 만드는 일이다.

예를 들어 다음 행렬을 생각해 보자.

A = np.array([
    [1.0, 1.0],
    [0.0, 1.0]
])

p = np.array([2.0, 1.0])
print(A @ p)

결과는 [3, 1]이다. 새 x좌표는 x + y가 되었고, 새 y좌표는 그대로다. 점이 위쪽에 있을수록 오른쪽으로 더 밀린다. 종이를 위에서 잡고 옆으로 비스듬히 민 것 같은 변환이다.

이처럼 행렬 하나는 평면 전체를 어떻게 바꿀지 정한다. 점 하나만 바꾸는 것이 아니라, 그 평면 위의 모든 점에 같은 규칙을 적용한다.

기저 벡터가 어디로 가는지 보면 변환이 보인다

행렬을 변환으로 이해할 때 가장 강력한 방법은 기저 벡터를 보는 것이다. 기저 벡터란 좌표계를 만드는 기본 방향 벡터다. 2차원에서는 보통 다음 두 벡터를 쓴다.

e_x = np.array([1.0, 0.0])
e_y = np.array([0.0, 1.0])

e_x는 x축 방향으로 한 칸 가는 벡터이고, e_y는 y축 방향으로 한 칸 가는 벡터다.

어떤 행렬 A가 있을 때 A @ e_x, A @ e_y를 계산하면 이 행렬이 x축과 y축의 기본 방향을 어디로 보내는지 알 수 있다.

A = np.array([
    [2.0, 1.0],
    [0.0, 1.0]
])

print("A @ e_x =", A @ e_x)
print("A @ e_y =", A @ e_y)

출력은 다음과 같다.

A @ e_x = [2. 0.]
A @ e_y = [1. 1.]

이 말은 원래 x축 방향 한 칸이 변환 후에는 [2, 0]이 되고, 원래 y축 방향 한 칸이 변환 후에는 [1, 1]이 된다는 뜻이다.

점 [2, 1]은 사실 이렇게 볼 수 있다.

[2, 1] = 2 * e_x + 1 * e_y

즉 x축 방향 두 칸, y축 방향 한 칸을 합친 것이다. 행렬을 곱한 뒤에는 이 기본 방향들이 바뀐다.

A @ [2, 1] = 2 * (A @ e_x) + 1 * (A @ e_y)

그래서 행렬 변환은 "좌표축 자체를 새롭게 잡는 일"처럼 볼 수 있다. 로봇 좌표계와 월드 좌표계를 오갈 때 이 감각은 매우 중요하다. 로봇의 앞쪽 방향과 왼쪽 방향이 월드에서 어디를 향하는지만 알면, 로봇 몸체의 모든 점을 월드 좌표로 옮길 수 있기 때문이다.

스케일 변환: 크게 만들거나 납작하게 만들기

가장 쉬운 변환은 크기를 바꾸는 변환이다. 이를 스케일 변환이라고 한다.

S = np.array([
    [2.0, 0.0],
    [0.0, 0.5]
])

p = np.array([1.0, 2.0])
print(S @ p)

결과는 [2, 1]이다. x방향으로는 2배 늘어나고, y방향으로는 절반으로 줄었다.

스케일 행렬은 대각선에 있는 숫자가 각 축 방향의 확대 비율을 나타낸다.

| sx  0 |
| 0  sy |

여기서 sx는 x방향 스케일, sy는 y방향 스케일이다. sx = 2이면 x방향으로 2배 커지고, sy = 0.5이면 y방향으로 절반이 된다.

로봇 시뮬레이션에서 스케일 변환은 실제 로봇을 직접 움직이는 데 쓰이기보다는, 로봇 모양을 그리거나 센서 범위를 시각화할 때 자주 등장한다. 예를 들어 기본 삼각형 모양을 만든 뒤 폭과 길이를 조절해 로봇 몸체처럼 보이게 만들 수 있다.

body = np.array([
    [1.0, 0.0],
    [-0.5, 0.5],
    [-0.5, -0.5],
    [1.0, 0.0]
])

S = np.array([
    [1.5, 0.0],
    [0.0, 0.8]
])

scaled_body = body @ S.T
print(scaled_body)

여기서는 body @ S.T를 사용했다. 앞에서는 S @ p처럼 열 벡터 방식으로 계산했다. 그런데 여러 점을 한 번에 다룰 때는 점들을 행으로 쌓아 body처럼 (점 개수, 2) 모양의 배열로 만드는 편이 편하다. 이 경우 각 점에 같은 행렬을 적용하려면 전치 행렬 S.T를 오른쪽에 곱한다.

즉 두 표현은 같은 일을 한다.

# 점 하나를 열 벡터처럼 생각하는 방식
p_new = S @ p

# 여러 점을 행으로 쌓아 한 번에 계산하는 방식
points_new = points @ S.T

로봇 시뮬레이션에서는 두 번째 방식이 매우 자주 쓰인다. 로봇 몸체, 센서 부채꼴, 바퀴 위치처럼 여러 점을 한 번에 움직여야 하기 때문이다.

반사 변환: 거울에 비친 좌표 만들기

이번에는 반사 변환을 보자. 반사는 좌표를 어떤 축을 기준으로 뒤집는 변환이다.

x축을 기준으로 반사하면 x좌표는 그대로이고 y좌표의 부호만 바뀐다.

Reflect_X = np.array([
    [1.0, 0.0],
    [0.0, -1.0]
])

p = np.array([2.0, 1.0])
print(Reflect_X @ p)

결과는 [2, -1]이다. 점이 x축 아래로 뒤집혔다.

y축을 기준으로 반사하면 y좌표는 그대로이고 x좌표의 부호만 바뀐다.

Reflect_Y = np.array([
    [-1.0, 0.0],
    [0.0, 1.0]
])

p = np.array([2.0, 1.0])
print(Reflect_Y @ p)

결과는 [-2, 1]이다.

반사 변환은 단순해 보이지만 좌표계를 다룰 때 아주 중요한 힌트를 준다. 어떤 그래프가 위아래로 뒤집혀 보이거나, 카메라 이미지의 좌우가 예상과 반대로 나타나거나, 로봇 좌표계의 y축 방향이 월드 좌표계와 다르게 정의되어 있을 때 이런 부호 변화가 숨어 있는 경우가 많다.

좌표계 문제에서 "왜 왼쪽으로 가야 하는데 오른쪽으로 가지?"라는 일이 생기면, 대부분 축 방향이나 회전 방향을 잘못 해석한 것이다. 반사 행렬은 그런 실수를 눈으로 확인하게 해 주는 좋은 도구다.

회전 변환: 방향을 바꾸는 행렬

로봇에서 가장 중요한 변환 중 하나는 회전이다. 로봇은 목표를 향해 방향을 바꾸고, 센서는 로봇 몸체에 붙은 상태로 함께 회전한다.

2차원에서 각도 theta만큼 회전시키는 행렬은 다음과 같다.

| cos(theta)  -sin(theta) |
| sin(theta)   cos(theta) |

Python으로 만들면 이렇게 쓴다.

def rotation_matrix(theta):
    c = np.cos(theta)
    s = np.sin(theta)
    return np.array([
        [c, -s],
        [s,  c]
    ])

90도 회전해 보자. NumPy의 삼각함수는 라디안 단위를 사용하므로 np.deg2rad(90)으로 도를 라디안으로 바꾼다.

R = rotation_matrix(np.deg2rad(90))

p = np.array([1.0, 0.0])
print(R @ p)

결과는 거의 [0, 1]이다.

[6.123234e-17 1.000000e+00]

첫 번째 값 6.123234e-17은 사실상 0이다. 컴퓨터가 실수를 계산하면서 생기는 아주 작은 오차다. 즉 x축 방향 벡터 [1, 0]이 y축 방향 [0, 1]로 돌아갔다.

회전 행렬도 기저 벡터로 보면 훨씬 직관적이다. 회전 행렬은 x축 기본 방향과 y축 기본 방향을 같은 각도만큼 돌린다. 그래서 어떤 점이든 그 점을 이루는 x성분과 y성분이 함께 돌아간다.

여기서 기억할 점은 회전은 원점을 중심으로 일어난다는 것이다. 점 [2, 0]을 90도 회전하면 [0, 2]가 된다. 점이 원점에서 같은 거리만큼 떨어진 채 방향만 바뀐다.

p = np.array([2.0, 0.0])
R = rotation_matrix(np.deg2rad(90))

print(R @ p)

로봇이 자기 자리에서 방향만 바꾸는 상황을 생각할 때도 이 감각이 필요하다. 로봇 몸체의 각 점을 로봇 중심 기준으로 만든 뒤 회전시키면, 로봇이 제자리에서 방향을 바꾸는 모양을 만들 수 있다.

여러 점을 한 번에 변환하기

로봇 몸체를 점들의 모음으로 만들어 보자. 앞쪽이 뾰족한 삼각형 로봇이다. 로봇의 중심은 원점에 있고, 앞쪽은 x축 양의 방향을 향한다고 하자.

robot_local = np.array([
    [1.0, 0.0],
    [-0.6, 0.5],
    [-0.4, 0.0],
    [-0.6, -0.5],
    [1.0, 0.0]
])

이 좌표들은 로봇 자신을 기준으로 한 좌표다. 즉 "로봇 중심에서 앞쪽으로 1만큼", "뒤쪽 왼쪽으로 조금" 같은 의미다. 이것을 로컬 좌표라고 부른다.

이 로봇을 월드 좌표계에서 어떤 위치와 방향으로 놓고 싶다면 어떻게 해야 할까? 먼저 로봇 몸체를 회전시키고, 그다음 월드 위치만큼 이동시키면 된다.

position = np.array([3.0, 2.0])
theta = np.deg2rad(30)

R = rotation_matrix(theta)

robot_world = robot_local @ R.T + position
print(robot_world)

여기서 핵심은 다음 순서다.

  1. 로봇 중심 기준의 몸체 점들을 만든다.

  2. 회전 행렬로 로봇의 방향을 바꾼다.

  3. 월드 위치 벡터를 더해 전체 몸체를 이동시킨다.

이 세 단계는 로봇 시각화의 기본 구조다. 나중에 센서, 카메라, 팔 끝 위치를 다룰 때도 같은 생각을 반복해서 사용한다.

그림으로 확인해 보자.

import numpy as np
import matplotlib.pyplot as plt


def rotation_matrix(theta):
    c = np.cos(theta)
    s = np.sin(theta)
    return np.array([
        [c, -s],
        [s,  c]
    ])


def transform_points(points, R, t):
    return points @ R.T + t


robot_local = np.array([
    [1.0, 0.0],
    [-0.6, 0.5],
    [-0.4, 0.0],
    [-0.6, -0.5],
    [1.0, 0.0]
])

position = np.array([3.0, 2.0])
theta = np.deg2rad(30)
R = rotation_matrix(theta)

robot_world = transform_points(robot_local, R, position)

plt.figure(figsize=(6, 6))

plt.plot(robot_local[:, 0], robot_local[:, 1], "o--", label="local shape")
plt.plot(robot_world[:, 0], robot_world[:, 1], "o-", label="world shape")

plt.scatter(position[0], position[1], c="red", label="robot center")

plt.axis("equal")
plt.grid(True)
plt.xlim(-2, 5)
plt.ylim(-2, 5)
plt.legend()
plt.show()

이 코드를 실행하면 원점 주변에 있는 기본 로봇 모양과, 회전하고 이동한 뒤 월드 위에 놓인 로봇 모양을 함께 볼 수 있다. 하나의 행렬과 하나의 위치 벡터만 있으면 몸체를 이루는 모든 점이 같은 규칙으로 변환된다.

이것이 행렬을 쓰는 이유다. 로봇의 점이 3개든, 30개든, 3000개든 같은 방식으로 한 번에 바꿀 수 있다.

이동은 더하기로, 회전은 행렬로

여기서 한 가지 중요한 사실을 짚고 넘어가자. 지금 우리가 사용한 2x2 행렬은 회전, 스케일, 반사처럼 원점을 기준으로 한 변환을 표현할 수 있다. 하지만 단순한 2x2 행렬 곱만으로는 모든 점을 일정하게 오른쪽으로 3, 위로 2만큼 이동시키는 변환을 표현할 수 없다.

그래서 이번 장에서는 이동을 벡터 덧셈으로 처리했다.

points_world = points_local @ R.T + position

이 한 줄에는 두 가지 변환이 들어 있다. points_local @ R.T는 로봇 몸체를 회전시키는 부분이고, + position은 회전된 몸체를 월드 위치로 옮기는 부분이다.

다음 장들에서는 이동과 회전을 하나의 큰 행렬로 묶는 방법을 배운다. 그때 등장하는 것이 동차 좌표계다. 지금은 "회전은 행렬 곱, 이동은 벡터 덧셈"이라고 이해하면 충분하다.

변환을 합성한다는 것

로봇의 실제 움직임은 변환 하나로 끝나지 않는다. 로봇 몸체를 회전시키고, 월드 좌표로 이동시키고, 그 위에 붙은 센서를 또 센서 좌표계로 변환해야 한다.

이처럼 여러 변환을 이어 붙이는 것을 변환의 합성이라고 한다. 합성은 "첫 번째 변환을 적용한 결과에 두 번째 변환을 다시 적용한다"는 뜻이다.

예를 들어 점 하나에 행렬 A를 먼저 적용하고, 그다음 행렬 B를 적용한다고 하자.

p1 = A @ p
p2 = B @ p1

이것은 한 줄로 쓰면 다음과 같다.

p2 = B @ A @ p

여기서 순서를 잘 보아야 한다. 실제로는 A가 먼저 적용되고, 그다음 B가 적용된다. 하지만 식에서는 벡터에 가까운 오른쪽 행렬부터 먼저 작동한다.

B @ A @ p

이 식은 "p에 A를 적용한 뒤, 그 결과에 B를 적용한다"는 뜻이다.

처음에는 이 순서가 헷갈린다. 하지만 로봇에서는 아주 중요하다. 센서 좌표를 로봇 좌표로 바꾸고, 로봇 좌표를 월드 좌표로 바꾸는 과정에서 순서가 바뀌면 전혀 다른 위치가 나온다.

변환 순서는 결과를 바꾼다

행렬 변환과 이동을 다룰 때 가장 흔한 실수는 순서를 바꾸는 것이다. "회전하고 이동하기"와 "이동하고 회전하기"는 같은 일이 아니다.

점 [2, 0]을 생각해 보자. 이 점을 원점 기준으로 90도 회전한 뒤 [3, 0]만큼 이동하는 경우와, 먼저 [3, 0]만큼 이동한 뒤 원점 기준으로 90도 회전하는 경우를 비교해 보자.

p = np.array([2.0, 0.0])
t = np.array([3.0, 0.0])
R = rotation_matrix(np.deg2rad(90))

rotate_then_move = R @ p + t
move_then_rotate = R @ (p + t)

print("rotate then move:", rotate_then_move)
print("move then rotate:", move_then_rotate)

출력은 대략 다음과 같다.

rotate then move: [3. 2.]
move then rotate: [0. 5.]

첫 번째 경우에는 점 [2, 0]이 먼저 [0, 2]로 회전하고, 그다음 오른쪽으로 3만큼 이동해 [3, 2]가 된다.

두 번째 경우에는 점 [2, 0]이 먼저 [5, 0]으로 이동하고, 그 점 전체가 원점을 중심으로 90도 회전해 [0, 5]가 된다.

둘 다 "회전"과 "이동"을 한 번씩 사용했지만 결과는 다르다. 이유는 회전이 원점을 기준으로 일어나기 때문이다. 먼저 멀리 이동한 뒤 회전하면, 원점에서 더 멀리 떨어진 점이 큰 원을 그리며 돌아간다.

그림으로 확인해 보자.

import numpy as np
import matplotlib.pyplot as plt


def rotation_matrix(theta):
    c = np.cos(theta)
    s = np.sin(theta)
    return np.array([
        [c, -s],
        [s,  c]
    ])


p = np.array([2.0, 0.0])
t = np.array([3.0, 0.0])
R = rotation_matrix(np.deg2rad(90))

p_rotate = R @ p
p_rotate_then_move = p_rotate + t

p_move = p + t
p_move_then_rotate = R @ p_move

plt.figure(figsize=(6, 6))

plt.scatter(p[0], p[1], c="black", label="start")
plt.scatter(p_rotate_then_move[0], p_rotate_then_move[1], c="blue", label="rotate then move")
plt.scatter(p_move_then_rotate[0], p_move_then_rotate[1], c="red", label="move then rotate")

plt.plot([p[0], p_rotate[0], p_rotate_then_move[0]],
         [p[1], p_rotate[1], p_rotate_then_move[1]],
         "o--", color="blue")

plt.plot([p[0], p_move[0], p_move_then_rotate[0]],
         [p[1], p_move[1], p_move_then_rotate[1]],
         "o--", color="red")

plt.axhline(0, color="gray", linewidth=1)
plt.axvline(0, color="gray", linewidth=1)
plt.axis("equal")
plt.grid(True)
plt.xlim(-1, 6)
plt.ylim(-1, 6)
plt.legend()
plt.show()

이 그림을 보면 두 경로가 완전히 다르다는 것을 알 수 있다. 파란 경로는 먼저 원점 주변에서 회전한 뒤 오른쪽으로 이동한다. 빨간 경로는 먼저 오른쪽으로 이동한 뒤 큰 반지름으로 회전한다.

로봇에서 이 차이는 매우 현실적인 문제로 나타난다. 로봇 팔의 관절을 먼저 돌리고 링크를 따라 이동하는 것과, 링크 끝을 먼저 이동시키고 나서 전체를 돌리는 것은 전혀 다른 자세를 만든다. 카메라가 로봇 몸체에 붙어 있을 때도 센서의 위치를 먼저 더하느냐, 로봇 방향으로 먼저 회전시키느냐에 따라 카메라가 엉뚱한 곳에 그려질 수 있다.

로봇 몸체를 회전하고 이동시키는 작은 시뮬레이션

이제 행렬을 실제 로봇 시각화에 써 보자. 로봇이 여러 위치와 방향을 지나간다고 가정하고, 각 자세에서 몸체 모양을 그려 보겠다.

import numpy as np
import matplotlib.pyplot as plt


def rotation_matrix(theta):
    c = np.cos(theta)
    s = np.sin(theta)
    return np.array([
        [c, -s],
        [s,  c]
    ])


def transform_robot(robot_local, x, y, theta):
    R = rotation_matrix(theta)
    t = np.array([x, y])
    return robot_local @ R.T + t


robot_local = np.array([
    [0.8, 0.0],
    [-0.5, 0.4],
    [-0.3, 0.0],
    [-0.5, -0.4],
    [0.8, 0.0]
])

poses = [
    (0.0, 0.0, np.deg2rad(0)),
    (1.5, 0.8, np.deg2rad(30)),
    (3.0, 1.0, np.deg2rad(70)),
    (4.0, 2.0, np.deg2rad(110)),
]

plt.figure(figsize=(7, 5))

for x, y, theta in poses:
    robot_world = transform_robot(robot_local, x, y, theta)
    plt.plot(robot_world[:, 0], robot_world[:, 1], "o-")
    plt.scatter(x, y, c="red", s=20)

path = np.array([[x, y] for x, y, theta in poses])
plt.plot(path[:, 0], path[:, 1], "k--", alpha=0.5, label="robot path")

plt.axis("equal")
plt.grid(True)
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()

이 코드는 단순하지만 매우 중요한 구조를 담고 있다. 로봇의 모양은 한 번만 robot_local로 정의한다. 그다음 자세가 바뀔 때마다 회전 행렬 R과 이동 벡터 t만 바꿔서 월드 좌표의 몸체 모양을 다시 계산한다.

이 방식이 좋은 이유는 로봇 모양과 로봇 자세를 분리할 수 있기 때문이다. 로봇의 기본 형태는 로컬 좌표계에 고정되어 있다. 월드 위에서 어디에 있고 어느 방향을 보는지는 변환이 결정한다.

이 생각은 앞으로 계속 반복된다. 센서도 마찬가지다. 카메라가 로봇 앞쪽에 0.5m 떨어져 있다면, 카메라의 로컬 위치를 정해 두고 로봇의 회전과 이동을 적용하면 월드에서 카메라가 어디에 있는지 구할 수 있다.

센서 위치도 같은 방식으로 변환한다

로봇 앞쪽에 작은 센서가 붙어 있다고 해 보자. 로봇 중심 기준으로 센서 위치가 [0.7, 0.2]라면, 로봇이 회전하고 이동할 때 센서의 월드 위치는 다음처럼 구할 수 있다.

robot_position = np.array([3.0, 2.0])
robot_theta = np.deg2rad(45)

sensor_local = np.array([0.7, 0.2])

R = rotation_matrix(robot_theta)
sensor_world = R @ sensor_local + robot_position

print(sensor_world)

로봇 몸체 점들을 변환할 때와 같은 원리다. 차이는 점이 하나뿐이라는 것뿐이다.

센서가 여러 개라면 배열로 만들면 된다.

sensors_local = np.array([
    [0.7, 0.2],
    [0.7, -0.2],
    [-0.4, 0.0]
])

sensors_world = sensors_local @ R.T + robot_position
print(sensors_world)

이제 로봇의 위치와 방향만 알면 로봇에 붙은 모든 센서의 월드 위치를 한 번에 계산할 수 있다. 이것이 행렬 변환이 로봇공학에서 강력한 이유다.

그리드가 변하는 모습을 보면 행렬이 더 쉬워진다

행렬이 평면 전체를 바꾼다는 감각을 얻으려면 격자점을 변환해 보는 것이 좋다. 여러 점이 한꺼번에 어떻게 움직이는지 보면 행렬의 성격이 드러난다.

import numpy as np
import matplotlib.pyplot as plt


A = np.array([
    [1.0, 0.8],
    [0.0, 1.0]
])

xs = np.linspace(-2, 2, 9)
ys = np.linspace(-2, 2, 9)

points = []
for x in xs:
    for y in ys:
        points.append([x, y])
points = np.array(points)

transformed = points @ A.T

plt.figure(figsize=(6, 6))
plt.scatter(points[:, 0], points[:, 1], label="original", alpha=0.6)
plt.scatter(transformed[:, 0], transformed[:, 1], label="transformed", alpha=0.6)

for p, q in zip(points, transformed):
    plt.plot([p[0], q[0]], [p[1], q[1]], color="gray", alpha=0.3)

plt.axhline(0, color="black", linewidth=1)
plt.axvline(0, color="black", linewidth=1)
plt.axis("equal")
plt.grid(True)
plt.legend()
plt.show()

이 예제의 행렬은 y값이 클수록 x방향으로 더 많이 밀어낸다. 그래서 정사각형 격자가 비스듬히 기울어진 격자로 바뀐다. 이것을 전단 변환이라고 부른다. 전단 변환은 이번 장의 핵심 변환은 아니지만, "행렬이 평면 전체를 바꾼다"는 느낌을 얻기에 좋은 예다.

격자 그림을 스케일 행렬, 반사 행렬, 회전 행렬에도 적용해 보자. 행렬만 바꾸면 같은 점들이 전혀 다른 모습으로 움직인다.

Scale = np.array([
    [2.0, 0.0],
    [0.0, 0.5]
])

Reflect_X = np.array([
    [1.0, 0.0],
    [0.0, -1.0]
])

Rotate_45 = rotation_matrix(np.deg2rad(45))

같은 점, 다른 행렬. 이것이 변환의 핵심이다.

로봇 좌표계에서 월드 좌표계로

앞에서 우리는 로봇 좌표계와 월드 좌표계를 구분했다. 로봇 좌표계에서는 로봇 앞쪽이 보통 x축 양의 방향이다. 로봇 왼쪽은 y축 양의 방향으로 둘 수 있다. 하지만 월드 좌표계에서는 로봇이 어디를 향하고 있느냐에 따라 로봇의 앞쪽 방향이 매번 달라진다.

로봇 중심 기준의 점 p_local을 월드 좌표로 바꾸는 식은 다음과 같다.

p_world = R @ p_local + robot_position

여러 점이라면 다음과 같다.

points_world = points_local @ R.T + robot_position

이 식은 앞으로 계속 등장한다. 로봇 몸체를 그릴 때도, 센서 위치를 구할 때도, 로봇 팔의 링크를 이어 붙일 때도 같은 형태가 반복된다.

여기서 R은 로봇 좌표계의 방향을 월드 좌표계에서 표현하는 회전 행렬이다. robot_position은 로봇 좌표계의 원점, 즉 로봇 중심이 월드에서 어디에 있는지를 나타낸다.

말로 풀면 다음과 같다.

  1. 로봇 기준으로 정의된 점을 로봇의 방향만큼 돌린다.

  2. 돌린 점을 로봇의 월드 위치만큼 옮긴다.

  3. 그 결과가 월드 좌표에서의 점 위치가 된다.

이제 로봇의 몸체든 센서든 "로봇에 붙어 있는 것"이라면 같은 방법으로 월드에 배치할 수 있다.

직접 확인해 볼 작은 실험

이번 장의 코드를 이해했다면 다음 실험을 해 보자. 행렬은 눈으로 볼수록 빨리 익숙해진다.

  • 스케일 행렬에서 x방향 배율을 0.5, 1.0, 2.0, -1.0으로 바꿔 보자.

  • 회전 각도를 0도, 45도, 90도, 180도로 바꾸고 로봇 몸체가 어떻게 놓이는지 보자.

  • 센서 위치 sensor_local을 [0.7, 0.2]에서 [1.0, 0.0], [0.0, 0.5], [-0.5, 0.0]으로 바꿔 보자.

  • robot_local @ R.T + position의 순서를 일부러 바꿔 보고 어떤 오류가 나거나 어떤 이상한 그림이 나오는지 확인해 보자.

특히 마지막 실험이 중요하다. 변환의 순서를 잘못 쓰면 코드가 실행되더라도 물리적으로 이상한 결과가 나올 수 있다. 로봇공학에서는 "코드가 돌아간다"와 "로봇의 의미가 맞다"가 항상 같은 말은 아니다.

행렬을 변환으로 받아들이기

이번 장에서 배운 핵심은 간단하다. 행렬은 좌표와 벡터를 바꾸는 도구다. 행렬을 곱하면 x와 y성분이 새롭게 섞이고, 그 결과 점의 위치나 벡터의 방향과 크기가 달라진다.

스케일 행렬은 크기를 바꾸고, 반사 행렬은 축을 기준으로 뒤집고, 회전 행렬은 원점을 기준으로 방향을 바꾼다. 여러 점을 배열로 묶으면 로봇 몸체 전체를 한 번에 변환할 수 있다. 그리고 회전과 이동을 함께 사용하면 로봇 로컬 좌표에 정의된 모양을 월드 좌표 위에 올려놓을 수 있다.

가장 조심해야 할 부분은 변환 순서다. 회전한 뒤 이동하는 것과 이동한 뒤 회전하는 것은 결과가 다르다. 로봇 몸체, 센서, 카메라, 로봇 팔을 다룰 때 이 순서 감각은 계속 중요해진다.

다음 장에서는 이 중에서도 가장 자주 쓰이는 2D 회전 행렬을 더 깊게 살펴본다. 각도와 라디안, 로컬 좌표와 글로벌 좌표, 그리고 로봇의 방향을 바꾸는 과정을 더 정교하게 다룰 것이다.

책 목록

로봇을 위한 수학과 Python: 좌표에서 시뮬레이터까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기