본문 바로가기
Image Processing/OpenCV

[OpenCV] 기하학(Geometry) -Rotation, Scaling, Translation, interpolation

by AteN 2022. 11. 27.

기하학(Geometry) 처리 

기하학은 점, 선, 면, 도형 등의 기하학적인 대상을 다루는 학문이다. 즉, 대상의 길이, 넓이 각도 등을 측정하거나 공간상의 특성을 연구하는 수학의 한 분야이다 

기하학의 영어 단어  'Geometry' 는 토지를 뜻하는 geo와 측량을 뜻하는 metry라는 단어가 합해져서 만들어진 용어이다 

영상처리에서 기하학 처리는 영상 내에 있는 기하학적인 대상의 공간적 배치를 변경하는 과정을 말한다. 이것을 화소의 입장에서 보면, 영상을 구성하는 화소들의 공간적 위치를 재배치하는 과정이라고 할 수 있다. 이러한 변화에는 크게 외정, 크기 변경, 평행이동등이 있다. 보통 영상처리 관련 논문에서는 이 세가지 변환을 일커러 RST 변환이라고 말한다. R은 Rotation, S는 Scaling,  T는 Translation 이다 

 

사상 (mapping)

기하학적 처리는 기본은 화소들의 배치를 변경하는 것이다. 화소의 배치를 변경하려면 사상 (mapping)이라는 의미를 이해해야 한다. 사상은 화소들의 배치를 변경할 때, 입력 영상의 좌표가 새롭게 배치될 해당 목적 영상의 좌표를 찾아서 화소 값을 옮기는 과정을 말한다 

여기서 순방향 사상 (forward mapping)과 역방향 사상 (reverse mapping)의 두 가지 방식이 있다. 

순방향 사상은 입력 영상의 좌표를 중심으로 목적 영상의 좌표를 계산하여 화소의 위치를 변환하는 방식이다 이 방식은 일반적으로 입력 영상과 목적 영상으 크기가 같을 때 유용하게 사용된다. 반면 영상의 크기가 달라지면 홀 (hole)이나 오버랩(overlap)의 문제가 발생할 수 있다.

 

홀은 입력 영상의 좌표들로 목적 영상의 좌표를 만드는 과정에서 사상되지 않는 화소를 가르킨다. 보통 영상을 확대하거나 회전할 때에 발생한다. 반면 오버랩은 영상을 축소할 때 주로 발생한다. 이것은 입력 영상의 여러 화소들이 목적 영상의 한 화소로 사상되는 것을 말한다 

이러한 문제를 해결할 수 있는 방법이 역방향 사상이다. 역방향 사상은 목적 영상의 좌표를 중심으로 역변환을 계산하여 해당하는 원본 영상의 좌표를 찾아서 화소값을 가져오는 방식이다. 

역방향 사상의 방식은 홀이나 오버랩은 발생하지 않는다. 다만 입력 영상의 한 화소를 목적 영상의 여러 화소에서 사용하게 되면 결과 영상의 품질이 떨어질 수 있다. 이런 문제를 해결하는 방법이 보간법이다. 

 

영상 회전 (Rotation)

  • 회전은 입력 영상의 모든 화소를 영상의 원점을 기준으로 원하는 각도 만큼 모든 화소에 대해서 회전 변환을 시키는 것을 말한다. 이것은 2차원 평면에서 회전 변환을 나타내느 행렬을 통해서 수식으로 표현할 수 있다.
  • 회번 현환의 혁행렬이 sin() 함수의 부호만 다르기 때문에 순바얗 사상과 역방향 사상도 단지 sin() 함수의 부호 만 차이가 잇다.
import numpy as np,  cv2
 
def bilinear_value(img, pt):
    x, y = np.int32(pt)
    if y >= img.shape[0]-1: y = y - 1
    if x >= img.shape[1]-1: x = x - 1

    P1, P2, P3, P4 = np.float32(img[y:y+2,x:x+2].flatten())
    alpha, beta = pt[1] - y,  pt[0] - x                   # 거리 비율

    M1 = P1 + alpha * (P3 - P1)                      # 1차 보간
    M2 = P2 + alpha * (P4 - P2)
    P  = M1 + beta  * (M2 - M1)                     # 2차 보간
    return np.clip(P, 0, 255)                       # 화소값 saturation후 반환
 
