object_follower.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. #!/usr/bin/env python
  2. """
  3. object_follower.py - Version 1.1 2013-12-20
  4. Follow a target published on the /roi topic using depth from the depth image.
  5. Created for the Pi Robot Project: http://www.pirobot.org
  6. Copyright (c) 2013 Patrick Goebel. All rights reserved.
  7. This program is free software; you can redistribute it and/or modify
  8. it under the terms of the GNU General Public License as published by
  9. the Free Software Foundation; either version 2 of the License, or
  10. (at your option) any later version.
  11. This program is distributed in the hope that it will be useful,
  12. but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. GNU General Public License for more details at:
  15. http://www.gnu.org/licenses/gpl.html
  16. """
  17. import rospy
  18. from sensor_msgs.msg import Image, RegionOfInterest, CameraInfo
  19. from geometry_msgs.msg import Twist
  20. from math import copysign, isnan
  21. from cv2 import cv as cv
  22. from cv_bridge import CvBridge, CvBridgeError
  23. import numpy as np
  24. import thread
  25. class ObjectFollower():
  26. def __init__(self):
  27. rospy.init_node("object_follower")
  28. # Set the shutdown function (stop the robot)
  29. rospy.on_shutdown(self.shutdown)
  30. # How often should we update the robot's motion?
  31. self.rate = rospy.get_param("~rate", 10)
  32. r = rospy.Rate(self.rate)
  33. # Scale the ROI by this factor to avoid background distance values around the edges
  34. self.scale_roi = rospy.get_param("~scale_roi", 0.9)
  35. # The max linear speed in meters per second
  36. self.max_linear_speed = rospy.get_param("~max_linear_speed", 0.3)
  37. # The minimum linear speed in meters per second
  38. self.min_linear_speed = rospy.get_param("~min_linear_speed", 0.02)
  39. # The maximum rotation speed in radians per second
  40. self.max_rotation_speed = rospy.get_param("~max_rotation_speed", 2.0)
  41. # The minimum rotation speed in radians per second
  42. self.min_rotation_speed = rospy.get_param("~min_rotation_speed", 0.5)
  43. # The x threshold (% of image width) indicates how far off-center
  44. # the ROI needs to be in the x-direction before we react
  45. self.x_threshold = rospy.get_param("~x_threshold", 0.1)
  46. # How far away from the goal distance (in meters) before the robot reacts
  47. self.z_threshold = rospy.get_param("~z_threshold", 0.05)
  48. # The maximum distance a target can be from the robot for us to track
  49. self.max_z = rospy.get_param("~max_z", 1.6)
  50. # The minimum distance to respond to
  51. self.min_z = rospy.get_param("~min_z", 0.5)
  52. # The goal distance (in meters) to keep between the robot and the person
  53. self.goal_z = rospy.get_param("~goal_z", 0.75)
  54. # How much do we weight the goal distance (z) when making a movement
  55. self.z_scale = rospy.get_param("~z_scale", 0.5)
  56. # How much do we weight (left/right) of the person when making a movement
  57. self.x_scale = rospy.get_param("~x_scale", 2.0)
  58. # Slow down factor when stopping
  59. self.slow_down_factor = rospy.get_param("~slow_down_factor", 0.8)
  60. # Initialize the global ROI
  61. self.roi = RegionOfInterest()
  62. # Publisher to control the robot's movement
  63. self.cmd_vel_pub = rospy.Publisher('cmd_vel', Twist, queue_size=5)
  64. # Intialize the movement command
  65. self.move_cmd = Twist()
  66. # Get a lock for updating the self.move_cmd values
  67. self.lock = thread.allocate_lock()
  68. # We will get the image width and height from the camera_info topic
  69. self.image_width = 0
  70. self.image_height = 0
  71. # We need cv_bridge to convert the ROS depth image to an OpenCV array
  72. self.cv_bridge = CvBridge()
  73. self.depth_array = None
  74. # Set flag to indicate when the ROI stops updating
  75. self.target_visible = False
  76. # Wait for the camera_info topic to become available
  77. rospy.loginfo("Waiting for camera_info topic...")
  78. rospy.wait_for_message('camera_info', CameraInfo)
  79. # Subscribe to the camera_info topic to get the image width and height
  80. rospy.Subscriber('camera_info', CameraInfo, self.get_camera_info, queue_size=1)
  81. # Wait until we actually have the camera data
  82. while self.image_width == 0 or self.image_height == 0:
  83. rospy.sleep(1)
  84. # Wait for the depth image to become available
  85. rospy.loginfo("Waiting for depth_image topic...")
  86. rospy.wait_for_message('depth_image', Image)
  87. # Subscribe to the depth image
  88. self.depth_subscriber = rospy.Subscriber("depth_image", Image, self.convert_depth_image, queue_size=1)
  89. # Subscribe to the ROI topic and set the callback to update the robot's motion
  90. rospy.Subscriber('roi', RegionOfInterest, self.set_cmd_vel, queue_size=1)
  91. # Wait until we have an ROI to follow
  92. rospy.loginfo("Waiting for an ROI to track...")
  93. rospy.wait_for_message('roi', RegionOfInterest)
  94. rospy.loginfo("ROI messages detected. Starting follower...")
  95. # Begin the tracking loop
  96. while not rospy.is_shutdown():
  97. # Acquire a lock while we're setting the robot speeds
  98. self.lock.acquire()
  99. try:
  100. if not self.target_visible:
  101. # If the target is not visible, stop the robot smoothly
  102. self.move_cmd.linear.x *= self.slow_down_factor
  103. self.move_cmd.angular.z *= self.slow_down_factor
  104. else:
  105. # Reset the flag to False by default
  106. self.target_visible = False
  107. # Send the Twist command to the robot
  108. self.cmd_vel_pub.publish(self.move_cmd)
  109. finally:
  110. # Release the lock
  111. self.lock.release()
  112. # Sleep for 1/self.rate seconds
  113. r.sleep()
  114. def set_cmd_vel(self, msg):
  115. # Acquire a lock while we're setting the robot speeds
  116. self.lock.acquire()
  117. try:
  118. # If the ROI has a width or height of 0, we have lost the target
  119. if msg.width == 0 or msg.height == 0:
  120. self.target_visible = False
  121. return
  122. else:
  123. self.target_visible = True
  124. self.roi = msg
  125. # Compute the displacement of the ROI from the center of the image
  126. target_offset_x = msg.x_offset + msg.width / 2 - self.image_width / 2
  127. try:
  128. percent_offset_x = float(target_offset_x) / (float(self.image_width) / 2.0)
  129. except:
  130. percent_offset_x = 0
  131. # Rotate the robot only if the displacement of the target exceeds the threshold
  132. if abs(percent_offset_x) > self.x_threshold:
  133. # Set the rotation speed proportional to the displacement of the target
  134. speed = percent_offset_x * self.x_scale
  135. self.move_cmd.angular.z = -copysign(max(self.min_rotation_speed,
  136. min(self.max_rotation_speed, abs(speed))), speed)
  137. else:
  138. self.move_cmd.angular.z = 0
  139. # Now compute the depth component
  140. # Initialize a few depth variables
  141. n_z = sum_z = mean_z = 0
  142. # Shrink the ROI slightly to avoid the target boundaries
  143. scaled_width = int(self.roi.width * self.scale_roi)
  144. scaled_height = int(self.roi.height * self.scale_roi)
  145. # Get the min/max x and y values from the scaled ROI
  146. min_x = int(self.roi.x_offset + self.roi.width * (1.0 - self.scale_roi) / 2.0)
  147. max_x = min_x + scaled_width
  148. min_y = int(self.roi.y_offset + self.roi.height * (1.0 - self.scale_roi) / 2.0)
  149. max_y = min_y + scaled_height
  150. # Get the average depth value over the ROI
  151. for x in range(min_x, max_x):
  152. for y in range(min_y, max_y):
  153. try:
  154. # Get a depth value in meters
  155. z = self.depth_array[y, x]
  156. # Check for NaN values returned by the camera driver
  157. if isnan(z):
  158. continue
  159. except:
  160. # It seems to work best if we convert exceptions to 0
  161. z = 0
  162. # A hack to convert millimeters to meters for the freenect driver
  163. if z > 100:
  164. z /= 1000.0
  165. # Check for values outside max range
  166. if z > self.max_z:
  167. continue
  168. # Increment the sum and count
  169. sum_z = sum_z + z
  170. n_z += 1
  171. # Stop the robot's forward/backward motion by default
  172. linear_x = 0
  173. # If we have depth values...
  174. if n_z:
  175. mean_z = float(sum_z) / n_z
  176. # Don't let the mean fall below the minimum reliable range
  177. mean_z = max(self.min_z, mean_z)
  178. # Check the mean against the minimum range
  179. if mean_z > self.min_z:
  180. # Check the max range and goal threshold
  181. if mean_z < self.max_z and (abs(mean_z - self.goal_z) > self.z_threshold):
  182. speed = (mean_z - self.goal_z) * self.z_scale
  183. linear_x = copysign(min(self.max_linear_speed, max(self.min_linear_speed, abs(speed))), speed)
  184. if linear_x == 0:
  185. # Stop the robot smoothly
  186. self.move_cmd.linear.x *= self.slow_down_factor
  187. else:
  188. self.move_cmd.linear.x = linear_x
  189. finally:
  190. # Release the lock
  191. self.lock.release()
  192. def convert_depth_image(self, ros_image):
  193. # Use cv_bridge() to convert the ROS image to OpenCV format
  194. try:
  195. # Convert the depth image using the default passthrough encoding
  196. depth_image = self.cv_bridge.imgmsg_to_cv2(ros_image, "passthrough")
  197. except CvBridgeError, e:
  198. print e
  199. # Convert the depth image to a Numpy array
  200. self.depth_array = np.array(depth_image, dtype=np.float32)
  201. def get_camera_info(self, msg):
  202. self.image_width = msg.width
  203. self.image_height = msg.height
  204. def shutdown(self):
  205. rospy.loginfo("Stopping the robot...")
  206. # Unregister the subscriber to stop cmd_vel publishing
  207. self.depth_subscriber.unregister()
  208. rospy.sleep(1)
  209. # Send an emtpy Twist message to stop the robot
  210. self.cmd_vel_pub.publish(Twist())
  211. rospy.sleep(1)
  212. if __name__ == '__main__':
  213. try:
  214. ObjectFollower()
  215. rospy.spin()
  216. except rospy.ROSInterruptException:
  217. rospy.loginfo("Object follower node terminated.")