script-volume-auto/README.md
Nicolas Fryder 86a75bd47b Mode comparaison a 2 nuages + remplissage de trous + fix critique de parsing
- Nouveau mode "cloud_compare" (en plus de "const_height") : comparaison entre une surface
  superieure et une limite inferieure, avec appariement de densite avant le niveau 0 puis
  decimation synchronisee x2 sur 5 etapes. UI : selecteur de mode + upload double avec labels
  explicites (surface superieure / limite inferieure).
- Remplissage des trous de scan (interpolation Delaunay bornee par max-edge-length) avant chaque
  calcul de volume, dans les deux modes. -VOLUME n'ayant aucune option native pour ca (verifie dans
  les sources CloudCompare), on rasterize+comble+exporte en nuage avant de le passer a -VOLUME.
  Override utilisateur possible pour la distance max, afin de ne pas combler les parties concaves
  du contour de l'objet mesure.
- Fix critique : CloudCompare formate les grands nombres avec une virgule comme separateur de
  milliers ("Volume: 14,244.46") ; le parsing tronquait silencieusement a la premiere virgule
  (14 244 devenait 14). Trouve en testant avec un vrai nuage industriel.
- Validation avec 2 vrais nuages utilisateur : le mode Z constant donnait ~50m3 (plan de reference
  sans rapport avec la base reelle de l'amas, nuage contenant du contexte environnant) ; le nouveau
  mode comparaison donne ~14 244m3, stable a +/-4% meme a 16x de decimation.
- CLAUDE.md mis a jour avec tous ces findings (limitation -VOLUME/LEAVE_EMPTY, technique de
  contournement, piege du separateur de milliers, architecture du mode 2 nuages).
2026-07-10 15:21:37 +02:00

10 KiB

Volume 2.5D & robustesse a la decimation

Application locale (Docker) qui, a partir d'un ou deux nuages de points (LAS/LAZ/COPC.laz/BIN) :

  1. calcule un volume en 2.5D via CloudCompare, selon 2 modes au choix :
    • Plan de reference Z constant : un seul nuage, compare a un plan horizontal (altitude minimale par defaut). Fiable uniquement si le nuage ne contient QUE l'objet mesure (pas de terrain/contexte environnant) ;
    • Comparaison a un second nuage : deux nuages, la surface superieure (sommet de l'amas) et la limite inferieure (base/socle) — les densites sont d'abord appariees (le plus dense decime pour matcher l'autre), puis les deux sont decimes ensemble a chaque etape ;
  2. dans les deux modes, decime progressivement en methode spatiale, facteur x2 sur 5 etapes (le pas spatial double a chaque etape, en partant d'un pas initial estime automatiquement = distance mediane au plus proche voisin, calculee par KD-tree sur un echantillon) ;
  3. recalcule le volume a chaque niveau avec une grille de calcul adaptee dynamiquement a la densite de chaque niveau (grid step = pas spatial du niveau x2), et comble les trous de scan avant chaque calcul (interpolation Delaunay bornee par une distance max, pour ne pas combler les parties concaves du contour de l'objet) ;
  4. assemble tous les nuages exploitables (complet + 5 decimations, x1 ou x2 selon le mode) dans un seul fichier .bin CloudCompare, nomme et consultable dans CloudCompare Desktop ;
  5. produit un dossier de resultats : rapport CSV/JSON, cartes de hauteur par niveau (et par nuage en mode comparaison), graphiques de synthese (volume, nombre de points, robustesse) ;
  6. expose tout ca dans une UI web simple avec historique des runs.

Demarrage

docker compose up -d --build

Puis ouvrir http://localhost:8080

Les donnees (uploads, resultats, base sqlite) sont persistees dans un volume Docker nomme app-data (pas un bind mount vers ./data, voir "Deploiement Coolify" ci-dessous pour la raison).

Authentification

L'UI et l'API sont protegees par HTTP Basic Auth (identifiants dans AUTH_USERNAME / AUTH_PASSWORD_HASH, hash bcrypt — jamais le mot de passe en clair). Seul /health reste public (healthcheck Docker/Coolify).

Si ces deux variables ne sont pas definies, l'auth est desactivee (pratique pour du dev local, un warning est logge au demarrage). En production/Coolify, toujours les definir.

Generer un hash pour un nouveau mot de passe :

node -e "console.log(require('bcryptjs').hashSync('VOTRE_MOT_DE_PASSE', 10))"

Puis definir dans Coolify (onglet "Environment Variables" de l'application, pas dans docker-compose.yml du repo, pour ne pas committer le hash) :

AUTH_USERNAME=nico
AUTH_PASSWORD_HASH=$2a$10$...   # sortie de la commande ci-dessus

En local avec docker compose, un fichier .env (gitignore) a la racine du repo fonctionne aussi :

AUTH_USERNAME=nico
AUTH_PASSWORD_HASH=$2a$10$...

Deploiement Coolify

Le repo est pret pour un deploiement Coolify via webhook (push sur main -> build + deploy auto) :

  • Le Dockerfile fait tourner lint + typecheck + tests unitaires avant de compiler — un commit qui casse l'un de ces trois echoue le build, donc n'est jamais deploye.
  • HEALTHCHECK integre au Dockerfile (curl sur /health, route publique non authentifiee) : Coolify l'utilise pour verifier que le deploiement est reellement sain.
  • Persistance : le volume /data doit etre monte comme un volume nomme/gere par Coolify, pas un bind mount vers le checkout git (Coolify re-clone le repo a chaque deploiement — un bind mount relatif au checkout perdrait toutes les donnees a chaque redeploy). Deux options :
    • Resource "Docker Compose" dans Coolify pointant sur docker-compose.yml du repo : le volume nomme app-data declare dedans est deja correct.
    • Resource "Application" (Dockerfile) : configurer un volume persistant sur /data depuis l'onglet "Storage" de Coolify, et le port expose sur 3000 (variable PORT, lue par l'appli).
  • Variables d'environnement a definir dans Coolify : AUTH_USERNAME / AUTH_PASSWORD_HASH (obligatoire, voir "Authentification" plus haut), et optionnellement MAX_UPLOAD_MB, STATS_SAMPLE_CAP, CC_TIMEOUT_MS si besoin de changer les defauts (voir tableau plus bas).
  • Le webhook de deploiement automatique est deja configure cote Coolify sur ce repo.

Utilisation

  1. "Nouveau run" -> choisir le type de comparaison :
    • Plan de reference Z constant : un fichier .las, .laz, .copc.laz ou .bin.
    • Comparaison a un second nuage : deux fichiers — "Surface superieure" (sommet de l'amas) et "Limite inferieure" (base/socle de l'amas).
  2. Optionnel (options avancees) : override du plan de reference Z (mode Z constant uniquement, par defaut altitude minimale d'un echantillon), du pas spatial initial (par defaut : distance mediane au plus proche voisin) et/ou de la distance max de remplissage des trous (par defaut : grille du niveau x3 — augmenter si des trous de scan legitimes restent vides, diminuer si des creux du contour de l'objet sont a tort combles).
  3. Le run tourne en arriere-plan (une file d'attente sequentielle traite les runs un par un). La page de detail se rafraichit automatiquement (polling 2s).
  4. Une fois termine : tableau des niveaux (points par nuage, volume, surface, % de cellules de grille "matching"), cartes de hauteur (par nuage en mode comparaison), graphiques de synthese, et telechargements (.bin fusionne, .csv, dossier complet en .zip).

Lecture des resultats

  • % de cellules "matching" (rapport de volume 2.5D de CloudCompare) : indique la part de la grille de calcul ou les deux surfaces sont effectivement comparables. Un niveau est marque "a verifier" des que ce taux passe sous 90 % (configurable via matchingCellsWarnThreshold, config.ts).
  • La decimation s'arrete automatiquement si un niveau (ou l'un des deux nuages en mode comparaison) tombe sous 25 points exploitables ; les niveaux suivants sont marques "skipped" avec la raison.
  • Choix du mode : le mode "Z constant" n'est fiable que si le nuage uploade ne contient QUE l'objet a mesurer. S'il contient aussi du terrain/contexte environnant (scan de site complet par exemple), le plan Z constant (altitude minimale de tout le nuage) n'a souvent aucun rapport avec la base reelle de l'objet et donne un volume trompeur — utiliser "Comparaison a un second nuage" avec une base dediee dans ce cas.

Parametres (variables d'environnement, voir docker-compose.yml)

Variable Defaut Description
PORT 3000 port interne du serveur
DATA_DIR /data dossier de persistance (uploads, resultats, sqlite)
AUTH_USERNAME (non defini = auth off) nom d'utilisateur HTTP Basic
AUTH_PASSWORD_HASH (non defini = auth off) hash bcrypt du mot de passe
MAX_UPLOAD_MB 2048 taille max d'un nuage uploade
STATS_SAMPLE_CAP 50000 nb de points echantillonnes pour estimer le pas spatial initial
CC_TIMEOUT_MS 600000 timeout par appel CloudCompare CLI

Formats supportes

.las, .laz, .copc.laz (lu comme un LAZ standard — l'indexation octree COPC n'est pas utilisee, seuls les points le sont), .bin (format natif CloudCompare).

E57 non supporte pour l'instant (exclu volontairement du perimetre initial).

Architecture

  • Backend : NestJS (TypeScript), file d'attente de jobs en memoire (un run a la fois), SQLite (better-sqlite3) pour l'historique des runs.
  • Orchestration du pipeline CloudCompare : chaque etape (estimation stats, conversion, decimation, volume, export) est un appel independant au CLI CloudCompare -SILENT ... (le CLI fonctionne comme une machine a etats sur UNE session de process, donc chaque etape = un process).
  • Scripts Python (python/) : stats_helper.py (distance au plus proche voisin via scipy KD-tree), render_images.py (cartes de hauteur + graphiques de synthese via matplotlib/numpy), las_to_xyz.py (conversion LAS/LAZ/COPC -> ASCII, voir "Limitation connue" ci-dessous).
  • Frontend : HTML/JS vanilla + Tailwind (CDN), servi en statique par NestJS. Pas de framework, pas de build front separe.
  • Image Docker : debian:trixie-slim + paquet apt cloudcompare (2.13.2, evite une compilation depuis les sources) + xvfb (CloudCompare reste une appli Qt, meme en -SILENT elle a besoin d'un display) + Node 20 + Python 3.

Limitations connues

  • Le paquet apt cloudcompare (Debian) ne fournit pas le plugin LAS/LAZ (uniquement le plugin "Core I/O"). Consequence : les fichiers LAS/LAZ/COPC sont convertis en ASCII XYZ via laspy (Python) avant d'etre transmis a CloudCompare, qui lit l'ASCII nativement sans plugin. Ca fonctionne bien mais perd les attributs LAS annexes (intensite, classification, RGB...) — non utilises par ce calcul de volume de toute facon.
  • Le meme paquet plante (assertion GDAL) sur l'export GeoTIFF natif (-RASTERIZE -OUTPUT_RASTER_Z). Les cartes de hauteur sont donc reconstruites nous-memes (export ASCII du nuage + binning numpy/scipy + rendu matplotlib) plutot que de dependre du rasterizer CloudCompare.
  • Le plan de reference Z et le pas de grille de volume sont approximatifs quand ils sont auto-detectes (calcules sur un echantillon aleatoire de points, pas la totalite du nuage) — largement suffisant pour comparer les 6 niveaux entre eux, mais a corriger via l'override si une valeur exacte est necessaire.
  • Un seul run traite a la fois (file d'attente sequentielle) : adapte a un usage local mono-utilisateur, pas concu pour un usage concurrent multi-utilisateurs.

Developpement local (sans Docker)

Necessite CloudCompare installe localement + Python 3 avec pip install -r python/requirements.txt.

npm install
npm run lint       # eslint
npm run typecheck  # tsc --noEmit
npm run test       # jest (tests unitaires)
npm run build
CLOUDCOMPARE_BIN="/chemin/vers/CloudCompare" PYTHON_BIN=python3 DATA_DIR=./data node dist/main.js

Voir CLAUDE.md pour le detail des comportements CloudCompare CLI verifies empiriquement (pieges non documentes, limitations du paquet apt Debian, etc.) — a lire avant de toucher au pipeline.