윤곽선 검출 (Contour Detection)

윤곽선 검출 (Contour Detection)

개요

윤곽선(Contour)은 동일한 색상이나 밝기를 가진 연속적인 점들의 곡선으로, 객체의 형태를 나타냅니다. 이 레슨에서는 findContours()를 사용한 윤곽선 검출, 계층 구조, 근사화, 면적/둘레 계산 등을 학습합니다.


목차

  1. 윤곽선 기초
  2. findContours() 함수
  3. 윤곽선 계층 구조
  4. 윤곽선 그리기와 근사화
  5. 윤곽선 속성 계산
  6. 객체 카운팅과 분리
  7. 연습 문제

1. 윤곽선 기초

윤곽선이란?

윤곽선 (Contour):
- 동일한 색상/밝기를 가진 연속적인 점들의 곡선
- 객체의 경계를 나타냄
- 이진 이미지에서 추출

원본 이미지           이진화              윤곽선 검출
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  ┌───┐      │     │  ■■■■■      │     │  ┌───┐      │
│  │ ● │      │     │  ■■■■■      │     │  │   │      │
│  └───┘      │ ──▶ │  ■■■■■      │ ──▶ │  └───┘      │
│        ┌──┐ │     │        ■■■ │     │        ┌──┐ │
│        └──┘ │     │        ■■■ │     │        └──┘ │
└─────────────┘     └─────────────┘     └─────────────┘
                         (흰색 영역)        (경계선만)

윤곽선 검출 과정

1. 이미지 읽기
      
      
2. 그레이스케일 변환
      
      
3. 이진화 (threshold)
      
      
4. 윤곽선 검출 (findContours)
      
      
5. 윤곽선 분석/그리기

기본 예제

import cv2
import numpy as np

