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
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