Skip to content

Commit 8f14a05

Browse files
committed
docs: real-image hero GIF (real footage -> V-JEPA 2 -> surprise)
Replace the synthetic occupancy hero with one built from REAL photographs: a panned real-video clip fed to the real V-JEPA 2 model; surprise stays low while the camera pans and spikes when the scene cuts. The old imagination GIF moves into the RViz section where it belongs. - tools/gif: gen_data `hero` (downloads picsum/Unsplash real photos, picks the 3 most-different scenes, primes the clip buffer, runs V-JEPA 2) + render_hero.html + build.sh target. - tools/pages: hero.gif becomes the landing hero; imagination joins the gallery.
1 parent 37399cc commit 8f14a05

6 files changed

Lines changed: 131 additions & 10 deletions

File tree

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ using *existing* World Models in robotics and autonomous driving.**
1212
> score trajectories, visualize imagination in RViz, and export rosbag2 data to
1313
> robot-learning datasets.
1414
15-
![imagined future occupancy + risk](docs/imagination.gif)
15+
![real video through V-JEPA 2, surprise spikes on scene changes](docs/hero.gif)
1616

17-
<sub>Imagined `FutureOccupancy` (green = near, red = far) and action-conditioned
18-
`RiskScore` from one ROS 2 call — here the GPU-free `dummy` adapter.</sub>
17+
<sub>Real footage → the real **V-JEPA 2** video model on a GPU → **surprise**:
18+
the latent stays calm while the camera pans and spikes the instant the scene
19+
cuts. Actual model output on real photographs.</sub>
1920

2021
## Highlights
2122

