[ MCATrack ] Transformation Matrix 과 Feature Extraction

이정민·2026년 3월 24일
post-thumbnail

MCATrack의 Magno-Motion Module의 근간이 되는 핵심 두 가지를 알아보자.

변환 행렬 (Transformation Matrix)란?

카메라가 움직이면 (Ego-motion), 화면 속의 모든 배경 픽셀들이 한꺼번에 이동한다.
변환 행렬은 '이전 프레임의 픽셀들이 현재 프레임에서 어디로 이동했는가?'를 설명해주는 수학적인 '지도' 역할을 해주는 것이다.

- Affine 변환 : 회전(Rotation), 크기 조절(Scale), 이동(Translation)을 포함한다. 행렬의 크기는 2×32\times 3이다.

에? 3×33 \times 3 아닌가요?
수학적으로는 3×33\times 3이 맞다. 하지만 OpenCV의 효율성을 위한 '생략'때문에 코드에서는 2×32\times 3으로 보인다.
어떤 점 (x,y)(x,y)를 Affine 변환하여 새로운 점 (x,y)(x', y')를 만드는 수식을 전개해보면 다음과 같다.
x=a11x+a12y+txx' = a_{11}x + a_{12}y + t_x
y=a21x+a22y+tyy' = a_{21}x + a_{22}y + t_y
이것을 행렬 곱셈으로 표현한다면, OpenCV가 반환하는 행렬은 2×32\times 3 행렬이 된다.
[xy]=[a11a12txa21a22ty][xy]\left[\begin{matrix} x' \\ y'\end{matrix}\right] = \left[\begin{matrix} a_{11} & a_{12} & t_x\\ a_{21} & a_{22} & t_y \end{matrix} \right] \left[\begin{matrix} x\\y\\\end{matrix}\right]

그런데, 아까 다중 프레임 연산을 위해 Chain Rule이 등장한다고 했는데, 2×32\times 3 행렬은 자기들끼지 곱할 수 없다. 떄문에 항상 맨 아래에 [0, 0, 1]을 붙여서 3×33 \times 3 정사각형 행렬로 만든다.
[xy1]=[a11a12txa21a22ty001]=[xy1]\left[\begin{matrix} x' \\ y' \\ 1\end{matrix}\right] = \left[\begin{matrix} a_{11} & a_{12} & t_x\\ a_{21} & a_{22} & t_y \\0 & 0 & 1\end{matrix} \right] =\left[\begin{matrix} x\\y\\1\end{matrix}\right]

  • Affine의 본질 : 맨 아랫줄이 무조건 [0, 0, 1]로 고정되어 있다. 그래서 OpenCV는 메모리와 연산 속도를 아끼기 위해 굳이 쓸모없는 아랫줄을 계산하거나 반환하지 않고 2×32\times 3만 준다.
    (cv2.warpAffine())도 이 2×32\times 3만 받아서 처리하도록 최적화되어 있다.
  • Homography의 본질 : 반면 Homography는 맨 아랫줄이 [h31, h32, 1]처럼 원근감을 계산하는 변수들로 채워져있기 때문에, 생략할 수 없이 반드시 3×33\times 3이어야한다.
    (cv2.warpPerspective)함수를 따로 써야한다.

- Homography 변환 : 원근감(Perspective) 변화까지 포함한다. 행렬의 크기는 3×33 \times 3이다.
t1t-1 시점의 프레임을 tt 시점으로 맞추기 위해 구한 행렬을 Mt1tM_{t-1\rightarrow t} 라고 하자. 이 행렬을 이전 이미지에 곱해주면 (Warping), 카메라가 흔들리지 않은것 처럼 배경이 고정된다.

변환 행렬은 자유도 (Degrees of Freedom, DoF)에 따라서 여러 단계로 나뉜다.

  • Translation (이동, 2 DoF) : 상하좌우 이동만 한다. (단순 Panning)
  • Euclidean / Rigid (강체 변환, 3 DoF) : 이동 + 회전이 추가된다. (모양과 크기 유지)
  • Similarity (유사 변환, 4 DoF) : 이동 + 회전 + 크기조절 + 기울임(Shear)이 추가된다. (평행선은 유지됨)
  • Projective / Homography (호모그래피, 8 DoF) : 이 모든 것에 원근감(Perspective)가 추가된다. (평행선이 만날 수 있다.)

