diff --git a/modules/Motor_Controller b/modules/Motor_Controller deleted file mode 160000 index bdad5d0f6f49b1d5ae7f1ed59e6d0d308cad605f..0000000000000000000000000000000000000000 --- a/modules/Motor_Controller +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bdad5d0f6f49b1d5ae7f1ed59e6d0d308cad605f diff --git a/modules/__init__.py b/modules/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/modules/robot.yaml b/modules/robot.yaml deleted file mode 100644 index f23471a1be074697ae3c5d878e121bbcc0e49829..0000000000000000000000000000000000000000 --- a/modules/robot.yaml +++ /dev/null @@ -1,45 +0,0 @@ -base_footprint2base_link : [0., 0., 0.125] -base_link2wheel_left_link : [0., 0.220, 0.0] -base_link2wheel_right_link : [0., -0.220, 0.0] -base_link2caster_left_link : [-0.455, 0.102, -0.080] -base_link2caster_right_link : [-0.455, -0.102, -0.080] -base_link2head_1_link : [-0.226, 0.000, 1.192] -base_link2head_2_link : [-0.183, 0.000, 1.284] -base_link2head_front_camera_link : [-0.120, -0.000, 1.466] -base_link2head_front_camera_optical_frame : [-0.120, -0.000, 1.466] -base_footprint_radius: 0.3 -wheel_radius : 0.125 -wheel_width : 0.075 -wheel_separation : 0.44 -wheel_torque : 6.0 -wheel_velocity : 1.0 -caster_radius : 0.045 -caster_width : 0.055 -caster_separation_x : 0.91 -caster_offset_y : 0.1025 -caster_offset_z : -0.08 -total_mass : 54.893 -wheel_mass : 1.65651 -L1 : [[0, 0, 1, 0], [1, 0, 0, 0.182], [0, 1, 0, -0.063], [0, 0, 0, 1]] -L2 : [[0, 0, -1, 0.093], [0, 1, 0, -0.043], [1, 0, 0, 0], [0, 0, 0, 1]] -L3 : [[1, 0, 0, 0], [0, 1, 0, 0.225], [0, 0, 1, -1.192], [0, 0, 0, 1]] -local_map_depth: 3.0 -local_map_px_size: 0.05 -max_speed_motor_wheel : 1.0 -max_forward_speed: 0.5 -max_backward_speed: -0.1 -max_forward_acc: 0.2 -max_ang_vel: 0.5 -max_ang_acc: 0.4 -max_pan_angle : 1.309 -min_pan_angle : -1.309 -max_tilt_angle : 0.4363 -min_tilt_angle : -0.2618 -max_speed_motor_head : 3.0 -head_friction : 1.0 -head_damping : 0.5 -head_cam_fov_h : 62.0 -head_cam_fov_v : 48.0 -base_cam_fov_h : 360.0 -has_pan : True -has_tilt : False diff --git a/modules/social_mpc.yaml b/modules/social_mpc.yaml deleted file mode 100644 index c8c94a312b13bdbc9c111f17dc68b7f22f4f5a85..0000000000000000000000000000000000000000 --- a/modules/social_mpc.yaml +++ /dev/null @@ -1,49 +0,0 @@ -logging_mode: 'INFO' # INFO, WARNING, ERROR, CRITICAL DEBUG -# mpc parameters -h: 0.2 -fw_time: 5. -u_lb: [-3.0, -0.5, -0.1] -u_ub: [3.0, 0.5, 0.2] -reg_parameter: [1., 10., 1.] -max_acceleration: [0.4, 0.4, 0.3] -max_iter_optim: 5 -# targets -goto_target_dim: 8 # x, y, ang, v, w, loss, angle flag, flag -human_target_dim: 8 # x, y, ang, v, w, loss, angle flag, flag -pan_target_dim: 5 # x, y, v, w, flag -target_pose_goto_dim: 5 # x, y, ang, local/global flag, flag -target_pose_look_dim: 4 # x, y, local/global flag, flag -goto_target_id: 14 # id -goto_target_pose: [0., 0., 0., 0., 0.] # pose (x, y, ang),local 1./global 0. flag, flag -human_target_id: -1 -pan_target_id: 13 -min_dist_to_robot: 0.85 # meter -relative_h_pose: [0.90, -0.2] # meter -check_object_fw_traj_offset: 0.4 # meter -# mpc losses parameters -loss: 0 # max_out_y_dist_ang, maxout_x_y_ang by default -wall_avoidance_points: [[0., -0.3]] # [[0., 0.2], [0.15, 0.1], [-0.15, 0.1], [0., -0.3]] # [[0., -0.3]] -weights_description: ['goto_weight', 'human_features_weight', 'object_cost_map_weight', 'social_cost_map_weight', 'cost_map_weight', 'wall_avoidance_weight'] -goto_weight: 1. -human_features_weight: 1. -object_cost_map_weight: 1000. -social_cost_map_weight: 20. -cost_map_weight: 1. -wall_avoidance_weight: 100. -loss_rad: [[0.2, 0.2, 0.1, 0.2, 0.2, 0.2, 0.1], [0.2, 0.2, 0.1, 0.2, 0.2, 0.2, 0.1], [0.2, 0.2, 0.1, 0.2, 0.2, 0.2, 0.1]] # [(x, y , ang) goto goal, (x, y, ang) human features, pan] x3 (for each mpc mode) -loss_coef: [[50, 50, 1, 0, 0, 0, 3], [0, 0, 0, 50, 50, 3, 3], [10, 10, 1, 50, 50, 2, 3]] # [(x, y , ang) goto goal, (x, y, ang) human features, pan] x3 (for each mpc mode) -# cost_map parameters -step_meshes: 5 # sampling every x cm -# ssn model parameters -group_detection_stride: 0.6 -# mpc preprocessing parameters -waypoint_distance: 1.0 #1.2 -orientation_distance_threshold: 0.5 -# controller parameters -path_loop_time: 1. -goal_loop_time: 1. -path_planner_enabled: true -goal_finder_enabled: true -update_goals_enabled: true -# map parameters -dilation_iter: 1 \ No newline at end of file diff --git a/mpi_sim/components/__init__.py b/mpi_sim/components/__init__.py index 7b16621ec69a9cb57e8668fb407e13aa44e6d395..4359fe81f65bdebc1dfc1112c2323a471e07946a 100644 --- a/mpi_sim/components/__init__.py +++ b/mpi_sim/components/__init__.py @@ -3,9 +3,15 @@ try: from mpi_sim.components.ari_navigation import ARINavigation except ImportError: pass +# Uncomment to debug +# from mpi_sim.components.ari_navigation import ARINavigation + from mpi_sim.components.gaze_generator import GazeGenerator from mpi_sim.components.object_detector import ObjectDetector -from mpi_sim.components.occupany_grid_mapping import OccupancyGridMapping +from mpi_sim.components.occupancy_grid_mapping import OccupancyGridMapping +from mpi_sim.components.social_mapping import SocialMapping from mpi_sim.components.social_force_navigation import SocialForceNavigation from mpi_sim.components.speech_generator import SpeechGenerator +from mpi_sim.components.raycast_sensor import RayCastSensor +from mpi_sim.components.point_drawer import PointDrawer diff --git a/mpi_sim/components/ari_navigation.py b/mpi_sim/components/ari_navigation.py index 6663b12cc42c9fea90cf6c60a757193605b7d987..e30a0ae07e7153d093dec4f5c5c3731cbc78544c 100644 --- a/mpi_sim/components/ari_navigation.py +++ b/mpi_sim/components/ari_navigation.py @@ -9,7 +9,8 @@ import yaml from collections import namedtuple from mpi_sim.utils.box2d_utils import constraint_angle import cv2 - +import os +import exputils as eu # TODO: need group identification for SocialMPC @@ -69,6 +70,54 @@ class ARINavigation(GeneratorComponent): dc.max_human_nb = 20 dc.max_groups_world = 10 + dc.robot_config = mpi_sim.AttrDict() + dc.robot_config.base_footprint2base_link = [0., 0., 0.125] + dc.robot_config.base_link2wheel_left_link = [0., 0.220, 0.0] + dc.robot_config.base_link2wheel_right_link = [0., -0.220, 0.0] + dc.robot_config.base_link2caster_left_link = [-0.455, 0.102, -0.080] + dc.robot_config.base_link2caster_right_link = [-0.455, -0.102, -0.080] + dc.robot_config.base_link2head_1_link = [-0.226, 0.000, 1.192] + dc.robot_config.base_link2head_2_link = [-0.183, 0.000, 1.284] + dc.robot_config.base_link2head_front_camera_link = [-0.120, -0.000, 1.466] + dc.robot_config.base_link2head_front_camera_optical_frame = [-0.120, -0.000, 1.466] + dc.robot_config.base_footprint_radius= 0.3 + dc.robot_config.wheel_radius = 0.125 + dc.robot_config.wheel_width = 0.075 + dc.robot_config.wheel_separation = 0.44 + dc.robot_config.wheel_torque = 6.0 + dc.robot_config.wheel_velocity = 1.0 + dc.robot_config.caster_radius = 0.045 + dc.robot_config.caster_width = 0.055 + dc.robot_config.caster_separation_x = 0.91 + dc.robot_config.caster_offset_y = 0.1025 + dc.robot_config.caster_offset_z = -0.08 + dc.robot_config.total_mass = 54.893 + dc.robot_config.wheel_mass = 1.65651 + dc.robot_config.L1 = [[0, 0, 1, 0], [1, 0, 0, 0.182], [0, 1, 0, -0.063], [0, 0, 0, 1]] + dc.robot_config.L2 = [[0, 0, -1, 0.093], [0, 1, 0, -0.043], [1, 0, 0, 0], [0, 0, 0, 1]] + dc.robot_config.L3 = [[1, 0, 0, 0], [0, 1, 0, 0.225], [0, 0, 1, -1.192], [0, 0, 0, 1]] + dc.robot_config.local_map_depth= 3.0 + dc.robot_config.local_map_px_size= 0.05 + dc.robot_config.max_speed_motor_wheel = 1.0 + dc.robot_config.max_forward_speed= 0.5 + dc.robot_config.max_backward_speed= -0.1 + dc.robot_config.max_forward_acc= 0.2 + dc.robot_config.max_ang_vel= 0.5 + dc.robot_config.max_ang_acc= 0.4 + dc.robot_config.max_pan_angle = 1.309 + dc.robot_config.min_pan_angle = -1.309 + dc.robot_config.max_tilt_angle = 0.4363 + dc.robot_config.min_tilt_angle = -0.2618 + dc.robot_config.max_speed_motor_head = 3.0 + dc.robot_config.head_friction = 1.0 + dc.robot_config.head_damping = 0.5 + dc.robot_config.head_cam_fov_h = 62.0 + dc.robot_config.head_cam_fov_v = 48.0 + dc.robot_config.base_cam_fov_h = 360.0 + dc.robot_config.has_pan = True + dc.robot_config.has_tilt = False + + return dc @@ -99,13 +148,15 @@ class ARINavigation(GeneratorComponent): # TODO: remove the need for configuration in yml files # add the configuration in the default config under a social_mpc and robot sub dictionary - config_filename = pkg_resources.resource_filename('modules', 'social_mpc.yaml') - self.mpc_controller = RobotController(config_filename=config_filename) - + + # config_filename = pkg_resources.resource_filename('modules', '/local_scratch/victor/Documents/RobotLearn/Projects/drl_robot_navigation/drl-navigation-training/code/multiparty_interaction_simulator/modules/social_mpc.yaml') + self.mpc_controller = RobotController() + # get the robot config - robot_config_filename = pkg_resources.resource_filename('modules', 'robot.yaml') - config = yaml.load(open(robot_config_filename), Loader=yaml.FullLoader) - self.robot_config = RobotConfig(config) + # robot_config_filename = pkg_resources.resource_filename('modules', 'robot.yaml') + # config = yaml.load(open(robot_config_filename), Loader=yaml.FullLoader) + config_dict = eu.AttrDict.from_yaml('/local_scratch/victor/Documents/RobotLearn/Projects/drl_robot_navigation/drl-navigation-training/code/multiparty_interaction_simulator/modules/Motor_Controller/social_mpc/config/robot.yaml').toDict() + self.robot_config = RobotConfig(dict=config_dict) # initialize the robot state self.state.robot_state = RobotState() @@ -387,7 +438,7 @@ class ARINavigation(GeneratorComponent): x_robot = self.object.position[0] y_robot = self.object.position[1] yaw_robot = self.object.orientation - yaw_robot_pan = yaw_robot - self.object.joints[-1].angle + yaw_robot_pan = yaw_robot - self.object.joints[-1].bodyA.angle x_final = self.mpc_controller.shared_target[2] y_final = self.mpc_controller.shared_target[3] th_final = self.mpc_controller.shared_target[4] @@ -433,8 +484,8 @@ class ARINavigation(GeneratorComponent): v = np.array([-np.sin(angle), np.cos(angle)]) self.state.robot_state.robot_velocity[0] = np.dot(np.asarray(self.object.position_velocity), v) self.state.robot_state.robot_velocity[1] = self.object.orientation_velocity - self.state.robot_state.joint_angles[:] = constraint_angle(-self.object.joints[-1].angle) - self.state.robot_state.joint_velocities[:] = -self.object.joints[-1].speed + self.state.robot_state.joint_angles[:] = constraint_angle(-self.object.joints[-1].bodyA.angle) + # self.state.robot_state.joint_velocities[:] = -np.array(self.object.joints[-1].bodyA.linearVelocity) kernel = cv2.getStructuringElement( cv2.MORPH_ELLIPSE, (2, 2) diff --git a/mpi_sim/components/occupancy_grid_mapping.py b/mpi_sim/components/occupancy_grid_mapping.py new file mode 100644 index 0000000000000000000000000000000000000000..4b1620d78d4a1868c1786f5f9b5fb9c1c9286a5b --- /dev/null +++ b/mpi_sim/components/occupancy_grid_mapping.py @@ -0,0 +1,249 @@ +import mpi_sim +from mpi_sim.core.sensor_component import SensorComponent +import numpy as np +from mpi_sim.utils.box2d_utils import constraint_angle +import cv2 + +# TODO: if there are several agents that do mapping, use a common mapping process to generate the global map to save resources + + +class OccupancyGridMapping(SensorComponent): + r"""Component that creates an occupancy grid map centered and oriented around its object, for example a robot agent.""" + + @staticmethod + def default_config(): + dc = SensorComponent.default_config() + dc.name = 'occupancy_grid_mapping' + dc.area = None # area of global map ((x_min, x_max), (y_min, y_max)), if none it is simulation.visible_area + dc.depth = 5.0 # depth if local in meters + dc.object_filter = None # filter for the elements that should be mapped + dc.resolution = 0.05 # resolution of the maps in m per cell + dc.update_global_map_automatically = False # should the global map be automatically updated each step + dc.transform = None # transform functions of the global map + dc.update_local_map_automatically = False # update local map each time the smulation is step + dc.imported_global_map_path = False # Import Global map - path + dc.imported_global_map_resolution = False # Import Global map - resolution + dc.imported_global_map_area = False # Import Global map - area (can either be a tuple of a function is the area is not squared) + dc.imported_global_map_px_coord_center = None + dc.transform_position_to_map_coordinate = None + dc.transform_real_to_sim_orientation = None + dc.position_noise = 0 + dc.orientation_noise = 0 + return dc + + + @property + def resolution(self): + r"""Get the tesolution of the maps in m per cell.""" + return self._resolution + + + @property + def global_map(self): + r"""Get the global occupancy grid map from which the local map is generated.""" + if self.state.global_map is None: + return None + else: + return self.update_global_map() + + + @property + def global_map_area(self): + r"""The area that is spanned by the global map defined as ((x_min, x_max), (y_min, y_max))""" + return self._global_map_area + + + @property + def global_map_shape(self): + r"""The shape of the global map defined as (width, height).""" + return self._global_map_shape + + + @property + def local_map(self): + r"""Get the local occupancy grid map that is centered and oriented around the object of the component.""" + if self.state.local_map is None: + return None + else: + return self.update_local_map() + + @property + def local_map_noisy(self): + r"""Get the noisy version of the local occupancy grid map that is centered and oriented around the object of the component.""" + if self.state.local_map_noisy is None: + return None + else: + return self.update_noisy_local_map() + + + @property + def local_map_area(self): + r"""The area that is spanned by the local map defined as ((x_min, x_max), (y_min, y_max)) in reference frame of the local map.""" + return self._local_map_area + + + @property + def local_map_shape(self): + r"""The shape of the local map defined as (width, height).""" + return self._local_map_shape + + + def set_global_map(self, global_map): + self.state.global_map = mpi_sim.utils.OccupancyGridMap( + map=global_map, + area=self._global_map_area, + resolution=self._resolution + ) + + def __init__(self, config=None, **kwargs): + super().__init__(config=config, **kwargs) + + + + if not self.config.imported_global_map_path : + self._resolution = self.config.resolution + self._global_map_area = self.config.area + + self._global_map_shape = None + self.state.global_map = None + + # is the global map initially created, if not do it before creating the local map + self.state.is_global_map_initialized = False + + # initialize the global map if possible + if self._global_map_area is not None: + self._global_map_shape = mpi_sim.utils.get_occupancy_grid_map_shape(area=self._global_map_area, resolution=self._resolution) + self.state.global_map = mpi_sim.utils.OccupancyGridMap( + map=np.zeros(self._global_map_shape), + area=self._global_map_area, + resolution=self._resolution + ) + else : + self._resolution = self.config.imported_global_map_resolution + self._global_map_area = self.config.imported_global_map_area + self.state.is_global_map_initialized = True #The global map is initialized and doesn't need to be updated + + if isinstance(self.config.imported_global_map_path, str): + + self.imported_global_map = np.load(file=self.config.imported_global_map_path) + + self.state.global_map = mpi_sim.utils.OccupancyGridMap( + map=self.imported_global_map, + area=self._global_map_area, + resolution=self._resolution + ) + + + else : + raise UserWarning('The parameter \'imported_global_map_path\' should be a type str but it is {}'.format(type(self.config.imported_global_map_path))) + + self._local_map_area = ((-self.config.depth, self.config.depth), (-self.config.depth, self.config.depth)) + local_map_length = int((self.config.depth / self._resolution) * 2) + self._local_map_shape = (local_map_length, local_map_length) + self.state.local_map = mpi_sim.utils.OccupancyGridMap( + map=np.zeros(self._local_map_shape), + area=None, + resolution=self._resolution + ) + + self.state.local_map_noisy = mpi_sim.utils.OccupancyGridMap( + map=np.zeros(self._local_map_shape), + area=None, + resolution=self._resolution + ) + + + def _add_to_simulation(self): + + # if the global map area should span the whole simulation (config.area=None), then read visible_area from the simulation when added to it + if self._global_map_area is None: + self._global_map_area = self.simulation.visible_area + self._global_map_shape = mpi_sim.utils.get_occupancy_grid_map_shape(area=self._global_map_area, resolution=self._resolution) + self.state.global_map = mpi_sim.utils.OccupancyGridMap( + map=np.zeros(self._global_map_shape), + area=self._global_map_area, + resolution=self._resolution + ) + + + def update_global_map(self): + if not self.config.imported_global_map_path : + self.state.global_map = mpi_sim.utils.create_occupancy_grid_map( + self.simulation, + area=self._global_map_area, + resolution=self._resolution, + object_filter=self.config.object_filter, + transform=self.config.transform + ) + else : + + #imported_global_map = cv2.circle(imported_global_map*255, center = [75, 65], radius = 7, color = (255, 255, 255), thickness=-1)/255 + if self.config.transform: + self.imported_global_map=self.config.transform(np.load(file=self.config.imported_global_map_path)) + self.state.global_map = mpi_sim.utils.OccupancyGridMap( + map=self.imported_global_map, + area=self._global_map_area, + resolution=self._resolution + ) + + + return self.state.global_map.map + + + def update_local_map(self): + if self.state.global_map is None: + self.update_global_map() + if not self.config.imported_global_map_path : + + self.state.local_map = mpi_sim.utils.create_local_perspective_occupancy_grid_map( + global_map=self.state.global_map, + position=self.object.position, + orientation=constraint_angle(self.object.orientation), + depth=self.config.depth, + ) + else : + # Same but we need to relocate the position from the center of the imported room + self.state.local_map = mpi_sim.utils.create_local_perspective_occupancy_grid_map( + global_map=self.state.global_map, + # find a way to explain this + position=self.object.position, + orientation=constraint_angle(self.config.transform_real_to_sim_orientation(self.object.orientation)), + depth=self.config.depth, + from_given_center = self.config.imported_global_map_px_coord_center, + transform_position_to_map_coordinate = self.config.transform_position_to_map_coordinate + ) + + return self.state.local_map.map + + def update_noisy_local_map(self): + if self.state.global_map is None: + self.update_global_map() + if not self.config.imported_global_map_path : + + self.state.local_map_noisy = mpi_sim.utils.create_local_perspective_occupancy_grid_map( + global_map=self.state.global_map, + position=self.object.position + self.config.position_noise*np.array([np.random.uniform(low=-1, high=1),np.random.uniform(low=-1, high=1)]), + orientation=constraint_angle(self.object.orientation)+ self.config.orientation_noise*np.random.uniform(low=-1, high=1), + depth=self.config.depth, + ) + else : + # Same but we need to relocate the position from the center of the imported room + self.state.local_map_noisy = mpi_sim.utils.create_local_perspective_occupancy_grid_map( + global_map=self.state.global_map, + position=self.object.position + self.config.position_noise*np.array([np.random.uniform(low=-1, high=1),np.random.uniform(low=-1, high=1)]), + orientation=constraint_angle(self.config.transform_real_to_sim_orientation(self.object.orientation)) + self.config.orientation_noise*np.random.uniform(low=-1, high=1), + depth=self.config.depth, + from_given_center = self.config.imported_global_map_px_coord_center, + transform_position_to_map_coordinate = self.config.transform_position_to_map_coordinate + ) + + return self.state.local_map_noisy.map + + + def _step(self): + if self.config.update_global_map_automatically or not self.state.is_global_map_initialized: + self.update_global_map() + self.state.is_global_map_initialized = True + if self.config.update_local_map_automatically : + self.update_local_map() + self.update_noisy_local_map() diff --git a/mpi_sim/components/occupany_grid_mapping.py b/mpi_sim/components/occupany_grid_mapping.py deleted file mode 100644 index 6ec48ccca723ef59a0b9323a4bffddbf6687c806..0000000000000000000000000000000000000000 --- a/mpi_sim/components/occupany_grid_mapping.py +++ /dev/null @@ -1,151 +0,0 @@ -import mpi_sim -import mpi_sim.processes -from mpi_sim.core.sensor_component import SensorComponent -import numpy as np -from mpi_sim.utils.box2d_utils import constraint_angle -import cv2 - - -# TODO: if there are several agents that do mapping, use a common mapping process to generate the global map to save resources - - -class OccupancyGridMapping(SensorComponent): - r"""Component that creates an occupancy grid map centered and oriented around its object, for example a robot agent.""" - - @staticmethod - def default_config(): - dc = SensorComponent.default_config() - dc.name = 'occupancy_grid_mapping' - dc.area = None # area of global map ((x_min, x_max), (y_min, y_max)), if none it is simulation.visible_area - dc.depth = 5.0 # depth if local in meters - dc.object_filter = None # filter for the elements that should be mapped - dc.resolution = 0.05 # resolution of the maps in m per cell - dc.update_global_map_automatically = True # should the global map be automatically updated each step - return dc - - - @property - def resolution(self): - r"""Get the tesolution of the maps in m per cell.""" - return self._resolution - - - @property - def global_map(self): - r"""Get the global occupancy grid map from which the local map is generated.""" - if self.state.global_map is None: - return None - else: - return self.state.global_map.map - - - @property - def global_map_area(self): - r"""The area that is spanned by the global map defined as ((x_min, x_max), (y_min, y_max))""" - return self._global_map_area - - - @property - def global_map_shape(self): - r"""The shape of the global map defined as (width, height).""" - return self._global_map_shape - - - @property - def local_map(self): - r"""Get the local occupancy grid map that is centered and oriented around the object of the component.""" - if self.state.local_map is None: - return None - else: - return self.state.local_map.map - - - @property - def local_map_area(self): - r"""The area that is spanned by the local map defined as ((x_min, x_max), (y_min, y_max)) in reference frame of the local map.""" - return self._local_map_area - - - @property - def local_map_shape(self): - r"""The shape of the local map defined as (width, height).""" - return self._local_map_shape - - - def __init__(self, config=None, **kwargs): - super().__init__(config=config, **kwargs) - - self._resolution = self.config.resolution - - self._global_map_area = self.config.area - self._global_map_shape = None - self.state.global_map = None - - # is the global map initially created, if not do it before creating the local map - self.state.is_global_map_initialized = False - - # initialize the global map if possible - if self._global_map_area is not None: - self._global_map_shape = mpi_sim.utils.get_occupancy_grid_map_shape(area=self._global_map_area, resolution=self._resolution) - self.state.global_map = mpi_sim.utils.OccupancyGridMap( - map=np.zeros(self._global_map_shape), - area=self._global_map_area, - resolution=self._resolution - ) - - self._local_map_area = ((-self.config.depth, self.config.depth), (-self.config.depth, self.config.depth)) - local_map_length = int((self.config.depth / self._resolution) * 2) - self._local_map_shape = (local_map_length, local_map_length) - self.state.local_map = mpi_sim.utils.OccupancyGridMap( - map=np.zeros(self._local_map_shape), - area=None, - resolution=self._resolution - ) - - - def _add_to_simulation(self): - - # if the global map area should span the whole simulation (config.area=None), then read visible_area from the simulation when added to it - if self._global_map_area is None: - self._global_map_area = self.simulation.visible_area - self._global_map_shape = mpi_sim.utils.get_occupancy_grid_map_shape(area=self._global_map_area, resolution=self._resolution) - self.state.global_map = mpi_sim.utils.OccupancyGridMap( - map=np.zeros(self._global_map_shape), - area=self._global_map_area, - resolution=self._resolution - ) - - - def update_global_map(self): - self.state.global_map = mpi_sim.utils.create_occupancy_grid_map( - self.simulation, - area=self._global_map_area, - resolution=self._resolution, - object_filter=self.config.object_filter - ) - - return self.state.global_map.map - - - def update_local_map(self): - - if self.state.global_map is None: - self.update_global_map() - - self.state.local_map = mpi_sim.utils.create_local_perspective_occupancy_grid_map( - global_map=self.state.global_map, - position=self.object.position, - orientation=constraint_angle(self.object.orientation), - depth=self.config.depth, - ) - - return self.state.local_map.map - - - def _step(self): - - if self.config.update_global_map_automatically or not self.state.is_global_map_initialized: - self.update_global_map() - self.state.is_global_map_initialized = True - - self.update_local_map() diff --git a/mpi_sim/components/point_drawer.py b/mpi_sim/components/point_drawer.py new file mode 100644 index 0000000000000000000000000000000000000000..d04f8ba7117aa08bb2e50b6b6cff1cac2e971207 --- /dev/null +++ b/mpi_sim/components/point_drawer.py @@ -0,0 +1,49 @@ +## Software gym_box2d_diff_drive +## +## Derived from pybox2d distributed under zlib License (c), 2008-2011 Ken Lauer +## sirkne at gmail dot com +## +## This file, corresponding to gym_box2d_diff_drive module of WP6, is part of a +## project that has received funding from the European Union’s Horizon 2020 +## research and innovation programme under grant agreement No 871245. +## +## Copyright (C) 2022 by INRIA +## Authors : Chris Reinke, Alex Auternaud, Victor Sanchez +## alex dot auternaud at inria dot fr +## victor dot sanchez at inria dot fr + +from mpi_sim.core.component import Component +import mpi_sim as sim +from Box2D import (b2Color, b2Vec2) +import pygame + +class PointDrawer(Component): + + @staticmethod + def default_config(): + dc = sim.SensorComponent.default_config() + dc.name = 'point_drawer' + return dc + + + def add_point_to_draw(self, point): + self.list_of_point_to_draw.append(point) + + @property + def clear_list_of_point(self): + self.list_of_point_to_draw.clear() + + def __init__(self, config=None, **kwargs): + super().__init__(config=config, **kwargs) + self.list_of_point_to_draw =[] + + def _step(self): + pass + # for point in self.list_of_point_to_draw : + # self.simulation.box2d_world.renderer.DrawPoint(b2Vec2(point[0],point[1]), 100.0, b2Color(1, 1, 1)) + # pygame.display.flip() + + + + + diff --git a/mpi_sim/components/raycast.py b/mpi_sim/components/raycast.py index d52379b548e376ccddacbb8d46bfeaeb9bcb9b86..d87026f8490a3d8194b7b1040cd9923e4d31ba6b 100644 --- a/mpi_sim/components/raycast.py +++ b/mpi_sim/components/raycast.py @@ -43,7 +43,7 @@ class RayCastClosestCallback(b2RayCastCallback): returning 1, you continue with the original ray clipping. By returning -1, you will filter out the current fixture (the ray will not hit it). """ - if fixture.body.userData['name'] not in self.wrong_fix: + if fixture.body.userData['name'] not in self.wrong_fix or not isinstance(fixture.body.userData['obj'], sim.objects.ari_robot.ARIRobot): self.hit = True self.fixture = fixture self.point = b2Vec2(point) diff --git a/mpi_sim/components/raycast_sensor.py b/mpi_sim/components/raycast_sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..0397a280ba07beddb8ee1f4bbb3aeccbd2a6a7a5 --- /dev/null +++ b/mpi_sim/components/raycast_sensor.py @@ -0,0 +1,230 @@ +## Software gym_box2d_diff_drive +## +## Derived from pybox2d distributed under zlib License (c), 2008-2011 Ken Lauer +## sirkne at gmail dot com +## +## This file, corresponding to gym_box2d_diff_drive module of WP6, is part of a +## project that has received funding from the European Union’s Horizon 2020 +## research and innovation programme under grant agreement No 871245. +## +## Copyright (C) 2022 by INRIA +## Authors : Chris Reinke, Alex Auternaud, Victor Sanchez +## alex dot auternaud at inria dot fr +## victor dot sanchez at inria dot fr + +import pygame +import numpy as np +from mpi_sim.core.sensor_component import SensorComponent +import mpi_sim as sim +from Box2D import (b2Color, b2Vec2, b2RayCastCallback) +# TODO: rename this component to something like ObjectDetector which can be configured to say which object types should be detected + + +class RayCastClosestCallback(b2RayCastCallback): + """This callback finds the closest hit""" + + def __init__(self, wrong_fix, **kwargs): + b2RayCastCallback.__init__(self, **kwargs) + self.wrong_fix = wrong_fix + self.fixture = None + self.point = None + self.normal = None + self.hit = False + + + def ReportFixture(self, fixture, point, normal, fraction): + """ + Called for each fixture found in the query. You control how the ray + proceeds by returning a float that indicates the fractional length of + the ray. By returning 0, you set the ray length to zero. By returning + the current fraction, you proceed to find the closest point. By + returning 1, you continue with the original ray clipping. By returning + -1, you will filter out the current fixture (the ray will not hit it). + """ + + if fixture.body.userData['name'] not in self.wrong_fix : + self.hit = True + self.fixture = fixture + self.point = b2Vec2(point) + self.normal = b2Vec2(normal) + + return fraction + +class RayCastSensor(SensorComponent): + + @staticmethod + def default_config(): + dc = sim.SensorComponent.default_config() + dc.name = 'raycast_sensor' + dc.field_of_view = 360 # Range of the lidar in degree + dc.delta_angle = 2. # Angle between two beams of lidar in degree + dc.ray_max_range = 20 # Max distance readable by a given beam + dc.noise_ratio = 0 + dc.draw_raycast = False + dc.update_raycast_every_step = False + dc.max_raycast_shape = 'square' # can either be a 'circle' or a 'square' + return dc + + @property + def raycast_distances(self): + if self.update_raycast_every_step : + return np.round(self.ray_distance_list, 4) + else : + return self.update_raycast() + + @property + def raycast_angles(self): + if self.update_raycast_every_step : + return self.ray_angle_list + else : + self.update_raycast() + return self.ray_angle_list + + @property + def raycast_angles_from_agent_view(self): + if self.update_raycast_every_step : + return sim.utils.constraint_angle(np.round(self.ray_angle_list-np.degrees(self.orientation),2), min_value=self.min_value_angle, max_value=self.max_value_angle) + else : + self.update_raycast() + return sim.utils.constraint_angle(np.round(self.ray_angle_list-np.degrees(self.orientation),2), min_value=self.min_value_angle, max_value=self.max_value_angle) + + + def __init__(self, config=None, **kwargs): + super().__init__(config=config, **kwargs) + self.wrong_fix = ['left wheel', 'right wheel', 'pan'] + self.fov = self.config.field_of_view + self.delta_angle = self.config.delta_angle + self.min_angle_fov = None + self.max_angle_fov = None + self.ray_distance_list = None # List containing the raycast distance + self.ray_angle_list = None # List containing the angle of each ray + self.coord_points_max_range_rays = None + self.position = None + self.orientation = None + self.coord_point_rays = None + self.draw = self.config.draw_raycast + self.check_done = False + self.max_value_angle = 180 + self.min_value_angle = -180 + self.update_raycast_every_step = self.config.update_raycast_every_step + + + def _polar_to_cartesian(self, distance, angle): + return distance*np.cos(angle), distance*np.sin(angle) + + def _update_coord_max_range_rays(self): + r''' Update the cartesian coordinates of the max range rays + + Note : It takes into account the orientation and position of the object ''' + + self.coord_points_max_range_rays = [] + if self.config.max_raycast_shape == 'circle': + for angle in self.ray_angle_list : + self.coord_points_max_range_rays.append(self._polar_to_cartesian(distance=self.config.ray_max_range, angle=np.radians(angle) + np.pi/2)) + elif self.config.max_raycast_shape == 'square': + for angle in self.ray_angle_list : + self.coord_points_max_range_rays.append(self._polar_to_cartesian(distance=np.sqrt(2)*self.config.ray_max_range, angle=np.radians(angle) + np.pi/2)) + self.coord_points_max_range_rays = np.clip(self.coord_points_max_range_rays, -self.config.ray_max_range, +self.config.ray_max_range) + self.coord_points_max_range_rays = np.array(self.coord_points_max_range_rays) + np.array(self.position) + + def _coord_around_object(self, position, angle, radius_object = 0.3): + r''' Computes the coord of a point around object given a certain angle + + Note : This function exists because the ARIRobot was overlapping with the fixture detection + To avoid the phenomenon we measure the fixture between a circle around the object and the max range ''' + return position + radius_object*np.array([np.cos(angle), np.sin(angle)]) + + def update_raycast(self): + self.position = self.object.position # cartesian + self.orientation = self.object.orientation # in radians + + # Update the field of view range w.r.t to the robot's orientation + self.min_angle_fov = np.round(np.degrees(self.orientation) + self.fov/2, 3) + self.max_angle_fov = np.round(np.degrees(self.orientation) - self.fov/2, 3) + + # Update the angle of the point cloud and constraint it around -180 and 180 degrees + self.ray_angle_list = np.linspace(self.min_angle_fov, self.max_angle_fov, int(self.fov/self.config.delta_angle) + 1) + + # Restriction and modulo in the range -180/180 + length = self.max_value_angle - self.min_value_angle + self.ray_angle_list = np.where(self.ray_angle_list > self.max_value_angle, self.min_value_angle + ((self.ray_angle_list - self.max_value_angle) % length), self.ray_angle_list) + self.ray_angle_list = np.where(self.ray_angle_list < self.min_value_angle, self.max_value_angle - ((self.min_value_angle - self.ray_angle_list) % length), self.ray_angle_list) + ray_angle_list_from_agent_viewpoint = np.round(self.ray_angle_list-np.degrees(self.orientation),3) + + # Update the variable self.coord_points_max_range_rays + self._update_coord_max_range_rays() + + # Update the raycast points whether there are obstacle between the object and the max range + self.ray_distance_list = [] + self.coord_point_rays = [] + # Measurement Noise + noise = np.random.normal(0,1,size=2)*self.config.noise_ratio + + + + for id, point_ray in enumerate(self.coord_points_max_range_rays): + callback = RayCastClosestCallback(wrong_fix=self.wrong_fix) + # Origin of rays around the object + position = self._coord_around_object(self.position, angle=np.radians(self.ray_angle_list[id])) + self.simulation.box2d_world.RayCast(callback, point1=position, point2=point_ray) + + if callback.hit and callback.fixture.body.userData['name'] != self.object.box2d_body.userData['name']: + self.ray_distance_list.append(sim.utils.measure_center_distance(self.position, np.array(callback.point)+ noise)) + self.coord_point_rays.append(np.array(callback.point)+ noise) + if self.draw : + self.point11 = self.simulation.box2d_world.renderer.to_screen(self.position) + if not ray_angle_list_from_agent_viewpoint[id]: + color = b2Color(1, 0, 0) + else : + color = b2Color(0.8, 0, 0.8) + self.draw_hit( + cb_point=np.array(callback.point)+ noise, + cb_normal=callback.normal, + point1=self.point11, + color=color + ) + else: + self.ray_distance_list.append(sim.utils.measure_center_distance(self.position, np.array(point_ray)+noise)) + self.coord_point_rays.append(np.array(point_ray)+noise) + if self.draw : + if not ray_angle_list_from_agent_viewpoint[id]: + color = b2Color(0.9, 0, 0) + else : + color = b2Color(0.8, 0, 0.8) + self.point11 = self.simulation.box2d_world.renderer.to_screen(self.position) + self.point22 = self.simulation.box2d_world.renderer.to_screen(np.array(point_ray)+noise) + self.simulation.box2d_world.renderer.DrawSegment(self.point11, self.point22, color) + self.simulation.box2d_world.renderer.DrawPoint(self.point22, 5.0, color) + + if self.draw : + pygame.display.flip() + + return np.round(self.ray_distance_list, 4) + + + def _check(self): + if not self.fov : + self.compute_raycast = False + if not self.delta_angle : + raise Warning('The \'delta_angle\' variable is has been set to {}. Please change that parameter to a non zero value'.format(self.delta_angle)) + + def _step(self): + if not self.check_done : + self._check() + self.check_done = True + if self.update_raycast_every_step : + self.update_raycast() + + + def draw_hit(self, cb_point, cb_normal, point1, color): + cb_point = self.simulation.box2d_world.renderer.to_screen(cb_point) + head = b2Vec2(cb_point) + 0.5 * cb_normal + + self.simulation.box2d_world.renderer.DrawPoint(cb_point, 5.0, color) + self.simulation.box2d_world.renderer.DrawSegment(point1, cb_point, color) + self.simulation.box2d_world.renderer.DrawSegment(cb_point, head, color) + + + + + diff --git a/mpi_sim/components/social_force_navigation.py b/mpi_sim/components/social_force_navigation.py index ee22ccfbf091c422a90974531184cc0605ef0d4c..e034e11cbcbaec7d34acb778d547dbc8f90355b6 100644 --- a/mpi_sim/components/social_force_navigation.py +++ b/mpi_sim/components/social_force_navigation.py @@ -13,7 +13,8 @@ class SocialForceNavigation(GeneratorComponent): dc.max_movement_force = 0.25 dc.max_rotation_force = 6. dc.goal_distance_threshold = 0.2 - dc.step_length_coeff = 1.0 + dc.step_length_coeff = 1 + dc.avoid_ari = True return dc ############################################################################### @@ -62,7 +63,6 @@ class SocialForceNavigation(GeneratorComponent): # print('stats : ', self.box2d_body.inertia, self.box2d_body.awake, self.box2d_body.active, self.box2d_body.transform) # print(self.box2d_body.localCenter, self.box2d_body.worldCenter, self.box2d_body.linearVelocity, self.box2d_body.angularVelocity) - # if goal position is given use it to update the human if self.state.goal_position is not None: @@ -92,7 +92,6 @@ class SocialForceNavigation(GeneratorComponent): next_orientation = self.object.orientation + f_orientation * self.config.step_length_coeff * self.simulation.step_length self.object.orientation = next_orientation - # TODO: include manual control if wanted # if self.state.goal_position is not None or self.state.goal_orientation is not None: @@ -113,7 +112,10 @@ class SocialForceNavigation(GeneratorComponent): f_repulsion = position * 0.0 # identify other agents inside the personal distance of the agent - agents = self.simulation.get_nearby_objects(self.object, self.config.personal_distance, (mpi_sim.objects.Human, mpi_sim.objects.ARIRobot)) + if self.config.avoid_ari : + agents = self.simulation.get_nearby_objects(self.object, self.config.personal_distance, (mpi_sim.objects.Human, mpi_sim.objects.ARIRobot)) + else : + agents = self.simulation.get_nearby_objects(self.object, self.config.personal_distance, mpi_sim.objects.Human) if agents: other_agents_pos = np.array([agent.position for agent in agents]) # [n_agent * 2 (x and y pos)] array @@ -137,7 +139,7 @@ class SocialForceNavigation(GeneratorComponent): goal_direction = self.goal_position - position goal_distance = np.linalg.norm(goal_direction) - + np.seterr(invalid='ignore') f_goal = min(1.0, goal_distance / self.config.goal_distance_threshold) * goal_direction/goal_distance return f_goal diff --git a/mpi_sim/components/social_mapping.py b/mpi_sim/components/social_mapping.py new file mode 100644 index 0000000000000000000000000000000000000000..27cce60b7990549f2a6e1b7c4d33da19948e6dba --- /dev/null +++ b/mpi_sim/components/social_mapping.py @@ -0,0 +1,213 @@ +import mpi_sim +from mpi_sim.core.sensor_component import SensorComponent +import numpy as np +from mpi_sim.utils.box2d_utils import constraint_angle + + +import cv2 + +# TODO: if there are several agents that do mapping, use a common mapping process to generate the global map to save resources + + +class SocialMapping(SensorComponent): + r"""Component that creates a social map centered and oriented around its object, for example a robot agent.""" + + @staticmethod + def default_config(): + dc = SensorComponent.default_config() + dc.name = 'social_mapping' + dc.area = None # area of global map ((x_min, x_max), (y_min, y_max)), if none it is simulation.visible_area + dc.depth = 5.0 # depth if local in meters + dc.object_filter = None # filter for the elements that should be mapped + dc.resolution = 0.05 # resolution of the maps in m per cell + dc.update_global_map_automatically = True # should the global map be automatically updated each step + dc.update_local_map_automatically = False # update local map each time the smulation is step + dc.imported_global_map_path = False # Import Global map - path + dc.imported_global_map_resolution = False # Import Global map - resolution + dc.imported_global_map_area = False # Import Global map - area (can either be a tuple of a function is the area is not squared) + dc.imported_global_map_px_coord_center = None + dc.transform_position_to_map_coordinate = None + dc.transform_real_to_sim_orientation = None + return dc + + + @property + def resolution(self): + r"""Get the tesolution of the maps in m per cell.""" + return self._resolution + + + @property + def global_map(self): + r"""Get the global social map from which the local map is generated.""" + if self.state.global_map is None: + return None + else: + return self.update_global_map() + + + @property + def global_map_area(self): + r"""The area that is spanned by the global map defined as ((x_min, x_max), (y_min, y_max))""" + return self._global_map_area + + + @property + def global_map_shape(self): + r"""The shape of the global map defined as (width, height).""" + return self._global_map_shape + + + @property + def local_map(self): + r"""Get the local social map that is centered and oriented around the object of the component.""" + if self.state.local_map is None: + return None + else: + return self.update_local_map() + + + @property + def local_map_area(self): + r"""The area that is spanned by the local map defined as ((x_min, x_max), (y_min, y_max)) in reference frame of the local map.""" + return self._local_map_area + + + @property + def local_map_shape(self): + r"""The shape of the local map defined as (width, height).""" + return self._local_map_shape + + def set_human_velocities(self, human_velocities): + r""" Set the velocities of the human """ + # Necessary when the humans are navigating with the navigation component + self.human_velocities= human_velocities + + + def set_global_map(self, global_map): + self.state.global_map = mpi_sim.utils.SocialMap( + map=global_map, + area=self._global_map_area, + resolution=self._resolution + ) + + def __init__(self, config=None, **kwargs): + super().__init__(config=config, **kwargs) + self.state.global_map = None + + if not self.config.imported_global_map_path : + self._resolution = self.config.resolution + self._global_map_area = self.config.area + + self._global_map_shape = None + + # is the global map initially created, if not do it before creating the local map + self.state.is_global_map_initialized = False + + # initialize the global map if possible + if self._global_map_area is not None: + self._global_map_shape = mpi_sim.utils.get_social_map_shape(area=self._global_map_area, resolution=self._resolution) + self.state.global_map = mpi_sim.utils.SocialMap( + map=np.zeros(self._global_map_shape), + area=self._global_map_area, + resolution=self._resolution + ) + + + + + else : + self._resolution = self.config.imported_global_map_resolution + self._global_map_area = self.config.imported_global_map_area + self.state.is_global_map_initialized = True #The global map is initialized and doesn't need to be updated + + if isinstance(self.config.imported_global_map_path, str): + + imported_global_map = np.load(file=self.config.imported_global_map_path) + self.shape_imported_global_map = np.shape(imported_global_map) + self.state.global_map = mpi_sim.utils.SocialMap( + map=np.zeros_like(imported_global_map), + area=self._global_map_area, + resolution=self._resolution + ) + + self._global_map_shape = np.shape(imported_global_map) + + else : + raise UserWarning('The parameter \'imported_global_map_path\' should be a type str but it is {}'.format(type(self.config.imported_global_map_path))) + + self.human_velocities = None + self._local_map_area = ((-self.config.depth, self.config.depth), (-self.config.depth, self.config.depth)) + local_map_length = int((self.config.depth / self._resolution) * 2) + self._local_map_shape = (local_map_length, local_map_length) + self.state.local_map = mpi_sim.utils.SocialMap( + map=np.zeros(self._local_map_shape), + area=None, + resolution=self._resolution + ) + + def _add_to_simulation(self): + + # if the global map area should span the whole simulation (config.area=None), then read visible_area from the simulation when added to it + if self._global_map_area is None: + self._global_map_area = self.simulation.visible_area + self._global_map_shape = mpi_sim.utils.get_social_map_shape(area=self._global_map_area, resolution=self._resolution) + self.state.global_map = mpi_sim.utils.SocialMap( + map=np.zeros(self._global_map_shape), + area=self._global_map_area, + resolution=self._resolution + ) + + + def update_global_map(self): + + if not self.config.imported_global_map_path : + + self.state.global_map = mpi_sim.utils.create_social_map( + self.simulation, + area=self._global_map_area, + resolution=self._resolution, + human_velocities=self.human_velocities + ) + else : + self.state.global_map = mpi_sim.utils.create_social_map( + self.simulation, + area=self._global_map_area, + resolution=self._resolution, + human_velocities=self.human_velocities, + flag_imported_global_map=True + ) + + return self.state.global_map.map + + + def update_local_map(self): + if self.state.global_map is None: + self.update_global_map() + if not self.config.imported_global_map_path : + self.state.local_map = mpi_sim.utils.create_local_perspective_social_map( + global_map=self.state.global_map, + position=self.object.position, + orientation=constraint_angle(self.object.orientation), + depth=self.config.depth, + ) + else : + # Same but we need to relocate the position from the center of the imported room + self.state.local_map = mpi_sim.utils.create_local_perspective_social_map( + global_map=self.state.global_map, + # find a way to explain this + position=self.object.position, + orientation=constraint_angle(self.config.transform_real_to_sim_orientation(self.object.orientation)), + depth=self.config.depth, + from_given_center = self.config.imported_global_map_px_coord_center, + transform_position_to_map_coordinate = self.config.transform_position_to_map_coordinate + ) + return self.state.local_map.map + + + def _step(self): + if self.config.update_global_map_automatically or not self.state.is_global_map_initialized: + self.update_global_map() + self.state.is_global_map_initialized = True + if self.config.update_local_map_automatically : + self.update_local_map() diff --git a/mpi_sim/core/simulation.py b/mpi_sim/core/simulation.py index 68d49bdcea42cc64e35a2ceb78b3b5434f1f2ec6..43f7140cf5818fef38a596e2674bf16c236e6b9f 100644 --- a/mpi_sim/core/simulation.py +++ b/mpi_sim/core/simulation.py @@ -814,6 +814,21 @@ class Simulation: # box2d simulation self._box2d_simulation.close() + def get_objects_in_simulation(self, object_to_avoid=None): + """Returns the objects in the simulation""" + objects = [] + # go through objects + for obj in self.objects.values(): + if object_to_avoid is None : + # only include objects that have a position property + if obj.position is not None: + objects.append(obj) + else : + if type(obj) != type(object_to_avoid): + if obj.position is not None: + objects.append(obj) + return objects + def get_nearby_objects(self, point, radius, object_types=None): """Returns objects that are within a specified radius of the given point (x,y) or object (obj.position). diff --git a/mpi_sim/objects/ari_robot.py b/mpi_sim/objects/ari_robot.py index 45f6cc2730cc74a0ec89e1204ca3c3929b1bb06e..854461ccd5abfe558b41fc80c9c38e6e20df4e1c 100644 --- a/mpi_sim/objects/ari_robot.py +++ b/mpi_sim/objects/ari_robot.py @@ -45,7 +45,7 @@ class ARIRobot(Object): dc.base_footprint_radius = 0.3 dc.total_mass = 54.893 dc.wheel_mass = 1.65651 - dc.enable_pan = True + dc.enable_pan = False # dc.enable_raycast = False dc.base_link2head_front_camera_optical_frame = [-0.120, -0.000, 1.466] dc.head_cam_fov_h = 62. @@ -57,6 +57,9 @@ class ARIRobot(Object): dc.max_movement_force = 0.25 dc.max_rotation_force = 6. + dc.linearDamping = 2 + dc.angularDamping = 2 + return dc @@ -129,6 +132,7 @@ class ARIRobot(Object): position = (0.0, 0.0) self.box2d_body.position = (float(position[0]), float(position[1])) + self.pan.body.position = np.array((float(position[0]), float(position[1]))) + np.array(self.pan_anchors) @property @@ -261,8 +265,8 @@ class ARIRobot(Object): ) self.box2d_body.userData = dict(obj=self) self.box2d_body.userData['name'] = self.config.id - self.box2d_body.linearDamping = 2 - self.box2d_body.angularDamping = 2 + self.box2d_body.linearDamping = self.config.linearDamping + self.box2d_body.angularDamping = self.config.angularDamping self.box2d_body.CreatePolygonFixture( box=(*wheel_dim, b2Vec2(tire_anchors[0]), 0), @@ -297,7 +301,7 @@ class ARIRobot(Object): localAnchorB=(0, 0), referenceAngle=0 ) - self.pan.body.position = self.box2d_body.worldCenter + self.pan_anchors + self.pan.body.position = np.array(self.config.position) + np.array(self.pan_anchors) self.joints.append(j) return self.box2d_body, self.pan.body @@ -331,8 +335,8 @@ class ARIRobot(Object): ############################# - if self.pan.body.awake: - self.pan.update_turn(None, self.joints[-1], self.pan_vel) # keys = None + # if self.pan.body.awake: + # self.pan.update_turn(None, self.joints[-1], self.pan_vel) # keys = None ############################ diff --git a/mpi_sim/objects/human.py b/mpi_sim/objects/human.py index 0470f49bb5a97de0f8fb05d50526b9891b04c903..6e4bc2517fa2eb2719e7ec4e7bd76e3d231e014b 100644 --- a/mpi_sim/objects/human.py +++ b/mpi_sim/objects/human.py @@ -46,6 +46,7 @@ class Human(Object): return mpi_sim.AttrDict( type=mpi_sim.components.SocialForceNavigation, name='navigation', + step_length_coeff = 0.2, ) @@ -116,6 +117,11 @@ class Human(Object): position_velocity = (0.0, 0.0) self.box2d_body.linearVelocity = (float(position_velocity[0]), position_velocity[1]) + # self.box2d_body.linearVelocity = calc_linear_velocity_from_forward_velocity( + # forward_velocity, + # self.box2d_body + # ) + @property @@ -139,6 +145,8 @@ class Human(Object): ############################################################################### + + @property def group(self): """The group the agent is part of. None if he is not part of the group.""" @@ -157,6 +165,7 @@ class Human(Object): self.box2d_body = None + # maybe used in future for key control # self.keys = None diff --git a/mpi_sim/processes/gui.py b/mpi_sim/processes/gui.py index 2f1e7ed526720dae30cce69fa9e0d251414f8ec5..024d2a24dca8b200949b0da509ea1b25c32509e7 100644 --- a/mpi_sim/processes/gui.py +++ b/mpi_sim/processes/gui.py @@ -105,11 +105,11 @@ class GUI(Process): #('Time of Impact', 'enable_continuous'), #('Sub-Stepping', 'enable_sub_stepping'), ('Draw', None), - ('Agent Info', 'draw_agent_information'), - ('Object Info', 'draw_object_information'), + # ('Agent Info', 'draw_agent_information'), + # ('Object Info', 'draw_object_information'), ('Dialogue', 'draw_dialogue'), - ('Debug Mode', 'draw_debug_information'), - ('Statistics', 'draw_stats'), + # ('Debug Mode', 'draw_debug_information'), + # ('Statistics', 'draw_stats'), ('RTF', 'draw_rtf'), #('Control', None), #('Pause', 'pause'), diff --git a/mpi_sim/utils/__init__.py b/mpi_sim/utils/__init__.py index 43c4fdecaffe98b7a5c5e6ec9825f851be1b6034..250982463fd5dcabfde2d63ba8257a5738736e26 100644 --- a/mpi_sim/utils/__init__.py +++ b/mpi_sim/utils/__init__.py @@ -39,4 +39,14 @@ from mpi_sim.utils.occupancy_grid_map import get_occupancy_grid_map_shape from mpi_sim.utils.occupancy_grid_map import transform_position_to_map_coordinate from mpi_sim.utils.occupancy_grid_map import transform_map_coordinate_to_position from mpi_sim.utils.occupancy_grid_map import OccupancyGridMap +from mpi_sim.utils.occupancy_grid_transform import RTABMAPlikeTransform +# social map + +# occupancy grid +from mpi_sim.utils.social_map import create_social_map +from mpi_sim.utils.social_map import create_local_perspective_social_map +from mpi_sim.utils.social_map import get_social_map_shape +from mpi_sim.utils.social_map import transform_position_to_map_coordinate +from mpi_sim.utils.social_map import transform_map_coordinate_to_position +from mpi_sim.utils.social_map import SocialMap diff --git a/mpi_sim/utils/measurements.py b/mpi_sim/utils/measurements.py index 8c9417b63cd2de7c8c7426cc3ee32cc63f39e6b5..61df664ae9c11cc57111701f9f6bab0cbc1913b7 100644 --- a/mpi_sim/utils/measurements.py +++ b/mpi_sim/utils/measurements.py @@ -2,6 +2,7 @@ import numpy as np import mpi_sim import collections import Box2D +from Box2D import b2CircleShape def measure_center_distance(p1, p2): @@ -23,7 +24,7 @@ def measure_center_distance(p1, p2): DistanceInfo = collections.namedtuple('DistanceInfo', ['distance', 'point_a', 'point_b']) -def measure_distance(p1, p2): +def measure_distance(p1, p2, simulation=None): """Measures the minimum distance between two points or objects (their bodies and shapes) in meters.""" if not isinstance(p1, mpi_sim.Object) and not isinstance(p2, mpi_sim.Object): @@ -65,7 +66,63 @@ def measure_distance(p1, p2): point_b = np.array(b2_dist_info.pointB) else: - raise NotImplementedError('Calculating the distance between a point and an object is currently not supported!') + # To measure the distance between an object and a position we create a temporary object + # Then we measure the distance between the new temporary object and the original object + if simulation is None: + raise UserWarning('The distance measurement between an object and a position needs the simulation') + + # In case the object is given in firt or second position + if isinstance(p1, mpi_sim.Object) : + _object = p1 + _point = p2 + else : + _object = p2 + _point = p1 + + distance = np.inf + point_a = None + point_b = None + + # Creating an object to measure the distance from + fake_dot_obj = mpi_sim.objects.RoundTable( + position=_point, + orientation=0, + radius = 0.000001, + ) + simulation.add_object(fake_dot_obj) + + # Measure the min distance for each bodies and each fixture + for body_a in fake_dot_obj.box2d_bodies: + for fixture_a in body_a.fixtures: + + # fixture must have a shape + if not fixture_a.shape: + continue + + for body_b in _object.box2d_bodies: + for fixture_b in body_b.fixtures: + + # fixture must have a shape + if not fixture_b.shape: + continue + + b2_dist_info = Box2D.b2Distance( + shapeA=fixture_a.shape, + shapeB=fixture_b.shape, + transformA=body_a.transform, + transformB=body_b.transform + ) + + if b2_dist_info.distance < distance: + distance = b2_dist_info.distance + point_a = np.array(b2_dist_info.pointA) + point_b = np.array(b2_dist_info.pointB) + + # Remove the fake object from simulation + simulation.remove_object(fake_dot_obj) + + # Correct the distance measurement w.r.t to its radius + distance += 0.000001 return DistanceInfo(distance, point_a, point_b) diff --git a/mpi_sim/utils/occupancy_grid_map.py b/mpi_sim/utils/occupancy_grid_map.py index fce115f2f1b80e789431e52a2fcd3d03628f1559..ef72ab265ccdcd6a2776d426f861f5bace686f4d 100644 --- a/mpi_sim/utils/occupancy_grid_map.py +++ b/mpi_sim/utils/occupancy_grid_map.py @@ -9,7 +9,6 @@ from collections import namedtuple OccupancyGridMap = namedtuple('OccupancyGridMap', ['map', 'area', 'resolution']) - def get_occupancy_grid_map_shape(area, resolution): r"""Calculates the size of the occupancy grid map that can be created via create_occupancy_grid_map(). @@ -30,6 +29,27 @@ def get_occupancy_grid_map_shape(area, resolution): return shape +def get_occupancy_grid_map_resolution(area, shape): + r"""Calculates the resolution of the occupancy grid map that can be created via create_occupancy_grid_map(). + + Args: + area (tuple of floats): Area of the simulation that should be mapped in ((x_min, x_max), (y_min, y_max)). + shape (tuple): Shape of the map as a tuple: (width, height) + + Returns: + Resolution in meter per cell. (float) + """ + + x_width = area[0][1] - area[0][0] + #y_width = area[1][1] - area[1][0] + + (width, height) = shape + + resolution = int(x_width / width) + #resolution = int(y_width / height) + + return resolution + def transform_position_to_map_coordinate(position, map_area, map_resolution): r"""Transform a position (x, y) in the simulation to a coordinate (x, y) of a map of the simulation. @@ -37,7 +57,7 @@ def transform_position_to_map_coordinate(position, map_area, map_resolution): Args: position (x,y): x,y - Position that should be transformed. map_area (tuple): Area ((x_min, x_max), (y_min, y_max)) of the simulation that the map is depicting. - map_area (float): Resolution of the map in m per cells. + map_resolution (float): Resolution of the map in m per cells. Returns: Map coordinate as a tuple (x, y). """ @@ -65,7 +85,7 @@ def transform_map_coordinate_to_position(map_coordinate, map_area, map_resolutio return x, y -def create_occupancy_grid_map(simulation, area=None, resolution=0.05, object_filter=None, default_occupancy_value=1.0, occupancy_value_depth=32): +def create_occupancy_grid_map(simulation, area=None, resolution=0.05, object_filter=None, default_occupancy_value=1.0, occupancy_value_depth=16, transform=None): r"""Creates an occupancy grid map. The size of the map depends on the visible_map_area of the simulation and the resolution of the map. @@ -145,13 +165,22 @@ def create_occupancy_grid_map(simulation, area=None, resolution=0.05, object_fil radius = math.ceil(fixture.shape.radius / resolution) pygame.draw.circle(surface, color, center, radius, 0) - occupancy_grid = np.array(pixel_array) - occupancy_grid = occupancy_grid / max_color_value + occupancy_grid = np.array(pixel_array) / max_color_value + + map = OccupancyGridMap(map=occupancy_grid, area=area, resolution=resolution) + + if transform : - return OccupancyGridMap(map=occupancy_grid, area=area, resolution=resolution) + if not isinstance(transform, (list, tuple)): + transform = [transform] + for t in transform : + map = t(map, simulation, bodies, default_occupancy_value, occupancy_value_depth) -def create_local_perspective_occupancy_grid_map(global_map, depth=5.0, position=(0,0), orientation=0.0): + return map + + +def create_local_perspective_occupancy_grid_map(global_map, depth=5.0, position=(0,0), orientation=0.0, from_given_center=None, transform_position_to_map_coordinate=transform_position_to_map_coordinate): r"""Creates a local perspective of an occupancy grid map. Args: @@ -172,12 +201,21 @@ def create_local_perspective_occupancy_grid_map(global_map, depth=5.0, position= # TODO: there should be a way to avoid the transpose operations, by choosing a different points that should be local_map_length = int(depth * 2 / global_map.resolution) + + if from_given_center is None : + pos_in_map = transform_position_to_map_coordinate( + position, + map_area=global_map.area, + map_resolution=global_map.resolution + ) + else : + pos_in_map = transform_position_to_map_coordinate( + center_position=from_given_center, + position=position, + map_resolution=global_map.resolution, + ) + - pos_in_map = transform_position_to_map_coordinate( - position, - map_area=global_map.area, - map_resolution=global_map.resolution - ) orientation = mpi_sim.utils.constraint_angle(orientation) # rotation in for cv operator is in opposite direction @@ -192,7 +230,6 @@ def create_local_perspective_occupancy_grid_map(global_map, depth=5.0, position= rect_ur = pos_in_map + 0.5 * hvect * local_map_length + 0.5 * vvect * local_map_length rect_lr = pos_in_map + 0.5 * hvect * local_map_length - 0.5 * vvect * local_map_length src_pts = np.vstack([rect_ll, rect_ul, rect_ur, rect_lr]).astype(np.float32) - src_pts = src_pts # print('src_pts', src_pts) # destination points in pixel space diff --git a/mpi_sim/utils/occupancy_grid_transform.py b/mpi_sim/utils/occupancy_grid_transform.py new file mode 100644 index 0000000000000000000000000000000000000000..462d54b375a8db5ebedd9c4592672cceccba102f --- /dev/null +++ b/mpi_sim/utils/occupancy_grid_transform.py @@ -0,0 +1,56 @@ +import mpi_sim +import numpy as np +import cv2 + +class MapTransform(): + @staticmethod + def default_config(): + dc = mpi_sim.AttrDict() + return dc + + def __init__(self, config=None, **kwargs): + super(MapTransform, self).__init__(config=config, **kwargs) + + + def __call__(self, map, simulation, bodies, default_occupancy_value, occupancy_value_depth): + raise Warning('Not Implemented') + + +class RTABMAPlikeTransform(MapTransform): + + @staticmethod + def default_config(): + dc = mpi_sim.AttrDict() + return dc + + + def __init__(self, config=None, **kwargs): + self.config = mpi_sim.combine_dicts(kwargs, config, self.default_config()) + + + def __call__(self, map, simulation, bodies, default_occupancy_value, occupancy_value_depth): + """Transforms the map to make it looking like a 2D inflated RTAB-Map.""" + + new_map = np.copy(map.map) + # Inflate the original map + k = 3 #np.random.randint(1,3) + kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) + inflate_map = cv2.dilate(new_map, kernel, iterations=1) + # Complete the noisy_map when there is a 1 on the inflate map with a given probability of 4% + sampled_map = np.zeros(np.shape(inflate_map)) + for x in range(len(inflate_map[0])): + for y in range(len(inflate_map[1])): + if inflate_map[x][y] == 1 and np.random.random() > 0.98: + sampled_map[x][y] = 1 + + # Now dilate the sparse 1 on the new noisy map with a circular kernel + k = 20 #np.random.randint(15,20) + kernel_noisy_map = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k,k)) + k = 10 #np.random.randint(5,10) + final_noisy_map = np.clip( + cv2.dilate(sampled_map, kernel_noisy_map, iterations=1) + cv2.dilate(new_map, kernel=cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)), iterations=1), + 0, default_occupancy_value + ) + new_occupancy_grid = np.where(final_noisy_map > 0.1, default_occupancy_value, np.min(map.map)) + + return mpi_sim.utils.OccupancyGridMap(map=new_occupancy_grid, area=map.area, resolution=map.resolution) \ No newline at end of file diff --git a/mpi_sim/utils/social_map.py b/mpi_sim/utils/social_map.py new file mode 100644 index 0000000000000000000000000000000000000000..621feeebec73b71346ee57136c7b321e78819d93 --- /dev/null +++ b/mpi_sim/utils/social_map.py @@ -0,0 +1,220 @@ +import mpi_sim +import numpy as np +import cv2 +from collections import namedtuple +import mpi_sim.utils.truong_2018 as ssn + + +SocialMap = namedtuple('SocialMap', ['map', 'area', 'resolution']) + +def get_social_map_shape(area, resolution): + r"""Calculates the size of the social map that can be created via create_social_map(). + + Args: + area (tuple of floats): Area of the simulation that should be mapped in ((x_min, x_max), (y_min, y_max)). + resolution (float): Resolution in meter per cell. + + Returns: + Shape of the map as a tuple: (width, height). + """ + + x_width = area[0][1] - area[0][0] + y_width = area[1][1] - area[1][0] + + width = int(x_width / resolution) + height = int(y_width / resolution) + shape = (width, height) + + return shape + +def get_social_map_resolution(area, shape): + r"""Calculates the resolution of the social map that can be created via create_social_map(). + + Args: + area (tuple of floats): Area of the simulation that should be mapped in ((x_min, x_max), (y_min, y_max)). + shape (tuple): Shape of the map as a tuple: (width, height) + + Returns: + Resolution in meter per cell. (float) + """ + + x_width = area[0][1] - area[0][0] + #y_width = area[1][1] - area[1][0] + + (width, height) = shape + + resolution = int(x_width / width) + #resolution = int(y_width / height) + + return resolution + + +def transform_position_to_map_coordinate(position, map_area, map_resolution): + r"""Transform a position (x, y) in the simulation to a coordinate (x, y) of a map of the simulation. + + Args: + position (x,y): x,y - Position that should be transformed. + map_area (tuple): Area ((x_min, x_max), (y_min, y_max)) of the simulation that the map is depicting. + map_resolution (float): Resolution of the map in m per cells. + + Returns: Map coordinate as a tuple (x, y). + """ + x_min = map_area[0][0] + y_min = map_area[1][0] + x = int((position[0] - x_min) / map_resolution) + y = int((position[1] - y_min) / map_resolution) + return x, y + + +def transform_map_coordinate_to_position(map_coordinate, map_area, map_resolution): + r"""Transform a map coordinate (x, y) of a map to a position (x, y) in the simulation. + + Args: + map_coordinate (x,y): x,y - Map coordinate that should be transformed. + map_area (tuple): Area ((x_min, x_max), (y_min, y_max)) of the simulation that the map is depicting. + map_area (float): Resolution of the map in m per cells. + + Returns: Position as a tuple (x, y). + """ + x_min = map_area[0][0] + y_min = map_area[1][0] + x = map_coordinate[0] * map_resolution + x_min + y = map_coordinate[1] * map_resolution + y_min + return x, y + + +def create_social_map(simulation, area=None, resolution=0.05, human_velocities=None, flag_imported_global_map=False): + r"""Creates an social map. + + The size of the map depends on the visible_map_area of the simulation and the resolution of the map. + The map encodes the x-y position via map[x,y]. + + Args: + simulation (mpi_sim.Simulation): Simulation for which a map should be created. + area (tuple): Area of the simulation that should be mapped in ((x_min, x_max), (y_min, y_max)). (Default: None) + If None, it uses the simulation.visible_area. + resolution (float): Resolution in meter per cell (Default: 0.05). + human_velocities (list) : List of [x,y] velocities of humans + + Returns: + social map in form of a SocialMap (namedtuple with map, area, and resolution). + """ + + if area is None: + area = simulation.visible_area + + # the size of the map depends on the world size and the resolution + + # visible_area: ((min_x, max_x), (min_y, max_y)) + x_width = area[0][1] - area[0][0] + y_width = area[1][1] - area[1][0] + width = x_width / resolution + height = y_width / resolution + + + + object_filter = dict(types=mpi_sim.objects.Human) + + bodies = simulation.box2d_simulation.get_bodies( + object_types=object_filter.get('types', None), + object_ids=object_filter.get('ids', None), + object_properties= object_filter.get('properties', None), + ) + + persons = [] + for id, bodie in enumerate(bodies) : + x, y = np.array(bodie.position) + theta = np.pi/2 + bodie.angle + vx,vy = 0,0 + if human_velocities is not None: + if len(human_velocities) > 0 : + vx = human_velocities[id][0] + vy = human_velocities[id][1] + v = np.sqrt(vx**2 + vy**2) + persons.append((x ,y, theta, v, False)) + + + x_coordinates = np.linspace(area[0][1], area[0][0], int(width)) + y_coordinates = np.linspace(area[1][1], area[1][0], int(height)) + x_mesh, y_mesh = np.meshgrid(x_coordinates, y_coordinates) + + if not flag_imported_global_map: + global_social_map = np.flip(np.flip(ssn.calc_eps(x_mesh, y_mesh, persons), axis=1), axis=0).T + else : + global_social_map = np.flip(ssn.calc_eps(x_mesh, y_mesh, persons), axis=1) + + map = SocialMap(map=global_social_map, area=area, resolution=resolution) + + return map + + +def create_local_perspective_social_map(global_map, depth=5.0, position=(0,0), orientation=0.0, from_given_center=None, transform_position_to_map_coordinate=transform_position_to_map_coordinate): + r"""Creates a local perspective of an social map. + + Args: + global_map (SocialMap): Global social map which is the source for the local perspective. + depth (float): Viewing depth of the map in meters. The map size is 2 * depth. (Default: 5) + position (tuple of floats): Position (x,y) of the local map center in the global map. (Default: (0,0)) + orientation (float): Orientation of the map in rad. (Default: 0) + 0 for north, -pi/2 for east, pi/2 for west, pi for south + + Returns: Local perspective of the social map in form of a SocialMap (named tuple with map, area, and resolution) + + """ + + # see https://theailearner.com/tag/cv2-warpperspective/ + # Attention: opencv uses a different coordinate frame to us, thus the code additionally transforms the coordinate frames by + # having the opposite rotation direction, and using transpose operations + + # TODO: there should be a way to avoid the transpose operations, by choosing a different points that should be + + local_map_length = int(depth * 2 / global_map.resolution) + + if from_given_center is None : + pos_in_map = transform_position_to_map_coordinate( + position, + map_area=global_map.area, + map_resolution=global_map.resolution + ) + else : + pos_in_map = transform_position_to_map_coordinate( + center_position=from_given_center, + position=position, + map_resolution=global_map.resolution, + ) + + orientation = mpi_sim.utils.constraint_angle(orientation) + + # rotation in for cv operator is in opposite direction + hvect = np.array([np.cos(-orientation), -np.sin(-orientation)]) + vvect = np.array([np.sin(-orientation), np.cos(-orientation)]) + # print('hvect', hvect) + # print('vvect', vvect) + + # source points in pixel space + rect_ll = pos_in_map - 0.5 * hvect * local_map_length - 0.5 * vvect * local_map_length + rect_ul = pos_in_map - 0.5 * hvect * local_map_length + 0.5 * vvect * local_map_length + rect_ur = pos_in_map + 0.5 * hvect * local_map_length + 0.5 * vvect * local_map_length + rect_lr = pos_in_map + 0.5 * hvect * local_map_length - 0.5 * vvect * local_map_length + src_pts = np.vstack([rect_ll, rect_ul, rect_ur, rect_lr]).astype(np.float32) + # print('src_pts', src_pts) + + # destination points in pixel space + dst_pts = np.array( + [[0, 0], + [0, local_map_length], + [local_map_length, local_map_length], + [local_map_length, 0]], + dtype=np.float32 + ) + # print('dst_pts', dst_pts) + + m = cv2.getPerspectiveTransform(src_pts, dst_pts) + # use transpose operations to bring image into coordinate frame of cv and then back to our coordinate system + local_map = cv2.warpPerspective( + global_map.map.T, + m, + (local_map_length, local_map_length) + ).T + + return SocialMap(map=local_map, resolution=global_map.resolution, area=None) diff --git a/mpi_sim/utils/truong_2018.py b/mpi_sim/utils/truong_2018.py new file mode 100644 index 0000000000000000000000000000000000000000..bf2b5914bf1e1bf8d6c3bda02b37d260e283b90f --- /dev/null +++ b/mpi_sim/utils/truong_2018.py @@ -0,0 +1,853 @@ +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches +from scipy import optimize +from scipy import ndimage +import scipy.interpolate +import skfmm + + +def calc_group_circle(persons): + persons_coordinates = [[person[0], person[1]] for person in persons] + x_g, y_g, r_g, _ = least_squares_circle(persons_coordinates) + return x_g, y_g, r_g + + +def least_squares_circle(coords): + """ + Circle fit using least-squares solver. + Inputs: + + - coords, list or numpy array with len>2 of the form: + [ + [x_coord, y_coord], + ..., + [x_coord, y_coord] + ] + + Outputs: + + - xc : x-coordinate of solution center (float) + - yc : y-coordinate of solution center (float) + - R : Radius of solution (float) + - residu : MSE of solution against training data (float) + + Taken from package circle-fit. + """ + + def calc_R(x, y, xc, yc): + """ + calculate the distance of each 2D points from the center (xc, yc) + """ + return np.sqrt((x - xc) ** 2 + (y - yc) ** 2) + + def f(c, x, y): + """ + calculate the algebraic distance between the data points + and the mean circle centered at c=(xc, yc) + """ + Ri = calc_R(x, y, *c) + return Ri - Ri.mean() + + # coordinates of the barycenter + x = np.array([x[0] for x in coords]) + y = np.array([x[1] for x in coords]) + x_m = np.mean(x) + y_m = np.mean(y) + center_estimate = x_m, y_m + center, ier = optimize.leastsq(f, center_estimate, args=(x,y)) + xc, yc = center + Ri = calc_R(x, y, *center) + R = Ri.mean() + residu = np.sum((Ri - R)**2) + return xc, yc, R, residu + + +def calc_default_hand_positions(person, **parameters): + '''Calculates the default postion for the hands of a person, so that they are on the side of og''' + + default_hand_distance = parameters.get('default_hand_distance', 0.2) + + x_p, y_p, theta_p, _, _ = person + + # define position as if person has a 0 angle orientation at position [0,0] + pos_lh = np.array([[0, default_hand_distance]]).transpose() + pos_rh = np.array([[0, -default_hand_distance]]).transpose() + + # rotate the positions according + rotation_mat = np.array([[np.cos(theta_p), -np.sin(theta_p)], + [np.sin(theta_p), np.cos(theta_p)]]) + + pos_lh = np.matmul(rotation_mat, pos_lh).transpose()[0] + pos_rh = np.matmul(rotation_mat, pos_rh).transpose()[0] + + # translate according to position of person + x_lh = pos_lh[0] + x_p + y_lh = pos_lh[1] + y_p + x_rh = pos_rh[0] + x_p + y_rh = pos_rh[1] + y_p + + return x_lh, y_lh, x_rh, y_rh + + +def _calc_single_eps(x, y, person, d_po=None, **parameters): + + A_p = parameters.get('A_p', 1.0) + sigma_p_x = parameters.get('sigma_p_x', 0.2) #0.45 + sigma_p_y = parameters.get('sigma_p_y', 0.2) #0.45 + alpha = parameters.get('alpha', np.pi / 2) + fov_angle = parameters.get('fov_angle', 60 * np.pi / 180) + f_v = parameters.get('f_v', 0.8) + f_front = parameters.get('f_front', 0.2) + f_fov = parameters.get('f_fov', 0) + F_sit = parameters.get('F_sit', 2.5) + + f_h = parameters.get('f_h', 0.0) + sigma_h_x = parameters.get('sigma_h_x', 0.28) + sigma_h_y = parameters.get('sigma_h_y', 0.28) + + w_1 = parameters.get('w_1', 1.0) + w_2 = parameters.get('w_2', 1.0) + + if len(person) == 5: + x_p, y_p, theta_p, v_p, is_sitting = person + x_lh, y_lh, x_rh, y_rh = calc_default_hand_positions(person) + else: + x_p, y_p, theta_p, v_p, is_sitting, x_lh, y_lh, x_rh, y_rh = person + + f_sit = F_sit if is_sitting else 1.0 + + sigma_p_y_i = sigma_p_y + + # personal space f_p + d = np.sqrt((x - x_p) ** 2 + (y - y_p) ** 2) + + # Algorithm 1 + sigma_p_x_i = np.full_like(x, sigma_p_x) + + # angle according to the perspective of the person + theta = np.arctan2(y - y_p, x - x_p) + theta_diff = np.abs(np.arctan2(np.sin(theta - theta_p), np.cos(theta - theta_p))) + + # check if an object needs to be considered + if d_po is None: + in_field_of_view_of_p_inds = theta_diff <= fov_angle + in_frontal_area_of_p_inds = (theta_diff <= alpha) & ~in_field_of_view_of_p_inds + + sigma_p_x_i[in_field_of_view_of_p_inds] = (1 + v_p * f_v + f_front * f_sit + f_fov) * sigma_p_x + sigma_p_x_i[in_frontal_area_of_p_inds] = (1 + v_p * f_v + f_front * f_sit) * sigma_p_x + else: + in_frontal_area_of_p_inds = (theta_diff <= alpha) + + sigma_p_x_i[in_frontal_area_of_p_inds] = d_po / 2.0 + + f_p = A_p * np.exp(-(d * np.cos(theta_diff) / (np.sqrt(2) * sigma_p_x_i)) ** 2 - + (d * np.sin(theta_diff) / (np.sqrt(2) * sigma_p_y_i)) ** 2) + + # left hand space + d_lh = np.sqrt((x_p - x_lh) ** 2 + (y_p - y_lh) ** 2) + theta_lh = np.arctan2(y_lh - y_p, x_lh - x_p) + sigma_lh_x = (1 + f_h * d_lh) * sigma_h_x + + f_lh = A_p * np.exp(-(d * np.cos(theta - theta_lh) / (np.sqrt(2) * sigma_lh_x)) ** 2 - + (d * np.sin(theta - theta_lh) / (np.sqrt(2) * sigma_h_y)) ** 2) + + # right hand space + d_rh = np.sqrt((x_p - x_rh) ** 2 + (y_p - y_rh) ** 2) + theta_rh = np.arctan2(y_rh - y_p, x_rh - x_p) + sigma_rh_x = (1 + f_h * d_rh) * sigma_h_x + + f_rh = A_p * np.exp(-(d * np.cos(theta - theta_rh) / (np.sqrt(2) * sigma_rh_x)) ** 2 - + (d * np.sin(theta - theta_rh) / (np.sqrt(2) * sigma_h_y)) ** 2) + + # combine hand and person spaces + f_hands = np.maximum(f_lh, f_rh) + f_cur_eps = np.maximum(w_1 * f_p, w_2 * f_hands) + + return f_cur_eps + + +def calc_eps(x, y, persons, objects=None, **parameters): + '''Calculates the extended personal space of a human.''' + + if not isinstance(persons, list): persons = [persons] + if objects is not None and not isinstance(objects, list): objects = [objects] + + T_dis = parameters.get('T_dis', 3.6) + T_ang = parameters.get('T_ang', 10 * np.pi / 180) + + f_eps = np.zeros_like(x) + + for person in persons: + x_p, y_p, theta_p, v_p, is_sitting = person[0:5] + + is_incorporated_object = False + + if objects is not None: + for object in objects: + + # check if the object should be considered + x_ob, y_ob = object + # check if object is in interaction with the person + d_po = np.sqrt((x_ob - x_p) ** 2 + (y_ob - y_p) ** 2) + theta_po = np.arctan2((y_ob - y_p), (x_ob - x_p)) + theta_diff = np.abs(np.arctan2(np.sin(theta_po - theta_p), np.cos(theta_po - theta_p))) + + # check if the object should be incorporated into eps + if d_po < T_dis and theta_diff < T_ang: + cur_f_eps = _calc_single_eps(x, y, person, d_po=d_po, **parameters) + f_eps = np.maximum(f_eps, cur_f_eps) + + is_incorporated_object = True + + # if no object was incorporated into the eps, then calculate one without an object + if not is_incorporated_object: + cur_f_eps = _calc_single_eps(x, y, person, **parameters) + f_eps = np.maximum(f_eps, cur_f_eps) + + return f_eps + + +def calc_sis(x, y, persons, theta_g=0.0, v_g= 0.0, **parameters): + + if not isinstance(persons, list): persons = [persons] + + if len(persons) == 1: + f_group = np.zeros_like(x) + else: + A_p = parameters.get('A_p', 1.0) + f_g = parameters.get('f_g', -0.5) # 0.0 + f_v = parameters.get('f_v', 0.8) + + # use a simple circle approximation if the center and radius of the group are not given + x_g, y_g, r_g = calc_group_circle(persons) + + sigma_g_x = (r_g / 2) * (1 + f_g) + sigma_g_y = (r_g / 2) * (1 + f_g) + + # if the group moves then differentiate between area in movement direction and area oposite to the mvoement direction + if v_g > 0.0: + sigma_g_x = np.full_like(x, sigma_g_x) + + theta = np.arctan2(y - y_g, x - x_g) + theta_diff = np.abs(np.arctan2(np.sin(theta - theta_g), np.cos(theta - theta_g))) + + sigma_g_x[theta_diff < np.pi/2] = (r_g / 2) * (1 + f_g + f_v * v_g) + + d = np.sqrt((x - x_g) ** 2 + (y - y_g) ** 2) + theta = np.arctan2(y - y_g, x - x_g) + + f_group = A_p * np.exp(-(d * np.cos(theta - theta_g) / (np.sqrt(2) * sigma_g_x)) ** 2 - + (d * np.sin(theta - theta_g) / (np.sqrt(2) * sigma_g_y)) ** 2) + + return f_group + + +def calc_dsz(x, y, persons, groups=None, objects=None, **parameters): + + if groups is not None and not isinstance(groups, list): groups = [groups] + + w_3 = parameters.get('w_3', 1.0) + w_4 = parameters.get('w_4', 1.0) + + # calc eps + f_eps = calc_eps(x, y, persons, objects, **parameters) + + # calc sis for all groups + f_sis = np.zeros_like(x) + if groups is not None: + for group in groups: + + # get persons of the group and the direction and velocity of the group + person_idxs, theta_g, v_g = group + cur_persons = [persons[p_idx] for p_idx in person_idxs] + + cur_f_sis = calc_sis(x, y, cur_persons, theta_g, v_g, **parameters) + f_sis = np.maximum(f_sis, cur_f_sis) + + # combine both + f_dsz = np.maximum(w_3 * f_eps, w_4 * f_sis) + + return f_dsz + + +def calc_goal_pos(x_mesh, y_mesh, f_dsz, robot, persons, **parameters): + + if not isinstance(persons, list): persons = [persons] + + fov_angle = parameters.get('fov_angle', 60 * np.pi / 180) + r_step = parameters.get('r_step', 0.1) + r_p_0 = parameters.get('r_p_0', 0.1) + dsz_border = parameters.get('dsz_border', 0.001) #0.03 + + # helper function to define the points on a circle + def calc_circle_points(x_mesh, y_mesh, x_c, y_c, r_c): + return (np.abs(np.hypot(x_c - x_mesh, y_c - y_mesh) - r_c) < 0.04) + + if len(persons) == 1: + # single person + x_c, y_c = persons[0][0], persons[0][1] + r_c = r_p_0 + else: + # group of people + x_c, y_c, r_c = calc_group_circle(persons) + + A_out = np.full_like(x_mesh, False, dtype=bool) + + while True: + r_c += r_step + A_in = calc_circle_points(x_mesh, y_mesh, x_c, y_c, r_c) + + A_in_1 = A_in.copy() + A_out_1 = np.full_like(x_mesh, False, dtype=bool) + + # check for circle points that are within all persons viewing point + for person in persons: + + x_p, y_p, theta_p = person[0], person[1], person[2] + + # calculate the angle of each point on the circle in relation to the person + theta = np.arctan2(y_mesh[A_in_1] - y_p, x_mesh[A_in_1] - x_p) + # angle according to the perspective of the person + theta_diff = np.abs(np.arctan2(np.sin(theta - theta_p), np.cos(theta - theta_p))) + + # check if circle points are in the viewing angle of the person + A_out_1[A_in_1] = (theta_diff <= fov_angle) + + # if there is no point that is viewed stop + if not np.any(A_out_1): + break + else: + # only check further points that are viewed by all people + A_in_1 = A_out_1.copy() + + if np.any(A_out_1): + # check if the points are outside the DMZ + points_outside_DSZ_inds = f_dsz[A_out_1] <= dsz_border + A_out[A_out_1] = points_outside_DSZ_inds + else: + # there are no points on the circle that can be viewed by all people + # --> just take points into account that are inside the DSZ + points_outside_DSZ_inds = f_dsz[A_in] <= dsz_border + A_out[A_in] = points_outside_DSZ_inds + + if np.any(A_out): + break + + A_out, goal = _filter_approaching_areas(x_mesh, y_mesh, A_out, robot, (x_c, y_c)) + + return goal, A_out + + +def _filter_approaching_areas(x_mesh, y_mesh, A_out, robot, target): + + A_out = A_out.astype(np.float) + + x_r, y_r = robot[0], robot[1] + x_t, y_t = target[0], target[1] + + # identify connected areas in A_out + labeled_A_out, n_areas = ndimage.measurements.label(A_out, np.ones((3,3), dtype=np.int)) + + min_d_r = np.inf + x_q, y_q = np.nan, np.nan + + # ignore area 1 + for area_id in range(1, n_areas+1): + + label_inds = (labeled_A_out == area_id) + + # center point of area (true center of mass) + center_x, center_y = ndimage.measurements.center_of_mass(label_inds) + + # identify the point in the 2d matrix that is the closest to the true center + grid_x, grid_y = np.indices(A_out.shape).astype(np.float) + d = np.sqrt((grid_x - center_x) ** 2 + (grid_y - center_y) ** 2) + + # replace A_out with the distance + A_out[label_inds] = d[label_inds] + + # find coordinates with the shortest distance + min_d_grid_idx = np.where(d == np.min(d)) + x_a_idx = min_d_grid_idx[0][0] + y_a_idx = min_d_grid_idx[1][0] + + x_a = x_mesh[x_a_idx, y_a_idx] + y_a = y_mesh[x_a_idx, y_a_idx] + + # distance to the robot + d_r = np.sqrt((x_a - x_r) ** 2 + (y_a - y_r) ** 2) + + if d_r < min_d_r: + min_d_r = d_r + x_q, y_q = x_a, y_a + + # calculate the goal orientation + theta_q = np.arctan2(y_t - y_q, x_t - x_q) + + goal = (x_q, y_q, theta_q) + + return A_out, goal + + +def calc_path(x_mesh, y_mesh, velocity_map, robot, goal, **parameters): + + max_steps = parameters.get('max_steps', 100) + position_step_width = parameters.get('position_step_width', 0.1) # in meter + rotation_step_width = parameters.get('rotation_step_width', np.pi*0.1) # in rad + min_goal_position_dist = parameters.get('min_goal_position_dist', 0.05) # in meter + min_goal_rotation_dist = parameters.get('min_goal_rotation_dist', 0.05) # in rad + + x_goal, y_goal, theta_goal = goal + x_robot, y_robot = robot[0], robot[1] + + # TODO: also update the rotation of the robot + theta_robot = theta_goal + + # compute the shortest travel time map based on the velocity map + # TODO: if the goal location is not reachable, then select the most nearby point that is reachable + + # define the goal location to the point in the world grid that is closest to the goal location + # phi there is -1, otherwise it is 1 + distance_to_goal = np.sqrt((x_mesh - x_goal) ** 2 + (y_mesh - y_goal) ** 2) + phi = np.ones_like(x_mesh) + phi[np.unravel_index(np.argmin(distance_to_goal), phi.shape)] = -1 + + travel_time_map = skfmm.travel_time(phi, velocity_map, order=1) + + # calc gradient iver travel_time map and create interpolation functions + grad_t_y, grad_t_x = np.gradient(travel_time_map) + + # interpolate the gradients and velocities + gradx_interp = scipy.interpolate.RectBivariateSpline(x_mesh[0, :], y_mesh[:, 0], grad_t_x) + grady_interp = scipy.interpolate.RectBivariateSpline(x_mesh[0, :], y_mesh[:, 0], grad_t_y) + velocity_interp = scipy.interpolate.RectBivariateSpline(x_mesh[0, :], y_mesh[:, 0], velocity_map) + + path = [] + path.append((x_robot, y_robot, theta_robot)) + + # create path by following the gradient of the travel time + for step in range(max_steps): + + position_goal_distance = np.sqrt((x_robot - x_goal) ** 2 + (y_robot - y_goal) ** 2) + rotation_goal_distance = np.abs(np.arctan2(np.sin(theta_robot - theta_goal), np.cos(theta_robot - theta_goal))) + + if position_goal_distance <= min_goal_position_dist and rotation_goal_distance <= min_goal_rotation_dist: + break + + movement_direction = np.array([-gradx_interp(y_robot, x_robot)[0][0], + -grady_interp(y_robot, x_robot)[0][0]]) + + movement_speed = velocity_interp(y_robot, x_robot)[0][0] + + # create normalized movement direction vector and change its size according to the movement speed + f_position = movement_direction / np.linalg.norm(movement_direction) * movement_speed + + # update the position of the robot + x_robot += f_position[0] * position_step_width + y_robot += f_position[1] * position_step_width + + f_orientation = 0.0 + # TODO: adapt the rotation of the robot + path.append((x_robot, y_robot, theta_robot)) + + return path + + +def occupancy_map_to_velocity_map(x_mesh, y_mesh, occupancy_map, **parameters): + '''Create a velocity map for a given occupancy map.''' + + sigma = parameters.get('sigma', 0.15) + + d_x = [x_mesh[0, 1] - x_mesh[0, 0], y_mesh[1, 0] - y_mesh[0, 0]] + + # use fast marching method to compute the min distance from free to occupied areas + phi = np.ones_like(occupancy_map, dtype=int) + phi[occupancy_map == 1] = -1 + + distance_map = skfmm.distance(phi, d_x, order=1) + + # compute velocity based on distance + velocity_map = 1.0 - np.exp(-distance_map/(np.sqrt(2)*sigma)) + velocity_map[occupancy_map == 1] = 0 + + return velocity_map + + +def get_default_occupany_map(x_mesh, y_mesh): + + occupancy_map = np.full_like(x_mesh, False, dtype=bool) + + # borders + occupancy_map[0, :] = True + occupancy_map[-1, :] = True + occupancy_map[:, 0] = True + occupancy_map[:, -1] = True + + return occupancy_map + + +def add_box_to_occupancy_map(x_mesh, y_mesh, occupancy_map, box): + + occupancy_map = np.copy(occupancy_map) + + x_box, y_box, width, height = box + + # identify the starting in x + x_start = x_box + x_end = x_start + width + y_start = y_box + y_end = y_box + height + + d_1_start_index = np.argmin(np.abs(x_mesh[0, :] - x_start)) + d_1_end_index = np.argmin(np.abs(x_mesh[0, :] - x_end)) + + d_0_start_index = np.argmin(np.abs(y_mesh[:, 0] - y_start)) + d_0_end_index = np.argmin(np.abs(y_mesh[:, 0] - y_end)) + + occupancy_map[d_0_start_index:d_0_end_index, d_1_start_index:d_1_end_index] = True + + return occupancy_map + + +def get_default_occupany_map(x_mesh, y_mesh): + + occupancy_map = np.full_like(x_mesh, False, dtype=bool) + + # borders + occupancy_map[0, :] = True + occupancy_map[-1, :] = True + occupancy_map[:, 0] = True + occupancy_map[:, -1] = True + + return occupancy_map + + +def plot_2d(x_mesh, y_mesh, space_model, occupancy_map=None, approaching_areas=None, persons=None, objects=None, goal=None, robot=None, path=None, title='', is_contur_lines=False): + + if persons is None: + persons = [] + elif not isinstance(persons, list): + persons = [persons] + + if objects is None: + objects = [] + elif not isinstance(objects, list): + objects = [objects] + + origin = 'lower' + + fig, ax = plt.subplots(constrained_layout=True) + + CS = ax.contourf(x_mesh, y_mesh, space_model, 20, cmap=plt.cm.binary, origin=origin) + + # Make a colorbar for the ContourSet returned by the contourf call. + cbar = fig.colorbar(CS) + cbar.ax.set_ylabel('space model value') + # Add the contour line levels to the colorbar + if is_contur_lines: + # Note that in the following, we explicitly pass in a subset of + # the contour levels used for the filled contours. Alternatively, + # We could pass in additional levels to provide extra resolution, + # or leave out the levels kwarg to use all of the original levels. + CS2 = ax.contour(CS, levels=CS.levels[::2], colors='r', origin=origin) + cbar.add_lines(CS2) + + if approaching_areas is not None: + inds = approaching_areas > 0 + ax.scatter(x_mesh[inds], y_mesh[inds], s=1, c=-approaching_areas[inds]) + + if robot is not None: + ax.scatter(robot[0], robot[1], s=150, c='g') + + if goal is not None: + # print goal position and orientation + ax.scatter(goal[0], goal[1], c='r') + plt.arrow(goal[0], goal[1], np.cos(goal[2]) * 0.1, np.sin(goal[2]) * 0.1, edgecolor='r') + + # draw persons + for person in persons: + if len(person) == 5: + x_p, y_p, theta_p, v_p, is_sitting = person + x_lh, y_lh, x_rh, y_rh = calc_default_hand_positions(person) + else: + x_p, y_p, theta_p, v_p, is_sitting, x_lh, y_lh, x_rh, y_rh = person + + # person + e_p = matplotlib.patches.Ellipse((x_p, y_p), 0.15, 0.25, + angle=theta_p*180/np.pi, linewidth=2, fill=False, zorder=2, edgecolor='b') + ax.add_patch(e_p) + + # left hand + e_lh = matplotlib.patches.Ellipse((x_lh, y_lh), 0.05, 0.05, linewidth=1, fill=False, zorder=2, edgecolor='b') + ax.add_patch(e_lh) + + # right hand + e_rh = matplotlib.patches.Ellipse((x_rh, y_rh), 0.05, 0.05, linewidth=1, fill=False, zorder=2, edgecolor='b') + ax.add_patch(e_rh) + + plt.arrow(x_p, y_p, np.cos(theta_p) * 0.1, np.sin(theta_p) * 0.1, edgecolor='b') + + # draw objects + for object in objects: + x_ob, y_ob = object + e = matplotlib.patches.Rectangle((x_ob-0.1, y_ob-0.1), 0.2, 0.2, edgecolor='r') + ax.add_patch(e) + + if occupancy_map is not None: + ax.scatter(x_mesh[occupancy_map == 1], y_mesh[occupancy_map == 1], c='k') + + if path is not None: + path_position = np.array([[point[0], point[1]] for point in path]) + ax.plot(path_position[:,0], path_position[:,1]) + + ax.set_title(title) + ax.set_xlabel('x') + ax.set_ylabel('y') + + plt.show() + + +if __name__ == '__main__': + + # TODO: the current group identification algorithm does not take into account the orientation of people + + # ############################### + # # EPS + # + # x_coordinates = np.linspace(0, 4, 400) + # y_coordinates = np.linspace(0, 4, 400) + # x_mesh, y_mesh = np.meshgrid(x_coordinates, y_coordinates) + # + # # FIGURE 2. b + # person = (2.0, 2.0, -np.pi/2, 0.0, False) # (x, y, theta, v, is_sitting) + # f_eps = calc_eps(x_mesh, y_mesh, person, f_fov=0.4, f_front=0.0) + # plot_2d(x_mesh, y_mesh, f_eps, persons=person, title='FOV based EPS (Fig. 2, b)') + # + # # FIGURE 2. c + # person = (2.0, 2.0, -np.pi/2, 0.0, False) # (x, y, theta, v, is_sitting) + # f_eps = calc_eps(x_mesh, y_mesh, person) + # plot_2d(x_mesh, y_mesh, f_eps, persons=person, title='Human frontal area-based EPS (Fig. 2, c)') + # + # # FIGURE 2. d + # person = (2.0, 2.0, -np.pi/2, 0.0, True) # (x, y, theta, v, is_sitting) + # #person = (2.0, 2.0, 0, 0.0, True) # (x, y, theta, v, is_sitting) + # f_eps = calc_eps(x_mesh, y_mesh, person) + # plot_2d(x_mesh, y_mesh, f_eps, persons=person, title='EPS of sitting person (Fig. 2, d)') + # + # # FIGURE 2. e + # person = (2.0, 2.0, -np.pi/2, 0.6, False) # (x, y, theta, v, is_sitting) + # f_eps = calc_eps(x_mesh, y_mesh, person) + # plot_2d(x_mesh, y_mesh, f_eps, persons=person, title='EPS of moving person (Fig. 2, e)') + # + # # FIGURE 2. f + # person = (2.0, 2.0, -np.pi/2, 0, False) # (x, y, theta, v, is_sitting) + # object = (2.0, 0.5) + # f_eps = calc_eps(x_mesh, y_mesh, person, object) + # plot_2d(x_mesh, y_mesh, f_eps, persons=person, objects=object, title='EPS of human with object (Fig. 2, f)') + + + # ############################### + # # SIS + # + # x_coordinates = np.linspace(0, 4, 400) + # y_coordinates = np.linspace(0, 4, 400) + # x_mesh, y_mesh = np.meshgrid(x_coordinates, y_coordinates) + # + # # # FIGURE 3. a + # persons = [(1.0, 2.0, 0.0, 0, False), # (x, y, theta, v, is_sitting) + # (3.0, 2.0, np.pi, 0, False)] + # theta_g, v_g = 0.0, 0.0 + # f_sis = calc_sis(x_mesh, y_mesh, persons, theta_g=theta_g, v_g=v_g) + # plot_2d(x_mesh, y_mesh, f_sis, persons=persons, title='SIS vis-a-vis (Fig. 3, a)') + # + # # FIGURE 3. b + # persons = [(1.5, 1.5, np.pi*0.4, 0, False), # (x, y, theta, v, is_sitting) + # (2.5, 1.5, np.pi*0.6, 0, False)] + # theta_g, v_g = 0.0, 0.0 + # f_sis = calc_sis(x_mesh, y_mesh, persons, theta_g=theta_g, v_g=v_g) + # plot_2d(x_mesh, y_mesh, f_sis, persons=persons, title='SIS v-shape (Fig. 3, b)') + # + # # FIGURE 3. c + # persons = [(1.5, 1.0, np.pi*0.4, 0, False), # (x, y, theta, v, is_sitting) + # (2.5, 1.0, np.pi*0.6, 0, False), + # (2.0, 3.0, np.pi*-0.5, 0, False)] + # theta_g, v_g = 0.0, 0.0 + # f_sis = calc_sis(x_mesh, y_mesh, persons, theta_g=theta_g, v_g=v_g) + # plot_2d(x_mesh, y_mesh, f_sis, persons=persons, title='SIS close group (Fig. 3, c)') + # + # # FIGURE 3. d + # persons = [(1.75, 1.3, np.pi*0.45, 0.5, False), # (x, y, theta, v, is_sitting) + # (2.25, 1.3, np.pi*0.55, 0.5, False)] + # theta_g, v_g = np.pi*0.5, 0.5 + # f_sis = calc_sis(x_mesh, y_mesh, persons, theta_g=theta_g, v_g=v_g) + # plot_2d(x_mesh, y_mesh, f_sis, persons=persons, title='SIS - Two people moving in abreast-shape (Fig. 3, d)') + # + # # FIGURE 3. e + # persons = [(1.5, 1.8, np.pi*0.45, 0, False), # (x, y, theta, v, is_sitting) + # (2.0, 1.2, np.pi*0.5, 0, False), + # (2.5, 1.8, np.pi*0.55, 0, False)] + # theta_g, v_g = np.pi * 0.5, 0.5 + # f_sis = calc_sis(x_mesh, y_mesh, persons, theta_g=theta_g, v_g=v_g) + # plot_2d(x_mesh, y_mesh, f_sis, persons=persons, title='SIS - Three people moving in v-shape (Fig. 3, e)') + + + # ############################### + # # DSZ + # + # x_coordinates = np.linspace(0, 6, 700) + # y_coordinates = np.linspace(0, 6, 700) + # x_mesh, y_mesh = np.meshgrid(x_coordinates, y_coordinates) + # + # # Fig 4 (a) + # persons = [(2.5, 1.8, np.pi * 0.45, 0.0, False), + # (3.5, 1.8, np.pi * 0.55, 0.0, False)] + # object = (3.0, 5.0) + # group = ([0, 1], 0.0, 0.0) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group, objects=object) + # plot_2d(x_mesh, y_mesh, f_dsz, persons=persons, objects=object, title='DSZ (Fig. 4, a)') + # + # # Fig 4 (b) + # persons = [(2.5, 1.9, np.pi * 0.25, 0.0, False), + # (3.0, 3.8, np.pi * -0.5, 0.0, False), + # (3.5, 1.9, np.pi * 0.75, 0.0, False)] + # group = ([0, 1, 2], 0.0, 0.0) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + # plot_2d(x_mesh, y_mesh, f_dsz, persons=persons, title='DSZ (Fig. 4, b)') + # + # + # # Fig 4 (c) + # persons = [(2.5, 3.0, np.pi * 0.5, 0.8, False), + # (3.0, 2.5, np.pi * 0.5, 0.8, False), + # (3.5, 3.0, np.pi * 0.5, 0.8, False)] + # group = ([0, 1, 2], np.pi * 0.5, 0.8) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + # plot_2d(x_mesh, y_mesh, f_dsz, persons=persons, title='DSZ (Fig. 4, c)') + # + # + # # Fig 4 (d) + # persons = [(2.5, 2.5, np.pi * 0.25, 0.0, False), + # (3.5, 2.5, np.pi * 0.75, 0.0, True)] + # group = ([0, 1], 0.0, 0.0) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + # plot_2d(x_mesh, y_mesh, f_dsz, persons=persons, title='DSZ (Fig. 4, d)') + + + ################################# + # Figure 5 - Approach areas + + # x_coordinates = np.linspace(0, 6, 700) + # y_coordinates = np.linspace(0, 6, 700) + # x_mesh, y_mesh = np.meshgrid(x_coordinates, y_coordinates) + # + # # Fig 5 d top - right + # robot = (5.0, 5.0) + # persons = [(2.5, 1.8, np.pi * 0.45, 0.0, False), + # (3.5, 1.8, np.pi * 0.55, 0.0, False)] + # object = (3.0, 5.0) + # group = ([0, 1], 0.0, 0.0) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group, objects=object) + # goal, A_out = calc_goal_pos(x_mesh, y_mesh, f_dsz, robot, [persons[p_idx] for p_idx in group[0]]) + # plot_2d(x_mesh, y_mesh, f_dsz, robot=robot, persons=persons, objects=object, approaching_areas=A_out, goal=goal, + # title='Approach area (Fig. 5, d, top right)') + # + # # Fig 5 d bottom right + # robot = (5.0, 5.0) + # persons = [(2.5, 1.9, np.pi * 0.25, 0.0, False), + # (3.0, 3.8, np.pi * -0.5, 0.0, False), + # (3.5, 1.9, np.pi * 0.75, 0.0, False)] + # group = ([0, 1, 2], 0.0, 0.0) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + # goal, A_out = calc_goal_pos(x_mesh, y_mesh, f_dsz, robot, [persons[p_idx] for p_idx in group[0]]) + # plot_2d(x_mesh, y_mesh, f_dsz, robot=robot, persons=persons, approaching_areas=A_out, goal=goal, + # title='Approach area (Fig. 5, d, bottom right)') + # + # # Fig 5 d top left + # robot = (5.0, 5.0) + # persons = [(2.5, 3.0, np.pi * 0.5, 0.8, False), + # (3.0, 2.5, np.pi * 0.5, 0.8, False), + # (3.5, 3.0, np.pi * 0.5, 0.8, False)] + # group = ([0, 1, 2], np.pi * 0.5, 0.8) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + # goal, A_out = calc_goal_pos(x_mesh, y_mesh, f_dsz, robot, [persons[p_idx] for p_idx in group[0]]) + # plot_2d(x_mesh, y_mesh, f_dsz, robot=robot, persons=persons, approaching_areas=A_out, goal=goal, + # title='Approach area (Fig. 5, d, top left)') + # + # # Fig 5 d bottom left + # robot = (5.0, 5.0) + # persons = [(2.5, 2.5, np.pi * 0.25, 0.0, False), + # (3.5, 2.5, np.pi * 0.75, 0.0, True)] + # group = ([0, 1], 0.0, 0.0) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + # goal, A_out = calc_goal_pos(x_mesh, y_mesh, f_dsz, robot, [persons[p_idx] for p_idx in group[0]]) + # plot_2d(x_mesh, y_mesh, f_dsz, robot=robot, persons=persons, approaching_areas=A_out, goal=goal, + # title='Approach area (Fig. 5, d, bottom left)') + + + + # ########################################## + # # PATH PLANNER + # + # x_coordinates = np.linspace(0, 6, 700) + # y_coordinates = np.linspace(0, 6, 700) + # + # x_mesh, y_mesh = np.meshgrid(x_coordinates, y_coordinates) + # + # # Fig 5 d bottom left + # robot = (3.0, 1.0, 0.0) + # persons = [(2.5, 2.5, np.pi * 0.25, 0.0, False), + # (3.5, 2.5, np.pi * 0.75, 0.0, True)] + # group = ([0, 1], 0.0, 0.0) + # f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + # goal, A_out = calc_goal_pos(x_mesh, y_mesh, f_dsz, robot, [persons[p_idx] for p_idx in group[0]]) + # # define velocity map + # velocity_map = 1.0 - f_dsz + # path = calc_path(x_mesh,y_mesh, velocity_map, robot, goal) + # plot_2d(x_mesh, y_mesh, f_dsz, robot=robot, persons=persons, approaching_areas=A_out, goal=goal, path=path, + # title='Robot path') + + + ########################################## + # OBSTACLES + + x_coordinates = np.linspace(0, 6, 600) + y_coordinates = np.linspace(0, 6, 600) + x_mesh, y_mesh = np.meshgrid(x_coordinates, y_coordinates) + + # default occupancy map + occupancy_map = get_default_occupany_map(x_mesh, y_mesh) + occupancy_map = add_box_to_occupancy_map(x_mesh, y_mesh, occupancy_map, (1.0, 1, 300, 0.2)) + velocity_map = occupancy_map_to_velocity_map(x_mesh, y_mesh, occupancy_map) + + robot = (3.0, 0.5, 0.0) + persons = [(2.5, 3.5, np.pi * 0.25, 0.0, False), + (3.5, 3.5, np.pi * 0.75, 0.0, True), + (1.0, 3.0, np.pi * -0.5, 0.0, False)] + group = ([0, 1], 0.0, 0.0) + + f_dsz = calc_dsz(x_mesh, y_mesh, persons, groups=group) + + goal, A_out = calc_goal_pos(x_mesh, y_mesh, f_dsz, robot, [persons[p_idx] for p_idx in group[0]]) + + # define velocity map + velocity_map = np.minimum(velocity_map, 1.0 - f_dsz) + + + path = calc_path(x_mesh,y_mesh, velocity_map, robot, goal) + + plot_2d(x_mesh, y_mesh, f_dsz, robot=robot, persons=persons, approaching_areas=A_out, goal=goal, path=path, + occupancy_map=occupancy_map, title='Robot path') + + + # TODO: + # allow different groups + # define which person or group to join + + + # TODO: group identification + + # TODO: Simulation + + # TODO: incorporate movement + + # TODO: compute the travel time to different possible goals to select one the with the optimal travel time and not the one that is closest to the robot diff --git a/requirements.txt b/requirements.txt index 20308afc4be1f0b071cf37464acf366c5ab446b1..550d4c7f2692ab606969cc05c6a220856c523433 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ attrs==21.4.0 backcall==0.2.0 beautifulsoup4==4.11.1 bleach==5.0.1 -Box2D==2.3.10 +Box2D==2.3.10 -f https://github.com/pybox2d/pybox2d.git cachetools==5.2.0 certifi==2022.5.18.1 cffi==1.15.1 @@ -43,7 +43,7 @@ ipython-genutils==0.2.0 ipywidgets==7.6.5 isort==5.10.1 jax==0.2.20 -jaxlib==0.1.71 +jaxlib==0.1.71 -f https://storage.googleapis.com/jax-releases/jax_releases.html jedi==0.18.1 Jinja2==3.1.2 joblib==1.1.0 @@ -61,7 +61,6 @@ matplotlib==3.5.2 matplotlib-inline==0.1.3 mccabe==0.7.0 mistune==2.0.4 --e git+https://gitlab.inria.fr/spring/wp6_robot_behavior/multiparty_interaction_simulator.git@f38e0e6373b38aeab5fbe41e78732d4aa3f1cdf8#egg=mpi_sim nbclassic==0.4.8 nbclient==0.7.0 nbconvert==7.2.5 diff --git a/setup.cfg b/setup.cfg index a72605bb5443865562a8a86216acb4c74ade6a8e..214125efafb54e1eaa020cf44549253f13c27ee1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ install_requires = six >= 1.16.0 scipy >= 1.8.1 opencv-python >= 4.6.0.66 -python_requires = - >=3.8, <3.9 +#python_requires = +# >=3.8, <=3.9.16 # required packages are in the setup.py \ No newline at end of file diff --git a/test/components/test_raycast_sensor.py b/test/components/test_raycast_sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..805d393215717805ff826218801ab8ec150be1ed --- /dev/null +++ b/test/components/test_raycast_sensor.py @@ -0,0 +1,116 @@ +import mpi_sim +import numpy.testing as np_test +from Box2D import b2PolygonShape, b2CircleShape +import numpy as np + + +def manual_test(): + + # create the simulation + sim = mpi_sim.Simulation( + visible_area = ((-6,6),(-6,6)), + + objects=[ + {'type': 'Wall', 'position': [-5.1, 0], 'orientation': np.pi, 'length': 10.2}, + {'type': 'Wall', 'position': [5.1, 0], 'orientation': 0., 'length': 10.2}, + {'type': 'Wall', 'position': [0., 5.1], 'orientation': np.pi / 2, 'length': 10.2}, + {'type': 'Wall', 'position': [0., -5.1], 'orientation': -np.pi / 2, 'length': 10.2}, + #{'type': 'RoundTable', 'id': 10, 'position': [1., 0], 'radius': 0.5} + ], + processes=[{'type': 'GUI'}] + ) + + + agent = mpi_sim.objects.ARIRobot( # type: ignore + components=[dict( + type=mpi_sim.components.RayCastSensor, + name = 'raycast', + field_of_view = 360, + delta_angle = 3, + ray_max_range = 3, + noise = False, + noise_ratio = 0.5, + draw_raycast = True + )], + position = (-2, 0), + orientation = -np.pi/2 + ) + + sim.add_object(agent) + sim.set_reset_point() + sim.reset() + for _ in range(30): + for _ in range(22): + agent.forward_velocity = 0.1 + agent.orientation_velocity = 0.1 + sim.step() + agent.raycast.raycast_distances + + +def test_3_rays_with_walls(): + + # create the simulation + sim = mpi_sim.Simulation( + visible_area = ((-6,6),(-6,6)), + + objects=[ + {'type': 'Wall', 'position': [-5.1, 0], 'orientation': np.pi, 'length': 10.2}, + {'type': 'Wall', 'position': [5.1, 0], 'orientation': 0., 'length': 10.2}, + {'type': 'Wall', 'position': [0., 5.1], 'orientation': np.pi / 2, 'length': 10.2}, + {'type': 'Wall', 'position': [0., -5.1], 'orientation': -np.pi / 2, 'length': 10.2}, + ], + ) + + + agent = mpi_sim.objects.ARIRobot( # type: ignore + components=[dict( + type=mpi_sim.components.RayCastSensor, + name = 'raycast', + field_of_view = 180, + delta_angle = 90, + + draw_raycast = False + )], + position = (0,0), + orientation = 0 + ) + + sim.add_object(agent) + sim.set_reset_point() + sim.reset() + sim.step() + np.testing.assert_almost_equal(agent.raycast.raycast_distances, [5,5,5], decimal = 2) + np.testing.assert_almost_equal(agent.raycast.raycast_angle_degrees, [90,0,-90], decimal = 2) + +def test_3_rays_without_walls(): + + # create the simulation + sim = mpi_sim.Simulation( + visible_area = ((-6,6),(-6,6)), + ) + + + agent = mpi_sim.objects.ARIRobot( # type: ignore + components=[dict( + type=mpi_sim.components.RayCastSensor, + name = 'raycast', + field_of_view = 180, + delta_angle = 90, + ray_max_range = 20, + draw_raycast = False + )], + position = (0,0), + orientation = 0 + ) + + sim.add_object(agent) + sim.set_reset_point() + sim.reset() + sim.step() + np.testing.assert_almost_equal(agent.raycast.raycast_distances, [20,20,20], decimal = 2) + np.testing.assert_almost_equal(agent.raycast.raycast_angle_degrees, [90,0,-90], decimal = 2) + +if __name__ == "__main__": +# test_3_rays_with_walls() +# test_3_rays_without_walls() + manual_test() \ No newline at end of file diff --git a/test/components/test_social_mapping.py b/test/components/test_social_mapping.py new file mode 100644 index 0000000000000000000000000000000000000000..4dd17710721345b47fc201ba088e4a8afd1a44c6 --- /dev/null +++ b/test/components/test_social_mapping.py @@ -0,0 +1,284 @@ + +#TODO + +# import mpi_sim +# import numpy.testing as np_test +# from Box2D import b2PolygonShape, b2CircleShape +# import numpy as np + + +# class DummyAgentObject(mpi_sim.Object): + +# @staticmethod +# def default_config(): +# dc = mpi_sim.Object.default_config() +# dc.position = (0, 0) +# dc.orientation = 1 +# dc.properties.append('dynamic') +# return dc + + +# @property +# def position(self): +# return self.box2d_body.position + + +# @property +# def orientation(self): +# return self.box2d_body.angle + + +# def _create_in_box2d_world(self, box2d_world): +# self.box2d_body = box2d_world.CreateStaticBody( +# position=self.config.position, +# angle=self.config.orientation, +# shapes=b2CircleShape(radius=.5), +# ) +# return self.box2d_body + + +# class PolygonObject(mpi_sim.Object): + +# @staticmethod +# def default_config(): +# dc = mpi_sim.Object.default_config() +# dc.position = (0, 0) +# dc.width = 1 +# dc.height = 1 +# dc.properties.append('static') +# return dc + + +# def _create_in_box2d_world(self, box2d_world): +# return box2d_world.CreateStaticBody( +# position=self.config.position, +# shapes=b2PolygonShape(box=(self.config.width, self.config.height)), +# ) + + +# def test_no_rotation(): + +# # create the simulation +# sim = mpi_sim.Simulation(visible_area=((0, 10), (0, 5))) + +# agent = DummyAgentObject( +# position=(0, 0), +# orientation=0, +# components=[dict( +# type=mpi_sim.components.OccupancyGridMapping, +# name='mapping', +# object_filter=dict(properties='static'), +# depth=3., +# resolution=1., +# )] +# ) + +# sim.add_object(agent) +# sim.add_object(PolygonObject(position=(1,1), width=1.5, height=1.5)) + +# # check default values before doing the mapping +# expected_global = np.array([ +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# ]) +# expected_global = np.flip(expected_global, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_global, agent.mapping.global_map) +# assert agent.mapping.global_map_shape == agent.mapping.global_map.shape +# assert agent.mapping.global_map_area == ((0, 10), (0, 5)) + +# expected_local = np.array([ +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# ]) +# expected_local = np.flip(expected_local, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_local, agent.mapping.local_map) +# assert agent.mapping.local_map_shape == agent.mapping.local_map.shape +# assert agent.mapping.local_map_area == ((-3, 3), (-3, 3)) + +# ####################################### + +# # update both maps +# agent.mapping.update_global_map() +# agent.mapping.update_local_map() + +# expected_global = np.array([ +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# ]) +# expected_global = np.flip(expected_global, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_global, agent.mapping.global_map) +# assert agent.mapping.global_map_shape == agent.mapping.global_map.shape +# assert agent.mapping.global_map_area == ((0, 10), (0, 5)) + +# expected_local = np.array([ +# [0., 0., 0., 1., 1., 1.], +# [0., 0., 0., 1., 1., 1.], +# [0., 0., 0., 1., 1., 1.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# ]) +# expected_local = np.flip(expected_local, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_local, agent.mapping.local_map) +# assert agent.mapping.local_map_shape == agent.mapping.local_map.shape +# assert agent.mapping.local_map_area == ((-3, 3), (-3, 3)) + + +# def test_east_rotation(): + +# # create the simulation +# sim = mpi_sim.Simulation(visible_area=((0, 10), (0, 5))) + +# agent = DummyAgentObject( +# position=(0, 0), +# orientation=-np.pi / 2, +# components=[dict( +# type=mpi_sim.components.OccupancyGridMapping, +# name='mapping', +# object_filter=dict(properties='static'), +# depth=3., +# resolution=1., +# )] +# ) + +# sim.add_object(agent) +# sim.add_object(PolygonObject(position=(1,1), width=1.5, height=1.5)) + +# # update both maps +# agent.mapping.update_global_map() +# agent.mapping.update_local_map() + +# expected_global = np.array([ +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# ]) +# expected_global = np.flip(expected_global, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_global, agent.mapping.global_map) +# assert agent.mapping.global_map_shape == agent.mapping.global_map.shape +# assert agent.mapping.global_map_area == ((0, 10), (0, 5)) + +# expected_local = np.array([ +# [0., 1., 1., 1., 0., 0.], +# [0., 1., 1., 1., 0., 0.], +# [0., 1., 1., 1., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# ]) +# expected_local = np.flip(expected_local, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_local, agent.mapping.local_map) +# assert agent.mapping.local_map_shape == agent.mapping.local_map.shape +# assert agent.mapping.local_map_area == ((-3, 3), (-3, 3)) + + +# def test_west_rotation(): + +# # create the simulation +# sim = mpi_sim.Simulation(visible_area=((0, 10), (0, 5))) + +# agent = DummyAgentObject( +# position=(0, 0), +# orientation=np.pi / 2, +# components=[dict( +# type=mpi_sim.components.OccupancyGridMapping, +# name='mapping', +# object_filter=dict(properties='static'), +# depth=3., +# resolution=1., +# )] +# ) + +# sim.add_object(agent) +# sim.add_object(PolygonObject(position=(1,1), width=1.5, height=1.5)) + +# # update both maps +# agent.mapping.update_global_map() +# agent.mapping.update_local_map() + +# expected_global = np.array([ +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# ]) +# expected_global = np.flip(expected_global, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_global, agent.mapping.global_map) +# assert agent.mapping.global_map_shape == agent.mapping.global_map.shape +# assert agent.mapping.global_map_area == ((0, 10), (0, 5)) + +# expected_local = np.array([ +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 1., 1., 1.], +# [0., 0., 0., 1., 1., 1.], +# [0., 0., 0., 1., 1., 1.], +# [0., 0., 0., 0., 0., 0.], +# ]) +# expected_local = np.flip(expected_local, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_local, agent.mapping.local_map) +# assert agent.mapping.local_map_shape == agent.mapping.local_map.shape +# assert agent.mapping.local_map_area == ((-3, 3), (-3, 3)) + + +# def test_south_rotation(): + +# # create the simulation +# sim = mpi_sim.Simulation(visible_area=((0, 10), (0, 5))) + +# agent = DummyAgentObject( +# position=(0, 0), +# orientation=np.pi, +# components=[dict( +# type=mpi_sim.components.OccupancyGridMapping, +# name='mapping', +# object_filter=dict(properties='static'), +# depth=3., +# resolution=1., +# )] +# ) + +# sim.add_object(agent) +# sim.add_object(PolygonObject(position=(1,1), width=1.5, height=1.5)) + +# # update both maps +# agent.mapping.update_global_map() +# agent.mapping.update_local_map() + +# expected_global = np.array([ +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# [1., 1., 1., 0., 0., 0., 0., 0., 0., 0.], +# ]) +# expected_global = np.flip(expected_global, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_global, agent.mapping.global_map) +# assert agent.mapping.global_map_shape == agent.mapping.global_map.shape +# assert agent.mapping.global_map_area == ((0, 10), (0, 5)) + +# expected_local = np.array([ +# [0., 0., 0., 0., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# [0., 1., 1., 1., 0., 0.], +# [0., 1., 1., 1., 0., 0.], +# [0., 1., 1., 1., 0., 0.], +# [0., 0., 0., 0., 0., 0.], +# ]) +# expected_local = np.flip(expected_local, axis=0).transpose() +# np_test.assert_array_almost_equal(expected_local, agent.mapping.local_map) +# assert agent.mapping.local_map_shape == agent.mapping.local_map.shape +# assert agent.mapping.local_map_area == ((-3, 3), (-3, 3))