HarmonyOS 7重建耗时拆解:采集/特征提取/优化各阶段占比
用户反馈"重建太慢",我们问慢在哪,没人答得上来。重建总耗时 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 里。这意味着采集阶段耗时是"用户拍了多久",不是"系统处理了多久"。
五阶段埋点的数据流可以画成一条链。每个阶段先埋点,最后统一汇总占比。
图里把可优化项和硬约束分开。优化阶段的迭代次数是权衡,不是能随便降的。
约束面:哪些阶段动得了,哪些动不了
五阶段里有的能优化,有的是硬约束。先看清楚边界。
| 阶段 | 耗时来源 | 能否优化 | 优化手段 |
|---|---|---|---|
| 采集 | 用户拍摄时长 | 不能直接优化 | 引导页缩短拍摄时间,但用户不可控 |
| 特征提取 | 帧数 × 单帧特征耗时 | 能 | 降采样帧率、降分辨率 |
| 点云初始化 | SfM 增量重建 | 部分能 | 限制关键帧数量 |
| 优化 | 高斯参数迭代次数 | 能但有限 | 降迭代次数,但质量跟着降 |
| 导出 | 序列化高斯点 | 能 | 选 PLY 比 GLB 快 |
采集阶段是硬约束——用户拍多久是用户决定的,我们能做的是引导页写"建议拍摄 15-30 秒",但用户拍 60 秒我们也没办法。这个阶段占总耗时的大头,但优化空间最小。
优化阶段占总耗时 50-60%,但算法是 Kit 内部的,我们改不了。能调的只有迭代次数,降迭代次数能减耗时,但重建质量跟着降。这是个权衡,不是单目标优化。
场景落地:三个场景的耗时拆解
我们选了三个不同复杂度的场景做拆解:简单(小摆件)、中等(家具)、复杂(室内一角)。每个场景在麒麟 9030 上跑 5 次取均值。
真机数据:各阶段耗时与占比
| 阶段 | 简单场景 | 中等场景 | 复杂场景 |
|---|---|---|---|
| 采集 | 18000 ms (用户控制) | 25000 ms | 35000 ms |
| 特征提取 | 850 ms | 1420 ms | 2180 ms |
| 点云初始化 | 420 ms | 780 ms | 1350 ms |
| 优化 | 2100 ms | 3800 ms | 6200 ms |
| 导出 | 180 ms | 320 ms | 540 ms |
| 重建总耗时 | 3550 ms | 6320 ms | 10270 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 ms | 780 ms | 3800 ms | 320 ms | 6320 ms | 4.0 |
| 降帧率 15fps | 720 ms | 780 ms | 3800 ms | 320 ms | 5620 ms | 3.9 |
| 降迭代次数 50% | 1420 ms | 780 ms | 2100 ms | 320 ms | 4620 ms | 3.4 |
| 降帧率 + 降迭代 | 720 ms | 780 ms | 2100 ms | 320 ms | 3920 ms | 3.2 |
| 导出改 PLY | 1420 ms | 780 ms | 3800 ms | 180 ms | 6180 ms | 4.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 fps | 90 帧 | 1420 ms |
| 15 fps | 45 帧 | 780 ms |
| 10 fps | 30 帧 | 580 ms |
| 5 fps | 15 帧 | 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 阶段切换是串行的,没有并行接口,跨阶段优化目前做不了
- 埋点要区分用户行为和系统行为,采集是用户行为,重建是系统行为,分开统计
更多推荐
所有评论(0)