Architecture Overview¶
High-level design of the pib3 library.
Design Goals¶
- Simple API: One-liner for common tasks
- Modular: Components can be used independently
- Backend-agnostic: Same code works with real robot, simulator, or visualization
- Well-documented: Clear examples and comprehensive docs
System Architecture¶
graph TB
subgraph Input
IMG[Image File]
ARR[NumPy Array]
PIL[PIL Image]
end
subgraph Processing
IMG --> IP[Image Processing]
ARR --> IP
PIL --> IP
IP --> SK[Sketch]
SK --> IK[IK Solver]
IK --> TJ[Trajectory]
end
subgraph Backends
TJ --> WB[Webots<br/>Simulator]
TJ --> RR[Real Robot<br/>via rosbridge]
end
Package Structure¶
pib3/
├── __init__.py # Public API exports
├── config.py # Configuration dataclasses
├── types.py # Core types (Stroke, Sketch)
├── image.py # Image → Sketch conversion
├── trajectory.py # Sketch → Trajectory + IK solver
├── audio.py # Audio playback, recording, TTS
├── backends/
│ ├── base.py # RobotBackend ABC
│ ├── webots.py # Webots simulator
│ └── robot.py # Real robot (rosbridge)
├── tools/
│ └── proto_converter.py # Webots proto → URDF
└── resources/
└── pib_model.urdf # Robot kinematic model
Data Flow¶
Image to Trajectory Pipeline¶
sequenceDiagram
participant User
participant image.py
participant trajectory.py
participant Backend
participant audio.py
User->>image.py: image_to_sketch("drawing.png")
image.py->>image.py: Load & grayscale
image.py->>image.py: Threshold & find contours
image.py->>image.py: Simplify & normalize
image.py-->>User: Sketch
User->>trajectory.py: sketch_to_trajectory(sketch)
trajectory.py->>trajectory.py: Map 2D → 3D points
trajectory.py->>trajectory.py: Solve IK for each point
trajectory.py->>trajectory.py: Interpolate & smooth
trajectory.py-->>User: Trajectory
User->>Backend: run_trajectory(trajectory)
Backend->>Backend: Convert to backend format
Backend->>Backend: Execute waypoints
User->>audio.py: speak("Hello!")
audio.py->>audio.py: TTS synthesis (Piper)
audio.py->>Backend: Play audio data
Backend->>Backend: Stream to speaker
User->>audio.py: record_audio(duration=5.0)
audio.py->>Backend: Capture from microphone
audio.py-->>User: Audio data (numpy int16)
Core Components¶
Sketch & Stroke¶
The fundamental data types for representing drawings:
@dataclass
class Point:
x: float # 0.0 to 1.0 (normalized)
y: float # 0.0 to 1.0 (normalized)
@dataclass
class Stroke:
points: List[Point] # Ordered points in one stroke
@dataclass
class Sketch:
strokes: List[Stroke] # Collection of strokes
All coordinates are normalized to [0, 1] for resolution independence.
Trajectory¶
Joint-space representation of robot motion:
@dataclass
class Trajectory:
joint_names: List[str] # 26 joint names
waypoints: np.ndarray # Shape: (N, 26) in radians
metadata: Dict[str, Any] # Source info, timestamps, etc.
Backend Interface¶
All backends implement the same interface:
from pib3 import Joint
class RobotBackend(ABC):
# Connection
def connect(self) -> None: ...
def disconnect(self) -> None: ...
# Joint control (accepts Joint enum or str)
def set_joint(self, name: Joint | str, position: float, unit: str = "percent") -> bool: ...
def get_joint(self, name: Joint | str, unit: str = "percent") -> Optional[float]: ...
def set_joints(self, positions: Dict[Joint | str, float], unit: str = "percent") -> bool: ...
def get_joints(self, names: List[Joint | str] = None, unit: str = "percent") -> Dict[str, float]: ...
# Trajectory
def run_trajectory(self, trajectory: Trajectory, rate_hz: float = 20.0) -> bool: ...
# Audio
def speak(self, text: str, output: AudioOutput = AudioOutput.ROBOT) -> None: ...
def play_audio(self, audio_data: np.ndarray, output: AudioOutput = AudioOutput.ROBOT) -> None: ...
def record_audio(self, duration: float, input_source: AudioInput = AudioInput.LOCAL) -> np.ndarray: ...
See the Base Backend API for complete documentation.
IK Solver¶
The inverse kinematics solver converts Cartesian end-effector positions to joint angles.
Algorithm¶
- Jacobian Computation: Calculate the geometric Jacobian for the kinematic chain from base to left index finger tip
- Damped Least Squares: Solve
Δq = J^T (JJ^T + λI)^{-1} Δxfor numerical stability - Iterative Refinement: Repeat until position error < tolerance or max iterations
- Joint Limits: Clamp solutions to valid ranges
Kinematic Chain¶
The solver uses the left arm chain for drawing:
base_link
└─ shoulder_vertical_left
└─ shoulder_horizontal_left
└─ upper_arm_left_rotation
└─ elbow_left
└─ lower_arm_left_rotation
└─ wrist_left
└─ index_left_stretch (end effector)
Configuration¶
from pib3 import IKConfig
config = IKConfig(
max_iterations=100,
tolerance=0.001, # 1mm
damping=0.1,
step_size=0.5,
)
Configuration System¶
All configurable parameters use dataclasses:
@dataclass
class TrajectoryConfig:
paper: PaperConfig
ik: IKConfig
@dataclass
class PaperConfig:
center_x: float = 0.15
center_y: float = 0.15
height_z: float = 0.74
size: float = 0.12
@dataclass
class IKConfig:
max_iterations: int = 100
tolerance: float = 0.001
damping: float = 0.1
Error Handling¶
The library uses a fail-soft approach:
- IK failures: Points that fail to solve are interpolated from neighbors
- Connection failures: Clear error messages with troubleshooting hints
- Invalid input: Validated early with descriptive exceptions
# IK progress shows success/failure per point
def on_progress(current, total, success):
status = "✓" if success else "✗"
print(f"{status} Point {current}/{total}")
trajectory = pib3.sketch_to_trajectory(
sketch,
progress_callback=on_progress,
)
# Check overall success rate
print(f"Success rate: {trajectory.metadata['success_rate']:.1%}")