Алгоритм компьютерного зрения для вычисления дорожного кривизны и смещения дорожного движения с использованием обработки изображений OpenCV, калибровки камеры, перспективного преобразования, цветовых масок, протлеев и полиномиальной подгонки.
Проект Advanced Lane Scince Sucgity находится на шаг дальше от [обнаружения линий линий полосы] при определении геометрии дороги впереди.
Используя видеозапись вождения на шоссе, цель этого проекта - вычислить радиус кривизны дороги. Изогнутые дороги - более сложная задача, чем прямые. Чтобы правильно вычислить кривизну, необходимо идентифицировать линии полосы движения, но в дополнение к этому изображения должны быть неискренными. Преобразование изображения необходимо для калибровки камеры и для преобразования перспективы, чтобы получить вид на глаза птичьего глаза на дорогу.
Этот проект реализован в 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 используется для преобразования изображений, используя матрицу камеры и коэффициенты искажения.
Результат техники калибровки камеры видна при сравнении этих изображений. В то время как на шахматной доске искажения более очевидны, на дорожной картинке оно более тонкое. Тем не менее, неискаженная картина приведет к неправильному расчету кривизны дороги.
Для калуклюатной кривизны идеальная перспектива - это взгляд птичьего глаза. Это означает, что дорога воспринимается сверху, а не под углом через лобовое стекло транспортного средства.
Эта перспективная преобразование вычисляется с использованием сценария прямой полосы движения и предварительного общего знания, что линии полосы движения на самом деле параллельны. Источники и точки назначения идентифицируются непосредственно из изображения для перспективного преобразования.
#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 Function, выполняется перспектива перспективы птичьего взгляда.
# 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)
Цель состоит в том, чтобы обработать изображение таким образом, что пиксели линии полосы движения сохранялись и легко отличаются от дороги. Четыре преобразования применяются, а затем объединяются.
Первое преобразование берет 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
Четвертое преобразование находится на компоненте HUE со значениями от 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 положения пикселя возвращаются из функции, а впоследствии полином второй степени устанавливается на каждой левой и правой стороне, чтобы найти лучшую линию подгонки выбранных пикселей.
# Fit a second order polynomial to each with np.polyfit() ###
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 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]
Поиск возвращает координаты leftx , lefty , rightx , righty пиксель, которые оснащены полиномиальной функцией второй степени для каждой левой и правой стороны.
Чтобы рассчитать радиус и положение транспортного средства на дороге в метрах, для преобразования пикселей необходимы коэффициенты масштабирования. Соответствующие значения масштабирования составляют от 30 метров до 720 пикселей в направлении Y и от 3,7 метра до 700 пикселей в измерении X.
Посадка полинома используется для преобразования. Используя 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 в нижней части изображения. Чтобы рассчитать положение транспортного средства, полиномиальная посадка в пикселях используется для определения положения x левой и правой полосы, соответствующей Y в нижней части изображения.
# 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 . Это основано на предположении, что камера установлена на центральной оси транспортного средства.