Skip to main content

Playground · research instrument

On-device AI

SLAM Odometry Arena

EKF visual-inertial odometry in the browser: unicycle robot, IMU drift, range-bearing features, covariance ellipsoids, sparse 3-D point cloud, and bag-of-words loop closure that snaps the map with a spatial audio ping and viewport flash. Tune noise, watch ATE grow, close the loop.

Independent research instrument — not claimed as MakerPortal shipped product code. Methods, equations, assumptions, and limitations are disclosed so you can inspect what the page does and does not establish.

Odometry & Sensing

Motion model — IMU / wheel noise

Platform

True pose
Est pose μ
ATE RMSE m
tr(P_rr) cov
Visible lm
Loop closures0
Map size N
BoW similarity
0.12thr 0.72

Robot drives a noisy unicycle circle. EKF predicts with noisy v,w then updates with range-bearing landmarks. Uncertainty ellipsoids grow without loop closure — enable it and return to start to hear snap and see map correction.

Top-down arena — trajectory, landmarks, ellipsoids, loop edge

true path EKF estimate loop snapdashed: odom only

Sparse 3-D point cloud — pinhole projection from robot frame

Size ∝ 1/depth, color by visual word. Height jitter = stereo reconstruction noise.

Live histogram BoW

Visual words = 32 bins quantized by position hash. Loop when cosine similarity spikes as you revisit.

Loop dist m
Drift before snap
FOV count
Time s
0.0

Anatomy of the arena

EKF SLAM pipeline

  1. State: μ ∈ R^(3+2N) = [x,y,θ, m1x,m1y…]. P covariance (3+2N)² initialized P_rr=0.01·I, P_mm=800·I for unknown landmarks.
  2. True world: N visual features uniformly in [-11,11]² with random height 0.3-2.4 m for cloud projection, descriptor = hash of cell + random BoW word in 0..31.
  3. Motion: unicycle v, ω. True integrates v, ω. Filter integrates v̂=v+𝒩(0,σ_m²), ω̂=ω+𝒩(0,(σ_m/2)²). Jacobian G = ∂f/∂[x,y,θ] = [[1,0,-v dt sinθ],[0,1,v dt cosθ],[0,0,1]]. P_rm = G P_rm.
  4. Observation: range r=√(dx²+dy²), bearing φ=atan2(dy,dx)−θ. ẑ = z_true+𝒩(0, diag(σ_o², (σ_o·0.6)²)). Only landmarks within range and |φ| < 95° generate measurement.
  5. Init: first sight sets μ_landmark = [x+ r cos(θ+φ), y+ r sin(θ+φ)], covariance clamped to 1.2, seen=true. Subsequent sights run sparse EKF update.
  6. Visualization: top-down shows true gray dots, estimated orange; ellipsoids 95% from marginal covariance. Robot triangle with σ ellipse. Odom-only (dead reckoning) dashed line diverges.

Motion Jacobian

Gt=[10vΔtsinθ01vΔtcosθ001]G_t=\begin{bmatrix}1&0&-v\Delta t\sin\theta\\0&1&v\Delta t\cos\theta\\0&0&1\end{bmatrix}

Cross-covariance propagates by P_rm ← G P_rm keeping landmark correlations.

Loop closure, point cloud, audio

  • Keyframes every ~1.1 m or 18° rotation: store pose, BoW 32-histogram, landmark IDs seen. History up to 90 frames.
  • BoW similarity: cosine sim = (h1·h2)/(‖h1‖‖h2‖). Candidate if sim>0.72, metric distance of estimated trajectory loop <1.8 m, travelled >12 m, and not same recent frame.
  • Correction: pose graph snap Δp=μ_kframe−μ_curr, applied 0.85 gain to μ_robot and shared landmarks shifted by Δp·0.7 averages (emulating BA). P_rr ×0.28 and P cross scaled 0.7 mimics information matrix boost.
  • Feedback: flash overlay α flashTimer, canvas CSS translate jitter proportional to drift energy, loop edge green line, ATE drop in readout.
  • Cloud projection: p_cam = R(-θ)([mₓ−x, mᵧ−y, h_z]ᵀ). u = cₓ + fₓ·pₓ/p_z, v = cᵧ − f_y·h_z/p_z, size ∝ 1/(p_z+0.2). Height noise simulated, size encodes depth uncertainty.
  • Spatial audio: HRTF PannerNode. Pos = loop vector rotated into listener frame. Snap = 880 Hz sine ×exp(-18t) + 2.6 kHz pluck + 2-6 kHz noise burst 80 ms. Gain = 0.22 + 0.18·drift_before.

Observation Jacobian (one landmark)

