Files
ykcheungab 433b76e43c 修复 Studio 失败测试并新增合并门禁
把丢失的 box 录像放回 apps/studio/tests/data/,修掉第 3 节 3–10 号与两处过时测试桩,
注册 timing 标记并让根目录收集零错误;新增 tools/gate.ps1 与可选的 pre-push 钩子。
验证:studio 524 通过 19 跳过、shared 467 通过 18 跳过、ui_check 632 项 0 失败、gate.ps1 退出码 0。
细节见 docs/test-repair-plan.zh.md。
2026-10-18 19:24:31 +08:00

37 KiB
Raw Permalink Blame History

Studio 三维显示架构与实施规范

版本:1.0 提案;日期:2026-09-14。目标读者:Studio、SDK、Blender 资产、ARM 平台和测试工程师。

本文的“必须”是接口正确性要求,“建议”是第一版实现选择,“待验证”不是已获硬件验收。本文配套 studio-3d-contracts.pyi 是接口骨架,不是可运行的三维功能。

1. 决策与实施范围

推荐使用现有 Qt 技术栈的 QOpenGLWidget + PyOpenGL(Qt 绑定为 PySide6,LGPLv3),使用现代着色器和 GPU 缓冲区;Open3D 保留为离线几何处理和开发验证工具。 不为三维显示更换整个 Studio UI 技术栈。

目标为 Jetson Orin Nano、QCS6490、Ubuntu。目前没有两块实际部署板的 Ubuntu/BSP、Qt 构建方式、窗口系统和驱动版本;这不阻碍接口设计,但禁止承诺“装好 pip 包即可兼容 ARM”。

方案 适合的职责 对本项目的判断
QOpenGLWidget + PyOpenGL 嵌入现有 Qt、动态网格、固定色标、分层渲染 首选;自己维护相机、shader、资源生命周期,工作量可控
Open3D 点云、配准、网格处理、离线算法验证 不作为 Studio 运行时必需依赖;其 GUI 与 ARM 图形栈有额外限制
Qt 自身 OpenGL 封装 Qt 上下文与资源管理;可作为 GL 调用适配实现 若 PyOpenGL 与目标平台 EGL 函数加载不匹配,在 GLDispatch 内替换绑定;需验证实际 PySide6 暴露的接口。Qt6 把 QOpenGLWidget 移到了 QtOpenGLWidgets,打包时该模块与 Qt6OpenGLWidgets.dll 都要收进 bundle
更换完整场景引擎/Qt 大版本 重型场景、复杂材质、动画 第一版需求不足以抵消迁移成本,暂不做

Open3D 官方将 ARM64 支持标为实验性;其支持矩阵区分 Linux 桌面 OpenGL 与 OpenGL ES,后者不支持其 Visualizer/GUI。新渲染模块的文档另要求 OpenGL 4.1。不能把 Jetson 上的运行结果等同于 QCS6490,也不能把 Python 包可安装当作 GUI 可用。Open3D ARM 支持、渲染要求

第一版包括:静态完整外观、动态感知表面、旋转/缩放/平移/复位/预设视角、固定或手动色标、图例、显示倍率、外壳显隐、性能与不可用提示。透明外壳、阴影、PBR、多重后处理、局部矢量和三维拾取列为后续项。

2. 当前代码与资产事实

代码检查基于当前工作区;审查期间 app HEAD 已更新为 0d8b79b,另有滤波相关未提交修改。本提案不覆盖这些修改,不要求恢复已经删除的旧 record/depth.py。

现有文件/类 当前内容 复用方式
orisys-sdk/src/orisys/finger/exports.py / FingerExportPaths、load_finger_export_bundle() 寻找并加载 Blender 的网格、内参、位姿 保持 SDK 兼容;不要将显示外壳塞进算法网格
orisys-sdk/src/orisys/unroll/precompute.py / load_exported_data() 加载 vertices/faces/逐角 UV/K/world_to_cam 离线导入与旧资产适配
orisys-sdk/src/orisys/finger/mesh.py / build_uv_surface_mesh() 从 UV world_xyz 构建表面,保存像素采样位置 复用几何算法;第一版在加载阶段构建,不每帧重新建拓扑
同文件 sample_field_at_mesh_vertices()、lift_uv_flow_to_3d() 逐顶点采样与光流切向映射 封装在 Finger 适配器,不依赖 Open3D viewer
同文件 lift_uv_deformation_to_3d()、deform_mesh_vertices() 相对形变示意;存在 normal_gain、近似 pixel_to_world 与显示倍率 只能在明确标记 approximate 的通道使用
orisys-sdk/src/orisys/finger/viewer.py Open3D 网格创建与更新 留给 SDK 示例,Studio 不导入
orisys-sdk/examples/viz/mesh_camera.py 观察相机;含额外 X/Z 旋转 作为迁移对照;不能将旋转常量散落到 Studio shader
orisys-app/shared/src/orisys_app_shared/acquisition/models.py / SdkPacket flow、depth、valid_mask、会话/帧身份 新增一个可选三维字段或独立显示订阅,避免多个散乱字段
orisys-app/apps/studio/src/orisys_studio/replay.py 当前回放视频和力,不保存/直接重算深度;重算属于采集入口 普通历史回放不能假装拥有三维测量数据

