10장. Sobel 과 Laplacian 경계 검출
실습 3 — 기울기 크기 합치기와 선만 남기기
가로·세로 기울기를 cv2.magnitude 로 합쳐 피타고라스 손계산과 맞는지 확인하고, cv2.addWeighted 로 어림한 값과 견줍니다. 마지막으로 크기에 기준을 걸어 경계 지도를 만들고 out_ch10_edges.png 로 저장합니다.
1단계. magnitude 와 피타고라스 손계산
import cv2
import numpy as np
img = cv2.imread("ch10_shapes.png", cv2.IMREAD_GRAYSCALE)
gx = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
gy = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
# magnitude: 칸마다 √(gx² + gy²)
mag = cv2.magnitude(gx, gy)
# 네모의 왼쪽 위 모서리(세로 40, 가로 60): 두 방향이 함께 바뀐다
x, y = float(gx[40, 60]), float(gy[40, 60])
print("gx, gy:", int(x), int(y))
print("손계산 √(gx² + gy²):", round((x ** 2 + y ** 2) ** 0.5, 1))
print("cv2.magnitude:", round(float(mag[40, 60]), 1))
# np.sqrt 로 그림 전체를 계산해도 같은지 본다. allclose: 끝자리 작은 차이는 같다고 본다
print("np.sqrt 와 같은가:", np.allclose(mag, np.sqrt(gx ** 2 + gy ** 2)))
print("크기의 가장 큰 값:", round(float(mag.max()), 1))gx, gy: 450 450
손계산 √(gx² + gy²): 636.4
cv2.magnitude: 636.4
np.sqrt 와 같은가: True
크기의 가장 큰 값: 805.02단계. addWeighted 어림값과 견주기
# addWeighted(그림1, 무게1, 그림2, 무게2, 더할 값): 그림1×무게1 + 그림2×무게2 + 더할 값
approx = cv2.addWeighted(np.abs(gx), # 그림1: 가로 기울기의 절댓값
0.5, # 무게1
np.abs(gy), # 그림2: 세로 기울기의 절댓값
0.5, # 무게2
0) # 더할 값
# 원의 테두리(밝기 차이 90)에서 세 자리를 견준다: 왼쪽(곧은 세로), 위(곧은 가로), 왼쪽 위(45도 비스듬)
# 왼쪽 위 (52, 202) 는 원의 가운데 (80, 230) 에서 왼쪽 위 대각선으로 반지름만큼 간 자리다
spots = {"왼쪽": (80, 190), "위": (40, 230), "왼쪽 위": (52, 202)}
for name, (yy, xx) in spots.items():
print(f"{name} ({yy}, {xx}): gx {int(gx[yy, xx])}, gy {int(gy[yy, xx])}")
for name, (yy, xx) in spots.items():
m, a = float(mag[yy, xx]), float(approx[yy, xx])
print(f"{name} ({yy}, {xx}): 크기 {m:.1f}, 어림값 {a:.1f}, 어림값/크기 {a / m:.3f}")왼쪽 (80, 190): gx 360, gy 0
위 (40, 230): gx 0, gy 360
왼쪽 위 (52, 202): gx 270, gy 270
왼쪽 (80, 190): 크기 360.0, 어림값 180.0, 어림값/크기 0.500
위 (40, 230): 크기 360.0, 어림값 180.0, 어림값/크기 0.500
왼쪽 위 (52, 202): 크기 381.8, 어림값 270.0, 어림값/크기 0.7073단계. 기준을 걸어 경계 지도 만들기
# 크기가 기준을 넘으면 255, 아니면 0 인 경계 지도
for t in (50, 100, 200, 400):
edges_t = np.where(mag > t, 255, 0).astype(np.uint8)
print(f"기준 {t}: 경계 칸 {int((edges_t == 255).sum())}, 비율 {float((edges_t == 255).mean()):.3f}")
# 기준 100 의 경계 지도를 저장한다
edges = np.where(mag > 100, 255, 0).astype(np.uint8)
print("저장:", cv2.imwrite("out_ch10_edges.png", edges))기준 50: 경계 칸 3331, 비율 0.043
기준 100: 경계 칸 3327, 비율 0.043
기준 200: 경계 칸 2341, 비율 0.030
기준 400: 경계 칸 1593, 비율 0.021
저장: True4단계. 크기, 어림값, 경계 지도 견주어 보기
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# 두 그림을 같은 배율로 줄여 견준다: 크기의 가장 큰 값이 255 가 되게
alpha = 255 / mag.max()
fig, axes = plt.subplots(1, 3, figsize=(12, 3.4))
panels = [("magnitude (√)", cv2.convertScaleAbs(mag, alpha=alpha)),
("addWeighted (어림값)", cv2.convertScaleAbs(approx, alpha=alpha)),
("경계 지도 (크기 > 100)", edges)]
for ax, (title, arr) in zip(axes, panels):
ax.imshow(arr, cmap="gray", vmin=0, vmax=255)
ax.set_title(title)
ax.axis("off")
plt.tight_layout()
plt.show()
코드 설명
- 네모의 왼쪽 위 모서리는 gx 450, gy 450 이고, 손계산 √(450² + 450²) 와
cv2.magnitude가 모두 636.4 입니다.np.sqrt(gx ** 2 + gy ** 2)로 그림 전체를 계산해도 같습니다(True). - 원의 곧은 왼쪽(gx 360)과 위(gy 360)에서는 크기가 360, 어림값이 180 으로 어림값이 크기의 0.500 입니다. 45도 비스듬한 왼쪽 위(gx 270, gy 270)에서는 크기 381.8, 어림값 270 으로 0.707 입니다. 같은 원의 테두리인데 어림값은 방향에 따라 비율이 달라집니다.
- 이 그림은 노이즈가 없어 기준 50 과 100 의 경계 칸이 3,331개와 3,327개로 거의 같습니다. 기준 200 에서는 크기 120 인 계단 띠의 안쪽 선이 빠져 2,341개, 기준 400 에서는 원의 곧은 테두리(360)도 빠져 1,593개가 남습니다. 기준 100 의 경계 지도는
out_ch10_edges.png로 저장했습니다. - 그림에서 어림값(가운데)은 같은 배율로 줄였을 때 크기(왼쪽)보다 전체가 어둡고, 원의 테두리가 비스듬한 곳만 더 밝아 고르지 않게 보입니다. 경계 지도(오른쪽)는 0 과 255 두 값뿐입니다.