Files
aiData/static/Test/GetCircle.html
HuangHai 22a6a3a0e8 'commit'
2026-01-24 07:48:05 +08:00

431 lines
13 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>人脸标注工具</title>
<style>
body { font-family: "Segoe UI", Roboto, Helvetica, Arial, sans-serif; display: flex; height: 100vh; margin: 0; overflow: hidden; background-color: #2c3e50; color: #ecf0f1; }
#sidebar {
width: 250px;
background: #34495e;
border-right: 1px solid #2c3e50;
display: flex;
flex-direction: column;
}
#sidebar h3 {
padding: 15px;
margin: 0;
background: #2c3e50;
text-align: center;
border-bottom: 1px solid #34495e;
}
#file-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
#main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
position: relative;
}
#canvas-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
overflow: hidden;
background: #2c3e50; /* Match body bg */
}
canvas {
background: #000;
cursor: crosshair;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
border: 2px solid #95a5a6;
}
.img-item {
cursor: pointer;
padding: 10px;
margin-bottom: 5px;
background: #ecf0f1;
color: #2c3e50;
border-radius: 4px;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.img-item:hover {
background: #bdc3c7;
}
.img-item.active {
background: #3498db;
color: white;
font-weight: bold;
}
.img-item.annotated .status::after {
content: '✓';
color: #27ae60;
font-weight: bold;
font-size: 1.2em;
}
.img-item.active.annotated .status::after {
color: #2ecc71;
}
#controls {
margin-top: 15px;
display: flex;
gap: 15px;
align-items: center;
background: #34495e;
padding: 10px 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
button {
padding: 8px 20px;
cursor: pointer;
font-size: 14px;
border: none;
border-radius: 4px;
background: #95a5a6;
color: white;
transition: background 0.2s;
}
button:hover { background: #7f8c8d; }
button.primary { background: #3498db; }
button.primary:hover { background: #2980b9; }
button.danger { background: #e74c3c; }
button.danger:hover { background: #c0392b; }
button.success { background: #27ae60; }
button.success:hover { background: #219150; }
#output-area {
width: 100%;
height: 120px;
margin-top: 15px;
position: relative;
}
#output {
width: 100%;
height: 100%;
font-family: 'Consolas', monospace;
padding: 10px;
box-sizing: border-box;
background: #ecf0f1;
color: #2c3e50;
border: none;
border-radius: 4px;
resize: none;
}
.instructions {
margin-top: 10px;
font-size: 0.9em;
color: #bdc3c7;
text-align: center;
}
</style>
</head>
<body>
<div id="sidebar">
<h3>图片列表 (1-12)</h3>
<div id="file-list"></div>
</div>
<div id="main">
<div id="canvas-container">
<canvas id="canvas"></canvas>
</div>
<div class="instructions">
操作指南: 鼠标拖拽画圆 | A/← 上一张 | D/→ 下一张 | Del 清除当前 | 点击下方按钮导出
</div>
<div id="controls">
<span style="min-width: 100px;">当前: <strong id="current-file">--</strong></span>
<button onclick="prevImage()">上一张 (A)</button>
<button class="primary" onclick="nextImage()">下一张 (D)</button>
<button class="danger" onclick="clearAnnotation()">清除 (Del)</button>
<button class="success" onclick="exportData()">生成 JSON</button>
</div>
<div id="output-area">
<textarea id="output" readonly placeholder="点击“生成 JSON”按钮此处将显示结果并自动复制到剪贴板..."></textarea>
</div>
</div>
<script>
// 配置
const imageFolder = './Images/';
const totalImages = 14;
const images = [];
for (let i = 1; i <= totalImages; i++) {
images.push(`${i}.jpg`);
}
// 状态
let currentIndex = 0;
const annotations = {}; // { "1.jpg": { x: 100, y: 100, r: 50 } }
// DOM 元素
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const fileListEl = document.getElementById('file-list');
const canvasContainer = document.getElementById('canvas-container');
let imgObj = new Image();
let isDragging = false;
let startX, startY;
let currentMouseX, currentMouseY;
// 初始化侧边栏
function renderSidebar() {
fileListEl.innerHTML = '';
images.forEach((name, idx) => {
const div = document.createElement('div');
div.className = `img-item ${idx === currentIndex ? 'active' : ''} ${annotations[name] ? 'annotated' : ''}`;
div.innerHTML = `<span>${name}</span><span class="status"></span>`;
div.onclick = () => loadImage(idx);
fileListEl.appendChild(div);
});
// 滚动到当前项
const activeItem = fileListEl.querySelector('.active');
if (activeItem) {
activeItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function loadImage(index) {
if (index < 0) index = images.length - 1;
if (index >= images.length) index = 0;
currentIndex = index;
const filename = images[currentIndex];
document.getElementById('current-file').textContent = filename;
imgObj = new Image();
imgObj.onload = () => {
fitCanvas();
draw();
renderSidebar();
};
imgObj.onerror = () => {
alert(`无法加载图片: ${imageFolder + filename}\n请确保图片存在且路径正确。`);
};
imgObj.src = imageFolder + filename;
}
function fitCanvas() {
if (!imgObj.width) return;
const containerWidth = canvasContainer.clientWidth;
const containerHeight = canvasContainer.clientHeight;
// 计算缩放比例,保持长宽比,并留一点边距
const scale = Math.min(
(containerWidth - 20) / imgObj.width,
(containerHeight - 20) / imgObj.height
);
// 设置 Canvas 的显示大小
canvas.style.width = (imgObj.width * scale) + 'px';
canvas.style.height = (imgObj.height * scale) + 'px';
// 设置 Canvas 的实际分辨率 (等于图片原始分辨率)
canvas.width = imgObj.width;
canvas.height = imgObj.height;
}
function draw() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (imgObj.width) {
ctx.drawImage(imgObj, 0, 0);
}
const name = images[currentIndex];
const data = annotations[name];
// 绘制已保存的标注 (绿色)
if (data) {
drawCircle(data.x, data.y, data.r, '#2ecc71', 4);
}
// 绘制正在拖拽的圆 (黄色)
if (isDragging) {
const dx = currentMouseX - startX;
const dy = currentMouseY - startY;
const r = Math.sqrt(dx*dx + dy*dy);
drawCircle(startX, startY, r, '#f1c40f', 2);
// 显示当前半径
ctx.fillStyle = '#f1c40f';
ctx.font = '20px Arial';
ctx.fillText(`R: ${Math.round(r)}`, currentMouseX + 10, currentMouseY + 10);
}
}
function drawCircle(x, y, r, color, lineWidth) {
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.stroke();
// 绘制圆心
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, lineWidth + 2, 0, 2 * Math.PI);
ctx.fill();
}
// 鼠标坐标映射 (从屏幕坐标到 Canvas 内部坐标)
function getMousePos(evt) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return {
x: (evt.clientX - rect.left) * scaleX,
y: (evt.clientY - rect.top) * scaleY
};
}
// 事件监听
canvas.addEventListener('mousedown', (e) => {
const pos = getMousePos(e);
startX = pos.x;
startY = pos.y;
isDragging = true;
currentMouseX = pos.x;
currentMouseY = pos.y;
draw();
});
canvas.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const pos = getMousePos(e);
currentMouseX = pos.x;
currentMouseY = pos.y;
draw();
});
canvas.addEventListener('mouseup', (e) => {
if (!isDragging) return;
isDragging = false;
const pos = getMousePos(e);
const dx = pos.x - startX;
const dy = pos.y - startY;
const r = Math.sqrt(dx*dx + dy*dy);
// 只有当半径大于 5 像素时才保存,防止误触
if (r > 5) {
annotations[images[currentIndex]] = {
x: Math.round(startX),
y: Math.round(startY),
r: Math.round(r)
};
}
draw();
renderSidebar();
});
// 窗口大小改变时重新适配
window.addEventListener('resize', () => {
if (imgObj.src) fitCanvas();
});
// 按钮功能
function prevImage() { loadImage(currentIndex - 1); }
function nextImage() { loadImage(currentIndex + 1); }
function clearAnnotation() {
if (annotations[images[currentIndex]]) {
delete annotations[images[currentIndex]];
draw();
renderSidebar();
}
}
function exportData() {
const result = [];
images.forEach(name => {
if (annotations[name]) {
result.push({
filename: name,
...annotations[name]
});
}
});
if (result.length === 0) {
alert("还没有任何标注数据!请先在图片上拖拽画圆。");
return;
}
const jsonStr = JSON.stringify(result, null, 2);
const outputEl = document.getElementById('output');
outputEl.value = jsonStr;
outputEl.select();
try {
document.execCommand('copy');
alert(`已生成 ${result.length} 条标注数据并复制到剪贴板!\n你可以直接粘贴到 Python 代码中使用。`);
} catch (err) {
alert('复制失败,请手动复制下方文本框中的内容。');
}
}
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'TEXTAREA') return;
switch(e.key) {
case 'a':
case 'A':
case 'ArrowLeft':
prevImage();
break;
case 'd':
case 'D':
case 'ArrowRight':
nextImage();
break;
case 'Delete':
case 'Backspace':
clearAnnotation();
break;
}
});
// 启动
loadImage(0);
</script>
</body>
</html>