HarmonyOS 7 + Spatial Recon Kit-C++:3DGS 重建结果的重力对齐与尺度门禁【鸿蒙心迹】
3DGS 重建完成并不等于产物可以直接进入业务场景。一个模型在自由轨道查看器里看起来完全正常,放到带地面、碰撞或尺寸标注的页面里,却可能倾斜二十度、悬在空中,或者把 120 mm 的标尺还原成 109 mm。画面仍然“像”,空间语义却已经错了。
本文用 AxisCal 演示如何在 Spatial Recon 结果之后增加一个应用层验收阶段。任务 GS-ALIGN-0067 的场景名为 desk_v4,状态依次为 RESULT_READY → AXIS_ESTIMATED → SCALE_LOCKED → TRANSFORMED → VERIFIED。样本地面法向量为 (0.08, 0.94, 0.33),估算倾角 20.1°;参考物实测 120 mm,模型内长度 109 mm,尺度因子为 1.101。所有数值用于解释算法和界面口径,不冒充真机跑分。

一、先看“看起来没错”的失败产物
自由相机查看器会主动绕模型旋转,人眼很容易把倾斜解释成观察角度。没有地平线、重力方向和已知长度时,重建坐标系绕任意轴旋转、整体平移或带一个全局尺度,都可能保持相对外观。问题直到模型被放进量尺、AR 摆放、空间导航或多批次拼接页面才暴露。
AxisCal 的输入不是采集帧,也不参与 3DGS 优化。它只消费重建管线已经给出的结果清单、可抽样的点/高斯中心以及应用侧采集的两个锚点:一组地面候选点和一段 120 mm 参考标记。这样可以把“重建算法是否完成”与“产物是否满足业务坐标契约”分开。
这里的边界必须说清。HarmonyOS Spatial Recon 管线负责端侧空间重建能力;本文的地面拟合、旋转矩阵、尺度估计和验收门禁是应用层策略,不宣称是平台自动保证,也不虚构一个不存在的“自动重力对齐 API”。如果业务拿不到稳定平面或已知长度,就只能输出无尺度、无绝对朝向的产物,不能用猜测填满字段。
二、把坐标契约写成产物的一部分
一个可交付的 3DGS 产物至少要说明:右手还是左手坐标系,哪一个轴向上,长度单位是什么,变换矩阵作用于点还是相机,矩阵按行优先还是列优先。只保存一个 scene.ply 或私有模型文件,调用者只能靠试错判断这些语义。
AxisCal 为每次重建附加 frame_manifest.json。其中 sourceFrame 标识管线原坐标,targetFrame 固定为应用内部的 Y-up 米制坐标;generation 为 17;transform 记录从源坐标到目标坐标的四阶矩阵;scale 记录 1.101;evidence 保存参考物、样本数量和残差。渲染页只消费通过门禁的 generation,避免上一批矩阵与下一批模型交叉。
这段代码解决什么问题:为重建结果定义明确的坐标与尺度清单,让后续页面不再靠轴交换和经验常量修正模型。
interface Vec3 { x: number; y: number; z: number }
interface FrameManifest {
taskId: string
scene: string
generation: number
sourceFrame: 'RECON_LOCAL'
targetFrame: 'APP_Y_UP_METERS'
upBefore: Vec3
upAfter: Vec3
scale: number
referenceMm: number
reconstructedMm: number
p95HeightErrorMm: number
matrixColumnMajor: number[]
}
const manifest: FrameManifest = {
taskId: 'GS-ALIGN-0067', scene: 'desk_v4', generation: 17,
sourceFrame: 'RECON_LOCAL', targetFrame: 'APP_Y_UP_METERS',
upBefore: { x: 0.08, y: 0.94, z: 0.33 },
upAfter: { x: 0, y: 1, z: 0 },
scale: 1.101, referenceMm: 120, reconstructedMm: 109,
p95HeightErrorMm: 7.6,
matrixColumnMajor: new Array<number>(16).fill(0)
}
代码中把单位写进字段名,避免 120 究竟是毫米还是米。矩阵布局也不能只写 matrix,否则 ArkTS、C++ 和渲染引擎之间很容易发生转置错误。状态 RESULT_READY 只表示源产物可读;清单完整并不代表矩阵已经可信,仍需后续证据和数值校验。
三、重力方向不是取最大的坐标轴
样本法向量 (0.08, 0.94, 0.33) 的 Y 分量最大,但直接把 Y 当“已经向上”会保留明显倾斜。更稳妥的方法是从用户确认的地面区域或可靠平面候选中抽样,用 RANSAC 之类的鲁棒估计排除桌腿、墙面和离群点,再统一法向量朝向。
地面本身也有歧义。桌面与地板都可能是大平面,面积最大不一定是地面。AxisCal 要求候选平面同时满足三个条件:法向量与采集阶段重力先验接近、支撑点高度位于样本低分位、平面覆盖达到阈值。若只满足一个条件,页面停在 AXIS_UNCERTAIN,要求重新选区,而不是继续生成“漂亮但错误”的矩阵。
这段代码解决什么问题:使用鲁棒平面拟合得到单位上方向,并在证据不足时显式失败,而不是按最大分量猜坐标轴。
struct Vec3 { double x, y, z; };
struct PlaneEvidence {
Vec3 normal;
double inlierRatio;
double residualMm;
std::size_t inliers;
};
PlaneEvidence EstimateGround(
const std::vector<Vec3>& points,
const Vec3& gravityHint) {
PlaneEvidence best = RansacPlane(points, 240, 0.006);
Vec3 n = Normalize(best.normal);
Vec3 up = Normalize(gravityHint * -1.0);
if (Dot(n, up) < 0.0) n = n * -1.0;
if (best.inliers < 24 || best.inlierRatio < 0.62 ||
AngleDegrees(n, up) > 28.0) {
throw FrameError("AXIS_UNCERTAIN");
}
best.normal = n;
return best;
}
迭代次数 240、距离阈值 0.006 m、最少 24 个内点和 28° 夹角都是 Demo 策略参数,不是平台固定值。状态只有在证据通过后才进入 AXIS_ESTIMATED。实际项目要按采集距离、点密度和传感器噪声校准阈值;用同一组样本既拟合又评估会得到过度乐观的残差,应保留独立验证点。
四、从两个向量构造最短旋转
得到源上方向后,需要把它旋转到 (0, 1, 0)。常见写法是分别求 yaw、pitch、roll,但欧拉角顺序、奇异位置和左右手约定很容易带来第二个错误。AxisCal 使用“从向量 a 到向量 b 的最短旋转”,再转成四元数或矩阵。
两个边界需要单独处理:a 与 b 几乎相同时使用单位旋转;a 与 b 几乎相反时,叉积接近零,却需要 180° 旋转,此时要选择一个与 a 正交的稳定轴。忽略第二个边界会让倒置模型产生 NaN。
这段代码解决什么问题:稳定地把估算上方向旋转到应用 Y-up 坐标,避免欧拉角顺序和反向向量奇异点。
Quat RotationBetween(Vec3 from, Vec3 to) {
Vec3 a = Normalize(from);
Vec3 b = Normalize(to);
double d = Clamp(Dot(a, b), -1.0, 1.0);
if (d > 0.999999) return Quat::Identity();
if (d < -0.999999) {
Vec3 basis = std::abs(a.x) < 0.8 ? Vec3{1, 0, 0}
: Vec3{0, 0, 1};
return Quat::FromAxisAngle(Normalize(Cross(a, basis)), M_PI);
}
Vec3 axis = Cross(a, b);
Quat q{1.0 + d, axis.x, axis.y, axis.z};
return Normalize(q);
}
这一步只修正朝向,不应该偷偷加入尺度和平移。分离计算让诊断页可以分别显示“倾角 20.1°”“尺度 1.101”和“地面偏移”,出现问题时能定位是哪一层。实现时还要确认 Quat 构造顺序是 w,x,y,z 还是 x,y,z,w,这是跨库接入最常见的静默错误之一。
五、尺度必须来自可追溯的参考物
纯视觉重建往往保留相对几何,却不天然承诺现实世界绝对尺度。AxisCal 使用采集画面中的标尺两端作为参考:现实距离 120 mm,源模型两点距离 109 mm,因此全局尺度 s = 120 / 109 = 1.100917...,界面显示为 1.101。
只有一条参考长度时,系统无法发现非均匀形变,所以它只允许计算统一尺度,不允许分别调整 X、Y、Z。若三个方向误差差异显著,应判定重建或锚点有问题,而不是用非均匀缩放把验证样本“拟合正确”。参考点也要远离反光、透明和边缘漂移区域。
这段代码解决什么问题:用已知实物长度计算单一全局尺度,并限制异常倍率,防止错误锚点把模型放大或缩小。
function lockUniformScale(
referenceMm: number,
reconstructedMm: number
): number {
if (referenceMm <= 0 || reconstructedMm <= 0) {
throw new Error('INVALID_SCALE_ANCHOR')
}
const scale = referenceMm / reconstructedMm
if (scale < 0.85 || scale > 1.20) {
throw new Error('SCALE_OUT_OF_RANGE')
}
return Math.round(scale * 1000) / 1000
}
const scale = lockUniformScale(120, 109) // 1.101
倍率范围 0.85~1.20 是当前 Demo 对小物体场景的保护线,并非通用标准。成功后状态进入 SCALE_LOCKED。不要在每次打开页面时重新测量和覆盖清单;尺度属于 generation 17 的产物元数据,应与模型一起提交。用户修改参考点时要创建新 generation,旧页面继续消费旧版本,直到新版本完整通过验收。

