Three challenge tracks for Locobot

This commit is contained in:
Chengshu Li 2020-02-17 16:10:56 -08:00
parent 318b9aa99f
commit a5705330a9
9 changed files with 801 additions and 1087 deletions

View File

@ -0,0 +1,63 @@
# scene
scene: building
model_id: Ohoopee
build_graph: true
load_texture: true
trav_map_erosion: 2
# robot
robot: Locobot
velocity: 0.1
# task, observation and action
task: pointgoal # pointgoal|objectgoal|areagoal|reaching
fisheye: false
is_discrete: false
additional_states_dim: 3
# reward
reward_type: geodesic
success_reward: 10.0
slack_reward: -0.01
potential_reward_weight: 1.0
collision_reward_weight: -0.1
collision_ignore_link_a_ids: [1, 2, 3, 4] # ignore collision with these agent's link ids
# discount factor
discount_factor: 0.99
# termination condition
death_z_thresh: 0.2
dist_tol: 0.333 # body width
max_step: 500
# sensor
output: [sensor, rgb, depth]
resolution: 256
fov: 90
# display
use_filler: true
display_ui: false
show_diagnostics: false
ui_num: 2
ui_components: [RGB_FILLED, DEPTH]
random:
random_initial_pose: false
random_target_pose: false
random_init_x_range: [-0.1, 0.1]
random_init_y_range: [-0.1, 0.1]
random_init_z_range: [-0.1, 0.1]
random_init_rot_range: [-0.1, 0.1]
speed:
timestep: 0.001
frameskip: 10
mode: web_ui #gui|headless
verbose: false
fast_lq_render: true
# debug
debug: false

View File

@ -208,7 +208,7 @@ class InteractiveObj(object):
org_pos, org_orn = p.getBasePositionAndOrientation(self.body_id) org_pos, org_orn = p.getBasePositionAndOrientation(self.body_id)
p.resetBasePositionAndOrientation(self.body_id, pos, org_orn) p.resetBasePositionAndOrientation(self.body_id, pos, org_orn)
def set_position_rotation(self, pos, orn): def set_position_orientation(self, pos, orn):
p.resetBasePositionAndOrientation(self.body_id, pos, orn) p.resetBasePositionAndOrientation(self.body_id, pos, orn)

View File

@ -95,6 +95,10 @@ class LocomotorRobot(BaseRobot):
def set_orientation(self, orn): def set_orientation(self, orn):
self.robot_body.set_orientation(orn) self.robot_body.set_orientation(orn)
def set_position_orientation(self, pos, orn):
self.robot_body.reset_position(pos)
self.robot_body.set_orientation(orn)
def move_by(self, delta): def move_by(self, delta):
new_pos = np.array(delta) + self.get_position() new_pos = np.array(delta) + self.get_position()
self.robot_body.reset_position(new_pos) self.robot_body.reset_position(new_pos)
@ -117,6 +121,10 @@ class LocomotorRobot(BaseRobot):
new_orn = qmult((euler2quat(delta, 0, 0)), orn) new_orn = qmult((euler2quat(delta, 0, 0)), orn)
self.robot_body.set_orientation(new_orn) self.robot_body.set_orientation(new_orn)
def keep_still(self):
for n, j in enumerate(self.ordered_joints):
j.set_motor_velocity(0.0)
def get_rpy(self): def get_rpy(self):
return self.robot_body.get_rpy() return self.robot_body.get_rpy()
@ -709,3 +717,70 @@ class JR2_Kinova(LocomotorRobot):
for j in range(16, 28): for j in range(16, 28):
p.setCollisionFilterPair(robot_id, robot_id, joint, j, 0) p.setCollisionFilterPair(robot_id, robot_id, joint, j, 0)
return ids return ids
class Locobot(LocomotorRobot):
mjcf_scaling = 1
model_type = "URDF"
default_scale = 1
def __init__(self, config):
self.config = config
self.velocity = config.get('velocity', 0.1)
self.wheel_dim = 2
self.action_high = config.get("action_high", None)
self.action_low = config.get("action_low", None)
LocomotorRobot.__init__(self,
"locobot/locobot.urdf",
"base_link",
action_dim=self.wheel_dim,
sensor_dim=55, # TODO: what is sensor_dim?
power=2.5,
scale=config.get("robot_scale", self.default_scale),
resolution=config.get("resolution", 64),
is_discrete=config.get("is_discrete", True),
control="velocity",
self_collision=False)
def set_up_continuous_action_space(self):
if self.action_high is not None and self.action_low is not None:
self.action_high = np.full(shape=self.wheel_dim, fill_value=self.action_high)
self.action_low = np.full(shape=self.wheel_dim, fill_value=self.action_low)
else:
self.action_high = np.array([self.velocity] * self.wheel_dim)
self.action_low = -self.action_high
self.action_space = gym.spaces.Box(shape=(self.action_dim,),
low=-1.0,
high=1.0,
dtype=np.float32)
def set_up_discrete_action_space(self):
assert False, "Locobot does not support discrete actions"
def apply_action(self, action):
real_action = self.action_to_real_action(action)
self.apply_real_action(real_action)
def calc_state(self):
base_state = LocomotorRobot.calc_state(self)
angular_velocity = self.robot_body.angular_velocity()
print(len(base_state), len(angular_velocity))
return np.concatenate((base_state, np.array(angular_velocity)))
def get_end_effector_position(self):
return self.parts['gripper_link'].get_position()
def load(self):
ids = self._load_model()
self.eyes = self.parts["eyes"]
robot_id = ids[0]
# disable collision for immediate parent
for joint in range(p.getNumJoints(robot_id)):
info = p.getJointInfo(robot_id, joint)
parent_id = info[-1]
p.setCollisionFilterPair(robot_id, robot_id, joint, parent_id, 0)
return ids

