9장. Morphology: 침식, 팽창, 열기, 닫기
실습 3 — 열기로 흰 잡티 지우기
깨끗한 글자 그림(ch09_letters_clean.png)에 씨앗을 고정해 흰 잡티를 뿌리고, 열기로 지웁니다. 흰 덩어리 수가 본래 글자 수(5)로 돌아오는지와, 본래 글자와 다른 칸이 얼마나 줄었는지 잽니다. 덩어리를 세는 cv2.connectedComponents 는 12장에서 자세히 배우고, 여기서는 개수만 씁니다.
1단계. 흰 잡티 뿌리기
import cv2
import numpy as np
clean = cv2.imread("ch09_letters_clean.png", cv2.IMREAD_GRAYSCALE)
# 씨앗 911 로 고정한 난수: 몇 번을 돌려도 같은 자리에 잡티가 생긴다
rng = np.random.default_rng(911)
noisy = clean.copy() # copy 하지 않으면 원본에도 점이 찍힌다
h, w = noisy.shape
for _ in range(60):
x, y = int(rng.integers(0, w)), int(rng.integers(0, h))
r = int(rng.integers(1, 3)) # 반지름 1 또는 2 칸
cv2.circle(noisy, (x, y), r, 255, -1) # -1: 속을 채운 원
def blobs(binary):
return cv2.connectedComponents(binary)[0] - 1
print("본래 글자의 흰 덩어리:", blobs(clean))
print("잡티 그림의 흰 덩어리:", blobs(noisy))
print("본래 글자와 다른 칸:", int((noisy != clean).sum()))본래 글자의 흰 덩어리: 5
잡티 그림의 흰 덩어리: 47
본래 글자와 다른 칸: 4662단계. 도장 크기별 열기
results = {}
for k in [3, 5, 7]:
se = cv2.getStructuringElement(cv2.MORPH_RECT, (k, k))
# MORPH_OPEN: 침식한 뒤 같은 도장으로 팽창
results[k] = cv2.morphologyEx(noisy, # 그림
cv2.MORPH_OPEN, # 연산 종류: 열기
se) # 구조 요소
out = results[k]
print(f"열기 {k}×{k}: 흰 덩어리 {blobs(out):2}, 본래 글자와 다른 칸 {int((out != clean).sum())}")열기 3×3: 흰 덩어리 30, 본래 글자와 다른 칸 250
열기 5×5: 흰 덩어리 5, 본래 글자와 다른 칸 15
열기 7×7: 흰 덩어리 6, 본래 글자와 다른 칸 853단계. 침식만 한 것과 견주기
se5 = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
eroded = cv2.erode(noisy, se5)
# 침식만 하면 잡티는 지워지지만 글자도 가늘어진다. 흰 칸 수로 견준다
print("본래 글자 흰 칸:", int((clean == 255).sum()))
print("침식 5×5 만 :", int((eroded == 255).sum()), "/ 흰 덩어리", blobs(eroded))
print("열기 5×5 :", int((results[5] == 255).sum()), "/ 흰 덩어리", blobs(results[5]))
# 열기 5×5 결과를 다음에 쓸 수 있게 저장한다
cv2.imwrite("out_ch09_opened.png", results[5])
print("저장: out_ch09_opened.png")본래 글자 흰 칸: 6375
침식 5×5 만 : 3610 / 흰 덩어리 5
열기 5×5 : 6378 / 흰 덩어리 5
저장: out_ch09_opened.png4단계. 격자로 견주기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
panels = [("잡티 그림", noisy), ("침식 5×5 만", eroded)] + [(f"열기 {k}×{k}", results[k]) for k in [3, 5, 7]]
panels.append(("본래 글자", clean))
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(f"{title} (덩어리 {blobs(im)})")
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
rng.integers(0, w)로 잡티의 가로 자리,rng.integers(1, 3)으로 반지름 1 또는 2 를 뽑습니다. 씨앗 911 로 고정했으므로 흰 덩어리는 늘 47개(글자 5 + 잡티 42), 본래 글자와 다른 칸은 466개입니다. 잡티 60개 가운데 일부는 글자와 겹치거나 서로 붙어 덩어리가 60개보다 적습니다.- 열기 3×3 은 반지름 1 의 잡티만 지워 덩어리가 30개 남습니다. 5×5 는 반지름 2 의 잡티(지름 5 칸)까지 지워 덩어리 5개, 다른 칸 15개로 본래 글자와 거의 같습니다.
- 열기 7×7 은 오히려 덩어리 6개, 다른 칸 85개로 나빠졌습니다. 잡티는 모두 지워졌지만, M 의 가운데 비스듬한 획에서 7×7 도장보다 좁은 곳이 끊겨 M 이 두 조각이 되었기 때문입니다. 다른 칸 85개 가운데 76개가 글자에서 깎여 나간 칸입니다. 크다고 좋은 것이 아닙니다.
- 침식 5×5 만 하면 잡티는 사라지지만 흰 칸이 3610개로 본래(6375)의 6할 남짓밖에 안 됩니다. 이어서 팽창한 열기 5×5 는 6378개로 본래 굵기를 되찾습니다.