diff --git a/.justfile b/.justfile index 38ebe83..3df223b 100644 --- a/.justfile +++ b/.justfile @@ -1,17 +1,20 @@ +remote_root := "ial-gpu-workstation-1:/home/ial-pangyg/docker-workspace/projects/mini-nav" +rsync_flags := "-avLh --progress --stats --itemize-changes" +upload_excludes := "--exclude=.jj/ --exclude=.git/ --exclude=.devenv/ --exclude=.direnv/ --exclude=deps/ --exclude=outputs/ --exclude=data/versioned_data/ --exclude=datasets/" + upload: - rsync -avLh --progress --stats --itemize-changes \ - --exclude='.jj/' \ - --exclude='.git/' \ - --exclude='.devenv/' \ - --exclude='.direnv/' \ - --exclude='deps/' \ - --exclude='outputs/' \ - --exclude='data/versioned_data/' \ - . ial-gpu-workstation-1:/home/ial-pangyg/docker-workspace/projects/mini-nav/ + if command -v cygpath >/dev/null 2>&1 || test -n "${MSYSTEM:-}" || test -n "${CYGWIN:-}"; then \ + env MSYS2_ARG_CONV_EXCL='*' rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \ + else \ + rsync {{ rsync_flags }} {{ upload_excludes }} . {{ remote_root }}/; \ + fi download: - rsync -avLh --progress --stats --itemize-changes \ - ial-gpu-workstation-1:/home/ial-pangyg/docker-workspace/projects/mini-nav/outputs . + if command -v cygpath >/dev/null 2>&1 || test -n "${MSYSTEM:-}" || test -n "${CYGWIN:-}"; then \ + env MSYS2_ARG_CONV_EXCL='*' rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \ + else \ + rsync {{ rsync_flags }} {{ remote_root }}/outputs .; \ + fi sync-pkgs: # micromamba env create -f ./environment.yml diff --git a/mini-nav/simulator/topdown.py b/mini-nav/simulator/topdown.py index 9700ffd..f0a08bb 100644 --- a/mini-nav/simulator/topdown.py +++ b/mini-nav/simulator/topdown.py @@ -1,9 +1,13 @@ from __future__ import annotations from dataclasses import dataclass, field -from importlib import import_module from typing import Any, Sequence +import numpy as np +from matplotlib.backends.backend_agg import FigureCanvasAgg +from matplotlib.figure import Figure +from PIL import Image + @dataclass(frozen=True) class TopDownSceneElements: @@ -28,10 +32,26 @@ def render_topdown_scene_map( elements: TopDownSceneElements, meters_per_pixel: float, style: TopDownRenderStyle | None = None, - maps_module: Any | None = None, - plt_module: Any | None = None, - use_matplotlib: bool = True, -) -> Any: +) -> Image.Image: + """Render a top-down scene map as a PIL Image. + + Uses matplotlib Agg backend (non-interactive) to draw room markers and + labels on the habitat top-down occupancy map, then returns the result + as a PIL Image. + + Args: + pathfinder: Habitat pathfinder instance. + elements: Scene elements containing room nodes to annotate. + meters_per_pixel: Resolution for the top-down map. + style: Visual style configuration. + + Returns: + PIL.Image.Image: Rendered top-down map with annotations. + + Raises: + ValueError: If room_nodes is empty or meters_per_pixel <= 0. + NotImplementedError: If object_nodes or edges are provided. + """ if not elements.room_nodes: raise ValueError("room_nodes must not be empty") @@ -47,37 +67,33 @@ def render_topdown_scene_map( if style is None: style = TopDownRenderStyle() - if maps_module is None: - maps_module = import_module("habitat.utils.visualizations.maps") + # Import habitat maps internally — treated as required dependency. + from habitat.utils.visualizations import maps + # Generate raw top-down occupancy map. map_height = float(elements.room_nodes[0].center[1]) - top_down_map = maps_module.get_topdown_map( + top_down_map = maps.get_topdown_map( pathfinder, height=map_height, meters_per_pixel=meters_per_pixel, ) - if not use_matplotlib: - return top_down_map - - if plt_module is None: - try: - plt_module = import_module("matplotlib.pyplot") - except ImportError: - return top_down_map - - plt_module.figure(figsize=style.figure_size) - plt_module.imshow(top_down_map, cmap=style.map_cmap) + # Render with matplotlib Agg backend (no GUI required). + fig = Figure(figsize=style.figure_size) + _ = FigureCanvasAgg(fig) + ax = fig.add_subplot(111) + ax.imshow(top_down_map, cmap=style.map_cmap) + # Annotate room positions and labels. for room_node in elements.room_nodes: - grid_y, grid_x = maps_module.to_grid( + grid_y, grid_x = maps.to_grid( float(room_node.center[2]), float(room_node.center[0]), top_down_map.shape, pathfinder=pathfinder, ) - plt_module.scatter(grid_x, grid_y, c=style.room_color, s=style.room_marker_size) - plt_module.text( + ax.scatter(grid_x, grid_y, c=style.room_color, s=style.room_marker_size) + ax.text( grid_x + style.room_label_offset, grid_y + style.room_label_offset, room_node.room_id, @@ -85,7 +101,12 @@ def render_topdown_scene_map( fontsize=8, ) - plt_module.title(style.title) - plt_module.axis("off") - plt_module.show() - return top_down_map + ax.set_title(style.title) + ax.axis("off") + fig.canvas.draw() + + # Extract RGB pixels from the Agg canvas buffer. + rgba = np.asarray(fig.canvas.buffer_rgba(), dtype=np.uint8) + rgb = rgba[..., :3].copy() + + return Image.fromarray(rgb) diff --git a/notebooks/verification.py b/notebooks/verification.py index 14b3bec..2b2e81d 100644 --- a/notebooks/verification.py +++ b/notebooks/verification.py @@ -107,21 +107,18 @@ def habitat_setup(HabitatSimulatorConfig, RoomNode, create_habitat_simulator, np def render_topdown( TopDownSceneElements, meters_per_pixel, + mo, render_topdown_scene_map, room_nodes, sim, ): - from habitat.utils.visualizations import maps - from matplotlib import pyplot as plt - - render_topdown_scene_map( + image = render_topdown_scene_map( pathfinder=sim.pathfinder, elements=TopDownSceneElements(room_nodes=room_nodes), meters_per_pixel=meters_per_pixel, - maps_module=maps, - plt_module=plt, ) - return + mo.image(image) + return (image,) @app.cell