Abstrakt: Aksidentet e trafikut janë bërë një nga problemet më serioze në botën e sotme. Rritja e numrit të automjeteve, gabimet njerëzore ndaj rregullave të qarkullimit rrugor dhe vështirësia për të mbikëqyrur rreziqet e situatës nga drejtuesit e mjeteve po kontribuojnë në shumicën e aksidenteve në rrugë. Zbulimi i korsisë është një komponent thelbësor për automjetet autonome. Në këtë punim, unë kam projektuar dhe zbatuar një algoritëm automatik të zbulimit të shënimit të korsisë. Eksperimente të ndryshme në një kontekst të SHBA-së tregojnë se qasja e propozuar mund të jetë shumë efektive në zbulimin e korsive. Algoritmi verifikohet si për korsitë e bardha ashtu edhe për korsitë e verdha duke përmirësuar zbulimin e ngjyrave dhe mesataren e modeleve kohore.

Fillimi

Ne do të zhvillojmë një tubacion të thjeshtë duke përdorur OpenCV dhe Python për të gjetur linjat e korsive në një imazh, më pas do ta zbatojmë këtë tubacion në një furnizim të plotë video. Programi i Udacity's Self-Driving Car më nisi me këto aftësi dhe ju rekomandoj shumë këtë program nëse doni të futeni në këtë fushë.

Ne do të shfrytëzojmë paketat e njohura SciPy dhe NumPy për të bërë llogaritje shkencore dhe paketën OpenCV për algoritmet e vizionit kompjuterik:

import matplotlib.pyplot as plt
import numpy as np
import cv2

Tubacioni i Zbulimit të Korsisë

Përsëritja e parë e algoritmit të zbulimit të korsisë funksionon mirë në rrugët jo të lakuara me një kamerë kryesore të montuar në pultin e automjetit. Në këto video kushtet e motit janë të mira dhe është ditë. Ne do të trajtojmë hijet, korsitë e vazhdueshme dhe të ndërprera me ngjyra të ndryshme.

Heq zhurmën:Duke filluar me një kornizë nga një video e marrë nga një kamerë e vetme në panelin e kontrollit, ne pastrojmë imazhin duke aplikuar një "turbullim Gaussian". Ky hap do të heqë zhurmën dhe detajet e vogla nga imazhi, siç janë objektet e largëta që janë të parëndësishme për qëllimin tonë.

def remove_noise(image, kernel_size):
    return cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)

Hiqni informacionin e ngjyrave: Hapi i dytë është konvertimi i imazhit në shkallë gri përpara se të izoloni rajonin e interesit. Ky hap do të nxjerrë në pah pikselët me një vlerë më të lartë ndriçimi, duke përfshirë ato që përcaktojnë korsitë e shënjimit në kornizë.

def discard_colors(image):
    return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

Zbulimi i skajeve:Hapi tjetër është të ekzekutoni "Zbulimi i skajeve të mundshëm". Algoritmi Canny zbulon skajet në një foto duke kërkuar ndryshime të shpejta në ngjyrë midis një piksel dhe fqinjëve të tij. Hapi i turbullimit dhe grisë do të ndihmojë që linjat e korsisë kryesore të dallohen. Rezultati do të japë një imazh bardh e zi.

def detect_edges(image, low_threshold, high_threshold):
    return cv2.Canny(image, low_threshold, high_threshold)
image = detect_edges(gray_image, low_threshold=50, high_threshold=150)
plt.imshow(image, cmap='gray')

Rajoni i interesit: Është e rëndësishme të hiqni sa më shumë zhurmë të jetë e mundur brenda një kornize. Duke pasur parasysh pozicionin dhe orientimin e kamerës, mund të supozojmë se korsitë do të vendosen në gjysmën e poshtme të figurës. Ne thjesht mund ta izolojmë atë zonë duke përdorur një formë trapezi. Ndonëse mund të jetë joshëse për të koduar hardkodi vendin më të mirë të mundshëm për të kërkuar linja, linjat mund të lëvizin ndërsa vozitni. Për këtë arsye, përdoret një raport i thjeshtë për vendosjen e një forme poligonale. Ne aplikojmë një raport bujar pasi nuk duam që rajoni ynë të jetë shumë i ngushtë duke rezultuar në mungesën e korsive jashtë rajonit të interesit.

