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개의 점을 모두 지나는 직선임.

Hough Transform을 이용한 직선 검출 방식

  1. 입력영상을 흑백 Grayscale 변환 처리
  2. Canny Edge 처리로 외곽선 영상을 획득
  3. 로우와 쎄타 간격 설정
  4. 외곽선 점들에 대해서 (p, Θ) 좌표값 구하기
  5. 오차범위 내의 (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 : 간격의 최대 길이, 이것보다 작은 간격은 하나의 선분으로 간주
    • 검출된 선분의 시작점과 끝점의 좌표를 반환

허프변환을 이용한 차선 찾기

  1. Image Read - 카메라 영상신호를 이미지로 읽기
  2. GrayScale - 흑백 이미지로 변환
  3. Gaussian Blur - 노이즈 제거
  4. Canny - edge 검출
  5. ROI - 관심영역 잘라내기
  6. 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()

hough_drive.py 실행 캡쳐 화면