实际读取资产结构(np.load(..., allow_pickle=False)):

资产目录 内容 实际结构
sensors/finger/assets/30fps-0703 mesh.npz vertices float32[1032,3];faces int32[1974,3];uv_data float32[5922,2];loop_to_vertex int32[5922]
同目录 camera_intrinsics.npz / world_to_cam.npy K float64[3,3];宽高 int32 标量;位姿 float32[4,4]
同目录 pixel_to_xyz.npz xyz_map float32[720,1280,3]
同目录 unroll_map_uv_s0.4.npz map_x/map_y/valid_mask 为 [300,280];world_xyz 为 [300,280,3];含 source_scale、flip_v、projection_K
sensors/finger/assets/visualization mesh.npz vertices[1005,3]、faces[1921,3],与运行网格不是同一个拓扑
同目录 pixel_to_xyz.npz / flatten_remap.npz xyz_map[500,500,3];展平映射及 flat_xyz[1024,1024,3]

不能把 1032 个导出顶点当作最终实时渲染顶点数。 当前 UV 网格函数是每个有效 UV 像素生成一个顶点,300×280 的网格在全有效时上限为 84,000 顶点;实际数量需要按 valid_mask 统计。两个资产目录也不能按数组下标直接拼接。

uv_data 为每个面角的数据,不能当作每个顶点的一份 UV。UV 接缝处若一个几何顶点拥有多个 UV,显示导出必须拆成多个 render vertex,并保留采样映射;不得强行平均 UV。

3. 坐标、单位与测量含义

3.1 坐标契约

新增场景的公共坐标定义为 sensor:右手系、长度单位米。X/Y/Z 的物理方向由资产 manifest 明确描述,并与力轴约定核对;现有 Blender 文件不能在没有证据时自动认定为米或自动认定 +Z 为传感器长轴。

统一记号 T_A_from_B:列向量满足 p_A = T_A_from_B @ p_B。平移在最后一列。JSON 矩阵按行序列化;上传 shader 时显式按列主序打包,GLES 使用 transpose=false,不让 NumPy 存储布局决定坐标含义。

转换分成三种:

  1. 算法相机:原始 K、畸变、world_to_cam、裁剪/缩放/flip_v,服务像素到曲面的映射,保留原有精度和版本。
  2. 资产坐标:Blender/glTF/旧 NPZ 到 sensor 的转换。离线发布时一次性烘焙到网格、法线和映射;运行时不再重复应用同一转换。
  3. 观察相机:用户 orbit/pan/zoom,仅改变 view/projection,不改变算法输入、标定和力轴。默认观察相机是显示资产的一部分,不自动等同于真实摄像头。

glTF 使用右手系、Y 向上、长度米;仍需显式转换到本项目的 sensor 轴约定。负缩放或坐标翻转要同步修正三角形绕序;法线使用逆转置,不能直接乘含非均匀缩放的模型矩阵。glTF 坐标与变换规范

3.2 “形变”不是天然的毫米

当前 lift_uv_deformation_to_3d() 对 vnormal 求模,再乘法线;这不能保留一个任意有符号法向位移的含义。mesh_uv_to_world_scale() 是由网格跨度估计每 UV 像素对应的世界尺度,不是经过验证的位移标定。

因此帧契约必须分开:

  • scalar:着色量,带 quantity、unit、signed、quality;如 flow_magnitude / px / false / approximate。
  • displacement_sensor_m:若提供,必须说明是 calibrated、approximate 还是仅 display_only;approximate 即使以米存储,也不能以毫米测量值向用户呈现。
  • display_gain:只作用于显示顶点 p_display = p_rest + gain * displacement,不得写回原始记录、改动标定或用放大后的值计算色标。
  • 没有可靠位移时,默认只在静态曲面着色。用户启用示意形变时显示“相对形变示意 ×N”,不得把 FN/FX/FY 总力直接当作每个顶点高度。

跨帧比较默认固定色标;有符号量使用围绕零的发散色图,非负量使用顺序色图。无效点显示中性颜色或遮罩,不当零值,超范围值夹取颜色并保留超范围状态。

4. 感知表面与非感知结构怎样分开

建议一个场景、两个独立 draw part,不把整机合成一个动态网格:

part_id / role 几何来源 每帧更新 显示规则
sensing_surface / sensing 真实感知曲面或绑定其采样映射的显示 LOD 顶点、必要时法线、scalar、valid 科学色图;参与传感色标和数据查询
housing / housing Blender 简化外壳 无;VBO/EBO 只上传一次 灰/石墨材质;不参加测量色标和峰值统计
reference_axes / reference 显示辅助几何 相机变化时更新视图 用户可隐藏;不伪装成测量