def region_of_interest(image, vertices):
    # defining a blank mask to start with mask = np.zeros_like(image)
    # defining a 3 channel or 1 channel color to fill the mask with depending on the input image
    if len(image.shape) > 2:
        channel_count = image.shape[2]  # i.e. 3 or 4 depending on your image
        ignore_mask_color = (255,) * channel_count
    else:
        ignore_mask_color = 255
    # filling pixels inside the polygon defined by "vertices" with the fill color
    cv2.fillPoly(mask, vertices, ignore_mask_color) . 
  
  # returning the image only where mask pixels are non-zero
    masked_image = cv2.bitwise_and(image, mask)
    return masked_image

xsize = img.shape[1]
ysize = img.shape[0]
dx1 = int(0.0725 * xsize)
dx2 = int(0.425 * xsize)
dy = int(0.6 * ysize)
# calculate vertices for region of interest
vertices = np.array([[(dx1, ysize), (dx2, dy), (xsize - dx2, dy), (xsize - dx1, ysize)]], dtype=np.int32)
image = region_of_interest(image, vertices)

Vizatoni vija:Ky funksion vizaton vija me ngjyrë dhe trashësi.
Vizatohen në imazh në vend. Nëse dëshironi t'i bëni linjat gjysmë transparente, mendoni të kombinoni këtë funksion me funksionin weighted_image() të dhënë më poshtë.

def draw_lines(image, lines, color=[255, 0, 0], thickness=2):
    for line in lines:
        for x1,y1,x2,y2 in line:
            cv2.line(image, (x1, y1), (x2, y2), color, thickness)

Transformimi Hough: Duke përdorur linjat probabiliste të Hough, ne identifikojmë vendndodhjen e vijave të korsisë në rrugë. Algoritmi "Hough transform" nxjerr të gjitha linjat që kalojnë nëpër secilën nga pikat tona të skajit dhe i grupon ato sipas ngjashmërisë. Funksioni HoughLinesP në OpenCV kthen një grup rreshtash të organizuar nga pikat fundore (x1, x1, x2, x2). Është e rëndësishme të kuptojmë se ne nuk po shohim më pikë në (x, y) sistemin koordinativ kartezian, por tani po shohim pika në (ρ, θ) sistemi i koordinatave polar në të cilin vepron transformimi Hough. Kuptimi i pjesëve të brendshme të këtij transformimi ndihmon në manipulimin e parametrave të ofruara për funksionin.

def hough_lines(image, rho, theta, threshold, min_line_len, max_line_gap):
    lines = cv2.HoughLinesP(image, rho, theta, threshold, np.array([]), minLineLength=min_line_len, maxLineGap=max_line_gap)
    return lines

rho = 0.8
theta = np.pi/180
threshold = 25
min_line_len = 50
max_line_gap = 200
lines = hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap)

Linjat e renditjes:Transformimi i Hough na rikthen linja të shumta, por ne duam vetëm dy korsi të dallueshme që makina jonë të lëvizë në mes. Ne kemi dy grupe të dallueshme që duam të zbulojmë: shënuesit e korsisë së majtë dhe të djathtë. Në këtë seksion, ne organizojmë linjat sipas pjerrësisë. "Pjerrësia e vijës" jepet nga:

Ne e përdorim këtë ekuacion për të organizuar linjat sipas pjerrësisë së tyre. Pjerrësitë pozitive janë për korsinë e djathtë dhe pjerrësitë negative janë për korsinë e majtë.

def slope(x1, y1, x2, y2):
    return (y1 - y2) / (x1 - x2)
def separate_lines(lines):
    right = []
    left = []
    for x1,y1,x2,y2 in lines[:, 0]:
        m = slope(x1,y1,x2,y2)
        if m >= 0:
            right.append([x1,y1,x2,y2,m])
        else:
            left.append([x1,y1,x2,y2,m])
    return right, left

right_lines, left_lines = separate_lines(lines)

Refuzoni të dhënat e jashtme:Më pas, ne filtrojmë linjat me pjerrësi të papranueshme që hedhin poshtë pjerrësinë e synuar të secilës rresht.

