11장. Canny 와 Edge 비교
성능을 숫자로 재기
그물로 물고기를 잡는다고 해 봅시다. 호수에 물고기가 20 마리 있고, 그물을 걷어 올리니 16 개가 걸렸는데 그중 물고기는 12 마리, 나머지 4 개는 깡통입니다. "걸린 것 가운데 물고기의 비율"은 12 ÷ 16 = 0.75 이고, "호수의 물고기 가운데 잡은 비율"은 12 ÷ 20 = 0.6 입니다. 앞의 것이 정밀도, 뒤의 것이 재현율입니다.
초록 상자의 정밀도는 "찾은 것 가운데 맞은 것"이라 헛것(깡통)이 많으면 떨어집니다. 다홍 상자의 재현율은 "정답 가운데 찾은 것"이라 빠뜨린 것(놓친 물고기)이 많으면 떨어집니다. 여기서 호수의 물고기가 정답 경계 칸, 그물에 걸린 것이 찾은 경계 칸, 깡통이 헛것입니다.
칸끼리 바로 견주면 안 되는 까닭
경계는 한 칸 두께의 가는 선이라, 찾은 선이 정답에서 한 칸만 옆으로 비켜나도 칸끼리는 하나도 겹치지 않습니다. 그래서 정답 경계를 9장에서 본 팽창(cv2.dilate)으로 한 겹 부풀려 두고, 그 안에 들어온 칸을 맞힌 것으로 칩니다. 이 여유를 허용 거리라고 합니다.
그림의 다섯 단계로 점수를 냅니다. 정밀도를 낼 때는 넓힌 정답 안에 찾은 칸이 몇 개 들었는지 세고, 재현율을 낼 때는 거꾸로 찾은 경계를 넓혀 그 안에 정답 칸이 몇 개 들었는지 셉니다. 마지막에 두 값을 F1 하나로 합칩니다.
노이즈 낀 도형 그림에서 Canny(블러 없이 50, 150)의 결과를 정답과 겹쳐 칠했습니다. 초록은 정답 근처에서 찾은 칸, 다홍은 정답에서 떨어진 헛것, 보라는 찾지 못한 정답 칸입니다. 다홍 점이 많아 정밀도가 낮고, 보라가 거의 없어 재현율은 높습니다.
손으로 계산해 보기
그물 예의 숫자로 F1 까지 계산합니다.
| 값 | 식 | 계산 | 결과 |
|---|---|---|---|
| 정밀도 | 맞은 것 ÷ 찾은 것 | 12 ÷ 16 | 0.75 |
| 재현율 | 찾은 정답 ÷ 정답 | 12 ÷ 20 | 0.6 |
| F1 | 2 × 정밀도 × 재현율 ÷ (정밀도 + 재현율) | 2 × 0.75 × 0.6 ÷ 1.35 | 0.667 |
F1 은 두 값의 보통 평균(0.675)보다 조금 낮습니다. 한쪽이 아주 낮으면 F1 도 크게 떨어집니다. 예를 들어 정밀도 1.0, 재현율 0.1 이면 보통 평균은 0.55 지만 F1 은 2 × 1.0 × 0.1 ÷ 1.1 = 0.182 입니다. 그래서 "모두 경계라고 하기"나 "확실한 것 몇 개만 찾기" 같은 꼼수가 통하지 않습니다.
짧은 예시
import cv2
import numpy as np
def edge_score(edges, answer):
"""허용 거리 1칸으로 (정밀도, 재현율, F1) 을 돌려준다."""
# 3×3 네모 구조 요소: 9차시의 cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) 와 같다
kernel = np.ones((3, 3), np.uint8)
near_answer = cv2.dilate(answer, kernel) # 넓힌 정답
near_found = cv2.dilate(edges, kernel) # 넓힌 찾은 경계
found, real = edges > 0, answer > 0
precision = (found & (near_answer > 0)).sum() / max(found.sum(), 1)
recall = (real & (near_found > 0)).sum() / real.sum()
f1 = 2 * precision * recall / (precision + recall) if precision + recall > 0 else 0.0
return round(float(precision), 3), round(float(recall), 3), round(float(f1), 3)
gray = cv2.imread("ch11_set/shapes.png", cv2.IMREAD_GRAYSCALE)
answer = cv2.imread("ch11_set/shapes_edge.png", cv2.IMREAD_GRAYSCALE)
print("Canny 50, 150:", edge_score(cv2.Canny(gray, 50, 150), answer))
print("Canny 100, 300:", edge_score(cv2.Canny(gray, 100, 300), answer))교안 PC 에서는 50, 150 이 (1.0, 1.0, 1.0), 100, 300 이 (1.0, 0.544, 0.704) 입니다. 깨끗한 그림이라 두 결과 모두 헛것은 없지만(정밀도 1.0), 기준값을 높이면 바탕과 밝기 차이가 작은 타원과 오른쪽 아래 네모를 통째로 놓쳐 재현율이 0.544 로 떨어지고, F1 도 0.704 로 내려갑니다. max(found.sum(), 1) 은 찾은 칸이 하나도 없을 때 0 으로 나누지 않게 막습니다.
주의할 점
- 허용 거리를 크게 잡을수록 점수가 후해집니다. 비교할 때는 모든 방법에 같은 허용 거리를 씁니다. 이 장은 1칸(3×3 팽창)입니다.
- 8장에서 본 "정답과 같은 칸의 비율"은 경계 검출에 맞지 않습니다. 경계가 그림의 2% 남짓이라 아무것도 찾지 않아도 0.98 이 나오기 때문입니다.