View File

@ -6,15 +6,18 @@ parentdir = os.path.dirname(currentdir)
os.sys.path.insert(0, parentdir) os.sys.path.insert(0, parentdir)
import pybullet_data import pybullet_data
from gibson2.data.datasets import get_model_path from gibson2.data.datasets import get_model_path
from gibson2.utils.utils import l2_distance
import numpy as np import numpy as np
from PIL import Image from PIL import Image
import cv2 import cv2
import networkx as nx import networkx as nx
from IPython import embed from IPython import embed
import pickle
class Scene: class Scene:
def load(self): def load(self):
raise (NotImplementedError()) raise NotImplementedError()
class EmptyScene(Scene): class EmptyScene(Scene):
""" """
@ -23,6 +26,7 @@ class EmptyScene(Scene):
def load(self): def load(self):
planeName = os.path.join(pybullet_data.getDataPath(), "mjcf/ground_plane.xml") planeName = os.path.join(pybullet_data.getDataPath(), "mjcf/ground_plane.xml")
self.ground_plane_mjcf = p.loadMJCF(planeName) self.ground_plane_mjcf = p.loadMJCF(planeName)
p.changeDynamics(self.ground_plane_mjcf[0], -1, lateralFriction=1)
return [item for item in self.ground_plane_mjcf] return [item for item in self.ground_plane_mjcf]
class StadiumScene(Scene): class StadiumScene(Scene):
@ -43,6 +47,9 @@ class StadiumScene(Scene):
return [item for item in self.stadium] + [item for item in self.ground_plane_mjcf] return [item for item in self.stadium] + [item for item in self.ground_plane_mjcf]
def get_random_floor(self):
return 0
def get_random_point(self, random_height=False): def get_random_point(self, random_height=False):
return self.get_random_point_floor(0, random_height) return self.get_random_point_floor(0, random_height)
@ -54,6 +61,17 @@ class StadiumScene(Scene):
np.random.uniform(0.4, 0.8) if random_height else 0.0 np.random.uniform(0.4, 0.8) if random_height else 0.0
]) ])
def get_floor_height(self, floor):
del floor
return 0.0
def reset_floor(self, floor=0, additional_elevation=0.05, height=None):
return
def get_shortest_path(self, floor, source_world, target_world, entire_path=False):
assert False, 'cannot compute shortest path in StadiumScene'
class BuildingScene(Scene): class BuildingScene(Scene):
""" """
Gibson Environment building scenes Gibson Environment building scenes
@ -90,9 +108,6 @@ class BuildingScene(Scene):
self.num_waypoints = num_waypoints self.num_waypoints = num_waypoints
self.waypoint_interval = int(waypoint_resolution / trav_map_resolution) self.waypoint_interval = int(waypoint_resolution / trav_map_resolution)
def l2_distance(self, a, b):
return np.linalg.norm(np.array(a) - np.array(b))
def load(self): def load(self):
""" """
Load the mesh into pybullet Load the mesh into pybullet
@ -141,19 +156,34 @@ class BuildingScene(Scene):
self.floors = sorted(list(map(float, f.readlines()))) self.floors = sorted(list(map(float, f.readlines())))
print('floors', self.floors) print('floors', self.floors)
for f in range(len(self.floors)): for f in range(len(self.floors)):
trav_map = Image.open(os.path.join(get_model_path(self.model_id), 'floor_trav_{}.png'.format(f))) trav_map = np.array(Image.open(
obstacle_map = Image.open(os.path.join(get_model_path(self.model_id), 'floor_{}.png'.format(f))) os.path.join(get_model_path(self.model_id), 'floor_trav_{}.png'.format(f))
))
obstacle_map = np.array(Image.open(
os.path.join(get_model_path(self.model_id), 'floor_{}.png'.format(f))
))
if self.trav_map_original_size is None: if self.trav_map_original_size is None:
width, height = trav_map.size height, width = trav_map.shape
assert width == height, 'trav map is not a square' assert height == width, 'trav map is not a square'
self.trav_map_original_size = height self.trav_map_original_size = height
self.trav_map_size = int(self.trav_map_original_size * self.trav_map_default_resolution / self.trav_map_resolution) self.trav_map_size = int(self.trav_map_original_size *
trav_map = np.array(trav_map.resize((self.trav_map_size, self.trav_map_size))) self.trav_map_default_resolution /
obstacle_map = np.array(obstacle_map.resize((self.trav_map_size, self.trav_map_size))) self.trav_map_resolution)
trav_map[obstacle_map == 0] = 0 trav_map[obstacle_map == 0] = 0
trav_map = cv2.resize(trav_map, (self.trav_map_size, self.trav_map_size))
trav_map = cv2.erode(trav_map, np.ones((self.trav_map_erosion, self.trav_map_erosion))) trav_map = cv2.erode(trav_map, np.ones((self.trav_map_erosion, self.trav_map_erosion)))
trav_map[trav_map < 255] = 0
if self.build_graph: if self.build_graph:
graph_file = os.path.join(get_model_path(self.model_id), 'floor_trav_{}.p'.format(f))
if os.path.isfile(graph_file):
print("load traversable graph")
with open(graph_file, 'rb') as pfile:
g = pickle.load(pfile)
else:
print("build traversable graph")
g = nx.Graph() g = nx.Graph()
for i in range(self.trav_map_size): for i in range(self.trav_map_size):
for j in range(self.trav_map_size): for j in range(self.trav_map_size):
@ -164,16 +194,18 @@ class BuildingScene(Scene):
for n in neighbors: for n in neighbors:
if 0 <= n[0] < self.trav_map_size and 0 <= n[1] < self.trav_map_size and \ if 0 <= n[0] < self.trav_map_size and 0 <= n[1] < self.trav_map_size and \
trav_map[n[0], n[1]] > 0: trav_map[n[0], n[1]] > 0:
g.add_edge(n, (i, j), weight=self.l2_distance(n, (i, j))) g.add_edge(n, (i, j), weight=l2_distance(n, (i, j)))
# only take the largest connected component # only take the largest connected component
largest_cc = max(nx.connected_components(g), key=len) largest_cc = max(nx.connected_components(g), key=len)
g = g.subgraph(largest_cc).copy() g = g.subgraph(largest_cc).copy()
self.floor_graph.append(g) with open(graph_file, 'wb') as pfile:
pickle.dump(g, pfile, protocol=pickle.HIGHEST_PROTOCOL)
self.floor_graph.append(g)
# update trav_map accordingly # update trav_map accordingly
trav_map[:, :] = 0 trav_map[:, :] = 0
for node in largest_cc: for node in g.nodes:
trav_map[node[0], node[1]] = 255 trav_map[node[0], node[1]] = 255
self.floor_map.append(trav_map) self.floor_map.append(trav_map)
@ -205,6 +237,11 @@ class BuildingScene(Scene):
def world_to_map(self, xy): def world_to_map(self, xy):
return np.flip((xy / self.trav_map_resolution + self.trav_map_size / 2.0)).astype(np.int) return np.flip((xy / self.trav_map_resolution + self.trav_map_size / 2.0)).astype(np.int)
def has_node(self, floor, world_xy):
map_xy = tuple(self.world_to_map(world_xy))
g = self.floor_graph[floor]
return g.has_node(map_xy)
def get_shortest_path(self, floor, source_world, target_world, entire_path=False): def get_shortest_path(self, floor, source_world, target_world, entire_path=False):
# print("called shortest path", source_world, target_world) # print("called shortest path", source_world, target_world)
assert self.build_graph, 'cannot get shortest path without building the graph' assert self.build_graph, 'cannot get shortest path without building the graph'
@ -216,20 +253,21 @@ class BuildingScene(Scene):
if not g.has_node(target_map): if not g.has_node(target_map):
nodes = np.array(g.nodes) nodes = np.array(g.nodes)
closest_node = tuple(nodes[np.argmin(np.linalg.norm(nodes - target_map, axis=1))]) closest_node = tuple(nodes[np.argmin(np.linalg.norm(nodes - target_map, axis=1))])
g.add_edge(closest_node, target_map, weight=self.l2_distance(closest_node, target_map)) g.add_edge(closest_node, target_map, weight=l2_distance(closest_node, target_map))
if not g.has_node(source_map): if not g.has_node(source_map):
nodes = np.array(g.nodes) nodes = np.array(g.nodes)
closest_node = tuple(nodes[np.argmin(np.linalg.norm(nodes - source_map, axis=1))]) closest_node = tuple(nodes[np.argmin(np.linalg.norm(nodes - source_map, axis=1))])
g.add_edge(closest_node, source_map, weight=self.l2_distance(closest_node, source_map)) g.add_edge(closest_node, source_map, weight=l2_distance(closest_node, source_map))
path_map = np.array(nx.astar_path(g, source_map, target_map, heuristic=self.l2_distance)) path_map = np.array(nx.astar_path(g, source_map, target_map, heuristic=l2_distance))
path_world = self.map_to_world(path_map) path_world = self.map_to_world(path_map)
geodesic_distance = np.sum(np.linalg.norm(path_world[1:] - path_world[:-1], axis=1)) geodesic_distance = np.sum(np.linalg.norm(path_world[1:] - path_world[:-1], axis=1))
path_world = path_world[::self.waypoint_interval]
if not entire_path: if not entire_path:
path_world = path_world[::self.waypoint_interval][:self.num_waypoints] path_world = path_world[:self.num_waypoints]
num_remaining_waypoints = self.num_waypoints - path_world.shape[0] num_remaining_waypoints = self.num_waypoints - path_world.shape[0]
if num_remaining_waypoints > 0: if num_remaining_waypoints > 0:
remaining_waypoints = np.tile(target_world, (num_remaining_waypoints, 1)) remaining_waypoints = np.tile(target_world, (num_remaining_waypoints, 1))

