跳转到内容

点云3D可视化与标注模块

刀刀
0字
0分钟
9/7/2026

业务背景与痛点

项目背景

在无人机室内巡检与数字孪生场景中,我们需要在浏览器端展示和管理海量点云数据(PCD格式),并支持空间标注功能。业务场景包括:

  • 模型资产管理:上传、下载、删除点云模型文件
  • 3D空间标注:在点云上标注关键点/框,记录位置、颜色、说明等信息
  • 多引擎适配:同时支持 Three.js(点云)和 Cesium(实景三维)两种渲染引擎

核心痛点

痛点具体表现影响
大文件加载崩溃单模型数十万至千万级点,主线程解析导致浏览器内存溢出页面卡死,用户体验极差
3D交互复杂画框、标点、视角切换等交互逻辑与渲染耦合严重代码难以维护,扩展成本高
多引擎适配Three.js 与 Cesium 标注逻辑重复实现开发效率低,一致性难保证
业务割裂3D查看器与2D列表、表单分离用户体验不连贯,开发重复劳动

技术选型与架构设计

技术栈选型

技术栈选型

核心选型决策:

技术选型理由替代方案对比
Three.js轻量级、点云渲染成熟、社区活跃Cesium(过重)、Babylon.js(学习曲线陡)
Web Worker主线程零阻塞,彻底解决大文件卡顿SharedArrayBuffer(兼容性差)、WebAssembly(开发成本高)
蓄水池抽样一趟扫描完成均匀抽样,空间O(K)随机跳采样(需预知总数)、分块抽样(实现复杂)
Composition API逻辑复用性强,类型推导完善Options API(代码分散)、Mixin(命名冲突)

整体架构

整体架构

核心实现机制

Web Worker + 蓄水池抽样(性能治本方案)

问题: 传统在主线程解析PCD文件,内存峰值 = 文件大小 × 2(ArrayBuffer + Geometry),100MB文件会导致浏览器崩溃。

解决方案: 将下载、解析、抽稀全部移入 Web Worker,使用蓄水池抽样算法,内存占用从 O(N) 降至 O(K)(K为目标点数)。

3.1.1 蓄水池抽样算法原理

typescript
// pointCloudWorker.ts 核心逻辑
function reservoirSampleBinary(
  dataBuffer: ArrayBuffer,
  header: PCDHeader,
  targetCount: number,
): { positions: Float32Array; colors: Float32Array | null } {
  const totalPoints = header.points
  const actualTarget = Math.min(targetCount, totalPoints)

  // 蓄水池:只存 K 个点
  const reservoirPos = new Float32Array(actualTarget * 3)
  const reservoirCol = header.hasColor
    ? new Float32Array(actualTarget * 3)
    : null

  for (let i = 0; i < totalPoints; i++) {
    // 1. 解析当前点
    const x = dataView.getFloat32(byteOffset + xOff, true)
    const y = dataView.getFloat32(byteOffset + yOff, true)
    const z = dataView.getFloat32(byteOffset + zOff, true)

    // 2. 蓄水池抽样逻辑
    if (i < actualTarget) {
      // 前 K 个点直接放入蓄水池
      reservoirPos[i * 3] = x
      reservoirPos[i * 3 + 1] = y
      reservoirPos[i * 3 + 2] = z
    } else {
      // 第 i 个点以 K/i 的概率替换蓄水池中的随机点
      const j = Math.floor(Math.random() * (i + 1))
      if (j < actualTarget) {
        reservoirPos[j * 3] = x
        reservoirPos[j * 3 + 1] = y
        reservoirPos[j * 3 + 2] = z
      }
    }
  }

  return { positions: reservoirPos, colors: reservoirCol }
}

算法优势:

  • 无需预知总点数,一趟扫描完成
  • 时间复杂度 O(N),空间复杂度 O(K)
  • 保证每个点被选中的概率相等(1/N)

3.1.2 流式下载与零拷贝传输

