Skip to content

Commit 87efc93

Browse files
jws-1fireblonde
andauthored
Rethinking following (#234)
Co-authored-by: fireblonde <nicollehchevska@gmail.com>
1 parent d59478f commit 87efc93

File tree

13 files changed

+1644711
-184
lines changed

13 files changed

+1644711
-184
lines changed

common/navigation/lasr_person_following/config/trained_leg_detector_res_TIAGo.yaml

Lines changed: 1644285 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
<launch>
22

33
<!-- params -->
4-
<param name="forest_file" value="$(find leg_tracker)/config/trained_leg_detector_res=0.33.yaml" />
4+
<param name="forest_file" value="$(find lasr_person_following)/config/trained_leg_detector_res_TIAGo.yaml" />
55
<param name="scan_topic" value="/scan" />
66
<param name="fixed_frame" value="odom"/>
7-
<param name="scan_frequency" value="10"/>
8-
<param name="max_detect_distance" value="5.0"/>
9-
<param name="dist_travelled_together_to_initiate_leg_pair" value="0.5"/>
7+
<param name="scan_frequency" value="15"/>
8+
<param name="max_detect_distance" value="10.0"/>
9+
<param name="dist_travelled_together_to_initiate_leg_pair" value="0.1"/>
1010
<param name="display_detected_people" value="true"/>
1111
<param name="max_leg_pairing_dist" value="0.5"/>
1212
<param name="max_std" value="0.9"/>
13+
<param name="min_points_per_cluster" value="5"/>
1314
<!-- run detect_leg_clusters -->
1415
<node pkg="leg_tracker" type="detect_leg_clusters" name="detect_leg_clusters" output="screen"/>
1516

1617
<!-- run joint_leg_tracker -->
1718
<node pkg="leg_tracker" type="joint_leg_tracker.py" name="joint_leg_tracker" output="screen"/>
1819

1920
<!-- run local_occupancy_grid_mapping -->
20-
<node pkg="leg_tracker" type="local_occupancy_grid_mapping" name="local_occupancy_grid_mapping" output="screen"/>
21-
21+
<node pkg="leg_tracker" type="local_occupancy_grid_mapping" name="local_occupancy_grid_mapping" output="screen"/>
22+
2223
</launch>
2324

common/navigation/lasr_person_following/launch/person_following.launch

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55

66
<node pkg="lasr_person_following" type="person_following.py" name="person_following" output="screen" />
77

8+
<node pkg="tiago_2dnav" type="navigation_camera_mgr.py" name="navigation_camera_mgr" output="screen"/>
89
</launch>
910

common/navigation/lasr_person_following/nodes/person_following.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,32 @@
99
FollowFeedback,
1010
)
1111

12+
from dynamic_reconfigure.srv import Reconfigure
13+
from dynamic_reconfigure.msg import Config, DoubleParameter, BoolParameter, IntParameter
14+
1215
from lasr_person_following import PersonFollower
1316

1417
import warnings
1518

1619

1720
class PersonFollowingServer:
18-
1921
_server: actionlib.SimpleActionServer
2022
_follower: PersonFollower
2123

2224
def __init__(self) -> None:
25+
self._dynamic_costmap = rospy.ServiceProxy(
26+
"/move_base/local_costmap/set_parameters", Reconfigure
27+
)
28+
self._dynamic_velocity = rospy.ServiceProxy(
29+
"/move_base/PalLocalPlanner/set_parameters", Reconfigure
30+
)
31+
self._dynamic_recovery = rospy.ServiceProxy(
32+
"/move_base/set_parameters", Reconfigure
33+
)
34+
35+
self._update_params()
36+
rospy.sleep(1)
37+
print("Updated params")
2338

