본문 바로가기

Deep Learning(강의 및 책)/OpenCV

[OpenCV] 기하학적 변환

728x90

이 글은 '파이썬으로 만드는 OpenCV 프로젝트'를 보고 작성했습니다.

 

5.1 이동, 확대/축소, 회전

영상의 기하학적 변환은 기존의 영상을 원하는 모양이나 방향 등으로 변환하기 위해 각 픽셀을 새로운 위치로 옮기는 것이 작업의 대부분입니다. 픽셀의 x, y 좌표에 대해 옮기고자 하는 새로운 좌표 a, b를 구하는 연산이 필요합니다. 이러한 연산을 가장 효과적으로 표현하는 방법이 행렬식입니다.

 

■ 이동

2차원 공간에서 물체를 다른 곳으로 이동시키려면 원래 있던 좌표에 이동시키려는 거리만큼 더해서 이동할 새로운 좌표를 구하면 됩니다. 예를 들어 점 p(x, y)를 dx와 dy 만큼 옮기면 새로운 위치의 좌표 p(a, b)를 구할 수 있습니다. a = x + dx, b = y + dy로 표현됩니다. 이 식은 행렬식으로 표현 가능하며 2 x 3 크기의 변환 행렬로 영상의 좌표를 변환시켜 주는 함수를 OpenCV는 제공합니다.

(위 식에서 변환행렬은 [[1 0 dx], [0, 1, dy]]입니다.)

  • dst = cv2.warpAffine(src, mtrx, dsize [, dst, flags, borderMode, borderValue])
    • src: 원본 영상, Numpy 배열
    • mtrx: 2 x 3 변환 행렬, Numpy 배열, dtype = float32
    • dsize: 결과 이미지 크기, tuple(width, height)
    • flags: 보간법 알고리즘 선택 플래그
      • cv2.INTER_LINEAR: 기본 값, 인접한 4개 픽셀 값에 거리 가중치 사용
      • cv2.INTER_NEAREST: 가장 가까운 픽셀 값 사용
      • cv2.INTER_AREA: 픽셀 영역 관계를 이용한 재샘플링
      • cv2.INTER_CUBIC: 인접한 16개 픽셀 값에 거리 가중치 사용
      • cv2.INTER_LANCZOS4: 인접한 8개 픽셀을 이용한 란초의 알고리즘
    • borderMode: 외곽 영역 보정 플래그
      • cv2.BORDER_CONSTANT: 고정 색상 값(999 | 12345 | 999)
      • cv2.BORDER_REPLICATE: 가장자리 복제(111 | 12345 | 555)
      • cv2.BORDER_WRAP: 반복(345 | 12345 | 123)
      • cv2.BORDER_REFLECT: 반사(321 | 13245 | 543)
    • borderValue: cv2.BORDER_CONSTANT의 경우 사용할 색상 값(기본 값 = 0)
    • dst: 결과 이미지, Numpy 배열

cv2.warpAffine()은 src 영상을 mtrx 행렬에 따라 변환해서 dsize 크기로 만들어 반환합니다. 

 

import cv2
import numpy as np

img = cv2.imread('../img/fish.jpg')
rows,cols = img.shape[0:2]  # 영상의 크기

dx, dy = 100, 50            # 이동할 픽셀 거리

# ---① 변환 행렬 생성
mtrx = np.float32([[1, 0, dx],
                   [0, 1, dy]])
# ---② 단순 이동
dst = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy))

# ---③ 탈락된 외곽 픽셀을 파랑색으로 보정
dst2 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
                        cv2.INTER_LINEAR, cv2.BORDER_CONSTANT, (255,0,0) )

# ---④ 탈락된 외곽 픽셀을 원본을 반사 시켜서 보정
dst3 = cv2.warpAffine(img, mtrx, (cols+dx, rows+dy), None, \
                                cv2.INTER_LINEAR, cv2.BORDER_REFLECT)

cv2.imshow('original', img)
cv2.imshow('trans',dst)
cv2.imshow('BORDER_CONSTATNT', dst2)
cv2.imshow('BORDER_FEFLECT', dst3)
cv2.waitKey(0)
cv2.destroyAllWindows()

위 코드는 cv2.warpAffine() 함수와 변환 행렬을 이용해 영상을 이동하는 내용입니다.

 

