Traitement LiDAR Drone avec Python : Du Vol au Jumeau Numérique

Acquisition LiDAR aérienne, en orbite, colorée par la hauteur
Ce que rapporte un vol, avant tout traitement.
Pipeline industriel complet : acquisition drone, traitement LAZ, classification automatique, génération de MNT/MNS et intégration dans un jumeau numérique BIM.
Intermédiaire
50 min
Laspy, PDAL, Open3D, Rasterio
"Le LiDAR drone a démocratisé la capture 3D de précision. En 2026, un ingénieur équipé de Python peut transformer un vol de 15 minutes en un jumeau numérique métrique exploitable par toute une équipe projet."

1. Planification de Vol et Paramètres d'Acquisition

La qualité du traitement dépend directement de la qualité de l'acquisition. Avant même d'écrire une ligne de Python, il faut paramétrer correctement le vol. Les capteurs DJI Zenmuse L2 et Livox Mid-360 dominent le marché en 2026 avec des densités 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")
Le conseil de Florent : Pour les relevés topographiques, visez une densité minimale de 50 pts/m2. Pour le BIM et la détection de défauts, montez à 150 pts/m2 en abaissant l'altitude et la vitesse.

Le vol est terminé, les fichiers .LAZ sont sur la carte SD. Comment les ingérer efficacement ?

2. Ingestion et Contrôle Qualité des Fichiers LAZ

Les fichiers bruts du drone contiennent souvent plusieurs dalles .LAZ qu'il faut fusionner, vérifier et nettoyer. Le backend lazrs (Rust) décompresse en parallèle sur plusieurs coeurs, là où laszip reste mono-thread, et s'installe sans dépendance C++.

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")
Le piège : Les drones DJI enregistrent les coordonnées en WGS84 (EPSG:4326) par défaut. Pensez à reprojeter en système métrique local (Lambert-93, UTM) avant tout calcul de distance ou de surface.

Les points sont propres et fusionnés. Comment classifier automatiquement le sol, la végétation et les bâtiments ?

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 référence pour la classification sol/non-sol sur données 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 Végétation basse 0.3m - 2.0m Vert clair
5 Végétation haute 2.0m - 30.0m Vert foncé
6 Bâtiment Structures Rouge

La classification est terminée. Comment générer les modèles numériques de terrain et de surface ?

4. Génération DTM/DSM avec Rasterio

Le DTM (Digital Terrain Model) représente le sol nu, tandis que le DSM (Digital Surface Model) inclut tous les objets. La différence 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")
Le conseil de Florent : Une résolution de 50 cm est idéale pour les relevés drone à 60m d'altitude. En dessous de 25 cm, vous risquez des artefacts d'interpolation dans les zones à faible densité de points sol.

Nos rasters sont prêts. Comment calculer les métriques dérivées utiles aux ingénieurs ?

5. Calcul de Métriques Dérivées : Pente, Aspect, Rugosité

Les modèles numériques bruts ne suffisent pas pour la prise de décision. Les métriques dérivées comme la pente, l'aspect (orientation) et la rugosite sont essentielles pour l'aménagement, l'hydrologie et la détection 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")
Le piège : 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 métriques terrain sont calculées. Comment intégrer tout cela dans un workflow BIM ?

6. Intégration BIM : Du Nuage au Modèle IFC

L'étape finale du pipeline consiste à injecter les données traitées dans un environnement BIM. Nous utilisons IfcOpenShell pour générer un fichier IFC contenant le terrain et les éléments détectés.

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")
Le concept : Pour un jumeau numérique complet, combinez le terrain IFC avec les bâtiments extraits par classification LiDAR et les modèles BIM existants. Le format IFC4x3 supporte nativement les alignements et les infrastructures linéaires.

Comment valider la précision de notre pipeline complet ?

7. Validation et Contrôle Qualité du Pipeline

Un pipeline de production exige des métriques de validation. Nous comparons nos DTM/DSM avec des points de contrôle au sol (GCP) mesurés 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")
Métrique Seuil Acceptable Seuil Excellent Unité
RMSE vertical < 0.10 < 0.05 mètres
MAE < 0.08 < 0.03 mètres
Biais < 0.05 < 0.02 mètres

Conclusion : Un Pipeline Industriel Complet

Nous avons construit un pipeline de bout en bout, de l'acquisition drone à l'intégration BIM :

Ce pipeline est directement déployable en production et constitue la base de tout projet de jumeau numérique géospatial.

Sur ce sujet : Ce que la formation couvre en acquisition et reconstruction.

Du Drone au Digital Twin

Ce tutoriel couvre les bases. Notre formation Elite vous apprend à industrialiser ces pipelines et à les connecter à des systèmes d'IA spatiale.

Recevoir le guide gratuit Passer le test (10 min)

Le programme et les tarifs →