Python과 OpenCV – 15 : 이미지의 등치선(Contours) – 1/5

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_contours/py_contours_begin/py_contours_begin.html#contours-getting-started 입니다.

등치선이란 이미지에서 동일한 색이나 강도값을 가지는 포인트를 연결한 모든 연속된 선을 의미합니다. 이 등치선은 객체 인식이나 식별을 위한 분석에 매우 유용한 도구입니다. 등치선을 보다 더 정확히 추출하기 위해서 바이너리 이미지를 사용하는데, 이를 위해서 이미지에 임계치(Threshold) 적용이나 Canny 외곽선 검출을 적용합니다. cv2.findCountors 함수가 등치선을 추출하는 함수인데, 등치선 추출 과정에서 원본 이미지에 대한 수정된 이미지를 반환합니다. OpenCV에서 등치선을 추출한다는 것은 검정색 배경으로부터 하얀색 객체를 발견한다는 것고 유사합니다. 그러므로 반드시 발견된 객체는 하얀색이고 배경은 검정색이라는 점을 기억해야 합니다.

바이너리 이미지에서 등치선을 추출하는 코드는 아래와 같습니다.

import numpy as np
import cv2

im = cv2.imread('./data/cornerTest.jpg')
imgray = cv2.cvtColor(im,cv2.COLOR_BGR2GRAY)
ret,thresh = cv2.threshold(imgray,127,255,0)
image, contours, hierarchy = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

print(contours)

등치선은 등치선을 구성하는 좌표에 대한 배열인데, 위 코드의 결과는 다음과 같습니다.

[array([[[  0,   0]],
        [[  0, 511]],
        [[511, 511]],
        [[511,   0]]], dtype=int32), 
 array([[[107, 126]],
        [[108, 125]],
        [[265, 125]],
        [[266, 126]],
        [[266, 168]],
        [[387, 168]],
        [[388, 169]],
        [[388, 375]],
        [[387, 376]],
        [[169, 376]],
        [[168, 375]],
        [[168, 270]],
        [[108, 270]],
        [[107, 269]]], dtype=int32)]

2개의 등치선이 추출된 것이고 각 등치선의 구성 좌표가 배열 요소로써 저장되어 있습니다. cv2.cvtColor 함수는 3개의 인자를 받습니다. 첫번째는 등고선 추출 대상이 되는 이미지이고, 두번째는 등치선 추출 방식이며 세번째는 등치선 결과에 대한 근사치화 방식에 대한 지정입니다. 이 함수의 반환은 등치선 추출 과정에서 변경된 이미지와 등치선의 구성 좌표 배열 그리고 등치선의 계층객체입니다. 두번째와 세번째의 인자는 추후 좀더 자세히 살펴보도록 하겠습니다.

등치선을 그리는 함수는 cv2.drawContours 입니다. 이 함수는 5개의 인자를 받는데, 첫번째는 등치선을 그릴 이미지, 두번째는 그릴 등치선을 구성하는 좌표 배열, 세번째는 등치선의 좌표 배열 중 몇번째 등치선 요소를 그릴 것인지에 대한 인덱스로써 -1일 경우 모든 등치선을 그립니다. 네번째는 그려질 등치선의 색상, 다섯번째는 등치선의 굵기입니다. 아래의 코드는 모든 등치선을 초록선으로 3픽셀 굵기로 그립니다.

img = cv2.drawContours(img, contours, -1, (0,255,0), 3)

다음 코드는 4번째 등치선 요소만을 그리는 코드입니다.

img = cv2.drawContours(img, contours, 3, (0,255,0), 3)

대부분의 경우, 위의 코드보다는 아래의 코드가 더 유용합니다.

cnt = contours[4]
img = cv2.drawContours(img, [cnt], 0, (0,255,0), 3)

앞서 cv2.findContours 함수의 인자 중 세번째인 등치선 결과에 대한 근사치화에 대한 인자에 대해 좀더 정리를 해 보겠습니다. 이 인자로 지정할 수 있는 값은 cv2.CHAIN_APPROX_NONE과 cv2.CHAIN_APPROX_SIMPLE 입니다. 앞서 등치선이란 이미지에서 동일한 색상값이나 강도값을 갖는 연속된 좌표의 리스트라고 하였습니다. 이때 좌표의 리스트에는 매우 많은 수의 좌표가 담기게 됩니다. 인자값을 cv2.CHAIN_APPROX_NONE로 지정하면 등치선을 구성하는 좌표에 대한 근사치화를 수행하지 않고, cv2.CHAIN_APPROX_SIMPLE로 지정하면 등치선을 구성하는 좌표들 중 직선을 구성하는 구간에서 시작점과 끝점만을 남기고 그 중간 좌표들은 제거시키는 방식입니다. 즉 cv2.CHAIN_APPROX_NONE로 수행한 등치선의 좌표 개수가 매우 많은데, 이를 cv2.CHAIN_APPROX_SIMPLE로 변경해 지정함으로써 등치선의 구성 좌표 개수를 최적화할 수 있습니다.

Python과 OpenCV – 14 : 이미지 피라미드(Image Piramid)

이 글의 원문은 https://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_pyramids/py_pyramids.html#pyramids 입니다.

동일한 영상에 대해서 이미지의 크기를 50% 크기만큼 연속적으로 줄이거나 늘려 생성된 이미지 집합을 이미지 피라미드라고 합니다. OpenCV에서 제공하는 이미지 피라미드 관련 함수는 cv2.pyrDown과 cv2.pryUp 인데, cv2.pyrDown 함수는 입력 이미지를 50% 크기로 줄인 이미지를 생성해 반환하고 cv2.pryUp은 입력 이미지를 200% 크기로 확대한 이미지를 생성해 반환합니다. 먼저 cv2.pryDown 함수의 예를 살펴보면..

