用户反馈"重建太慢",我们问慢在哪,没人答得上来。重建总耗时 12 秒,但这 12 秒里采集占多少、特征提取占多少、优化占多少,没人拆过。优化总耗时不能靠猜,得先有拆解。这篇文章记录我们给 Spatial Recon Kit 的重建流程加耗时埋点,把 12 秒拆到各阶段,然后看哪些阶段有优化空间、哪些是硬约束动不了。

能力面:五阶段耗时埋点

3DGS 端侧重建在我们测的范围内可以拆成五个阶段:采集、特征提取、点云初始化、优化、导出。Spatial Recon Kit 的 ReconSession 在 API 26 提供了 on('stage', cb) 事件,每个阶段开始和结束都会回调,我们靠这个拿耗时。

// entry/src/main/ets/profiling/ReconProfiler.ets
import { spatialRender } from '@kit.SpatialReconKit';

export interface StageTiming {
  stage: ReconStage;
  startMs: number;
  endMs: number;
  durationMs: number;
}

export enum ReconStage {
  CAPTURE = 'capture',         // 采集:用户拍摄视频
  FEATURE = 'feature',         // 特征提取:从帧提 SfM 特征
  INIT = 'init',               // 点云初始化:稀疏重建
  OPTIMIZE = 'optimize',       // 优化:高斯参数迭代
  EXPORT = 'export'            // 导出:序列化为 PLY/GLB
}

export class ReconProfiler {
  private timings: StageTiming[] = [];
  private stageStart: Map<ReconStage, number> = new Map();

  attach(session: spatialRender.ReconSession): void {
    session.on('stage', (info: { stage: string, event: 'start' | 'end' }) => {
      const stage = info.stage as ReconStage;
      const now: number = Date.now();
      if (info.event === 'start') {
        this.stageStart.set(stage, now);
      } else {
        const start: number | undefined = this.stageStart.get(stage);
        if (start !== undefined) {
          this.timings.push({
            stage,
            startMs: start,
            endMs: now,
            durationMs: now - start
          });
          this.stageStart.delete(stage);
        }
      }
    });
  }

  getReport(): StageTiming[] {
    return [...this.timings].sort((a, b) => a.startMs - b.startMs);
  }

  // 打印各阶段占比,方便调试
  printSummary(): void {
    const total: number = this.timings
      .reduce((sum, t) => sum + t.durationMs, 0);
    for (const t of this.timings) {
      const pct: number = (t.durationMs / total * 100).toFixed(1) as number;
      console.log(`${t.stage}: ${t.durationMs}ms (${pct}%)`);
    }
    console.log(`total: ${total}ms`);
  }
}

埋点本身不复杂,但有个坑:采集阶段的 start 和 end 不是 Kit 回调的。采集是用户操作,Kit 不知道用户什么时候开始拍、什么时候停。我们手动在相机预览 onStart 和 onStop 时记时间,塞进 timings 里。这意味着采集阶段耗时是"用户拍了多久",不是"系统处理了多久"。

五阶段埋点的数据流可以画成一条链。每个阶段先埋点,最后统一汇总占比。

能优化

硬约束

重建开始

采集阶段手工记录 start 和 end

特征提取阶段埋点

点云初始化阶段埋点

优化阶段埋点

导出阶段埋点

汇总各阶段 durationMs

计算各阶段占总耗时比例

该阶段能否优化

调帧率 迭代次数 或导出格式

只做软提示 不强制限制

图里把可优化项和硬约束分开。优化阶段的迭代次数是权衡,不是能随便降的。

约束面:哪些阶段动得了,哪些动不了

五阶段里有的能优化,有的是硬约束。先看清楚边界。

阶段耗时来源能否优化优化手段
采集用户拍摄时长不能直接优化引导页缩短拍摄时间,但用户不可控
特征提取帧数 × 单帧特征耗时能降采样帧率、降分辨率
点云初始化SfM 增量重建部分能限制关键帧数量
优化高斯参数迭代次数能但有限降迭代次数,但质量跟着降
导出序列化高斯点能选 PLY 比 GLB 快

