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

182 lines
10 KiB
Markdown

# 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
```bash
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 :
```bash
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`.
```bash
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](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.