# 이미지 읽기
img = cv2.imread('image.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 이진화
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# 윤곽선 검출
contours, hierarchy = cv2.findContours(
    binary,
    cv2.RETR_EXTERNAL,      # 외곽 윤곽선만
    cv2.CHAIN_APPROX_SIMPLE  # 압축
)

print(f"검출된 윤곽선 수: {len(contours)}")

# 윤곽선 그리기
result = img.copy()
cv2.drawContours(result, contours, -1, (0, 255, 0), 2)

cv2.imshow('Contours', result)
cv2.waitKey(0)

2. findContours() 함수

함수 시그니처

contours, hierarchy = cv2.findContours(image, mode, method)
파라미터 설명
image 입력 이진 이미지 (8비트 단일 채널)
mode 윤곽선 검색 모드 (RETR_*)
method 윤곽선 근사화 방법 (CHAIN_*)
contours 검출된 윤곽선 리스트
hierarchy 윤곽선 계층 구조

검색 모드 (Retrieval Mode)

┌────────────────────────────────────────────────────────────────────┐
│                         RETR_EXTERNAL                              │
├────────────────────────────────────────────────────────────────────┤
│  가장 바깥쪽 윤곽선만 검출                                          │
│                                                                    │
│  ┌──────────────┐                                                  │
│  │  ┌────────┐  │   → 외곽 사각형만 검출                           │
│  │  │ ┌────┐ │  │                                                  │
│  │  │ └────┘ │  │                                                  │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                           RETR_LIST                                │
├────────────────────────────────────────────────────────────────────┤
│  모든 윤곽선 검출, 계층 구조 없음 (동일 레벨)                       │
│                                                                    │
│  ┌──────────────┐                                                  │
│  │  ┌────────┐  │   → 3개 모두 검출, 부모-자식 관계 없음           │
│  │  │ ┌────┐ │  │                                                  │
│  │  │ └────┘ │  │                                                  │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                           RETR_CCOMP                               │
├────────────────────────────────────────────────────────────────────┤
│  2레벨 계층 구조                                                   │
│  - 레벨 1: 외곽 윤곽선                                             │
│  - 레벨 2: 구멍 (내부 윤곽선)                                      │
│                                                                    │
│  ┌──────────────┐   레벨 1 (외곽)                                  │
│  │  ┌────────┐  │   레벨 2 (구멍)                                  │
│  │  │ ■■■■■■ │  │   (내부의 흰색 영역은 레벨 2)                    │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                           RETR_TREE                                │
├────────────────────────────────────────────────────────────────────┤
│  완전한 계층 구조 (부모-자식 관계)                                  │
│                                                                    │
│  ┌──────────────┐   레벨 0 (최외곽)                                │
│  │  ┌────────┐  │   레벨 1                                         │
│  │  │ ┌────┐ │  │   레벨 2                                         │
│  │  │ │ ■■ │ │  │   레벨 3                                         │
│  │  │ └────┘ │  │                                                  │
│  │  └────────┘  │                                                  │
│  └──────────────┘                                                  │
└────────────────────────────────────────────────────────────────────┘

근사화 방법 (Approximation Method)

┌────────────────────────────────────────────────────────────────────┐
│                      CHAIN_APPROX_NONE                             │
├────────────────────────────────────────────────────────────────────┤
│  모든 윤곽선 점 저장                                               │
│                                                                    │
│      • • • • • •                                                   │
│    •           •    → 모든 경계 픽셀 저장                          │
│    •           •       메모리 많이 사용                            │
│    •           •                                                   │
│      • • • • • •                                                   │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                     CHAIN_APPROX_SIMPLE                            │
├────────────────────────────────────────────────────────────────────┤
│  직선 부분은 끝점만 저장 (압축)                                    │
│                                                                    │
│      •         •                                                   │
│                      → 4개 꼭짓점만 저장                           │
│                         메모리 효율적                              │
│                                                                    │
│      •         •                                                   │
└────────────────────────────────────────────────────────────────────┘

┌────────────────────────────────────────────────────────────────────┐
│                    CHAIN_APPROX_TC89_L1                            │
│                    CHAIN_APPROX_TC89_KCOS                          │
├────────────────────────────────────────────────────────────────────┤
│  Teh-Chin 체인 근사화 알고리즘                                     │
│  → 더 공격적인 압축                                                │
└────────────────────────────────────────────────────────────────────┘

모드별 예제

import cv2
import numpy as np

def compare_retrieval_modes(image_path):
    """윤곽선 검색 모드 비교"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    modes = [
        (cv2.RETR_EXTERNAL, 'RETR_EXTERNAL'),
        (cv2.RETR_LIST, 'RETR_LIST'),
        (cv2.RETR_CCOMP, 'RETR_CCOMP'),
        (cv2.RETR_TREE, 'RETR_TREE')
    ]

    for mode, name in modes:
        contours, hierarchy = cv2.findContours(
            binary.copy(),
            mode,
            cv2.CHAIN_APPROX_SIMPLE
        )

        result = img.copy()
        cv2.drawContours(result, contours, -1, (0, 255, 0), 2)

        print(f"{name}: {len(contours)} contours")
        cv2.imshow(name, result)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

compare_retrieval_modes('nested_shapes.jpg')

3. 윤곽선 계층 구조

hierarchy 구조

hierarchy[i] = [Next, Previous, First_Child, Parent]

Next:        같은 레벨의 다음 윤곽선 인덱스 (-1: 없음)
Previous:    같은 레벨의 이전 윤곽선 인덱스 (-1: 없음)
First_Child:  번째 자식 윤곽선 인덱스 (-1: 없음)
Parent:      부모 윤곽선 인덱스 (-1: 없음)

예시:
┌───────────────────────────────────┐
 ┌─────────────┐ ┌─────────────┐  
    ┌───┐                     
     A             B        
    └───┘                     
       C                      
 └─────────────┘ └─────────────┘  
                  D                
└───────────────────────────────────┘

RETR_TREE 결과:
Index 0 (D): Next=-1, Prev=-1, Child=1, Parent=-1  (최외곽)
Index 1 (C): Next=2,  Prev=-1, Child=3, Parent=0
Index 2 (B): Next=-1, Prev=1,  Child=-1, Parent=0
Index 3 (A): Next=-1, Prev=-1, Child=-1, Parent=1

계층 구조 탐색

import cv2
import numpy as np

def analyze_hierarchy(image_path):
    """윤곽선 계층 구조 분석"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    contours, hierarchy = cv2.findContours(
        binary,
        cv2.RETR_TREE,
        cv2.CHAIN_APPROX_SIMPLE
    )

    if hierarchy is None:
        print("윤곽선이 없습니다.")
        return

    hierarchy = hierarchy[0]  # (1, N, 4) -> (N, 4)

    print("계층 구조 분석:")
    print("-" * 50)

    for i, h in enumerate(hierarchy):
        next_c, prev_c, first_child, parent = h

        # 레벨 계산
        level = 0
        p = parent
        while p != -1:
            level += 1
            p = hierarchy[p][3]  # 부모의 부모

        indent = "  " * level
        print(f"{indent}윤곽선 {i}:")
        print(f"{indent}  레벨: {level}")
        print(f"{indent}  부모: {parent}")
        print(f"{indent}  자식: {first_child}")
        print(f"{indent}  면적: {cv2.contourArea(contours[i]):.0f}")