def contain(p, shape):                              # 좌표(y,x)가 범위내 인지 검사
    return 0<= p[0] < shape[0] and 0<= p[1] < shape[1]
 
# 수행시간 체크 함수
stime = 0
def ck_time(mode = 0 , msg = ""):
    global stime

    if (mode ==0 ):
        stime = time.perf_counter()

    elif (mode==1):
       etime = time.perf_counter()
       elapsed = (etime - stime)
       print("수행시간 = %.5f sec" % elapsed)  # 초 단위 경과 시간

    elif (mode == 2):
        etime = time.perf_counter()
        return (etime - stime)

    elif (mode== 3 ):
        etime = time.perf_counter()
        elapsed = (etime - stime)
        print("%s = %.5f sec" %(msg, elapsed))  # 초 단위 경과 시간
 
def rotate(img, degree):
    dst = np.zeros(img.shape[:2], img.dtype)                     # 목적 영상 생성
    radian = (degree/180) * np.pi                               # 회전 각도 - 라디언
    sin, cos = np.sin(radian), np.cos(radian)   # 사인, 코사인 값 미리 계산

    for i in range(img.shape[0]):                                       # 목적 영상 순회 - 역방향 사상
        for j in range(img.shape[1]):
            y = -j * sin + i * cos
            x =  j * cos + i * sin                  # 회선 변환 수식
            if contain((y, x), img.shape):             # 입력 영상의 범위 확인
                dst[i, j] = bilinear_value(img, [x, y])           # 화소값 양선형 보간
    return dst
 
def rotate_pt(img, degree, pt):
    dst = np.zeros(img.shape[:2], img.dtype)                     # 목적 영상 생성
    radian = (degree/180) * np.pi                               # 회전 각도 - 라디언
    sin, cos = np.sin(radian), np.cos(radian)   # 사인, 코사인 값 미리 계산

    for i in range(img.shape[0]):                              # 목적 영상 순회 - 역방향 사상
        for j in range(img.shape[1]):
            jj, ii = np.subtract((j, i), pt)                # 중심좌표 평행이동,
            y = -jj * sin + ii * cos               # 회선 변환 수식
            x =  jj * cos + ii * sin
            x, y = np.add((x, y), pt)
            if contain((y, x), img.shape):                      # 입력 영상의 범위 확인
                dst[i, j] = bilinear_value(img, [x, y])           # 화소값 양선형 보간
    return dst
 
image = cv2.imread('img/rotate.jpg', cv2.IMREAD_GRAYSCALE)
if image is None: raise Exception("영상 파일을 읽기 에러")
 
center = np.divmod(image.shape[::-1], 2)[0]
dst1 = rotate(image, 20)                                        # 원점 기준 회전 변환
dst2 = rotate_pt(image, 20, center )                             # 영상 중심 기준 회전 변환
 
cv2.imshow("image", image)
cv2.imshow("dst1-rotated on org", dst1)
cv2.imshow("dst2-rotated on center", dst2)
cv2.waitKey(0)

 

영상 크기 변경 (scaling)

  • 크기 변경(scaling)은 입력 영상의 가로와 세로로 크기를 변경해서 목적 영상을 만드는 방버이다. 목적 영상이 입력영상보다 커지면 확대가 되고, 작이졈 축소가 된다.
import numpy as np, cv2, time
def scaling(img, size):  # 크기 변경 함수
    dst = np.zeros(size[::-1], img.dtype)  # 행렬과 크기는 원소가 역순
    ratioY, ratioX = np.divide(size[::-1], img.shape[:2])
    y = np.arange(0, img.shape[0], 1)
    x = np.arange(0, img.shape[1], 1)
    y, x = np.meshgrid(y, x)
    i, j = np.int32(y * ratioY), np.int32(x * ratioX)
    dst[i, j] = img[y, x]
    return dst
 
