mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-24 02:06:15 +00:00
migration: detect mislabeled SO arms and relabel to 'unknown'
A robot_type of so100/so101 is treated as wrong when the joint dim isn't a multiple of 6, or (when names are present) the first 6 joints don't match the canonical SO set. Such datasets are migrated structurally to v3.0 with joints left untouched and robot_type relabeled 'unknown', instead of being skipped or degrees-converted on a false assumption.
This commit is contained in:
@@ -25,6 +25,9 @@ def is_so_robot_type(rt: str) -> bool:
|
|||||||
return bool(rt) and (rt.startswith(SO_PREFIXES) or rt in SO_EXACT) and rt not in NEVER_FIX
|
return bool(rt) and (rt.startswith(SO_PREFIXES) or rt in SO_EXACT) and rt not in NEVER_FIX
|
||||||
|
|
||||||
|
|
||||||
|
SO_JOINTS = ("shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper")
|
||||||
|
|
||||||
|
|
||||||
def is_end_effector(info: dict) -> bool:
|
def is_end_effector(info: dict) -> bool:
|
||||||
"""True if action/observation.state are task-space end-effector features (e.g. ``ee_x``,
|
"""True if action/observation.state are task-space end-effector features (e.g. ``ee_x``,
|
||||||
``ee_roll``) rather than joint angles. Such datasets are out of scope for the joint fix."""
|
``ee_roll``) rather than joint angles. Such datasets are out of scope for the joint fix."""
|
||||||
@@ -38,6 +41,22 @@ def is_end_effector(info: dict) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_mislabeled_so(info: dict) -> bool:
|
||||||
|
"""True when ``robot_type`` claims SO but the features prove it isn't a standard 6-DOF SO arm:
|
||||||
|
the joint dim isn't a multiple of 6, or (when names are present) the first 6 joints don't match
|
||||||
|
the canonical SO set. Such datasets keep their joints untouched and are relabeled 'unknown'
|
||||||
|
rather than being degrees-converted on a wrong assumption."""
|
||||||
|
feats = info.get("features", {})
|
||||||
|
dims = [feats[c]["shape"][0] for c in ("action", "observation.state") if feats.get(c, {}).get("shape")]
|
||||||
|
if any(d % 6 != 0 for d in dims):
|
||||||
|
return True
|
||||||
|
for key in ("action", "observation.state"):
|
||||||
|
names = [str(n).lower() for n in (feats.get(key, {}).get("names") or [])]
|
||||||
|
if len(names) >= 6 and not all(SO_JOINTS[i] in names[i] for i in range(6)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def load_info(root: Path) -> dict:
|
def load_info(root: Path) -> dict:
|
||||||
return json.loads((Path(root) / "meta" / "info.json").read_text())
|
return json.loads((Path(root) / "meta" / "info.json").read_text())
|
||||||
|
|
||||||
@@ -106,6 +125,10 @@ def classify(root) -> dict:
|
|||||||
return {**out, "is_so": False, "encoding": "end_effector",
|
return {**out, "is_so": False, "encoding": "end_effector",
|
||||||
"note": "task-space end-effector features"}
|
"note": "task-space end-effector features"}
|
||||||
|
|
||||||
|
if is_so_robot_type(rt) and is_mislabeled_so(info):
|
||||||
|
return {**out, "is_so": False, "encoding": "non_so", "mislabeled_so": True,
|
||||||
|
"note": "robot_type claims SO but joint dim/names don't match a 6-DOF SO arm"}
|
||||||
|
|
||||||
is_so = is_so_robot_type(rt)
|
is_so = is_so_robot_type(rt)
|
||||||
if not is_so:
|
if not is_so:
|
||||||
return {**out, "is_so": False, "encoding": "non_so"}
|
return {**out, "is_so": False, "encoding": "non_so"}
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ def _stack(col_values) -> np.ndarray:
|
|||||||
return np.stack([np.asarray(v, dtype=np.float64) for v in col_values]) # (N, D)
|
return np.stack([np.asarray(v, dtype=np.float64) for v in col_values]) # (N, D)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_robot_type(root: Path, robot_type: str) -> None:
|
||||||
|
info_path = root / "meta" / "info.json"
|
||||||
|
info = json.loads(info_path.read_text())
|
||||||
|
info["robot_type"] = robot_type
|
||||||
|
info_path.write_text(json.dumps(info, indent=4))
|
||||||
|
|
||||||
|
|
||||||
def _rewrite_parquet(root: Path, encoding: str) -> None:
|
def _rewrite_parquet(root: Path, encoding: str) -> None:
|
||||||
for pq in sorted((root / "data").glob("*/*.parquet")):
|
for pq in sorted((root / "data").glob("*/*.parquet")):
|
||||||
df = pd.read_parquet(pq)
|
df = pd.read_parquet(pq)
|
||||||
@@ -62,6 +69,14 @@ def fix_dataset_in_place(root) -> dict:
|
|||||||
"""Returns the classification dict augmented with the action taken."""
|
"""Returns the classification dict augmented with the action taken."""
|
||||||
root = Path(root)
|
root = Path(root)
|
||||||
cls = classify(root)
|
cls = classify(root)
|
||||||
|
if cls.get("mislabeled_so"):
|
||||||
|
# robot_type claims SO but the joints prove otherwise (wrong dim or non-SO names).
|
||||||
|
# Relabel to 'unknown' and migrate structurally rather than degrees-converting on a
|
||||||
|
# false assumption; the joint values are left exactly as recorded.
|
||||||
|
_set_robot_type(root, "unknown")
|
||||||
|
return {**cls, "robot_type": "unknown", "converted": False,
|
||||||
|
"action": f"structural v2.1->v3.0 only; robot_type relabeled '{cls.get('robot_type')}'"
|
||||||
|
"->'unknown' (joints don't match a 6-DOF SO arm), joint values left unchanged"}
|
||||||
enc = cls.get("encoding")
|
enc = cls.get("encoding")
|
||||||
if not cls.get("is_so") or enc in ("radians", "unknown", "non_so"):
|
if not cls.get("is_so") or enc in ("radians", "unknown", "non_so"):
|
||||||
reason = {
|
reason = {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from pathlib import Path
|
|||||||
from huggingface_hub import HfApi
|
from huggingface_hub import HfApi
|
||||||
|
|
||||||
import so_arm_frame
|
import so_arm_frame
|
||||||
from classify import classify, is_end_effector, is_so_robot_type, load_info
|
from classify import classify, is_end_effector, load_info
|
||||||
from fix_dataset import fix_dataset_in_place
|
from fix_dataset import fix_dataset_in_place
|
||||||
|
|
||||||
SRC_REPO = "HuggingFaceVLA/community_dataset_v3"
|
SRC_REPO = "HuggingFaceVLA/community_dataset_v3"
|
||||||
@@ -168,14 +168,6 @@ def migrate_one(api, dst_repo, sub, work_dir, no_upload) -> dict:
|
|||||||
if is_end_effector(info):
|
if is_end_effector(info):
|
||||||
return {"root": sub, "robot_type": info.get("robot_type"),
|
return {"root": sub, "robot_type": info.get("robot_type"),
|
||||||
"action": "skipped: end-effector (task-space) dataset, out of scope"}
|
"action": "skipped: end-effector (task-space) dataset, out of scope"}
|
||||||
feats = info.get("features", {})
|
|
||||||
dims = [feats[c]["shape"][0] for c in ("action", "observation.state") if feats.get(c, {}).get("shape")]
|
|
||||||
if is_so_robot_type(info.get("robot_type", "") or "") and any(d % 6 != 0 for d in dims):
|
|
||||||
# We only migrate datasets usable right away by specifying joints: a clean stack of
|
|
||||||
# 6-DOF SO arms. Extra appended columns (bbox, EE pose, ...) push the dim off a
|
|
||||||
# multiple of 6 and mean the degrees mapping doesn't cleanly apply -> out of scope.
|
|
||||||
return {"root": sub, "robot_type": info.get("robot_type"),
|
|
||||||
"action": f"skipped: non-standard SO arm (joint dims {dims} not a multiple of 6), out of scope"}
|
|
||||||
|
|
||||||
result = fix_dataset_in_place(local) # SO-arm value fix (or structural_only)
|
result = fix_dataset_in_place(local) # SO-arm value fix (or structural_only)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user