空间重建最让人头疼的并不总是算法耗时,而是按下“开始”之前,输入素材究竟是不是我们以为的那一批。一个场景采了八张图,如果其中一张重复上传,另一张丢了标定信息,界面仍然写着“已采集八张”,操作者便容易把数量误当成质量。这里讨论一个刻意收窄的工程环节:在 HarmonyOS 的 ArkTS 页面里,为 3DGS 多视角采集制作帧清单审计,把可解释的退回理由留在进入原生重建之前。

这不是对 Spatial Recon Kit 重建效果的实测,也不是私造其 C 接口。官方介绍明确了端侧重建属于空间计算能力;至于某设备是否支持、原生帧结构如何填充、相机内参来自哪里,都应在实际接入时以相应版本的官方开发指南和设备环境为准。本文只处理应用完全能够自主管理的清单、状态、幂等和补录入口。

一、八条采集记录不等于八条有效输入

设想一个小型展柜采集任务 SCN-506,界面写明原始帧八条。资料由拍摄模块提供,附带帧编号、沙箱图片索引、宽高、标定引用和内容指纹。示例清单里六条可进入下一步,F-006 与 F-002 使用了相同内容指纹,F-007 没有标定引用,因此待补录两条。审计结果是“接纳 6/8、退回 2”,不是“算法预计成功率 75%”。后者需要模型层的实际验证,不能从简单清单推出来。

图片索引与内容指纹在这个示例里都由上游采集适配层给出。指纹字段仅用来演示“相同内容不应因为改了文件名就自动算新帧”;正式应用要自己选择可靠的内容摘要算法,并确认摘要输入是原始压缩文件还是解码后像素。两种算法产生的去重含义不同:前者容易被压缩参数扰动,后者的像素规范化又可能受到方向和色彩空间影响。本文不将示例指纹伪称为真实 SHA 结果。

项目暂命名 SceneLedger。页面为 Index.ets,审计函数为 auditFrames(),日志标签固定使用 FRAME_AUDIT。不把判断写进每个卡片的 onClick,因为以后要支持后台重试、清单导入和出错复查;规则必须是独立、可重放的纯数据逻辑。

二、用明确的退回原因代替一个布尔值

第一段代码定义业务侧帧清单与审计函数。重点不是校验所有拍摄品质,而是先杜绝最容易定位、也最容易反复出现的两类输入错误。contentTag 是上游给出的演示指纹,calibrationKey 只代表有可关联的标定资料,并不证明标定数值准确。正式对接需要另有标定文件的存在性与内容检查。

interface FrameRow {
  frameId: string;
  fileKey: string;
  width: number;
  height: number;
  calibrationKey: string;
  contentTag: string;
}
interface FrameIssue { frameId: string; reason: string; }
interface AuditReport { accepted: number; rejected: number; issues: FrameIssue[]; }

function auditFrames(rows: FrameRow[]): AuditReport {
  const seen = new Map<string, string>();
  const issues: FrameIssue[] = [];
  let accepted = 0;
  for (const row of rows) {
    let reason = '';
    if (row.width <= 0 || row.height <= 0 || !row.fileKey) {
      reason = 'INVALID_IMAGE_META';
    } else if (!row.calibrationKey.trim()) {
      reason = 'MISSING_CALIBRATION';
    } else if (seen.has(row.contentTag)) {
      reason = `DUPLICATE_CONTENT ref=${seen.get(row.contentTag)}`;
    }
    if (reason) { issues.push({ frameId: row.frameId, reason }); }
    else { seen.set(row.contentTag, row.frameId); accepted++; }
  }
  return { accepted, rejected: issues.length, issues };
}

