You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

167 lines
7.7 KiB

  1. # Object Handover Demo
  2. FUNMAP is a hardware-targeted perception, planning, and navigation framework developed by Hello Robot for ROS developers and researchers. Some of the key features provided by FUNMAP include cliff detection, closed-loop navigation, and mapping. In this tutorial, we will explore an object-handover demo using FUNMAP.
  3. ## Motivation
  4. Through this demo, we demonstrate human mouth detection using the stretch_deep_perception package, and demonstrate object delivery with navigation using FUNMAP. The robot is teleoperated to have a person in the view of its camera. The person requesting the object must face the robot. We use OpenVINO to perform facial recognition.
  5. ## Workspace Setup
  6. Ideally, this demo requires the person requesting the object to be facing the robot’s camera. Use keyboard teleop to place the object in the robot’s gripper.
  7. ## How-to-run
  8. After building and sourcing the workspace, home the robot:
  9. ```bash
  10. stretch_robot_home.py
  11. ```
  12. This ensures that the underlying stretch_body package knows the exact joint limits and provides the user with a good starting joint configuration.
  13. After homing, launch the object handover demo:
  14. ```bash
  15. ros2 launch stretch_demos handover_object.launch.py
  16. ```
  17. This command will launch stretch_driver, stretch_funmap, and the handover_object nodes.
  18. In a new terminal, launch keyboard teleoperation:
  19. ```bash
  20. ros2 run stretch_core keyboard_teleop --ros-args -p handover_object_on:=true
  21. ```
  22. You will be presented with a keyboard teleoperation menu in a new terminal window. Use key commands to get the Stretch configured as per the above workspace setup guidelines. Once the robot is ready, press ‘y’ or ‘Y’ to trigger object handover.
  23. ## Code Explained
  24. The object_handover node uses the joint_trajectory_server inside stretch_core to send out target joint positions.
  25. ```python
  26. self.trajectory_client = ActionClient(self, FollowJointTrajectory, '/stretch_controller/follow_joint_trajectory', callback_group=self.callback_group)
  27. server_reached = self.trajectory_client.wait_for_server(timeout_sec=60.0)
  28. if not server_reached:
  29. self.get_logger().error('Unable to connect to joint_trajectory_server. Timeout exceeded.')
  30. sys.exit()
  31. ```
  32. Additionally, the node also subscribes to mouth positions detected by stretch_deep_perception, and Stretch’s joint state topics.
  33. ```python
  34. self.joint_states_subscriber = self.create_subscription(JointState, '/stretch/joint_states', qos_profile=1, callback=self.joint_states_callback, callback_group=self.callback_group)
  35. self.mouth_position_subscriber = self.create_subscription(MarkerArray, '/nearest_mouth/marker_array', qos_profile=1, callback=self.mouth_position_callback, callback_group=self.callback_group)
  36. ```
  37. Whenever the node receives a mouth position message, it computes a handoff XYZ coordinate depending upon the current wrist and mouth positions:
  38. ```python
  39. def mouth_position_callback(self, marker_array):
  40. with self.move_lock:
  41. for marker in marker_array.markers:
  42. if marker.type == self.mouth_marker_type:
  43. mouth_position = marker.pose.position
  44. self.mouth_point = PointStamped()
  45. self.mouth_point.point = mouth_position
  46. header = self.mouth_point.header
  47. header.stamp = marker.header.stamp
  48. header.frame_id = marker.header.frame_id
  49. # header.seq = marker.header.seq
  50. self.logger.info('******* new mouth point received *******')
  51. lookup_time = Time(seconds=0) # return most recent transform
  52. timeout_ros = Duration(seconds=0.1)
  53. old_frame_id = self.mouth_point.header.frame_id
  54. new_frame_id = 'base_link'
  55. stamped_transform = self.tf2_buffer.lookup_transform(new_frame_id, old_frame_id, lookup_time, timeout_ros)
  56. points_in_old_frame_to_new_frame_mat = rn.numpify(stamped_transform.transform)
  57. camera_to_base_mat = points_in_old_frame_to_new_frame_mat
  58. grasp_center_frame_id = 'link_grasp_center'
  59. stamped_transform = self.tf2_buffer.lookup_transform(new_frame_id,
  60. grasp_center_frame_id,
  61. lookup_time,
  62. timeout_ros
  63. )
  64. grasp_center_to_base_mat = rn.numpify(stamped_transform.transform)
  65. mouth_camera_xyz = np.array([0.0, 0.0, 0.0, 1.0])
  66. mouth_camera_xyz[:3] = rn.numpify(self.mouth_point.point)[:3]
  67. mouth_xyz = np.matmul(camera_to_base_mat, mouth_camera_xyz)[:3]
  68. fingers_xyz = grasp_center_to_base_mat[:,3][:3]
  69. handoff_object = True
  70. if handoff_object:
  71. # attempt to handoff the object at a location below
  72. # the mouth with respect to the world frame (i.e.,
  73. # gravity)
  74. target_offset_xyz = np.array([0.0, 0.0, -0.2])
  75. else:
  76. object_height_m = 0.1
  77. target_offset_xyz = np.array([0.0, 0.0, -object_height_m])
  78. target_xyz = mouth_xyz + target_offset_xyz
  79. fingers_error = target_xyz - fingers_xyz
  80. self.logger.info(f'fingers_error = {str(fingers_error)}')
  81. delta_forward_m = fingers_error[0]
  82. delta_extension_m = -fingers_error[1]
  83. delta_lift_m = fingers_error[2]
  84. max_lift_m = 1.0
  85. lift_goal_m = self.lift_position + delta_lift_m
  86. lift_goal_m = min(max_lift_m, lift_goal_m)
  87. self.lift_goal_m = lift_goal_m
  88. self.mobile_base_forward_m = delta_forward_m
  89. max_wrist_extension_m = 0.5
  90. wrist_goal_m = self.wrist_position + delta_extension_m
  91. if handoff_object:
  92. # attempt to handoff the object by keeping distance
  93. # between the object and the mouth distance
  94. wrist_goal_m = wrist_goal_m - 0.25 # 25cm from the mouth
  95. wrist_goal_m = max(0.0, wrist_goal_m)
  96. self.wrist_goal_m = min(max_wrist_extension_m, wrist_goal_m)
  97. self.handover_goal_ready = True
  98. ```
  99. The delta between the wrist XYZ and mouth XYZ is used to calculate the lift position, base forward translation, and wrist extension.
  100. Once the user triggers the handover object service, the node sends out joint goal positions for the base, lift, and the wrist, to deliver the object near the person’s mouth:
  101. ```python
  102. self.logger.info("Starting object handover!")
  103. with self.move_lock:
  104. # First, retract the wrist in preparation for handing out an object.
  105. pose = {'wrist_extension': 0.005}
  106. self.move_to_pose(pose)
  107. if self.handover_goal_ready:
  108. pose = {'joint_lift': self.lift_goal_m}
  109. self.move_to_pose(pose)
  110. tolerance_distance_m = 0.01
  111. at_goal = self.move_base.forward(self.mobile_base_forward_m,
  112. detect_obstacles=False,
  113. tolerance_distance_m=tolerance_distance_m
  114. )
  115. pose = {'wrist_extension': self.wrist_goal_m}
  116. self.move_to_pose(pose)
  117. self.handover_goal_ready = False
  118. ```
  119. ## Results and Expectations
  120. This demo serves as an experimental setup to explore object delivery with Stretch. Please be advised that this code is not expected to work perfectly. Some of the shortcomings of the demo include:
  121. - The node requires the target user's face to be in the camera view while triggering the demo. As it stands, it does not keep any past face detections in its memory.
  122. - Facial landmarks detection might not work well for some faces and is highly variable to the deviation from the faces that the algorithm was originally trained on.
  123. Users are encouraged to try this demo and submit improvements.