🛰️ IGN / Données géographiques

API IGN LiDAR HD COPC :
analyser le potentiel solaire d'un bâtiment en Python

L'IGN met à disposition gratuitement les données LiDAR HD de la France entière avec une densité de 10 points/m². Ce tutoriel vous montre comment les exploiter en Python pour extraire les plans de toit, calculer l'inclinaison, l'orientation et estimer le potentiel solaire — sans lidar physique.

📅 2026-02-10 ⏱ 8 min de lecture IGNLiDAR HDCOPCPythonPhotovoltaïque

Pourquoi le LiDAR HD est un game-changer pour vos études PV

Jusqu'à récemment, analyser la géométrie d'un toit pour une étude photovoltaïque nécessitait soit une visite terrain, soit l'achat de données LiDAR commerciales (Pictometry, Nearmap…). Depuis 2021, l'IGN réalise un relevé LiDAR HD couvrant l'intégralité du territoire français métropolitain avec une densité de 10 points/m² — gratuit, ouvert, sous licence ODbL.

Ces données permettent, pour n'importe quelle adresse en France :

ℹ️ Format COPC
Le LiDAR HD IGN est distribué au format Cloud Optimized Point Cloud (COPC), une variante de LAZ qui permet le streaming HTTP Range — vous ne téléchargez que la portion du nuage qui vous intéresse, sans rapatrier des fichiers de plusieurs gigaoctets.

Accéder aux données LiDAR HD de l'IGN

Les dalles LiDAR HD sont accessibles via le service de téléchargement de la Géoplateforme IGN. Chaque dalle couvre 1 km² et pèse entre 50 et 500 Mo selon la densité de végétation. L'API téléchargement permet de découvrir les dalles couvrant une emprise géographique donnée.

Python ign_lidar_discovery.py
import requests

# Endpoint de recherche de dalles LiDAR HD IGN (Géoplateforme)
LIDAR_SEARCH_URL = "https://data.geopf.fr/wfs/ows"

def trouver_dalles_lidar(lat: float, lon: float, buffer_m: int = 100) -> list:
    """
    Trouve les dalles LiDAR HD couvrant un point GPS avec un buffer en mètres.
    Retourne la liste des URLs de téléchargement COPC.
    """
    # Convertir en rectangle bbox (approx. 1 degré ≈ 111 km)
    delta = buffer_m / 111000
    bbox = f"{lon - delta},{lat - delta},{lon + delta},{lat + delta}"

    params = {
        "SERVICE": "WFS",
        "VERSION": "2.0.0",
        "REQUEST": "GetFeature",
        "TYPENAMES": "LIDAR-HD:nuages-de-points",
        "BBOX": bbox,
        "SRSNAME": "EPSG:4326",
        "OUTPUTFORMAT": "application/json",
    }

    resp = requests.get(LIDAR_SEARCH_URL, params=params, timeout=30)
    resp.raise_for_status()
    features = resp.json().get("features", [])

    urls = []
    for feat in features:
        props = feat.get("properties", {})
        dl_url = props.get("download_link_copc") or props.get("url_telechargement")
        if dl_url:
            urls.append(dl_url)

    return urls


# Exemple : toiture d'un entrepôt à Lyon
dalle_urls = trouver_dalles_lidar(lat=45.7484, lon=4.8467)
print(f"{len(dalle_urls)} dalle(s) LiDAR HD trouvée(s)")

Streaming COPC avec HTTP Range requests

L'avantage majeur du format COPC est sa structure arborescente (octree) compatible avec le streaming HTTP Range. Au lieu de télécharger toute une dalle de 200 Mo, on récupère uniquement les chunks correspondant à l'emprise d'un bâtiment — quelques centaines de Ko en pratique.

Python copc_stream.py
import requests
import struct
import numpy as np
from dataclasses import dataclass

# ────── Lecture de l'en-tête COPC (premiers 375 octets) ─────────────────────

