OpenCV 비전 실무9장 · Morphology: 침식, 팽창, 열기, 닫기14 / 21← 이전목차다음 →MetaContents

9장. Morphology: 침식, 팽창, 열기, 닫기

실습 1 — 구조 요소와 손계산 확인하기

구조 요소 세 가지를 만들어 숫자로 보고, 본문의 8×8 그림에 침식과 팽창을 NumPy 로 직접 계산해 cv2.erode, cv2.dilate 의 결과와 견줍니다. 파일은 쓰지 않습니다.

1단계. 구조 요소 세 가지 만들기

Python
import cv2
import numpy as np

# getStructuringElement = get structuring element(구조 요소 얻기)
shapes = {"RECT(네모)": cv2.MORPH_RECT,
          "CROSS(십자)": cv2.MORPH_CROSS,
          "ELLIPSE(타원)": cv2.MORPH_ELLIPSE}
for name, shape in shapes.items():
    se = cv2.getStructuringElement(shape, (5, 5))   # 5×5 도장
    # 결과: 1 과 0 으로 된 5×5 표와, 1 인 칸의 개수
    print(f"[{name}] 1 인 칸 {int(se.sum())}개")
    print(se)
출력
[RECT(네모)] 1 인 칸 25개
[[1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]]
[CROSS(십자)] 1 인 칸 9개
[[0 0 1 0 0]
 [0 0 1 0 0]
 [1 1 1 1 1]
 [0 0 1 0 0]
 [0 0 1 0 0]]
[ELLIPSE(타원)] 1 인 칸 17개
[[0 0 1 0 0]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [1 1 1 1 1]
 [0 0 1 0 0]]

2단계. 침식을 NumPy 로 직접 계산하기

Python
# 본문의 8×8 그림: 4×4 흰 네모, 튀어나온 한 칸, 떨어진 점
img = np.zeros((8, 8), np.uint8)
img[1:5, 1:5] = 255
img[3, 5] = 255
img[6, 6] = 255
se = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

def my_erode(img, se):
    """도장의 1 자리가 모두 흰 칸이면 흰색(255)을 쓴다."""
    r = se.shape[0] // 2                              # 도장의 반지름: 3×3 이면 1
    # 그림 둘레를 흰색으로 r 칸 덧댄다. OpenCV 의 침식이 그림 밖을 흰색으로 치는 것과 같게
    pad = np.pad(img, r, constant_values=255)
    out = np.zeros_like(img)
    for y in range(img.shape[0]):
        for x in range(img.shape[1]):
            window = pad[y:y + 2 * r + 1, x:x + 2 * r + 1]   # 도장 크기만큼의 이웃
            if np.all(window[se == 1] == 255):              # 1 자리가 모두 흰색인가
                out[y, x] = 255
    return out

