본문 바로가기
Programming/Capstone Design

[Capstone Design]3. 차선 인식(Lane Detection) - 5

by NoiB 2022. 4. 14.
반응형

지난 시간에는 직선으로 인식하는 최소 길이를 트랙바를 이용해서 조절하는 것까지 진행을 했었죠. 하지만 그렇게 하더라도 원하지 않는 친구들이 좀 남아있었던 것을 보셨을 겁니다. 그렇다면 우리는 이제 뭘 더 진행해야 불필요한 친구들을 걸러낼 수 있을까요? 이번 시간에는 각도를 이용해서 필터링을 진행해볼까 합니다.

 

눈치 빠르신 분들은 각도 얘기를 듣자마자 방법을 떠올리셨을지도 모르겠네요. 각도 얘기를 하기에 앞서서 잠깐 소실점(Vanishing Point)에 대해서 얘기를 먼저 해보겠습니다. 미술이나 사진 등의 분야에서 자주 나오는 얘기인 소실점은 원근법과 상당히 밀접한 관련이 있는데요. 우리가 저 멀리 있는 무언가를 보거나 혹은 촬영을 하면 실제로는 평행한 선들이 어떤 점으로 모여드는 것처럼 보이게 됩니다. 여기서 그 선들이 모이는 점이 바로 소실점인데요. 말보단 보는 게 빠르겠죠.

분명히 우리가 이 안을 걸어가면서 벽을 확인하면 벽면은 전부 동일한 크기의 직사각형으로 이루어져 있을겁니다. 하지만 멀리서 본다면 점차 벽이 작아지면서 한 점으로 모여드는 것처럼 보이죠. 이 것을 차선 인식에 적용시켜보면 어떨까요? 차에 탄 채 정상적으로 주행을 하고 있다고 가정을 하면 차 안에서 보는 우리의 시야에는 분명히 평행해야 할 차선이 한 점을 향해 모여들고 있을 겁니다.

그렇다면 우리는 이렇게 생각을 해볼 수도 있겠죠. 우리가 목표로 하는 차선은 소실점이 화면의 정가운데에 있을 때 어떤 특정한 각도를 가지고 가운데로 모여들겠구나. 그러면 특정 각도 범위 밖의 녀석들은 걸러내면 되지 않을까? 잠깐 지난 시간에 사용했던 사진을 다시 가져오면,

여기서 아주 밀접한 채 평행을 이루고 있는 두 선이 차선입니다. 그 두 선을 제외한 다른 선들은 어떻죠? 아무리 봐도 일단 차선은 아니죠. 화면 가운데로 모이지도 않을 것이고요. 정확하게 판단하기는 어렵겠습니다만, 대략 15도 이하, 165도 이상 정도 되는 각도를 가지고 있는 것 같습니다. 그렇다면 우리는 직선을 검출한 다음에 원하지 않는 각도를 가지는 친구들을 걸러내면 되겠구나 하고 생각이 드는 찰나에 번뜩 지난 시간에 했던 내용이 기억나실 겁니다. 이 각도도 트랙바로 조절하면서 보면 안되려나? 그래서 이번 시간에는 트랙바 기능을 이용해서 각도 범위를 조절하면서 필터링을 진행해보겠습니다.

 

트랙바의 구현에 앞서서 각도를 제한하는 코드를 먼저 짜 봐야겠네요. 허프 변환 코드를 보셨던 분은 반환 값이 [x1, y1, x2, y2]의 형태로 나온다는 것을 아실 겁니다. 즉 선분의 양 끝점의 좌표가 반환된다는 뜻이죠. (y좌표의 변화량)/(x좌표의 변화량)이 기울기라는 사실을 다들 알고 계실 겁니다. 하지만 삼각함수를 사용하면 기울기를 각도 값으로 나타낼 수 있죠. 그래서 arctan를 이용합니다. numpy에 들어있으니 굳이 math를 또 import 하지 않아도 됩니다. 하지만 주의하실 점은 각도를 라디안으로 반환하기 때문에 나중 작업을 편하게 하기 위해서 도로 변환합니다. 180을 곱하고 파이로 나눠주시면 됩니다. 간단하게 잘 동작하는지 보기 위해서 최대를 160도, 최소를 100도로 잡아보았습니다.

def restrict_deg(lines):
    lines = np.squeeze(lines)#one time ok
    slope_deg = np.rad2deg(np.arctan2(lines[:,1]-lines[:,3],lines[:,0]-lines[:,2]))
    lines = lines[np.abs(slope_deg)<160]#cannot use and & index true catch
    slope_deg = slope_deg[np.abs(slope_deg)<160]
    lines = lines[np.abs(slope_deg)>100]
    slope_deg = slope_deg[np.abs(slope_deg)>100]#where can i use slope
    return lines, slope_deg