analyze_hierarchy('nested_shapes.jpg')

특정 레벨의 윤곽선만 추출

import cv2
import numpy as np

def get_contours_at_level(contours, hierarchy, level):
    """특정 레벨의 윤곽선만 반환"""
    if hierarchy is None:
        return []

    hierarchy = hierarchy[0]
    result = []

    for i in range(len(contours)):
        # 현재 윤곽선의 레벨 계산
        current_level = 0
        parent = hierarchy[i][3]
        while parent != -1:
            current_level += 1
            parent = hierarchy[parent][3]

        if current_level == level:
            result.append(contours[i])

    return result

def get_outer_contours(contours, hierarchy):
    """최외곽 윤곽선만 반환 (부모가 없는 것)"""
    if hierarchy is None:
        return []

    hierarchy = hierarchy[0]
    result = []

    for i in range(len(contours)):
        if hierarchy[i][3] == -1:  # 부모가 없음
            result.append(contours[i])

    return result

def get_inner_contours(contours, hierarchy, parent_idx):
    """특정 윤곽선의 자식(내부) 윤곽선 반환"""
    if hierarchy is None:
        return []

    hierarchy = hierarchy[0]
    result = []

    # 첫 번째 자식
    child = hierarchy[parent_idx][2]

    while child != -1:
        result.append(contours[child])
        child = hierarchy[child][0]  # 다음 형제

    return result

