batch predict & disk cache and ui improvements
This commit is contained in:
parent
ce2116733e
commit
3e2f695c08
181
src/App.vue
181
src/App.vue
@ -8,6 +8,11 @@ const frames = ref<VideoFrame[]>([]);
|
|||||||
|
|
||||||
const rawCanvas = useTemplateRef('raw-canvas');
|
const rawCanvas = useTemplateRef('raw-canvas');
|
||||||
const processedCanvas = useTemplateRef('processed-canvas');
|
const processedCanvas = useTemplateRef('processed-canvas');
|
||||||
|
const decodeProgress = ref(0);
|
||||||
|
const trackProgress = ref(0);
|
||||||
|
const predictProgress = ref(0);
|
||||||
|
|
||||||
|
const showActual = ref(false);
|
||||||
|
|
||||||
let rawCtx: CanvasRenderingContext2D;
|
let rawCtx: CanvasRenderingContext2D;
|
||||||
let processedCtx: CanvasRenderingContext2D;
|
let processedCtx: CanvasRenderingContext2D;
|
||||||
@ -16,14 +21,25 @@ onMounted(() => {
|
|||||||
processedCtx = processedCanvas.value!.getContext('2d')!;
|
processedCtx = processedCanvas.value!.getContext('2d')!;
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxFrames = 128;
|
const maxFrames = 512;
|
||||||
const seqLen = 120;
|
const seqLen = 120;
|
||||||
|
|
||||||
const decodeProgress = ref(0);
|
|
||||||
const trackProgress = ref(0);
|
|
||||||
|
|
||||||
const centerPosArr = ref<({ x: number; y: number } | null)[]>([]);
|
const centerPosArr = ref<({ x: number; y: number } | null)[]>([]);
|
||||||
const predictedPosArr = ref<[number, number][][]>([]);
|
const predictedPosArr = ref<[number, number][][]>([]);
|
||||||
|
|
||||||
|
// 计算序列的稳定哈希,用作 localStorage 的键
|
||||||
|
const MAX_BATCH = 64;
|
||||||
|
function hashSequence(seq: Array<[number, number]>): string {
|
||||||
|
const s = JSON.stringify(seq);
|
||||||
|
let h = 5381;
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
h = ((h << 5) + h) + s.charCodeAt(i); // h*33 + c
|
||||||
|
h |= 0; // 32-bit
|
||||||
|
}
|
||||||
|
// 仅使用哈希值本身作为键名,避免与其它缓存键冲突的概率
|
||||||
|
return (h >>> 0).toString(16);
|
||||||
|
}
|
||||||
async function handleFileChange(event: Event) {
|
async function handleFileChange(event: Event) {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
if (input.files && input.files[0]) {
|
if (input.files && input.files[0]) {
|
||||||
@ -49,15 +65,19 @@ async function handleFileChange(event: Event) {
|
|||||||
let lastPos = null
|
let lastPos = null
|
||||||
|
|
||||||
for (let index = 0; index < frames.value.length; index++) {
|
for (let index = 0; index < frames.value.length; index++) {
|
||||||
|
const cacheKeyname = `${file.name}-${index}`;
|
||||||
|
if (localStorage.getItem(cacheKeyname)) {
|
||||||
|
centerPosArr.value[index] = JSON.parse(localStorage.getItem(cacheKeyname)!) as { x: number; y: number } | null;
|
||||||
|
trackProgress.value++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
selectedIndex.value = index;
|
selectedIndex.value = index;
|
||||||
await reqNextFrame();
|
await reqNextFrame();
|
||||||
|
|
||||||
const center = tracker.detectYellowBall(rawCtx.getImageData(0, 0, w, h), lastPos);
|
const center = tracker.detectYellowBall(rawCtx.getImageData(0, 0, w, h), lastPos);
|
||||||
centerPosArr.value[index] = center;
|
centerPosArr.value[index] = center;
|
||||||
lastPos = center;
|
lastPos = center;
|
||||||
|
localStorage.setItem(cacheKeyname, JSON.stringify(center));
|
||||||
console.log(center);
|
|
||||||
|
|
||||||
|
|
||||||
trackProgress.value++;
|
trackProgress.value++;
|
||||||
}
|
}
|
||||||
@ -72,27 +92,65 @@ async function handleFileChange(event: Event) {
|
|||||||
console.warn("视频帧数不足,无法预测");
|
console.warn("视频帧数不足,无法预测");
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (let index = firstAvailableIndex; index < centerPosArr.value.length - seqLen; index++) {
|
// 准备任务清单:每个任务 = { seq, key, outIndex }
|
||||||
const seq = centerPosArr.value.slice(index, index + seqLen).map(p => p ? [p.x, p.y] : [0, 0]);
|
type Task = { seq: Array<[number, number]>; key: string; outIndex: number };
|
||||||
|
const tasks: Task[] = [];
|
||||||
|
|
||||||
|
const lastStart = centerPosArr.value.length - seqLen; // 不包含
|
||||||
|
for (let start = firstAvailableIndex; start < lastStart; start++) {
|
||||||
|
const seq_: Array<[number, number]> = centerPosArr.value
|
||||||
|
.slice(start, start + seqLen)
|
||||||
|
.map(p => p ? [p.x, p.y] as [number, number] : [0, 0]);
|
||||||
|
const key = hashSequence(seq_);
|
||||||
|
const outIndex = start + seqLen; // 预测对应的“当前帧”索引
|
||||||
|
tasks.push({ seq: seq_, key, outIndex });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先尝试本地缓存命中
|
||||||
|
const pending: Task[] = [];
|
||||||
|
for (const t of tasks) {
|
||||||
|
const cached = localStorage.getItem(t.key);
|
||||||
|
if (cached) {
|
||||||
|
try {
|
||||||
|
const pred: [number, number][] = JSON.parse(cached);
|
||||||
|
predictedPosArr.value[t.outIndex] = pred;
|
||||||
|
// 进度按已完成的最大下标推进
|
||||||
|
predictProgress.value = Math.max(predictProgress.value, t.outIndex);
|
||||||
|
} catch { pending.push(t); }
|
||||||
|
} else {
|
||||||
|
pending.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分批请求后端,最多 64 条/批
|
||||||
|
for (let i = 0; i < pending.length; i += MAX_BATCH) {
|
||||||
|
const batch = pending.slice(i, i + MAX_BATCH);
|
||||||
const body = {
|
const body = {
|
||||||
sequences: [seq],
|
sequences: batch.map(b => b.seq),
|
||||||
steps: 30,
|
steps: 30,
|
||||||
return_angles: true,
|
return_angles: true,
|
||||||
unwrap_from_last: true
|
unwrap_from_last: true
|
||||||
}
|
};
|
||||||
|
|
||||||
const resp = await fetch("http://127.0.0.1:8000/predict", {
|
const resp = await fetch("http://127.0.0.1:8000/predict", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
});
|
});
|
||||||
const json = await resp.json();
|
const json = await resp.json();
|
||||||
console.log(json);
|
const preds: [number, number][][] = json.pred_xy || [];
|
||||||
|
|
||||||
predictedPosArr.value[index + seqLen] = json.pred_xy[0];
|
|
||||||
|
|
||||||
|
// 将结果回填到对应位置并写入缓存
|
||||||
|
for (let j = 0; j < batch.length; j++) {
|
||||||
|
const t = batch[j];
|
||||||
|
const pred = preds[j];
|
||||||
|
if (!pred) continue;
|
||||||
|
predictedPosArr.value[t.outIndex] = pred;
|
||||||
|
localStorage.setItem(t.key, JSON.stringify(pred))
|
||||||
|
predictProgress.value = Math.max(predictProgress.value, t.outIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +189,7 @@ function renderBaseFrame(index: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawPredictionPartial(preds: Array<[number, number]>, count: number) {
|
function drawPredictionPartial(preds: Array<[number, number]>, actual: Array<[number, number]>, count: number) {
|
||||||
if (!preds.length) return;
|
if (!preds.length) return;
|
||||||
const [sx, sy] = preds[0];
|
const [sx, sy] = preds[0];
|
||||||
const [ex, ey] = preds[preds.length - 1];
|
const [ex, ey] = preds[preds.length - 1];
|
||||||
@ -166,6 +224,30 @@ function drawPredictionPartial(preds: Array<[number, number]>, count: number) {
|
|||||||
processedCtx.lineWidth = 4;
|
processedCtx.lineWidth = 4;
|
||||||
processedCtx.arc(preds[last][0], preds[last][1], 20, 0, Math.PI * 2);
|
processedCtx.arc(preds[last][0], preds[last][1], 20, 0, Math.PI * 2);
|
||||||
processedCtx.stroke();
|
processedCtx.stroke();
|
||||||
|
|
||||||
|
if (!showActual.value) return
|
||||||
|
const gradient2 = processedCtx.createLinearGradient(sx, sy, ex, ey);
|
||||||
|
gradient2.addColorStop(0, 'rgba(255, 255, 0, 0.95)');
|
||||||
|
gradient2.addColorStop(1, 'rgba(255, 255, 0, 0.3)');
|
||||||
|
processedCtx.beginPath();
|
||||||
|
processedCtx.lineWidth = 12;
|
||||||
|
processedCtx.lineCap = 'round';
|
||||||
|
processedCtx.lineJoin = 'round';
|
||||||
|
processedCtx.moveTo(sx, sy);
|
||||||
|
for (let i = 1; i <= last; i++) {
|
||||||
|
let pos = actual[i]
|
||||||
|
if (!pos) break
|
||||||
|
const [x, y] = pos;
|
||||||
|
processedCtx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
processedCtx.strokeStyle = gradient2;
|
||||||
|
processedCtx.stroke();
|
||||||
|
// 在终点画一个黄色小球
|
||||||
|
processedCtx.beginPath();
|
||||||
|
processedCtx.strokeStyle = 'yellow';
|
||||||
|
processedCtx.lineWidth = 4;
|
||||||
|
processedCtx.arc(actual[last][0], actual[last][1], 20, 0, Math.PI * 2);
|
||||||
|
processedCtx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
function animatePrediction(index: number) {
|
function animatePrediction(index: number) {
|
||||||
@ -183,10 +265,11 @@ function animatePrediction(index: number) {
|
|||||||
const elapsed = now - animStartTs;
|
const elapsed = now - animStartTs;
|
||||||
// 目标画到的点下标(包含起点),至少为 1(起点),至多为 preds.length - 1
|
// 目标画到的点下标(包含起点),至少为 1(起点),至多为 preds.length - 1
|
||||||
const targetIdx = Math.min(preds.length - 1, Math.floor(elapsed / perSeg) + 1);
|
const targetIdx = Math.min(preds.length - 1, Math.floor(elapsed / perSeg) + 1);
|
||||||
|
const actual: [number, number][] = centerPosArr.value.slice(index, index + targetIdx + 1).map(p => p ? [p.x, p.y] as [number, number] : [0, 0]);
|
||||||
|
|
||||||
// 每帧重绘背景与部分轨迹
|
// 每帧重绘背景与部分轨迹
|
||||||
renderBaseFrame(index);
|
renderBaseFrame(index);
|
||||||
drawPredictionPartial(preds, targetIdx);
|
drawPredictionPartial(preds, actual, targetIdx);
|
||||||
|
|
||||||
if (targetIdx < preds.length - 1) {
|
if (targetIdx < preds.length - 1) {
|
||||||
animReqId = requestAnimationFrame(step);
|
animReqId = requestAnimationFrame(step);
|
||||||
@ -198,6 +281,8 @@ function animatePrediction(index: number) {
|
|||||||
animReqId = requestAnimationFrame(step);
|
animReqId = requestAnimationFrame(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
watch(selectedIndex, async (newIndex) => {
|
watch(selectedIndex, async (newIndex) => {
|
||||||
if (!rawCanvas.value || !processedCanvas.value) return;
|
if (!rawCanvas.value || !processedCanvas.value) return;
|
||||||
if (!rawCtx || !processedCtx) return;
|
if (!rawCtx || !processedCtx) return;
|
||||||
@ -268,27 +353,45 @@ const predRangeStyle = computed<CSSProperties>(() => {
|
|||||||
|
|
||||||
const currentCursorStyle = computed<CSSProperties>(() => {
|
const currentCursorStyle = computed<CSSProperties>(() => {
|
||||||
if (framesLen.value === 0) return { left: '0%' };
|
if (framesLen.value === 0) return { left: '0%' };
|
||||||
return { left: `${toPercent(currentIndex.value)}%` };
|
return { left: `${toPercent(currentIndex.value - 0.5)}%` };
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<h1 style="font-size: larger;">基于 Transformer 的单摆轨迹预测模型</h1>
|
||||||
<div>
|
<div>
|
||||||
<input type="file" name="video" id="video" @change="handleFileChange"
|
<input type="file" name="video" id="video" @change="handleFileChange"
|
||||||
accept="video/*" />
|
accept="video/*" /><br>
|
||||||
解码:<progress :value="decodeProgress" :max="1"></progress>
|
解码:<progress :value="decodeProgress" :max="1"></progress><br>
|
||||||
追踪:<progress :value="trackProgress" :max="frames.length - 1"></progress>
|
追踪:<progress :value="trackProgress" :max="frames.length - 1"></progress><br>
|
||||||
<input type="range" min="0" :max="frames.length - 1" style="width: 100%;"
|
预测:<progress :value="predictProgress"
|
||||||
v-model="selectedIndex" />
|
:max="frames.length - 1"></progress><br>
|
||||||
|
|
||||||
|
显示实际:<input type="checkbox" name="" id="" v-model="showActual">
|
||||||
|
<span style="color: gray;font-size: small;float: inline-end;">
|
||||||
|
<span v-if="showActual">黄色为实际运动轨迹,</span>
|
||||||
|
<span>蓝色为预测运动轨迹</span>
|
||||||
|
</span>
|
||||||
<!-- 自定义时间轴:淡蓝色=用于预测的输入序列,绿色渐变=预测范围,红线=当前帧位置 -->
|
<!-- 自定义时间轴:淡蓝色=用于预测的输入序列,绿色渐变=预测范围,红线=当前帧位置 -->
|
||||||
<div class="timeline" aria-label="prediction timeline">
|
<canvas ref="raw-canvas" style="display: none;" />
|
||||||
<div class="range seq" :style="seqRangeStyle"></div>
|
|
||||||
<div class="range pred" :style="predRangeStyle"></div>
|
|
||||||
<div class="cursor" :style="currentCursorStyle"></div>
|
|
||||||
</div>
|
|
||||||
<canvas ref="raw-canvas" style="display: none;"/>
|
|
||||||
<canvas ref="processed-canvas" />
|
<canvas ref="processed-canvas" />
|
||||||
|
<div class="timeline" aria-label="prediction timeline">
|
||||||
|
<div class="range seq" :style="seqRangeStyle">参考</div>
|
||||||
|
<div class="range pred" :style="predRangeStyle">预测</div>
|
||||||
|
<div class="cursor" :style="currentCursorStyle"> </div>
|
||||||
|
</div>
|
||||||
|
<div class="index-indicator">
|
||||||
|
<span>0</span>
|
||||||
|
<span :style="currentCursorStyle"
|
||||||
|
style="position: absolute;transform: translateX(-50%);">{{ currentIndex
|
||||||
|
}}</span>
|
||||||
|
<span>{{ frames.length - 1 }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="range" min="0" :max="frames.length - 1" style="width: 100%;"
|
||||||
|
:value="selectedIndex"
|
||||||
|
@input="(e) => selectedIndex = Number((e.target as HTMLInputElement).value)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -296,6 +399,7 @@ const currentCursorStyle = computed<CSSProperties>(() => {
|
|||||||
canvas {
|
canvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
@ -304,11 +408,18 @@ canvas {
|
|||||||
background: #eeeeee;
|
background: #eeeeee;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline .range {
|
.timeline .range {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline .cursor {
|
.timeline .cursor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -3px;
|
top: -3px;
|
||||||
@ -316,4 +427,14 @@ canvas {
|
|||||||
width: 2px;
|
width: 2px;
|
||||||
background: #ff3b30;
|
background: #ff3b30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.index-indicator {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user