locus-tag
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
DetectionBatchwith parallel arrays for IDs, corners, and poses. - Memory: Uses
bumpaloarena 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.