上图是 DevEco Studio 风格的演示配图,不是真实 IDE 截图。左侧目录区分管线适配、轴估计、尺度门禁和页面;中间代码显示 EstimateGround 与 lockUniformScale;右侧模拟器统一展示 GS-ALIGN-0067;底部 HiLog 记录 20.1°、1.101 和 generation 17,方便与正文逐项核对。
六、组合顺序决定模型是落地还是漂移
旋转、缩放和平移都正确,组合顺序仍可能错。AxisCal 的约定是列向量:先以源原点缩放,再旋转到 Y-up,最后沿 Y 轴平移,使地面验证点的中位高度为 0。矩阵写成 T × R × S。若渲染引擎使用行向量,代码和矩阵顺序必须一起调整,不能只把数组转置。
平移采用验证地面点的中位数,不用最小值。最小值很容易被一个离群高斯拉到负方向,导致主体悬空。对桌面模型而言,如果目标是把桌面而非房间地板放到零高度,清单还应明确 anchorSurface=desk,避免调用方误解。
生成矩阵后状态进入 TRANSFORMED,但仍不直接发布。应用先把 24 个未参与拟合的验证点变换到目标坐标,计算它们到 Y=0 平面的绝对高度误差。该样本 P95 为 7.6 mm,残余倾角 1.4°,满足 Demo 的 10 mm 和 2° 门禁。
七、运行页要把证据放在结果旁边
AxisCal 运行页显示场景 desk_v4、任务 GS-ALIGN-0067、generation 17、当前状态 SCALE_LOCKED,并把 20.1° 源倾角与 1.101 尺度因子并列。演示进度 74% 表示五阶段中的计算与校验进度,不代表重建算法训练进度。

