본문 바로가기
Study/Python

Python opencv로 좌표 형태의 도형을 mask 이미지 배열로 변환, 또 역으로

by 개발새-발 2021. 7. 13.
반응형

Computer vision과 관련된 AI 경진대회들 중에서는 이미지에서 특정 물체를 분리해야 하는 과제를 주는 대회들이 존재한다. 이때, '어떤 물체가 어디에 존재한다'라는 정보를 mask 이미지로 주는 대회가 있는가 하면 영역을 다각형으로 표현하여 이 다각형의 좌표로 제공하는 대회가 있다. Segmentation 모델의 대부분은 mask 이미지를 받고, 또 mask 이미지의 형태로 결과를 반환한다. 다각형 좌표로 제공받았다면, 이를 mask 이미지로 변환하는 과정이 필요하다. 이 과정에 대하여 알아보자.

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

좌표 형태의 도형을 mask배열로 만들기

opencv에 이미 관련 기능이 구현이 되어있다. 다각형 점들의 좌표를 이용하여 주어진 배열에 다각형들을 그리는 fillPoly를 사용하면 된다. 다각형이 그려질 mask 배열, 다각형의 정보가 저장되어있는 numpy배열을 담은 리스트, 채워줄 값을 넘겨주면 된다. 아래 코드는 mask배열에 다각형 polygon1과 polygon2를 그린다. 다각형이 그려진 위치의 값은 1로 정하였다.

mask1 = np.zeros((100,100),dtype = np.uint8)
polygon1 = np.array([[5,5],[30,30],[60,10],[70,70],[50,45],[5,80]])
polygon2 = np.array([[40,80],[45,75],[43,70],[60,80],[50,90]])

cv2.fillPoly(mask1,[polygon1,polygon2],1)
plt.imshow(mask1)
<matplotlib.image.AxesImage at 0x1e5a8327f10>

만약 각각의 도형이 다른 값을 나타내어 mask에 다른 숫자로 나타내야 한다면 fillpoly에서 채워줄 값만 바꾸어 그려주면 된다. 아래 코드는 polygon1을 1로, polygon2를 2로 mask배열에 그린다.

mask2 = np.zeros((100,100),dtype = np.uint8)
polygon1 = np.array([[5,5],[30,30],[60,10],[70,70],[50,45],[5,80]])
polygon2 = np.array([[40,80],[45,75],[43,70],[60,80],[50,90]])

cv2.fillPoly(mask2,[polygon1],1)
cv2.fillPoly(mask2,[polygon2],2)

plt.imshow(mask2)
<matplotlib.image.AxesImage at 0x1e5a82d88e0>

mask 배열에서 도형 윤곽의 좌표값들을 가져오기

mask 배열에서 다각형들을 다시 그릴 수 있는 좌표값들을 가져와보자. 가장 편한 방법 각 도형의 윤곽선들의 좌표를 불러오는 것이다. 이 또한 opencv에 관련 기능이 구현이 되어 있다. findContours를 사용하여 도형 윤곽의 좌표를 가져와 보자.

contours, hierarchy = cv2.findContours(mask1, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)

한 줄의 코드로 윤곽선들의 좌표를 가져왔다. 위 코드에서 contoursnumpy배열을 담고 있는 리스트이고, 리스트에서 각각의 배열들은 윤곽선들의 좌표를 담고 있다.

len(contours)
2
contours[0].shape
(47, 1, 2)
contours[0][:10]
array([[[43, 70]],

       [[43, 71]],

       [[44, 72]],

       [[44, 73]],

       [[45, 74]],

       [[45, 75]],

       [[44, 76]],

       [[43, 77]],

       [[42, 78]],

       [[41, 79]]], dtype=int32)

받아온 윤곽선들의 좌표들을 점으로 찍어 확인해보자.

canvas = np.zeros((100,100),dtype=np.uint8)

for contour in contours:
    contour = contour.squeeze(1)
    canvas[contour.T[1],contour.T[0]] = 1

plt.imshow(canvas)
<matplotlib.image.AxesImage at 0x1e5a846bc10>

아래 두 코드는 contours에 담겨있던 각각의 윤곽선들을 따로 표시한다.

canvas = np.zeros((100,100),dtype=np.uint8)
contour = contours[0].squeeze(1)
canvas[contour.T[1],contour.T[0]] = 1    
plt.imshow(canvas)
<matplotlib.image.AxesImage at 0x1e5a84c0d90>

canvas = np.zeros((100,100),dtype=np.uint8)
contour = contours[1].squeeze(1)
canvas[contour.T[1],contour.T[0]] = 1    
plt.imshow(canvas)
<matplotlib.image.AxesImage at 0x1e5a8513970>

특정 값으로 masking 한 도형들의 윤곽만 불러오기

mask2를 생성할 때 하나의 도형은 1로, 다른 하나는 2로 그려주었었다. 이 배열을 findContours를 이용하여 윤곽선을 가져오면, 숫자 구분 없이 모든 도형의 윤곽선을 불러오게 된다.

