# Traitement LiDAR Drone avec Python : Du Vol au Jumeau Numérique

> Guide complet pour traiter des données LiDAR drone avec Python. De la planification de vol a la génération de DTM/DSM et l'intégration BIM. Tutoriel expert.

- Adresse : https://fr.learngeodata.eu/blog/lidar-drone-python-2026
- Rubrique : Articles techniques
- Langue : fr
- Auteur : Dr. Florent Poux (https://fr.learngeodata.eu/formateur/florent-poux)
- Éditeur : 3D Geodata Academy (organisme de formation, France)
- Index du site pour les assistants : https://fr.learngeodata.eu/llms.txt
- Publié le 2026-02-22

---
Ce que rapporte un vol, avant tout traitement.

## 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`.

```
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 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++.

```
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")
```

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.

```
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.

```
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")
```

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.

```
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")
```

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.

```
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")
```

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.

```
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 :

- **Acquisition :** Planification paramétrée pour une densité optimale.
- **Ingestion :** Fusion et nettoyage des dalles LAZ avec Laspy.
- **Classification :** SMRF via PDAL pour la séparation sol/végétation/bâtiment.
- **Rasterisation :** Génération DTM/DSM avec Rasterio et interpolation scipy.
- **BIM :** Export IFC pour intégration dans les workflows construction.

Ce pipeline est directement déployable en production et constitue la base de tout projet de jumeau numérique géospatial.

### 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.

[Le programme et les tarifs →](https://fr.learngeodata.eu/tarifs)
