31 lines
3.2 KiB
JavaScript
31 lines
3.2 KiB
JavaScript
import {dataUrl} from './images.mjs';
|
|
|
|
const escape = (value) => String(value).replace(/[&<>"']/gu, (c) => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c]));
|
|
|
|
export function annotatedSvg(image, parts) {
|
|
const {width: w, height: h} = image;
|
|
const stroke = Math.max(w, h) / 400;
|
|
const font = Math.max(w, h) / 65;
|
|
const byId = new Map(parts.map((part) => [part.id, part]));
|
|
const overlays = parts.map((part, index) => {
|
|
const color = `hsl(${(index * 137.5) % 360} 85% 55%)`;
|
|
const [px, py] = [part.pivot_norm[0] * w, part.pivot_norm[1] * h];
|
|
const parent = byId.get(part.parent);
|
|
const line = parent ? `<line x1="${px}" y1="${py}" x2="${parent.pivot_norm[0] * w}" y2="${parent.pivot_norm[1] * h}" stroke-dasharray="${stroke * 3}"/>` : '';
|
|
let box = '';
|
|
if (part.bbox_norm_xyxy) {
|
|
const [x0, y0, x1, y1] = part.bbox_norm_xyxy;
|
|
box = `<rect x="${x0 * w}" y="${y0 * h}" width="${(x1 - x0) * w}" height="${(y1 - y0) * h}"/>`;
|
|
}
|
|
const points = part.positive_points_norm.map(([x, y]) => `<circle cx="${x * w}" cy="${y * h}" r="${stroke * 1.5}" fill="${color}"/>`).join('');
|
|
return `<g fill="none" stroke="${color}" stroke-width="${stroke}">${line}${box}${points}<circle cx="${px}" cy="${py}" r="${stroke * 3}"/><text x="${Math.min(px + 4, w * 0.6)}" y="${Math.max(font, py - 5)}" fill="${color}" stroke="#111" stroke-width="${stroke / 2}" paint-order="stroke" font-size="${font}" font-family="sans-serif">${escape(part.id)}</text></g>`;
|
|
}).join('\n');
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><rect width="100%" height="100%" fill="#262632"/><image href="${dataUrl(image)}" width="${w}" height="${h}"/>${overlays}</svg>`;
|
|
}
|
|
|
|
export function understandingReport(image, data, validation, metadata) {
|
|
const svg = data && validation.valid ? annotatedSvg(image, data.parts) : `<img src="${dataUrl(image)}" alt="source">`;
|
|
const rows = data && validation.valid ? data.parts.map((part) => `<tr><td>${escape(part.id)}</td><td>${escape(part.parent ?? 'root')}</td><td>${escape(part.visibility)}</td><td>${escape(part.occluded_by.join(', '))}</td><td>${part.confidence}</td></tr>`).join('') : '';
|
|
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>角色结构理解检查</title><style>body{font:16px system-ui;max-width:1100px;margin:24px auto;padding:16px;background:#f5f5f5}svg,img{max-width:100%;height:auto;max-height:85vh}td,th{padding:8px;border:1px solid #ccc}table{border-collapse:collapse}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style><h1>角色结构理解</h1><p>此图仅包含模型预测的粗框、关节点(空心圆)和正点(实心圆),尚未执行 SAM 分割或补全。结构校验通过不代表语义正确。</p><p>状态:${validation.valid ? 'needs_review(待检查)' : 'failed'}</p>${svg}<h2>校验</h2><pre>${escape(JSON.stringify(validation, null, 2))}</pre><h2>部件</h2><table><tr><th>ID</th><th>父部件</th><th>可见性</th><th>遮挡物</th><th>自报置信度</th></tr>${rows}</table><h2>模型说明</h2><pre>${escape(JSON.stringify(data?.notes || [], null, 2))}</pre><h2>请求记录</h2><pre>${escape(JSON.stringify(metadata, null, 2))}</pre></html>`;
|
|
}
|