Localização da pista com técnicas de visão computacional

A oportunidade de carros autônomos com rastreamento de veiculos apresenta tantos desafios técnicos divertidos e únicos.
Desde que entrei na Scale AI como engenheiro de clientes, tenho mergulhado profundamente em todas as coisas autônomas e tentando aprender o máximo possível sobre IA, visão computacional e qualquer coisa remotamente relacionada a essas coisas.
Recentemente, me inscrevi no programa Nanodegree de carro autônomo da Udacity e achei muito interessante o primeiro projeto sobre as técnicas de localização de pistas com visão por computador.
O problema:
O objetivo é gravar vídeos de direção na rodovia e anotar as marcações da faixa do veículo, criando um pipeline de imagens para identificar e, finalmente, desenhar marcações da faixa.

rastreamento de veiculos,rastreador veicular, rastreamento moto

No final, nossa saída será assim:

Minha abordagem:
Entraremos nos detalhes abaixo, mas no nível mais alto, a abordagem que desenvolvi foi:
Extrair apenas os pixels relacionados a branco e amarelo de um determinado quadro de vídeo
Converta a imagem em escala de cinza e aplique um leve desfoque gaussiano para remover o ruído
Calcular arestas usando um detector de arestas inteligente
Aplique uma máscara específica regional para focar apenas a área na frente do carro
Execute uma transformação hough sobre as arestas restantes para identificar linhas retas
Divida as linhas em “esquerda” e “direita” pela inclinação, calcule a inclinação mediana e a interceptação em y para todas as linhas “esquerda” e “direita”
Calcular pontos de interseção do rastreador veicular
Acompanhe os últimos pontos de interseção da linha de quadros e pontos de interceptação x, além de suavizar de quadro a quadro
Desenhe pontos de interseção e interceptação de x suavizados no quadro
Pegue um pouco de pipoca e aproveite as anotações suaves como manteiga
Encerro este blog com algumas reflexões e pensamentos sobre melhorias.
1. Extraindo os pixels branco e amarelo
Para começar, quero explicar por que essa etapa é uma virada no jogo.
Se você pular direto para a detecção de arestas e o cálculo de linhas sem primeiro extrair os matizes de cores que lhe interessam, as arestas resultantes se parecerão com o que está abaixo para esta imagem:

Sem isolamento de cores, se você observar atentamente, a linha longa direita nem está no marcador de faixa real, é a área em que ela vai das sombras ao sol.
Com o isolamento de cores, as marcações de linha amarela e branca aparecem claramente na imagem.
Digite o espaço de cores HSV
Geralmente pensamos nas cores em termos de valores de vermelho, verde e azul ou RGB, em que diferentes proporções de vermelho, verde e azul compõem a cor de um pixel.
Com o RGB, um pixel obtém sua cor pelo brilho ou intensidade de cada uma das três cores, mas isso se torna um problema quando você deseja encontrar cores com brilho variável.
O HSV vira o RGB na cabeça e separa a cor de um pixel em Matiz, Saturação e Valor. Todos os pixels amarelos-amarelos teriam aproximadamente o mesmo valor de matiz, independentemente de serem um amarelo escuro ou um amarelo muito brilhante, exatamente o que queremos, pois as estradas e as condições de iluminação mudam muito!
Extraindo as cores:
Eu mesmo tive que brincar com uma ferramenta on-line para descobrir como as variações de amarelo e branco eram representadas e, em seguida, aplicá-la à minha imagem, criando uma máscara de onde estão as cores que eu quero e removendo todo o resto.
# Converter imagem em espaço de cores HSV
hsv_image = cv2.cvtColor (imagem, cv2.COLOR_BGR2HSV)
# Definir faixa de cores no HSV
lower_yellow = np.array ([30,80,80])
upper_yellow = np.array ([255.255.255])
lower_white = np.array ([0,0,200])
upper_white = np.array ([255,20,255])
# Limite a imagem HSV para obter apenas cores azuis
yellow_mask = cv2.inRange (hsv_image, lower_yellow, upper_yellow)
white_mask = cv2.inRange (hsv_image, lower_white, upper_white)
mask = cv2.bitwise_or (máscara amarela, máscara branca)
# Bitwise-AND mascara a imagem original
color_isolated = cv2.bitwise_and (imagem, imagem, máscara = máscara)
Uma observação tática rápida, você precisa converter valores HSV em uma escala de 0 a 255 em vez de, digamos, 0-360 graus para uma Matiz ou Saturação de “70%”.

