logo

도형 탐지

윤곽선의 다각형 근사와 원형도, 무게중심, 볼록 껍질

실습 준비

import cv2 as cv
import numpy as np
from yeom import to_pil
# 도형 이미지를 읽고 밝기 영상으로 변환
shapes_image = cv.imread("shapes.webp")
shapes_gray = cv.cvtColor(
    shapes_image,
    cv.COLOR_BGR2GRAY,
)

# 도형을 흰색 전경으로 이진화
_, shapes_binary = cv.threshold(
    shapes_gray,
    60,
    255,
    cv.THRESH_BINARY,
)

# 열림과 닫힘으로 작은 잡음과 도형 내부의 빈틈 정리
kernel = cv.getStructuringElement(
    cv.MORPH_ELLIPSE,
    (3, 3),
)
shapes_cleaned = cv.morphologyEx(
    shapes_binary,
    cv.MORPH_OPEN,
    kernel,
)
shapes_cleaned = cv.morphologyEx(
    shapes_cleaned,
    cv.MORPH_CLOSE,
    kernel,
)

# 정리된 이진 영상에서 바깥 윤곽선만 추출
shape_contours, _ = cv.findContours(
    shapes_cleaned,
    cv.RETR_EXTERNAL,
    cv.CHAIN_APPROX_SIMPLE,
)

len(shape_contours)
실행 결과
17

윤곽선 추출에서 만든 이진화·열림·닫힘·윤곽선 추출 흐름을 그대로 사용함.

실행 결과

다각형 분류와 볼록 껍질의 코드 및 결과 이미지는 shape.ipynb를 처음부터 끝까지 실행하여 저장함.

다각형 근사

다각형 근사(polygonal approximation)는 많은 점으로 이루어진 윤곽선을 적은 꼭짓점의 선분으로 단순화하는 방법임. OpenCV의 cv.approxPolyDP()는 Ramer–Douglas–Peucker 알고리즘을 사용함.

# 첫 번째 윤곽선의 둘레 계산
contour = shape_contours[0]
perimeter = cv.arcLength(contour, True)

# 둘레의 1%를 허용 오차로 사용해 닫힌 다각형으로 근사
approximation = cv.approxPolyDP(
    contour,
    0.01 * perimeter,
    True,
)

len(contour), len(approximation)
실행 결과
(94, 5)

윤곽선을 이루던 94개의 점이 5개의 꼭짓점으로 줄어듦.

  • 첫 번째 인자: 원본 윤곽선 좌표
  • epsilon=0.01 * perimeter: 원본 윤곽선과 근사 선분 사이에서 허용할 최대 거리
  • True: 닫힌 다각형으로 근사
  • epsilon 증가: 더 많은 세부 점을 제거하여 꼭짓점 수 감소
  • epsilon 감소: 원래 윤곽선의 굴곡을 더 많이 보존

근사 결과의 꼭짓점이 3개면 삼각형, 4개면 사각형, 5개면 오각형으로 분류 가능함.

원형도와 도형 분류

원은 픽셀 경계가 계단 모양이므로 다각형 근사 후에도 여러 꼭짓점이 남음. 면적 와 둘레 로 원형도(circularity)를 계산하여 구분함.

완전한 원의 원형도는 1이며 길거나 불규칙한 도형일수록 작아짐. 이 실습에서는 circularity > 0.85를 원으로 분류함.

# 도형별 검출 개수를 기록할 딕셔너리 준비
shape_counts = {
    "triangle": 0,
    "quadrilateral": 0,
    "pentagon": 0,
    "circle": 0,
}

for contour in shape_contours:
    # 각 윤곽선의 면적·둘레·근사 꼭짓점 계산
    area = cv.contourArea(contour)
    perimeter = cv.arcLength(contour, True)
    approximation = cv.approxPolyDP(
        contour,
        0.01 * perimeter,
        True,
    )

    # 면적과 둘레를 이용해 원에 가까운 정도 계산
    circularity = (
        4 * np.pi * area / perimeter**2
    )

    # 원형도를 먼저 확인하고 나머지는 꼭짓점 수로 분류
    if circularity > 0.85:
        label = "circle"
    elif len(approximation) == 3:
        label = "triangle"
    elif len(approximation) == 4:
        label = "quadrilateral"
    elif len(approximation) == 5:
        label = "pentagon"
    else:
        continue

    shape_counts[label] += 1