两部分都转换到 sensor 坐标,分别持有索引和材质。感知面不与外壳共用可变顶点缓冲。Blender 推荐两个明确命名的 Collection/Object,导出器按角色读取,不以颜色猜角色。

第一版外壳使用不透明材质和简单漫反射;感知面采用保留色值的着色,光照不能让同一数值在不同朝向看起来像不同数值。先保留“外壳显示/隐藏”,透明外壳后做:它涉及排序和深度写入,不能只把 alpha 调小。

衔接原则:

  • 模型接缝由美术在统一坐标中设计;外壳不复制一层完全重叠的感知面,避免 z-fighting。
  • 若显示放大产生明显穿插,可提供“示意形变”倍率上限或仅着色模式;不能为美观强行锁定测量边界并改写数据。
  • 外壳 LOD 可独立简化。感知面简化必须同时生成新的采样绑定,不能保留旧 vertex_id/UV 表后换拓扑。
  • 拾取后续如加入,返回 part_id;点到 housing 时显示“非感知结构”,不能返回一个虚构力值。

5. 文件格式与复制保护

5.1 三层资产

层 推荐格式 发布策略
内部源文件 .blend、高精度机械模型、原始相机标定 留在受控资产仓库;不随客户程序分发
制作交换 外观采用 .glb;算法几何/映射保留现有 NPZ/NPY 用于美术与导出工具;GLB 是标准交换格式,不是保密格式
Studio 运行包 自定义扩展名 .oryscene,内部为签名 manifest + 数值数组块;可选加密 渲染器仅接收已解包、已验证的 SceneAssets,不感知文件格式

第一版无需在 ARM 运行时实现完整 glTF/PBR 引擎:离线 exporter 将 GLB 的必要节点、变换、法线和基础材质编译成紧凑数组;运行时只支持本规范的固定 mesh/material 子集。GLB 动画、骨骼、任意扩展、Draco 等默认不进入第一版。

既有 SDK NPZ/NPY 路径继续兼容。若要加密算法资产,应另行给 SDK 增加只读 AssetProvider/内存流入口;仅加密 Studio 包而继续分发相同明文算法网格,不应宣称那些几何已得到保护。

5.2 明确保护目标

用户确认目标为提高逆向和复制成本,不是保证客户端无法提取模型。

建议依次落实:

  1. 最小交付:仅发布简化外壳与需要的感知几何;去掉内部机械结构、隐藏对象、原始命名、无用相机、设计历史和高精度源资产。
  2. 完整性和来源:构建机签名发布包,客户端只带验签公钥;manifest 的版本、资源角色、大小、哈希和兼容信息都纳入签名。SHA-256 只能检查内容,不等同于防篡改签名。
  3. 可选加密:使用成熟库的认证加密,例如 AES-GCM;同一密钥下每次包加密的 nonce 必须唯一,元数据作为关联数据绑定。不要自制 XOR、把密码藏在扩展名或把 ZIP 当加密。认证加密接口及 nonce 约束
  4. 密钥隔离:签名私钥不能进入客户软件;内容密钥经 KeyProvider 获取,可按产品/客户授权。若需要离线,明确离线授权/缓存失效流程。Python 内硬编码全局密钥只能提供很弱的阻力;原生加载器、设备密钥保护可提高成本但不能防运行时捕获。
  5. 解密范围:尽量内存解密、不输出明文临时文件、日志不打印密钥;Python/驱动内存副本不能承诺被完全擦除。

即使加密,渲染最终仍需在 CPU/GPU 获得几何。若某结构绝不能泄露,就不要交付该结构,改成视觉代理模型。包格式和加密不能代替这一决策。

5.3 .oryscene v1 逻辑内容

建议封装采用有大小上限的 manifest 与数组条目,不执行脚本、不使用 pickle。压缩是容量优化,不属于安全机制。读取器拒绝路径穿越、未知必须扩展、异常尺寸、越界索引、非有限坐标及解压超限。

envelope.json              # magic、envelope_version、key_id、nonce、签名信息
payload                    # 可选认证加密;解开后是下列逻辑内容
  manifest.json
  sensing/positions.npy    # float32[N,3],sensor 坐标、米
  sensing/normals.npy      # float32[N,3],单位向量
  sensing/triangles.npy    # uint32[M,3]
  sensing/sample_indices.npy  # int32[N,4],UV 网格展平下标
  sensing/sample_weights.npy  # float32[N,4],双线性权重
  housing/positions.npy
  housing/normals.npy
  housing/triangles.npy

manifest 必须包含:schema_version、asset_id、asset_version、geometry_id、sensor_model、适用硬件修订范围、geometry_sha256、mapping_sha256、coordinate_frame、length_unit、源坐标到 sensor 的烘焙记录、算法网格/相机配置哈希、UV 宽高及原点方向、source_scale/crop/flip_v、各 part_id/role/LOD/材质/数组类型尺寸、所有内容哈希、默认观察相机、编译工具版本。