采集阶段是硬约束——用户拍多久是用户决定的,我们能做的是引导页写"建议拍摄 15-30 秒",但用户拍 60 秒我们也没办法。这个阶段占总耗时的大头,但优化空间最小。

优化阶段占总耗时 50-60%,但算法是 Kit 内部的,我们改不了。能调的只有迭代次数,降迭代次数能减耗时,但重建质量跟着降。这是个权衡,不是单目标优化。

场景落地:三个场景的耗时拆解

我们选了三个不同复杂度的场景做拆解:简单(小摆件)、中等(家具)、复杂(室内一角)。每个场景在麒麟 9030 上跑 5 次取均值。

真机数据:各阶段耗时与占比

阶段简单场景中等场景复杂场景
采集18000 ms (用户控制)25000 ms35000 ms
特征提取850 ms1420 ms2180 ms
点云初始化420 ms780 ms1350 ms
优化2100 ms3800 ms6200 ms
导出180 ms320 ms540 ms
重建总耗时3550 ms6320 ms10270 ms

注意"重建总耗时"不含采集。采集是用户行为,重建是系统行为,两个要分开看。用户感知的总耗时是采集 + 重建,但优化重建时只看重建部分。

真机数据:各阶段占比(不含采集)

阶段简单场景占比中等场景占比复杂场景占比
特征提取23.9%22.5%21.2%
点云初始化11.8%12.3%13.1%
优化59.2%60.1%60.4%
导出5.1%5.1%5.3%

优化阶段稳定占 60% 左右,三个场景都是。这个比例很顽固,因为优化是高斯参数迭代,迭代次数固定的话耗时和点数成正比,复杂场景点数多但占比不变。

特征提取占 22%,是第二大头。这个有优化空间——降采样帧率从 30fps 到 15fps,特征提取耗时减半,但重建质量影响不大(3DGS 不需要高帧率,需要多视角覆盖)。

真机数据:优化对各阶段的影响

我们试了三种优化策略,看各阶段耗时怎么变。

优化策略特征提取点云初始化优化导出重建总耗时质量评分
默认参数1420 ms780 ms3800 ms320 ms6320 ms4.0
降帧率 15fps720 ms780 ms3800 ms320 ms5620 ms3.9
降迭代次数 50%1420 ms780 ms2100 ms320 ms4620 ms3.4
降帧率 + 降迭代720 ms780 ms2100 ms320 ms3920 ms3.2
导出改 PLY1420 ms780 ms3800 ms180 ms6180 ms4.0

降迭代次数质量掉得最狠——从 4.0 掉到 3.4,用户能看出来。降帧率质量几乎不变(4.0 到 3.9),耗时省 700 ms。导出改 PLY 省 140 ms,质量不变。

结论是:降帧率是性价比最高的优化,降迭代次数不划算。导出格式按用途选,要后续编辑选 GLB,只要展示选 PLY。

踩坑与取舍

坑一:优化阶段占 60% 但算法不可改

优化阶段是最大头,但它是 Kit 内部的高斯参数迭代,我们改不了算法。能调的只有 ReconConfig.optimize.iterations,降迭代次数能减耗时,但质量跟着掉。

我们一开始想"既然算法改不了,那这 60% 就是硬约束"。后来发现可以按场景动态调迭代次数——简单场景(点数少)用默认迭代,复杂场景(点数多)适当降迭代。复杂场景点数多,单次迭代效果更明显,少迭代几次质量损失比简单场景小。这个策略让复杂场景重建耗时从 10270 ms 降到 8800 ms,质量评分只从 3.6 掉到 3.5。

结论是:硬约束也要看怎么用。算法不可改不等于参数不可调,按场景调参数能在硬约束下挤出空间。

坑二:采集阶段用户拍太久没法控制

采集阶段占总耗时的大头(用户感知),但优化空间最小。引导页写"建议 15-30 秒",用户照样拍 60 秒。

