Algoritmo de visão computacional para calcular a curvatura da estrada e o deslocamento do veículo de pista usando o processamento da imagem OpenCV, calibração da câmera, transformação de perspectiva, máscaras coloridas, soluços e ajuste polinomial.
O Projeto Avançado de Finding de Finding é um passo mais longe da [detecção de linhas de pista] na identificação da geometria da estrada à frente.
Usando uma gravação em vídeo da direção da estrada, o objetivo deste projeto é calcular o raio da curvatura da estrada. As estradas curvas são uma tarefa mais desafiadora do que as retas. Para calcular corretamente a curvatura, as linhas de pista precisam ser identificadas, mas, além disso, as imagens precisam ser não distorcidas. A transformação da imagem é necessária para a calibração da câmera e para a transformação de perspectiva para obter uma visão ocular de um pássaro da estrada.
Este projeto é implementado no Python e usa a Biblioteca de Processamento de Imagens OpenCV. O código -fonte pode ser encontrado no AdvancedLaneFinding.ipynb
[Distorção óptica] é um fenômeno físico que ocorre na gravação de imagem, na qual as linhas retas são projetadas como ligeiramente curvadas quando percebidas através das lentes da câmera. O vídeo de direção da estrada é gravado usando a câmera frontal no carro e as imagens são distorcidas. Os coeficientes de distorção são específicos para cada câmera e podem ser calculados usando formas geométricas conhecidas.
As imagens de tabuleiro de xadrez capturadas com a câmera incorporada são fornecidas na pasta camera_cal . A vantagem dessas imagens é que elas têm alto contraste e geometria conhecida. As imagens fornecidas presentes 9 * 6 cantos para trabalhar.
# 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)
Os pontos de objeto são definidos com base no entendimento comum de que, em um padrão de tabuleiro de xadrez, todos os quadrados são iguais. Isso implica que os pontos do objeto terão coordenadas X e Y geradas a partir de índices de grade e z é sempre 0. Os pontos de imagem representam os pontos de objeto correspondentes encontrados na imagem usando a função findChessboardCorners do OpenCV.
# 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)
Após a digitalização de todas as imagens, a lista de pontos de imagem possui dados suficientes para comparar com os pontos do objeto para calcular a matriz da câmera e os coeficientes de distorção. Isso leva a uma matriz precisa da câmera e a identificação dos coeficientes de distorção usando a função 'calibratecamera'.
ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)
undist = cv2.undistort(img, mtx, dist, None, mtx)
A função OpenCV undistort é usada para transformar as imagens usando a matriz da câmera e os coeficientes de distorção.
O resultado da técnica de calibração da câmera é visível ao comparar essas imagens. Enquanto na imagem do tabuleiro de xadrez, a distorção é mais óbvia, na foto da estrada é mais sutil. No entanto, uma imagem não distorcida levaria a um cálculo incorreto da curvatura da estrada.
Para calucuar a curvatura, a perspectiva ideal é a visão dos olhos de um pássaro. Isso significa que a estrada é percebida de cima, em vez de em um ângulo através do pára -brisa do veículo.
Essa transformação de perspectiva é calculada usando um cenário de faixa reta e conhecimento comum anterior de que as linhas de pista são de fato paralelas. Os pontos de origem e destino são identificados diretamente da imagem para a transformação da 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
])
O OpenCV fornece funções de transformação de perspectiva para calcular a matriz de transformação para as imagens, dados os pontos de origem e destino. Usando a função warpPerspective , é realizada a transformação da perspectiva do olho do pássaro.
# 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)
O objetivo é processar a imagem de tal maneira que os pixels da linha de pista são preservados e facilmente diferenciados da estrada. Quatro transformações são aplicadas e depois combinadas.
A primeira transformação leva o x sobel na imagem em escala cinza. Isso representa o derivado na direção x e ajuda a detectar linhas que tendem a ser verticais. Somente os valores acima de um limite mínimo são mantidos.
# 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
A segunda transformação seleciona os pixels brancos na imagem em escala cinza. O branco é definido por valores entre 200 e 255, que foram escolhidos usando tentativa e erro nas imagens fornecidas.
# 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
A terceira transformação está no componente de saturação usando o espaço de cores HLS. Isso é particularmente importante para detectar linhas amarelas na estrada 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
A quarta transformação está no componente de matiz com valores de 10 a 25, que foram identificados como correspondentes ao amarelo.
hue_binary = np.zeros_like(H)
# Detect pixels that are yellow using the hue component
hue_binary[(H > 10) & (H <= 25)] = 1
A detecção da linha da pista é realizada em imagens limiares binárias que já foram distorcidas e distorcidas. Inicialmente, um histograma é calculado na imagem. Isso significa que os valores de pixels são somados em cada coluna para detectar a posição X mais provável das linhas de pista esquerda e direita.
# 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
Começando com essas posições base na parte inferior da imagem, o método da janela deslizante é aplicado subindo para cima, procurando pixels de linha. Os pixels de pista são considerados quando as coordenadas X e Y estão dentro da área definida pela janela. Quando pixels suficientes são detectados para ter certeza de que fazem parte de uma linha, sua posição média é calculada e mantida como ponto de partida para a próxima janela 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 esses pixels são montados em uma lista de suas coordenadas X e Y. Isso é feito simetricamente nas duas linhas de pista. As posições de pixels leftx , lefty , rightx e righty são devolvidas da função e depois, um polinômio de segundo grau é montado em cada lado esquerdo e direito para encontrar o melhor ajuste de linha dos pixels selecionados.
# Fit a second order polynomial to each with np.polyfit() ###
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
Aqui, os pixels da linha esquerda e direita identificados estão marcados em vermelho e azul, respectivamente. O polinômio de segundo grau é rastreado na imagem resultante.
Para acelerar a pesquisa da linha de pista de um quadro de vídeo para o próximo, são usadas informações do ciclo anterior. É mais provável que a próxima imagem tenha linhas de faixa nas proximidades das linhas de pista anteriores. É aqui que o ajuste polinomial para a linha esquerda e a linha direita da imagem anterior são usados para definir a área de busca.
O método da janela deslizante ainda é usado, mas, em vez de começar com os pontos de pico do histograma, a pesquisa é realizada ao longo das linhas anteriores com uma determinada margem para a largura da janela.
# 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]
A pesquisa retorna as coordenadas de pixels leftx , lefty , rightx e righty que estão equipados com uma função polinomial de segundo grau para cada lado esquerdo e direito.
Para calcular o raio e a posição do veículo na estrada em metros, são necessários fatores de escala para converter de pixels. Os valores de escala correspondentes são de 30 metros a 720 pixels na direção y e 3,7 metros a 700 pixels na dimensão x.
Um ajuste polinomial é usado para fazer a conversão. Usando as coordenadas X dos pixels alinhados da linha ajustada de cada linha de pista direita e esquerda, os fatores de conversão são aplicados e o ajuste polinomial é realizado em cada um.
# 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])
O raio da curvatura é calculado usando o ponto Y na parte inferior da imagem. Para calcular a posição do veículo, o ajuste polinomial em pixels é usado para determinar a posição X da faixa esquerda e direita correspondente ao y na parte inferior da imagem.
# 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
A média desses dois valores fornece a posição do centro da pista na imagem. Se o centro da pista for deslocado para a direita pela quantidade nbp de pixels, o que significa que o carro é deslocado para a esquerda pelos nbp * xm_per_pix meters . Isso se baseia no pressuposto de que a câmera está montada no eixo central do veículo.