hash 的定义必须稳定:签名不包含自身;按规定的规范化 manifest 字节及有序条目摘要计算。明文内容哈希用于 geometry_id/兼容性,密文完整性由认证加密与签名处理,二者不能混称。

sample_indices 的每一行最多四个有效下标,未使用项为 -1 且权重 0;有效权重和为 1。加载后验证下标属于 manifest 声明的 UV 域。运行期若任一正权重点无效,默认将该顶点标无效,不跨遮罩补值。

编译器与加载器要共用 JSON Schema。下面仅为字段关系示例,尖括号值由编译器生成,不能作为有效发布包:

{
  "schema_version": "1.0",
  "asset_id": "finger-display",
  "asset_version": "1.0.0",
  "geometry_id": "<geometry-digest>",
  "coordinate_frame": "sensor",
  "length_unit": "m",
  "compatible": {"sensor_model": "finger", "hardware_revisions": ["<revision>"]},
  "field_domain": {
    "id": "<domain-digest>", "shape_hw": [300, 280],
    "index_order": "row-major", "origin": "top-left",
    "source_scale": 0.4, "flip_v": "<actual-value>",
    "mapping_sha256": "<mapping-digest>", "camera_sha256": "<camera-digest>"
  },
  "parts": [
    {"id": "sensing_surface", "role": "sensing", "mesh": "sensing", "binding": "uv-lod0"},
    {"id": "housing", "role": "housing", "mesh": "housing"}
  ],
  "entries": {
    "sensing/positions.npy": {"dtype": "<f4", "shape": ["<N>", 3], "sha256": "<digest>"}
  }
}

6. 模块边界与文件骨架

orisys-app/shared/src/orisys_app_shared/scene3d/
  contracts.py       # DTO、枚举、形状/单位/身份校验,无 Qt/OpenGL
  assets.py          # AssetLoader、LegacyFingerImporter、包验证
  sampling.py        # SurfaceSampler:UV 场 → 顶点字段,纯 NumPy
  composer.py        # SceneComposer:字段 → RenderFrame,含法线/显示倍率

orisys-app/apps/studio/src/orisys_studio/scene3d/
  coordinator.py     # 订阅、latest-only 调度、generation/状态、错误
  adapters.py        # SdkPacket/FingerRuntime → SurfaceFieldFrame
  gl_backend.py      # GLDispatch、GLRenderer、shader、GPU 资源
  camera.py          # OrbitCamera;纯数学,不读取算法相机配置
  widget.py          # Sensor3DWidget(QOpenGLWidget);Qt 事件与上下文
  panel.py           # 视角/倍率/色标/外壳显隐,委托 coordinator

orisys-app/tools/scene_assets/
  compile_scene.py   # 离线旧 NPZ + GLB → 运行包
  validate_scene.py  # 数值、角色、坐标、兼容性和包完整性检查
  probe_gl.py        # 在实际 Qt 窗口中探测上下文并输出诊断 JSON

这些是建议职责,不要求第一天全部拆成独立大类。体量小时 sampling/composer 可同文件,shader 可与 backend 同目录;禁止把 SDK、解密、重建网格、OpenGL 和 UI 事件塞到一个 Widget。

依赖方向:

flowchart LR
  SDK[SDK / 采集线程] --> A[FrameAdapter]
  A --> Q[LatestFrameMailbox]
  Q --> C[SceneComposer / CPU 工作线程]
  P[AssetLoader / 已验证资产] --> C
  C --> R[Latest RenderFrame]
  R --> W[Sensor3DWidget / GUI线程]
  W --> G[GLRenderer / 当前Qt上下文]
  U[Panel / OrbitCamera] --> W
  SDK --> D[原有全量记录通道]

三维帧丢弃只影响显示,不增加录像/CSV 丢帧数;不能给全量记录通道施加渲染背压。SDK 数学层也不能反向 import Studio 或 OpenGL。

7. 核心数据接口

完整签名见同目录 studio-3d-contracts.pyi。下面的 shape 是硬性契约;frozen dataclass 不会冻结 NumPy 数组,发布方必须复制或转移所有权并置只读。

