Skip to content

User Guide

This guide covers advanced configuration and features of the Locus detector.

Profiles: the one knob you should reach for first

Detector settings are loaded from JSON profiles. Three are shipped in the wheel: standard, grid, and high_accuracy.

import locus

detector = locus.Detector(profile="standard")      # default; dense multi-tag
tags = detector.detect(img)

Per-call orchestration (decimation, threads, families) stays outside the profile because it describes how the detector is invoked, not what it detects:

detector = locus.Detector(
    profile="standard",
    decimation=2,                                 # 4x preprocessing speedup
    families=[locus.TagFamily.AprilTag36h11],
)

Tweaking a profile

Load a shipped profile, edit the relevant nested group, then pass it back:

base = locus.DetectorConfig.from_profile("standard").model_dump()
base["threshold"]["tile_size"] = 16               # larger tiles run faster
base["decoder"]["min_contrast"] = 10.0

custom = locus.DetectorConfig.model_validate(base)
detector = locus.Detector(config=custom)

model_validate runs the full invariant suite — radius ordering, fill-ratio ordering, and the cross-group compatibility checks (e.g. EdLines refuses Erf refinement) — so any inconsistency surfaces as a pydantic.ValidationError before the Rust detector is constructed.

Loading a custom profile from JSON

For reproducibility, teams typically keep their tuned profile under version control as a JSON file and load it via from_profile_json:

with open("my_profile.json") as f:
    detector = locus.Detector(config=locus.DetectorConfig.from_profile_json(f.read()))

The shipped standard.json is a good starting template; copy it, edit the nested groups, and load the copy. The JSON Schema at schemas/profile.schema.json powers editor autocomplete.

Specialized Profiles

Checkerboard Detection

Used for calibration patterns or densely packed tags where black squares touch. The grid profile uses 4-way connectivity to prevent component merging:

detector = locus.Detector(profile="grid")

High-Accuracy Metrology

For isolated-tag pose extraction at high resolution (EdLines + geometric corners + no sub-pixel refinement):

detector = locus.Detector(profile="high_accuracy")

Targeted Families

Searching for fewer families reduces the decoding search space and improves latency:

detector.set_families([
    locus.TagFamily.AprilTag36h11,
    locus.TagFamily.ArUco4x4_50,
])

Precise Configuration

Every detection knob lives in one of five nested groups (threshold, quad, decoder, pose, segmentation). Tweak any of them by round- tripping a profile through a dict:

base = locus.DetectorConfig.from_profile("standard").model_dump()
base["quad"]["min_area"] = 16                    # filter small components
base["quad"]["subpixel_refinement_sigma"] = 0.8  # corner-refinement kernel
base["decoder"]["min_contrast"] = 10.0           # bit-transition sensitivity

detector = locus.Detector(config=locus.DetectorConfig.model_validate(base))

Pose Estimation

Locus implements modern pose solvers to recover the 6-DOF transformation between the camera and the tag.

IPPE-Square

For standard detection, we use IPPE-Square (Infinitesimal Plane-Based Pose Estimation). It provides an analytical solution that resolves the Necker reversal (perspective flip) ambiguity.

intrinsics = locus.CameraIntrinsics(fx=800.0, fy=800.0, cx=640.0, cy=360.0)

# Pass intrinsics and tag size (meters) to enable pose estimation
tags = detector.detect(
    img,
    intrinsics=intrinsics,
    tag_size=0.16
)

for t in tags:
    if t.pose:
        print(f"Translation: {t.pose.translation}") # [x, y, z]
        print(f"Rotation: {t.pose.rotation}")       # 3x3 Matrix

Per-tag Covariance

When the call supplies an img view (and tag-size + intrinsics), Locus computes the Structure Tensor at each corner to estimate position uncertainty and runs Anisotropic Weighted Levenberg-Marquardt refinement. The returned pose carries a 6×6 covariance matrix. Pose-only refits that skip the image fall back to unweighted Huber LM and report no covariance.

tags = detector.detect(
    img,
    intrinsics=intrinsics,
    tag_size=0.16,
)

for t in tags:
    if t.pose_covariance:
        # Full 6x6 covariance matrix [R|t]
        print(f"Pose Uncertainty: {t.pose_covariance}")