HarmonyOS 7 SceneForge 3DGS端侧重建实录 01:SpatialRecon × DataFrame:1080×1440帧规格归一化、会话创建与首批推帧【鸿蒙心迹】
PixelForge 系列收口以后,这次我把主线切到一个完全不同的方向:HarmonyOS 7 的 3DGS 端侧重建。
新 Demo 叫 SceneForge。
它的目标很具体:用户围着桌上的茶具走一圈,App 把多视角图像、相机内参和位姿送进 Spatial Recon Kit,在端侧生成 3DGS 场景,后面再进入结果保存和渲染。
这个系列固定 6 篇,不中途改 X:
01 1080×1440 帧规格归一化、会话创建与首批推帧
02 模糊帧、低基线、重复视角与位姿跳变过滤
03 Session 生命周期、前后台模式与重建进度
04 ModelWriteInfo、PLY/MP4 结果保存与文件状态
05 spatialRender × GSPlugin:模型加载、相机与渲染资源
06 采集覆盖率、重建耗时、模型结果与资源回归验收
第一篇先不谈“模型最终有多好看”,只把一件更基础的事情做稳:
所有输入帧必须在进入 Spatial Recon Kit 前,满足明确的规格、内参和时间序列约束。
当前官方文档对自定义 HMS_SpatialRecon_DataFrame 的要求非常直接:一帧数据不仅包含图像,还包含焦距、主点、畸变参数、位置、四元数和时间戳;当前重建输入的图像格式只支持 RGB,重建指南还明确要求输入图像尺寸为 1080×1440。另外,调用 StartSession 之后就不能再继续 PushFrame,所以“采集 / 推帧”和“开始重建”必须严格分成两个阶段。
本轮统一数据:
taskId:
recon_20261003_01
sessionId:
sf_session_20261003_01
scene:
tabletop_tea_set_01
rawFrameSize:
1440 × 1920
normalizedFrameSize:
1080 × 1440
scale:
0.75
sourceFrames:
48
normalizedFrames:
48
pushedFrames:
48 / 48
pushFailed:
0
frameFormat:
RGB
rawIntrinsics:
fx=1228.8
fy=1231.2
cx=720
cy=960
normalizedIntrinsics:
fx=921.6
fy=923.4
cx=540
cy=720
timestampMonotonic:
true
supportCheck:
SUCCESS
prepareCost:
612ms
progressAfterStart:
4.8%
status:
RECON_RUNNING

