Skip to content

API Reference

This page provides detailed information about the Locus Python API.

Core Interface

The primary entry point for using Locus is the Detector class.

locus.Detector

Source code in crates/locus-py/locus/__init__.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
class Detector:
    def __init__(
        self,
        decimation: int = 1,
        threads: int = 0,
        families: list[TagFamily] | None = None,
        threshold_tile_size: int = 8,
        threshold_min_range: int = 10,
        adaptive_threshold_constant: int = 0,
        quad_min_area: int = 16,
        quad_min_fill_ratio: float = 0.10,
        quad_min_edge_score: float = 4.0,
        decoder_min_contrast: float = 20.0,
        max_hamming_error: int = 2,
        **kwargs: Any,
    ):
        # Map enum to int values for Rust. Rust expects a Vec<i32>.
        if families is None:
            families = [TagFamily.AprilTag36h11]

        family_values = [int(f) for f in families]

        # Merge explicit args into kwargs for create_detector
        rust_kwargs: dict[str, Any] = {
            "threshold_tile_size": threshold_tile_size,
            "threshold_min_range": threshold_min_range,
            "adaptive_threshold_constant": adaptive_threshold_constant,
            "quad_min_area": quad_min_area,
            "quad_min_fill_ratio": quad_min_fill_ratio,
            "quad_min_edge_score": quad_min_edge_score,
            "decoder_min_contrast": decoder_min_contrast,
            "max_hamming_error": max_hamming_error,
        }
        rust_kwargs.update(kwargs)

        # Prepare kwargs for Rust by converting enums to ints
        final_rust_kwargs: dict[str, Any] = {}
        for k, v in rust_kwargs.items():
            # Boolean types must be preserved
            if isinstance(v, bool):
                final_rust_kwargs[k] = v
            # PyO3 enums might not inherit from enum.Enum but are int-convertible
            elif hasattr(v, "__int__"):
                final_rust_kwargs[k] = int(v)
            elif isinstance(v, enum.Enum):
                final_rust_kwargs[k] = v.value
            else:
                final_rust_kwargs[k] = v

        self._inner = _create_detector(
            decimation=decimation, threads=threads, families=family_values, **final_rust_kwargs
        )

    @staticmethod
    def production_config() -> "Detector":
        """Create a detector with high-fidelity production defaults."""
        d = Detector.__new__(Detector)
        d._inner = _production_config()
        return d

    @staticmethod
    def fast_config() -> "Detector":
        """Create a detector with low-latency defaults."""
        d = Detector.__new__(Detector)
        d._inner = _fast_config()
        return d

    def config(self) -> DetectorConfig:
        """Returns the current detector configuration."""
        raw = self._inner.config()
        # Create a dictionary of all fields to populate the Pydantic model
        fields = {field: getattr(raw, field) for field in DetectorConfig.model_fields}
        return DetectorConfig(**fields)

    def set_families(self, families: list[TagFamily]):
        """Update the tag families to be detected."""
        family_values = [int(f) for f in families]
        self._inner.set_families(family_values)

    def detect(
        self,
        img: np.ndarray,
        intrinsics: CameraIntrinsics | None = None,
        tag_size: float | None = None,
        pose_estimation_mode: PoseEstimationMode = PoseEstimationMode.Fast,
        debug_telemetry: bool = False,
        **kwargs,
    ) -> DetectionBatch:
        """
        Detect tags in the image.

        Args:
            img: Input grayscale image (np.uint8).
            intrinsics: Optional CameraIntrinsics for 3D pose estimation.
            tag_size: Optional physical tag size (meters).
            pose_estimation_mode: Fast or Accurate.

        Returns:
            A vectorized DetectionBatch object.
        """
        if img.dtype != np.uint8:
            raise ValueError(f"Input image must be uint8, got {img.dtype}")

        res_dict = self._inner.detect(
            img,
            intrinsics=intrinsics,
            tag_size=tag_size,
            pose_estimation_mode=pose_estimation_mode,
            debug_telemetry=debug_telemetry,
            **kwargs,
        )

        tel_res = res_dict.pop("telemetry", None)
        telemetry = None
        if tel_res is not None:
            telemetry = PipelineTelemetry(**tel_res)

        return DetectionBatch(**res_dict, telemetry=telemetry)

Functions

__init__

