资讯动态

HarmonyOS 7 Spatial Reconstruction:端侧重建分段采集与失败恢复【鸿蒙心迹】

发布时间:2026/10/9 15:14:45 来源:尧图企业网站定制
一个房间扫到七成用户把手机转向门口预览里的点云还在增长页面却突然显示“采集完成”。三秒后再打开任务详情重建仍在运行但最后一批帧数和刚才看到的数字对不上。这样的故障很容易被归结为“异步回调乱序”真正处理起来却不能靠给按钮加一个loading就解决。这次把场景收窄到一间会议室用ReconFlowLab管理端侧空间重建任务采集页面叫CaptureSessionPage详情页叫ReconTaskDetailPage演示任务固定为recon_20261009_01。系统能力来自 Spatial Recon Kit本文重点则放在系统能力外围的工程协议怎样让一段采集有明确归属怎样冻结已经提交的数据怎样区分界面暂停、任务暂停与重建完成以及中断后到底能恢复到哪一层。文章中的截图、日志和数字是为同一个 Demo 设计的可复现工程演示数据并非真实设备跑出的基准报告代码主要展示 ArkTS 业务调度层与 Native 重建适配层的边界不把自定义方法冒充官方 SDK 接口。真正接入时仍需按设备、SDK 版本和官方文档补齐 C/C 会话及帧输入实现。一、先定位72% 的页面为什么先宣告结束我最先排查的不是算法而是任务页面的三个事件源。第一路来自相机或 AR 采集数据持续给出带时间戳、内参和位姿的帧第二路来自重建引擎它有自己的会话生命周期第三路是 ArkUI 页面既能接收用户的暂停操作也会因为路由切换、窗口变化、后台切换而触发生命周期事件。三个源如果共用一个isRunning布尔值迟早出事。在这个 Demo 的复现场景里MeetingRoom_A已经接纳320 个关键帧其中312 个位姿有效、8 帧被标记为丢弃。界面显示采集进度72%、质量等级High。注意这里的进度是业务采集完成度不是 3DGS 优化器的真实收敛百分比Quality也是业务评价标签不是官方算法保证的精度分级。把它们放到同一张图里只是为了让用户知道“这一段数据处于什么状态”。故障触发点发生在分段提交前后页面接到一次结束请求采集队列中还有帧在路上外层状态先从RUNNING变成DONE随后一条旧批次回调抵达又把已经冻结的统计覆盖。最终表现是“画面看起来完成了数据还在变化”。这不是 UI 单点故障而是缺少提交边界。我把日志按taskId epoch sequence重排后才确认关键问题某个异步回调携带的是旧采集代次epoch12但页面已经进入下一阶段。单纯在回调处判断“页面是否还显示”没有价值必须根据数据属于哪一代决定能否入账。二、把会话、采集批次、页面状态拆成三层Spatial Recon Kit 的官方能力覆盖 C/C 侧空间重建会话以及 3DGS 数据相关处理。对于本例Native 层负责能力检查、创建会话、向会话提交符合格式的采集数据、启动或结束处理以及输出结果ArkTS 负责屏幕、用户操作、任务描述和可持久化检查点。业务里的freezeEpoch()、submitFrameBatch()不是官方同名接口而是我们在ReconstructionTaskService里封装的动作。拆开后要明确三组状态。页面状态是VISIBLE / HIDDEN / DESTROYED只解释 UI 是否可交互。采集状态是READY / CAPTURING / FROZEN / STOPPED解释帧是否还能入队。重建状态是READY / RECONSTRUCTING / FAILED / DONE解释 Native 计算和输出是否完成。为了让用户看到简单结果再用一个投影状态RUNNING / PAUSED / SAVED / FAILED呈现任务整体情况。这三组状态不是枚举名字多了一些而是能解决实际争议页面隐藏不意味着重建会话必须立即销毁采集冻结也不意味着 Native 重建停止文件路径已分配不意味着文件已经可用。只有事件从正确的层进入才能有确定的状态迁移。工程目录按职责切成下面几块pages不直接握住 Native 会话句柄services不直接修改 ArkUI 组件内部状态ReconFlowLab/ └── entry/src/main/ets/ ├── pages/ │ ├── CaptureSessionPage.ets │ └── ReconTaskDetailPage.ets ├── services/ │ └── ReconstructionTaskService.ets ├── utils/ │ └── FrameBatchGate.ets ├── model/ │ ├── ReconTaskState.ets │ └── CaptureFrame.ets └── nativebridge/ └── SpatialReconAdapter.etsSpatialReconAdapter的实现可以走 N-API / Native 包装层但只暴露“创建、提交、结束、读取结果、释放”等应用需要的动作。比如官方数据帧结构中包含相机内参、位姿、时间戳和图像数据采集适配时必须保留同一坐标体系和时间顺序不应把一张普通PixelMap直接当作具备完整定位信息的空间帧。这也是为什么实际工程不能只围绕界面上的帧数做文章。三、第一段代码给所有采集数据加上代次最小修复是建立不可回退的单调代次。每次冻结一个批次就把当前代次封口冻结完成后下一段才能启用新代次。下面是业务队列的核心模型刻意不依赖任何具体的 Native SDK 名称// utils/FrameBatchGate.ets应用层批次闸门exportinterfaceCaptureFrame{frameId:string;timestampNs:number;epoch:number;poseValid:boolean;}exportclassFrameBatchGate{privateepoch:number12;privatefrozen:booleanfalse;privatepending:CaptureFrame[][];privatediscardedLate:number0;getcurrentEpoch():number{returnthis.epoch;}getlateDiscardCount():number{returnthis.discardedLate;}accept(frame:CaptureFrame):boolean{if(this.frozen||frame.epoch!this.epoch){this.discardedLate;returnfalse;}this.pending.push(frame);returntrue;}freezeAndDrain():CaptureFrame[]{this.frozentrue;constbatchthis.pending.slice();this.pending[];returnbatch;}nextEpoch():void{if(!this.frozen){thrownewError(epoch is not frozen);}this.epoch;this.frozenfalse;}}这里freezeAndDrain()的意义不是“清空数组”而是拿出一份不会继续增长的提交快照。accept()一旦发现代次不匹配宁可计入丢弃统计也不能追加到新代次。需要强调epoch属于应用层帧批次协议不是 Spatial Recon Kit 提供的固定字段真正喂给 Native 时仍要按官方帧结构转换。图中红色标注的“丢弃旧代次迟到 MOVE1”要额外说明。MOVE在本 Demo 中是采集引导控件的触摸移动事件它会改变引导框位置、辅助采样窗口它不是空间重建原始帧类型。我们把这一次晚到的手势消息单独计数不能与 8 个被丢弃的采集帧混为一谈。换句话说Dropped Frames 8是采集统计late MOVE 1是 UI 交互防抖和代次校验统计。项目里另有一个不能省略的规则同一批次必须按稳定batchId做幂等处理。队列清空并不保证 Native 层已经确认接收如果提交期间应用异常退出恢复时可能重复发送。业务层要保存批次是否“待提交、已提交、已确认”重复batchId不应让关键帧计数加两次。四、第二段代码冻结数据而不是冻结整个页面我把冻结操作做成显式事务先挡住新帧再拿到当前批次再等待 Native 适配层确认最后写检查点。原型中最危险的实现是先更新 UI再等待 Native 返回这会制造短时间的“成功假象”。新版把状态变化放到确认之后让界面永远显示最后一次已确认的状态。// services/ReconstructionTaskService.ets关键业务流程适配层接口为应用自定义exportinterfaceReconSnapshot{taskId:string;epoch:number;stage:string;progress:number;keyframes:number;validPoses:number;droppedFrames:number;}exportclassReconstructionTaskService{constructor(privategate:FrameBatchGate,privateadapter:SpatialReconAdapter,privatejournal:TaskJournal){}asyncfreezeCaptureEpoch(taskId:string):PromiseReconSnapshot{constepochthis.gate.currentEpoch;constframesthis.gate.freezeAndDrain();constbatchId${taskId}_epoch${epoch};awaitthis.journal.markPending(taskId,batchId,frames.length);awaitthis.adapter.submitFrameBatch(taskId,batchId,frames);constacceptedawaitthis.adapter.confirmBatch(taskId,batchId);awaitthis.journal.markCommitted(taskId,batchId,accepted);constsnapshot:ReconSnapshot{taskId,epoch,stage:CAPTURING,progress:72,keyframes:320,validPoses:312,droppedFrames:8};awaitthis.journal.saveSnapshot(snapshot);this.gate.nextEpoch();returnsnapshot;}}上面的TaskJournal、SpatialReconAdapter和confirmBatch()都是业务侧抽象接口320 / 312 / 8 / 72是本演示固定复现断面的快照值真实实现应来自已确认结果而不能写死。这里用它们是为了和截图、日志、页面一一对应方便读者对照协议。confirmBatch()的具体“确认”语义需要 Native 适配层明确是成功入队、被引擎接收还是已经完成处理三种语义不能混写。这段逻辑还留下一条异常边界。如果submitFrameBatch()失败闸门此时仍是frozen不能盲目执行nextEpoch()。用户可以选择重试当前batchId或者把这一批记为失败并显式丢弃。异常分支可以在外层提供“重试 / 结束任务”的选择避免永远锁死页面同时要给开发日志输出错误码与失败阶段。TaskJournal建议采用应用沙箱内可靠的持久化机制并为快照加版本号与校验摘要。图片和大二进制帧数据不适合塞进 Preferences业务元数据、状态日志和索引可以落在关系型存储原始帧根据体量走文件。真实工程的重点不是选哪个存储 API而是什么时候承认一个检查点可以用于恢复。五、DevEco Studio 里对照三种证据图 02 展示了CaptureSessionPage.ets、ReconstructionTaskService.ets、FrameBatchGate.ets的工程协作视图。左侧工程树便于追踪代码归属中间定位到freezeCaptureEpoch()右侧模拟器展示RUNNING / CAPTURING / 72%底部 HiLog 留下同一个taskId的提交记录。它是为讲解生成的 IDE 风格示意截图不是从真实 DevEco Studio 导出的构建证明。我通常会给日志建立一条固定检索模式taskId必有epoch必有batchId能追溯错误要带stage。只打印“处理成功”这样的句子排查时几乎帮不上忙。例如本次应当能看到类似日志10-09 10:24:13.456 [ReconFlowLab] recon_20261009_01 stageCAPTURING progress72 keyframes320 validPoses312 dropped8 10-09 10:24:13.789 [ReconFlowLab] freezeCaptureEpoch epoch12 committed 10-09 10:24:14.012 [FrameBatchGate] submitFrameBatch idb_epoch12_... size15 10-09 10:24:14.356 [ReconFlowLab] handleLateMoveDiscard dropped1这里的顺序更应该看成事件来源的综合日志而不是宣称每行按显示顺序构成一个原子事务。不同线程记录、异步缓冲和日志刷新都可能让观测顺序与真实发生顺序不完全一致。判断提交顺序以持久化事务号和确认记录为准不以控制台肉眼排序为准。截图中size15是当次提交子批次大小并不等于整个任务的关键帧总数 320。为了避免两个页面都能直接改变任务状态页面只订阅一个TaskSnapshot。如果 Native 回调在页面销毁后抵达服务层更新快照即可不再访问销毁组件的控制器。这样从采集页切到详情页时任务仍是同一个任务UI 只是切换观察视角。六、运行页的 320、312、8 到底在说什么图 03 是CaptureSessionPage的设计复现recon_20261009_01处于RUNNING采集子阶段是CAPTURING进度条72%。真正要盯住的是三个数字之间的关系320 312 8它说明当前演示统计口径下“已纳入统计的关键帧”被分成有效位姿与丢弃两组。真实场景可能还存在待验证或重复帧因此不能把这个等式当成 SDK 的永恒规则我们特意让本例没有“待验证”组便于检查快照的封闭性。采集页面的High只能说明业务规则判断当前数据覆盖还不错。比如一个会议室有大片白墙、重复纹理和强反光关键帧数很高也可能定位不稳用户移动得太快内参和外参虽然都存在数据仍可能不够可靠。因此我更愿意展示“丢帧原因、视角覆盖、连续位姿间隔”而不是只展示一个漂亮的质量评级。另外屏幕显示的125K 点云预览点数不能等价为最终模型中的高斯点数量。预览可能经过采样、抽稀、降精度甚至来自单独的可视化缓冲。312 MB也只是这个演示页面给出的预览监控读数不是 Spatial Recon Kit 对所有设备的内存承诺。把数据口径写到 UI 说明或者调试页面是技术产品降低沟通成本的重要一步。暂停按钮也得定义清楚点击“暂停采集”是停止接纳新采集帧还是要求 Native 停止计算本例采用前者并把已提交的计算继续留在服务层“结束任务”才进入终止 / 输出流程。没有明确产品定义的话很容易在电量、发热、后台运行以及用户心理预期上产生冲突。七、第三段代码恢复必须先验证检查点真正需要恢复时不能只看磁盘上有没有名为frame_320的文件。检查点至少要带任务 ID、序列号、数据版本、最后确认的批次、输出状态以及用来校验的摘要。哪怕文件存在只要它的版本与当前数据协议不兼容也应该退回到明确的失败分支而不是继续拼接。// 服务层恢复流程业务协议示意并非系统 SDK 接口exportinterfaceReconCheckpoint{taskId:string;schema:number;lastCommittedEpoch:number;frameCursor:string;checksum:string;nativeStateReusable:boolean;}exportasyncfunctionrecoverReconTask(taskId:string,journal:TaskJournal,adapter:SpatialReconAdapter):Promisestring{constcp:ReconCheckpoint|undefinedawaitjournal.loadCheckpoint(taskId);if(!cp||cp.schema!2||!cp.checksum){returnRECOVERY_NEEDS_RECAPTURE;}constvalid:booleanawaitjournal.verifyCheckpoint(cp);if(!valid){returnRECOVERY_CHECKSUM_FAILED;}if(!cp.nativeStateReusable){awaitjournal.markNeedRebuild(taskId,cp.frameCursor);returnRECOVER_FROM_COMMITTED_INPUT;}awaitadapter.restoreApprovedCheckpoint(taskId,cp);awaitjournal.markRecoveryStarted(taskId);returnRECOVERY_RESTARTED;}这段代码故意把nativeStateReusable放到了检查点协议里因为恢复输入帧和恢复算法内部优化器状态是两回事。只有对应设备和 SDK 明确支持、并且我们确实保存过可还原的 Native 状态才能调用适配层的restoreApprovedCheckpoint()。否则只能从已经确认、仍可读取的输入帧重建甚至要求重新采集缺失视角。不能把业务数据库里的RUNNING改回去就宣称 3DGS 引擎已原地恢复。图 04 的pose_gap_017 recovered也是业务演示中“位姿缺口恢复过程”完成的状态标记不是官方固定回调名。恢复“17 帧位姿”意在解释一次补算事件不能证明所有输入帧和最终模型都被无损找回。对产品最重要的是可解释的降级哪一部分数据仍有效、哪一段需要补扫、是否影响已保存的模型、用户下一步该做什么。八、完成态详情页不要把进度、模型、文件混为一谈ReconTaskDetailPage把任务投影到了RECONSTRUCTING这时页面看到86%、最后检查点frame_320、丢弃帧8、恢复队列1。这个 86% 是演示应用根据阶段和子任务权重计算的综合进度和前面的采集 72% 不冲突72% 是采集阶段的覆盖目标86% 是整体任务在恢复并进入重建后的综合估算。截图里的128,640 个三角形尤其容易产生概念误导。3DGS 的核心表示是带有位置、颜色、尺度、旋转和透明度等属性的高斯点不等同于三角网格。我们这里把128,640明确标成可选网格代理 / 调试映射统计用于检查落点映射和稀疏 / 稠密数据融合的中间结果而不是说 Spatial Recon Kit 的 3DGS 输出天然就是这个三角形网格。工程落地时如果没有网格代理模块这个指标完全可以不显示。输出目标meetingroom_a.gs同样是 Demo 自定义占位命名不代表官方规定的文件扩展名。Native 实际导出格式、模型描述文件及可加载路径以所用 SDK 的输出格式和文档为准。要做到真正的SAVED必须经历格式确认、文件写入完成、数据校验、元数据记录以及必要的模型加载验证。提前给一个文件名只能说明“有输出意图”。我把验收分成四条而不是看一次“绿色成功”第一条是任意切页与前后台切换都不会重复提交同一个批次第二条是epoch12冻结以后旧消息只能被拒绝不能污染epoch13第三条是恢复路径严格区分重放输入与恢复 Native 内部状态第四条是只有磁盘结果被确认后才进入可分享的完成态。这几条通过后页面的“结束任务”按钮才有可信的业务含义。1. 设备能力检查应该比开始按钮更早如果把这个 Demo 放到真机第一道关卡其实是能力检查不是点击“开始采集”。不同设备的系统版本、相机特性、内存条件和对应 Spatial Recon Kit 支持范围可能不同。进入页面时先确认可用能力再决定是否开放采集入口检测失败时显示“当前设备暂不支持该能力”及下一步指引而不是让用户扫了半个房间以后才发现无法重建。这里也要区分“设备具备空间重建能力”和“该应用已完成所需资源、权限与 Native 模块初始化”后者失败应当能够单独诊断。另一个实际风险是长时间采集时的热状态与后台切换。页面的onPageHide、应用进入后台和 Native 会话停止三者未必在同一个时刻发生。应用可以先拒绝新帧、提交仍在队列里的批次并在用户回到前台以后重新评估设备能力和任务状态但不能借业务抽象绕开系统对后台摄像头、后台计算和资源占用的限制。演示能在前台顺利工作只说明前台路径可行不能证明锁屏后也能继续采集。2. 从日志定位“丢失一帧”之前先统一时间基准最后补一条容易造成误判的小细节空间帧的时间戳、UI 触摸时间、持久化写入时间和 HiLog 输出时间可能来自不同时间基准。以10:24:13排序日志只能辅助阅读不能用于证明两帧的物理先后。如果timestampNs代表采集端的单调时间它应保留到 Native 数据帧业务日志再补充自己的接收时间与提交序号。只有这些关联信息齐全开发者才能区分“相机确实漏采”“数据送到队列但未提交”和“日志缓冲晚刷新”三种完全不同的故障。九、收尾时真正要留下的工程规则这次最有价值的变化不是把一张重建界面做得更像专业扫描软件而是给原本混在一起的事件定了边界。用户看见的采集进度、Native 真正接纳的帧批次、持久化的检查点、模型导出状态各有自己的时间线。页面可以切换服务可以回调算法可以失败但已经确认的历史不能被迟到消息改写。当前实现仍有很多值得继续扩展的地方帧队列背压、单帧时间戳异常、相机内参变化、设备能力差异、散热与资源管理、后台执行限制、Native 会话释放等都不应靠一个万能状态字段掩盖。尤其是真机接入阶段要依据官方示例确认 C API 的生命周期、支持设备、回调约束以及实际输出格式再完善桥接层。最终留下的原则很简单先让采集数据有归属再让状态变化有确认最后才谈失败后的恢复。端侧 3DGS 的复杂性确实在算法但工程体验能不能稳定往往取决于这些算法外围看似普通的协议。参考资料华为开发者Spatial Recon Kit 重建三维场景C/C华为开发者Spatial Recon Kit 术语华为开发者Spatial Recon 数据帧结构图示说明封面、IDE 和手机运行图均为围绕 ReconFlowLab 统一生成的技术场景示意不能替代 DevEco Studio 工程截图、编译记录或真实设备测试报告。业务封装方法及数值用来说明设计协议不代表官方 SDK 原生 API 或性能承诺。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