H=[Hrobot  0  Hlm  0],  Hr=[dx/rdy/r0dy/qdx/q1],  Hlm=[dx/rdy/rdy/qdx/q]H = [H_{robot}\;0\;H_{lm}\;0],\;H_r=\begin{bmatrix}-dx/r&-dy/r&0\\dy/q&-dx/q&-1\end{bmatrix},\;H_{lm}=\begin{bmatrix}dx/r&dy/r\\-dy/q&dx/q\end{bmatrix}

q=dx²+dy², r=√q. Sparse so S is 2×2 only.

The math and physics, in full

EKF Predict

μtt1=f(μt1,ut),Ptt1=FtPt1FtT+Rt\mu_{t|t-1}=f(\mu_{t-1},u_t),\quad P_{t|t-1}=F_t P_{t-1}F_t^T+R_t

F_t = diag(G_t, I_2N). R_t only affects robot 3x3 block: sigma_v², sigma_w².

Range-bearing

h(x)=[(mxx)2+(myy)2atan2(myy,mxx)θ]h(x)=\begin{bmatrix}\sqrt{(m_x-x)^2+(m_y-y)^2}\\ \operatorname{atan2}(m_y-y,m_x-x)-\theta\end{bmatrix}

EKF Update

S=HPHT+Q,  K=PHTS1,  μμ+K(zh(μ)),  P(IKH)PS=H P H^T+Q,\;K=P H^T S^{-1},\;\mu\gets\mu+K(z-h(\mu)),\;P\gets(I-KH)P

H sparse 2×(3+2N). S⁻¹ analytic 2×2. Innovation angle wrapped to (−π,π].

95% ellipse

λ1,2=a+c2±((ac)/2)2+b2,  axes=2.447λ\lambda_{1,2}=\frac{a+c}{2}\pm\sqrt{((a-c)/2)^2+b^2},\;axes=2.447\sqrt{\lambda}

Loop & BA

BoW sim=hchkhchk,if >0.72 candidate\text{BoW sim}= \frac{h_c\cdot h_k}{\|h_c\|\|h_k\|},\quad \text{if }>0.72\text{ candidate}JBA=i,jzijh(Ti,Xj)Σij2,Δ=(JTJ+λI)1JTrJ_{BA}=\sum_{i,j}\|z_{ij}-h(T_i,X_j)\|^2_{\Sigma_{ij}},\quad \Delta^* = -(J^TJ+\lambda I)^{-1}J^T r

Loop adds pose-pose constraint factor; BA optimizes over window. Filtering alone drifts O(√ distance).

Gear behind this build

SLAM stack · 11 picks

Hardware picks11

More gear across every app: the full Gear list →

Hardware Kit Builder

Build the physical hardware platform. Select your components below to generate a live, real-time bill of materials and build instructions.

Build this lab

SLAM / odometry edge stack

Jetson + SparkFun ZED-F9P RTK + ICM-20948 — outdoor pose and onboard mapping. ZED-F9P and ICM are SparkFun Originals (10%).

Prices shown were retrieved from the Amazon Product Advertising API on 19 July 2026 and are indicative only — the price and availability on Amazon at the time of purchase apply.

Estimated total

$530

Prices from Amazon catalog cache · may change

Open primary listing ↗

Kit Total

Buy ↗

Core solver — TypeScript

// EKF SLAM minimal core: unicycle predict + sparse range-bearing update
type State = { mu: number[]; P: number[][]; seen: boolean[]; };

function predict(mu: number[], P: number[][], v: number, w: number, dt: number, q: number[]) {
  const th = mu[2];
  const G = [[1,0,-v*dt*Math.sin(th)],[0,1,v*dt*Math.cos(th)],[0,0,1]];
  // robot
  mu[0] += v*dt*Math.cos(th);
  mu[1] += v*dt*Math.sin(th);
  mu[2] += w*dt;
  // P_rr = G P_rr G^T + Q
  const Prr = [[P[0][0],P[0][1],P[0][2]],[P[1][0],P[1][1],P[1][2]],[P[2][0],P[2][1],P[2][2]]];
  const GP = matMul(G, Prr);
  const GPGT = matMul(GP, transpose(G));
  addDiag(GPGT, q); // Q = diag(motionNoise)
  // copy back + cross terms P_rm = G P_rm
}