const DEMO_FRAMES: FrameRow[] = [
  { frameId: 'F-001', fileKey: 'img/01.jpg', width: 1280, height: 960, calibrationKey: 'CAL-A', contentTag: 'h01' },
  { frameId: 'F-002', fileKey: 'img/02.jpg', width: 1280, height: 960, calibrationKey: 'CAL-A', contentTag: 'h02' },
  { frameId: 'F-003', fileKey: 'img/03.jpg', width: 1280, height: 960, calibrationKey: 'CAL-A', contentTag: 'h03' },
  { frameId: 'F-004', fileKey: 'img/04.jpg', width: 1280, height: 960, calibrationKey: 'CAL-A', contentTag: 'h04' },
  { frameId: 'F-005', fileKey: 'img/05.jpg', width: 1280, height: 960, calibrationKey: 'CAL-A', contentTag: 'h05' },
  { frameId: 'F-006', fileKey: 'img/06.jpg', width: 1280, height: 960, calibrationKey: 'CAL-A', contentTag: 'h02' },
  { frameId: 'F-007', fileKey: 'img/07.jpg', width: 1280, height: 960, calibrationKey: '', contentTag: 'h07' },
  { frameId: 'F-008', fileKey: 'img/08.jpg', width: 1280, height: 960, calibrationKey: 'CAL-A', contentTag: 'h08' }
];

这段判断有刻意的顺序:文件元信息先于标定,再处理重复。对同一条坏记录只报告首个阻断原因,页面比较容易给出清晰的修复动作。seen 只记被接纳记录的指纹,否则一条缺标定的记录会把后续合法输入误判成重复。Map 是内存结构,页面退出即可回收,不涉及特殊权限;要跨启动保留审计结果,应另存清单快照和输入版本,而不是仅保留一个成功数量。

项目不根据相机角度粗暴删除相似帧。多视角采集允许相近的拍摄位置,简单“角度桶冲突”既可能错杀必要素材,也不等价于像素重复。内容摘要也不是几何覆盖率指标,这一点必须写进验收说明,避免业务把“没有重复”解读为“模型一定完整”。

三、补录一条时,旧结果不能覆盖新清单

清单检查虽然是本地运算,但实际项目的指纹计算、磁盘读取和相机元数据整理经常是异步的。用户可能在等待过程中替换 F-007 的标定引用,又立即执行第二次检查。如果第一次慢任务最后返回,直接覆盖页面,就会把旧清单问题重新显示出来。这里借用请求版本门控的思想,但对象不是上一轮“文搜图乱序回包”,而是同一次采集清单的输入修订:任何一次补录都会废弃旧快照的审计提交资格。

第二段代码把“开始审计”和“应用结果”拆开。revision 是应用自己的递增整数,并非系统任务 ID。拍摄模块在内容更新时必须调用 invalidate(),否则这个门控就形同虚设。

class ManifestGate {
  private revision: number = 0;
  invalidate(): void { this.revision++; }
  begin(): number { return ++this.revision; }
  isCurrent(ticket: number): boolean { return ticket === this.revision; }
}

async function auditSnapshot(rows: FrameRow[], gate: ManifestGate): Promise<AuditReport | null> {
  const ticket = gate.begin();
  const snapshot: FrameRow[] = rows.map((row: FrameRow) => ({ ...row }));
  const report = auditFrames(snapshot);
  return gate.isCurrent(ticket) ? report : null;
}

演示函数本身没有耗时操作,所以通常会立即得到结果。这里故意保留 Promise 边界,是为了未来接入真实 I/O 后不用把页面状态规则推倒重来。关键风险是拷贝仅复制对象字段:此例所有字段都是值类型与字符串,浅拷贝足够;若对象里放入可变的原始字节数组,则还需要复制或冻结该数据。null 的语义是“版本失效,应忽略”,不是“检查通过”。

不要让多个点击动作同时修改同一份 rows;更稳妥的办法是为每次清单版本建立不可变快照,完成后按版本合并结果。想进一步优化,可以对文件摘要缓存设置 fileKey + size + modifiedAt 等复合缓存键,但修改时间不是防篡改证据,关键业务仍要有内容校验。

四、把可解释结果交还给页面

页面只提供两个按钮:“检查采集帧”和“查看待补帧”。本例初始数据的最后两条人为放入异常项,属于为文章制作的演示输入,不是某台设备抓出的真实数据。显示区固定使用 SCN-506、8 / 6 / 2,HiLog 的文字亦取自同一清单。正式版应把事件写到可检索的日志渠道,并注意不要打印文件绝对路径、拍摄位置等敏感内容。

