一批准备用于空间重建的照片,从采集侧交给内容侧时,最容易被忽略的不是像素够不够,而是“照片是否还在原来的坐标系里”。图片被裁去一圈,又缩成较小尺寸,视觉上没有明显变化;如果仍然沿用旧相机内参,后续看到的几何位置却已经不对。本文把问题收窄到交付前的离线检查:用 Node.js 判断裁切区域、缩放尺寸和内参换算是否一致,再讨论怎样让 HarmonyOS 应用展示这份结果。

一、先把输入合同说清楚

Demo 叫 IntrinsicCropGate,任务编号 IC-410。这不是重建算法,也不直接调用 3DGS 端侧重建服务。它只接受一个来源相机配置、若干裁切后的输出图片记录和每张图片随附的内参。来源图宽高为 1600×1200,焦距参数为 fx=1200、fy=1180,主点为 cx=790、cy=600。所有数值均是本次构造样本,不是某款手机的标定值。

项目规定:裁切区域必须完全位于来源画面内;输出宽高是正整数;fx、fy 大于零;主点落在输出图的合法像素范围。通过这些基础条件之后,再用来源内参推导期望值,逐项与交接文件内的实测字段比较。为便于制作可重复的样本,最大绝对差阈值设为 0.5 像素;阈值属于内部交接约定,不来自 Huawei Spatial Recon Kit 文档。

我不建议把“裁切后依旧能打开图片”当成数据正确的证明。图片解码器只关心文件可否解码,不会替我们理解相机主点为何偏移。数据接收方也不宜擅自对错误内参做自动修正,因为缺失的裁切日志、不同的插值方式和上游坐标约定都可能影响修正是否成立。

二、用同一套公式描述裁切与缩放

设来源内参为 fx、fy、cx、cy,裁切矩形左上角为 x、y,裁切宽高为 rw、rh;输出图宽高为 ow、oh。缩放比例分别为 sx=ow/rw、sy=oh/rh。裁切相当于先将主点减去裁切起点,然后再缩放,因此目标内参应当是 fx'=fx×sx、fy'=fy×sy、cx'=(cx-x)×sx、cy'=(cy-y)×sy。横向和纵向比例分别计算,不能在非等比缩放时偷用一个系数。

问题说明:这四个字段的计算是纯数据变换,应先从文件与 UI 解耦。核心计算可以用下面的 Node.js 片段表达。

const sx = output.width / roi.width;
const sy = output.height / roi.height;
const expected = {
  fx: base.fx * sx,
  fy: base.fy * sy,
  cx: (base.cx - roi.x) * sx,
  cy: (base.cy - roi.y) * sy
};

如果来源图裁切为从 (200,100) 开始的 1200×900,再缩为 800×600,那么 sx 和 sy 都是三分之二,期望 fx'=800、fy'≈786.6667、cx'≈393.3333、cy'≈333.3333。这些值不是通过图像内容猜出来的,而是由明确的裁切合同推导出来的。只保存缩放比例却丢了裁切原点,主点换算就没有足够的信息。

模型还要约定像素坐标系。本文将图像左上角作为坐标原点,横向向右、纵向向下;没有展开像素中心偏移半个单位、相机畸变重投影或额外旋转的情况。真实工程如果改变过坐标系,应把转换矩阵和约定一起交付,而不是照抄本 Demo 的数字。

三、先拦截非法输入,再比较偏差

只比较四个数的差,容易把无效数据送入计算。例如裁切矩形越过图像右边界时,推导出来的焦距仍是有限数字,看起来甚至很接近;又如把主点写成 845,输出图宽只有 800,数值也能通过普通减法。因此错误处理必须分层,并规定每类错误在结果中的优先顺序。

问题说明:下面是示例脚本采用的判定顺序缩写,实际可执行版本位于 tools/intrinsics-audit.mjs,包括 NaN、负数、非整数尺寸和对象缺失检查。

if (roi.x + roi.width > base.width || roi.y + roi.height > base.height) {
  return { id: sample.id, status: 'HOLD_ROI' };
}
if (measured.cx < 0 || measured.cx >= output.width ||
    measured.cy < 0 || measured.cy >= output.height) {
  return { id: sample.id, status: 'HOLD_CENTER' };
}
const maxDelta = Math.max(...Object.keys(expected)
  .map(key => Math.abs(expected[key] - measured[key])));
return { id: sample.id,
  status: maxDelta <= tolerance ? 'PASS' : 'HOLD_CALIB' };

HOLD_ROI 代表上游传来的裁切记录不可信,应找采集或编辑环节补原始 ROI;HOLD_CENTER 表示主点已经落到目标图外,不能当作普通浮点误差;HOLD_CALIB 才进入内参偏差讨论。正式脚本还以 HOLD_SCHEMA 拒绝未提供字段及不合法的数据类型,避免比较运算中的隐式类型转换产生假通过。

