logo

윤곽선 추출

윤곽선의 면적·둘레·모멘트 측정

실습 준비

import cv2 as cv
from yeom import to_pil
image = cv.imread("shapes.webp")
gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)

_, binary = cv.threshold(
    gray,
    60,   # 임계값
    255,  # 임계값보다 밝은 픽셀에 넣을 값
    cv.THRESH_BINARY,
)

image.shape
실행 결과
(561, 600, 3)
to_pil(binary)

임계값 60보다 밝은 픽셀은 흰색 처리. 도형 17개와 배경의 작은 밝은 점이 함께 남는다.

도형 원본과 임계값 60으로 만든 잡음이 포함된 이진 이미지

결과 이미지는 contour.ipynb 실행 결과.

윤곽선 추출에는 모폴로지 연산으로 잡음을 정리한 cleaned 이미지를 사용.

kernel = cv.getStructuringElement(
    cv.MORPH_ELLIPSE,
    (3, 3),
)
opened = cv.morphologyEx(
    binary,
    cv.MORPH_OPEN,
    kernel,
)
cleaned = cv.morphologyEx(
    opened,
    cv.MORPH_CLOSE,
    kernel,
)

윤곽선 추출

윤곽선(contour)은 같은 객체의 경계를 따라 연결된 좌표 집합. cv.findContours()는 8비트 단일 채널 이미지에서 0이 아닌 픽셀을 전경으로 보고 윤곽선을 탐색.

contours, hierarchy = cv.findContours(
    cleaned,
    cv.RETR_EXTERNAL,
    cv.CHAIN_APPROX_SIMPLE,
)

len(contours), hierarchy.shape
실행 결과
(17, (1, 17, 4))
  • cv.RETR_EXTERNAL: 가장 바깥쪽 윤곽선만 반환
  • cv.CHAIN_APPROX_SIMPLE: 직선 구간의 중간 좌표를 줄이고 양 끝점 위주로 저장
  • contours[i]: i번째 윤곽선의 좌표 배열, 기본 형태는 (N, 1, 2)
  • hierarchy[0, i]: [다음, 이전, 첫 자식, 부모] 인덱스, 대상이 없으면 -1

cv.RETR_EXTERNAL은 바깥 윤곽선만 반환하므로 이 실습에서는 계층 미사용. 도형 안의 구멍처럼 중첩된 윤곽선까지 필요하면 cv.RETR_TREE로 전체 계층 탐색. OpenCV 4의 cv.findContours()는 입력 이미지를 변경하지 않아 사본 불필요.

모든 윤곽선을 초록색으로 표시. contourIdx=-1은 리스트의 모든 윤곽선을 지정.

contour_image = image.copy()
cv.drawContours(
    contour_image,
    contours,
    -1,         # 모든 윤곽선
    (0, 255, 0),
    2,          # 선 두께
)

to_pil(contour_image)

정리된 이진 마스크와 원본 위에 표시한 17개 외부 윤곽선

면적과 둘레

cv.contourArea()는 윤곽선이 감싸는 면적을 계산. cv.arcLength()는 윤곽선 선분의 길이를 합산해 둘레 계산.

main_contour = max(
    contours,
    key=cv.contourArea,
)

area = cv.contourArea(main_contour)
perimeter = cv.arcLength(
    main_contour,
    True,  # 처음과 끝이 이어진 폐곡선
)

area, perimeter
실행 결과
(10145.5, 454.7422024011612)
  • 면적: 10,145.5픽셀²
  • 둘레: 약 454.7픽셀
  • True: 처음과 끝이 연결된 폐곡선으로 둘레 계산

면적이 가장 큰 윤곽선은 화면 아래쪽의 주황색 사각형. 실제 물체 선택에는 가장 큰 윤곽선 하나 또는 최소 면적을 넘는 여러 윤곽선 사용 가능.

모멘트와 무게중심

이미지 모멘트(image moment)는 픽셀 또는 윤곽선 좌표의 분포를 나타내는 요약값. 아래 수식은 이미지 전체를 대상으로 한 정의. cv.moments()에 윤곽선을 넘기면 픽셀 수 대신 윤곽선 다각형으로 계산.

  • m00: 윤곽선의 면적
  • m10: x좌표를 반영한 1차 모멘트
  • m01: y좌표를 반영한 1차 모멘트

무게중심은 1차 모멘트를 면적으로 나누어 계산.

moments = cv.moments(main_contour)

if moments["m00"] == 0:
    raise ValueError("윤곽선의 면적이 0입니다")

center_x = int(
    moments["m10"] / moments["m00"]
)
center_y = int(
    moments["m01"] / moments["m00"]
)

(center_x, center_y)  # 무게중심 (x, y)
실행 결과
(344, 450)

m00=0이면 나눗셈 불가. 열린 선이나 퇴화 윤곽선에 대비해 먼저 확인.

주요 물체 표시

가장 큰 윤곽선은 초록색, 모멘트로 계산한 무게중심은 빨간색 점으로 표시.

main_result = image.copy()

cv.drawContours(
    main_result,
    [main_contour],
    -1,
    (0, 255, 0),
    4,
)
cv.circle(
    main_result,
    (center_x, center_y),
    7,           # 반지름
    (0, 0, 255), # BGR 순서이므로 빨간색
    -1,          # 음수는 내부를 채움
)

to_pil(main_result)

가장 큰 주황색 사각형의 윤곽선과 무게중심 표시

처리 흐름: 이진화 → 열림·닫힘 → 윤곽선 추출 → 면적 기준 선택 → 모멘트 계산 → 결과 표시. 모폴로지 연산으로 잡음을 먼저 제거하면 작은 점의 오검출 감소.

Previous
모폴로지 연산