第三段代码是界面层的核心动作示意。为了突出状态流转,省略常规的样式修饰;示例数组 DEMO_FRAMES 已列在第一段代码中:共八条,其中 F-006 的 contentTag 与 F-002 相同,F-007 的 calibrationKey 为空,其余六条满足当前检查。数据数组是本地演示夹具,不涉及照片权限。

@Entry
@Component
struct SceneLedgerIndex {
  @State private summary: string = '待检查';
  @State private issuesText: string = '尚无审计结果';
  private readonly gate: ManifestGate = new ManifestGate();

  async runAudit(): Promise<void> {
    this.summary = '检查中';
    const result = await auditSnapshot(DEMO_FRAMES, this.gate);
    if (!result) { return; }
    this.summary = `原始 8 / 接纳 ${result.accepted} / 退回 ${result.rejected}`;
    this.issuesText = result.issues.map((v: FrameIssue) =>
      `${v.frameId} ${v.reason}`).join('\n');
    console.info(`FRAME_AUDIT SCN-506 checked=8 accepted=${result.accepted} rejected=${result.rejected}`);
  }

  build() {
    Column({ space: 18 }) {
      Text('SceneLedger · SCN-506').fontSize(23)
      Text(this.summary).fontSize(18)
      Text(this.issuesText).fontSize(14)
      Button('检查采集帧').onClick(() => { this.runAudit(); })
      Button('查看待补帧').onClick(() => { console.info('FRAME_AUDIT open issues'); })
    }.padding(22).width('100%')
  }
}

这里的 DEMO_FRAMES、FrameRow 和 ManifestGate 必须按前面约定的名字在工程中导入;示例为了版面只列关键实现,不应整段当成零配置可安装工程。console.info 方便在 HiLog 观察,但正式应用最好携带清单修订号,避免不同批次日志串在一起。如果用户离开页面时有尚未结束的文件读取,页面应调用 invalidate() 阻止回写;并按底层 I/O 模块要求清理文件句柄。ManifestGate 不替系统清理任何相机资源。

五、用两种极端输入核对规则边界

测试顺序先固定输入再改变一处字段。第一次构造八条演示数据,预期 accepted=6、rejected=2,两条异常分别是 F-006 DUPLICATE_CONTENT ref=F-002 与 F-007 MISSING_CALIBRATION。第二次只补上 F-007 的合法引用并重新检查,预期变为七条接纳、一条退回。再替换 F-006 的内容与可信指纹,预期才可能变为八条接纳。每一步都需要检查究竟更换了原始文件,还是只改了标签;只改字符串并不能消除真实重复内容。

图中的手机界面表现第一次预检后的目标状态,故障标签、任务号与正文共用一套示例值,方便读者知道遇到问题应该看哪里。

还要增加一个顺序压力场景:先启动较慢的文件摘要整理,随后补录一张图片并触发新检查,观察旧任务的结果是否被丢弃。此实验当前没有实机结果,所以只列可执行的断言,不填毫秒数。遇到图片读不出、摘要失败或设备不支持原生重建时,页面应停在“待修复/能力不可用”,不能假装模型已经开始构建。

最终进入 Spatial Recon Kit 的前置条件也不止这个审计结果。相机内参真实性、图片分辨率范围、可用内存、原生资源释放顺序、设备型号以及重建会话返回码,都需要按官方 API 文档另做接入验证。本文真正能保证的是:业务清单可以解释自己接纳了哪些输入、拒绝了哪些输入以及原因;它不能代替 3DGS 算法质量评估。把这条边界说清楚,比把进度条画成百分之百更有价值。

资料边界: 华为 HarmonyOS 空间计算能力介绍:https://developer.huawei.com/consumer/cn/features/spatialization ;Spatial Recon Kit C/C++ 重建流程入口:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline 。本文未给出无法从公开检索核实的原生接口签名。

Logo

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

更多推荐