diff --git a/CLAUDE.md b/CLAUDE.md index 6cf3bef..3d91d0e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -LiDAR archaeological processing pipeline that generates 19 terrain visualizations from LAZ/LAS point clouds. Runs in Docker with optional NVIDIA GPU acceleration (CuPy). Designed for French LiDAR HD data in Lambert 93 (EPSG:2154). +LiDAR archaeological processing pipeline that generates 18 terrain visualizations from LAZ/LAS point clouds. Runs in Docker with optional NVIDIA GPU acceleration (CuPy). Designed for French LiDAR HD data in Lambert 93 (EPSG:2154). ## Commands @@ -33,7 +33,7 @@ docker run --rm --gpus all -v $(pwd)/input:/data/input:ro -v $(pwd)/output:/data - **`cli.py`** — argparse + logging setup. Entry point via `python -m lidar_pipeline`. - **`pipeline.py`** — `LidarArchaeoPipeline` orchestrator. `VIZ_STEPS` registry maps names to generate functions. `FilePrefixFilter` for parallel logging. - **`dtm.py`** — PDAL ground classification (SMRF/PMF/CSF + auto-detection) and DTM generation via scipy `binned_statistic_2d`. -- **`visualizations.py`** — 17 `generate_*` functions + 2 IGN overlay lambdas. All take `(dem_file, basename, vis_dir, resolution)` and return a TIF path or None. +- **`visualizations.py`** — 16 `generate_*` functions + 2 IGN overlay lambdas. All take `(dem_file, basename, vis_dir, resolution)` and return a TIF path or None. - **`gpu.py`** — CuPy/numpy abstraction: `HAS_GPU`, `to_gpu()`, `to_cpu()`, `xp_gaussian_filter()`, `xp_uniform_filter()`, `xp_minimum_filter()`, `gpu_cleanup()`. Falls back to CPU gracefully. - **`ign.py`** — IGN WMTS tile download + overlay generation for orthophoto and topographic maps. - **`rendering.py`** — `COLORMAPS` dict maps filename keywords to (cmap, title, legend, description). `tif_to_png()` converts TIF→WebP with legend/scale/north arrow. `generate_pdf_report()` creates A3 PDF. diff --git a/README.md b/README.md index c2df8ca..3f63d0f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Workflow automatisé pour générer des visualisations exploitables à partir de données LiDAR HD (IGN) pour la détection de structures archéologiques. Tourne en Docker avec accélération GPU optionnelle (NVIDIA/CuPy). -## Visualisations (19 par fichier) +## Visualisations (18 par fichier) ### Visualisations principales | # | Visualisation | Utilité archéologique | @@ -26,14 +26,13 @@ Workflow automatisé pour générer des visualisations exploitables à partir de | 13 | **Rugosité** | Écart-type de l'élévation | Surfaces anthropiques vs naturelles | | 14 | **Anomalies statistiques** | Z-score + Local Moran's I | Anomalies topographiques significatives | | 15 | **Ondelette Mexican Hat** | CWT 2D multi-échelle | Tumulus, fossés circulaires | -| 16 | **Texture GLCM** | Contraste, entropie, homogénéité | Labour, surfaces archéologiques | -| 17 | **Accumulation de flux** | Algorithme D8 hydrologique | Fossés d'enceinte, routes antiques | +| 16 | **Accumulation de flux** | Algorithme D8 hydrologique | Fossés d'enceinte, routes antiques | ### Cartes de référence IGN | # | Visualisation | Source | |---|--------------|--------| -| 18 | **Photographie aérienne IGN** | Orthophotographie WMTS | -| 19 | **Carte topographique IGN** | Plan IGN V2 WMTS | +| 17 | **Photographie aérienne IGN** | Orthophotographie WMTS | +| 18 | **Carte topographique IGN** | Plan IGN V2 WMTS | ## Classification du sol diff --git a/lidar_pipeline/pipeline.py b/lidar_pipeline/pipeline.py index c685ab7..69929ae 100644 --- a/lidar_pipeline/pipeline.py +++ b/lidar_pipeline/pipeline.py @@ -44,7 +44,7 @@ from .visualizations import ( generate_hillshade, generate_slope, generate_aspect, generate_curvature, generate_lrm, generate_svf, generate_openness, generate_mslrm, generate_tpi, generate_depressions, generate_sailore, - generate_roughness, generate_anomalies, generate_wavelet, generate_texture, + generate_roughness, generate_anomalies, generate_wavelet, generate_flow, ) from .gpu import gpu_cleanup @@ -71,7 +71,6 @@ VIZ_STEPS = [ ('roughness', generate_roughness), ('anomalies', generate_anomalies), ('wavelet', generate_wavelet), - ('texture', generate_texture), ('flow', generate_flow), ('ortho', lambda d, b, v, r: generate_ign_overlay( d, b, v, r, diff --git a/lidar_pipeline/rendering.py b/lidar_pipeline/rendering.py index 6fbed65..5aadc48 100644 --- a/lidar_pipeline/rendering.py +++ b/lidar_pipeline/rendering.py @@ -156,13 +156,6 @@ COLORMAPS = { 'description': 'Transformée en ondelette 2D — excellente pour détecter structures circulaires', 'vmin_mode': 'symmetric', 'sym_pct': (2, 98), }, - 'texture': { - 'cmap': 'inferno', - 'title': 'Texture GLCM (Contraste + Entropie - Homogénéité)', - 'legend': 'Analyse de la texture du relief (fenêtre 5m)\nClair = Texture hétérogène (labour, ruines, sol perturbé)\nSombre = Texture homogène (sol nu, route, zone plate)\n\nCombine contraste, entropie et homogénéité', - 'description': 'Distingue surfaces anthropiques (labour, chemins) des naturelles', - 'vmin_mode': 'symmetric', 'sym_pct': (2, 98), - }, 'flow': { 'cmap': 'Blues', 'title': 'Accumulation de Flux Hydrologique (D8)', @@ -525,7 +518,7 @@ def generate_pdf_report(basename, vis_dir, pdf_dir, resolution): order = ['mslrm', 'svf', 'negative_openness', 'positive_openness', 'sailore', 'depressions', 'hillshade_multi', 'lrm', 'tpi', 'slope', 'curvature', 'aspect', - 'roughness', 'anomalies', 'wavelet', 'texture', 'flow'] + 'roughness', 'anomalies', 'wavelet', 'flow'] def sort_key(f): name = f.stem.lower() diff --git a/lidar_pipeline/tests/test_visualizations.py b/lidar_pipeline/tests/test_visualizations.py index 1d73592..ffe1647 100644 --- a/lidar_pipeline/tests/test_visualizations.py +++ b/lidar_pipeline/tests/test_visualizations.py @@ -190,14 +190,6 @@ class TestWavelet: assert result.exists() -class TestTexture: - def test_generates_tif(self, synthetic_dem, tmp_output_dir): - from lidar_pipeline.visualizations import generate_texture - result = generate_texture(synthetic_dem, "test", tmp_output_dir, 5.0) - assert result is not None - assert result.exists() - - class TestFlow: def test_generates_tif(self, synthetic_dem, tmp_output_dir): from lidar_pipeline.visualizations import generate_flow diff --git a/lidar_pipeline/visualizations.py b/lidar_pipeline/visualizations.py index 5c2e3da..a22cc93 100644 --- a/lidar_pipeline/visualizations.py +++ b/lidar_pipeline/visualizations.py @@ -676,88 +676,6 @@ def generate_wavelet(dem_file, basename, vis_dir, resolution): return None -# ============================================================ -# Texture GLCM -# ============================================================ - -def generate_texture(dem_file, basename, vis_dir, resolution): - """GLCM-inspired texture analysis — contrast, entropy, homogeneity (GPU-accelerated).""" - gpu_tag = " [GPU]" if HAS_GPU else "" - logger.info(f" → Texture GLCM{gpu_tag}...") - t0 = time.time() - output = vis_dir / f"{basename}_texture.tif" - - try: - dem_np, transform, crs = _read_dem(dem_file) - - # Hillshade — compute on CPU to avoid holding DEM on GPU during texture - gy, gx = np.gradient(dem_np, resolution) - slope = np.arctan(np.sqrt(gx**2 + gy**2)) - alt_rad = np.radians(45) - az_rad = np.radians(315) - aspect = np.arctan2(gy, gx) - shading = (np.sin(alt_rad) * np.cos(slope) + - np.cos(alt_rad) * np.sin(slope) * - np.cos(az_rad - aspect)) - hillshade = np.clip(shading, 0, 1) - - valid = np.asarray(hillshade[~np.isnan(hillshade)]) - if len(valid) == 0: - raise ValueError("No valid data for texture analysis") - lo, hi = np.percentile(valid, (1, 99)) - img = np.clip((hillshade - lo) / max(hi - lo, 0.001), 0, 1) - del hillshade, shading, slope, aspect, gy, gx # free memory - - window = int(5 / resolution) - if window % 2 == 0: - window += 1 - - # Contrast (variance) — GPU-accelerated - img_gpu = to_gpu(img.astype(np.float32)) - local_mean = xp_uniform_filter(img_gpu, size=window) - local_mean_sq = xp_uniform_filter(img_gpu * img_gpu, size=window) - contrast = to_cpu(local_mean_sq - local_mean * local_mean).astype(np.float64) - del img_gpu, local_mean, local_mean_sq # free GPU memory - - # Entropy — compute bin-by-bin to avoid large 3D allocation - n_bins = 16 - img_clean = np.nan_to_num(img, nan=0.0) - img_uint8 = np.clip(img_clean * 255, 0, 255).astype(np.uint8) - quantized = (img_uint8 // (256 // n_bins)).astype(np.int32) - entropy = np.zeros_like(img, dtype=np.float64) - win_area = max(window * window, 1) - - for b in range(n_bins): - plane = (quantized == b).astype(np.float32) - plane_gpu = to_gpu(plane) - prob_plane = to_cpu(xp_uniform_filter(plane_gpu, size=window)) - prob_val = prob_plane / win_area - prob_val = np.clip(prob_val, 1e-10, None) - entropy -= prob_val * np.log2(prob_val) - del plane_gpu # free GPU memory per bin - - del quantized, img_uint8 # free CPU memory - - # Homogeneity — 1 / (1 + variance) - homogeneity = 1.0 / (1.0 + contrast) - - def norm(arr): - valid_arr = arr[~np.isnan(arr)] - if len(valid_arr) == 0: - return arr - std_val = max(np.std(valid_arr), 0.01) - return (arr - np.mean(valid_arr)) / std_val - - texture_combined = 0.4 * norm(contrast) + 0.4 * norm(entropy) - 0.2 * norm(homogeneity) - - _save_tif(output, texture_combined, transform, crs) - logger.info(f" ✓ Texture terminée ({time.time()-t0:.1f}s){gpu_tag}") - return output - except Exception as e: - logger.error(f" ✗ Erreur texture GLCM: {e}", exc_info=True) - return None - - # ============================================================ # Flow accumulation # ============================================================