10장. Sobel 과 Laplacian 경계 검출
실습 4 — Laplacian 으로 경계 찾기
cv2.Laplacian 으로 두 번 뺀 값을 구해 손계산과 견주고, 값이 0 을 지나는 곳(영교차)이 경계 자리와 맞는지 확인합니다. 4-이웃·8-이웃 커널과 ksize 에 따른 차이도 봅니다.
1단계. 한 줄에서 손계산과 견주기
import cv2
import numpy as np
img = cv2.imread("ch10_shapes.png", cv2.IMREAD_GRAYSCALE)
# Laplacian(그림, 결과 자료형, ksize): ksize=1 이면 4-이웃 커널
lap = cv2.Laplacian(img, cv2.CV_64F, ksize=1)
print("가로줄 80, 가로 57~62:", [int(v) for v in lap[80, 57:63]])
# 손계산: 왼쪽 + 오른쪽 - 2 × 자기 (세로로는 바뀌지 않는 줄이라 가로만 본다)
r = img[80].astype(int)
for x in (59, 60):
print(f"가로 {x}: {r[x - 1]} + {r[x + 1]} - 2×{r[x]} = {r[x - 1] + r[x + 1] - 2 * r[x]}")가로줄 80, 가로 57~62: [0, 0, 150, -150, 0, 0]
가로 59: 50 + 200 - 2×50 = 150
가로 60: 50 + 200 - 2×200 = -1502단계. 0 을 지나는 곳 찾기
line = lap[80]
# 옆 칸끼리 곱해 음수면 부호가 바뀐 것: 그 사이에서 0 을 지난다
cross = np.nonzero(line[:-1] * line[1:] < 0)[0]
print("0 을 지나는 곳(왼쪽 칸 번호):", [int(k) for k in cross])
for k in cross:
print(f" 가로 {k}~{k + 1}: Laplacian {int(line[k])} → {int(line[k + 1])}, 밝기 {r[k]} → {r[k + 1]}")0 을 지나는 곳(왼쪽 칸 번호): [59, 139, 189, 270]
가로 59~60: Laplacian 150 → -150, 밝기 50 → 200
가로 139~140: Laplacian -150 → 150, 밝기 200 → 50
가로 189~190: Laplacian 90 → -270, 밝기 50 → 140
가로 270~271: Laplacian -270 → 90, 밝기 140 → 503단계. 4-이웃·8-이웃과 ksize 견주기
# 4-이웃 커널을 filter2D 로 직접 걸면 Laplacian(ksize=1) 과 같다
k4 = np.array([[0, 1, 0], [1, -4, 1], [0, 1, 0]], dtype=np.float32)
k8 = np.array([[1, 1, 1], [1, -8, 1], [1, 1, 1]], dtype=np.float32)
print("4-이웃 filter2D 와 같은가:", np.array_equal(cv2.filter2D(img, cv2.CV_64F, k4), lap))
lap8 = cv2.filter2D(img, cv2.CV_64F, k8)
# ksize 3 은 무게가 다른 3×3 커널, ksize 5 는 블러를 섞은 5×5 커널이다
results = {"4-이웃 (ksize 1)": lap, "8-이웃": lap8,
"ksize 3": cv2.Laplacian(img, cv2.CV_64F, ksize=3),
"ksize 5": cv2.Laplacian(img, cv2.CV_64F, ksize=5)}
for name, L in results.items():
# 가장 큰 절댓값과, 가로줄 80 의 네모 왼쪽 변 둘레에서 0 이 아닌 칸 수
print(f"{name}: 가장 큰 절댓값 {int(np.abs(L).max())}, 왼쪽 변 둘레의 0 이 아닌 칸 {int((L[80, 50:71] != 0).sum())}")4-이웃 filter2D 와 같은가: True
4-이웃 (ksize 1): 가장 큰 절댓값 540, 왼쪽 변 둘레의 0 이 아닌 칸 2
8-이웃: 가장 큰 절댓값 1080, 왼쪽 변 둘레의 0 이 아닌 칸 2
ksize 3: 가장 큰 절댓값 1080, 왼쪽 변 둘레의 0 이 아닌 칸 2
ksize 5: 가장 큰 절댓값 7200, 왼쪽 변 둘레의 0 이 아닌 칸 44단계. 밝기와 Laplacian 을 함께 그리기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
xs = np.arange(40, 160)
fig, axes = plt.subplots(2, 1, figsize=(9, 5), sharex=True)
axes[0].plot(xs, r[40:160])
axes[0].set_ylabel("밝기")
axes[0].set_title("가로줄 80 의 밝기 (가로 40~159)")
axes[1].plot(xs, line[40:160], marker="o", ms=3)
axes[1].axhline(0, color="gray", lw=0.8)
# 0 을 지나는 곳에 세로 점선을 긋는다
for k in cross:
if 40 <= k < 160:
axes[1].axvline(k + 0.5, color="gray", ls=":")
axes[1].set_ylabel("Laplacian")
axes[1].set_xlabel("가로 칸 번호")
axes[1].set_title("Laplacian (ksize 1): 경계 양옆에 + 와 -")
plt.tight_layout()
plt.show()
코드 설명
cv2.Laplacian(img, cv2.CV_64F, ksize=1)은 가로줄 80 의 가로 59 에서 150, 가로 60 에서 −150 을 돌려줍니다. 손계산 50 + 200 − 2×50 = 150, 50 + 200 − 2×200 = −150 과 같습니다.- 옆 칸끼리 곱해 음수인 곳, 곧 0 을 지나는 곳은 가로 59~60, 139~140, 189~190, 270~271 입니다. 실습 1에서
np.diff로 찾은 밝기가 바뀌는 네 자리와 똑같습니다. 원에서는 90 → −270 처럼 크기가 다른데, 원의 가장 왼쪽 점이라 위·아래 칸이 바탕이어서 세로 두 번 빼기가 함께 더해졌기 때문입니다. - 4-이웃 커널을
cv2.filter2D로 걸면cv2.Laplacian(ksize=1)과 똑같습니다(True). 가장 큰 절댓값은 4-이웃 540, 8-이웃과 ksize 3 은 1,080, ksize 5 는 7,200 입니다. 왼쪽 변 둘레에서 0 이 아닌 칸은 ksize 5 만 4칸이고 나머지는 2칸이라, 넓은 커널일수록 경계가 굵어집니다. - 그래프에서 Laplacian 은 경계마다 + 와 − 가 한 칸씩 붙어 나오고, 그 사이의 점선(0 을 지나는 곳)이 밝기 계단의 자리와 맞습니다. 밝아지는 경계는 + 다음 −, 어두워지는 경계는 − 다음 + 입니다.