目录
-------------------------------------------------- 一、需求全景图
-------------------------------------------------- 二、系统架构(微内核 + 插件)
-------------------------------------------------- 三、技术细节
-------------------------------------------------- 四、关键代码示例
-------------------------------------------------- 五、Docker 一键部署
-------------------------------------------------- 六、二次开发路线图
-------------------------------------------------- 七、小结
可二次开发”。
内容包含:
1. 需求定义
2. 系统架构
3. 详细技术方案(前端、后端、AI 辅助、权限、审计)
4. 关键代码示例(Python + React)
5. Docker 一键部署脚本
6. 二次开发指南
--------------------------------------------------
一、需求全景图
| 维度 | 目标需求 | 备注 |
|---|---|---|
| 标注类型 | 2D 检测框、旋转框、多边形、关键点、语义分割、OCR、3D 点云 | 支持一键切换 |
| 数据模态 | 图像、视频、激光雷达(PCD)、音频 | 统一抽象为“Asset” |
| AI 辅助 | 预标注、交互式分割(SAM)、一键跟踪(DeepSORT)、主动学习 | 插件化 |
| 协同 | 角色:管理员 / 标注员 / 审核员 / 质检员,支持并发锁、冲突合并 | 基于任务流 |
| 审计 | 标注历史、版本回滚、质检抽样、绩效统计 | 全链路日志 |
| 部署 | 私有化、云端、离线笔记本 | Docker + WASM |
--------------------------------------------------
二、系统架构(微内核 + 插件)
```
┌────────────────────────────┐
│ Web 前端 (React + Canvas)│ ←→ REST / WebSocket
├────────────────────────────┤
│ Gateway (Nginx + HTTPS) │
├────────────────────────────┤
│ Core-Service (Python) │ ←→ 插件注册中心
│ ├─ asset-service │ 上传/转码/缩略图
│ ├─ label-service │ 标注 CRUD + 几何运算
│ ├─ task-service │ 任务流引擎(Temporal)
│ ├─ ai-service │ 预标注模型池
│ └─ audit-service │ 日志 / 回滚 / 质检
├────────────────────────────┤
│ PostgreSQL + PostGIS │ 元数据
│ Redis │ 缓存 / 锁
│ MinIO / OSS │ 文件存储
│ Vector DB (Milvus) │ 主动学习 embedding
└────────────────────────────┘
```
--------------------------------------------------
三、技术细节
1. 前端:React + Konva.js(Canvas 2D)+ Three.js(3D)
分层渲染:
- Layer0:原始 Asset
- Layer1:标注几何(Konva.Shape)
- Layer2:AI Overlay(半透明)
- Layer3:交互手柄
统一数据结构:
```ts
interface Label {
id: string;
type: 'bbox' | 'polygon' | 'keypoint' | 'mask';
geometry: any; // GeoJSON / COCO 格式
meta: {
category: string;
attrs: Record<string, any>;
};
track_id?: number; // 视频跟踪
ts?: number; // 视频帧时间戳
}
```
2. 后端:FastAPI + SQLAlchemy + Celery
a) 上传接口(支持大文件分片)
```python
@router.post("/asset")
async def upload(file: UploadFile, background: BackgroundTasks):
key = await save_to_minio(file)
background.add_task(generate_thumbnails, key)
return {"asset_id": key}
```
b) 标注保存(自动版本化)
```python
@router.put("/label/{asset_id}")
async def save_label(asset_id: str, labels: list[Label], user: User = Depends(get_current)):
version = await label_service.save(asset_id, labels, user.id)
await audit_service.log(asset_id, version, user.id, labels)
```
c) AI 预标注插件接口
```python
class PreLabelPlugin(ABC):
@abstractmethod
async def predict(self, asset: Asset) -> list[Label]:
...
```
3. AI 模型集成
- 检测:YOLOv8 / Detectron2
- 分割:SAM(Segment Anything)
- 跟踪:ByteTrack / DeepSORT
- OCR:PaddleOCR
统一封装为 gRPC 微服务,注册到 ai-service,前端一键调用 `/ai/predict`。
4. 任务流(Temporal)
```
CreateTask → SplitAsset → AssignToUser → PreLabel → UserLabel → Review → Aggregate → Export
```
5. 权限 & 审计
- RBAC:管理员、标注员、审核员、质检员
- 行级锁:基于 Redis RedLock,避免并发覆盖
- 审计表:PostgreSQL Logical Decoding + Debezium → Kafka → ClickHouse BI
--------------------------------------------------
四、关键代码示例
1. Canvas 绘制 bbox(React + Konva)
```tsx
import { Rect, Transformer } from 'react-konva';
function Bbox({ label, onChange }) {
const shapeRef = useRef();
const trRef = useRef();
useEffect(() => trRef.current.nodes([shapeRef.current]), []);
return (
<>
<Rect
ref={shapeRef}
x={label.geometry.x}
y={label.geometry.y}
width={label.geometry.width}
height={label.geometry.height}
stroke="#00F"
draggable
onDragEnd={(e) => onChange({ ...label, geometry: { ...label.geometry, x: e.target.x(), y: e.target.y() } })}
/>
<Transformer ref={trRef} boundBoxFunc={(oldBox, newBox) => newBox} />
</>
);
}
```
2. Python 几何工具(多边形 IoU)
```python
from shapely.geometry import Polygon
def polygon_iou(p1, p2):
a, b = Polygon(p1), Polygon(p2)
return a.intersection(b).area / a.union(b).area
```
3. SAM 交互式分割(后端插件)
```python
from segment_anything import sam_model_registry, SamPredictor
class SAMPlugin(PreLabelPlugin):
def __init__(self, ckpt_path):
sam = sam_model_registry["vit_h"](checkpoint=ckpt_path).cuda()
self.predictor = SamPredictor(sam)
async def predict(self, asset: Asset, points, labels) -> list[Label]:
img = cv2.imread(asset.local_path)
self.predictor.set_image(img)
mask, *_ = self.predictor.predict(point_coords=points, point_labels=labels)
return mask_to_polygon(mask)
```
--------------------------------------------------
五、Docker 一键部署
docker-compose.yml
```yaml
version: '3.9'
services:
web:
build: ./frontend
ports: ["80:80"]
api:
build: ./backend
env_file: .env
depends_on: [db, redis, minio]
db:
image: postgis/postgis:15-3.3
environment:
POSTGRES_PASSWORD: label123
redis:
image: redis:7-alpine
minio:
image: minio/minio
command: server /data --console-address ":9001"
ports: ["9000:9000", "9001:9001"]
```
启动
```bash
docker-compose up -d
# 浏览器访问 http://localhost
```
--------------------------------------------------
六、二次开发路线图
| 阶段 | 目标 | 建议 PR 方向 |
|---|---|---|
| 1 | 私有化部署脚本 | Ansible / Helm |
| 2 | 3D 点云标注 | 基于 Potree.js 或 Open3D WebGL |
| 3 | 自动质检 | 利用 CLIP 计算 embedding,聚类找异常标签 |
| 4 | 离线模式 | PWA + SQLite + WASM 模型推理 |
| 5 | 市场插件 | 开放 API,支持第三方上传 AI 插件 |
--------------------------------------------------
七、小结
这份方案覆盖从需求、架构、代码、部署到扩展的“端到端”落地路径。你可以:
- 用 Docker 一键拉起完整系统;
- 用插件机制接入真实 AI 模型(YOLO / SAM / OCR);
- 用任务流实现多人协同;
- 用审计体系满足合规要求。
案例
下面给出一条“从 0 到 1”的最小可运行示例,演示:
1. 导入一张图像;
2. 用 YOLOv8 自动识别行人(person),得到检测框;
3. 把检测框即时渲染成“可人工微调”的标注框;
4. 一键导出 COCO 或 Pascal VOC 格式的标签文件。
整个流程控制在 **1 个 Python 文件 + 1 个 HTML 文件** 内,离线即可跑通,方便嵌入到你自己的标注工具里。
--------------------------------------------------
一、环境 30 秒装好
```bash
# CPU 也能跑,GPU 更快
pip install ultralytics opencv-python==4.8.1.78 flask==2.3.3
```
模型权重第一次运行会自动下载 `yolov8n.pt`(5.9 MB)。
--------------------------------------------------
二、文件结构
```
auto_label_demo/
├─ app.py # Flask 后端:上传 + 预测 + 保存
└─ templates/
└─ index.html # 前端:画布显示/微调/导出
```
--------------------------------------------------
三、后端 app.py(核心 60 行)
```python
from flask import Flask, request, jsonify, send_from_directory, render_template
from ultralytics import YOLO
import cv2, json, os, uuid
app = Flask(__name__)
UPLOAD = 'static/upload'
os.makedirs(UPLOAD, exist_ok=True)
model = YOLO('yolov8n.pt') # 行人 person 的类别 id = 0
@app.route('/')
def index():
return render_template('index.html')
@app.route('/upload', methods=['POST'])
def upload():
f = request.files['file']
ext = f.filename.rsplit('.', 1)[-1]
img_path = os.path.join(UPLOAD, f"{uuid.uuid4()}.{ext}")
f.save(img_path)
# 1. 推理
results = model.predict(img_path, conf=0.35, iou=0.45, verbose=False)[0]
# 2. 组装标注框(仅 person)
h, w = results.orig_shape
boxes = []
for xyxy, cls in zip(results.boxes.xyxy.cpu().numpy(),
results.boxes.cls.cpu().numpy()):
if int(cls) == 0: # 0 是 person
x1, y1, x2, y2 = map(float, xyxy)
boxes.append({
"cls": "person",
"xyxy": [x1, y1, x2, y2],
"xywh": [(x1+x2)/2/w, (y1+y2)/2/h, (x2-x1)/w, (y2-y1)/h] # 归一化
})
return jsonify({"img": '/' + img_path.replace('\\', '/'),
"boxes": boxes})
@app.route('/save', methods=['POST'])
def save():
data = request.json
out = data['img'].rsplit('/', 1)[0] + '/' + data['img'].rsplit('/', 1)[1].rsplit('.', 1)[0] + '.json'
with open(out.replace('/static/', UPLOAD + '/'), 'w') as f:
json.dump(data, f, indent=2)
return jsonify({"ok": True})
if __name__ == '__main__':
app.run(debug=True, port=5000)
```
--------------------------------------------------
四、前端 templates/index.html(画布交互 80 行)
```html
<!doctype html>
<html>
<head>
<title>Auto Label (YOLOv8)</title>
<style>canvas{border:1px solid #000}</style>
</head>
<body>
<input type="file" id="file" accept="image/*">
<button onclick="exportJson()">导出标签</button>
<br>
<canvas id="cv" width="800" height="600"></canvas>
<script>
let img = new Image(), boxes = [], drag = null, scale = 1;
document.getElementById('file').onchange = async (e) => {
const fd = new FormData();
fd.append('file', e.target.files[0]);
const res = await fetch('/upload', {method:'POST', body:fd}).then(r=>r.json());
img.src = res.img;
boxes = res.boxes.map(b=>({...b, selected:false}));
img.onload = draw;
};
function draw(){
const ctx = cv.getContext('2d');
cv.width = img.width; cv.height = img.height;
ctx.clearRect(0,0,cv.width,cv.height);
ctx.drawImage(img,0,0);
boxes.forEach(b=>{
ctx.strokeStyle = b.selected ? '#0f0' : '#f00';
ctx.lineWidth = 2;
const [x1,y1,x2,y2] = b.xyxy;
ctx.strokeRect(x1,y1,x2-x1,y2-y1);
});
}
cv.onmousedown = e=>{
const rect = cv.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
boxes.forEach(b=>{
const [x1,y1,x2,y2] = b.xyxy;
b.selected = x>=x1 && x<=x2 && y>=y1 && y<=y2;
});
draw();
};
async function exportJson(){
await fetch('/save', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({img:img.src, boxes})
});
alert('已保存 '+img.src.replace('.jpg','.json'));
}
</script>
</body>
</html>
2744

被折叠的 条评论
为什么被折叠?