def scaling2(img, size):  # 크기 변경 함수
    dst = np.zeros(size[::-1], img.dtype)  # 행렬과 크기는 원소가 역순
    ratioY, ratioX = np.divide(size[::-1], img.shape[:2])
    for y in range(img.shape[0]):  # 입력 영상 순회 - 순방향 사상
        for x in range(img.shape[1]):
            i, j = int(y * ratioY), int(x * ratioX)  # 목적 영상의 y, x 좌표
            dst[i, j] = img[y, x]
    return dst
def time_check(func, image, size, title):  ## 수행시간 체크 함수
    start_time = time.perf_counter()
    ret_img = func(image, size)
    elapsed = (time.perf_counter() - start_time) * 1000
    print(title, " 수행시간 = %0.2f ms" % elapsed)
    return ret_img
 
image = cv2.imread('img/scaling.jpg', cv2.IMREAD_GRAYSCALE)
if image is None: raise Exception("영상 파일을 읽기 에러")
 
dst1 = scaling(image, (150, 200))  # 크기 변경 - 축소
dst2 = scaling2(image, (150, 200))  # 크기 변경 - 축소
dst3 = time_check(scaling, image, (300, 400), "[방법1] 정방행렬 방식>")
dst4 = time_check(scaling2, image, (300, 400), "[방법2] 반복문 방식>")
[방법1] 정방행렬 방식>  수행시간 = 3.90 ms
[방법2] 반복문 방식>  수행시간 = 121.54 ms
cv2.imshow("image", image)
cv2.imshow("dst1- zoom out", dst1)
cv2.imshow("dst3- zoom out", dst3)
cv2.resizeWindow("dst1- zoom out", 260, 200)  # 윈도우 크기 확장
cv2.waitKey(0)

평행이동(translation)

  • 일반적으로 그래프에 좌표를 표시할 대와는 다르게 영상에서 원점 좌표는 기본적으로 최상단 왼쪽이다. 평행이동(translation)은 영상의 원점을 기준으로 모슨 화소를 동일하게 가로 방향과 세로 방향을 옮기는 것을 말한다.
  • 평행 이동할 화소가 없는 부분인 상단과 왼쪽 부분은 0(검은색) 혹은 255(흰색)로 지정한다
import numpy as np, cv2
 
def contain(p, shape):                              # 좌표(y,x)가 범위내 인지 검사
    return 0<= p[0] < shape[0] and 0<= p[1] < shape[1]
 
def translate(img, pt):
    dst = np.zeros(img.shape, img.dtype)            # 목적 영상 생성
    for i in range(img.shape[0]):                   # 목적 영상 순회 - 역방향 사상
        for j in range(img.shape[1]):
            x, y = np.subtract((j, i) , pt)         # 좌표는 가로, 세로 순서 
            if contain((y, x), img.shape):          # 영상 범위 확인
                dst[i, j] = img[y, x]               # 행렬은 행, 열 순서 
    return dst
 
image = cv2.imread('img/translate.jpg', cv2.IMREAD_GRAYSCALE)
if image is None: raise Exception("영상 파일을 읽기 에러")
 
dst1 = translate(image, (30, 80))                  # x = 30, y = 80 으로 평행이동
dst2 = translate(image, (-70, -50))
 
cv2.imshow("image", image)
cv2.imshow("dst1: trans to (80, 30)", dst1);
cv2.imshow("dst2: trans to (-50, -70)", dst2);
cv2.waitKey(0)
  • 실행 결과에서 dst1 은 가로 30, 세로 30 화소만큼 평행이동한다. 또한 dst2는 가로 -70세로 -50만큰 평행 이동한다. 양수로 평행이동하면 오른쪽 하단으로 영상이 옮겨지고, 음수로 평행 이동할 경우에는 영상이 왼쪽, 상당으로 이동하게 된다. 또한 빈공간은 0으로 채워서 검은색이 되며, 목적 영상의 크기를 벗어나는 화소들은 목적 영상에서 제거된다.

양선형 보간법 (bilinear interpolation)

  • 영상을 확대할 때 확대비율이 커지면, 최근접 이웃 보간법은 모자이크 현상 혹은 경계부분에서 계단 현상이 나타나게 된다. 이러한 문제를 보완하는 방법이 양선형 보간법 이다.
  • 여기서 선형(linear)의 의미는 쉽게 표현하자면 직선의 특징을 가진 것이라 할 수 있는데, 직선의 방정식을 예로 들 수 있다. 두 개 화소의 값을 알고 있을 때 그 값을 직선으로 그려보면 이때 직선 위에 위치한 화소들의 값은 직선의 쉭을 이용해서 쉽게 게산할 수 있다.