一、Spatial Recon Kit 真正吃进去的不是“照片”,而是一帧完整的相机数据
如果只看产品体验,很容易把 3DGS 端侧重建理解成:
拍 48 张照片
→ 调一个建模 API
真正写到 C/C++ 层就不是这样。
HMS_SpatialRecon_DataFrame 至少包含:
focalX / focalY
principalX / principalY
distortionCoef[8]
imageWidth / imageHeight
position[3]
rotation[4]
timestamp
imageData
format
也就是说,每一帧真正描述的是:
这张图是什么、相机当时在哪、镜头参数是什么、这一帧发生在什么时候。
这也是为什么 SceneForge 第一篇不从“渲染模型”开始,而是从 DataFrame 开始。
二、原始采集 1440×1920,不能直接推给 Spatial Recon
当前测试源帧:
1440 × 1920
官方当前重建指南要求输入:
1080 × 1440
好在两者宽高比相同,都是 3:4。
所以本轮不需要裁剪,只做统一缩放:
scale = 1080 / 1440
= 0.75
问题来了:
图像缩小以后,相机内参也必须同步缩放。
如果只改 imageWidth / imageHeight,却还保留原来的 fx / fy / cx / cy,DataFrame 内部就会自相矛盾。
三、分辨率缩放后,焦距和主点也要同步 ×0.75
原始内参:
fx=1228.8
fy=1231.2
cx=720
cy=960
缩放后:
fx=921.6
fy=923.4
cx=540
cy=720
这段代码解决的就是“图像规格变了以后,如何保持相机模型一致”。
struct CameraIntrinsics {
float fx;
float fy;
float cx;
float cy;
};
CameraIntrinsics NormalizeIntrinsics(
const CameraIntrinsics& raw,
float scale)
{
return CameraIntrinsics {
.fx = raw.fx * scale,
.fy = raw.fy * scale,
.cx = raw.cx * scale,
.cy = raw.cy * scale
};
}
这里的畸变参数没有因为统一缩放而重新定义。
SceneForge 仍然沿用采集链给出的同一组畸变模型。
如果未来不是等比例缩放,而是裁剪、旋转或做了复杂图像变换,内参转换就不能继续只乘一个比例。
四、DataFrame 组装一定要把“图像 / 内参 / 位姿 / 时间戳”绑定到同一帧
下一步是构造 HMS_SpatialRecon_DataFrame。
HMS_SpatialRecon_DataFrame BuildFrame(
const NormalizedFrame& item)
{
HMS_SpatialRecon_DataFrame frame {};
frame.focalX = item.intrinsics.fx;
frame.focalY = item.intrinsics.fy;
frame.principalX = item.intrinsics.cx;
frame.principalY = item.intrinsics.cy;
for (int i = 0; i < 8; i++) {
frame.distortionCoef[i] =
item.distortion[i];
}
frame.imageWidth = 1080;
frame.imageHeight = 1440;
frame.position[0] = item.position[0];
frame.position[1] = item.position[1];
frame.position[2] = item.position[2];
frame.rotation[0] = item.rotation[0];
frame.rotation[1] = item.rotation[1];
frame.rotation[2] = item.rotation[2];
frame.rotation[3] = item.rotation[3];
frame.timestamp = item.timestampNs;
frame.imageData = item.rgb.data();
frame.format =
SPATIAL_RECON_IMAGEDATA_FORMAT_RGB;
return frame;
}
这段代码看起来只是“填结构体”,真正要防的是错帧。
最危险的一种 Bug 是:
imageData 来自第 17 帧
pose 却来自第 18 帧
代码不会一定立刻崩溃,但重建输入已经失真。
所以 SceneForge 在 C++ 侧用同一个 NormalizedFrame 统一承载:
图像
内参
位姿
时间戳
不允许页面层分别传四组数组再临时拼。
五、时间戳必须保持单调,至少不能出现明显倒退
本轮:
timestampMonotonic=true
SceneForge 会在推帧前做一次序列验证:
SceneForge 的实现里会顺序遍历 48 帧,只要发现后一帧 timestampNs 小于等于前一帧,就直接把整组输入标成 TIMESTAMP_NOT_MONOTONIC,不再进入 Session。这里不需要复杂算法,关键是把“时间序列是否合法”作为启动前的硬条件,而不是等重建异常以后再猜。
当前 48 帧全部通过。
这一条虽然不是“画质算法”,但对多视角采集非常重要。
如果时间戳和位姿序列出现逆序,后面很难判断是重建算法问题,还是输入序列本身就错了。
六、创建 Session 前先做设备能力检查
Spatial Recon Kit 官方 C API 提供:
HMS_SpatialRecon_IsSupport(
SPATIAL_RECON_MODEL_TYPE_GS
)
当前模型类型只支持 3DGS。
SceneForge 的会话入口先做:
HMS_SpatialReconStatus CreateGsSession(
const char* workPath,
HMS_SpatialRecon_Session** session)
{
auto support =
HMS_SpatialRecon_IsSupport(
SPATIAL_RECON_MODEL_TYPE_GS);
if (support !=
SPATIAL_RECON_STATUS_SUCCESS) {
return support;
}
return HMS_SpatialRecon_CreateSession(
SPATIAL_RECON_MODEL_TYPE_GS,
workPath,
session);
}
本轮:
supportCheck=SUCCESS
只有支持检查通过以后才创建 Session。
实际真机能力可能受设备硬件、系统版本和环境影响,所以不能在 UI 里把“有这个 API”直接等价成“当前设备一定能重建”。
七、WorkPath 是 Session 生命周期的一部分
HMS_SpatialRecon_CreateSession 需要传:
workPath
官方说明要求它是已经存在的可用目录,用于重建过程中的数据和临时文件。
SceneForge 当前每个任务单独目录:
/data/storage/el2/base/files/
sceneforge/recon_20261003_01/
不会把不同 Session 的中间数据全部扔进同一个公共目录。
这样后面 04 做结果保存和失败清理时,可以直接按 taskId 收口。
八、48 帧必须全部 Push 完,再调用 StartSession
这个顺序是第一篇最重要的 API 边界。
官方接口说明明确指出:
PushFrame
属于数据输入阶段;
StartSession
进入重建阶段;
StartSession 调用以后
不能再继续成功 PushFrame。
所以 SceneForge 的会话骨架固定:
bool FeedAndStart(
HMS_SpatialRecon_Session* session,
std::vector<NormalizedFrame>& frames)
{
for (auto& item : frames) {
auto frame =
BuildFrame(item);
auto status =
HMS_SpatialRecon_PushFrame(
session,
&frame);
if (status !=
SPATIAL_RECON_STATUS_SUCCESS) {
return false;
}
}
auto start =
HMS_SpatialRecon_StartSession(
session,
nullptr,
OnReconFinished);
return start ==
SPATIAL_RECON_STATUS_SUCCESS;
}
本轮:
48 / 48
push success
pushFailed=0
然后才进入:
StartSession
九、StartSession 是异步动作,页面状态不能直接写“完成”
官方 HMS_SpatialRecon_StartSession 是异步启动。
所以函数返回成功只代表:
重建任务成功开始
不是:
3DGS 模型已经完成
SceneForge 项目状态明确区分:
FRAMESET_READY
RECON_RUNNING
RECON_FINISHED
SAVE_RUNNING
RESULT_READY
01 最终停在:
RECON_RUNNING
十、4.8% 来自 GetProgress,不是自己估出来的
任务启动后,C++ 层调用:
Native 层定时调用 HMS_SpatialRecon_GetProgress(session, &progress, &stage) 读取真实进度与阶段,再通过桥接层把轻量 Snapshot 推给 ArkTS。页面只显示结果,不自己“估算”百分比。
本轮 UI 第一次稳定采样:
progressAfterStart=4.8%
这个数字只是当前 Demo 的一次运行数据。
真正产品不能写死“开始 600ms 后应该有 5%”。
十一、ArkTS 只订阅状态,不直接拥有 Native Session
SceneForge 的 ArkTS 页面不持有:
HMS_SpatialRecon_Session*
Native Session 由 C++ Manager 管理。
ArkTS 只拿轻量快照:
export interface ReconSnapshot {
taskId: string
sessionId: string
sourceFrames: number
pushedFrames: number
progress: number
status: string
}
这条边界后面会非常重要。
03 做 Pause / Resume 和前后台运行模式时,Session 仍然只属于 Native Owner。
十二、DevEco 图里为什么保留“模拟器只预览 UI”的提示
开发图:

