- Backend NestJS orchestrant CloudCompare CLI (stats, decimation x2 sur 5 etapes, volume, assemblage .bin) - Scripts Python (laspy, scipy, matplotlib) pour la conversion LAS/LAZ et la generation d'images - Frontend web simple (Tailwind CDN) avec historique des runs - Image Docker (debian:trixie-slim + cloudcompare apt + xvfb) prete pour deploiement Coolify - Lint/typecheck/tests unitaires integres comme portes de qualite au build Docker - CLAUDE.md documentant les comportements CloudCompare CLI verifies empiriquement
72 lines
2.1 KiB
Python
72 lines
2.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Calcule des statistiques (distance moyenne/mediane au plus proche voisin, bbox)
|
|
a partir d'un echantillon de points XYZ (fichier ASCII exporte par CloudCompare).
|
|
|
|
Usage:
|
|
python3 stats_helper.py --input sample.xyz
|
|
|
|
Sortie: un objet JSON unique sur stdout.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
import numpy as np
|
|
from scipy.spatial import cKDTree
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--input", required=True, help="Fichier ASCII XYZ (espace separe)")
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
pts = np.loadtxt(args.input, dtype=np.float64, usecols=(0, 1, 2))
|
|
except Exception as exc: # fichier vide, format inattendu, etc.
|
|
print(json.dumps({"error": f"lecture impossible: {exc}"}))
|
|
return 1
|
|
|
|
if pts.ndim == 1:
|
|
# une seule ligne -> un seul point
|
|
pts = pts.reshape(1, -1)
|
|
|
|
count = int(pts.shape[0])
|
|
if count < 2:
|
|
print(json.dumps({"error": f"pas assez de points dans l'echantillon ({count})"}))
|
|
return 1
|
|
|
|
bbox = {
|
|
"xmin": float(np.min(pts[:, 0])),
|
|
"xmax": float(np.max(pts[:, 0])),
|
|
"ymin": float(np.min(pts[:, 1])),
|
|
"ymax": float(np.max(pts[:, 1])),
|
|
"zmin": float(np.min(pts[:, 2])),
|
|
"zmax": float(np.max(pts[:, 2])),
|
|
}
|
|
|
|
tree = cKDTree(pts)
|
|
# k=2 : le plus proche voisin de chaque point est le point lui-meme (distance 0),
|
|
# le 2e plus proche est le vrai plus proche voisin.
|
|
dists, _ = tree.query(pts, k=2, workers=-1)
|
|
nn_dist = dists[:, 1]
|
|
nn_dist = nn_dist[np.isfinite(nn_dist) & (nn_dist > 0)]
|
|
|
|
if nn_dist.size == 0:
|
|
print(json.dumps({"error": "distances au plus proche voisin toutes nulles/invalides (points dupliques ?)"}))
|
|
return 1
|
|
|
|
result = {
|
|
"count": count,
|
|
"mean_nn": float(np.mean(nn_dist)),
|
|
"median_nn": float(np.median(nn_dist)),
|
|
"std_nn": float(np.std(nn_dist)),
|
|
"bbox": bbox,
|
|
"approximate": True,
|
|
}
|
|
print(json.dumps(result))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|