import cv2

img0 = cv2.imread('./data/apple.jpg')
img1 = cv2.pyrDown(img0)
img2 = cv2.pyrDown(img1)
img3 = cv2.pyrDown(img2)

cv2.imwrite('img0.jpg', img0)
cv2.imwrite('img1.jpg', img1)
cv2.imwrite('img2.jpg', img2)
cv2.imwrite('img3.jpg', img3)

위의 예제에서 apple.jpg 이미지는 512×512 크기의 이미지인데, img1은 256×256으로.. img2는 128×128으로.. img3는 64×64로 크기가 축소된 이미지가 저장됩니다. 다음은 cv2.pryUp 함수에 대한 예제입니다.

import cv2

img0 = cv2.imread('./data/apple.jpg')
img1 = cv2.pyrUp(img0)
img2 = cv2.pyrUp(img1)
img3 = cv2.pyrUp(img2)

cv2.imwrite('img0.jpg', img0)
cv2.imwrite('img1.jpg', img1)
cv2.imwrite('img2.jpg', img2)
cv2.imwrite('img3.jpg', img3)

위의 예제 역시 유사하게 apple.jpg 이미지는 512×512이고, img1.jpg는 1024×1024로.. img2는 2048×2048로.. img3는 4096×4096의 크기로 확대되어 저장됩니다.

지금까지의 이미지 피라미드는 Gaussian Pyramid 라고 하는데, Laplacian Pyramids 라는 종류의 피라미드가 있습니다. Laplacian Pyramids는 Gaussian Pyramid로부터 생성되며 OpenCV에서 이를 위해 별도로 제공하는 함수는 없습니다. Gaussian Pyramid는 Gaussian Pyramid로부터 생성된 이미지들에 대한 외곽선 추출 이미지들의 집합입니다.

이미지 피라미드에 대한 응용 중 하나로 Image Blending이 있습니다. 하나의 예제 코드를 언급할텐데, 그 예제 코드의 결과를 먼저 보면..

A 영상과 B 영상의 반씩 혼합하여 ls_과 real이라는 새로운 영상을 만드는 것인데 real은 단순히 A와 B 각각 좌우에 대한 이미지를 붙인 이미지이지만 ls_는 A와 B를 붙인 경계가 날카롭지 않고 자연스럽습니다. 이런 ls_를 생성하는 절차는 다음과 같습니다.

  1. A와 B 이미지를 로딩한다.
  2. A와 B에 대한 Gaussian Pyramid를 생성한다. (6개 정도)
  3. Gaussian Pyramid로 생성된 결과로부터 Laplacian Pyramids를 생성한다.
  4. Laplacian Pyramids의 각 레벨에 대한 A와 B 영상의 반절씩 조합한다.
  5. 앞서 조합한 이미지 피라미드로부터 원본 이미지를 다시 생성한다.

위의 절차에 대한 코드는 다음과 같습니다.

import cv2
import numpy as np
from matplotlib import pyplot as plt

A = cv2.imread('./data/apple.jpg')
B = cv2.imread('./data/orange.jpg')
 
# generate Gaussian pyramid for A
G = A.copy()
gpA = [G]
for i in range(6):
    G = cv2.pyrDown(G)
    gpA.append(G)

# generate Gaussian pyramid for B
G = B.copy()
gpB = [G]
for i in range(6):
    G = cv2.pyrDown(G)
    gpB.append(G)

# generate Laplacian Pyramid for A
lpA = [gpA[5]]
for i in range(5,0,-1):
    GE = cv2.pyrUp(gpA[i])
    L = cv2.subtract(gpA[i-1],GE)
    lpA.append(L)

# generate Laplacian Pyramid for B
lpB = [gpB[5]]
for i in range(5,0,-1):
    GE = cv2.pyrUp(gpB[i])
    L = cv2.subtract(gpB[i-1],GE)
    lpB.append(L)

# Now add left and right halves of images in each level
LS = []
xxx = 0
for la,lb in zip(lpA,lpB):
    rows,cols,dpt = la.shape
    ls = np.hstack((la[:,0:int(cols/2)], lb[:,int(cols/2):]))
    LS.append(ls)

# now reconstruct
ls_ = LS[0]
for i in range(1,6):
    ls_ = cv2.pyrUp(ls_)
    ls_ = cv2.add(ls_, LS[i])

# image with direct connecting each half
real = np.hstack((A[:,:int(cols/2)],B[:,int(cols/2):]))

b, g, r = cv2.split(A)
A = cv2.merge([r,g,b])
plt.subplot(2,2,1), plt.imshow(A), plt.title('A'), plt.xticks([]), plt.yticks([])

b, g, r = cv2.split(B)
B = cv2.merge([r,g,b])
plt.subplot(2,2,2), plt.imshow(B), plt.title('B'), plt.xticks([]), plt.yticks([])

b, g, r = cv2.split(ls_)
ls_ = cv2.merge([r,g,b])
plt.subplot(2,2,3), plt.imshow(ls_), plt.title('ls_'), plt.xticks([]), plt.yticks([])

b, g, r = cv2.split(real)
real = cv2.merge([r,g,b])
plt.subplot(2,2,4), plt.imshow(real), plt.title('real'), plt.xticks([]), plt.yticks([])

plt.show()