OpenCV 비전 실무10장 · Sobel 과 Laplacian 경계 검출19 / 21← 이전목차다음 →MetaContents

10장. Sobel 과 Laplacian 경계 검출

프로젝트 1 — 도로 그림에서 차선과 정지선 찾기

운전 연수 수업을 준비하는 강사가, 위에서 내려다본 도로 그림으로 "컴퓨터가 차선을 어떻게 찾는지" 보여 주려고 합니다. 도로에는 세로로 뻗은 차선과 가로로 그어진 정지선, 횡단보도가 있습니다. Sobel 의 가로·세로 기울기로 경계를 찾고, 경계를 방향에 따라 "세로 선(차선 쪽)"과 "가로 선(정지선 쪽)"으로 나눠 다른 색으로 칠한 그림을 저장해 보세요. 방향마다 경계 칸이 몇 개인지도 표로 정리합니다.

그림은 ch10_road.png 하나입니다. OpenCV 로 도형을 그려 만든 가상의 도로이고, 실제 장소나 사람과 관계없습니다.

그림 속 요소자리와 모양경계의 방향
풀밭과 가장자리 흰 실선왼쪽 끝과 오른쪽 끝세로
가운데 노란 두 줄가로 153~156, 163~166세로
흰 점선 차선가로 90~93(정지선 위쪽만), 226~229세로(점선의 끝은 가로)
정지선세로 160~171, 왼쪽 길을 가로지름가로
횡단보도 흰 띠 세 줄세로 186~229, 가로로 긴 띠가로(띠의 양 끝은 세로)
자동차 한 대오른쪽 길, 위에서 본 네모세로와 가로

요구사항

  1. ch10_road.png 를 컬러로 읽고 흑백으로 바꾼 뒤, Gaussian 5×5 블러를 겁니다.
  2. Sobel(ksize 3, CV_64F)로 가로·세로 기울기를 구하고, cv2.magnitude 로 크기를 구합니다.
  3. 크기가 기준(200)을 넘는 칸을 경계로 봅니다. 경계 칸 가운데 |gx| 가 |gy| 보다 큰 칸은 "세로 선", 나머지는 "가로 선"으로 나눕니다.
  4. 원본 컬러 그림 위에 세로 선은 빨강(BGR 로 (0, 0, 255)), 가로 선은 파랑((255, 0, 0))으로 칠해 out_ch10_lanes.png 로 저장하고 띄웁니다.
  5. 방향마다 경계 칸 수, 경계 칸 가운데 비율, 그림 전체에서의 비율을 표로 찍습니다.
  6. 가로 선 칸이 가장 많은 가로줄 다섯 개를 찍어, 정지선과 횡단보도의 자리와 맞는지 확인합니다.

힌트

  1. 방향 나누기는 np.abs(gx) > np.abs(gy) 로 True/False 판을 만들고, 경계 판과 & 로 겹칩니다.
  2. 칠하기는 out = img.copy() 뒤 out[vertical] = (0, 0, 255) 처럼 True 인 칸에 색을 한꺼번에 넣습니다.
  3. 경계 칸 수는 int(mask.sum()), 비율은 round(float(...), 3) 으로 찍습니다.
  4. 가로줄마다 가로 선 칸 수는 horizontal.sum(axis=1) 로, 많은 순서는 np.argsort(...)[::-1][:5] 로 얻습니다.
  5. 저장(cv2.imwrite)은 상대 경로로 하고, Matplotlib 으로 띄울 때는 cv2.COLOR_BGR2RGB 로 바꿉니다.

채점 기준

항목기준점수
읽기와 블러컬러로 읽고 흑백·블러 5×5 를 건다10
기울기와 크기CV_64F 로 gx, gy 를 구하고 cv2.magnitude 로 합친다20
방향 나누기기준을 넘는 칸을 |gx|, |gy| 로 세로·가로로 나눈다25
결과 그림두 색으로 칠해 저장하고 띄운다20
표와 확인방향별 칸 수·비율 표와 가로줄 다섯 개를 찍고 해석한다25

코드의 흐름

흑백 읽기와 블러, Sobel 기울기, 기준을 넘는 경계, 방향 나누기, 표시와 표의 다섯 단계를 이은 흐름도

그림의 다섯 단계를 예시 풀이의 단계와 같은 순서로 짰습니다. 넷째 단계가 이 프로젝트의 핵심입니다. 세로로 선 차선은 가로로 가로지를 때 밝기가 바뀌므로 |gx| 가 크고, 가로로 누운 정지선은 |gy| 가 큽니다. 본문의 "빼는 방향과 찾아지는 선의 방향은 직각"을 그대로 씁니다.

실제 데이터로 해 보려면

실제 도로 사진은 카메라가 앞을 비스듬히 보므로 차선이 세로가 아니라 먼 곳으로 모이는 비스듬한 선으로 찍힙니다. 이때는 |gx| 와 |gy| 를 견주는 대신 cv2.phase 로 각도를 구해 "차선이 있을 법한 각도 범위"로 나눕니다. 연습용 도로 영상은 AI Hub(aihub.or.kr)처럼 공개 데이터를 내려받을 수 있는 곳을 찾습니다. 내려받기 전에 이용 약관(상업적 이용, 재배포 금지)을 확인하고, 사람의 얼굴이나 자동차 번호판이 찍힌 사진은 개인정보 처리 기준을 먼저 확인합니다. 그림자, 비에 젖은 노면, 낡은 차선처럼 실제 사진에는 이 가상 그림에 없는 경계가 많으므로, 기준값은 사진을 몇 장 훑어본 뒤 다시 정합니다.

결과 보기 (예시 풀이)

1단계. 읽고 기울기 구하기

Python
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.0

2단계. 경계를 방향별로 나누기

Python
# 요구사항 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 / 가로 선: 3636

3단계. 두 색으로 칠해 저장하기

Python
# 요구사항 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단계. 방향별 표와 가로줄 확인

Python
# 요구사항 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 로 다듬어 봅니다.

← 이전 절10장 · 실습 5 — 노이즈 낀 그림에서 블러 먼저 하기다음 절 →10장 · 정리