Files
orisys-app/apps/autocal/docs/finger-debug-findings.md
T
2026-09-21 16:11:45 +08:00

113 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.** The corrected solver solves the real surface as:
| Envelope | Worst residual | Median | Within 0.01 deg | Within 0.5 deg |
|---|---|---|---|---|
| pitch 360 deg, **yaw +-90 deg** (the machine) | **0.21 deg** | 0.00 deg | **98.4%** | **100%** |
| pitch 360 deg, yaw +-180 deg | 0.24 deg | 0.00 deg | — | 100% |
So **every** vertex of the sensing patch is reachable, and widening the yaw range
buys essentially nothing (0.21 deg → 0.24 deg worst). The 0.21 deg worst case is
a genuinely clamped vertex: the sweep's own minimum at that vertex is 0.211 deg
at the yaw clamp, so the number is physical, not lost precision.
Why the yaw limit is not the constraint, which the earlier note had backwards:
- The pitch stage turns a vector around a circle in the plane **perpendicular to
the pitch axis**.
- The yaw stage can reach a given great circle as long as the yawed normal is not
parallel to the pitch axis. A normal parallel to the pitch axis after yawing
would have to be perpendicular to the tool axis already, and the finger patch
normals — which point along sensor +Y, mapping to platform **+Z**, the tool
direction — are nothing of the sort.
- Pitch travel is therefore what does the work, and the 360 deg stage provides it.
**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.
## 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 |
| 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 |
Not yet built: capture of picked points into the sequence, and the live-sensor
counterpart of the simulated pose.