做3DGS素材接入时,有个问题比模型能不能加载更靠前:同一组相机内参,在重建图、压缩预览图和展示图之间究竟有没有被正确换算。画面看起来偏一点,往往会被误认为高斯点的位置不准;可如果焦距和主点仍保留原始分辨率的数值,后面的定位和可视化都会跟着误判。这篇文章只处理数据入口,不假设掌握重建算法,也不把业务校验函数写成华为系统的相机接口。

设想一个叫 LensGate 的校验页,任务号为 CAM-2303。它收到三份内参:CAM-101 正常,CAM-102 的主点跑出了源图右边界,CAM-103 的源图宽度为零。系统只把合法内参从 1920×1080 缩放到 960×540,其余留在审阅队列。这里要追求的不是漂亮的成功百分比,而是把“非法输入到底非法在哪里”留给素材供应方。

一、把内参当成有出处的数据

相机内参通常记录为焦距 fx、fy 与主点 cx、cy,再配上采集图像的宽高。它们都与某一个像素坐标系绑定。只存四个浮点数而丢掉宽高,后续就无法知道这些数是来自原图、裁剪图还是已经缩小的封面。比较稳妥的做法是把宽高放进同一条记录,配上能够追溯到素材的业务ID,导入时再要求每一项都可解释。

LensGate不读取真实相机硬件,也不请求Camera Kit权限。它接受的是素材管线输出的JSON结构,校验发生在进入3DGS场景资源之前。华为官方关于空间计算能力的介绍说明了3DGS重建和渲染的应用领域;不过这里的数值规则属于应用自行约定的前置准入逻辑,不是Spatial Recon Kit向应用暴露的同名校验API。把两者分清,文章和代码才不会在系统升级后产生莫名其妙的依赖。

第一段代码专门定义数据契约。设计时故意保留两条错误数据,不是为了凑异常分支,而是让展示页能清楚对应供应商发来的哪份记录。目标尺寸固定为960×540,暂时不支持裁剪、旋转和畸变矫正。

interface CameraInput {
  id: string;
  width: number;
  height: number;
  fx: number;
  fy: number;
  cx: number;
  cy: number;
}
interface CameraOutput {
  id: string;
  status: string;
  fx: number;
  fy: number;
  cx: number;
  cy: number;
}
const DEMO_CAMERAS: CameraInput[] = [
  { id: 'CAM-101', width: 1920, height: 1080,
    fx: 1350, fy: 1348, cx: 959.5, cy: 539.5 },
  { id: 'CAM-102', width: 1920, height: 1080,
    fx: 1350, fy: 1348, cx: 2250, cy: 539.5 },
  { id: 'CAM-103', width: 0, height: 1080,
    fx: 1350, fy: 1348, cx: 959.5, cy: 539.5 }
];

CameraInput约定源图尺寸与内参在同一坐标系,CameraOutput保存审查后的数值和状态。数组并不从网络加载,也没有文件句柄需要在页面消失时关闭;它只是可复现的演示输入。工程中换成JSON解析结果后,要先检查字段类型,不能把字符串“1920”直接当数字用。尤其是JSON可能携带null或缺字段,这时应在反序列化边界拒绝,不能指望类型声明替运行时做检查。

二、缩放公式简单,前提却不简单

对于没有裁剪、仅做线性缩放的图像,横向比例是目标宽除以原宽,纵向比例是目标高除以原高。于是新焦距分别是 fx × sx 和 fy × sy,新主点分别是 cx × sx 和 cy × sy。如果从1920×1080缩成960×540,比例都是0.5,CAM-101 的预期结果便是 fx=675、fy=674、cx=479.75、cy=269.75。这些数字应写在调试设计里,避免截图改了尺寸却忘了同步文章。

这里有一处很容易被忽略:检查必须发生在除法之前。宽为零不应该得到一个无穷大的比例再被传给渲染器。主点若已超出原图,缩放之后即使恰好落到其他容器里,也不应被自动“修正”。素材本身的物理含义不明确,业务层不能替上游猜。焦距为负、非有限数、目标尺寸越界也应该分别归类,最好为每一种异常留下稳定的枚举值,便于批量统计。

第二段代码把准入与换算放在同一个纯函数中。拒绝项返回零值与明确状态,调用方只能在状态为PASS时使用换算后的数值。这里的错误状态均由业务定义,并不是SDK抛出的官方错误码。

function rejected(id: string, status: string): CameraOutput {
  return { id, status, fx: 0, fy: 0, cx: 0, cy: 0 };
}
function checkResize(cam: CameraInput, tw: number,
  th: number): CameraOutput {
  const values: number[] = [cam.width, cam.height, cam.fx,
    cam.fy, cam.cx, cam.cy, tw, th];
  if (!values.every((v: number) => Number.isFinite(v))) {
    return rejected(cam.id, 'NON_FINITE');
  }
  if (cam.width <= 0 || cam.height <= 0 || tw <= 0 || th <= 0) {
    return rejected(cam.id, 'INVALID_DIMENSION');
  }
  if (cam.fx <= 0 || cam.fy <= 0) {
    return rejected(cam.id, 'BAD_FOCAL');
  }
  if (cam.cx < 0 || cam.cx >= cam.width ||
      cam.cy < 0 || cam.cy >= cam.height) {
    return rejected(cam.id, 'PRINCIPAL_OUTSIDE');
  }
  const sx: number = tw / cam.width;
  const sy: number = th / cam.height;
  return { id: cam.id, status: 'PASS',
    fx: cam.fx * sx, fy: cam.fy * sy,
    cx: cam.cx * sx, cy: cam.cy * sy };
}

