본문 바로가기
Programming/Capstone Design

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

by NoiB 2022. 4. 13.
반응형

지난 시간에 허프 변환까지 진행했었죠? 오늘은 이제 이 직선을 가지고 무엇을 할지에 대해서 얘기를 드려보겠습니다.

 

그전에 잠깐, 우리는 왜 차선 인식을 진행하고 있나요? 가끔 수단에 집중하다 보면 목적을 잊어버리는 일이 생기기 때문에 항상 이렇게 목적이 무엇이었는지 떠올리는 습관을 가지면 좋습니다. 저 스스로에게 하는 얘기이기도 하고요.

 

차선 인식을 해야 하는 이유는 여기서 검출한 차선을 이용해 차량의 자율주행을 구현하기 위함이었죠. 그러기 위해서 차선만 검출하는 노력을 해왔던 거고요. 하지만 지금은 이렇게 깔끔하게 차선만 인식이 되어서 별 문제가 없는 것 같지만 여기에는 함정이 숨어 있습니다.

 

예시를 들어드리기 위해 다른 이미지를 가져와 봤습니다. 해당 사진에 이전 포스팅에서 사용했던 코드를 그대로 적용시켜 보겠습니다.

 

이상하죠? 전에 썼던 사진은 깔끔하게 잘 나왔는데 이번엔 차선이 검출이 되긴 했는지 조차 의심스러운 상황입니다. 위에서 제가 말씀드렸던 함정이 바로 이것인데요. 저희는 지난 포스팅에서 관심영역(ROI)를 적용시키면서 불필요한 데이터를 걸렀다고 생각했지만 관심영역 속에 있는 불필요한 데이터는 전혀 걸려내지 못했습니다. 그래서 관심영역 속에 있는 조건에 맞는 선을 컴퓨터는 정직하게 검출해준 것이죠. 이래서는 전혀 차선 인식에 써먹지 못합니다. 세상 모든 도로가 완벽하게 깔끔하고 그림자 하나 없이 도로와 차선이 완벽하게 인식이 되도록 만들어져 있지 않기 때문이죠. 따라서 우리는 이런 불필요한 선을 걸러내는 작업을 진행해야 합니다.

 

그렇다면 차선을 제외한 다른 선들은 어떻게 걸러내면 좋을까요? 간단하게 생각해보면 차선은 상당히 연속적인 선이 나타날 테니 기준을 정하고 그 아래의 길이를 갖는 선은 걸러버리는 방법이 있겠죠. 물론 OpenCV의 허프 변환은 함수 자체가 해당 기능을 탑재하고 있습니다. 살짝 건드려볼까요?

minLineLength 변수를 120으로 바꿔봤습니다. 아까보다는 깔끔해졌지만 여전히 남아있는 친구들이 있죠. 그렇게 쉽게 마음대로 따라와 줄 생각은 없나봅니다. 그리고 한 가지 더 문제가 있죠. 상황이 안좋아서 차선의 길이가 120보다 짧으면 어떻게 해야 할까요? 검출이 안되니까 코드를 수정하고 나오는지 확인하고 또 수정하면서 진행을 해야할까요? 상당히 비효율적이죠. 그렇다면 검출이 되나 안되나를 보면서 변수를 수정한다면 어떨까요? 사실 해당 문제의 경우 저는 거의 프로젝트의 막바지에 추가했었던 기능입니다만 지금 쓴다고 해서 잘못될 것은 없으니까요. 우리는 trackbar라는 기능을 이용해서 화면을 보면서 변수를 조절해볼까 합니다.

 

Trackbar(트랙바)

