Fondamentaux de la lecture et de l'affichage d'images
OpenCV expose des méthodes natives pour charger, visualiser et persister des données visuelles. La fonction cv2.imread importe une matrice de pixels, tandis que cv2.imshow ouvre une fenêtre d'affichage. Pour empêcher la fermeture immédiate de la fenêtre, il est indispensable d'intercaler un appel à cv2.waitKey().
import cv2
import numpy as np
scene = cv2.imread("route_originale.jpg", cv2.IMREAD_GRAYSCALE)
cv2.imshow("Aperçu", scene)
cv2.waitKey(0)
cv2.imwrite("sortie_gris.png", scene)
Extraction des contours par l'algorithme de Canny
L'algorithme de Canny identifie les transitions brusques d'intensité lumineuse en calculant les gradients locaux. Pour atténuer le bruit et les artefacts de réflexion, une stratégie à double seuil est appliquée. Les pixels dépassant le seuil supérieur sont considérés comme des contours forts, ceux situés entre les deux seuils comme des contours faibles. Seuls les segments faibles connectés aux segments forts sont conservés.
contours = cv2.Canny(scene, 150, 300)
Isolation de la région d'intérêt (ROI)
La zone supérieure de l'image (ciel, végétation, véhicules lointains) n'est pas pertinente pour la navigation. Nous définissons un masque polygonal couvrant la chaussée visible, puis appliquons une opération binaire d'intersection pour isoler les contours pertinents.
masque = np.zeros_like(contours)
polygone = np.array([[[0, scene.shape[0]], [320, 240], [480, 240], [640, scene.shape[0]]]])
cv2.fillPoly(masque, polygone, 255)
contours_filtrés = cv2.bitwise_and(contours, masque)
Détection de segments linéaires avec Hough
La transformée de Hough probabiliste (HoughLinesP) extrait des segments discrets à partir des contours. Chaque segment est représenté par ses coordonnées d'extrémités $(x_1, y_1, x_2, y_2)$. En calculant la pente $\frac{\Delta y}{\Delta x}$, nous séparons les segments appatrenant à la voie gauche (pente positive) de ceux de la voie droite (pente négative).
segments_bruts = cv2.HoughLinesP(contours_filtrés, 1, np.pi/180, 15, minLineLength=40, maxLineGap=20)
def calculer_pente(segment):
x1, y1, x2, y2 = segment[0]
return (y2 - y1) / (x2 - x1)
segments_gauche = [s for s in segments_bruts if calculer_pente(s) > 0]
segments_droite = [s for s in segments_bruts if calculer_pente(s) < 0]
Nettoyage statistique des anomalies
Certaines lignes détectées correspondent à des reflets ou à du bruit résiduel. Une approche itérative permet de retirer les segments dont la pente s'écarte significativement de la moyenne du groupe.
def filtrer_outliers(collection, marge=0.2):
if not collection:
return collection
pentes = [calculer_pente(s) for s in collection]
while collection:
moyenne = np.mean(pentes)
écarts = [abs(p - moyenne) for p in pentes]
idx_max = np.argmax(écarts)
if écarts[idx_max] > marge:
pentes.pop(idx_max)
collection.pop(idx_max)
else:
break
return collection
segments_gauche = filtrer_outliers(segments_gauche)
segments_droite = filtrer_outliers(segments_droite)
Ajustement polynomial et synthèse de la trajectoire
Une fois le bruit éliminé, nous regroupons les coordonnées des segments restants pour déterminer une équation linéaire unique par voie via la méthode des moindres carrés. Les fonctions np.polyfit et np.polyval génèrent les coefficients de la droite, puis nous en déduisons deux points extrêmes pour le rendu.
def synthetiser_droite(collection):
absisses = np.ravel([[s[0][0], s[0][2]] for s in collection])
ordonnees = np.ravel([[s[0][1], s[0][3]] for s in collection])
coeffs = np.polyfit(absisses, ordonnees, 1)
x_min, x_max = absisses.min(), absisses.max()
y_min = np.polyval(coeffs, x_min)
y_max = np.polyval(coeffs, x_max)
return np.array([[x_min, y_min], [x_max, y_max]], dtype=np.int32)
droite_gauche = synthetiser_droite(segments_gauche)
droite_droite = synthetiser_droite(segments_droite)
Superposition graphique
Les vecteurs calculés sont dessinés sur l'image source à l'aide de cv2.line, avec une épaisseur et une couleur contrastées pour une visaulisation claire.
frame_couleur = cv2.imread("route_originale.jpg")
cv2.line(frame_couleur, tuple(droite_gauche[0]), tuple(droite_gauche[1]), (0, 255, 255), 5)
cv2.line(frame_couleur, tuple(droite_droite[0]), tuple(droite_droite[1]), (0, 255, 255), 5)
cv2.imshow("Résultat", frame_couleur)
cv2.waitKey(0)
Extension au traitement vidéo
Le pipeline est encapsulé dans une fonction réutilisable pour traiter un flux vidéo frame par frame. cv2.VideoCapture extrait chaque image, qui est passée au détecteur avant d'être affichée dans une boucle continue.
def detecter_voies(image_source):
flou = cv2.GaussianBlur(image_source, (5, 5), 1)
gris = cv2.cvtColor(flou, cv2.COLOR_BGR2GRAY)
contours = cv2.Canny(gris, 150, 300)
masque = np.zeros_like(contours)
polygone = np.array([[[0, gris.shape[0]], [300, 220], [340, 220], [640, gris.shape[0]]]])
cv2.fillPoly(masque, polygone, 255)
contours_roi = cv2.bitwise_and(contours, masque)
segments = cv2.HoughLinesP(contours_roi, 1, np.pi/180, 15, minLineLength=40, maxLineGap=20)
if segments is None:
return image_source
gauche = filtrer_outliers([s for s in segments if calculer_pente(s) > 0])
droite = filtrer_outliers([s for s in segments if calculer_pente(s) < 0])
if gauche and droite:
line_g = synthetiser_droite(gauche)
line_d = synthetiser_droite(droite)
cv2.line(image_source, tuple(line_g[0]), tuple(line_g[1]), (0, 255, 255), 5)
cv2.line(image_source, tuple(line_d[0]), tuple(line_d[1]), (0, 255, 255), 5)
return image_source
source_video = cv2.VideoCapture("trafic.mp4")
while source_video.isOpened():
succes, frame = source_video.read()
if not succes:
break
frame_traitee = detecter_voies(frame)
cv2.imshow("Flux temps réel", frame_traitee)
if cv2.waitKey(30) & 0xFF == ord('q'):
break
source_video.release()
cv2.destroyAllWindows()