def reject_outliers(data, cutoff, threshold=0.08):
    data = np.array(data)
    data = data[(data[:, 4] >= cutoff[0]) & (data[:, 4] <= cutoff[1])]
    m = np.mean(data[:, 4], axis=0)
    return data[(data[:, 4] <= m+threshold) & (data[:, 4] >= m-threshold)]
if right_lines and left_lines:
    right = reject_outliers(right_lines,  cutoff=(0.45, 0.75))
    left = reject_outliers(left_lines, cutoff=(-0.85, -0.6))

Vendosja e regresionit linear: Më në fund bashkojmë grupet majtas dhe djathtas duke përdorur regresionin linear. "Regresioni linear" është një përpjekje për të gjetur marrëdhënien më të mirë midis një grupi pikash. Ne marrim një vijë që kalon në distancën më të afërt të mundshme nga çdo pikë. Ne aplikojmë regresion linear në grupet e linjave majtas dhe djathtas.

def lines_linreg(lines_array):
    x = np.reshape(lines_array[:, [0, 2]], (1, len(lines_array) * 2))[0]
    y = np.reshape(lines_array[:, [1, 3]], (1, len(lines_array) * 2))[0]
    A = np.vstack([x, np.ones(len(x))]).T
    m, c = np.linalg.lstsq(A, y)[0]
    x = np.array(x)
    y = np.array(x * m + c)
    return x, y, m, c

x, y, m, c = lines_linreg(lines)
# This variable represents the top-most point in the image where we can reasonable draw a line to.
min_y = np.min(y)
# Calculate the top point using the slopes and intercepts we got from linear regression.
top_point = np.array([(min_y - c) / m, min_y], dtype=int)
# Repeat this process to find the bottom left point.
max_y = np.max(y)
bot_point = np.array([(max_y - c) / m, max_y], dtype=int)

Vijat e hapësirës: Duke përdorur një gjeometri të thjeshtë (y = mx + b), ne llogarisim "ekstrem". Ne përdorim rezultatin e regresionit linear për të ekstrapoluar në ato ekstreme. Zgjatim linjat majtas dhe djathtas në të gjithë imazhin dhe këputim vijën duke përdorur rajonin tonë të mëparshëm të interesit.

def extend_point(x1, y1, x2, y2, length):
    line_len = np.sqrt((x1 - x2)**2 + (y1 - y2)**2)
    x = x2 + (x2 - x1) / line_len * length
    y = y2 + (y2 - y1) / line_len * length
    return x, y
x1e, y1e = extend_point(bot_point[0],bot_point[1],top_point[0],top_point[1], -1000) # bottom point
x2e, y2e = extend_point(bot_point[0],bot_point[1],top_point[0],top_point[1],  1000) # top point
# return the line.
line = np.array([[x1e,y1e,x2e,y2e]])
return np.array([line], dtype=np.int32)

Vizatoni linjat dhe ktheni imazhin përfundimtar: Hapi i fundit është të mbivendosni linjat majtas dhe djathtas mbi imazhin origjinal për të vërtetuar vizualisht korrektësinë dhe saktësinë e zbatimit të tubacionit tonë duke përdorur një llogaritje të peshuar për çdo piksel. Imazhi i rezultatit llogaritet si më poshtë:

imazhi_fillestar * α + imazh * β + γ

SHËNIM: imazhi_fillestar dhe imazhi duhet të kenë të njëjtën formë!

def weighted_image(image, initial_image, α=0.8, β=1., λ=0.):
    return cv2.addWeighted(initial_image, α, image, β, λ)

line_image = np.copy((image)*0)
draw_lines(line_image, lines, thickness=3)
line_image = region_of_interest(line_image, vertices)
final_image = weighted_image(line_image, image)
return final_image

Daljet fillestare të videos

Teknika të avancuara

Transformimi i ngjyrave

Në versionin e mësipërm të tubacionit, vendosëm të heqim informacionin e ngjyrave dhe të mbështetemi ekskluzivisht në ndriçimin e pikselëve për të zbuluar shenjat e korsisë në rrugë. Funksionon mirë gjatë ditës dhe me një terren të thjeshtë, por ne kemi vërejtur se saktësia e zbulimit të korsisë bie ndjeshëm në kushte më pak ideale. Duke e transformuar imazhin origjinal nga hapësira e ngjyrave RGB në hapësirën e ngjyrave HSV (më shumë informacion rreth HSL dhe HSV këtu), ne duam të filtrojmë pikselët që nuk janë ngjyra e dhënë e korsive. Përdorimi i vlerave të nuancës dhe ngopjes, jo sa i errët është një piksel, do të sigurojë që linjat e një ngjyre të caktuar të zbulohen më lehtë në rajone me hije ose me kontrast të ulët.

