docs: give the multi-view design and geometry facts one home each

Two workspace documents described the multi-view ownership decision by
overlapping each other and had both gone stale: neither matched the shipped
code, and the architecture document was referenced by nothing at all. They are
archived in the workspace, and this repo now carries the parts that belong to
it.

New: docs/geometry-measurements.md. Measured properties of the r01 sensing
surface, the sensing camera, the UV layouts, the printed cell pitch and the
runtime unroll domain - topology (1032 v / 1974 t), 355.77 mm2 area with 38.1%
near-vertical wall, the off-origin centroid, 94.5% inward-facing normals, the
0.516 x 0.714 mm measured pitch against a wrong 0.205 x 0.3556 mm nominal cell,
75.6% runtime coverage, and incidence over that region (p50 55.6 deg, invariant
to adding views). Merge-mode agreement is included, with an explicit note that
these values are camera-specific and expire when a camera moves - the Xynova
surface already needs 70 deg where r01 ships 62 deg.

Wiring:
- README.md points at the measurements and says they expire.
- design-parameters.md section 4 keeps the key semantics and now links to the
  workspace record for rationale and to the measurements for data, instead of
  restating either.
- design.yaml: replace a self-referential comment pointing back at the same
  document with the keys location and the rationale location.

No behaviour change: the YAML edit is a comment, and no source file is touched.
This commit is contained in:
ykcheungab
2026-09-30 16:29:39 +08:00
parent 1de4431657
commit e7618faed4
4 changed files with 160 additions and 1 deletions
+5
View File
@@ -8,6 +8,11 @@ For the full long-term workflow see `docs/workflow/sensor-design-workflow.md` in
workspace root (`D:\orisys_ws\docs\workflow\sensor-design-workflow.md`).
This repository intentionally implements a **minimal v1** for a small team.
Measured properties of the geometry and cameras — topology, surface area, normals, incidence,
cell pitch, and the runtime unroll domain — are in
[`docs/geometry-measurements.md`](docs/geometry-measurements.md). The multi-view ownership design
and the optics model both depend on them, and they expire when a camera moves.
## Two buckets (daily work)
| Bucket | Where | Contents |
+5
View File
@@ -198,6 +198,11 @@ raster pixels = px_per_mm × physical millimetres
## 4. `texture_patch_views`:多个投影视图和区域分工
本节说明**每个键的语义**。为什么这样设计——为什么所有权在生成之前决定、为什么单位是三角形、
以及两种 seam 模式各自的适用条件——见
`D:\orisys_ws\docs\workflow\multiview-ownership-decision.md`。测量数据(拓扑、相机、入射角、
cell pitch)见 [`geometry-measurements.md`](geometry-measurements.md)。
这个流程较容易混淆,因为它同时使用三类信息。
### 4.1 三种角色
+149
View File
@@ -0,0 +1,149 @@
# Measured geometry, camera and runtime facts (`sensors/finger/r01`)
Reference measurements of the finger `r01` sensing surface, the sensing camera, and the runtime
unroll domain. These are properties of the physical part, not of any one design, so they live
here rather than in a decision record and are reused by the multi-view ownership work, the probe
bench and the optics model.
Every row states how it was obtained. Anything not re-derivable is marked as such.
## 1. Method
Read-only, never saved. Blender 5.0.1, invoked as `blender -b -P <script>` against
`sensors/finger/r01/source/geometry.blend`. The probe scripts lived in
`orisys-sensor-design/.scratch/multiview-arch/`, which is scratch and has been removed — so the
numbers below are **not re-derivable by re-running a stored script**. Anything you need to trust
today must be re-measured from the mesh, from the runtime asset named in each row, or from the
recorded blend hash.
Recorded blend hash for the 2026-09-27 measurement set: `61093B1A…`.
## 2. Sensing surface
| Fact | Value | Note |
|---|---|---|
| Topology | 1032 verts / 1974 polys, **all triangles**, 5922 loops, no modifiers | The mesh is pre-triangulated, which is what makes the loop-triangle index a valid stable key |
| Dimensions | 15.81 × 15.0 × 7.5 mm bbox | |
| Surface area | **355.77 mm²** | 1.5× the bbox footprint |
| Area distribution | **38.1 % near-vertical wall, 0 % flat top** | The cap has a 15 mm-deep circular end, so almost half the surface faces sideways |
| Position | x ∈ [−15.81, −1.77] mm; area-weighted centroid ≈ **(−8.13, 0.00, 3.14) mm** | **The surface is not centred on the origin.** Any view aimed at the origin misses it |
| Face normals | **94.5 % inward-facing** (`normal.z < 0`), `normal.z ∈ [−0.9999, +0.6755]` | Winding is not trustworthy on this mesh; see §5 |
`design.yaml` declares `expected.sensing_triangles: 1974`, but **nothing validates it** — the
value is a comment, not a gate.
## 3. UV layouts
| Fact | Value |
|---|---|
| Maps | exactly two: `produce` and `compute`, 5922 loops each; `compute` is active |
| `compute` extent | u ∈ [0.00009, 1.0], v ∈ [0.1425, 0.85742] — the empty v-band is the shipped map's domain shape, not a defect |
| Texel density uniformity | per-polygon (UV area / world area): `compute` min 2.5e-4, p05 1.1e-3, **median 1.391e-3**, p95 1.872e-3, max 4.061e-3 → **spread 2.92×** |
| `produce` density | min **0.0** — **degenerate zero-area UV faces exist**, median 2.131e-3, max 4.216e-3 |
The unwrap's non-uniformity across the surface (2.92×) is **larger** than the axis-aligned
anisotropy `docs/xynova-texture-investigation.md` measured (1.17×) — that earlier figure was taken
at the centre point, which is the good case.
`compute` is the **runtime contract**: the shipped `unroll_map_uv_s0.4.npz` was built from this
mesh and this UV layout, so changing `compute` invalidates that asset and is a release event, not
a build side effect.
## 4. Sensing camera (`cam_sensor`)
| Fact | Value | Note |
|---|---|---|
| Type and lens | PERSP, effective lens 3.5 mm | |
| Sensor width | `sensor_width` 1.794 × object scale **6.009** = 10.78 mm | The 6.009 object scale must be divided out by anything that derives a view from this camera, or the pitch is wrong by 6× |
| Position | origin 4.62 mm above the surface | |
| H-FOV | ≈ **114°** | Extremely wide and very close: foreshortening is unavoidable |
| Coverage | 1887/1974 triangles inside the frustum (**95.6 %**), **0 occluded**, **0 triangles beyond 75° incidence** | The camera is not edge-on-limited |
| Incidence over the surface | p50 **42.1°** | Intrinsic to a 355.8 mm² cap with 38 % near-vertical wall |
## 5. Printed cell pitch (measured on the surface)
| Fact | Value | Note |
|---|---|---|
| Adjacent camera-cell pitch | median **0.516 mm horizontal × 0.714 mm vertical**; horizontal spread 0.449–0.568 mm | The cell pitch is *finer vertically than horizontally on the surface* |
| `design.yaml` nominal cell | `vector.width_mm/nx = 0.205`, `vector.height_mm/ny = 0.3556` | **Wrong as a physical cell size.** The vector page (16.4 × 16.0 mm) was sized to the bbox, not to the camera's 80×45 grid |
| Consumers of the wrong value | `cli.py:521` (`cmd_inspect`'s `nominal_cell_mm`), `cli.py:751` (`pitch_mm = vector_width_mm / nx`) | `pitch_mm` feeds `sample_stack`, and therefore the thickness/contact numbers in the optics model |
| Grid vs surface | 80 cells × 0.516 mm = 41 mm of surface pitch across a 14.0 mm-wide surface | A cell spans ~13 surface fragments; the patch colour table is indexed by camera cell, so colour varies much faster than the cell pitch suggests |
| Page scale | `3280/3200 = 1.025 = 16.4/16.0`, exactly 200 px/mm both axes | Any change to `nx`/`ny` or the page must preserve this, or `_warn_if_page_aspect_differs` fires and `analyse_map` returns `inconclusive` |
The optics model currently derives the printed cell from the page arithmetic above. Replacing it
with the measured 0.516 mm changes the reported thickness/contact numbers, so the two must move
together.
## 6. Runtime unroll domain
From `orisys-sdk/sensors/finger/assets/30fps-0703/unroll_map_uv_s0.4.npz`:
| Fact | Value |
|---|---|
| Grid | 280 × 300 |
| Valid ratio | 0.408 — **34270 valid pixels** |
| Keys | includes `world_xyz`, `projection_K`, `source_scale`, `edge_band`, `edge_expand`, `method` |
| Surface coverage | ≈ **268.8 mm² = 75.6 %** of the 355.77 mm² surface (nearest-centroid association, max 1.0 mm error) |
**Incidence over the runtime region** (the part that actually matters, not the whole surface):
| Measure | Front ortho | Best of 2 views |
|---|---|---|
| p50 | **55.6°** | **55.6°** |
| p90 | 89.5° | 83.2° |
| > 60° | 44.97 % | — |
| > 75° | 30.4 % | — |
Two findings govern the whole multi-view design:
1. **Adding views improves the tail, not the median.** p90 moves; p50 does not move at all.
2. **Azimuthal rotation about the cap axis is a measurable no-op** — identical statistics at
az = 0/±26/±45°. The cap is a surface of revolution, so rotating about its axis changes
nothing.
## 7. Merge-mode agreement (2026-09-27, blend sha256 `61093B1A…`)
Measured on this revision, and the reason `merge.incidence_limit_deg: 62.0` rather than 60:
| Quantity | Value |
|---|---|
| Authored seam | the **y = 0 plane** |
| `authored` vs `computed` agreement | within **0.0014** of coverage at a 60° incidence limit; **identical from 62° up** |
| Hole fraction at 60° | **0.0218** — fails the `max_hole_fraction: 0.02` gate |
| Hole fraction at 62° | **0.0000** |
So neither mode is a compromise at this revision, and the 62° default is a measured threshold,
not a preference.
> **This window is camera-specific and expires when a camera moves.** The y = 0 coincidence holds
> because the two region cameras are mirrored about XZ at azimuth 308.26° / 231.74°, elevation
> −24.46°, distance 32.62 mm. `incidence_limit_deg` describes a *camera-pose and surface*
> combination, not a property of the sensor model, so moving a region camera invalidates it and
> the scan must be re-run.
>
> It has already happened once: on the Xynova surface, **62° produces 4.8 % hole**, which is why
> that revision sets `incidence_limit_deg: 70.0` explicitly and the YAML carries a warning not to
> delete it. The in-code defaults (`texture_views.MERGE_DEFAULTS` and `texture_merge.py`) stay at
> 62.0 with a test requiring the two to agree, so removing the override silently restores a value
> that is wrong for that surface.
>
> A previous revision of `design.yaml` also recorded "hole 0.100 mm² / fraction 0.0002 at 62°" and
> "4960/5007 triangles owned, 20454 + 21483 fragments". Those figures were valid before the
> cameras moved and are **retracted**; they are recorded here only to show why camera-dependent
> numbers must state their re-measurement trigger.
**A trap worth keeping:** assigning a component by *count of visible triangles* does not work
here — both views see ≈100 % of each component's triangles, so the count ties and a naive rule
silently hands both components to the first view. Component assignment uses **qualifying
footprint**, which folds in both the incidence limit and the footprint floor.
## 8. What these numbers imply for any view set
- The sensing surface is offset from the origin, so a frame centred on the origin misses it;
derived frames must be built from the surface's own extent.
- The incidence p50 over the runtime region is intrinsic to the cap's shape. No finite view set
removes it; it can only shorten the tail.
- The dominant print-distortion term is the UV unwrap's density spread (2.92×), not projection.
- 94.5 % inward normals means anything that trusts winding — a back-face cull, a renderer, a
"which side is the surface" heuristic — will get this mesh backwards. Either fix the winding in
the authored blend, or keep taking `abs()` and record why.
+1 -1
View File
@@ -62,7 +62,7 @@ texture_patch:
# and now serve as each pair's REGION camera, deciding what that pair may own.
# Ownership closes the overlap: both layouts are full-surface, so without a partition printing
# both would double-ink essentially the whole part.
# See docs/design-parameters.md part 2.
# Keys: docs/design-parameters.md section 4. Why: D:\orisys_ws\docs\workflow\multiview-ownership-decision.md
texture_patch_views:
enabled: true
pattern_camera: cam_sensor # the ink source; a pair's `camera` only decides ownership