Spaces:
Sleeping
Sleeping
| APP.core.tracker = {}; | |
| APP.core.tracker.matchAndUpdateTracks = function (dets, dtSec) { | |
| const { state } = APP.core; | |
| const { CONFIG } = APP.core; | |
| const { normBBox, lerp, now, $ } = APP.core.utils; | |
| const { defaultAimpoint } = APP.core.physics; | |
| const { log } = APP.ui.logging; | |
| const videoEngage = $("#videoEngage"); | |
| const rangeBase = $("#rangeBase"); // Fixed Selector | |
| if (!videoEngage) return; | |
| // IOU helper | |
| function iou(a, b) { | |
| const ax2 = a.x + a.w, ay2 = a.y + a.h; | |
| const bx2 = b.x + b.w, by2 = b.y + b.h; | |
| const ix1 = Math.max(a.x, b.x), iy1 = Math.max(a.y, b.y); | |
| const ix2 = Math.min(ax2, bx2), iy2 = Math.min(ay2, by2); | |
| const iw = Math.max(0, ix2 - ix1), ih = Math.max(0, iy2 - iy1); | |
| const inter = iw * ih; | |
| const ua = a.w * a.h + b.w * b.h - inter; | |
| return ua <= 0 ? 0 : inter / ua; | |
| } | |
| // Convert detections to bbox in video coordinates | |
| const w = videoEngage.videoWidth || state.frame.w; | |
| const h = videoEngage.videoHeight || state.frame.h; | |
| const detObjs = dets.map(d => ({ | |
| bbox: normBBox(d.bbox, w, h), | |
| label: d.class, | |
| score: d.score, | |
| depth_rel: Number.isFinite(d.depth_rel) ? d.depth_rel : null, | |
| depth_est_m: d.depth_est_m, | |
| depth_valid: d.depth_valid | |
| })); | |
| // mark all tracks as unmatched | |
| const tracks = state.tracker.tracks; | |
| const used = new Set(); | |
| for (const tr of tracks) { | |
| let best = null; | |
| let bestI = 0.0; | |
| let bestIdx = -1; | |
| for (let i = 0; i < detObjs.length; i++) { | |
| if (used.has(i)) continue; | |
| const IoU = iou(tr.bbox, detObjs[i].bbox); | |
| if (IoU > bestI) { | |
| bestI = IoU; | |
| best = detObjs[i]; | |
| bestIdx = i; | |
| } | |
| } | |
| // Strict matching threshold | |
| if (best && bestI >= CONFIG.TRACK_MATCH_THRESHOLD) { | |
| used.add(bestIdx); | |
| // Velocity with Exponential Moving Average (EMA) for smoothing | |
| const cx0 = tr.bbox.x + tr.bbox.w * 0.5; | |
| const cy0 = tr.bbox.y + tr.bbox.h * 0.5; | |
| const cx1 = best.bbox.x + best.bbox.w * 0.5; | |
| const cy1 = best.bbox.y + best.bbox.h * 0.5; | |
| const rawVx = (cx1 - cx0) / Math.max(1e-3, dtSec); | |
| const rawVy = (cy1 - cy0) / Math.max(1e-3, dtSec); | |
| // Alpha of 0.3 means 30% new value, 70% history | |
| tr.vx = tr.vx * 0.7 + rawVx * 0.3; | |
| tr.vy = tr.vy * 0.7 + rawVy * 0.3; | |
| // smooth bbox update | |
| tr.bbox.x = lerp(tr.bbox.x, best.bbox.x, 0.7); | |
| tr.bbox.y = lerp(tr.bbox.y, best.bbox.y, 0.7); | |
| tr.bbox.w = lerp(tr.bbox.w, best.bbox.w, 0.6); | |
| tr.bbox.h = lerp(tr.bbox.h, best.bbox.h, 0.6); | |
| // Logic: Only update label if the new detection is highly confident | |
| // AND the current track doesn't have a "premium" label (like 'drone'). | |
| const protectedLabels = ["drone", "uav", "missile"]; | |
| const isProtected = protectedLabels.some(l => (tr.label || "").toLowerCase().includes(l)); | |
| if (!isProtected || (best.label && protectedLabels.some(l => best.label.toLowerCase().includes(l)))) { | |
| tr.label = best.label || tr.label; | |
| } | |
| tr.score = best.score || tr.score; | |
| if (Number.isFinite(best.depth_rel)) { | |
| tr.depth_rel = best.depth_rel; | |
| } | |
| if (best.depth_valid) { | |
| // EMA Smoothing | |
| const newD = best.depth_est_m; | |
| if (tr.depth_est_m == null) tr.depth_est_m = newD; | |
| else tr.depth_est_m = tr.depth_est_m * 0.7 + newD * 0.3; | |
| tr.depth_valid = true; | |
| } | |
| tr.lastSeen = now(); | |
| } else { | |
| // Decay velocity | |
| tr.vx *= 0.9; | |
| tr.vy *= 0.9; | |
| } | |
| } | |
| // Limit total tracks | |
| if (tracks.length < CONFIG.MAX_TRACKS) { | |
| for (let i = 0; i < detObjs.length; i++) { | |
| if (used.has(i)) continue; | |
| // create new track only if big enough | |
| const a = detObjs[i].bbox.w * detObjs[i].bbox.h; | |
| if (a < (w * h) * 0.0025) continue; | |
| const newId = `T${String(state.tracker.nextId++).padStart(2, "0")}`; | |
| const ap = defaultAimpoint(detObjs[i].label); | |
| tracks.push({ | |
| id: newId, | |
| label: detObjs[i].label, | |
| bbox: { ...detObjs[i].bbox }, | |
| score: detObjs[i].score, | |
| aimRel: { relx: ap.relx, rely: ap.rely, label: ap.label }, | |
| baseAreaFrac: (detObjs[i].bbox.w * detObjs[i].bbox.h) / (w * h), | |
| baseRange_m: rangeBase ? +rangeBase.value : 1000, | |
| baseDwell_s: 5.5, | |
| reqP_kW: 42, | |
| depth_rel: detObjs[i].depth_rel, | |
| depth_est_m: detObjs[i].depth_est_m, | |
| depth_valid: detObjs[i].depth_valid, | |
| // GPT properties | |
| gpt_distance_m: null, | |
| gpt_direction: null, | |
| gpt_description: null, | |
| // Track state | |
| lastSeen: now(), | |
| vx: 0, vy: 0, | |
| dwellAccum: 0, | |
| killed: false, | |
| state: "TRACK", | |
| assessT: 0 | |
| }); | |
| log(`New track created: ${newId} (${detObjs[i].label})`, "t"); | |
| } | |
| } | |
| // prune old tracks | |
| const tNow = now(); | |
| state.tracker.tracks = tracks.filter(tr => (tNow - tr.lastSeen) < CONFIG.TRACK_PRUNE_MS || tr.killed); | |
| }; | |
| // Polling for backend tracks | |
| APP.core.tracker.syncWithBackend = async function (frameIdx) { | |
| const { state } = APP.core; | |
| const { $ } = APP.core.utils; | |
| const jobId = state.hf.asyncJobId; | |
| if (!jobId || !state.hf.baseUrl) return; | |
| try { | |
| const resp = await fetch(`${state.hf.baseUrl}/detect/tracks/${jobId}/${frameIdx}`); | |
| if (!resp.ok) return; | |
| const dets = await resp.json(); | |
| if (!dets || !Array.isArray(dets)) return; | |
| // Transform backend format to frontend track format | |
| // Backend: { bbox: [x1, y1, x2, y2], label: "car", track_id: "T01", angle_deg: 90, ... } | |
| // Frontend: { id: "T01", bbox: {x,y,w,h}, label: "car", angle_deg: 90, ... } | |
| const videoEngage = $("#videoEngage"); | |
| const w = videoEngage ? (videoEngage.videoWidth || state.frame.w) : state.frame.w; | |
| const h = videoEngage ? (videoEngage.videoHeight || state.frame.h) : state.frame.h; | |
| const newTracks = dets.map(d => { | |
| const x = d.bbox[0], y = d.bbox[1]; | |
| const wBox = d.bbox[2] - d.bbox[0]; | |
| const hBox = d.bbox[3] - d.bbox[1]; | |
| // Normalize | |
| const nx = x / w; | |
| const ny = y / h; | |
| const nw = wBox / w; | |
| const nh = hBox / h; | |
| return { | |
| id: d.track_id || `T${Math.floor(Math.random() * 1000)}`, // Fallback | |
| label: d.label, | |
| bbox: { x: nx, y: ny, w: nw, h: nh }, | |
| score: d.score, | |
| angle_deg: d.angle_deg, | |
| gpt_distance_m: d.gpt_distance_m, | |
| angle_deg: d.angle_deg, | |
| gpt_distance_m: d.gpt_distance_m, | |
| speed_kph: d.speed_kph, | |
| depth_est_m: d.depth_est_m, | |
| depth_rel: d.depth_rel, | |
| depth_valid: d.depth_valid, | |
| // Keep UI state fields | |
| lastSeen: Date.now(), | |
| state: "TRACK" | |
| }; | |
| }); | |
| // Update state | |
| state.tracker.tracks = newTracks; | |
| state.detections = newTracks; // Keep synced | |
| } catch (e) { | |
| console.warn("Track sync failed", e); | |
| } | |
| }; | |
| APP.core.tracker.predictTracks = function (dtSec) { | |
| const { state } = APP.core; | |
| const { $ } = APP.core.utils; | |
| const videoEngage = $("#videoEngage"); | |
| if (!videoEngage) return; | |
| const w = videoEngage.videoWidth || state.frame.w; | |
| const h = videoEngage.videoHeight || state.frame.h; | |
| // Simple clamp util locally or imported | |
| const clamp = (val, min, max) => Math.min(max, Math.max(min, val)); | |
| state.tracker.tracks.forEach(tr => { | |
| if (tr.killed) return; | |
| tr.bbox.x = clamp(tr.bbox.x + tr.vx * dtSec * 0.12, 0, w - 1); | |
| tr.bbox.y = clamp(tr.bbox.y + tr.vy * dtSec * 0.12, 0, h - 1); | |
| }); | |
| }; | |