Trajectory Generation¶
Functions and classes for generating robot trajectories.
Overview¶
Trajectory generation converts 2D sketches to 3D robot joint positions using inverse kinematics.
graph LR
A[Sketch] --> B[Map to 3D]
B --> C[Solve IK]
C --> D[Interpolate]
D --> E[Trajectory]
Main Function¶
generate_trajectory¶
generate_trajectory
¶
Convenience function: Convert image directly to robot trajectory.
This is a one-shot function that combines image_to_sketch() and sketch_to_trajectory() for simple use cases.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Union[str, Path, ndarray, Image]
|
Input image (file path, numpy array, or PIL Image). |
required |
output_path
|
Optional[Union[str, Path]]
|
If provided, save trajectory JSON to this path. |
None
|
config
|
Optional[TrajectoryConfig]
|
Full configuration (uses sensible defaults if None). |
None
|
initial_q
|
Optional[Union[ndarray, dict, Trajectory]]
|
Initial joint configuration to start IK solving from. Can be one of: - numpy array of shape (n_joints,) with joint positions in radians - dict mapping joint names to positions in radians - Trajectory object (uses last waypoint) - None (default): use fixed initial pose for drawing This enables sequential trajectory execution where each new trajectory starts from the robot's current position. |
None
|
Returns:
| Type | Description |
|---|---|
Trajectory
|
Trajectory object ready for execution. |
Example
import pib3 trajectory = pib3.generate_trajectory("my_drawing.png") trajectory.to_json("output.json")
With custom config¶
from pib3 import TrajectoryConfig, PaperConfig config = TrajectoryConfig( ... paper=PaperConfig(size=0.20, drawing_scale=0.9), ... ) trajectory = pib3.generate_trajectory("drawing.png", config=config)
Sequential trajectories (robot draws multiple images)¶
traj1 = pib3.generate_trajectory("image1.png") traj2 = pib3.generate_trajectory("image2.png", initial_q=traj1) traj3 = pib3.generate_trajectory("image3.png", initial_q=traj2)
Source code in pib3/__init__.py
Usage¶
import pib3
# Basic usage
trajectory = pib3.generate_trajectory("drawing.png")
trajectory.to_json("output.json")
# With configuration
from pib3 import TrajectoryConfig, PaperConfig
config = TrajectoryConfig(
paper=PaperConfig(size=0.15, drawing_scale=0.9)
)
trajectory = pib3.generate_trajectory("drawing.png", config=config)
# Save directly while generating
trajectory = pib3.generate_trajectory("drawing.png", output_path="output.json")
# Sequential trajectories: start each IK from the previous trajectory's end pose
traj1 = pib3.generate_trajectory("image1.png")
traj2 = pib3.generate_trajectory("image2.png", initial_q=traj1)
sketch_to_trajectory¶
sketch_to_trajectory
¶
Convert a Sketch to a robot Trajectory using inverse kinematics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sketch
|
Sketch
|
Sketch object containing strokes to draw. |
required |
config
|
Optional[TrajectoryConfig]
|
Trajectory configuration. Uses defaults if None. |
None
|
progress_callback
|
Optional[Callable[[int, int, bool], None]]
|
Optional callback(current_point, total_points, success). |
None
|
initial_q
|
Optional[Union[ndarray, Dict[str, float], Trajectory]]
|
Initial joint configuration to start IK solving from. Can be one of: - numpy array of shape (n_joints,) with joint positions in radians - dict mapping joint names to positions in radians - Trajectory object (uses last waypoint) - None (default): use fixed initial pose for drawing This allows sequential trajectory execution where each new trajectory starts from the end position of the previous one. |
None
|
Returns:
| Type | Description |
|---|---|
Trajectory
|
Trajectory object with joint positions for each waypoint. |
Raises:
| Type | Description |
|---|---|
ImportError
|
If roboticstoolbox is not installed. |
RuntimeError
|
If no IK solutions are found. |
Example
from pib3 import image_to_sketch, sketch_to_trajectory sketch = image_to_sketch("drawing.png") trajectory = sketch_to_trajectory(sketch) trajectory.to_json("output.json")
Sequential trajectories¶
traj1 = sketch_to_trajectory(sketch1) traj2 = sketch_to_trajectory(sketch2, initial_q=traj1) # Start from traj1 end
Source code in pib3/trajectory.py
808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 | |
Usage¶
import pib3
# Step-by-step approach
sketch = pib3.image_to_sketch("drawing.png")
trajectory = pib3.sketch_to_trajectory(sketch)
# With progress callback
def on_progress(current, total, success):
print(f"Point {current}/{total}: {'OK' if success else 'FAIL'}")
trajectory = pib3.sketch_to_trajectory(
sketch,
progress_callback=on_progress
)
# With custom config
from pib3 import TrajectoryConfig
config = TrajectoryConfig(...)
trajectory = pib3.sketch_to_trajectory(sketch, config)
Trajectory Class¶
Trajectory
dataclass
¶
Robot joint trajectory for executing a drawing.
Stores joint positions in canonical Webots motor radians.
Attributes:
| Name | Type | Description |
|---|---|---|
joint_names |
List[str]
|
Names of joints in order (36 for PIB). |
waypoints |
ndarray
|
Array of shape (N, num_joints) with joint positions in radians. |
metadata |
dict
|
Additional info (paper position, IK stats, etc.) |
coordinate_frame |
str
|
Frame the waypoints are stored in. Canonical is "webots" (Webots motor radians); backends remap from this frame at playback time (see RobotBackend._trajectory_to_backend_radians). |
__len__
¶
to_json
¶
Save trajectory to JSON file with unit metadata.
Source code in pib3/trajectory.py
from_json
classmethod
¶
Load trajectory from JSON file.
Validates the file shape up front so a slightly malformed file fails
loudly (with a pointer to what's wrong) instead of silently degrading
downstream — e.g. a missing arm metadata field misrouting joints
in sequential-trajectory code.
Source code in pib3/trajectory.py
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
to_webots_format
¶
Convert waypoints to Webots motor positions (identity).
Waypoints are already in absolute radians. Per-joint offsets from the Webots starting position are applied by the backend, not here.
Source code in pib3/trajectory.py
to_robot_format
¶
Convert waypoints to real robot format (centidegrees).
Raw unit conversion only (radians → centidegrees). It does not
apply the per-backend joint-convention remap that finger joints need
(Webots vs real-robot finger sign/range differ). For correct playback
on the real robot use RealRobotBackend.run_trajectory(traj), which
remaps via _trajectory_to_backend_radians before sending.
Source code in pib3/trajectory.py
Creating Trajectories¶
import numpy as np
from pib3 import Trajectory
# From arrays
joint_names = ["joint_0", "joint_1", "joint_2"]
waypoints = np.array([
[0.0, 0.0, 0.0],
[0.1, 0.2, 0.3],
[0.2, 0.4, 0.6],
])
trajectory = Trajectory(
joint_names=joint_names,
waypoints=waypoints,
metadata={"source": "custom"}
)
Saving and Loading¶
from pib3 import Trajectory
# Save to JSON
trajectory.to_json("my_trajectory.json")
# Load from JSON
loaded = Trajectory.from_json("my_trajectory.json")
# Access data
print(f"Waypoints: {len(loaded)}")
print(f"Joints: {loaded.joint_names}")
print(f"Metadata: {loaded.metadata}")
Format Conversion¶
# Get waypoints in Webots format (no offset, canonical format)
webots_waypoints = trajectory.to_webots_format()
# Get waypoints in robot format (centidegrees)
robot_waypoints = trajectory.to_robot_format()
to_robot_format() is a raw unit conversion
It only converts radians → centidegrees; it does not apply the
per-backend finger-convention remap. For correct playback on the real robot
use RealRobotBackend.run_trajectory(traj), which remaps finger joints
before sending.
JSON Format¶
{
"format_version": "1.0",
"unit": "radians",
"coordinate_frame": "webots",
"joint_names": ["turn_head_motor", "tilt_forward_motor", ...],
"waypoints": [
[0.1, 0.2, 0.3, ...],
[0.15, 0.25, 0.35, ...]
],
"metadata": {
"source": "pib3",
"robot_model": "pib",
"success_rate": 0.95,
"created_at": "2024-01-01T12:00:00Z"
}
}
Schema Validation¶
Trajectory.from_json() validates the file and raises a clear error instead of silently accepting malformed data:
| Check | Failure mode |
|---|---|
Top level is a dict |
ValueError |
format_version matches supported |
warning logged |
unit equals the expected value |
ValueError on mismatch |
joint_names is a list[str] |
ValueError |
waypoints is a 2D numeric array with columns matching len(joint_names) |
ValueError |
metadata is a dict |
ValueError |
This keeps downstream execution from failing deep inside IK or motor code with opaque IndexError/KeyError tracebacks.
IK Solver Details¶
The inverse kinematics solver uses:
- Algorithm: Levenberg-Marquardt via roboticstoolbox
ikine_LM, on the expert-calibrated DH model (withbase+tool). Targets are solved in the torso frame, in millimetres; the returned joint degrees map directly to motor commands. - Robustness: Each point is attempted in layers (warm-start → ignore joint limits → random restarts), first success wins. This recovers many poses that a single limited solve would report as failures.
- Convergence: Position error below
tolerance. - Fallback: Linear interpolation for points that still fail.
Solver Parameters¶
| Parameter | Effect |
|---|---|
max_iterations |
Iterations per search attempt (ikine_LM ilimit). |
slimit |
Random-restart attempts per point. Higher = more poses solved, slower on hard targets. |
tolerance |
Position tolerance (m). Smaller = more precise, harder to converge. |
Validating the IK¶
Before driving real motors, sanity-check the IK offline (no hardware) with
verify_ik. It solves a
torso-frame target, runs forward kinematics on the result, and returns the
round-trip position error: