Keep trajectory comparisons in chat; reserve the visualizer for annotations
Browse files- README.md +2 -6
- frontend/src/JobsView.tsx +2 -4
- frontend/src/TrajectoryPreview.tsx +0 -76
- frontend/src/main.tsx +1 -3
- frontend/src/trajectory.css +0 -21
- server.py +1 -23
- slam-pipeline.json +2 -3
- slam_pipeline.py +0 -1
- slam_view.py +0 -95
- test_slam.py +0 -55
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 |
-
|
| 84 |
-
|
| 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
|
| 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={
|
| 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 |
-
|
| 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(
|
| 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": "
|
| 7 |
"slam_job.py": "25449ada3483cd2ba6851db334b1614df2809a7ae1640a7788ad218a05fd5cd6",
|
| 8 |
"slam_native.py": "2199bf30d0a06583e25493149663edcdc5869e94d32b900f5db3f9b94912ee62",
|
| 9 |
"slam_parallel.py": "254bf2e4f3e1872d34413b1c6c467b82aafbd975eed3f67e89bb699bc81b582b",
|
| 10 |
-
"slam_pipeline.py": "
|
| 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]})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|