Files
orisys-app/apps/autocal/docs/finger-debug-findings.md
ykcheungab bcc55a3b97 修正朝上判断、接触高度与过期姿态目标
朝上判断误用平台轴、接触高度报旋转前数值、多点对准复用缓存姿态,且多点会话被复核入口拒绝;
现按修正计划改正并接通 autocal-multipoint-1.0 只读复核,缺失帧源如实报告而不补零。

离线验证:新回归在 dc5a6f8 上失败、此处通过,panel_check 通过,两截图点接触高度复现
19.0510 / 21.5781 mm;实机项未做,详见 .scratch/autocal-motion-fix/verification.md。
2026-10-15 06:52:28 +08:00

122 lines
6.8 KiB
Markdown
Raw Permalink 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.**
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 90 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.
**Correction (motion-correction-plan.zh.md task 1).** This note used to claim that
**51 of 1032 vertices end inverted** whichever branch is chosen, because the tool
direction was antiparallel to the sensor's own up axis. That count came from
`uprightness` comparing a platform axis through `to_sensor(np.eye(3)[1])`, which is
the sensor's own **-Z** — a horizontal axis — so a body rolled about the vertical
was reported as turned over. Measured against the post-mount nominal up (the tool
axis) no exported vertex is inverted, and the corrected flag changes no vertex's
branch selection. What did invert poses was the `approach_sign` polarity bug
recorded above; the residual could not see it, and neither could that up test. See
`inverse-kinematics.md` → *the face is aimed at +Z* and *the two exact poses of one
normal*.
## 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.