pepijn223 HF Staff commited on
Commit
572730f
·
verified ·
1 Parent(s): 9021dbb

Keep trajectory comparisons in chat; reserve the visualizer for annotations

Browse files
README.md CHANGED
@@ -80,12 +80,8 @@ The Grabette reference comparison still showed 12–20 cm initial-frame position
80
  RMSE on those four clips (8–12 cm after rigid position alignment, without a scale
81
  fit). Its existing trajectories are SLAM-derived references. This is an
82
  experimental reconstruction, not validated robot pose ground truth.
83
- Open `/?trajectory=owner/output-dataset` to rotate the 3D trajectory overlay,
84
- choose an episode and inspect its error; private results use your Studio login.
85
- Add `&compare=owner/other-output-dataset` to overlay another run. Comparison
86
- requires identical source actions, frame indices and timestamps; both runs are
87
- shown against the source reference with scale fixed at one. The optional rigid
88
- alignment is a diagnostic full-trajectory fit and is labeled explicitly.
89
 
90
  **Hand actions** uses EgoHand with native SOMA/MHR hands and
91
  **DROID-W + MoGe Small** inside the existing annotation wizard. Choose a human-POV
 
80
  RMSE on those four clips (8–12 cm after rigid position alignment, without a scale
81
  fit). Its existing trajectories are SLAM-derived references. This is an
82
  experimental reconstruction, not validated robot pose ground truth.
83
+ Trajectory comparisons and calibration diagnostics are reviewed in chat.
84
+ The Space visualizer is dedicated to dataset annotations.
 
 
 
 
85
 
86
  **Hand actions** uses EgoHand with native SOMA/MHR hands and
87
  **DROID-W + MoGe Small** inside the existing annotation wizard. Choose a human-POV
frontend/src/JobsView.tsx CHANGED
@@ -47,7 +47,6 @@ export type Progress = {
47
  };
48
  annotation_url?: string | null;
49
  hand_visualizer_url?: string | null;
50
- slam_visualizer_url?: string | null;
51
  output_mode?: string;
52
  human_videos?: Record<string, number> | null;
53
  };
