기하학적 처리

Seungpil Choi·2022년 7월 23일
1

기하학적 처리

기하학적 변환(geometric transformation)이란?

기하학적 변환은 영상을 이동핫거나 영상의 모양을 변형하는 처리이다. 기하학적 변환 연산은 크게 2가지로 나눌 수 있다. 첫째로, 어파인 변환(affine transformation)이 있다. 어파인 변환은 (행렬의 곲셈 + 덧셈 형태)로 표현되는 변환을 의미한다. 어파인 변환의 예로는 평행이동(translation), 회전(rotation), 크기변환(scaling), 반사, 밀림 변환(shearx) 등이 있다. 둘째로 굴곡(warping) 변환이라 불리는 비선형 기하학적 연산이 있다. 굴곡 변환은 영상에 비선형적인 왜곡을 가져오는 변환이다.

왜 우리는 영상을 기하학적으로 변환하는 것일까?

기하학적 변환은 관측과 렌즈의 이상에 의해 발생하는 다양한 기하학적 왜곡을 없애는 데 사용되기도 하고, 서로 다른 센서로부터 얻은 영상들을 일치시키는 데 사용되기도 하고, 모핑처럼 영화에서 특수효과를 위하여 사용되기도 한다.

기하학적 변환의 2가지 절차

기하학적 변환에는 2가지 절차가 필요하다. 첫 번째 절차는 입력 영상의 화소가 출력 영상에서 어디로 가느냐를 계산하는 절차이다. 덧셈이나 곱셈, 싸인함수와 코싸인 함수를 이용하면 어렵지 않게 계산이 가능하다. 두번째 절차는 소수점 위치에 놓인 화소의 값을 주위 화소들을 이용하여 추정하는 절차이다. 화소의 위치를 계산하다보면 정수가 아니라 소수점 위치가 나올 수 있다. 이것을 보간법(interpolation)이라고 한다.

순방향 변환(forward mapping)

다음은 기하학적 변환의 일반식이다. I(x,y) -> O(x', y')
모든 입력 화소는 이러한 변환을 통하여 처리된다. 입력 영상으로 부터 출력 영상으로의 변환 처리를 순방향 사상이라 부른다.

평행 이동

  • x' = x + Tx
  • y' = y + Ty

크기 변환

  • x' = x * Sx
  • y' = y * Sy

회전

  • x' = x cosθ - y sinθ
  • x' = x sinθ - y cosθ
img = cv2.imread('lena.png', 0)
plt.imshow(img, cmap='gray')

dst = np.zeros((img.shape[0]*2,img.shape[1]*2), dtype=np.uint8)

for y in range(img.shape[0]):
  for x in range(img.shape[1]):
    new_x = x * 2
    new_y = y * 2
    dst[new_y,new_x] = img[y, x]

plt.imshow(dst, cmap='gray')

위의 코드는 2배로 크기 변환을 수행한 것이다. 확대된 영상을 보면 중간중간 값이 없는 화소가 많이 있음을 알 수 있다. 순방향 사상을 사용하면 출력 영상 곳곳에 검은색 홀이 생기게 된다.

역방향 사상(reverse mapping)

앞서 나온 순방향 사상을 하게 되면 검은색 홀이 생성될 수 있다. 역방향 사상을 사용하면 검은색 홀의 생성을 피할 수 있다.
역방향 사상의 일반식은 다음과 같다. O(x,y) <- I(x',y')
예를 들어, 입력 영상을 2배로 확대하는 크기 변환은 아래와 같다.

  • x = x'/Sx
  • y = y'/Sy
img = cv2.imread('lena.png', 0)
plt.imshow(img, cmap='gray')
dst = np.zeros((img.shape[0]*2,img.shape[1]*2), dtype=np.uint8)

for y in range(dst.shape[0]):
  for x in range(dst.shape[1]):
    new_x = x // 2
    new_y = y // 2
    dst[y,x] = img[new_y, new_x]

plt.imshow(dst, cmap='gray')

기본적으로 OpenCV는 역방향 사상을 사용한다. 따라서 OpenCV함수들을 사용할 때는 역방향 사상에 신경쓰지 않아도 된다.

보간법(interpolation)

앞에서 역방향 사상을 사용하여 추력 화소에 대응되는 입력 화소를 찾았다. 그런데 문제는 계산된 위치가 정수가 아니라 실수라는 것이다. 가급적이면 주위의 화소들을 모두 사용하여 정확하게 값을 추정하는 것이 좋다. 변환된 좌표가 정수 좌표로 직접 대응되지 못할 때는 보간법을 사용하는 것이 좋다. 보간법은 우리가 이미 알고 있는 데이터 값들을 이용하여 우리가 모르는 값을 추정하는 수학적인 방법이다. 많은 방법들이 있지만 양선형 보간법이 가장 많이 사용된다. 물론 OpenCV 함수에서는 내부적으로 보간법을 사용한다.

