Files
aiData/static/Test/GetCircle.html

431 lines
13 KiB
HTML
Raw Normal View History

2026-01-24 07:41:29 +08:00
<!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/';
2026-01-24 07:48:05 +08:00
const totalImages = 14;
2026-01-24 07:41:29 +08:00
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>