@@ -241,8 +240,7 @@ export default function JobsView({
241
  : null;
242
  const annotations = p?.annotation_url;
243
  const hands = p?.hand_visualizer_url;
244
- const slam = p?.slam_visualizer_url;
245
- const action = slam ? "View 3D trajectories" : hands
246
  ? "View 3D hands"
247
  : annotations
248
  ? "Show annotations"
@@ -313,7 +311,7 @@ export default function JobsView({
313
  </span>
314
  <span className="compact-job-action">
315
  <a
316
- href={slam || hands || annotations || results || logs}
317
  target="_blank"
318
  rel="noreferrer"
319
  onClick={(e) => e.stopPropagation()}
 
47
  };
48
  annotation_url?: string | null;
49
  hand_visualizer_url?: string | null;
 
50
  output_mode?: string;
51
  human_videos?: Record<string, number> | null;
52
  };
 
240
  : null;
241
  const annotations = p?.annotation_url;
242
  const hands = p?.hand_visualizer_url;
243
+ const action = hands
 
244
  ? "View 3D hands"
245
  : annotations
246
  ? "Show annotations"
 
311
  </span>
312
  <span className="compact-job-action">
313
  <a
314
+ href={hands || annotations || results || logs}
315
  target="_blank"
316
  rel="noreferrer"
317
  onClick={(e) => e.stopPropagation()}
frontend/src/TrajectoryPreview.tsx DELETED
@@ -1,76 +0,0 @@
1
- import {useEffect, useState} from "react";
2
- import "./trajectory.css";
3
-
4
- type Point = [number,number,number];
5
- type Track = {dataset:string;predicted:Point[];rigid_aligned:Point[]|null;rmse:{initial_mm:number;rigid_mm:number}|null};
6
- type Trajectory = Track & {episode:number;total_episodes:number;frames:number;fps:number;frame_indices:number[];reference:Point[]|null;reference_note:string;comparison?:Track};
7
-
8
- export default function TrajectoryPreview({dataset}:{dataset:string}) {
9
- const [data,setData]=useState<Trajectory|null>(null);
10
- const [episode,setEpisode]=useState(0);
11
- const [error,setError]=useState("");
12
- const [angle,setAngle]=useState([-.65,.45]);
13
- const [zoom,setZoom]=useState(1);
14
- const [aligned,setAligned]=useState(false);
15
- const [frame,setFrame]=useState(0);
16
- const [compare,setCompare]=useState(new URLSearchParams(window.location.search).get("compare") || "");
17
- const [compareDraft,setCompareDraft]=useState(compare);
18
- useEffect(()=>{
19
- let active=true;
20
- setData(null);setError("");
21
- fetch(`/api/slam-trajectory?dataset=${encodeURIComponent(dataset)}&episode=${episode}${compare?`&compare=${encodeURIComponent(compare)}`:""}`,{credentials:"same-origin"})
22
- .then(async r=>{const d=await r.json();if(!r.ok)throw new Error(d.detail||"Could not load trajectory");return d as Trajectory;})
23
- .then(d=>{if(active){setData(d);setFrame(d.predicted.length-1);}})
24
- .catch(e=>{if(active)setError(e.message);});
25
- return ()=>{active=false;};
26
- },[dataset,episode,compare]);
27
- const prediction=data && aligned && data.rigid_aligned ? data.rigid_aligned : data?.predicted;
28
- const comparison=data?.comparison && aligned && data.comparison.rigid_aligned ? data.comparison.rigid_aligned : data?.comparison?.predicted;
29
- const points=[...(prediction||[]),...(comparison||[]),...(data?.reference||[])];
30
- const center=[0,1,2].map(axis=>points.length ? points.reduce((s,p)=>s+p[axis],0)/points.length : 0);
31
- const extent=Math.max(.05,...points.flatMap(p=>p.map((v,i)=>Math.abs(v-center[i]))));
32
- const scale=165/extent*zoom;
33
- function project(p:number[]):[number,number] {
34
- const [x,y,z]=p.map((v,i)=>v-center[i]);
35
- const a=x*Math.cos(angle[0])+z*Math.sin(angle[0]);
36
- const b=-x*Math.sin(angle[0])+z*Math.cos(angle[0]);
37
- return [350+a*scale,240+(y*Math.cos(angle[1])-b*Math.sin(angle[1]))*scale];
38
- }
39
- const path=(values:Point[])=>values.slice(0,frame+1).map((p,i)=>`${i?"L":"M"}${project(p).join(",")}`).join(" ");
40
- return <main className="trajectory-preview">
41
- <header><a href="/">← Annotation Studio</a><a href={`/auth/login?next=${encodeURIComponent(window.location.pathname+window.location.search)}`}>Sign in for private datasets</a></header>
42
- <h1>DROID-W and dataset trajectories</h1><p className="field-help">{dataset}</p>
43
- <form className="trajectory-controls" onSubmit={e=>{e.preventDefault();setCompare(compareDraft.trim());const url=new URL(window.location.href);if(compareDraft.trim())url.searchParams.set("compare",compareDraft.trim());else url.searchParams.delete("compare");window.history.replaceState(null,"",url);}}>
44
- <label>Compare another run of the same episodes<input type="text" value={compareDraft} placeholder="owner/dataset-without-intrinsics" onChange={e=>setCompareDraft(e.target.value)}/></label><button type="submit">Compare in 3D</button>
45
- </form>
46
- <div className="trajectory-controls">
47
- <label>Episode <input type="number" min={0} max={data ? data.total_episodes-1 : undefined} value={episode} onChange={e=>setEpisode(Math.max(0,Number(e.target.value)))}/></label>
48
- <label>Alignment <select value={aligned?"rigid":"initial"} onChange={e=>setAligned(e.target.value==="rigid")}><option value="initial">Initial camera frames</option><option value="rigid">Rigid position alignment · no scale fit</option></select></label>
49
- <label>Zoom <input type="range" min="0.5" max="3" step="0.05" value={zoom} onChange={e=>setZoom(Number(e.target.value))}/></label>
50
- </div>
51
- {error && <p role="alert" className="message error">{error}</p>}
52
- {!data && !error && <p role="status">Loading 3D trajectories…</p>}
53
- {data && <>
54
- <div className="trajectory-legend"><span className="trajectory-droid">━━ {compare?"Selected run":"DROID-W"}</span>{comparison && <span className="trajectory-comparison">━ · Comparison run</span>}{data.reference && <span className="trajectory-reference">┄┄ Dataset reference</span>}<span>Metres · X right · Y down · Z forward</span></div>
55
- {data.comparison && <p className="field-help">Comparison: {data.comparison.dataset}</p>}
56
- <svg className="trajectory-canvas" viewBox="0 0 700 480" role="img" aria-label="Interactive 3D camera trajectory overlay; drag to rotate"
57
- onPointerDown={e=>e.currentTarget.setPointerCapture(e.pointerId)}
58
- onPointerMove={e=>{if(e.buttons)setAngle(a=>[a[0]+e.movementX*.008,Math.max(-1.4,Math.min(1.4,a[1]+e.movementY*.008))]);}}>
59
- <title>DROID-W and source dataset camera trajectories</title>
60
- {([0,1,2] as const).map(axis=>{
61
- const p=[...center];p[axis]+=extent;
62
- const a=project(center),b=project(p);
63
- return <g key={axis}><line x1={a[0]} y1={a[1]} x2={b[0]} y2={b[1]} className="trajectory-axis"/><text x={b[0]+5} y={b[1]-5}>{["X","Y","Z"][axis]} {extent.toFixed(2)} m</text></g>;
64
- })}
65
- {data.reference && <path d={path(data.reference)} className="trajectory-reference-line"/>}
66
- {comparison && <path d={path(comparison)} className="trajectory-comparison-line"/>}
67
- {prediction && <path d={path(prediction)} className="trajectory-droid-line"/>}
68
- {prediction && <circle cx={project(prediction[Math.min(frame,prediction.length-1)])[0]} cy={project(prediction[Math.min(frame,prediction.length-1)])[1]} r="4" className="trajectory-current"/>}
69
- </svg>
70
- <label className="trajectory-frame">Frame {data.frame_indices[frame]} / {data.frames-1}<input type="range" min="0" max={data.predicted.length-1} value={frame} onChange={e=>setFrame(Number(e.target.value))}/></label>
71
- <p aria-live="polite">{data.rmse ? `Position RMSE: ${(aligned?data.rmse.rigid_mm:data.rmse.initial_mm).toFixed(1)} mm · scale fixed at 1` : "No compatible Cartesian reference pose column in this dataset."}</p>
72
- {data.comparison?.rmse && <p>Comparison RMSE: {(aligned?data.comparison.rmse.rigid_mm:data.comparison.rmse.initial_mm).toFixed(1)} mm · scale fixed at 1</p>}
73
- <p className="field-help">Drag to rotate. {data.reference_note}</p>
74
- </>}
75
- </main>;
76
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/main.tsx CHANGED
@@ -39,7 +39,6 @@ import "./style.css";
39
  import "./wizard.css";
40
  import AnnotationExamples from "./AnnotationExamples";
41
  import SlamSettings, { defaultSlam, type SlamOptions } from "./SlamSettings";
42
- import TrajectoryPreview from "./TrajectoryPreview";
43
  import JobsView, { type Job, type Progress } from "./JobsView";
44
  import HandActionSettings, {
45
  HandPipelineExplanation,
@@ -2211,5 +2210,4 @@ function App() {
2211
  );
2212
  }
2213
 
2214
- const trajectoryDataset = new URLSearchParams(window.location.search).get("trajectory");
2215
- createRoot(document.getElementById("root")!).render(trajectoryDataset ? <TrajectoryPreview dataset={trajectoryDataset}/> : <App />);
 
39
  import "./wizard.css";
40
  import AnnotationExamples from "./AnnotationExamples";
41
  import SlamSettings, { defaultSlam, type SlamOptions } from "./SlamSettings";
 
42
  import JobsView, { type Job, type Progress } from "./JobsView";
43
  import HandActionSettings, {
44
  HandPipelineExplanation,
 
2210
  );
2211
  }
2212
 
2213
+ createRoot(document.getElementById("root")!).render(<App />);
 
frontend/src/trajectory.css DELETED
@@ -1,21 +0,0 @@
1
- .trajectory-preview { max-width: 1000px; margin: auto; padding: 28px; }
2
- .trajectory-preview header,.trajectory-controls,.trajectory-legend { display: flex; flex-wrap: wrap; align-items: center; gap: 20px; margin-bottom: 24px; }
3
- .trajectory-preview header { justify-content: space-between; }
4
- .trajectory-controls label { display: flex; flex-direction: column; gap: 6px; }
5
- .trajectory-controls input[type=number] { width: 100px; }
6
- .trajectory-preview select,.trajectory-preview input { padding: 8px; color: var(--fg); background: var(--surface-0); border: 1px solid var(--line); border-radius: 5px; }
7
- .trajectory-canvas { display: block; width: 100%; height: auto; touch-action: none; cursor: grab; background: var(--surface-0); }
8
- .trajectory-canvas:active { cursor: grabbing; }
9
- .trajectory-canvas text { fill: var(--fg-muted); font-size: 12px; }
10
- .trajectory-axis { stroke: var(--line-strong); stroke-width: 1; }
11
- .trajectory-reference-line { stroke: var(--green); stroke-width: 2; stroke-dasharray: 6 3; fill: none; }
12
- .trajectory-droid-line { stroke: var(--accent); stroke-width: 2; fill: none; }
13
- .trajectory-comparison-line { stroke: var(--fg); stroke-width: 2; stroke-dasharray: 9 3 2 3; fill: none; }
14
- .trajectory-comparison { color: var(--fg); }
15
- .trajectory-controls input[type=text] { width: min(70vw, 440px); }
16
- .trajectory-droid { color: var(--accent); }
17
- .trajectory-reference { color: var(--green); }
18
- .trajectory-current { fill: var(--accent); }
19
- .trajectory-frame { display: flex; align-items: center; gap: 16px; margin-top: 20px; }
20
- .trajectory-frame input { flex: 1; }
21
- @media(max-width:480px) { .trajectory-preview {padding:16px;} .trajectory-controls {align-items:stretch;flex-direction:column;} .trajectory-frame {flex-wrap:wrap;} }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
server.py CHANGED
@@ -24,7 +24,6 @@ from prompt_config import public_catalog, validate_prompts
24
  from pydantic import BaseModel, Field
25
  from reporting import annotation_url, episode_progress, hand_visualizer_url, usage_costs
26
  from slam_pipeline import catalog as slam_catalog
27
- from slam_view import trajectory_data
28
  from studio import COPY, FEATURES, launch, prepare, read_cameras
29
 
30
  app = FastAPI(title="LeRobot Annotation Studio", docs_url=None, redoc_url=None, openapi_url=None)
@@ -183,10 +182,6 @@ def login(request: Request):
183
  if not client_id or not HOST:
184
  return RedirectResponse("/?error=local-preview")
185
  sid, current = create_session()
186
- destination = request.query_params.get("next", "/")
187
- current["auth_return"] = (
188
- destination if destination.startswith("/?trajectory=") and len(destination) < 1024 else "/"
189
- )
190
  current["oauth_state"] = secrets.token_urlsafe(32)
191
  current["verifier"] = secrets.token_urlsafe(64)
192
  current["auth_started"] = time.time()
@@ -220,7 +215,6 @@ def callback(request: Request, code: str = "", state: str = "", error: str = "")
220
  ):
221
  return RedirectResponse("/?error=sign-in")
222
  verifier = current.pop("verifier", "")
223
- destination = current.pop("auth_return", "/")
224
  current.pop("oauth_state", None)
225
  try:
226
  result = requests.post(
@@ -248,7 +242,7 @@ def callback(request: Request, code: str = "", state: str = "", error: str = "")
248
  user={"name": user["name"], "fullname": user.get("fullname") or user["name"]},
249
  expires=time.time() + min(int(data.get("expires_in", 86400)), 86400),
250
  )
251
- return cookie(RedirectResponse(destination), sid)
252
 
253
 
254
  @app.post("/auth/logout")
@@ -350,19 +344,6 @@ def slam_pipeline():
350
  return slam_catalog()
351
 
352
 
353
- @app.get("/api/slam-trajectory")
354
- def slam_trajectory(request: Request, dataset: str, episode: int = 0, compare: str | None = None):
355
- current = session(request) or {}
356
- if episode < 0 or not ESTIMATES.acquire(blocking=False):
357
- raise HTTPException(400, "Choose a valid episode, or retry when other previews finish")
358
- try:
359
- return trajectory_data(dataset, episode, current.get("token", False), compare_dataset=compare)
360
- except Exception as exc:
361
- raise HTTPException(400, safe_error(exc)) from None
362
- finally:
363
- ESTIMATES.release()
364
-
365
-
366
  @app.post("/api/prompts/validate")
367
  def check_prompts(options: PromptInput, request: Request):
368
  check_origin(request)
@@ -484,9 +465,6 @@ def progress(job_id: str, request: Request):
484
  "cost": usage_costs(job, report),
485
  "annotation_url": annotation_url(report),
486
  "hand_visualizer_url": hand_visualizer_url(report),
487
- "slam_visualizer_url": "/?" + urlencode({"trajectory": output})
488
- if report.get("manifest", {}).get("slam")
489
- else None,
490
  "output_mode": report.get("manifest", {}).get("mode"),
491
  "model": report.get("manifest", {}).get("model"),
492
  "captioner": report.get("manifest", {}).get("captioner"),
 
24
  from pydantic import BaseModel, Field
25
  from reporting import annotation_url, episode_progress, hand_visualizer_url, usage_costs
26
  from slam_pipeline import catalog as slam_catalog
 
27
  from studio import COPY, FEATURES, launch, prepare, read_cameras
28
 
29
  app = FastAPI(title="LeRobot Annotation Studio", docs_url=None, redoc_url=None, openapi_url=None)
 
182
  if not client_id or not HOST:
183
  return RedirectResponse("/?error=local-preview")
184
  sid, current = create_session()
 
 
 
 
185
  current["oauth_state"] = secrets.token_urlsafe(32)
186
  current["verifier"] = secrets.token_urlsafe(64)
187
  current["auth_started"] = time.time()
 
215
  ):