여러 프레임 (Mutli-Frame)의 정렬 : 행렬곱 (Chain Rule)

2개의 프레임 (t1,tt-1, t)는 그냥 매칭해서 구하면 되지만, 과거의 프레임들(t2,t3...t-2, t-3 ...)을 모두 현재 시점 (tt)로 가져오려면 어떻게 해야할까?

가장 직관적인 방법은 매번 tt 프레임과 과거 프레임을 직접 비교하는 것이다. 하지만 이 방법은 시간이 지날수록 카메라가 너무 많이 이동해서 공통된 배경이 사라지거나 원근이 바뀌어 매칭 자체가 실패해버린다.

때문에 인접한 프레임끼리만 행렬을 구한 뒤, 그것들을 곱해서(Matrix Mutiplication) 한 번에 건너뛰는 행렬을 만드는 것이다.

  • t1t-1 프레임을 tt로 보내는 행렬 : Mt1tM_{t-1 \rightarrow t}
  • t2t-2 프레임을 t1t-1로 보내는 행렬 : Mt2t1M_{t-2 \rightarrow t-1}
    ** t2t-2 프레임을 현재(tt)로 한 번에 보내는 행렬 Mt2tM_{t-2 \rightarrow t}는 어떻게 구할까?

Mt2t=Mt1t×Mt2t1M_{t-2 \rightarrow t} = M_{t-1 \rightarrow t} \times M_{t-2 \rightarrow t-1}

이런 식으로 과거의 어떤 프레임이든 인접 프레임 간의 행렬들을 차례로 곱해준다면, 현재 시점으로 완벽하게 겹치도록 변환할 수 있다. 이것이 다중 프레임 정렬의 핵심이다.

3D 공간 상의 변환 (4×44\times 4 행렬)

현실 세계 (3D)의 어떤 점 (X,Y,Z)(X, Y, Z)를 이동시키고 회전할 때에는, 동차 좌표계를 사용하면 벡터는 [X,Y,Z,1]T[X, Y, Z, 1]^T이 되고, 변환 행렬은 4×44\times 4가 된다.

