script-volume-auto/README.md
Nicolas Fryder 7f2e4c323b Auth HTTP Basic + grille de volume adaptative par niveau
- Grille de calcul du volume adaptee dynamiquement au pas spatial de chaque niveau (au lieu d'une
  grille fixe derivee du niveau 0) : evite l'effondrement artificiel du "matching cells %" observe
  des le premier niveau de decimation (99% -> 58% -> 13% -> 3% -> 0.9% -> 0.2%), qui refletait un
  desalignement grille/densite plutot qu'une vraie perte d'information.
- Authentification HTTP Basic sur toute l'app (UI + API), identifiants en env (AUTH_USERNAME /
  AUTH_PASSWORD_HASH hashe bcrypt), desactivable en dev local si non definis. /health reste public
  pour le healthcheck Docker/Coolify.
2026-07-10 14:34:27 +02:00

8.8 KiB

Volume 2.5D & robustesse a la decimation

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

  1. calcule son volume en 2.5D via CloudCompare (plan de reference = altitude minimale du nuage) ;
  2. le 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 le meme plan de reference, et une grille de calcul adaptee dynamiquement a la densite de chaque niveau (grid step = pas spatial du niveau x2) pour eviter des grilles artificiellement vides quand la decimation depasse la resolution de depart ;
  4. assemble les 6 nuages (complet + 5 decimations) 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, 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 un fichier .las, .laz, .copc.laz ou .bin.
  2. Optionnel : override du plan de reference Z (par defaut : altitude minimale d'un echantillon du nuage) et/ou du pas spatial initial (par defaut : distance mediane au plus proche voisin).
  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 6 niveaux (points, volume, surface, % de cellules de grille "matching"), cartes de hauteur, 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 (nuage / plan de reference) sont effectivement comparables. Quand la decimation depasse la resolution de la grille de calcul, ce pourcentage chute — c'est le signal principal de perte de robustesse. Un niveau est marque "a verifier" des que ce taux passe sous 90 % (configurable via MATCHING_CELLS_WARN_THRESHOLD cote code, config.ts).
  • La decimation s'arrete automatiquement si un niveau tombe sous 25 points exploitables ; les niveaux suivants sont marques "skipped" avec la raison.

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.