Add multi-GPU support and fix scale bar / location map overlap

Multi-GPU:
- gpu.py: lazy CuPy initialization so CUDA_VISIBLE_DEVICES takes
  effect before context creation in worker processes
- gpu.py: detect GPU count via nvidia-smi (no CUDA import needed)
- gpu.py: add set_active_gpu() to assign workers to specific GPUs
- pipeline.py: distribute files across GPUs (file % num_gpus) in
  parallel mode so both GPUs are used simultaneously
- pipeline.py: log GPU count when multiple GPUs detected

Layout fixes:
- rendering.py: move scale bar left of location map to avoid overlap
  (scale bar ends at fig_x=0.78, map starts at 0.82)
- rendering.py: expand location map inset to 0.16x0.13 fig coords
- rendering.py: return bounds from _download_location_map so imshow
  extent matches the actual IGN tile coverage (80km context)
- ign.py: add min_zoom parameter to download_ign_tiles, fixing the
  location map that was broken (zoom 10 blocked by hardcoded min_zoom=15)
This commit is contained in:
Antoine Jacquin
2026-05-15 12:00:34 +02:00
parent 4848f25326
commit 5af53a390f
3 changed files with 145 additions and 31 deletions

View File

@ -274,15 +274,16 @@ def _apply_colormap(data, tif_file):
def _download_location_map(min_x, max_x, min_y, max_y):
"""Download a wide-area IGN topographic map for location context.
Downloads a zoomed-out IGN PLANIGNV2 tile covering 5-10x the processed
zone extent, giving a wider geographic context. Results are cached to
Downloads a zoomed-out IGN PLANIGNV2 tile covering ~80km around the
processed zone, giving wider geographic context. Results are cached to
avoid re-downloading for each visualization in the same tile.
Args:
min_x, max_x, min_y, max_y: DTM bounds in Lambert 93.
Returns:
numpy array (H, W, 3) uint8, or None on failure.
Tuple (image_array, bounds_dict) where bounds_dict has keys
'min_x', 'max_x', 'min_y', 'max_y' in Lambert 93, or None on failure.
"""
import hashlib
@ -323,7 +324,13 @@ def _download_location_map(min_x, max_x, min_y, max_y):
)
if result is not None:
_location_map_cache[cache_key] = result
bounds = {
'min_x': context_min_x, 'max_x': context_max_x,
'min_y': context_min_y, 'max_y': context_max_y,
}
cached = (result, bounds)
_location_map_cache[cache_key] = cached
return cached
return result
except Exception as e:
@ -637,6 +644,7 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None,
edgecolor='#cccccc', alpha=0.9))
# Scale bar — adaptive with alternating black/white segments
# Position: left of the location map to avoid overlap
extent_m_x = max_x - min_x
scale_m, scale_label = _nice_scale(extent_m_x)
pixels_per_meter = 1.0 / pixel_size_x
@ -646,7 +654,17 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None,
bar_bottom_y = 0.55
bar_top_y = 0.85
bar_height = bar_top_y - bar_bottom_y
scale_start_x = 0.80
# Place scale bar so it ends before the location map (map starts at x=0.82 in fig coords)
# map_ax occupies [0.82, 0.02, 0.16, 0.13] in figure coords
# info_ax occupies [data_left, 0.015, width, 0.09]
# Scale bar end in fig coords = info_ax.left + (scale_start_x + scale_px/width) * info_ax.width
# We need: info_ax.left + (scale_start_x + scale_px/width) * info_ax.width < 0.80
scale_end_frac = scale_px / width # fraction of info_ax width
info_ax_width = data_width_frac + cbar_width + 0.02
# Calculate scale_start_x so scale bar ends at fig_x = 0.78 (leaving gap before map at 0.82)
max_scale_end_fig = 0.78
scale_end_in_info = (max_scale_end_fig - data_left) / info_ax_width
scale_start_x = max(0.05, scale_end_in_info - scale_end_frac)
for seg_i in range(n_segments):
color = 'black' if seg_i % 2 == 0 else 'white'
@ -666,15 +684,17 @@ def tif_to_png(tif_file, vis_dir, resolution, keep_tif=False, source_info=None,
color='black', linewidth=1, transform=info_ax.transAxes, clip_on=False)
# Location inset map — IGN topographic background with processed zone marker
map_ax = fig.add_axes([0.84, 0.02, 0.14, 0.12])
# Positioned in lower-right corner, above the info bar
map_ax = fig.add_axes([0.82, 0.02, 0.16, 0.13])
# Try to download a wide-area IGN topo map for location context
location_map = _download_location_map(min_x, max_x, min_y, max_y)
if location_map is not None:
# Draw IGN topo map as background
location_result = _download_location_map(min_x, max_x, min_y, max_y)
if location_result is not None:
location_map, loc_bounds = location_result
# Draw IGN topo map as background with correct bounds
map_ax.imshow(location_map, aspect='equal', extent=[
min_x - (max_x - min_x) * 2, max_x + (max_x - min_x) * 2,
min_y - (max_y - min_y) * 2, max_y + (max_y - min_y) * 2
loc_bounds['min_x'], loc_bounds['max_x'],
loc_bounds['min_y'], loc_bounds['max_y']
])
# Mark the processed zone with a red rectangle
rect_x1, rect_x2 = min_x, max_x