Spaces:
Running
Running
File size: 10,778 Bytes
d646dd7 af470e1 d646dd7 af470e1 d646dd7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | """Batch directory file discovery helpers — shared by modules and worker.
从批次解压目录(input_root)按 source_keys/source_key/position 匹配视频、
按位置匹配 parquet。匹配时排除辅助流(_aux)和骨骼视频。
"""
from __future__ import annotations
from pathlib import Path
from app.lerobot_v21 import (
canonical_source_key,
is_depth_source,
iter_video_streams,
)
def _keys_of(config: dict) -> list[str]:
keys: list[str] = []
raw_keys = config.get("source_keys")
if isinstance(raw_keys, str):
keys = [k.strip() for k in raw_keys.split(",") if k.strip()]
elif isinstance(raw_keys, (list, tuple)):
keys = [str(k) for k in raw_keys if str(k).strip()]
single = config.get("source_key") or config.get("position")
if single and str(single).strip() and str(single) not in keys:
keys.insert(0, str(single))
return keys
# 相机名 → 模块类型推断(source_key 匹配不上时的兜底,保证项目上传过的
# 数据类型在输入面板可见)。旧卡规则保留(存量工作流兼容);新体系下
# stereo_camera/stereo_rgbd_camera 先认领 stereo_*,mono_camera 兜底认领剩余相机名。
_CAMERA_TYPE_RULES: dict[str, tuple[str, ...]] = {
"stereo_camera": ("stereo",),
"stereo_rgbd_camera": ("stereo",),
"rgbd_camera": ("rgbd",),
"fisheye_camera": ("fisheye",),
"rgb_camera": ("rgb", "ego"),
}
# 匹配优先级(显式顺序,不依赖注册序):双目先认领,单目兜底
_MATCH_PRIORITY = ["gripper_device", "stereo_rgbd_camera", "stereo_camera", "rgbd_camera", "mono_camera"]
# 传感器类输入源关键词:mono 兜底认领时跳过(不抢手套/IMU/深度等)
_SENSOR_NAME_KEYWORDS = ("glove", "imu", "action", "tactile", "sensor",
"depth", "hand")
def match_input_modules(available: set[str]) -> list[dict]:
"""输入模块与可用输入源的匹配结果(projects/devices input-sources 共用)。
``available``: 小写化的可用输入源集合(相机主目名 + 传感器名)。
匹配分两步:
1. 模块 source_key(s) 与 available **双向子串**匹配 —— 精确名
(stereo_left vs stereo_left)与包含关系(left_glove_joint vs
left_glove)都覆盖;
2. source_key 未命中时按类型兜底:stereo_camera 认领含 stereo 的
相机名;mono_camera 认领剩余的非传感器相机名(head_left_rgb /
d435_rgb / ego_rgb 等任意单目命名,与设备名无关);旧 rgb/fisheye
卡规则保留(存量工作流)。
同一输入源只归属一个模块(优先子串匹配;兜底按 _MATCH_PRIORITY)。
返回 [{type, label, matched, matched_keys}]。
"""
from app.processing.catalog import module_catalog
by_type = {
d.get("type") or d.get("slug"): d
for d in module_catalog()
if d.get("category") == "input"
}
order = [t for t in _MATCH_PRIORITY if t in by_type]
order += [t for t in by_type if t not in order] # 其余按注册序
modules = []
used: set[str] = set() # 已归属的输入源,避免重复匹配
for node_type in order:
d = by_type[node_type]
keys = _keys_of(dict(d.get("default_config") or {}))
hits: list[str] = []
# 1) source_key 双向子串匹配
for k in keys:
kl = k.lower()
for a in sorted(available):
if a in used:
continue
if kl in a or a in kl:
hits.append(a)
used.add(a)
break
# 2) 类型兜底
if not hits:
if node_type == "gripper_device":
# A UMI gripper publishes several internal channel names but
# represents one physical acquisition source.
gripper_hits = []
for a in sorted(available):
if a in used:
continue
if "gripper" in a or "umi" in a:
gripper_hits.append(a)
used.add(a)
hits.extend(gripper_hits)
elif node_type == "mono_camera":
# 单目 = 兜底认领剩余相机名(跳过传感器/深度类名称)
for a in sorted(available):
if a in used:
continue
# A color slot published by a depth camera is still a
# normal video input (e.g. D435_depth_rgb). Only pure
# depth streams should be excluded from Mono Video.
if (any(kw in a for kw in _SENSOR_NAME_KEYWORDS)
and not ("rgb" in a and "depth" in a)):
continue
hits.append(a)
used.add(a)
break
elif node_type in {"rgbd_camera", "stereo_rgbd_camera"}:
# RGB-D is identified from a color stream carrying a depth
# marker. Pure depth streams are not normal video inputs.
for a in sorted(available):
if a in used:
continue
if node_type == "stereo_rgbd_camera" and "stereo" not in a:
continue
if ("depth" in a or "rgbd" in a) and any(
token in a for token in ("rgb", "color")):
hits.append(a)
used.add(a)
break
else:
rules = _CAMERA_TYPE_RULES.get(node_type)
if rules:
for a in sorted(available):
if a in used:
continue
# rgb 兜底不抢 stereo/fisheye 的相机名
if node_type == "rgb_camera" and any(
kw in a for kw in ("stereo", "fisheye")):
continue
if any(kw in a for kw in rules):
hits.append(a)
used.add(a)
break
modules.append({
"type": node_type,
"label": d.get("label"),
"matched": bool(hits),
"matched_keys": hits,
})
return modules
def _is_main_video(path: Path, source_key: str | None = None) -> bool:
"""主 RGB 流判定:排除深度流、辅助流(_aux)和预览渲染流。"""
p = path.as_posix().lower()
return (not is_depth_source(source_key)
and "/depth/" not in p and "/processed/" not in p
and "_aux" not in p and "skeleton" not in p)
def find_videos(input_root: Path, config: dict) -> list[Path]:
"""按 source_keys(或 source_key/position)匹配视频列表,保持配置顺序。
双目兼容:stereo_camera 可配置 ``source_keys: "stereo_left,stereo_right"``,
每个 key 匹配一个视频;匹配时排除辅助流(_aux)、骨骼视频、深度预览
与 processed 处理产物。
自动识别:未配置 key → 返回 videos/ 目录下全部主 RGB 视频(按路径
字典序稳定排序,left 在 right 前)。配置了显式 key 但未命中时返回空
列表,让对应输入节点 skipped,避免把另一台相机误认成该设备。
"""
expected_name = Path(str(config.get("expected_name") or "")).name
videos_root = input_root / "videos"
stream_entries = iter_video_streams(videos_root) if videos_root.is_dir() else []
videos = [path for source, path in stream_entries
if _is_main_video(path, source)]
# Compatibility fallback for malformed/legacy uploads where videos are
# not under a videos/ root. The canonical v2.1 path always uses the
# source-aware list above.
if not videos:
videos = sorted(input_root.rglob("*.mp4"))
if expected_name:
for path in videos:
if path.name == expected_name and _is_main_video(path):
return [path]
keys = _keys_of(config)
found: list[Path] = []
used: set[Path] = set()
for key in keys:
key_l = key.lower()
canonical_key_l = canonical_source_key(key).lower()
# Match the source identity before falling back to path text. This
# keeps old workflows using D435_head_rgb/D435_depth_rgb compatible
# with the canonical D435_rgb directory after v2.1 migration.
candidates = [p for source, p in stream_entries
if p in videos and p not in used
and canonical_source_key(source).lower() == canonical_key_l]
if not candidates:
candidates = [p for source, p in stream_entries
if p in videos and p not in used
and (key_l in source.lower()
or source.lower() in key_l)]
if not candidates:
candidates = [p for p in videos
if key_l in p.as_posix().lower() and p not in used
and _is_main_video(p)]
if not candidates:
continue
# 优先主目(左目在右目之前,字典序稳定)
candidates.sort(key=lambda p: p.as_posix())
found.append(candidates[0])
used.add(candidates[0])
if found:
return found
# An explicit source key is tied to one device. Do not auto-detect and
# reuse another camera when that device is absent; the input module will
# mark itself skipped and downstream nodes can use available inputs.
if keys:
return []
# 自动识别回退:只扫 canonical videos/ 主目录树
pool = videos
mains = sorted([p for p in pool if _is_main_video(p)],
key=lambda p: p.as_posix())
if mains:
return mains
# videos/ 无结果时兜底全树扫描(兼容不规范的旧批次布局)
return sorted([p for p in videos if _is_main_video(p)],
key=lambda p: p.as_posix())
def find_video(input_root: Path, config: dict) -> Path:
"""按 source_key 匹配单个视频(兼容旧节点)。"""
videos = find_videos(input_root, config)
if not videos:
raise FileNotFoundError("No MP4 video found in uploaded episode")
return videos[0]
def find_parquet(input_root: Path) -> list[Path]:
"""批次内全部 parquet(排除 auto_labels/hand_kpts 合并产物与 meta 目录)。"""
out = []
for p in sorted(input_root.rglob("*.parquet")):
lower = p.as_posix().lower()
if "auto_labels" in lower or "hand_kpts" in lower:
continue
if "/meta/" in lower:
continue
out.append(p)
return out
|