Skip to content

locus-tag

CI Docs License: MIT OR Apache-2.0

Locus detects AprilTag and ArUco markers as well as AprilGrid and ChArUco boards. The library is implemented in Rust and provides zero-copy Python bindings.

[!WARNING] Experimental Status: API is subject to breaking changes until 1.0.0 ships. The main workstreams towards 1.0.0 are reducing the API surface and validating against non-synthetic data. Until then, this library isn't recommended for production systems. The support for distortion models is experimental and requires a ground-up redesign. Addition of default tag families may happen on request, the current defaults were chosen to be minimal and support most commonly used tags.

Technical Capabilities

  • Zero-Copy Ingestion: Accesses NumPy arrays via the Python Buffer Protocol.
  • Parallel Execution: Releases the Python GIL during detection to allow multi-threaded use.
  • Vectorized Results: Returns a DetectionBatch with parallel arrays for IDs, corners, and poses.
  • Memory: Uses bumpalo arena allocation for zero heap allocations in the detection loop.
  • Solvers: 6-DOF recovery using IPPE-Square or weighted Levenberg-Marquardt with corner uncertainty.

Performance Profiles

Profiles are selected by name; the three shipped profiles are authored as JSON files and embedded in the wheel.

profile ICRA 2020 Recall Corner RMSE Primary Characteristics
"standard" 96.2% 0.315 px Production default; balanced recall/precision.
"grid" 91.4% 0.458 px 4-connectivity for touching tags (checkerboards).
"high_accuracy" 46.3%* 0.16 px EdLines + GN optimizer; prioritized for metrology.

*high_accuracy is optimized for high-resolution near-field images (Hugging Face Hub datasets).

Comparison (ICRA 2020 Forward - 50 images)

Detector Recall RMSE
Locus (Standard) 96.2% 0.315 px
AprilTag 3 (UMich) 62.3% 0.22 px
OpenCV 33.2% 0.92 px

Installation

pip install locus-tag

The PyPI wheel is compiled for rectified (pinhole) imagery. For unrectified cameras (Brown-Conrady polynomial, Kannala-Brandt equidistant fisheye), see Install with distortion support.

Quick Start

Basic Detection

import cv2
import locus

img = cv2.imread("tags.jpg", cv2.IMREAD_GRAYSCALE)
detector = locus.Detector(families=[locus.TagFamily.AprilTag36h11])

# batch contains parallel NumPy arrays
batch = detector.detect(img)
print(f"IDs: {batch.ids}")
print(f"Corners: {batch.corners.shape}") # (N, 4, 2)

6-DOF Pose Estimation

from locus import Detector, CameraIntrinsics

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

# Returns [tx, ty, tz, qx, qy, qz, qw] for each tag
batch = detector.detect(
    img,
    intrinsics=intrinsics,
    tag_size=0.10,  # physical side length in meters
)

if batch.poses is not None:
    # First tag translation
    print(batch.poses[0, :3])

Configuration Overrides

Settings are nested and validated by Pydantic. Start from a shipped profile, edit the group you care about, and hand it back to the detector:

base = locus.DetectorConfig.from_profile("high_accuracy").model_dump()
base["quad"]["upscale_factor"] = 2
base["decoder"]["max_hamming_error"] = 1

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

Visual Debugging

Built-in integration with the Rerun SDK:

batch = detector.detect(img, debug_telemetry=True)
if batch.telemetry:
    print(batch.telemetry.subpixel_jitter)

Documentation

License

Dual-licensed under Apache 2.0 or MIT.