Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ python scripts/setup/download_assets.py --only mjcf mediapipe examples
somehand replay --recording recordings/pico_right.pkl
```

Use a MANUS ROS 2 glove topic with the safe viewer backend:

```bash
source /opt/ros/humble/setup.bash
source ~/manus_ros2_ws/install/local_setup.bash

somehand manus-ros2 \
--topic /manus_glove_0 \
--hand left \
--backend viewer \
--config configs/retargeting/left/revo2_left.yaml
```

The topic number does not define left or right. The adapter also validates the
`side` field inside every `ManusGlove` message. Real hardware output remains an
explicit `--backend real` choice.

## API Quick Start

See [API Usage](docs/en/api.md) for stable imports, one-step retargeting, and session orchestration.
Expand Down
65 changes: 59 additions & 6 deletions src/somehand/application/bihand_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from somehand.infrastructure.config_loader import load_bihand_config

from .engine import RetargetingEngine
from .manus_calibration import CalibratedManusRetargetingEngine


def _copy_step_result(result: RetargetingStepResult) -> RetargetingStepResult:
Expand All @@ -27,18 +28,70 @@ def _copy_step_result(result: RetargetingStepResult) -> RetargetingStepResult:
class BiHandRetargetingEngine:
"""Stable application-layer entry for one-step bi-hand retargeting."""

def __init__(self, config: BiHandRetargetingConfig, *, input_type: str = "landmarks"):
def __init__(
self,
config: BiHandRetargetingConfig,
*,
input_type: str = "landmarks",
left_engine: RetargetingEngine | None = None,
right_engine: RetargetingEngine | None = None,
):
self.config = config
self.input_type = input_type
self.left_engine = RetargetingEngine.from_config_path(config.left_config_path, input_type=input_type)
self.right_engine = RetargetingEngine.from_config_path(config.right_config_path, input_type=input_type)
self._left_result = self._neutral_result(self.left_engine, hand_side="left")
self._right_result = self._neutral_result(self.right_engine, hand_side="right")
self.left_engine = (
left_engine
if left_engine is not None
else RetargetingEngine.from_config_path(
config.left_config_path, input_type=input_type
)
)
self.right_engine = (
right_engine
if right_engine is not None
else RetargetingEngine.from_config_path(
config.right_config_path, input_type=input_type
)
)
self._left_result = self._neutral_result(
self.left_engine, hand_side="left"
)
self._right_result = self._neutral_result(
self.right_engine, hand_side="right"
)

@classmethod
def from_config_path(cls, config_path: str, *, input_type: str = "landmarks") -> "BiHandRetargetingEngine":
def from_config_path(
cls, config_path: str, *, input_type: str = "landmarks"
) -> "BiHandRetargetingEngine":
return cls(load_bihand_config(config_path), input_type=input_type)

@classmethod
def from_calibrated_manus_paths(
cls,
*,
config_path: str,
left_calibration_path: str,
right_calibration_path: str,
input_type: str = "manus_bihand_ros2",
) -> "BiHandRetargetingEngine":
config = load_bihand_config(config_path)
left_engine = CalibratedManusRetargetingEngine.from_config_path(
config.left_config_path,
calibration_path=left_calibration_path,
input_type=input_type,
)
right_engine = CalibratedManusRetargetingEngine.from_config_path(
config.right_config_path,
calibration_path=right_calibration_path,
input_type=input_type,
)
return cls(
config,
input_type=input_type,
left_engine=left_engine,
right_engine=right_engine,
)

def describe(self) -> dict[str, object]:
return {
"left_model_name": self.left_engine.config.hand.name,
Expand Down
Loading