37 KiB
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 存储布局决定坐标含义。
转换分成三种:
- 算法相机:原始
K、畸变、world_to_cam、裁剪/缩放/flip_v,服务像素到曲面的映射,保留原有精度和版本。 - 资产坐标:Blender/glTF/旧 NPZ 到 sensor 的转换。离线发布时一次性烘焙到网格、法线和映射;运行时不再重复应用同一转换。
- 观察相机:用户 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 明确保护目标
用户确认目标为提高逆向和复制成本,不是保证客户端无法提取模型。
建议依次落实:
- 最小交付:仅发布简化外壳与需要的感知几何;去掉内部机械结构、隐藏对象、原始命名、无用相机、设计历史和高精度源资产。
- 完整性和来源:构建机签名发布包,客户端只带验签公钥;manifest 的版本、资源角色、大小、哈希和兼容信息都纳入签名。SHA-256 只能检查内容,不等同于防篡改签名。
- 可选加密:使用成熟库的认证加密,例如 AES-GCM;同一密钥下每次包加密的 nonce 必须唯一,元数据作为关联数据绑定。不要自制 XOR、把密码藏在扩展名或把 ZIP 当加密。认证加密接口及 nonce 约束
- 密钥隔离:签名私钥不能进入客户软件;内容密钥经 KeyProvider 获取,可按产品/客户授权。若需要离线,明确离线授权/缓存失效流程。Python 内硬编码全局密钥只能提供很弱的阻力;原生加载器、设备密钥保护可提高成本但不能防运行时捕获。
- 解密范围:尽量内存解密、不输出明文临时文件、日志不打印密钥;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_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. 实施拆分与交付物
- 平台 PoC:probe_gl、三角形、静态感知网格+外壳、resize/关闭测试;两块 ARM 至少拿到能力报告,再冻结 GLDispatch/profile。
- 资产合同:旧 NPZ 导入、统一坐标、roles、manifest、绑定验证和一套简化外壳;不改 SDK 算法网格。
- 实时数据:FrameAdapter、SurfaceSampler、SceneComposer、最新帧调度;先 scalar 着色,再加明确标记的示意位移。
- Qt 产品接入:Sensor3DWidget、相机/色标/倍率/显隐、异常与生命周期、现有记录隔离测试。
- 发布保护与性能:精简/签名包、可选加密与 KeyProvider、平台锁定、性能/长时/故障测试。
每项交付包括:实现、公共接口单测、输入样例、失败样例、兼容说明、平台诊断报告。先评审本文的坐标/场语义/资产合同,再开始组件并行开发;不要等画面完成后才决定数组每一维和单位的含义。