Spaces:
Sleeping
Sleeping
<html> | |
<head> | |
<style> | |
.canvas-container { | |
position: relative; | |
display: inline-block; | |
border: 2px solid #ddd; | |
border-radius: 8px; | |
overflow: hidden; | |
background-color: #f8f9fa; | |
} | |
.bbox-canvas { | |
cursor: crosshair; | |
display: block; | |
background-color: white; | |
} | |
.bbox-list { | |
margin-top: 10px; | |
padding: 10px; | |
background-color: #f8f9fa; | |
border-radius: 5px; | |
max-height: 200px; | |
overflow-y: auto; | |
} | |
.bbox-item { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 5px; | |
margin: 2px 0; | |
background-color: white; | |
border-radius: 3px; | |
border-left: 4px solid #007bff; | |
} | |
.bbox-input { | |
width: 150px; | |
padding: 2px 5px; | |
border: 1px solid #ddd; | |
border-radius: 3px; | |
} | |
.delete-btn { | |
background-color: #dc3545; | |
color: white; | |
border: none; | |
padding: 2px 8px; | |
border-radius: 3px; | |
cursor: pointer; | |
font-size: 12px; | |
} | |
.delete-btn:hover { | |
background-color: #c82333; | |
} | |
.clear-btn { | |
background-color: #6c757d; | |
color: white; | |
border: none; | |
padding: 5px 15px; | |
border-radius: 3px; | |
cursor: pointer; | |
margin-top: 10px; | |
} | |
.clear-btn:hover { | |
background-color: #5a6268; | |
} | |
.color-indicator { | |
width: 20px; | |
height: 20px; | |
border-radius: 3px; | |
border: 2px solid white; | |
box-shadow: 0 0 3px rgba(0,0,0,0.3); | |
} | |
.info-text { | |
margin: 10px 0; | |
padding: 8px; | |
background-color: #e3f2fd; | |
border-radius: 4px; | |
font-size: 14px; | |
color: #1976d2; | |
} | |
.debug-info { | |
margin: 10px 0; | |
padding: 8px; | |
background-color: #fff3cd; | |
border-radius: 4px; | |
font-size: 12px; | |
color: #856404; | |
font-family: monospace; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="info-text"> | |
💡 拖拽鼠标在画布上绘制边界框,然后为每个框添加描述 | |
</div> | |
<div class="canvas-container"> | |
<canvas id="bboxCanvas" class="bbox-canvas" width="512" height="512"></canvas> | |
</div> | |
<script> | |
console.log('边界框组件已加载'); | |
const canvas = document.getElementById('bboxCanvas'); | |
const ctx = canvas.getContext('2d'); | |
const debugInfo = document.getElementById('debugInfo'); | |
let isDrawing = false; | |
let startX, startY; | |
let boxes = []; | |
let currentBox = null; | |
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', '#98D8C8', '#F7DC6F']; | |
let colorIndex = 0; | |
// 添加调试日志函数 | |
function log(message) { | |
console.log(message); | |
debugInfo.textContent = `调试: ${message}`; | |
} | |
// 初始化画布 | |
function initCanvas() { | |
ctx.fillStyle = '#ffffff'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
ctx.strokeStyle = '#ddd'; | |
ctx.lineWidth = 1; | |
ctx.strokeRect(0, 0, canvas.width, canvas.height); | |
log('画布已初始化'); | |
} | |
// 事件监听器 | |
canvas.addEventListener('mousedown', startDrawing); | |
canvas.addEventListener('mousemove', draw); | |
canvas.addEventListener('mouseup', stopDrawing); | |
canvas.addEventListener('mouseleave', stopDrawing); // 添加鼠标离开事件 | |
function startDrawing(e) { | |
isDrawing = true; | |
const rect = canvas.getBoundingClientRect(); | |
startX = e.clientX - rect.left; | |
startY = e.clientY - rect.top; | |
log(`开始绘制: (${Math.round(startX)}, ${Math.round(startY)})`); | |
} | |
function draw(e) { | |
if (!isDrawing) return; | |
const rect = canvas.getBoundingClientRect(); | |
const currentX = e.clientX - rect.left; | |
const currentY = e.clientY - rect.top; | |
redrawCanvas(); | |
// 绘制当前正在绘制的框 | |
ctx.strokeStyle = colors[colorIndex % colors.length]; | |
ctx.lineWidth = 2; | |
ctx.setLineDash([5, 5]); | |
ctx.strokeRect(startX, startY, currentX - startX, currentY - startY); | |
ctx.setLineDash([]); | |
} | |
function stopDrawing(e) { | |
if (!isDrawing) return; | |
isDrawing = false; | |
const rect = canvas.getBoundingClientRect(); | |
const endX = e.clientX - rect.left; | |
const endY = e.clientY - rect.top; | |
const width = Math.abs(endX - startX); | |
const height = Math.abs(endY - startY); | |
if (width > 10 && height > 10) { | |
const box = { | |
x: Math.min(startX, endX), | |
y: Math.min(startY, endY), | |
width: width, | |
height: height, | |
color: colors[colorIndex % colors.length], | |
label: '', | |
id: Date.now() | |
}; | |
boxes.push(box); | |
colorIndex++; | |
addBoxToList(box); | |
redrawCanvas(); | |
updateOutput(); | |
log(`添加边界框: ${boxes.length}个`); | |
} else { | |
redrawCanvas(); | |
log('边界框太小,已忽略'); | |
} | |
} | |
function redrawCanvas() { | |
// 清除画布并重新绘制背景 | |
ctx.fillStyle = '#ffffff'; | |
ctx.fillRect(0, 0, canvas.width, canvas.height); | |
// 绘制所有边界框 | |
boxes.forEach((box, index) => { | |
ctx.strokeStyle = box.color; | |
ctx.lineWidth = 2; | |
ctx.setLineDash([]); | |
ctx.strokeRect(box.x, box.y, box.width, box.height); | |
// 绘制标签 | |
if (box.label) { | |
ctx.fillStyle = box.color; | |
ctx.font = '14px Arial'; | |
ctx.fillText(box.label, box.x, box.y - 5); | |
} | |
// 绘制索引号 | |
ctx.fillStyle = box.color; | |
ctx.font = 'bold 12px Arial'; | |
ctx.fillText(`${index + 1}`, box.x + 3, box.y + 15); | |
}); | |
} | |
function addBoxToList(box) { | |
const bboxItems = document.getElementById('bboxItems'); | |
const item = document.createElement('div'); | |
item.className = 'bbox-item'; | |
item.id = `bbox-item-${box.id}`; | |
item.innerHTML = ` | |
<div style="display: flex; align-items: center; gap: 10px;"> | |
<div class="color-indicator" style="background-color: ${box.color}"></div> | |
<input type="text" class="bbox-input" placeholder="输入描述..." | |
onchange="updateBoxLabel(${box.id}, this.value)" | |
oninput="updateBoxLabel(${box.id}, this.value)"> | |
<span style="font-size: 12px; color: #666;"> | |
(${Math.round(box.x)}, ${Math.round(box.y)}, ${Math.round(box.width)}, ${Math.round(box.height)}) | |
</span> | |
</div> | |
<button class="delete-btn" onclick="deleteBox(${box.id})">删除</button> | |
`; | |
bboxItems.appendChild(item); | |
} | |
function updateBoxLabel(boxId, label) { | |
const box = boxes.find(b => b.id === boxId); | |
if (box) { | |
box.label = label; | |
redrawCanvas(); | |
updateOutput(); | |
log(`更新标签: ${label}`); | |
} | |
} | |
function deleteBox(boxId) { | |
const oldLength = boxes.length; | |
boxes = boxes.filter(b => b.id !== boxId); | |
redrawBboxList(); | |
redrawCanvas(); | |
updateOutput(); | |
log(`删除边界框: ${oldLength} -> ${boxes.length}`); | |
} | |
function clearAllBoxes() { | |
boxes = []; | |
redrawBboxList(); | |
redrawCanvas(); | |
updateOutput(); | |
log('清除所有边界框'); | |
} | |
function redrawBboxList() { | |
const bboxItems = document.getElementById('bboxItems'); | |
bboxItems.innerHTML = ''; | |
boxes.forEach(box => addBoxToList(box)); | |
} | |
function updateOutput() { | |
try { | |
// 将边界框数据传递给Gradio | |
const boxData = boxes.map(box => ({ | |
x: box.x / canvas.width, // 归一化坐标 | |
y: box.y / canvas.height, | |
width: box.width / canvas.width, | |
height: box.height / canvas.height, | |
label: box.label || '' | |
})); | |
const dataString = JSON.stringify(boxData); | |
log(`发送数据: ${boxData.length}个边界框`); | |
// 直接查找Gradio输入框(因为组件直接嵌入在页面中) | |
const bboxInput = document.querySelector('#bbox_data textarea'); | |
if (bboxInput) { | |
bboxInput.value = dataString; | |
// 触发多种事件确保Gradio能检测到变化 | |
bboxInput.dispatchEvent(new Event('input', { bubbles: true })); | |
bboxInput.dispatchEvent(new Event('change', { bubbles: true })); | |
bboxInput.dispatchEvent(new Event('blur', { bubbles: true })); | |
log('直接更新Gradio输入框成功'); | |
} else { | |
// 如果直接查找失败,尝试延迟查找 | |
setTimeout(() => { | |
const delayedBboxInput = document.querySelector('#bbox_data textarea') || | |
document.querySelector('[data-testid="textbox"] textarea'); | |
if (delayedBboxInput) { | |
delayedBboxInput.value = dataString; | |
delayedBboxInput.dispatchEvent(new Event('input', { bubbles: true })); | |
delayedBboxInput.dispatchEvent(new Event('change', { bubbles: true })); | |
log('延迟更新Gradio输入框成功'); | |
} else { | |
log('未找到Gradio输入框'); | |
} | |
}, 500); | |
} | |
// 同时触发自定义事件作为备用 | |
document.dispatchEvent(new CustomEvent('bbox_data_update', { | |
detail: { data: boxData, dataString: dataString } | |
})); | |
} catch (error) { | |
log(`更新输出时出错: ${error.message}`); | |
console.error('updateOutput error:', error); | |
} | |
} | |
// 接收来自Gradio的图片更新 | |
window.addEventListener('message', function(event) { | |
if (event.data && event.data.type === 'update_image') { | |
const img = new Image(); | |
img.onload = function() { | |
canvas.width = img.width; | |
canvas.height = img.height; | |
ctx.drawImage(img, 0, 0); | |
redrawCanvas(); | |
log('图片已更新'); | |
}; | |
img.src = event.data.imageUrl; | |
} | |
}); | |
// 页面加载完成后初始化 | |
window.addEventListener('load', function() { | |
initCanvas(); | |
log('组件已就绪'); | |
}); | |
// 立即初始化 | |
initCanvas(); | |
</script> | |
</body> | |
</html> |