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_36h12and AprilTag36h11). - 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: Dict—ARUCO_MIP_36h12orAPRILTAG_36h11.max_attempts: int— retries per candidate with small corner jitter.1is fastest (real-time default); raise (up to ~10) for harder images.detect(image) -> DetectionResultdetect_batch(images, num_threads=0) -> list[DetectionResult]estimate_pose(corners, camera_matrix, dist_coeffs, marker_size) -> (rvecs, tvecs)—cornersis(N, 4, 2)float32; outputs are(N, 3)float64.
FractalDetector(config, marker_size=-1.0)
config: str— one ofFRACTAL_2L_6,FRACTAL_3L_6,FRACTAL_4L_6,FRACTAL_5L_6.marker_size: float— outer marker side in metres; if set,points_3dis returned in metres (otherwise normalized).detect(image, with_inner_points=False) -> DetectionResultdetect_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/tvecare float64(3,),reproj_erris 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;imagemust be a writable BGRuint8array.
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
uint8array is wrapped as acv::Matover 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_batchscales. - Thread safety. The ArUco detector is stateless and shared across batch
workers. The fractal detector is not thread-safe, so
detect_batchuses 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1e68e9269fbf5895463278bc62bd68a9b8f9beeebfb2f98ec791d6171b2db8e0
|
|
| MD5 |
82ca189ed281550719d6b7f272975be8
|
|
| BLAKE2b-256 |
9a39d00db6613178250b97c462f0efd37f323979080770c88477bf2c405ea596
|
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
- Download URL: nanofractal-0.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.13, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
999eb21ace61254745bde0a05553f250c385b4abdb82ce168dbc98f2f03cd42a
|
|
| MD5 |
cff363e376fedf7caa04a0f582f61c64
|
|
| BLAKE2b-256 |
4fb1a06ccf2bbf4c7d0ae597ac10c5656db0ec89e23b37edc9e33828c7ecfa0e
|
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
- Download URL: nanofractal-0.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.12, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9026c89b5161054ff90068c3f2032333f89ed84ca8d4652f21872fa616641fec
|
|
| MD5 |
fc11002c1e73932e9c9b9abfce365333
|
|
| BLAKE2b-256 |
274ece673a0e903cb285920e499237c5bd243818ff37d04d8e5f072930e7e228
|
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
- Download URL: nanofractal-0.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.11, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3baa0c30ead6a6cb7cb7ae8b2ab7bda6b126d72d14705951b1779753a52a9276
|
|
| MD5 |
618d69b6f20dd3aabec7091d0ea4bbe6
|
|
| BLAKE2b-256 |
55cc69f299333e40899d882118808bf91299337bd7ccc26ed1d02da2255b02b4
|
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
- Download URL: nanofractal-0.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.10, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fa7b1fa2e87dd9ee97f1186f11ea7fd56d3d5489a4809528baa704ccb6064a76
|
|
| MD5 |
22e90195b9fcc5883c7e7f7681d8e54d
|
|
| BLAKE2b-256 |
95556afb81f834614487f040cd644388f86991f806de1b7597feb2249bd51a04
|
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
- Download URL: nanofractal-0.1.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 3.2 MB
- Tags: CPython 3.9, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ae7c95418cb5627a76e6582229f9e12dd9ee49fd1d0f3716a6e78d5fe5aad11e
|
|
| MD5 |
26cbd17a15ccdc6f0b89bb7a0d895e26
|
|
| BLAKE2b-256 |
cfc8c12696401dd022e611a977be317fb7041cec04ed9f1319326fce559737de
|