11장. Canny 와 Edge 비교
4단계 이력 임계값
등산로의 표지 리본을 떠올려 봅시다. 진하고 또렷한 리본이 달린 길은 확실한 등산로입니다. 색이 바랜 리본이 달린 길은 애매하지만, 확실한 등산로에서 이어져 나온 길이라면 믿고 따라갑니다. 어디에도 이어지지 않은 바랜 리본 하나는 잘못 달린 것으로 보고 무시합니다. 이력 임계값은 이렇게 기준을 두 개 두고, 이어져 있는지를 함께 봅니다.
그림의 세 상자가 칸을 나누는 세 무리입니다. 초록 상자의 강한 경계(high 이상)는 무조건 남고, 다홍 상자의 약한 경계(low 와 high 사이)는 강한 경계와 이어져 있을 때만 남고, 보라 상자의 low 아래 칸은 무조건 지워집니다. 여기서 진한 리본이 강한 경계, 바랜 리본이 약한 경계, 강한 경계에서 이어져 나온 길이 "이어진 약한 경계"입니다.
손으로 계산해 보기
low = 50, high = 100 으로 5×6 칸을 나눕니다. 숫자는 비최대 억제 뒤의 기울기 크기입니다.
왼쪽 격자에서 120 은 high(100) 이상이라 강한 경계(초록)입니다. 그 오른쪽으로 이어진 80, 60, 70 은 약한 경계(살구)지만 120 과 이웃해 줄줄이 이어지므로 모두 남습니다. 아래쪽의 60, 70 과 오른쪽 끝의 90 도 약한 경계지만 강한 경계와 이어지지 않아 지워집니다. 30 은 low 아래라 처음부터 버립니다. 오른쪽 결과에는 120, 80, 60, 70 네 칸만 남습니다.
| 칸의 값 | 무리 | 강한 경계와 이어졌나 | 결과 |
|---|---|---|---|
| 120 | 강한 경계(100 이상) | 자기가 강한 경계 | 남긴다 |
| 첫 줄의 80, 60 과 둘째 줄의 70 | 약한 경계(50~99) | 그렇다(120 에서 줄줄이) | 남긴다 |
| 넷째 줄의 60, 70 | 약한 경계 | 아니다 | 지운다 |
| 오른쪽 끝의 90 | 약한 경계 | 아니다 | 지운다 |
| 30 | low 아래 | 볼 필요 없다 | 지운다 |
"이웃"은 위·아래·왼쪽·오른쪽과 대각선 네 칸까지 여덟 칸을 말합니다.
짧은 예시
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
keep = grid >= high # 강한 경계에서 출발한다
weak = (grid >= low) & (grid < high) # 약한 경계 후보
changed = True
while changed: # 더 늘지 않을 때까지 되풀이한다
changed = False
for y, x in zip(*np.nonzero(weak & ~keep)):
# 여덟 이웃 가운데 이미 남긴 칸이 있으면 이 칸도 남긴다
if keep[max(y - 1, 0):y + 2, max(x - 1, 0):x + 2].any():
keep[y, x] = True
changed = True
print("남은 칸의 값:", grid[keep].tolist()) # 결과: [120, 80, 60, 70]np.nonzero 는 참인 칸의 (세로 번호들, 가로 번호들)을 돌려줍니다. 남긴 칸 옆의 약한 칸을 남기고, 다시 그 옆을 보는 일을 더 늘지 않을 때까지 되풀이합니다. ~keep 은 참과 거짓을 뒤집습니다.
주의할 점
- 기준을 하나만 쓰면 높게 잡을 때 약한 선이 끊기고, 낮게 잡을 때 헛것이 늡니다. 이력 임계값은 높은 기준으로 헛것을 막고, 이어짐으로 끊김을 메웁니다.
- 강한 경계에 이어진 노이즈는 약해도 살아남습니다. 그래서 블러로 노이즈를 먼저 줄여 두어야 합니다.