Spaces:
Running
on
Zero
Running
on
Zero
| <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> |