Files
orisys-sensor-design/docs/geometry-measurements.md
T
ykcheungab e7618faed4 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.
2026-09-30 16:29:39 +08:00

8.8 KiB
Raw Blame History

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.