10장. Sobel 과 Laplacian 경계 검출
프로젝트 1 — 도로 그림에서 차선과 정지선 찾기
운전 연수 수업을 준비하는 강사가, 위에서 내려다본 도로 그림으로 "컴퓨터가 차선을 어떻게 찾는지" 보여 주려고 합니다. 도로에는 세로로 뻗은 차선과 가로로 그어진 정지선, 횡단보도가 있습니다. Sobel 의 가로·세로 기울기로 경계를 찾고, 경계를 방향에 따라 "세로 선(차선 쪽)"과 "가로 선(정지선 쪽)"으로 나눠 다른 색으로 칠한 그림을 저장해 보세요. 방향마다 경계 칸이 몇 개인지도 표로 정리합니다.
그림은 ch10_road.png 하나입니다. OpenCV 로 도형을 그려 만든 가상의 도로이고, 실제 장소나 사람과 관계없습니다.
| 그림 속 요소 | 자리와 모양 | 경계의 방향 |
|---|---|---|
| 풀밭과 가장자리 흰 실선 | 왼쪽 끝과 오른쪽 끝 | 세로 |
| 가운데 노란 두 줄 | 가로 153~156, 163~166 | 세로 |
| 흰 점선 차선 | 가로 90~93(정지선 위쪽만), 226~229 | 세로(점선의 끝은 가로) |
| 정지선 | 세로 160~171, 왼쪽 길을 가로지름 | 가로 |
| 횡단보도 흰 띠 세 줄 | 세로 186~229, 가로로 긴 띠 | 가로(띠의 양 끝은 세로) |
| 자동차 한 대 | 오른쪽 길, 위에서 본 네모 | 세로와 가로 |
요구사항
ch10_road.png를 컬러로 읽고 흑백으로 바꾼 뒤, Gaussian 5×5 블러를 겁니다.- Sobel(ksize 3,
CV_64F)로 가로·세로 기울기를 구하고,cv2.magnitude로 크기를 구합니다. - 크기가 기준(200)을 넘는 칸을 경계로 봅니다. 경계 칸 가운데 |gx| 가 |gy| 보다 큰 칸은 "세로 선", 나머지는 "가로 선"으로 나눕니다.
- 원본 컬러 그림 위에 세로 선은 빨강(BGR 로
(0, 0, 255)), 가로 선은 파랑((255, 0, 0))으로 칠해out_ch10_lanes.png로 저장하고 띄웁니다. - 방향마다 경계 칸 수, 경계 칸 가운데 비율, 그림 전체에서의 비율을 표로 찍습니다.
- 가로 선 칸이 가장 많은 가로줄 다섯 개를 찍어, 정지선과 횡단보도의 자리와 맞는지 확인합니다.
힌트
- 방향 나누기는
np.abs(gx) > np.abs(gy)로 True/False 판을 만들고, 경계 판과&로 겹칩니다. - 칠하기는
out = img.copy()뒤out[vertical] = (0, 0, 255)처럼 True 인 칸에 색을 한꺼번에 넣습니다. - 경계 칸 수는
int(mask.sum()), 비율은round(float(...), 3)으로 찍습니다. - 가로줄마다 가로 선 칸 수는
horizontal.sum(axis=1)로, 많은 순서는np.argsort(...)[::-1][:5]로 얻습니다. - 저장(
cv2.imwrite)은 상대 경로로 하고, Matplotlib 으로 띄울 때는cv2.COLOR_BGR2RGB로 바꿉니다.
채점 기준
| 항목 | 기준 | 점수 |
|---|---|---|
| 읽기와 블러 | 컬러로 읽고 흑백·블러 5×5 를 건다 | 10 |
| 기울기와 크기 | CV_64F 로 gx, gy 를 구하고 cv2.magnitude 로 합친다 | 20 |
| 방향 나누기 | 기준을 넘는 칸을 |gx|, |gy| 로 세로·가로로 나눈다 | 25 |
| 결과 그림 | 두 색으로 칠해 저장하고 띄운다 | 20 |
| 표와 확인 | 방향별 칸 수·비율 표와 가로줄 다섯 개를 찍고 해석한다 | 25 |
코드의 흐름
그림의 다섯 단계를 예시 풀이의 단계와 같은 순서로 짰습니다. 넷째 단계가 이 프로젝트의 핵심입니다. 세로로 선 차선은 가로로 가로지를 때 밝기가 바뀌므로 |gx| 가 크고, 가로로 누운 정지선은 |gy| 가 큽니다. 본문의 "빼는 방향과 찾아지는 선의 방향은 직각"을 그대로 씁니다.
실제 데이터로 해 보려면
실제 도로 사진은 카메라가 앞을 비스듬히 보므로 차선이 세로가 아니라 먼 곳으로 모이는 비스듬한 선으로 찍힙니다. 이때는 |gx| 와 |gy| 를 견주는 대신 cv2.phase 로 각도를 구해 "차선이 있을 법한 각도 범위"로 나눕니다. 연습용 도로 영상은 AI Hub(aihub.or.kr)처럼 공개 데이터를 내려받을 수 있는 곳을 찾습니다. 내려받기 전에 이용 약관(상업적 이용, 재배포 금지)을 확인하고, 사람의 얼굴이나 자동차 번호판이 찍힌 사진은 개인정보 처리 기준을 먼저 확인합니다. 그림자, 비에 젖은 노면, 낡은 차선처럼 실제 사진에는 이 가상 그림에 없는 경계가 많으므로, 기준값은 사진을 몇 장 훑어본 뒤 다시 정합니다.
결과 보기 (예시 풀이)
1단계. 읽고 기울기 구하기
import cv2
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Malgun Gothic"
plt.rcParams["axes.unicode_minus"] = False
# 요구사항 1: 컬러로 읽고, 흑백으로 바꾸고, 블러를 건다
img = cv2.imread("ch10_road.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
print("모양:", img.shape)
print("밝기: 아스팔트", int(gray[120, 60]), "/ 흰 선", int(gray[120, 30]), "/ 노란 선", int(gray[120, 154]))
# 요구사항 2: 가로·세로 기울기와 크기
gx = cv2.Sobel(blur, cv2.CV_64F, 1, 0, ksize=3)
gy = cv2.Sobel(blur, cv2.CV_64F, 0, 1, ksize=3)
mag = cv2.magnitude(gx, gy)
print("크기의 가장 큰 값:", round(float(mag.max()), 1))모양: (240, 320, 3)
밝기: 아스팔트 70 / 흰 선 235 / 노란 선 192
크기의 가장 큰 값: 412.02단계. 경계를 방향별로 나누기
# 요구사항 3: 크기가 기준을 넘는 칸이 경계
T = 200
edge = mag > T
# |gx| 가 크면 가로로 가로지를 때 밝기가 바뀐다 → 세로로 선 선
vertical = edge & (np.abs(gx) > np.abs(gy))
horizontal = edge & ~(np.abs(gx) > np.abs(gy)) # ~ 는 True/False 를 뒤집는다
print("경계 칸:", int(edge.sum()), "/ 세로 선:", int(vertical.sum()), "/ 가로 선:", int(horizontal.sum()))경계 칸: 10176 / 세로 선: 6540 / 가로 선: 36363단계. 두 색으로 칠해 저장하기
# 요구사항 4: 원본 위에 세로 선은 빨강, 가로 선은 파랑(BGR 순서)
out = img.copy()
out[vertical] = (0, 0, 255)
out[horizontal] = (255, 0, 0)
print("저장:", cv2.imwrite("out_ch10_lanes.png", out))
fig, axes = plt.subplots(1, 2, figsize=(10, 3.8))
axes[0].imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
axes[0].set_title("도로 그림 (가상)")
axes[1].imshow(cv2.cvtColor(out, cv2.COLOR_BGR2RGB))
axes[1].set_title("세로 선 빨강, 가로 선 파랑")
for ax in axes:
ax.axis("off")
plt.tight_layout()
plt.show()저장: True
4단계. 방향별 표와 가로줄 확인
# 요구사항 5: 방향별 칸 수와 비율
total = img.shape[0] * img.shape[1]
rows = []
for name, m in [("세로 선", vertical), ("가로 선", horizontal), ("합계", edge)]:
n = int(m.sum())
rows.append({"방향": name, "칸 수": n,
"경계 가운데 비율": round(n / int(edge.sum()), 3),
"전체 가운데 비율": round(n / total, 3)})
print(pd.DataFrame(rows).to_string(index=False))
# 요구사항 6: 가로 선 칸이 가장 많은 가로줄 다섯 개
per_row = horizontal.sum(axis=1)
top5 = np.argsort(per_row)[::-1][:5]
print("가로 선이 많은 가로줄:", [(int(y), int(per_row[y])) for y in top5]) 방향 칸 수 경계 가운데 비율 전체 가운데 비율
세로 선 6540 0.643 0.085
가로 선 3636 0.357 0.047
합계 10176 1.000 0.133
가로 선이 많은 가로줄: [(159, 124), (160, 124), (172, 120), (171, 120), (173, 118)]방향별 표를 보면 기준 200 을 넘은 경계 칸 10,176개(그림 전체의 0.133) 가운데 세로 선이 6,540개(0.643), 가로 선이 3,636개(0.357)입니다. 이 도로는 차선이 위아래로 길게 뻗어 있으므로 세로 선이 더 많은 것이 맞습니다. 가로 선이 가장 많은 가로줄은 159·160(124칸)과 171·172(120칸), 173(118칸)으로, 정지선(세로 160~171)의 위 끝과 아래 끝입니다. 횡단보도의 띠도 가로줄마다 106칸씩 가로 선으로 잡혔지만, 띠의 길이(가로 40~145)가 정지선보다 짧아 다섯 안에 들지 못했습니다. 결과 그림에서도 차선과 가장자리 실선, 노란 두 줄은 빨강, 정지선과 횡단보도의 긴 변은 파랑으로 칠해졌고, 점선과 띠의 짧은 끝만 반대 색입니다. 한편 오른쪽 길의 자동차는 전혀 칠해지지 않았습니다. 자동차의 짙은 빨강은 흑백 밝기가 93 으로 아스팔트(70)와 비슷해, 블러 뒤의 기울기 크기가 가장 커도 84 라 기준 200 에 한참 못 미쳤기 때문입니다. 경계 검출은 "색이 다른 곳"이 아니라 "밝기가 다른 곳"을 찾는다는 점과, 기준을 낮추면 자동차는 잡히지만 그만큼 다른 약한 경계도 함께 늘어난다는 점을 수업에서 짚어 줄 만합니다. 선이 경계 폭만큼 굵게 칠해진 것은 11장의 Canny 로 다듬어 봅니다.