Files
orisys-app/apps/autocal/docs/finger-debug-findings.md
T
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

6.8 KiB
Raw Blame History

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
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
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.