@@ -113,7 +114,11 @@ risk readout on top — all from the GPU-free dummy model.
113114
ros2 launch world_model_viz imagination_demo.launch.py # rviz:=false on headless
114115
```
115116

116-
The viewer republishes `MarkerArray` on `/world_model_viz/imagination`.
117+
![imagined future occupancy + risk](docs/imagination.gif)
118+
119+
<sub>Top-down: a robot and an obstacle whose future occupancy the model predicts
120+
(green = soon → red = later), with the risk of the planned action. The viewer
121+
republishes `MarkerArray` on `/world_model_viz/imagination`.</sub>
117122

118123
### 4. See the imagination (Foxglove)
119124

docs/hero.gif

1.07 MB
Loading

tools/gif/build.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ FPS_imagination=12 ; FRAMES_imagination=32
1616
FPS_nav2=11 ; FRAMES_nav2=43
1717
FPS_ijepa=6 ; FRAMES_ijepa=18
1818
FPS_compare=6 ; FRAMES_compare=18
19+
FPS_hero=6 ; FRAMES_hero=24
1920
OUT_imagination=imagination.gif
2021
OUT_nav2=nav2_scoring.gif
2122
OUT_ijepa=ijepa_surprise.gif
2223
OUT_compare=jepa_compare.gif
24+
OUT_hero=hero.gif
2325

2426
build_one() {
2527
local kind="$1" work; work="$(mktemp -d)"
@@ -37,7 +39,7 @@ build_one() {
3739
}
3840

3941
case "${1:-all}" in
40-
all) build_one imagination; build_one nav2; build_one ijepa; build_one compare ;;
41-
imagination|nav2|ijepa|compare) build_one "$1" ;;
42-
*) echo "usage: $0 [imagination|nav2|ijepa|compare|all]"; exit 2 ;;
42+
all) build_one imagination; build_one nav2; build_one ijepa; build_one compare; build_one hero ;;
43+
imagination|nav2|ijepa|compare|hero) build_one "$1" ;;
44+
*) echo "usage: $0 [imagination|nav2|ijepa|compare|hero|all]"; exit 2 ;;
4345
esac

tools/gif/gen_data.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,67 @@ def gen_compare():
146146
return {"frames": out}
147147

148148

149+
def gen_hero():
150+
"""Hero GIF: REAL photographs panned into a video, fed to the real V-JEPA 2
151+
encoder; surprise spikes at scene cuts. Needs network (picsum.photos, fixed
152+
seeds, Unsplash-licensed) + a GPU. Output is the derived GIF, not the photos.
153+
"""
154+
import base64
155+
import io
156+
import urllib.request
157+
from PIL import Image
158+
159+
W, Hh, CROP, PAN = 512, 384, 384, 8
160+
candidates = ["forest", "ocean", "city", "desert", "snow", "market"]
161+
162+
def fetch(seed):
163+
url = f"https://picsum.photos/seed/{seed}/{W}/{Hh}"
164+
data = urllib.request.urlopen(url, timeout=30).read()
165+
return Image.open(io.BytesIO(data)).convert("RGB")
166+
167+
imgs = [fetch(s) for s in candidates]
168+
means = [np.asarray(im.resize((32, 32)), np.float32).reshape(-1, 3).mean(0) for im in imgs]
169+
# greedily pick the 3 most mutually-different scenes -> punchier cuts
170+
chosen = [0]
171+
while len(chosen) < 3:
172+
best, bestd = None, -1
173+
for i in range(len(imgs)):
174+
if i in chosen:
175+
continue
176+
d = min(float(np.linalg.norm(means[i] - means[c])) for c in chosen)
177+
if d > bestd:
178+
best, bestd = i, d
179+
chosen.append(best)
180+
181+
def crop(im, k):
182+
x = int((W - CROP) * k / (PAN - 1))
183+
return np.asarray(im.crop((x, 0, x + CROP, CROP)).resize((256, 256), Image.BILINEAR)).astype(np.uint8)
184+
185+
frames = [crop(imgs[c], k) for c in chosen for k in range(PAN)]
186+
187+
wm = load_model("vjepa2", entry="vjepa2_vit_large", device="cuda", dtype="float16", clip_len=16)
188+
for _ in range(16): # prime the clip buffer (no warmup blip)
189+
wm.predict_future(Observation(image=frames[0]), horizon=1)
190+
wm.reset()
191+
192+
out = []
193+
for f in frames:
194+
r = float(wm.predict_future(Observation(image=f), horizon=1).risk)
195+
thumb = Image.fromarray(f).resize((150, 150), Image.BILINEAR)
196+
buf = io.BytesIO()
197+
thumb.save(buf, "JPEG", quality=82)
198+
out.append({"img": "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode(),
199+
"surprise": round(r, 4)})
200+
cuts = [PAN, 2 * PAN]
201+
return {"frames": out, "cuts": cuts}
202+
203+
149204
GENERATORS = {
150205
"imagination": gen_imagination,
151206
"nav2": gen_nav2,
152207
"ijepa": gen_ijepa,
153208
"compare": gen_compare,
209+
"hero": gen_hero,
154210
}
155211

156212

tools/gif/render_hero.html

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!doctype html>
2+
<html lang="en"><head><meta charset="utf-8">
3+
<style>html,body{margin:0;background:#1b1b20}#c{display:block}</style></head>
4+
<body>
5+
<canvas id="c" width="760" height="420"></canvas>
6+
<script>
7+
const ctx=document.getElementById('c').getContext('2d');
8+
const PX0=392,PX1=734,PY0=110,PY1=300,THR=0.05;
9+
function text(s,x,y,sz,col,al){ctx.fillStyle=col;ctx.font=`${sz}px system-ui,sans-serif`;ctx.textAlign=al||'left';ctx.fillText(s,x,y);}
10+
function xmap(k,N){return PX0+(N<2?0:k/(N-1))*(PX1-PX0);}
11+
12+
window.setData=(D)=>{
13+
window.DATA=D;
14+
window.SMAX=Math.max(0.1, Math.max(...D.frames.map(f=>f.surprise))*1.15);
15+
window.IMGS=D.frames.map(f=>{const im=new Image();im.src=f.img;return im;});
16+
Promise.all(window.IMGS.map(im=>im.decode().catch(()=>{}))).then(()=>{window.imagesReady=true;});
17+
};
18+
function ymap(s){return PY1-Math.max(0,Math.min(1,s/window.SMAX))*(PY1-PY0);}
19+
20+
function renderFrame(i){
21+
const D=window.DATA,F=D.frames,N=F.length,s=F[i].surprise,anom=s>THR;
22+
ctx.fillStyle='#1b1b20';ctx.fillRect(0,0,760,420);
23+
text('A World Model watches real video',28,32,21,'#f0f0f3');
24+
text('real footage → V-JEPA 2 → surprise · the latent jumps when the scene changes',28,54,13,'#9aa0ad');
25+
text('real photographs (picsum/Unsplash) · V-JEPA 2 ViT-L · GPU',28,408,12,'#6a6a74');
26+
27+
// real camera frame
28+
const IX=28,IY=72,IS=300;
29+
ctx.fillStyle='#000';ctx.fillRect(IX,IY,IS,IS);
30+
if(window.IMGS&&window.IMGS[i])ctx.drawImage(window.IMGS[i],IX,IY,IS,IS);
31+
ctx.strokeStyle=anom?'#ff5a4a':'#33333c';ctx.lineWidth=anom?5:1;ctx.strokeRect(IX-0.5,IY-0.5,IS+1,IS+1);
32+
text('camera',IX+10,IY+24,15,'#fff');
33+
if(anom){text('● scene change',IX+IS-12,IY+24,15,'#ff5a4a','right');}
34+
35+
// surprise sparkline
36+
text('surprise (latent novelty)',PX0,PY0-14,14,'#9a9aa6');
37+
ctx.strokeStyle='#33333c';ctx.lineWidth=1;ctx.strokeRect(PX0-0.5,PY0-0.5,PX1-PX0+1,PY1-PY0+1);
38+
ctx.setLineDash([5,4]);ctx.strokeStyle='rgba(255,90,74,0.5)';
39+
ctx.beginPath();ctx.moveTo(PX0,ymap(THR));ctx.lineTo(PX1,ymap(THR));ctx.stroke();ctx.setLineDash([]);
40+
text('alert',PX1,ymap(THR)-5,11,'rgba(255,120,108,0.85)','right');
41+
// faint full + bright up-to-i
42+
ctx.strokeStyle='rgba(140,140,152,0.28)';ctx.lineWidth=2;ctx.beginPath();
43+
F.forEach((f,k)=>{const x=xmap(k,N),y=ymap(f.surprise);k?ctx.lineTo(x,y):ctx.moveTo(x,y);});ctx.stroke();
44+
ctx.strokeStyle='#5ad1ff';ctx.lineWidth=3;ctx.beginPath();
45+
for(let k=0;k<=i;k++){const x=xmap(k,N),y=ymap(F[k].surprise);k?ctx.lineTo(x,y):ctx.moveTo(x,y);}ctx.stroke();
46+
const cx=xmap(i,N),cy=ymap(s);
47+
ctx.fillStyle=anom?'#ff5a4a':'#5ad1ff';if(anom){ctx.shadowColor='#ff5a4a';ctx.shadowBlur=14;}
48+
ctx.beginPath();ctx.arc(cx,cy,6,0,7);ctx.fill();ctx.shadowBlur=0;
49+
50+
text('surprise',PX0,PY1+34,13,'#9a9aa6');
51+
text(s.toFixed(3),PX0,PY1+64,26,anom?'#ff5a4a':'#5ad1ff');
52+
text('low while the camera pans,',PX1,PY1+28,13,'#8b8b96','right');
53+
text('high the instant the scene cuts',PX1,PY1+48,13,'#8b8b96','right');
54+
}
55+
window.renderFrame=renderFrame;window.__ready=true;
56+
</script>
57+
</body></html>

tools/pages/build_site.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
REPO_URL = "https://github.com/rsasaki0109/worldmodels_ros2"
1818

1919
CAPTIONS = {
20-
"imagination.gif": "Imagined future occupancy + risk from one ROS 2 call (dummy).",
20+
"hero.gif": "Real footage → V-JEPA 2 → surprise; the latent spikes when the scene cuts.",
21+
"imagination.gif": "Top-down: predicted future occupancy of an obstacle (green→red) + risk.",
2122
"jepa_compare.gif": "Same camera stream, two real World Models: I-JEPA vs V-JEPA 2 surprise.",
2223
"nav2_scoring.gif": "Nav2 candidate paths ranked by model-based risk; safest highlighted.",
2324
"ijepa_surprise.gif": "Real I-JEPA surprise spiking on scene changes (GPU).",
2425
}
25-
HERO = "imagination.gif"
26-
ORDER = ["jepa_compare.gif", "nav2_scoring.gif", "ijepa_surprise.gif"]
26+
HERO = "hero.gif"
27+
ORDER = ["jepa_compare.gif", "imagination.gif", "nav2_scoring.gif", "ijepa_surprise.gif"]
2728

2829

2930
def _card(gif: str) -> str:

0 commit comments

Comments
 (0)