AI智能标注与检测平台
首次训练需要安装 Python 和 PyTorch(需联网,约 15 分钟)。
服务地址: http://localhost:3001
| 端点 | POST /api/detect/json |
| Content-Type | application/json |
| image | String, base64编码图片 (可含 data:image 前缀) |
| confidence | Float (可选, 默认0.35), 置信度阈值 |
| success | 是否成功 |
| detections | 检测对象列表: label, confidence, bbox |
| annotated_image | 标注后全图 (base64 JPEG) |
| cropped_items | 裁剪物品列表: classification_label, classification_confidence, item_confidence, item_image, annotated_item_image |
| saved_crops | 服务端保存的文件名 |
| processing_time_ms | 推理耗时(毫秒) |
| annotated_file | 全图标注文件路径 |
curl -X POST http://localhost:3001/api/detect/json \
-H "Content-Type: application/json" \
-d '{
"image": "data:image/jpeg;base64,/9j/4AAQ...",
"confidence": 0.35
}'
// 拍照或选择图片,获取 base64 后调用 API
async function detect(imageBase64, confidence = 0.35) {
const r = await fetch('http://localhost:3001/api/detect/json', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageBase64, confidence })
});
const data = await r.json();
if (data.success) {
// data.annotated_image → 标注大图
// data.cropped_items[i].item_image → 裁剪图
// data.cropped_items[i].classification_label → "阳性"/"阴性"
// data.cropped_items[i].classification_confidence → 置信度
console.log(data);
}
return data;
}
function detectXHR(imageBase64, confidence, onProgress, onResult) {
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:3001/api/detect/json');
xhr.setRequestHeader('Content-Type', 'application/json');
// JSON 请求没有原生 upload progress,
// 可以用 xhr.onprogress 监听响应下载进度
xhr.onprogress = (e) => {
if (e.lengthComputable && onProgress) {
const pct = Math.round(e.loaded / e.total * 100);
onProgress('下载中 ' + pct + '%');
}
};
xhr.onload = () => {
const data = JSON.parse(xhr.responseText);
if (onResult) onResult(data);
};
xhr.send(JSON.stringify({ image: imageBase64, confidence }));
}
下面是一份完整的 HTML 文件,保存为 camera.html 即可独立使用。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Multi-Detect 拍照推理</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; padding: 20px; }
h1 { color: #e94560; margin-bottom: 16px; }
.controls { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
button, input { padding: 10px 20px; border: 1px solid #0f3460; border-radius: 6px; background: #16213e; color: #e0e0e0; cursor: pointer; font-size: 14px; }
button:hover { border-color: #e94560; }
video { width: 100%; max-width: 480px; border-radius: 8px; display: none; border: 1px solid #0f3460; }
canvas { display: none; }
#result { margin-top: 16px; }
#result img { max-width: 100%; border-radius: 8px; border: 1px solid #0f3460; }
.crop-card { display: inline-block; margin: 8px; background: #16213e; border-radius: 8px; overflow: hidden; border: 1px solid #0f3460; vertical-align: top; }
.crop-card img { width: 200px; height: 160px; object-fit: contain; display: block; background: #0d1120; }
.crop-card .info { padding: 8px; font-size: 13px; }
.crop-card .label { color: #e94560; font-weight: 600; }
.crop-card .conf { color: #2ecc71; }
.status { color: #a0a0c0; font-size: 13px; margin: 8px 0; }
</style>
</head>
<body>
<h1>Multi-Detect 拍照推理</h1>
<div class="controls">
<button id="btn-camera">打开摄像头</button>
<button id="btn-capture" disabled>拍照检测</button>
<input type="file" id="file-input" accept="image/*">
<label>置信度: <input id="conf" type="range" min="0.05" max="0.95" step="0.05" value="0.35">
<span id="conf-val">0.35</span></label>
</div>
<video id="video" autoplay playsinline></video>
<canvas id="canvas"></canvas>
<div class="status" id="status"></div>
<div id="result"></div>
<script>
const API = 'http://localhost:3001/api/detect/json';
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const status = document.getElementById('status');
document.getElementById('conf').addEventListener('input', e => {
document.getElementById('conf-val').textContent = parseFloat(e.target.value).toFixed(2);
});
document.getElementById('btn-camera').addEventListener('click', async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
video.srcObject = stream;
video.style.display = 'block';
document.getElementById('btn-capture').disabled = false;
} catch (e) { status.textContent = '摄像头不可用: ' + e.message; }
});
document.getElementById('btn-capture').addEventListener('click', () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
const b64 = canvas.toDataURL('image/jpeg', 0.85);
detectWith(b64);
});
document.getElementById('file-input').addEventListener('change', () => {
const file = document.getElementById('file-input').files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => detectWith(reader.result);
reader.readAsDataURL(file);
});
async function detectWith(imageBase64) {
status.textContent = '检测中...';
document.getElementById('result').innerHTML = '';
const conf = parseFloat(document.getElementById('conf').value);
try {
const r = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: imageBase64, confidence: conf })
});
const data = await r.json();
if (data.success) {
status.textContent = '检测完成 · ' + data.processing_time_ms + 'ms';
let html = data.annotated_image
? '<img src="' + data.annotated_image + '" alt="检测结果">'
: '';
if (data.cropped_items) {
html += data.cropped_items.map(c => {
const pct = c.classification_confidence > 0
? ' ' + (c.classification_confidence * 100).toFixed(1) + '%' : '';
const src = c.annotated_item_image || c.item_image;
return '<div class="crop-card"><img src="' + src + '"><div class="info"><span class="label">' + (c.classification_label || '物品') + '</span><span class="conf">' + pct + '</span></div></div>';
}).join('');
}
document.getElementById('result').innerHTML = html;
} else {
status.textContent = '检测失败: ' + (data.error || '未知');
}
} catch (e) { status.textContent = '网络错误: ' + e.message; }
}
</script>
</body>
</html>
backend_data/images/ 目录backend_data/datasets/ 目录切换到训练标签页,系统会自动检测训练环境。如提示「Python 未安装」,点击「安装环境」按钮,系统将自动在线安装所需依赖(需联网,约 15 分钟)。
rotate_data.bat 生成旋转增强数据prepare_marking.bat 裁剪物品生成标记数据集| 轮数 | 训练迭代次数,推荐 500-1000 |
| 批次 | 每批处理的图片数,显存不足时降低(如 8 或 4) |
| 学习率 | 模型更新步长,默认 0.0005 适用于大多数场景 |
| 图片尺寸 | 输入分辨率,推荐 640,大尺寸需更多显存 |
训练过程中实时显示 Loss(损失值)和 mAP(平均精度)。Loss 持续下降、mAP 持续上升说明训练正常。
backend_data/crops/ 目录卡片上显示两个数值:左侧为物品检出置信度,色彩条为阴阳性分类置信度。绿色≥70%、黄色 40-70%、红色<40%。滑块只影响分类结果的显示门槛,不影响模型内部计算。
| Ctrl+S | 保存当前标注 |
| Ctrl+Z | 撤销上一个操作 |
| Delete | 删除选中的多边形 |
| 鼠标滚轮 | 缩放画布 / 灯箱图片放大缩小 |
| 空格+拖拽 | 平移画布 |
| 双击图片 | 灯箱中显示或闭合多边形 |
| Esc | 关闭弹窗 / 灯箱 |
train_yolo26.bat(脚本已内置 TDR 修复)。setup_training.bat 重新安装训练环境,或在 config.json 中手动配置 Python 路径。