页面上的红色箭头指向尺度因子,提醒开发者:模型“变正”只能证明旋转生效,不能证明尺寸正确。按钮“生成并校验变换”在平面证据与尺度锚点都通过后才可用。若 AXIS_UNCERTAIN 或 SCALE_OUT_OF_RANGE,页面保留源产物和证据,不生成一个默认矩阵继续运行。
设备状态栏固定为 08:26、5G、Wi-Fi、68% 电量,便于同一批图片核对。它是设计演示的一部分,不应被解读为采集发生在该时间或该设备上。
八、诊断页负责证明门禁,而不是装饰
诊断页把变换前后的法向量、参考长度、尺度、残余倾角、验证点数量和 P95 误差放在同一屏。状态链从 RESULT_READY 到 VERIFIED 完整显示,任一阶段失败都不会跳到绿色结论。

样本法向量从 (0.08, 0.94, 0.33) 变为 (0.00, 1.00, 0.00);参考长度 109 mm 经统一缩放对应 120 mm;包围盒为 0.82 × 0.61 × 0.47 m;24 个验证点的 P95 高度误差为 7.6 mm。红圈标在“P95 7.6 mm”和最终 VERIFIED 上,表达的是门禁证据,而非视觉效果。
诊断时不能只测变换后的拟合点。拟合点本来就被算法优化过,误差小是预期结果。保留独立验证集、多个参考长度和不同方向的线段,才能发现局部漂移。如果验证点不足,结果应为 EVIDENCE_INSUFFICIENT,而不是把 P95 留空仍标通过。
九、资源生命周期和多 generation 并发
坐标验收会读取较大的点集或高斯中心,不能把完整数据复制到 ArkTS 状态对象。推荐在 C++ 侧流式抽样和拟合,只把法向量、残差、计数与矩阵返回 UI。原生缓冲区、管线结果句柄和页面监听器要成对释放;页面离开时取消当前审计,晚到回调必须携带 generation。
假设 generation 17 正在计算矩阵,用户重新选择参考点并启动 generation 18。17 的回调即使成功,也不能覆盖 18 的页面状态或清单。提交前比较 result.generation === activeGeneration,不一致时只记录 STALE_RESULT_DROPPED 并释放对应资源。
模型文件与 frame_manifest.json 也要一起发布。可以先写入临时目录,校验摘要和矩阵字段,再原子切换可见清单。这里沿用的是通用产物提交原则,但本文门禁的重点是坐标证据;不要把“文件写成功”误当成“空间语义通过”。
十、何时应该拒绝自动修正
以下情况不适合自动对齐:场景没有可靠水平面;重力先验缺失且存在多个相似大平面;参考物被遮挡;两个尺度锚点几乎重合;尺度因子超过保护范围;变换后验证残差反而增大。正确行为是保留源产物、输出可诊断错误并请求补充证据。
即使门禁通过,1.4° 和 7.6 mm 也只说明在本组验证点上的结果满足 Demo 阈值。医疗测量、工业检测等高精度场景需要经过标定的传感器、可追溯量具和领域认证,不能直接采用本文方案。3DGS 擅长新视角渲染,不应被默认等同于高精度测量网格。
AxisCal 最终把一个模糊的“模型看着有点歪”拆成可审核事实:源法向量、重力先验、统一旋转、参考长度、尺度因子、矩阵顺序、独立验证误差和 generation 所有权。只有这些证据同时成立,状态才进入 VERIFIED。重建完成是算法事件,坐标可信才是工程交付事件。
1. 采集侧要为后处理留下证据
后处理无法凭空创造重力和尺度。采集阶段至少应保存帧时间、设备姿态可用性、参考物标记以及用户确认的平面区域。若隐私策略不允许长期保存原始图像,也要在采集会话结束前提取必要锚点,并把来源和置信度写入清单。只留下最终 3DGS 文件,之后再让算法猜哪一面是地面,通常会把不确定性藏进矩阵。
锚点不应绑定屏幕像素坐标。画面旋转、裁剪和分辨率变化后,像素位置会失去意义;应关联到采集帧 ID、相机参数和可重投影的特征。参考物若由用户手点两端,界面还应放大局部区域并给出最小像素距离,避免两个点太近导致尺度对一个像素的误差极其敏感。
2. 平面选择需要业务语义参与
办公室场景里,桌面、地板、墙面和柜门都可能产生高质量平面。纯几何算法可以找出它们,却不知道哪一个应该成为业务零平面。AxisCal 把算法候选与业务标签分开:算法返回面积、法向、残差和高度分位,用户或上层策略选择 floor、desk 或 wall。清单保存选择理由,后续查看器才能知道 Y=0 表示什么。
自动选择时,可以为候选打分,但不能只按面积排序。重力夹角、位置、覆盖率、纹理稳定性和跨帧一致性共同决定可信度。得分接近的两个候选应提示人工确认;“第一名”并不等于“足够确定”。把置信度阈值写入策略版本,也便于后续比较算法调整是否改变了历史产物。
3. 数值稳定性要在 C++ 边界处理
点坐标可能离原点很远,直接用单精度累计协方差会损失精度。拟合前可先减去样本质心,内部使用 double 计算,最终再根据渲染需求转换为 float。归一化向量前检查长度,叉积接近零时走显式分支;矩阵生成后验证所有元素为有限数,行列式和正交误差在允许范围内。
变换大量高斯中心时,不应为每个点在 ArkTS 与 C++ 之间来回复制。把矩阵一次传入原生处理层,使用连续缓冲区批量执行;UI 只接收包围盒、统计值和少量抽样点。若模型格式允许渲染时附加根变换,还可以先不重写全部数据,但导出给外部系统时必须明确矩阵是否已烘焙,不能让调用方重复应用。
4. 尺度验收需要第二把尺子
用 120 mm 标记计算尺度后,最好再用未参与计算的第二段已知长度验收。如果第二段 300 mm 在变换后成为 326 mm,虽然第一段完全正确,仍说明局部几何或锚点存在问题。可以报告相对误差、绝对误差和方向,避免只用平均值掩盖某一方向的偏差。
对于没有实体标尺的环境,可以使用已标定相机基线、可信深度或业务物体尺寸作为约束,但每种来源的误差模型不同。应用应在清单里标注 scaleSource,例如 physical_marker、calibrated_depth 或 unknown。unknown 不是失败,只表示产物只能用于视觉浏览,不能显示现实尺寸。
5. 回归测试要覆盖矩阵约定
最有效的自动测试不是对复杂模型截图,而是准备几个小型合成点集:水平平面、倾斜 20.1° 的平面、完全倒置平面、带离群点的平面和没有主平面的球面。每个样本都有已知旋转和尺度,断言变换后上方向、原点、包围盒与 P95 误差。这样能快速发现四元数分量顺序和矩阵转置问题。
跨语言测试则把同一矩阵分别交给 C++ 和 ArkTS 的参考实现,比较若干基准点。仅比较矩阵数组不够,因为两个错误约定可能恰好生成同一串数字,却在乘法一侧解释不同。测试报告应打印输入点、输出点和布局声明,出现差异时不需要靠肉眼旋转模型寻找原因。
6. 交付清单比一张“通过”截图更可靠
真正交付时,除模型与 frame_manifest.json 外,还应保存策略版本、源产物摘要、锚点摘要、验证统计和失败原因字典。查看器读取不认识的 manifest 版本时应拒绝测量功能,而不是忽略字段继续展示尺寸。视觉浏览可以降级,测量结论不应降级。
最后复核四件事:源坐标与目标坐标是否写清;旋转、缩放、平移顺序是否有测试;尺度是否来自可追溯证据;验证点是否独立于拟合样本。满足这些条件,VERIFIED 才是可复现的工程状态,而不是页面上的绿色标签。
团队协作时还要把“重新对齐”的权限收紧。普通查看页面不应悄悄改写 manifest,只有明确的校准流程才能创建新 generation。新结果通过前,线上消费者继续引用旧版本;失败记录保留证据但不覆盖当前指针。这样可以避免一次误点参考物就让所有下游页面同时看到尺寸跳变,也便于在问题出现后回溯是哪组锚点、哪版策略和哪次矩阵计算改变了产物。
参考资料:
更多推荐



所有评论(0)