自动驾驶标注数据分片上传

66 阅读3分钟

自动驾驶标注平台:标注结果分片上传指南

一、标注结果特点

自动驾驶标注结果通常包含:

  • 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
    ↓
检查服务器已上传分片(断点续传)
    ↓
分片上传(跳过已上传的)
    ↓
所有分片完成 → 通知服务器合并
    ↓
服务器合并 → 保存到数据库 → 返回成功
    ↓
前端显示成功提示

这样就实现了针对标注结果的可靠上传方案!