每个团队都有内置质检不知道的规则:某个话题必须存在、丢帧比例的上限、传感器数值的合理范围、任务片段的最短时长。质检算法就是把这样的规则写成代码。它查看数据、报告发现,从不修改任何内容,所以你可以放心地加很多个。
质检默认每条录制运行一次,读取标准化后的 recording;设置 reads="raw" 后读取上传的原始文件。写 level="episode" 后改为每个任务片段运行一次,读取 recording 和 episodes。无论哪种,都通过 out(一个 CheckWriter)写出 checks。
状态
每条结果都有六种状态之一:pass、warn、fail、error、not_evaluated 和 not_applicable。只有 pass 显示为绿色,其余五种都不会显示为通过。这样,一个没能检查数据的质检,永远不会看起来像是认可了数据。
你要写什么
| 方法 | 作用 |
|---|---|
out.result(id, status, value=None, threshold=None, message="", evidence=None, at=None) | 报告一条结果。 |
out.evidence_path(name) | 获得一个写证据文件(例如图表)的路径。 |
每个结果 id 都要先在装饰器里用 checks=[Check(id, title)] 声明,写出未声明 id 的结果会被 CHECK_ID_UNDECLARED 拒绝。结果可以用 at 指明发生的位置,查看器就能直接跳到那一刻。
示例:录制质检
这个质检报告相机出现间隙的帧所占的比例,超过你设定的上限就给出警告。文件是 packages/sdk-python/examples/v1/check-frame-gaps/main.py。
"""Check (recording): the share of frames where a camera had a gap.
In: the canonical recording (the frames of every camera and which of them are valid).
Out: one check result, "pass" or "warn", with the worst share and where the first gap is.
Why: a camera that drops frames (a USB hiccup on a head-mounted rig, a stalled robot camera) breaks
video-based training; the check tells you before the recording reaches a dataset version.
"""
from dataclasses import dataclass
from embodiflow import Check, CheckWriter, Context, Recording, algorithm, param
@dataclass
class Params:
max_ratio: float = param(0.01, minimum=0.0, maximum=1.0, title="Largest accepted gap share")
@algorithm.check(
name="frame-gaps",
title="Frame gaps",
checks=[Check("frame_gap_ratio", "Share of frames missing from a camera")],
params=Params,
)
def run(recording: Recording, out: CheckWriter, ctx: Context[Params]) -> None:
worst, worst_key, first_gap = 0.0, "", None
for camera in recording.cameras:
valid = camera.valid()
ratio = float(1.0 - valid.mean()) if len(valid) else 0.0
if ratio > worst:
worst, worst_key = ratio, camera.key
first_gap = int((~valid).argmax())
status = "pass" if worst <= ctx.params.max_ratio else "warn"
at = {"startS": first_gap / recording.fps, "endS": (first_gap + 1) / recording.fps, "camera": worst_key} if first_gap is not None else None
out.result("frame_gap_ratio", status, value=worst, threshold=ctx.params.max_ratio, message=f"{worst:.1%} of {worst_key or 'camera'} frames are gaps", at=at)它的测试是 packages/sdk-python/examples/v1/check-frame-gaps/test_main.py:用一份很小的测试数据,通过真实的运行器运行这段代码,并检查写出的值。想从它开始,运行 embodiflow algorithm init my-algorithm --category check,它会写出 main.py 和 test_main.py;再运行 embodiflow algorithm test my-algorithm(需要装有 pytest 和 SDK 的 Python)。
示例:检查上传的原始文件
reads="raw" 的质检在标准化之前运行,可以在任何繁重的处理开始之前拦下有问题的上传。这个例子检查 MCAP 文件里是否有标准化算法需要的话题,required 是一个列表参数。文件是 packages/sdk-python/examples/v1/check-raw-topics/main.py。
"""Check (raw): the MCAP file has the topics a standardizer needs, at a sane rate.
In: the uploaded files (raw), before any standardizing: an MCAP file recorded from ROS 2.
Out: two check results: all required topics are there, and each publishes fast enough.
Why: a missing or slow camera topic is cheapest to find right after the upload, not after a long workflow.
A missing topic fails the check but not the step; an upload without an MCAP is "not applicable".
"""
from dataclasses import dataclass
from embodiflow import Check, CheckWriter, Context, RawFiles, algorithm, param
@dataclass
class Params:
required: list[str] = param(["/camera/image"], widget="tags", min_items=1, title="Required topics")
min_rate_hz: float = param(5.0, minimum=0.0, title="Lowest accepted rate (Hz)")
@algorithm.check(
name="raw-topics",
title="Required MCAP topics",
reads="raw",
checks=[Check("topics_present", "Required topics are present"), Check("topic_rate", "Required topics publish fast enough")],
params=Params,
)
def run(raw: RawFiles, out: CheckWriter, ctx: Context[Params]) -> None:
wanted = ctx.params.required
if not raw.find("**/*.mcap"):
ctx.not_applicable("NO_MCAP", "no .mcap file in this upload")
topics = {t.name: t for t in raw.mcap().topics}
missing = [name for name in wanted if name not in topics]
out.result("topics_present", "fail" if missing else "pass", message=f"missing: {', '.join(missing)}" if missing else "all required topics found")
slow = [name for name in wanted if name in topics and topics[name].rate < ctx.params.min_rate_hz]
out.result("topic_rate", "warn" if slow else "pass", threshold=ctx.params.min_rate_hz, message=f"slow: {', '.join(slow)}" if slow else "rates are fine")它的测试是 packages/sdk-python/examples/v1/check-raw-topics/test_main.py:用一份很小的测试数据,通过真实的运行器运行这段代码,并检查写出的值。想从它开始,运行 embodiflow algorithm init my-algorithm --category check --reads raw,它会写出 main.py 和 test_main.py;再运行 embodiflow algorithm test my-algorithm(需要装有 pytest 和 SDK 的 Python)。
示例:任务片段质检
写 level="episode" 后,质检会得到一个 episode 参数,里面有该任务片段的时间窗口和任务文字。这个例子把短到无法训练的任务片段判为 fail,缺少任务文字时给出警告。文件是 packages/sdk-python/examples/v1/check-episode-duration/main.py。
"""Check (episode level): is each episode long enough to train on, and does it say what the robot or person does?
In: one recording and one episode of it (a start and an end on the recording's timeline, plus a task text).
Out: two check results for that episode, "pass", "warn" or "fail". The step itself still succeeds.
Why: very short episodes and episodes without a task text are the usual reasons a training run learns nothing,
so this check flags them before the episode reaches a dataset version.
"""
from dataclasses import dataclass
from embodiflow import Check, CheckWriter, Context, Episode, Recording, algorithm, param
@dataclass
class Params:
min_s: float = param(2.0, minimum=0.0, title="Shortest accepted episode (s)")
@algorithm.check(
name="episode-duration",
title="Episode duration and task",
level="episode",
checks=[
Check("episode_duration", "The episode is long enough"),
Check("episode_task", "The episode has a task text"),
],
params=Params,
)
def run(recording: Recording, episode: Episode, out: CheckWriter, ctx: Context[Params]) -> None:
# `episode.start_s` and `episode.end_s` are seconds on the recording's timeline.
long_enough = episode.duration_s >= ctx.params.min_s
out.result(
"episode_duration",
"pass" if long_enough else "fail",
value=episode.duration_s,
threshold=ctx.params.min_s,
message=f"{episode.duration_s:.2f} s",
at={"startS": episode.start_s, "endS": episode.end_s},
)
has_task = bool(episode.task and episode.task.strip())
out.result("episode_task", "pass" if has_task else "warn", message=episode.task or "no task text")它的测试是 packages/sdk-python/examples/v1/check-episode-duration/test_main.py:用一份很小的测试数据,通过真实的运行器运行这段代码,并检查写出的值。想从它开始,运行 embodiflow algorithm init my-algorithm --category check --level episode,它会写出 main.py 和 test_main.py;再运行 embodiflow algorithm test my-algorithm(需要装有 pytest 和 SDK 的 Python)。
值得知道
想让质检停止这条录制,就在工作流里给它的步骤设置 onFail: stop。任何一条结果为 fail 时,这条录制后续都不再运行。原始文件质检最适合这样用。见在工作流中使用算法。
在任务片段质检里,recording 参数是该任务片段的时间窗口,episode 参数带有它的任务和标签。
团队质检的结果 id 保存时会加上算法名称作前缀,不会和内置 id 冲突。另外,当数据根本不归你的质检处理时,例如录制里没有 MCAP 文件,请调用 ctx.not_applicable(code, message),而不是报告失败。