rastreamento de veiculos,rastreador veicular, rastreamento moto
Uma imagem após extrair os pixels amarelo e branco dela
2. Convertendo imagem em escala de cinza e desfocagem
O desfoque é uma técnica comum para remover “ruído” de pixels específicos, ajudando linhas e transições de arestas a serem mais suaves em vez de irregulares.
# Converter em escala de cinza
cinza = cv2.cvtColor (cor_isolada, cv2.COLOR_RGB2GRAY)
# Definir um tamanho de kernel e aplicar suavização gaussiana
kernel_size = 5
blur_gray = cv2.GaussianBlur (cinza, (tamanho do kernel, tamanho do kernel), 0)

Convertendo para escala de cinza e depois sutilmente desfocada – queremos fazer a mesma coisa apenas com nossas cores isoladas
3. Detecção de Borda
Queremos extrair todas as arestas em torno das regiões coloridas para extrair as linhas específicas em nossa imagem. Vamos usar isso em algumas etapas para aproximar as linhas da faixa.
Eu usei o algoritmo Canny Edge Detector com alguns limites razoáveis ​​de detecção.
# Defina nossos parâmetros para Canny e aplique
low_threshold = 50
high_threshold = 150
bordas = cv2.Canny (blur_gray, low_threshold, high_threshold)

Resultados da execução de nossa detecção de borda, incluindo uma visão ampliada da faixa direita
4. Mascaramento Regional
Você pode perceber que há um monte de ruído na extrema esquerda e direita da imagem.
Dada a posição fixa da câmera no veículo, é seguro supor que as faixas na frente do veículo com as quais nos preocupamos sempre estarão aproximadamente nas mesmas partes da imagem.
Podemos remover outras linhas e ruídos em potencial em nossa imagem aplicando uma máscara regional que permite que apenas pixels na região existam, deixando todos os outros pretos.
Eu vim com essa região para limitar minha faixa, procurando:

Usamos o método fillPoly do OpenCV para criar uma máscara regional e aplicá-la à nossa imagem.
# Em seguida, criaremos uma imagem de arestas mascaradas usando cv2.fillPoly ()
mask = np.zeros_like (arestas)
ignore_mask_color = 255
# Defina um polígono de quatro lados para mascarar
imshape = image.shape
vértices = np.array ([
[
(0, formatar [0]), # canto inferior esquerdo
(imshape [1] * .35, imshape [0] * .6), # canto superior esquerdo
(imshape [1] * .65, imshape [0] * .6), # canto superior direito
(imshape [1], imshape [0]) # canto inferior direito
]
], dtype = np.int32)

# Faça o mascaramento
cv2.fillPoly (máscara, vértices, ignore_mask_color)
masked_edges = cv2.bitwise_and (arestas, máscara)
Agora temos uma imagem parecida com esta:

5. Obtenha linhas com uma Hough Transform
Uma transformação Hough é uma das coisas mais legais e impressionantes do rastreamento moto para mim.
Não poderei explicar completamente aqui, mas ele essencialmente mapeia cada pixel para uma equação linear em que os eixos 2d vão de “x” e “y” a “m” e “b” representando as partes da equação para uma linha, y = mx * be, em seguida, colocamos tudo em coordenadas polares em vez de coordenadas cartesianas para evitar a divisão por 0 erros, muita diversão!