交接文件里的 measured 只是业务方提交的内参,不表示运行了标定仪器。用 expected 与 measured 命名,是为了区分推导数据和待核对数据;文章不会把构造数值写成真实设备测量。对于内参的来源、相机畸变模型和重建软件的具体读取格式,必须由真正的重建链路单独核查。

四、让每条记录保留独立可追溯结果

fixtures/camera-cases.json 内置五个用例。CAM-71 未做裁切;CAM-72 是合法裁切加等比缩放;CAM-73 的主点超出输出宽度;CAM-74 把应为 800 的横向焦距误写成 760;CAM-75 从横坐标 1000 开始截取宽 700 的区域,越过来源图的右边界。

问题说明:若只返回整体失败布尔值,产品页面就无法告诉编辑人员该改哪份素材。脚本逐项写报告,控制台也打印固定任务号,便于本地构造测试和 CI 归档。

const results = data.cases.map(sample =>
  audit(sample, data.camera, data.tolerancePx));
const summary = Object.fromEntries(
  ['PASS', 'HOLD_ROI', 'HOLD_CENTER', 'HOLD_CALIB', 'HOLD_SCHEMA']
    .map(status => [status, results.filter(r => r.status === status).length]));
for (const row of results) console.log(`${data.task} ${row.id} ${row.status}`);

使用 node tools/intrinsics-audit.mjs 可以在工程目录下生成 reports/camera-report.json。本轮在 Node.js 环境执行构造数据,得到 CAM-71 PASS、CAM-72 PASS、CAM-73 HOLD_CENTER、CAM-74 HOLD_CALIB、CAM-75 HOLD_ROI,汇总是 PASS 2 / HOLD 3。--ci 模式遇到 HOLD 会设置退出码 2,表示拦截符合预期,不是脚本崩溃。报告应和具体图片批次一起归档,避免第二天同名文件覆盖后无法复现。

五、HarmonyOS 页面只消费检查结果

在设备应用中,可以使用 ArkUI 页面和列表组件展示这份 JSON:列表左侧显示相机编号,中部显示 PASS 或拦截类别,详情区域列出 ROI 与期望内参。按钮叫“检查构造内参”,它在本文中表示触发示例数据重放。DevEco 风格配图里的右侧开发环境和底部 HiLog 都是按相同数据设计的示意。

页面要保留三个交互细节。第一,切换记录时展示同一份报告内的数据,不要让当前选中的相机影响其他条目。第二,不要把 HOLD 的条目默认隐藏,否则使用者容易误以为全部通过。第三,显示误差阈值和坐标约定,让读者知道阈值变化后结论可能变化,而不是把 0.5 像素当成物理定律。

对于需要实际接入 HarmonyOS 系统能力的工程,应另外确认照片获取方式、文件 URI 生命周期和是否允许读取所选素材。这里的 Node.js 脚本设计为构建时或素材交付阶段运行,不能原样塞入 ArkTS 沙箱中执行;移动端消费的是产出 JSON,而不是调用 node:fs。这样把可重复的门禁与真实平台 API 适配分开,错误更容易定位。

再看一个容易误判的情况:来源图已经按某种畸变模型做过矫正,但数据记录只写“已裁切”。这种场景即使四元数和焦距数值接近,主点坐标也未必还能按普通裁切公式处理。门禁应在元数据中添加处理类型与转换顺序,并要求标注人员能回到原始文件追查。对于未知变换,宁可输出待补充信息,也不要为了让批次通过而补一个看似合理的数。

除了逐项比对最大偏差,还可以在报告中记录四个字段各自的误差,方便区分“所有值按比例偏移”与“只有主点移动”。这类差异通常指向不同的上游处理环节。当前示例只用最大绝对差做准入,原因是样本规模小、合同简单;真正的数据链路可以据此扩展,而不用让前端承担几何校正职责。

六、失败、清理与下一步

JSON.parse 失败、样本字段类型异常、输出目录不可写,都应该中止本次报告生成并保留旧版合格结果,不能悄悄把异常记录成 PASS。示例脚本只写一个本地报告;在多人并发制作的工程里,还需要临时文件、原子替换以及运行唯一标识来避免覆盖竞态。所有文件读写由 Node 同步 API 在短作业中完成,进程退出时关闭描述符;长期服务则建议显式管理句柄。

它只把容易被忽略的几何数据换算问题,提前变成一份可解释、可复现的交接报告。开发者可继续查阅华为官方的 空间计算与 3DGS 能力介绍 以及 空间重建相关开发指南,再根据实际 SDK 文档对接后续处理链路。

Logo

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

更多推荐