[2023-04-11] 1. 허프변환 기반 차선인식
허프변환 기반 차선인식
허프 변환을 이용한 차선 추출
- Hough Transform
Image Space vs Parameter Space
-
Image Space : x, y 좌표계
- Image Space 에서의 직선은
- Parameter Space 에서 점으로 표현할 수 있음
- 기울기와 y 절편만 알면 Image Space 에서 직선을 그릴 수 있음
- Image Space 에서의 점은
- Parameter Space 에서 직선으로 표현할 수 있음
- 그 직선은 Image Space에서 (x1, y1) 점을 지나는 모든 직선을 의미함
- Image Space 에서의 직선은
-
Parameter Space : m(기울기), b(절편) 좌표계
-
Parameter Space에서 두 직선의 교점은
- m과 b가 같은 경우에 생김 (m = 기울기, b = 절편)
- Image Space에서 두 점을 지나는 직선을 의미
Image Space에서 직선을 찾는 방법
- Canny를 통해 edge를 찾고 그 edge의 점들을 Parameter Space로 표현
- Parameter Space에서 겹치는 직선이 많은 교점일수록 그 교점이 의미하는 Image Space에서의 직선이 존재할 가능성이 높음
- 사람은 한번에 직선을 찾을 수 있지만 컴퓨터는 다음과 같은 방법으로 찾아야 함
- 점들을 다 Parameter Space에서의 선으로 다 바꿈
- Paramter Space에서 선들이 많이 만나는 교점을 찾음
- 그 점의 기울기와 절편을 구하면 Image Space에서의 직선을 찾을 수 있음
하지만 기울기가 무한대인 직선은?
- m의 기울기가 존재하지 않으면 Parameter Space에서 어떻게 점을 찍나..?
- 그래서 Hough Space 도입
Hough Space
- 원점에서 직선에 수선의 발을 내려서 수선을 긋고 원점과 직선의 거리 로우(ρ)와 수선과 x축과의 각도 (θ)로 직선을 표현
- Parameter Space와 마찬가지로 Hough Space에서의 점은 Image Space에서의 직선을 의미함
- Hough Space의 곡선
- Hough Space에서는 Image Space의 점이 곡선으로 표현 됨
- 기울기가 무한대인 직선도 표현이 가능
- 그 이외의 의미하는 바는 Parameter Space와 모두 같음
- Hough Space에서 곡선이 많이 겹치는 교점일수록 Image Space에서는 직선이 존재할 확률이 높음
Hough Transform
- 3 점의 Angle과 Distance 값 (각도 θ , 로우 ρ)
- 한 점을 지나는 여러 각도(0, 30, 60, 90, 120, 150)의 선과 원점간의 거리를 구해보면
- 세타 값을 6개만 넣어서 로우 값을 구한다.
- 3개의 곡선들이 만나는 점은 분홍색 선을 의미하는 것으로, 3개의 점을 모두 지나는 직선임을 알 수 있음
- 한 점을 지나는 여러 각도(0, 30, 60, 90, 120, 150)의 선과 원점간의 거리를 구해보면
- 세타와 로우의 간격은 어떻게 할 것인가?
Hough Transform을 이용하여 직선 검출 방식
- 다음과 같은 순서로 진행하여 이미지에서 직선을 검출
- 입력 영상을 흑백 GrayScale 변환 처리
- Canny Edge 처리로 외곽선 영상을 획득
- ρ 와 θ 간격 설정
- 외곽선 점들에 대해서 (ρ, θ) 좌표값 구하기
- 오차범위 내의 (ρ, θ) 좌표값을 갖는 외곽선 점들이 하나의 직선을 구성한다고 판정
HoughLines 함수
- cv2.HoughLines(image, rho, theta, threshold)
- image : 8bit, 흑백 이미지 (대체적으로 Canny Edge를 한 흑백 이미지를 줌)
- rho : hough space에서 얼만큼 ρ를 증가시키면서 조사할 것인지
- theta : hough space에서 얼만큼 θ를 증가시키면서 조사할 것인지
- threshold : hough space에서 threshold 이상의 직선이 겹치는 교점은 하나의 직선을 형성한다고 판단
- threshold가 높다 : 직선으로 인정하는 규정이 까다롭다 -> 검출되는 직선은 적지만 확실한 직선들로 검출
- threshold가 낮다 : 직선으로 인정하는 규정이 후하다 -> 검출되는 직선은 많지만 불확실한 직선들도 검출
- 검출된 직선의 ρ, θ를 반환
HoughLinesP 함수
- cv2.HoughLinesP(image, rho, theta, threshold, mineLineLength, maxLineGap)
- minLineLength : 선분의 최소 길이, 이것보다 짧은 선분은 버림
- maxLineGap : 간격의 최대 길이, 이것보다 작은 간격은 하나의 선분으로 간주
-
선분 찾기
- 검출된 선분의 시작점과 끝점의 좌표를 반환
HoughLines vs HoughLinesP
- HoughLines 함수는 직선을 검출
- HoughLinesP 함수는 시작점과 끝점이 있는 선분을 검출
- 허프변환을 이용한 차선 찾기
- Image Read : 카메라 영상신호를 이미지로 읽기
- GrayScale : 흑백 이미지로 변환
- Gaussian Blur : 노이즈 제거
- Canny : edge 검출
- ROI : 관심영역 잘라내기
- HoughLinesP : 선분 검출
패키지 생성
$ catkin_create_pkg hough_drive std_msgs rospy
$ mkdir launch
파이썬 코드 작성 및 launch 파일
- hough_find.py 프로그램
- 영상에서 차선의 위치를 찾아 표시
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import rospy
import numpy as np
import cv2, random, math, time
# 영상 사이즈는 가로 x 세로 640 x 480
# ROI 영역 : 세로 480 크기에서 420~460, 40 픽셀만큼 잘라서 사용
Width = 640
Height = 480
Offset = 420
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 = (lpos + rpos) / 2
cv2.rectangle(img, (lpos - 5, 15 + offset), (lpos + 5, 25 + offset), (0, 255, 0), 2) # lpos 위치에 녹색 사각형 그리기
cv2.rectangle(img, (rpos - 5, 15 + offset), (rpos + 5, 25 + offset), (0, 255, 0), 2) # rpos 위치에 녹색 사각형 그리기
cv2.rectangle(img, (center-5, 15 + offset), (center+5, 25 + offset), (0, 255, 0), 2) # lpos, rpos 사이에 녹색 사각형 그리기
cv2.rectangle(img, (315, 15 + offset), (325, 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
# calculate slope & filtering with threshold
slopes = []
new_lines = []
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): # 선분의 기울기를 구해서, 기울기 절대값이 10 이하인 것만 추출 (원근법떄문에 차선이 비스듬하게 보이니깐)
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
# OpenCV 좌표계에서는 아래방향으로 y가 증가하므로 기울기 계산법이 다름
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)
# 허브변환 함수로 찾아낸 직선을 대상으로 Parameter Space (m,b 좌표계)에서 m의 평균값을 먼저 구하고 그걸로 b의 값을 구함
x_avg = x_sum / (size * 2)
y_avg = y_sum / (size * 2)
m = m_sum / size # 모든 선분 기울기의 평균 값
b = y_avg - m * x_avg # 모든 선분 y절편의 평균 값 (y=mx+b)
# m의 평균값을 구하는 이유 : 허브변환 함수의 결과로 하나가 아닌 여러개의 선이 검출되기 때문에 찾은 선들의 평균값을 이용하고자 함 -> 각 선분들을 대표하는 한가지의 선분으로 평균값을 사용하자
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)
if m == 0 and b == 0: # 선을 못 찾으면 왼쪽은 0으로 오른쪽은 사진의 맨 오른쪽으로 pos를 잡음
if left:
pos = 0
if right:
pos = Width
else:
y = Gap / 2 # ROI의 세로 값을 2로 나눔
pos = (y - b) / m # y값으로 x값을 찾음 (y=mx+b)
b += Offset
x1 = (Height - b) / float(m)
x2 = ((Height/2) - b) / float(m)
cv2.line(img, (int(x1), Height), (int(x2), (Height/2)), (255, 0,0), 3) # 640 x 480 원본 이미지의 맨 아래 (y 값 480)의 x1과 이미지 중간(y값 240)의 x2를 구해 (x1, 480)과 (x2,240) 두 점을 잇는 파란색 선을 구함
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) # GRAY 색상으로 변환
# blur
kernel_size = 5
blur_gray = cv2.GaussianBlur(gray,(kernel_size, kernel_size), 0) # Gaussian Blur 처리
# canny edge
low_threshold = 60
high_threshold = 70
edge_img = cv2.Canny(np.uint8(blur_gray), low_threshold, high_threshold) # Canny Edge 외곽선 따기
# HoughLinesP
roi = edge_img[Offset : Offset+Gap, 0 : Width] # 얇은 띠가 됨
all_lines = cv2.HoughLinesP(roi,1,math.pi/180,30,30,10) # 위에서 작업한 영상의 ROI 영역에서 선분 찾기
# 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) # 총 4개의 사각형을 그림 (왼쪽 차선, 오른쪽 차선, 왼쪽 차선과 오른쪽 차선 사각형의 중점, 화면 한가운데의 고정 빨간색 사각형)
return lpos, rpos
def draw_steer(image, steer_angle) :
global Width, Height, arrow_pic
arrow_pic = cv2.imread("steer_arrow.png", cv2.IMREAD_COLOR) # 주향 이미지 읽어들이기
origin_Height = arrow_pic.shape[0]
origin_Width = arrow_pic.shape[1]
steer_wheel_center = origin_Height * 0.74 # 이미지 크기 축소를 위해 크기 계산
arrow_Height = Height/2
arrow_Width = (arrow_Height * 462)/728
matrix = cv2.getRotationMatrix2D((origin_Width/2, steer_wheel_center), (steer_angle) * 2.5, 0.7) # steer_angle에 비례하여 회전
# 이미지 크기를 영상에 맞춤
arrow_pic = cv2.warpAffine(arrow_pic, matrix, (origin_Width+60, origin_Height))
arrow_pic = cv2.resize(arrow_pic, dsize = (arrow_Width, arrow_Height), interpolation = cv2.INTER_AREA)
# 전체 그림 위에 핸들모양의 그림을 오버레이
gray_arrow = cv2.cvtColor(arrow_pic, cv2.COLOR_BGR2GRAY)
_, mask = cv2.threshold(gray_arrow, 1, 255, cv2.THRESH_BINARY_INV)
arrow_roi = image[arrowt_Height: Height, (Width/2 - arrow_Width/2) : (Width/2 + arrow_Width/2)]
arrow_roi = cv2.add(arrow_pic, arrow_roi, mask=mask)
res = cv2.add(arrow_roi, arrow_pic)
image[(Height - arrow_Height): Height, (Width/2 -arrow_Width/2): (Width/2 + arrow_Width/2)] = res
cv2.imshow('steer',image) # 원본 사진, 검출 차선, 평균 차선, 차선위치 표시, 화면 중앙표시, 핸들 그림, 조향각 화살표 표시
def start():
global image, Width, Height
cap = cv2.VideoCapture("hough_track.avi") # 동영상 파일 읽기
while not rospy.is_shutdown() :
ret, image = cap.read() # 동영상 파일에서 이미지 한장 읽기
time.sleep(0.03)
pos, frame = process_image(image) # 허브변환 기반으로 영상처리 진행하여 차선을 찾고 위치를 표시
center = (pos[0]+ pos[1]) / 2
angle = 320 - center
steer_angle = angle * 0.4 # 실제 자동차의 steer_angle은 0도에서 20도까지이지만 자이카는 0에서 50도까지이므로 비율을 맞추기 위해 0.4를 곱함
draw_steer(frame, steer_angle) # 왼쪽 차선과 오른쪽 차선의 중간점과 화면 중앙과의 차이를 가지고 핸들 조향각을 결정하여 핸들 그림 표시
if cv2.waitKey(1) & 0xFF == ord('q') :
break
if __name__ == '__main__':
start()
조향각의 설정
- 양쪽 차선의 중점이 영상의 중앙에서 벗어난 픽셀 수를 이용하여 핸들 꺽임 조향각을 결정함
- 차량 속도와 조향각 결정까지의 처리 지연시간을 고려 해야함
실행
- hough_drive 패키지의 src 디렉터리에 hough_track.avi 파일과 steer_arrow.png 파일을 준비
$ chmod +x hough_find.py
$ python hough_find.py
실행결과
- 왼쪽과 오른쪽 차선에 녹색 사각형 + 그 둘의 중간 위체에 녹색 사각형 + 화면의 중앙에 빨간색 사각형 + 아래 중앙에 핸들 모양과 화살표를 확인 할 수 있음
- hough_track.avi 영상의 차선에 맞춰 유동적으로 변하는 결과를 확인 할 수 있음