From 9a9927cc9ae852bb3bb0376f7ed6f9ece9def470 Mon Sep 17 00:00:00 2001 From: Dmytro Mishkin <ducha.aiki@gmail.com> Date: Thu, 11 Apr 2019 08:13:25 +0200 Subject: [PATCH] Slambased agents (#39) * ORBSLAM2 agent refactored for new api * Added monodepth agent --- baselines/agents/slam_agents.py | 623 ++++++++++++++++++ baselines/config/default.py | 27 +- baselines/slambased/README.md | 39 ++ baselines/slambased/data/mp3d3_small1k.yaml | 69 ++ baselines/slambased/data/slam-based-agent.png | Bin 0 -> 72060 bytes baselines/slambased/install_deps.sh | 62 ++ baselines/slambased/mappers.py | 122 ++++ baselines/slambased/monodepth.py | 573 ++++++++++++++++ baselines/slambased/path_planners.py | 512 ++++++++++++++ baselines/slambased/reprojection.py | 289 ++++++++ baselines/slambased/utils.py | 43 ++ configs/tasks/pointnav_rgbd.yaml | 24 + 12 files changed, 2381 insertions(+), 2 deletions(-) create mode 100644 baselines/agents/slam_agents.py create mode 100644 baselines/slambased/README.md create mode 100644 baselines/slambased/data/mp3d3_small1k.yaml create mode 100644 baselines/slambased/data/slam-based-agent.png create mode 100755 baselines/slambased/install_deps.sh create mode 100644 baselines/slambased/mappers.py create mode 100644 baselines/slambased/monodepth.py create mode 100644 baselines/slambased/path_planners.py create mode 100644 baselines/slambased/reprojection.py create mode 100644 baselines/slambased/utils.py create mode 100644 configs/tasks/pointnav_rgbd.yaml diff --git a/baselines/agents/slam_agents.py b/baselines/agents/slam_agents.py new file mode 100644 index 000000000..741718639 --- /dev/null +++ b/baselines/agents/slam_agents.py @@ -0,0 +1,623 @@ +import argparse +import numpy as np +import torch +import random +import time +import os +import PIL +from math import pi +import torch.nn.functional as F +import orbslam2 +import habitat +from baselines.slambased.utils import ( + generate_2dgrid, +) +from baselines.slambased.reprojection import ( + homogenize_p, + get_distance, + project_tps_into_worldmap, + get_direction, + habitat_goalpos_to_mapgoal_pos, + planned_path2tps, + angle_to_pi_2_minus_pi_2 +) +from baselines.slambased.reprojection import ( + angle_to_pi_2_minus_pi_2 as norm_ang +) +from habitat.sims.habitat_simulator import ( + SimulatorActions, + SIM_ACTION_TO_NAME, + SIM_NAME_TO_ACTION, +) +from baselines.slambased.mappers import DirectDepthMapper +from baselines.slambased.path_planners import DifferentiableStarPlanner + +from baselines.config.default import cfg +from habitat.config.default import get_config + +from baselines.slambased.monodepth import MonoDepthEstimator + +# https://sumit-ghosh.com/articles/python-download-progress-bar/ +import sys +import requests +def download(url, filename): + with open(filename, 'wb') as f: + response = requests.get(url, stream=True) + total = response.headers.get('content-length') + if total is None: + f.write(response.content) + else: + downloaded = 0 + total = int(total) + for data in response.iter_content(chunk_size=max(int(total/1000), 1024*1024)): + downloaded += len(data) + f.write(data) + done = int(50*downloaded/total) + sys.stdout.write('\r[{}{}]'.format('â–ˆ' * done, '.' * (50-done))) + sys.stdout.flush() + sys.stdout.write('\n') + + +def ResizePIL2(np_img, size = 256): + im1 = PIL.Image.fromarray(np_img) + return np.array(im1.resize((size,size))) + +def make_good_config_for_orbslam2(config): + config.SIMULATOR.AGENT_0.SENSORS = ["RGB_SENSOR", "DEPTH_SENSOR"] + config.SIMULATOR.RGB_SENSOR.WIDTH = 256 + config.SIMULATOR.RGB_SENSOR.HEIGHT = 256 + config.SIMULATOR.DEPTH_SENSOR.WIDTH = 256 + config.SIMULATOR.DEPTH_SENSOR.HEIGHT = 256 + config.BASELINE.ORBSLAM2.CAMERA_HEIGHT = config.SIMULATOR.DEPTH_SENSOR.POSITION[1] + config.BASELINE.ORBSLAM2.H_OBSTACLE_MIN = 0.3 * config.BASELINE.ORBSLAM2.CAMERA_HEIGHT + config.BASELINE.ORBSLAM2.H_OBSTACLE_MAX = 1.0 * config.BASELINE.ORBSLAM2.CAMERA_HEIGHT + config.BASELINE.ORBSLAM2.MIN_PTS_IN_OBSTACLE = config.SIMULATOR.DEPTH_SENSOR.WIDTH/2.0 + return + +class RandomAgent(object): + r"""Simplest agent, which returns random actions, + until reach the goal + """ + + def __init__( + self, + config + ): + super(RandomAgent, self).__init__() + self.num_actions = config.NUM_ACTIONS + self.dist_threshold_to_stop = config.DIST_TO_STOP + self.reset() + return + + def reset(self): + self.steps = 0 + return + + def update_internal_state(self, habitat_observation): + self.obs = habitat_observation + self.steps += 1 + return + + def is_goal_reached(self): + dist = self.obs["pointgoal"][0] + return dist <= self.dist_threshold_to_stop + + def act(self, habitat_observation=None, random_prob=1.0): + self.update_internal_state(habitat_observation) + # Act + # Check if we are done + if self.is_goal_reached(): + action = SIM_NAME_TO_ACTION[SimulatorActions.STOP.value] + else: + action = random.randint(0, self.num_actions - 1) + return action + + +class BlindAgent(RandomAgent): + def __init__( + self, + config + ): + super(BlindAgent, self).__init__() + self.pos_th = config.DIST_TO_STOP + self.angle_th = config.ANGLE_TH + self.reset() + return + + def decide_what_to_do(self): + distance_to_goal = self.obs["pointgoal"][0] + angle_to_goal = norm_ang(np.array(self.obs["pointgoal"][1])) + command = SIM_NAME_TO_ACTION[SimulatorActions.STOP.value] + if distance_to_goal <= self.pos_th: + return command + if abs(angle_to_goal) < self.angle_th: + command = "move_forward" + else: + if (angle_to_goal > 0) and (angle_to_goal < pi): + command = "look_left" + elif angle_to_goal > pi: + command = "look_right" + elif (angle_to_goal < 0) and (angle_to_goal > -pi): + command = "look_right" + else: + command = "look_left" + return command + + def act(self, habitat_observation=None, random_prob=0.1): + self.update_internal_state(habitat_observation) + # Act + if self.is_goal_reached(): + return SIM_NAME_TO_ACTION[SimulatorActions.STOP.value] + command = self.decide_what_to_do() + random_action = random.randint(0, self.num_actions - 1) + act_randomly = np.random.uniform(0, 1, 1) < random_prob + if act_randomly: + action = random_action + else: + action = SIM_NAME_TO_ACTION[command] + return action + + +class ORBSLAM2Agent(RandomAgent): + def __init__( + self, + config, + device=torch.device("cuda:0"), + ): + self.num_actions = config.NUM_ACTIONS + self.dist_threshold_to_stop = config.DIST_TO_STOP + self.slam_vocab_path = config.SLAM_VOCAB_PATH + assert os.path.isfile(self.slam_vocab_path) + self.slam_settings_path = config.SLAM_SETTINGS_PATH + assert os.path.isfile(self.slam_settings_path) + self.slam = orbslam2.System( + self.slam_vocab_path, self.slam_settings_path, orbslam2.Sensor.RGBD + ) + self.slam.set_use_viewer(False) + self.slam.initialize() + self.device = device + self.map_size_meters = config.MAP_SIZE + self.map_cell_size = config.MAP_CELL_SIZE + self.pos_th = config.DIST_REACHED_TH + self.next_wp_th = config.NEXT_WAYPOINT_TH + self.angle_th = config.ANGLE_TH + self.obstacle_th = config.MIN_PTS_IN_OBSTACLE + self.depth_denorm = config.DEPTH_DENORM + self.planned_waypoints = [] + self.mapper = DirectDepthMapper( + camera_height = config.CAMERA_HEIGHT, + near_th = config.D_OBSTACLE_MIN, + far_th = config.D_OBSTACLE_MAX, + h_min = config.H_OBSTACLE_MIN, + h_max = config.H_OBSTACLE_MAX, + map_size = config.MAP_SIZE, + map_cell_size = config.MAP_CELL_SIZE, + device = device) + self.planner = DifferentiableStarPlanner( + max_steps = config.PLANNER_MAX_STEPS, + preprocess = config.PREPROCESS_MAP, + beta = config.BETA, + device = device) + self.slam_to_world = 1.0 + self.timestep = 0.1 + self.timing = False + self.reset() + return + + def reset(self): + super(ORBSLAM2Agent, self).reset() + self.offset_to_goal = None + self.tracking_is_OK = False + self.waypointPose6D = None + self.unseen_obstacle = False + self.action_history = [] + self.planned_waypoints = [] + self.map2DObstacles = self.init_map2d() + n, ch, height, width = self.map2DObstacles.size() + self.coordinatesGrid = generate_2dgrid(height, width, False).to( + self.device + ) + self.pose6D = self.init_pose6d() + self.action_history = [] + self.pose6D_history = [] + self.position_history = [] + self.planned2Dpath = torch.zeros((0)) + self.slam.reset() + self.cur_time = 0 + self.toDoList = [] + self.waypoint_id = 0 + if self.device != torch.device("cpu"): + torch.cuda.empty_cache() + return + + def update_internal_state(self, habitat_observation): + super(ORBSLAM2Agent, self).update_internal_state( + habitat_observation + ) + self.cur_time += self.timestep + rgb, depth = self.rgb_d_from_observation(habitat_observation) + t = time.time() + try: + self.slam.process_image_rgbd(rgb, depth, self.cur_time) + if self.timing: + print(time.time() - t, "ORB_SLAM2") + self.tracking_is_OK = str(self.slam.get_tracking_state()) == "OK" + except BaseException: + print("Warning!!!! ORBSLAM processing frame error") + self.tracking_is_OK = False + if not self.tracking_is_OK: + self.reset() + t = time.time() + self.set_offset_to_goal(habitat_observation) + if self.tracking_is_OK: + trajectory_history = np.array(self.slam.get_trajectory_points()) + self.pose6D = homogenize_p( + torch.from_numpy(trajectory_history[-1])[1:] + .view(3, 4) + .to(self.device) + ).view(1, 4, 4) + self.trajectory_history = trajectory_history + if len(self.position_history) > 1: + previous_step = get_distance( + self.pose6D.view(4, 4), + torch.from_numpy(self.position_history[-1]) + .view(4, 4) + .to(self.device), + ) + if ( + SIM_ACTION_TO_NAME[self.action_history[-1]] + == "move_forward" + ): + self.unseen_obstacle = ( + previous_step.item() <= 0.001 + ) # hardcoded threshold for not moving + current_obstacles = self.mapper( + torch.from_numpy(depth).to(self.device).squeeze(), self.pose6D + ).to(self.device) + self.current_obstacles = current_obstacles + self.map2DObstacles = torch.max( + self.map2DObstacles, current_obstacles.unsqueeze(0).unsqueeze(0) + ) + if self.timing: + print(time.time() - t, "Mapping") + return True + + def init_pose6d(self): + return torch.eye(4).float().to(self.device) + + def map_size_in_cells(self): + return int(self.map_size_meters / self.map_cell_size) + + def init_map2d(self): + return ( + torch.zeros(1, + 1, + self.map_size_in_cells(), + self.map_size_in_cells()) + .float() + .to(self.device) + ) + + def get_orientation_on_map(self): + self.pose6D = self.pose6D.view(1, 4, 4) + return torch.tensor( + [ + [self.pose6D[0, 0, 0], self.pose6D[0, 0, 2]], + [self.pose6D[0, 2, 0], self.pose6D[0, 2, 2]], + ] + ) + + def get_position_on_map(self, do_floor=True): + return project_tps_into_worldmap( + self.pose6D.view(1, 4, 4), + self.map_cell_size, + self.map_size_meters, + do_floor, + ) + + def act(self, habitat_observation, random_prob=0.1): + # Update internal state + t = time.time() + cc = 0 + update_is_ok = self.update_internal_state(habitat_observation) + while not update_is_ok: + update_is_ok = self.update_internal_state(habitat_observation) + cc += 1 + if cc > 2: + break + if self.timing: + print(time.time() - t, " s, update internal state") + self.position_history.append( + self.pose6D.detach().cpu().numpy().reshape(1, 4, 4) + ) + success = self.is_goal_reached() + if success: + action = SIM_NAME_TO_ACTION[SimulatorActions.STOP.value] + self.action_history.append(action) + return action + # Plan action + t = time.time() + self.planned2Dpath, self.planned_waypoints = self.plan_path() + if self.timing: + print(time.time() - t, " s, Planning") + t = time.time() + # Act + if self.waypointPose6D is None: + self.waypointPose6D = self.get_valid_waypoint_pose6d() + if ( + self.is_waypoint_reached(self.waypointPose6D) + or not self.tracking_is_OK + ): + self.waypointPose6D = self.get_valid_waypoint_pose6d() + self.waypoint_id += 1 + action = self.decide_what_to_do() + # May be random? + random_action = random.randint(0, self.num_actions - 1) + what_to_do = np.random.uniform(0, 1, 1) + if what_to_do < random_prob: + action = random_action + if self.timing: + print(time.time() - t, " s, get action") + self.action_history.append(action) + return action + + def is_waypoint_good(self, pose6d): + p_init = self.pose6D.squeeze() + dist_diff = get_distance(p_init, pose6d) + valid = dist_diff > self.next_wp_th + return valid.item() + + def is_waypoint_reached(self, pose6d): + p_init = self.pose6D.squeeze() + dist_diff = get_distance(p_init, pose6d) + reached = dist_diff <= self.pos_th + return reached.item() + + def get_waypoint_dist_dir(self): + angle = get_direction( + self.pose6D.squeeze(), self.waypointPose6D.squeeze(), 0, 0 + ) + dist = get_distance( + self.pose6D.squeeze(), self.waypointPose6D.squeeze() + ) + return torch.cat( + [ + dist.view(1, 1), + torch.sin(angle).view(1, 1), + torch.cos(angle).view(1, 1), + ], + dim=1, + ) + + def get_valid_waypoint_pose6d(self): + p_next = self.planned_waypoints[0] + while not self.is_waypoint_good(p_next): + if len(self.planned_waypoints) > 1: + self.planned_waypoints = self.planned_waypoints[1:] + p_next = self.planned_waypoints[0] + else: + p_next = self.estimatedGoalPos6D.squeeze() + break + return p_next + + def set_offset_to_goal(self, observation): + self.offset_to_goal = ( + torch.from_numpy(observation["pointgoal"]).float().to(self.device) + ) + self.estimatedGoalPos2D = habitat_goalpos_to_mapgoal_pos( + self.offset_to_goal, + self.pose6D.squeeze(), + self.map_cell_size, + self.map_size_meters, + ) + self.estimatedGoalPos6D = planned_path2tps( + [self.estimatedGoalPos2D], + self.map_cell_size, + self.map_size_meters, + 1.0, + ).to(self.device)[0] + return + + def rgb_d_from_observation(self, habitat_observation): + rgb = habitat_observation["rgb"] + depth = None + if "depth" in habitat_observation: + depth = ( + self.depth_denorm * habitat_observation["depth"] + ) + return rgb, depth + + def prev_plan_is_not_valid(self): + if len(self.planned2Dpath) == 0: + return True + pp = torch.cat(self.planned2Dpath).detach().cpu().view(-1, 2) + binary_map = self.map2DObstacles.squeeze().detach() >= self.obstacle_th + obstacles_on_path = ( + binary_map[pp[:, 0].long(), pp[:, 1].long()] + ).long().sum().item() > 0 + return obstacles_on_path # obstacles_nearby or obstacles_on_path + + def rawmap2_planner_ready(self, rawmap, start_map, goal_map): + map1 = (rawmap / float(self.obstacle_th)) ** 2 + map1 = ( + torch.clamp(map1, min=0, max=1.0) + - start_map + - F.max_pool2d(goal_map, 3, stride=1, padding=1) + ) + return torch.relu(map1) + + def plan_path(self, overwrite=False): + t = time.time() + if ( + (not self.prev_plan_is_not_valid()) + and (not overwrite) + and (len(self.planned_waypoints) > 0) + ): + return self.planned2Dpath, self.planned_waypoints + self.waypointPose6D = None + current_pos = self.get_position_on_map() + start_map = torch.zeros_like(self.map2DObstacles).to(self.device) + start_map[ + 0, 0, current_pos[0, 0].long(), current_pos[0, 1].long() + ] = 1.0 + goal_map = torch.zeros_like(self.map2DObstacles).to(self.device) + goal_map[ + 0, + 0, + self.estimatedGoalPos2D[0, 0].long(), + self.estimatedGoalPos2D[0, 1].long(), + ] = 1.0 + path, cost = self.planner( + self.rawmap2_planner_ready( + self.map2DObstacles, start_map, goal_map + ).to(self.device), + self.coordinatesGrid.to(self.device), + goal_map.to(self.device), + start_map.to(self.device), + ) + if len(path) == 0: + return path, [] + if self.timing: + print(time.time() - t, " s, Planning") + t = time.time() + planned_waypoints = planned_path2tps( + path, self.map_cell_size, self.map_size_meters, 1.0, False + ).to(self.device) + return path, planned_waypoints + + def planner_prediction_to_command(self, p_next): + command = "stop" + p_init = self.pose6D.squeeze() + d_angle_rot_th = self.angle_th + pos_th = self.pos_th + if get_distance(p_init, p_next) <= pos_th: + return command + d_angle = angle_to_pi_2_minus_pi_2( + get_direction(p_init, p_next, ang_th=d_angle_rot_th, pos_th=pos_th) + ) + if abs(d_angle) < d_angle_rot_th: + command = "move_forward" + else: + if (d_angle > 0) and (d_angle < pi): + command = "look_left" + elif d_angle > pi: + command = "look_right" + elif (d_angle < 0) and (d_angle > -pi): + command = "look_right" + else: + command = "look_left" + return command + + def decide_what_to_do(self): + action = None + if self.is_goal_reached(): + action = SIM_NAME_TO_ACTION[SimulatorActions.STOP.value] + return action + if self.unseen_obstacle: + command = "look_right" + return SIM_NAME_TO_ACTION[command] + command = "stop" + command = self.planner_prediction_to_command(self.waypointPose6D) + return SIM_NAME_TO_ACTION[command] + +class ORBSLAM2MonodepthAgent(ORBSLAM2Agent): + def __init__( + self, + config, + device=torch.device("cuda:0"), + monocheckpoint = 'baselines/slambased/data/mp3d_resnet50.pth', + ): + self.num_actions = config.NUM_ACTIONS + self.dist_threshold_to_stop = config.DIST_TO_STOP + self.slam_vocab_path = config.SLAM_VOCAB_PATH + assert os.path.isfile(self.slam_vocab_path) + self.slam_settings_path = config.SLAM_SETTINGS_PATH + assert os.path.isfile(self.slam_settings_path) + self.slam = orbslam2.System( + self.slam_vocab_path, self.slam_settings_path, orbslam2.Sensor.RGBD + ) + self.slam.set_use_viewer(False) + self.slam.initialize() + self.device = device + self.map_size_meters = config.MAP_SIZE + self.map_cell_size = config.MAP_CELL_SIZE + self.pos_th = config.DIST_REACHED_TH + self.next_wp_th = config.NEXT_WAYPOINT_TH + self.angle_th = config.ANGLE_TH + self.obstacle_th = config.MIN_PTS_IN_OBSTACLE + self.depth_denorm = config.DEPTH_DENORM + self.planned_waypoints = [] + self.mapper = DirectDepthMapper( + camera_height = config.CAMERA_HEIGHT, + near_th = config.D_OBSTACLE_MIN, + far_th = config.D_OBSTACLE_MAX, + h_min = config.H_OBSTACLE_MIN, + h_max = config.H_OBSTACLE_MAX, + map_size = config.MAP_SIZE, + map_cell_size = config.MAP_CELL_SIZE, + device = device) + self.planner = DifferentiableStarPlanner( + max_steps = config.PLANNER_MAX_STEPS, + preprocess = config.PREPROCESS_MAP, + beta = config.BETA, + device = device) + self.slam_to_world = 1.0 + self.timestep = 0.1 + self.timing = False + self.checkpoint = monocheckpoint + if not os.path.isfile(self.checkpoint): + mp3d_url = 'http://cmp.felk.cvut.cz/~mishkdmy/navigation/mp3d_ft_monodepth_resnet50.pth' + suncg_me_url = 'http://cmp.felk.cvut.cz/~mishkdmy/navigation/suncg_me_resnet.pth' + suncg_mf_url = 'http://cmp.felk.cvut.cz/~mishkdmy/navigation/suncg_mf_resnet.pth' + url = mp3d_url + print ("No monodepth checkpoint found. Downloading...", url) + download(url, self.checkpoint) + self.monodepth = MonoDepthEstimator(self.checkpoint) + self.reset() + return + + def rgb_d_from_observation(self, habitat_observation): + rgb = habitat_observation["rgb"] + depth = ResizePIL2(self.monodepth.compute_depth(PIL.Image.fromarray(rgb).resize((320,320))), 256)#/1.75 + depth[depth > 3.0] = 0 + depth[depth < 0.1] = 0 + return rgb, np.array(depth).astype(np.float32) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--agent-type", + default="orbslam2-rgbd", + choices=["blind", + "orbslam2-rgbd", + "orbslam2-rgb-monod"], + ) + parser.add_argument( + "--task-config", type=str, default="tasks/pointnav_rgbd.yaml" + ) + args = parser.parse_args() + + config = get_config() + agent_config = cfg() + config.defrost() + config.BASELINE = agent_config.BASELINE + make_good_config_for_orbslam2(config) + + if args.agent_type == 'blind': + agent = BlindAgent(config.BASELINE.ORBSLAM2) + elif args.agent_type == 'orbslam2-rgbd': + agent = ORBSLAM2Agent(config.BASELINE.ORBSLAM2) + elif args.agent_type == 'orbslam2-rgb-monod': + agent = ORBSLAM2MonodepthAgent(config.BASELINE.ORBSLAM2) + else: + raise ValueError(args.agent_type, 'is unknown type of agent') + benchmark = habitat.Benchmark(args.task_config) + metrics = benchmark.evaluate(agent) + for k, v in metrics.items(): + habitat.logger.info("{}: {:.3f}".format(k, v)) + + +if __name__ == "__main__": + main() + diff --git a/baselines/config/default.py b/baselines/config/default.py index fe44c9396..d100fd79b 100644 --- a/baselines/config/default.py +++ b/baselines/config/default.py @@ -5,8 +5,9 @@ # LICENSE file in the root directory of this source tree. import os +import numpy as np from typing import Optional - +from habitat import get_config from habitat.config import Config as CN DEFAULT_CONFIG_DIR = "configs/" @@ -17,7 +18,7 @@ DEFAULT_CONFIG_DIR = "configs/" _C = CN() _C.SEED = 100 # ----------------------------------------------------------------------------- -# BASELINES +# BASELINE # ----------------------------------------------------------------------------- _C.BASELINE = CN() # ----------------------------------------------------------------------------- @@ -27,6 +28,28 @@ _C.BASELINE.RL = CN() _C.BASELINE.RL.SUCCESS_REWARD = 10.0 _C.BASELINE.RL.SLACK_REWARD = -0.01 # ----------------------------------------------------------------------------- +# ORBSLAM2 BASELINE +# ----------------------------------------------------------------------------- +_C.BASELINE.ORBSLAM2 = CN() +_C.BASELINE.ORBSLAM2.SLAM_VOCAB_PATH = "baselines/slambased/data/ORBvoc.txt" +_C.BASELINE.ORBSLAM2.SLAM_SETTINGS_PATH = "baselines/slambased/data/mp3d3_small1k.yaml" +_C.BASELINE.ORBSLAM2.MAP_CELL_SIZE = 0.1 +_C.BASELINE.ORBSLAM2.MAP_SIZE = 40 +_C.BASELINE.ORBSLAM2.CAMERA_HEIGHT = get_config().SIMULATOR.DEPTH_SENSOR.POSITION[1] +_C.BASELINE.ORBSLAM2.BETA = 100 +_C.BASELINE.ORBSLAM2.H_OBSTACLE_MIN = 0.3 * _C.BASELINE.ORBSLAM2.CAMERA_HEIGHT +_C.BASELINE.ORBSLAM2.H_OBSTACLE_MAX = 1.0 * _C.BASELINE.ORBSLAM2.CAMERA_HEIGHT +_C.BASELINE.ORBSLAM2.D_OBSTACLE_MIN = 0.1 +_C.BASELINE.ORBSLAM2.D_OBSTACLE_MAX = 4.0 +_C.BASELINE.ORBSLAM2.PREPROCESS_MAP = True +_C.BASELINE.ORBSLAM2.MIN_PTS_IN_OBSTACLE = get_config().SIMULATOR.DEPTH_SENSOR.WIDTH/2.0 +_C.BASELINE.ORBSLAM2.ANGLE_TH = float(np.deg2rad(15)) +_C.BASELINE.ORBSLAM2.DIST_REACHED_TH = 0.15 +_C.BASELINE.ORBSLAM2.NEXT_WAYPOINT_TH = 0.5 +_C.BASELINE.ORBSLAM2.NUM_ACTIONS = 3 +_C.BASELINE.ORBSLAM2.DIST_TO_STOP = 0.05 +_C.BASELINE.ORBSLAM2.PLANNER_MAX_STEPS = 500 +_C.BASELINE.ORBSLAM2.DEPTH_DENORM = get_config().SIMULATOR.DEPTH_SENSOR.MAX_DEPTH def cfg( diff --git a/baselines/slambased/README.md b/baselines/slambased/README.md new file mode 100644 index 000000000..d70d3ade3 --- /dev/null +++ b/baselines/slambased/README.md @@ -0,0 +1,39 @@ +### Handcrafted agent baseline adopted from the paper "Benchmarking Classic and Learned Navigation in Complex 3D Environments" + +Project website: https://sites.google.com/view/classic-vs-learned-navigation +Paper: https://arxiv.org/abs/1901.10915 + +If you use this code or the provided environments in your research, please cite the following: + + @ARTICLE{Navigation2019, + author = {{Mishkin}, Dmytro and {Dosovitskiy}, Alexey and {Koltun}, Vladlen}, + title = "{Benchmarking Classic and Learned Navigation in Complex 3D Environments}", + year = 2019, + month = Jan, + archivePrefix = {arXiv}, + eprint = {1901.10915}, + } + + + +## Dependencies: + +- conda +- numpy +- pytorch +- ORBSLAM2 + + +## Tested with: +- Ubuntu 16.04 +- python 3.6 +- pytorch 0.4, 1.0 + + +- Install Anaconda https://www.anaconda.com/download/#linux + +- Install dependencies via ./install_deps.sh. It should install everything except the datasets. + +Simple example of working with agents is shown in (../handcrafted-agent-example.ipynb) + + diff --git a/baselines/slambased/data/mp3d3_small1k.yaml b/baselines/slambased/data/mp3d3_small1k.yaml new file mode 100644 index 000000000..c814c080d --- /dev/null +++ b/baselines/slambased/data/mp3d3_small1k.yaml @@ -0,0 +1,69 @@ +%YAML:1.0 + +#-------------------------------------------------------------------------------------------- +# Camera Parameters. Adjust them! +#-------------------------------------------------------------------------------------------- + +# Camera calibration and distortion parameters (OpenCV) +#For resolution 256x256, FOV 90 deg +Camera.fx: 128.0 +Camera.fy: 128.0 +Camera.cx: 127.0 +Camera.cy: 127.0 + + +Camera.k1: 0.0 +Camera.k2: 0.0 +Camera.p1: 0.0 +Camera.p2: 0.0 + +# Camera frames per second +Camera.fps: 30.0 + +# IR projector baseline times fx (aprox.) +Camera.bf: 50.0 + +# Color order of the images (0: BGR, 1: RGB. It is ignored if images are grayscale) +Camera.RGB: 1 + +# Close/Far threshold. Baseline times. +#ThDepth: 40.0 +ThDepth: 70.0 + +# Deptmap values factor +DepthMapFactor: 1.0 + +#-------------------------------------------------------------------------------------------- +# ORB Parameters +#-------------------------------------------------------------------------------------------- + +# ORB Extractor: Number of features per image +ORBextractor.nFeatures: 1000 + +# ORB Extractor: Scale factor between levels in the scale pyramid +ORBextractor.scaleFactor: 1.2 + +# ORB Extractor: Number of levels in the scale pyramid +ORBextractor.nLevels: 8 + +# ORB Extractor: Fast threshold +# Image is divided in a grid. At each cell FAST are extracted imposing a minimum response. +# Firstly we impose iniThFAST. If no corners are detected we impose a lower value minThFAST +# You can lower these values if your images have low contrast +ORBextractor.iniThFAST: 5 +ORBextractor.minThFAST: 1 + +#-------------------------------------------------------------------------------------------- +# Viewer Parameters +#-------------------------------------------------------------------------------------------- +Viewer.KeyFrameSize: 0.1 +Viewer.KeyFrameLineWidth: 1 +Viewer.GraphLineWidth: 1 +Viewer.PointSize:2 +Viewer.CameraSize: 0.15 +Viewer.CameraLineWidth: 2 +Viewer.ViewpointX: 0 +Viewer.ViewpointY: -10 +Viewer.ViewpointZ: -0.1 +Viewer.ViewpointF: 2000 + diff --git a/baselines/slambased/data/slam-based-agent.png b/baselines/slambased/data/slam-based-agent.png new file mode 100644 index 0000000000000000000000000000000000000000..3aafc634fa034e68bb4fea5f50a72406a554db56 GIT binary patch literal 72060 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�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 AWB>pF literal 0 HcmV?d00001 diff --git a/baselines/slambased/install_deps.sh b/baselines/slambased/install_deps.sh new file mode 100755 index 000000000..75002474f --- /dev/null +++ b/baselines/slambased/install_deps.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +DIR1=$(pwd) +MAINDIR=$(pwd)/3rdparty +mkdir ${MAINDIR} +cd ${MAINDIR} +#conda create -y -n "HandcraftedAgents" python=3.6 +source activate HandcraftedAgents +conda install opencv -y +conda install pytorch torchvision -c pytorch -y +conda install -c conda-forge imageio -y +conda install ffmpeg -c conda-forge -y +cd ${MAINDIR} +mkdir eigen3 +cd eigen3 +wget http://bitbucket.org/eigen/eigen/get/3.3.5.tar.gz +tar -xzf 3.3.5.tar.gz +cd eigen-eigen-b3f3d4950030 +mkdir build +cd build +cmake .. -DCMAKE_INSTALL_PREFIX=${MAINDIR}/eigen3_installed/ +make install +cd ${MAINDIR} +wget https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.zip +unzip glew-2.1.0.zip +cd glew-2.1.0/ +cd build +cmake ./cmake -DCMAKE_INSTALL_PREFIX=${MAINDIR}/glew_installed +make -j4 +make install +cd ${MAINDIR} +#pip install numpy --upgrade +rm Pangolin -rf +git clone https://github.com/stevenlovegrove/Pangolin.git +cd Pangolin +mkdir build +cd build +cmake .. -DCMAKE_PREFIX_PATH=${MAINDIR}/glew_installed/ -DCMAKE_LIBRARY_PATH=${MAINDIR}/glew_installed/lib/ -DCMAKE_INSTALL_PREFIX=${MAINDIR}/pangolin_installed +cmake --build . +cd ${MAINDIR} +rm ORB_SLAM2 -rf +rm ORB_SLAM2-PythonBindings -rf +git clone https://github.com/ducha-aiki/ORB_SLAM2 +git clone https://github.com/ducha-aiki/ORB_SLAM2-PythonBindings +cd ${MAINDIR}/ORB_SLAM2 +sed -i "s,cmake .. -DCMAKE_BUILD_TYPE=Release,cmake .. -DCMAKE_BUILD_TYPE=Release -DEIGEN3_INCLUDE_DIR=${MAINDIR}/eigen3_installed/include/eigen3/ -DCMAKE_INSTALL_PREFIX=${MAINDIR}/ORBSLAM2_installed ,g" build.sh +ln -s ${MAINDIR}/eigen3_installed/include/eigen3/Eigen ${MAINDIR}/ORB_SLAM2/Thirdparty/g2o/g2o/core/Eigen +./build.sh +cd build +make install +cd ${MAINDIR} +cd ORB_SLAM2-PythonBindings/src +ln -s ${MAINDIR}/eigen3_installed/include/eigen3/Eigen Eigen +cd ${MAINDIR}/ORB_SLAM2-PythonBindings +mkdir build +cd build +CONDA_DIR=$(dirname $(dirname $(which conda))) +sed -i "s,lib/python3.5/dist-packages,${CONDA_DIR}/envs/HandcraftedAgents/lib/python3.6/site-packages/,g" ../CMakeLists.txt +cmake .. -DPYTHON_INCLUDE_DIR=$(python -c "from distutils.sysconfig import get_python_inc; print(get_python_inc())") -DPYTHON_LIBRARY=$(python -c "import distutils.sysconfig as sysconfig; print(sysconfig.get_config_var('LIBDIR'))")/libpython3.6m.so -DPYTHON_EXECUTABLE:FILEPATH=`which python` -DCMAKE_LIBRARY_PATH=${MAINDIR}/ORBSLAM2_installed/lib -DCMAKE_INCLUDE_PATH=${MAINDIR}/ORBSLAM2_installed/include;${MAINDIR}/eigen3_installed/include/eigen3 -DCMAKE_INSTALL_PREFIX=${MAINDIR}/pyorbslam2_installed +make +make install +cp ${MAINDIR}/ORB_SLAM2/Vocabulary/ORBvoc.txt ${DIR1}/data/ diff --git a/baselines/slambased/mappers.py b/baselines/slambased/mappers.py new file mode 100644 index 000000000..4e873ba68 --- /dev/null +++ b/baselines/slambased/mappers.py @@ -0,0 +1,122 @@ +import numpy as np +import torch +import torch.nn as nn +from baselines.slambased.reprojection import ( + get_map_size_in_cells, + project2d_pcl_into_worldmap, + reproject_local_to_global, +) + + +def depth2local3d(depth, fx, fy, cx, cy): + """Projects depth map to 3d point cloud + with origin in the camera focus + """ + device = depth.device + h, w = depth.squeeze().size() + x = torch.linspace(0, w - 1, w).to(device) + y = torch.linspace(0, h - 1, h).to(device) + xv, yv = torch.meshgrid([x, y]) + dfl = depth.t().flatten() + return torch.cat( + [ + (dfl * (xv.flatten() - cx) / fx).unsqueeze(-1), # x + (dfl * (yv.flatten() - cy) / fy).unsqueeze(-1), # y + dfl.unsqueeze(-1), + ], + dim=1, + ) # z + + +def pcl_to_obstacles(pts3d, map_size=40, cell_size=0.2, min_pts= 10): + """Counts number of 3d points in 2d map cell. + Height is sum-pooled. + """ + device = pts3d.device + map_size_in_cells = get_map_size_in_cells(map_size, cell_size) - 1 + init_map = torch.zeros( + (map_size_in_cells, map_size_in_cells), device=device + ) + if len(pts3d) <= 1: + return init_map + num_pts, dim = pts3d.size() + pts2d = torch.cat([pts3d[:, 2:3], pts3d[:, 0:1]], dim=1) + data_idxs = torch.round( + project2d_pcl_into_worldmap(pts2d, map_size, cell_size) + ) + if len(data_idxs) > min_pts: + u, counts = np.unique( + data_idxs.detach().cpu().numpy(), axis=0, return_counts=True + ) + init_map[u[:, 0], u[:, 1]] = torch.from_numpy(counts).to( + dtype=torch.float32, device=device + ) + return init_map + + +class DirectDepthMapper(nn.Module): + """Estimates obstacle map given the depth image + ToDo: replace numpy histogram counting with differentiable + pytorch soft count like in + https://papers.nips.cc/paper/7545-unsupervised-learning-of-shape-and-pose-with-differentiable-point-clouds.pdf + """ + + def __init__( + self, + camera_height=0, + near_th=0.1, + far_th=4.0, + h_min=0.0, + h_max=1.0, + map_size=40, + map_cell_size=0.1, + device=torch.device("cpu"), + **kwargs + ): + super(DirectDepthMapper, self).__init__() + self.device = device + self.near_th = near_th + self.far_th = far_th + self.h_min_th = h_min + self.h_max_th = h_max + self.camera_height = camera_height + self.map_size_meters = map_size + self.map_cell_size = map_cell_size + return + + def forward(self, depth, pose=torch.eye(4).float()): + self.device = depth.device + # Works for FOV = 90 degrees + # Should be adjusted, if FOV changed + self.fx = float(depth.size(1)) / 2.0 + self.fy = float(depth.size(0)) / 2.0 + self.cx = int(self.fx) - 1 + self.cy = int(self.fy) - 1 + pose = pose.to(self.device) + local_3d_pcl = depth2local3d( + depth, self.fx, self.fy, self.cx, self.cy + ) + idxs = (torch.abs(local_3d_pcl[:, 2]) < self.far_th) * ( + torch.abs(local_3d_pcl[:, 2]) >= self.near_th + ) + survived_points = local_3d_pcl[idxs] + if len(survived_points) < 20: + map_size_in_cells = ( + get_map_size_in_cells(self.map_size_meters, + self.map_cell_size) - 1 + ) + init_map = torch.zeros( + (map_size_in_cells, map_size_in_cells), device=self.device + ) + return init_map + global_3d_pcl = reproject_local_to_global(survived_points, pose)[:, :3] + # Because originally y looks down and from agent camera height + global_3d_pcl[:, 1] = -global_3d_pcl[:, 1] + self.camera_height + idxs = (global_3d_pcl[:, 1] > self.h_min_th) * ( + global_3d_pcl[:, 1] < self.h_max_th + ) + global_3d_pcl = global_3d_pcl[idxs] + obstacle_map = pcl_to_obstacles( + global_3d_pcl, self.map_size_meters, self.map_cell_size + ) + return obstacle_map diff --git a/baselines/slambased/monodepth.py b/baselines/slambased/monodepth.py new file mode 100644 index 000000000..d047a6170 --- /dev/null +++ b/baselines/slambased/monodepth.py @@ -0,0 +1,573 @@ +""" +The code below is taked from https://github.com/JunjH/Revisiting_Single_Depth_Estimation +Revisiting Single Image Depth Estimation: Toward Higher Resolution Maps With Accurate Object Boundaries +Junjie Hu and Mete Ozay and Yan Zhang and Takayuki Okatani +WACV 2019 +""" + + +import torch +import torch.nn.parallel +""" +ResNet code gently borrowed from +https://github.com/pytorch/vision/blob/master/torchvision/models/py +""" + +import torch.nn as nn +import math +import torch.utils.model_zoo as model_zoo +import torch.nn.functional as F +import torch +import numpy as np +import pdb +import os +from PIL import Image +accimage = None +from torchvision import transforms, utils + + +__all__ = ['ResNet', 'resnet18', 'resnet34', 'resnet50', 'resnet101', + 'resnet152'] + + +model_urls = { + 'resnet18': 'https://download.pytorch.org/models/resnet18-5c106cde.pth', + 'resnet34': 'https://download.pytorch.org/models/resnet34-333f7ec4.pth', + 'resnet50': 'https://download.pytorch.org/models/resnet50-19c8e357.pth', + 'resnet101': 'https://download.pytorch.org/models/resnet101-5d3b4d8f.pth', + 'resnet152': 'https://download.pytorch.org/models/resnet152-b121ed2d.pth', +} + + +def conv3x3(in_planes, out_planes, stride=1): + "3x3 convolution with padding" + return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride, + padding=1, bias=False) + +class BasicBlock(nn.Module): + expansion = 1 + + def __init__(self, inplanes, planes, stride=1, downsample=None): + super(BasicBlock, self).__init__() + self.conv1 = conv3x3(inplanes, planes, stride) + self.bn1 = nn.BatchNorm2d(planes) + self.relu = nn.ReLU(inplace=True) + self.conv2 = conv3x3(planes, planes) + self.bn2 = nn.BatchNorm2d(planes) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class Bottleneck(nn.Module): + expansion = 4 + + def __init__(self, inplanes, planes, stride=1, downsample=None): + super(Bottleneck, self).__init__() + self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False) + self.bn1 = nn.BatchNorm2d(planes) + self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, + padding=1, bias=False) + self.bn2 = nn.BatchNorm2d(planes) + self.conv3 = nn.Conv2d(planes, planes * 4, kernel_size=1, bias=False) + self.bn3 = nn.BatchNorm2d(planes * 4) + self.relu = nn.ReLU(inplace=True) + self.downsample = downsample + self.stride = stride + + def forward(self, x): + residual = x + + out = self.conv1(x) + out = self.bn1(out) + out = self.relu(out) + + out = self.conv2(out) + out = self.bn2(out) + out = self.relu(out) + + out = self.conv3(out) + out = self.bn3(out) + + if self.downsample is not None: + residual = self.downsample(x) + + out += residual + out = self.relu(out) + + return out + + +class ResNet(nn.Module): + + def __init__(self, block, layers, num_classes=1000): + self.inplanes = 64 + super(ResNet, self).__init__() + self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, + bias=False) + self.bn1 = nn.BatchNorm2d(64) + self.relu = nn.ReLU(inplace=True) + self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) + self.layer1 = self._make_layer(block, 64, layers[0]) + self.layer2 = self._make_layer(block, 128, layers[1], stride=2) + self.layer3 = self._make_layer(block, 256, layers[2], stride=2) + self.layer4 = self._make_layer(block, 512, layers[3], stride=2) + self.avgpool = nn.AvgPool2d(7, stride=1) + self.fc = nn.Linear(512 * block.expansion, num_classes) + + for m in self.modules(): + if isinstance(m, nn.Conv2d): + n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels + m.weight.data.normal_(0, math.sqrt(2. / n)) + elif isinstance(m, nn.BatchNorm2d): + m.weight.data.fill_(1) + m.bias.data.zero_() + + def _make_layer(self, block, planes, blocks, stride=1): + downsample = None + if stride != 1 or self.inplanes != planes * block.expansion: + downsample = nn.Sequential( + nn.Conv2d(self.inplanes, planes * block.expansion, + kernel_size=1, stride=stride, bias=False), + nn.BatchNorm2d(planes * block.expansion), + ) + + layers = [] + layers.append(block(self.inplanes, planes, stride, downsample)) + self.inplanes = planes * block.expansion + for i in range(1, blocks): + layers.append(block(self.inplanes, planes)) + + return nn.Sequential(*layers) + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + + x = self.layer1(x) + x = self.layer2(x) + x = self.layer3(x) + x = self.layer4(x) + + x = self.avgpool(x) + x = x.view(x.size(0), -1) + x = self.fc(x) + + return x + +def resnet18(pretrained=False, **kwargs): + """Constructs a ResNet-18 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(BasicBlock, [2, 2, 2, 2], **kwargs) + if pretrained: + model.load_state_dict(model_zoo.load_url(model_urls['resnet18'])) + return model + + +def resnet34(pretrained=False, **kwargs): + """Constructs a ResNet-34 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(BasicBlock, [3, 4, 6, 3], **kwargs) + if pretrained: + model.load_state_dict(model_zoo.load_url(model_urls['resnet34'])) + return model + + +def resnet50(pretrained=False, **kwargs): + """Constructs a ResNet-50 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 4, 6, 3], **kwargs) + if pretrained: + model.load_state_dict(model_zoo.load_url(model_urls['resnet50'], 'pretrained_model/encoder')) + return model + + +def resnet101(pretrained=False, **kwargs): + """Constructs a ResNet-101 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 4, 23, 3], **kwargs) + if pretrained: + model.load_state_dict(model_zoo.load_url(model_urls['resnet101'])) + return model + + +def resnet152(pretrained=False, **kwargs): + """Constructs a ResNet-152 model. + Args: + pretrained (bool): If True, returns a model pre-trained on ImageNet + """ + model = ResNet(Bottleneck, [3, 8, 36, 3], **kwargs) + if pretrained: + model.load_state_dict(model_zoo.load_url(model_urls['resnet152'])) + return model + + +class model(nn.Module): + def __init__(self, Encoder, num_features, block_channel): + + super(model, self).__init__() + + self.E = Encoder + self.D = D(num_features) + self.MFF = MFF(block_channel) + self.R = R(block_channel) + + + def forward(self, x): + x_block1, x_block2, x_block3, x_block4 = self.E(x) + x_decoder = self.D(x_block1, x_block2, x_block3, x_block4) + x_mff = self.MFF(x_block1, x_block2, x_block3, x_block4,[x_decoder.size(2),x_decoder.size(3)]) + out = self.R(torch.cat((x_decoder, x_mff), 1)) + + return out + + +class _UpProjection(nn.Sequential): + + def __init__(self, num_input_features, num_output_features): + super(_UpProjection, self).__init__() + + self.conv1 = nn.Conv2d(num_input_features, num_output_features, + kernel_size=5, stride=1, padding=2, bias=False) + self.bn1 = nn.BatchNorm2d(num_output_features) + self.relu = nn.ReLU(inplace=True) + self.conv1_2 = nn.Conv2d(num_output_features, num_output_features, + kernel_size=3, stride=1, padding=1, bias=False) + self.bn1_2 = nn.BatchNorm2d(num_output_features) + + self.conv2 = nn.Conv2d(num_input_features, num_output_features, + kernel_size=5, stride=1, padding=2, bias=False) + self.bn2 = nn.BatchNorm2d(num_output_features) + + def forward(self, x, size): + x = F.upsample(x, size=size, mode='bilinear') + x_conv1 = self.relu(self.bn1(self.conv1(x))) + bran1 = self.bn1_2(self.conv1_2(x_conv1)) + bran2 = self.bn2(self.conv2(x)) + + out = self.relu(bran1 + bran2) + + return out + +class E_resnet(nn.Module): + + def __init__(self, original_model, num_features = 2048): + super(E_resnet, self).__init__() + self.conv1 = original_model.conv1 + self.bn1 = original_model.bn1 + self.relu = original_model.relu + self.maxpool = original_model.maxpool + + self.layer1 = original_model.layer1 + self.layer2 = original_model.layer2 + self.layer3 = original_model.layer3 + self.layer4 = original_model.layer4 + + + def forward(self, x): + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + x = self.maxpool(x) + + x_block1 = self.layer1(x) + x_block2 = self.layer2(x_block1) + x_block3 = self.layer3(x_block2) + x_block4 = self.layer4(x_block3) + + return x_block1, x_block2, x_block3, x_block4 + + +class D(nn.Module): + + def __init__(self, num_features = 2048): + super(D, self).__init__() + self.conv = nn.Conv2d(num_features, num_features // + 2, kernel_size=1, stride=1, bias=False) + num_features = num_features // 2 + self.bn = nn.BatchNorm2d(num_features) + + self.up1 = _UpProjection( + num_input_features=num_features, num_output_features=num_features // 2) + num_features = num_features // 2 + + self.up2 = _UpProjection( + num_input_features=num_features, num_output_features=num_features // 2) + num_features = num_features // 2 + + self.up3 = _UpProjection( + num_input_features=num_features, num_output_features=num_features // 2) + num_features = num_features // 2 + + self.up4 = _UpProjection( + num_input_features=num_features, num_output_features=num_features // 2) + num_features = num_features // 2 + + + def forward(self, x_block1, x_block2, x_block3, x_block4): + x_d0 = F.relu(self.bn(self.conv(x_block4))) + x_d1 = self.up1(x_d0, [x_block3.size(2), x_block3.size(3)]) + x_d2 = self.up2(x_d1, [x_block2.size(2), x_block2.size(3)]) + x_d3 = self.up3(x_d2, [x_block1.size(2), x_block1.size(3)]) + x_d4 = self.up4(x_d3, [x_block1.size(2)*2, x_block1.size(3)*2]) + + return x_d4 + +class MFF(nn.Module): + + def __init__(self, block_channel, num_features=64): + + super(MFF, self).__init__() + + self.up1 = _UpProjection( + num_input_features=block_channel[0], num_output_features=16) + + self.up2 = _UpProjection( + num_input_features=block_channel[1], num_output_features=16) + + self.up3 = _UpProjection( + num_input_features=block_channel[2], num_output_features=16) + + self.up4 = _UpProjection( + num_input_features=block_channel[3], num_output_features=16) + + self.conv = nn.Conv2d( + num_features, num_features, kernel_size=5, stride=1, padding=2, bias=False) + self.bn = nn.BatchNorm2d(num_features) + + + def forward(self, x_block1, x_block2, x_block3, x_block4, size): + x_m1 = self.up1(x_block1, size) + x_m2 = self.up2(x_block2, size) + x_m3 = self.up3(x_block3, size) + x_m4 = self.up4(x_block4, size) + + x = self.bn(self.conv(torch.cat((x_m1, x_m2, x_m3, x_m4), 1))) + x = F.relu(x) + + return x + + +class R(nn.Module): + def __init__(self, block_channel): + + super(R, self).__init__() + + num_features = 64 + block_channel[3]//32 + self.conv0 = nn.Conv2d(num_features, num_features, + kernel_size=5, stride=1, padding=2, bias=False) + self.bn0 = nn.BatchNorm2d(num_features) + + self.conv1 = nn.Conv2d(num_features, num_features, + kernel_size=5, stride=1, padding=2, bias=False) + self.bn1 = nn.BatchNorm2d(num_features) + + self.conv2 = nn.Conv2d( + num_features, 1, kernel_size=5, stride=1, padding=2, bias=True) + + def forward(self, x): + x0 = self.conv0(x) + x0 = self.bn0(x0) + x0 = F.relu(x0) + + x1 = self.conv1(x0) + x1 = self.bn1(x1) + x1 = F.relu(x1) + + x2 = self.conv2(x1) + + return x2 +def _is_pil_image(img): + return isinstance(img, Image.Image) + +def _is_numpy_image(img): + return isinstance(img, np.ndarray) and (img.ndim in {2, 3}) + +class Scale(object): + def __init__(self, size): + self.size = size + + def __call__(self, image): + image = self.changeScale(image,self.size) + + return image + + def changeScale(self, img, size, interpolation=Image.BILINEAR): + ow, oh = size + + return img.resize((ow, oh), interpolation) + +class CenterCrop(object): + def __init__(self, size): + self.size = size + + def __call__(self, image): + image = self.centerCrop(image,self.size) + + return image + + def centerCrop(self,image, size): + w1, h1 = image.size + tw, th = size + + if w1 == tw and h1 == th: + return image + + x1 = int(round((w1 - tw) / 2.)) + y1 = int(round((h1 - th) / 2.)) + + image = image.crop((x1, y1, tw+x1, th+y1)) + + return image + + +class ToTensor(object): + """Convert a ``PIL.Image`` or ``numpy.ndarray`` to tensor. + Converts a PIL.Image or numpy.ndarray (H x W x C) in the range + [0, 255] to a torch.FloatTensor of shape (C x H x W) in the range [0.0, 1.0]. + """ + + def __call__(self, image): + image = self.to_tensor(image) + + return image + + + def to_tensor(self,pic): + if not(_is_pil_image(pic) or _is_numpy_image(pic)): + raise TypeError('pic should be PIL Image or ndarray. Got {}'.format(type(pic))) + + if isinstance(pic, np.ndarray): + + img = torch.from_numpy(pic.transpose((2, 0, 1))) + return img.float().div(255) + + + if accimage is not None and isinstance(pic, accimage.Image): + nppic = np.zeros([pic.channels, pic.height, pic.width], dtype=np.float32) + pic.copyto(nppic) + return torch.from_numpy(nppic) + + # handle PIL Image + if pic.mode == 'I': + img = torch.from_numpy(np.array(pic, np.int32, copy=False)) + elif pic.mode == 'I;16': + img = torch.from_numpy(np.array(pic, np.int16, copy=False)) + else: + img = torch.ByteTensor(torch.ByteStorage.from_buffer(pic.tobytes())) + # PIL image mode: 1, L, P, I, F, RGB, YCbCr, RGBA, CMYK + if pic.mode == 'YCbCr': + nchannel = 3 + elif pic.mode == 'I;16': + nchannel = 1 + else: + nchannel = len(pic.mode) + img = img.view(pic.size[1], pic.size[0], nchannel) + # put it from HWC to CHW format + # yikes, this transpose takes 80% of the loading time/CPU + img = img.transpose(0, 1).transpose(0, 2).contiguous() + if isinstance(img, torch.ByteTensor): + return img.float().div(255) + else: + return img + + + +class Normalize(object): + def __init__(self, mean, std): + self.mean = mean + self.std = std + + def __call__(self, image): + image = self.normalize(image, self.mean, self.std) + + return image + + def normalize(self, tensor, mean, std): + for t, m, s in zip(tensor, mean, std): + t.sub_(m).div_(s) + + return tensor + + + +def define_model(is_resnet, is_densenet, is_senet): + if is_resnet: + original_model = resnet50(pretrained = False) + Encoder = E_resnet(original_model) + model1 = model(Encoder, num_features=2048, block_channel = [256, 512, 1024, 2048]) + if is_densenet: + original_model = dendensenet161(pretrained=False) + Encoder = E_densenet(original_model) + model1 = model(Encoder, num_features=2208, block_channel = [192, 384, 1056, 2208]) + if is_senet: + original_model = senet154(pretrained=False) + Encoder = E_senet(original_model) + model1 = model(Encoder, num_features=2048, block_channel = [256, 512, 1024, 2048]) + + return model1 + + +class MonoDepthEstimator: + def __init__(self, checkpoint='./pretrained_model/model_resnet'): + self.model = define_model(is_resnet=True, is_densenet=False, is_senet=False) + self.model = torch.nn.DataParallel(self.model).cuda() + cpt = torch.load(checkpoint) + if 'state_dict' in cpt.keys(): + cpt = cpt['state_dict'] + self.model.load_state_dict(cpt) + self.model.eval() + self.init_preprocessor() + + def init_preprocessor(self): + __imagenet_stats = {'mean': [0.485, 0.456, 0.406], + 'std': [0.229, 0.224, 0.225]} + + self.transform = transforms.Compose([ + Scale([320, 240]), + #CenterCrop([304, 228]), + ToTensor(), + Normalize(__imagenet_stats['mean'], + __imagenet_stats['std']) + ]) + + def preprocess(self, image): + image_torch = self.transform(image).unsqueeze(0) + return image_torch.cuda() + + def compute_depth(self, image): + # Input: image is a PIL image + # Output: depth is a numpy array + image_torch = self.preprocess(image) + #print(image_torch.size()) + depth_torch = self.model(image_torch) + depth = depth_torch.view(depth_torch.size(2),depth_torch.size(3)).data.cpu().numpy() + return depth + diff --git a/baselines/slambased/path_planners.py b/baselines/slambased/path_planners.py new file mode 100644 index 000000000..adc2eb97b --- /dev/null +++ b/baselines/slambased/path_planners.py @@ -0,0 +1,512 @@ +import numpy as np +import torch +import torch.nn.functional as F +import torch.nn as nn +import matplotlib.pyplot as plt +from baselines.slambased.utils import generate_2dgrid + + +def safe_roi_2d(array2d, ymin, ymax, xmin, xmax): + (h, w) = array2d.shape + return max(0, ymin), min(ymax, h), max(0, xmin), min(xmax, w) + + +def f2ind(ten, i): + # Float to index + return torch.round(ten[i]).long() + + +def init_neights_to_channels(ks=3): + r"""Convolutional kernel, + which maps nighborhood into channels + """ + weights = np.zeros((ks * ks, 1, ks, ks), dtype=np.float32) + for y in range(ks): + for x in range(ks): + weights[x * ks + y, 0, y, x] = 1.0 + return weights + + +class SoftArgMin(nn.Module): + def __init__(self, beta=5): + super(SoftArgMin, self).__init__() + self.beta = beta + return + + def forward(self, x, coords2d=None): + bx_sm = F.softmax(self.beta * (-x).view(1, -1), dim=1) + if coords2d is None: + coords2d = generate_2dgrid(x.size(2), x.size(3), False) + coords2d_flat = coords2d.view(2, -1) + return (bx_sm.expand_as(coords2d_flat) * coords2d_flat).sum( + dim=1 + ) / bx_sm.sum(dim=1) + + +class HardArgMin(nn.Module): + def __init__(self): + super(HardArgMin, self).__init__() + return + + def forward(self, x, coords2d=None): + val, idx = x.view(-1).min(dim=0) + if coords2d is None: + coords2d = generate_2dgrid(x.size(2), x.size(3), False) + coords2d_flat = coords2d.view(2, -1) + return coords2d_flat[:, idx].view(2) + + +class DifferentiableStarPlanner(nn.Module): + def __init__( + self, + max_steps=500, + visualize=False, + preprocess=False, + beta=100, + connectivity="eight", + device=torch.device("cpu"), + **kwargs + ): + super(DifferentiableStarPlanner, self).__init__() + self.eps = 1e-12 + self.max_steps = max_steps + self.visualize = visualize + self.inf = 1e7 + self.ob_cost = 10000.0 + self.device = device + self.beta = beta + self.preprocess = preprocess + # self.argmin = SoftArgMin(beta) + self.argmin = HardArgMin() + self.neights2channels = nn.Conv2d(1, 9, kernel_size=(3, 3), bias=False) + self.neights2channels.weight.data = torch.from_numpy( + init_neights_to_channels(3) + ) + self.neights2channels.to(device) + self.preprocessNet = nn.Conv2d( + 1, 1, kernel_size=(3, 3), padding=1, bias=False + ) + self.preprocessNet.weight.data = torch.from_numpy( + np.array( + [ + [ + [ + [0.00001, 0.0001, 0.00001], + [0.0001, 1, 0.0001], + [0.00001, 0.0001, 0.00001], + ] + ] + ], + dtype=np.float32, + ) + ) + self.preprocessNet.to(device) + if connectivity == "eight": + self.gx_to_right = nn.Conv2d(1, 1, kernel_size=(1, 3), bias=False) + self.gx_to_right.weight.data = torch.from_numpy( + np.array([[[[0, 1, -1]]]], dtype=np.float32) + ) + self.gx_to_right.to(device) + + self.gx_to_left = nn.Conv2d(1, 1, kernel_size=(1, 3), bias=False) + self.gx_to_left.weight.data = torch.from_numpy( + np.array([[[[-1, 1, 0]]]], dtype=np.float32) + ) + self.gx_to_left.to(device) + + self.gy_to_up = nn.Conv2d(1, 1, kernel_size=(3, 1), bias=False) + self.gy_to_up.weight.data = torch.from_numpy( + np.array([[[[0], [1], [-1]]]], dtype=np.float32) + ) + self.gy_to_up.to(device) + + self.gy_to_down = nn.Conv2d(1, 1, kernel_size=(3, 1), bias=False) + self.gy_to_down.weight.data = torch.from_numpy( + np.array([[[[-1], [1], [0]]]], dtype=np.float32) + ) + self.gy_to_down.to(device) + else: + raise ValueError('Only "eight" connectivity now supported') + return + + def preprocess_obstacle_map(self, obstacle_map): + if self.preprocess: + return self.preprocessNet( + obstacle_map + ) + return obstacle_map + + def coords2grid(self, node_coords, h, w): + grid = node_coords.squeeze() - torch.FloatTensor( + (h / 2.0, w / 2.0) + ).to(self.device) + grid = grid / torch.FloatTensor((h / 2.0, w / 2.0)).to(self.device) + return grid.view(1, 1, 1, 2).flip(3) + + def init_closelistmap(self): + return torch.zeros_like( + self.start_map + ).float() + + def init_openlistmap(self): + return self.start_map.clone() + + def init_g_map(self): + return torch.clamp( + self.inf + * (torch.ones_like(self.start_map) - self.start_map.clone()), + min=0, + max=self.inf, + ) + + def safe_roi_2d(self, ymin, ymax, xmin, xmax): + return ( + int(max(0, torch.round(ymin).item())), + int(min(torch.round(ymax).item(), self.height)), + int(max(0, torch.round(xmin).item())), + int(min(torch.round(xmax).item(), self.width)), + ) + + def forward( + self, + obstacles, + coords, + start_map, + goal_map, + non_obstacle_cost_map=None, + additional_steps=50, + return_path=True, + ): + self.trav_init_time = 0 + self.trav_mask_time = 0 + self.trav_soft_time = 0 + self.conv_time = 0 + self.close_time = 0 + + self.obstacles = self.preprocess_obstacle_map( + obstacles.to(self.device) + ) + self.start_map = start_map.to(self.device) + self.been_there = torch.zeros_like(self.start_map).to( + torch.device("cpu") + ) + self.coords = coords.to(self.device) + self.goal_map = goal_map.to(self.device) + self.been_there = torch.zeros_like(self.goal_map).to(self.device) + self.height = obstacles.size(2) + self.width = obstacles.size(3) + m, goal_idx = torch.max(self.goal_map.view(-1), 0) + c_map = self.calculate_local_path_costs( + non_obstacle_cost_map + ) + # c_map might be non persistent in map update + self.g_map = self.init_g_map() + self.close_list_map = self.init_closelistmap() + self.open_list_map = self.init_openlistmap() + not_done = False + step = 0 + stopped_by_max_iter = False + if self.visualize: + self.fig, self.ax = plt.subplots(1, 1) + self.image = self.ax.imshow( + self.g_map.squeeze().cpu().detach().numpy().astype(np.float32), + animated=True, + ) + self.fig.canvas.draw() + not_done = (self.close_list_map.view(-1)[goal_idx].item() < 1.0) or ( + self.g_map.view(-1)[goal_idx].item() >= 0.9 * self.ob_cost + ) + rad = 1 + self.start_coords = ( + (self.coords * self.start_map.expand_as(self.coords)) + .sum(dim=2) + .sum(dim=2) + .squeeze() + ) + node_coords = self.start_coords + self.goal_coords = ( + (self.coords * self.goal_map.expand_as(self.coords)) + .sum(dim=2) + .sum(dim=2) + .squeeze() + ) + self.max_steps = 4 * int( + torch.sqrt( + ((self.start_coords - self.goal_coords) ** 2).sum() + 1e-6 + ).item() + ) + while not_done: + ymin, ymax, xmin, xmax = self.safe_roi_2d( + node_coords[0] - rad, + node_coords[0] + rad + 1, + node_coords[1] - rad, + node_coords[1] + rad + 1, + ) + if ( + (ymin - 1 > 0) + and (xmin - 1 > 0) + and (ymax + 1 < self.height) + and (xmax + 1 < self.width) + ): + n2c = self.neights2channels( + self.g_map[:, :, ymin - 1:ymax + 1, xmin - 1:xmax + 1] + ) + self.g_map[:, :, ymin:ymax, xmin:xmax] = torch.min( + self.g_map[:, :, ymin:ymax, xmin:xmax].clone(), + (n2c + c_map[:, :, ymin:ymax, xmin:xmax]).min( + dim=1, keepdim=True + )[0], + ) + self.close_list_map[:, :, ymin:ymax, xmin:xmax] = torch.max( + self.close_list_map[:, :, ymin:ymax, xmin:xmax], + self.open_list_map[:, :, ymin:ymax, xmin:xmax], + ) + self.open_list_map[:, :, ymin:ymax, xmin:xmax] = F.relu( + F.max_pool2d( + self.open_list_map[ + :, :, ymin - 1:ymax + 1, xmin - 1:xmax + 1 + ], + 3, + stride=1, + padding=0, + ) + - self.close_list_map[:, :, ymin:ymax, xmin:xmax] + - self.obstacles[:, :, ymin:ymax, xmin:xmax] + ) + else: + self.g_map = torch.min( + self.g_map, + ( + self.neights2channels( + F.pad(self.g_map, (1, 1, 1, 1), "replicate") + ) + + c_map + ).min(dim=1, keepdim=True)[0], + ) + self.close_list_map = torch.max( + self.close_list_map, self.open_list_map + ) + self.open_list_map = F.relu( + F.max_pool2d(self.open_list_map, 3, stride=1, padding=1) + - self.close_list_map + - self.obstacles + ) + step += 1 + if step >= self.max_steps: + stopped_by_max_iter = True + break + not_done = ( + self.close_list_map.view(-1)[goal_idx].item() < 1.0 + ) or (self.g_map.view(-1)[goal_idx].item() >= 0.1 * self.inf) + rad += 1 + if not stopped_by_max_iter: + for i in range(additional_steps): + # now propagating beyong start point + self.g_map = torch.min( + self.g_map, + ( + self.neights2channels( + F.pad(self.g_map, (1, 1, 1, 1), "replicate") + ) + + c_map + ).min(dim=1, keepdim=True)[0], + ) + self.close_list_map = torch.max( + self.close_list_map, self.open_list_map + ) + self.open_list_map = F.relu( + F.max_pool2d(self.open_list_map, 3, stride=1, padding=1) + - self.close_list_map + - self.obstacles + ) + if return_path: + out_path, cost = self.reconstruct_path() + return out_path, cost + return + + def calculate_local_path_costs(self, non_obstacle_cost_map=None): + coords = self.coords + h = coords.size(2) + w = coords.size(3) + obstacles_pd = F.pad(self.obstacles, (1, 1, 1, 1), "replicate") + if non_obstacle_cost_map is None: + learned_bias = torch.ones_like(self.obstacles).to( + obstacles_pd.device + ) + else: + learned_bias = non_obstacle_cost_map.to(obstacles_pd.device) + left_diff_sq = ( + self.gx_to_left( + F.pad(coords[:, 1:2, :, :], (1, 1, 0, 0), "replicate") + ) + ** 2 + ) + right_diff_sq = ( + self.gx_to_right( + F.pad(coords[:, 1:2, :, :], (1, 1, 0, 0), "replicate") + ) + ** 2 + ) + up_diff_sq = ( + self.gy_to_up( + F.pad(coords[:, 0:1, :, :], (0, 0, 1, 1), "replicate") + ) + ** 2 + ) + down_diff_sq = ( + self.gy_to_down( + F.pad(coords[:, 0:1, :, :], (0, 0, 1, 1), "replicate") + ) + ** 2 + ) + out = torch.cat([ + # Order in from up to down, from left to right + # hopefully same as in PyTorch + torch.sqrt(left_diff_sq + up_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 0:h, 0:w], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + torch.sqrt(left_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 0:h, 1:w + 1], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + torch.sqrt(left_diff_sq + down_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 2:h + 2, 0:w], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + torch.sqrt(up_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 0:h, 1:w + 1], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + 0 * right_diff_sq + + self.ob_cost + * obstacles_pd[:, :, 1:h + 1, 1:w + 1], # current center + torch.sqrt(down_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 2:h + 2, 1:w + 1], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + torch.sqrt(right_diff_sq + up_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 0:h, 2:w + 2], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + torch.sqrt(right_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 1:h + 1, 2:w + 2], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + torch.sqrt(right_diff_sq + down_diff_sq + self.eps) + + self.ob_cost + * torch.max( + obstacles_pd[:, :, 2:h + 2, 2:w + 2], + obstacles_pd[:, :, 1:h + 1, 1:w + 1], + ), + ], + dim=1, + ) + return out + torch.clamp( + learned_bias.expand_as(out), min=0, max=self.ob_cost + ) + + def propagate_traversal(self, node_coords, close, g, coords): + ymin, ymax, xmin, xmax = self.safe_roi_2d( + node_coords[0] - 1, + node_coords[0] + 2, + node_coords[1] - 1, + node_coords[1] + 2, + ) + mask = close[:, :, ymin:ymax, xmin:xmax] > 0 + mask[ + :, :, f2ind(node_coords, 0) - ymin, f2ind(node_coords, 1) - xmin + ] = 0 + mask = mask > 0 + current_g_cost = g[:, :, ymin:ymax, xmin:xmax][mask].clone() + if len(current_g_cost.view(-1)) == 0: + # we are kind surrounded by obstacles, + # but still need to output something + mask = torch.relu( + 1.0 - self.been_there[:, :, ymin:ymax, xmin:xmax] + ) + mask[ + :, + :, + f2ind(node_coords, 0) - ymin, + f2ind(node_coords, 1) - xmin, + ] = 0 + mask = mask > 0 + current_g_cost = g[:, :, ymin:ymax, xmin:xmax][mask].clone() + if len(current_g_cost.view(-1)) > 1: + current_g_cost = current_g_cost - torch.min(current_g_cost).item() + current_g_cost = current_g_cost + 0.41 * torch.randperm( + len(current_g_cost), + dtype=torch.float32, + device=torch.device("cpu"), + ) / (len(current_g_cost)) + # + coords_roi = coords[:, :, ymin:ymax, xmin:xmax] + out = self.argmin( + current_g_cost, coords_roi[mask.expand_as(coords_roi)] + ) + return out + + def get_clean_costmap_and_goodmask(self): + good_mask = 1 - F.max_pool2d(self.obstacles, 3, stride=1, padding=1) + costmap = self.g_map + obstacle_cost_corrected = 10000.0 + sampling_map = torch.clamp(costmap, min=0, max=obstacle_cost_corrected) + return sampling_map, good_mask + + def reconstruct_path(self): + out_path = [] + goal_coords = ( + self.goal_coords.cpu() + ) + start_coords = ( + self.start_coords.cpu() + ) + + cost = self.g_map[:, :, f2ind(goal_coords, 0), f2ind(goal_coords, 1)] + # Traversing + done = False + node_coords = goal_coords.cpu() + out_path.append(node_coords) + self.been_there = 0 * self.been_there.cpu() + self.been_there[ + :, :, f2ind(node_coords, 0), f2ind(node_coords, 1) + ] = 1.0 + self.close_list_map = self.close_list_map.cpu() + self.g_map = self.g_map.cpu() + self.coords = self.coords.cpu() + count1 = 0 + while not done: + node_coords = self.propagate_traversal( + node_coords, self.close_list_map, self.g_map, self.coords + ) + self.been_there[ + :, :, f2ind(node_coords, 0), f2ind(node_coords, 1) + ] = 1.0 + if torch.norm(node_coords - out_path[-1], 2).item() < 0.3: + y = node_coords.flatten()[0].long() + x = node_coords.flatten()[1].long() + print(self.g_map[0, 0, y - 2:y + 3, x - 2:x + 3]) + print("loop in out_path", node_coords) + raise ValueError("loop in out_path") + return out_path, cost + out_path.append(node_coords) + done = torch.norm(node_coords - start_coords.cpu(), 2).item() < 0.3 + count1 += 1 + if count1 > 250: + break + return out_path, cost diff --git a/baselines/slambased/reprojection.py b/baselines/slambased/reprojection.py new file mode 100644 index 000000000..566b47745 --- /dev/null +++ b/baselines/slambased/reprojection.py @@ -0,0 +1,289 @@ +import numpy as np +import torch +from math import ceil, floor + + + +def p_zx(p): + return p[(0, 2), 3] + + +def get_map_size_in_cells(map_size_in_meters, cell_size_in_meters): + return int(ceil(map_size_in_meters / cell_size_in_meters)) + 1 + + +def get_pos_diff(p_init, p_fin): + return p_zx(p_fin) - p_zx(p_init) + + +def get_distance(p_init, p_fin): + return torch.norm(get_pos_diff(p_init, p_fin)) + + +def get_pos_diffs(ps): + return ps[1:, (0, 2), 3] - ps[: (ps.size(0) - 1), (0, 2), 3] + + +def angle_to_pi_2_minus_pi_2(angle): + if angle < -np.pi: + angle = 2.0 * np.pi + angle + if angle > np.pi: + angle = -2.0 * np.pi + angle + return angle + + +def get_direction(p_init, p_fin, ang_th=0.2, pos_th=0.1): + pos_diff = get_pos_diff(p_init, p_fin) + if torch.norm(pos_diff, 2).item() < pos_th: + return 0 + else: + needed_angle = torch.atan2(pos_diff[1], pos_diff[0]) + current_angle = torch.atan2(p_init[2, 0], p_init[0, 0]) + to_rotate = angle_to_pi_2_minus_pi_2( + -np.pi / 2.0 + needed_angle - current_angle + ) + if torch.abs(to_rotate).item() < ang_th: + return 0 + return to_rotate + + +def reproject_local_to_global(xyz_local, p): + device = xyz_local.device + num, dim = xyz_local.size() + if dim == 3: + xyz = torch.cat( + [ + xyz_local, + torch.ones((num, 1), dtype=torch.float32, device=device), + ], + dim=1, + ) + elif dim == 4: + xyz = xyz_local + else: + raise ValueError( + "3d point cloud dim is neighter 3, or 4 (homogenious)" + ) + # print(xyz.shape, P.shape) + xyz_global = torch.mm(p.squeeze(), xyz.t()) + return xyz_global.t() + + +def project2d_pcl_into_worldmap(zx, map_size, cell_size): + device = zx.device + shift = int(floor(get_map_size_in_cells(map_size, cell_size) / 2.0)) + topdown2index = torch.tensor( + [[1.0 / cell_size, 0, shift], [0, 1.0 / cell_size, shift], [0, 0, 1]], + device=device, + ) + world_coords_h = torch.cat( + [zx.view(-1, 2), torch.ones((len(zx), 1), device=device)], dim=1 + ) + world_coords = torch.mm(topdown2index, world_coords_h.t()) + return world_coords.t()[:, :2] + + +def get_pose2d(poses6d): + poses6d = poses6d.view(-1, 4, 4) + poses2d = poses6d[:, (0, 2)] + poses2d = poses2d[:, :, (0, 2, 3)] + return poses2d + + +def get_rotation_matrix(angle_in_radians): + angle_in_radians = angle_in_radians.view(-1, 1, 1) + sin_a = torch.sin(angle_in_radians) + cos_a = torch.cos(angle_in_radians) + a1x = torch.cat([cos_a, sin_a], dim=2) + a2x = torch.cat([-sin_a, cos_a], dim=2) + transform = torch.cat([a1x, a2x], dim=1) + return transform + + +def normalize_zx_ori(p): + p2d = get_pose2d(p) + norms = torch.norm(p2d[:, 0, :2], dim=1).view(-1, 1, 1) + out = torch.cat( + [ + torch.cat( + [p[:, :3, :3] / norms.expand(p.size(0), 3, 3), p[:, 3:, :3]], + dim=1, + ), + p[:, :, 3:], + ], + dim=2, + ) + return out + + +def add_rot_wps(p): + planned_tps_norm = normalize_zx_ori(p) + pos_diffs = get_pos_diffs(planned_tps_norm) + + angles = torch.atan2(pos_diffs[:, 1], pos_diffs[:, 0]) + rotmats = get_rotation_matrix(angles) + planned_tps_norm[:p.size(0) - 1, 0, 0] = rotmats[:, 0, 0] + planned_tps_norm[:p.size(0) - 1, 0, 2] = rotmats[:, 0, 1] + planned_tps_norm[:p.size(0) - 1, 2, 0] = rotmats[:, 1, 0] + planned_tps_norm[:p.size(0) - 1, 2, 2] = rotmats[:, 1, 1] + + planned_points2 = planned_tps_norm.clone() + + planned_points2[1:, 0, 0] = planned_tps_norm[:p.size(0) - 1, 0, 0] + planned_points2[1:, 0, 2] = planned_tps_norm[:p.size(0) - 1, 0, 2] + planned_points2[1:, 2, 0] = planned_tps_norm[:p.size(0) - 1, 2, 0] + planned_points2[1:, 2, 2] = planned_tps_norm[:p.size(0) - 1, 2, 2] + out = torch.stack( + (planned_points2.unsqueeze(0), planned_tps_norm.unsqueeze(0)), dim=0 + ).squeeze() + out = out.permute(1, 0, 2, 3).contiguous().view(-1, 4, 4) + return out + + +def planned_path2tps(path, cell_size, map_size, agent_h, add_rot=False): + '''Path is list of 2d coordinates from planner, in map cells. + tp is trajectory pose, 4x4 matrix - same format, + as in localization module + ''' + path = torch.cat(path).view(-1, 2) + # print(path.size()) + num_pts = len(path) + planned_tps = torch.eye(4).unsqueeze(0).repeat((num_pts, 1, 1)) + planned_tps[:, 0, 3] = path[:, 1] # switch back x and z + planned_tps[:, 1, 3] = agent_h + planned_tps[:, 2, 3] = path[:, 0] # switch back x and z + shift = int(floor(get_map_size_in_cells(map_size, cell_size) / 2.0)) + planned_tps[:, 0, 3] = planned_tps[:, 0, 3] - shift + planned_tps[:, 2, 3] = planned_tps[:, 2, 3] - shift + p = torch.tensor( + [ + [1.0 / cell_size, 0, 0, 0], + [0, 1.0 / cell_size, 0, 0], + [0, 0, 1.0 / cell_size, 0], + [0, 0, 0, 1], + ] + ) + planned_tps = torch.bmm( + p.inverse().unsqueeze(0).expand(num_pts, 4, 4), planned_tps + ) + if add_rot: + return add_rot_wps(planned_tps) + return planned_tps + + +def habitat_goalpos_to_tp(ro_phi, p_curr): + '''Convert distance and azimuth to + trajectory pose, 4x4 matrix - same format, + as in localization module + ''' + device = ro_phi.device + offset = torch.tensor( + [ + -ro_phi[0] * torch.sin(ro_phi[1]), + 0, + ro_phi[0] * torch.cos(ro_phi[1]), + ] + ).to(device) + if p_curr.size(1) == 3: + p_curr = homogenize_p(p_curr) + goal_tp = torch.mm( + p_curr.to(device), + torch.cat( + [ + offset + * torch.tensor( + [1.0, 1.0, 1.0], dtype=torch.float32, device=device + ), + torch.tensor([1.0], device=device), + ] + ).reshape(4, 1), + ) + return goal_tp + + +def habitat_goalpos_to_mapgoal_pos(offset, p_curr, cell_size, map_size): + '''Convert distance and azimuth to + map cell coordinates + ''' + device = offset.device + goal_tp = habitat_goalpos_to_tp(offset, p_curr) + goal_tp1 = torch.eye(4).to(device) + goal_tp1[:, 3:] = goal_tp + projected_p = project_tps_into_worldmap( + goal_tp1.view(1, 4, 4), cell_size, map_size + ) + return projected_p + + + +def homogenize_p(tps): + device = tps.device + tps = tps.view(-1, 3, 4) + return torch.cat( + [ + tps.float(), + torch.tensor([0, 0, 0, 1.0]) + .view(1, 1, 4) + .expand(tps.size(0), 1, 4) + .to(device), + ], + dim=1, + ) + + +def project_tps_into_worldmap(tps, cell_size, map_size, do_floor=True): + '''Convert 4x4 pose matrices (trajectory poses) to + map cell coordinates + ''' + if len(tps) == 0: + return [] + if isinstance(tps, list): + return [] + device = tps.device + topdown_p = torch.tensor([[1.0, 0, 0, 0], [0, 0, 1.0, 0]]).to(device) + world_coords = torch.bmm( + topdown_p.view(1, 2, 4).expand(tps.size(0), 2, 4), + tps[:, :, 3:].view(-1, 4, 1), + ) + shift = int(floor(get_map_size_in_cells(map_size, cell_size) / 2.0)) + topdown2index = torch.tensor( + [[1.0 / cell_size, 0, shift], [0, 1.0 / cell_size, shift], [0, 0, 1]] + ).to(device) + world_coords_h = torch.cat( + [world_coords, torch.ones((len(world_coords), 1, 1)).to(device)], dim=1 + ) + world_coords = torch.bmm( + topdown2index.unsqueeze(0).expand(world_coords_h.size(0), 3, 3), + world_coords_h, + )[:, :2, 0] + if do_floor: + return ( + torch.floor(world_coords.flip(1)) + 1 + ) # for having revesrve (z,x) ordering + return world_coords.flip(1) + + +def project_tps_into_worldmap_numpy(tps, slam_to_world, cell_size, map_size): + if len(tps) == 0: + return [] + if isinstance(tps, list): + return [] + # tps is expected in [n,4,4] format + topdown_p = np.array([[slam_to_world, 0, 0, 0], [0, 0, slam_to_world, 0]]) + try: + world_coords = np.matmul( + topdown_p.reshape(1, 2, 4), tps[:, :, 3:].reshape(-1, 4, 1) + ) + except BaseException: + return [] + shift = int(floor(get_map_size_in_cells(map_size, cell_size) / 2.0)) + topdown2index = np.array( + [[1.0 / cell_size, 0, shift], [0, 1.0 / cell_size, shift], [0, 0, 1]] + ) + world_coords_h = np.concatenate( + [world_coords, np.ones((len(world_coords), 1, 1))], axis=1 + ) + world_coords = np.matmul(topdown2index, world_coords_h)[:, :2, 0] + return ( + world_coords[:, ::-1].astype(np.int32) + 1 + ) # for having revesrve (z,x) ordering diff --git a/baselines/slambased/utils.py b/baselines/slambased/utils.py new file mode 100644 index 000000000..8b39671c7 --- /dev/null +++ b/baselines/slambased/utils.py @@ -0,0 +1,43 @@ +import numpy as np +import torch +import time +from PIL import Image + + +def generate_2dgrid(h, w, centered=False): + if centered: + x = torch.linspace(-w / 2 + 1, w / 2, w) + y = torch.linspace(-h / 2 + 1, h / 2, h) + else: + x = torch.linspace(0, w - 1, w) + y = torch.linspace(0, h - 1, h) + grid2d = torch.stack( + [y.repeat(w, 1).t().contiguous().view(-1), x.repeat(h)], 1 + ) + return grid2d.view(1, h, w, 2).permute(0, 3, 1, 2) + + +def str2bool(v): + if v.lower() in ("yes", "true", "t", "y", "1"): + return True + elif v.lower() in ("no", "false", "f", "n", "0"): + return False + + +def resize_pil(np_img, size=128): + im1 = Image.fromarray(np_img) + im1.thumbnail((size, size)) + return np.array(im1) + + +def find_map_size(h, w): + map_size_in_meters = int(0.1 * 3 * max(h, w)) + if map_size_in_meters % 10 != 0: + map_size_in_meters = map_size_in_meters + ( + 10 - (map_size_in_meters % 10) + ) + return map_size_in_meters + + +def gettimestr(): + return time.strftime("%Y-%m-%d--%H_%M_%S", time.gmtime()) diff --git a/configs/tasks/pointnav_rgbd.yaml b/configs/tasks/pointnav_rgbd.yaml new file mode 100644 index 000000000..9635a93b0 --- /dev/null +++ b/configs/tasks/pointnav_rgbd.yaml @@ -0,0 +1,24 @@ +ENVIRONMENT: + MAX_EPISODE_STEPS: 500 +SIMULATOR: + AGENT_0: + SENSORS: ['RGB_SENSOR', 'DEPTH_SENSOR'] + HABITAT_SIM_V0: + GPU_DEVICE_ID: 0 + RGB_SENSOR: + WIDTH: 256 + HEIGHT: 256 + DEPTH_SENSOR: + WIDTH: 256 + HEIGHT: 256 +TASK: + TYPE: Nav-v0 + SUCCESS_DISTANCE: 0.2 + SENSORS: ['POINTGOAL_SENSOR'] + POINTGOAL_SENSOR: + TYPE: PointGoalSensor + GOAL_FORMAT: POLAR + MEASUREMENTS: ['SPL'] + SPL: + TYPE: SPL + SUCCESS_DISTANCE: 0.2 -- GitLab