类型 关键字段 不变量
FrameIdentity source_id、session_id、frame_id、t_rel_s、generation、request_id source_id 区分实时/录像;generation 切换源时增加;request_id 区分回放请求
FieldDescriptor quantity、unit、quality、signed 不允许没有单位和质量说明的深度/位移
SurfaceFieldFrame identity、geometry_id、field_domain_id、scalar[H,W]、valid[H,W]、可选 displacement[H,W,3] 同一个采集帧、同一 UV 域、数组只读;缺失字段不能沿用前一帧
MeshPart part_id、role、positions[N,3]、normals[N,3]、triangles[M,3]、材质 米、sensor 坐标;索引小于 N;role 决定是否动态
SurfaceBinding sensing part_id、geometry_id、domain_id、shape_hw、indices[N,4]、weights[N,4] 与 sensing 顶点数一致;LOD 改变必须更换绑定
SceneAssets asset_id/hash、geometry_id、parts、binding、camera_presets 第一版一个 sensing part,0..多个 housing;不能把相机预设作为映射
DisplaySettings gain、deform_enabled、ColorScale、housing_visible、max_fps 只影响显示;同一 scalar_descriptor 才能沿用色标
RenderFrame identity、asset_hash、geometry_id、动态 positions/normals/scalar/valid 无 GPU handle;携带完整身份,用于拒绝旧结果
RenderStats displayed_frames、superseded_frames、compose_ms、upload_ms、paint_ms、estimated_gpu_bytes 统计不和录制混用;CPU 提交时间不冒充真实 GPU 执行时间

8. 类与函数实施合同

8.1 资产加载与编译

类/函数 输入 输出 线程、错误与副作用
AssetLoader.load(path, expected, keys) 运行包路径、ExpectedGeometry、可选 KeyProvider SceneAssets CPU 工作线程;有文件 IO;签名/哈希/域/单位不匹配抛 ThreeDError,不能退回同名不同资产
AssetLoader.validate(assets, expected) 资产、期望设备/域 tuple[Diagnostic,...] 纯校验;error 级诊断禁止发布资产,warning 可显示
KeyProvider.get_content_key(key_id) 密钥标识 bytes 平台授权层;不得写日志;加载器不内置通用主密钥
LegacyFingerImporter.convert(export_dir, map_path, transform, units) 旧资产目录、运行 UV 映射、已确认的变换/尺度 SceneAssets 仅开发/离线;缺少坐标依据必须拒绝,不从文件名猜
SceneCompiler.compile(surface, housing_glb, output, options) 源资产、可选 GLB、输出、签名/打包选项 CompileReport 离线;校验后写临时包,再原子替换目标;私钥通过外部配置提供

旧 NPZ 导入器要先验证 loop_to_vertex 与面角顺序;LOD 导出后重新生成 binding;保持算法和显示资产各自版本,避免改外壳导致算法标定哈希变化。

LegacyFingerImporter.convert() 的 meters_per_source_unit 只负责先把源顶点长度换成米;transform_sensor_from_source 随后只包含刚体旋转和平移,平移单位为米,不重复含缩放。对 source 法线只应用旋转。GLB 本身使用米,其 transform 同样不重复进行单位换算。

8.2 字段适配、采样与合成

类/函数 输入 输出 线程与行为
FingerFrameAdapter.snapshot(packet, runtime, identity) 采集包、当前拥有的 FingerRuntime、身份 SurfaceFieldFrame 或 None 只能在 runtime 所在线程读其数组;没有三维订阅时返回 None;复制本帧所需数据,禁止 GUI 再追读 runtime
BoxFrameAdapter.snapshot(packet, identity, domain) 平面数据及已确认域 SurfaceFieldFrame 或 None 第一版可只支持 scalar;未知空间尺度不能造毫米高度场
SurfaceSampler.sample(frame, binding) 只读 UV 字段、绑定 VertexFields CPU;按 indices/weights 向量化采样,检查域;不在每帧搜索最近三角形
SceneComposer.compose(assets, frame, settings) 场景、字段、显示设置 RenderFrame CPU 工作线程;应用位移/倍率并更新动态法线;不操作 Qt、不创建 GL handle
SceneComposer.rest_frame(assets, identity) 场景与身份 RenderFrame 清除旧颜色/位移;用于刚加载、缺数据或断连后的静态参考

surface 原始坐标、动态位置和 scalar 分开持有。颜色映射建议在 shader 中用 scalar + LUT + 固定范围完成,避免每帧在 Python 分配大量 RGB 数组;适配器不得每帧遍历全部 Blender 面来建映射。

8.3 帧调度与状态

类/函数 输入 输出 线程与行为
LatestFrameMailbox.offer(frame) SurfaceFieldFrame OfferResult 采集线程可调用;容量 1,替换旧帧并计数,锁内只换引用
LatestFrameMailbox.take() 无 frame 或 None CPU 合成线程;取出并清空;没有帧不忙轮询
ThreeDCoordinator.activate(assets, source_id) 场景与数据源 generation:int GUI;取消旧请求、清空两个 mailbox、建立源身份
ThreeDCoordinator.submit(frame) 帧 OfferResult 任意生产线程;快速校验身份,错误/过期拒绝;不直接触发 paint
ThreeDCoordinator.tick() 无 None GUI 定时器;收最新 RenderFrame,调用 widget.present(),按上限请求 update
ThreeDCoordinator.set_visible(bool) 可见性 None GUI;隐藏时取消三维合成/上传/渲染任务和订阅;不停止采集/记录
ThreeDCoordinator.set_settings(settings) 显示设置 None GUI;色标/相机可独立刷新,位移倍率变化需重合成当前字段
ThreeDCoordinator.close() 无 None GUI;取消任务,停止定时器,清 mailbox,断开信号;幂等

