perceiving-deformable-linear-objects
Read the cable's centreline once per step, and carry it through the loop as
text.
Why this is its own skill
A rigid object is perceived once and then transformed; a cable has no pose,
only a shape, and the shape changes every time a crossing moves it. The thing
a routing loop needs is therefore an ordered 3D centreline — 42 nodes in
arc-length order, not a bounding box and not a principal axis — and a rule for
when to trust a fresh reading of it over the one it already has. That rule is
the skill: a cold fit has no history and is the better estimate whenever the
whole rod is in frame; a tracker is the better estimate only when it is not.
Which case a frame is in is measurable (length against the prior) and the
script routes on it.
Measured over eight arm poses from a bench camera pitched 55 degrees off the
vertical, scored against the rod's own bodies:
| estimate |
median |
p90 |
max |
| cold fit per frame |
6.7 mm |
8.4 mm |
9.3 mm |
| tracked from the prior |
9.0 mm |
9.7 mm |
9.9 mm |
The tracker is the worse estimate when nothing is hidden, and monotonically so
across passes (7.6, 8.6, 9.2, 9.6, 9.6, 9.9 mm): each pass drags the prior
along instead of carrying a hidden stretch through. It is still the right
fallback, because the cold fit's own failure is sharp rather than gradual — a
skeleton broken by the arm merges to one fragment, measured at 338 mm of a
488 mm rod (0.69), while honest fits ran 0.99–1.06 of the truth. The
whole_rod gate (0.80) sits between those two populations.
When to use
- A loop needs the current shape of a cable, rope, or hose once per station or
per step, and the shape only changes when the robot moves it.
- A fixed third-person RGB-D camera can be placed where it sees the whole
object. Measured: an eye-in-hand camera frames a fifth of the cable at best,
and an A/B over three episodes came out identical (0.667 mean) — the move it
costs buys nothing. Pitch the camera off the vertical: overhead, the hand
covered the rod exactly when the rod was moving (28 of 42 nodes visible,
60 mm RMS); at 55 degrees it sees 41 of 42 and fits at 6.7 mm.
When NOT to use
- Every-frame tracking during a motion. This skill spends one detector call
and one fit per read; call it between motions, not inside one.
- Rigid, linear parts (a shaft, a handle) — fit an axis with
geometry.fit_linear_feature instead.
State flow
prior == "" or index == 0 ──► seed from scene.rod ──► found (seeded)
│ no seed
▼
prior == "" ──► cold fit every mask candidate, take the THINNEST ──► found (initialised) | lost
│ prior present
▼
for camera in (chosen, track_camera, init_camera):
cold fit ──► length ≥ whole_rod × len(prior)? ──► found (refit)
else track prior onto the frame ──► ≥ min_visible nodes seen? ──► found (tracked)
nothing improved ──► found (held; rod == prior)
- Seeded. On the first pass (
index == 0) — or whenever there is no
prior — the survey's rod (the scene JSON's rod key, from
perceiving-routing-fixtures) is returned as the model. The model must be
born from an unobstructed view of the whole object, and that view exists
exactly once per episode, before the first move; re-fitting after the hand
has parked over the first station read 381 mm of a 500 mm rod.
- Initialised. With no prior and no seed, the bench camera is read, up to
mask_candidates masks are fitted, and the thinnest plausible one wins.
The top-scoring mask is not always the cable: on a grey bench with white
arms a prompt sometimes returns the rod merged with an arm, scored
confidently — measured on a 700 mm rod as 963 and 983 mm seeds on two of
six episodes, both of which failed. Radius (mask area over centreline
length) separates them where score cannot; max_rod_radius (12 mm) is the
ceiling.
- Refit. The look's camera is tried first, then
track_camera, then
init_camera. A fresh fit whose arclength reaches whole_rod of the
prior's is taken as is.
- Tracked. A short fit hands the frame to
curve.track_centerline, which
moves the prior's nodes by what the frame says about each and carries the
unanswered ones on the displacement field of their visible neighbours. The
update is believed only when at least min_visible nodes (6, about 70 mm
of a 500 mm rod) had real correspondence — a tracker that accepted every
frame would walk the model onto whatever happened to be visible.
- Held. Nothing improved on the prior; it is handed back unchanged.
Inputs
prior — the centreline the loop carries, as JSON text; empty on a cold
start. The graph binds it to the latest rod written upstream (the
survey's on the first pass, this skill's own afterwards).
scene — the survey JSON from perceiving-routing-fixtures; only its
rod is read here.
camera — the camera the look chose for this pass (empty: the bench camera).
index — the pass number; 0 is the pass before the first move.
- Bench constants as parameters with the measured defaults:
init_camera
and track_camera ("cable"), rod_query ("thin white cable"),
rod_score (0.20), whole_rod (0.80), min_visible (6),
max_rod_radius (0.012), curve_nodes (42), mask_candidates (4).
Outputs
rod — the centreline as JSON text: the points of a Centerline, an
ordered list of [x, y, z] metres at 0.01 mm precision. It is passed as
text so it round-trips through a loop unchanged (json.loads gives the
list; a JSON Centerline object with a points key is also accepted on
the way in). On held and lost it is the prior that was given ("null"
when there was none), so a pass never binds a worse model than it had.
arclength_m, source (seeded | initialised | refit | tracked |
held | no-camera | no-mask | no-fit), camera (which view carried
this update), visible (nodes with real correspondence; the node count for
a fit).
The router field is route. A node that raises binds no outputs, so the next
reader of rod falls back to whatever was written before it.
Required end states
| End state |
Meaning |
found |
rod carries a centreline (see source for how it was earned). |
lost |
No model could be born and there was no prior to hold. |
1---2name: perceiving-deformable-linear-objects3description: Read a cable's ordered 3D centreline from one RGB-D frame and keep it current across a task — seed it from an unobstructed survey, re-fit it cold whenever the whole rod is in view, and fall back to tracking the carried prior only when the fresh fit comes back short. Use when a manipulation loop needs the current shape of a deformable linear object (a cable, rope, or hose) once per step rather than every frame, from a fixed third-person camera.4license: Apache-2.05---67# perceiving-deformable-linear-objects89Read the cable's centreline once per step, and carry it through the loop as10text.1112## Why this is its own skill1314A rigid object is perceived once and then transformed; a cable has no pose,15only a shape, and the shape changes every time a crossing moves it. The thing16a routing loop needs is therefore an **ordered 3D centreline** — 42 nodes in17arc-length order, not a bounding box and not a principal axis — and a rule for18when to trust a fresh reading of it over the one it already has. That rule is19the skill: a cold fit has no history and is the better estimate whenever the20whole rod is in frame; a tracker is the better estimate only when it is not.21Which case a frame is in is measurable (length against the prior) and the22script routes on it.2324Measured over eight arm poses from a bench camera pitched 55 degrees off the25vertical, scored against the rod's own bodies:2627| estimate | median | p90 | max |28|---|---|---|---|29| cold fit per frame | 6.7 mm | 8.4 mm | 9.3 mm |30| tracked from the prior | 9.0 mm | 9.7 mm | 9.9 mm |3132The tracker is the worse estimate when nothing is hidden, and monotonically so33across passes (7.6, 8.6, 9.2, 9.6, 9.6, 9.9 mm): each pass drags the prior34along instead of carrying a hidden stretch through. It is still the right35fallback, because the cold fit's own failure is sharp rather than gradual — a36skeleton broken by the arm merges to one fragment, measured at 338 mm of a37488 mm rod (0.69), while honest fits ran 0.99–1.06 of the truth. The38`whole_rod` gate (0.80) sits between those two populations.3940## When to use4142- A loop needs the current shape of a cable, rope, or hose once per station or43 per step, and the shape only changes when the robot moves it.44- A fixed third-person RGB-D camera can be placed where it sees the whole45 object. Measured: an eye-in-hand camera frames a fifth of the cable at best,46 and an A/B over three episodes came out identical (0.667 mean) — the move it47 costs buys nothing. Pitch the camera off the vertical: overhead, the hand48 covered the rod exactly when the rod was moving (28 of 42 nodes visible,49 60 mm RMS); at 55 degrees it sees 41 of 42 and fits at 6.7 mm.5051## When NOT to use5253- Every-frame tracking during a motion. This skill spends one detector call54 and one fit per read; call it between motions, not inside one.55- Rigid, linear parts (a shaft, a handle) — fit an axis with56 `geometry.fit_linear_feature` instead.5758## State flow5960```text61prior == "" or index == 0 ──► seed from scene.rod ──► found (seeded)62 │ no seed63 ▼64prior == "" ──► cold fit every mask candidate, take the THINNEST ──► found (initialised) | lost65 │ prior present66 ▼67for camera in (chosen, track_camera, init_camera):68 cold fit ──► length ≥ whole_rod × len(prior)? ──► found (refit)69 else track prior onto the frame ──► ≥ min_visible nodes seen? ──► found (tracked)70nothing improved ──► found (held; rod == prior)71```72731. **Seeded.** On the first pass (`index == 0`) — or whenever there is no74 prior — the survey's `rod` (the `scene` JSON's `rod` key, from75 `perceiving-routing-fixtures`) is returned as the model. The model must be76 born from an unobstructed view of the whole object, and that view exists77 exactly once per episode, before the first move; re-fitting after the hand78 has parked over the first station read 381 mm of a 500 mm rod.792. **Initialised.** With no prior and no seed, the bench camera is read, up to80 `mask_candidates` masks are fitted, and the thinnest plausible one wins.81 The top-scoring mask is not always the cable: on a grey bench with white82 arms a prompt sometimes returns the rod merged with an arm, scored83 confidently — measured on a 700 mm rod as 963 and 983 mm seeds on two of84 six episodes, both of which failed. Radius (mask area over centreline85 length) separates them where score cannot; `max_rod_radius` (12 mm) is the86 ceiling.873. **Refit.** The look's camera is tried first, then `track_camera`, then88 `init_camera`. A fresh fit whose arclength reaches `whole_rod` of the89 prior's is taken as is.904. **Tracked.** A short fit hands the frame to `curve.track_centerline`, which91 moves the prior's nodes by what the frame says about each and carries the92 unanswered ones on the displacement field of their visible neighbours. The93 update is believed only when at least `min_visible` nodes (6, about 70 mm94 of a 500 mm rod) had real correspondence — a tracker that accepted every95 frame would walk the model onto whatever happened to be visible.965. **Held.** Nothing improved on the prior; it is handed back unchanged.9798## Inputs99100- `prior` — the centreline the loop carries, as JSON text; empty on a cold101 start. The graph binds it to the latest `rod` written upstream (the102 survey's on the first pass, this skill's own afterwards).103- `scene` — the survey JSON from `perceiving-routing-fixtures`; only its104 `rod` is read here.105- `camera` — the camera the look chose for this pass (empty: the bench camera).106- `index` — the pass number; 0 is the pass before the first move.107- Bench constants as parameters with the measured defaults: `init_camera`108 and `track_camera` (`"cable"`), `rod_query` (`"thin white cable"`),109 `rod_score` (0.20), `whole_rod` (0.80), `min_visible` (6),110 `max_rod_radius` (0.012), `curve_nodes` (42), `mask_candidates` (4).111112## Outputs113114- `rod` — the centreline as **JSON text: the `points` of a `Centerline`**, an115 ordered list of `[x, y, z]` metres at 0.01 mm precision. It is passed as116 text so it round-trips through a loop unchanged (`json.loads` gives the117 list; a JSON `Centerline` object with a `points` key is also accepted on118 the way in). On `held` and `lost` it is the prior that was given (`"null"`119 when there was none), so a pass never binds a worse model than it had.120- `arclength_m`, `source` (`seeded` | `initialised` | `refit` | `tracked` |121 `held` | `no-camera` | `no-mask` | `no-fit`), `camera` (which view carried122 this update), `visible` (nodes with real correspondence; the node count for123 a fit).124125The router field is `route`. A node that raises binds no outputs, so the next126reader of `rod` falls back to whatever was written before it.127128## Required end states129130| End state | Meaning |131|---|---|132| `found` | `rod` carries a centreline (see `source` for how it was earned). |133| `lost` | No model could be born and there was no prior to hold. |