Perception
Capture a wrist-camera image and relocalize an object from its AprilTags.
Perception lets your skill look at the deck. Two functions live here: one captures a still image from an arm's wrist camera, and one relocalizes a known object from its AprilTags and refines its stored world pose. Both are imported with from execution.execution_functions import *.
localize_object_tagsmoves an arm when you pass viewpoints. On real hardware the arm physically travels to each viewpoint before it captures. No return value confirms the path is clear. Treat every call that visits viewpoints as commanded motion and stage the deck accordingly.
Capturing an image
capture_image(arm, capture_name, save_to_project=False, export=False)Takes a single still from one arm's wrist camera at the arm's current pose. It does not move the arm.
Parameters
arm(str): which wrist camera to use. Either"left_arm"or"right_arm". Any other value fails the capture.capture_name(str): a name for this capture, and the name of the folder it lands in on the machine.
That folder is not keyed by run. A later run using the same
capture_nameoverwrites the earlier capture. If you need one copy per run, passsave_to_project=Trueorexport=True; both of those are keyed by run. Otherwise vary the name yourself.
save_to_project(bool, defaultFalse): setTrueto also save the capture into the project data folder atdata/captures/<run>/<capture_name>/, keyed by run, so it travels with the project and syncs to the cloud. Left atFalse, the capture is still written on the machine undercapture_name, but nothing goes into the synced project.export(bool, defaultFalse): setTrueto also upload this capture to your lab's data export bucket, under<prefix>/<project>/<run>/captures/<capture_name>/. The upload happens in the background, so it adds nothing to the time the capture itself takes. With no bucket configured, or outside a run, it does nothing and logs one warning.
save_to_projectandexportare two different destinations, and you can use either or both.save_to_projectkeeps the image with the project, so it comes down withzeon sync.exportpushes it to a bucket your lab owns, which is only configurable on a local install.exportis the newer of the two and is the one to reach for when the destination is your own storage.
Returns
A string locating the saved capture, or None if the capture failed. It points at a folder, not a single file, and it is the on-machine folder even when save_to_project=True.
The folder holds the colour image, a depth map (captures take depth by default), and a metadata file carrying the camera's intrinsics, its pose in the world frame, and the capture time. On real hardware the wrist camera writes its two infrared images alongside.
Hand the returned path to an operator prompt to show the image to a human. There is no capture browser in the app.
Failure behaviour
capture_image fails soft. It never raises into your skill. On an unknown arm, an unavailable camera, or any capture error it logs the problem and returns None. Check for None before you rely on the result.
path = capture_image("left_arm", "deck_overview")
if path is None:
# capture did not succeed; decide how your skill proceeds
...Relocalizing an object from AprilTags
Experimental. AprilTag relocalization is under active development. It needs printed tags at a known size and a calibrated rig, and its parameters and acceptance behaviour may still change. Check the returned pose before you rely on it.
localize_object_tags(
object_name,
viewpoints=None,
*,
arm="left_arm",
tag_edge_m=None,
collection=None,
use_prior=True,
min_detections=2,
pos_sigma_gate_mm=15.0,
max_move_mm=None,
max_fitness_mm=None,
# advanced, sensible defaults below
mode="corner",
sigma_px=1.0,
huber_px=2.0,
prior_std_pos_mm=20.0,
prior_std_rot_deg=10.0,
use_full_theta=False,
index=None,
)Re-reads a known object's AprilTags and refines its stored world pose. It captures one or more camera views, solves for the object pose anchored on the pose the object already has, and writes the refined pose back only if the solve passes the acceptance gates below. It fails soft: it logs and returns a diagnostic dict, and never raises into your skill.
Two consequences of an accepted solve are easy to miss:
- For an articulated object, the solved joint angles are written back too. Relocalizing a tagged object with a lid or a rotor overwrites the joint configuration the world was holding for it, not only its pose.
- The object ends up fixed in the world. If it was attached to an arm, the accepted write releases it to a fixed pose. Do not relocalize something an arm is carrying.
Parameters you will usually set
object_name(str): the object to relocalize, by its world id or itsmetadata["name"].viewpoints(list, defaultNone): where to look from.Nonetakes one capture fromarmat the arm's current pose. No motion.- A list of anchor-name strings moves the arm to each named viewpoint anchor and captures there. Define these anchors on the object in its
object_model.yaml. - A list of
{"anchor": name, "arm": "left_arm"|"right_arm"}dicts lets you choose the arm per viewpoint.
All captures in the list are fused into one solve.
arm(str, default"left_arm"): which wrist camera to use whenviewpointsisNone, and the default arm for bare-string viewpoint anchors.tag_edge_m(float, metres, defaultNone): the physical AprilTag edge length. Left atNoneit is taken from the object's tag collection, falling back to0.020(20 mm) when the object has none. Whatever it resolves to must match your printed tags. A wrong value mis-scales the recovered pose.collection(str, defaultNone): which physical unit's tag set to solve against, for an object type that has more than one. Left atNoneit uses the unit this object was placed with, then the type's only collection, then the tags recorded in the object model itself. See Localizing an object for what a tagged unit is.use_prior(bool, defaultTrue): keepTrueto anchor the solve on the object's existing pose (relocalization). SetFalsefor a cold start, such as a first placement or when you suspect the object moved a long way.
Acceptance gates
The refined pose is written back only if the solve is valid, saw at least min_detections tag observations, and its position uncertainty is within pos_sigma_gate_mm. Two optional guards can reject a suspect solve.
min_detections(int, default2): minimum number of tag observations required to accept.pos_sigma_gate_mm(float, mm, default15.0): reject if the solved position 1-sigma uncertainty exceeds this.max_move_mm(float, mm, defaultNone): if set, reject when the refined pose would move further than this from the stored pose. Catches a far-off prior fighting the tags or a genuine large move.Nonedisables the guard.max_fitness_mm(float, mm, defaultNone): if set, reject when the worst tag-to-surface fit residual exceeds this.Nonedisables the guard.
max_fitness_mmcurrently has no effect. The fit residual it reads is only produced when object meshes are handed to the solver, and the live path does not load them. Setting the parameter logs a warning that the guard is inactive, and the solve is accepted or rejected on the other gates alone. Do not rely on it to catch a confident-but-wrong pose. Usemax_move_mmandpos_sigma_gate_mm, and checkafteryourself.
Advanced parameters
These have sensible defaults and are rarely changed.
mode(str, default"corner"): the tag residual model."corner"is the robust default."pose"is a legacy path. Both use the same prior, gate, and write-back.sigma_px(float, pixels, default1.0) andhuber_px(float, pixels, default2.0): per-corner pixel noise and outlier threshold. Used only in"corner"mode.prior_std_pos_mm(float, mm, default20.0) andprior_std_rot_deg(float, deg, default10.0): how much drift the prior expects in position and rotation.use_full_theta(bool, defaultFalse): use the full calibration-uncertainty prior. Only valid when the frozen calibration matches the current rig.index(defaultNone): a disambiguator when several objects share the same name.
Return value
A dict. On success:
{
"success": True,
"views": "...", # which arm, or "N/M anchors" reached
"result": { ... }, # diagnostic, see below
"before": {"xyz": [...], "wxyz": [...]}, # pose before write-back
"after": {"xyz": [...], "wxyz": [...]}, # pose after write-back
"moved_mm": 3.1, # distance the object pose moved, mm
}The result diagnostic holds:
object_id: the resolved world object id.valid(bool): whether the solve was valid.n_detections,n_tags(int): tag observations used, and distinct tags seen.pos_sigma_mm(float, mm),rot_sigma_deg(float, deg): the solved pose 1-sigma uncertainty.joint_angles(dict): joint name to angle for the solved configuration.fitness_mm(dict): per-object worst tag-to-surface residual, in mm, when available.prior_used(bool): whether the stored pose was used as a prior.mode(str),views(str): echoes of what ran.moved_mm(float, mm),fitness_max_mm(float, mm): the candidate move against the stored pose and the worst fit residual, as read by the guards.
before.xyz and after.xyz are position triples. before.wxyz and after.wxyz are orientation quaternions in w, x, y, z order.
When the solve is rejected or cannot run
Every failure path returns a top-level success: False with a reason string, so branch on the returned dict's top-level success, for example loc["success"]. The result sub-dict, when present, does not carry a success key, and several failure paths omit result entirely. The reasons you may see:
"missing_uncertainty_prior": no calibration-uncertainty prior for the active setup. Also returnspath."object_not_found": no object matchedobject_name."no_object_model": the object has noobject_model.yaml."tag_collection_unresolved": the object type has several tagged units and the call could not tell which one to use, or the handle you passed incollectiondoes not exist. Also returnscollections, the list of handles it knows about, anderror. Pass one of them incollection."bad_tag_collection": the tagged unit's file could not be read. Also returnserror."no_captures": no usable camera views were gathered. Also returnsbefore."solve_failed": the solver raised. Also returnserrorandbefore."gate_failed": the solve ran but did not pass acceptance. Returnsgate(a list of the failed gate reasons), plusresult,before, andafter: None.
The gate failure never writes the object pose, so a rejected relocalization leaves the world unchanged.
Either outcome is written into the run log, so you can see what happened without reading the returned dict. An accepted solve logs Localized <name>: moved <n> mm (σ <x> mm, <n> detections), and a rejected one logs Localization of <name> rejected: followed by the gates that failed.
loc = localize_object_tags(
"my_plate",
viewpoints=["view_a", "view_b"],
max_move_mm=25.0,
)
if not loc["success"]:
# inspect loc["reason"] and, on a gate failure, loc["gate"]
...
else:
moved = loc["moved_mm"]Anchors are the mechanism behind viewpoints. See Anchors and Anchor snapping for how anchor poses follow the object.
Related
Updated about 2 months ago