# 사용 예
img = cv2.imread('nested.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

contours, hierarchy = cv2.findContours(
    binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE
)

# 레벨 0 윤곽선
level0 = get_contours_at_level(contours, hierarchy, 0)

# 최외곽 윤곽선
outer = get_outer_contours(contours, hierarchy)

result = img.copy()
cv2.drawContours(result, outer, -1, (0, 255, 0), 2)
cv2.imshow('Outer Contours', result)
cv2.waitKey(0)

4. 윤곽선 그리기와 근사화

cv2.drawContours() 함수

cv2.drawContours(image, contours, contourIdx, color, thickness)
파라미터 설명
image 그릴 이미지
contours 윤곽선 리스트
contourIdx 그릴 윤곽선 인덱스 (-1: 모두)
color 색상 (B, G, R)
thickness 선 두께 (-1: 채우기)
import cv2
import numpy as np

def draw_contours_examples(image, contours):
    """다양한 방식으로 윤곽선 그리기"""

    # 모든 윤곽선 그리기
    result1 = image.copy()
    cv2.drawContours(result1, contours, -1, (0, 255, 0), 2)

    # 특정 윤곽선만 그리기
    result2 = image.copy()
    if len(contours) > 0:
        cv2.drawContours(result2, contours, 0, (255, 0, 0), 3)

    # 윤곽선 채우기
    result3 = image.copy()
    cv2.drawContours(result3, contours, -1, (0, 0, 255), -1)

    # 각 윤곽선 다른 색으로
    result4 = image.copy()
    for i, contour in enumerate(contours):
        color = tuple(np.random.randint(0, 255, 3).tolist())
        cv2.drawContours(result4, [contour], 0, color, 2)

    return result1, result2, result3, result4

cv2.approxPolyDP() - 다각형 근사화

Douglas-Peucker 알고리즘:
윤곽선을 더 적은 점으로 근사화

epsilon (정밀도):
- 작을수록: 원본에 가까움 (점 많음)
- 클수록: 단순화 (점 적음)

예시:
원본 (많은 점)          epsilon=0.01         epsilon=0.05
      •  •  •                 •                     •
   •        •              •     •                •   •
  •          •            •       •              •     •
  •          •             •     •                  •
   •        •               •   •
      •  •  •                 •                     •
import cv2
import numpy as np

def approximate_contour(contour, epsilon_ratio=0.02):
    """
    윤곽선 다각형 근사화
    epsilon_ratio: 둘레 대비 허용 오차 비율
    """
    # 윤곽선 둘레 계산
    perimeter = cv2.arcLength(contour, True)

    # epsilon = 둘레 * 비율
    epsilon = epsilon_ratio * perimeter

    # 근사화
    approx = cv2.approxPolyDP(contour, epsilon, True)

    return approx

def compare_approximations(image, contour):
    """다양한 epsilon으로 근사화 비교"""
    epsilons = [0.001, 0.01, 0.02, 0.05, 0.1]

    for eps in epsilons:
        result = image.copy()
        approx = approximate_contour(contour, eps)

        cv2.drawContours(result, [approx], 0, (0, 255, 0), 2)

        # 꼭짓점 표시
        for point in approx:
            x, y = point[0]
            cv2.circle(result, (x, y), 5, (0, 0, 255), -1)

        cv2.putText(result, f'epsilon={eps}, points={len(approx)}',
                    (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)

        cv2.imshow(f'Approximation {eps}', result)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

# 사용 예
img = cv2.imread('shape.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

if contours:
    compare_approximations(img, contours[0])

도형 식별 (꼭짓점 수로)

import cv2
import numpy as np

def identify_shape(contour):
    """꼭짓점 수로 도형 식별"""
    perimeter = cv2.arcLength(contour, True)
    approx = cv2.approxPolyDP(contour, 0.04 * perimeter, True)
    vertices = len(approx)

    if vertices == 3:
        return "Triangle"
    elif vertices == 4:
        # 정사각형 vs 직사각형 구분
        x, y, w, h = cv2.boundingRect(approx)
        aspect_ratio = w / float(h)
        if 0.95 <= aspect_ratio <= 1.05:
            return "Square"
        else:
            return "Rectangle"
    elif vertices == 5:
        return "Pentagon"
    elif vertices == 6:
        return "Hexagon"
    elif vertices > 6:
        # 원형 여부 확인
        area = cv2.contourArea(contour)
        circularity = 4 * np.pi * area / (perimeter ** 2)
        if circularity > 0.8:
            return "Circle"
        else:
            return f"Polygon ({vertices} vertices)"
    else:
        return "Unknown"

def label_shapes(image_path):
    """이미지의 모든 도형 식별 및 라벨링"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)

    contours, _ = cv2.findContours(
        binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    result = img.copy()

    for contour in contours:
        # 너무 작은 윤곽선 무시
        if cv2.contourArea(contour) < 100:
            continue

        # 도형 식별
        shape = identify_shape(contour)

        # 무게중심 계산
        M = cv2.moments(contour)
        if M["m00"] != 0:
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])
        else:
            cx, cy = 0, 0

        # 윤곽선 그리기
        cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)

        # 라벨 표시
        cv2.putText(result, shape, (cx - 40, cy),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)

    cv2.imshow('Shapes', result)
    cv2.waitKey(0)

label_shapes('shapes.jpg')

5. 윤곽선 속성 계산

둘레와 면적

import cv2
import numpy as np

def contour_properties(contour):
    """윤곽선의 기본 속성 계산"""

    # 면적
    area = cv2.contourArea(contour)

    # 둘레 (closed=True: 닫힌 곡선)
    perimeter = cv2.arcLength(contour, True)

    # 경계 사각형
    x, y, w, h = cv2.boundingRect(contour)
    bounding_area = w * h

    # 면적 비율 (Extent)
    extent = area / bounding_area if bounding_area > 0 else 0

    # 원형도 (Circularity)
    circularity = 4 * np.pi * area / (perimeter ** 2) if perimeter > 0 else 0

    # 컨벡스 헐
    hull = cv2.convexHull(contour)
    hull_area = cv2.contourArea(hull)

    # 솔리디티 (Solidity)
    solidity = area / hull_area if hull_area > 0 else 0

    return {
        'area': area,
        'perimeter': perimeter,
        'extent': extent,
        'circularity': circularity,
        'solidity': solidity
    }

# 사용 예
img = cv2.imread('shape.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

for i, contour in enumerate(contours):
    props = contour_properties(contour)
    print(f"윤곽선 {i}:")
    print(f"  면적: {props['area']:.0f}")
    print(f"  둘레: {props['perimeter']:.1f}")
    print(f"  Extent: {props['extent']:.2f}")
    print(f"  원형도: {props['circularity']:.2f}")
    print(f"  Solidity: {props['solidity']:.2f}")

경계 도형

import cv2
import numpy as np

def bounding_shapes(image, contour):
    """윤곽선의 다양한 경계 도형"""
    result = image.copy()

    # 1. 경계 사각형 (Bounding Rectangle)
    x, y, w, h = cv2.boundingRect(contour)
    cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 0), 2)

    # 2. 회전된 경계 사각형 (Rotated Rectangle)
    rect = cv2.minAreaRect(contour)
    box = cv2.boxPoints(rect)
    box = np.int_(box)
    cv2.drawContours(result, [box], 0, (255, 0, 0), 2)

    # 3. 최소 외접원 (Minimum Enclosing Circle)
    (cx, cy), radius = cv2.minEnclosingCircle(contour)
    cv2.circle(result, (int(cx), int(cy)), int(radius), (0, 0, 255), 2)

    # 4. 타원 피팅 (Fitting Ellipse)
    if len(contour) >= 5:  # 최소 5개 점 필요
        ellipse = cv2.fitEllipse(contour)
        cv2.ellipse(result, ellipse, (255, 255, 0), 2)

    # 5. 직선 피팅 (Fitting Line)
    rows, cols = image.shape[:2]
    [vx, vy, x, y] = cv2.fitLine(contour, cv2.DIST_L2, 0, 0.01, 0.01)
    lefty = int((-x * vy / vx) + y)
    righty = int(((cols - x) * vy / vx) + y)
    cv2.line(result, (cols-1, righty), (0, lefty), (0, 255, 255), 2)

    return result

# 사용 예
img = cv2.imread('shape.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

if contours:
    result = bounding_shapes(img, contours[0])
    cv2.imshow('Bounding Shapes', result)
    cv2.waitKey(0)

컨벡스 헐

컨벡스 헐 (Convex Hull):
점 집합을 감싸는 가장 작은 볼록 다각형

      •  •
    •      •          ┌──────────┐
  •          •   →   │          │
    •  •   •         │          │
        • •          └──────────┘
   원본 윤곽선          컨벡스 헐

컨벡시티 결함 (Convexity Defects):
윤곽선과 컨벡스 헐 사이의 가장 깊은 점
→ 손가락 검출 등에 사용
import cv2
import numpy as np

def convex_hull_analysis(image, contour):
    """컨벡스 헐 분석"""
    result = image.copy()

    # 컨벡스 헐 계산
    hull = cv2.convexHull(contour)

    # 원본 윤곽선
    cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)

    # 컨벡스 헐
    cv2.drawContours(result, [hull], 0, (0, 0, 255), 2)

    # 컨벡시티 결함 (손가락 검출 등에 유용)
    hull_indices = cv2.convexHull(contour, returnPoints=False)
    if len(hull_indices) > 3 and len(contour) > 3:
        defects = cv2.convexityDefects(contour, hull_indices)

        if defects is not None:
            for i in range(defects.shape[0]):
                s, e, f, d = defects[i, 0]
                start = tuple(contour[s][0])
                end = tuple(contour[e][0])
                far = tuple(contour[f][0])

                # 결함 깊이가 일정 이상인 경우만 표시
                if d / 256 > 10:  # 깊이 임계값
                    cv2.circle(result, far, 5, (255, 0, 255), -1)
                    cv2.line(result, start, far, (255, 0, 255), 1)
                    cv2.line(result, far, end, (255, 0, 255), 1)

    return result

# 사용 예 (손 이미지)
img = cv2.imread('hand.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

if contours:
    # 가장 큰 윤곽선 선택
    largest = max(contours, key=cv2.contourArea)
    result = convex_hull_analysis(img, largest)
    cv2.imshow('Convex Hull', result)
    cv2.waitKey(0)

6. 객체 카운팅과 분리

객체 카운팅

import cv2
import numpy as np

def count_objects(image_path, min_area=100):
    """이미지에서 객체 수 세기"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 적응형 이진화
    binary = cv2.adaptiveThreshold(
        gray, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY_INV,
        11, 2
    )

    # 모폴로지 연산으로 노이즈 제거
    kernel = np.ones((3, 3), np.uint8)
    binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
    binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)

    # 윤곽선 검출
    contours, _ = cv2.findContours(
        binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    # 크기 필터링
    valid_contours = [c for c in contours if cv2.contourArea(c) >= min_area]

    result = img.copy()
    for i, contour in enumerate(valid_contours):
        # 무게중심
        M = cv2.moments(contour)
        if M["m00"] != 0:
            cx = int(M["m10"] / M["m00"])
            cy = int(M["m01"] / M["m00"])

            # 번호 표시
            cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)
            cv2.putText(result, str(i + 1), (cx - 10, cy + 5),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)

    print(f"검출된 객체 수: {len(valid_contours)}")

    cv2.imshow('Counted Objects', result)
    cv2.waitKey(0)

    return len(valid_contours)

# 동전 세기 예제
count_objects('coins.jpg', min_area=500)

객체 분리 및 추출

import cv2
import numpy as np

def extract_objects(image_path, output_dir='objects/'):
    """개별 객체를 분리하여 저장"""
    import os
    os.makedirs(output_dir, exist_ok=True)

    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)

    contours, _ = cv2.findContours(
        binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    objects = []
    for i, contour in enumerate(contours):
        # 너무 작은 객체 무시
        if cv2.contourArea(contour) < 100:
            continue

        # 경계 사각형
        x, y, w, h = cv2.boundingRect(contour)

        # 여백 추가
        padding = 10
        x1 = max(0, x - padding)
        y1 = max(0, y - padding)
        x2 = min(img.shape[1], x + w + padding)
        y2 = min(img.shape[0], y + h + padding)

        # 객체 영역 추출
        roi = img[y1:y2, x1:x2].copy()
        objects.append(roi)

        # 저장
        cv2.imwrite(f'{output_dir}object_{i:03d}.jpg', roi)

    print(f"{len(objects)}개 객체 추출 완료")
    return objects

# 사용 예
objects = extract_objects('multiple_objects.jpg')

특정 모양만 검출

import cv2
import numpy as np

def find_circles(image_path):
    """원형 객체만 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    contours, _ = cv2.findContours(
        binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    result = img.copy()
    circles = []

    for contour in contours:
        area = cv2.contourArea(contour)
        perimeter = cv2.arcLength(contour, True)

        if perimeter == 0:
            continue

        # 원형도 계산
        circularity = 4 * np.pi * area / (perimeter ** 2)

        # 원형도가 0.8 이상이면 원으로 판단
        if circularity > 0.8 and area > 100:
            circles.append(contour)
            cv2.drawContours(result, [contour], 0, (0, 255, 0), 2)

            # 중심점 표시
            M = cv2.moments(contour)
            if M["m00"] != 0:
                cx = int(M["m10"] / M["m00"])
                cy = int(M["m01"] / M["m00"])
                cv2.circle(result, (cx, cy), 5, (0, 0, 255), -1)

    print(f"원형 객체 수: {len(circles)}")
    cv2.imshow('Circles', result)
    cv2.waitKey(0)

    return circles

def find_rectangles(image_path):
    """사각형 객체만 검출"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    contours, _ = cv2.findContours(
        binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    result = img.copy()
    rectangles = []

    for contour in contours:
        area = cv2.contourArea(contour)
        if area < 100:
            continue

        # 다각형 근사화
        peri = cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, 0.04 * peri, True)

        # 꼭짓점이 4개면 사각형
        if len(approx) == 4:
            rectangles.append(contour)
            cv2.drawContours(result, [approx], 0, (0, 255, 0), 2)

    print(f"사각형 객체 수: {len(rectangles)}")
    cv2.imshow('Rectangles', result)
    cv2.waitKey(0)

    return rectangles

# 사용 예
find_circles('shapes.jpg')
find_rectangles('shapes.jpg')

7. 연습 문제

문제 1: 동전 카운터

동전 이미지에서 동전 수를 세고 총 금액을 계산하세요 (크기로 구분).

힌트 동전 크기(면적 또는 반지름)를 기준으로 동전 종류를 분류합니다.
정답 코드
import cv2
import numpy as np

def count_coins_by_size(image_path):
    """동전 크기별로 분류하고 개수 세기"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (11, 11), 0)

    # Canny 엣지 검출 + 닫힘 연산
    edges = cv2.Canny(blurred, 30, 150)
    kernel = np.ones((3, 3), np.uint8)
    edges = cv2.dilate(edges, kernel, iterations=2)
    edges = cv2.erode(edges, kernel, iterations=1)

    # 윤곽선 검출
    contours, _ = cv2.findContours(
        edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    result = img.copy()

    # 크기별 분류 (반지름 기준)
    small_coins = []   # 10원
    medium_coins = []  # 50원
    large_coins = []   # 100원

    for contour in contours:
        area = cv2.contourArea(contour)
        if area < 500:  # 노이즈 무시
            continue

        # 최소 외접원
        (x, y), radius = cv2.minEnclosingCircle(contour)

        # 원형도 확인
        perimeter = cv2.arcLength(contour, True)
        if perimeter > 0:
            circularity = 4 * np.pi * area / (perimeter ** 2)
            if circularity < 0.7:  # 원형 아님
                continue

        # 크기별 분류 (예시 임계값)
        if radius < 30:
            small_coins.append((int(x), int(y), int(radius)))
            color = (255, 0, 0)  # 파랑 - 10원
        elif radius < 40:
            medium_coins.append((int(x), int(y), int(radius)))
            color = (0, 255, 0)  # 녹색 - 50원
        else:
            large_coins.append((int(x), int(y), int(radius)))
            color = (0, 0, 255)  # 빨강 - 100원

        cv2.circle(result, (int(x), int(y)), int(radius), color, 2)

    # 결과 출력
    total = (len(small_coins) * 10 +
             len(medium_coins) * 50 +
             len(large_coins) * 100)

    print(f"10원: {len(small_coins)}개")
    print(f"50원: {len(medium_coins)}개")
    print(f"100원: {len(large_coins)}개")
    print(f"총액: {total}원")

    cv2.imshow('Coins', result)
    cv2.waitKey(0)

count_coins_by_size('coins.jpg')

문제 2: 문서 사각형 검출

이미지에서 문서(종이)의 윤곽선을 찾고 4개의 꼭짓점을 반환하세요.

힌트 가장 큰 4각형 윤곽선을 찾습니다. approxPolyDP로 4개 점으로 근사화합니다.
정답 코드
import cv2
import numpy as np

def find_document(image_path):
    """문서 영역의 4개 꼭짓점 찾기"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)

    # 엣지 검출
    edges = cv2.Canny(blurred, 50, 150)

    # 윤곽선 검출
    contours, _ = cv2.findContours(
        edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )

    # 면적순 정렬
    contours = sorted(contours, key=cv2.contourArea, reverse=True)

    document_corners = None

    for contour in contours[:5]:  # 상위 5개만 확인
        peri = cv2.arcLength(contour, True)
        approx = cv2.approxPolyDP(contour, 0.02 * peri, True)

        # 4개 꼭짓점이면 문서
        if len(approx) == 4:
            document_corners = approx
            break

    if document_corners is not None:
        result = img.copy()
        cv2.drawContours(result, [document_corners], 0, (0, 255, 0), 3)

        # 꼭짓점 표시
        for point in document_corners:
            x, y = point[0]
            cv2.circle(result, (x, y), 10, (0, 0, 255), -1)

        cv2.imshow('Document', result)
        cv2.waitKey(0)

        return document_corners.reshape(4, 2)
    else:
        print("문서를 찾지 못했습니다.")
        return None

corners = find_document('document.jpg')
if corners is not None:
    print("문서 꼭짓점:", corners)

문제 3: 빈 공간 검출

이진 이미지에서 구멍(빈 공간)의 수를 세세요.

힌트 RETR_CCOMP 또는 RETR_TREE를 사용하여 내부 윤곽선(구멍)을 찾습니다.
정답 코드
import cv2
import numpy as np

def count_holes(image_path):
    """객체 내부의 구멍 수 세기"""
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

    # RETR_CCOMP: 2레벨 계층 (외곽 + 구멍)
    contours, hierarchy = cv2.findContours(
        binary, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
    )

    if hierarchy is None:
        return 0

    hierarchy = hierarchy[0]

    result = img.copy()
    holes = []

    for i, h in enumerate(hierarchy):
        # 부모가 있는 윤곽선 = 구멍
        if h[3] != -1:  # 부모가 있음
            area = cv2.contourArea(contours[i])
            if area > 50:  # 노이즈 무시
                holes.append(contours[i])
                cv2.drawContours(result, [contours[i]], 0, (0, 0, 255), 2)

    print(f"구멍 수: {len(holes)}")

    cv2.imshow('Holes', result)
    cv2.waitKey(0)

    return len(holes)

count_holes('donut.jpg')

추천 문제

난이도 주제 설명
기본 검출 findContours로 객체 개수 세기
⭐⭐ 면적 필터 특정 크기 범위의 객체만 검출
⭐⭐ 도형 분류 삼각형, 사각형, 원 구분
⭐⭐⭐ 문서 스캐너 문서 검출 후 투시 변환
⭐⭐⭐ 손가락 카운터 컨벡시티 결함으로 손가락 세기

다음 단계


참고 자료

to navigate between lessons