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

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

실습 5 — 순서 비교와 그레이디언트로 테두리 보기

끊긴 글자에 흰 잡티까지 뿌려 잡티와 구멍이 함께 있는 그림을 만들고, 열기 → 닫기와 닫기 → 열기를 견줍니다. 이어서 그레이디언트, 탑햇, 블랙햇으로 테두리와 바뀐 곳만 골라 봅니다.

1단계. 잡티와 구멍이 함께 있는 그림 만들기

Python
import cv2
import numpy as np

clean = cv2.imread("ch09_letters_clean.png", cv2.IMREAD_GRAYSCALE)
dirty = cv2.imread("ch09_letters_broken.png", cv2.IMREAD_GRAYSCALE)
# 씨앗 912 로 흰 잡티 60개를 더 뿌린다
rng = np.random.default_rng(912)
h, w = dirty.shape
for _ in range(60):
    cv2.circle(dirty, (int(rng.integers(0, w)), int(rng.integers(0, h))), int(rng.integers(1, 3)), 255, -1)

def blobs(binary):
    return cv2.connectedComponents(binary)[0] - 1

print("흰 덩어리:", blobs(dirty), "/ 본래 글자와 다른 칸:", int((dirty != clean).sum()))
출력
흰 덩어리: 58 / 본래 글자와 다른 칸: 981

2단계. 두 순서 견주기

Python
se = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
opened = cv2.morphologyEx(dirty, cv2.MORPH_OPEN, se)
closed = cv2.morphologyEx(dirty, cv2.MORPH_CLOSE, se)
orders = {"열기 → 닫기": cv2.morphologyEx(opened, cv2.MORPH_CLOSE, se),
          "닫기 → 열기": cv2.morphologyEx(closed, cv2.MORPH_OPEN, se)}
for name, out in orders.items():
    accuracy = round(float((out == clean).mean()), 3)     # 맞힌 칸 비율
    print(f"{name}: 흰 덩어리 {blobs(out)}, 다른 칸 {int((out != clean).sum())}, 맞힌 칸 비율 {accuracy}")

# 가장자리(맨 위·아래 줄, 맨 왼쪽·오른쪽 칸)에 흰 칸이 몇 개 남았는지 본다
out = orders["닫기 → 열기"]
edge = int((out[0] == 255).sum() + (out[-1] == 255).sum() + (out[:, 0] == 255).sum() + (out[:, -1] == 255).sum())
print("닫기 → 열기 결과의 가장자리 흰 칸:", edge)
출력
열기 → 닫기: 흰 덩어리 8, 다른 칸 900, 맞힌 칸 비율 0.977
닫기 → 열기: 흰 덩어리 8, 다른 칸 282, 맞힌 칸 비율 0.993
닫기 → 열기 결과의 가장자리 흰 칸: 10

3단계. 그레이디언트, 탑햇, 블랙햇

Python
k3 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# MORPH_GRADIENT: 팽창 − 침식 = 테두리
grad = cv2.morphologyEx(clean, cv2.MORPH_GRADIENT, k3)
by_hand = cv2.subtract(cv2.dilate(clean, k3), cv2.erode(clean, k3))
print("글자 테두리 칸:", int((grad == 255).sum()), "/ 팽창 − 침식과 같은가:", np.array_equal(grad, by_hand))

# MORPH_TOPHAT: 원본 − 열기 → 열기가 지운 작은 흰 것
tophat = cv2.morphologyEx(dirty, cv2.MORPH_TOPHAT, se)
# MORPH_BLACKHAT: 닫기 − 원본 → 닫기가 메운 작은 검은 것
blackhat = cv2.morphologyEx(dirty, cv2.MORPH_BLACKHAT, se)
print("탑햇 흰 칸:", int((tophat == 255).sum()), "/ 블랙햇 흰 칸:", int((blackhat == 255).sum()))
출력
글자 테두리 칸: 2803 / 팽창 − 침식과 같은가: True
탑햇 흰 칸: 1035 / 블랙햇 흰 칸: 602

4단계. 격자로 보기

Python
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False

panels = [("잡티 + 구멍", dirty), ("열기 → 닫기", orders["열기 → 닫기"]), ("닫기 → 열기", orders["닫기 → 열기"]),
          ("그레이디언트 3×3", grad), ("탑햇 5×5", tophat), ("블랙햇 5×5", blackhat)]
fig, axes = plt.subplots(2, 3, figsize=(12, 4.2))
for ax, (title, im) in zip(axes.flat, panels):
    ax.imshow(im, cmap="gray", vmin=0, vmax=255)
    ax.set_title(title)
    ax.axis("off")
plt.tight_layout()
plt.show()
잡티와 구멍이 있는 글자, 열기 → 닫기, 닫기 → 열기, 그레이디언트, 탑햇, 블랙햇을 2행 3열로 놓은 결과

코드 설명

  • 끊긴 글자에 잡티를 더하니 흰 덩어리가 58개, 다른 칸이 981개입니다.
  • 열기 → 닫기는 덩어리 8개, 맞힌 칸 비율 0.977, 닫기 → 열기는 덩어리 8개, 맞힌 칸 비율 0.993 입니다. 덩어리 수는 같지만 다른 칸이 900개와 282개로 크게 다릅니다. 격자에서 보듯 열기 → 닫기는 M 이 두 조각으로 갈리고 R 과 H 의 틈이 남았고, 닫기 → 열기는 글자가 온전합니다.
  • 닫기 → 열기 결과의 가장자리에 흰 칸이 10개 남았습니다. 위쪽 가장자리에 붙은 잡티 둘이 열기에서 깎이지 않은 것입니다. 열기의 침식이 그림 밖을 흰색으로 치기 때문입니다.
  • cv2.morphologyEx(clean, cv2.MORPH_GRADIENT, k3) 는 글자 테두리 2803칸을 남기고, cv2.subtract(팽창, 침식) 과 같습니다(True). 탑햇(1035칸)은 열기가 지운 잡티를, 블랙햇(602칸)은 닫기가 메운 구멍과 틈을 보여 줍니다. 둘 다 끊긴 획의 짧은 조각도 함께 잡힙니다.
← 이전 절9장 · 실습 4 — 닫기로 끊긴 글자와 구멍 메우기다음 절 →9장 · 프로젝트 1 — 부품 검사 사진의 이진 마스크 다듬기