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>
80 lines
2.6 KiB
Python
80 lines
2.6 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])),
|
|
}
|
|
# Percentiles bas de Z : contrairement a zmin (sensible a un unique point aberrant, ex. bruit
|
|
# multi-echo), un percentile est un estimateur robuste du plan de reference et n'est PAS biaise
|
|
# par le sous-echantillonnage (la distribution marginale de Z ne depend pas de la densite de
|
|
# points, au contraire de la distance au plus proche voisin). Voir CONCEPT.md §7.7.
|
|
z_p01 = float(np.percentile(pts[:, 2], 0.1))
|
|
z_p1 = float(np.percentile(pts[:, 2], 1.0))
|
|
|
|
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,
|
|
"z_p01": z_p01,
|
|
"z_p1": z_p1,
|
|
"approximate": True,
|
|
}
|
|
print(json.dumps(result))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|