216
  return RedirectResponse("/?error=sign-in")
217
  verifier = current.pop("verifier", "")
 
218
  current.pop("oauth_state", None)
219
  try:
220
  result = requests.post(
 
242
  user={"name": user["name"], "fullname": user.get("fullname") or user["name"]},
243
  expires=time.time() + min(int(data.get("expires_in", 86400)), 86400),
244
  )
245
+ return cookie(RedirectResponse("/"), sid)
246
 
247
 
248
  @app.post("/auth/logout")
 
344
  return slam_catalog()
345
 
346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  @app.post("/api/prompts/validate")
348
  def check_prompts(options: PromptInput, request: Request):
349
  check_origin(request)
 
465
  "cost": usage_costs(job, report),
466
  "annotation_url": annotation_url(report),
467
  "hand_visualizer_url": hand_visualizer_url(report),
 
 
 
468
  "output_mode": report.get("manifest", {}).get("mode"),
469
  "model": report.get("manifest", {}).get("model"),
470
  "captioner": report.get("manifest", {}).get("captioner"),
slam-pipeline.json CHANGED
@@ -3,12 +3,11 @@
3
  "coordinate_frame": "initial optical camera; X right, Y down, Z forward",
4
  "expected_code": {
5
  "grabette-camera.json": "9ce2f5b0211184faafbe549b1b60d70ebea66c5459682d49f55ae5e69e4c8532",
6
- "server.py": "b7a1f176054468b9180eeb7bcd6482676870cf3838e79ae49a61dca492ef386f",
7
  "slam_job.py": "25449ada3483cd2ba6851db334b1614df2809a7ae1640a7788ad218a05fd5cd6",
8
  "slam_native.py": "2199bf30d0a06583e25493149663edcdc5869e94d32b900f5db3f9b94912ee62",
9
  "slam_parallel.py": "254bf2e4f3e1872d34413b1c6c467b82aafbd975eed3f67e89bb699bc81b582b",
10
- "slam_pipeline.py": "4ac5fe3e0a54e18065f718eda2db68b3f594d7384b8073675e5b92f52ccc3962",
11
- "slam_view.py": "32092424288d07e3be2308b789d4a1e93d15a42e6120aee842539de2dc898968",
12
  "studio.py": "ffe3cddf755f4999c7e5d5f790805c74fbb37aa93eed24d777ca8dda64accb41",
13
  "subset.py": "c32c62ca23155c602a4c92e67a76613bc2c4dec4ddf9dd4dc554600bb99034d3"
14
  },
 
