[Python] OpenCV로 만든 자율주행 코드 리뷰
지금부터 본격적으로 나와 내 동료가 작성한 코드의 리뷰를 시작하겠다.
참고 잘 따라온다면 우리가 짠 코드와 그 당시 생각들을 엿볼 수 있을 것이다.
OpenCV 자율주행 코드 리뷰
# LaneDetector_16.py
우리의 코드 중 가장 기본이 되는 뼈대 코드이다.
나와 내 동료는 각자 다른 방식으로 접근했다. 나는 열악한 환경을 고려하여 정확도가 조금 떨어지더라도 빠른 OpenCV를 채택하였고, 내 동료는 정확도가 중요하다고 생각하여 딥러닝을 선택하였다. 결국, OpenCV를 자율주행에 쓰는 것으로 채택했다.
import numpy as np
import cv2
import time
from Steering import *
from Preprocessing import *
from StopDetector import *
다음과 같은 모듈들을 끌어왔다. numpy 모듈은 배열을 더 쉽고 간편하게 다루기 위해 필요한 모듈이고, cv2 모듈은 자율주행을 할 때 전처리 과정과 contour을 하기 위해 필요한 모듈이다. 그리고 마지막으로 지연 시간을 구하기 위해 time 모듈을 사용하였다.
WIDTH = 640
HEIGHT = 360
kernel_size=11
low_threshold=120
high_threshold=255
theta=np.pi/180
lower_blue = (115-30, 10, 10)
upper_blue = (115+30, 255, 255)
lower_red = (6-6, 30, 30)
upper_red = (6+4, 255, 255)
lower_yellow = (19-4, 30, 30)
upper_yellow = (19+30, 255, 255)
isUseableRed=False
isInStopArea=False
아래에는 다음과 같은 전역 변수들을 설정해 주었다. 중간의 lower_색상, upper_색상은 HSV 색상 공간을 활용한 색상이다. lower 값에서 upper 값에 해당하는 색상들을 추출해준다. 그리고 제일 마지막에 boolean값들은 정지선 안에 있는지를 나타내는 변수이다.
def setup_path():
path = "./source/KakaoTalk_20221007_063507621.mp4"
cap=cv2.VideoCapture(path) #path
return cap
다음 메서드는 임시로 테스트해보기 위해 콘을 놓고 사람이 주행하는 테스트 영상의 주소 값을 넣어놓은 것이다. 실제로 사용할 때는 cv2.VideoCapture(path) 부분의 path 부분에 0을 넣어서 운영체제에서 인식하는 카메라를 사용하여 자율주행을 한다.
def setup_countours():
obj_b = cv2.imread('./source/corn_data/lavacorn_nb.png', cv2.IMREAD_GRAYSCALE)#wad
obj_s = cv2.imread('./source/corn_data/lavacorn_ns.png', cv2.IMREAD_GRAYSCALE)#wad
obj_contours_b,_=cv2.findContours(obj_b, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)#wad
obj_contours_s,_=cv2.findContours(obj_s, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)#wad
obj_pts_b=obj_contours_b[0]#wad
obj_pts_s=obj_contours_s[0]#wad
return obj_pts_b, obj_pts_s
이 메서드는 contour을 설정하는 것이다(메서드에 오타가 났다). 이 함수를 사용하여 contour을 사용하여 물체를 검출하기 전에 준비를 시켜준다.
def setup_linear_reg():
global p_r_m
global p_r_n
global p_l_m
global p_l_n
p_r_m=0.3
p_r_n=37
p_l_m=-0.3
p_l_n=238
이름에서 볼 수 있듯이 이 함수는 선형 회귀로 왼쪽, 오른쪽 차선을 그리기 전에 초기값을 설정해 놓는 것이다. 이로 인해 처음에 선이 없더라도 직선으로 주행할 수 있다. 변수명이 헷갈릴 수도 있는데 실제로 내 동료도 헷갈려서 여기에 적는다. p는 past를 뜻하고, r과 l은 각각 right와 left, m과 n은 각각 기울기와 y 절편인데, mx + n에서 따왔다.
def depart_points(img, points):
right_line=[]
left_line=[]
stop_line=[]
for p in points:
x1, y1, x2, y2 = p
label = plot_one_box([x1, y1, x2, y2], img)
x = int((x1 + x2)/2)
y = int(y2)
if label == 'blue':
left_line.append([x, y])
elif label == 'yellow':
right_line.append([x, y])
elif label == 'red':
stop_line.append([x, y])
else:
pass
return right_line, left_line, stop_line
이 메서드를 통해서 점들을 구분한다. contour을 이용해 추출한 콘들의 아랫부분의 중심에 점을 찍는다. 그 이후 이 점들을 콘의 색상값에 따라서 분류하는 역할을 한다.
def find_contours(img_thresh, obj_pts_b, obj_pts_s)->list:
contours,_=cv2.findContours(img_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
points = []
isSameCorn=False
for pts in contours:
if cv2.contourArea(pts) <60:
continue
rc=cv2.boundingRect(pts)
dist_b=cv2.matchShapes(obj_pts_b, pts, cv2.CONTOURS_MATCH_I3, 0)
dist_s=cv2.matchShapes(obj_pts_s, pts, cv2.CONTOURS_MATCH_I3, 0)
if dist_b <0.5 or dist_s<0.4:
mid_x = (2*rc[0]+rc[2])/2
for p in points:
if p[0]<=mid_x and p[2]>=mid_x:
isSameCorn=True
break
if not isSameCorn and 40<=mid_x<=600:
cv2.rectangle(img_thresh, rc, (255, 0,0),1)
cv2.imshow("img", img_thresh)
points.append([rc[0], rc[1], rc[0]+rc[2], rc[1]+rc[3]])
isSameCorn=False
return points
이 메서드에서 위에서 언급했듯이 corn을 contour을 통해 찾고, 좌표값을 추출한다.
정지선과 관련된 코드는 starter(), 즉, main 부분에서 처리하였다.
# Preprocessing.py
Preprocessing이라는 이름처럼 전처리를 담당하는 파일이다. 전처리를 통해 정확도가 낮은 점을 보완하였다. 나는 이 전처리 알고리즘을 짜는 것이 가장 큰 고민거리였다.
def grayscale(img):
img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
return img
위의 메서드는 img를 흑백 사진으로 변환해주는 역할을 한다. 흑백 사진으로 바꾸면 색상값을 알 수 없지 않냐는 질문이 있을 수도 있지만 흑백으로 변환하면 더욱 정확하게 콘과 배경을 분류할 수 있다. 여기서는 색상값을 알 수 없기 때문에 점들을 전부 하나의 배열에 담는다. 그리고 LaneDetector.py에서 이 점들의 집합을 원본 사진의 좌표값에 해당하는 색상값을 얻어와서 다시 빨강, 파랑, 노랑의 색상으로 분기한다.
def gaussian_blur(img, kernel_size):
return cv2.GaussianBlur(img, (kernel_size, kernel_size), 0)
이 메서드는 img를 메인에서 선언했던 kernel_size를 바탕으로 블러 처리를 하는 것이다. 이것으로 노이즈를 획기적으로 줄일 수 있었다.
def region_of_interest(img, vertices):
mask=np.zeros_like(img)
if len(img.shape)>2:
channel_count = img.shape[2]
ignore_mask_color=(255,)*channel_count
else:
ignore_mask_color=255
cv2.fillPoly(mask, vertices, ignore_mask_color)
masked_image=cv2.bitwise_and(img,mask)
return masked_image
관심 영역을 지정하는 메서드이다. 관심 영역이란 내가 필요한 부분만 보겠다는 것이다. 콘은 특정한 부분에만 존재한다. 따라서 콘이 존재하는 부분만 사다리꼴로 잘라서 나머지 부분을 검은색으로 칠해주면 노이즈를 더욱 줄일 수 있다. 이 사다리꼴 관심 영역은 vertices에 들어가는 값을 수정함으로써 조정할 수 있다.
def preprocessing(img, low_threshold, high_threshold, kernel_size): #640*360
img_hsv=cv2.cvtColor(img,cv2.COLOR_BGR2HSV)
if len(img.shape)>2:
channel_count=img.shape[2]
ignore_mask_color=(255,) * channel_count
else:
ignore_mask_color=255
mask=np.zeros_like(img)
vertices=np.array([[(20, 315),
(20, 210),
(160, 130),
(470, 130),
(620, 210),
(620, 315)]], dtype=np.int32)
cv2.fillPoly(mask, vertices, ignore_mask_color)
masked_image=region_of_interest(img_hsv, vertices)
lower_blue = (110-3, 120, 150) # hsv 이미지에서 바이너리 이미지로 생성 , 적당한 값 30
upper_blue = (110+5, 255, 255)
lower_red = (6-6, 110, 120) # hsv 이미지에서 바이너리 이미지로 생성 , 적당한 값 30
upper_red = (6+4, 255, 255)
lower_yellow = (19-1, 110, 120) # hsv 이미지에서 바이너리 이미지로 생성 , 적당한 값 30
upper_yellow = (19+5, 255, 255)
mask_hsv_red = cv2.inRange(masked_image, lower_red, upper_red)
mask_hsv_blue = cv2.inRange(masked_image, lower_blue, upper_blue)
mask_hsv_yellow = cv2.inRange(masked_image, lower_yellow, upper_yellow)
mask_hsv=cv2.bitwise_or(mask_hsv_red, mask_hsv_blue)
mask_hsv=cv2.bitwise_or(mask_hsv, mask_hsv_yellow)
stop_img = cv2.bitwise_and(img, img, mask=mask_hsv)
# img_gray=grayscale(mask_hsv)
img_blur = gaussian_blur(mask_hsv, kernel_size)
ret, img_thresh=threshold(img_blur, low_threshold, high_threshold)
return img_thresh
이것이 전처리를 진행하는 핵심 메서드이다. 아까 흑백 사진을 사용하는 진짜 의미가 여기에 드러난다. 색상이 존재하면 어느 특정 색상만이 잘 보인다는 단점이 있었다. 따라서 특정 색상의 콘만 인식이 잘 되고, 다른 색상은 인식이 잘 되지 않았는데, 만약 흑백으로 바꾼다면 이것이 해결된다. Threshold를 사용하면 회색 중 밝은 색상과 어두운 색상이 있을 것이다. 이것을 0 또는 255의 값으로 바꾸어서 어두운 부분은 그냥 검은색으로, 밝은 부분은 그냥 흰색으로 바꾸어 버린다. 따라서 어떤 색상인지에 관계없이 콘이면 255, 나머지는 0의 값으로 바뀌게 된다. 따라서 우리는 콘을 인식할 수 있다. 그리고 원래 preprocessing 내부에 있는 lower_색상, upper_색상으로 나누어진 부분을 한 개로만 작성했었다. 하지만 그러니 오차가 너무 크게 생겨서 색상별로 필터를 적용하는 형식으로 변경했다. 그리고 이 세 개의 값들을 or bit연산을 해서 합쳐서, 세 개의 콘 색상들을 모두 드러낼 수 있었다.
# Steering.py
Hypertangent를 사용하여 바퀴의 속도에 따라 조향에 변화를 주는 것이다.
이렇게 함으로써 차량의 속도가 더 빠를수록 핸들을 조금만 돌리도록 프로그래밍할 수 있었다.
# StopDetector.py
다음 코드는 전체가 빨간색인 정지 영역에서 동작하는 코드이다.
만약 전체가 빨강 콘이라면 기존의 파란색과 노란색을 기준으로 오른쪽, 왼쪽 차선을 나눈 것과는 다른 알고리즘이 필요하였다. 이것을 이미 그어진 오른쪽, 왼쪽 가상 차선과 각 콘의 좌표값의 거리를 비교해서 더 가까운 차선의 좌표값 집합에 포함시켰다. 그리고 이것을 통해 또 선형 회귀를 진행하는 방식으로 차선을 그렸다.
전체 코드를 보고 싶다면 아래 github링크를 참조하기 바란다.
GitHub - BBAAMM/openCV-driving-22: 한양대학교 Erica baqu4이라는 동아리에서 1학년 1학기에 참여했던 자율
한양대학교 Erica baqu4이라는 동아리에서 1학년 1학기에 참여했던 자율주행 프로젝트입니다. - GitHub - BBAAMM/openCV-driving-22: 한양대학교 Erica baqu4이라는 동아리에서 1학년 1학기에 참여했던 자율주행
github.com