10장. Sobel 과 Laplacian 경계 검출
실습 5 — 노이즈 낀 그림에서 블러 먼저 하기
도형 그림에 가우시안 노이즈를 더한 뒤, 블러 없이와 Gaussian 블러 뒤에 Sobel 크기와 Laplacian 을 구해 고른 바탕에서 노이즈를 경계로 잘못 찾은 칸을 셉니다. 블러 크기를 바꿔 가며 표로 견줍니다.
1단계. 노이즈 더하기
import cv2
import numpy as np
img = cv2.imread("ch10_shapes.png", cv2.IMREAD_GRAYSCALE)
# 6차시에서 본 것과 같은 꼴의 가우시안 노이즈: 씨앗 1010, 평균 0, 표준편차 20
rng = np.random.default_rng(1010)
noisy = np.clip(img.astype(np.float64) + rng.normal(0, 20, img.shape), 0, 255).astype(np.uint8)
# 도형 사이의 고른 바탕: 세로 125~144, 가로 150~309
bg = (slice(125, 145), slice(150, 310))
print("바탕의 밝기 표준편차: 원본", round(float(img[bg].std()), 1), "/ 노이즈", round(float(noisy[bg].std()), 1))
print("바탕 다섯 칸:", [int(v) for v in noisy[130, 150:155]])바탕의 밝기 표준편차: 원본 0.0 / 노이즈 20.1
바탕 다섯 칸: [96, 24, 72, 61, 49]2단계. 블러 없이와 블러 먼저 견주기
def false_ratio(src, t=100):
"""고른 바탕에서 Sobel 크기와 |Laplacian| 이 기준 t 를 넘는 칸의 비율을 돌려준다."""
mag = cv2.magnitude(cv2.Sobel(src, cv2.CV_64F, 1, 0), cv2.Sobel(src, cv2.CV_64F, 0, 1))
lap = np.abs(cv2.Laplacian(src, cv2.CV_64F, ksize=3))
return round(float((mag[bg] > t).mean()), 3), round(float((lap[bg] > t).mean()), 3)
blur5 = cv2.GaussianBlur(noisy, (5, 5), 0)
for name, src in [("원본", img), ("노이즈", noisy), ("노이즈 → 블러 5", blur5)]:
s, l = false_ratio(src)
print(f"{name}: Sobel 잘못 찾은 비율 {s}, Laplacian 잘못 찾은 비율 {l}")원본: Sobel 잘못 찾은 비율 0.0, Laplacian 잘못 찾은 비율 0.0
노이즈: Sobel 잘못 찾은 비율 0.372, Laplacian 잘못 찾은 비율 0.591
노이즈 → 블러 5: Sobel 잘못 찾은 비율 0.0, Laplacian 잘못 찾은 비율 0.03단계. 블러 크기별 표 만들기
import pandas as pd
rows = []
for k in (0, 3, 5, 9):
# k=0 은 블러 없이. GaussianBlur(그림, (k, k), 0): 시그마는 크기에 맞춰 자동
src = noisy if k == 0 else cv2.GaussianBlur(noisy, (k, k), 0)
s, l = false_ratio(src)
# 진짜 경계가 얼마나 약해지나: 네모의 왼쪽 변(가로줄 80, 가로 50~70)에서 가장 큰 크기
mag = cv2.magnitude(cv2.Sobel(src, cv2.CV_64F, 1, 0), cv2.Sobel(src, cv2.CV_64F, 0, 1))
rows.append({"블러": "없음" if k == 0 else f"{k}×{k}", "바탕 표준편차": round(float(src[bg].std()), 1),
"Sobel 오검출": s, "Laplacian 오검출": l, "왼쪽 변 최대 크기": round(float(mag[80, 50:71].max()))})
table = pd.DataFrame(rows)
print(table.to_string(index=False)) 블러 바탕 표준편차 Sobel 오검출 Laplacian 오검출 왼쪽 변 최대 크기
없음 20.1 0.372 0.591 639
3×3 7.6 0.010 0.014 452
5×5 5.5 0.000 0.000 372
9×9 3.4 0.000 0.000 2574단계. 2행 3열로 견주어 보기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
fig, axes = plt.subplots(2, 3, figsize=(12, 6))
for i, (name, src) in enumerate([("블러 없이", noisy), ("블러 5×5 뒤", blur5)]):
mag = cv2.magnitude(cv2.Sobel(src, cv2.CV_64F, 1, 0), cv2.Sobel(src, cv2.CV_64F, 0, 1))
lap = cv2.Laplacian(src, cv2.CV_64F, ksize=3)
# 경계 지도는 기준 100 으로 만든다
panels = [(f"{name}: 입력", src),
(f"{name}: Sobel 크기 > 100", np.where(mag > 100, 255, 0).astype(np.uint8)),
(f"{name}: |Laplacian| > 100", np.where(np.abs(lap) > 100, 255, 0).astype(np.uint8))]
for ax, (title, arr) in zip(axes[i], panels):
ax.imshow(arr, cmap="gray", vmin=0, vmax=255)
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
- 표준편차 20 의 노이즈를 더하자 고른 바탕의 밝기 표준편차가 0.0 에서 20.1 로 커졌습니다. 바탕 다섯 칸이 96, 24, 72, 61, 49 처럼 원래 값 50 둘레로 크게 흔들립니다.
- 블러 없이 기준 100 을 걸면 고른 바탕의 0.372 를 Sobel 이, 0.591 을 Laplacian 이 경계로 잘못 찾습니다. 두 번 빼는 Laplacian 이 노이즈에 더 민감합니다. Gaussian 5×5 를 먼저 걸면 둘 다 0.0 이 됩니다.
- 블러 크기별 표에서 3×3 만으로도 잘못 찾은 비율이 0.010, 0.014 로 크게 줄고, 5×5 부터는 0 입니다. 그 대신 네모 왼쪽 변의 가장 큰 크기가 639(블러 없이) → 452 → 372 → 257(9×9)로 작아집니다. 블러가 셀수록 진짜 경계도 약해지므로, 기준을 넘는 가장 작은 블러를 고릅니다.
- 그림의 위 줄은 바탕 전체가 흰 점으로 덮였고, 아래 줄의 Sobel 은 테두리만 깨끗하게 남았습니다. 아래 줄의 Laplacian 은 테두리가 군데군데 끊겼습니다. 블러 뒤의 Laplacian 값은 작아지므로, Sobel 에 맞춘 기준 100 이 Laplacian 에는 높았던 것입니다. 방법을 바꾸면 기준도 그 방법에 맞춰 다시 고릅니다.