head_tracker.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. #!/usr/bin/env python
  2. """
  3. head_tracker.py - Version 1.1 2013-12-20
  4. Move the head to track a target published on the /roi topic.
  5. Created for the Pi Robot Project: http://www.pirobot.org
  6. Copyright (c) 2010 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 JointState, RegionOfInterest, CameraInfo
  19. from dynamixel_controllers.srv import *
  20. from std_msgs.msg import Float64
  21. from math import radians
  22. import thread
  23. class HeadTracker():
  24. def __init__(self):
  25. rospy.init_node("head_tracker")
  26. rospy.on_shutdown(self.shutdown)
  27. rate = rospy.get_param("~rate", 20)
  28. r = rospy.Rate(rate)
  29. tick = 1.0 / rate
  30. # Keep the speed updates below about 5 Hz; otherwise the servos
  31. # can behave erratically.
  32. speed_update_rate = rospy.get_param("~speed_update_rate", 10)
  33. speed_update_interval = 1.0 / speed_update_rate
  34. # How big a change do we need in speed before we push an update
  35. # to the servos?
  36. self.speed_update_threshold = rospy.get_param("~speed_update_threshold", 0.01)
  37. # What are the names of the pan and tilt joints in the list of dynamixels?
  38. self.head_pan_joint = rospy.get_param('~head_pan_joint', 'head_pan_joint')
  39. self.head_tilt_joint = rospy.get_param('~head_tilt_joint', 'head_tilt_joint')
  40. self.joints = [self.head_pan_joint, self.head_tilt_joint]
  41. # Joint speeds are given in radians per second
  42. self.default_joint_speed = rospy.get_param('~default_joint_speed', 0.3)
  43. self.max_joint_speed = rospy.get_param('~max_joint_speed', 0.5)
  44. # How far ahead or behind the target (in radians) should we aim for?
  45. self.lead_target_angle = rospy.get_param('~lead_target_angle', 1.0)
  46. # The pan/tilt thresholds indicate what percentage of the image window
  47. # the ROI needs to be off-center before we make a movement
  48. self.pan_threshold = rospy.get_param("~pan_threshold", 0.05)
  49. self.tilt_threshold = rospy.get_param("~tilt_threshold", 0.05)
  50. # The gain_pan and gain_tilt parameter determine how responsive the
  51. # servo movements are. If these are set too high, oscillation can result.
  52. self.gain_pan = rospy.get_param("~gain_pan", 1.0)
  53. self.gain_tilt = rospy.get_param("~gain_tilt", 1.0)
  54. # Set limits on the pan and tilt angles
  55. self.max_pan = rospy.get_param("~max_pan", radians(145))
  56. self.min_pan = rospy.get_param("~min_pan", radians(-145))
  57. self.max_tilt = rospy.get_param("~max_tilt", radians(90))
  58. self.min_tilt = rospy.get_param("~min_tilt", radians(-90))
  59. # How long we are willing to wait (in seconds) for a target before re-centering the servos?
  60. self.recenter_timeout = rospy.get_param('~recenter_timeout', 5)
  61. # Monitor the joint states of the pan and tilt servos
  62. self.joint_state = JointState()
  63. rospy.Subscriber('joint_states', JointState, self.update_joint_state, queue_size=1)
  64. # Wait until we actually have joint state values
  65. while self.joint_state == JointState():
  66. rospy.sleep(1)
  67. # Initialize the servo services and publishers
  68. self.init_servos()
  69. # Center the pan and tilt servos at the start
  70. self.center_head_servos()
  71. # Set a flag to indicate when the target has been lost
  72. self.target_visible = False
  73. # Set a timer to determine how long a target is no longer visible
  74. target_lost_timer = 0.0
  75. # Set a timer to track when we do a speed update
  76. speed_update_timer = 0.0
  77. # Initialize the pan and tilt speeds to zero
  78. pan_speed = tilt_speed = 0.0
  79. # Get a lock for updating the self.move_cmd values
  80. self.lock = thread.allocate_lock()
  81. # Wait for messages on the three topics we need to monitor
  82. rospy.loginfo("Waiting for roi and camera_info topics.")
  83. rospy.wait_for_message('camera_info', CameraInfo)
  84. rospy.wait_for_message('joint_states', JointState)
  85. rospy.wait_for_message('roi', RegionOfInterest)
  86. # Subscribe to camera_info topics and set the callback
  87. self.image_width = self.image_height = 0
  88. rospy.Subscriber('camera_info', CameraInfo, self.get_camera_info, queue_size=1)
  89. # Wait until we actually have the camera data
  90. while self.image_width == 0 or self.image_height == 0:
  91. rospy.sleep(1)
  92. # Subscribe to roi topics and set the callback
  93. self.roi_subscriber = rospy.Subscriber('roi', RegionOfInterest, self.set_joint_cmd, queue_size=1)
  94. rospy.loginfo("Ready to track target.")
  95. while not rospy.is_shutdown():
  96. # Acquire the lock
  97. self.lock.acquire()
  98. try:
  99. # If we have lost the target, stop the servos
  100. if not self.target_visible:
  101. self.pan_speed = 0.0
  102. self.tilt_speed = 0.0
  103. # Keep track of how long the target is lost
  104. target_lost_timer += tick
  105. else:
  106. self.target_visible = False
  107. target_lost_timer = 0.0
  108. # If the target is lost long enough, re-center the servos
  109. if target_lost_timer > self.recenter_timeout:
  110. rospy.loginfo("Cannot find target.")
  111. self.center_head_servos()
  112. target_lost_timer = 0.0
  113. else:
  114. # Update the servo speeds at the appropriate interval
  115. if speed_update_timer > speed_update_interval:
  116. if abs(self.last_pan_speed - self.pan_speed) > self.speed_update_threshold:
  117. self.set_servo_speed(self.head_pan_joint, self.pan_speed)
  118. self.last_pan_speed = self.pan_speed
  119. if abs(self.last_tilt_speed - self.tilt_speed) > self.speed_update_threshold:
  120. self.set_servo_speed(self.head_tilt_joint, self.tilt_speed)
  121. self.last_tilt_speed = self.tilt_speed
  122. speed_update_timer = 0.0
  123. # Update the pan position
  124. if self.last_pan_position != self.pan_position:
  125. self.set_servo_position(self.head_pan_joint, self.pan_position)
  126. self.last_pan_position = self.pan_position
  127. # Update the tilt position
  128. if self.last_tilt_position != self.tilt_position:
  129. self.set_servo_position(self.head_tilt_joint, self.tilt_position)
  130. self.last_tilt_position = self.tilt_position
  131. speed_update_timer += tick
  132. finally:
  133. # Release the lock
  134. self.lock.release()
  135. r.sleep()
  136. def set_joint_cmd(self, msg):
  137. # Acquire the lock
  138. self.lock.acquire()
  139. try:
  140. # If we receive an ROI messages with 0 width or height, the target is not visible
  141. if msg.width == 0 or msg.height == 0:
  142. self.target_visible = False
  143. return
  144. # If the ROI stops updating this next statement will not happen
  145. self.target_visible = True
  146. # Compute the displacement of the ROI from the center of the image
  147. target_offset_x = msg.x_offset + msg.width / 2 - self.image_width / 2
  148. target_offset_y = msg.y_offset + msg.height / 2 - self.image_height / 2
  149. try:
  150. percent_offset_x = float(target_offset_x) / (float(self.image_width) / 2.0)
  151. percent_offset_y = float(target_offset_y) / (float(self.image_height) / 2.0)
  152. except:
  153. percent_offset_x = 0
  154. percent_offset_y = 0
  155. # Set the target position ahead or behind the current position
  156. current_pan = self.joint_state.position[self.joint_state.name.index(self.head_pan_joint)]
  157. # Pan the camera only if the x target offset exceeds the threshold
  158. if abs(percent_offset_x) > self.pan_threshold:
  159. # Set the pan speed proportional to the target offset
  160. self.pan_speed = min(self.max_joint_speed, max(0, self.gain_pan * abs(percent_offset_x)))
  161. if target_offset_x > 0:
  162. self.pan_position = max(self.min_pan, current_pan - self.lead_target_angle)
  163. else:
  164. self.pan_position = min(self.max_pan, current_pan + self.lead_target_angle)
  165. else:
  166. self.pan_speed = 0
  167. self.pan_position = current_pan
  168. # Set the target position ahead or behind the current position
  169. current_tilt = self.joint_state.position[self.joint_state.name.index(self.head_tilt_joint)]
  170. # Tilt the camera only if the y target offset exceeds the threshold
  171. if abs(percent_offset_y) > self.tilt_threshold:
  172. # Set the tilt speed proportional to the target offset
  173. self.tilt_speed = min(self.max_joint_speed, max(0, self.gain_tilt * abs(percent_offset_y)))
  174. if target_offset_y < 0:
  175. self.tilt_position = max(self.min_tilt, current_tilt - self.lead_target_angle)
  176. else:
  177. self.tilt_position = min(self.max_tilt, current_tilt + self.lead_target_angle)
  178. else:
  179. self.tilt_speed = 0
  180. self.tilt_position = current_tilt
  181. finally:
  182. # Release the lock
  183. self.lock.release()
  184. def center_head_servos(self):
  185. rospy.loginfo("Centering servos.")
  186. self.servo_speed[self.head_pan_joint](self.default_joint_speed)
  187. self.servo_speed[self.head_tilt_joint](self.default_joint_speed)
  188. current_tilt = self.joint_state.position[self.joint_state.name.index(self.head_tilt_joint)]
  189. current_pan = self.joint_state.position[self.joint_state.name.index(self.head_pan_joint)]
  190. while abs(current_tilt) > 0.05 or abs(current_pan) > 0.05:
  191. self.servo_position[self.head_pan_joint].publish(0)
  192. self.servo_position[self.head_tilt_joint].publish(0)
  193. rospy.sleep(0.1)
  194. current_tilt = self.joint_state.position[self.joint_state.name.index(self.head_tilt_joint)]
  195. current_pan = self.joint_state.position[self.joint_state.name.index(self.head_pan_joint)]
  196. self.servo_speed[self.head_pan_joint](0.0)
  197. self.servo_speed[self.head_tilt_joint](0.0)
  198. def init_servos(self):
  199. # Create dictionaries to hold the speed, position and torque controllers
  200. self.servo_speed = dict()
  201. self.servo_position = dict()
  202. self.torque_enable = dict()
  203. # Connect to the set_speed services and define a position publisher for each servo
  204. rospy.loginfo("Waiting for joint controllers services...")
  205. for joint in sorted(self.joints):
  206. # The set_speed services
  207. set_speed_service = '/' + joint + '/set_speed'
  208. rospy.wait_for_service(set_speed_service)
  209. self.servo_speed[joint] = rospy.ServiceProxy(set_speed_service, SetSpeed, persistent=True)
  210. # Initialize the servo speed to the default_joint_speed
  211. self.servo_speed[joint](self.default_joint_speed)
  212. # The position controllers
  213. self.servo_position[joint] = rospy.Publisher('/' + joint + '/command', Float64, queue_size=5)
  214. # A service to enable/disable servo torque
  215. torque_enable = '/' + joint + '/torque_enable'
  216. rospy.wait_for_service(torque_enable)
  217. self.torque_enable[joint] = rospy.ServiceProxy(torque_enable, TorqueEnable)
  218. self.torque_enable[joint](False)
  219. self.pan_position = 0
  220. self.tilt_position = 0
  221. self.pan_speed = 0
  222. self.tilt_speed = 0
  223. self.last_pan_position = 0
  224. self.last_tilt_position = 0
  225. self.last_tilt_speed = 0
  226. self.last_pan_speed = 0
  227. def set_servo_speed(self, servo, speed):
  228. # Guard against a speed of exactly zero which means
  229. # "move as fast as you can" to a Dynamixel servo.
  230. if speed == 0:
  231. speed = 0.01
  232. self.servo_speed[servo](speed)
  233. def set_servo_position(self, servo, position):
  234. self.servo_position[servo].publish(position)
  235. def update_joint_state(self, msg):
  236. self.joint_state = msg
  237. def get_camera_info(self, msg):
  238. self.image_width = msg.width
  239. self.image_height = msg.height
  240. def shutdown(self):
  241. rospy.loginfo("Shutting down head tracking node.")
  242. # Turn off updates from the /roi subscriber
  243. try:
  244. self.roi_subscriber.unregister()
  245. except:
  246. pass
  247. # Center the servos
  248. self.center_head_servos()
  249. rospy.sleep(2)
  250. # Relax all servos to give them a rest.
  251. rospy.loginfo("Relaxing pan and tilt servos.")
  252. for servo in self.joints:
  253. self.torque_enable[servo](False)
  254. if __name__ == '__main__':
  255. try:
  256. HeadTracker()
  257. rospy.spin()
  258. except rospy.ROSInterruptException:
  259. rospy.loginfo("Head tracking node terminated.")