보간 방법 옵션 상수

옵션 상수  설명
cv2.INTER_NEAREST 0 최근접 이웃 보간
cv2.INTER_LINEAR 1 양선형 보간(기본값)
cv2.INTER_CUBIC 2 바이큐빅 보간 - 4x4 이웃 화소 이용
cv2.INTER_AREA 3 픽셀 영역의 관계로 리샘플링
cv2.INTER_LANCZOS4 4 Lanczos 보간 - 8x8 이웃 화소 이용
import numpy as np, cv2
 
def scaling_nearest(img, size):                                # 크기 변경 함수
    dst = np.zeros(size[::-1], img.dtype)                      # 행렬과 크기는 원소가 역순
    ratioY, ratioX = np.divide(size[::-1], img.shape[:2])
    i = np.arange(0, size[1], 1)
    j = np.arange(0, size[0], 1)
    i, j = np.meshgrid(i, j)
    y, x = np.int32(i / ratioY), np.int32(j / ratioX)
    dst[i,j] = img[y,x]

    return dst
 
# 단일 화소 양선형 보간 수행 함수 
def bilinear_value(img, pt):                         
    x, y = np.int32(pt)
    if x >= img.shape[1]-1: x = x -1 # 영상 범위 벗어남을 처리
    if y >= img.shape[0]-1: y = y - 1

    P1, P3, P2, P4 = np.float32(img[y:y+2,x:x+2].flatten())
   ## 4개의 화소 가져옴 – 화소 직접 접근
   #  P1 = float(img[y, x] )                         # 상단 왼쪽 화소
   #  P3 = float(img[y + 0, x + 1])                  # 상단 오른쪽 화소
   #  P2 = float(img[y + 1, x + 0])                  # 하단 왼쪽 화소
   #  P4 = float(img[y + 1, x + 1])                  # 하단 오른쪽 화소

    alpha, beta = pt[1] - y,  pt[0] - x              # 거리 비율
    M1 = P1 + alpha * (P3 - P1)                      # 1차 보간
    M2 = P2 + alpha * (P4 - P2)
    P  = M1 + beta  * (M2 - M1)                      # 2차 보간
    return  np.clip(P, 0, 255)                       # 화소값 saturation후 반환
 
def scaling_bilinear(img, size):                        	# 양선형 보간
    ratioY, ratioX = np.divide(size[::-1], img.shape[:2])  # 변경 크기 비율

    dst = [[ bilinear_value(img, (j/ratioX, i/ratioY))     # for문 이용한 리스트 생성
             for j in range(size[0])]
           for i in range(size[1])]
    return np.array(dst, img.dtype)
 
image = cv2.imread('img/interpolation.jpg', cv2.IMREAD_GRAYSCALE)
if image is None: raise Exception("영상 파일 읽기 에러")
 
size = (350, 400)
dst1 = scaling_bilinear(image, size)                # 크기 변경 - 양선형 보간
dst2 = scaling_nearest(image, size)                 # 크기 변경 - 최근접 이웃 보간
dst3 = cv2.resize(image, size, 0, 0, cv2.INTER_LINEAR)  # OpenCV 함수 적용
dst4 = cv2.resize(image, size, 0, 0, cv2.INTER_NEAREST)
 
cv2.imshow("image", image)
cv2.imshow("User_bilinear", dst1)
cv2.imshow("User_Nearest", dst2)
cv2.imshow("OpenCV_bilinear", dst3)
cv2.imshow("OpenCV_Nearest", dst4)
cv2.waitKey(0)

'Image Processing > OpenCV' 카테고리의 다른 글

[OpenCV] 모폴로지 (morphology)  (0) 2022.11.25
[OpenCV] 필터링 (Filtering)  (0) 2022.11.25
[OpenCV] 에지 검출  (1) 2022.11.25
[OpenCV] 블러링과 샤프닝  (0) 2022.11.24
[OpenCV] 03. 화소처리 (컬러 공간 변환)  (0) 2022.11.23

댓글