rastreamento de veiculos,rastreador veicular, rastreamento moto
A parte importante é que obtemos um conjunto de pontos, x1, y1, x2, y2, representando o início e os pontos finais das linhas encontradas em nossa imagem.
# Definir os parâmetros de transformação Hough
rho = resolução de distância 2 # em pixels
theta = np.pi / 180 # resolução angular em radianos
threshold = 30 # número mínimo de votos
min_line_length = 20 # número mínimo de pixels que formam uma linha
max_line_gap = 1 # intervalo máximo em pixels entre linhas
# Execute Hough na imagem detectada pela borda
# Output “lines” é uma matriz que contém pontos finais de linhas
lines = cv2.HoughLinesP (
masked_edges, rho, theta, threshold,
np.array ([]), min_line_length, max_line_gap
)
6. Identifique nossas equações de pista
Nossa transformação Hough nos deu um conjunto de pontos de extremidade de linha para todas as linhas encontradas em nossa última imagem acima. Felizmente, a maioria dessas linhas representará as bordas longas das marcações da faixa. Às vezes, podem ser retornadas 5, 10 ou até mais de 20 linhas, dependendo do nível de ruído e das marcações de faixa capturadas.
Etapa 1: inicie a matemática do ensino médio e calcule as partes m e b para cada linha com nossa prática fórmula de subida / corrida.
# Repita as “linhas” de saída para calcular m e b
mxb = matriz np ([[0,0]])
para linha em linha:
para x1, y1, x2, y2 na linha:
m = (y2-y1) / (x2-x1)
b = y1 + -1 * m * x1
mxb = np.vstack ((mxb, [m, b]))
Etapa 2: Separe as equações das linhas nas faixas esquerda e direita e calcule a inclinação e a interceptação em mediana.
Eu escolhi usar a mediana para reduzir os valores discrepantes, eles são realmente possíveis se, por acaso, traçamos uma linha para outra parte da estrada ou cena que não se relaciona diretamente a um marcador de linha.
Uma percepção importante é que os marcadores da faixa esquerda e da direita terão inerentemente declives opostos, de modo que um seja positivo e outro negativo.
Faremos uma indexação de matriz sofisticada para calcular nossa inclinação mediana e interceptar as linhas esquerda e direita de nossa inclinação.
median_right_m = np.median (mxb [mxb [:, 0]> 0,0])
median_left_m = np.median (mxb [mxb [:, 0] <0,0])
median_right_b = np.median (mxb [mxb [:, 0]> 0,1])
median_left_b = np.median (mxb [mxb [:, 0] <0,1])
Incrível, temos nossas equações lineares para nossas linhas!
7. Calcular pontos de interseção de linha e pontos de interceptação em X
Agora que temos nossas duas linhas, vamos descobrir onde elas se cruzam, bem como a parte inferior da imagem. Isso será útil quando quisermos desenhá-los em nossa imagem original.
Para uma reciclagem, aqui estão algumas equações da Wikipedia dadas ax + c = bx + d.

# Calcular o ponto de interseção de nossas duas linhas
x_intersect = (median_left_b – median_right_b) / (median_right_m – median_left_m)
y_intersect = median_right_m * (median_left_b – median_right_b) / (median_right_m – median_left_m) + median_right_b
# Calcular os pontos de interceptação X
# x = (y – b) / m
left_bottom = (imshape [0] – median_left_b) / median_left_m
right_bottom = (imshape [0] – median_right_b) / median_right_m
8. Histórico de linhas de rastreamento para suavização
Se parássemos aqui, teríamos linhas desenhadas em nossas imagens, mas, quando você colocar essas anotações em um vídeo, verá muito jumpiness como pequenas diferenças nas equações de linha e como os algoritmos de detecção de bordas destacam os pixels, causando pequenas variações em nossas variáveis ​​de equação.
A solução é sacrificar uma pequena quantidade de capacidade de resposta por uma transição suavizada das equações de linha vistas anteriormente.
Quando usado em uma configuração de vídeo, meu algoritmo recebe ativamente como entrada, utiliza e retorna um histórico de equações de linha vistas anteriormente. Vamos ver como isso se parece mais abaixo.
# Crie uma matriz de histórico para suavizar
num_frames_to_median = 19
new_history = [left_bottom, right_bottom, x_intersect, y_intersect]
if (history.shape [0] == 1): # Primeira vez, crie uma matriz maior
history = new_history
para i no intervalo (num_frames_to_median):
history = np.vstack ((history, new_history))
elif (not (np.isnan (new_history) .any ())):
história [: – 1 ,:] = história [1:]
história [-1,:] = new_history
# Calcular os pontos de linha suavizados
left_bottom_median = np.median (histórico [:, 0])
right_bottom_median = np.median (histórico [:, 1])
x_intersect_median = np.median (histórico [:, 2])
y_intersect_median = np.median (histórico [:, 3])
Acontece que as faixas se movem relativamente lentamente de um quadro de vídeo para o próximo, para que possamos ter uma contagem relativamente alta de quantos quadros estamos usando a mediana para calcular a linha atual.
Após algumas experiências, escolhi 19 quadros para calcular a mediana de, que em 25 quadros por segundo, equivale a um atraso teórico de (19/2) / 25, ou 0,38 segundos. Ao fazer isso, você equilibra suavidade com atraso.
9. Desenhando linhas em quadros de vídeo
Etapa 1: quando tivermos nossos pontos de linha finais, podemos desenhar linhas em uma única imagem.
# Crie uma imagem em branco para desenhar linhas
line_image = np.copy (imagem) * 0
# Crie nossas linhas
cv2.line (
line_image,
(np.int_ (left_bottom_median), imshape [0]),
(np.int_ (x_intersect_median), np.int_ (y_intersect_median)),
(255,0,0), 10
)
cv2.line (
line_image,
(np.int_ (right_bottom_median), imshape [0]),
(np.int_ (x_intersect_median), np.int_ (y_intersect_median)),
(0,0.255), 10
)
# Desenhe as linhas na imagem
lane_edges = cv2.addWeighted (imagem, 0,8, line_image, 1, 0)
Etapa 2: processar um vídeo inteiro
history = np.array ([[0, 0, 0, 0]]) # Inicializar histórico
images_list = []
# Leia no videoclipe de base
base_clip = VideoFileClip (input_filename)
# Processe cada quadro de vídeo, observe como o “histórico” é passado
para quadro em base_clip.iter_frames ():
[imagem, histórico] = draw_lanes_on_img (quadro, histórico)
images_list.append (imagem)
# Crie um novo ImageSequenceClip, ou seja, nosso vídeo!
clip = ImageSequenceClip (images_list, fps = base_clip.fps)
# Salve nosso vídeo
% time clip.write_videofile (output_filename, audio = False)
10. Desfrute de algumas anotações de pista suaves
Abaixo estão alguns exemplos de vídeos processados ​​com o código acima:

Reflexões
O que poderia ser feito ainda:
Tive três idéias que ainda não implementei no código acima.
Criando um kernel personalizado para detecção de borda
Nas etapas 2 e 3, aplicamos um desfoque à nossa imagem para remover o ruído e, em seguida, encontrar as bordas. Embora sejam coisas que você pode fazer para convencer uma imagem, existem todos os tipos de kernels diferentes que você pode criar para destacar, diminuir ou modificar o valor de um pixel com base nos pixels ao redor.
Em teoria, como as faixas têm ângulos um pouco fixos, podemos construir um kernel personalizado que destaca pixels onde os pixels acima e abaixo dele formam uma linha angular.
Um filtro Sobel é outro tipo de detector de borda útil para encontrar, muito especificamente, linhas retas nas imagens. Poderíamos emprestar conceitos semelhantes de como os filtros Sobel funcionam para criar um detector de borda para linhas angulares nos ângulos aproximados que a linha da pista segue.
Filtrando Linhas de Transformação Hough
Embora a maioria das linhas que retornam de nossa transformação Hough esteja esperançosamente relacionada aos marcadores de faixa, podemos projetar quaisquer linhas óbvias onde esse não for o caso.
Diretamente na transformação Hough, ou no processamento das linhas, devemos ser capazes de filtrar, digamos, quaisquer linhas horizontais como as que conhecemos não poderiam estar relacionadas a um marcador de faixa.
Alavancar linhas paralelas
Limitações:
Sem comutação de faixa
Esse algoritmo assume que você já está em uma faixa e permanecerá nessa faixa. Quando você está no meio da mudança de faixa, seus ângulos de linha ficam quase verticais e acho que esse algoritmo enfrentaria um problema, pois estou separando as faixas em “esquerda” e “direita”. Uma abordagem melhor seria desenhar e rastrear TODAS as faixas vistas e depois calcular a posição do carro em relação às faixas.
Os marcadores de faixa devem estar presentes e explícitos
Normalmente, os marcadores de faixa nas rodovias são bastante explícitos. Se um marcador de faixa simplesmente não fosse pintado, coberto por alguma coisa ou se aparecesse uma saída de rodovia, esse algoritmo não saberia o que fazer.
Contudo
Eu me diverti muito trabalhando neste projeto. Existem muitas maneiras muito melhores de resolver isso em 2020, mas voltar ao básico e ver até onde você pode levar as técnicas clássicas de visão por computador foram uma explosão!

 

 

Fonte

Site Footer