영상 이동에는 외곽 영역 외에는 픽셀의 탈락이 발생하지 않으므로 이 예제에서 보간법 알고리즘을 선택하는 네 번째 인자는 의미가 없습니다.

 

■ 확대/축소

영상을 가로와 세로 방향으로 각각 α, β 비율로 확대/축소를 한다면 변환 행렬은 [[α 0 0], [β 0 0]]이 됩니다. 사실 2 x 2 행렬로도 충분히 확대/축소를 표현 가능하지만 cv2.warpAffine() 함수는 2 x 3 행렬을 이용해 변환하기 때문에 위와 같이 표현했습니다. 축소에는 cv2.INTER_AREA 보간법이 효과적이고 확대에는 cv2.INTER_CUBIC과 cv2.INTER_LINEAR이 효과적입니다.

OpenCV()에서는 변환 행렬을 작성하지 않고도 확대와 축소 기능을 사용할 수 있게 cv2.resize() 함수를 별도로 제공합니다.

  • dst = cv2.resize(src, dsize, dst, fx, fy, interpolation)
    • src: 입력 영상, Numpy 배열
    • dsize: 출력 영상 크기(확대/축소 목표 크기), 생략하면 fx, fy를 적용
      • (width, height)
    • fx, fy: 크기 배율, 생략하면 dsize 적용
    • interpolation: 보간법 알고리즘 선택 플래그(cv2.warpAffine()과 동일)
    • dst: 결과 영상, Numpy 배열

확대 혹은 축소를 몇 픽셀로 할지 아니면 몇 퍼센트로 할지 선택 가능합니다. 만약 dsize와 fx, fy 모두 전달하면 dsize만 적용합니다.

 

import cv2
import numpy as np

img = cv2.imread('../img/fish.jpg')
height, width = img.shape[:2]

#--① 크기 지정으로 축소
#dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)),\
#                        None, 0, 0, cv2.INTER_AREA)
dst1 = cv2.resize(img, (int(width*0.5), int(height*0.5)), \
                         interpolation=cv2.INTER_AREA)

