Caler les faisceaux de vol, décimer l'openness et cibler les couches affichées
Trois chantiers liés à la qualité et au coût des rendus : - Calage vertical des faisceaux : les passes d'une tuile peuvent être biaisées de quelques cm (±2,5 cm mesurés sur 1000_6882), créant des marches aux recouvrements. Les offsets par PointSourceId sont mesurés sur les points sol de la tuile (réf. médiane itérée) et retranchés ≥ 0,5 cm avant rastérisation, avec sidecar de cache et application au plancher bare-earth. - Openness décimée ×2 : lancé de rayons sur grille par blocs (max/min) puis rééchantillonnage bilinéaire — 532 s → 40 s par tuile à 0,2 m sur CPU, signal archéologique préservé. Réglable --openness-downsample. - Génération webapp concentrée sur les couches affichées : les défauts /api/generate, /api/preview et le sélecteur génèrent le panneau complet (slope, aspect, pos_open) au lieu d'aspect seul.
This commit is contained in:
@ -55,7 +55,7 @@ class FilePrefixFilter(logging.Filter):
|
||||
_file_filter = FilePrefixFilter()
|
||||
|
||||
from .progress import report_event
|
||||
from .dtm import classify_ground, create_dtm_fast
|
||||
from .dtm import classify_ground, create_dtm_fast, STRIP_ALIGN_VERSION, STRIP_ALIGN_THRESHOLD
|
||||
from .visualizations import (
|
||||
SharedDEM,
|
||||
generate_hillshade, generate_slope, generate_aspect,
|
||||
@ -109,7 +109,7 @@ VIZ_STEPS = [
|
||||
class LidarArchaeoPipeline:
|
||||
"""Orchestrates the LiDAR archaeological analysis pipeline."""
|
||||
|
||||
def __init__(self, input_dir, output_dir, resolution=0.5, workers=1, force=False, ground_method='auto', ign_classes="sol", force_classify=False, keep_tif=False, bare_earth=False, quality=98, only_viz=None, skip_viz=None, output_format='avif', gpu_ids=None, no_index=False, incremental_index=False):
|
||||
def __init__(self, input_dir, output_dir, resolution=0.5, workers=1, force=False, ground_method='auto', ign_classes="sol", force_classify=False, keep_tif=False, bare_earth=False, quality=98, only_viz=None, skip_viz=None, output_format='avif', gpu_ids=None, no_index=False, incremental_index=False, strip_align=True, openness_downsample=None):
|
||||
self.input_dir = Path(input_dir)
|
||||
self.output_dir = Path(output_dir)
|
||||
# Accept single float or comma-separated string for multi-resolution
|
||||
@ -134,6 +134,8 @@ class LidarArchaeoPipeline:
|
||||
self.gpu_ids = gpu_ids
|
||||
self.no_index = no_index
|
||||
self.incremental_index = incremental_index
|
||||
self.strip_align = strip_align
|
||||
self.openness_downsample = openness_downsample
|
||||
self._last_index_rebuild = 0.0
|
||||
self.temp_dir = self.output_dir / "temp"
|
||||
|
||||
@ -368,6 +370,27 @@ class LidarArchaeoPipeline:
|
||||
recorded = self._dtm_method_name(basename, res_suffix)
|
||||
return recorded is None or recorded == self._effective_ground_method()
|
||||
|
||||
def _strip_align_matches(self, basename, res_suffix):
|
||||
"""True si le sidecar de calage des faisceaux correspond à la config.
|
||||
|
||||
Un DTM sans sidecar (antérieur au calage) est régénéré pour mesurer
|
||||
et consigner ses offsets ; un sidecar de version ou de seuil
|
||||
différents aussi. Calage désactivé : tout DTM porteur d'un sidecar
|
||||
(donc calé) est régénéré non calé.
|
||||
"""
|
||||
sidecar = self.dtm_dir / f"{basename}_dtm{res_suffix}_stripalign.json"
|
||||
if not self.strip_align:
|
||||
return not sidecar.exists()
|
||||
if not sidecar.exists():
|
||||
return False
|
||||
try:
|
||||
import json
|
||||
data = json.loads(sidecar.read_text(encoding="utf-8"))
|
||||
return (data.get("version") == STRIP_ALIGN_VERSION
|
||||
and abs(float(data.get("threshold", -1)) - STRIP_ALIGN_THRESHOLD) < 1e-9)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _write_dtm_method(self, basename, res_suffix):
|
||||
"""Record the ground classification method used to build a DTM."""
|
||||
try:
|
||||
@ -416,7 +439,10 @@ class LidarArchaeoPipeline:
|
||||
res_suffix = self._res_suffix(res)
|
||||
dtm_path = self.dtm_dir / f"{basename}_dtm{res_suffix}.tif"
|
||||
if dtm_path.exists() and not self.force_classify:
|
||||
if method_matches:
|
||||
if not self._strip_align_matches(basename, res_suffix):
|
||||
logger.info(f" DTM{res_suffix} sans calage de faisceaux conforme — régénération (offsets verticaux mesurés et appliqués)")
|
||||
dtm_path.unlink()
|
||||
elif method_matches:
|
||||
import rasterio
|
||||
try:
|
||||
with rasterio.open(dtm_path) as src:
|
||||
@ -468,7 +494,8 @@ class LidarArchaeoPipeline:
|
||||
output_suffix=res_suffix,
|
||||
source_laz=laz_file,
|
||||
bare_earth=self.bare_earth,
|
||||
pure=pure_ign)
|
||||
pure=pure_ign,
|
||||
strip_align=self.strip_align)
|
||||
t_dtm = time.time() - t2
|
||||
if not dtm_file:
|
||||
logger.error(f" ✗ Échec DTM {res}m/px ({t_dtm:.1f}s)")
|
||||
@ -483,6 +510,12 @@ class LidarArchaeoPipeline:
|
||||
self._write_dtm_method(basename, res_suffix)
|
||||
|
||||
# Process each resolution: visualizations + PDF
|
||||
# Option de calcul (surcharge le défaut du module) appliquée ICI car les
|
||||
# workers (spawn) réimportent les modules à froid : c'est le seul endroit
|
||||
# qui s'exécute dans le processus qui fait le calcul.
|
||||
if self.openness_downsample is not None:
|
||||
from . import visualizations as _viz_mod
|
||||
_viz_mod.OPENNESS_DOWNSAMPLE = max(1, int(self.openness_downsample))
|
||||
all_vis_results = {}
|
||||
for res in self.resolutions:
|
||||
res_suffix = self._res_suffix(res)
|
||||
@ -580,7 +613,7 @@ class LidarArchaeoPipeline:
|
||||
active_ids = self.gpu_ids if self.gpu_ids else available_gpu_ids()
|
||||
resolutions_str = ','.join(str(r) for r in self.resolutions)
|
||||
future_to_file = {
|
||||
executor.submit(_process_file_standalone, str(laz_file), str(self.input_dir), str(self.output_dir), resolutions_str, self.force, self.ground_method, self.ign_classes, self.force_classify, self.keep_tif, self.bare_earth, self.quality, self.only_viz, self.skip_viz, self.output_format, active_ids[file_idx % len(active_ids)] if active_ids else None): laz_file
|
||||
executor.submit(_process_file_standalone, str(laz_file), str(self.input_dir), str(self.output_dir), resolutions_str, self.force, self.ground_method, self.ign_classes, self.force_classify, self.keep_tif, self.bare_earth, self.quality, self.only_viz, self.skip_viz, self.output_format, active_ids[file_idx % len(active_ids)] if active_ids else None, self.openness_downsample): laz_file
|
||||
for file_idx, laz_file in enumerate(files)
|
||||
}
|
||||
done = 0
|
||||
@ -682,7 +715,7 @@ class LidarArchaeoPipeline:
|
||||
logger.warning(f" Note: Impossible de supprimer les fichiers temporaires: {e}")
|
||||
|
||||
|
||||
def _process_file_standalone(laz_file_str, input_dir, output_dir, resolution, force=False, ground_method='auto', ign_classes="sol", force_classify=False, keep_tif=False, bare_earth=False, quality=98, only_viz=None, skip_viz=None, output_format='avif', gpu_id=None):
|
||||
def _process_file_standalone(laz_file_str, input_dir, output_dir, resolution, force=False, ground_method='auto', ign_classes="sol", force_classify=False, keep_tif=False, bare_earth=False, quality=98, only_viz=None, skip_viz=None, output_format='avif', gpu_id=None, openness_downsample=None):
|
||||
"""Standalone function for multiprocessing — creates its own pipeline instance.
|
||||
|
||||
Each worker gets its own temp directory to avoid file conflicts.
|
||||
@ -709,7 +742,7 @@ def _process_file_standalone(laz_file_str, input_dir, output_dir, resolution, fo
|
||||
worker_logger.addHandler(handler)
|
||||
worker_logger.addFilter(_file_filter)
|
||||
|
||||
pipeline = LidarArchaeoPipeline(input_dir, output_dir, resolution=resolution, workers=1, force=force, ground_method=ground_method, ign_classes=ign_classes, force_classify=force_classify, keep_tif=keep_tif, bare_earth=bare_earth, quality=quality, only_viz=only_viz, skip_viz=skip_viz, output_format=output_format)
|
||||
pipeline = LidarArchaeoPipeline(input_dir, output_dir, resolution=resolution, workers=1, force=force, ground_method=ground_method, ign_classes=ign_classes, force_classify=force_classify, keep_tif=keep_tif, bare_earth=bare_earth, quality=quality, only_viz=only_viz, skip_viz=skip_viz, output_format=output_format, openness_downsample=openness_downsample)
|
||||
basename = _file_basename(laz_file_str)
|
||||
pipeline.temp_dir = pipeline.output_dir / "temp" / basename
|
||||
pipeline.temp_dir.mkdir(exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user