Skip to content

Commit c9c68fa

Browse files
committed
Refine dataset filtering and ground alignment
1 parent 5423d32 commit c9c68fa

11 files changed

Lines changed: 502 additions & 240 deletions

File tree

teleopit/inputs/pico4_provider.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,15 @@ def _has_non_degenerate_positions(positions: NDArray[np.float64]) -> bool:
121121
return extent > 1e-6
122122

123123

124-
def _compute_ground_lift_offset(positions: NDArray[np.float64]) -> float:
124+
def _compute_ground_alignment_offset(positions: NDArray[np.float64]) -> float:
125125
pos = np.asarray(positions, dtype=np.float64).reshape(-1, 3)
126126
if pos.size == 0:
127127
return 0.0
128128
finite_mask = np.all(np.isfinite(pos), axis=1)
129129
if not np.any(finite_mask):
130130
return 0.0
131131
min_z = float(np.min(pos[finite_mask, 2]))
132-
return max(-min_z, 0.0)
132+
return -min_z
133133

134134

135135
def _bridge_accepts_video_enabled(bridge_cls: type[Any]) -> bool:
@@ -233,7 +233,7 @@ def __init__(
233233
self._last_source_seq: int | None = None
234234
self._controller_snapshot: PicoControllerSnapshot | None = None
235235
self._hand_snapshot: PicoHandSnapshot | None = None
236-
self._ground_lift_offset: float | None = None
236+
self._ground_alignment_offset: float | None = None
237237
self._bridge = bridge_cls(
238238
host=bridge_host,
239239
port=int(bridge_port),
@@ -415,15 +415,15 @@ def _accept_pico_frame(self, frame: Any) -> bool:
415415
and timestamp - self._last_frame_timestamp > self._timestamp_gap_reset_s
416416
):
417417
self._frame_cache.clear()
418-
self._ground_lift_offset = None
418+
self._ground_alignment_offset = None
419419
logger.warning(
420420
"Pico4InputProvider timestamp-gap reset | gap=%.4fs",
421421
timestamp - self._last_frame_timestamp,
422422
)
423423
if self._last_frame_timestamp is not None and timestamp <= self._last_frame_timestamp + 1e-9:
424424
timestamp = self._last_frame_timestamp + 1e-6
425425

426-
human_frame = self._apply_ground_lift(human_frame)
426+
human_frame = self._apply_ground_alignment(human_frame)
427427
self._frame_cache.append(human_frame, timestamp, fps_timestamp=timestamp)
428428
self._last_raw_body_joints = body_joints.copy()
429429
self._last_frame_timestamp = timestamp
@@ -534,17 +534,17 @@ def _convert_body_joints_to_frame(body_joints: NDArray[np.float64]) -> HumanFram
534534
result[name] = (np.asarray(pos, dtype=np.float64), np.asarray(quat, dtype=np.float64))
535535
return result
536536

537-
def _apply_ground_lift(self, human_frame: HumanFrame) -> HumanFrame:
538-
"""Apply one fixed Z lift so the initial Pico skeleton sits on the floor."""
539-
if self._ground_lift_offset is None:
537+
def _apply_ground_alignment(self, human_frame: HumanFrame) -> HumanFrame:
538+
"""Apply one fixed Z offset so the initial Pico skeleton sits on the floor."""
539+
if self._ground_alignment_offset is None:
540540
positions = np.asarray([value[0] for value in human_frame.values()], dtype=np.float64)
541541
if _has_non_degenerate_positions(positions):
542-
self._ground_lift_offset = _compute_ground_lift_offset(positions)
542+
self._ground_alignment_offset = _compute_ground_alignment_offset(positions)
543543
else:
544544
return human_frame
545545

546-
offset = float(self._ground_lift_offset)
547-
if offset <= 0.0:
546+
offset = float(self._ground_alignment_offset)
547+
if abs(offset) <= 1e-12:
548548
return human_frame
549549

550550
z_offset = np.array([0.0, 0.0, offset], dtype=np.float64)

teleopit/runtime/assets.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
PROJECT_ROOT = Path(__file__).resolve().parents[2]
77
GMR_ASSETS_ROOT = PROJECT_ROOT / "teleopit" / "retargeting" / "gmr" / "assets"
88
UNITREE_G1_MJLAB_XML = GMR_ASSETS_ROOT / "unitree_g1" / "g1_mjlab.xml"
9-
UNITREE_G1_MJLAB_PAYLOAD_XML = GMR_ASSETS_ROOT / "unitree_g1" / "g1_mjlab_payload.xml"
109

1110

1211
def missing_gmr_assets_message(path: str | Path, *, label: str = "Required asset") -> str:

tests/test_dataset_v2.py

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,26 @@ def test_load_dataset_spec_parses_preprocess(tmp_path: Path) -> None:
131131
hash_salt: ""
132132
preprocess:
133133
normalize_root_xy: true
134-
ground_align: clip_min_foot
134+
ground_align: none
135135
min_frames: 10
136+
max_all_off_ground_s: 0.5
137+
off_ground_height: 0.08
136138
sources:
137139
- name: clips
138140
type: npz
139141
input: {tmp_path / 'npz_source'}
142+
exclude_patterns: ["*obstacle*"]
140143
""",
141144
encoding="utf-8",
142145
)
143146

144147
spec = load_dataset_spec(spec_path)
145148
assert spec.preprocess.normalize_root_xy is True
146-
assert spec.preprocess.ground_align == "clip_min_foot"
149+
assert spec.preprocess.ground_align == "none"
147150
assert spec.preprocess.min_frames == 10
151+
assert spec.preprocess.max_all_off_ground_s == 0.5
152+
assert spec.preprocess.off_ground_height == 0.08
153+
assert spec.sources[0].exclude_patterns == ("*obstacle*",)
148154

149155

150156
def test_load_dataset_spec_parses_seed_filter_preset(tmp_path: Path) -> None:
@@ -428,6 +434,71 @@ def test_collect_source_files_with_report_handles_single_file_source(tmp_path: P
428434
assert [item.rel_no_suffix.as_posix() for item in legacy_items] == ["clip_a"]
429435

430436

437+
def test_collect_source_files_with_report_applies_exclude_patterns(tmp_path: Path) -> None:
438+
input_root = tmp_path / "lafan1"
439+
input_root.mkdir(parents=True, exist_ok=True)
440+
(input_root / "walk1.bvh").write_text("placeholder", encoding="utf-8")
441+
(input_root / "obstacle_run.bvh").write_text("placeholder", encoding="utf-8")
442+
nested = input_root / "subject1"
443+
nested.mkdir()
444+
(nested / "obstacle_jump.bvh").write_text("placeholder", encoding="utf-8")
445+
446+
source = DatasetSourceSpec(
447+
name="lafan1",
448+
type="bvh",
449+
input=str(input_root),
450+
bvh_format="lafan1",
451+
exclude_patterns=("*obstacle*",),
452+
)
453+
454+
items, _scan_root, report = dataset_builder._collect_source_files_with_report(
455+
source,
456+
quiet=True,
457+
)
458+
459+
assert [item.rel_no_suffix.as_posix() for item in items] == ["walk1"]
460+
assert report["scanned_files"] == 3
461+
assert report["path_rejected_files"] == 2
462+
assert report["kept_files"] == 1
463+
assert report["filtered_files"] == 2
464+
assert report["path_reject_reasons"] == {"*obstacle*": 2}
465+
466+
467+
def test_collect_source_files_with_report_preserves_path_excludes_with_metadata(tmp_path: Path) -> None:
468+
input_root = tmp_path / "seed_source" / "g1" / "csv"
469+
input_root.mkdir(parents=True, exist_ok=True)
470+
for name in ("walk_a.csv", "walk_b.csv", "obstacle_walk.csv"):
471+
(input_root / name).write_text("placeholder", encoding="utf-8")
472+
473+
metadata_csv = tmp_path / "metadata.csv"
474+
with metadata_csv.open("w", encoding="utf-8", newline="") as handle:
475+
writer = csv.DictWriter(handle, fieldnames=["move_g1_path", "is_mirror"])
476+
writer.writeheader()
477+
for name in ("walk_a.csv", "walk_b.csv", "obstacle_walk.csv"):
478+
writer.writerow({"move_g1_path": f"g1/csv/{name}", "is_mirror": "False"})
479+
480+
source = DatasetSourceSpec(
481+
name="seed",
482+
type="seed_csv",
483+
input=str(input_root),
484+
metadata_csv=str(metadata_csv),
485+
filters={"is_mirror": [False]},
486+
exclude_patterns=("*obstacle*",),
487+
)
488+
489+
items, _scan_root, report = dataset_builder._collect_source_files_with_report(
490+
source,
491+
quiet=True,
492+
)
493+
494+
assert [item.rel_no_suffix.as_posix() for item in items] == ["walk_a", "walk_b"]
495+
assert report["scanned_files"] == 3
496+
assert report["path_rejected_files"] == 1
497+
assert report["kept_files"] == 2
498+
assert report["filtered_files"] == 1
499+
assert report["path_reject_reasons"] == {"*obstacle*": 1}
500+
501+
431502
def test_build_dataset_from_spec_writes_shard_directories(tmp_path: Path) -> None:
432503
npz_input = tmp_path / "npz_source"
433504
_write_npz_from_pkl(npz_input / "clip_a.npz")
@@ -460,6 +531,35 @@ def test_build_dataset_from_spec_writes_shard_directories(tmp_path: Path) -> Non
460531
assert "clip_lengths" in train_data.files
461532

462533

534+
def test_collect_clip_rows_ignores_stale_excluded_cached_npz(tmp_path: Path) -> None:
535+
npz_input = tmp_path / "npz_source"
536+
for name in ("keep_a.npz", "keep_b.npz", "obstacle_old.npz"):
537+
_write_npz_from_pkl(npz_input / name)
538+
539+
spec = DatasetSpec(
540+
name="demo_dataset",
541+
target_fps=30,
542+
val_percent=5,
543+
hash_salt="",
544+
sources=[
545+
DatasetSourceSpec(
546+
name="npz_src",
547+
type="npz",
548+
input=str(npz_input),
549+
exclude_patterns=("*obstacle*",),
550+
)
551+
],
552+
)
553+
paths = dataset_builder.resolve_dataset_paths(spec, output_root=tmp_path / "datasets")
554+
source_dir = paths.clips_root / "npz_src"
555+
for name in ("keep_a.npz", "keep_b.npz", "obstacle_old.npz"):
556+
_write_npz_from_pkl(source_dir / name)
557+
558+
rows = dataset_builder.collect_clip_rows(spec, paths=paths)
559+
560+
assert sorted(row.clip_id for row in rows) == ["npz_src:keep_a", "npz_src:keep_b"]
561+
562+
463563
def test_convert_source_to_npz_clips_applies_preprocess(tmp_path: Path) -> None:
464564
npz_input = tmp_path / "npz_source"
465565
_write_npz_from_pkl(npz_input / "clip_a.npz")
@@ -503,6 +603,66 @@ def test_convert_source_to_npz_clips_applies_preprocess(tmp_path: Path) -> None:
503603
assert np.isclose(float(np.min(foot_z)), 0.0)
504604

505605

606+
def test_convert_source_to_npz_clips_skips_all_off_ground_clips_before_ground_align(tmp_path: Path) -> None:
607+
npz_input = tmp_path / "npz_source"
608+
_write_npz_from_pkl(npz_input / "keep.npz")
609+
_write_npz_from_pkl(npz_input / "float.npz")
610+
611+
for name, floating in (("keep.npz", False), ("float.npz", True)):
612+
path = npz_input / name
613+
clip = dict(np.load(path, allow_pickle=True))
614+
body_names = [str(body_name) for body_name in clip["body_names"].tolist()]
615+
left_idx = body_names.index("left_ankle_roll_link")
616+
right_idx = body_names.index("right_ankle_roll_link")
617+
body_pos_w = np.asarray(clip["body_pos_w"]).copy()
618+
if floating:
619+
body_pos_w[..., 2] = np.maximum(body_pos_w[..., 2], 0.3)
620+
else:
621+
body_pos_w[:, left_idx, 2] = 0.0
622+
body_pos_w[:, right_idx, 2] = 0.3
623+
clip["body_pos_w"] = body_pos_w
624+
np.savez(path, **clip)
625+
626+
source = DatasetSourceSpec(name="npz_src", type="npz", input=str(npz_input))
627+
output_dir = tmp_path / "dataset" / "clips" / "npz_src"
628+
report = convert_source_to_npz_clips(
629+
source,
630+
output_dir,
631+
jobs=1,
632+
preprocess=dataset_builder.DatasetPreprocessSpec(
633+
ground_align="clip_min_foot",
634+
max_all_off_ground_s=0.05,
635+
off_ground_height=0.08,
636+
),
637+
)
638+
639+
assert report["clips"] == 1
640+
assert (output_dir / "keep.npz").is_file()
641+
assert not (output_dir / "float.npz").exists()
642+
assert (output_dir / "float.npz.filtered.json").is_file()
643+
644+
def _unexpected_run_conversion_tasks(*_args, **_kwargs):
645+
raise AssertionError("filtered clip should be skipped by marker on incremental rebuild")
646+
647+
monkeypatch = pytest.MonkeyPatch()
648+
monkeypatch.setattr(dataset_builder, "run_conversion_tasks", _unexpected_run_conversion_tasks)
649+
try:
650+
second_report = convert_source_to_npz_clips(
651+
source,
652+
output_dir,
653+
jobs=1,
654+
preprocess=dataset_builder.DatasetPreprocessSpec(
655+
ground_align="clip_min_foot",
656+
max_all_off_ground_s=0.05,
657+
off_ground_height=0.08,
658+
),
659+
)
660+
finally:
661+
monkeypatch.undo()
662+
663+
assert second_report["clips"] == 1
664+
665+
506666
def test_merge_clip_dicts_rejects_inconsistent_body_names(tmp_path: Path) -> None:
507667
clip_a = tmp_path / "clip_a.npz"
508668
clip_b = tmp_path / "clip_b.npz"

tests/test_domain_randomization.py

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ def test_general_tracking_domain_randomization_matches_gr00t_active_set() -> Non
1919
"add_joint_default_pos",
2020
"physics_material",
2121
"randomize_rigid_body_mass",
22-
"randomize_dexhand_payload_mass",
23-
"randomize_gimbal_payload_mass",
24-
"randomize_dexhand_payload_pos",
25-
"randomize_gimbal_payload_pos",
2622
}
2723

2824
push_robot = events["push_robot"]
@@ -69,46 +65,6 @@ def test_general_tracking_domain_randomization_matches_gr00t_active_set() -> Non
6965
assert mass.params["asset_cfg"].body_names == "torso_link"
7066
assert mass.params["alpha_range"] == (-0.1, 0.45)
7167

72-
dexhand_mass = events["randomize_dexhand_payload_mass"]
73-
assert dexhand_mass.func is dr.pseudo_inertia
74-
assert dexhand_mass.mode == "startup"
75-
assert dexhand_mass.params["asset_cfg"].body_names == (
76-
"left_dexhand_payload",
77-
"right_dexhand_payload",
78-
)
79-
assert dexhand_mass.params["alpha_range"] == (-1, 0)
80-
81-
gimbal_mass = events["randomize_gimbal_payload_mass"]
82-
assert gimbal_mass.func is dr.pseudo_inertia
83-
assert gimbal_mass.mode == "startup"
84-
assert gimbal_mass.params["asset_cfg"].body_names == ("head_gimbal_payload",)
85-
assert gimbal_mass.params["alpha_range"] == (-1, 0)
86-
87-
dexhand_pos = events["randomize_dexhand_payload_pos"]
88-
assert dexhand_pos.func is dr.body_pos
89-
assert dexhand_pos.mode == "startup"
90-
assert dexhand_pos.params["asset_cfg"].body_names == (
91-
"left_dexhand_payload",
92-
"right_dexhand_payload",
93-
)
94-
assert dexhand_pos.params["operation"] == "abs"
95-
assert dexhand_pos.params["ranges"] == {
96-
0: (0.055, 0.095),
97-
1: (-0.02, 0.02),
98-
2: (-0.02, 0.02),
99-
}
100-
101-
gimbal_pos = events["randomize_gimbal_payload_pos"]
102-
assert gimbal_pos.func is dr.body_pos
103-
assert gimbal_pos.mode == "startup"
104-
assert gimbal_pos.params["asset_cfg"].body_names == ("head_gimbal_payload",)
105-
assert gimbal_pos.params["operation"] == "abs"
106-
assert gimbal_pos.params["ranges"] == {
107-
0: (0.05, 0.09),
108-
1: (-0.02, 0.02),
109-
2: (0.43, 0.47),
110-
}
111-
11268

11369
def test_play_env_disables_training_only_domain_randomization() -> None:
11470
import mjlab.tasks # noqa: F401
@@ -122,8 +78,4 @@ def test_play_env_disables_training_only_domain_randomization() -> None:
12278
assert "add_joint_default_pos" not in play_cfg.events
12379
assert "physics_material" not in play_cfg.events
12480
assert "randomize_rigid_body_mass" not in play_cfg.events
125-
assert "randomize_dexhand_payload_mass" not in play_cfg.events
126-
assert "randomize_gimbal_payload_mass" not in play_cfg.events
127-
assert "randomize_dexhand_payload_pos" not in play_cfg.events
128-
assert "randomize_gimbal_payload_pos" not in play_cfg.events
12981
assert play_cfg.events == {}

0 commit comments

Comments
 (0)