최근접 보간법(Nearest Neighbor Interpolation)

최근접 보간법은 가장 간단한 보간법이다. 변환된 위치와 가장 가까운 화소값을 사용하는 방법이다. 실수 좌표에다가 0.5를 더한 후에 실수 값을 정수 값으로 만듦으로써 간단히 구현할 수 있다. 계산 과정은 간단하지만 여러가지 단점이 발생한다.

  • x_nn = (int)(x_float + 0.5)
  • y_nn = (int)(y_float + 0.5)

양선형 보간법(Bilinear Interpolation)

우리가 이미 알고 있는 4개의 인접 화소의 갑을 이용한다. 양선형 보간법은 비례식을 이용하여 중간에 놓인 화소의 값을 추정하는 방법이다. 먼저 선형 보간법(linear interpolation)을 살펴보면, 선형 보간법은 비례식을 이용하여 직선상에 놓인 점의 값을 유추하는 방법이다.

위의 그림에서 두 지점 p1, p2에서의 값이 각각 f(p1), f(p2)일때, p1,p2 사이의 임의의 지점 p에서의 데이터 값 f(p)는 다음과 같이 계산될 수 있다.

이것을 2차원 공간으로 확장한 것이 양선형 보간법이다.

OpenCV 함수들을 사용할 때 우리는 원하는 보간법을 지정할 수 있다. 따라서 우리가 손수 보간법을 작성할 필요는 없다.

OpenCV 함수를 사용한 기본 변환

일반적인 어파인 변환

평행 이동이나 회전, 크기 변환과 같은 일반적인 어파인 변환은 행렬을 사용한다. 어파인 변환은 행렬의 곱셈과 덧셈으로 이루어진다.
A = [[a00, a01], [a10, a11]]
B = [b0, b1]

이때, [x2, y2] = A X [x1, y1] + B 이다.

  • 평행 이동

    • A=[[1, 0],[0, 1]], B=[b1, b2]
  • 회전

    • A=[[cosθ, -sinθ],[sinθ, cosθ]], B=[0, 0]
  • 크기 변환

    • A=[[a00, 0],[0, a11]], B=[0, 0]

OpenCV에서는 이 2개의 행렬을 모아서 어파인 변환을 나타내는 행렬을 정의한다.
M = [A B] = [[a00, a01, b0], [a10, a11, b1]]
결론적으로 알아야 할 것은 "어파인 변환은 하나의 행렬로 표현이 가능하다" 이다.
행렬만 OpenCV에 전달하고 warpAffine() 함수를 호출하면 OpenCV에서 행렬을 이용하여 영상을 어파인 변환한다. 우리가 몇 개의 대응되는 점들을 넘기면 OpenCV가 행렬을 자동으로 계산하게 할 수도 있다.

  • warpAffine() 매개변수
    • src : 입력영상
    • M : 2X3 변환 행렬(CV_64FC1 type)
    • dsize : 출력 영상의 크기
    • flags : 보간법
      • INTER_NEAREST = 0 // 최근접 보간법
      • INTER_LINEAR = 1 // 양선형 보간법
      • INTER_CUBIC = 2 // 3차 보간법
      • INTER_AREA = 3
      • INTER_LANCZOS4 =4
      • INTER_MAX = 7
      • WARP_FILL_OUTLIERS = 8
      • WARP_INVERSE_MAP = 16

크기 변환

크기 변환은 OpenCV에서 resize() 함수를 사용한다.

  • resize() 매개변수
    • src : 입력 영상
    • dsize : 출력 영상크기. 0이면 (round(fx*src.cols), round(fy*src.rows))로 계산됨
    • fx : x축 상의 배율. 만약 0이면 (double)dsize.width / src.cols로 계산
    • fy : x축 상의 배율. 만약 0이면 (double)dsize.height / src.rows로 계산
    • interpolation : 보간법
img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

dst = cv2.resize(img, (1000, 1000))
plt.imshow(dst)

평행 이동

평행이동은 OpenCV에서 변환 행렬을 작성해야한다. 평행 이동을 나타내는 행렬은 다음과 같다.
M = [[1, 0, tx], [0, 1, ty]] => double형으로 만들어야한다. CV_64FC1 타입
변환행렬이 만들어지면 warpAffine() 함수를 사용하면된다.

img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

tx = 100
ty = 20
mat = np.asarray([[1, 0, tx], [0, 1, ty]], dtype=np.float64)

dst = cv2.warpAffine(img, mat, (img.shape[0], img.shape[1]))
plt.imshow(dst)

회전 변환