View File

@ -36,6 +36,7 @@ class Simulator:
self.gravity = gravity self.gravity = gravity
self.timestep = timestep self.timestep = timestep
self.mode = mode self.mode = mode
# renderer # renderer
self.resolution = resolution self.resolution = resolution
self.fov = fov self.fov = fov
@ -63,8 +64,7 @@ class Simulator:
""" """
Destroy the MeshRenderer and physics simulator and start again. Destroy the MeshRenderer and physics simulator and start again.
""" """
self.renderer.release() self.disconnect()
p.disconnect(self.cid)
self.load() self.load()
def load(self): def load(self):
@ -91,6 +91,7 @@ class Simulator:
self.cid = p.connect(p.DIRECT) self.cid = p.connect(p.DIRECT)
p.setTimeStep(self.timestep) p.setTimeStep(self.timestep)
p.setGravity(0, 0, -self.gravity) p.setGravity(0, 0, -self.gravity)
p.setPhysicsEngineParameter(enableFileCaching=0)
if self.mode == 'gui' and not self.render_to_tensor: if self.mode == 'gui' and not self.render_to_tensor:
self.add_viewer() self.add_viewer()
@ -101,14 +102,13 @@ class Simulator:
self.objects = [] self.objects = []
def import_scene(self, scene, texture_scale=1.0, load_texture=True, class_id=0): def import_scene(self, scene, texture_scale=1.0, load_texture=True, class_id=0):
""" """
Import a scene. A scene could be a synthetic one or a realistic Gibson Environment. Import a scene. A scene could be a synthetic one or a realistic Gibson Environment.
:param scene: Scene object :param scene: Scene object
:param texture_scale: Option to scale down the texture for rendering :param texture_scale: Option to scale down the texture for rendering
:param load_texture: If you don't need rgb output, texture loading could be skipped to make rendering faster :param load_texture: If you don't need rgb output, texture loading could be skipped to make rendering faster
:param class_id: The class_id for background for rendering semantics. :param class_id: Class id for rendering semantic segmentation
""" """
new_objects = scene.load() new_objects = scene.load()
@ -153,7 +153,7 @@ class Simulator:
def import_object(self, object, class_id=0): def import_object(self, object, class_id=0):
""" """
:param object: Object to load :param object: Object to load
:param class_id: class_id to show for semantic segmentation mask :param class_id: Class id for rendering semantic segmentation
""" """
new_object = object.load() new_object = object.load()
self.objects.append(new_object) self.objects.append(new_object)
@ -215,7 +215,7 @@ class Simulator:
Import a robot into Simulator Import a robot into Simulator
:param robot: Robot :param robot: Robot
:param class_id: class_id to show for semantic segmentation mask :param class_id: Class id for rendering semantic segmentation
:return: id for robot in pybullet :return: id for robot in pybullet
""" """
@ -298,7 +298,7 @@ class Simulator:
Import articulated objects into simulator Import articulated objects into simulator
:param obj: :param obj:
:param class_id: class_id to show for semantic segmentation mask :param class_id: Class id for rendering semantic segmentation
:return: pybulet id :return: pybulet id
""" """
ids = obj.load() ids = obj.load()
@ -382,6 +382,12 @@ class Simulator:
""" """
p.stepSimulation() p.stepSimulation()
self.sync()
def sync(self):
"""
Update positions in renderer without stepping the simulation. Usually used in the reset() function
"""
for instance in self.renderer.instances: for instance in self.renderer.instances:
if instance.dynamic: if instance.dynamic:
self.update_position(instance) self.update_position(instance)
@ -427,5 +433,6 @@ class Simulator:
clean up the simulator clean up the simulator
""" """
if self.isconnected(): if self.isconnected():
p.resetSimulation(physicsClientId=self.cid)
p.disconnect(self.cid) p.disconnect(self.cid)
self.renderer.release() self.renderer.release()

