From 0887d240f72c1c5844c459aab75859c81e05244c Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Wed, 16 Sep 2026 20:12:00 +0200 Subject: [PATCH] =?UTF-8?q?Acc=C3=A9l=C3=A9rer=20le=20ray-tracing=20(SVF,?= =?UTF-8?q?=20openness)=20~10x=20via=20accumulation=20des=20tangentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lidar_pipeline/visualizations.py | 81 ++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/lidar_pipeline/visualizations.py b/lidar_pipeline/visualizations.py index 90b8ffd..4864b5c 100644 --- a/lidar_pipeline/visualizations.py +++ b/lidar_pipeline/visualizations.py @@ -298,10 +298,18 @@ def _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m=Non recording the max upward angle (positive openness) and max downward angle (negative openness) reached at each radius checkpoint. - Padding is done on CPU (numpy) to avoid GPU memory pressure and - pre-compiled kernel mismatches (CUDA_ERROR_NO_BINARY_FOR_GPU on sm_89). - The padded array is transferred to GPU once, then each direction is - processed and results are streamed back to CPU. + Optimisation : on accumule la TANGENTE de l'angle (dz/dist) au lieu de + l'angle lui-même — atan étant strictement croissante, max(angles) = + atan(max(tangentes)). L'arctan (coûteuse, pleine image) n'est donc plus + appliquée qu'aux checkpoints de rayon, pas à chaque pas de rayon. + Un seul couple de max cumulés est maintenu, snapshoté à chaque checkpoint + (les rayons étant emboîtés, chaque checkpoint réutilisait avant le même + calcul 3 fois). fmax ignore les NaN du padding : plus de nan_to_num/where. + + Padding on CPU (numpy) to avoid GPU memory pressure and pre-compiled + kernel mismatches (CUDA_ERROR_NO_BINARY_FOR_GPU on sm_89). The padded + array is transferred to GPU once, then each direction is processed and + results are streamed back to CPU. Args: dem: CPU numpy array — filled DEM (no NaN), shape (rows, cols). @@ -338,6 +346,12 @@ def _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m=Non # Free the CPU copy — we don't need it anymore del padded_np + # Checkpoints triés par pas : (step, r_idx). Les snapshots sont pris quand + # le pas courant atteint le pas du checkpoint — les rayons ne dépassent + # donc pas le plus grand checkpoint demandé (équivalent au break d'avant). + checkpoints = sorted((radii_steps[r_idx], r_idx) for r_idx in range(n_radii)) + last_step = checkpoints[-1][0] + # Process one direction at a time to limit GPU memory. # Store results as flat CPU arrays — transfer back to GPU at the end. pos_results = [None] * n_dirs @@ -346,9 +360,9 @@ def _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m=Non for d_idx in range(n_dirs): ddx, ddy = dx_dir[d_idx], dy_dir[d_idx] - # Pre-compute valid steps for this direction + # Pre-compute valid steps for this direction (jusqu'au dernier checkpoint) valid_steps = [] - for step in range(1, max_dist + 1): + for step in range(1, last_step + 1): px = int(round(ddx * step)) py = int(round(ddy * step)) dist_m = math.sqrt((ddx * step * res) ** 2 + (ddy * step * res) ** 2) @@ -356,11 +370,11 @@ def _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m=Non continue valid_steps.append((step, px, py, dist_m)) - # Running max angles per radius - running_pos = xp.zeros((n_radii, rows, cols)) - running_neg = xp.zeros((n_radii, rows, cols)) - # Track which radius checkpoints have been passed - radii_remaining = set(range(n_radii)) + # Max cumulé des tangentes (float32 : moitié de VRAM vs float64) + running_pos = xp.zeros((rows, cols), dtype=np.float32) + running_neg = xp.zeros((rows, cols), dtype=np.float32) + snapshots = {} + cp_queue = list(checkpoints) for step, px, py, dist_m in valid_steps: # Slice from padded array, subtract original dem @@ -369,34 +383,33 @@ def _ray_trace_horizons_core(dem, rows, cols, res, n_dirs, max_dist, radii_m=Non elev_diff = view - dem del view # free slice reference - # Positive: angle to terrain above viewer - pos_angle = xp.arctan2(xp.maximum(elev_diff, 0), dist_m) - # Negative: angle to terrain below viewer - neg_angle = xp.arctan2(xp.maximum(-elev_diff, 0), dist_m) + # Tangentes des angles (positive : terrain au-dessus, négative : en + # dessous). fmax propage le non-NaN : le bord de padding ne compte + # pas, comme avec l'ancien where(isnan) — en une seule opération. + running_pos = xp.fmax(running_pos, + xp.maximum(elev_diff, 0) / dist_m) + running_neg = xp.fmax(running_neg, + xp.maximum(-elev_diff, 0) / dist_m) del elev_diff # free intermediate - # Update running max for all radius checkpoints still active - for r_idx in radii_remaining: - pos_angle_safe = xp.nan_to_num(pos_angle, nan=0) - neg_angle_safe = xp.nan_to_num(neg_angle, nan=0) - running_pos[r_idx] = xp.where(xp.isnan(pos_angle), running_pos[r_idx], - xp.maximum(running_pos[r_idx], pos_angle_safe)) - running_neg[r_idx] = xp.where(xp.isnan(neg_angle), running_neg[r_idx], - xp.maximum(running_neg[r_idx], neg_angle_safe)) - - # Check which radii have been passed - new_remaining = set() - for r_idx in radii_remaining: - if step < radii_steps[r_idx]: - new_remaining.add(r_idx) - radii_remaining = new_remaining - if not radii_remaining: + # Snapshot du checkpoint atteint : conversion en angle UNE fois + while cp_queue and step >= cp_queue[0][0]: + _, r_idx = cp_queue.pop(0) + snapshots[r_idx] = (xp.arctan(running_pos), + xp.arctan(running_neg)) + if not cp_queue: break + # Checkpoints jamais atteints (steps invalides) : état final du balayage + while cp_queue: + _, r_idx = cp_queue.pop(0) + snapshots[r_idx] = (xp.arctan(running_pos), + xp.arctan(running_neg)) + # Store results on CPU, free GPU memory before next direction - pos_results[d_idx] = to_cpu(running_pos) - neg_results[d_idx] = to_cpu(running_neg) - del running_pos, running_neg + pos_results[d_idx] = to_cpu(xp.stack([snapshots[r][0] for r in range(n_radii)])) + neg_results[d_idx] = to_cpu(xp.stack([snapshots[r][1] for r in range(n_radii)])) + del running_pos, running_neg, snapshots gpu_cleanup() # Free the large padded array