tw、th表示输出像素尺寸,sx、sy是独立计算的缩放比例。实际应用还要给尺寸设上限,例如按工程允许的最大纹理尺寸预先拦截极端输入;上面函数只展示最小闭环。Number.isFinite是语言标准能力,不能把它描述为3DGS专有接口。纯函数没有异步请求、监听器和缓存,因而也没有对应的释放动作,页面可以在任何生命周期阶段重新调用它。

三、先让状态看得懂,再谈可视化

检查器最怕把三份记录都画成差不多的表格,却不告诉开发者谁能进入下一阶段。LensGate的页面设计只保留任务编号、总状态以及逐条结果。CAM-101标记绿色PASS,CAM-102展示PRINCIPAL_OUTSIDE,CAM-103展示INVALID_DIMENSION。总计1/3 PASS,批次状态为BLOCKED。如果后续需要导入渲染服务,应由外层控制器筛选PASS记录,绝不能仅依据数组是否非空来启动渲染。

第三段代码使用ArkUI的基础声明式组件展示结果。为避免页面二次进入时不断追加记录,aboutToAppear直接替换数组。真正接入异步数据源时,还要设计请求版本号和取消策略;本例特意保持同步,专注于数值边界。

@Entry
@Component
struct LensGatePage {
  @State rows: CameraOutput[] = [];
  aboutToAppear(): void {
    this.rows = DEMO_CAMERAS.map((c: CameraInput) =>
      checkResize(c, 960, 540));
  }
  build() {
    Column({ space: 16 }) {
      Text('LensGate / CAM-2303').fontSize(22).fontWeight(FontWeight.Bold)
      Text('1/3 PASS · BLOCKED').fontColor(Color.Red)
      ForEach(this.rows, (row: CameraOutput) => {
        Row() {
          Text(row.id).width('34%')
          Text(row.status).fontSize(15)
        }.width('100%').padding(12)
      }, (row: CameraOutput) => row.id)
    }.width('100%').padding(24)
  }
}

rows是页面状态数组,稳定键使用id,而非数组下标。页面销毁后这组内存数据自然回收;如果将来增加异步文件解析,应在组件离开时取消订阅并检查结果是否仍属于当前任务。这里的FontWeight、Text、ForEach是ArkUI显示能力,不代表真实开发环境已经完成构建。下面的DevEco Studio风格图片按本文状态、变量和函数绘制,属于合成讲解画面。

四、怎样把失败记录查明白

先检查源JSON是否标注了图片宽高和裁剪历史。假设有一张图拍摄时是1920×1080,进入模型前却被左右各裁去160像素,主点不仅要缩放,还需要先减掉裁剪偏移。只做当前公式会制造一个“数值合法、几何不准”的假通过。因此在本Demo中,数据契约强制假设没有裁剪;缺少预处理说明的素材应交人工复核,不能默默当作等比缩小。

再检查fx、fy是否沿用源像素单位,有没有供应方使用归一化坐标或毫米单位。假如焦距已经被除以宽高,再乘缩放比例就会二次缩小。对于没有清晰单位说明的来源,应在转换前记录原始字段、规范名称和版本,不能靠一个阈值猜测单位。3DGS场景往往涉及多张照片,单张内参异常会使视角关联判断出现偏差;把状态定位到记录ID比直接输出“场景失败”更能指导排错。

还要观察边界像素的定义。有的生产链以0.5作为左上角像素中心,有的校准结果以整数像素坐标表示。本文采用0 ≤ cx < width、0 ≤ cy < height的宽松合法性检查,不把像素中心的半格差异强行判错。需要做高精度重投影时,应在同一个成像模型下明确像素中心约定,统一坐标和误差范围;否则两个看似相同的算法可能有系统性的半像素偏移。

五、预期屏幕与回归样本

手机端合成演示页顶部显示任务CAM-2303,中部先给出1/3 PASS和BLOCKED,下面依次列三张记录卡。CAM-101展示缩放后的四项内参,CAM-102以橙色提示主点越界,CAM-103以红色提示尺寸为零。底部有“检查输入”说明,而不是“开始重建”按钮。

回归时应至少准备五类输入:正确的等比缩放、非等比缩放、主点越界、零尺寸、非有限数。非等比缩放在纯Resize中允许横纵焦距使用不同系数,但如果图片经历了适应容器的等比缩放加留白,必须把偏移也写入变换矩阵。负焦距和主点缺失分别需要稳定错误分类,不能混进网络异常或者解析失败。页面截图检查的重点是ID、数值、颜色与状态相符,而不是让红绿色块代替问题说明。

六、发布边界与资料核对

这套方案适合接收外部3DGS素材、导入离线相机元数据、在资源准备阶段做准入审查的应用。它不检查镜头畸变参数,不验证外参旋转和平移,也不证明重建成果的几何质量。一个PASS只说明该记录符合本Demo的输入契约,并不保证渲染画面准确。尤其不能将此处的checkResize误写成HarmonyOS Spatial Recon Kit的接口名。

资料层面,华为开发者站点的空间计算专题可以确认3DGS端侧重建和渲染能力方向;ArkTS运行时资料可确认标准数值语言环境和页面开发基础。本例没有调用Spatial Recon Kit的模型加载、重建或渲染API,因此不存在未经确认的系统参数。后续如果要连接真实3DGS工作流,第一步应该先补齐相机标定与裁剪信息,再将合格结果交给明确版本的渲染适配层,而不是反过来让UI推导原始数据究竟来自哪里。

参考资料:HarmonyOS空间计算能力;ArkTS运行时概述。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