状态机:Disabled → Loading → Ready → Live;缺字段为 NoData,资产/GL 失败为 Unavailable。隐藏为 Suspended,恢复时用最新帧;断连时清旧动态帧并显示参考模型/“无实时数据”。过期帧、旧 generation 和旧 request_id 不得重新覆盖新场景。

两个 mailbox 各容量 1,加一个正在计算的帧;Qt 信号只通知状态,不排队传递每帧大数组。CPU 任务不得无限提交给线程池。完整数据记录继续走原有通道。

标准失败语义:

code 触发 调用方处理
asset_auth_failed 签名/认证标签错误 拒绝资产,进入 Unavailable;不尝试明文回退
geometry_mismatch 场景、绑定、设备几何不一致 拒绝帧/资产,显示预期与实际 ID
field_domain_mismatch H/W、scale、flip 或域不同 拒绝帧;不能自动 resize 后继续冒充原映射
field_unavailable 当前帧缺 scalar/有效数据 NoData,清除旧测量颜色;允许参考模型
stale_frame generation/request_id 过期 安静丢弃并计数,不弹窗打断操作
gl_unsupported 实际上下文达不到所选 profile Unavailable,二维照常;附 probe 信息
shader_compile_failed shader 编译/链接失败 Unavailable,日志保留 shader 版本和错误信息
gpu_budget_exceeded 分配超过配置预算 显式选择更低 LOD 或拒绝三维,不无限重试

参考帧 rest_frame() 的 scalar 全部不可用,descriptor 使用 quantity="unavailable"、unit="1"、quality="display_only",UI 隐藏测量色条。渲染器不得因为它与先前色标不匹配而留下旧颜色。

8.4 OpenGL 与 Qt

类/函数 输入 输出 限制
configure_surface_format(profile) desktop33 / gles30 None QApplication 创建之前;只选一个已验证平台配置
GLDispatch.bind_current_context() 已经 current 的 Qt context GLCapabilities 选择和实际 GL/ES 匹配的函数绑定;不自行创建第二套 EGL 窗口
GLRenderer.initialize(capabilities) 实际能力 None GUI/current context;编译对应 shader,失败抛 shader diagnostic
GLRenderer.upload_scene(assets) 验证后的资产 None 静态 VBO/EBO/LUT 一次分配;建立预算
GLRenderer.update_surface(render_frame) 同资产动态帧 None 更新既有动态缓冲;不重建 housing/索引
GLRenderer.draw(camera, settings, framebuffer, viewport) 相机矩阵、设置、Qt FBO、物理像素尺寸 RenderStats 一次场景绘制,不读磁盘、不调用 SDK、不处理 Qt 事件循环
GLRenderer.release() 无 None 必须正确 context current;幂等;GPU 资源清零
Sensor3DWidget.initializeGL() Qt 调用 None 创建 backend,连接 context.aboutToBeDestroyed;失败进入 Unavailable
Sensor3DWidget.paintGL() Qt 调用 None 验证最新帧身份,上传并绘制;无新帧只在相机/设置变化时重画
Sensor3DWidget.resizeGL(w,h) Qt 尺寸 None 校准 DPR 与实际 framebuffer 尺寸,更新投影;不缓存旧 FBO
Sensor3DWidget.present(frame) RenderFrame None GUI,仅保留 CPU 帧并调用 update;不要求调用点已 current
Sensor3DWidget.release_resources() 无 None makeCurrent → release → doneCurrent;销毁/重建/关闭均能安全重复

Qt 的 widget 使用自身 FBO,不能把默认目标绑回 0;Qt 生命周期入口已经提供 current context,其他入口需显式管理。重新挂接窗口可能导致 context 重建,要从 CPU 资产恢复 GPU 资源。清理绑定 aboutToBeDestroyed,同时在关闭时显式执行。Qt 5.15 QOpenGLWidget

shader 维护两个头部变体:桌面 #version 330 core;ES #version 300 es,显式 float/int precision。共享计算逻辑,禁止 glBegin/glEnd、固定管线矩阵、GL_QUADS、GLU/GLUT。第一版不依赖 geometry shader、compute shader、桌面专用 polygon mode。

buffer 使用连续 float32 和合法的 uint32 索引;draw call 按 part 合批。若最终必须支持 GLES2,作为独立降级项目处理着色器、VAO 和 16 位索引限制,不能以把版本号改成 2.0 作为完成。

8.5 相机和 UI 接口

