Translate the whole project to English and fix outdated comments and help

Comments, docstrings, logs, CLI help, map UI, legends, PDF sheet, scripts,
compose files and AGENTS.md are now English. Data keys stay unchanged
(relief_oriente, densite_sol, visualisations/, API JSON keys, link params).
Wrong comments and help defaults found along the way are corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2026-09-27 23:16:45 +02:00
parent cc1c22d2b8
commit fb892ea9f2
52 changed files with 4356 additions and 4323 deletions

View File

@ -14,8 +14,8 @@ from .gpu import log_gpu_status
logger = logging.getLogger("lidar")
# Motif pkill des processus PDAL de CE run (renseigné dans main) — cf.
# _kill_orphan_pdal : borné à notre output pour épargner les autres runs.
# pkill pattern for the PDAL processes of THIS run (set in main) — see
# _kill_orphan_pdal: scoped to our output to spare other runs.
_pdal_kill_pattern = None
@ -26,7 +26,7 @@ def setup_logging(verbose=False, debug=False):
verbose: If True, include timestamps and level names.
debug: If True, set level to DEBUG and add file:line info.
"""
# Ensure UTF-8 output for French messages (é, è, ê, etc.)
# Ensure UTF-8 output for non-ASCII characters in messages (✓, ✗, →, ≠, etc.)
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
@ -63,38 +63,38 @@ def setup_logging(verbose=False, debug=False):
def main():
"""Entry point for the LiDAR archaeological pipeline."""
parser = argparse.ArgumentParser(
description="Pipeline LiDAR pour détection archéologique",
description="LiDAR pipeline for archaeological detection",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""\
Exemples:
Traitement standard:
Examples:
Standard processing (0.2 m/px):
python -m lidar_pipeline /data/input -o /data/output
Haute résolution avec accélération GPU:
python -m lidar_pipeline /data/input -o /data/output -r 0.2 -g
Coarser 0.5 m/px resolution:
python -m lidar_pipeline /data/input -o /data/output -r 0.5
Sélectionner un GPU spécifique (index 0):
Select a specific GPU (index 0):
python -m lidar_pipeline /data/input -o /data/output -g 0
Sélectionner plusieurs GPUs (indices 0 et 2):
Select several GPUs (indices 0 and 2):
python -m lidar_pipeline /data/input -o /data/output -g 0,2
Utiliser tous les GPUs disponibles:
Use all available GPUs:
python -m lidar_pipeline /data/input -o /data/output -g all
Mode verbeux (timestamps):
Verbose mode (timestamps):
python -m lidar_pipeline /data/input -o /data/output -v
Mode debug (détails internes):
Debug mode (internal details):
python -m lidar_pipeline /data/input -o /data/output --debug
Forcer la régénération de tous les fichiers:
Force regeneration of all files:
python -m lidar_pipeline /data/input -o /data/output --force
Traiter un seul fichier (pour tests):
Process a single file (for testing):
python -m lidar_pipeline /data/input -o /data/output --file LHD_FXX_1000_6881_PTS_LAMB93_IGN69.copc
Traitement parallèle (4 workers):
Parallel processing (4 workers):
python -m lidar_pipeline /data/input -o /data/output -w 4
"""
)
@ -102,183 +102,188 @@ def main():
"input",
nargs="?",
default="/data/input",
help="Dossier contenant les fichiers LAZ/LAS (défaut: /data/input ; "
"optionnel pour --rebuild-index)"
help="Directory containing the LAZ/LAS files (default: /data/input; "
"optional with --rebuild-index)"
)
parser.add_argument(
"-o", "--output",
default="/data/output",
help="Dossier de sortie (défaut: /data/output)"
help="Output directory (default: /data/output)"
)
parser.add_argument(
"-r", "--resolution",
type=str,
default="0.2",
help="Résolution en m/px, ou multiples séparées par virgules (défaut: 0.2)"
help="Resolution in m/px, or several comma-separated values (default: 0.2)"
)
parser.add_argument(
"-w", "--workers",
type=str,
default="auto",
help="Nombre de workers pour traitement parallèle, ou « auto » = "
"cœurs - 2 évalué au lancement du run (défaut: auto)"
help="Number of workers for parallel processing, or \"auto\" = "
"cores - 2 clamped to [2, 16], evaluated when the run starts "
"(default: auto)"
)
parser.add_argument(
"-g", "--gpu",
nargs="?",
const="all",
default=None,
metavar="LISTE",
help="Sélectionner le(s) GPU(s) à utiliser : un index (ex: -g 0), "
"une liste (ex: -g 0,2), ou 'all' pour tous. "
"Sans -g : tous les GPU disponibles, un par worker (round-robin)."
metavar="LIST",
help="Select the GPU(s) to use: an index (e.g. -g 0), "
"a list (e.g. -g 0,2), or 'all' for all of them. "
"Without -g: all available GPUs; workers are spread across them "
"within the free VRAM, the excess runs on CPU."
)
parser.add_argument(
"-f", "--force",
action="store_true",
help="Régénérer tous les fichiers même si les WebP existent déjà"
help="Regenerate all files even if the output images (AVIF/WebP) already exist"
)
parser.add_argument(
"--force-classification",
action="store_true",
help="Reclassifier le sol même si la méthode est inchangée (régénère aussi le DTM "
"et les images). Sans ce flag, changer --ground-classification suffit : la "
"méthode enregistrée est comparée et un changement déclenche la reclassification."
help="Reclassify the ground even if the method is unchanged (also regenerates the "
"DTM and the images). Without this flag, changing --ground-classification is "
"enough: the recorded method is compared and a change triggers reclassification."
)
parser.add_argument(
"--openness-downsample",
type=int,
default=None,
metavar="FACTEUR",
help="Facteur de sous-échantillonnage du calcul d'openness (lancé de rayons) : "
"grille décimée par blocs puis rééchantillonnage. Défaut : 2 (~8× plus "
"rapide, rendu quasi identique) ; 1 = pleine résolution"
metavar="FACTOR",
help="Downsampling factor for the openness computation (ray tracing): "
"block-decimated grid, then resampling. Default: 2 (cost divided by "
"factor³, ~13x faster measured, near-identical rendering); 1 = full resolution"
)
parser.add_argument(
"--edge-buffer",
type=float,
default=100.0,
metavar="METRES",
help="Raccord des bords : étendre le MNT d'une bande de N mètres remplie avec "
"les points sol des 8 tuiles LAZ voisines, pour que les rendus à grand "
"noyau (openness, SVF, LRM) soient continus d'une tuile à l'autre. "
"100 m couvre tous les rayons ; les images finales sont recadrées sur "
"la dalle 1 km exacte. Défaut : 100 (toujours appliqué en production) ; "
"0 = désactivé. Un changement de valeur régénère les DTM concernés."
metavar="METERS",
help="Edge stitching: extend the DTM by an N-meter band filled with the "
"ground points of the 8 neighboring LAZ tiles, so that large-kernel "
"renders (openness, SVF, LRM) are continuous from one tile to the next. "
"100 m covers all radii; final images are cropped to the exact 1 km "
"tile. Default: 100 (always applied in production); 0 = disabled. "
"Changing the value regenerates the affected DTMs."
)
parser.add_argument(
"--no-strip-align",
action="store_true",
help="Désactiver le calage vertical des faisceaux de vol. Par défaut, les écarts "
"verticaux ≥ 0,5 cm entre lignes de vol (PointSourceId) d'une tuile sont mesurés "
"sur les points sol et corrigés avant rastérisation du MNT, puis la gigue "
"intra-faisceau (décalages aléatoires des lignes de balayage successives d'une "
"même passe, type vibration capteur) est corrigée par fenêtres de temps GPS "
"(offsets et séries consignés dans DTM/*_stripalign.json)"
help="Disable vertical alignment of flight strips. By default, vertical offsets "
"≥ 0.5 cm between the flight strips (PointSourceId) of a tile are measured on "
"the ground points and corrected before DTM rasterization, then each scan line "
"is adjusted jointly (offset and roll) against the other strips, with a "
"per-strip scan-angle calibration profile (GPS-time window jitter correction "
"when scan_angle is missing); corrections are recorded in "
"DTM/*_stripalign.json"
)
parser.add_argument(
"--keep-tif",
action="store_true",
help="Conserver les fichiers TIFF (DTM + visualisations) pour pouvoir régénérer les WebP sans recalculer"
help="Keep the TIFF files (DTM + visualizations) so the output images can be regenerated without recomputing"
)
parser.add_argument(
"--ground-classification",
choices=["auto", "ign", "smrf", "csf"],
default="ign",
help="Méthode de classification du sol : auto (préfère la pré-classification IGN si "
"présente — base rapide — sinon détection SMRF/CSF), ign, smrf, csf. "
"Avec ign, le MNT est la rasterisation pure des classes choisies "
"(--ign-classes) sans aucune retouche ; avec smrf/csf, il est complété "
"par le retour le plus bas par cellule + interpolation des trous. "
"(défaut: ign, imposé en production)"
help="Ground classification method: auto (prefers the IGN pre-classification when "
"present — fast path — otherwise SMRF/CSF detection), ign, smrf, csf. "
"With ign, the DTM is rasterized from the chosen classes (--ign-classes); "
"with smrf/csf, ground is classified by PDAL first. In every case small gaps "
"are filled within the point envelope. "
"(default: ign, enforced in production)"
)
parser.add_argument(
"--ign-classes",
default="sol",
help="Classes LAS extraites pour le MNT avec la classification IGN (méthode "
"ign/auto) : liste noms ou codes séparés par virgules — "
"sol(2), unclassified(1), eau(9), virtuel(66), pont(17), sursol(64). "
"Ex: --ign-classes sol,unclassified. Changer la liste reclassifie les "
"dalles concernées. (défaut: sol)"
help="LAS classes extracted for the DTM with the IGN classification (ign/auto "
"method): comma-separated list of names or codes — "
"sol(2, ground), unclassified(1), eau(9, water), virtuel(66, virtual), "
"pont(17, bridge), sursol(64, above-ground). "
"E.g. --ign-classes sol,unclassified. Changing the list reclassifies the "
"affected tiles. (default: sol)"
)
parser.add_argument(
"--quality",
type=int,
default=60,
help="Qualité image (1-100, défaut: 60). Utilisez 100 pour lossless."
help="Image quality (1-100, default: 60). 100 requests lossless (honored in WebP; "
"Pillow ignores it in AVIF)."
)
parser.add_argument(
"--lossless",
action="store_true",
help="Forcer la compression lossless (équivalent à --quality 100)"
help="Force lossless compression (same as --quality 100)"
)
parser.add_argument(
"--format",
choices=["webp", "avif"],
default="avif",
help="Format de sortie : avif (défaut, meilleure qualité) ou webp"
help="Output format: avif (default, better quality) or webp"
)
parser.add_argument(
"--only",
nargs="+",
type=str,
default=None,
help="Générer uniquement ces visualisations (ex: --only hillshade svf lrm)"
help="Generate only these visualizations (e.g. --only hillshade svf mslrm)"
)
parser.add_argument(
"--skip",
nargs="+",
type=str,
default=None,
help="Exclure ces visualisations (ex: --skip ortho topo)"
help="Exclude these visualizations (e.g. --skip ortho topo)"
)
parser.add_argument(
"--file",
nargs="+",
type=str,
default=None,
help="Traiter un ou plusieurs fichiers LAZ/LAS (nom complet sans extension, ex: LHD_FXX_1000_6882_PTS_LAMB93_IGN69.copc)"
help="Process one or more LAZ/LAS files (full name without extension, e.g. LHD_FXX_1000_6882_PTS_LAMB93_IGN69.copc)"
)
parser.add_argument(
"--fetch-tiles",
nargs="+",
default=None,
metavar="COL,ROW",
help="Télécharger ces dalles LiDAR HD depuis l'IGN avant traitement "
"(tuiles non encore générées, ex: --fetch-tiles 1055,6882 1056,6883)"
help="Download these LiDAR HD tiles from IGN before processing "
"(tiles not generated yet, e.g. --fetch-tiles 1055,6882 1056,6883)"
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Mode verbeux : affiche les timestamps et niveaux"
help="Verbose mode: show timestamps and levels"
)
parser.add_argument(
"--debug",
action="store_true",
help="Mode debug : affiche les détails internes (fichier:ligne)"
help="Debug mode: show internal details (file:line)"
)
parser.add_argument(
"--rebuild-index",
action="store_true",
help="Régénérer uniquement le catalogue des tuiles traitées (vignettes, sous-tuiles, inventaire — sans retraiter)"
help="Only rebuild the catalog of processed tiles (thumbnails, subtiles, inventory — no reprocessing)"
)
parser.add_argument(
"--no-index",
action="store_true",
help="Ne pas générer le catalogue (vignettes + inventaire) à la fin du traitement"
help="Do not build the catalog (thumbnails + inventory), neither after each tile nor at the end of processing"
)
parser.add_argument(
"--incremental-index",
action="store_true",
help="Conservé pour compatibilité : le catalogue est désormais régénéré après "
"chaque tuile terminée par défaut (sauf --no-index)"
help="Kept for compatibility: the catalog is now rebuilt after every "
"finished tile by default (unless --no-index)"
)
parser.add_argument(
"--quality-backfill",
action="store_true",
help="Calculer uniquement les sidecars qualité (densité sol, dates de vol) "
"des LAZ du dossier d'entrée qui n'en ont pas, puis régénérer l'inventaire "
"(sans DTM ni visualisation, ~5 s par dalle)"
help="Only compute the quality sidecars (ground density, flight dates) "
"of the input LAZ files that lack one, then rebuild the inventory "
"(no DTM or visualization, ~5 s per tile)"
)
args = parser.parse_args()
@ -291,7 +296,7 @@ def main():
logger.addFilter(_file_filter)
logger.info("=" * 60)
logger.info("Pipeline LiDAR Archéologique")
logger.info("LiDAR Archaeological Pipeline")
logger.info("=" * 60)
# Parse --gpu into a list of GPU IDs
@ -304,16 +309,16 @@ def main():
try:
gpu_ids = [int(g.strip()) for g in gpu_arg.split(',')]
except ValueError:
parser.error(f"GPU invalide: {gpu_arg!r}. Utilisez un index, une liste (0,2) ou 'all'.")
parser.error(f"Invalid GPU: {gpu_arg!r}. Use an index, a list (0,2) or 'all'.")
if gpu_ids is not None:
from .gpu import restrict_gpus
restrict_gpus(gpu_ids)
# Kill orphan PDAL processes on interrupt or termination. Le motif est
# borné à NOTRE répertoire temporaire (pdal pipeline <output>/temp/…) : un
# pkill global massacrerient les PDAL des autres runs de la machine (ex.
# job de génération concurrent). Pas de hook atexit : une sortie normale n'a
# aucune raison de tuer quoi que ce soit.
# Kill orphan PDAL processes on interrupt or termination. The pattern is
# scoped to OUR temp directory (pdal pipeline <output>/temp/...): a global
# pkill would slaughter the PDAL processes of other runs on the machine
# (e.g. a concurrent generation job). No atexit hook: a normal exit has no
# reason to kill anything.
global _pdal_kill_pattern
_pdal_kill_pattern = f"pdal pipeline {args.output}/temp/"
signal.signal(signal.SIGINT, _kill_orphan_pdal)
@ -322,14 +327,14 @@ def main():
log_gpu_status()
try:
# Mode --rebuild-index : régénère uniquement le catalogue, sans retraiter
# --rebuild-index mode: only rebuild the catalog, no reprocessing
if args.rebuild_index:
from .index import build_index
index_path = build_index(args.output, args.format)
if index_path:
logger.info(f"Catalogue généré : {index_path}")
logger.info(f"Catalog built: {index_path}")
else:
logger.warning("Aucune tuile traitée trouvée — catalogue non généré")
logger.warning("No processed tile found — catalog not built")
return
if args.quality_backfill:
@ -338,13 +343,13 @@ def main():
from .quality import backfill_quality
n = backfill_quality(args.input, args.output,
codes=tuple(parse_ign_classes(args.ign_classes)))
logger.info(f"{n} sidecar(s) qualité écrit(s)")
logger.info(f"{n} quality sidecar(s) written")
build_index(args.output, args.format)
return
# Nouveau run : remise à zéro du journal d'événements (file de
# génération). Ici, avant le téléchargement — les événements download
# survivent au démarrage du traitement (process_all ne tronque plus).
# New run: reset the event log (generation queue). Done here, before
# downloading — download events survive the start of processing
# (process_all no longer truncates the log).
from .progress import reset_events
reset_events(args.output)
@ -358,10 +363,10 @@ def main():
skip_viz = [v.strip() for item in args.skip for v in item.split(',')]
resolutions = [float(r.strip()) for r in args.resolution.split(',') if r.strip()]
# Téléchargement des dalles IGN manquantes avant le traitement.
# only_viz/resolutions : une tuile n'est ignorée que si elle possède
# déjà toutes les visualisations demandées — une tuile incomplète est
# (re)téléchargée pour générer ses visualisations manquantes.
# Download missing IGN tiles before processing.
# only_viz/resolutions: a tile is skipped only if it already has all
# the requested visualizations — an incomplete tile is
# (re)downloaded to generate its missing visualizations.
if args.fetch_tiles:
from .fetch_ign import fetch_tiles, parse_tile_specs
try:
@ -369,14 +374,14 @@ def main():
except ValueError as e:
logger.error(str(e))
return
logger.info(f"Téléchargement de {len(specs)} dalle(s) LiDAR HD depuis l'IGN...")
logger.info(f"Downloading {len(specs)} LiDAR HD tile(s) from IGN...")
fetched = fetch_tiles(args.input, specs, args.output,
only_viz=only_viz, resolutions=resolutions,
force=args.force or args.force_classification)
if fetched:
logger.info(f"{len(fetched)} dalle(s) téléchargée(s) — traitement...")
logger.info(f"{len(fetched)} tile(s) downloaded — processing...")
else:
logger.warning("Aucune dalle téléchargée (déjà présentes ou introuvables)")
logger.warning("No tile downloaded (already present or not found)")
pipeline = LidarArchaeoPipeline(
input_dir=args.input,
output_dir=args.output,
@ -420,7 +425,7 @@ def main():
# Remove duplicates
matches = list(dict.fromkeys(matches))
if not matches:
logger.warning(f"Aucun fichier trouvé pour: {pattern}")
logger.warning(f"No file found for: {pattern}")
continue
selected_files.extend(matches)
# Remove duplicates across patterns
@ -431,17 +436,17 @@ def main():
seen.add(f)
unique_files.append(f)
if not unique_files:
logger.error("Aucun fichier trouvé pour les motifs spécifiés")
logger.error("No file found for the given patterns")
sys.exit(1)
logger.info(f"Traitement de {len(unique_files)} fichier(s) sélectionné(s)")
logger.info(f"Processing {len(unique_files)} selected file(s)")
for laz_file in unique_files:
logger.info(f" → {laz_file.name}")
# Réutilise process_all : workers parallèles, résumé, index, nettoyage
# Reuse process_all: parallel workers, summary, index, cleanup
pipeline.process_all(files=unique_files)
else:
pipeline.process_all()
except Exception as e:
logger.error(f"Erreur fatale: {e}", exc_info=True)
logger.error(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
@ -450,8 +455,8 @@ def _kill_orphan_pdal(signum=None, frame=None):
import subprocess
# Belt-and-suspenders: os.killpg below is the primary mechanism.
# This handles the edge case where a child escapes the process group.
# Motif borné à notre output (cf. main) pour ne pas tuer les PDAL des
# autres runs de la machine.
# Pattern scoped to our output (see main) so the PDAL processes of other
# runs on the machine are not killed.
if _pdal_kill_pattern:
try:
import re
@ -460,7 +465,7 @@ def _kill_orphan_pdal(signum=None, frame=None):
except Exception:
pass
if signum is not None:
logger.info("Interruption — nettoyage des processus")
logger.info("Interrupted — cleaning up processes")
# Force-kill all child processes immediately
try:
import os