hand-tracking-teleop 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hand-tracking-teleop might be problematic. Click here for more details.
- hand_tracking/__init__.py +17 -0
- hand_tracking/cli.py +279 -0
- hand_tracking/hand_detector.py +475 -0
- hand_tracking/visualizer_3d.py +214 -0
- hand_tracking/visualizer_combined.py +221 -0
- hand_tracking/visualizer_rerun.py +255 -0
- hand_tracking_teleop-0.1.0.dist-info/METADATA +436 -0
- hand_tracking_teleop-0.1.0.dist-info/RECORD +14 -0
- hand_tracking_teleop-0.1.0.dist-info/WHEEL +5 -0
- hand_tracking_teleop-0.1.0.dist-info/entry_points.txt +6 -0
- hand_tracking_teleop-0.1.0.dist-info/top_level.txt +3 -0
- mapping/__init__.py +12 -0
- mapping/kinematic_mapper.py +405 -0
- robot_interface/dual_interface.py +780 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Kinematic mapper for converting hand landmarks to robot joint angles.
|
|
4
|
+
|
|
5
|
+
This module implements the core mapping algorithms that translate human hand
|
|
6
|
+
poses into robot joint configurations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import yaml
|
|
11
|
+
from typing import Dict, List, Optional, Tuple
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
import math
|
|
14
|
+
|
|
15
|
+
# Import hand landmarks from hand tracking module
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
sys.path.append(str(Path(__file__).parent.parent))
|
|
19
|
+
from hand_tracking.hand_detector import HandLandmarks
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RobotJointState:
|
|
24
|
+
"""Container for robot joint state."""
|
|
25
|
+
joint_names: List[str]
|
|
26
|
+
joint_positions: np.ndarray # Shape: (27,) for
|
|
27
|
+
timestamp: float
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class KinematicMapper:
|
|
31
|
+
"""Maps human hand poses to robot joint configurations."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, config_path: Optional[str] = None, control_mode: str = "full_body"):
|
|
34
|
+
"""
|
|
35
|
+
Initialize kinematic mapper with configuration.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
config_path: Path to configuration file
|
|
39
|
+
control_mode: Control mode - "full_body" or "fingers_only"
|
|
40
|
+
"""
|
|
41
|
+
self.config = self._load_config(config_path)
|
|
42
|
+
self.joint_names = self._get_joint_names()
|
|
43
|
+
self.control_mode = control_mode
|
|
44
|
+
|
|
45
|
+
# Previous joint states for smoothing
|
|
46
|
+
self.previous_joints = np.zeros(27)
|
|
47
|
+
|
|
48
|
+
def _load_config(self, config_path: Optional[str]) -> Dict:
|
|
49
|
+
"""Load configuration from YAML file."""
|
|
50
|
+
if config_path is None:
|
|
51
|
+
# Use default config path
|
|
52
|
+
config_path = Path(__file__).parent.parent.parent / "config" / "robot_mapping_config.yaml"
|
|
53
|
+
|
|
54
|
+
if not Path(config_path).exists():
|
|
55
|
+
# Return default configuration
|
|
56
|
+
return self._get_default_config()
|
|
57
|
+
|
|
58
|
+
with open(config_path, 'r') as f:
|
|
59
|
+
return yaml.safe_load(f)
|
|
60
|
+
|
|
61
|
+
def _get_default_config(self) -> Dict:
|
|
62
|
+
"""Get default configuration if file doesn't exist."""
|
|
63
|
+
return {
|
|
64
|
+
'robot_mapping': {
|
|
65
|
+
'mapping': {
|
|
66
|
+
'workspace': {
|
|
67
|
+
'x_range': [-0.8, 0.8],
|
|
68
|
+
'y_range': [-0.6, 0.6],
|
|
69
|
+
'z_range': [0.5, 1.5]
|
|
70
|
+
},
|
|
71
|
+
'smoothing': {
|
|
72
|
+
'joint_alpha': 0.9
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
'joint_limits': {
|
|
76
|
+
'shoulder_pan': [-3.14, 3.14],
|
|
77
|
+
'shoulder_lift': [-1.57, 1.57],
|
|
78
|
+
'elbow_flex': [-2.61, 0.0],
|
|
79
|
+
'finger_joint': [0.0, 1.57]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def _get_joint_names(self) -> List[str]:
|
|
85
|
+
"""Get ordered list of joint names."""
|
|
86
|
+
return [
|
|
87
|
+
# Left arm (7 DOF)
|
|
88
|
+
"left_shoulder_x", "left_shoulder_z", "left_shoulder_y",
|
|
89
|
+
"left_elbow_y", "left_wrist_z", "left_wrist_x", "left_wrist_y",
|
|
90
|
+
# Right arm (7 DOF)
|
|
91
|
+
"right_shoulder_x", "right_shoulder_z", "right_shoulder_y",
|
|
92
|
+
"right_elbow_y", "right_wrist_z", "right_wrist_x", "right_wrist_y",
|
|
93
|
+
# Torso (1 DOF)
|
|
94
|
+
"Torso_lin_z",
|
|
95
|
+
# Left hand (6 DOF)
|
|
96
|
+
"left_thumb_finger_proximal_pitch", "left_thumb_finger_proximal_yaw",
|
|
97
|
+
"left_index_finger_proximal", "left_middle_finger_proximal",
|
|
98
|
+
"left_ring_finger_proximal", "left_pinky_finger_proximal",
|
|
99
|
+
# Right hand (6 DOF)
|
|
100
|
+
"right_thumb_finger_proximal_pitch", "right_thumb_finger_proximal_yaw",
|
|
101
|
+
"right_index_finger_proximal", "right_middle_finger_proximal",
|
|
102
|
+
"right_ring_finger_proximal", "right_pinky_finger_proximal"
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
def set_control_mode(self, mode: str):
|
|
106
|
+
"""
|
|
107
|
+
Set the control mode.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
mode: Control mode - "full_body" or "fingers_only"
|
|
111
|
+
"""
|
|
112
|
+
valid_modes = ["full_body", "fingers_only"]
|
|
113
|
+
if mode not in valid_modes:
|
|
114
|
+
raise ValueError(f"Invalid control mode: {mode}. Valid modes: {valid_modes}")
|
|
115
|
+
self.control_mode = mode
|
|
116
|
+
print(f"Control mode set to: {mode}")
|
|
117
|
+
|
|
118
|
+
def map_hands_to_joints(self, hands: List[HandLandmarks]) -> RobotJointState:
|
|
119
|
+
"""
|
|
120
|
+
Main mapping function: convert hand landmarks to robot joint angles.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
hands: List of detected hand landmarks
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
RobotJointState with joint positions for all 27 DOF
|
|
127
|
+
"""
|
|
128
|
+
# Initialize joint positions
|
|
129
|
+
joint_positions = np.zeros(27)
|
|
130
|
+
|
|
131
|
+
# Separate left and right hands
|
|
132
|
+
left_hand = None
|
|
133
|
+
right_hand = None
|
|
134
|
+
|
|
135
|
+
for hand in hands:
|
|
136
|
+
if hand.handedness == "Left":
|
|
137
|
+
left_hand = hand
|
|
138
|
+
elif hand.handedness == "Right":
|
|
139
|
+
right_hand = hand
|
|
140
|
+
|
|
141
|
+
# Apply control mode logic
|
|
142
|
+
if self.control_mode == "full_body":
|
|
143
|
+
# Map both arms and hands when detected
|
|
144
|
+
if left_hand is not None:
|
|
145
|
+
left_arm_joints = self._map_hand_to_arm(left_hand, "left")
|
|
146
|
+
left_finger_joints = self._map_hand_to_fingers(left_hand, "left")
|
|
147
|
+
joint_positions[0:7] = left_arm_joints # Left arm
|
|
148
|
+
joint_positions[15:21] = left_finger_joints # Left fingers
|
|
149
|
+
|
|
150
|
+
if right_hand is not None:
|
|
151
|
+
right_arm_joints = self._map_hand_to_arm(right_hand, "right")
|
|
152
|
+
right_finger_joints = self._map_hand_to_fingers(right_hand, "right")
|
|
153
|
+
joint_positions[7:14] = right_arm_joints # Right arm
|
|
154
|
+
joint_positions[21:27] = right_finger_joints # Right fingers
|
|
155
|
+
|
|
156
|
+
elif self.control_mode == "fingers_only":
|
|
157
|
+
# Only map fingers, keep arms at current position
|
|
158
|
+
if left_hand is not None:
|
|
159
|
+
left_finger_joints = self._map_hand_to_fingers(left_hand, "left")
|
|
160
|
+
joint_positions[15:21] = left_finger_joints # Left fingers
|
|
161
|
+
|
|
162
|
+
if right_hand is not None:
|
|
163
|
+
right_finger_joints = self._map_hand_to_fingers(right_hand, "right")
|
|
164
|
+
joint_positions[21:27] = right_finger_joints # Right fingers
|
|
165
|
+
|
|
166
|
+
# Set torso joint (index 14) - keep neutral for now
|
|
167
|
+
joint_positions[14] = 0.0
|
|
168
|
+
|
|
169
|
+
# Apply smoothing
|
|
170
|
+
joint_positions = self._apply_smoothing(joint_positions)
|
|
171
|
+
|
|
172
|
+
# Apply joint limits
|
|
173
|
+
joint_positions = self._apply_joint_limits(joint_positions)
|
|
174
|
+
|
|
175
|
+
return RobotJointState(
|
|
176
|
+
joint_names=self.joint_names,
|
|
177
|
+
joint_positions=joint_positions,
|
|
178
|
+
timestamp=0.0 # Will be set by caller
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _map_hand_to_arm(self, hand: HandLandmarks, side: str) -> np.ndarray:
|
|
182
|
+
"""Map hand position and orientation to arm joint angles."""
|
|
183
|
+
landmarks = hand.landmarks
|
|
184
|
+
|
|
185
|
+
# Get key points
|
|
186
|
+
wrist = landmarks[0] # Wrist
|
|
187
|
+
|
|
188
|
+
# Calculate hand position relative to camera
|
|
189
|
+
hand_pos = self._landmarks_to_world_position(wrist)
|
|
190
|
+
hand_orientation = self._calculate_hand_orientation(landmarks)
|
|
191
|
+
|
|
192
|
+
# Simple mapping to arm joints
|
|
193
|
+
arm_joints = np.zeros(7)
|
|
194
|
+
|
|
195
|
+
# Shoulder pan: based on hand x position
|
|
196
|
+
arm_joints[0] = np.clip(hand_pos[0] * 2.0, -1.5, 1.5) # shoulder_pan
|
|
197
|
+
|
|
198
|
+
# Shoulder lift: based on hand y position
|
|
199
|
+
arm_joints[1] = np.clip(-hand_pos[1] * 1.5, -1.0, 1.0) # shoulder_lift
|
|
200
|
+
|
|
201
|
+
# Upper arm roll: based on hand orientation
|
|
202
|
+
arm_joints[2] = hand_orientation[2] * 0.5 # upper_arm_roll
|
|
203
|
+
|
|
204
|
+
# Elbow flex: based on hand distance (closer = more bent)
|
|
205
|
+
hand_distance = np.linalg.norm(hand_pos)
|
|
206
|
+
elbow_flex = np.clip((1.0 - hand_distance) * 2.0, -2.0, 0.0)
|
|
207
|
+
arm_joints[3] = elbow_flex # elbow_flex
|
|
208
|
+
|
|
209
|
+
# Wrist joints: based on hand orientation
|
|
210
|
+
arm_joints[4] = hand_orientation[0] * 0.8 # forearm_roll
|
|
211
|
+
arm_joints[5] = hand_orientation[1] * 0.6 # wrist_flex
|
|
212
|
+
arm_joints[6] = hand_orientation[2] * 0.4 # wrist_roll
|
|
213
|
+
|
|
214
|
+
return arm_joints
|
|
215
|
+
|
|
216
|
+
def _map_hand_to_fingers(self, hand: HandLandmarks, side: str) -> np.ndarray:
|
|
217
|
+
"""Map hand landmarks to finger joint angles."""
|
|
218
|
+
landmarks = hand.landmarks
|
|
219
|
+
finger_joints = np.zeros(6)
|
|
220
|
+
|
|
221
|
+
# Simple finger mapping based on finger curl
|
|
222
|
+
wrist = landmarks[0] # Wrist
|
|
223
|
+
|
|
224
|
+
# Thumb (2 DOF) - pitch and yaw
|
|
225
|
+
thumb_tip = landmarks[4] # Thumb tip
|
|
226
|
+
thumb_mcp = landmarks[2] # Thumb MCP
|
|
227
|
+
thumb_curl = self._calculate_finger_curl(wrist, thumb_mcp, thumb_tip)
|
|
228
|
+
finger_joints[0] = np.clip(thumb_curl * 3.0, 0.0, 1.5) # thumb_proximal_pitch (increased scale)
|
|
229
|
+
finger_joints[1] = np.clip(thumb_curl * 2.0, 0.0, 1.5) # thumb_proximal_yaw (increased scale)
|
|
230
|
+
|
|
231
|
+
# Index finger (1 DOF)
|
|
232
|
+
index_tip = landmarks[8] # Index tip
|
|
233
|
+
index_mcp = landmarks[5] # Index MCP
|
|
234
|
+
index_curl = self._calculate_finger_curl(wrist, index_mcp, index_tip)
|
|
235
|
+
finger_joints[2] = np.clip(index_curl * 2.5, 0.0, 1.5) # index_proximal (increased scale)
|
|
236
|
+
|
|
237
|
+
# Middle finger (1 DOF)
|
|
238
|
+
middle_tip = landmarks[12] # Middle tip
|
|
239
|
+
middle_mcp = landmarks[9] # Middle MCP
|
|
240
|
+
middle_curl = self._calculate_finger_curl(wrist, middle_mcp, middle_tip)
|
|
241
|
+
finger_joints[3] = np.clip(middle_curl * 2.5, 0.0, 1.5) # middle_proximal (increased scale)
|
|
242
|
+
|
|
243
|
+
# Ring finger (1 DOF)
|
|
244
|
+
ring_tip = landmarks[16] # Ring tip
|
|
245
|
+
ring_mcp = landmarks[13] # Ring MCP
|
|
246
|
+
ring_curl = self._calculate_finger_curl(wrist, ring_mcp, ring_tip)
|
|
247
|
+
finger_joints[4] = np.clip(ring_curl * 2.5, 0.0, 1.5) # ring_proximal (increased scale)
|
|
248
|
+
|
|
249
|
+
# Pinky finger (1 DOF)
|
|
250
|
+
pinky_tip = landmarks[20] # Pinky tip
|
|
251
|
+
pinky_mcp = landmarks[17] # Pinky MCP
|
|
252
|
+
pinky_curl = self._calculate_finger_curl(wrist, pinky_mcp, pinky_tip)
|
|
253
|
+
finger_joints[5] = np.clip(pinky_curl * 2.5, 0.0, 1.5) # pinky_proximal (increased scale)
|
|
254
|
+
|
|
255
|
+
return finger_joints
|
|
256
|
+
|
|
257
|
+
def _landmarks_to_world_position(self, wrist_landmark: np.ndarray) -> np.ndarray:
|
|
258
|
+
"""Convert wrist landmark to world position."""
|
|
259
|
+
workspace = self.config['robot_mapping']['mapping']['workspace']
|
|
260
|
+
|
|
261
|
+
# Map from image coordinates to world coordinates
|
|
262
|
+
x = (wrist_landmark[0] / 640.0 - 0.5) * 2.0 # Normalize to [-1, 1]
|
|
263
|
+
y = (wrist_landmark[1] / 480.0 - 0.5) * 2.0 # Normalize to [-1, 1]
|
|
264
|
+
z = wrist_landmark[2] # Use MediaPipe's depth estimate
|
|
265
|
+
|
|
266
|
+
# Scale to workspace
|
|
267
|
+
world_pos = np.array([
|
|
268
|
+
x * (workspace['x_range'][1] - workspace['x_range'][0]) / 2.0,
|
|
269
|
+
y * (workspace['y_range'][1] - workspace['y_range'][0]) / 2.0,
|
|
270
|
+
z * (workspace['z_range'][1] - workspace['z_range'][0]) / 2.0 + workspace['z_range'][0]
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
return world_pos
|
|
274
|
+
|
|
275
|
+
def _calculate_hand_orientation(self, landmarks: np.ndarray) -> np.ndarray:
|
|
276
|
+
"""Calculate hand orientation from landmarks."""
|
|
277
|
+
# Use wrist, middle MCP, and index MCP to define hand orientation
|
|
278
|
+
wrist = landmarks[0]
|
|
279
|
+
middle_mcp = landmarks[9]
|
|
280
|
+
index_mcp = landmarks[5]
|
|
281
|
+
|
|
282
|
+
# Calculate vectors
|
|
283
|
+
palm_vector = middle_mcp - wrist
|
|
284
|
+
finger_vector = index_mcp - wrist
|
|
285
|
+
|
|
286
|
+
# Calculate orientation angles (simplified)
|
|
287
|
+
roll = np.arctan2(palm_vector[1], palm_vector[0])
|
|
288
|
+
pitch = np.arctan2(palm_vector[2], np.linalg.norm(palm_vector[:2]))
|
|
289
|
+
yaw = np.arctan2(finger_vector[1], finger_vector[0])
|
|
290
|
+
|
|
291
|
+
return np.array([roll, pitch, yaw])
|
|
292
|
+
|
|
293
|
+
def _calculate_finger_curl(self, wrist, mcp, tip):
|
|
294
|
+
"""Calculate finger curl based on distance ratios."""
|
|
295
|
+
# Calculate distances (landmarks are numpy arrays with [x, y, z])
|
|
296
|
+
wrist_to_mcp = np.sqrt((mcp[0] - wrist[0])**2 + (mcp[1] - wrist[1])**2)
|
|
297
|
+
mcp_to_tip = np.sqrt((tip[0] - mcp[0])**2 + (tip[1] - mcp[1])**2)
|
|
298
|
+
wrist_to_tip = np.sqrt((tip[0] - wrist[0])**2 + (tip[1] - wrist[1])**2)
|
|
299
|
+
|
|
300
|
+
# Calculate expected distance if finger was straight
|
|
301
|
+
expected_distance = wrist_to_mcp + mcp_to_tip
|
|
302
|
+
|
|
303
|
+
# Curl ratio: 0 = straight, 1 = fully curled
|
|
304
|
+
if expected_distance > 0:
|
|
305
|
+
curl_ratio = 1.0 - (wrist_to_tip / expected_distance)
|
|
306
|
+
# Apply exponential scaling to make small curls more noticeable
|
|
307
|
+
curl_ratio = curl_ratio ** 0.7 # Makes small values larger
|
|
308
|
+
return max(0.0, min(1.0, curl_ratio))
|
|
309
|
+
return 0.0
|
|
310
|
+
|
|
311
|
+
def _calculate_finger_angles(self, landmarks: np.ndarray, finger_indices: List[int]) -> np.ndarray:
|
|
312
|
+
"""Calculate finger joint angles from landmarks."""
|
|
313
|
+
if len(finger_indices) < 3:
|
|
314
|
+
return np.array([0.0])
|
|
315
|
+
|
|
316
|
+
angles = []
|
|
317
|
+
|
|
318
|
+
# Calculate angles between consecutive finger segments
|
|
319
|
+
for i in range(len(finger_indices) - 2):
|
|
320
|
+
p1 = landmarks[finger_indices[i]]
|
|
321
|
+
p2 = landmarks[finger_indices[i + 1]]
|
|
322
|
+
p3 = landmarks[finger_indices[i + 2]]
|
|
323
|
+
|
|
324
|
+
# Calculate vectors
|
|
325
|
+
v1 = p1 - p2
|
|
326
|
+
v2 = p3 - p2
|
|
327
|
+
|
|
328
|
+
# Calculate angle between vectors
|
|
329
|
+
cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6)
|
|
330
|
+
angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))
|
|
331
|
+
|
|
332
|
+
# Convert to joint angle (0 = straight, positive = bent)
|
|
333
|
+
joint_angle = np.pi - angle
|
|
334
|
+
angles.append(joint_angle)
|
|
335
|
+
|
|
336
|
+
return np.array(angles)
|
|
337
|
+
|
|
338
|
+
def _apply_smoothing(self, joint_positions: np.ndarray) -> np.ndarray:
|
|
339
|
+
"""Apply temporal smoothing to joint positions."""
|
|
340
|
+
alpha = self.config['robot_mapping']['mapping']['smoothing']['joint_alpha']
|
|
341
|
+
|
|
342
|
+
# Exponential moving average
|
|
343
|
+
smoothed = alpha * self.previous_joints + (1 - alpha) * joint_positions
|
|
344
|
+
self.previous_joints = smoothed.copy()
|
|
345
|
+
|
|
346
|
+
return smoothed
|
|
347
|
+
|
|
348
|
+
def _apply_joint_limits(self, joint_positions: np.ndarray) -> np.ndarray:
|
|
349
|
+
"""Apply joint limits to prevent invalid configurations."""
|
|
350
|
+
limits = self.config['robot_mapping']['joint_limits']
|
|
351
|
+
|
|
352
|
+
# Apply limits to different joint types
|
|
353
|
+
for i, joint_name in enumerate(self.joint_names):
|
|
354
|
+
if 'shoulder_pan' in joint_name:
|
|
355
|
+
joint_positions[i] = np.clip(joint_positions[i],
|
|
356
|
+
limits['shoulder_pan'][0], limits['shoulder_pan'][1])
|
|
357
|
+
elif 'shoulder_lift' in joint_name:
|
|
358
|
+
joint_positions[i] = np.clip(joint_positions[i],
|
|
359
|
+
limits['shoulder_lift'][0], limits['shoulder_lift'][1])
|
|
360
|
+
elif 'elbow_flex' in joint_name:
|
|
361
|
+
joint_positions[i] = np.clip(joint_positions[i],
|
|
362
|
+
limits['elbow_flex'][0], limits['elbow_flex'][1])
|
|
363
|
+
elif any(finger in joint_name for finger in ['thumb', 'index', 'middle', 'ring', 'pinky']):
|
|
364
|
+
joint_positions[i] = np.clip(joint_positions[i],
|
|
365
|
+
limits['finger_joint'][0], limits['finger_joint'][1])
|
|
366
|
+
|
|
367
|
+
return joint_positions
|
|
368
|
+
|
|
369
|
+
def get_joint_names(self) -> List[str]:
|
|
370
|
+
"""Get the ordered list of joint names."""
|
|
371
|
+
return self.joint_names.copy()
|
|
372
|
+
|
|
373
|
+
def get_available_modes(self) -> List[str]:
|
|
374
|
+
"""Get list of available control modes."""
|
|
375
|
+
return ["full_body", "fingers_only"]
|
|
376
|
+
|
|
377
|
+
def get_current_mode(self) -> str:
|
|
378
|
+
"""Get current control mode."""
|
|
379
|
+
return self.control_mode
|
|
380
|
+
|
|
381
|
+
def is_ik_compatible(self) -> bool:
|
|
382
|
+
"""Check if current mode is compatible with IK control."""
|
|
383
|
+
return self.control_mode == "full_body"
|
|
384
|
+
|
|
385
|
+
def requires_both_hands(self) -> bool:
|
|
386
|
+
"""Check if current mode requires both hands for IK."""
|
|
387
|
+
return False # Individual planning groups work with any number of hands
|
|
388
|
+
|
|
389
|
+
def print_joint_state(self, joint_state: RobotJointState):
|
|
390
|
+
"""Print joint state in a readable format."""
|
|
391
|
+
print(f"Robot Joint State - Mode: {self.control_mode}")
|
|
392
|
+
print("-" * 50)
|
|
393
|
+
|
|
394
|
+
for i, (name, pos) in enumerate(zip(joint_state.joint_names, joint_state.joint_positions)):
|
|
395
|
+
if abs(pos) > 0.001: # Only print non-zero joints
|
|
396
|
+
print(f"{i:2d}. {name:<25} : {pos:6.3f} rad ({np.degrees(pos):6.1f}°)")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == "__main__":
|
|
400
|
+
# Test the kinematic mapper
|
|
401
|
+
mapper = KinematicMapper()
|
|
402
|
+
print("Kinematic mapper initialized successfully!")
|
|
403
|
+
print(f"Joint names: {len(mapper.get_joint_names())}")
|
|
404
|
+
for i, name in enumerate(mapper.get_joint_names()):
|
|
405
|
+
print(f"{i:2d}. {name}")
|