__init__(decimation: int = 1, threads: int = 0, families: list[TagFamily] | None = None, threshold_tile_size: int = 8, threshold_min_range: int = 10, adaptive_threshold_constant: int = 0, quad_min_area: int = 16, quad_min_fill_ratio: float = 0.1, quad_min_edge_score: float = 4.0, decoder_min_contrast: float = 20.0, max_hamming_error: int = 2, **kwargs: Any)
Source code in crates/locus-py/locus/__init__.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def __init__(
    self,
    decimation: int = 1,
    threads: int = 0,
    families: list[TagFamily] | None = None,
    threshold_tile_size: int = 8,
    threshold_min_range: int = 10,
    adaptive_threshold_constant: int = 0,
    quad_min_area: int = 16,
    quad_min_fill_ratio: float = 0.10,
    quad_min_edge_score: float = 4.0,
    decoder_min_contrast: float = 20.0,
    max_hamming_error: int = 2,
    **kwargs: Any,
):
    # Map enum to int values for Rust. Rust expects a Vec<i32>.
    if families is None:
        families = [TagFamily.AprilTag36h11]

    family_values = [int(f) for f in families]

    # Merge explicit args into kwargs for create_detector
    rust_kwargs: dict[str, Any] = {
        "threshold_tile_size": threshold_tile_size,
        "threshold_min_range": threshold_min_range,
        "adaptive_threshold_constant": adaptive_threshold_constant,
        "quad_min_area": quad_min_area,
        "quad_min_fill_ratio": quad_min_fill_ratio,
        "quad_min_edge_score": quad_min_edge_score,
        "decoder_min_contrast": decoder_min_contrast,
        "max_hamming_error": max_hamming_error,
    }
    rust_kwargs.update(kwargs)

    # Prepare kwargs for Rust by converting enums to ints
    final_rust_kwargs: dict[str, Any] = {}
    for k, v in rust_kwargs.items():
        # Boolean types must be preserved
        if isinstance(v, bool):
            final_rust_kwargs[k] = v
        # PyO3 enums might not inherit from enum.Enum but are int-convertible
        elif hasattr(v, "__int__"):
            final_rust_kwargs[k] = int(v)
        elif isinstance(v, enum.Enum):
            final_rust_kwargs[k] = v.value
        else:
            final_rust_kwargs[k] = v

    self._inner = _create_detector(
        decimation=decimation, threads=threads, families=family_values, **final_rust_kwargs
    )

detect

detect(img: ndarray, intrinsics: CameraIntrinsics | None = None, tag_size: float | None = None, pose_estimation_mode: PoseEstimationMode = PoseEstimationMode.Fast, debug_telemetry: bool = False, **kwargs) -> DetectionBatch

Detect tags in the image.

Parameters:

Name Type Description Default
img ndarray

Input grayscale image (np.uint8).

required
intrinsics CameraIntrinsics | None

Optional CameraIntrinsics for 3D pose estimation.

None
tag_size float | None

Optional physical tag size (meters).

None
pose_estimation_mode PoseEstimationMode

Fast or Accurate.

Fast

Returns:

Type Description
DetectionBatch

A vectorized DetectionBatch object.

Source code in crates/locus-py/locus/__init__.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def detect(
    self,
    img: np.ndarray,
    intrinsics: CameraIntrinsics | None = None,
    tag_size: float | None = None,
    pose_estimation_mode: PoseEstimationMode = PoseEstimationMode.Fast,
    debug_telemetry: bool = False,
    **kwargs,
) -> DetectionBatch:
    """
    Detect tags in the image.

    Args:
        img: Input grayscale image (np.uint8).
        intrinsics: Optional CameraIntrinsics for 3D pose estimation.
        tag_size: Optional physical tag size (meters).
        pose_estimation_mode: Fast or Accurate.

    Returns:
        A vectorized DetectionBatch object.
    """
    if img.dtype != np.uint8:
        raise ValueError(f"Input image must be uint8, got {img.dtype}")

    res_dict = self._inner.detect(
        img,
        intrinsics=intrinsics,
        tag_size=tag_size,
        pose_estimation_mode=pose_estimation_mode,
        debug_telemetry=debug_telemetry,
        **kwargs,
    )

    tel_res = res_dict.pop("telemetry", None)
    telemetry = None
    if tel_res is not None:
        telemetry = PipelineTelemetry(**tel_res)

    return DetectionBatch(**res_dict, telemetry=telemetry)

Configuration

Locus uses Pydantic for robust configuration validation.

locus.DetectorConfig

Bases: BaseModel

Pipeline-level configuration for the Locus detector. These settings are typically set once during detector instantiation.

