timeline
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { SSEDataSource, Panel, ResizeHandle } from 'mpr-ui-framework'
|
||||
import 'mpr-ui-framework/src/tokens.css'
|
||||
import LogPanel from './panels/LogPanel.vue'
|
||||
@@ -11,8 +11,10 @@ import TimelinePanel from './panels/TimelinePanel.vue'
|
||||
import CostStatsPanel from './panels/CostStatsPanel.vue'
|
||||
import SourceSelector from './panels/SourceSelector.vue'
|
||||
import StageConfigSliders from './components/StageConfigSliders.vue'
|
||||
import FrameStrip from './components/FrameStrip.vue'
|
||||
import type { StatsUpdate, RunContext } from './types/sse-contract'
|
||||
import type { FrameOverlay } from 'mpr-ui-framework/src/renderers/FrameRenderer.vue'
|
||||
import { matchTracks, renderTracksToImageData, imageDataToPngB64 } from 'mpr-ui-framework/src/cv'
|
||||
import { usePipelineStore } from './stores/pipeline'
|
||||
|
||||
const pipeline = usePipelineStore()
|
||||
@@ -127,6 +129,18 @@ const checkpointFrames = ref<{ seq: number; timestamp: number; jpeg_b64: string
|
||||
const checkpointFrameIndex = ref(0)
|
||||
const checkpointStage = ref<string | null>(null) // which stage the checkpoint is at
|
||||
|
||||
// Frame strip selection (indices into checkpointFrames)
|
||||
const stripSelStart = ref(0)
|
||||
const stripSelEnd = computed(() =>
|
||||
stripSelEndOverride.value ?? Math.max(0, checkpointFrames.value.length - 1),
|
||||
)
|
||||
const stripSelEndOverride = ref<number | null>(null)
|
||||
|
||||
// Per-frame CV results — accumulated across all processed frames
|
||||
const allFrameRegions = ref<Record<number, Array<{ x: number; y: number; w: number; h: number; confidence: number; label: string }>>>({})
|
||||
const allFrameDebug = ref<Record<number, { edge_overlay_b64: string; lines_overlay_b64: string }>>({})
|
||||
const frameDimensions = ref<{ w: number; h: number } | null>(null)
|
||||
|
||||
|
||||
source.on<{ frame_ref: number; jpeg_b64: string }>('frame_update', (e) => {
|
||||
currentFrameImage.value = e.jpeg_b64
|
||||
@@ -151,6 +165,13 @@ async function loadCheckpoint(job: string, stage: string) {
|
||||
currentFrameRef.value = first.seq
|
||||
}
|
||||
|
||||
// Reset accumulated CV results and strip selection for this checkpoint
|
||||
allFrameRegions.value = {}
|
||||
allFrameDebug.value = {}
|
||||
frameDimensions.value = null
|
||||
stripSelStart.value = 0
|
||||
stripSelEndOverride.value = null
|
||||
|
||||
status.value = 'idle'
|
||||
} catch (e) {
|
||||
console.error('Failed to load checkpoint:', e)
|
||||
@@ -163,6 +184,61 @@ function setCheckpointFrame(index: number) {
|
||||
const frame = checkpointFrames.value[index]
|
||||
currentFrameImage.value = frame.jpeg_b64
|
||||
currentFrameRef.value = frame.seq
|
||||
updateDisplayForFrame(frame.seq)
|
||||
}
|
||||
|
||||
function updateDisplayForFrame(seq: number) {
|
||||
// Update boxes for this frame from accumulated results
|
||||
const regions = allFrameRegions.value[seq] ?? []
|
||||
editorBoxes.value = regions.map(r => ({
|
||||
x: r.x, y: r.y, w: r.w, h: r.h,
|
||||
confidence: r.confidence,
|
||||
label: r.label ?? 'edge_region',
|
||||
stage: 'detect_edges',
|
||||
}))
|
||||
|
||||
// Update Canny/Hough overlays for this frame (preserving visibility/opacity)
|
||||
const debug = allFrameDebug.value[seq]
|
||||
if (debug) {
|
||||
const overlays: FrameOverlay[] = []
|
||||
if (debug.edge_overlay_b64) {
|
||||
const existing = editorOverlays.value.find(o => o.label === 'Canny edges')
|
||||
overlays.push({ src: debug.edge_overlay_b64, label: 'Canny edges', visible: existing?.visible ?? true, opacity: existing?.opacity ?? 0.25 })
|
||||
}
|
||||
if (debug.lines_overlay_b64) {
|
||||
const existing = editorOverlays.value.find(o => o.label === 'Hough lines')
|
||||
overlays.push({ src: debug.lines_overlay_b64, label: 'Hough lines', visible: existing?.visible ?? true, opacity: existing?.opacity ?? 0.25 })
|
||||
}
|
||||
// Re-append track overlay if it exists
|
||||
const trackOverlay = editorOverlays.value.find(o => o.label === 'Motion tracks')
|
||||
if (trackOverlay) overlays.push(trackOverlay)
|
||||
editorOverlays.value = overlays
|
||||
}
|
||||
|
||||
// Re-render track overlay with updated currentSeq
|
||||
if (Object.keys(allFrameRegions.value).length >= 2 && frameDimensions.value) {
|
||||
updateTrackOverlay(seq)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTrackOverlay(currentSeq: number) {
|
||||
const dims = frameDimensions.value
|
||||
if (!dims || Object.keys(allFrameRegions.value).length < 2) return
|
||||
const tracks = matchTracks(allFrameRegions.value)
|
||||
const imageData = renderTracksToImageData(tracks, dims.w, dims.h, currentSeq)
|
||||
const b64 = await imageDataToPngB64(imageData)
|
||||
const existing = editorOverlays.value.find(o => o.label === 'Motion tracks')
|
||||
const trackOverlay: FrameOverlay = {
|
||||
src: b64,
|
||||
label: 'Motion tracks',
|
||||
visible: existing?.visible ?? true,
|
||||
opacity: existing?.opacity ?? 0.9,
|
||||
srcFormat: 'png',
|
||||
}
|
||||
editorOverlays.value = [
|
||||
...editorOverlays.value.filter(o => o.label !== 'Motion tracks'),
|
||||
trackOverlay,
|
||||
]
|
||||
}
|
||||
|
||||
// Load checkpoint when in editor mode with a job (scenario URL)
|
||||
@@ -194,35 +270,34 @@ const editorBoxes = ref<import('mpr-ui-framework/src/renderers/FrameRenderer.vue
|
||||
function onReplayResult(result: {
|
||||
regions_by_frame?: Record<string, unknown[]>
|
||||
debug?: Record<string, { edge_overlay_b64: string; lines_overlay_b64: string; horizontal_count: number; pair_count: number }>
|
||||
frameWidth?: number
|
||||
frameHeight?: number
|
||||
}) {
|
||||
// Update boxes
|
||||
if (result.regions_by_frame) {
|
||||
const firstRegions = Object.values(result.regions_by_frame)[0] as any[] ?? []
|
||||
editorBoxes.value = firstRegions.map((r: any) => ({
|
||||
x: r.x, y: r.y, w: r.w, h: r.h,
|
||||
confidence: r.confidence,
|
||||
label: r.label ?? 'edge_region',
|
||||
stage: 'detect_edges',
|
||||
}))
|
||||
// Store frame dimensions for track overlay rendering
|
||||
if (result.frameWidth && result.frameHeight) {
|
||||
frameDimensions.value = { w: result.frameWidth, h: result.frameHeight }
|
||||
}
|
||||
|
||||
// Update overlays — only when debug data is present, preserve existing otherwise
|
||||
if (result.debug) {
|
||||
const firstDebug = Object.values(result.debug)[0]
|
||||
if (firstDebug) {
|
||||
const overlays: FrameOverlay[] = []
|
||||
if (firstDebug.edge_overlay_b64) {
|
||||
// Preserve visibility/opacity from existing overlays if they exist
|
||||
const existing = editorOverlays.value.find(o => o.label === 'Canny edges')
|
||||
overlays.push({ src: firstDebug.edge_overlay_b64, label: 'Canny edges', visible: existing?.visible ?? true, opacity: existing?.opacity ?? 0.25 })
|
||||
}
|
||||
if (firstDebug.lines_overlay_b64) {
|
||||
const existing = editorOverlays.value.find(o => o.label === 'Hough lines')
|
||||
overlays.push({ src: firstDebug.lines_overlay_b64, label: 'Hough lines', visible: existing?.visible ?? true, opacity: existing?.opacity ?? 0.25 })
|
||||
}
|
||||
editorOverlays.value = overlays
|
||||
// Merge incoming per-frame regions into accumulated store
|
||||
if (result.regions_by_frame) {
|
||||
for (const [seqStr, regions] of Object.entries(result.regions_by_frame)) {
|
||||
allFrameRegions.value[Number(seqStr)] = regions as any[]
|
||||
}
|
||||
}
|
||||
|
||||
// Merge incoming per-frame debug overlays into accumulated store
|
||||
if (result.debug) {
|
||||
for (const [seqStr, dbg] of Object.entries(result.debug)) {
|
||||
allFrameDebug.value[Number(seqStr)] = {
|
||||
edge_overlay_b64: dbg.edge_overlay_b64,
|
||||
lines_overlay_b64: dbg.lines_overlay_b64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update display for the currently shown frame
|
||||
const currentSeq = currentFrameRef.value ?? 0
|
||||
updateDisplayForFrame(currentSeq)
|
||||
}
|
||||
|
||||
function onJobStarted(newJobId: string) {
|
||||
@@ -338,10 +413,25 @@ function onJobStarted(newJobId: string) {
|
||||
:job-id="jobId"
|
||||
:frame-image="currentFrameImage"
|
||||
:frame-ref="currentFrameRef"
|
||||
:frames="checkpointFrames"
|
||||
:selection-start="stripSelStart"
|
||||
:selection-end="stripSelEnd"
|
||||
@replay-result="onReplayResult"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Frame strip: thumbnails + selection handles -->
|
||||
<FrameStrip
|
||||
v-if="checkpointFrames.length > 0"
|
||||
:frames="checkpointFrames"
|
||||
:current-index="checkpointFrameIndex"
|
||||
:selection-start="stripSelStart"
|
||||
:selection-end="stripSelEnd"
|
||||
@frame-click="setCheckpointFrame"
|
||||
@selection-change="(s, e) => { stripSelStart.value = s; stripSelEndOverride.value = e }"
|
||||
/>
|
||||
|
||||
<!-- Bottom: debug overlays + close -->
|
||||
<div class="editor-bottom">
|
||||
<div class="overlay-controls">
|
||||
|
||||
Reference in New Issue
Block a user