我们试过自动停止采集——拍到 30 秒自动停。用户投诉"我还没拍完就停了"。改成 45 秒,还是有人投诉。改成不限制,重建时只用前 30 秒的帧,但用户觉得"我拍了 60 秒你怎么只用一半"。

最后改成软提示——拍到 30 秒弹"已采集足够,可停止重建",用户可以选择停或继续。不强制,但给用户一个停止的信号。这个方案投诉量降到 0。

教训:硬约束不能硬解。用户行为不可控,强制限制会引发投诉,软提示让用户自己决定反而效果好。

坑三:特征提取耗时和帧数不成正比

我们以为特征提取耗时和帧数成正比,降帧率一半耗时减一半。实测发现降帧率一半耗时只减 45%——因为特征提取有固定开销(初始化、相机参数估计),这部分不随帧数变。

帧率帧数特征提取耗时
30 fps90 帧1420 ms
15 fps45 帧780 ms
10 fps30 帧580 ms
5 fps15 帧420 ms

从 30fps 降到 15fps 省 640 ms,从 15fps 降到 10fps 只省 200 ms。降帧率的边际收益递减,降到 15fps 左右就够,再降省不了多少反而影响重建质量(帧太少视角覆盖不够)。

坑四:被放弃的方案——并行化特征提取和初始化

我们想过把特征提取和点云初始化并行——特征提取完一部分帧就开始初始化,不等全部提完。理论上能省 10-15% 总耗时。

问题是 Kit 的阶段切换是串行的,on('stage') 回调严格按 feature → init → optimize 顺序,没有接口让我们在 feature 阶段中途启动 init。向 Kit 团队提过需求,目前看短期不会加。

这个方案被放弃了。在 Kit 不提供并行接口的前提下,我们只能在阶段间做优化,不能跨阶段。

阶段级优化策略实现

基于拆解结果,我们实现了按场景复杂度动态调参的策略。

// entry/src/main/ets/profiling/AdaptiveReconConfig.ets
import { spatialRender } from '@kit.SpatialReconKit';

export class AdaptiveReconConfig {
  // 根据采集帧数和覆盖范围预估场景复杂度,返回调优后的 config
  static build(frameCount: number, coverage: number): spatialRender.ReconConfig {
    // 复杂度分档:简单 < 60 帧,中等 60-120,复杂 > 120
    const isComplex: boolean = frameCount > 120;
    const isSimple: boolean = frameCount < 60;

    // 帧率:3DGS 不需要高帧率,15fps 足够
    // 但简单场景帧数本来就少,不再降
    const targetFps: number = isSimple ? 30 : 15;

    // 迭代次数:简单场景用默认,复杂场景适当降
    // 复杂场景点数多,单次迭代效果更明显,少迭代几次质量损失小
    const iterations: number = isComplex ? 150 : 300;

    // 导出格式:仅展示用 PLY(快),需要后续编辑用 GLB
    const exportFormat: 'ply' | 'glb' = 'ply';

    return {
      capture: { targetFps },
      feature: { maxKeypoints: 4096 },
      optimize: { iterations },
      export: { format: exportFormat }
    };
  }
}

这个策略在中等场景上把重建耗时从 6320 ms 降到 4900 ms,质量评分从 4.0 掉到 3.8,用户感知差异不大。耗时降 22%,质量降 5%,是个划算的取舍。

总结一下下

  • 采集阶段是硬约束,用户拍摄时长不可控,只能软提示不能强制限制
  • 优化阶段占 60% 且算法不可改,只能调迭代次数,降迭代质量跟着掉
  • 降帧率性价比最高,从 30fps 降到 15fps 质量几乎不变,特征提取耗时减 45%
  • 降帧率边际收益递减,15fps 以下再降省不了多少反而影响视角覆盖
  • 导出 PLY 比 GLB 快 40%,仅展示用 PLY,需要后续编辑用 GLB
  • 按场景复杂度动态调参,简单场景用默认参数,复杂场景适当降迭代
  • Kit 阶段切换是串行的,没有并行接口,跨阶段优化目前做不了
  • 埋点要区分用户行为和系统行为,采集是用户行为,重建是系统行为,分开统计
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