OrbitCamera 方法 输入 输出/行为
fit(bounds, aspect) 场景包围盒、宽高比 None;设置 center/distance/near/far,使完整模型可见
orbit(delta_px, viewport) 鼠标二维增量、视口 None;围绕 target 旋转,不旋转模型数据
pan(delta_px, viewport) 鼠标增量、视口 None;按距离/投影换算世界平移
zoom(wheel_steps) 滚轮步长 None;指数缩放并限制近远范围,避免穿模
set_preset(preset) CameraPreset None;前/侧/顶/等轴测/默认
matrices(viewport) 物理视口尺寸 CameraMatrices(view, projection)

建议交互:左键旋转,Shift+左键或中键平移,滚轮缩放,复位按钮回资产默认视角。手势保存在 panel/widget,具体轨迹计算在 OrbitCamera。相机、外壳显隐、倍率与色标可存 UI preferences,不改变 SDK 参数。

Panel 控件:三维开关、预设视角、复位、外壳显隐、显示形变开关与倍率、色标范围/自动范围、量名称/单位/quality、帧号/数据不可用提示。默认不把算法相机内参暴露成用户的观察相机控件。

9. 现有文件怎样接入

文件 类/函数入口 最小修改
apps/studio/src/orisys_studio/main.py main() 在 QApplication 前选择 GL profile;避免 app_window 的导入链提前加载错误 PyOpenGL platform
shared/.../acquisition/models.py SdkPacket 可选 surface_frame 或显示订阅 DTO;向后兼容默认 None,模型不 import Qt/OpenGL
shared/.../acquisition/finger_worker.py 实际采集循环的组包位置 仅订阅时,在 SDK 已计算完且 runtime 线程拥有数据时调用 adapter;发布带 UV 域身份的只读快照
shared/.../acquisition/sdk_worker.py 平面组包位置 输出已经确认语义的 scalar/有效遮罩;无位移不伪造
apps/studio/src/orisys_studio/app_window.py StudioMainWindow.__init__()、_on_run_tick()、_render_run_image()、closeEvent() 只装配 panel/coordinator、切换视图与关闭;不写 GL 调用、资产解密或顶点映射
apps/studio/src/orisys_studio/replay.py 打开录像、set_position()、关闭 对三维能力显式返回 unavailable;只有独立重算/保存的场数据源才发布 SurfaceFieldFrame
apps/studio/pyproject.toml 或项目依赖入口 optional dependencies 新增 three-d 安装项及每平台锁定文件;Open3D 不进入默认 Studio 依赖
tools/ 与 CI 新 probe/export/validate 测试 纯 CPU 合同测试与真窗口 GL 冒烟分开

当前工作区正在改动采集滤波,工程师接入前应按函数职责定位,不依据本提案的旧行号覆盖文件。不得从 GUI 线程直接读取 worker.sensor 或修改它的图形状态。

10. 实时、回放与记录

实时:感知图/三维/力曲线共享 FrameIdentity;显示可降帧,记录通道保持既有语义。新 scalar 或 displacement 的计算不得为三维单独再滤一次,导致与二维不一致。

普通现有记录仅有视频和力时:显示静态参考模型并提示“三维场未记录”。完整重算必须通过独立数据源,携带 SDK/算法配置/几何/标定版本与恢复质量;不能自动重启已经删除的旧深度重算类。

未来如增加可追溯三维记录,建议另存分块 SurfaceFieldFrame 数据与 frame_id 索引,记录 quantity/unit/quality/domain_id/geometry_id,以及场是否 calibrated;不要仅保存着色后的网格或 UI 屏幕。session.json 增加可选 visualization3d 能力/资产版本块,旧文件缺此块视为不支持。此记录扩展不是第一版实时渲染的前置条件。

回放拖动时递增 request_id,CPU 合成结束后再次核对 request_id;无效/缺帧必须清理上次动态结果,不能让光标已前进但三维还冒充当前帧。

11. ARM 与安装/打包规范

11.1 依赖

  • 继续使用项目验证过的 Python、PySide6、NumPy;新增 PyOpenGL。先建立兼容版本范围,PoC 通过后分别锁定 Windows/Jetson/QCS6490 的版本与 wheel 哈希,不在文档虚构已验证版本。 Qt 绑定选 PySide6(LGPLv3)而不是 PyQt5(GPLv3-or-commercial):两个 app 都只允许 PySide6,由 apps/studio/tests/test_studio_binding_purity.py 与 apps/autocal/tests/test_binding_purity.py 静态强制;构建环境里出现第二个 binding 会让 pyqtgraph 绑错(setup.ps1 会卸载 PyQt5/PyQt6/PySide2,main.py 在 import pyqtgraph 之前设 PYQTGRAPH_QT_LIB=PySide6)。
  • PyOpenGL_accelerate 是可选优化,不作为首轮 ARM 安装前提;官方也将它列为可选项。PyOpenGL 安装
  • 加密发布模式另引入成熟密码库;Blender、GLB 转换工具只在资产构建环境,客户运行时不依赖 Blender。
  • PyOpenGL 不自带 GPU 驱动。不要为了满足依赖让 pip/系统包覆盖板卡 BSP 指定的 EGL/GLES 库。

