able to see images
This commit is contained in:
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user