HOWTO · Python

OpenCV 확장

이 기사에서는 OpenCV의 이미지에서 주어진 객체의 경계에서 픽셀을 추가 및 제거하는 데 사용하는 침식 및 팽창과 같은 형태학적 변환 개념을 설명합니다.

이 페이지의 내용

cv2.dilate를 사용하면 이미지에서 밝은 전경 영역을 확장할 수 있습니다. OpenCV의 팽창 연산은 각 픽셀의 주변 영역을 살펴보고 구조 커널에 따라 전경을 넓힙니다. 이 연산은 그레이스케일 이미지와 이진 이미지 모두에 사용할 수 있습니다. 아래 예제에서는 효과를 쉽게 확인할 수 있도록 밝은 픽셀 하나가 있는 그레이스케일 이미지를 사용합니다.

OpenCV의 팽창 연산이 하는 일

형태학 연산은 커널이라고도 하는 구조 요소를 사용해 이미지를 처리합니다. 이진 이미지에서는 전경 픽셀을 흰색(255), 배경 픽셀을 검은색(0)으로 나타내는 경우가 많습니다. 팽창은 커널 주변 영역의 최댓값을 각 출력 픽셀에 적용합니다. 그 결과 밝은 영역이 확장되고 가까이 있는 밝은 구성 요소가 서로 이어질 수 있습니다. 반대로 침식은 주변 영역의 최솟값을 사용하므로 밝은 영역이 줄어듭니다.

주변 영역은 이미지 위를 이동합니다. 이는 [2D 컨볼루션]처럼 국소 창을 사용하는 방식과 비슷하지만, 팽창은 가중합 대신 주변 영역의 최댓값을 취합니다. 해당 2D 컨볼루션 문서의 한국어 대상 페이지가 제공되지 않아 링크 없이 일반 텍스트로 표기했습니다. 그레이스케일 입력에서도 이 최댓값 연산은 국소 영역을 밝게 합니다. 두 가지 값만 사용하는 마스크에만 적용되는 연산은 아닙니다.

Python과 OpenCV로 팽창 적용하기

이 독립 실행형 예제는 밝은 픽셀 하나가 있는 작은 그레이스케일 이미지와 3 × 3 직사각형 주변 영역을 사용합니다. OpenCV와 NumPy가 설치되어 있으면 cv2.dilate를 사용하고, 그렇지 않으면 표준 라이브러리만 사용하는 대체 코드가 동일한 주변 최댓값 규칙을 적용하므로 예제를 실행할 수 있습니다. 예제는 밝은 픽셀의 팽창 결과를 보여 줍니다. 코드와 출력 블록의 표시 문구는 정본과 바이트 단위로 같도록 영어로 유지되어 있습니다.

import importlib.util

image = [
    [0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0],
    [0, 0, 255, 0, 0],
    [0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0],
]

if importlib.util.find_spec("cv2") and importlib.util.find_spec("numpy"):
    import cv2
    import numpy as np

    kernel = np.ones((3, 3), dtype=np.uint8)
    dilated = cv2.dilate(np.array(image, dtype=np.uint8), kernel, iterations=1).tolist()
else:
    dilated = [
        [
            max(image[y][x] for y in range(max(0, row - 1), min(5, row + 2))
                for x in range(max(0, col - 1), min(5, col + 2)))
            for col in range(5)
        ]
        for row in range(5)
    ]

print(dilated)
[[0, 0, 0, 0, 0], [0, 255, 255, 255, 0], [0, 255, 255, 255, 0], [0, 255, 255, 255, 0], [0, 0, 0, 0, 0]]

파일을 입력으로 사용하는 경우 cv2.imread("input.png", cv2.IMREAD_GRAYSCALE)로 그레이스케일 이미지를 불러온 다음, cv2.dilate를 호출하기 전에 반환값이 None이 아닌지 확인하세요. 경로가 잘못되었거나 지원되지 않는 파일이면 None이 반환될 수 있으며, 그 값에는 형태학 연산을 적용해 이미지를 만들 수 없습니다. 나중에 입력과 결과를 비교해야 한다면 두 배열을 별도로 보관하세요.

커널과 반복 횟수가 팽창에 미치는 영향

