11장. Canny 와 Edge 비교
실습 3 — 이력 임계값의 효과
이력 임계값을 작은 격자에서 직접 해 보고, 옅어지는 띠 그림(ch11_set/fade.png)에서 low 만 바꿀 때와 high 만 바꿀 때를 견줍니다. 이 그림의 띠는 왼쪽(밝기 60)에서 오른쪽(176)으로 갈수록 바탕(180)과 비슷해져 경계가 점점 약해지고, 오른쪽 아래의 원은 바탕과 밝기 차이가 25 뿐입니다. 이어진 흰 덩어리의 개수(연결 요소)도 셉니다.
1단계. 작은 격자에서 이력 임계값 해 보기
import cv2
import numpy as np
grid = np.array([[0, 120, 80, 60, 0, 0],
[0, 0, 0, 70, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 60, 70, 0, 0, 90],
[0, 0, 0, 0, 30, 0]])
low, high = 50, 100
# low 이상인 칸을 모두 흰색으로 만든 판
candidate = np.where(grid >= low, 255, 0).astype(np.uint8)
# cv2.connectedComponents: 이어진 흰 덩어리마다 1, 2, 3, ... 번호를 매긴다. 0 은 바탕
count, labels = cv2.connectedComponents(candidate,
connectivity=8) # 대각선 이웃까지 이어진 것으로 본다
print("덩어리 수(바탕 포함):", count)
print(labels)
# 강한 경계(high 이상)가 하나라도 든 덩어리 번호만 남긴다
strong_labels = set(labels[grid >= high].tolist())
keep = np.isin(labels, list(strong_labels))
print("강한 경계가 든 덩어리:", sorted(strong_labels))
print("남은 칸의 값:", grid[keep].tolist())덩어리 수(바탕 포함): 4
[[0 1 1 1 0 0]
[0 0 0 1 0 0]
[0 0 0 0 0 0]
[0 2 2 0 0 3]
[0 0 0 0 0 0]]
강한 경계가 든 덩어리: [1]
남은 칸의 값: [120, 80, 60, 70]2단계. high 를 두고 low 만 바꾸기
# 옅어지는 띠 그림과 정답 경계. 연습용으로 그려 만든 가상의 그림이다
gray = cv2.imread("ch11_set/fade.png", cv2.IMREAD_GRAYSCALE)
answer = cv2.imread("ch11_set/fade_edge.png", cv2.IMREAD_GRAYSCALE)
kernel = np.ones((3, 3), np.uint8)
def recall(edges):
"""정답 경계 가운데, 찾은 경계에서 한 칸 안에 든 칸의 비율"""
near = cv2.dilate(edges, kernel) # 9차시에서 본 팽창: 흰 덩어리를 한 겹 부풀린다
return round(float(((answer > 0) & (near > 0)).sum() / (answer > 0).sum()), 3)
def pieces(edges):
"""이어진 흰 덩어리의 개수(바탕 빼고). 많을수록 선이 잘게 끊겼다"""
return cv2.connectedComponents(edges, connectivity=8)[0] - 1
low_results = {}
for low in [10, 50, 100, 150]:
e = cv2.Canny(gray, low, 150)
low_results[low] = e
# 띠의 위쪽 테두리(세로 48~52)가 가로 몇 번째 칸까지 이어졌나
right_end = int(np.nonzero(e[48:53].any(axis=0))[0].max())
print(f"low {low:3}, high 150: 재현율 {recall(e)}, 덩어리 {pieces(e)}개, 위 테두리 끝 {right_end}")low 10, high 150: 재현율 0.739, 덩어리 1개, 위 테두리 끝 300
low 50, high 150: 재현율 0.67, 덩어리 1개, 위 테두리 끝 297
low 100, high 150: 재현율 0.596, 덩어리 1개, 위 테두리 끝 263
low 150, high 150: 재현율 0.523, 덩어리 1개, 위 테두리 끝 2293단계. low 를 두고 high 만 바꾸기
high_results = {}
for high in [80, 150, 400, 700]:
e = cv2.Canny(gray, 50, high)
high_results[high] = e
print(f"low 50, high {high:3}: 재현율 {recall(e)}, 덩어리 {pieces(e)}개, "
f"경계 칸 {int((e > 0).sum())}개")low 50, high 80: 재현율 0.931, 덩어리 2개, 경계 칸 847개
low 50, high 150: 재현율 0.67, 덩어리 1개, 경계 칸 615개
low 50, high 400: 재현율 0.67, 덩어리 1개, 경계 칸 615개
low 50, high 700: 재현율 0.0, 덩어리 0개, 경계 칸 0개4단계. 결과를 두 줄로 견주기
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, 5.4))
for ax, (low, e) in zip(axes[0], low_results.items()):
ax.imshow(e, cmap="gray", vmin=0, vmax=255)
ax.set_title(f"low {low}, high 150")
ax.axis("off")
for ax, (high, e) in zip(axes[1], high_results.items()):
ax.imshow(e, cmap="gray", vmin=0, vmax=255)
ax.set_title(f"low 50, high {high}")
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
cv2.connectedComponents(candidate, connectivity=8)은 low 이상인 칸을 이어진 덩어리로 묶어, 바탕(0)과 덩어리 1·2·3 으로 모두 4개의 번호를 매깁니다. 강한 경계 120 이 든 덩어리는 1번뿐이라, 본문 손계산처럼 120, 80, 60, 70 만 남습니다.- high 를 150 에 두고 low 를 150, 100, 50, 10 으로 낮추면 띠의 위 테두리가 가로 229, 263, 297, 300 번째 칸까지 이어지고, 재현율이 0.523, 0.596, 0.67, 0.739 로 오릅니다. 왼쪽 끝의 강한 경계에서 출발해, 오른쪽으로 갈수록 약해지는 테두리를 따라가기 때문입니다. low 와 high 가 같은 150 이면 기준 하나로 자른 것과 같습니다.
- low 를 50 에 두고 high 를 바꾸면 80 에서만 원이 나타나 덩어리가 2개, 재현율이 0.931 이 됩니다. 원은 바탕과 차이가 25 뿐이라 기울기가 150 에 못 미치고, 강한 경계가 하나도 없으니 high 150 이상에서는 통째로 사라집니다.
- high 400 은 150 과 결과가 같습니다(경계 칸 615개). 띠의 왼쪽 끝은 아주 진해서 400 도 넘기 때문입니다. high 700 이면 넘는 칸이 하나도 없어 경계도 0개입니다. 강한 경계가 없으면 약한 경계가 이어질 곳도 없습니다.