Traitement LiDAR Drone avec Python : Du Vol au Jumeau Numerique

Pipeline industriel complet : acquisition drone, traitement LAZ, classification automatique, generation de MNT/MNS et integration dans un jumeau numerique BIM.
Intermediaire
50 min
Laspy, PDAL, Open3D, Rasterio
"Le LiDAR drone a democratise la capture 3D de precision. En 2026, un ingenieur equipe de Python peut transformer un vol de 15 minutes en un jumeau numerique metrique exploitable par toute une equipe projet."

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.

flight_params.py
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")
Florent's Tip: Pour les releves topographiques, visez une densite minimale de 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.

ingest_laz.py
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")
Pitfall: Les drones DJI enregistrent les coordonnees en 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.

classify_pdal.py
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.

dtm_dsm_generator.py
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")
Florent's Tip: Une resolution de 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.

terrain_metrics.py
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")
Pitfall: Attention aux 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.

export_ifc.py
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")
Pro Concept: Pour un jumeau numerique complet, combinez le terrain IFC avec les batiments extraits par classification LiDAR et les modeles BIM existants. Le format 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.

validation.py
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 :

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)