3
  "coordinate_frame": "initial optical camera; X right, Y down, Z forward",
4
  "expected_code": {
5
  "grabette-camera.json": "9ce2f5b0211184faafbe549b1b60d70ebea66c5459682d49f55ae5e69e4c8532",
6
+ "server.py": "5322b54c4002959b8b28251f4997c299d3921e7663680d8819bb577a181698b9",
7
  "slam_job.py": "25449ada3483cd2ba6851db334b1614df2809a7ae1640a7788ad218a05fd5cd6",
8
  "slam_native.py": "2199bf30d0a06583e25493149663edcdc5869e94d32b900f5db3f9b94912ee62",
9
  "slam_parallel.py": "254bf2e4f3e1872d34413b1c6c467b82aafbd975eed3f67e89bb699bc81b582b",
10
+ "slam_pipeline.py": "8b3b1777519ccaa90f9f4613060f5c634092566e7fda4d085d1675f61308243d",
 
11
  "studio.py": "ffe3cddf755f4999c7e5d5f790805c74fbb37aa93eed24d777ca8dda64accb41",
12
  "subset.py": "c32c62ca23155c602a4c92e67a76613bc2c4dec4ddf9dd4dc554600bb99034d3"
13
  },
slam_pipeline.py CHANGED
@@ -13,7 +13,6 @@ FILES = {
13
  "slam_pipeline.py",
14
  "slam_job.py",
15
  "slam_native.py",
16
- "slam_view.py",
17
  "slam_parallel.py",
18
  "server.py",
19
  "studio.py",
 
13
  "slam_pipeline.py",
14
  "slam_job.py",
15
  "slam_native.py",
 
16
  "slam_parallel.py",
17
  "server.py",
18
  "studio.py",
slam_view.py DELETED
@@ -1,95 +0,0 @@
1
- """Numerical, permission-checked trajectory previews for saved SLAM datasets."""
2
-
3
- import hashlib
4
- import json
5
-
6
- import numpy as np
7
- import pyarrow.parquet as pq
8
- from huggingface_hub import hf_hub_download
9
-
10
-
11
- def trajectory_data(dataset, episode, token=False, compare_dataset=None):
12
- """Return bounded 3D paths; never expose the caller's Hub credentials."""
13
- from studio import read_selection, relative_path
14
-
15
- selection = read_selection(dataset, episode, 1, token=token)
16
- row, info = selection["episodes"][0], selection["info"]
17
- if int(row["episode_index"]) != episode:
18
- raise ValueError("The selected episode does not exist")
19
- name = relative_path(
20
- info["data_path"].format(
21
- chunk_index=int(row["data/chunk_index"]), file_index=int(row["data/file_index"])
22
- )
23
- )
24
- path = hf_hub_download(dataset, name, repo_type="dataset", revision=selection["revision"], token=token)
25
- table = pq.read_table(path, filters=[("episode_index", "=", episode)])
26
- column = "annotation.camera_initial_from_optical"
27
- if column not in table.column_names:
28
- raise ValueError("This dataset has no saved standalone SLAM camera poses")
29
- predicted = np.asarray(table[column].to_pylist(), dtype=float)
30
- if predicted.shape != (len(table), 4, 4) or not np.isfinite(predicted).all():
31
- raise ValueError("Invalid saved camera trajectory")
32
- predicted = np.linalg.inv(predicted[0]) @ predicted
33
- reference, aligned, rmse = None, None, None
34
- names = info["features"].get("action", {}).get("names", [])
35
- if names[:9] == ["x", "y", "z", *[f"r6d_{i}" for i in range(6)]]:
36
- values = np.asarray(table["action"].to_pylist(), dtype=float)
37
- a, b = values[:, 3:6], values[:, 6:9]
38
- a = a / np.linalg.norm(a, axis=1)[:, None]
39
- b = b - (a * b).sum(1)[:, None] * a
40
- b = b / np.linalg.norm(b, axis=1)[:, None]
41
- rotation = np.stack([a, b, np.cross(a, b)], axis=1)
42
- reference = (values[:, :3] - values[0, :3]) @ rotation[0]
43
- valid = np.isfinite(reference).all(axis=1)
44
- if "is_lost" in table.column_names:
45
- valid &= np.asarray(table["is_lost"].to_pylist()) < 0.5
46
- if valid.sum() >= 8:
47
- p, r = predicted[:, :3, 3], reference
48
- p0, r0 = p[valid].mean(0), r[valid].mean(0)
49
- u, _, vt = np.linalg.svd((p[valid] - p0).T @ (r[valid] - r0))
50
- transform = vt.T @ np.diag([1.0, 1.0, np.linalg.det(vt.T @ u.T)]) @ u.T
51
- aligned = (p - p0) @ transform.T + r0
52
- rmse = {
53
- "initial_mm": float(1000 * np.sqrt(np.mean(np.sum((p[valid] - r[valid]) ** 2, axis=1)))),
54
- "rigid_mm": float(1000 * np.sqrt(np.mean(np.sum((aligned[valid] - r[valid]) ** 2, axis=1)))),
55
- }
56
- keep = np.unique(np.rint(np.linspace(0, len(table) - 1, min(2000, len(table)))).astype(int))
57
- fingerprint = hashlib.sha256(
58
- json.dumps(
59
- {
60
- name: table[name].to_pylist()
61
- for name in ("action", "timestamp", "frame_index")
62
- if name in table.column_names
63
- },
64
- allow_nan=False,
65
- ).encode()
66
- ).hexdigest()
67
- result = {
68
- "dataset": dataset,
69
- "revision": selection["revision"],
70
- "episode": episode,
71
- "total_episodes": info["total_episodes"],
72
- "frames": len(table),
73
- "fps": info["fps"],
74
- "frame_indices": keep.tolist(),
75
- "predicted": predicted[keep, :3, 3].tolist(),
76
- "reference": reference[keep].tolist() if reference is not None else None,
77
- "rigid_aligned": aligned[keep].tolist() if aligned is not None else None,
78
- "rmse": rmse,
79
- "scale_fitted": False,
80
- "source_fingerprint": fingerprint,
81
- "reference_note": "Existing dataset pose estimates, normalized to their first pose. Camera mounting and metric accuracy are not established by this overlay.",
82
- }
83
- if compare_dataset:
84
- other = trajectory_data(compare_dataset, episode, token=token)
85
- if (
86
- reference is None
87
- or other["reference"] is None
88
- or fingerprint != other["source_fingerprint"]
89
- or info["fps"] != other["fps"]
90
- ):
91
- raise ValueError("Compare matching source episodes with identical actions, frames and timestamps")
92
- result["comparison"] = {
93
- key: other[key] for key in ("dataset", "revision", "predicted", "rigid_aligned", "rmse")
94
- }
95
- return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_slam.py CHANGED
@@ -114,58 +114,3 @@ def test_grabette_preset_scales_fisheye_without_changing_source():
114
  assert result["calibration"]["revision"] == "48b9a5c3706c197954b2cd9502b036b87d9e9abb"
115
  with pytest.raises(ValueError, match="aspect ratio"):
116
  grabette_calibration({"shape": [360, 640, 3]})
117
-
118
-
119
- def test_saved_3d_preview_retains_scale_error(tmp_path):
120
- from slam_view import trajectory_data
121
-
122
- count = 20
123
- poses = np.tile(np.eye(4), (count, 1, 1))
124
- poses[:, 0, 3] = np.arange(count) * 0.02
125
- actions = [[i * 0.01, 0, 0, 1, 0, 0, 0, 1, 0] for i in range(count)]
126
- path = tmp_path / "episode.parquet"
127
- import pyarrow.parquet as pq
128
-
129
- pq.write_table(pa.table({"episode_index": [0] * count, POSE: poses.tolist(), "action": actions}), path)
130
- selection = {
131
- "revision": "a" * 40,
132
- "episodes": [{"episode_index": 0, "data/chunk_index": 0, "data/file_index": 0}],
133
- "info": {
134
- "data_path": "data/{file_index}.parquet",
135
- "fps": 50,
136
- "total_episodes": 1,
137
- "features": {"action": {"names": ["x", "y", "z", *[f"r6d_{i}" for i in range(6)]]}},
138
- },
139
- }
140
- with (
141
- patch("studio.read_selection", return_value=selection),
142
- patch("slam_view.hf_hub_download", return_value=str(path)) as download,
143
- ):
144
- result = trajectory_data("alice/output", 0, token=False)
145
- assert result["scale_fitted"] is False
146
- assert result["rmse"]["rigid_mm"] > 50
147
- assert download.call_args.kwargs["token"] is False
148
- with (
149
- patch("studio.read_selection", return_value=selection),
150
- patch("slam_view.hf_hub_download", return_value=str(path)),
151
- ):
152
- overlay = trajectory_data("alice/calibrated", 0, token=False, compare_dataset="alice/baseline")
153
- assert overlay["comparison"]["dataset"] == "alice/baseline"
154
- assert overlay["comparison"]["predicted"] == overlay["predicted"]
155
- changed = tmp_path / "different-episode.parquet"
156
- pq.write_table(
157
- pa.table(
158
- {
159
- "episode_index": [0] * count,
160
- POSE: poses.tolist(),
161
- "action": [[v + 0.1 for v in a] for a in actions],
162
- }
163
- ),
164
- changed,
165
- )
166
- with (
167
- patch("studio.read_selection", return_value=selection),
168
- patch("slam_view.hf_hub_download", side_effect=[str(path), str(changed)]),
169
- ):
170
- with pytest.raises(ValueError, match="matching source episodes"):
171
- trajectory_data("alice/calibrated", 0, compare_dataset="alice/different")
 
114
  assert result["calibration"]["revision"] == "48b9a5c3706c197954b2cd9502b036b87d9e9abb"
115
  with pytest.raises(ValueError, match="aspect ratio"):
116
  grabette_calibration({"shape": [360, 640, 3]})