회전 변환의 경우에는 우리가 회전 중심도 알고 있고 회전의 각도도 알고 있는 경우가 대부분이다. 이때는 getRotationMatrix2D() 함수를 사용한다. 이 함수를 사용하여 행렬을 얻은 후에 warpAffine() 함수를 호출 한다.

  • getRotaionMatrix2D() 매개변수
    • center : 입력 영상에서 회전의 중심
    • angle : 회전 각도(단위 : 도)
    • scale : 배율
img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

center = (img.shape[0]//2, img.shape[1]//2)
mat = cv2.getRotationMatrix2D(center, 45, 1.0)
dst = cv2.warpAffine(img, mat, (img.shape[0], img.shape[1]))
plt.imshow(dst)

밀림 변환

밀림변환도 행렬을 이용하여 표현할 수 있다. x방향의 밀림 변환은 다음과 같은 행렬로 나타낼 수 있다.
M = [[1 sh_x, 0],[0, 1, 0]]
일반적인 밀림 변환의 행렬은 다음과 같다.
M = [[1 sh_x, 0],[sh_y, 1, 0]]

여기서 sh_x는 x방향으로 밀리는 정도이고 sh_y는 y방향으로 밀리는 정도이다.

img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

sh_x = 0.1
sh_y = 0
mat = np.asarray([[1, sh_x, 0], [sh_y, 1, 0]], dtype=np.float64)

dst = cv2.warpAffine(img, mat, (img.shape[0], img.shape[1]))
plt.imshow(dst)

3점을 이용한 어파인 변환

이미지에서 3점을 이용하여 변환 행렬을 계산할 수 있다.

img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

warp_mat = np.zeros((2,3), dtype=np.float32)

warp_mat = np.zeros((2,3), dtype=np.float32)

srcTri = np.array([[0,0], 
                   [img.shape[1]-1.0, 0], 
                   [0, img.shape[0] - 1.0]], dtype=np.float32)

dstTri = np.array([[img.shape[1]*0.0, img.shape[0]*0.33], 
                   [img.shape[1]*0.85, img.shape[0]*0.25], 
                   [img.shape[1]*0.15,  img.shape[0]*0.7]],dtype=np.float32)

warp_mat = cv2.getAffineTransform(srcTri, dstTri)

warp_dst = cv2.warpAffine(img, warp_mat, (img.shape[0], img.shape[1]))
plt.imshow(warp_dst)

원근 변환

원근 변환은 카메라나 우리의 눈이 영상을 캡처하는 방법이다. 우리 눈이나 카메라의 렌즈는 가까이 있는 물체는 크게 보고, 멀리 있는 물체는 작게 본다. 따라서 영상을 캡처하는 경우에는 약간의 원근 왜곡이 일어나게 된다. 이것을 바로 잡는 것이 원근 변환이다. 원근 변환은 어파인 변환과는 약간 다르다. 어파인 변환에서는 평행한 직선들은 평행을 유지한다. 하지만 원근 변환에서는 평행한 직선들이 평행을 유지하지 않을 수도 있다. 원근 변환은 3X3 변환 행렬이 필요하다. 입력영상과 출력영상에서 4개의 대응점이 필요하고 4개중 3개는 하나의 직선 위에 있으면 안된다. 변환행렬은 getPerspectiveTransform() 함수로 계산할 수 있다. 계산된 행렬을 warpPerspective() 함수에 전달하고 호출하면 된다.

img = cv2.imread('sudoku.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

inputs = np.array([[175,210], 
                   [105,845], 
                   [770,190], 
                   [830,830]], dtype=np.float32)

outputs = np.array([[0,0], 
                    [0, img.shape[0]], 
                    [img.shape[1], 0], 
                    [img.shape[1], img.shape[0]]], dtype=np.float32)

mat = cv2.getPerspectiveTransform(inputs, outputs)
dst = cv2.warpPerspective(img, mat, (img.shape[0], img.shape[1]))
plt.imshow(dst)

영상 워핑

일반적인 워핑을 구현하려면 다음과 같이 입력 영상의 좌표에 임의의 함수를 적용하면 된다.
x' = fx(x)
y' = fy(y)

함수의 형태에 따라서 여러 가지 변형이 가능하다. 여기서는 간단히 싸인 함수를 영상의 x좌표에서 적용하여 수평 방향으로만 왜곡을 가하는 프로그램을 작성한다.
x' = 25.0 + sin(2πx/180.0)
y' = y
여기서도 역방향 변환을 사용한다.

img = cv2.imread('lena.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.imshow(img)

dst = np.zeros(img.shape, dtype=np.uint8)

for i in range(img.shape[0]):
  for j in range(img.shape[1]):
    offset_x = (int)(25.0 * math.sin(2 * 3.14 * i / 180))
    offset_y = 0

    if(j + offset_x < img.shape[0]):
      dst[i,j] = img[i, (j+offset_x) % img.shape[1]]
    else:
      dst[i,j] = 0

plt.imshow(dst)

0개의 댓글