Algoritmo de visión por computadora para calcular la curvatura de la carretera y el desplazamiento del vehículo del carril utilizando el procesamiento de imágenes OpenCV, la calibración de la cámara, la transformación de perspectiva, las máscaras de color, las solentajes y el ajuste polinomial.
El proyecto de búsqueda de carril avanzado está un paso más allá de [detección de líneas de carril] al identificar la geometría del camino por delante.
Utilizando una grabación de video de la conducción de carreteras, el objetivo de este proyecto es calcular el radio de la curvatura de la carretera. Las carreteras curvas son una tarea más desafiante que las heterosexuales. Para calcular correctamente la curvatura, las líneas del carril deben identificarse, pero además de eso, las imágenes deben ser no distorsionadas. La transformación de la imagen es necesaria para la calibración de la cámara y para la transformación de perspectiva para obtener la vista de un pájaro de la carretera.
Este proyecto se implementa en Python y utiliza la biblioteca de procesamiento de imágenes OpenCV. El código fuente se puede encontrar en AdvancedLaneFinding.ipynb
[La distorsión óptica] es un fenómeno físico que ocurre en la grabación de imágenes, en el que las líneas rectas se proyectan como las ligeramente curvas cuando se perciben a través de las lentes de la cámara. El video de conducción de la carretera se graba utilizando la cámara frontal en el automóvil y las imágenes están distorsionadas. Los coeficientes de distorsión son específicos para cada cámara y se pueden calcular utilizando formas geométricas conocidas.
Las imágenes de tablero de ajedrez capturadas con la cámara incrustada se proporcionan en la carpeta camera_cal . La ventaja de estas imágenes es que tienen un alto contraste y geometría conocida. Las imágenes proporcionadas presentan 9 * 6 esquinas para trabajar.
# 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)
Los puntos de objeto se establecen en función de la comprensión común de que en un patrón de tablero de ajedrez, todos los cuadrados son iguales. Esto implica que los puntos de objeto tendrán coordenadas X e Y generadas a partir de índices de cuadrícula, y Z siempre es 0. Los puntos de imagen representan los puntos de objeto correspondientes que se encuentran en la imagen utilizando la función de 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)
Después de escanear a través de todas las imágenes, la lista de puntos de imagen tiene suficientes datos para comparar con los puntos del objeto para calcular la matriz de la cámara y los coeficientes de distorsión. Esto conduce a una matriz de cámara precisa y una identificación de coeficientes de distorsión utilizando la función 'CalibrateCamera'.
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
undist = cv2.undistort(img, mtx, dist, None, mtx)
La función OpenCV undistort se usa para transformar las imágenes utilizando la matriz de la cámara y los coeficientes de distorsión.
El resultado de la técnica de calibración de la cámara es visible al comparar estas imágenes. Mientras que en la imagen del tablero de ajedrez, la distorsión es más obvia, en la imagen de la carretera es más sutil. Sin embargo, una imagen sin distorsionar conduciría a un cálculo incorrecto de la curvatura de la carretera.
Para calucluar la curvatura, la perspectiva ideal es la vista de un pájaro. Esto significa que el camino se percibe desde arriba, en lugar de en ángulo a través del parabrisas del vehículo.
Esta transformación de perspectiva se calcula utilizando un escenario de carril recto y un conocimiento común previo de que las líneas de carril son de hecho paralelas. Los puntos de origen y destino se identifican directamente desde la imagen para la transformación de perspectiva.
#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 proporciona funciones de transformación de perspectiva para calcular la matriz de transformación para las imágenes dados los puntos de origen y destino. Utilizando la función warpPerspective , se realiza la transformación de la perspectiva de la vista del pájaro.
# 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)
El objetivo es procesar la imagen de tal manera que los píxeles de la línea del carril se conservan y se diferencien fácilmente de la carretera. Se aplican cuatro transformaciones y luego se combinan.
La primera transformación toma el x sobel en la imagen a escala gris. Esto representa la derivada en la dirección X y ayuda a detectar líneas que tienden a ser verticales. Solo se mantienen los valores superiores a un umbral mínimo.
# 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
La segunda transformación selecciona los píxeles blancos en la imagen a escala gris. El blanco se define por los valores entre 200 y 255 que fueron elegidos utilizando prueba y error en las imágenes dadas.
# 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
La tercera transformación está en el componente de saturación utilizando el espacio de colores HLS. Esto es particularmente importante para detectar líneas amarillas en el camino de concreto claro.
# 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
La cuarta transformación está en el componente HUE con valores de 10 a 25, que se identificaron como correspondientes al amarillo.
hue_binary = np.zeros_like(H)
# Detect pixels that are yellow using the hue component
hue_binary[(H > 10) & (H <= 25)] = 1
La detección de la línea del carril se realiza en imágenes umbrales binarias que ya han sido sin distorsiones y deformadas. Inicialmente se calcula un histograma en la imagen. Esto significa que los valores de píxeles se suman en cada columna para detectar la posición X más probable de las líneas de carril izquierda y derecha.
# 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
Comenzando con estas posiciones base en la parte inferior de la imagen, el método de ventana deslizante se aplica hacia arriba buscando píxeles de línea. Los píxeles del carril se consideran cuando las coordenadas X e Y están dentro del área definida por la ventana. Cuando se detectan suficientes píxeles para confiar en que son parte de una línea, su posición promedio se calcula y se mantiene como punto de partida para la siguiente ventana ascendente.
# 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)
Todos estos píxeles se unen en una lista de sus coordenadas X e Y. Esto se hace simétricamente en ambas líneas de carril. leftx , lefty , rightx y las posiciones de píxeles righty se devuelven de la función y luego, se coloca un polinomio de segundo grado en cada lado izquierdo y derecho para encontrar el mejor ajuste de línea de los píxeles seleccionados.
# Fit a second order polynomial to each with np.polyfit() ###
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
Aquí, los píxeles de línea izquierda y derecha identificados están marcados en rojo y azul respectivamente. El polinomio de segundo grado se remonta a la imagen resultante.
Para acelerar la búsqueda de la línea del carril de un marco de video al siguiente, se utiliza información del ciclo anterior. Es más probable que la siguiente imagen tenga líneas de carril en proximidad a las líneas de carril anteriores. Aquí es donde el ajuste polinomial para la línea izquierda y la línea derecha de la imagen anterior se utilizan para definir el área de búsqueda.
El método de la ventana deslizante todavía se usa, pero en lugar de comenzar con los puntos máximos del histograma, la búsqueda se realiza a lo largo de las líneas anteriores con un margen dado para el ancho de la ventana.
# 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]
La búsqueda devuelve las coordenadas de píxeles leftx , lefty , rightx , Righty righty que están equipadas con una función polinomial de segundo grado para cada lado izquierdo y derecho.
Para calcular el radio y la posición del vehículo en la carretera en metros, se necesitan factores de escala para convertirse de píxeles. Los valores de escala correspondientes son de 30 metros a 720 píxeles en la dirección Y y de 3.7 metros a 700 píxeles en la dimensión X.
Se utiliza un ajuste polinomial para hacer la conversión. Usando las coordenadas X de los píxeles alineados desde la línea ajustada de cada línea de carril derecha e izquierda, se aplican los factores de conversión y se realiza el ajuste polinomial en cada uno.
# 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])
El radio de la curvatura se calcula usando el punto Y en la parte inferior de la imagen. Para calcular la posición del vehículo, el ajuste polinomio en los píxeles se usa para determinar la posición X del carril izquierdo y derecho correspondiente a la y en la parte inferior de la imagen.
# 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
El promedio de estos dos valores da la posición del centro del carril en la imagen. Si el centro del carril se desplaza hacia la derecha por la cantidad de píxeles nbp , eso significa que el automóvil se desplaza hacia la izquierda por nbp * xm_per_pix meters . Esto se basa en la suposición de que la cámara está montada en el eje central del vehículo.