Files
mediaproc/ui/framework/src/renderers/FrameRenderer.vue
2026-03-26 22:22:35 -03:00

177 lines
4.8 KiB
Vue

<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
export interface FrameBBox {
x: number
y: number
w: number
h: number
confidence: number
label: string
resolved_brand?: string | null
source?: string | null
stage?: string | null
ocr_text?: string | null
}
export interface FrameOverlay {
/** Base64 JPEG image (same dimensions as main image) */
src: string
label: string
visible: boolean
/** Opacity 0-1, default 0.5 */
opacity?: number
}
const props = defineProps<{
/** Base64 JPEG image */
imageSrc: string
/** Bounding boxes to overlay */
boxes: FrameBBox[]
/** Debug overlay layers (edge images, line visualizations, etc.) */
overlays?: FrameOverlay[]
}>()
const canvas = ref<HTMLCanvasElement | null>(null)
const container = ref<HTMLElement | null>(null)
function draw() {
const cvs = canvas.value
const ctr = container.value
if (!cvs || !ctr || !props.imageSrc) return
const ctx = cvs.getContext('2d')
if (!ctx) return
const img = new window.Image()
img.onload = () => {
cvs.width = ctr.clientWidth
cvs.height = ctr.clientHeight
const scale = Math.min(cvs.width / img.width, cvs.height / img.height)
const dx = (cvs.width - img.width * scale) / 2
const dy = (cvs.height - img.height * scale) / 2
ctx.clearRect(0, 0, cvs.width, cvs.height)
ctx.drawImage(img, dx, dy, img.width * scale, img.height * scale)
// Draw debug overlays (edge images, line visualizations)
drawOverlays(ctx, dx, dy, img.width * scale, img.height * scale)
// Draw bounding boxes on top
for (const box of props.boxes) {
const bx = dx + box.x * scale
const by = dy + box.y * scale
const bw = box.w * scale
const bh = box.h * scale
const color = sourceColor(box)
const resolved = box.resolved_brand || box.ocr_text
ctx.strokeStyle = color
ctx.lineWidth = 2
if (!resolved) {
ctx.setLineDash([4, 3])
}
ctx.strokeRect(bx, by, bw, bh)
ctx.setLineDash([])
}
}
img.src = `data:image/jpeg;base64,${props.imageSrc}`
}
/** Pending overlay images that need async loading */
const overlayCache = new Map<string, HTMLImageElement>()
function drawOverlays(ctx: CanvasRenderingContext2D, dx: number, dy: number, dw: number, dh: number) {
const layers = props.overlays ?? []
for (const layer of layers) {
if (!layer.visible || !layer.src) continue
const cached = overlayCache.get(layer.src)
if (cached && cached.complete) {
ctx.globalAlpha = layer.opacity ?? 0.5
ctx.drawImage(cached, dx, dy, dw, dh)
ctx.globalAlpha = 1.0
} else if (!cached) {
// Load async, redraw when ready
const overlay = new window.Image()
overlay.onload = () => draw()
overlay.src = `data:image/jpeg;base64,${layer.src}`
overlayCache.set(layer.src, overlay)
}
}
}
const SOURCE_COLORS: Record<string, string> = {
yolo: '#f5a623', // yellow — raw detection
ocr: '#ff8c42', // orange — text extracted
ocr_matched: '#3ecf8e', // green — brand resolved
local_vlm: '#4f9cf9', // blue — VLM resolved
cloud_llm: '#a78bfa', // purple — cloud resolved
unresolved: '#e05252', // red — nothing matched
}
// CV region labels — distinct from source-based colors
const REGION_COLORS: Record<string, string> = {
edge_region: '#00bcd4', // cyan
contour_region: '#ffd54f', // yellow
color_region: '#e040fb', // magenta
candidate: '#4caf50', // green — passed readability
rejected: '#e05252', // red — failed readability
}
function sourceColor(box: FrameBBox): string {
if (REGION_COLORS[box.label]) return REGION_COLORS[box.label]
if (box.resolved_brand) return SOURCE_COLORS.ocr_matched
if (box.source && SOURCE_COLORS[box.source]) return SOURCE_COLORS[box.source]
return confidenceColor(box.confidence)
}
function confidenceColor(conf: number): string {
if (conf >= 0.7) return 'var(--conf-high)'
if (conf >= 0.4) return 'var(--conf-mid)'
return 'var(--conf-low)'
}
watch(() => [props.imageSrc, props.boxes, props.overlays], () => nextTick(draw), { deep: true })
onMounted(() => {
nextTick(draw)
const observer = new ResizeObserver(() => draw())
if (container.value) observer.observe(container.value)
onUnmounted(() => observer.disconnect())
})
</script>
<template>
<div ref="container" class="frame-renderer">
<canvas ref="canvas" />
<div v-if="!imageSrc" class="frame-empty">No frame</div>
</div>
</template>
<style scoped>
.frame-renderer {
width: 100%;
height: 100%;
min-height: 200px;
position: relative;
}
.frame-renderer canvas {
display: block;
width: 100%;
height: 100%;
}
.frame-empty {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-dim);
}
</style>