116 lines
6.3 KiB
Markdown
116 lines
6.3 KiB
Markdown
# Finger calibration-point debugger — findings so far
|
||
|
||
Status: subsystem 1 (UV picking) works and is measured. Subsystem 2 (simulated
|
||
5-axis pose) works and is measured. This note records the numbers, and the two
|
||
solver bugs that produced confident wrong ones on the way here.
|
||
|
||
## What is established
|
||
|
||
### The export bundle (`finger/r01`, `compute` profile)
|
||
|
||
| Quantity | Value |
|
||
|---|---|
|
||
| Mesh | 1032 vertices, 1974 triangles (matches `profiles/compute.yaml`) |
|
||
| UV atlas | **one connected island**, u 0→1, v 0.142→0.857 |
|
||
| Pose | `world_to_cam`, uniform scale `0.018388452316735537`, `det(R) = +1` |
|
||
| Physical size | sensing **15.0 × 7.5 × 15.81 mm**; housing 16.0 × 9.5 × 21.0 mm |
|
||
| Surface area | 355.77 mm² |
|
||
| Origin | housing base at `y ≈ 0`; sensing spans `y ≈ 0.99 … 8.49 mm` |
|
||
| Curvature | ~12% of area within 15° of one axis; area-weighted normal ≈ `(0, 0.57, 0.26)` |
|
||
|
||
**The mesh is not in millimetres.** `blender/scripts/export.py` transforms
|
||
vertices by `t_sensor_from_world @ world`, and the pose's linear block carries the
|
||
`origin_sensor` empty's own uniform scale — 1/54.4 on `finger/r01`. Physical size
|
||
is *mesh units × that factor*, so the factor is derived from the file (and
|
||
refused if it is not a uniform rotation) rather than assumed. A consumer that
|
||
treats the vertices as millimetres is wrong by 54×.
|
||
|
||
### UV picking
|
||
|
||
`rasterise()` builds, in one pass, a flat image of the UV layout **and** a
|
||
`pixel → (position, normal, face)` table, at 1024×1024 in 0.24 s. Measured:
|
||
|
||
- valid (surface-bearing) pixels: 72.9%
|
||
- round trip `vertex → its UV pixel → looked-up 3D`: worst error **0.0081 mm**
|
||
- a pixel in a UV-island gap returns `None` rather than a fabricated `(0,0,0)`
|
||
|
||
Because the atlas is a single island, a click maps to exactly one 3D point.
|
||
|
||
## The supposed finding, and its retraction
|
||
|
||
An earlier revision of this note reported that the **yaw +-90 deg limit made a
|
||
quarter of the patch unalignable**, worst residual 65.1 deg, only 47 of 61
|
||
sampled points reachable. **That was a solver bug, not a property of the
|
||
machine.**
|
||
|
||
A later revision then reported the residual as 0.21 deg worst with the yaw limit
|
||
"not the constraint". **That was also wrong**, in the opposite direction: the
|
||
limit itself was wrong. The bench confirmed the yaw stage turns a **full half-turn
|
||
either way (+-180 deg)**, not +-90.
|
||
|
||
With the corrected envelope, solved by closed form
|
||
(see `inverse-kinematics.md`):
|
||
|
||
| Envelope | Worst residual | Within 0.5 deg | In range |
|
||
|---|---|---|---|
|
||
| pitch 360 deg, **yaw +-180 deg** (the machine) | **1.2e-06 deg** | 100% | **1032 / 1032** |
|
||
|
||
Every vertex is reachable, and the residual is at the floating-point floor rather
|
||
than at any clamp. The "0.21 deg worst, genuinely clamped vertex" claim above was
|
||
an artefact of the wrong limit and no longer holds.
|
||
|
||
**No mounting change is required for reachability.** The three options the earlier
|
||
note listed (re-mount, state a tolerance, index the plate) are all unnecessary on
|
||
the reachability evidence. Whether they help *repeatability* — a shorter pitch
|
||
move, a better-conditioned contact — is a separate bench question, not a
|
||
kinematic one.
|
||
|
||
One thing the corrected solve did surface: **51 of 1032 vertices end inverted**,
|
||
whichever closed-form branch is chosen, because the tool direction is
|
||
antiparallel to the sensor's own up axis. That is the tool-polarity question, it
|
||
needs the bench, and it is *not* visible in the residual. See
|
||
`inverse-kinematics.md` → *Still open*.
|
||
|
||
## What was wrong before, and why it is worth recording
|
||
|
||
**Four** solver versions preceded this one, each wrong in a way that produced
|
||
confident numbers:
|
||
|
||
- rotating the **sensor-frame** normal while the stages act in the platform frame
|
||
(uniform ~90 deg error);
|
||
- composing the stages in the wrong order;
|
||
- a **seeded** search, which left 22.7 deg on the table for normals whose best yaw
|
||
sat far from the analytic guess;
|
||
- a sweep whose yaw ranking used "how close does the yawed normal come to the tool
|
||
axis", which is **not** the quantity that matters. What matters is where the
|
||
whole circle the pitch stage sweeps sits. Ranking by the wrong one left **68 deg**
|
||
on the table while looking entirely reasonable — this is the bug that produced
|
||
the retracted 65 deg finding above.
|
||
|
||
The solver is now a sweep of every yaw/pitch combination, evaluated as a grid one
|
||
yaw at a time (7.8 ms per point, 1032 vertices in 9 s), then a local polish to
|
||
1e-4 deg. It is verified two ways: it is **never worse than a brute-force search**
|
||
at 2 deg on random normals, and its returned residual matches an **independently
|
||
composed rotation matrix** to 8e-8 deg. Both checks are tests, not one-off probes.
|
||
|
||
The residual is measured from the actual rotations, so a wrong derivation shows up
|
||
as a number rather than as a plausible pose.
|
||
|
||
## Implemented so far
|
||
|
||
| Module | State |
|
||
|---|---|
|
||
| `sensor/frames.py` | mount mapping as a named constant; refuses a non-rotation |
|
||
| `sensor/export_bundle.py` | load + validate; derives mm/unit; refuses a non-uniform or mirrored pose, and count mismatches |
|
||
| `sensor/uv_flat.py` | flat image + pixel→3D table; gap pixels are `None` |
|
||
| `motion/kinematics.py` | constrained search + measured residual; stage limits are parameters; singular case reported, not guessed |
|
||
| `ui/sensor_panel.py` | picker, readouts, point table; refuses gap pixels with a reason; picked rows kept at full precision, not re-read from the 3-decimal display text |
|
||
| `ui/pose_preview.py` | 3D preview coloured by residual, tool axis drawn; the legend is a sibling label, because text composited over the GL surface is unreadable once it overflows. The scene, the palette and the framing rule are described in [finger-preview.md](finger-preview.md) |
|
||
| `motion/kinematics.py` | closed-form solve, exact to 1.2e-06°; the sweep is kept only as the reference the tests check it against — see [inverse-kinematics.md](inverse-kinematics.md) |
|
||
| workspace `finger` tab | wired, checked at 1440×900 and 1920×1080 in both themes |
|
||
| startup | the remembered bundle loads automatically, so the tab comes up working instead of showing a filled-in path over two empty views |
|
||
| `tools/preview_snapshot.py` | photographs the 3D pane with a real GL context, at rest or mid-playback, pane-only or composited into the page |
|
||
|
||
Not yet built: capture of picked points into the sequence, and the live-sensor
|
||
counterpart of the simulated pose.
|