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 :
- Extraire les plans de toit avec leur inclinaison et orientation au degré près
- Calculer les surfaces disponibles pour l'installation PV
- Modéliser les ombrages portés (cheminées, lucarnes, bâtiments voisins)
- Estimer la production solaire en combinant avec PVGIS
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.
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.
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).
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.
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)
}
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 :
- Adresse → coordonnées GPS via API IGN BAN
- Coordonnées → dalle COPC via API Géoplateforme LiDAR HD
- Nuage de points → plans de toit (RANSAC) → inclinaison + azimut
- Paramètres toit → API PVGIS → 8 760 données horaires de production
- Production horaire + courbe de charge client → taux d'autoconsommation + TRI
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
# 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