Skip to main content

High-performance fiducial marker detection (ArUco Nano v6 + Fractal)

Project description

nanofractal

High-performance fiducial-marker detection for Python. nanofractal wraps two compact, header-only C++ detectors with nanobind:

  • ArUco Nano v6 — square markers (ARUCO_MIP_36h12 and AprilTag 36h11).
  • Fractal markers — nested markers that stay detectable under heavy occlusion and expose many inner corner correspondences for accurate, long-range pose.

It is built for speed: zero-copy NumPy ↔ cv::Mat, the GIL is released during detection, and a parallel batch API scales across cores.

single-frame detect():   ~0.43 ms @ 640x480   ~1.3 ms @ 1280x720   ~2.9 ms @ 1920x1080
batch detect_batch():    ~3.2x throughput on 4 threads

Measured on a desktop CPU with max_attempts=1; your numbers will vary.


Installation

pip install nanofractal

Wheels bundle a minimal OpenCV, so no system OpenCV is required at runtime.

Build from source

You need a C++17 compiler, CMake ≥ 3.18 and a development OpenCV (core, imgproc, calib3d, features2d):

# Debian/Ubuntu
sudo apt-get install -y build-essential cmake libopencv-dev

pip install .

Quick start

Inputs are plain NumPy uint8 arrays — either (H, W) grayscale or (H, W, 3) BGR, and C-contiguous (use np.ascontiguousarray if unsure). Any image loader works; the examples use OpenCV.

Detect ArUco / AprilTag markers

import cv2
import nanofractal as nf

image = cv2.imread("scene.png")                  # (H, W, 3) uint8 BGR
det = nf.ArucoDetector(nf.Dict.ARUCO_MIP_36h12)  # or nf.Dict.APRILTAG_36h11

res = det.detect(image)
print(res.ids)        # int32   (N,)       e.g. [ 7 42]
print(res.corners)    # float32 (N, 4, 2)  clockwise corners, subpixel

Estimate pose

estimate_pose runs solvePnP (IPPE) for every detected marker at once.

import numpy as np

camera_matrix = np.array([[600, 0, 320],
                          [0, 600, 240],
                          [0,   0,   1]], dtype=np.float64)
dist_coeffs = np.zeros(5, dtype=np.float64)

rvecs, tvecs = det.estimate_pose(res.corners, camera_matrix, dist_coeffs,
                                 marker_size=0.05)   # marker side in metres
# rvecs, tvecs: float64 (N, 3) — rotation (Rodrigues) and translation per marker

Detect fractal markers

fdet = nf.FractalDetector("FRACTAL_5L_6", marker_size=0.85)  # size in metres (optional)

res = fdet.detect(image)
print(res.ids, res.corners.shape)   # outer 4 corners of each fractal marker

Fractal pose + visualization (occlusion-robust)

FractalDetector.estimate_pose returns one marker pose (rvec, tvec, reproj_err) or None. It uses every visible inner and outer corner correspondence when available (accurate, robust to occlusion) and otherwise falls back to the four outer corners — so you never call solvePnP yourself or worry about the empty-inner-points case. reproj_err (RMS pixels) lets you gate noisy poses.

fdet = nf.FractalDetector("FRACTAL_5L_6", marker_size=0.85)  # size in metres

res = fdet.detect(image, with_inner_points=True)
pose = fdet.estimate_pose(res, camera_matrix, dist_coeffs)
if pose is not None:
    rvec, tvec, reproj_err = pose      # rvec, tvec: float64 (3,); reproj_err: px
    fdet.draw(image, res, camera_matrix, dist_coeffs, rvec, tvec)  # corners + axes

draw(image, result, ...) overlays marker outlines, ids and (given a pose) the frame axes in place — no cv2.polylines/drawFrameAxes boilerplate. Without a pose, fdet.draw(image, res) just draws the outlines.

The raw correspondences are still exposed if you prefer to run PnP yourself:

res.points_2d   # float32 (M, 2) image points  (None unless with_inner_points=True)
res.points_3d   # float32 (M, 3) object points (planar, z = 0)

Parallel batch (offline throughput)

Process many frames across a thread pool. The GIL is released, so it scales with cores. num_threads=0 uses all cores.

frames = [cv2.imread(p) for p in paths]            # list of uint8 arrays
results = det.detect_batch(frames, num_threads=0)  # list[DetectionResult]
for r in results:
    print(r.ids)

API

ArucoDetector(dictionary=Dict.ARUCO_MIP_36h12, max_attempts=1)

  • dictionary: DictARUCO_MIP_36h12 or APRILTAG_36h11.
  • max_attempts: int — retries per candidate with small corner jitter. 1 is fastest (real-time default); raise (up to ~10) for harder images.
  • detect(image) -> DetectionResult
  • detect_batch(images, num_threads=0) -> list[DetectionResult]
  • estimate_pose(corners, camera_matrix, dist_coeffs, marker_size) -> (rvecs, tvecs)corners is (N, 4, 2) float32; outputs are (N, 3) float64.

FractalDetector(config, marker_size=-1.0)

  • config: str — one of FRACTAL_2L_6, FRACTAL_3L_6, FRACTAL_4L_6, FRACTAL_5L_6.
  • marker_size: float — outer marker side in metres; if set, points_3d is returned in metres (otherwise normalized).
  • detect(image, with_inner_points=False) -> DetectionResult
  • detect_batch(images, num_threads=0) -> list[DetectionResult]
  • estimate_pose(result, camera_matrix, dist_coeffs) -> (rvec, tvec, reproj_err) | None — single-marker pose; uses inner+outer points when ≥ 4, else the 4 outer corners; rvec/tvec are float64 (3,), reproj_err is RMS pixels.
  • draw(image, result, camera_matrix=None, dist_coeffs=None, rvec=None, tvec=None, axis_length=None) -> image — draw outlines + ids (and frame axes when a pose is given) in place; image must be a writable BGR uint8 array.