typescript
// pointCloudWorker.ts - 流式下载
async function fetchWithProgress(
  url: string,
  onProgress: (pct: number, status: string) => void,
): Promise<ArrayBuffer> {
  const response = await fetch(url)
  const contentLength = Number(response.headers.get('content-length') || '0')

  const reader = response.body.getReader()
  const chunks: Uint8Array[] = []
  let received = 0

  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    chunks.push(value)
    received += value.length

    // 实时上报进度
    const pct = Math.round((received / contentLength) * 30)
    onProgress(pct, `正在下载 ${receivedMB}MB / ${totalMB}MB`)
  }

  // 合并 chunks
  const totalLength = chunks.reduce((sum, c) => sum + c.length, 0)
  const arrayBuffer = new ArrayBuffer(totalLength)
  const uint8 = new Uint8Array(arrayBuffer)
  let offset = 0
  for (const chunk of chunks) {
    uint8.set(chunk, offset)
    offset += chunk.length
  }

  return arrayBuffer
}

// 主线程与 Worker 通信 - Transferable 零拷贝
worker.postMessage(
  { type: 'loadAndDownsample', url, targetCount: maxPoints },
  [posArray.buffer], // Transferable 对象,所有权转移,零拷贝
)

性能对比:

方案100MB PCD文件内存峰值主线程阻塞
传统方案15-20秒200MB+严重卡顿
Web Worker + 蓄水池8-12秒15MB(15万点)零阻塞

多引擎统一标注契约

问题: Three.js 和 Cesium 需要分别实现标注功能,导致代码重复、色板不一致、事件载荷不同。

解决方案: 抽取 useAnnotationShared.ts 作为单一事实来源(Single Source of Truth)。

typescript
// useAnnotationShared.ts - 统一契约
export const ANNOTATION_COLOR_HEX: Record<string, string> = {
  blue: '#3B82F6',
  green: '#22C55E',
  yellow: '#EAB308',
  orange: '#F97316',
  purple: '#8B5CF6',
  red: '#EF4444',
  cyan: '#06B6D4',
  pink: '#EC4899',
}

export interface BoxDrawnPayload {
  positionX: number
  positionY: number
  positionZ: number
  width: number
  height: number
  depth: number
}

export const isValidBoxSize = (
  size: { width: number; height: number; depth: number },
  min = 0.01,
): boolean => size.width > min || size.height > min || size.depth > min

业务层零感知切换引擎:

vue
<!-- Annotate.vue -->
<template v-if="modelFormat === 'pcd'">
  <PointCloudViewer
    :annotations="annotationList"
    :draw-mode="drawMode"
    @box-drawn="handleBoxDrawn"  <!-- 统一事件载荷 -->
  />
</template>
<template v-else>
  <CesiumSceneViewer
    :annotations="annotationList"
    :draw-mode="drawMode"
    @box-drawn="handleBoxDrawn"  <!-- 同一处理函数 -->
  />
</template>

<script setup lang="ts">
// 统一处理逻辑,不感知底层引擎
const handleBoxDrawn = (boxData: BoxDrawnPayload) => {
  formData.value = {
    type: 'box',
    positionX: boxData.positionX,
    positionY: boxData.positionY,
    positionZ: boxData.positionZ,
    width: boxData.width,
    height: boxData.height,
    depth: boxData.depth,
  }
  visible.value = true
}
</script>

沉浸式3D交互系统

左键画框实现

核心流程:

左键画框实现

关键代码:

typescript
// PointCloudViewer.vue - 画框逻辑
const getIntersectionPoint = (event: PointerEvent): THREE.Vector3 | null => {
  const rect = containerRef.value.getBoundingClientRect()
  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
  mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1

  raycaster.setFromCamera(mouse, renderer.getCamera())

  // 优先与点云求交,否则投影到相机前方平面
  if (currentPointCloud) {
    const intersects = raycaster.intersectObject(currentPointCloud, false)
    if (intersects.length > 0) return intersects[0].point
  }

  //  fallback:投影到相机前方平面
  const planeNormal = new THREE.Vector3(0, 0, 1).applyQuaternion(
    camera.quaternion,
  )
  const plane = new THREE.Plane().setFromNormalAndCoplanarPoint(
    planeNormal,
    center,
  )
  return raycaster.ray.intersectPlane(plane, target)
}

const onPointerUp = () => {
  if (startPoint && currentPoint) {
    const box = new THREE.Box3().setFromPoints([startPoint, currentPoint])
    const center = box.getCenter(new THREE.Vector3())
    const size = box.getSize(new THREE.Vector3())

    // 过滤误触极小框
    if (isValidBoxSize({ width: size.x, height: size.y, depth: size.z })) {
      emit('box-drawn', {
        positionX: center.x,
        positionY: center.y,
        positionZ: center.z,
        width: size.x,
        height: size.y,
        depth: size.z,
      })
    }
  }
}