Source code in crates/locus-py/locus/_config.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class DetectorConfig(BaseModel):
    """
    Pipeline-level configuration for the Locus detector.
    These settings are typically set once during detector instantiation.
    """

    model_config = ConfigDict(frozen=False, arbitrary_types_allowed=True)

    threshold_tile_size: int = Field(default=8, ge=2, le=64)
    threshold_min_range: int = Field(default=10, ge=0, le=255)

    enable_bilateral: bool = Field(default=False)
    bilateral_sigma_space: float = Field(default=0.8, ge=0.1)
    bilateral_sigma_color: float = Field(default=30.0, ge=0.1)

    enable_sharpening: bool = Field(default=False)

    enable_adaptive_window: bool = Field(default=False)
    threshold_min_radius: int = Field(default=2, ge=1)
    threshold_max_radius: int = Field(default=15, ge=1)
    adaptive_threshold_constant: int = Field(default=0)
    adaptive_threshold_gradient_threshold: int = Field(default=10, ge=0, le=255)

    quad_min_area: int = Field(default=16, ge=1)
    quad_max_aspect_ratio: float = Field(default=10.0, ge=1.0)
    quad_min_fill_ratio: float = Field(default=0.10, ge=0.0, le=1.0)
    quad_max_fill_ratio: float = Field(default=0.98, ge=0.0, le=1.0)
    quad_min_edge_length: float = Field(default=4.0, ge=0.0)
    quad_min_edge_score: float = Field(default=4.0, ge=0.0)
    subpixel_refinement_sigma: float = Field(default=0.6, ge=0.0)
    segmentation_margin: int = Field(default=1)
    segmentation_connectivity: SegmentationConnectivity = Field(
        default_factory=lambda: SegmentationConnectivity.Eight
    )
    upscale_factor: int = Field(default=1, ge=1)
    decoder_min_contrast: float = Field(default=20.0, ge=0.0)
    refinement_mode: CornerRefinementMode = Field(default_factory=lambda: CornerRefinementMode.Erf)
    decode_mode: DecodeMode = Field(default_factory=lambda: DecodeMode.Hard)
    max_hamming_error: int = Field(default=2, ge=0)
    gwlf_transversal_alpha: float = Field(default=0.01, ge=0.0)
    quad_max_elongation: float = Field(default=0.0, ge=0.0)
    quad_min_density: float = Field(default=0.0, ge=0.0, le=1.0)
    quad_extraction_mode: int = Field(default=0)

locus.DetectOptions

Bases: BaseModel

Per-call options for tag detection. Controls which tag families to decode for a specific frame.

Source code in crates/locus-py/locus/_config.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class DetectOptions(BaseModel):
    """
    Per-call options for tag detection.
    Controls which tag families to decode for a specific frame.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    families: list[TagFamily] = Field(default_factory=list)
    decimation: int = Field(default=1, ge=1)
    intrinsics: tuple[float, float, float, float] | CameraIntrinsics | None = Field(default=None)
    tag_size: float | None = Field(default=None, ge=0.0)
    pose_estimation_mode: PoseEstimationMode = Field(
        default_factory=lambda: PoseEstimationMode.Fast
    )

    @classmethod
    def all(cls) -> "DetectOptions":
        return cls(
            families=[
                TagFamily.AprilTag36h11,
                TagFamily.ArUco4x4_50,
                TagFamily.ArUco4x4_100,
            ]
        )

Data Models

These classes represent the output and internal state of the detection pipeline.

locus.DetectionBatch dataclass

Vectorized detection results.

This dataclass contains parallel NumPy arrays representing a batch of detections.

Source code in crates/locus-py/locus/__init__.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass(frozen=True)
class DetectionBatch:
    """
    Vectorized detection results.

    This dataclass contains parallel NumPy arrays representing a batch of detections.
    """

    ids: np.ndarray  # Shape: (N,), Dtype: int32
    corners: np.ndarray  # Shape: (N, 4, 2), Dtype: float32
    error_rates: np.ndarray  # Shape: (N,), Dtype: float32
    poses: np.ndarray | None = None  # Shape: (N, 7), Dtype: float32. [tx, ty, tz, qx, qy, qz, qw]
    telemetry: "PipelineTelemetry | None" = None
    rejected_corners: np.ndarray | None = None  # Shape: (M, 4, 2), Dtype: float32
    rejected_error_rates: np.ndarray | None = None  # Shape: (M,), Dtype: float32

    @property
    def centers(self) -> np.ndarray:
        """Compute centers from corners: (N, 2)"""
        return np.mean(self.corners, axis=1)

    def __len__(self) -> int:
        return len(self.ids)

Attributes

centers property
centers: ndarray

Compute centers from corners: (N, 2)

locus.Pose

Geometry

locus.CameraIntrinsics

Enumerations

locus.TagFamily

Bases: IntEnum

locus.DecodeMode

Bases: IntEnum

locus.PoseEstimationMode

Bases: IntEnum

locus.CornerRefinementMode

Bases: IntEnum

locus.SegmentationConnectivity

Bases: IntEnum