간단하게 말하면 트랙바라는 기능은 우리가 조절하기를 원하는 변수를 선택해서 해당 변수를 코드 상이 아니라 트랙바 윈도우에서 조절하는 기능이라고 할 수 있겠습니다. 굳이 조절만 하는거라면 그냥 코드를 수정하는게 낫지 않나요?라는 질문도 나올 수 있겠네요. 물론 코드에서 직접 변수를 바꾸는 쪽이 훨씬 편합니다. 몇글자 지우고 몇글자 쓰면 되니까요. 그렇지만 이 기능을 사용하는 이유는 해당 변수를 직접 조절하면서 즉각적으로 변수의 변화가 어떤 영향을 미치는지 확인이 가능하다는 장점이 있기 때문입니다(물론 반복문에서 입력을 받아서 변화시키면서 보는 방법도 가능하지만 트랙바를 사용하는 것보다 귀찮아지겠죠).

 

cv2.namedWindow(winname)

트랙바 기능을 쓰기 위해서는 꼭 먼저 써줘야 하는 함수입니다. 일단 윈도우 이름을 정해야지 해당 윈도우에 트랙바를 생성할 수 있습니다. 문자열로 써주시면 되겠습니다.

 

cv2.createTrackbar(trackbarName, windowName, value, count, onChange)

트랙바를 생성하는 함수입니다. 트랙바 이름, 윈도우 이름, 초기값, 최대값(최소값은 자동으로 0입니다), 콜백 함수 이 5가지의 요소로 정의합니다. 콜백 또는 인터럽트라고 불리는 함수는 어떤 조건을 만족하면 해당 인터럽트 루틴을 실행하는 함수입니다. 여기서의 onChange가 그 기능을 맡고 있구요. onChange는 트랙바를 사용자가 조작해서 값이 변경되면 해당 콜백 함수를 실행하는 기능을 합니다. 예를 들어서 콜백 함수 내에서 허프 변환을 시키면 반복문을 사용하지 않고 값이 바뀔 때만 함수를 실행할 테니 상대적으로 시스템에 과부하를 줄일 수 있겠죠(현재 코드에서는 이미지 저장을 위해서 콜백 함수는 pass 하고 반복문을 사용하고 있습니다). 참고로 버전의 문제인지 함수를 사용할 때 keyword argument를 작성하면 안돌아가더라구요. 저만 그런건지 잘 모르겠습니다.

 

cv2.getTrackbarPos(trackbarname,winname)

현재 트랙바의 위치 값을 반환하는 함수입니다. 해당 코드에서는 허프 변환의 minLineLength의 값을 조절하기 위해 사용했습니다.

cv2.namedWindow(winname='Lane Detection')
cv2.createTrackbar('minLine', 'Lane Detection', 0, 200, nothing)
while cv2.waitKey(1) != ord('q'):
    min_line_len = cv2.getTrackbarPos(trackbarname='minLine', winname='Lane Detection')
    hough_img = lane_detection(min_line_len)
    cv2.imshow('Lane Detection',hough_img)

트랙바를 사용하기 위해 변경한 코드는 위와 같습니다.

 

https://www.youtube.com/watch?v=elEJif3Cd3Y 

동영상으로 보는 편이 빠를 것 같아서 급하게 찍어봤습니다. trackbar를 이용해서 우리가 원하는 정도만 검출이 되게끔 미리 걸러놓을 수 있겠죠. 이대로 끝난 게 아닙니다만 너무 오랜만에 올리는 개발 관련 포스팅이기도 하고... 정확히는 예전부터 계속 조금씩 임시저장해가면서 써왔는데 뭔가 마음의 짐 같아서 후딱 올려버리고 싶은 것이기도 합니다.

 

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 hough(img,h,w,min_line_len):
    lines = cv2.HoughLinesP(img, rho=1, theta=np.pi/180, threshold=30, minLineLength=min_line_len, maxLineGap=30)
    line_img = np.zeros((h, w, 3), dtype=np.uint8)
    for line in lines:
        for x1,y1,x2,y2 in line:
            cv2.line(line_img, (x1, y1), (x2, y2), color=[255,0,0], thickness=2)
    return line_img

def lane_detection(min_line_len):
    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)
    return hough_img

def nothing(pos):
    pass

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

    cv2.imwrite('./hough_img1.jpg',hough_img)
    cv2.destroyAllWindows()

전체 코드는 위와 같습니다.

반응형