右键球坐标环绕

设计思路: 禁用默认 OrbitControls,手动实现基于球坐标的相机环绕,支持平滑拖拽和预设视角切换。

typescript
// PointCloudViewer.vue - 球坐标环绕
const ORBIT_SENSITIVITY = 0.005
const CLICK_THRESHOLD = 3 // 像素,小于此值视为点击

const getSpherical = (): { theta: number; phi: number; radius: number } => {
  const camera = renderer.getCamera()
  const center = getModelCenter()
  const offset = camera.position.clone().sub(center)
  const radius = offset.length()
  const phi = Math.acos(THREE.MathUtils.clamp(offset.y / radius, -1, 1))
  const theta = Math.atan2(offset.z, offset.x)
  return { theta, phi, radius }
}

const onRightPointerMove = (event: PointerEvent) => {
  if (!isRightDragging) return

  const deltaX = event.clientX - rightDragStart.x
  const deltaY = event.clientY - rightDragStart.y

  // 鼠标右移 → theta 减小 → 相机向右环绕
  // 鼠标下移 → phi 增大 → 相机向下倾斜
  const theta = orbitStartTheta - deltaX * ORBIT_SENSITIVITY
  const phi = orbitStartPhi + deltaY * ORBIT_SENSITIVITY
  const radius = getSpherical().radius

  setFromSpherical(theta, phi, radius)
}

const onRightPointerUp = (event: PointerEvent) => {
  // 拖拽距离小 → 快速点击 → 循环预设视角
  if (rightDragTotalDelta < CLICK_THRESHOLD) {
    currentViewIndex = (currentViewIndex + 1) % PRESET_VIEWS.length
    switchToView(currentViewIndex)
    return
  }

  // 拖拽结束 → 输出当前视角JSON,方便回填固定视角
  console.log(
    '[右键环绕] 当前视角',
    JSON.stringify({
      position: camera.position.toArray(),
      target: center.toArray(),
      up: camera.up.toArray(),
    }),
  )
}

预设视角平滑过渡:

typescript
const PRESET_VIEWS: PresetView[] = [
  { name: '默认', direction: [0.9, 0.08, 0.43], up: [0, 1, 0] },
  { name: '俯视', direction: [0, 1, 0.001], up: [0, 0, 1] },
  { name: '正视', direction: [0, 0, 1], up: [0, 1, 0] },
  { name: '右视', direction: [1, 0, 0], up: [0, 1, 0] },
]

const switchToView = (viewIndex: number) => {
  const view = PRESET_VIEWS[viewIndex % PRESET_VIEWS.length]
  const camera = renderer.getCamera()
  const center = getModelCenter()
  const distance = getModelRadius() * 1.4

  const targetPos = center
    .clone()
    .addScaledVector(new THREE.Vector3(...view.direction).normalize(), distance)

  // 500ms 缓动动画
  const duration = 500
  const startTime = performance.now()

  const animate = (now: number) => {
    const elapsed = now - startTime
    const t = Math.min(elapsed / duration, 1.0)
    const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2

    camera.position.lerpVectors(startPos, targetPos, ease)
    camera.up.lerpVectors(startUp, targetUp, ease).normalize()
    camera.lookAt(center)

    if (t < 1) requestAnimationFrame(animate)
  }

  requestAnimationFrame(animate)
}

企业级基建复用

SearchForm 配置化搜索

模型管理页面应用:

vue
<!-- index.vue - 模型列表搜索 -->
<script setup lang="ts">
const filterForm = ref({
  modelName: '',
  createTime: '',
})

const formItems = [
  {
    label: '文件名称',
    key: 'modelName',
    span: 6,
    props: {
      placeholder: '请输入文件名称',
      prefixIcon: Search,
    },
  },
  {
    label: '创建时间',
    key: 'createTime',
    type: 'datePicker',
    span: 6,
    props: {
      type: 'daterange',
      onChange: (e: Date[] | null) => {
        filterForm.value.createTime = e ? dayjs(e[0]).format('YYYY-MM-DD') : ''
      },
    },
  },
]
</script>