mine = my_erode(img, se)
cv = cv2.erode(img, se)
print("직접 계산한 침식 (1 = 흰 칸):")
print(mine // 255)
print("cv2.erode 와 같은가:", np.array_equal(mine, cv))
print("흰 칸:", int((img == 255).sum()), "→", int((cv == 255).sum()))
출력
직접 계산한 침식 (1 = 흰 칸):
[[0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 1 1 0 0 0 0]
 [0 0 1 1 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0]]
cv2.erode 와 같은가: True
흰 칸: 18 → 4

3단계. 팽창을 NumPy 로 직접 계산하기

Python
def my_dilate(img, se):
    """도장의 1 자리에 흰 칸이 하나라도 있으면 흰색(255)을 쓴다."""
    r = se.shape[0] // 2
    # 팽창은 그림 밖을 검정으로 친다
    pad = np.pad(img, r, constant_values=0)
    out = np.zeros_like(img)
    for y in range(img.shape[0]):
        for x in range(img.shape[1]):
            window = pad[y:y + 2 * r + 1, x:x + 2 * r + 1]
            if np.any(window[se == 1] == 255):              # 하나라도 흰색인가
                out[y, x] = 255
    return out

mine_d = my_dilate(img, se)
cv_d = cv2.dilate(img, se)
print("직접 계산한 팽창 (1 = 흰 칸):")
print(mine_d // 255)
print("cv2.dilate 와 같은가:", np.array_equal(mine_d, cv_d))
print("흰 칸:", int((img == 255).sum()), "→", int((cv_d == 255).sum()))
# connectedComponents: 첫 값이 (흰 덩어리 수 + 바탕 1)이라 1 을 뺀다
print("흰 덩어리:", cv2.connectedComponents(img)[0] - 1, "→", cv2.connectedComponents(cv_d)[0] - 1)
출력
직접 계산한 팽창 (1 = 흰 칸):
[[1 1 1 1 1 1 0 0]
 [1 1 1 1 1 1 0 0]
 [1 1 1 1 1 1 1 0]
 [1 1 1 1 1 1 1 0]
 [1 1 1 1 1 1 1 0]
 [1 1 1 1 1 1 1 1]
 [0 0 0 0 0 1 1 1]
 [0 0 0 0 0 1 1 1]]
cv2.dilate 와 같은가: True
흰 칸: 18 → 47
흰 덩어리: 2 → 1

4단계. 원본, 침식, 팽창을 칸 그림으로 보기

Python
import matplotlib.pyplot as plt

# 그래프의 한글이 깨지지 않게 맑은 고딕을 쓰고, 빼기 기호가 네모로 나오지 않게 한다
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

fig, axes = plt.subplots(1, 3, figsize=(11, 4))
for ax, (title, im) in zip(axes, [("원본", img), ("침식 3×3", cv), ("팽창 3×3", cv_d)]):
    ax.imshow(im, cmap="gray", vmin=0, vmax=255)
    # 칸마다 1 또는 0 을 적는다. 흰 칸 위에는 검은 글자, 검은 칸 위에는 흰 글자
    for y in range(8):
        for x in range(8):
            v = int(im[y, x] // 255)
            ax.text(x, y, str(v), ha="center", va="center", color="black" if v else "white")
    ax.set_title(f"{title} (흰 칸 {int((im == 255).sum())})")
    ax.set_xticks([])
    ax.set_yticks([])
plt.tight_layout()
plt.show()
8×8 원본, 3×3 침식, 3×3 팽창 결과를 칸마다 1 과 0 을 적어 나란히 놓은 결과

코드 설명

  • cv2.getStructuringElement(모양, (5, 5)) 는 1 과 0 으로 된 uint8 표를 돌려줍니다. 1 인 칸은 네모 25개, 십자 9개, 타원 17개로 본문의 그림과 같습니다.
  • my_erode 는 칸마다 도장 크기의 이웃(window)을 잘라 window[se == 1] 로 도장의 1 자리만 고르고, np.all(... == 255) 로 모두 흰지 봅니다. 결과는 가운데 2×2 네 칸뿐이고 cv2.erode 와 같습니다(True).
  • np.pad(img, r, constant_values=255) 는 그림 둘레에 r 칸을 덧댑니다. 침식은 그림 밖을 흰색(255), 팽창은 검정(0)으로 덧대야 OpenCV 와 결과가 같습니다.
  • my_dilate 는 np.all 대신 np.any 를 쓴 것만 다릅니다. 흰 칸이 18개에서 47개로 늘고, 네모와 떨어진 점이 이어져 흰 덩어리가 2개에서 1개가 됩니다.
  • 칸 그림에서 침식은 네모의 둘레 한 겹과 튀어나온 칸, 떨어진 점을 지웠고, 팽창은 모든 흰 칸을 사방으로 한 칸씩 넓혔습니다.
← 이전 절9장 · 실습 지도다음 절 →9장 · 실습 2 — 침식과 팽창의 크기와 횟수 바꿔 보기