diff --git a/lidar_pipeline/visualizations.py b/lidar_pipeline/visualizations.py index d6416ce..857899d 100644 --- a/lidar_pipeline/visualizations.py +++ b/lidar_pipeline/visualizations.py @@ -665,12 +665,16 @@ def generate_mslrm(dem_file, basename, vis_dir, resolution, shared=None): # Adaptive scales: finer at higher resolution min_scale = max(2.0, resolution * 4) - candidate_scales = [2, 5, 10, 20, 50, 100, 200] + # Archaeological scales: focus on 2-50m range. + # Small features (ditches, walls, post-holes) need 2-10m. + # Medium features (enclosures, roundhouses) need 10-25m. + # Large scales (50m+) are kept only for context with low weight. + candidate_scales = [2, 3, 5, 8, 10, 15, 25, 50] sigmas = [s for s in candidate_scales if s >= min_scale] - # Archaeological weights: favor 5-25m range (ditches, enclosures, tumulus) + # Weights: favor small-to-medium scales where archaeo features live scale_weights = { - 2: 0.8, 5: 2.0, 10: 1.8, 20: 1.5, 50: 1.0, 100: 0.6, 200: 0.4, + 2: 1.5, 3: 1.8, 5: 2.0, 8: 1.8, 10: 1.5, 15: 1.3, 25: 1.0, 50: 0.5, } weights = np.array([scale_weights.get(s, 1.0) for s in sigmas]) @@ -685,10 +689,12 @@ def generate_mslrm(dem_file, basename, vis_dir, resolution, shared=None): local_mean = _filter_nanaware(dem_np, xp_gaussian_filter, sigma=sigma_px) lrm = dem_np - local_mean lrm[nan_mask] = np.nan - # Std normalization: x / std — preserves sign and contrast better than z-score + # Std normalization valid_lrm = lrm[~nan_mask] lrm_std = max(np.nanstd(valid_lrm), 0.01) if len(valid_lrm) > 0 else 0.01 lrm = lrm / lrm_std + # Clip |z| to 3.0 to prevent large-scale outliers from drowning small features + lrm = np.clip(lrm, -3.0, 3.0) lrm_stack.append(lrm.astype(np.float32)) # Weighted combination — preserve sign for RdBu_r colormap