Spaces:
Running
Running
File size: 26,001 Bytes
23e2124 345280b 838532b 345280b c54cb53 345280b c54cb53 345280b 9f74f65 19818b7 345280b c54cb53 345280b 9f74f65 c54cb53 9f74f65 345280b 9f74f65 c54cb53 9f74f65 c54cb53 9f74f65 345280b 5a08999 345280b 5a08999 345280b 5a08999 345280b 838532b 345280b 838532b 345280b 838532b 345280b 5a08999 345280b c54cb53 345280b 5a08999 345280b 5a08999 345280b c54cb53 838532b c54cb53 37edeb5 c54cb53 345280b c54cb53 345280b c54cb53 345280b 5a08999 c54cb53 345280b c54cb53 345280b c54cb53 345280b c54cb53 345280b c54cb53 345280b 37edeb5 345280b c54cb53 345280b 987fd86 c54cb53 345280b c57bb0b 19818b7 c57bb0b 53bc8e1 c54cb53 345280b 23e2124 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Postdata AI · Vehicle Exit Detection</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500;600&family=Funnel+Sans:wght@300..700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/ort.min.js"></script>
<style>
:root{--bg:#0A0B0F;--card:#13151B;--line:#2C2F38;--text:#F2F2F4;--muted:#A9ADB6;--accent:#E8453C;--ok:#7CC87A;--warn:#F5C542}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--text);font-family:'Funnel Sans',Arial,sans-serif}
.wrap{max-width:1100px;margin:0 auto;padding:28px 16px 60px}
.brand{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:15px;letter-spacing:.04em}
.brand b{font-family:'Chakra Petch',sans-serif;color:var(--text);font-size:20px;letter-spacing:0}
.tag{display:inline-block;background:#30333C;color:#E6E7EA;padding:6px 14px;font-size:14px;margin:22px 0 10px}
h1{font-family:'Chakra Petch',sans-serif;font-weight:500;font-size:clamp(28px,4.5vw,46px);line-height:1.1;margin:0 0 10px}
p.lead{color:var(--muted);font-size:17px;max-width:820px;line-height:1.5;margin:0 0 22px}
.controls{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-bottom:14px}
button,label.btn{background:var(--card);color:var(--text);border:1px solid var(--line);padding:10px 16px;font:inherit;font-size:15px;cursor:pointer}
button.primary{background:var(--accent);border-color:var(--accent);color:#fff}
button:disabled{opacity:.45;cursor:default}
input[type=file]{display:none}
.stage{background:var(--card);border:1px solid var(--line);position:relative}
canvas#view{display:block;width:100%;height:auto;background:#000}
.status{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:10px 14px;border-top:1px solid var(--line);color:var(--muted);font-size:14px}
.bar{height:4px;background:#1d2027}.bar i{display:block;height:100%;width:0;background:var(--accent);transition:width .2s}
.grid{display:grid;grid-template-columns:1.2fr 1fr;gap:18px;margin-top:18px}
@media(max-width:760px){.grid{grid-template-columns:1fr}}
.card{background:var(--card);border:1px solid var(--line);padding:16px}
.card h3{font-family:'Chakra Petch',sans-serif;font-weight:500;margin:0 0 10px;font-size:20px}
table{width:100%;border-collapse:collapse;font-size:14px}
td,th{text-align:left;padding:7px 6px;border-bottom:1px solid var(--line)}
th{color:var(--muted);font-weight:400}
.alert{color:#ff7b72;font-weight:600}
.legend span{display:inline-flex;align-items:center;gap:6px;margin-right:14px;font-size:14px;color:var(--muted)}
.legend i{width:12px;height:12px;display:inline-block}
.foot{color:#7D818B;font-size:13px;margin-top:26px;line-height:1.6}
a{color:var(--muted)}
</style>
</head>
<body>
<div class="wrap">
<div class="brand"><b>Postdata</b> AI</div>
<div class="tag">Computer vision demo · Pose estimation · Runs in your browser</div>
<h1>Detect the moment a person gets out of a vehicle</h1>
<p class="lead">RTMPose finds 17 body keypoints for every person and RTMDet finds the vehicles. A simple rule turns this into an event: the person was inside the vehicle zone, then they stand up on their own feet or step away from the vehicle, so the app raises <b>Person out of vehicle</b> with a timestamp. Your video never leaves this device.</p>
<div class="controls">
<button id="exBtn" class="primary">▶ Run example: film scene (1968, public domain)</button>
<label class="btn" for="file">Upload your own video</label><input id="file" type="file" accept="video/*">
<button id="liveBtn">Re-run example live</button>
<button id="playBtn" disabled>Replay result</button>
<button id="recBtn" disabled>Save result as video</button>
</div>
<div class="stage">
<canvas id="view" width="960" height="540"></canvas>
<div class="bar"><i id="prog"></i></div>
<div class="status"><span id="status">Press ▶ Run example to see the result.</span><span id="perf"></span></div>
</div>
<div class="grid">
<div class="card">
<h3>Events</h3>
<table><thead><tr><th>Time</th><th>Person</th><th>Event</th></tr></thead><tbody id="events"><tr><td colspan="3" style="color:#7D818B">Run the example or upload a clip.</td></tr></tbody></table>
</div>
<div class="card">
<h3>How it works</h3>
<div class="legend" style="margin-bottom:10px">
<span><i style="background:#28a0e6"></i>Vehicle</span>
<span><i style="background:#ffb43c"></i>In vehicle</span>
<span><i style="background:#ffd700"></i>Exiting</span>
<span><i style="background:#f03c3c"></i>Out of vehicle</span>
<span><i style="background:#78c88c"></i>On foot</span>
</div>
<p style="color:var(--muted);font-size:14px;line-height:1.55;margin:0">The same pipeline covers truck-yard rules: driver out of the cab, a person in a truck lane, or a wheel chock check. In production it runs in real time on site cameras, on-prem or in your own Azure tenant.</p>
</div>
</div>
<div class="foot">
Models: <a href="https://github.com/open-mmlab/mmpose/tree/main/projects/rtmpose">RTMPose-m</a> (body keypoints) and <a href="https://github.com/open-mmlab/mmdetection/tree/main/configs/rtmdet">RTMDet-s</a> (COCO objects), OpenMMLab, Apache-2.0, run with ONNX Runtime Web.
Example footage: <i>Night of the Living Dead</i> (1968, dir. George A. Romero), public domain, via <a href="https://commons.wikimedia.org/wiki/File:Night_of_the_Living_Dead_(1968).webm">Wikimedia Commons</a>.
Demo by <a href="https://www.postdata.ai">Postdata AI</a>. This is a demo, not a safety system.
</div>
</div>
<script>
const MODEL_BASES = ['models/', 'https://huggingface.co/ziq/rtm/resolve/main/'];
const EXAMPLE = { url: 'example.webm', start: 0.8, end: 8.4 };
const ANALYSIS_FPS = 10, MAX_SECONDS = 30, VEHICLES = {2:'car',5:'bus',7:'truck'}, PERSON = 0;
const L_HIP=11,R_HIP=12,L_KNEE=13,R_KNEE=14,L_ANK=15,R_ANK=16;
const SKELETON=[[15,13],[13,11],[16,14],[14,12],[11,12],[5,11],[6,12],[5,6],[5,7],[6,8],[7,9],[8,10],[1,2],[0,1],[0,2],[1,3],[2,4],[3,5],[4,6]];
const COLORS={'IN VEHICLE':'#ffb43c','EXITING':'#ffd700','OUT OF VEHICLE':'#f03c3c','ON FOOT':'#78c88c'};
const $=id=>document.getElementById(id);
const view=$('view'), vctx=view.getContext('2d');
const work=document.createElement('canvas'), wctx=work.getContext('2d',{willReadFrequently:true});
const crop=document.createElement('canvas'); crop.width=192; crop.height=256; const cctx=crop.getContext('2d',{willReadFrequently:true});
let det=null, pose=null, video=null, results=[], events=[], clip={start:0,end:0}, busy=false;
window.lastClip=null;
let modelMsg='Loading models in the background…';
function placeholder(msg){ if(video) return; const W=view.width,H=view.height; vctx.fillStyle='#0E1015'; vctx.fillRect(0,0,W,H);
vctx.textAlign='center'; vctx.fillStyle='#F2F2F4'; vctx.font='500 26px "Chakra Petch",Arial'; vctx.fillText(msg,W/2,H/2-6);
vctx.fillStyle='#7D818B'; vctx.font='400 16px "Funnel Sans",Arial'; vctx.fillText(modelMsg,W/2,H/2+26); vctx.textAlign='left'; }
function setStatus(s){$('status').textContent=s; placeholder(s)}
function setModelMsg(s){ modelMsg=s; if(!busy) placeholder($('status').textContent); }
function setProg(f){$('prog').style.width=(100*f).toFixed(1)+'%'}
const prog={};
async function fetchModel(name){
for(const base of MODEL_BASES){
try{
const r=await fetch(base+name); if(!r.ok) continue;
const total=+r.headers.get('content-length')||0; const rd=r.body.getReader(); const parts=[]; let got=0;
while(true){ const {done,value}=await rd.read(); if(done) break; parts.push(value); got+=value.length; prog[name]=[got,total];
const g=Object.values(prog).reduce((a,b)=>a+b[0],0), t=Object.values(prog).reduce((a,b)=>a+(b[1]||b[0]),0);
setModelMsg(`Live mode: loading models ${Math.round(100*g/Math.max(t,1))}% (≈95 MB, first time only)`); }
const buf=new Uint8Array(got); let o=0; for(const p of parts){ buf.set(p,o); o+=p.length; } return buf;
}catch(e){ /* try next source */ }
}
throw new Error('could not download '+name);
}
let pendingRun=null;
async function loadModels(){
ort.env.wasm.wasmPaths='https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/';
ort.env.wasm.numThreads = self.crossOriginIsolated ? Math.min(4, navigator.hardwareConcurrency||1) : 1;
const opt={executionProviders:['wasm'],graphOptimizationLevel:'all'};
const [a,b]=await Promise.all([fetchModel('rtmdet-s.onnx'),fetchModel('rtmpose-m.onnx')]);
setModelMsg('Live mode: preparing models…');
det=await ort.InferenceSession.create(a,opt); pose=await ort.InferenceSession.create(b,opt);
setModelMsg('Live mode ready: upload your own clip.'); if(!busy && !video) setStatus('Press ▶ Run example to see the result.');
if(pendingRun){ const f=pendingRun; pendingRun=null; f(); }
}
// ---------- detection (RTMDet-s, COCO, end2end with NMS) ----------
async function detect(src,W,H){
const S=640, r=Math.min(S/W,S/H);
work.width=S; work.height=S; wctx.fillStyle='rgb(114,114,114)'; wctx.fillRect(0,0,S,S);
wctx.drawImage(src,0,0,Math.round(W*r),Math.round(H*r));
const px=wctx.getImageData(0,0,S,S).data, n=S*S, f=new Float32Array(3*n);
for(let i=0;i<n;i++){const R=px[4*i],G=px[4*i+1],B=px[4*i+2];
f[i]=(B-103.53)/57.375; f[n+i]=(G-116.28)/57.12; f[2*n+i]=(R-123.675)/58.395;}
const out=await det.run({[det.inputNames[0]]:new ort.Tensor('float32',f,[1,3,S,S])});
let dets,labels; for(const k of det.outputNames){const t=out[k]; if(t.dims.length===3&&t.dims[2]===5) dets=t; else labels=t;}
const res=[]; const N=dets.dims[1];
for(let i=0;i<N;i++){const s=dets.data[5*i+4]; if(s<0.3) continue;
const lab=Number(labels.data[i]);
res.push({x1:dets.data[5*i]/r,y1:dets.data[5*i+1]/r,x2:dets.data[5*i+2]/r,y2:dets.data[5*i+3]/r,score:s,label:lab});}
return res;
}
// ---------- pose (RTMPose-m, SimCC, 256x192) ----------
async function estimate(src,b){
let cx=(b.x1+b.x2)/2, cy=(b.y1+b.y2)/2, w=(b.x2-b.x1)*1.25, h=(b.y2-b.y1)*1.25;
const ar=192/256; if(w>h*ar) h=w/ar; else w=h*ar;
const x0=cx-w/2, y0=cy-h/2;
cctx.fillStyle='#000'; cctx.fillRect(0,0,192,256);
cctx.drawImage(src,x0,y0,w,h,0,0,192,256);
const px=cctx.getImageData(0,0,192,256).data, n=192*256, f=new Float32Array(3*n);
for(let i=0;i<n;i++){f[i]=(px[4*i]-123.675)/58.395; f[n+i]=(px[4*i+1]-116.28)/57.12; f[2*n+i]=(px[4*i+2]-103.53)/57.375;}
const out=await pose.run({[pose.inputNames[0]]:new ort.Tensor('float32',f,[1,3,256,192])});
let sx,sy; for(const k of pose.outputNames){const t=out[k]; if(t.dims[2]===384) sx=t; else sy=t;}
const kp=[],sc=[];
for(let j=0;j<17;j++){
let mx=-1e9,ix=0; for(let i=0;i<384;i++){const v=sx.data[j*384+i]; if(v>mx){mx=v;ix=i}}
let my=-1e9,iy=0; for(let i=0;i<512;i++){const v=sy.data[j*512+i]; if(v>my){my=v;iy=i}}
kp.push([x0+(ix/2)/192*w, y0+(iy/2)/256*h]); sc.push(Math.min(mx,my));
}
return {kp,sc};
}
// ---------- geometry & state ----------
function angle(k,a,b,c){const v1=[k[a][0]-k[b][0],k[a][1]-k[b][1]],v2=[k[c][0]-k[b][0],k[c][1]-k[b][1]];
const cos=(v1[0]*v2[0]+v1[1]*v2[1])/(Math.hypot(...v1)*Math.hypot(...v2)+1e-6); return Math.acos(Math.max(-1,Math.min(1,cos)))*180/Math.PI;}
function standing(p){const s=p.sc,thr=0.45;
if(Math.min(s[L_ANK],s[R_ANK],s[L_KNEE],s[R_KNEE],s[L_HIP],s[R_HIP])<thr) return false;
return angle(p.kp,L_HIP,L_KNEE,L_ANK)>150 && angle(p.kp,R_HIP,R_KNEE,R_ANK)>150;}
function overlap(p,v){const ix=Math.max(0,Math.min(p.x2,v.x2)-Math.max(p.x1,v.x1)),iy=Math.max(0,Math.min(p.y2,v.y2)-Math.max(p.y1,v.y1));
return ix*iy/Math.max(1,(p.x2-p.x1)*(p.y2-p.y1));}
function containment(a,b){const ix=Math.max(0,Math.min(a.x2,b.x2)-Math.max(a.x1,b.x1)),iy=Math.max(0,Math.min(a.y2,b.y2)-Math.max(a.y1,b.y1));
return ix*iy/Math.max(1,Math.min((a.x2-a.x1)*(a.y2-a.y1),(b.x2-b.x1)*(b.y2-b.y1)));}
function sim(a,b){const d=Math.hypot((a.x1+a.x2-b.x1-b.x2)/2,(a.y1+a.y2-b.y1-b.y2)/2), w=Math.max(a.x2-a.x1,b.x2-b.x1);
return Math.max(iou(a,b), d<0.6*w ? 0.5*(1-d/(0.6*w)) : 0);}
function iou(a,b){const ix=Math.max(0,Math.min(a.x2,b.x2)-Math.max(a.x1,b.x1)),iy=Math.max(0,Math.min(a.y2,b.y2)-Math.max(a.y1,b.y1)),i=ix*iy;
return i/((a.x2-a.x1)*(a.y2-a.y1)+(b.x2-b.x1)*(b.y2-b.y1)-i+1e-6);}
let tracks=[], nextId=1;
function updateTracks(people,vehicles,t){
const pairs=[], used=new Set(), cand=[];
tracks.forEach((tr,ti)=>people.forEach((p,pi)=>cand.push([sim(tr.box,p.box),ti,pi])));
cand.sort((a,b)=>b[0]-a[0]); const ta=new Set();
for(const [v,ti,pi] of cand){ if(v<0.1||ta.has(ti)||used.has(pi)) continue; ta.add(ti); used.add(pi); pairs.push([tracks[ti],pi]); }
tracks.forEach((tr,ti)=>{ if(!ta.has(ti)) tr.missed++; else tr.missed=0; });
people.forEach((p,pi)=>{ if(!used.has(pi)){ const tr={id:nextId++,box:p.box,state:null,inVeh:false,standT:null,awayT:null,missed:0}; tracks.push(tr); pairs.push([tr,pi]); }});
tracks=tracks.filter(tr=>tr.missed<ANALYSIS_FPS*1.5);
let alert=null;
for(const [tr,pi] of pairs){
const p=people[pi]; tr.box=p.box;
const near=Math.max(0,...vehicles.map(v=>overlap(p.box,v)));
const st=standing(p); p.standing=st;
tr.standT = st ? (tr.standT??t) : null;
tr.awayT = (tr.inVeh && near<0.25 && p.sc[5]>0.4 && p.sc[6]>0.4) ? (tr.awayT??t) : null;
let s;
if(near>0.3 && !st){ s = (tr.state===null||tr.state==='IN VEHICLE') ? 'IN VEHICLE' : 'EXITING'; tr.inVeh=true; }
else if(tr.inVeh && ((tr.standT!==null && t-tr.standT>=0.4) || (tr.awayT!==null && t-tr.awayT>=0.5))) s='OUT OF VEHICLE';
else if(tr.inVeh) s='EXITING';
else s='ON FOOT';
if(s==='EXITING' && tr.state==='IN VEHICLE') events.push({t,id:tr.id,ev:'Exiting vehicle'});
if(s==='OUT OF VEHICLE' && tr.state!=='OUT OF VEHICLE' && !events.some(e=>e.alert && t-e.t<2)){ events.push({t,id:tr.id,ev:'Person out of vehicle',alert:true}); alert=`ALERT · person #${tr.id} out of vehicle at ${fmt(t)}`; }
tr.state=s; p.id=tr.id; p.state=s;
}
return alert;
}
function fmt(t){return (t-clip.start).toFixed(1)+' s'}
// ---------- drawing ----------
function drawFrame(r,t,lastAlert){
const W=view.width,H=view.height;
vctx.drawImage(curImg||video,0,0,W,H);
if(!r) return;
const k=W/video.videoWidth;
vctx.lineWidth=3; vctx.font='600 15px "Funnel Sans",Arial';
for(const v of r.vehicles){ vctx.strokeStyle='#28a0e6'; vctx.strokeRect(v.x1*k,v.y1*k,(v.x2-v.x1)*k,(v.y2-v.y1)*k);
tag(`vehicle ${v.score.toFixed(2)}`,v.x1*k,v.y1*k,'#28a0e6'); }
for(const p of r.people){
const col=COLORS[p.state]||'#fff';
vctx.strokeStyle='rgba(255,255,255,.9)'; vctx.lineWidth=3;
for(const [a,b] of SKELETON){ if(p.sc[a]<0.35||p.sc[b]<0.35) continue;
vctx.beginPath(); vctx.moveTo(p.kp[a][0]*k,p.kp[a][1]*k); vctx.lineTo(p.kp[b][0]*k,p.kp[b][1]*k); vctx.stroke(); }
for(let j=0;j<17;j++){ if(p.sc[j]<0.35) continue; vctx.fillStyle=j>=11?col:'#00e5ff';
vctx.beginPath(); vctx.arc(p.kp[j][0]*k,p.kp[j][1]*k,4,0,7); vctx.fill(); }
if(p.standing) for(const a of [L_ANK,R_ANK]){ vctx.fillStyle='#f03c3c'; vctx.beginPath(); vctx.arc(p.kp[a][0]*k,p.kp[a][1]*k,8,0,7); vctx.fill(); }
vctx.strokeStyle=col; vctx.lineWidth=2; vctx.strokeRect(p.box.x1*k,p.box.y1*k,(p.box.x2-p.box.x1)*k,(p.box.y2-p.box.y1)*k);
tag(`#${p.id} ${p.state}`,p.box.x1*k,p.box.y1*k,col);
}
vctx.fillStyle='rgba(10,11,15,.82)'; vctx.fillRect(0,0,W,34);
vctx.fillStyle='#F2F2F4'; vctx.font='500 16px "Chakra Petch",Arial';
vctx.fillText(`Postdata AI · RTMPose + RTMDet · t = ${fmt(t)}`,12,23);
if(lastAlert){ vctx.fillStyle='rgba(220,40,40,.92)'; vctx.fillRect(0,H-48,W,48);
vctx.fillStyle='#fff'; vctx.font='600 22px "Chakra Petch",Arial'; vctx.fillText(lastAlert,16,H-16); }
}
function tag(text,x,y,col){ const w=vctx.measureText(text).width+12; vctx.fillStyle=col; vctx.fillRect(x,Math.max(0,y-22),w,22);
vctx.fillStyle='#111'; vctx.fillText(text,x+6,Math.max(16,y-6)); }
function renderEvents(){
const tb=$('events'); if(!events.length){ tb.innerHTML='<tr><td colspan="3" style="color:#7D818B">No exit detected in this clip.</td></tr>'; return; }
tb.innerHTML=events.map(e=>`<tr><td>${fmt(e.t)}</td><td>#${e.id}</td><td class="${e.alert?'alert':''}">${e.ev}</td></tr>`).join('');
}
// ---------- analysis & playback ----------
function seek(t){return new Promise(res=>{video.onseeked=()=>res(); video.currentTime=t;});}
async function openVideo(src,start,end){
if(video){video.pause(); video.removeAttribute('src'); video.load();}
video=document.createElement('video'); video.crossOrigin='anonymous'; video.muted=true; video.playsInline=true; video.preload='auto'; video.src=src;
await new Promise((res,rej)=>{video.onloadedmetadata=res; video.onerror=()=>rej(new Error('Cannot open this video'));});
clip.start=start||0; clip.end=Math.min(end||video.duration, clip.start+MAX_SECONDS);
const W=960; view.width=W; view.height=Math.round(W*video.videoHeight/video.videoWidth);
}
// Grab frames by normal 1x playback (reliable on any browser and on files without a seek index), then analyse them.
function captureFrames(){ return new Promise(async (res)=>{
const frames=[]; const steps=Math.max(1,Math.round((clip.end-clip.start)*ANALYSIS_FPS)); let next=0, fin=false;
const W=video.videoWidth,H=video.videoHeight;
const grab=async(mt)=>{ while(next<=steps && mt>=clip.start+next/ANALYSIS_FPS-0.03){ const c=document.createElement('canvas'); c.width=W; c.height=H; c.getContext('2d').drawImage(video,0,0,W,H); frames.push({t:clip.start+next/ANALYSIS_FPS,img:c}); next++; }
setProg(0.25*next/(steps+1)); setStatus(`Reading video… ${Math.round(100*next/(steps+1))}%`);
if(next>steps||video.ended||mt>=clip.end+0.05) done(); };
const done=()=>{ if(fin) return; fin=true; video.pause(); clearInterval(iv); res(frames); };
const cb=(now,meta)=>{ if(fin) return; grab(meta.mediaTime); if(!fin) video.requestVideoFrameCallback(cb); };
await seek(clip.start); video.playbackRate=1;
if(video.requestVideoFrameCallback) video.requestVideoFrameCallback(cb);
const iv=setInterval(()=>{ if(!video.requestVideoFrameCallback) grab(video.currentTime); if(video.ended) done(); },20);
video.onended=done; video.play().catch(done);
});}
let curImg=null;
async function analyze(){
busy=true; ['exBtn','liveBtn','playBtn','recBtn'].forEach(id=>$(id).disabled=true);
results=[]; events=[]; tracks=[]; nextId=1; renderEvents();
const W=video.videoWidth,H=video.videoHeight;
const frames=await captureFrames(); const t0=performance.now();
let lastAlert=null, alertT=-1;
for(let i=0;i<frames.length;i++){
const {t,img}=frames[i]; curImg=img;
const dets=await detect(img,W,H);
const vehicles=dets.filter(d=>d.label in VEHICLES && d.score>0.35);
const people=[], kept=[];
for(const d of dets.filter(d=>d.label===PERSON && d.score>0.4).sort((a,b)=>b.score-a.score)){
if(kept.some(k=>containment(d,k)>0.6)) continue; kept.push(d);
}
for(const d of kept.slice(0,6)){
const p=await estimate(img,d); const good=p.sc.filter(s=>s>0.3).length; if(good<5) continue;
p.box={x1:d.x1,y1:d.y1,x2:d.x2,y2:d.y2}; people.push(p);
}
const a=updateTracks(people,vehicles,t); if(a){lastAlert=a; alertT=t;}
const r={t,vehicles,people:people.map(p=>({...p})),alert:(t-alertT<2.5)?lastAlert:null};
results.push(r); drawFrame(r,t,r.alert); renderEvents();
setProg(0.25+0.75*(i+1)/frames.length); const fps=(i+1)/((performance.now()-t0)/1000);
setStatus(`Analysing frame ${i+1} / ${frames.length}`); $('perf').textContent=`${fps.toFixed(1)} frames/s in this browser`;
}
curImg=null;
setStatus(`Done. ${events.filter(e=>e.alert).length} exit event(s). Press "Replay result" or "Save result as video".`);
busy=false; ['exBtn','liveBtn','playBtn','recBtn'].forEach(id=>$(id).disabled=false);
}
function nearest(t){let best=null,bd=1e9; for(const r of results){const d=Math.abs(r.t-t); if(d<bd){bd=d;best=r}} return best;}
async function replay(record=false){
if(!results.length) return;
['exBtn','liveBtn','playBtn','recBtn'].forEach(id=>$(id).disabled=true);
await seek(clip.start);
let rec=null, chunks=[];
if(record){
const type=['video/mp4;codecs=avc1','video/webm;codecs=vp9','video/webm'].find(t=>MediaRecorder.isTypeSupported(t));
rec=new MediaRecorder(view.captureStream(30),{mimeType:type,videoBitsPerSecond:6e6});
rec.ondataavailable=e=>e.data.size&&chunks.push(e.data); rec.start();
}
video.playbackRate=1; await video.play();
await new Promise(res=>{
const loop=()=>{ const t=video.currentTime; const r=nearest(t); drawFrame(r,t,r&&r.alert);
if(t>=clip.end||video.ended){ video.pause(); res(); return; } requestAnimationFrame(loop); };
requestAnimationFrame(loop);
});
if(rec){ await new Promise(res=>{rec.onstop=res; rec.stop();});
const blob=new Blob(chunks,{type:rec.mimeType}); window.lastClip=blob;
if(!window.NO_DOWNLOAD){ const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='postdata-rtmpose-exit.'+(rec.mimeType.includes('mp4')?'mp4':'webm'); a.click(); } }
['exBtn','liveBtn','playBtn','recBtn'].forEach(id=>$(id).disabled=false);
}
let _exPromise=null;
function exampleSrc(){
if(!_exPromise) _exPromise=(async()=>{
for(let attempt=0; attempt<4; attempt++){
const ac=new AbortController(); const to=setTimeout(()=>ac.abort(),25000);
try{ const r=await fetch(EXAMPLE.url+(attempt?('?r='+attempt):''),{signal:ac.signal}); if(!r.ok) throw new Error(r.status);
const b=await r.blob(); clearTimeout(to); return URL.createObjectURL(b); }
catch(e){ clearTimeout(to); }
}
_exPromise=null; throw new Error('could not load the example clip, please retry');
})();
return _exPromise;
}
exampleSrc().catch(()=>{});
let _preP=null; function precomputed(){ if(!_preP) _preP=fetch('example-results.json').then(r=>{if(!r.ok) throw new Error('results '+r.status); return r.json();}).catch(e=>{_preP=null; throw e;}); return _preP; }
precomputed().catch(()=>{});
$('exBtn').onclick=async()=>{ if(busy) return; busy=true; ['exBtn','liveBtn','playBtn','recBtn'].forEach(id=>$(id).disabled=true);
try{ setStatus('Loading example clip…'); setProg(0);
const [src,pre]=await Promise.all([exampleSrc(),precomputed()]);
await openVideo(src,EXAMPLE.start,EXAMPLE.end); clip.end=Math.min(clip.end,pre.results[pre.results.length-1].t+0.1); results=pre.results; events=pre.events; renderEvents(); setProg(1);
setStatus('Playing the RTMPose + RTMDet output for this clip (computed in the browser with these models).');
$('perf').textContent='Re-run live or upload your own clip to run the models on this device';
busy=false; await replay(false);
setStatus(`Done. ${events.filter(e=>e.alert).length} exit event(s). Press "Replay result", "Re-run example live" or upload your own clip.`);
}catch(e){ setStatus('Error: '+e.message+'. Please press the button again.'); }
busy=false; ['exBtn','liveBtn','playBtn','recBtn'].forEach(id=>$(id).disabled=false); };
$('liveBtn').onclick=async()=>{ if(busy) return; if(!det){ pendingRun=()=>$('liveBtn').onclick(); setStatus('Live run will start as soon as the models are loaded…'); $('perf').textContent=modelMsg; return; } try{ setStatus('Loading example clip…'); await openVideo(await exampleSrc(),EXAMPLE.start,EXAMPLE.end); await analyze(); await replay(false);}catch(e){setStatus('Error: '+e.message)} };
$('file').onchange=async e=>{ const f=e.target.files[0]; if(!f||busy) return; if(!det){ pendingRun=()=>$('file').onchange(e); setStatus('Your clip will start as soon as the models are loaded…'); $('perf').textContent=modelMsg; return; } try{ await openVideo(URL.createObjectURL(f),0,null); await analyze(); await replay(false);}catch(err){setStatus('Error: '+err.message)} };
$('playBtn').onclick=()=>replay(false);
$('recBtn').onclick=()=>replay(true);
// Embed hook: a parent page can ask for the example result as a video clip.
window.addEventListener('message', async e=>{
if(!(e.data && e.data.cmd==='example-record')) return;
while(!det) await new Promise(r=>setTimeout(r,500));
window.NO_DOWNLOAD=true;
await openVideo(await exampleSrc(),EXAMPLE.start,EXAMPLE.end); await analyze(); await replay(true);
e.source.postMessage({kind:'clip', blob:window.lastClip}, e.origin);
});
window.addEventListener('message', async e=>{
if(!(e.data && e.data.cmd==='record-raw')) return;
const v=document.createElement('video'); v.crossOrigin='anonymous'; v.muted=true; v.preload='auto'; v.src=EXAMPLE.url;
await new Promise(r=>v.onloadedmetadata=r); await new Promise(r=>{v.onseeked=r; v.currentTime=EXAMPLE.start-0.8;});
const W=640,H=480,c=document.createElement('canvas'); c.width=W; c.height=H; const g=c.getContext('2d'); g.drawImage(v,0,0,W,H);
const rec=new MediaRecorder(c.captureStream(30),{mimeType:'video/webm;codecs=vp9',videoBitsPerSecond:3e6}); const ch=[];
rec.ondataavailable=ev=>ev.data.size&&ch.push(ev.data);
await new Promise(r=>{ if(v.readyState>=4) r(); else {v.oncanplaythrough=r; setTimeout(r,15000);} });
rec.start(200); await v.play();
await new Promise(res=>{const loop=()=>{g.drawImage(v,0,0,W,H); if(v.currentTime>=EXAMPLE.end+0.6||v.ended){v.pause(); res(); return;} requestAnimationFrame(loop)}; loop();});
await new Promise(r=>{rec.onstop=r; rec.stop();});
e.source.postMessage({kind:'raw', blob:new Blob(ch,{type:'video/webm'})}, e.origin);
});
placeholder('Press ▶ Run example to see the result.');
document.fonts && document.fonts.ready.then(()=>placeholder($('status').textContent));
loadModels().catch(e=>setModelMsg('Live mode unavailable: could not load models ('+e.message+')'));
</script>
</body>
</html>
|