Skip to content

Commit 21902bf

Browse files
authored
Add geometry-based robot class generator (#642)
* Add geometry-based robot class generator without LLM Add robot_class_generator.py that generates Python robot classes from URDF geometry without requiring LLM or API keys. Key features: - Automatic kinematic chain detection using NetworkX graph analysis - Geometry-based limb type detection (arm, leg, head, torso) - TCP estimation from gripper fingertip midpoint or mesh extent - Tool frame and hand link detection from link structure - Support for single-arm and dual-arm robots * Add generate-robot-class to skr CLI Move robot class generator from examples to skr CLI command. Update README.md and docs/source/cli.rst with usage documentation. * Support ROS package:// path in generate_robot_class Automatically detect if URDF is inside a ROS package and generate default_urdf_path with package:// format instead of absolute path. * Support package:// URLs in URDF.load Resolve package:// URLs using resolve_filepath before loading URDF. This allows generated robot classes with package:// default_urdf_path to work correctly when loaded from within a ROS package directory. * Improve gripper detection with geometric symmetry - Remove 'gripper_link' from tool_frame patterns (was too broad) - Add _find_symmetric_gripper_midpoint() for geometry-based detection - Detect gripper fingers by finding symmetric child links (same parent, opposite x/y coordinates) instead of relying on naming patterns - Support package:// URLs in URDF.load * Use mesh-based fingertip positions for gripper midpoint - Calculate gripper midpoint using _get_fingertip_position() instead of link.worldpos() for more accurate TCP estimation - Skip _calculate_gripper_tcp_offset if offset already set by symmetric gripper detection to avoid overwriting correct values * Use link origins for symmetric gripper midpoint Mesh-based fingertip detection doesn't work well for grippers with opposing fingers (different directions). Use link origin midpoint instead for symmetric gripper pairs. * Fix package:// path double conversion Skip _convert_to_ros_package_path if input is already a package:// URL to avoid corrupting the path. * Add gripper orientation calculation to robot class generator Calculate end_coords orientation for symmetric gripper links so that: - x-axis points in gripper forward direction (from parent to midpoint) - y-axis points in gripper opening direction (between fingers) - z-axis is computed as x cross y The rotation is output as RPY angles in radians in the generated CascadedCoords initialization. * Externalize patterns and improve robot class generator Based on feedback, this commit makes the following improvements: 1. Add PatternConfig class for externalizing all "magic words" - Users can customize patterns for non-standard naming conventions - Patterns can be extended or overridden via constructor - force_groups option to bypass auto-detection for specific groups 2. Use pre-compiled regex for pattern matching - Improves performance by compiling patterns once - All pattern matching now uses PatternConfig.matches() 3. Switch mesh priority for TCP estimation - Visual mesh now preferred over collision mesh - Visual meshes have more accurate vertex positions for fingertips - Collision meshes may be simplified convex hulls 4. Add documentation about geometric detection limitations - Y-coordinate based left/right detection assumes T-pose - Notes added to docstrings about potential failure cases Example usage: config = PatternConfig( patterns={'right_arm': ['RA_', 'right_arm_j']}, force_groups={'head': ['neck_link', 'head_link']} ) generate_robot_class_from_geometry(robot, config=config) * Fix non-deterministic Y-axis direction in gripper orientation The Y-axis direction was dependent on the iteration order of child links, which is non-deterministic. This caused the gripper orientation to be inconsistent between runs. Fix: Ensure Y-axis always points toward the positive direction of the axis with the largest absolute component. This makes the orientation calculation deterministic regardless of iteration order. * Fix gripper orientation when finger origins are at parent origin When finger link origins are located at the parent origin (like Fetch), use fingertip mesh positions to determine the approach direction instead of falling back to an arbitrary axis. This fixes incorrect gripper orientations for robots like Fetch where the finger links' origins are at [0, ±y, 0] relative to gripper_link, resulting in a midpoint at the origin. * Delete generate-robot-class command * Extract magic numbers as named constants for better maintainability - Add module-level constants for geometric detection thresholds - Remove unused center calculation in _estimate_tcp_from_mesh - Replace hardcoded values with descriptive constants: - SYMMETRY_POSITION_TOLERANCE, MIN_POSITION_OFFSET, Z_SIMILARITY_TOLERANCE - MAX_SEARCH_DEPTH, POSITION_EPSILON, FINGERTIP_RATIO, ASYMMETRY_RATIO - Y_THRESHOLD_WEAK, Y_THRESHOLD_STRONG, MIN_ARM_JOINTS
1 parent a3fb53f commit 21902bf

File tree

7 files changed

+1840
-0
lines changed

7 files changed

+1840
-0
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ skr visualize-mesh mesh_file.stl
6767

6868
# Convert wheel collision models
6969
skr convert-wheel-collision robot.urdf --output converted.urdf
70+
71+
# Generate robot class from URDF geometry
72+
skr generate-robot-class robot.urdf --output MyRobot.py
7073
```
7174

7275
### Legacy Commands (still supported)

docs/source/cli.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,29 @@ Convert wheel collision models in URDF files.
109109
110110
skr convert-wheel-collision robot.urdf --output converted.urdf
111111
112+
generate-robot-class
113+
~~~~~~~~~~~~~~~~~~~~
114+
115+
Generate Python robot class from URDF geometry. This tool automatically detects
116+
kinematic chains (arms, legs, head, torso) and generates a Python class with
117+
appropriate properties and end-effector coordinates.
118+
119+
No LLM or API keys required - uses only URDF structure and geometry.
120+
121+
.. code-block:: bash
122+
123+
# Generate robot class and print to stdout
124+
skr generate-robot-class robot.urdf
125+
126+
# Save to file
127+
skr generate-robot-class robot.urdf --output MyRobot.py
128+
129+
# Specify custom class name
130+
skr generate-robot-class robot.urdf --class-name MyCustomRobot --output MyRobot.py
131+
132+
# Show detected groups without generating code
133+
skr generate-robot-class robot.urdf --show-groups
134+
112135
Backward Compatibility
113136
----------------------
114137

@@ -124,6 +147,7 @@ For backward compatibility, all original individual commands are still available
124147
urdf-hash robot.urdf
125148
visualize-mesh mesh_file.stl
126149
convert-wheel-collision robot.urdf --output converted.urdf
150+
generate-robot-class robot.urdf --output MyRobot.py
127151
128152
Getting Help
129153
------------

skrobot/apps/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ def get_available_apps():
2222
'urdf_hash': ('Calculate URDF hash', None),
2323
'convert_wheel_collision': ('Convert wheel collision model', None),
2424
'extract_sub_urdf': ('Extract sub-URDF from a root link', None),
25+
'generate_robot_class': ('Generate robot class from URDF geometry', None),
2526
}
2627

2728
for filename in os.listdir(apps_dir):
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env python
2+
3+
"""Generate robot class from URDF geometry.
4+
5+
This tool generates Python robot class code with kinematic chain properties
6+
(arm, right_arm, left_arm, etc.) from any URDF robot model.
7+
8+
No LLM or API keys required - uses only URDF structure and geometry.
9+
"""
10+
11+
import argparse
12+
13+
14+
def main():
15+
parser = argparse.ArgumentParser(
16+
description='Generate robot class from URDF geometry')
17+
parser.add_argument(
18+
'input_urdfpath',
19+
type=str,
20+
help='Input URDF path')
21+
parser.add_argument(
22+
'--output', '-o',
23+
type=str,
24+
default=None,
25+
help='Output path for generated Python file')
26+
parser.add_argument(
27+
'--class-name', '-c',
28+
type=str,
29+
default=None,
30+
help='Class name for the generated class (default: auto-generated)')
31+
parser.add_argument(
32+
'--show-groups',
33+
action='store_true',
34+
help='Show detected groups without generating code')
35+
args = parser.parse_args()
36+
37+
from skrobot.models.urdf import RobotModelFromURDF
38+
from skrobot.urdf.robot_class_generator import generate_groups_from_geometry
39+
from skrobot.urdf.robot_class_generator import generate_robot_class_from_geometry
40+
41+
robot = RobotModelFromURDF(urdf_file=args.input_urdfpath)
42+
43+
print(f"Robot: {robot.name}")
44+
print(f"Links: {len(robot.link_list)}")
45+
print(f"Joints: {len(robot.joint_list)}")
46+
print()
47+
48+
if args.show_groups:
49+
groups, end_effectors, end_coords_info, robot_name = \
50+
generate_groups_from_geometry(robot)
51+
print("Detected groups:")
52+
for group_name, group_data in groups.items():
53+
links = group_data.get('links', [])
54+
tip = group_data.get('tip_link', 'N/A')
55+
print(f" {group_name}: {len(links)} links, tip={tip}")
56+
print()
57+
print("End coordinates:")
58+
for group_name, ec_info in end_coords_info.items():
59+
parent = ec_info.get('parent_link', 'N/A')
60+
pos = ec_info.get('pos', [0, 0, 0])
61+
print(f" {group_name}: parent={parent}, pos={pos}")
62+
return
63+
64+
code = generate_robot_class_from_geometry(
65+
robot,
66+
output_path=args.output,
67+
class_name=args.class_name,
68+
urdf_path=args.input_urdfpath,
69+
)
70+
71+
if args.output:
72+
print(f"Generated class saved to: {args.output}")
73+
else:
74+
print("Generated code:")
75+
print("-" * 60)
76+
print(code)
77+
78+
79+
if __name__ == '__main__':
80+
main()

skrobot/urdf/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from skrobot.urdf.modularize_urdf import find_root_link
44
from skrobot.urdf.modularize_urdf import transform_urdf_to_macro
55
from skrobot.urdf.primitives_converter import convert_meshes_to_primitives
6+
from skrobot.urdf.robot_class_generator import generate_groups_from_geometry
7+
from skrobot.urdf.robot_class_generator import generate_robot_class_from_geometry
68
from skrobot.urdf.scale_urdf import scale_urdf
79
from skrobot.urdf.transform_urdf import transform_urdf_with_world_link
810
from skrobot.urdf.wheel_collision_converter import convert_wheel_collisions_to_cylinders
@@ -23,4 +25,6 @@
2325
'get_mesh_dimensions',
2426
'extract_sub_urdf',
2527
'scale_urdf',
28+
'generate_groups_from_geometry',
29+
'generate_robot_class_from_geometry',
2630
]

0 commit comments

Comments
 (0)