update 3d scafold
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
# Studio 三维显示架构与实施规范
|
||||
|
||||
版本:1.0 提案;日期:2026-09-14。目标读者:Studio、SDK、Blender 资产、ARM 平台和测试工程师。
|
||||
|
||||
本文的“必须”是接口正确性要求,“建议”是第一版实现选择,“待验证”不是已获硬件验收。本文配套 `studio-3d-contracts.pyi` 是接口骨架,不是可运行的三维功能。
|
||||
|
||||
## 1. 决策与实施范围
|
||||
|
||||
**推荐现有 PyQt5 的 QOpenGLWidget + PyOpenGL,使用现代着色器和 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 内替换绑定;需验证实际 PyQt5 暴露的接口 |
|
||||
| 更换完整场景引擎/Qt 大版本 | 重型场景、复杂材质、动画 | 第一版需求不足以抵消迁移成本,暂不做 |
|
||||
|
||||
Open3D 官方将 ARM64 支持标为实验性;其支持矩阵区分 Linux 桌面 OpenGL 与 OpenGL ES,后者不支持其 Visualizer/GUI。新渲染模块的文档另要求 OpenGL 4.1。不能把 Jetson 上的运行结果等同于 QCS6490,也不能把 Python 包可安装当作 GUI 可用。[Open3D ARM 支持](https://www.open3d.org/docs/release/arm.html)、[渲染要求](https://www.open3d.org/docs/release/tutorial/visualization/cpu_rendering.html)
|
||||
|
||||
第一版包括:静态完整外观、动态感知表面、旋转/缩放/平移/复位/预设视角、固定或手动色标、图例、显示倍率、外壳显隐、性能与不可用提示。透明外壳、阴影、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 坐标与变换规范](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#coordinate-system-and-units)
|
||||
|
||||
### 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 约束](https://cryptography.io/en/latest/hazmat/primitives/aead/#cryptography.hazmat.primitives.ciphers.aead.AESGCM)
|
||||
4. **密钥隔离**:签名私钥不能进入客户软件;内容密钥经 KeyProvider 获取,可按产品/客户授权。若需要离线,明确离线授权/缓存失效流程。Python 内硬编码全局密钥只能提供很弱的阻力;原生加载器、设备密钥保护可提高成本但不能防运行时捕获。
|
||||
5. **解密范围**:尽量内存解密、不输出明文临时文件、日志不打印密钥;Python/驱动内存副本不能承诺被完全擦除。
|
||||
|
||||
即使加密,渲染最终仍需在 CPU/GPU 获得几何。若某结构绝不能泄露,就不要交付该结构,改成视觉代理模型。包格式和加密不能代替这一决策。
|
||||
|
||||
### 5.3 `.oryscene` v1 逻辑内容
|
||||
|
||||
建议封装采用有大小上限的 manifest 与数组条目,不执行脚本、不使用 pickle。压缩是容量优化,不属于安全机制。读取器拒绝路径穿越、未知必须扩展、异常尺寸、越界索引、非有限坐标及解压超限。
|
||||
|
||||
```text
|
||||
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。下面仅为字段关系示例,尖括号值由编译器生成,不能作为有效发布包:
|
||||
|
||||
```json
|
||||
{
|
||||
"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. 模块边界与文件骨架
|
||||
|
||||
```text
|
||||
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。
|
||||
|
||||
依赖方向:
|
||||
|
||||
```mermaid
|
||||
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](https://doc.qt.io/qt-5/qopenglwidget.html)
|
||||
|
||||
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、PyQt5、NumPy;新增 PyOpenGL。先建立兼容版本范围,PoC 通过后分别锁定 Windows/Jetson/QCS6490 的版本与 wheel 哈希,不在文档虚构已验证版本。
|
||||
- PyOpenGL_accelerate 是可选优化,不作为首轮 ARM 安装前提;官方也将它列为可选项。[PyOpenGL 安装](https://pyopengl.sourceforge.net/documentation/installation.html)
|
||||
- 加密发布模式另引入成熟密码库;Blender、GLB 转换工具只在资产构建环境,客户运行时不依赖 Blender。
|
||||
- PyOpenGL 不自带 GPU 驱动。不要为了满足依赖让 pip/系统包覆盖板卡 BSP 指定的 EGL/GLES 库。
|
||||
|
||||
拟定安装入口(工程师实现 extras 后才可执行):
|
||||
|
||||
```text
|
||||
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、平台锁定、性能/长时/故障测试。
|
||||
|
||||
每项交付包括:实现、公共接口单测、输入样例、失败样例、兼容说明、平台诊断报告。先评审本文的坐标/场语义/资产合同,再开始组件并行开发;不要等画面完成后才决定数组每一维和单位的含义。
|
||||
Reference in New Issue
Block a user