6.0 KiB
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
Nonerather 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.