contours, hierarchy = cv2.findContours(mask2, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
len(contours)
2

원하는 수로 그린 도형만 불러오고 싶다면 mask를 조작해준 뒤에 findContours를 사용하여야 한다. 아래 코드는 원하는 값으로 그려진 도형의 윤곽선을 불러온다.

# Get polygon I want
mask_poly_1 = (mask2 == 1).astype(np.uint8)
mask_poly_2 = (mask2 == 2).astype(np.uint8)

# Get contour points
contours_value1, _ = cv2.findContours(mask_poly_1,cv2.RETR_LIST,cv2.CHAIN_APPROX_NONE)
contours_value2, _ = cv2.findContours(mask_poly_2,cv2.RETR_LIST,cv2.CHAIN_APPROX_NONE)

각각의 결과를 그려서 확인해보자.

# plotting
name = ['Polygon value 1','Polygon value 2']

fig, axes = plt.subplots(1,2)
fig.set_size_inches((10,10))

for i,(name,contours) in enumerate(zip(name,[contours_value1,contours_value2])):
    canvas = np.zeros((100,100),dtype=np.uint8)
    contour = contours[0].squeeze(1)
    canvas[contour.T[1],contour.T[0]] = 1

    axes[i].imshow(canvas)
    axes[i].set_title(name)

점의 개수 줄이기

사실 우리는 윤곽선의 모든 점들의 정보가 필요한 것이 아니다. 꼭짓점의 좌표만 있어도 우리는 그 점들을 기준으로 다시 다각형을 그려낼 수 있다. 간단하게 사각형을 예시로 들어보자. 가로, 세로의 길이가 각각 10인 정사각형이 있다. 윤곽선의 점들의 개수는 36개이다. 하지만, 꼭짓점의 정보만 가져간다면 4개의 점만 필요할 것이다. 아래 코드는 정사각형의 윤곽선 전체의 점과 이를 단순화한 점들을 그림으로 표시한다. 청록색의 점들은 정사각형의 윤곽선 전체의 점들을 가져온 것이며 노란색 점들은 이를 줄여서 단순화한 것이다.

mask = np.zeros((20,20),dtype=np.uint8)
square = np.array([[5,5],[5,14],[14,14],[14,5]])

# APPROX_NONE vs APPROX_SIMPLE
cv2.fillPoly(mask,[square],1)
contours_approx_none, _ = cv2.findContours(mask,cv2.RETR_LIST,cv2.CHAIN_APPROX_NONE)
contours_approx_simple, _ = cv2.findContours(mask,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)

# plot
canvas = np.zeros((20,20),dtype=np.uint8)
contours_approx_none = contours_approx_none[0].squeeze(1) 
contours_approx_simple = contours_approx_simple[0].squeeze(1)
canvas[contours_approx_none.T[1],contours_approx_none.T[0]] = 1
canvas[contours_approx_simple.T[1],contours_approx_simple.T[0]] = 2

plt.figure(figsize=(6,6))
plt.imshow(canvas)
<matplotlib.image.AxesImage at 0x1e5a880ed30>

print("Number of every point in contour : ",len(contours_approx_none))
print("Simplified : ",len(contours_approx_simple))
Number of every point in contour :  36
Simplified :  4

필요한 점들을 간추려서 가져오는 방법은 findContours에서 Contour approximation method 옵션을 CHAIN_APPROX_SIMPLE로 부여하는 것이다. 원하다면 CHAIN_APPROX_TC89_L1, CHAIN_APPROX_TC89_KCOS 로도 부여가 가능하다. 각각이 다른 근사 알고리즘이다. 각 옵션에 대해 findContours를 실행해보고 이들의 결과를 확인하고, 가져온 점들로 다시 fillPoly를 수행하여보자. 다시 생성된 다각형들이 본래의 다각형과 차이가 있는지도 확인해보았다.

fig, axes = plt.subplots(3,4)
fig.set_size_inches((16,13))

names = ['None','Simple','TC89_L1','TC89_KCOS']
contours_found = []

for i, mode in enumerate([cv2.CHAIN_APPROX_NONE,cv2.CHAIN_APPROX_SIMPLE, cv2.CHAIN_APPROX_TC89_L1, cv2.CHAIN_APPROX_TC89_KCOS]):
    canvas = np.zeros((100,100),dtype=np.uint8)
    contours, hierarchy = cv2.findContours(mask1, cv2.RETR_LIST,mode)
    contours_found.append(contours)

    for contour in contours:
        contour = contour.squeeze(1)
        canvas[contour.T[1],contour.T[0]] = 1

    mask = np.zeros((100,100),dtype=np.uint8)
    cv2.fillPoly(mask,contours,1)

    axes[0,i].imshow(canvas)
    axes[0,i].set_title(names[i])
    axes[1,i].imshow(mask)
    axes[1,i].set_title(names[i]+' after fillPoly')
    axes[2,i].imshow(mask ^ mask1)
    axes[2,i].set_title(names[i]+' Diff')

for name,contours in zip(names,contours_found):
    n_points = 0
    for c in contours:
        n_points += c.shape[0]
    print("Points from",name," : ",n_points)
Points from None  :  307
Points from Simple  :  99
Points from TC89_L1  :  54
Points from TC89_KCOS  :  44

요약

간단하게 요약하자면 다음과 같다.

  • 다각형들의 좌표로 mask 이미지를 만들 때 (polygons2mask)
     cv2.fillPoly(mask,[polygon1,polygon2,...],1)
  • mask 이미지에서 다각형을 다시 만들 수 있는 좌표를 가져오고자 할 때 (mask2polygons)
     contours, _ = cv2.findContours(mask,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE)
반응형

댓글