Ne krijojmë dy filtra me ngjyra që do të nxjerrin të bardhat dhe të verdhat në imazh dhe do t'i zbatojnë ato për t'i bërë të zeza çdo piksel tjetër.

def filter_image(image):
    hsv_image = cv2.cvtColor(image,cv2.COLOR_BGR2HSV)
    sensitivity = 51
    lower_white = np.array([0,0,255-sensitivity])
    upper_white = np.array([255,sensitivity,255])
    lower_yellow = np.array([18,102,204], np.uint8)
    upper_yellow = np.array([25,255,255], np.uint8)
    white_mask = cv2.inRange(hsv_image, lower_white, upper_white)
    yellow_mask = cv2.inRange(hsv_image, lower_yellow, upper_yellow)
    filtered_image = cv2.bitwise_and(image, image, mask=white_mask+yellow_mask)
    return filtered_image

Mesatarja e kornizës së mëparshme

Në pamjet e videos, ne do të mesatarizojmë linjat e mëparshme të kuadrove me linjat aktuale të kuadrit për të përmirësuar algoritmin tonë të zbulimit të korsive. Kujtimi i çdo linja të fundit të kornizës do të parandalojë nervozizmin duke përdorur mesataren dhe do të sigurojë një zgjidhje kur tubacioni ynë nuk arrin të zbulojë linja në kushte më pak se ideale.

global prev_left
global prev_right
prev_left = None
prev_right = None
def pipeline(image):
    global prev_left
    global prev_right
right = reject_outliers(right_lines,  cutoff=(0.45, 0.75))
if len(right) != 0:
    right = merge_lines(right)
    right = merge_prev(right, prev_right)
    prev_right = right
else:
    right = prev_right
left = reject_outliers(left_lines, cutoff=(-0.85, -0.6))
if len(left) != 0:
    left = merge_lines(left)
    left = merge_prev(left, prev_left)
    prev_left = left
else:
    left = prev_left

Daljet përfundimtare të videos

Tubacioni i mësipërm funksionon mirë në rrugë jo të lakuar me kamerën e montuar në pultin e automjetit në një pozicion të palëvizshëm. Do të duhet të punohet më shumë për ta bërë këtë punë në rrugë të lakuara, në kushte të ndryshme ndriçimi dhe moti dhe me një kamerë të montuar në pika të ndryshme.

konkluzioni

Në këtë projekt, unë kam zhvilluar dhe zbatuar një algoritëm për zbulimin e korsive me ngjyrë të bardhë dhe të verdhë në rrugë. Metoda e zbulimit të korsisë është e fuqishme dhe efektive në gjetjen e korsive të sakta duke përdorur orientimin e ngjyrave dhe të skajeve. Kontributet kryesore janë procedura e segmentimit të ngjyrave që identifikon korsitë me ngjyrë të verdhë ose të bardhë e ndjekur nga orientimi i skajit në të cilin eliminohen kufijtë, zbulohen korsitë, etiketohen rajonet majtas dhe djathtas, hiqen pikat e jashtme dhe më në fund mbetet një rresht për rajon pas përdorimit të një regresioni linear në çdo grup. Duke qenë se kamera mbetet konstante në lidhje me sipërfaqen e rrugës, pjesa e rrugës e imazhit mund të pritet ekskluzivisht duke ofruar koordinata, në mënyrë që identifikimi i korsive të bëhet shumë më efikas. Rezultatet eksperimentale tregojnë efektivitetin e metodës së propozuar në rastet e korsive me ngjyrë të verdhë dhe të bardhë. E gjithë puna është bërë në një mënyrë statike duke përdorur imazhe statike dhe shtrihet për të zbuluar korsitë në video.

Depoja e kodit për këtë projekt mund të gjendet "këtu". Projekti ynë i ardhshëm do të jetë rreth "klasifikimit të shenjave të trafikut": si të trajnojmë një rrjet nervor konvolucionist për të klasifikuar shenjat e trafikut. Shihemi atje.