9장. Morphology: 침식, 팽창, 열기, 닫기
실습 2 — 침식과 팽창의 크기와 횟수 바꿔 보기
도형 판(ch09_shapes.png)을 도장 크기 3, 5, 9 로 침식·팽창하고, 흰 칸 수와 흰 덩어리 수가 어떻게 바뀌는지 표로 봅니다. iterations 로 되풀이한 결과가 큰 도장 한 번과 같은지도 확인합니다.
1단계. 도형 판 읽고 덩어리 세기
import cv2
import numpy as np
import pandas as pd
# 검은 바탕에 흰 도형을 그린 가상의 이진 그림
shapes = cv2.imread("ch09_shapes.png", cv2.IMREAD_GRAYSCALE)
print("모양:", shapes.shape, "/ 들어 있는 값:", [int(v) for v in np.unique(shapes)])
print("흰 칸 수:", int((shapes == 255).sum()))
# connectedComponents = connected components(이어진 덩어리들)
# 돌려주는 값: (덩어리 수 + 바탕 1, 칸마다 덩어리 번호를 적은 그림)
count, labels = cv2.connectedComponents(shapes)
print("흰 덩어리 수:", count - 1)모양: (200, 320) / 들어 있는 값: [0, 255]
흰 칸 수: 15931
흰 덩어리 수: 82단계. 도장 크기별 흰 칸과 덩어리 수 표
def blobs(binary):
"""흰 덩어리 수를 돌려준다."""
return cv2.connectedComponents(binary)[0] - 1
rows, outs = [], {}
for k in [3, 5, 9]:
se = cv2.getStructuringElement(cv2.MORPH_RECT, (k, k))
outs[("침식", k)] = cv2.erode(shapes, se)
outs[("팽창", k)] = cv2.dilate(shapes, se)
for op in ["침식", "팽창"]:
out = outs[(op, k)]
rows.append({"연산": op, "도장": f"{k}×{k}",
"흰 칸": int((out == 255).sum()), "흰 덩어리": blobs(out)})
# 원본 줄을 맨 위에 넣어 견주기 쉽게 한다
table = pd.DataFrame([{"연산": "원본", "도장": "-", "흰 칸": int((shapes == 255).sum()),
"흰 덩어리": blobs(shapes)}] + rows)
print(table.to_string(index=False))연산 도장 흰 칸 흰 덩어리
원본 - 15931 8
침식 3×3 14158 7
팽창 3×3 17779 8
침식 5×5 12664 5
팽창 5×5 19678 7
침식 9×9 10512 4
팽창 9×9 23568 73단계. iterations 와 큰 도장 견주기
k3 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
for n in [1, 2, 3, 4]:
# iterations=n: 3×3 침식을 n 번 되풀이한다
eroded = cv2.erode(shapes, k3, iterations=n)
# 같은 세기의 큰 도장: 한 번에 n 칸씩 깎으려면 크기 2n + 1
big = cv2.getStructuringElement(cv2.MORPH_RECT, (2 * n + 1, 2 * n + 1))
same = np.array_equal(eroded, cv2.erode(shapes, big))
print(f"3×3 을 {n}번: 흰 칸 {int((eroded == 255).sum()):5}, 덩어리 {blobs(eroded)}"
f" / {2 * n + 1}×{2 * n + 1} 한 번과 같은가: {same}")3×3 을 1번: 흰 칸 14158, 덩어리 7 / 3×3 한 번과 같은가: True
3×3 을 2번: 흰 칸 12664, 덩어리 5 / 5×5 한 번과 같은가: True
3×3 을 3번: 흰 칸 11420, 덩어리 8 / 7×7 한 번과 같은가: True
3×3 을 4번: 흰 칸 10512, 덩어리 4 / 9×9 한 번과 같은가: True4단계. 격자로 견주기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# 윗줄은 침식, 아랫줄은 팽창. 맨 왼쪽은 원본
fig, axes = plt.subplots(2, 4, figsize=(13, 4.6))
for i, op in enumerate(["침식", "팽창"]):
axes[i, 0].imshow(shapes, cmap="gray", vmin=0, vmax=255)
axes[i, 0].set_title("원본")
for j, k in enumerate([3, 5, 9], start=1):
out = outs[(op, k)]
axes[i, j].imshow(out, cmap="gray", vmin=0, vmax=255)
axes[i, j].set_title(f"{op} {k}×{k} (덩어리 {blobs(out)})")
for ax in axes.flat:
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
- 도형 판은 0 과 255 두 값만 있는 이진 그림이고, 흰 칸 15931개, 흰 덩어리 8개(네모, 원, 가는 선, 작은 점 둘, 두 원, 고리)입니다.
- 침식은 도장이 클수록 흰 칸이 줄어듭니다(14158 → 12664 → 10512). 3×3 은 작은 점 하나만 지워 덩어리가 7개가 되고, 5×5 는 가는 선과 남은 점까지 지워 5개, 9×9 는 고리까지 지워 4개(네모, 원, 두 원)만 남깁니다.
- 팽창은 흰 칸이 늘어납니다(17779 → 19678 → 23568). 3×3 은 덩어리 8개 그대로지만, 5×5 부터 4 칸 떨어진 두 원이 하나로 붙어 7개가 됩니다.
- 3×3 침식을 n 번 되풀이한 결과는 (2n + 1)×(2n + 1) 침식 한 번과 네 경우 모두 같았습니다(
True). 3번(7×7)에서 덩어리가 8개로 오히려 늘어난 것은 굵기 6 칸의 고리가 다 지워지기 전에 작은 조각 네 개로 끊겼기 때문입니다. 너무 센 침식은 덩어리를 지우기만 하는 게 아니라 쪼개기도 합니다.