shape_counts
실행 결과
{'triangle': 2, 'quadrilateral': 7, 'pentagon': 5, 'circle': 3}

원형도를 먼저 확인해야 픽셀 경계가 여러 꼭짓점으로 근사된 원을 다각형으로 잘못 분류하는 문제를 줄일 수 있음.

이 결과에는 오분류가 2개 있음

실제 도형은 삼각형 2개, 사각형 9개, 오각형 3개, 원 3개임. 위 결과에서 오각형으로 분류된 5개 중 2개는 모서리가 살짝 둥근 사각형(파란색 세로 직사각형과 작은 진홍색 사각형)임. 둘레의 1%인 epsilon으로는 한 모서리가 두 점으로 남아 꼭짓점이 5개로 세어짐.

epsilon을 둘레의 2%로 올리면 {'triangle': 2, 'quadrilateral': 9, 'pentagon': 3, 'circle': 3}으로 실제 구성과 일치함. 꼭짓점 수로 분류할 때는 결과를 눈으로 확인하며 epsilon을 조정해야 함.

무게중심 표시

각 윤곽선의 무게중심을 계산.

centroids = []

for contour in shape_contours:
    # 윤곽선 좌표 분포를 나타내는 모멘트 계산
    moments = cv.moments(contour)

    # 면적이 0이 아닐 때 1차 모멘트를 면적으로 나눠 무게중심 계산
    if moments["m00"] != 0:
        center_x = int(
            moments["m10"] / moments["m00"]
        )
        center_y = int(
            moments["m01"] / moments["m00"]
        )
        centroids.append((center_x, center_y))

len(centroids)
실행 결과
17

실습 결과에서 빨간색 T는 삼각형, 초록색 Q는 사각형, 파란색 P는 오각형, 자홍색 C는 원을 의미함. 각 문자 옆의 점이 모멘트로 구한 무게중심임.

다각형 근사와 원형도로 분류한 도형 및 무게중심

도형 분류 기준은 데이터에 맞게 조정

epsilon과 원형도 임계값은 해상도, 잡음, 촬영 각도에 따라 달라짐. 기울어진 사각형은 그대로 꼭짓점 4개를 가질 수 있지만, 원근 왜곡이나 가려짐이 크면 다른 꼭짓점 수로 근사될 수 있음.

볼록 껍질

볼록 껍질(convex hull)은 윤곽선의 모든 점을 포함하는 가장 작은 볼록 다각형임. 두 점을 이은 선분이 항상 도형 내부에 있으면 볼록 도형임.

  • 별과 번개처럼 안쪽으로 들어간 오목한 부분을 바깥쪽 선분으로 연결
  • 원본 윤곽선의 세부 굴곡보다 전체 외곽 형태를 표현
  • 손가락 사이의 오목한 영역이나 물체의 파인 부분을 분석할 때 기준으로 사용
# 볼록 껍질을 확인할 이미지를 밝기 영상으로 변환
convex_image = cv.imread("convex.webp")
convex_gray = cv.cvtColor(
    convex_image,
    cv.COLOR_BGR2GRAY,
)

# 흰 배경은 0, 색 도형은 255가 되도록 반전 이진화
_, convex_binary = cv.threshold(
    convex_gray,
    0,
    255,
    cv.THRESH_BINARY_INV | cv.THRESH_OTSU,
)
# 이진 영상에서 각 도형의 바깥 윤곽선 추출
convex_contours, _ = cv.findContours(
    convex_binary,
    cv.RETR_EXTERNAL,
    cv.CHAIN_APPROX_SIMPLE,
)
hull_result = convex_image.copy()
# 원본 윤곽선은 초록색, 볼록 껍질은 빨간색으로 표시
for contour in convex_contours:
    hull = cv.convexHull(contour)
    cv.drawContours(
        hull_result,
        [contour],
        -1,
        (0, 180, 0),
        2,
    )
    cv.drawContours(
        hull_result,
        [hull],
        -1,
        (0, 0, 255),
        3,
    )
len(convex_contours)
실행 결과
3
to_pil(hull_result)

세 도형의 원본 윤곽선과 볼록 껍질 비교

초록색은 원본 윤곽선, 빨간색은 볼록 껍질임. 오목한 별과 번개의 파인 부분은 빨간 선이 건너뛰며, 오각형에서는 두 선이 거의 겹침.

Previous
윤곽선 추출