5.23. 원근 보정

경고

임의의 3x3 transform 행렬은 OpenMV Cam N6 에서만 지원됩니다 – 다른 모든 보드에서는 이 키워드가 조용히 무시됩니다. 그 외의 곳에서 실행해야 하는 애플리케이션은 정형화된 rotation_corr() 메서드(그리고 그 corners= 형태)를 사용하거나 보정된 이미지를 보드 밖에서 미리 계산해야 합니다.

정형화된 rotation_corr() 메서드는 특정 계열의 원근 워핑을 작은 매개변수 집합 뒤에 묶어 패키징하며, 지원되는 모든 보드에서 실행됩니다. 일부 애플리케이션은 그 형태에 맞지 않는 워핑을 필요로 합니다. 한 사각형에서 다른 사각형으로의 임의의 사영 재매핑, 오프라인에서 이미 산출된 알려진 마운팅에 대한 보정, 또는 어떤 상류 알고리즘이 이미 만들어 넘겨준 워핑 행렬 등이 그렇습니다. 이러한 경우를 위해 draw_image() 은 – copy(), crop(), scale() 과 함께 – 워핑을 직접 기술하는 직접 만든 3x3 행렬을 받는 transform 키워드를 받습니다.

5.23.1. 어파인 및 사영 변환

기하 워핑은 동차 좌표(homogeneous coordinates) 로 표현됩니다. 즉 픽셀 위치 (x, y)1 을 덧붙인 것에 3x3 행렬을 곱합니다.

어파인(affine) 형태가 출발점입니다. 그 맨 아랫줄은 \((0, 0, 1)\) 로 고정됩니다:

\[\begin{split}\begin{bmatrix} x' \\ y' \\ 1 \end{bmatrix} = \begin{bmatrix} a & b & c \\ d & e & f \\ 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\end{split}\]

풀어서 쓰면, 각 출력 좌표는 입력 좌표들의 선형 결합에 상수를 더한 것입니다:

\[x' = a x + b y + c, \qquad y' = d x + e y + f\]

이는 스케일링, 회전, 전단(shearing), 평행이동을 임의로 조합한 것을 모두 포괄하며, 이 모든 변환 하에서 평행선은 평행하게 유지됩니다.

사영(projective) (원근) 형태는 맨 아랫줄을 자유롭게 풉니다:

\[\begin{split}\begin{bmatrix} x'' \\ y'' \\ w' \end{bmatrix} = \begin{bmatrix} a & b & c \\ d & e & f \\ g & h & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}, \qquad (x', y') = \left( \frac{x''}{w'}, \; \frac{y''}{w'} \right)\end{split}\]

풀어서 쓰면:

\[x' = \frac{a x + b y + c}{g x + h y + 1}, \qquad y' = \frac{d x + e y + f}{g x + h y + 1}\]

\(w' = g x + h y + 1\) 로 나누는 것이 바로 이 변환을 단순한 어파인이 아니라 사영으로 만드는 요소입니다. \(g\)\(h\) 가 모두 0일 때, \(w'\) 는 1로 유지되고 나눗셈은 아무 작용도 하지 않습니다 – 다시 어파인 형태가 됩니다. 둘 중 하나라도 0이 아니면 \(w'\) 가 입력 위치에 따라 달라지고 서로 다른 위치의 픽셀이 서로 다른 양만큼 단축(foreshorten)되어, 더 이상 평행선이 평행하게 유지되지 않습니다 – 이는 정확히 평평한 평면을 비스듬한 각도에서 바라볼 때의 키스톤 효과입니다. 사영 변환은 직선을 직선으로 보내는 가장 일반적인 기하 워핑입니다. 스케일링, 뒤집기, 전치, 회전, 그리고 네 모서리 회전 보정은 모두 그 특수한 경우입니다.

이름 붙은 변환들은 어파인 형태에서 직접 도출됩니다. 항등 변환은 항등 행렬이며, 또한:

\[\begin{split}\underbrace{\begin{bmatrix} 1 & 0 & t_x \\ 0 & 1 & t_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{translate by } (t_x, \; t_y)} \qquad \underbrace{\begin{bmatrix} s_x & 0 & 0 \\ 0 & s_y & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{scale by } (s_x, \; s_y)} \qquad \underbrace{\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{rotate by } \theta}\end{split}\]

대부분의 직접 만든 변환에서 애플리케이션은 이들 중 하나를 기반으로 시작하여 추가 연산마다 행렬을 더 곱해 나가고, 결국 합성 워핑을 기술하는 단일 3x3 행렬로 끝맺습니다. 행렬은 오른쪽에서 왼쪽으로 적용됩니다. \(M = T R S\) 는 스케일을 먼저 실행한 다음 회전, 그다음 평행이동을 실행합니다. 누구나 결국에는 필요로 하는 합성은 이미지 중심을 기준으로 한 회전입니다 – 단순한 회전 행렬은 좌측 상단 모서리의 픽셀 원점을 중심으로 이미지를 돌리므로, 중심을 맞춘 버전은 중심 \((c_x, c_y)\) 을 원점으로 옮기고, 회전한 다음, 다시 되돌립니다:

\[\begin{split}M = \underbrace{\begin{bmatrix} 1 & 0 & c_x \\ 0 & 1 & c_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{move centre back}} \underbrace{\begin{bmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{bmatrix}}_{\text{rotate}} \underbrace{\begin{bmatrix} 1 & 0 & -c_x \\ 0 & 1 & -c_y \\ 0 & 0 & 1 \end{bmatrix}}_{\text{move centre to origin}}\end{split}\]

5.23.2. transform 키워드

행렬은 3x3 ulab.numpy.ndarray 로 공급되어 transform 키워드를 통해 들어갑니다. 사용할 메서드는 draw_image() 으로, 소스를 대상에 그리면서 행렬을 통해 워핑합니다 – 결과는 애플리케이션이 제어하는 버퍼에 들어가며, 워핑은 호출의 다른 모든 것, 즉 스케일링, 알파 블렌딩, 마스킹과 합성됩니다.

import ulab.numpy as np

M = np.array([[1.2,  0.0, -20.0],
              [0.0,  1.2, -15.0],
              [0.0,  0.0,   1.0]])

canvas.draw_image(img, transform=M)

이 예제는 img 를 각 방향으로 1.2배 스케일링하고 각각 왼쪽으로 20픽셀, 위로 15픽셀 이동시켜 canvas 위에 워핑합니다 – 위에서 설명한 행렬 항목들로부터 직접 만든 어파인 워핑입니다. copy(), crop(), scale() 의 동일한 키워드는 워핑을 이미지 자체에 적용합니다.