View File

@ -1,4 +1,5 @@
from gibson2.core.physics.robot_locomotors import Turtlebot, Husky, Ant, Humanoid, JR2, JR2_Kinova, Freight, Fetch from gibson2.core.physics.robot_locomotors \
import Turtlebot, Husky, Ant, Humanoid, JR2, JR2_Kinova, Freight, Fetch, Locobot
from gibson2.core.simulator import Simulator from gibson2.core.simulator import Simulator
from gibson2.core.physics.scene import BuildingScene, StadiumScene from gibson2.core.physics.scene import BuildingScene, StadiumScene
import gibson2 import gibson2
@ -16,21 +17,31 @@ class BaseEnv(gym.Env):
config_file, config_file,
model_id=None, model_id=None,
mode='headless', mode='headless',
action_timestep=1 / 10.0,
physics_timestep=1 / 240.0,
device_idx=0): device_idx=0):
""" """
:param config_file: config_file path :param config_file: config_file path
:param model_id: override model_id in config file :param model_id: override model_id in config file
:param mode: headless or gui mode :param mode: headless or gui mode
:param device_idx: which GPU to run the simulation and rendering on :param action_timestep: environment executes action per action_timestep second
:param physics_timestep: physics timestep for pybullet
:param device_idx: device_idx: which GPU to run the simulation and rendering on
""" """
self.config = parse_config(config_file) self.config = parse_config(config_file)
if model_id is not None: if model_id is not None:
self.config['model_id'] = model_id self.config['model_id'] = model_id
self.mode = mode
self.action_timestep = action_timestep
self.physics_timestep = physics_timestep
self.simulator = Simulator(mode=mode, self.simulator = Simulator(mode=mode,
timestep=physics_timestep,
use_fisheye=self.config.get('fisheye', False), use_fisheye=self.config.get('fisheye', False),
resolution=self.config.get('resolution', 64), resolution=self.config.get('resolution', 64),
fov=self.config.get('fov', 90), fov=self.config.get('fov', 90),
device_idx=device_idx) device_idx=device_idx)
self.simulator_loop = int(self.action_timestep / self.simulator.timestep)
self.load() self.load()
def reload(self, config_file): def reload(self, config_file):
@ -77,6 +88,8 @@ class BaseEnv(gym.Env):
robot = Freight(self.config) robot = Freight(self.config)
elif self.config['robot'] == 'Fetch': elif self.config['robot'] == 'Fetch':
robot = Fetch(self.config) robot = Fetch(self.config)
elif self.config['robot'] == 'Locobot':
robot = Locobot(self.config)
else: else:
raise Exception('unknown robot type: {}'.format(self.config['robot'])) raise Exception('unknown robot type: {}'.format(self.config['robot']))
@ -99,10 +112,16 @@ class BaseEnv(gym.Env):
self.simulator.step() self.simulator.step()
def step(self, action): def step(self, action):
return NotImplementedError """
Overwritten by subclasses
"""
return NotImplementedError()
def reset(self): def reset(self):
return NotImplementedError """
Overwritten by subclasses
"""
return NotImplementedError()
def set_mode(self, mode): def set_mode(self, mode):
self.simulator.mode = mode self.simulator.mode = mode

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import numpy as np import numpy as np
import yaml import yaml
from scipy.spatial.transform import Rotation as R
# File I/O related # File I/O related
def parse_config(config): def parse_config(config):
@ -8,19 +8,29 @@ def parse_config(config):
config_data = yaml.load(f, Loader=yaml.FullLoader) config_data = yaml.load(f, Loader=yaml.FullLoader)
return config_data return config_data
# Geometry related # Geometry related
def rotate_vector_3d(v, r, p, y): def rotate_vector_3d(v, r, p, y):
"""Rotates vector by roll, pitch and yaw counterclockwise""" """Rotates 3d vector by roll, pitch and yaw counterclockwise"""
rot_x = np.array([[1, 0, 0], [0, np.cos(-r), -np.sin(-r)], [0, np.sin(-r), np.cos(-r)]]) local_to_global = R.from_euler('xyz', [r, p, y]).as_dcm()
rot_y = np.array([[np.cos(-p), 0, np.sin(-p)], [0, 1, 0], [-np.sin(-p), 0, np.cos(-p)]]) global_to_local = local_to_global.T
rot_z = np.array([[np.cos(-y), -np.sin(-y), 0], [np.sin(-y), np.cos(-y), 0], [0, 0, 1]]) return np.dot(global_to_local, v)
return np.dot(rot_x, np.dot(rot_y, np.dot(rot_z, v)))
def rotate_vector_2d(v, yaw):
"""Rotates 2d vector by yaw counterclockwise"""
local_to_global = R.from_euler('z', yaw).as_dcm()
global_to_local = local_to_global.T
global_to_local = global_to_local[:2, :2]
if len(v.shape) == 1:
return np.dot(global_to_local, v)
elif len(v.shape) == 2:
return np.dot(global_to_local, v.T).T
else:
print('Incorrect input shape for rotate_vector_2d', v.shape)
return v
def l2_distance(v1, v2): def l2_distance(v1, v2):
"""Returns the L2 distance between vector v1 and v2.""" """Returns the L2 distance between vector v1 and v2."""
return np.linalg.norm(v1 - v2) return np.linalg.norm(np.array(v1) - np.array(v2))
def quatFromXYZW(xyzw, seq): def quatFromXYZW(xyzw, seq):

View File

@ -110,7 +110,7 @@ setup(
'gym==0.12', 'gym==0.12',
'Pillow==5.4.1', 'Pillow==5.4.1',
'PyYAML==5.3', 'PyYAML==5.3',
'pybullet==2.4.1', 'pybullet==2.4.9',
'transforms3d==0.3.1', 'transforms3d==0.3.1',
'tqdm==4.19.9', 'tqdm==4.19.9',
'Pillow==5.4.1', 'Pillow==5.4.1',