script-volume-auto/python/las_to_xyz.py
Nicolas Fryder 9d6ebf5177 Fiabilise le pipeline de volume et corrige plusieurs failles de securite
Pipeline: corrige le biais de sous-echantillonnage sur median_nn (facteur
sqrt(total/echantillon), gonflait le pas spatial jusqu'a x20 sur les gros
nuages), applique un decalage global X/Y pour la precision float32 en
coordonnees projetees (Lambert-93), fixe la portee de remplissage des trous
sur la grille du niveau 0 (au lieu de croitre a chaque niveau), utilise un
percentile robuste pour zref au lieu du minimum brut, et filtre plus
precisement le fichier de grille parasite genere par -VOLUME.

Securite: valide l'id de run (uuid) sur toutes les routes avant de construire
un chemin filesystem (traversee de repertoire post-auth via zip/csv/bin/images),
assainit le nom de fichier uploade avant multer, retire enableCors() (surface
inutile), passe le conteneur en utilisateur non-root.

Documente l'ensemble de la methode et ses limites dans CONCEPT.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 18:13:27 +02:00

63 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""
Convertit un fichier LAS/LAZ/COPC (via laspy) en nuage ASCII XYZ brut, lisible
nativement par CloudCompare sans plugin LAS (le paquet apt Debian de CloudCompare
ne fournit pas le plugin LAS/LAZ - seulement le plugin "Core I/O").
Un .copc.laz est simplement lu comme un LAZ standard (les points sont
identiques, seule l'indexation octree COPC, inutile ici, est ignoree).
Decalage global X/Y : CloudCompare stocke les coordonnees en float32 en interne. En coordonnees
projetees (ex. Lambert-93 / EPSG:2154, Y ~ 6.8e6 m), l'ULP de float32 a cette magnitude vaut ~0.5m,
ce qui quantifie silencieusement la position des points sur une grille de 0.5m - incompatible avec
un pas de calcul de l'ordre du decimetre (voir CONCEPT.md §7.2). On soustrait donc un decalage fixe
(coin bas de la bbox, arrondi au metre, lu directement dans l'entete LAS - pas de second passage sur
les points) de X et Y avant ecriture. Le volume 2.5D etant invariant par translation, ce decalage
n'affecte aucun resultat ; il est renvoye en sortie pour tracabilite. Z n'est pas decale : en
alti-metrique (~10^2-10^3 m), la precision float32 y est deja largement suffisante.
Usage:
python3 las_to_xyz.py --input nuage.laz --output nuage.xyz
"""
import argparse
import json
import math
import sys
import laspy
import numpy as np
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--input", required=True)
parser.add_argument("--output", required=True)
args = parser.parse_args()
try:
with laspy.open(args.input) as reader:
offset_x = math.floor(reader.header.mins[0])
offset_y = math.floor(reader.header.mins[1])
total = 0
with open(args.output, "w", encoding="ascii") as out:
for chunk in reader.chunk_iterator(2_000_000):
x = np.asarray(chunk.x) - offset_x
y = np.asarray(chunk.y) - offset_y
xyz = np.column_stack([x, y, np.asarray(chunk.z)])
np.savetxt(out, xyz, fmt="%.6f")
total += xyz.shape[0]
except Exception as exc:
print(json.dumps({"error": f"conversion LAS/LAZ impossible: {exc}"}))
return 1
if total == 0:
print(json.dumps({"error": "le fichier LAS/LAZ ne contient aucun point"}))
return 1
print(json.dumps({"ok": True, "count": total, "offsetX": offset_x, "offsetY": offset_y}))
return 0
if __name__ == "__main__":
sys.exit(main())