自动驾驶标注平台:标注结果分片上传指南
一、标注结果特点
自动驾驶标注结果通常包含:
- 3D点云标注:.pcd + JSON标注框
- 图像标注:图片 + 2D框/分割mask
- 时序标注:多帧关联的轨迹数据
- 元数据:标注者信息、审核状态等
文件特点:JSON文件较大(几MB到几十MB),需要可靠上传
二、简化实现方案
1. 标注结果上传组件
// AnnotationUploader.js
class AnnotationUploader {
constructor() {
this.chunkSize = 2 * 1024 * 1024; // 2MB每片
}
// 核心方法:上传标注结果
async uploadAnnotation(annotationData, taskId) {
// 1. 将标注数据转为Blob
const blob = new Blob(
[JSON.stringify(annotationData)],
{ type: 'application/json' }
);
// 2. 生成唯一标识
const fileId = `annotation_${taskId}_${Date.now()}`;
// 3. 分片上传
return await this.uploadWithChunks(blob, fileId);
}
// 分片上传逻辑
async uploadWithChunks(blob, fileId) {
const totalChunks = Math.ceil(blob.size / this.chunkSize);
// 检查已上传的分片(断点续传)
const uploaded = await this.getUploadedChunks(fileId);
for (let i = 0; i < totalChunks; i++) {
if (uploaded.includes(i)) {
console.log(`分片 ${i} 已上传,跳过`);
continue;
}
// 切片
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, blob.size);
const chunk = blob.slice(start, end);
// 上传分片
await this.uploadChunk(chunk, {
fileId,
chunkIndex: i,
totalChunks
});
// 更新进度
this.onProgress?.(i + 1, totalChunks);
}
// 通知服务器合并
return await this.mergeFile(fileId, totalChunks);
}
// 上传单个分片
async uploadChunk(chunk, meta) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('fileId', meta.fileId);
formData.append('chunkIndex', meta.chunkIndex);
formData.append('totalChunks', meta.totalChunks);
const response = await fetch('/api/annotation/upload-chunk', {
method: 'POST',
body: formData,
headers: {
'Authorization': `Bearer ${this.getToken()}`
}
});
if (!response.ok) {
throw new Error(`分片${meta.chunkIndex}上传失败`);
}
return response.json();
}
// 查询已上传的分片(断点续传关键)
async getUploadedChunks(fileId) {
try {
const response = await fetch(
`/api/annotation/upload-status?fileId=${fileId}`,
{
headers: {
'Authorization': `Bearer ${this.getToken()}`
}
}
);
const data = await response.json();
return data.uploadedChunks || [];
} catch (error) {
return []; // 首次上传
}
}
// 合并文件
async mergeFile(fileId, totalChunks) {
const response = await fetch('/api/annotation/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.getToken()}`
},
body: JSON.stringify({ fileId, totalChunks })
});
return response.json();
}
getToken() {
return localStorage.getItem('token');
}
}
2. 在标注编辑器中使用
// AnnotationEditor.vue
export default {
data() {
return {
annotations: {
taskId: '12345',
frames: [
{
frameId: 0,
objects: [
{
id: 'obj_1',
type: 'car',
position: { x: 10, y: 20, z: 0 },
rotation: { x: 0, y: 0, z: 1.57 },
size: { width: 4.5, length: 2, height: 1.8 }
}
]
}
],
metadata: {
annotator: 'user_001',
timestamp: Date.now()
}
},
uploadProgress: 0,
isUploading: false
}
},
methods: {
// 保存标注结果
async saveAnnotations() {
this.isUploading = true;
const uploader = new AnnotationUploader();
// 设置进度回调
uploader.onProgress = (current, total) => {
this.uploadProgress = Math.round((current / total) * 100);
};
try {
const result = await uploader.uploadAnnotation(
this.annotations,
this.annotations.taskId
);
this.$message.success('标注结果保存成功');
console.log('文件路径:', result.filePath);
} catch (error) {
this.$message.error('保存失败: ' + error.message);
// 可以重试
} finally {
this.isUploading = false;
}
},
// 自动保存(每5分钟)
setupAutoSave() {
setInterval(() => {
if (!this.isUploading) {
this.saveAnnotations();
}
}, 5 * 60 * 1000);
}
},
mounted() {
this.setupAutoSave();
}
}
3. 后端接口(Python Flask示例)
from flask import Flask, request, jsonify
import os
import json
app = Flask(__name__)
TEMP_DIR = './temp_chunks'
UPLOAD_DIR = './annotations'
# 接收分片
@app.route('/api/annotation/upload-chunk', methods=['POST'])
def upload_chunk():
chunk = request.files['chunk']
file_id = request.form['fileId']
chunk_index = request.form['chunkIndex']
# 创建临时目录
chunk_dir = os.path.join(TEMP_DIR, file_id)
os.makedirs(chunk_dir, exist_ok=True)
# 保存分片
chunk_path = os.path.join(chunk_dir, f'chunk_{chunk_index}')
chunk.save(chunk_path)
return jsonify({'success': True, 'chunkIndex': chunk_index})
# 查询上传状态
@app.route('/api/annotation/upload-status', methods=['GET'])
def upload_status():
file_id = request.args.get('fileId')
chunk_dir = os.path.join(TEMP_DIR, file_id)
if not os.path.exists(chunk_dir):
return jsonify({'uploadedChunks': []})
# 获取已上传的分片
chunks = [
int(f.split('_')[1])
for f in os.listdir(chunk_dir)
if f.startswith('chunk_')
]
return jsonify({'uploadedChunks': sorted(chunks)})
# 合并文件
@app.route('/api/annotation/merge', methods=['POST'])
def merge_file():
data = request.json
file_id = data['fileId']
total_chunks = data['totalChunks']
chunk_dir = os.path.join(TEMP_DIR, file_id)
output_path = os.path.join(UPLOAD_DIR, f'{file_id}.json')
# 合并分片
with open(output_path, 'wb') as output_file:
for i in range(total_chunks):
chunk_path = os.path.join(chunk_dir, f'chunk_{i}')
with open(chunk_path, 'rb') as chunk_file:
output_file.write(chunk_file.read())
# 清理临时文件
import shutil
shutil.rmtree(chunk_dir)
# 解析并保存到数据库
with open(output_path, 'r') as f:
annotation_data = json.load(f)
save_to_database(annotation_data) # 保存到数据库
return jsonify({
'success': True,
'filePath': output_path,
'taskId': annotation_data.get('taskId')
})
def save_to_database(annotation_data):
# 保存到数据库的逻辑
pass
三、UI组件示例
<template>
<div class="annotation-save">
<!-- 保存按钮 -->
<el-button
type="primary"
@click="saveAnnotations"
:loading="isUploading"
>
{{ isUploading ? '保存中...' : '保存标注' }}
</el-button>
<!-- 进度条 -->
<el-progress
v-if="isUploading"
:percentage="uploadProgress"
:status="uploadProgress === 100 ? 'success' : ''"
/>
<!-- 断点续传提示 -->
<el-alert
v-if="hasUnfinishedUpload"
title="检测到未完成的上传"
type="warning"
:closable="false"
>
<el-button size="small" @click="resumeUpload">
继续上传
</el-button>
</el-alert>
</div>
</template>
<script>
export default {
data() {
return {
isUploading: false,
uploadProgress: 0,
hasUnfinishedUpload: false
}
},
async mounted() {
// 检查是否有未完成的上传
this.checkUnfinishedUpload();
},
methods: {
async checkUnfinishedUpload() {
const fileId = `annotation_${this.taskId}_*`;
// 检查localStorage或服务器
// ...
},
async resumeUpload() {
// 继续之前的上传
await this.saveAnnotations();
}
}
}
</script>
四、关键优化点
1. 压缩标注数据
// 上传前压缩
import pako from 'pako';
const compressed = pako.gzip(JSON.stringify(annotationData));
const blob = new Blob([compressed], { type: 'application/gzip' });
2. 增量保存
// 只保存变更的帧
const changedFrames = this.annotations.frames.filter(f => f.modified);
await uploader.uploadAnnotation({
taskId: this.taskId,
frames: changedFrames,
isIncremental: true
});
3. 离线缓存
// 网络断开时保存到IndexedDB
if (!navigator.onLine) {
await saveToIndexedDB(this.annotations);
this.$message.info('已离线保存,联网后自动上传');
}
五、完整流程
标注编辑器
↓
点击保存按钮
↓
生成标注JSON → 转为Blob → 计算fileId
↓
检查服务器已上传分片(断点续传)
↓
分片上传(跳过已上传的)
↓
所有分片完成 → 通知服务器合并
↓
服务器合并 → 保存到数据库 → 返回成功
↓
前端显示成功提示
这样就实现了针对标注结果的可靠上传方案!