2439
self._server = actionlib.SimpleActionServer(
2540
"follow_person", FollowAction, execute_cb=self._execute_cb, auto_start=False
@@ -31,6 +46,7 @@ def __init__(self) -> None:
3146
self._server.start()
3247

3348
def _execute_cb(self, _: FollowGoal) -> None:
49+
print("Executing follow_person action")
3450
while not self._follower.begin_tracking(ask=False):
3551
rospy.logwarn("No people found, retrying...")
3652
rospy.sleep(rospy.Duration(1.0))
@@ -45,6 +61,23 @@ def _execute_cb(self, _: FollowGoal) -> None:
4561
def _preempt_cb(self) -> None:
4662
raise NotImplementedError("Preempt not implemented yet")
4763

64+
def _update_params(self):
65+
config = Config()
66+
config.ints.append(IntParameter(name="width", value=4))
67+
config.ints.append(IntParameter(name="height", value=4))
68+
self._dynamic_costmap(config)
69+
70+
config = Config()
71+
config.doubles.append(DoubleParameter(name="max_vel_x", value=0.6))
72+
73+
self._dynamic_velocity(config)
74+
75+
config = Config()
76+
config.bools.append(BoolParameter(name="recovery_behavior_enabled", value=1))
77+
config.bools.append(BoolParameter(name="clearing_rotation_allowed", value=0))
78+
79+
self._dynamic_recovery(config)
80+
4881

4982
if __name__ == "__main__":
5083
rospy.init_node("person_following_server")

common/navigation/lasr_person_following/src/lasr_person_following/person_following.py

Lines changed: 128 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@
1313

1414
from typing import Union, List
1515

16-
import actionlib
1716
from move_base_msgs.msg import MoveBaseAction, MoveBaseActionGoal, MoveBaseGoal
1817
from actionlib_msgs.msg import GoalStatus
18+
from sensor_msgs.msg import PointCloud2
1919

2020
import rosservice
2121
import tf2_ros as tf
2222
import tf2_geometry_msgs # type: ignore
2323
from tf2_geometry_msgs.tf2_geometry_msgs import do_transform_pose
2424
from nav_msgs.srv import GetPlan
25-
from nav_msgs.msg import Path
25+
from lasr_vision_msgs.srv import DetectWave
26+
from play_motion_msgs.msg import PlayMotionAction
27+
from play_motion_msgs.msg import PlayMotionGoal
2628

29+
from lasr_vision_msgs.srv import DetectWaveRequest
2730

2831
from math import atan2
2932
import numpy as np
@@ -38,7 +41,6 @@
3841

3942

4043
class PersonFollower:
41-
4244
# Parameters
4345
_start_following_radius: float
4446
_start_following_angle: float
@@ -71,17 +73,19 @@ def __init__(
7173
self,
7274
start_following_radius: float = 2.0,
7375
start_following_angle: float = 45.0,
74-
n_secs_static_finished: float = 10.0,
75-
n_secs_static_plan_close: float = 5.0,
76-
new_goal_threshold: float = 0.5,
76+
n_secs_static_finished: float = 8.0,
77+
n_secs_static_plan_close: float = 10.0,
78+
new_goal_threshold: float = 2.0,
7779
stopping_distance: float = 1.0,
80+
static_speed: float = 0.0015,
7881
):
7982
self._start_following_radius = start_following_radius
8083
self._start_following_angle = start_following_angle
8184
self._n_secs_static_finished = n_secs_static_finished
8285
self._n_secs_static_plan_close = n_secs_static_plan_close
8386
self._new_goal_threshold = new_goal_threshold
8487
self._stopping_distance = stopping_distance
88+
self._static_speed = static_speed
8589

8690
self._track_id = None
8791

@@ -120,6 +124,16 @@ def __init__(
120124
if not self._transcribe_speech_client_available:
121125
rospy.logwarn("Transcribe speech client not available")
122126

127+
self._detect_wave = rospy.ServiceProxy("/detect_wave", DetectWave)
128+
if not self._detect_wave.wait_for_service(rospy.Duration.from_sec(10.0)):
129+
rospy.logwarn("Detect wave service not available")
130+
131+
self._play_motion = actionlib.SimpleActionClient(
132+
"play_motion", PlayMotionAction
133+
)
134+
if not self._play_motion.wait_for_server(rospy.Duration.from_sec(10.0)):
135+
rospy.logwarn("Play motion client not available")
136+
123137
def _tf_pose(self, pose: PoseStamped, target_frame: str):
124138
trans = self._buffer.lookup_transform(
125139
target_frame, pose.header.frame_id, rospy.Time(0), rospy.Duration(1.0)
@@ -129,7 +143,7 @@ def _tf_pose(self, pose: PoseStamped, target_frame: str):
129143
def _robot_pose_in_odom(self) -> Union[PoseStamped, None]:
130144
try:
131145
current_pose: PoseWithCovarianceStamped = rospy.wait_for_message(
132-
"/amcl_pose", PoseWithCovarianceStamped
146+
"/robot_pose", PoseWithCovarianceStamped
133147
)
134148
except AttributeError:
135149
return None
@@ -203,8 +217,11 @@ def begin_tracking(self, ask: bool = False) -> bool:
203217
rospy.loginfo(f"Tracking person with ID {self._track_id}")
204218
return True
205219

206-
def _recover_track(self) -> bool:
207-
if self._tts_client_available:
220+
def _recover_track(self, say) -> bool:
221+
if not say:
222+
self._tts("I SAW A person waving", wait=True)
223+
224+
if self._tts_client_available and say:
208225
self._tts("I lost track of you, please come back", wait=True)
209226

210227
while not self.begin_tracking(ask=True) and not rospy.is_shutdown():
@@ -213,6 +230,58 @@ def _recover_track(self) -> bool:
213230

214231
return True
215232

233+
# recover with vision, look up and check if person is waving
234+
def _recover_vision(self, prev_pose: PoseStamped) -> bool:
235+
# look up with playmotion and detect wave service
236+
# if detected, begin tracking
237+
238+
# use play motion to look up
239+
self._cancel_goal()
240+
241+
goal = PlayMotionGoal()
242+
goal.motion_name = "look_centre"
243+
self._play_motion.send_goal_and_wait(goal)
244+
245+
self._tts("Can you wave at me so that i can try to find you easily", wait=True)
246+
247+
# detect wave
248+
try:
249+
pcl = rospy.wait_for_message("/xtion/depth_registered/points", PointCloud2)
250+
req = DetectWaveRequest()
251+
req.pcl_msg = pcl
252+
req.dataset = "resnet50"
253+
req.confidence = 0.1
254+
response = self._detect_wave(req)
255+
if response.wave_detected:
256+
rospy.loginfo("Wave detected, beginning tracking")
257+
if np.isnan(response.wave_position.point.x) or np.isnan(
258+
response.wave_position.point.y
259+
):
260+
return False
261+
goal_pose = self._tf_pose(
262+
PoseStamped(
263+
pose=Pose(
264+
position=Point(
265+
x=response.wave_position.point.x,
266+
y=response.wave_position.point.y,
267+
z=response.wave_position.point.z,
268+
),
269+
orientation=Quaternion(0, 0, 0, 1),
270+
),
271+
header=pcl.header,
272+
),
273+
"map",
274+
)
275+
rospy.loginfo(goal_pose.pose.position)
276+
goal_pose.pose.orientation = self._compute_face_quat(
277+
prev_pose.pose, goal_pose.pose
278+
)
279+
self._move_base(goal_pose)
280+
return True
281+
except rospy.ServiceException as e:
282+
rospy.loginfo(f"Error detecting wave: {e}")
283+
return False
284+
216285
def _euclidian_distance(self, p1: Pose, p2: Pose) -> float:
217286
return np.linalg.norm(
218287
np.array([p1.position.x, p1.position.y])
@@ -314,8 +383,10 @@ def follow(self) -> None:
314383
person_trajectory: PoseArray = PoseArray()
315384
person_trajectory.header.frame_id = "odom"
316385
prev_track: Union[None, Person] = None
386+
prev_goal: Union[None, PoseStamped] = None
317387
last_goal_time: Union[None, rospy.Time] = None
318388
going_to_person: bool = False
389+
track_vels: [float] = []
319390

320391
while not rospy.is_shutdown():
321392

@@ -326,15 +397,38 @@ def follow(self) -> None:
326397
filter(lambda track: track.id == self._track_id, tracks.people),
327398
None,
328399
)
400+
# keep a sliding window of the tracks velocity
401+
if track is not None:
402+
track_vels.append((track.vel_x, track.vel_y))
403+
if len(track_vels) > 10:
404+
track_vels.pop(0)
329405

330406
if track is None:
331407
rospy.loginfo("Lost track of person, recovering...")
332-
self._cancel_goal()
333408
person_trajectory = PoseArray()
334-
self._recover_track()
409+
recovery = self._recover_vision(prev_goal)
410+
self._recover_track(say=not recovery)
335411
prev_track = None
336412
continue
337413

414+
if prev_track is None:
415+
robot_pose: PoseStamped = self._robot_pose_in_odom()
416+
417+
goal_pose = self._tf_pose(
418+
PoseStamped(
419+
pose=Pose(
420+
position=track.pose.position,
421+
orientation=robot_pose.pose.orientation,
422+
),
423+
header=tracks.header,
424+
),
425+
"map",
426+
)
427+
self._move_base(goal_pose)
428+
prev_goal = goal_pose
429+
last_goal_time = rospy.Time.now()
430+
prev_track = track
431+
338432
# Distance to the previous pose
339433
dist_to_prev = (
340434
self._euclidian_distance(track.pose, prev_track.pose)
@@ -350,42 +444,28 @@ def follow(self) -> None:
350444
"map",
351445
)
352446
self._move_base(goal_pose)
447+
prev_goal = goal_pose
353448
prev_track = track
354449
last_goal_time = rospy.Time.now()
355-
person_trajectory.poses.append(track.pose)
356-
person_trajectory.header.stamp = rospy.Time.now()
357-
self._person_trajectory_pub.publish(person_trajectory)
358-
going_to_person = False
359-
elif last_goal_time is not None:
360-
delta_t: float = (rospy.Time.now() - last_goal_time).to_sec()
361-
if (
362-
self._n_secs_static_plan_close
363-
<= delta_t
364-
< self._n_secs_static_finished
365-
) and not going_to_person:
366-
self._cancel_goal()
367-
goal_pose = self._get_pose_on_path(
368-
self._tf_pose(self._robot_pose_in_odom(), "map"),
369-
self._tf_pose(
370-
PoseStamped(pose=track.pose, header=tracks.header),
371-
"map",
372-
),
373-
self._stopping_distance,
374-
)
375-
if goal_pose is not None:
376-
self._move_base(goal_pose)
377-
going_to_person = True
378-
else:
379-
rospy.logwarn("Could not find a path to the person")
380-
elif delta_t >= self._n_secs_static_finished:
381-
rospy.loginfo(
382-
"Person has been static for too long, checking if we are finished"
383-
)
384-
self._move_base_client.wait_for_result()
385-
if self._check_finished():
386-
rospy.loginfo("Finished following person")
387-
break
388-
else:
389-
rospy.loginfo("Person is not finished, continuing")
390-
last_goal_time = None
391-
continue
450+
elif (
451+
self._move_base_client.get_state() in [GoalStatus.ABORTED]
452+
and prev_goal is not None
453+
):
454+
rospy.logwarn("Goal was aborted, retrying")
455+
rospy.logwarn((rospy.Time.now() - last_goal_time).to_sec())
456+
rospy.logwarn(track.pose == prev_track.pose)
457+
rospy.logwarn("")
458+
self._move_base(prev_goal)
459+
elif (
460+
np.mean([np.linalg.norm(vel) for vel in track_vels])
461+
< self._static_speed
462+
):
463+
464+
rospy.logwarn("Person has been static for too long, stopping")
465+
self._cancel_goal()
466+
if self._check_finished():
467+
rospy.loginfo("Finished following person")
468+
break
469+
rospy.loginfo("")
470+
rospy.loginfo(np.mean([np.linalg.norm(vel) for vel in track_vels]))
471+
rospy.loginfo("")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<launch>
2+
<description>Start the BodyPix service</description>
3+
<usage doc="BodyPix service"></usage>
4+
<usage doc="Preload models and enable debug topic">debug:=true preload:=['resnet50', 'mobilenet50']</usage>
5+
6+
<arg name="debug" default="true" doc="Whether to publish plotted images to /bodypix/debug/model_name" />
7+
<arg name="preload" default="[]" doc="Array of models to preload when starting the service" />
8+
9+
<node name="bodypix_gesture_service" pkg="lasr_vision_bodypix" type="gesture_service.py" output="screen">
10+
<param name="debug" type="bool" value="$(arg debug)" />
11+
<param name="preload" type="yaml" value="$(arg preload)" />
12+
</node>
13+
</launch>

0 commit comments

Comments
 (0)