허프변환 기반 차선인식

허프 변환을 이용한 차선 추출

  • Hough Transform

image

Image Space vs Parameter Space

  • Image Space : x, y 좌표계

    • Image Space 에서의 직선은
      • Parameter Space 에서 점으로 표현할 수 있음
      • 기울기와 y 절편만 알면 Image Space 에서 직선을 그릴 수 있음
    • Image Space 에서의 점은
      • Parameter Space 에서 직선으로 표현할 수 있음
      • 그 직선은 Image Space에서 (x1, y1) 점을 지나는 모든 직선을 의미함
  • Parameter Space : m(기울기), b(절편) 좌표계

image

  • Parameter Space에서 두 직선의 교점은

    • m과 b가 같은 경우에 생김 (m = 기울기, b = 절편)
    • Image Space에서 두 점을 지나는 직선을 의미

    image

Image Space에서 직선을 찾는 방법

  • Canny를 통해 edge를 찾고 그 edge의 점들을 Parameter Space로 표현
  • Parameter Space에서 겹치는 직선이 많은 교점일수록 그 교점이 의미하는 Image Space에서의 직선이 존재할 가능성이 높음
  • 사람은 한번에 직선을 찾을 수 있지만 컴퓨터는 다음과 같은 방법으로 찾아야 함
  1. 점들을 다 Parameter Space에서의 선으로 다 바꿈
  2. Paramter Space에서 선들이 많이 만나는 교점을 찾음
  3. 그 점의 기울기와 절편을 구하면 Image Space에서의 직선을 찾을 수 있음

image

하지만 기울기가 무한대인 직선은?

  • m의 기울기가 존재하지 않으면 Parameter Space에서 어떻게 점을 찍나..?
  • 그래서 Hough Space 도입

image

Hough Space

  • 원점에서 직선에 수선의 발을 내려서 수선을 긋고 원점과 직선의 거리 로우(ρ)와 수선과 x축과의 각도 (θ)로 직선을 표현
    • Parameter Space와 마찬가지로 Hough Space에서의 점은 Image Space에서의 직선을 의미함

image

  • Hough Space의 곡선
    • Hough Space에서는 Image Space의 점이 곡선으로 표현 됨

image

  • 기울기가 무한대인 직선도 표현이 가능

image

  • 그 이외의 의미하는 바는 Parameter Space와 모두 같음
    • Hough Space에서 곡선이 많이 겹치는 교점일수록 Image Space에서는 직선이 존재할 확률이 높음

image

Hough Transform

  • 3 점의 Angle과 Distance 값 (각도 θ , 로우 ρ)
    • 한 점을 지나는 여러 각도(0, 30, 60, 90, 120, 150)의 선과 원점간의 거리를 구해보면
      • 세타 값을 6개만 넣어서 로우 값을 구한다.
    • 3개의 곡선들이 만나는 점은 분홍색 선을 의미하는 것으로, 3개의 점을 모두 지나는 직선임을 알 수 있음

image

  • 세타와 로우의 간격은 어떻게 할 것인가?

image

Hough Transform을 이용하여 직선 검출 방식

  • 다음과 같은 순서로 진행하여 이미지에서 직선을 검출
    1. 입력 영상을 흑백 GrayScale 변환 처리
    2. Canny Edge 처리로 외곽선 영상을 획득
    3. ρ 와 θ 간격 설정
    4. 외곽선 점들에 대해서 (ρ, θ) 좌표값 구하기
    5. 오차범위 내의 (ρ, θ) 좌표값을 갖는 외곽선 점들이 하나의 직선을 구성한다고 판정

HoughLines 함수

  • cv2.HoughLines(image, rho, theta, threshold)
    • image : 8bit, 흑백 이미지 (대체적으로 Canny Edge를 한 흑백 이미지를 줌)
    • rho : hough space에서 얼만큼 ρ를 증가시키면서 조사할 것인지
    • theta : hough space에서 얼만큼 θ를 증가시키면서 조사할 것인지
    • threshold : hough space에서 threshold 이상의 직선이 겹치는 교점은 하나의 직선을 형성한다고 판단
      • threshold가 높다 : 직선으로 인정하는 규정이 까다롭다 -> 검출되는 직선은 적지만 확실한 직선들로 검출
      • threshold가 낮다 : 직선으로 인정하는 규정이 후하다 -> 검출되는 직선은 많지만 불확실한 직선들도 검출
  • 검출된 직선의 ρ, θ를 반환

image

HoughLinesP 함수

  • cv2.HoughLinesP(image, rho, theta, threshold, mineLineLength, maxLineGap)
    • minLineLength : 선분의 최소 길이, 이것보다 짧은 선분은 버림
    • maxLineGap : 간격의 최대 길이, 이것보다 작은 간격은 하나의 선분으로 간주
  • 선분 찾기

  • 검출된 선분의 시작점과 끝점의 좌표를 반환

image

HoughLines vs HoughLinesP

  • HoughLines 함수는 직선을 검출
  • HoughLinesP 함수는 시작점과 끝점이 있는 선분을 검출

image

  • 허프변환을 이용한 차선 찾기
    • 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()

image

image

image

image

image

image

image

image

image

image

image

조향각의 설정

  • 양쪽 차선의 중점이 영상의 중앙에서 벗어난 픽셀 수를 이용하여 핸들 꺽임 조향각을 결정함
  • 차량 속도와 조향각 결정까지의 처리 지연시간을 고려 해야함

image

실행

  • hough_drive 패키지의 src 디렉터리에 hough_track.avi 파일과 steer_arrow.png 파일을 준비
  • $ chmod +x hough_find.py
  • $ python hough_find.py

실행결과

  • 왼쪽과 오른쪽 차선에 녹색 사각형 + 그 둘의 중간 위체에 녹색 사각형 + 화면의 중앙에 빨간색 사각형 + 아래 중앙에 핸들 모양과 화살표를 확인 할 수 있음
  • hough_track.avi 영상의 차선에 맞춰 유동적으로 변하는 결과를 확인 할 수 있음

image