ROS
허프변환 기반 차선인식
mihee
2022. 4. 10. 18:14
Image Spcae vs Parameter Space
- Image Space (x, y 좌표계)
- Image Space에서의 직선은 Parameter Space에서 점으로 표현할 수 있음
- Parameter Space (m(기울기), b(y절편) 좌표계)
- Parameter Space에서의 두 직선의 교점은 m, b가 같은 경우에 생김
- Image Space에서 두 점을 지나는 직선을 의미
- 직선을 찾는 방법
- Canny를 통해 edge를 찾고 그 edge의 점들을 parameter Space로 표현
- Parameter Space에서 겹치는 직선이 많은 교점수록 그 교점이 의미하는 Image Space에서의 직선이 존재할 가능성이 높음
- Parameter Spcae의 한계
- 기울기가 무한대인 직선은 표현이 어려움(m = 무한대)
- Hough Space를 도입
Hough Space의 점
- 원점에서 직선에 수선의 발을 내려서 수선을 긋고, 원점과 직선의 거리 p와 수선과 x축과의 각도 세타로 직선을 표현
- Parameter Space와 마찬가지로 Hough Space에서의 점은 Image Space에서의 직선을 의미
- Hough Space에서는 Image Space의 점이 곡선으로 표현됨.
- 기울기가 무한대인 직선도 표현 가능
- Hough Space에서 곡선이 많이 겹치는 교점일수록 Image Space에서 직선이 존재할 확률이 높음.
Hough Transform
- 세 점의 Angle과 Distance 값
- 한 점을 지나는 여러 각도(0, 30, 60, 90, 120, 150)의 선과 원점간의 거리를 구해보면
- 쎄타 값을 6개만 넣어서 로우 값을 구한다 --> Hough 스페이스에 곡선을 그린다
- 곡선들이 만나는 점(if 쎄타 = 60, p = 80)
- 분홍색 선이 3개의 점을 모두 지나는 직선임.
- 한 점을 지나는 여러 각도(0, 30, 60, 90, 120, 150)의 선과 원점간의 거리를 구해보면
Hough Transform을 이용한 직선 검출 방식
- 입력영상을 흑백 Grayscale 변환 처리
- Canny Edge 처리로 외곽선 영상을 획득
- 로우와 쎄타 간격 설정
- 외곽선 점들에 대해서 (p, Θ) 좌표값 구하기
- 오차범위 내의 (p, Θ) 좌표값을 갖는 외곽선 점들이 하나의 직선을 구성한다고 판정.
HoughLines 함수
- cv2.HoughLines(image, rho, theta, threshold)
- image : 8bit, 흑백 이미지
- rho : hough space에서 얼마만큼 p를 증가시키면서 조사할 것인지
- theta : hough space에서 얼마만큼 Θ를 증가시키면서 조사할 것인지
- threshold : hough space에서 threshold 이상의 직선이 겹치는 교점은 하나의 직선을 형성한다고 판단함.
- 검출된 직선의 p, Θ를 반환
- threshold가 높으면 (직선으로 인정하는 규정이 까다로우면)
- 검출되는 직선은 적지만 확실한 직선들이 검출됨
- threshold가 낮으면 (직선으로 인정하는 규정이 후하면)
- 많은 직선이 검출되지만 불확실한 직선들도 검출됨.
HoughLinesP 함수
- cv2.HoughLinesP(image, rho, theta, threshold, minLineLength, maxLineGap)
- minLineLength : 선분의 최소 길이, 이것보다 짧은 선분은 버린다.
- maxLineGap : 간격의 최대 길이, 이것보다 작은 간격은 하나의 선분으로 간주
- 검출된 선분의 시작점과 끝점의 좌표를 반환
허프변환을 이용한 차선 찾기
- Image Read - 카메라 영상신호를 이미지로 읽기
- GrayScale - 흑백 이미지로 변환
- Gaussian Blur - 노이즈 제거
- Canny - edge 검출
- ROI - 관심영역 잘라내기
- HoughLinesP - 선분 검출
#! /usr/bin/env python3
# _*_ coding: utf-8 _*_
#import rospy
import numpy as np
import cv2, random, math, time
Width = 640
Height = 480
Offset = 420 # ROI 영역 = 세로 480 크기에서 420~460, 40 픽셀 만큼 잘라서 사용
Gap = 40
# 허프변환 함수로 검출된 모든 선분을 알록달록하게 출력
# draw lines
def draw_lines(img, lines):
global Offset
for line in lines:
x1, y1, x2, y2 = line[0]
color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
img = cv2.line(img, (x1, y1+Offset), (x2, y2+Offset), color, 2)
return img
#draw rectangle
def draw_rectangle(img, lpos, rpos, offset=0):
center = int(np.float(lpos + rpos) / 2)
cv2.rectangle(img, (lpos - 5, 15 + offset),
(lpos + 5, 25 + offset),
(0, 255, 0), 2)
cv2.rectangle(img, (rpos - 5, 15 + offset),
(rpos + 5, 25 + offset),
(0, 255, 0), 2)
cv2.rectangle(img, ((center - 5), (15 + offset)),
((center + 5), (25 + offset)),
(0, 255, 0), 2)
cv2.rectangle(img, (315 - 5, 15 + offset),
(325 + 5, 25 + offset),
(0, 0, 255), 2)
return img
# left lines, right lines
def divide_left_right(lines):
global Width
low_slope_threshold = 0
high_slope_threshold = 10
slopes = []
new_lines = []
# 선분의 기울기를 구해서 기울기 절대값이 10 이하인 것만 추출
for line in lines:
x1, y1, x2, y2 = line[0]
if x2 - x1 == 0:
slope = 0
else:
slope = float(y2-y1) / float(x2-x1)
if (abs(slope) > low_slope_threshold) and (abs(slope) < high_slope_threshold):
slopes.append(slope)
new_lines.append(line[0])
# 허프변환 함수로 검출한 선분들의 기울기를 비교하여 왼쪽 차선과 오른쪽 차선을 구분
# divide lines left to right
left_lines = []
right_lines = []
for j in range(len(slopes)):
Line = new_lines[j]
slope = slopes[j]
x1, y1, x2, y2 = Line
if (slope < 0) and (x2 < Width/2 - 90): # 왼쪽 선분
left_lines.append([Line.tolist()])
elif (slope > 0) and (x1 > Width/2 + 90): # 오른쪽 선분
right_lines.append([Line.tolist()])
return left_lines, right_lines
# get average m, b of lines
def get_line_params(lines):
# sum of x, y, m
x_sum = 0.0
y_sum = 0.0
m_sum = 0.0
size = len(lines)
if size == 0:
return 0,0
for line in lines:
x1, y1, x2, y2 = line[0]
x_sum += x1 + x2
y_sum += y1 + y2
m_sum += float(y2 - y1) / float(x2 - x1)
x_avg = x_sum / (size * 2)
y_avg = y_sum / (size * 2)
m = m_sum / size
b = y_avg - m * x_avg
return m, b
# get lpos, rpos
def get_line_pos(img, lines, left=False, right=False):
global Width, Height
global Offset, Gap
m, b = get_line_params(lines)
# 차선이 인식되지 않으면 left, right를 0과 width값으로 설정(화면의 끝)
if m == 0 and b == 0:
if left:
pos = 0
if right:
pos = Width
else:
y = Gap / 2
pos = (y - b) / m
# y값이 480의 x1과 y값이 240의 x2를 구해 두 점을 잇는 파란색 선을 그림.
b += Offset
x1 = (Height - b) / float(m)
x2 = ((Height/2) -b) / float(m)
cv2.line(img, (int(x1), Height), (int(x2), int(np.float(Height/2))), (255, 0, 0), 3)
return img, int(pos)
# show image and return lpos, rpos
def process_image(frame):
global Width
global Offset, Gap
#gray
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# blur
kernel_size = 5
blur_gray = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
# canny edge
low_threshold = 60
high_threshold = 70
edge_img = cv2.Canny(np.uint8(blur_gray), low_threshold, high_threshold)
# HoughLinesP
roi = edge_img[Offset : Offset+Gap, 0 : Width]
all_lines = cv2.HoughLinesP(roi, 1, math.pi/180, 30, 30, 10)
# divide left, right lines
if all_lines is None:
return 0, 640
left_lines, right_lines = divide_left_right(all_lines)
# get center of lines
frame, lpos = get_line_pos(frame, left_lines, left=True)
frame, rpos = get_line_pos(frame, right_lines, right=True)
# draw lines
frame = draw_lines(frame, left_lines)
frame = draw_lines(frame, right_lines)
frame = cv2.line(frame, (230, 235), (410, 235), (255, 255, 255), 2)
# draw rectangle
frame = draw_rectangle(frame, lpos, rpos, offset=Offset)
cv2.imshow('calibration', frame)
return lpos, rpos
def start():
global image, Width, Height
cap = cv2.VideoCapture('xycar_track1.mp4')
#while not rospy.is_shutdoown():
while True:
ret, image = cap.read()
time.sleep(0.03)
lpos, rpos = process_image(image)
print(lpos, rpos)
#center = ((pos[0]+ pos[1]) / 2)
#angle = 320 - center
#steer_angle = angle * 0.4
#draw_steer(frame, steer_angle)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
if __name__ == '__main__':
start()