feat: 添加逆运动学轨迹重采样
This commit is contained in:
@@ -0,0 +1,90 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EpisodeTrajectory:
|
||||||
|
source_path: Path
|
||||||
|
times_s: np.ndarray
|
||||||
|
target_poses: np.ndarray
|
||||||
|
initial_joints: np.ndarray
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_quaternion(values: np.ndarray) -> np.ndarray:
|
||||||
|
quaternion = np.asarray(values, dtype=float)
|
||||||
|
if quaternion.shape != (4,) or not np.isfinite(quaternion).all():
|
||||||
|
raise ValueError("quaternion must contain 4 finite values")
|
||||||
|
norm = float(np.linalg.norm(quaternion))
|
||||||
|
if norm <= 1e-12:
|
||||||
|
raise ValueError("quaternion norm must be positive")
|
||||||
|
return quaternion / norm
|
||||||
|
|
||||||
|
|
||||||
|
def _slerp_quaternion(
|
||||||
|
start: np.ndarray,
|
||||||
|
end: np.ndarray,
|
||||||
|
fraction: float,
|
||||||
|
) -> np.ndarray:
|
||||||
|
first = _normalized_quaternion(start)
|
||||||
|
second = _normalized_quaternion(end)
|
||||||
|
dot = float(np.dot(first, second))
|
||||||
|
if dot < 0.0:
|
||||||
|
second = -second
|
||||||
|
dot = -dot
|
||||||
|
dot = float(np.clip(dot, -1.0, 1.0))
|
||||||
|
if dot > 1.0 - 1e-8:
|
||||||
|
return _normalized_quaternion(
|
||||||
|
first + float(fraction) * (second - first)
|
||||||
|
)
|
||||||
|
angle = float(np.arccos(dot))
|
||||||
|
sine = float(np.sin(angle))
|
||||||
|
return _normalized_quaternion(
|
||||||
|
np.sin((1.0 - fraction) * angle) / sine * first
|
||||||
|
+ np.sin(fraction * angle) / sine * second
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resample_trajectory(
|
||||||
|
trajectory: EpisodeTrajectory,
|
||||||
|
sample_rate_hz: float,
|
||||||
|
) -> EpisodeTrajectory:
|
||||||
|
if not np.isfinite(sample_rate_hz) or sample_rate_hz <= 0.0:
|
||||||
|
raise ValueError("sample_rate_hz must be finite and positive")
|
||||||
|
source_times = np.asarray(trajectory.times_s, dtype=float)
|
||||||
|
poses = np.asarray(trajectory.target_poses, dtype=float)
|
||||||
|
if source_times.ndim != 1 or poses.shape != (source_times.size, 7):
|
||||||
|
raise ValueError("trajectory must contain N timestamps and N x 7 poses")
|
||||||
|
if source_times.size < 2 or np.any(np.diff(source_times) <= 0.0):
|
||||||
|
raise ValueError("trajectory timestamps must be strictly increasing")
|
||||||
|
|
||||||
|
duration = float(source_times[-1] - source_times[0])
|
||||||
|
count = int(round(duration * sample_rate_hz)) + 1
|
||||||
|
target_times = np.linspace(source_times[0], source_times[-1], count)
|
||||||
|
target_poses = np.empty((count, 7), dtype=float)
|
||||||
|
for axis in range(3):
|
||||||
|
target_poses[:, axis] = np.interp(
|
||||||
|
target_times,
|
||||||
|
source_times,
|
||||||
|
poses[:, axis],
|
||||||
|
)
|
||||||
|
for index, timestamp in enumerate(target_times):
|
||||||
|
right = int(np.searchsorted(source_times, timestamp, side="right"))
|
||||||
|
right = min(max(right, 1), source_times.size - 1)
|
||||||
|
left = right - 1
|
||||||
|
interval = source_times[right] - source_times[left]
|
||||||
|
fraction = float((timestamp - source_times[left]) / interval)
|
||||||
|
target_poses[index, 3:] = _slerp_quaternion(
|
||||||
|
poses[left, 3:], poses[right, 3:], fraction
|
||||||
|
)
|
||||||
|
target_poses[0] = poses[0]
|
||||||
|
target_poses[-1] = poses[-1]
|
||||||
|
return EpisodeTrajectory(
|
||||||
|
source_path=trajectory.source_path,
|
||||||
|
times_s=target_times - target_times[0],
|
||||||
|
target_poses=target_poses,
|
||||||
|
initial_joints=np.asarray(trajectory.initial_joints, dtype=float).copy(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
TEST_DIR = Path(__file__).resolve().parent
|
||||||
|
if str(TEST_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(TEST_DIR))
|
||||||
|
|
||||||
|
import ik_method_comparison as comparison
|
||||||
|
|
||||||
|
|
||||||
|
def test_slerp_uses_shortest_arc_and_returns_unit_quaternion() -> None:
|
||||||
|
start = np.asarray([0.0, 0.0, 0.0, 1.0])
|
||||||
|
end = -np.asarray([0.0, 0.0, math.sin(0.1), math.cos(0.1)])
|
||||||
|
|
||||||
|
actual = comparison._slerp_quaternion(start, end, 0.5)
|
||||||
|
|
||||||
|
assert np.linalg.norm(actual) == pytest.approx(1.0)
|
||||||
|
assert actual == pytest.approx(
|
||||||
|
[0.0, 0.0, math.sin(0.05), math.cos(0.05)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_resample_trajectory_keeps_endpoints_and_uses_requested_rate() -> None:
|
||||||
|
trajectory = comparison.EpisodeTrajectory(
|
||||||
|
source_path=Path("episode.hdf5"),
|
||||||
|
times_s=np.asarray([0.0, 0.5, 1.0]),
|
||||||
|
target_poses=np.asarray(
|
||||||
|
[
|
||||||
|
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
[0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
]
|
||||||
|
),
|
||||||
|
initial_joints=np.zeros(7),
|
||||||
|
)
|
||||||
|
|
||||||
|
actual = comparison.resample_trajectory(trajectory, 4.0)
|
||||||
|
|
||||||
|
assert actual.times_s == pytest.approx([0.0, 0.25, 0.5, 0.75, 1.0])
|
||||||
|
assert actual.target_poses[0] == pytest.approx(trajectory.target_poses[0])
|
||||||
|
assert actual.target_poses[-1] == pytest.approx(trajectory.target_poses[-1])
|
||||||
|
assert actual.target_poses[:, 0] == pytest.approx(actual.times_s)
|
||||||
Reference in New Issue
Block a user