Suppression de la visualisation Texture GLCM
- Suppression de generate_texture() de visualizations.py - Suppression de l'entrée 'texture' de VIZ_STEPS et COLORMAPS - Suppression du test TestTexture - Mise à jour README (19 → 18 visualisations) - Mise à jour CLAUDE.md (17 → 16 fonctions generate_*) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## 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
|
## 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`.
|
- **`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.
|
- **`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`.
|
- **`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.
|
- **`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.
|
- **`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.
|
- **`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.
|
||||||
|
|||||||
@ -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).
|
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
|
### Visualisations principales
|
||||||
| # | Visualisation | Utilité archéologique |
|
| # | 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 |
|
| 13 | **Rugosité** | Écart-type de l'élévation | Surfaces anthropiques vs naturelles |
|
||||||
| 14 | **Anomalies statistiques** | Z-score + Local Moran's I | Anomalies topographiques significatives |
|
| 14 | **Anomalies statistiques** | Z-score + Local Moran's I | Anomalies topographiques significatives |
|
||||||
| 15 | **Ondelette Mexican Hat** | CWT 2D multi-échelle | Tumulus, fossés circulaires |
|
| 15 | **Ondelette Mexican Hat** | CWT 2D multi-échelle | Tumulus, fossés circulaires |
|
||||||
| 16 | **Texture GLCM** | Contraste, entropie, homogénéité | Labour, surfaces archéologiques |
|
| 16 | **Accumulation de flux** | Algorithme D8 hydrologique | Fossés d'enceinte, routes antiques |
|
||||||
| 17 | **Accumulation de flux** | Algorithme D8 hydrologique | Fossés d'enceinte, routes antiques |
|
|
||||||
|
|
||||||
### Cartes de référence IGN
|
### Cartes de référence IGN
|
||||||
| # | Visualisation | Source |
|
| # | Visualisation | Source |
|
||||||
|---|--------------|--------|
|
|---|--------------|--------|
|
||||||
| 18 | **Photographie aérienne IGN** | Orthophotographie WMTS |
|
| 17 | **Photographie aérienne IGN** | Orthophotographie WMTS |
|
||||||
| 19 | **Carte topographique IGN** | Plan IGN V2 WMTS |
|
| 18 | **Carte topographique IGN** | Plan IGN V2 WMTS |
|
||||||
|
|
||||||
## Classification du sol
|
## Classification du sol
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ from .visualizations import (
|
|||||||
generate_hillshade, generate_slope, generate_aspect, generate_curvature,
|
generate_hillshade, generate_slope, generate_aspect, generate_curvature,
|
||||||
generate_lrm, generate_svf, generate_openness,
|
generate_lrm, generate_svf, generate_openness,
|
||||||
generate_mslrm, generate_tpi, generate_depressions, generate_sailore,
|
generate_mslrm, generate_tpi, generate_depressions, generate_sailore,
|
||||||
generate_roughness, generate_anomalies, generate_wavelet, generate_texture,
|
generate_roughness, generate_anomalies, generate_wavelet,
|
||||||
generate_flow,
|
generate_flow,
|
||||||
)
|
)
|
||||||
from .gpu import gpu_cleanup
|
from .gpu import gpu_cleanup
|
||||||
@ -71,7 +71,6 @@ VIZ_STEPS = [
|
|||||||
('roughness', generate_roughness),
|
('roughness', generate_roughness),
|
||||||
('anomalies', generate_anomalies),
|
('anomalies', generate_anomalies),
|
||||||
('wavelet', generate_wavelet),
|
('wavelet', generate_wavelet),
|
||||||
('texture', generate_texture),
|
|
||||||
('flow', generate_flow),
|
('flow', generate_flow),
|
||||||
('ortho', lambda d, b, v, r: generate_ign_overlay(
|
('ortho', lambda d, b, v, r: generate_ign_overlay(
|
||||||
d, b, v, r,
|
d, b, v, r,
|
||||||
|
|||||||
@ -156,13 +156,6 @@ COLORMAPS = {
|
|||||||
'description': 'Transformée en ondelette 2D — excellente pour détecter structures circulaires',
|
'description': 'Transformée en ondelette 2D — excellente pour détecter structures circulaires',
|
||||||
'vmin_mode': 'symmetric', 'sym_pct': (2, 98),
|
'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': {
|
'flow': {
|
||||||
'cmap': 'Blues',
|
'cmap': 'Blues',
|
||||||
'title': 'Accumulation de Flux Hydrologique (D8)',
|
'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',
|
order = ['mslrm', 'svf', 'negative_openness',
|
||||||
'positive_openness', 'sailore', 'depressions', 'hillshade_multi',
|
'positive_openness', 'sailore', 'depressions', 'hillshade_multi',
|
||||||
'lrm', 'tpi', 'slope', 'curvature', 'aspect',
|
'lrm', 'tpi', 'slope', 'curvature', 'aspect',
|
||||||
'roughness', 'anomalies', 'wavelet', 'texture', 'flow']
|
'roughness', 'anomalies', 'wavelet', 'flow']
|
||||||
|
|
||||||
def sort_key(f):
|
def sort_key(f):
|
||||||
name = f.stem.lower()
|
name = f.stem.lower()
|
||||||
|
|||||||
@ -190,14 +190,6 @@ class TestWavelet:
|
|||||||
assert result.exists()
|
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:
|
class TestFlow:
|
||||||
def test_generates_tif(self, synthetic_dem, tmp_output_dir):
|
def test_generates_tif(self, synthetic_dem, tmp_output_dir):
|
||||||
from lidar_pipeline.visualizations import generate_flow
|
from lidar_pipeline.visualizations import generate_flow
|
||||||
|
|||||||
@ -676,88 +676,6 @@ def generate_wavelet(dem_file, basename, vis_dir, resolution):
|
|||||||
return None
|
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
|
# Flow accumulation
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user