拟定安装入口(工程师实现 extras 后才可执行):

python -m pip install -e ./apps/studio[three-d]
python tools/scene_assets/probe_gl.py --profile desktop33 --output gl-report.json
python tools/scene_assets/probe_gl.py --profile gles30 --output gl-report.json

两种 profile 是不同启动配置,先按实际平台选其一;不要求同一进程依次更换 Qt/PyOpenGL 平台。PyOpenGL 的 platform 选择必须在第一次 import OpenGL 之前完成,并匹配 Qt 实际使用的 GLX/EGL;不能对所有 Linux 机器一律强设 EGL。探测程序必须建立实际 Qt widget,输出取得的 context,而非只查询系统库是否存在。

11.2 待补验收矩阵

平台 首轮验证路径 必须记录
开发机 Windows desktop33 GPU/驱动、Qt/Python/PyOpenGL、实际 GL/GLSL、DPR
Jetson Orin Nano / Ubuntu BSP 支持的 desktop GL;需要时另验证 ES 板卡内存、JetPack/BSP、桌面会话、功耗模式、驱动/renderer、Qt 实际 backend
QCS6490 / Ubuntu BSP 支持的 EGL/GLES3 路径优先验证 模组厂商/BSP、Ubuntu、Wayland/X11、EGL/GLES、Qt 构建、驱动能力

不根据 SoC 名称猜桌面图形支持。QCS6490 与 Jetson 最终 profile 由探测报告决定;若驱动/绑定不满足,保持二维功能并显示具体原因。不得把 llvmpipe 等软件渲染误报为目标 GPU 验收通过。

12. 性能预算与验收用例

建议起始目标,不是目前测量结果:动态感知面约 20k 顶点/40k 三角形,外壳约 10k–20k 三角形;实际 LOD 由曲面误差和上板性能共同决定。将 full UV 网格保留为参考质量模式。

建议 30 FPS 显示上限;低配可明确降为 15 FPS,采集和记录速率不能因此下降。以 20k 顶点、position/normal/scalar/valid 约 32 字节计算,单份动态数组约 0.64 MB;此估算不含多份 CPU/GPU 缓冲、输入 UV 场、纹理和驱动开销。

测试 通过标准
原点/轴/尺度 三个非共线已知点,表面与外壳对齐;法线方向、绕序、米/毫米换算正确
资产身份 1005/1032 拓扑混用、UV 尺寸不同、flip_v 不同、错误哈希均被拒绝
采样 已知常量/斜坡/单点脉冲/NaN/遮罩场与 CPU 参考一致;接缝无跨域插值
几何分层 改 scalar/gain 只更新 sensing;housing 顶点和 GPU 静态资源不变
测量含义 approximate 不显示成已标定位移;增大 gain 不改变原始 scalar 和力值
生命周期 隐藏/显示、反复 resize、切换布局、断连、源切换、关闭以及上下文重建无崩溃/泄漏
过期帧 旧 source/generation/request_id 永远不能覆盖新场景;无数据不显示旧测量
渲染限制 shader 故障/GL 不足时降到二维并给诊断;无软件 renderer 冒充 GPU
记录隔离 同样输入下,开/关三维的 CSV 行序和有效样本相同;三维超时不阻塞记录
性能 上板统计 p50/p95 合成、上传提交、绘制、总响应延迟,报告分辨率/网格/功耗配置;无无限任务增长
长时 建议 2 小时采集+记录+三维后,CPU/GPU 内存不持续增长;报告最大 RSS 和队列上界
包保护 改 manifest/密文/条目哈希被拒绝;无明文临时资产;客户包无签名私钥/内部源模型

CPU 合同测试在无 GUI 环境运行;GL 测试必须使用可见/实际窗口系统的上下文,offscreen UI 测试不能代替 ARM GPU 验证。

13. 实施拆分与交付物

  1. 平台 PoC:probe_gl、三角形、静态感知网格+外壳、resize/关闭测试;两块 ARM 至少拿到能力报告,再冻结 GLDispatch/profile。
  2. 资产合同:旧 NPZ 导入、统一坐标、roles、manifest、绑定验证和一套简化外壳;不改 SDK 算法网格。
  3. 实时数据:FrameAdapter、SurfaceSampler、SceneComposer、最新帧调度;先 scalar 着色,再加明确标记的示意位移。
  4. Qt 产品接入:Sensor3DWidget、相机/色标/倍率/显隐、异常与生命周期、现有记录隔离测试。
  5. 发布保护与性能:精简/签名包、可选加密与 KeyProvider、平台锁定、性能/长时/故障测试。

每项交付包括:实现、公共接口单测、输入样例、失败样例、兼容说明、平台诊断报告。先评审本文的坐标/场语义/资产合同,再开始组件并行开发;不要等画面完成后才决定数组每一维和单位的含义。