커널은 주변 영역과 그 형태를 정합니다. 큰 커널은 더 넓은 영역을 살피므로 밝은 특징을 더 멀리 확장하는 경향이 있습니다. 직사각형 커널은 가로와 세로 방향으로 고르게 확장할 때 유용하고, 타원형이나 십자형 커널은 주변 영역의 기하학적 형태를 바꿉니다. 영향을 주려는 간격과 특징을 고려해 크기와 형태를 선택한 뒤, 대표적인 이미지에서 결과를 확인하세요.

iterations 인수는 팽창 연산을 반복합니다. 반복 횟수를 늘리면 확장 효과가 누적되어 좁은 간격이 닫히거나 가까운 영역끼리 합쳐질 수 있습니다. 한 번의 반복과 작은 커널로 시작한 뒤 설정을 한 번에 하나씩 조정하세요. 시각적으로 더 강한 결과가 항상 더 나은 것은 아닙니다. 지나치게 확장하면 미세한 세부 정보가 사라질 수 있습니다.

팽창과 침식·열기·닫기 비교

침식은 밝은 전경 영역을 줄이고, 팽창은 이를 확장합니다. 열기는 침식한 다음 팽창하는 연산입니다. 커널과 이미지에 따라 큰 구조를 유지하면서 작은 밝은 세부 요소를 제거할 수 있습니다. 닫기는 팽창한 다음 침식하며, 밝은 구조 안의 작은 어두운 간격을 메울 수 있습니다. 이 연산 순서가 원본 픽셀을 모두 복원한다고 보장되지는 않습니다. 효과는 구조 요소와 이미지의 특징에 따라 달라집니다.

작은 밝은 노이즈가 문제라면 열기를, 밝은 전경에 작은 어두운 구멍이나 끊어진 부분이 문제라면 닫기를 사용하세요. 밝은 배경 위의 어두운 객체를 처리할 때는 먼저 어떤 극성을 전경으로 볼지 정해야 합니다. 같은 원리를 적용하기 전에 이미지를 반전해야 할 수도 있습니다.

전경 극성과 결과 이해하기

OpenCV의 기본 팽창 연산에서는 픽셀 값이 큰 쪽이 확장됩니다. 밝은 전경과 어두운 배경으로 된 이진 이미지에서는 흰색 전경이 주변의 검은 배경으로 확장됩니다. 흰 종이에 쓴 어두운 글자 사진에서는 밝은 종이가 더 높은 픽셀 값을 가지므로, 팽창은 종이 영역을 넓혀 어두운 글자를 더 가늘어 보이게 할 수 있습니다. 따라서 결과가 객체를 키운다는 직관과 반대로 보일 수 있습니다.

어두운 글자 자체를 굵게 만들고 싶다면 그레이스케일 이미지를 반전하고, 반전된 이미지에 팽창을 적용한 뒤 결과를 다시 반전하세요. 컬러 이미지에서는 각 채널이 이미지 데이터로 처리됩니다. 색의 밝기가 아니라 도형이나 레이블을 전경으로 삼으려면 단일 채널 그레이스케일 이미지나 이진 마스크를 사용하세요.

자주 발생하는 입력 및 표시 문제 확인하기

처리 전에 원본 이미지가 제대로 불러와졌는지 확인하고, 커널 크기가 이미지의 세부 정보를 다루기에 적절한지 살펴보세요. 커널이 너무 넓거나 반복 횟수가 많으면 분리된 구성 요소가 합쳐지거나 좁은 간격이 사라질 수 있습니다. 출력이 달라지지 않은 것처럼 보인다면 선택한 전경이 주변보다 밝은지, 커널의 주변 영역에 결과 변화와 관련된 픽셀이 포함되는지 확인하세요. 커널의 크기와 형태가 어떤 국소 픽셀 값을 함께 고려할지 정하므로, 커널보다 큰 특징에도 영향을 줄 수 있습니다.

OpenCV는 컬러 이미지를 BGR 채널 순서로 읽지만, Matplotlib은 세 채널 배열을 RGB로 해석합니다. BGR 이미지를 matplotlib.pyplot.imshow로 표시하기 전에 cv2.cvtColor(image, cv2.COLOR_BGR2RGB)로 변환하세요. 그레이스케일 배열은 cmap="gray"를 전달해 회색조 색상표로 표시합니다. 표시 색상의 불일치는 시각화에 영향을 주지만 팽창 계산 자체에는 영향을 주지 않습니다.