mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-23 09:46:00 +00:00
migration: scope subfolder download, keep normalized joints as-is
- download_subfolder: fetch only the target sub-dataset subtree instead of enumerating the whole community_dataset_v3 monorepo tree (fixes apparent hang) - normalized SO gripper (RANGE_0_100) left in native 0..100 frame, matching degrees_new datasets, instead of remapping to +/-45deg - uncalibrated normalized datasets: skip identity value rewrite, keep normalized units and flag them APPROXIMATE on the dataset card - remove --allow-uncalibrated flag and its CANON_IS_CALIBRATED side effect
This commit is contained in:
@@ -7,8 +7,8 @@ from pathlib import Path
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
import so_arm_frame
|
||||||
from classify import classify
|
from classify import classify
|
||||||
from so_arm_frame import to_degrees
|
|
||||||
|
|
||||||
VALUE_COLS = ("observation.state", "action")
|
VALUE_COLS = ("observation.state", "action")
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ def _rewrite_parquet(root: Path, encoding: str) -> None:
|
|||||||
changed = False
|
changed = False
|
||||||
for col in VALUE_COLS:
|
for col in VALUE_COLS:
|
||||||
if col in df.columns:
|
if col in df.columns:
|
||||||
conv = to_degrees(_stack(df[col].values), encoding, n_joints_per_arm=6)
|
conv = so_arm_frame.to_degrees(_stack(df[col].values), encoding, n_joints_per_arm=6)
|
||||||
df[col] = list(conv.astype(np.float32))
|
df[col] = list(conv.astype(np.float32))
|
||||||
changed = True
|
changed = True
|
||||||
if changed:
|
if changed:
|
||||||
@@ -71,6 +71,13 @@ def fix_dataset_in_place(root) -> dict:
|
|||||||
}.get(enc, "no joint conversion applicable")
|
}.get(enc, "no joint conversion applicable")
|
||||||
return {**cls, "converted": False,
|
return {**cls, "converted": False,
|
||||||
"action": f"structural v2.1->v3.0 only ({reason}); joint values left unchanged"}
|
"action": f"structural v2.1->v3.0 only ({reason}); joint values left unchanged"}
|
||||||
|
if enc == "normalized" and not so_arm_frame.CANON_IS_CALIBRATED:
|
||||||
|
# Without per-robot calibration the un-normalization is an identity (placeholder
|
||||||
|
# spans == 100), so rewriting is pointless. Keep the normalized values as-is and let
|
||||||
|
# the dataset card flag them APPROXIMATE instead.
|
||||||
|
return {**cls, "converted": False,
|
||||||
|
"action": "structural v2.1->v3.0 only; joint values kept in normalized units "
|
||||||
|
"(-100..100 / 0..100), NOT converted to degrees (uncalibrated -> APPROXIMATE)"}
|
||||||
# drop stray files that would otherwise be uploaded
|
# drop stray files that would otherwise be uploaded
|
||||||
for junk in (root / "meta").glob("info.json.bak"):
|
for junk in (root / "meta").glob("info.json.bak"):
|
||||||
junk.unlink()
|
junk.unlink()
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ result under the same path into a NEW repo, then delete the local copy. Resumabl
|
|||||||
uv run python run_migration.py --dst-repo HuggingFaceVLA/community_dataset_v3_degrees \
|
uv run python run_migration.py --dst-repo HuggingFaceVLA/community_dataset_v3_degrees \
|
||||||
--work-dir /big/disk/cdv3_work --manifest manifest.csv
|
--work-dir /big/disk/cdv3_work --manifest manifest.csv
|
||||||
Flags: --only-classify (just write manifest), --no-push (fix+convert locally, keep output,
|
Flags: --only-classify (just write manifest), --no-push (fix+convert locally, keep output,
|
||||||
no upload), --folder-name A [B ...] (target specific dataset folders), --limit N,
|
no upload), --folder-name A [B ...] (target specific dataset folders), --limit N.
|
||||||
--allow-uncalibrated (accept placeholder CANON ranges).
|
Uncalibrated `normalized` datasets keep their normalized joint units (flagged APPROXIMATE on
|
||||||
|
the card); paste fitted CANON ranges in so_arm_frame.py to convert them to degrees instead.
|
||||||
"""
|
"""
|
||||||
import argparse, csv, json, shutil, sys, traceback
|
import argparse, csv, json, shutil, sys, traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from huggingface_hub import HfApi, snapshot_download
|
from huggingface_hub import HfApi
|
||||||
|
|
||||||
import so_arm_frame
|
import so_arm_frame
|
||||||
from classify import classify, load_info
|
from classify import classify, load_info
|
||||||
@@ -21,6 +22,28 @@ from fix_dataset import fix_dataset_in_place
|
|||||||
SRC_REPO = "HuggingFaceVLA/community_dataset_v3"
|
SRC_REPO = "HuggingFaceVLA/community_dataset_v3"
|
||||||
|
|
||||||
|
|
||||||
|
def download_subfolder(sub: str, work_dir: str, patterns: list[str] | None = None) -> None:
|
||||||
|
"""Download only ``SRC_REPO/{sub}/...`` into ``work_dir``.
|
||||||
|
|
||||||
|
``snapshot_download`` walks the entire repo tree (``list_repo_tree(recursive=True)``
|
||||||
|
with no path scope) before applying ``allow_patterns``. On this 791-dataset monorepo
|
||||||
|
that whole-repo enumeration is pathologically slow and looks like a hang. Listing the
|
||||||
|
scoped ``path_in_repo=sub`` subtree and fetching its files directly avoids it.
|
||||||
|
"""
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
|
||||||
|
from huggingface_hub import hf_hub_download
|
||||||
|
from huggingface_hub.hf_api import RepoFile
|
||||||
|
|
||||||
|
api = HfApi()
|
||||||
|
for entry in api.list_repo_tree(SRC_REPO, path_in_repo=sub, repo_type="dataset", recursive=True):
|
||||||
|
if not isinstance(entry, RepoFile):
|
||||||
|
continue
|
||||||
|
if patterns and not any(fnmatch(entry.path, pat) for pat in patterns):
|
||||||
|
continue
|
||||||
|
hf_hub_download(SRC_REPO, filename=entry.path, repo_type="dataset", local_dir=work_dir)
|
||||||
|
|
||||||
|
|
||||||
def list_datasets(api: HfApi, repo: str) -> list[str]:
|
def list_datasets(api: HfApi, repo: str) -> list[str]:
|
||||||
files = api.list_repo_files(repo, repo_type="dataset")
|
files = api.list_repo_files(repo, repo_type="dataset")
|
||||||
roots = {p[: -len("/meta/info.json")] for p in files if p.endswith("/meta/info.json")}
|
roots = {p[: -len("/meta/info.json")] for p in files if p.endswith("/meta/info.json")}
|
||||||
@@ -69,7 +92,9 @@ def _write_dataset_card(local: Path, sub: str, result: dict) -> None:
|
|||||||
joint_actions = {
|
joint_actions = {
|
||||||
"degrees_old": "per-joint offsets and axis directions corrected to the post-#777 frame (values stay in degrees)",
|
"degrees_old": "per-joint offsets and axis directions corrected to the post-#777 frame (values stay in degrees)",
|
||||||
"degrees_new": "already in the post-#777 degrees frame; values unchanged",
|
"degrees_new": "already in the post-#777 degrees frame; values unchanged",
|
||||||
"normalized": "un-normalized to physical degrees using canonical joint ranges",
|
"normalized": ("un-normalized to physical degrees using calibrated joint ranges"
|
||||||
|
if converted_degrees else
|
||||||
|
"left in normalized units (-100..100 joints, 0..100 gripper); NOT converted to degrees"),
|
||||||
"radians": "left unchanged (already in radians)",
|
"radians": "left unchanged (already in radians)",
|
||||||
"unknown": "left unchanged (encoding could not be determined)",
|
"unknown": "left unchanged (encoding could not be determined)",
|
||||||
}
|
}
|
||||||
@@ -94,8 +119,9 @@ def _write_dataset_card(local: Path, sub: str, result: dict) -> None:
|
|||||||
else:
|
else:
|
||||||
lines += ["- Joint values: not applicable (not an SO-100/101 dataset)"]
|
lines += ["- Joint values: not applicable (not an SO-100/101 dataset)"]
|
||||||
if approx:
|
if approx:
|
||||||
lines += ["", "> **Note:** normalized (-100..100 / 0..100) joint values were un-normalized "
|
lines += ["", "> **Note:** per-robot calibration was unavailable, so joint state/action were "
|
||||||
"using *placeholder* canonical joint ranges (uncalibrated). These values are APPROXIMATE."]
|
"left in their original *normalized* units (-100..100 joints, 0..100 gripper) rather "
|
||||||
|
"than converted to physical degrees. Treat these joint values as APPROXIMATE."]
|
||||||
if result.get("ambiguous"):
|
if result.get("ambiguous"):
|
||||||
lines += ["", "> **Note:** joint-encoding detection was flagged ambiguous; conversion used the "
|
lines += ["", "> **Note:** joint-encoding detection was flagged ambiguous; conversion used the "
|
||||||
"best-guess encoding above and may warrant manual review."]
|
"best-guess encoding above and may warrant manual review."]
|
||||||
@@ -134,8 +160,7 @@ def migrate_one(api, dst_repo, sub, work_dir, no_upload) -> dict:
|
|||||||
local = Path(work_dir) / sub
|
local = Path(work_dir) / sub
|
||||||
if local.parent.exists():
|
if local.parent.exists():
|
||||||
shutil.rmtree(local.parent, ignore_errors=True) # clean any partial
|
shutil.rmtree(local.parent, ignore_errors=True) # clean any partial
|
||||||
snapshot_download(SRC_REPO, repo_type="dataset", revision="main",
|
download_subfolder(sub, work_dir)
|
||||||
allow_patterns=[f"{sub}/*"], local_dir=work_dir)
|
|
||||||
|
|
||||||
info = load_info(local)
|
info = load_info(local)
|
||||||
if info.get("codebase_version") != "v2.1":
|
if info.get("codebase_version") != "v2.1":
|
||||||
@@ -191,17 +216,11 @@ def main():
|
|||||||
ap.add_argument("--no-push", action="store_true",
|
ap.add_argument("--no-push", action="store_true",
|
||||||
help="Fix + convert locally but do NOT upload; the converted v3.0 output is "
|
help="Fix + convert locally but do NOT upload; the converted v3.0 output is "
|
||||||
"kept under --work-dir for inspection instead of being deleted.")
|
"kept under --work-dir for inspection instead of being deleted.")
|
||||||
ap.add_argument("--allow-uncalibrated", action="store_true",
|
|
||||||
help="Permit converting 'normalized' (-100..100) datasets using the PLACEHOLDER "
|
|
||||||
"canonical joint ranges in so_arm_frame.py. Omit this to force running "
|
|
||||||
"calibrate_canonical_ranges.py first (recommended for a real run).")
|
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
no_upload = args.no_push
|
no_upload = args.no_push
|
||||||
if not no_upload and not args.only_classify and not args.dst_repo:
|
if not no_upload and not args.only_classify and not args.dst_repo:
|
||||||
ap.error("--dst-repo is required unless --no-push or --only-classify is set.")
|
ap.error("--dst-repo is required unless --no-push or --only-classify is set.")
|
||||||
|
|
||||||
if args.allow_uncalibrated:
|
|
||||||
so_arm_frame.CANON_IS_CALIBRATED = True
|
|
||||||
api = HfApi()
|
api = HfApi()
|
||||||
if args.folder_name:
|
if args.folder_name:
|
||||||
subs = resolve_folders(api, SRC_REPO, args.folder_name)
|
subs = resolve_folders(api, SRC_REPO, args.folder_name)
|
||||||
@@ -224,8 +243,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
if args.only_classify:
|
if args.only_classify:
|
||||||
# classify without full download: fetch just the meta/ of this sub
|
# classify without full download: fetch just the meta/ of this sub
|
||||||
snapshot_download(SRC_REPO, repo_type="dataset",
|
download_subfolder(sub, args.work_dir, patterns=[f"{sub}/meta/*"])
|
||||||
allow_patterns=[f"{sub}/meta/*"], local_dir=args.work_dir)
|
|
||||||
row = {"root": sub, **classify(Path(args.work_dir) / sub)}
|
row = {"root": sub, **classify(Path(args.work_dir) / sub)}
|
||||||
shutil.rmtree(Path(args.work_dir) / sub.split("/")[0], ignore_errors=True)
|
shutil.rmtree(Path(args.work_dir) / sub.split("/")[0], ignore_errors=True)
|
||||||
elif not no_upload and already_done(api, args.dst_repo, sub, dst_files):
|
elif not no_upload and already_done(api, args.dst_repo, sub, dst_files):
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Example (numeric smoke test on one namespace, no SLURM):
|
|||||||
python slurm_migrate.py --slurm 0 --workers 1 \
|
python slurm_migrate.py --slurm 0 --workers 1 \
|
||||||
--dst-repo HuggingFaceVLA/community_dataset_v3_degrees \
|
--dst-repo HuggingFaceVLA/community_dataset_v3_degrees \
|
||||||
--work-dir ./cdv3_work --manifest-dir ./cdv3_manifests \
|
--work-dir ./cdv3_work --manifest-dir ./cdv3_manifests \
|
||||||
--folder-name Beegbrain --allow-uncalibrated
|
--folder-name Beegbrain
|
||||||
|
|
||||||
Full run on the cluster:
|
Full run on the cluster:
|
||||||
python slurm_migrate.py \
|
python slurm_migrate.py \
|
||||||
@@ -24,7 +24,6 @@ Full run on the cluster:
|
|||||||
--logs-dir /fsx/$USER/logs/cdv3_migrate \
|
--logs-dir /fsx/$USER/logs/cdv3_migrate \
|
||||||
--workers 64 --partition hopper-cpu --qos normal \
|
--workers 64 --partition hopper-cpu --qos normal \
|
||||||
--cpus-per-task 4 --mem-per-cpu 4G \
|
--cpus-per-task 4 --mem-per-cpu 4G \
|
||||||
--allow-uncalibrated \
|
|
||||||
--env-command "source /fsx/$USER/venvs/lerobot/bin/activate; export HF_TOKEN=<token>"
|
--env-command "source /fsx/$USER/venvs/lerobot/bin/activate; export HF_TOKEN=<token>"
|
||||||
|
|
||||||
IMPORTANT: workers must reach the internet (HF download + upload) and have a write-scoped
|
IMPORTANT: workers must reach the internet (HF download + upload) and have a write-scoped
|
||||||
@@ -54,7 +53,6 @@ class MigrateShard(PipelineStep):
|
|||||||
migration_dir,
|
migration_dir,
|
||||||
no_push=False,
|
no_push=False,
|
||||||
only_classify=False,
|
only_classify=False,
|
||||||
allow_uncalibrated=False,
|
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.subs = subs
|
self.subs = subs
|
||||||
@@ -64,7 +62,6 @@ class MigrateShard(PipelineStep):
|
|||||||
self.migration_dir = migration_dir
|
self.migration_dir = migration_dir
|
||||||
self.no_push = no_push
|
self.no_push = no_push
|
||||||
self.only_classify = only_classify
|
self.only_classify = only_classify
|
||||||
self.allow_uncalibrated = allow_uncalibrated
|
|
||||||
|
|
||||||
def run(self, data=None, rank: int = 0, world_size: int = 1):
|
def run(self, data=None, rank: int = 0, world_size: int = 1):
|
||||||
# Pickled onto the worker: keep self-contained. The migration package dir must be on
|
# Pickled onto the worker: keep self-contained. The migration package dir must be on
|
||||||
@@ -80,16 +77,13 @@ class MigrateShard(PipelineStep):
|
|||||||
if self.migration_dir not in sys.path:
|
if self.migration_dir not in sys.path:
|
||||||
sys.path.insert(0, self.migration_dir)
|
sys.path.insert(0, self.migration_dir)
|
||||||
|
|
||||||
import so_arm_frame
|
|
||||||
from classify import classify
|
from classify import classify
|
||||||
from huggingface_hub import HfApi
|
from huggingface_hub import HfApi
|
||||||
from run_migration import SRC_REPO, already_done, migrate_one
|
from run_migration import already_done, download_subfolder, migrate_one
|
||||||
|
|
||||||
from lerobot.utils.utils import init_logging
|
from lerobot.utils.utils import init_logging
|
||||||
|
|
||||||
init_logging()
|
init_logging()
|
||||||
if self.allow_uncalibrated:
|
|
||||||
so_arm_frame.CANON_IS_CALIBRATED = True
|
|
||||||
|
|
||||||
my_subs = self.subs[rank::world_size]
|
my_subs = self.subs[rank::world_size]
|
||||||
if not my_subs:
|
if not my_subs:
|
||||||
@@ -122,14 +116,7 @@ class MigrateShard(PipelineStep):
|
|||||||
for i, sub in enumerate(my_subs):
|
for i, sub in enumerate(my_subs):
|
||||||
try:
|
try:
|
||||||
if self.only_classify:
|
if self.only_classify:
|
||||||
from huggingface_hub import snapshot_download
|
download_subfolder(sub, work_dir, patterns=[f"{sub}/meta/*"])
|
||||||
|
|
||||||
snapshot_download(
|
|
||||||
SRC_REPO,
|
|
||||||
repo_type="dataset",
|
|
||||||
allow_patterns=[f"{sub}/meta/*"],
|
|
||||||
local_dir=work_dir,
|
|
||||||
)
|
|
||||||
row = {"root": sub, **classify(Path(work_dir) / sub)}
|
row = {"root": sub, **classify(Path(work_dir) / sub)}
|
||||||
shutil.rmtree(Path(work_dir) / sub.split("/")[0], ignore_errors=True)
|
shutil.rmtree(Path(work_dir) / sub.split("/")[0], ignore_errors=True)
|
||||||
elif not self.no_push and already_done(api, self.dst_repo, sub, dst_files):
|
elif not self.no_push and already_done(api, self.dst_repo, sub, dst_files):
|
||||||
@@ -205,7 +192,6 @@ def main():
|
|||||||
p.add_argument("--limit", type=int, default=None, help="Only the first N sub-datasets (ignored with --folder-name).")
|
p.add_argument("--limit", type=int, default=None, help="Only the first N sub-datasets (ignored with --folder-name).")
|
||||||
p.add_argument("--only-classify", action="store_true", help="Only classify + write manifest; no convert/upload.")
|
p.add_argument("--only-classify", action="store_true", help="Only classify + write manifest; no convert/upload.")
|
||||||
p.add_argument("--no-push", action="store_true", help="Fix + convert locally, keep output, do not upload.")
|
p.add_argument("--no-push", action="store_true", help="Fix + convert locally, keep output, do not upload.")
|
||||||
p.add_argument("--allow-uncalibrated", action="store_true", help="Accept placeholder CANON ranges.")
|
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
if not args.no_push and not args.only_classify and not args.dst_repo:
|
if not args.no_push and not args.only_classify and not args.dst_repo:
|
||||||
@@ -236,7 +222,6 @@ def main():
|
|||||||
MIGRATION_DIR,
|
MIGRATION_DIR,
|
||||||
no_push=args.no_push,
|
no_push=args.no_push,
|
||||||
only_classify=args.only_classify,
|
only_classify=args.only_classify,
|
||||||
allow_uncalibrated=args.allow_uncalibrated,
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
logs_dir=args.logs_dir,
|
logs_dir=args.logs_dir,
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ Two calibration-free branches + one that needs an assumed canonical range:
|
|||||||
* degrees_new (`*_follower` recorded with use_degrees=True, not saturated): already
|
* degrees_new (`*_follower` recorded with use_degrees=True, not saturated): already
|
||||||
degrees. EXACT.
|
degrees. EXACT.
|
||||||
* normalized (`*_follower`, -100..100 joints / 0..100 gripper, saturates at bounds):
|
* normalized (`*_follower`, -100..100 joints / 0..100 gripper, saturates at bounds):
|
||||||
already mid-range-zero; only the SCALE is missing (per-robot range_min/max
|
5 arm joints are mid-range-zero, only the SCALE is missing (per-robot
|
||||||
is not stored) -> use assumed canonical per-joint spans below. APPROXIMATE.
|
range_min/max not stored) -> use assumed canonical spans below. APPROXIMATE.
|
||||||
|
The gripper (0..100) is kept in its native frame, matching degrees_new.
|
||||||
* radians -> untouched.
|
* radians -> untouched.
|
||||||
|
|
||||||
Joint order per arm: shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper.
|
Joint order per arm: shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper.
|
||||||
@@ -23,13 +24,12 @@ JOINT_ORDER = ["shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wri
|
|||||||
SIGNS = np.array([1.0, -1.0, 1.0, 1.0, 1.0, 1.0], dtype=np.float64)
|
SIGNS = np.array([1.0, -1.0, 1.0, 1.0, 1.0, 1.0], dtype=np.float64)
|
||||||
OFFSETS_DEG = np.array([0.0, 90.0, 90.0, 0.0, 0.0, 0.0], dtype=np.float64)
|
OFFSETS_DEG = np.array([0.0, 90.0, 90.0, 0.0, 0.0, 0.0], dtype=np.float64)
|
||||||
|
|
||||||
# --- Canonical per-joint spans (DEGREES) used ONLY to invert the -100..100 / 0..100
|
# --- Canonical per-joint spans (DEGREES) used ONLY to invert the -100..100 normalization of
|
||||||
# normalization when per-robot calibration is unavailable. joints 0..4 (RANGE_M100_100):
|
# the 5 arm joints (RANGE_M100_100) when per-robot calibration is unavailable: normalized
|
||||||
# normalized +/-100 -> +/-HALF_RANGE. gripper (RANGE_0_100): 0..100 -> centered at 50,
|
# +/-100 -> +/-HALF_RANGE. The gripper (RANGE_0_100) is left in its native 0..100 frame in
|
||||||
# span FULL_RANGE. THESE ARE PLACEHOLDERS — run calibrate_canonical_ranges.py and paste
|
# every SO dataset, so it needs no canonical span. THESE ARE PLACEHOLDERS — run
|
||||||
# the fitted values here before a production run. ---
|
# calibrate_canonical_ranges.py and paste the fitted values here before a production run. ---
|
||||||
CANON_HALF_RANGE_DEG = np.array([100.0, 100.0, 100.0, 100.0, 100.0], dtype=np.float64) # 5 arm joints
|
CANON_HALF_RANGE_DEG = np.array([100.0, 100.0, 100.0, 100.0, 100.0], dtype=np.float64) # 5 arm joints
|
||||||
CANON_GRIPPER_FULL_RANGE_DEG = 90.0
|
|
||||||
CANON_IS_CALIBRATED = False # flipped to True once you paste fitted values
|
CANON_IS_CALIBRATED = False # flipped to True once you paste fitted values
|
||||||
|
|
||||||
|
|
||||||
@@ -43,9 +43,10 @@ def _convert_arm(x: np.ndarray, encoding: str) -> np.ndarray:
|
|||||||
if encoding == "degrees_new":
|
if encoding == "degrees_new":
|
||||||
return x
|
return x
|
||||||
if encoding == "normalized":
|
if encoding == "normalized":
|
||||||
new_deg = np.empty_like(x)
|
new_deg = np.array(x, dtype=np.float64)
|
||||||
new_deg[..., :5] = (x[..., :5] / 100.0) * CANON_HALF_RANGE_DEG
|
new_deg[..., :5] = (x[..., :5] / 100.0) * CANON_HALF_RANGE_DEG
|
||||||
new_deg[..., 5] = (x[..., 5] / 100.0 - 0.5) * CANON_GRIPPER_FULL_RANGE_DEG
|
# gripper is RANGE_0_100 in every SO dataset (including use_degrees=True / degrees_new),
|
||||||
|
# so it is already frame-consistent and must be left untouched, not remapped to +/-deg.
|
||||||
return new_deg
|
return new_deg
|
||||||
raise ValueError(f"unknown encoding: {encoding!r}")
|
raise ValueError(f"unknown encoding: {encoding!r}")
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ def to_degrees(arr, encoding: str, n_joints_per_arm: int = 6) -> np.ndarray:
|
|||||||
if encoding == "normalized" and not CANON_IS_CALIBRATED:
|
if encoding == "normalized" and not CANON_IS_CALIBRATED:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"CANON ranges are placeholders. Run calibrate_canonical_ranges.py and set "
|
"CANON ranges are placeholders. Run calibrate_canonical_ranges.py and set "
|
||||||
"CANON_* + CANON_IS_CALIBRATED=True, or pass --allow-uncalibrated to accept them."
|
"CANON_* + CANON_IS_CALIBRATED=True before converting 'normalized' datasets to degrees."
|
||||||
)
|
)
|
||||||
out = np.empty_like(arr)
|
out = np.empty_like(arr)
|
||||||
for a in range(d // n_joints_per_arm):
|
for a in range(d // n_joints_per_arm):
|
||||||
|
|||||||
Reference in New Issue
Block a user