깔끔하게 걸러져서 나온 것을 확인할 수 있습니다. 사실 각도의 경우는 구태여 트랙바로 이리저리 해볼 필요가 없기는 합니다만 제가 프로젝트를 진행할 때 카메라의 화각에 따라서 차선이 잘 안 보이는 경우가 있어서 카메라가 바닥을 보도록 머리를 내릴 경우 차선의 각도도 90도에 가까워지기 때문에 가변 할 수 있도록 시스템을 짜는 건 좋은 일입니다. 정 필요 없어지거나 시스템이 타이트할 경우 완전히 컨트롤된 상황을 설계하고 해당 코드는 빼도 되겠죠(물론 카메라를 쓰는 순간 조도에 엄청난 영향을 받기 때문에 정말 많은 변수들을 자주 바꿔야 하는 상황이 있어서 빼게 될 일은 없으실 것 같습니다).

 

트랙바의 경우 지난 포스팅에서 진행을 했었기 때문에 빠르게 넘어가겠습니다.

https://youtu.be/nQRooHDFe14

문제없이 잘 작동되는 걸 확인할 수 있네요. 크게 어렵지는 않으신가요? 다 끝내고 나서 정리를 하려니 그렇게 어렵지만은 않았던 것 같은데 프로젝트를 진행할 때는 왜 그렇게 어렵게만 느껴졌는지... 아무튼 이번 포스팅도 여기서 마치겠습니다. 도움이 되셨기를 바랍니다.

 

import cv2
import numpy as np

def grayscale(img):
    return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)

def gaussian_blur(img, kernel_size):
    return cv2.GaussianBlur(img, (kernel_size, kernel_size), 0)

def canny(img, low_threshold, high_threshold):
    return cv2.Canny(img, low_threshold, high_threshold)

def roi(img,h,w):
    mask = np.zeros_like(img)
    vertices = np.array([[(w/10,h), (w/10,h*3/4), (w*9/10,h*3/4), (w*9/10,h)]], dtype=np.int32)
    cv2.fillPoly(mask, vertices, 255)
    roi_img = cv2.bitwise_and(img, mask)
    return roi_img

def restrict_deg(lines,min_slope,max_slope):
    lines = np.squeeze(lines)#one time ok
    slope_deg = np.rad2deg(np.arctan2(lines[:,1]-lines[:,3],lines[:,0]-lines[:,2]))
    lines = lines[np.abs(slope_deg)<max_slope]#cannot use and & index true catch
    slope_deg = slope_deg[np.abs(slope_deg)<max_slope]
    lines = lines[np.abs(slope_deg)>min_slope]
    slope_deg = slope_deg[np.abs(slope_deg)>min_slope]#where can i use slope
    return lines, slope_deg

def hough(img,h,w,min_line_len,min_slope,max_slope):
    lines = cv2.HoughLinesP(img, rho=1, theta=np.pi/180, threshold=30, minLineLength=min_line_len, maxLineGap=30)#return = [[x1,y1,x2,y2],[...],...]
    lanes, slopes = restrict_deg(lines,min_slope,max_slope)
    lane_img = np.zeros((h, w, 3), dtype=np.uint8)
    for x1,y1,x2,y2 in lanes:
        cv2.line(lane_img, (x1, y1), (x2, y2), color=[255,0,0], thickness=2)
    return lane_img

def lane_detection(min_line_len,min_slope,max_slope):
    origin_img = cv2.imread('./slope_test.jpg')
    h,w = origin_img.shape[:2]
    gray_img = grayscale(origin_img)
    blur_img = gaussian_blur(gray_img, 5)
    canny_img = canny(blur_img, 50, 200)
    roi_img = roi(canny_img,h,w)
    hough_img = hough(roi_img,h,w,min_line_len,min_slope,max_slope)
    return hough_img

def nothing(pos):
    pass

if __name__ == '__main__':
    cv2.namedWindow(winname='Lane Detection')
    cv2.createTrackbar('houghMinLine', 'Lane Detection', 0, 200, nothing)#don't write keyword
    cv2.createTrackbar('slopeMinDeg', 'Lane Detection', 100, 180, nothing)
    cv2.createTrackbar('slopeMaxDeg', 'Lane Detection', 160, 180, nothing)
    while cv2.waitKey(1) != ord('q'):
        min_line_len = cv2.getTrackbarPos(trackbarname='houghMinLine', winname='Lane Detection')
        min_slope = cv2.getTrackbarPos('slopeMinDeg','Lane Detection')
        max_slope = cv2.getTrackbarPos('slopeMaxDeg','Lane Detection')
        hough_img = lane_detection(min_line_len,min_slope,max_slope)
        cv2.imshow('Lane Detection',hough_img)

    cv2.imwrite('./hough_img1.jpg',hough_img)
    cv2.destroyAllWindows()
    
#It will be great that we can select the instant roi region using click when we run the code.

혹시 잊으셨을까 봐 다시 드리는 말씀이지만 이미지를 불러오는 경로를 언제나 체크하셔야 합니다. 단순히 제 코드를 그대로 사용하신다면 아마 동작하지 않을 확률이 높아요.

반응형