|
from typing import Union |
|
import numbers |
|
import numpy as np |
|
import scipy.interpolate as si |
|
import scipy.spatial.transform as st |
|
|
|
def rotation_distance(a: st.Rotation, b: st.Rotation) -> float: |
|
return (b * a.inv()).magnitude() |
|
|
|
def pose_distance(start_pose, end_pose): |
|
start_pose = np.array(start_pose) |
|
end_pose = np.array(end_pose) |
|
start_pos = start_pose[:3] |
|
end_pos = end_pose[:3] |
|
start_rot = st.Rotation.from_rotvec(start_pose[3:]) |
|
end_rot = st.Rotation.from_rotvec(end_pose[3:]) |
|
pos_dist = np.linalg.norm(end_pos - start_pos) |
|
rot_dist = rotation_distance(start_rot, end_rot) |
|
return pos_dist, rot_dist |
|
|
|
class PoseTrajectoryInterpolator: |
|
def __init__(self, times: np.ndarray, poses: np.ndarray): |
|
assert len(times) >= 1 |
|
assert len(poses) == len(times) |
|
if not isinstance(times, np.ndarray): |
|
times = np.array(times) |
|
if not isinstance(poses, np.ndarray): |
|
poses = np.array(poses) |
|
|
|
if len(times) == 1: |
|
|
|
self.single_step = True |
|
self._times = times |
|
self._poses = poses |
|
else: |
|
self.single_step = False |
|
assert np.all(times[1:] >= times[:-1]) |
|
|
|
pos = poses[:,:3] |
|
rot = st.Rotation.from_rotvec(poses[:,3:]) |
|
|
|
self.pos_interp = si.interp1d(times, pos, |
|
axis=0, assume_sorted=True) |
|
self.rot_interp = st.Slerp(times, rot) |
|
|
|
@property |
|
def times(self) -> np.ndarray: |
|
if self.single_step: |
|
return self._times |
|
else: |
|
return self.pos_interp.x |
|
|
|
@property |
|
def poses(self) -> np.ndarray: |
|
if self.single_step: |
|
return self._poses |
|
else: |
|
n = len(self.times) |
|
poses = np.zeros((n, 6)) |
|
poses[:,:3] = self.pos_interp.y |
|
poses[:,3:] = self.rot_interp(self.times).as_rotvec() |
|
return poses |
|
|
|
def trim(self, |
|
start_t: float, end_t: float |
|
) -> "PoseTrajectoryInterpolator": |
|
assert start_t <= end_t |
|
times = self.times |
|
should_keep = (start_t < times) & (times < end_t) |
|
keep_times = times[should_keep] |
|
all_times = np.concatenate([[start_t], keep_times, [end_t]]) |
|
|
|
all_times = np.unique(all_times) |
|
|
|
all_poses = self(all_times) |
|
return PoseTrajectoryInterpolator(times=all_times, poses=all_poses) |
|
|
|
def drive_to_waypoint(self, |
|
pose, time, curr_time, |
|
max_pos_speed=np.inf, |
|
max_rot_speed=np.inf |
|
) -> "PoseTrajectoryInterpolator": |
|
assert(max_pos_speed > 0) |
|
assert(max_rot_speed > 0) |
|
time = max(time, curr_time) |
|
|
|
curr_pose = self(curr_time) |
|
pos_dist, rot_dist = pose_distance(curr_pose, pose) |
|
pos_min_duration = pos_dist / max_pos_speed |
|
rot_min_duration = rot_dist / max_rot_speed |
|
duration = time - curr_time |
|
duration = max(duration, max(pos_min_duration, rot_min_duration)) |
|
assert duration >= 0 |
|
last_waypoint_time = curr_time + duration |
|
|
|
|
|
trimmed_interp = self.trim(curr_time, curr_time) |
|
times = np.append(trimmed_interp.times, [last_waypoint_time], axis=0) |
|
poses = np.append(trimmed_interp.poses, [pose], axis=0) |
|
|
|
|
|
final_interp = PoseTrajectoryInterpolator(times, poses) |
|
return final_interp |
|
|
|
def schedule_waypoint(self, |
|
pose, time, |
|
max_pos_speed=np.inf, |
|
max_rot_speed=np.inf, |
|
curr_time=None, |
|
last_waypoint_time=None |
|
) -> "PoseTrajectoryInterpolator": |
|
assert(max_pos_speed > 0) |
|
assert(max_rot_speed > 0) |
|
if last_waypoint_time is not None: |
|
assert curr_time is not None |
|
|
|
|
|
start_time = self.times[0] |
|
end_time = self.times[-1] |
|
assert start_time <= end_time |
|
|
|
if curr_time is not None: |
|
if time <= curr_time: |
|
|
|
|
|
return self |
|
|
|
start_time = max(curr_time, start_time) |
|
|
|
if last_waypoint_time is not None: |
|
|
|
|
|
if time <= last_waypoint_time: |
|
end_time = curr_time |
|
else: |
|
end_time = max(last_waypoint_time, curr_time) |
|
else: |
|
end_time = curr_time |
|
|
|
end_time = min(end_time, time) |
|
start_time = min(start_time, end_time) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
assert start_time <= end_time |
|
assert end_time <= time |
|
if last_waypoint_time is not None: |
|
if time <= last_waypoint_time: |
|
assert end_time == curr_time |
|
else: |
|
assert end_time == max(last_waypoint_time, curr_time) |
|
|
|
if curr_time is not None: |
|
assert curr_time <= start_time |
|
assert curr_time <= time |
|
|
|
trimmed_interp = self.trim(start_time, end_time) |
|
|
|
|
|
|
|
|
|
duration = time - end_time |
|
end_pose = trimmed_interp(end_time) |
|
pos_dist, rot_dist = pose_distance(pose, end_pose) |
|
pos_min_duration = pos_dist / max_pos_speed |
|
rot_min_duration = rot_dist / max_rot_speed |
|
duration = max(duration, max(pos_min_duration, rot_min_duration)) |
|
assert duration >= 0 |
|
last_waypoint_time = end_time + duration |
|
|
|
|
|
times = np.append(trimmed_interp.times, [last_waypoint_time], axis=0) |
|
poses = np.append(trimmed_interp.poses, [pose], axis=0) |
|
|
|
|
|
final_interp = PoseTrajectoryInterpolator(times, poses) |
|
return final_interp |
|
|
|
|
|
def __call__(self, t: Union[numbers.Number, np.ndarray]) -> np.ndarray: |
|
is_single = False |
|
if isinstance(t, numbers.Number): |
|
is_single = True |
|
t = np.array([t]) |
|
|
|
pose = np.zeros((len(t), 6)) |
|
if self.single_step: |
|
pose[:] = self._poses[0] |
|
else: |
|
start_time = self.times[0] |
|
end_time = self.times[-1] |
|
t = np.clip(t, start_time, end_time) |
|
|
|
pose = np.zeros((len(t), 6)) |
|
pose[:,:3] = self.pos_interp(t) |
|
pose[:,3:] = self.rot_interp(t).as_rotvec() |
|
|
|
if is_single: |
|
pose = pose[0] |
|
return pose |
|
|