Traitement LiDAR Drone avec Python : Du Vol au Jumeau Numerique
1. Planification de Vol et Parametres d'Acquisition
La qualite du traitement depend directement de la qualite de l'acquisition. Avant meme d'ecrire une ligne de Python, il faut parametrer correctement le vol. Les capteurs DJI Zenmuse L2 et Livox Mid-360 dominent le marche en 2026 avec des densites de points atteignant 200 pts/m2.
from dataclasses import dataclass
@dataclass
class FlightPlan:
"""Parametres de vol pour acquisition LiDAR drone."""
altitude_m: float = 80.0 # Altitude AGL
speed_ms: float = 5.0 # Vitesse de vol
overlap_pct: float = 70.0 # Recouvrement lateral
scan_rate_hz: int = 240000 # Frequence du LiDAR
fov_deg: float = 70.0 # Champ de vision
returns: int = 5 # Nombre de retours
@property
def swath_width(self):
import math
return 2 * self.altitude_m * math.tan(math.radians(self.fov_deg / 2))
@property
def point_density(self):
"""Densite theorique en pts/m2."""
return self.scan_rate_hz / (self.speed_ms * self.swath_width)
plan = FlightPlan(altitude_m=60, speed_ms=4)
print(f"Fauchee : {plan.swath_width:.1f} m")
print(f"Densite : {plan.point_density:.0f} pts/m2")
50 pts/m2. Pour le BIM et la detection de defauts, montez a 150 pts/m2 en abaissant l'altitude et la vitesse.
Le vol est termine, les fichiers .LAZ sont sur la carte SD. Comment les ingerer efficacement ?
2. Ingestion et Controle Qualite des Fichiers LAZ
Les fichiers bruts du drone contiennent souvent plusieurs dalles .LAZ qu'il faut fusionner, verifier et nettoyer. Le backend lazrs (Rust) offre des vitesses de decompression 10x superieures au laszip classique.
import laspy
import numpy as np
from pathlib import Path
def ingest_drone_laz(laz_dir, output_path):
"""Fusionne et valide les dalles LAZ d'un vol drone."""
laz_files = sorted(Path(laz_dir).glob("*.laz"))
print(f"Dalles detectees : {len(laz_files)}")
all_points = []
total_count = 0
for laz_file in laz_files:
las = laspy.read(str(laz_file))
# Controle qualite : verifier le CRS et les retours
if las.header.point_count == 0:
print(f"ATTENTION : {laz_file.name} est vide, ignore.")
continue
# Extraire les coordonnees avec precision float64
coords = np.vstack((las.x, las.y, las.z)).T
# Filtre de bruit : supprimer les points aberrants (z < 0 ou z > 500)
valid = (coords[:, 2] > 0) & (coords[:, 2] < 500)
coords = coords[valid]
all_points.append(coords)
total_count += len(coords)
print(f" {laz_file.name}: {len(coords):,} points valides")
merged = np.vstack(all_points)
print(f"Total fusionne : {total_count:,} points")
return merged
points = ingest_drone_laz("./vol_2026_02/", "merged.laz")
WGS84 (EPSG:4326) par defaut. Pensez a reprojeter en systeme metrique local (Lambert-93, UTM) avant tout calcul de distance ou de surface.
Les points sont propres et fusionnes. Comment classifier automatiquement le sol, la vegetation et les batiments ?
3. Classification Automatique avec PDAL
PDAL (Point Data Abstraction Library) est le couteau suisse du traitement LiDAR en ligne de commande et en Python. Son filtre SMRF (Simple Morphological Filter) est la reference pour la classification sol/non-sol sur donnees drone.
import pdal
import json
def classify_ground(input_laz, output_laz):
"""Pipeline PDAL : classification sol + vegetation + batiments."""
pipeline_json = {
"pipeline": [
input_laz,
{
"type": "filters.assign",
"assignment": "Classification[:]=0"
},
{
# Filtre morphologique pour extraire le sol
"type": "filters.smrf",
"slope": 0.15,
"window": 18,
"threshold": 0.5,
"scalar": 1.2
},
{
# Classification hauteur : vegetation basse / haute
"type": "filters.hag_nn"
},
{
"type": "filters.assign",
"assignment": "Classification[0:3]=3",
"where": "HeightAboveGround > 0.3 AND HeightAboveGround < 2.0"
},
{
"type": "filters.assign",
"assignment": "Classification[0:5]=5",
"where": "HeightAboveGround >= 2.0 AND HeightAboveGround < 30.0"
},
{
"type": "writers.las",
"filename": output_laz,
"compression": "laszip"
}
]
}
pipeline = pdal.Pipeline(json.dumps(pipeline_json))
count = pipeline.execute()
print(f"Points traites : {count:,}")
return pipeline.arrays[0]
classified = classify_ground("merged.laz", "classified.laz")
| Code ASPRS | Classe | Description | Couleur |
|---|---|---|---|
| 2 | Sol | Terrain naturel | Marron |
| 3 | Vegetation basse | 0.3m - 2.0m | Vert clair |
| 5 | Vegetation haute | 2.0m - 30.0m | Vert fonce |
| 6 | Batiment | Structures | Rouge |
La classification est terminee. Comment generer les modeles numeriques de terrain et de surface ?
4. Generation DTM/DSM avec Rasterio
Le DTM (Digital Terrain Model) represente le sol nu, tandis que le DSM (Digital Surface Model) inclut tous les objets. La difference des deux donne le CHM (Canopy Height Model), essentiel en foresterie.
import numpy as np
import rasterio
from rasterio.transform import from_bounds
from scipy.interpolate import griddata
def generate_raster(points, values, resolution, output_path, crs="EPSG:2154"):
"""Genere un raster GeoTIFF a partir de points 3D."""
x_min, y_min = points[:, :2].min(axis=0)
x_max, y_max = points[:, :2].max(axis=0)
# Grille reguliere
cols = int((x_max - x_min) / resolution)
rows = int((y_max - y_min) / resolution)
xi = np.linspace(x_min, x_max, cols)
yi = np.linspace(y_min, y_max, rows)
grid_x, grid_y = np.meshgrid(xi, yi)
# Interpolation IDW sur la grille
grid_z = griddata(
points[:, :2], values,
(grid_x, grid_y),
method="linear",
fill_value=np.nan
)
# Ecriture GeoTIFF
transform = from_bounds(x_min, y_min, x_max, y_max, cols, rows)
with rasterio.open(
output_path, "w",
driver="GTiff",
height=rows, width=cols,
count=1, dtype="float32",
crs=crs, transform=transform,
compress="lzw"
) as dst:
dst.write(grid_z.astype(np.float32), 1)
print(f"Raster genere : {output_path} ({cols}x{rows} px)")
# DTM : uniquement les points sol (classe 2)
ground_mask = classified["Classification"] == 2
ground_pts = np.vstack((classified["X"], classified["Y"], classified["Z"])).T
generate_raster(ground_pts[ground_mask], ground_pts[ground_mask, 2], 0.5, "dtm_50cm.tif")
# DSM : tous les points (premier retour)
generate_raster(ground_pts, ground_pts[:, 2], 0.5, "dsm_50cm.tif")
50 cm est ideale pour les releves drone a 60m d'altitude. En dessous de 25 cm, vous risquez des artefacts d'interpolation dans les zones a faible densite de points sol.
Nos rasters sont prets. Comment calculer les metriques derivees utiles aux ingenieurs ?
5. Calcul de Metriques Derivees : Pente, Aspect, Rugosite
Les modeles numeriques bruts ne suffisent pas pour la prise de decision. Les metriques derivees comme la pente, l'aspect (orientation) et la rugosite sont essentielles pour l'amenagement, l'hydrologie et la detection de risques.
import rasterio
import numpy as np
from scipy.ndimage import generic_filter
def compute_slope_aspect(dtm_path, output_slope, output_aspect):
"""Calcul de la pente et de l'aspect a partir du DTM."""
with rasterio.open(dtm_path) as src:
dem = src.read(1)
res = src.res[0] # Resolution en metres
profile = src.profile.copy()
# Gradient (derivees partielles)
dy, dx = np.gradient(dem, res)
# Pente en degres
slope = np.degrees(np.arctan(np.sqrt(dx**2 + dy**2)))
# Aspect (orientation) en degres
aspect = np.degrees(np.arctan2(-dy, dx))
aspect = (aspect + 360) % 360
# Ecriture des rasters derivees
for data, path in [(slope, output_slope), (aspect, output_aspect)]:
with rasterio.open(path, "w", **profile) as dst:
dst.write(data.astype(np.float32), 1)
print(f"Pente max : {slope.max():.1f} degres")
return slope, aspect
def compute_roughness(dtm_path, output_path, window=5):
"""Rugosite = ecart-type local de l'elevation."""
with rasterio.open(dtm_path) as src:
dem = src.read(1)
profile = src.profile.copy()
roughness = generic_filter(dem, np.std, size=window)
with rasterio.open(output_path, "w", **profile) as dst:
dst.write(roughness.astype(np.float32), 1)
return roughness
slope, aspect = compute_slope_aspect("dtm_50cm.tif", "slope.tif", "aspect.tif")
roughness = compute_roughness("dtm_50cm.tif", "roughness.tif")
NoData en bordure du raster. Les calculs de gradient sur des valeurs NaN propageront des artefacts. Appliquez un masque ou utilisez np.nan_to_num avant le calcul.
Les metriques terrain sont calculees. Comment integrer tout cela dans un workflow BIM ?
6. Integration BIM : Du Nuage au Modele IFC
L'etape finale du pipeline consiste a injecter les donnees traitees dans un environnement BIM. Nous utilisons IfcOpenShell pour generer un fichier IFC contenant le terrain et les elements detectes.
import ifcopenshell
import ifcopenshell.api as api
import numpy as np
def create_terrain_ifc(dtm_points, output_path):
"""Cree un fichier IFC avec le terrain en IfcSite."""
ifc = ifcopenshell.file(schema="IFC4")
# Entites de base
project = api.run("root.create_entity", ifc, ifc_class="IfcProject", name="Drone Survey")
site = api.run("root.create_entity", ifc, ifc_class="IfcSite", name="Terrain")
# Sous-echantillonner pour le maillage IFC
step = max(1, len(dtm_points) // 50000)
sampled = dtm_points[::step]
# Creer la representation geometrique du terrain
# (Triangulation de Delaunay simplifiee)
from scipy.spatial import Delaunay
tri = Delaunay(sampled[:, :2])
print(f"Terrain IFC : {len(sampled)} vertices, {len(tri.simplices)} triangles")
ifc.write(output_path)
print(f"Fichier IFC exporte : {output_path}")
create_terrain_ifc(ground_pts[ground_mask], "terrain_drone.ifc")
IFC4x3 supporte nativement les alignements et les infrastructures lineaires.
Comment valider la precision de notre pipeline complet ?
7. Validation et Controle Qualite du Pipeline
Un pipeline de production exige des metriques de validation. Nous comparons nos DTM/DSM avec des points de controle au sol (GCP) mesures au GNSS RTK.
import numpy as np
import rasterio
def validate_dtm(dtm_path, gcp_file):
"""Compare le DTM avec les points de controle GNSS."""
gcps = np.loadtxt(gcp_file, delimiter=",", skiprows=1)
# Colonnes : X, Y, Z_gnss
with rasterio.open(dtm_path) as src:
errors = []
for x, y, z_gnss in gcps:
row, col = src.index(x, y)
z_dtm = src.read(1)[row, col]
if not np.isnan(z_dtm):
errors.append(z_dtm - z_gnss)
errors = np.array(errors)
rmse = np.sqrt(np.mean(errors**2))
mae = np.mean(np.abs(errors))
bias = np.mean(errors)
print(f"GCPs valides : {len(errors)}")
print(f"RMSE : {rmse:.3f} m")
print(f"MAE : {mae:.3f} m")
print(f"Biais : {bias:.3f} m")
return {"rmse": rmse, "mae": mae, "bias": bias}
metrics = validate_dtm("dtm_50cm.tif", "gcps_rtk.csv")
| Metrique | Seuil Acceptable | Seuil Excellent | Unite |
|---|---|---|---|
| RMSE vertical | < 0.10 | < 0.05 | metres |
| MAE | < 0.08 | < 0.03 | metres |
| Biais | < 0.05 | < 0.02 | metres |
Conclusion : Un Pipeline Industriel Complet
Nous avons construit un pipeline de bout en bout, de l'acquisition drone a l'integration BIM :
- Acquisition : Planification parametree pour une densite optimale.
- Ingestion : Fusion et nettoyage des dalles LAZ avec Laspy.
- Classification : SMRF via PDAL pour la separation sol/vegetation/batiment.
- Rasterisation : Generation DTM/DSM avec Rasterio et interpolation scipy.
- BIM : Export IFC pour integration dans les workflows construction.
Ce pipeline est directement deployable en production et constitue la base de tout projet de jumeau numerique geospatial.
🚀 Du Drone au Digital Twin
Ce tutoriel couvre les bases. Notre formation Elite vous apprend a industrialiser ces pipelines et a les connecter a des systemes d'IA spatiale.
Rejoindre l'Elite (120h)