diff --git a/src/lerobot/annotations/camera_curation/curator.py b/src/lerobot/annotations/camera_curation/curator.py index 941a54963..4619c899e 100644 --- a/src/lerobot/annotations/camera_curation/curator.py +++ b/src/lerobot/annotations/camera_curation/curator.py @@ -104,6 +104,10 @@ def _parse_verdict(camera_key: str, result: Any, cfg: CameraCurationConfig) -> C raw_label = result.get("view_label") label = str(raw_label).strip().lower().replace(" ", "_") if raw_label else "" view_label = label if is_valid_view_label(label, cfg.view_vocabulary, cfg.allow_combos) else None + if view_label is not None: + # Canonicalize combo order (``wrist_left`` -> ``left_wrist``) so the set + # of possible keys is deterministic regardless of the VLM's word order. + view_label = _order_combo(view_label.split("_"), cfg.view_vocabulary) if raw_label and view_label is None: logger.warning( "camera %s: VLM returned view_label=%r which is not in the vocabulary %s; leaving unlabeled", diff --git a/src/lerobot/annotations/camera_curation/prompts/camera_curation.txt b/src/lerobot/annotations/camera_curation/prompts/camera_curation.txt index f76416d7e..3a3ee0963 100644 --- a/src/lerobot/annotations/camera_curation/prompts/camera_curation.txt +++ b/src/lerobot/annotations/camera_curation/prompts/camera_curation.txt @@ -5,9 +5,15 @@ together to judge the camera, not any single moment. Do two things and return them as one JSON object. 1. QUALITY. Decide whether this camera view is usable for training a policy. - Mark it UNUSABLE if it is blurry / out of focus, badly over- or - under-exposed, mostly occluded, static/frozen, corrupted, or otherwise does - not clearly show the scene. Otherwise it is usable. + Mark it UNUSABLE only for a genuine capture defect: blurry / out of focus, + badly over- or under-exposed, static/frozen, corrupted, or otherwise not + showing the scene clearly. Otherwise it is usable. + + Do NOT mark a view unusable because the robot's own gripper, arm, or + end-effector is visible or partially blocks the view — that is EXPECTED, + especially for a wrist-mounted camera where the gripper is normally in frame. + Only true image-quality problems count; the robot occluding its own + workspace does not. 2. VIEW LABEL. Choose the single best label for where this camera is mounted / what it looks at, using ONLY this closed vocabulary: diff --git a/tests/annotations/test_camera_curation.py b/tests/annotations/test_camera_curation.py index 545d7c86e..45b7fcef8 100644 --- a/tests/annotations/test_camera_curation.py +++ b/tests/annotations/test_camera_curation.py @@ -124,6 +124,15 @@ def test_curate_cameras_parses_and_validates(tmp_path): assert verdicts["observation.images.c"].view_label is None # no frames +def test_curate_cameras_canonicalizes_combo_order(): + cfg = CameraCurationConfig(view_vocabulary=VOCAB) + frames = {"observation.images.a": [_tiny_image()]} + # VLM emits the combo in the "wrong" order; we normalize to _wrist. + vlm = _queued_vlm([{"usable": True, "blur_reason": None, "view_label": "wrist_left"}]) + (verdict,) = curate_cameras(frames, cfg, vlm) + assert verdict.view_label == "left_wrist" + + def test_build_name_mapping_and_collision(): cfg = CameraCurationConfig(view_vocabulary=VOCAB) existing = {"observation.images.cam_0": {}, "observation.images.cam_1": {}, "observation.images.top": {}}