function updateLandmark(mu: number[], P: number[][], zid: number, z: [number,number], R: number[][]){
  const rx=mu[0], ry=mu[1], rt=mu[2];
  const lix = 3+zid*2;
  const dx = mu[lix]-rx, dy = mu[lix+1]-ry;
  const q = dx*dx+dy*dy, r = Math.sqrt(q);
  const h = [r, Math.atan2(dy,dx)-rt];
  const Hr = [[-dx/r, -dy/r, 0],[ dy/q, -dx/q, -1]];
  const Hl = [[ dx/r,  dy/r],[-dy/q,  dx/q]];
  // sparse Jacobian 2x(3+2N) -> use only 5 columns
  // S = H P H^T + R (2x2), K = P H^T S^{-1}
  const innov = [z[0]-h[0], wrapAngle(z[1]-h[1])];
  // ... K, mu+=K*innov, P=(I-KH)P
  // loop closure: compare BoW histograms
}

function wrapAngle(a:number){ return Math.atan2(Math.sin(a), Math.cos(a)); }

Abridged: the Kalman gain, the covariance update and the loop-closure test are elided where the comments say so.

Export · Soft gate

Export EKF trajectory + landmarks

Robot poses (x,y,θ), covariance diag, loop-closure events as CSV — free watermarked, clean after email. Great for validating against RealSense D455 bag + ZED-F9P RTK ground truth.

File · slam-trajectory.csv

slam-trajectory.csvtext/csv+ watermark line on free path

Free download adds a small footer: # Export from makerportal.ai — free watermarked build. Unlock c…Clean export removes footer. Both are generated fresh from your current sim tuning.

Privacy: email stays in your browser localStorage (mp_export_email_slam-odometry-arena) + unlock flag (mp_export_unlock_slam-odometry-arena). If Buttondown username is configured, we also POST to Buttondown (privacy-first mode, no tracking pixels per D-014). See privacy → affiliates & email.

Unlock clean export

Soft gate — no hard paywall, no Clerk. Email stays local unless you explicitly check the newsletter box. Unsubscribe anytime. RSS at /rss.xml.

Export → Fab bonusAfter export, your tuned stackup can be ordered via PCBWay/JLCPCB CTA (when live) — see /privacy#affiliates for live merchants.

Frequently asked questions

What state does EKF-SLAM maintain?

μ = [x, y, θ, m1x, m1y, m2x, m2y, …]ᵀ dimension 3+2N. Covariance P is (3+2N)². Robot prediction uses differential-drive unicycle model, landmarks are static. Each range-bearing observation updates only 5 columns via sparse Jacobians, so S is 2×2 and K = P Hᵀ S⁻¹ can be formed from 5 columns of P.

How are uncertainty ellipses drawn?

For landmark j, marginal Σ = P[lm:lm+2, lm:lm+2] 2×2. Eigen-decomposition λ₁,₂ = (a+c)/2 ± √(((a−c)/2)²+b²) with orientation atan2(λ₁−a,b). Axes scaled by √χ²₂(0.95)=2.447 for 95% ellipse. Robot ellipse uses P[0:2,0:2]. Ellipse growth after long odometry without observation = drift visualization.

How is loop closure detected and corrected?

Keyframes store histograms of visual words (BoW): each landmark quantized descriptor hashed into 32-bin visual vocabulary. Current BoW compared via cosine similarity to historic keyframes (>0.72) plus metric proximity of true poses (<1.5m) and travelled distance >12m. On match, a pose-graph correction snaps μ_robot toward matched keyframe pose: Δ = 0.85·(μ_matched−μ_curr), applied to robot and to shared landmarks. P_rr scaled ×0.28, mimicking information gain from loop constraint.

Why bundle adjustment here vs filtering?

EKF is filtering: marginalizes past poses. Visual-inertial SLAM production systems run bundle adjustment minimizing ∑ ‖z−h(T_i, X_j)‖²_{Σ} over window of keyframes and 3-D points – a nonlinear least-squares solved by Levenberg-Marquardt with Schur complement. This arena shows filtering drift; toggling loop closure on/off lets you see filtering alone accumulates ATE ~ linear in distance, while loop closure resets error similar to global BA.

How is audio spatialized on snap?

Web Audio: AudioContext → PannerNode (HRTF, position derived from relative loop closure vector) → DynamicsCompressor. Snap sound is dual oscillator (880 Hz + 2.6 kHz) with exponential decay 0.25 s + filtered noise burst 2-6 kHz. Panner azimuth = atan2(dy,dx) − θ, elevation from point-cloud height. Flash viewport via CSS overlay alpha blending.

Shareable still

The instrument, captured—not illustrated.

This 16:9 frame is rendered from the real browser instrument above. It is the page's canonical preview for image search, link unfurls, and posts that need to show what the tool actually does.

Download 1280 × 720 JPEG
Visual-Inertial SLAM Arena — live MakerPortal instrument screenshot
Canonical capture · real UI · no generated scientific artwork