10장. Sobel 과 Laplacian 경계 검출
실습 1 — 한 줄 밝기로 기울기 계산하기
도형 그림(ch10_shapes.png)의 가로줄 하나를 꺼내 옆 칸끼리 손으로 빼 보고, cv2.filter2D 에 [-1, 0, 1] 커널을 넣은 결과와 같은지 확인합니다. 마지막으로 밝기와 기울기를 위아래 그래프로 견줍니다.
1단계. 그림을 읽고 한 줄 꺼내기
import cv2
import numpy as np
# 연습용 도형 그림을 흑백으로 읽는다. OpenCV 로 그려 만든 가상의 그림이다
img = cv2.imread("ch10_shapes.png", cv2.IMREAD_GRAYSCALE)
print("모양:", img.shape, "/ 자료형:", img.dtype)
# 가로줄 80: 네모(가로 60~139)와 원(가로 190~270 쯤)을 가로지른다
row = img[80]
print("네모 왼쪽 변 둘레(가로 56~63):", [int(v) for v in row[56:64]])
print("네모 오른쪽 변 둘레(가로 136~143):", [int(v) for v in row[136:144]])모양: (240, 320) / 자료형: uint8
네모 왼쪽 변 둘레(가로 56~63): [50, 50, 50, 50, 200, 200, 200, 200]
네모 오른쪽 변 둘레(가로 136~143): [200, 200, 200, 200, 50, 50, 50, 50]2단계. 옆 칸끼리 손으로 빼기
# uint8 배열끼리 빼면 음수가 0~255 안으로 돌아간다(50 - 200 → 106)
# row[140:141] 처럼 한 칸짜리 배열로 꺼내 뺀다
wrapped = row[140:141] - row[139:140]
print("uint8 그대로 뺀 값(가로 139 → 140):", int(wrapped[0]))
# 정수(int)로 바꾼 뒤 빼야 음수가 그대로 남는다
r = row.astype(int)
print("int 로 바꿔 뺀 값(가로 139 → 140):", int(r[140] - r[139]))
# np.diff: 옆 칸끼리 뺀 값. diff[k] = r[k+1] - r[k] 이라 칸이 하나 줄어든다
diff = np.diff(r)
print("np.diff 의 길이:", len(diff))
# 0 이 아닌 곳: 값이 바뀌는 칸 사이의 왼쪽 칸 번호
where = np.nonzero(diff)[0]
print("밝기가 바뀌는 곳:", [(int(k), int(diff[k])) for k in where])uint8 그대로 뺀 값(가로 139 → 140): 106
int 로 바꿔 뺀 값(가로 139 → 140): -150
np.diff 의 길이: 319
밝기가 바뀌는 곳: [(59, 150), (139, -150), (189, 90), (270, -90)]3단계. filter2D 의 [-1, 0, 1] 과 견주기
# 1줄 3칸짜리 차이 커널: 칸마다 오른쪽 이웃 - 왼쪽 이웃
kx = np.array([[-1, 0, 1]], dtype=np.float32)
# filter2D(그림, 결과 자료형, 커널): CV_64F 로 받아야 음수가 남는다
gx = cv2.filter2D(img, cv2.CV_64F, kx)
line = gx[80]
# 손으로 같은 계산: 두 칸 오른쪽 값 - 원래 값을 가운데 칸에 적는다
by_hand = r[2:] - r[:-2]
# 가장자리 두 칸을 뺀 나머지가 같은지 본다
print("손계산과 같은가:", np.array_equal(line[1:-1], by_hand))
# 0 이 아닌 칸: 경계마다 두 칸씩 나온다
nz = np.nonzero(line)[0]
print("0 이 아닌 칸:", [int(k) for k in nz])
print("그 칸의 값:", [int(line[k]) for k in nz])손계산과 같은가: True
0 이 아닌 칸: [59, 60, 139, 140, 189, 190, 270, 271]
그 칸의 값: [150, 150, -150, -150, 90, 90, -90, -90]4단계. 밝기와 기울기를 위아래로 그리기
import matplotlib.pyplot as plt
# 그래프의 한글이 깨지지 않게 맑은 고딕을 쓰고, 빼기 기호가 네모로 나오지 않게 한다
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# sharex=True: 두 그래프가 같은 가로축(칸 번호)을 쓴다
fig, axes = plt.subplots(2, 1, figsize=(9, 5), sharex=True)
axes[0].plot(r)
axes[0].set_ylabel("밝기")
axes[0].set_title("가로줄 80 의 밝기")
axes[1].plot(line)
axes[1].axhline(0, color="gray", lw=0.8) # 0 기준선
axes[1].set_ylabel("기울기 (오른쪽 - 왼쪽)")
axes[1].set_xlabel("가로 칸 번호")
axes[1].set_title("[-1, 0, 1] 커널로 잰 가로 기울기")
plt.tight_layout()
plt.show()![가로줄 80 의 밝기 계단과 [-1, 0, 1] 커널로 잰 가로 기울기를 위아래로 그린 그래프](assets/img/nb/ch10_lab1_1.png)
코드 설명
row[140:141] - row[139:140]처럼uint8배열끼리 빼면 50 − 200 = −150 이 0~255 안으로 돌아가 106 이 됩니다.astype(int)로 바꾼 뒤 빼야 −150 이 그대로 남습니다.np.diff(r)는 옆 칸끼리 뺀 값 319개를 돌려줍니다. 0 이 아닌 곳은 네 군데뿐입니다. 가로 59 → 60 은 +150(네모로 들어감), 139 → 140 은 −150(네모에서 나옴), 189 → 190 은 +90, 270 → 271 은 −90(원)입니다. 원은 바탕과의 차이가 90 이라 값도 작습니다.cv2.filter2D(img, cv2.CV_64F, kx)의 결과는 손계산(r[2:] - r[:-2])과 똑같습니다(True). 경계마다 두 칸(59·60, 139·140, 189·190, 270·271)이 같은 값을 받습니다.- 그래프의 아래 줄에서 기울기는 고른 곳에서 0 이고, 밝기 계단마다 뾰족하게 솟습니다. 밝아지는 곳은 위(+), 어두워지는 곳은 아래(−)로 솟고, 계단이 높을수록(150 대 90) 더 높이 솟습니다.