def lire_header_copc(url: str) -> dict:
    """Lit uniquement l'en-tête LAS/COPC via HTTP Range (evite dl complet)."""
    headers = {"Range": "bytes=0-374"}
    resp = requests.get(url, headers=headers, timeout=15)

    data = resp.content
    # Signature LAS : "LASF" en ASCII
    if data[:4] != b"LASF":
        raise ValueError("Fichier non-LAS – URL incorrecte ?")

    # Quelques champs utiles de l'en-tête LAS 1.4
    point_format  = data[104]
    point_count   = struct.unpack_from(", data, 247)[0]
    scale_x, scale_y, scale_z = struct.unpack_from(", data, 131)
    off_x, off_y, off_z       = struct.unpack_from(", data, 155)
    min_x, max_x = struct.unpack_from(", data, 179)
    min_y, max_y = struct.unpack_from(", data, 195)
    min_z, max_z = struct.unpack_from(", data, 211)

    return {
        "point_format": point_format,
        "point_count": point_count,
        "scale": (scale_x, scale_y, scale_z),
        "offset": (off_x, off_y, off_z),
        "bbox": {
            "x": (min_x, max_x), "y": (min_y, max_y), "z": (min_z, max_z)
        },
    }

Extraction des plans de toit par segmentation

Une fois les points du bâtiment récupérés (classification LAS = 6 « building »), on applique un algorithme RANSAC pour détecter les plans dominants dans le nuage de points. Chaque plan correspond à un pan de toit avec une inclinaison (tilt) et une orientation (azimuth).

Python roof_segmentation.py
import numpy as np
from sklearn.linear_model import RANSACRegressor
from typing import List, Tuple


def detecter_plans_toit(
    points: np.ndarray,             # shape (N, 3) – X, Y, Z en Lambert-93
    min_inliners: int = 50,         # points minimum par plan
    residual_threshold: float = 0.1 # tolérance RANSAC en mètres
) -> List[dict]:
    """
    Détecte itérativement les plans de toit par RANSAC.
    Retourne pour chaque pan : inclinaison (°), azimut (°), surface (m²).
    """
    remaining = points.copy()
    plans = []

    while len(remaining) >= min_inliners:
        # Ajustement plan z = ax + by + c (toit non vertical)
        X_feat = remaining[:, :2]   # X, Y
        y_feat = remaining[:, 2]    # Z

        ransac = RANSACRegressor(
            residual_threshold=residual_threshold,
            min_samples=3,
            max_trials=200
        )
        try:
            ransac.fit(X_feat, y_feat)
        except Exception:
            break

        mask_inliers = ransac.inlier_mask_
        if np.sum(mask_inliers) < min_inliners:
            break

        # Normale au plan : n = [-a, -b, 1] normalisé
        a, b = ransac.estimator_.coef_
        normale = np.array([-a, -b, 1.0])
        normale /= np.linalg.norm(normale)

        # Inclinaison = angle avec la verticale
        inclinaison_deg = np.degrees(np.arccos(abs(normale[2])))

        # Azimut = orientation du pan vers le bas de pente (0° = N, 90° = E, 180° = S, 270° = O)
        azimut_deg = (np.degrees(np.arctan2(a, b)) + 360) % 360

        # Surface projetée du pan (m²)
        inlier_pts = remaining[mask_inliers]
        hull_2d_area = _convex_hull_area(inlier_pts[:, :2])
        surface_reelle = hull_2d_area / np.cos(np.radians(inclinaison_deg))

        plans.append({
            "inclinaison": round(inclinaison_deg, 1),
            "azimut": round(azimut_deg, 1),
            "surface_m2": round(surface_reelle, 1),
            "nb_points": int(np.sum(mask_inliers)),
            "orientation_label": _azimut_vers_label(azimut_deg),
        })

        remaining = remaining[~mask_inliers]

    return plans


def _azimut_vers_label(az: float) -> str:
    """Convertit un azimut en label cardinal (Sud, Sud-Ouest…)"""
    labels = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"]
    idx = int((az + 22.5) / 45) % 8
    return labels[idx]


def _convex_hull_area(pts: np.ndarray) -> float:
    """Superficie de l'enveloppe convexe 2D (formule de Shoelace)."""
    from scipy.spatial import ConvexHull
    try:
        hull = ConvexHull(pts)
        return hull.volume   # en 2D, .volume = aire
    except:
        return 0.0

Calcul du potentiel PV par pan

Avec l'inclinaison et l'azimut de chaque pan de toit, on dispose des deux paramètres clés pour ensuite interroger l'API PVGIS et obtenir la production annuelle estimée. On combine les surfaces avec un taux d'occupation standard de 0,85 pour tenir compte des inter-ranges et des marges périphériques.

Python potentiel_pv.py
def calculer_puissance_max(
    plans: list,
    puissance_wc_par_m2: float = 200.0,  # Wc/m² (ex. panneaux 400 Wc en 2 m²)
    taux_occupation: float = 0.85,
    inclinaison_min: float = 10.0,       # exclure les toits quasi-plats
    azimut_tolere: tuple = (90, 270)      # exclure les pans plein nord
) -> dict:
    """
    Calcule la puissance PV installable par pan de toit.
    Exclut les pans défavorables (nord pur, trop plat).
    """
    resultats = []
    total_kwc = 0.0

    for plan in plans:
        az = plan["azimut"]
        incl = plan["inclinaison"]

        # Filtrer les azimuts nord (270° à 90° en passant par 0°)
        nord = az > 270 or az < 90

        if nord or incl < inclinaison_min:
            plan["kwc_installable"] = 0.0
            plan["eligible_pv"] = False
            continue

        surface_utile = plan["surface_m2"] * taux_occupation
        kwc = (surface_utile * puissance_wc_par_m2) / 1000
        plan["kwc_installable"] = round(kwc, 2)
        plan["eligible_pv"] = True
        total_kwc += kwc
        resultats.append(plan)

    return {
        "plans_eligibles": resultats,
        "total_kwc": round(total_kwc, 2),
        "nb_plans": len(resultats)
    }
💡 Conseil pratique

Pour des projets en toitures terrasses (inclinaison 0°–5°), utilisez l'argument inclinaison_min=0 et calculez manuellement le gain de production avec une inclinaison de rehausseur à 10°–15° Sud. HeliaPV propose ce calcul automatiquement dans son module de calpinage.

Intégration complète : LiDAR → PVGIS → rapport

Le flux complet d'un bureau d'études peut être automatisé en chaîne :

  1. Adresse → coordonnées GPS via API IGN BAN
  2. Coordonnées → dalle COPC via API Géoplateforme LiDAR HD
  3. Nuage de points → plans de toit (RANSAC) → inclinaison + azimut
  4. Paramètres toit → API PVGIS → 8 760 données horaires de production
  5. Production horaire + courbe de charge client → taux d'autoconsommation + TRI
⚠️ Couverture LiDAR HD IGN

La couverture nationale est en cours et devrait être complète fin 2026. Vérifiez la disponibilité de votre zone sur geoplateforme.ign.fr avant de baser une étude sur ces données. En cas de dalle manquante, HeliaPV bascule automatiquement sur Google Solar API.

Librairies Python recommandées

Bash requirements.txt
# Lecture et traitement nuages de points
laspy[lazrs,copc]>=2.5
pyproj>=3.6         # conversions entre systèmes de coordonnées
numpy>=1.26
scipy>=1.12         # ConvexHull, spatial
scikit-learn>=1.4   # RANSACRegressor

# Optionnel : visualisation
open3d>=0.18        # visualisation 3D nuages de points
matplotlib>=3.8

HeliaPV fait tout ça automatiquement

Saisissez une adresse, HeliaPV charge le LiDAR HD IGN, segmente les toits, calcule la production PVGIS et génère votre rapport en moins de 30 secondes. Aucune ligne de code requise pour vos études courantes.

Essai gratuit – 50 analyses offertes En savoir plus pour les BET
← Tous les articles Prochain : API GPU / PLU →