DetectionResult

field dtype / shape meaning
ids int32 (N,) marker ids
corners float32 (N, 4, 2) outer corners (subpixel, clockwise)
points_2d float32 (M, 2) or None inner+outer image points (fractal, with_inner_points=True)
points_3d float32 (M, 3) or None matching object points

Empty results are returned as correctly-shaped empty arrays ((0,), (0, 4, 2)), never None.

Errors

  • Wrong dtype / non-contiguous input → TypeError.
  • Unsupported shape, empty frame, invalid dictionary or fractal config → ValueError.

Performance notes

  • Zero-copy input. A contiguous uint8 array is wrapped as a cv::Mat over the same buffer — no copy. Non-contiguous or wrong-dtype inputs raise instead of silently copying.
  • GIL released during the native detection, so other Python threads keep running and detect_batch scales.
  • Thread safety. The ArUco detector is stateless and shared across batch workers. The fractal detector is not thread-safe, so detect_batch uses a pool of independent detectors (one per worker). A single detector object is fine to call from one thread at a time.

Citation

If you use this in research, please cite the original work:

  • F. J. Romero-Ramirez, R. Muñoz-Salinas, R. Medina-Carnicer, "Speeded up detection of squared fiducial markers", Image and Vision Computing, 76, 2018.
  • S. Garrido-Jurado, R. Muñoz-Salinas, F. J. Madrid-Cuevas, R. Medina-Carnicer, "Generation of fiducial marker dictionaries using mixed integer linear programming", Pattern Recognition, 51, 2016.
  • F. J. Romero-Ramirez, R. Muñoz-Salinas, R. Medina-Carnicer, "Fractal Markers: A New Approach for Long-Range Marker Pose Estimation Under Occlusion", IEEE Access, 7, 2019.

License

Apache-2.0. The vendored detectors (ArUco Nano, Fractal markers) are © their authors and used under their terms; see third_party/ and PATCHES.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nanofractal-0.1.1.tar.gz (58.0 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

nanofractal-0.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (3.2 MB view details)

Uploaded CPython 3.13manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanofractal-0.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (3.2 MB view details)

Uploaded CPython 3.12manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanofractal-0.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (3.2 MB view details)

Uploaded CPython 3.11manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanofractal-0.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (3.2 MB view details)

Uploaded CPython 3.10manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

nanofractal-0.1.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (3.2 MB view details)

Uploaded CPython 3.9manylinux: glibc 2.27+ x86-64manylinux: glibc 2.28+ x86-64

File details

Details for the file nanofractal-0.1.1.tar.gz.

File metadata

  • Download URL: nanofractal-0.1.1.tar.gz
  • Upload date:
  • Size: 58.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nanofractal-0.1.1.tar.gz
Algorithm Hash digest
SHA256 1e68e9269fbf5895463278bc62bd68a9b8f9beeebfb2f98ec791d6171b2db8e0
MD5 82ca189ed281550719d6b7f272975be8
BLAKE2b-256 9a39d00db6613178250b97c462f0efd37f323979080770c88477bf2c405ea596

See more details on using hashes here.

File details

Details for the file nanofractal-0.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanofractal-0.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 999eb21ace61254745bde0a05553f250c385b4abdb82ce168dbc98f2f03cd42a
MD5 cff363e376fedf7caa04a0f582f61c64
BLAKE2b-256 4fb1a06ccf2bbf4c7d0ae597ac10c5656db0ec89e23b37edc9e33828c7ecfa0e

See more details on using hashes here.

File details

Details for the file nanofractal-0.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanofractal-0.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 9026c89b5161054ff90068c3f2032333f89ed84ca8d4652f21872fa616641fec
MD5 fc11002c1e73932e9c9b9abfce365333
BLAKE2b-256 274ece673a0e903cb285920e499237c5bd243818ff37d04d8e5f072930e7e228

See more details on using hashes here.

File details

Details for the file nanofractal-0.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanofractal-0.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 3baa0c30ead6a6cb7cb7ae8b2ab7bda6b126d72d14705951b1779753a52a9276
MD5 618d69b6f20dd3aabec7091d0ea4bbe6
BLAKE2b-256 55cc69f299333e40899d882118808bf91299337bd7ccc26ed1d02da2255b02b4

See more details on using hashes here.

File details

Details for the file nanofractal-0.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanofractal-0.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 fa7b1fa2e87dd9ee97f1186f11ea7fd56d3d5489a4809528baa704ccb6064a76
MD5 22e90195b9fcc5883c7e7f7681d8e54d
BLAKE2b-256 95556afb81f834614487f040cd644388f86991f806de1b7597feb2249bd51a04

See more details on using hashes here.

File details

Details for the file nanofractal-0.1.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for nanofractal-0.1.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 ae7c95418cb5627a76e6582229f9e12dd9ee49fd1d0f3716a6e78d5fe5aad11e
MD5 26cbd17a15ccdc6f0b89bb7a0d895e26
BLAKE2b-256 cfc8c12696401dd022e611a977be317fb7041cec04ed9f1319326fce559737de

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page