<template>
  <SearchForm
    v-model="filterForm"
    :form-items="formItems"
    :btn-span="10"
    @search="dynamicTableRef?.initTableData"
    @reset="dynamicTableRef?.initTableData"
  >
    <template #handleButton>
      <el-button :icon="Upload" @click="handleImportModel">
        导入模型
      </el-button>
    </template>
  </SearchForm>
</template>

核心特性:

  • 配置化驱动,减少70%模板代码
  • 支持自定义插槽(#handleButton 添加导入按钮)
  • 智能重置策略(keep 属性保留分页参数)

DynamicTable 动态表格

模型列表配置:

vue
<!-- index.vue - 模型列表 -->
<script setup lang="ts">
const columns = [
  { label: '文件名称', prop: 'modelName', minWidth: 200 },
  { label: '格式', prop: 'modelFormat', width: 140 },
  { label: '模型大小', prop: 'modelSize', width: 140 },
  { label: '创建时间', prop: 'createTime', width: 200 },
  { label: '操作', slotName: 'cell-action', width: 200, align: 'center' },
]
</script>

<template>
  <DynamicTable
    ref="dynamicTableRef"
    :request-method="async (pagination) => {
      const res = await modelApi.getModelList({ ...pagination, ...filterForm })
      return {
        ...res,
        rows: res.rows.filter((item) => item.modelFormat !== 'png'),
      }
    }}"
    :columns="columns"
  >
    <!-- 自定义格式列 -->
    <template #cell-modelFormat="{ row }">
      <el-tag :type="row.modelFormat === 'b3dm' ? 'success' : 'primary'">
        {{ row.modelFormat }}
      </el-tag>
    </template>

    <!-- 自定义操作列 -->
    <template #cell-action="{ row }">
      <el-button link type="primary" @click="handleAnnotate(row)">
        标注
      </el-button>
      <el-button link type="primary" @click="handleDownload(row)">
        下载
      </el-button>
      <el-button
        v-permission="{ menu: 'map_model_delete' }"
        link
        type="danger"
        @click="handleDelete(row)"
      >
        删除
      </el-button>
    </template>
  </DynamicTable>
</template>

核心特性:

  • 注入 requestMethod 自动接管分页与Loading
  • 配置化列定义,支持自定义插槽
  • 内置轮询、合并单元格、多选联动等高级功能

应用场景与实战

模型资产管理(index.vue)

功能流程:

关键实现:

typescript
// index.vue - 文件导入校验
const validTypes = [
  'b3dm',
  'pcd',
  'glb',
  'gltf',
  'osgb',
  'osgbz',
  'obj',
  'ply',
  'stl',
  'fbx',
  '3ds',
  'dae',
  'ifc',
  'step',
  'iges',
  'x3d',
  'vrml',
]

const handleFileChange = async (event: Event) => {
  const file = (event.target as HTMLInputElement).files?.[0]
  const type = file?.name.split('.').pop()?.toLowerCase()

  if (!validTypes.includes(type || '')) {
    ElMessage.error('请选择有效的模型文件')
    return
  }

  await uploadFile(file)
}

标注工作台(Annotate.vue)

功能流程:

标注工作台

关键实现:

vue
<!-- Annotate.vue - 模式切换与事件处理 -->
<script setup lang="ts">
const drawMode = ref<'point' | 'box'>('point')

// 点击空白处(新增点)
const handlePointCloudClick = (position: {
  x: number
  y: number
  z: number
}) => {
  if (drawMode.value !== 'point') return

  formData.value = {
    type: 'point',
    positionX: position.x,
    positionY: position.y,
    positionZ: position.z,
  }
  visible.value = true
}

// 画框完成
const handleBoxDrawn = (boxData: BoxDrawnPayload) => {
  formData.value = {
    type: 'box',
    positionX: boxData.positionX,
    positionY: boxData.positionY,
    positionZ: boxData.positionZ,
    width: boxData.width,
    height: boxData.height,
    depth: boxData.depth,
  }
  visible.value = true
}
</script>

<template>
  <!-- 模式切换工具栏 -->
  <div class="absolute top-4 left-1/2 -translate-x-1/2 z-10">
    <el-radio-group v-model="drawMode" size="small">
      <el-radio-button value="point">
        <el-icon><Aim /></el-icon> 标点
      </el-radio-button>
      <el-radio-button value="box">
        <el-icon><Crop /></el-icon> 画框
      </el-radio-button>
    </el-radio-group>
  </div>

  <!-- Three.js 点云分支 -->
  <PointCloudViewer
    v-if="modelFormat === 'pcd'"
    :draw-mode="drawMode"
    @point-cloud-click="handlePointCloudClick"
    @box-drawn="handleBoxDrawn"
  />

  <!-- Cesium 实景分支 -->
  <CesiumSceneViewer
    v-else
    :draw-mode="drawMode"
    @scene-click="handlePointCloudClick"
    @box-drawn="handleBoxDrawn"
  />
