able to see images

This commit is contained in:
2025-12-23 17:04:59 -07:00
parent 0072a86591
commit 50b2bf0305

View File

@ -38,208 +38,235 @@ except ImportError:
DEFAULT_MODEL_SIZE = "small" DEFAULT_MODEL_SIZE = "small"
# Gradio 6 sanitizes <script> tags inside gr.HTML content, so any canvas drawing code # Gradio 6 sanitizes <script> tags inside gr.HTML content. Also, js_on_load only
# must live in the component's supported js_on_load hook. # runs when the component is first mounted, not when its HTML updates. We
# therefore install a global initializer (via demo.launch(js=...)) and have
# js_on_load call into it when available.
CANVAS_JS_ON_LOAD = r""" CANVAS_JS_ON_LOAD = r"""
(() => { (() => {
const root = element; if (window.__initAnnotationCanvas) {
if (!root) return; window.__initAnnotationCanvas(element);
const canvas = root.querySelector('#annotation-canvas');
const imgEl = root.querySelector('#annotation-img');
const initialBoxesEl = root.querySelector('#annotation-initial-boxes');
if (!canvas || !imgEl || !initialBoxesEl) return;
const ctx = canvas.getContext('2d');
const displayWidth = canvas.width;
const displayHeight = canvas.height;
let boxes = [];
try {
const raw = initialBoxesEl.value || initialBoxesEl.textContent || '[]';
boxes = JSON.parse(raw);
if (!Array.isArray(boxes)) boxes = [];
} catch (_) {
boxes = [];
} }
})();
"""
const hiddenInput = document.getElementById('canvas-boxes-data'); CANVAS_GLOBAL_JS = r"""
const syncHidden = () => { (() => {
if (!hiddenInput) return; function init(root) {
hiddenInput.value = JSON.stringify(boxes); if (!root) return;
hiddenInput.dispatchEvent(new Event('input', { bubbles: true })); const canvas = root.querySelector('#annotation-canvas');
}; const imgEl = root.querySelector('#annotation-img');
syncHidden(); const initialBoxesEl = root.querySelector('#annotation-initial-boxes');
if (!canvas || !imgEl || !initialBoxesEl) return;
let isDragging = false; // Ensure we don't double-bind if Gradio reuses the DOM node.
let dragStart = null; if (canvas.dataset.bound === '1') {
let selectedCorner = null; // Still redraw in case boxes were updated.
let selectedBoxIndex = -1; if (canvas.__redraw) canvas.__redraw();
let creatingBox = false; return;
let createStart = null;
function redraw() {
ctx.clearRect(0, 0, displayWidth, displayHeight);
// Base image is rendered via <img> below the canvas.
boxes.forEach((box) => {
const [x1, y1, x2, y2] = box.bbox;
const label = box.label || 'knot';
const conf = box.confidence || 1.0;
ctx.strokeStyle = 'red';
ctx.lineWidth = 3;
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
const handleSize = 6;
ctx.fillStyle = 'red';
ctx.fillRect(x1 - handleSize, y1 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillRect(x2 - handleSize, y1 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillRect(x1 - handleSize, y2 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillRect(x2 - handleSize, y2 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillStyle = 'red';
ctx.font = '16px Arial';
const text = conf < 1.0 ? `${label} ${conf.toFixed(2)}` : label;
ctx.fillText(text, x1, y1 - 5);
});
if (creatingBox && createStart && dragStart) {
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const x = Math.min(createStart.x, dragStart.x);
const y = Math.min(createStart.y, dragStart.y);
const w = Math.abs(createStart.x - dragStart.x);
const h = Math.abs(createStart.y - dragStart.y);
ctx.strokeRect(x, y, w, h);
ctx.setLineDash([]);
} }
} canvas.dataset.bound = '1';
function getCornerAt(x, y) { const ctx = canvas.getContext('2d');
const handleSize = 6; const displayWidth = canvas.width;
for (let i = 0; i < boxes.length; i++) { const displayHeight = canvas.height;
const [x1, y1, x2, y2] = boxes[i].bbox;
const corners = [ let boxes = [];
{ x: x1, y: y1, type: 'top-left' }, try {
{ x: x2, y: y1, type: 'top-right' }, const raw = initialBoxesEl.value || initialBoxesEl.textContent || '[]';
{ x: x1, y: y2, type: 'bottom-left' }, boxes = JSON.parse(raw);
{ x: x2, y: y2, type: 'bottom-right' }, if (!Array.isArray(boxes)) boxes = [];
]; } catch (_) {
for (const corner of corners) { boxes = [];
if ( }
x >= corner.x - handleSize &&
x <= corner.x + handleSize && const hiddenInput = document.getElementById('canvas-boxes-data');
y >= corner.y - handleSize && const syncHidden = () => {
y <= corner.y + handleSize if (!hiddenInput) return;
) { hiddenInput.value = JSON.stringify(boxes);
return { boxIndex: i, corner: corner.type, pos: corner }; hiddenInput.dispatchEvent(new Event('input', { bubbles: true }));
};
syncHidden();
let isDragging = false;
let dragStart = null;
let selectedCorner = null;
let selectedBoxIndex = -1;
let creatingBox = false;
let createStart = null;
function redraw() {
ctx.clearRect(0, 0, displayWidth, displayHeight);
// Base image is rendered via <img> below the canvas.
boxes.forEach((box) => {
const [x1, y1, x2, y2] = box.bbox;
const label = box.label || 'knot';
const conf = box.confidence || 1.0;
ctx.strokeStyle = 'red';
ctx.lineWidth = 3;
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
const handleSize = 6;
ctx.fillStyle = 'red';
ctx.fillRect(x1 - handleSize, y1 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillRect(x2 - handleSize, y1 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillRect(x1 - handleSize, y2 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillRect(x2 - handleSize, y2 - handleSize, handleSize * 2, handleSize * 2);
ctx.fillStyle = 'red';
ctx.font = '16px Arial';
const text = conf < 1.0 ? `${label} ${conf.toFixed(2)}` : label;
ctx.fillText(text, x1, y1 - 5);
});
if (creatingBox && createStart && dragStart) {
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
const x = Math.min(createStart.x, dragStart.x);
const y = Math.min(createStart.y, dragStart.y);
const w = Math.abs(createStart.x - dragStart.x);
const h = Math.abs(createStart.y - dragStart.y);
ctx.strokeRect(x, y, w, h);
ctx.setLineDash([]);
}
}
canvas.__redraw = redraw;
function getCornerAt(x, y) {
const handleSize = 6;
for (let i = 0; i < boxes.length; i++) {
const [x1, y1, x2, y2] = boxes[i].bbox;
const corners = [
{ x: x1, y: y1, type: 'top-left' },
{ x: x2, y: y1, type: 'top-right' },
{ x: x1, y: y2, type: 'bottom-left' },
{ x: x2, y: y2, type: 'bottom-right' },
];
for (const corner of corners) {
if (
x >= corner.x - handleSize &&
x <= corner.x + handleSize &&
y >= corner.y - handleSize &&
y <= corner.y + handleSize
) {
return { boxIndex: i, corner: corner.type, pos: corner };
}
} }
} }
return null;
} }
return null;
}
// Ensure we don't double-bind if Gradio reuses the DOM node. imgEl.addEventListener(
if (canvas.dataset.bound === '1') { 'error',
() => {
ctx.clearRect(0, 0, displayWidth, displayHeight);
ctx.fillStyle = '#ffcccc';
ctx.fillRect(0, 0, displayWidth, displayHeight);
ctx.fillStyle = 'black';
ctx.font = '16px Arial';
ctx.fillText('Image failed to load', 10, 30);
},
{ once: true }
);
redraw(); redraw();
return;
}
canvas.dataset.bound = '1';
// If the <img> fails to load, draw a message on the canvas. canvas.addEventListener('mousedown', (e) => {
imgEl.addEventListener('error', () => { const rect = canvas.getBoundingClientRect();
ctx.clearRect(0, 0, displayWidth, displayHeight); const x = (e.clientX - rect.left) * (displayWidth / rect.width);
ctx.fillStyle = '#ffcccc'; const y = (e.clientY - rect.top) * (displayHeight / rect.height);
ctx.fillRect(0, 0, displayWidth, displayHeight);
ctx.fillStyle = 'black';
ctx.font = '16px Arial';
ctx.fillText('Image failed to load', 10, 30);
}, { once: true });
// Initial draw selectedCorner = getCornerAt(x, y);
redraw(); if (selectedCorner) {
isDragging = true;
canvas.addEventListener('mousedown', (e) => { selectedBoxIndex = selectedCorner.boxIndex;
const rect = canvas.getBoundingClientRect(); canvas.style.cursor = 'move';
const x = (e.clientX - rect.left) * (displayWidth / rect.width); } else {
const y = (e.clientY - rect.top) * (displayHeight / rect.height); creatingBox = true;
createStart = { x, y };
selectedCorner = getCornerAt(x, y); dragStart = { x, y };
if (selectedCorner) { canvas.style.cursor = 'crosshair';
isDragging = true;
selectedBoxIndex = selectedCorner.boxIndex;
canvas.style.cursor = 'move';
} else {
creatingBox = true;
createStart = { x, y };
dragStart = { x, y };
canvas.style.cursor = 'crosshair';
}
});
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (displayWidth / rect.width);
const y = (e.clientY - rect.top) * (displayHeight / rect.height);
if (isDragging && selectedCorner) {
const box = boxes[selectedBoxIndex];
if (selectedCorner.corner === 'top-left') {
box.bbox[0] = Math.min(x, box.bbox[2] - 10);
box.bbox[1] = Math.min(y, box.bbox[3] - 10);
} else if (selectedCorner.corner === 'top-right') {
box.bbox[2] = Math.max(x, box.bbox[0] + 10);
box.bbox[1] = Math.min(y, box.bbox[3] - 10);
} else if (selectedCorner.corner === 'bottom-left') {
box.bbox[0] = Math.min(x, box.bbox[2] - 10);
box.bbox[3] = Math.max(y, box.bbox[1] + 10);
} else if (selectedCorner.corner === 'bottom-right') {
box.bbox[2] = Math.max(x, box.bbox[0] + 10);
box.bbox[3] = Math.max(y, box.bbox[1] + 10);
} }
syncHidden(); });
redraw();
return;
}
if (creatingBox && createStart) { canvas.addEventListener('mousemove', (e) => {
dragStart = { x, y }; const rect = canvas.getBoundingClientRect();
redraw(); const x = (e.clientX - rect.left) * (displayWidth / rect.width);
return; const y = (e.clientY - rect.top) * (displayHeight / rect.height);
}
const corner = getCornerAt(x, y); if (isDragging && selectedCorner) {
canvas.style.cursor = corner ? 'move' : 'crosshair'; const box = boxes[selectedBoxIndex];
}); if (selectedCorner.corner === 'top-left') {
box.bbox[0] = Math.min(x, box.bbox[2] - 10);
canvas.addEventListener('mouseup', () => { box.bbox[1] = Math.min(y, box.bbox[3] - 10);
if (creatingBox && createStart && dragStart) { } else if (selectedCorner.corner === 'top-right') {
const x1 = Math.min(createStart.x, dragStart.x); box.bbox[2] = Math.max(x, box.bbox[0] + 10);
const y1 = Math.min(createStart.y, dragStart.y); box.bbox[1] = Math.min(y, box.bbox[3] - 10);
const x2 = Math.max(createStart.x, dragStart.x); } else if (selectedCorner.corner === 'bottom-left') {
const y2 = Math.max(createStart.y, dragStart.y); box.bbox[0] = Math.min(x, box.bbox[2] - 10);
if (x2 - x1 > 10 && y2 - y1 > 10) { box.bbox[3] = Math.max(y, box.bbox[1] + 10);
boxes.push({ } else if (selectedCorner.corner === 'bottom-right') {
bbox: [x1, y1, x2, y2], box.bbox[2] = Math.max(x, box.bbox[0] + 10);
label: 'knot', box.bbox[3] = Math.max(y, box.bbox[1] + 10);
confidence: 1.0, }
source: 'manual',
});
syncHidden(); syncHidden();
redraw(); redraw();
return;
} }
}
isDragging = false; if (creatingBox && createStart) {
creatingBox = false; dragStart = { x, y };
selectedCorner = null; redraw();
selectedBoxIndex = -1; return;
createStart = null; }
dragStart = null;
canvas.style.cursor = 'crosshair'; const corner = getCornerAt(x, y);
}); canvas.style.cursor = corner ? 'move' : 'crosshair';
});
canvas.addEventListener('mouseup', () => {
if (creatingBox && createStart && dragStart) {
const x1 = Math.min(createStart.x, dragStart.x);
const y1 = Math.min(createStart.y, dragStart.y);
const x2 = Math.max(createStart.x, dragStart.x);
const y2 = Math.max(createStart.y, dragStart.y);
if (x2 - x1 > 10 && y2 - y1 > 10) {
boxes.push({
bbox: [x1, y1, x2, y2],
label: 'knot',
confidence: 1.0,
source: 'manual',
});
syncHidden();
redraw();
}
}
isDragging = false;
creatingBox = false;
selectedCorner = null;
selectedBoxIndex = -1;
createStart = null;
dragStart = null;
canvas.style.cursor = 'crosshair';
});
}
window.__initAnnotationCanvas = init;
function scan() {
document.querySelectorAll('[data-annotation-root="1"]').forEach((root) => init(root));
}
const obs = new MutationObserver(() => scan());
obs.observe(document.documentElement, { childList: true, subtree: true });
window.addEventListener('load', () => scan());
setTimeout(() => scan(), 0);
setTimeout(() => scan(), 250);
setTimeout(() => scan(), 1000);
})(); })();
""" """
@ -642,7 +669,7 @@ class AnnotationApp:
<div style="display: inline-block; border: 1px solid #ccc; padding: 5px;"> <div style="display: inline-block; border: 1px solid #ccc; padding: 5px;">
<div style="font-size: 12px; color: #666; margin-bottom: 4px;">Canvas Size: {display_width}x{display_height}</div> <div style="font-size: 12px; color: #666; margin-bottom: 4px;">Canvas Size: {display_width}x{display_height}</div>
<textarea id="annotation-initial-boxes" style="display:none;">{boxes_escaped}</textarea> <textarea id="annotation-initial-boxes" style="display:none;">{boxes_escaped}</textarea>
<div style="position: relative; width: {display_width}px; height: {display_height}px;"> <div data-annotation-root="1" style="position: relative; width: {display_width}px; height: {display_height}px;">
<img id="annotation-img" src="data:image/png;base64,{img_base64}" <img id="annotation-img" src="data:image/png;base64,{img_base64}"
style="position:absolute; left:0; top:0; width:{display_width}px; height:{display_height}px;" /> style="position:absolute; left:0; top:0; width:{display_width}px; height:{display_height}px;" />
<canvas id="annotation-canvas" width="{display_width}" height="{display_height}" <canvas id="annotation-canvas" width="{display_width}" height="{display_height}"
@ -1650,6 +1677,7 @@ def main():
demo.launch( demo.launch(
server_name="0.0.0.0", server_name="0.0.0.0",
server_port=args.port, server_port=args.port,
js=CANVAS_GLOBAL_JS,
share=False share=False
) )