diff --git a/habitat/config/default.py b/habitat/config/default.py index aaad2860ccaa43d019c2c29c5c32109261f059dd..a4e191734720f50ccc2ca550baa19038985f51e9 100644 --- a/habitat/config/default.py +++ b/habitat/config/default.py @@ -54,6 +54,17 @@ _C.TASK.SPL = CN() _C.TASK.SPL.TYPE = "SPL" _C.TASK.SPL.SUCCESS_DISTANCE = 0.2 # ----------------------------------------------------------------------------- +# # TopDownMap MEASUREMENT +# ----------------------------------------------------------------------------- +_C.TASK.TOP_DOWN_MAP = CN() +_C.TASK.TOP_DOWN_MAP.TYPE = "TopDownMap" +_C.TASK.TOP_DOWN_MAP.MAX_EPISODE_STEPS = _C.ENVIRONMENT.MAX_EPISODE_STEPS +_C.TASK.TOP_DOWN_MAP.MAP_PADDING = 3 +_C.TASK.TOP_DOWN_MAP.NUM_TOPDOWN_MAP_SAMPLE_POINTS = 20000 +_C.TASK.TOP_DOWN_MAP.MAP_RESOLUTION = 1250 +_C.TASK.TOP_DOWN_MAP.DRAW_SOURCE_AND_TARGET = True +_C.TASK.TOP_DOWN_MAP.DRAW_BORDER = True +# ----------------------------------------------------------------------------- # # COLLISIONS MEASUREMENT # ----------------------------------------------------------------------------- _C.TASK.COLLISIONS = CN() diff --git a/habitat/tasks/nav/nav_task.py b/habitat/tasks/nav/nav_task.py index f99c7eb20d2dcb08029fe30337a7331c8599e2f0..2498939ffdfe83a6211bc663a342aa0b503dd554 100644 --- a/habitat/tasks/nav/nav_task.py +++ b/habitat/tasks/nav/nav_task.py @@ -6,9 +6,11 @@ from typing import Any, List, Optional, Type -import habitat +import cv2 import numpy as np from gym import spaces + +import habitat from habitat.config import Config from habitat.core.dataset import Episode, Dataset from habitat.core.embodied_task import Measurements @@ -23,8 +25,10 @@ from habitat.tasks.utils import ( cartesian_to_polar, quaternion_rotate_vector, ) +from habitat.utils.visualizations import maps COLLISION_PROXIMITY_TOLERANCE: float = 1e-3 +MAP_THICKNESS_SCALAR: int = 1250 def merge_sim_episode_config( @@ -204,6 +208,78 @@ class PointGoalSensor(habitat.Sensor): return direction_vector_agent +class StaticPointGoalSensor(habitat.Sensor): + """ + Sensor for PointGoal observations which are used in the StaticPointNav + task. For the agent in simulator the forward direction is along negative-z. + In polar coordinate format the angle returned is azimuth to the goal. + Args: + sim: reference to the simulator for calculating task observations. + config: config for the PointGoal sensor. Can contain field for + GOAL_FORMAT which can be used to specify the format in which + the pointgoal is specified. Current options for goal format are + cartesian and polar. + Attributes: + _goal_format: format for specifying the goal which can be done + in cartesian or polar coordinates. + """ + + def __init__(self, sim: Simulator, config: Config): + self._sim = sim + self._goal_format = getattr(config, "GOAL_FORMAT", "CARTESIAN") + assert self._goal_format in ["CARTESIAN", "POLAR"] + + super().__init__(sim, config) + self._initial_vector = None + self.current_episode_id = None + + def _get_uuid(self, *args: Any, **kwargs: Any): + return "static_pointgoal" + + def _get_sensor_type(self, *args: Any, **kwargs: Any): + return SensorTypes.STATIC_GOAL_VECTOR + + def _get_observation_space(self, *args: Any, **kwargs: Any): + if self._goal_format == "CARTESIAN": + sensor_shape = (3,) + else: + sensor_shape = (2,) + return spaces.Box( + low=np.finfo(np.float32).min, + high=np.finfo(np.float32).max, + shape=sensor_shape, + dtype=np.float32, + ) + + def get_observation(self, observations, episode): + episode_id = (episode.episode_id, episode.scene_id) + if self.current_episode_id != episode_id: + # Only compute the direction vector when a new episode is started. + self.current_episode_id = episode_id + agent_state = self._sim.get_agent_state() + ref_position = agent_state.position + rotation_world_agent = agent_state.rotation + + direction_vector = ( + np.array(episode.goals[0].position, dtype=np.float32) + - ref_position + ) + direction_vector_agent = quaternion_rotate_vector( + rotation_world_agent.inverse(), direction_vector + ) + + if self._goal_format == "POLAR": + rho, phi = cartesian_to_polar( + -direction_vector_agent[2], direction_vector_agent[0] + ) + direction_vector_agent = np.array( + [rho, -phi], dtype=np.float32 + ) + + self._initial_vector = direction_vector_agent + return self._initial_vector + + class HeadingSensor(habitat.Sensor): """ Sensor for observing the agent's heading in the global coordinate frame. @@ -341,7 +417,6 @@ class Collisions(habitat.Measure): self._sim = sim self._config = config self._metric = None - super().__init__() def _get_uuid(self, *args: Any, **kwargs: Any): @@ -363,6 +438,149 @@ class Collisions(habitat.Measure): self._metric += 1 +class TopDownMap(habitat.Measure): + """Top Down Map measure + """ + + def __init__(self, sim: Simulator, config: Config): + self._sim = sim + self._config = config + self._grid_delta = config.MAP_PADDING + self._step_count = None + self._map_resolution = (config.MAP_RESOLUTION, config.MAP_RESOLUTION) + self._num_samples = config.NUM_TOPDOWN_MAP_SAMPLE_POINTS + self._ind_x_min = None + self._ind_x_max = None + self._ind_y_min = None + self._ind_y_max = None + self._previous_xy_location = None + self._coordinate_min = maps.COORDINATE_MIN + self._coordinate_max = maps.COORDINATE_MAX + self._top_down_map = None + self._cell_scale = ( + self._coordinate_max - self._coordinate_min + ) / self._map_resolution[0] + super().__init__() + + def _get_uuid(self, *args: Any, **kwargs: Any): + return "top_down_map" + + def _check_valid_nav_point(self, point: List[float]): + self._sim.is_navigable(point) + + def get_original_map(self, episode): + top_down_map = maps.get_topdown_map( + self._sim, + self._map_resolution, + self._num_samples, + self._config.DRAW_BORDER, + ) + + range_x = np.where(np.any(top_down_map, axis=1))[0] + range_y = np.where(np.any(top_down_map, axis=0))[0] + + self._ind_x_min = range_x[0] + self._ind_x_max = range_x[-1] + self._ind_y_min = range_y[0] + self._ind_y_max = range_y[-1] + + if self._config.DRAW_SOURCE_AND_TARGET: + # mark source point + s_x, s_y = maps.to_grid( + episode.start_position[0], + episode.start_position[2], + self._coordinate_min, + self._coordinate_max, + self._map_resolution, + ) + point_padding = 2 * int( + np.ceil(self._map_resolution[0] / MAP_THICKNESS_SCALAR) + ) + top_down_map[ + s_x - point_padding : s_x + point_padding + 1, + s_y - point_padding : s_y + point_padding + 1, + ] = maps.MAP_SOURCE_POINT_INDICATOR + + # mark target point + t_x, t_y = maps.to_grid( + episode.goals[0].position[0], + episode.goals[0].position[2], + self._coordinate_min, + self._coordinate_max, + self._map_resolution, + ) + top_down_map[ + t_x - point_padding : t_x + point_padding + 1, + t_y - point_padding : t_y + point_padding + 1, + ] = maps.MAP_TARGET_POINT_INDICATOR + + return top_down_map + + def reset_metric(self, episode): + self._step_count = 0 + self._metric = None + self._top_down_map = self.get_original_map(episode) + agent_position = self._sim.get_agent_state().position + a_x, a_y = maps.to_grid( + agent_position[0], + agent_position[2], + self._coordinate_min, + self._coordinate_max, + self._map_resolution, + ) + self._previous_xy_location = (a_y, a_x) + + def update_metric(self, episode, action): + self._step_count += 1 + house_map, map_agent_x, map_agent_y = self.update_map( + self._sim.get_agent_state().position + ) + + # Rather than return the whole map which may have large empty regions, + # only return the occupied part (plus some padding). + house_map = house_map[ + self._ind_x_min + - self._grid_delta : self._ind_x_max + + self._grid_delta, + self._ind_y_min + - self._grid_delta : self._ind_y_max + + self._grid_delta, + ] + + self._metric = house_map + + def update_map(self, agent_position): + a_x, a_y = maps.to_grid( + agent_position[0], + agent_position[2], + self._coordinate_min, + self._coordinate_max, + self._map_resolution, + ) + # Don't draw over the source point + color = ( + min(self._step_count * 245 / self._config.MAX_EPISODE_STEPS, 245) + + 10 + if self._top_down_map[a_x, a_y] != maps.MAP_SOURCE_POINT_INDICATOR + else maps.MAP_SOURCE_POINT_INDICATOR + ) + color = int(color) + + thickness = int( + np.round(self._map_resolution[0] * 2 / MAP_THICKNESS_SCALAR) + ) + cv2.line( + self._top_down_map, + self._previous_xy_location, + (a_y, a_x), + color, + thickness=thickness, + ) + + self._previous_xy_location = (a_y, a_x) + return self._top_down_map, a_x, a_y + + class NavigationTask(habitat.EmbodiedTask): def __init__( self, diff --git a/habitat/utils/visualizations/maps.py b/habitat/utils/visualizations/maps.py index 1790f06f819cb5dc84db97cd27cd5f4544a36f62..9dca6e2487746ae40038e148ea08bb9a330fb810 100644 --- a/habitat/utils/visualizations/maps.py +++ b/habitat/utils/visualizations/maps.py @@ -26,6 +26,12 @@ COORDINATE_EPSILON = 1e-6 COORDINATE_MIN = -62.3241 - COORDINATE_EPSILON COORDINATE_MAX = 90.0399 + COORDINATE_EPSILON +MAP_INVALID_POINT = 0 +MAP_VALID_POINT = 1 +MAP_BORDER_INDICATOR = 2 +MAP_SOURCE_POINT_INDICATOR = 4 +MAP_TARGET_POINT_INDICATOR = 6 + def draw_agent( image: np.ndarray, @@ -158,7 +164,7 @@ def pointnav_draw_target_birdseye_view( return im_position -def _to_grid( +def to_grid( realworld_x: float, realworld_y: float, coordinate_min: float, @@ -179,7 +185,7 @@ def _to_grid( return grid_x, grid_y -def _from_grid( +def from_grid( grid_x: int, grid_y: int, coordinate_min: float, @@ -215,11 +221,11 @@ def _outline_border(top_down_map): top_down_map[:-1] != top_down_map[1:] ) - top_down_map[:, :-1][left_right_block_nav] = 2 - top_down_map[:, 1:][left_right_nav_block] = 2 + top_down_map[:, :-1][left_right_block_nav] = MAP_BORDER_INDICATOR + top_down_map[:, 1:][left_right_nav_block] = MAP_BORDER_INDICATOR - top_down_map[:-1][up_down_block_nav] = 2 - top_down_map[1:][up_down_nav_block] = 2 + top_down_map[:-1][up_down_block_nav] = MAP_BORDER_INDICATOR + top_down_map[1:][up_down_nav_block] = MAP_BORDER_INDICATOR def get_topdown_map( @@ -245,24 +251,24 @@ def get_topdown_map( the flag is set). """ top_down_map = np.zeros(map_resolution, dtype=np.uint8) - grid_delta = 3 + border_padding = 3 start_height = sim.get_agent_state().position[1] # Use sampling to find the extrema points that might be navigable. + range_x = (map_resolution[0], 0) + range_y = (map_resolution[1], 0) for _ in range(num_samples): point = sim.sample_navigable_point() # Check if on same level as original if np.abs(start_height - point[1]) > 0.5: continue - g_x, g_y = _to_grid( + g_x, g_y = to_grid( point[0], point[2], COORDINATE_MIN, COORDINATE_MAX, map_resolution ) + range_x = (min(range_x[0], g_x), max(range_x[1], g_x)) + range_y = (min(range_y[0], g_y), max(range_y[1], g_y)) - top_down_map[g_x, g_y] = 1 - - range_x = np.where(np.any(top_down_map, axis=1))[0] - range_y = np.where(np.any(top_down_map, axis=0))[0] # Pad the range just in case not enough points were sampled to get the true # extrema. padding = int(np.ceil(map_resolution[0] / 125)) @@ -274,18 +280,19 @@ def get_topdown_map( max(range_y[0] - padding, 0), min(range_y[-1] + padding + 1, top_down_map.shape[1]), ) - top_down_map[:] = 0 # Search over grid for valid points. for ii in range(range_x[0], range_x[1]): for jj in range(range_y[0], range_y[1]): - realworld_x, realworld_y = _from_grid( + realworld_x, realworld_y = from_grid( ii, jj, COORDINATE_MIN, COORDINATE_MAX, map_resolution ) valid_point = sim.is_navigable( [realworld_x, start_height, realworld_y] ) - top_down_map[ii, jj] = 1 if valid_point else 0 + top_down_map[ii, jj] = ( + MAP_VALID_POINT if valid_point else MAP_INVALID_POINT + ) # Draw border if necessary if draw_border: @@ -293,12 +300,12 @@ def get_topdown_map( range_x = np.where(np.any(top_down_map, axis=1))[0] range_y = np.where(np.any(top_down_map, axis=0))[0] range_x = ( - max(range_x[0] - grid_delta, 0), - min(range_x[-1] + grid_delta + 1, top_down_map.shape[0]), + max(range_x[0] - border_padding, 0), + min(range_x[-1] + border_padding + 1, top_down_map.shape[0]), ) range_y = ( - max(range_y[0] - grid_delta, 0), - min(range_y[-1] + grid_delta + 1, top_down_map.shape[1]), + max(range_y[0] - border_padding, 0), + min(range_y[-1] + border_padding + 1, top_down_map.shape[1]), ) _outline_border( diff --git a/requirements.txt b/requirements.txt index 462ce1f911be94cf5688b968ecd1b93842dafc91..404248b3c68f2f8a0f9c6f73013371140eb839bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,9 +2,9 @@ gym==0.10.9 numpy>=1.16.1 yacs>=0.1.5 numpy-quaternion>=2019.3.18.14.33.20 +opencv-python>=3.3.0 # visualization optional dependencies imageio>=2.2.0 imageio-ffmpeg>=0.2.0 -opencv-python>=3.3.0 scipy>=1.0.0 tqdm>=4.0.0