</template>

标注渲染与交互

标注点渲染(Three.js):

typescript
// usePointCloudInteraction.ts - 创建标注点
createAnnotation(annotation: AnnotationPoint): void {
  const group = new THREE.Group()
  group.position.set(
    annotation.positionX,
    annotation.positionY,
    annotation.positionZ
  )

  const color = this.getColorValue(annotation.color)

  // 1. 球体(主体)
  const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 16, 16),
    new THREE.MeshPhongMaterial({
      color,
      emissive: color,
      emissiveIntensity: 0.3,
      transparent: true,
      opacity: 0.9,
    })
  )
  group.add(sphere)

  // 2. 光晕(发光效果)
  const glow = new THREE.Mesh(
    new THREE.SphereGeometry(0.8, 16, 16),
    new THREE.MeshBasicMaterial({
      color,
      transparent: true,
      opacity: 0.2,
    })
  )
  group.add(glow)

  // 3. 文字标签(CanvasTexture)
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')!
  canvas.width = 256
  canvas.height = 64

  context.fillStyle = 'rgba(0, 0, 0, 0.7)'
  context.roundRect(0, 0, 256, 64, 8)
  context.fill()

  context.fillStyle = 'white'
  context.font = 'bold 28px Microsoft YaHei'
  context.textAlign = 'center'
  context.fillText(annotation.annotationText || `标注${annotation.id}`, 128, 32)

  const sprite = new THREE.Sprite(
    new THREE.SpriteMaterial({
      map: new THREE.CanvasTexture(canvas),
      transparent: true,
    })
  )
  sprite.position.y = 1.5
  sprite.scale.set(3, 0.75, 1)
  group.add(sprite)

  this.annotationGroup.add(group)
  this.annotationMeshes.set(annotation.id, group)
}

踩坑记录与解决方案

大文件加载崩溃

问题: 100MB PCD文件在主线程解析,浏览器内存峰值超过2GB,导致页面崩溃。

原因分析:

  • ArrayBuffer 占用 100MB
  • Three.js Geometry 复制一份数据,再占 100MB
  • 响应式对象开销、临时变量等额外占用

解决方案:

  1. 移入 Web Worker,主线程零阻塞
  2. 蓄水池抽样,内存从 O(N) 降至 O(K)
  3. Transferable 零拷贝传输

效果对比:

指标优化前优化后
加载时间15-20秒8-12秒
内存峰值200MB+15MB(15万点)
主线程阻塞严重卡顿零阻塞

PCD RGB颜色解码异常

问题: PCD文件的 rgb 字段是 float32 类型,直接读取显示颜色异常。

原因: PCD 的 rgb 字段使用 IEEE 754 位模式编码 RGB 整数值,需要重新解释为 uint32。

解决方案:

typescript
// pointCloudWorker.ts - PCD RGB 解码
function decodePCDRGB(rgbFloat: number): [number, number, number] {
  const buf = new ArrayBuffer(4)
  new Float32Array(buf)[0] = rgbFloat
  const rgbInt = new Uint32Array(buf)[0]

  return [
    ((rgbInt >> 16) & 0xff) / 255, // R
    ((rgbInt >> 8) & 0xff) / 255, // G
    (rgbInt & 0xff) / 255, // B
  ]
}

坐标系不一致导致模型倒伏

问题: PCD 文件使用 Z-Up 坐标系,Three.js 使用 Y-Up 坐标系,直接加载模型倒伏。

解决方案:

typescript
// usePointCloudProcessor.ts - 坐标系转换
static convertCoordinateSystem(geometry: THREE.BufferGeometry): void {
  const positions = geometry.attributes.position.array as Float32Array

  for (let i = 0; i < positions.length; i += 3) {
    const oldX = positions[i]
    const oldY = positions[i + 1]
    const oldZ = positions[i + 2]

    // Z-Up → Y-Up: (x, y, z) → (x, z, -y)
    positions[i] = oldX
    positions[i + 1] = oldZ
    positions[i + 2] = -oldY
  }

  geometry.attributes.position.needsUpdate = true
  geometry.computeBoundingBox()
}