[XYZ1]=[R11R12R13TxR21R22R23TyR31R32R33Tz0001][XYZ1]\left[\begin{matrix} X' \\ Y' \\ Z' \\ 1\end{matrix}\right] = \left[\begin{matrix} R_{11} & R_{12} & R_{13}&T_x\\ R_{21} & R_{22} & R_{23}&T_y \\R_{31} & R_{32} & R_{33} &T_z \\ 0 & 0 & 0 & 1\end{matrix} \right] \left[\begin{matrix} X\\Y\\Z\\1\end{matrix}\right]
이것이 바로 3D 비전에서 다루는 절대적인 진리인데..

현재 내가 겪고 있는 Anti-UAV 프레임 정렬 문제는 3D 공간이 아니다. 카메라는 3D 세상을 촬영하여 깊이(ZZ축)을 짓눌러버리고 2D 평면 이미지 (픽셀 좌표계)로 만든다.

MCATrack에서 인접 프레임 간 정렬 (Inter-frame Registration)을 수행할 때에는 3D 깊이를 알 수 없으니, 오직 2D 이미지 평면의 픽셀 (x,y)(x, y)들이 어떻게 이동했는지만을 계산한다.

2D 공간의 점(x,y)(x, y)를 동차 좌표계로 나타내면 depth-축 이 날아가고 [x,y,1]T[x, y, 1]^T라는 3×13 \times 1 벡터가 된다.

이를 변환하려면 행렬도 당연히 3×33\times 3으로 줄어든다.

Affine 변환에서 하단이 왜 [0, 0, 1]인가?

호모그래피의 3×33\times 3 행렬에서 맨 아랫줄 [h31,h32,h33][h_{31}, h_{32}, h_{33}]은 2D 평면이 3D 공간상에서 기울어질 때 발생하는 원근 왜곡 (Perspective Distortion)을 계산하는 파라미터이다.


하지만 하늘 배경의 드론 추적에서는 원근 변화를 무시하고 평행선을 유지하는 Affine 변환을 사용하는데, 수학적으로 원근 왜곡이 없다는 것은 h31h_{31}h32h_{32}가 정확히 0이라는 것을 의미한다. 또한 스케일 정규화를 위해 h33h_{33}은 1이 된다.

따라서 2D Affine 변환의 동차 행렬은 반드시 아래와 같은 형태가 된다.
[xy1]=[a11a12txa21a22ty001]=[xy1]\left[\begin{matrix} x' \\ y' \\ 1\end{matrix}\right] = \left[\begin{matrix} a_{11} & a_{12} & t_x\\ a_{21} & a_{22} & t_y \\0 & 0 & 1\end{matrix} \right] =\left[\begin{matrix} x\\y\\1\end{matrix}\right]
이제 이 행렬 곱셈을 픽셀 좌표를 구하는 대수 방정식으로 풀어보자.
1. x=a11x+a12y+txx' = a_{11}x + a_{12}y + t_x
2. y=a21x+a22y+tyy' = a_{21}x + a_{22}y + t_y
3. 1=0x+0y+11 = 0 \cdot x + 0\cdot y + 1

세 번째 수식을 보면 1=11=1이다.
이 수식은 새로운 픽셀 좌표 (x,y)(x', y')을 계산하는데에 아무런 쓸모가 없는 건데..
한 프레임에 수백만 개의 픽셀이 있는 상황에서 이 무의미한 연산을 수백만 번 반복하는 것은 자원 낭비이다.

그래서 OpenCV의 연구원들은 메모리와 연산량을 아끼기 위해 Affine 변환에서 쓸모가 없는 맨 아랫줄을 아예 메모리에서 잘라내버렸다.

Homogeneous Coordinate (동차 좌표계 )

이론은 행렬을 곱하면 된다고 하지만, 막상 파이썬 (OpenCV)에서 Affine 변환 행렬을 2개 np.dot()으로 곱하려고 하면 에러가 발생한다.

이유는 OpenCV가 반환하는 Affine 행렬의 크기가 2×32\times 3 이기 때문인데, 수학적으로 2×32\times 3 끼리의 행렬곱은 불가능하다.

그래서 곱셈을 하기 전에 이 행렬을 3×33\times 3 정사각형 행렬 (Homogeneous Matrix)로 만들어줘야 한다.

  1. 2×32\times 3 행렬의 맨 아래에 [0, 0, 1]이라는 한 줄을 추가해서 3×33\times 3으로 만든다.
  2. 이제 3×33\times 3 행렬끼리 곱한다. (np.matmul)
  3. 계산이 끝나면 맨 아랫줄을 다시 잘라내어 2×32 \times 3으로 만든 뒤, cv2.warpAffine()에 집어넣는다.

Boundary Artifacts

카메라가 오른쪽으로 Panning 했다고 가정해보자.
이전 프레임을 현재 프레임에 맞추기 위해 왼쪽으로 당겨오면, 원래 화면에 없던 오른쪽 가장자리 부분에는 빈 공간이 생긴다.

  • OpenCV나 PyTorch에서는 이 빈 공간을 기본적으로 검은색(0)으로 채워버린다.
  • 이 상태로 cv2.absdiff(warped_prev, curr)를 실행하면 현재 프레임의 오른쪽 가장자리 배경(예를 들어 하늘의 색 150) 과 검은색(0)이 빼기가 되면서, 가장자리에 거대한 하얀색 노이즈가 생겨버린다. Magno-Motion 모듈은 이 거대한 노이즈를 엄청나게 빠르게 움직이는 거대한 타겟으로 생각하고 시야를 완전히 빼앗긴다.
  • 변환 시 빈 공간을 검은색으로 두지 말고, 가장자리 픽셀을 복사하여 늘리거나 (Replicate), 반사(Reflect)시켜줘야 한다.
    이렇게 해야 차분(absdiff)를 했을 때 경계면이 0으로 깔끔하게 상쇄된다.
    - OpenCV : borderMode=cv2.BORDER_REPLICATE

Feature Extraction

특징점이란?

카메라가 이동했을 때 두 프레임 간의 변환 행렬을 구하려면 "이전 프레임의 A 픽셀이 현재 프레임의 B 픽셀로 이동했다" 라는 확신이 필요하다.
이 때 아무런 픽셀이나 잡아버리면 Aperture Problem이라는 오류에 빠지게 된다.

  • 밋밋한 영역 (Flat) : 주변 픽셀이 다 똑같은 색이다. 카메라가 오른쪽으로 10픽셀을 이동했는지, 20픽셀을 이동했는지 구분할 수 없다.
  • 직선 영역 (Edge) : 가로로 뻗은 전선 위에서 한 점을 잡았다고 해보자. 카메라가 가로로만 움직인다면 전선은 똑같은 선으로 보이기 떄문에 이동량을 알 수 없다.
  • 모서리 영역 (Corner) : 가로선과 세로선이 교차하는 꼭짓점이다. 어느 방향으로 움직이든 주변 픽셀과 명암 대비가 확 달라지기 때문에 이동 방향과 거리를 100% 특정할 수 있다.

    결국 좋은 특징점은 코너 (Corner)를 의미한다.

구현에서 특징점 검출을 위해 ORB (Oriented FAST and Rotated BRIEF) 를 사용했다. 이는 두 가지 알고리즘의 결합인데, 빠른 속도와 회전 불변성 때문에 실무에서 많이 쓰인다.

import cv2

orb = cv2.ORB_create(
    nfeatures=40000, # 최대 추출 특징점 수 
    scaleFactor=1.2, # 이미지 피라미드에서 레벨 간 축소 비율
    nlevels=8, # 피라미드 레벨 수
    edgeThreshold=31, # 이미지 경계에서 Keypoint 검출하지 않는 Margin
    firstLevel=0, # 피라미드 시작 레벨
    WTA_K = 2, # BRIEF descriptor에서 사용하는 비교 픽셀 수 
    scoreType=cv2.ORB_HARRIS_SCORE, # Key Point Ranking 방식 
    patchSize=31, # descriptor 계산에 사용하는 Patch 크기
    fastThreshold=20 # FAST corner detection threshold
)

STEP 1 : 코너 찾기 (FAST 알고리즘)
어떤 픽셀 PP가 코너인지 확인하기 위해 PP를 중심으로 원을 그리듯 주변 16개의 픽셀 밝기를 검사한다.
만약 PP보다 '확실히 더 밝거나 확실히 더 어두운' 픽셀들이 연속으로 9개 이상 존재한다면, 그 점을 코너로 합격시킨다.


STEP 2: BRIEF descriptor
찾은 특징점을 다음 프레임에서 찾을 수 있도록 Descriptor를 만들어야 한다.

  • 특징점 주변의 픽셀들을 무작위로 두 개씩(A, B) 짝지어 밝기를 비교한다.
  • A가 B보다 밝으면 1, 어두우면 0 이런 식으로 256번을 비교하여 256비트 이진수 문자열을 생성한다.
  • 이진수로 되어 있어, 두 특징점이 같은지 비교할 때 컴퓨터의 XOR 연산(해밍거리)로 일치여부를 판별한다.

    주의할 점
    - 적외선 카메라는 열을 감지하기 때문에 텍스쳐가 가시광선 카메라처럼 뚜렷하지 않고 뭉개져 보인다.
    - 하늘에는 Corner가 없어서 ORB의 FAST 알고리즘이 측징점을 찾지 못하는 경우가 有
    - 코너가 없으니, 자글자글한 열 노이즈나 압축 Artifact를 코너로 착각하고 특징점으로 잡아버린다.

0개의 댓글