FADA-Mobile / js /visualizer.js
mshz88's picture
Upload folder using huggingface_hub
222e211 verified
/**
* Canvas-based annotation renderer.
* Ported from hf_space/visualizer.py (PIL -> Canvas 2D).
*/
import { CLASS_COLORS, _RAW_PALETTE } from "./constants.js";
function hexToRgb(hex) {
hex = hex.replace("#", "");
return [parseInt(hex.slice(0,2),16), parseInt(hex.slice(2,4),16), parseInt(hex.slice(4,6),16)];
}
function getColor(label) {
let hex = CLASS_COLORS[label];
if (!hex) hex = _RAW_PALETTE[Math.abs(hashCode(label)) % _RAW_PALETTE.length];
return hexToRgb(hex);
}
function hashCode(s) {
let h = 0;
for (let i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0;
return h;
}
/**
* Draw annotations on a canvas.
* @param {HTMLCanvasElement} canvas - Target canvas (must be sized to image dimensions).
* @param {HTMLImageElement|ImageBitmap} image - Source image.
* @param {object} parsed - Rescaled ParsedOutput with pixel coordinates.
* @param {object} opts - { lineWidth, fontSize, maskAlpha }
*/
export function drawAnnotations(canvas, image, parsed, opts = {}) {
const lw = opts.lineWidth || 3;
const fs = opts.fontSize || 14;
const maskAlpha = opts.maskAlpha || 0.25;
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
// Detections: solid bounding boxes
for (const det of parsed.detections) {
const [r,g,b] = getColor(det.label);
const [x1,y1,x2,y2] = det.bbox;
ctx.strokeStyle = `rgb(${r},${g},${b})`;
ctx.lineWidth = lw;
ctx.setLineDash([]);
ctx.strokeRect(x1, y1, x2-x1, y2-y1);
drawLabel(ctx, det.label, x1, y1, [r,g,b], fs);
}
// Segmentations: filled polygon + dashed bbox
for (const seg of parsed.segmentations) {
const [r,g,b] = getColor(seg.label);
const [x1,y1,x2,y2] = seg.bbox;
// Filled polygon
if (seg.polygon && seg.polygon.length >= 3) {
ctx.fillStyle = `rgba(${r},${g},${b},${maskAlpha})`;
ctx.strokeStyle = `rgb(${r},${g},${b})`;
ctx.lineWidth = 1;
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(seg.polygon[0][0], seg.polygon[0][1]);
for (let i = 1; i < seg.polygon.length; i++) ctx.lineTo(seg.polygon[i][0], seg.polygon[i][1]);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// Dashed bbox
ctx.strokeStyle = `rgb(${r},${g},${b})`;
ctx.lineWidth = lw;
ctx.setLineDash([10, 6]);
ctx.strokeRect(x1, y1, x2-x1, y2-y1);
ctx.setLineDash([]);
drawLabel(ctx, seg.label, x1, y1, [r,g,b], fs);
}
// Keypoints
for (const kp of parsed.keypoints) {
const [r,g,b] = getColor(kp.label);
const [x1,y1,x2,y2] = kp.bbox;
// Dotted bbox
ctx.strokeStyle = `rgb(${r},${g},${b})`;
ctx.lineWidth = lw;
ctx.setLineDash([6, 4]);
ctx.strokeRect(x1, y1, x2-x1, y2-y1);
ctx.setLineDash([]);
drawLabel(ctx, kp.label, x1, y1, [r,g,b], fs);
const rad = Math.max(3, Math.min(canvas.width, canvas.height) / 150);
const visPts = [];
kp.keypoints.forEach(([kx, ky, vis], idx) => {
if (vis === 0) return;
visPts.push([kx, ky]);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(kx, ky, rad, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.font = `${fs}px sans-serif`;
ctx.fillText(String(idx + 1), kx + rad + 2, ky - rad + fs);
});
// Connect consecutive visible keypoints
if (visPts.length >= 2) {
ctx.strokeStyle = `rgb(${r},${g},${b})`;
ctx.lineWidth = Math.max(1, lw - 1);
ctx.beginPath();
ctx.moveTo(visPts[0][0], visPts[0][1]);
for (let i = 1; i < visPts.length; i++) ctx.lineTo(visPts[i][0], visPts[i][1]);
ctx.stroke();
}
}
// Classification labels at top
let yOff = 10;
ctx.font = `bold ${fs}px sans-serif`;
for (const cls of parsed.classifications) {
const [r,g,b] = getColor(cls.label);
const text = `View: ${cls.label}`;
const tm = ctx.measureText(text);
const pad = 4;
ctx.fillStyle = "rgba(50,50,50,0.78)";
ctx.fillRect(10 - pad, yOff - pad, tm.width + 2*pad, fs + 2*pad);
ctx.fillStyle = "white";
ctx.fillText(text, 10, yOff + fs);
yOff += fs + 2*pad + 6;
}
}
function drawLabel(ctx, label, x, y, [r,g,b], fs) {
ctx.font = `bold ${fs}px sans-serif`;
const tm = ctx.measureText(label);
const pad = 3;
const ly = Math.max(0, y - fs - 2*pad);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x - pad, ly, tm.width + 2*pad, fs + 2*pad);
ctx.fillStyle = "white";
ctx.fillText(label, x, ly + fs + pad);
}