画框模式误触生成极小框

问题: 鼠标点击与拖拽的像素级抖动导致生成宽度<0.01的无效框。

解决方案:

typescript
// useAnnotationShared.ts - 统一阈值策略
export const isValidBoxSize = (
  size: { width: number; height: number; depth: number },
  min = 0.01,
): boolean => {
  return size.width > min || size.height > min || size.depth > min
}

// PointCloudViewer.vue - 过滤无效框
const onPointerUp = () => {
  if (startPoint && currentPoint) {
    const box = new THREE.Box3().setFromPoints([startPoint, currentPoint])
    const size = box.getSize(new THREE.Vector3())

    if (isValidBoxSize({ width: size.x, height: size.y, depth: size.z })) {
      emit('box-drawn', { ... })
    }
  }
}

右键拖拽与画框冲突

问题: 默认 OrbitControls 占用鼠标事件,与自定义画框逻辑冲突。

解决方案:

  1. 画框模式禁用 OrbitControls (controls.enabled = false)
  2. 右键单独实现球坐标环绕,手动接管 pointerdown/move/up
  3. 事件挂到 window,拖出画布仍可跟踪
typescript
// PointCloudViewer.vue - 事件绑定
domElement.addEventListener('pointerdown', onPointerDown) // 左键画框
domElement.addEventListener('pointerdown', onRightPointerDown) // 右键环绕
window.addEventListener('pointermove', onRightPointerMove) // 挂 window
window.addEventListener('pointerup', onRightPointerUp)

完整代码结构

文件组织

txt
src/
├── composables/
│   ├── cloud/
│   │   ├── usePointCloudLoader.ts       # 点云加载器
│   │   ├── usePointCloudProcessor.ts    # 点云处理器
│   │   ├── usePointCloudRenderer.ts     # 点云渲染器
│   │   └── usePointCloudInteraction.ts  # 点云交互控制器
│   └── annotation/
│       └── useAnnotationShared.ts       # 标注共享工具
├── workers/
│   └── pointCloudWorker.ts              # Web Worker (蓄水池抽样)
├── components/
│   ├── map/
│   │   ├── PointCloudViewer.vue         # 3D点云查看器
│   │   └── CesiumSceneViewer.vue        # Cesium实景查看器
│   └── annotate/
│       └── AnnotateItem.vue             # 标注编辑弹窗
├── views/
│   └── device-manage/
│       ├── index.vue                    # 模型管理页面
│       └── annotate.vue                 # 标注工作台
└── types/
    └── cloud.ts                         # 类型定义

总结与展望

技术价值总结

通过 "底层 Worker 算力卸载 + 中层引擎契约统一 + 上层基建配置驱动" 的三层架构,成功解决了海量点云在 Web 端的性能与交互难题:

维度成果
性能100MB文件加载时间从20秒降至10秒,内存峰值从200MB降至15MB
开发效率配置化表单/表格减少70%模板代码,多引擎统一契约避免重复实现
用户体验主线程零阻塞,画框/标点/视角切换流畅,媲美专业CAD软件
可维护性分层架构清晰,类型安全完善,单元测试覆盖率>80%

后续扩展方向

  1. LOD多细节层次(Level of Detail)

    • 结合八叉树(Octree)切片,实现基于相机距离的动态加载
    • 突破15万点的渲染上限,支持千万级点云流畅展示
  2. 点云测量工具

    • 两点测距、三点测角、剖面分析
    • 体积计算、表面积估算
  3. 标注数据导出

    • 支持 COCO / YOLO 格式的 3D BBox 数据集导出
    • 反哺 AI 算法训练,形成数据闭环
  4. 协作标注

    • WebSocket 实时同步多用户标注
    • 版本管理、标注审核流程

附录:性能测试数据

文件大小原始点数抽样后点数加载时间内存峰值FPS
10MB50万15万4.2秒8MB60
50MB250万15万7.8秒12MB60
100MB500万15万11.3秒15MB55
200MB1000万15万18.6秒18MB50

贡献者

The avatar of contributor named as duyidao duyidao
The avatar of contributor named as 刀刀 刀刀

页面历史

刀刀博客累计访客 人;文档累计访问量共