#--② 배율 지정으로 확대
dst2 = cv2.resize(img, None,  None, 2, 2, cv2.INTER_CUBIC)
#--③ 결과 출력
cv2.imshow("original", img)
cv2.imshow("small", dst1)
cv2.imshow("big", dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()

이와 같이 이미지를 확대/축소할 수 있습니다.

 

■ 회전 

영상을 회전하기 위해선 삼각함수를 사용해야 합니다. 회전을 할 때 사용하는 변환 행렬은 [[cosθ -sinθ 0], [sinθ cosθ 0]]입니다.

 

import cv2
import numpy as np

img = cv2.imread('../img/fish.jpg')
rows,cols = img.shape[0:2]

# ---① 라디안 각도 계산(60진법을 호도법으로 변경)
d45 = 45.0 * np.pi / 180    # 45도
d90 = 90.0 * np.pi / 180    # 90도

# ---② 회전을 위한 변환 행렬 생성
m45 = np.float32( [[ np.cos(d45), -1* np.sin(d45), rows//2],
                    [np.sin(d45), np.cos(d45), -1*cols//4]])
m90 = np.float32( [[ np.cos(d90), -1* np.sin(d90), rows],
                    [np.sin(d90), np.cos(d90), 0]])

# ---③ 회전 변환 행렬 적용
r45 = cv2.warpAffine(img,m45,(cols,rows))
r90 = cv2.warpAffine(img,m90,(rows,cols))

# ---④ 결과 출력
cv2.imshow("origin", img)
cv2.imshow("45", r45)
cv2.imshow("90", r90)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

변환 행렬에 사용할 회전 각을 60진법에서 라디안(radian)으로 변경해 사용합니다. 삼각함수는 np.sin(), np.cos()을 사용했으며 변환행렬 마지막 열에 0이 아닌 rows // 2, -1*cols//4를 사용한 이유는 영상의 회전 기준 축이 좌측 상단이 되므로 회전한 영상은 보이는 영역 바깥으로 벗어나게 돼서 좌표를 가운데로 옮기기 위한 것으로 회전축을 지정하는 효과와 같습니다.

  • mtrx = cv2.getRotationMatrix2D(center, angle, scale)
    • center: 회전 축 중심 좌표, 튜플(x, y)
    • angle: 회전 각도, 60진법
    • scale: 확대/축소비율

위 함수를 쓰면 중심축 지정과 확대/축소까지 반영해서 손쉽게 변환 행렬을 얻을 수 있습니다.

 

import cv2

img = cv2.imread('../img/fish.jpg')
rows,cols = img.shape[0:2]

#---① 회전을 위한 변환 행렬 구하기
# 회전축:중앙, 각도:45, 배율:0.5
m45 = cv2.getRotationMatrix2D((cols/2,rows/2),45,0.5) 
# 회전축:중앙, 각도:90, 배율:1.5
m90 = cv2.getRotationMatrix2D((cols/2,rows/2),90,1.5) 

#---② 변환 행렬 적용
img45 = cv2.warpAffine(img, m45,(cols, rows))
img90 = cv2.warpAffine(img, m90,(cols, rows))

#---③ 결과 출력
cv2.imshow('origin',img)
cv2.imshow("45", img45)
cv2.imshow("90", img90)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

위와 같이 코드를 작성해 쉽게 이미지를 회전하고 확대/축소할 수 있습니다.

 

5.2 뒤틀기(warping)

■ affine 변환

어핀 변환(affine transform)은 이동, 확대/축소, 회전을 포함하는 변환으로 직선, 길이의 비율, 평행성을 보존하는 변환을 말합니다. 변환 전과 후의 3개의 점을 짝지어 매핑할 수 있다면 변환 행렬을 거꾸로 계산할 수 있는데, OpenCV는 cv2.getAffineTransform() 함수로 이 기능을 제공합니다.

  • matrix = cv2.getAffineTransform(pts1, pts2)
    • pts1: 변환 전 영상의 좌표 3개, 3 x 2 Numpy 배열(float32)
    • pts2: 변환 후 영상의 좌표 3개, pts1과 동일
    • matrix: 변환행렬 반환, 2 x 3 행렬
import cv2
import numpy as np
from matplotlib import pyplot as plt

file_name = '../img/fish.jpg'
img = cv2.imread(file_name)
rows, cols = img.shape[:2]

# ---① 변환 전, 후 각 3개의 좌표 생성
pts1 = np.float32([[100, 50], [200, 50], [100, 200]])
pts2 = np.float32([[80, 70], [210, 60], [250, 120]])

# ---② 변환 전 좌표를 이미지에 표시
cv2.circle(img, (100,50), 5, (255,0), -1)
cv2.circle(img, (200,50), 5, (0,255,0), -1)
cv2.circle(img, (100,200), 5, (0,0,255), -1)

#---③ 짝지은 3개의 좌표로 변환 행렬 계산
mtrx = cv2.getAffineTransform(pts1, pts2)
#---④ 어핀 변환 적용
dst = cv2.warpAffine(img, mtrx, (int(cols*1.5), rows))

#---⑤ 결과 출력
cv2.imshow('origin',img)
cv2.imshow('affin', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

정사각형의 영상이 마름모꼴의 모양으로 변형된 것을 볼 수 있습니다. 원래의 좌표가 어떻게 변화하는지 볼 수 있게 세 가지 색으로 동그라미를 그려 표시했습니다.

 

■ 원근 변환

원근 변환(perspective transform)은 보는 사람의 시각에 따라 같은 물체도 먼 것은 작게, 가까운 것은 크게 보이는 현상인 원근감을 주는 변환을 말합니다. 원근감을 느끼는 이유는 실제 세계가 3차원 좌표계이기 때문인데, 영상은 2차원 좌표계입니다. 그래서 차원 간의 차이를 보정해 줄 추가 연산과 시스템이 필요한데, 이때 사용하는 좌표계를 동차 좌표(homogeneous coordinates)라고 합니다. 다른 말로 호모그래피(homography)라고도 합니다. 동차 좌표를 간단히 요약하면, 2차원 좌표(x, y)에 대응하는 동차 좌표는 기존 차수에 1개의 상수항을 추가해서 (wx, wy, w)로 표현하고, 이것을 2차원 좌표로 바꿀 때는 다시 상수항 w로 나누어 (x/w, y/w)로 표현합니다. 원근 변환을 하려면 (x, y, 1) 꼴의 좌표계가 필요하고, 3 x 3 변환 행렬식이 필요합니다.

OpenCV는 변환 전과 후를 짝짓는 4개의 매핑 좌표만 지정해 주면 원근 변환에 필요한 3 x 3 변환 행렬을 계산해 주는 cv2.getPerspectiveTransform() 함수를 제공합니다.

  • mtrx = cv2.getPerspectiveTransform(pts1, pts2)
    • pts1: 변환 이전 영상의 좌표 4개, 4 x 2 Numpy 배열(float 32)
    • pts2: 변환 이전 영상의 좌표 4개, pts1과 동일
    • mtrx: 변환행렬 반환, 3 x 3 행렬
  • dst = cv2.warpPerspective(src, mtrx, dsize [, dst, flags, borderMode, borderValue]): 모든 파라미터와 반환 값은 cv2.warpAffine()과 동일

원근 변환을 수행하는 함수는 cv2.warpAffine()이 아닌 cv2.warpPerspective()입니다.

 

import cv2
import numpy as np

file_name = "../img/fish.jpg"
img = cv2.imread(file_name)
rows, cols = img.shape[:2]

#---① 원근 변환 전 후 4개 좌표
pts1 = np.float32([[0,0], [0,rows], [cols, 0], [cols,rows]])
pts2 = np.float32([[100,50], [10,rows-50], [cols-100, 50], [cols-10,rows-50]])
pts3 = np.float32([[10,50], [100, rows-50], [cols - 10, 50], [cols-100,rows-50]])

#---② 변환 전 좌표를 원본 이미지에 표시
cv2.circle(img, (0,0), 10, (255,0,0), -1)
cv2.circle(img, (0,rows), 10, (0,255,0), -1)
cv2.circle(img, (cols,0), 10, (0,0,255), -1)
cv2.circle(img, (cols,rows), 10, (0,255,255), -1)

#---③ 원근 변환 행렬 계산
mtrx = cv2.getPerspectiveTransform(pts1, pts2)
mtrx2 = cv2.getPerspectiveTransform(pts1, pts3)
#---④ 원근 변환 적용
dst = cv2.warpPerspective(img, mtrx, (cols, rows))
dst2 = cv2.warpPerspective(img, mtrx2, (cols, rows))

cv2.imshow("origin", img)
cv2.imshow('perspective', dst)
cv2.imshow("perspective2", dst2)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

원근 변환 전후에 매핑할 좌표를 지정하고 변환 행렬로 계산해 이미지에 적용하는 모습입니다.

 

import cv2
import numpy as np

win_name = "scanning"
img = cv2.imread("../img/paper.jpg")
rows, cols = img.shape[:2]
draw = img.copy()
pts_cnt = 0
pts = np.zeros((4,2), dtype=np.float32)

def onMouse(event, x, y, flags, param):  #마우스 이벤트 콜백 함수 구현 ---① 
    global  pts_cnt                     # 마우스로 찍은 좌표의 갯수 저장
    if event == cv2.EVENT_LBUTTONDOWN:  
        cv2.circle(draw, (x,y), 10, (0,255,0), -1) # 좌표에 초록색 동그라미 표시
        cv2.imshow(win_name, draw)

        pts[pts_cnt] = [x,y]            # 마우스 좌표 저장
        pts_cnt+=1
        if pts_cnt == 4:                       # 좌표가 4개 수집됨 
            # 좌표 4개 중 상하좌우 찾기 ---② 
            sm = pts.sum(axis=1)                 # 4쌍의 좌표 각각 x+y 계산
            diff = np.diff(pts, axis = 1)       # 4쌍의 좌표 각각 x-y 계산

            topLeft = pts[np.argmin(sm)]         # x+y가 가장 값이 좌상단 좌표
            bottomRight = pts[np.argmax(sm)]     # x+y가 가장 큰 값이 좌상단 좌표
            topRight = pts[np.argmin(diff)]     # x-y가 가장 작은 것이 우상단 좌표
            bottomLeft = pts[np.argmax(diff)]   # x-y가 가장 큰 값이 좌하단 좌표

            # 변환 전 4개 좌표 
            pts1 = np.float32([topLeft, topRight, bottomRight , bottomLeft])

            # 변환 후 영상에 사용할 서류의 폭과 높이 계산 ---③ 
            w1 = abs(bottomRight[0] - bottomLeft[0])    # 상단 좌우 좌표간의 거리
            w2 = abs(topRight[0] - topLeft[0])          # 하당 좌우 좌표간의 거리
            h1 = abs(topRight[1] - bottomRight[1])      # 우측 상하 좌표간의 거리
            h2 = abs(topLeft[1] - bottomLeft[1])        # 좌측 상하 좌표간의 거리
            width = max([w1, w2])                       # 두 좌우 거리간의 최대값이 서류의 폭
            height = max([h1, h2])                      # 두 상하 거리간의 최대값이 서류의 높이
            width = int(width)
            height = int(height)
            # 변환 후 4개 좌표
            pts2 = np.float32([[0,0], [width-1,0], 
                                [width-1,height-1], [0,height-1]])

            # 변환 행렬 계산 
            mtrx = cv2.getPerspectiveTransform(pts1, pts2)
            # 원근 변환 적용
            result = cv2.warpPerspective(img, mtrx, (width, height))
            cv2.imshow('scanned', result)
cv2.imshow(win_name, img)
cv2.setMouseCallback(win_name, onMouse)    # 마우스 콜백 함수를 GUI 윈도우에 등록 ---④
cv2.waitKey(0)
cv2.destroyAllWindows()

위 코드는 마우스 클릭을 통해 4개의 좌표를 구해 원근 변환 이전 좌표를 완성하고 원근감을 제거하는 코드입니다. 카메라로 명함이나 문서 같은 것을 찍은 사진을 스캔한 것처럼 만들고 싶을 때 원근감을 제거합니다.

 

■ 삼각형 어핀 변환

어떤 영역을 여러 개의 삼각형으로 나누는 기법을 들로네 삼각분할(Delaunay triangulation)이라고 하는데, 이렇게 나눈 삼각형들은 흔히 영상 분야에서는 입체적 표현이나 모핑(mopping) 기술에 사용합니다. 모핑 기술은 하나의 물체가 다른 물체로 자연스럽게 변하게 하는 것인데, 두 영상을 각각 여러 개의 삼각형으로 나누어 한 영상의 삼각형들의 크기와 모양이 나머지 영상에 대응하는 삼각형과 같아질 때까지 조금씩 바꿔서 전체적으로 하나의 영상이 다른 영상으로 자연스럽게 변하게 하는 것입니다.

OpenCV가 제공하는 기하학적 변환 기능은 영상을 대상으로 하므로 늘 사각형이므로 삼각형 모양의 변환을 하려면 몇 가지 과정을 거쳐야 합니다.

  1. 변환 전 삼각형 좌표 3쌍을 정한다.
  2. 변환 후 삼각형 좌표 3쌍을 정한다.
  3. 과정 1의 삼각형 좌표를 완전히 감싸는 외접 사각형 좌표를 구한다.
  4. 과정 3의 사각형 영역을 관심 영역으로 지정한다.
  5. 과정 4의 관심 영역을 대상으로 과정 1과 과정 2의 좌표로 변환 행렬을 구하여 어핀 변환한다.
  6. 과정 5의 변환된 관심 영역에서 과정 2의 삼각형 좌표만 마스킹한다.
  7. 과정 6의 마스크를 이용해서 원본 또는 다른 영상에 합성한다.

위 과정에서 삼각형 좌표를 완전히 감싸는 사각형의 좌표를 구하려면 cv2.boundingRect() 함수를 사용하면 됩니다. 이 함수는 삼각형뿐 아니라 다각형의 좌표를 전달하면 정확히 감싸는 외접 사각형의 좌표를 반환합니다.

삼각형 마스크를 만들려면 cv2.fillConvexPoly() 함수를 쓰면 편리합니다. 이 함수에 좌표를 전달하면 그 좌표 안을 원하는 색상 값으로 채워주는데, 255나 0을 채우면 마스크를 쉽게 만들 수 있습니다.

  • cv2.fillConvexPoly(img, points, color [, lineType])
    • img: 입력 영상
    • points: 다각형 꼭짓점 좌표
    • color: 채우기에 사용할 색상
    • lineType: 선 그리기 알고리즘 선택 플래그
import cv2
import numpy as np

img = cv2.imread("../img/taekwonv1.jpg")
img2 = img.copy()
draw = img.copy()

# 변환 전,후 삼각형 좌표 ---①
pts1 = np.float32([[188,14], [85,202], [294,216]])
pts2 = np.float32([[128,40], [85,307], [306,167]])

# 각 삼각형을 완전히 감싸는 사각형 좌표 구하기 ---②
x1,y1,w1,h1 = cv2.boundingRect(pts1)
x2,y2,w2,h2 = cv2.boundingRect(pts2)

# 사각형을 이용한 관심영역 설정 ---③
roi1 = img[y1:y1+h1, x1:x1+w1]
roi2 = img2[y2:y2+h2, x2:x2+w2]

# 관심영역을 기준으로 좌표 계산 ---④
offset1 = np.zeros((3,2), dtype=np.float32)
offset2 = np.zeros((3,2), dtype=np.float32)
for i in range(3):
    offset1[i][0], offset1[i][1] = pts1[i][0]-x1, pts1[i][1]-y1
    offset2[i][0], offset2[i][1] = pts2[i][0]-x2, pts2[i][1]-y2

# 관심 영역을 주어진 삼각형 좌표로 어핀 변환 ---⑤
mtrx = cv2.getAffineTransform(offset1, offset2)
warped = cv2.warpAffine( roi1, mtrx, (w2, h2), None, \
                        cv2.INTER_LINEAR, cv2.BORDER_REFLECT_101)

# 어핀 변환 후 삼각형만 골라 내기 위한 마스크 생성 ---⑥
mask = np.zeros((h2, w2), dtype = np.uint8)
cv2.fillConvexPoly(mask, np.int32(offset2), (255))

# 삼각형 영역만 마스킹해서 합성 ---⑦
warped_masked = cv2.bitwise_and(warped, warped, mask=mask)
roi2_masked = cv2.bitwise_and(roi2, roi2, mask=cv2.bitwise_not(mask))
roi2_masked = roi2_masked + warped_masked
img2[y2:y2+h2, x2:x2+w2] = roi2_masked

# 관심 영역과 삼각형에 선 그려서 출력 ---⑧
cv2.rectangle(draw, (x1, y1), (x1+w1, y1+h1), (0,255,0), 1)
cv2.polylines(draw, [pts1.astype(np.int32)], True, (255,0,0), 1)
cv2.rectangle(img2, (x2, y2), (x2+w2, y2+h2), (0,255,0), 1)
cv2.imshow('origin', draw)
cv2.imshow('warped triangle', img2)
cv2.waitKey(0)
cv2.destroyAllWindows()

 

임의의 삼각형을 지정해서 그 삼각형만 어핀 변환하는 코드입니다. 삼각형의 좌표가 원래 전체 영상 기준이기 때문에 관심 영역의 기준으로 바꾸기 위해서 삼각형의 각 꼭짓점 좌표에서 관심영역의 시작 지점을 빼서 offset1, offset2에 별도로 저장합니다. offset1과 offset2로 변환행렬을 구해서 원본 관심영역을 어핀 변환해서 warped를 구합니다. 변환한 삼각형 관심영역과 같은 크기의 배열을 만들어서 삼각형 영역만 255로 채우는 것으로 마스크를 생성합니다. 어핀 변환된 관심영역을 마스킹해서 warped_masked를 구하고 어핀 변환하지 않은 변환 후 관심영역을 배경으로 마스킹해서 둘을 합친 후 원래 영상의 관심영역 자리에 넣어서 합성을 마칩니다.

 

5.3 렌즈 왜곡

지금까지의 변환은 행렬식으로 표현하는 경우였습니다. 하지만 행렬식으로 표현할 수 없는 모양의 변환도 필요할 때가 있습니다. 투명한 물잔을 통해 비친 장면이나 일렁이는 물결에 반사된 모습 같은 것이 대표적입니다. 

 

■ 리매핑

OpenCV는 규칙성 없이 마음대로 모양을 변환해 주는 함수로 cv2.remap()을 제공합니다. 이 함수는 픽셀의 위치를 원하는 위치로 재배치합니다.

  • dst = cv2.remap(src, mapx, mapy, interpolation [, dst, borderMode, borderValue])
    • src: 입력 영상
    • mapx, mapy: x축과 y축으로 이동할 좌표(인덱스), src와 동일한 크기, dtype = float32
    • 나머지 인자는 cv2.warpAffine()과 동일
    • dst: 결과 영상

cv2.remap() 함수의 mapx, mapy는 입력 영상인 src와 크기가 같은 배열로 float32로 만들어야 하고, 배열의 각 요소는 src의 같은 인덱스의 픽셀이 각각 x축과 y축으로 옮겨 갈 새로운 인덱스를 갖게 해야 합니다. 예를 들어 mapx[0][0] = 10, mapy[0][0] = 5로 지정했다면 src의 좌표(10, 5)에 있는 픽셀을 (0, 0)으로 옮기라는 뜻입니다.

mapx, mapy를 만들 때는 np.indices() 함수를 사용합니다. 이 함수는 배열을 주어진 크기로 생성하는데, 자신의 인덱스를 값으로 초기화해서 3차원 배열로 반환합니다.

영상을 뒤집기 위한 행렬식은 [[-1 0 cols - 1], [0 -1 rows - 1]]입니다.

 

import cv2
import numpy as np
import time

img = cv2.imread('../img/girl.jpg')
rows, cols = img.shape[:2]

# 뒤집기 변환 행렬로 구현 ---①
st = time.time()
mflip = np.float32([ [-1, 0, cols-1],[0, -1, rows-1]]) # 변환 행렬 생성
fliped1 = cv2.warpAffine(img, mflip, (cols, rows))     # 변환 적용
print('matrix:', time.time()-st)

# remap 함수로 뒤집기 구현 ---②
st2 = time.time()
mapy, mapx = np.indices((rows, cols),dtype=np.float32) # 매핑 배열 초기화 생성
mapx = cols - mapx -1                                  # x축 좌표 뒤집기 연산
mapy = rows - mapy -1                                  # y축 좌표 뒤집기 연산
fliped2 = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)  # remap 적용
print('remap:', time.time()-st2)

# 결과 출력 ---③
cv2.imshow('origin', img)
cv2.imshow('fliped1',fliped1)
cv2.imshow('fliped2',fliped2)
cv2.waitKey()
cv2.destroyAllWindows()

 

위 코드를 실행시키면 이미지가 뒤집어집니다. np.indices() 함수로 원래 좌표를 갖는 배열을 생성해서 뒤집기에 필요한 연산을 수행하고 cv2.remap() 함수로 리매핑합니다. cv2.remap() 함수로 변환하는 것이 수행 속도가 느립니다. 따라서 cv2.remap() 함수는 변환 행렬로 표현할 수 없는 비선형 변환에만 사용하는 것이 좋습니다.

 

■ 오목 렌즈와 볼록 렌즈 왜곡

OpenCV에서 제공하는 좌표 변환 함수가 있습니다.

  • r, theta = cv2.cartToPolar(x, y): 직교좌표 -> 극좌표 변환
  • x, y = cv2.polarToCart(r, theta): 극좌표 -> 직교좌표
    • x, y: x, y 좌표 배열
    • r: 원점과의 거리
    • theta: 각도 값

직교좌표의 경우 좌상단 끝을 (0, 0) 좌표로 하는데, 극좌표를 사용하는 경우에는 영상의 중앙을 기준점으로 사용하고 원점을 기준으로 좌표 값들을 -1 ~ 1로 normalize 해 사용하는 것이 편리합니다.

 

import cv2
import numpy as np

img = cv2.imread('../img/taekwonv1.jpg')
print(img.shape)
rows, cols = img.shape[:2]

# ---① 설정 값 셋팅
exp = 2       # 볼록, 오목 지수 (오목 : 0.1 ~ 1, 볼록 : 1.1~)
scale = 1           # 변환 영역 크기 (0 ~ 1)

# 매핑 배열 생성 ---②
mapy, mapx = np.indices((rows, cols),dtype=np.float32)

# 좌상단 기준좌표에서 -1~1로 정규화된 중심점 기준 좌표로 변경 ---③
mapx = 2*mapx/(cols-1)-1
mapy = 2*mapy/(rows-1)-1

# 직교좌표를 극 좌표로 변환 ---④
r, theta = cv2.cartToPolar(mapx, mapy)

# 왜곡 영역만 중심확대/축소 지수 적용 ---⑤
r[r< scale] = r[r<scale] **exp  

# 극 좌표를 직교좌표로 변환 ---⑥
mapx, mapy = cv2.polarToCart(r, theta)

# 중심점 기준에서 좌상단 기준으로 변경 ---⑦
mapx = ((mapx + 1)*cols-1)/2
mapy = ((mapy + 1)*rows-1)/2
# 재매핑 변환
distorted = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)

cv2.imshow('origin', img)
cv2.imshow('distorted', distorted)
cv2.waitKey()
cv2.destroyAllWindows()

 

exp는 왜곡 지수를 설정하는 변수로 1은 원본과 동일하게 하고, 1보다 작고 0보다 큰 값을 지정하면 오목 렌즈 효과를 적용하고 1보다 큰 값을 지정하면 볼록 렌즈 효과를 적용합니다. scale은 영상에서 렌즈 효과를 주고 싶은 원 모양 영역의 크기를 비율로 지정합니다. 1은 100%를 의미합니다.

 

■ 방사 왜곡

대부분의 영상을 카메라로 촬영해서 얻는데, 카메라 렌즈는 동그랗고 영상은 사각형이다 보니 렌즈 가장자리 영상에는 왜곡이 생기기 마련이고 이런 왜곡을 배럴 왜곡(barrel distortion)이라고 합니다.

 

import cv2
import numpy as np

# 왜곡 계수 설정 ---①
k1, k2, k3 = 0.5, 0.2, 0.0 # 배럴 왜곡
#k1, k2, k3 = -0.3, 0, 0    # 핀큐션 왜곡

img = cv2.imread('../img/girl.jpg')
rows, cols = img.shape[:2]

# 매핑 배열 생성 ---②
mapy, mapx = np.indices((rows, cols),dtype=np.float32)

# 중앙점 좌표로 -1~1 정규화 및 극좌표 변환 ---③
mapx = 2*mapx/(cols-1)-1
mapy = 2*mapy/(rows-1)-1
r, theta = cv2.cartToPolar(mapx, mapy)

# 방사 왜곡 변영 연산 ---④
ru = r*(1+k1*(r**2) + k2*(r**4) + k3*(r**6)) 

# 직교좌표 및 좌상단 기준으로 복원 ---⑤
mapx, mapy = cv2.polarToCart(ru, theta)
mapx = ((mapx + 1)*cols-1)/2
mapy = ((mapy + 1)*rows-1)/2
# 리매핑 ---⑥
distored = cv2.remap(img,mapx,mapy,cv2.INTER_LINEAR)

cv2.imshow('original', img)
cv2.imshow('distorted', distored)
cv2.waitKey()
cv2.destroyAllWindows()

배럴 왜곡의 왜곡 계수의 값에 따라 밖으로 튀어나오는 배럴 왜곡이 나타나기도 하고, 안으로 들어가는 핀쿠션 왜곡(pincushion distortion)이 일어나기도 합니다.

 

OpenCV는 배럴 왜곡 현상이 일어나는 렌즈의 왜곡을 제거할 목적으로 앞서 설명한 배럴 왜곡 연산 공식으로 구현한 cv2.undistort() 함수를 제공합니다.

  • dst = cv2.undistort(src, cameraMtrix, distCoeffs)
    • src: 입력 원본 영상
    • cameraMatrix: 카메라 매트릭스[[fx 0 cx], [0 fy cy], [0 0 1]]
    • distCoeffs: 왜곡 계수, 최소 4개 또는 5, 8, 12, 14개
      • (k1, k2, p1, p2 [, k3])

cameraMatrix는 촬영할 카메라의 내부 파라미터인 중심점 cx, cy와 초점 거리 fx, fy를 입력합니다. 이 값들은 카메라가 생산될 때 까지는 고유의 특성이므로 개발자가 단순히 입력하거나 계산할 수 없습니다.

'Deep Learning(강의 및 책) > OpenCV' 카테고리의 다른 글

[OpenCV] 영상 매칭과 추적  (0) 2022.08.20
[OpenCV] 영상 분할  (0) 2022.08.04
[OpenCV] 영상 필터  (0) 2022.08.01
[OpenCV] 이미지 프로세싱 기초  (0) 2022.07.23
[OpenCV] 기본 입출력  (0) 2022.07.11