OpenCV 이미지 처리, 카메라 캘리브레이션, 원근 변형, 컬러 마스크, 윙오 및 다항식을 사용하여 도로 곡률 및 차선 차량 오프셋을 계산하기위한 컴퓨터 비전 알고리즘.
Advanced Lane 찾기 프로젝트는 앞으로 도로의 형상을 식별하는 [레인 라인 감지]에서 한 걸음 더 나아가고 있습니다.
고속도로 운전의 비디오 녹화를 사용 하여이 프로젝트의 목표는 도로 곡률의 반경을 계산하는 것입니다. 곡선 도로는 직선 도로보다 더 어려운 작업입니다. 곡률을 올바르게 계산하려면 차선을 식별해야하지만 그 위에는 이미지를 분리해야합니다. 이미지 변환은 카메라 교정과 원근 변환을 위해서는 도로의 조감도를 얻기 위해 필요합니다.
이 프로젝트는 Python에서 구현되었으며 OpenCV 이미지 처리 라이브러리를 사용합니다. 소스 코드는 AdvancedLaneFinding.ipynb 에서 찾을 수 있습니다
[광학 왜곡]은 이미지 기록에서 발생하는 물리적 현상으로, 카메라 렌즈를 통해 인식 할 때 직선이 약간 구부러진 것으로 예상됩니다. 고속도로 운전 비디오는 자동차의 전면 카메라를 사용하여 녹음되며 이미지는 왜곡됩니다. 왜곡 계수는 각 카메라에 특이 적이며 알려진 기하학적 형태를 사용하여 계산할 수 있습니다.
임베디드 카메라로 캡처 한 체스 보드 이미지는 camera_cal 폴더에 제공됩니다. 이 이미지의 장점은 대비가 높고 알려진 지오메트리를 가지고 있다는 것입니다. 이미지는 9 * 6 모서리와 함께 제공됩니다.
# Object points are real world points, here a 3D coordinates matrix is generated
# z coordinates are 0 and x, y are equidistant as it is known that the chessboard is made of identical squares
objp = np.zeros((6*9,3), np.float32)
objp[:,:2] = np.mgrid[0:9,0:6].T.reshape(-1,2)
객체 포인트는 체스 보드 패턴에서 모든 제곱이 동일하다는 일반적인 이해에 기초하여 설정됩니다. 이는 객체 포인트가 그리드 인덱스에서 x 및 y 좌표가 생성되고 z는 항상 0입니다. 이미지 포인트는 OpenCV의 함수 findChessboardCorners 사용하여 이미지에서 발견 된 해당 객체 점을 나타냅니다.
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Find the chessboard corners
nx = 9
ny = 6
ret, corners = cv2.findChessboardCorners(gray, (nx, ny), None)
모든 이미지를 스캔 한 후 이미지 포인트 목록에는 카메라 매트릭스 및 왜곡 계수를 계산하기 위해 객체 포인트와 비교할 수있는 충분한 데이터가 있습니다. 이로 인해 'Calibratecamera'기능을 사용하여 정확한 카메라 매트릭스 및 왜곡 계수 식별이 발생합니다.
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
undist = cv2.undistort(img, mtx, dist, None, mtx)
OpenCV undistort 함수는 카메라 행렬 및 왜곡 계수를 사용하여 이미지를 변환하는 데 사용됩니다.
이 그림을 비교할 때 카메라 캘리브레이션 기술의 결과가 보입니다. 체스 판 그림에서 왜곡이 더 분명하지만 도로 사진에서는 더 미묘합니다. 그럼에도 불구하고, 분리되지 않은 그림은 잘못된 도로 곡률 계산으로 이어질 것입니다.
곡률을 enclubed하기 위해 이상적인 관점은 새의 시선입니다. 이것은 도로가 차량 앞 유리를 통한 각도 대신 위에서 인식된다는 것을 의미합니다.
이 관점 변환은 직선 차선 시나리오와 차선이 실제로 평행하다는 사전 공통 지식을 사용하여 계산됩니다. 소스 및 대상 지점은 원근 변환을 위해 이미지에서 직접 식별됩니다.
#Source points taken from images with straight lane lines, these are to become parallel after the warp transform
src = np.float32([
(190, 720), # bottom-left corner
(596, 447), # top-left corner
(685, 447), # top-right corner
(1125, 720) # bottom-right corner
])
# Destination points are to be parallel, taking into account the image size
dst = np.float32([
[offset, img_size[1]], # bottom-left corner
[offset, 0], # top-left corner
[img_size[0]-offset, 0], # top-right corner
[img_size[0]-offset, img_size[1]] # bottom-right corner
])
OpenCV는 원근 변환 기능을 제공하여 소스 및 대상 지점이 주어진 이미지의 변환 매트릭스를 계산합니다. warpPerspective 기능을 사용하여 조감도 관점 변환이 수행됩니다.
# Calculate the transformation matrix and it's inverse transformation
M = cv2.getPerspectiveTransform(src, dst)
M_inv = cv2.getPerspectiveTransform(dst, src)
warped = cv2.warpPerspective(undist, M, img_size)
목적은 차선 라인 픽셀이 도로와 쉽게 차별화되는 방식으로 이미지를 처리하는 것입니다. 4 개의 변환이 적용된 다음 결합됩니다.
첫 번째 변환은 회색 스케일 이미지에서 x sobel 가져옵니다. 이것은 X 방향의 미분을 나타내며 수직 인 경향이있는 선을 감지하는 데 도움이됩니다. 최소 임계 값 이상의 값 만 유지됩니다.
# Transform image to gray scale
gray_img =cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply sobel (derivative) in x direction, this is usefull to detect lines that tend to be vertical
sobelx = cv2.Sobel(gray_img, cv2.CV_64F, 1, 0)
abs_sobelx = np.absolute(sobelx)
# Scale result to 0-255
scaled_sobel = np.uint8(255*abs_sobelx/np.max(abs_sobelx))
sx_binary = np.zeros_like(scaled_sobel)
# Keep only derivative values that are in the margin of interest
sx_binary[(scaled_sobel >= 30) & (scaled_sobel <= 255)] = 1
두 번째 변환은 회색 스케일 이미지에서 흰색 픽셀을 선택합니다. 흰색은 주어진 그림에서 시행 착오를 사용하여 선택된 200에서 255 사이의 값으로 정의됩니다.
# Detect pixels that are white in the grayscale image
white_binary = np.zeros_like(gray_img)
white_binary[(gray_img > 200) & (gray_img <= 255)] = 1
세 번째 변환은 HLS Colorspace를 사용하여 채도 구성 요소에 있습니다. 이것은 가벼운 콘크리트 도로에서 노란색 선을 감지하는 데 특히 중요합니다.
# Convert image to HLS
hls = cv2.cvtColor(img, cv2.COLOR_BGR2HLS)
H = hls[:,:,0]
S = hls[:,:,2]
sat_binary = np.zeros_like(S)
# Detect pixels that have a high saturation value
sat_binary[(S > 90) & (S <= 255)] = 1
네 번째 변환은 10에서 25 사이의 값을 가진 색조 성분에 있으며, 이는 노란색에 해당하는 것으로 식별되었습니다.
hue_binary = np.zeros_like(H)
# Detect pixels that are yellow using the hue component
hue_binary[(H > 10) & (H <= 25)] = 1
레인 라인 감지는 이미 분해되지 않고 뒤틀린 이진 임계 이미지에서 수행됩니다. 처음에는 히스토그램이 이미지에서 계산됩니다. 이것은 왼쪽 및 오른쪽 차선 라인의 가장 가능성이 높은 X 위치를 감지하기 위해 각 열에서 픽셀 값이 합산됨을 의미합니다.
# Take a histogram of the bottom half of the image
histogram = np.sum(binary_warped[binary_warped.shape[0]//2:,:], axis=0)
# Find the peak of the left and right halves of the histogram
# These will be the starting point for the left and right lines
midpoint = np.int(histogram.shape[0]//2)
leftx_base = np.argmax(histogram[:midpoint])
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
이미지 하단의 이러한 기본 위치부터 시작하면 슬라이딩 윈도우 방법이 라인 픽셀을 검색하여 위로 올라갑니다. 레인 픽셀은 X 및 Y 좌표가 창에 의해 정의 된 영역 내에있을 때 고려됩니다. 충분한 픽셀이 자신이 선의 일부임을 확신 할 수 있도록 감지되면 평균 위치는 계산되어 다음 위쪽 창의 시작점으로 유지됩니다.
# Choose the number of sliding windows
nwindows = 9
# Set the width of the windows +/- margin
margin = 100
# Set minimum number of pixels found to recenter window
minpix = 50
# Identify window boundaries in x and y (and right and left)
win_y_low = binary_warped.shape[0] - (window+1)*window_height
win_y_high = binary_warped.shape[0] - window*window_height
win_xleft_low = leftx_current - margin
win_xleft_high = leftx_current + margin
win_xright_low = rightx_current - margin
win_xright_high = rightx_current + margin
# Identify the nonzero pixels in x and y within the window #
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]
# Append these indices to the lists
left_lane_inds.append(good_left_inds)
right_lane_inds.append(good_right_inds)
이 모든 픽셀은 x 및 y 좌표 목록에 구성됩니다. 이것은 두 차선에서 대칭 적으로 수행됩니다. leftx , lefty , rightx , righty Pixel 위치는 함수에서 반환 된 후, 이후에 2도 다항식이 각 왼쪽과 오른쪽에 장착되어 선택한 픽셀의 최상의 라인을 찾습니다.
# Fit a second order polynomial to each with np.polyfit() ###
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
여기서 식별 된 왼쪽 및 오른쪽 라인 픽셀은 각각 빨간색과 파란색으로 표시됩니다. 2도 다항식은 결과 이미지에서 추적됩니다.
한 비디오 프레임에서 다음 비디오 프레임으로 레인 라인 검색 속도를 높이려면 이전 사이클의 정보가 사용됩니다. 다음 이미지는 이전 차선 선에 근접한 차선 라인을 가질 가능성이 높습니다. 이곳은 이전 이미지의 왼쪽 선과 오른쪽 줄에 적합한 다항식이 검색 영역을 정의하는 데 사용됩니다.
슬라이딩 윈도우 방법은 여전히 사용되지만 히스토그램의 피크 포인트로 시작하는 대신 검색은 창의 너비에 주어진 마진으로 이전 줄을 따라 수행됩니다.
# Set the area of search based on activated x-values within the +/- margin of our polynomial function
left_lane_inds = ((nonzerox > (prev_left_fit[0]*(nonzeroy**2) + prev_left_fit[1]*nonzeroy +
prev_left_fit[2] - margin)) & (nonzerox < (prev_left_fit[0]*(nonzeroy**2) +
prev_left_fit[1]*nonzeroy + prev_left_fit[2] + margin))).nonzero()[0]
right_lane_inds = ((nonzerox > (prev_right_fit[0]*(nonzeroy**2) + prev_right_fit[1]*nonzeroy +
prev_right_fit[2] - margin)) & (nonzerox < (prev_right_fit[0]*(nonzeroy**2) +
prev_right_fit[1]*nonzeroy + prev_right_fit[2] + margin))).nonzero()[0]
# Again, extract left and right line pixel positions
leftx = nonzerox[left_lane_inds]
lefty = nonzeroy[left_lane_inds]
rightx = nonzerox[right_lane_inds]
righty = nonzeroy[right_lane_inds]
검색은 각 왼쪽과 오른쪽에 대해 2도 다항식 함수가 장착 된 leftx , lefty , rightx , righty Pixel 좌표를 반환합니다.
도로에서 반경과 차량의 위치를 계산하려면 픽셀에서 변환하는 데 스케일링 계수가 필요합니다. 해당 스케일링 값은 y 방향으로 30 미터 ~ 720 픽셀, X 치수에서 3.7 미터 ~ 700 픽셀입니다.
다항식 맞춤은 변환을 위해 사용됩니다. 각 오른쪽 및 왼쪽 차선 라인의 적합선에서 정렬 된 픽셀의 X 좌표를 사용하여 변환 계수가 적용되고 다항식이 각각에 수행됩니다.
# Define conversions in x and y from pixels space to meters
ym_per_pix = 30/720 # meters per pixel in y dimension
xm_per_pix = 3.7/700 # meters per pixel in x dimension
left_fit_cr = np.polyfit(ploty*ym_per_pix, left_fitx*xm_per_pix, 2)
right_fit_cr = np.polyfit(ploty*ym_per_pix, right_fitx*xm_per_pix, 2)
# Define y-value where we want radius of curvature
# We'll choose the maximum y-value, corresponding to the bottom of the image
y_eval = np.max(ploty)
# Calculation of R_curve (radius of curvature)
left_curverad = ((1 + (2*left_fit_cr[0]*y_eval*ym_per_pix + left_fit_cr[1])**2)**1.5) / np.absolute(2*left_fit_cr[0])
right_curverad = ((1 + (2*right_fit_cr[0]*y_eval*ym_per_pix + right_fit_cr[1])**2)**1.5) / np.absolute(2*right_fit_cr[0])
곡률의 반경은 이미지 하단의 y 점을 사용하여 계산됩니다. 차량의 위치를 계산하기 위해 픽셀의 다항식 맞춤은 이미지의 하단에서 y에 해당하는 왼쪽 및 오른쪽 차선의 X 위치를 결정하는 데 사용됩니다.
# Define conversion in x from pixels space to meters
xm_per_pix = 3.7/700 # meters per pixel in x dimension
# Choose the y value corresponding to the bottom of the image
y_max = binary_warped.shape[0]
# Calculate left and right line positions at the bottom of the image
left_x_pos = left_fit[0]*y_max**2 + left_fit[1]*y_max + left_fit[2]
right_x_pos = right_fit[0]*y_max**2 + right_fit[1]*y_max + right_fit[2]
# Calculate the x position of the center of the lane
center_lanes_x_pos = (left_x_pos + right_x_pos)//2
# Calculate the deviation between the center of the lane and the center of the picture
# The car is assumed to be placed in the center of the picture
# If the deviation is negative, the car is on the felt hand side of the center of the lane
veh_pos = ((binary_warped.shape[1]//2) - center_lanes_x_pos) * xm_per_pix
이 두 값의 평균은 이미지에서 차선 중심의 위치를 제공합니다. 차선 중앙이 오른쪽으로 오른쪽으로 nbp 양의 픽셀로 이동하면 차량이 nbp * xm_per_pix meters 로 왼쪽으로 이동 함을 의미합니다. 이것은 카메라가 차량의 중앙 축에 장착된다는 가정에 기초합니다.