右侧是 SceneForge 的诊断页面,用来检查:
taskId
内参归一化
帧数
进度
状态
但当前 Spatial Recon Kit 的真实重建能力需要在支持设备上验证,不能把模拟器 UI 预览当成真实 3DGS 重建执行结果。
所以诊断页底部明确提示:
模拟器仅预览诊断 UI;
真实 Spatial Recon 重建需支持设备。
这样配图和真实能力边界不冲突。
十三、运行图只证明“帧集已经正确进入重建阶段”
最终运行图:

统一数据:
1440×1920
→
1080×1440
fx:
1228.8 → 921.6
fy:
1231.2 → 923.4
cx:
720 → 540
cy:
960 → 720
48 / 48
PushFrame success
progress:
4.8%
status:
RECON_RUNNING
这一篇没有展示最终模型。
这是刻意的。
因为第一篇真正应该验收的是:
数据帧输入协议正确,Session 正确创建,48 帧全部推入,重建成功开始。
十四、第一篇最后固定六组反向测试
第一组,1080×1440 RGB 正常通过。
第二组,输入尺寸不是 1080×1440,先归一化,不直接 Push。
第三组,内参未同步缩放,预检直接失败。
第四组,timestamp 倒退,帧集拒绝启动。
第五组,设备 IsSupport 失败,不创建 Session。
第六组,StartSession 调用以后再尝试 PushFrame,项目层直接禁止这种状态转换。
这些都能被明确识别以后,01 才算稳定。
十五、下一篇真正的问题不是“多推几帧”,而是“该不该推”
48 帧全部成功并不意味着:
帧越多越好。
真实采集会出现:
用户停着不动;
连续拍到几乎一样的视角;
手抖导致模糊;
位姿突然跳变;
环绕轨迹里局部帧密度过高。
02 会继续同一个 SceneForge,把 72 帧原始采集先过一层 Frame Gate。
目标数据:
72 captured
54 accepted
18 rejected
真正开始处理“输入质量”而不是“接口是否能调用”。
十六、DataFrame 和 ARFrame 两条输入链不要在同一次任务里随意混用
Spatial Recon Kit 支持两种数据输入方式:
HMS_SpatialRecon_PushFrame
自定义 DataFrame
HMS_SpatialRecon_PushARFrame
AR Engine 帧
SceneForge 01 选择的是前者,因为我想把:
图像规格
相机内参
位姿
时间戳
每一项都显式记录下来,便于后续做 Frame Gate 和回归测试。
这并不代表 PushARFrame 不适合产品。
相反,如果业务本身已经在用 AR Engine 做实时跟踪,直接推 ARFrame 可以减少一层手动数据组装。
但同一个 Demo 里最好先固定一种主输入链。
如果一会儿 DataFrame、一会儿 ARFrame,后面出了位姿错位,很难判断问题来自:
自定义相机模型
还是 AR 跟踪数据。
所以 SceneForge 把输入方式也记进任务快照:
inputMode=
CUSTOM_DATAFRAME
后面真要切 ARFrame,会单独建一组基线。
十七、RGB Buffer 的生命周期必须覆盖 PushFrame 调用
HMS_SpatialRecon_DataFrame.imageData 是一个原始指针。
SceneForge 当前做法是:
NormalizedFrame 持有 RGB Buffer
BuildFrame 只借用指针
HMS_SpatialRecon_PushFrame 返回以后
当前 Frame 才允许释放 Buffer
不能这样写:
临时 vector 创建 frame.imageData
→ vector 作用域结束
→ 再 PushFrame
那样 imageData 已经变成悬空指针。
第一篇专门把 Buffer Owner 固定下来,就是为了后面批量采集时不把“偶发 INVALID_FRAME_DATA”误判成系统问题。
十八、Session 状态必须阻止错误 API 顺序
SceneForge 给 Native Session 自己做了状态机:
CREATED
FEEDING
DATA_READY
RECON_RUNNING
RECON_FINISHED
SAVING
DESTROYED
允许的关键转换只有:
CREATED → FEEDING
FEEDING → DATA_READY
DATA_READY → RECON_RUNNING
如果当前已经:
RECON_RUNNING
项目层根本不会再调用 HMS_SpatialRecon_PushFrame。
不是等官方 API 返回错误以后再处理,而是在业务边界先挡住。
这种状态机到 03 会继续扩展:
PAUSED
BACKGROUND
但第一篇先把“采集阶段”和“重建阶段”彻底分开。
十九、CreateSession 成功也不代表 WorkPath 永远安全
官方要求 workPath 必须是已经存在的目录。
SceneForge 在创建 Session 前额外确认:
目录存在
当前应用可写
taskId 对应目录为空或可恢复
剩余空间满足项目阈值
如果上一次异常退出留下旧中间文件,不直接覆盖。
当前策略:
同 taskId
→ 走恢复检查
新 taskId
→ 创建新目录
后面 04 保存模型时,结果文件和临时重建数据也会继续沿用这套目录隔离。
二十、第一篇的 612ms 不是重建耗时
prepareCost=612ms 只包含:
48 帧规格归一化
内参同步
DataFrame 组装
能力检查
Session 创建
PushFrame
真正 3DGS 重建是在 StartSession 之后异步进行的。
所以:
612ms
不能写成:
48 帧重建完成只要 612ms
这类口径非常容易在图表里被误读。
SceneForge 从第一篇开始就把:
prepare
reconstruct
save
render
四段时间拆开。
二十一、模拟器页面与真机重建日志也要分来源
因为真实 Spatial Recon 需要支持设备,SceneForge 的开发环境会同时存在:
模拟器 UI 预览日志
真机 Native 重建日志
两者都叫 SceneForge 会很混乱。
所以日志 Tag 分开:
SceneForge/UI
SceneForge/SpatialRecon
SceneForge/FrameGate
真正发布问题排查时,一眼能知道:
这是 UI 假数据
还是 Native 真机结果。
用户看到的 02 图是 DevEco 诊断布局;真实重建数据仍以真机运行日志为准。
二十二、为什么第一篇没有直接保存 PLY
从产品角度当然希望:
拍完
→ 直接看到模型。
但工程上如果 01 同时做:
DataFrame
Session
Progress
SaveResult
Render
任何一步失败都很难定位。
所以本篇故意停在:
RECON_RUNNING
证明输入和会话已经正确。
04 再专门处理:
ModelWriteInfo
PLY / MP4
保存阶段
文件结果
这种拆法虽然慢一点,但连载里的每一篇都能回答一个明确工程问题。
二十三、输入归一化结果最好生成一份 FrameSet Manifest
48 帧全部 Push 成功以后,SceneForge 会额外保存一份轻量 Manifest,记录:
taskId
sessionId
inputMode
frameCount
frameSpec
intrinsicsScale
firstTimestamp
lastTimestamp
workPath
它不保存 RGB 像素,只保存这组输入“为什么可以被重建”的证据。后续如果 03 的进度异常,或者 04 保存模型失败,可以先回看 Manifest,确认真正进入 Session 的帧集是不是当初那一组,而不是重新翻相册猜输入。
二十四、01 最终固定的是“重建启动协议”
SceneForge 到这一篇真正形成的不是一个页面,而是一套协议:
设备支持检查
→ 帧规格归一化
→ 内参与位姿绑定
→ 时间戳校验
→ 创建 Session
→ 全量 PushFrame
→ StartSession
→ GetProgress
以后采集来源换成相机、AR Engine 或导入数据,只要最终进入这套协议,下游的 Session 生命周期、保存与渲染就可以继续复用。第一篇先把这条边界定死,后面连载才不会每一期重新解释“怎样才算一组合法输入”。
参考资料
- Spatial Recon Kit 空间计算能力入口:
https://developer.huawei.com/consumer/cn/features/spatialization - Spatial Recon Kit 术语:
https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/spatial-recon-glossary - spatial_recon_interface.h:
https://developer.huawei.com/consumer/cn/doc/harmonyos-references/capi-spatial-recon-interface-h - 重建三维场景(C/C++):
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline
更多推荐



所有评论(0)