napa-vision 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. napa_vision-0.4.0/CMakeLists.txt +48 -0
  2. napa_vision-0.4.0/LICENSE +77 -0
  3. napa_vision-0.4.0/MANIFEST.in +5 -0
  4. napa_vision-0.4.0/PKG-INFO +326 -0
  5. napa_vision-0.4.0/README.md +286 -0
  6. napa_vision-0.4.0/THIRD_PARTY_NOTICES +37 -0
  7. napa_vision-0.4.0/benchmarks/corpus.example.json +19 -0
  8. napa_vision-0.4.0/docs/guide.md +282 -0
  9. napa_vision-0.4.0/examples/c/detect_pgm.c +64 -0
  10. napa_vision-0.4.0/pyproject.toml +99 -0
  11. napa_vision-0.4.0/setup.cfg +4 -0
  12. napa_vision-0.4.0/setup.py +46 -0
  13. napa_vision-0.4.0/src/napa_vision/__init__.py +78 -0
  14. napa_vision-0.4.0/src/napa_vision/_native/__init__.py +1 -0
  15. napa_vision-0.4.0/src/napa_vision/_native/families/tag16h5.h +40 -0
  16. napa_vision-0.4.0/src/napa_vision/_native/families/tag25h9.h +41 -0
  17. napa_vision-0.4.0/src/napa_vision/_native/families/tag36h10.h +422 -0
  18. napa_vision-0.4.0/src/napa_vision/_native/families/tag36h11.h +133 -0
  19. napa_vision-0.4.0/src/napa_vision/_native/families/tagCircle21h7.h +42 -0
  20. napa_vision-0.4.0/src/napa_vision/_native/families/tagCircle49h12.h +10958 -0
  21. napa_vision-0.4.0/src/napa_vision/_native/families/tagCustom48h12.h +7071 -0
  22. napa_vision-0.4.0/src/napa_vision/_native/families/tagStandard41h12.h +388 -0
  23. napa_vision-0.4.0/src/napa_vision/_native/families/tagStandard52h13.h +8154 -0
  24. napa_vision-0.4.0/src/napa_vision/_native/families.h +43 -0
  25. napa_vision-0.4.0/src/napa_vision/_native/napa_core.c +2055 -0
  26. napa_vision-0.4.0/src/napa_vision/_native/napa_core.h +78 -0
  27. napa_vision-0.4.0/src/napa_vision/_version.py +3 -0
  28. napa_vision-0.4.0/src/napa_vision/benchmark.py +519 -0
  29. napa_vision-0.4.0/src/napa_vision/calibration.py +413 -0
  30. napa_vision-0.4.0/src/napa_vision/compat.py +200 -0
  31. napa_vision-0.4.0/src/napa_vision/covariance.py +705 -0
  32. napa_vision-0.4.0/src/napa_vision/geometry.py +187 -0
  33. napa_vision-0.4.0/src/napa_vision/napa_detector.py +244 -0
  34. napa_vision-0.4.0/src/napa_vision/processing.py +162 -0
  35. napa_vision-0.4.0/src/napa_vision/provenance.py +87 -0
  36. napa_vision-0.4.0/src/napa_vision/py.typed +1 -0
  37. napa_vision-0.4.0/src/napa_vision/solvers.py +822 -0
  38. napa_vision-0.4.0/src/napa_vision/tags.py +193 -0
  39. napa_vision-0.4.0/src/napa_vision/types.py +340 -0
  40. napa_vision-0.4.0/src/napa_vision/workflows.py +134 -0
  41. napa_vision-0.4.0/src/napa_vision.egg-info/PKG-INFO +326 -0
  42. napa_vision-0.4.0/src/napa_vision.egg-info/SOURCES.txt +57 -0
  43. napa_vision-0.4.0/src/napa_vision.egg-info/dependency_links.txt +1 -0
  44. napa_vision-0.4.0/src/napa_vision.egg-info/entry_points.txt +4 -0
  45. napa_vision-0.4.0/src/napa_vision.egg-info/requires.txt +21 -0
  46. napa_vision-0.4.0/src/napa_vision.egg-info/top_level.txt +1 -0
  47. napa_vision-0.4.0/tests/test_calibration.py +77 -0
  48. napa_vision-0.4.0/tests/test_compat.py +82 -0
  49. napa_vision-0.4.0/tests/test_covariance.py +537 -0
  50. napa_vision-0.4.0/tests/test_families.py +128 -0
  51. napa_vision-0.4.0/tests/test_homography.py +45 -0
  52. napa_vision-0.4.0/tests/test_napa_detector.py +289 -0
  53. napa_vision-0.4.0/tests/test_performance_matrix.py +131 -0
  54. napa_vision-0.4.0/tests/test_pose.py +230 -0
  55. napa_vision-0.4.0/tests/test_pose_quality.py +142 -0
  56. napa_vision-0.4.0/tests/test_processing.py +176 -0
  57. napa_vision-0.4.0/tests/test_replay_benchmark.py +201 -0
  58. napa_vision-0.4.0/tests/test_tags.py +118 -0
  59. napa_vision-0.4.0/tests/test_workflows.py +22 -0
@@ -0,0 +1,48 @@
1
+ # Napa. Copyright (c) 2026 Zachary Wilson. PolyForm Noncommercial License 1.0.0: see LICENSE.
2
+ # Napa's detector core as a plain C library, for C and C++ programs. Python users: pip install napa-vision.
3
+ #
4
+ # cmake -S . -B build && cmake --build build && cmake --install build --prefix /usr/local
5
+ #
6
+ # Then, in your project: find_package(napa) and target_link_libraries(app PRIVATE napa::napa).
7
+ cmake_minimum_required(VERSION 3.15)
8
+ file(STRINGS src/napa_vision/_version.py NAPA_VERSION_LINE REGEX "^__version__")
9
+ string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" NAPA_VERSION "${NAPA_VERSION_LINE}")
10
+ project(napa VERSION ${NAPA_VERSION} LANGUAGES C)
11
+
12
+ option(BUILD_SHARED_LIBS "Build a shared library" OFF)
13
+ option(NAPA_BUILD_EXAMPLES "Build the example program" OFF)
14
+ if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
15
+ set(CMAKE_BUILD_TYPE Release)
16
+ endif()
17
+
18
+ set(NAPA_NATIVE ${CMAKE_CURRENT_SOURCE_DIR}/src/napa_vision/_native)
19
+ add_library(napa ${NAPA_NATIVE}/napa_core.c)
20
+ add_library(napa::napa ALIAS napa)
21
+ target_include_directories(napa PUBLIC $<BUILD_INTERFACE:${NAPA_NATIVE}> $<INSTALL_INTERFACE:include>)
22
+ set_target_properties(napa PROPERTIES
23
+ C_STANDARD 99
24
+ C_STANDARD_REQUIRED ON
25
+ C_VISIBILITY_PRESET hidden
26
+ POSITION_INDEPENDENT_CODE ON
27
+ PUBLIC_HEADER ${NAPA_NATIVE}/napa_core.h)
28
+ if(MSVC)
29
+ target_compile_options(napa PRIVATE $<$<CONFIG:Release>:/O2>)
30
+ else()
31
+ # Signed overflow isn't relied on; -fno-wrapv keeps compilers free to optimise the inner loops.
32
+ target_compile_options(napa PRIVATE $<$<CONFIG:Release>:-O3> -fno-wrapv)
33
+ target_link_libraries(napa PRIVATE m)
34
+ endif()
35
+
36
+ include(GNUInstallDirs)
37
+ install(TARGETS napa EXPORT napaTargets
38
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
39
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
40
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
41
+ PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
42
+ install(FILES LICENSE THIRD_PARTY_NOTICES DESTINATION ${CMAKE_INSTALL_DATADIR}/doc/napa)
43
+ install(EXPORT napaTargets NAMESPACE napa:: FILE napaConfig.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/napa)
44
+
45
+ if(NAPA_BUILD_EXAMPLES)
46
+ add_executable(napa_detect_pgm examples/c/detect_pgm.c)
47
+ target_link_libraries(napa_detect_pgm PRIVATE napa)
48
+ endif()
@@ -0,0 +1,77 @@
1
+ Required Notice: Copyright (c) 2026 Zachary Wilson (https://github.com/zwatx/NAPA)
2
+ Required Notice: Napa ("Not Another Pixel Algorithm") was created by Zachary Wilson. Copies, forks, and works based on it must keep these notices and credit Zachary Wilson as Napa's author.
3
+ Required Notice: Commercial use needs a separate license from Zachary Wilson; ask through https://github.com/zwatx/NAPA.
4
+
5
+ # PolyForm Noncommercial License 1.0.0
6
+
7
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
8
+
9
+ ## Acceptance
10
+
11
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
12
+
13
+ ## Copyright License
14
+
15
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
16
+
17
+ ## Distribution License
18
+
19
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
20
+
21
+ ## Notices
22
+
23
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
24
+
25
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
26
+
27
+ ## Changes and New Works License
28
+
29
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
30
+
31
+ ## Patent License
32
+
33
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
34
+
35
+ ## Noncommercial Purposes
36
+
37
+ Any noncommercial purpose is a permitted purpose.
38
+
39
+ ## Personal Uses
40
+
41
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
42
+
43
+ ## Noncommercial Organizations
44
+
45
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
46
+
47
+ ## Fair Use
48
+
49
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
54
+
55
+ ## Patent Defense
56
+
57
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
58
+
59
+ ## Violations
60
+
61
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
62
+
63
+ ## No Liability
64
+
65
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
66
+
67
+ ## Definitions
68
+
69
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
70
+
71
+ **You** refers to the individual or entity agreeing to these terms.
72
+
73
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
74
+
75
+ **Your licenses** are all the licenses granted to you for the software under these terms.
76
+
77
+ **Use** means anything you do with the software requiring one of your licenses.
@@ -0,0 +1,5 @@
1
+ include LICENSE THIRD_PARTY_NOTICES CMakeLists.txt
2
+ recursive-include examples *.c
3
+ include benchmarks/corpus.example.json
4
+ include docs/guide.md
5
+ recursive-include src/napa_vision/_native *.c *.h
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.4
2
+ Name: napa-vision
3
+ Version: 0.4.0
4
+ Summary: Napa: a fast, dependency-free tag36h11 AprilTag detector with optional pose processing
5
+ Author: Zachary Wilson
6
+ License-Expression: PolyForm-Noncommercial-1.0.0
7
+ Project-URL: Homepage, https://github.com/zwatx/NAPA
8
+ Project-URL: Repository, https://github.com/zwatx/NAPA.git
9
+ Project-URL: Issues, https://github.com/zwatx/NAPA/issues
10
+ Keywords: apriltag,computer-vision,fiducial,pose-estimation,robotics
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
19
+ Requires-Python: >=3.11
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ License-File: THIRD_PARTY_NOTICES
23
+ Requires-Dist: numpy>=1.24
24
+ Provides-Extra: gui
25
+ Requires-Dist: opencv-python>=4.7; extra == "gui"
26
+ Provides-Extra: headless
27
+ Requires-Dist: opencv-python-headless>=4.7; extra == "headless"
28
+ Provides-Extra: benchmark
29
+ Requires-Dist: robotpy-apriltag==2026.2.2; extra == "benchmark"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest<10,>=8; extra == "test"
32
+ Requires-Dist: pytest-cov>=5; extra == "test"
33
+ Provides-Extra: dev
34
+ Requires-Dist: build>=1.2; extra == "dev"
35
+ Requires-Dist: pytest<10,>=8; extra == "dev"
36
+ Requires-Dist: pytest-cov>=5; extra == "dev"
37
+ Requires-Dist: ruff>=0.11; extra == "dev"
38
+ Requires-Dist: twine>=5; extra == "dev"
39
+ Dynamic: license-file
40
+
41
+ # Napa
42
+
43
+ **Not Another Pixel Algorithm**: Napa skips most of the pixels in each frame.
44
+
45
+ Napa is a fast AprilTag detector for any camera. It reads every AprilTag family, and `tag36h11`, the FRC family, by
46
+ default. Give it a frame and it returns tag IDs and sub-pixel
47
+ corners, plus poses if you supply a calibration. It opens no cameras, starts no threads, and publishes nothing, so a
48
+ webcam, a Raspberry Pi, a headset, recorded video, or a camera SDK can all feed it the same way.
49
+
50
+ The detector is a small C library with no dependencies, plus a Python binding that needs only numpy.
51
+
52
+ ## Compared with standard AprilTag
53
+
54
+ "Standard AprilTag" here means the upstream AprilTag 3 library at its default settings (`quad_decimate=2`). Both
55
+ detectors ran single-threaded on one pinned core of an Intel Core Ultra 7 256V laptop (Windows 11, Ubuntu-like Linux
56
+ under WSL2, best-performance power mode), taking turns on the same grayscale frames. Earlier published results came
57
+ from a desktop Intel i7-12700H, so absolute times differ from those.
58
+
59
+ The benchmark has **3,000 distinct frames holding 5,521 tags**. Every frame is rendered separately, with random tag
60
+ IDs, positions, sizes, angles, 3D tilt, backgrounds, and camera degradations, and every tag has exact sub-pixel
61
+ ground truth. [The benchmark guide](benchmarks/README.md) describes each scene.
62
+
63
+ **Tags found.** Napa found **99.8%** of tags. Standard AprilTag found 86.2%, and AprilTag at full resolution
64
+ (`quad_decimate=1`) 99.4%. Napa finds the small tags that standard AprilTag's decimation loses, and in every one of
65
+ the 60 scene and resolution combinations it found at least as many tags as standard AprilTag.
66
+
67
+ **Range.** Napa sees tags from much farther away. A tag looks smaller the farther it is, so the smallest tag a
68
+ detector can find sets its range. A separate sweep rendered 50 tags at each of eleven sizes, from 12 to 96 px across
69
+ the black square, with light noise and up to 15° of tilt, and counted what each detector found:
70
+
71
+ | Smallest black square found | Standard AprilTag | AprilTag at full resolution | Napa |
72
+ | --- | ---: | ---: | ---: |
73
+ | Every tag | 96 px | 40 px | **24 px** |
74
+ | Most tags | 64 px (36 of 50) | 32 px (39 of 50) | **20 px (46 of 50)** |
75
+
76
+ That is **4× the range of standard AprilTag** and 1.7× the range of AprilTag at full resolution. For example, a
77
+ 6.5-inch FRC tag seen by a 1280×720 camera with a 70° field of view is 24 px across at 6.3 m, where standard
78
+ AprilTag needs it within 1.6 m. To see farther still, lower `min_tag_px` (see [the guide](docs/guide.md#detection)).
79
+
80
+ **Speed.** Napa was faster on every one of the 3,000 frames:
81
+
82
+ - **Noise-free frames: 5.7× to 12.6× faster** (median per frame, by resolution). The slowest single frame was
83
+ still 4.8× faster.
84
+ - **Realistic frames: about 21× faster** (median). Every other scene includes at least light sensor noise
85
+ (σ ≈ 1.5 grey levels), which a real sensor always has. Standard AprilTag's time climbs steeply with noise: on a
86
+ 1280×720 frame its median is 1.35 ms with none, and 10–15 ms in most noisy scenes (overexposed and JPEG frames,
87
+ whose clipping and compression smooth the noise, take 2–2.6 ms). Napa stays under about 0.5 ms. The slowest single
88
+ frame here was 3.6× faster.
89
+ - Against AprilTag at full resolution, Napa was 87× faster (median) and at least 11.8× faster on every frame.
90
+
91
+ These are the 1280×720 results; the guide has every resolution:
92
+
93
+ | Scene, 1280×720 | Tags | AprilTag | Napa | Speedup (median) | Slowest frame | Tags found (AprilTag → Napa) |
94
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
95
+ | Noise-free | 104 | 1.35 ms | 0.16 ms | 8× | 6.9× | 104 → 104 |
96
+ | One tag | 50 | 13.1 ms | 0.20 ms | 65× | 46× | 50 → 50 |
97
+ | 2–12 tags | 354 | 9.64 ms | 0.49 ms | 20× | 8.0× | 354 → 354 |
98
+ | Small tags (32–53 px) | 120 | 11.6 ms | 0.29 ms | 41× | 11.6× | 0 → 120 |
99
+ | Large tags | 50 | 10.6 ms | 0.29 ms | 37× | 13.4× | 50 → 50 |
100
+ | Steep angle (40–65°) | 112 | 10.6 ms | 0.35 ms | 31× | 11.6× | 109 → 111 |
101
+ | Defocus blur | 89 | 10.2 ms | 0.32 ms | 32× | 9.2× | 89 → 89 |
102
+ | Motion blur | 94 | 10.2 ms | 0.38 ms | 17× | 9.1× | 94 → 94 |
103
+ | Heavy noise (σ 4–14) | 104 | 9.66 ms | 0.52 ms | 19× | 6.0× | 104 → 104 |
104
+ | Uneven lighting | 103 | 11.0 ms | 0.37 ms | 29× | 12.0× | 103 → 103 |
105
+ | Low contrast | 100 | 13.9 ms | 0.23 ms | 66× | 39× | 95 → 99 |
106
+ | Underexposed | 99 | 11.3 ms | 0.33 ms | 35× | 13.6× | 99 → 99 |
107
+ | Overexposed | 112 | 2.04 ms | 0.26 ms | 7× | 5.4× | 112 → 112 |
108
+ | JPEG artefacts | 100 | 2.58 ms | 0.25 ms | 8× | 4.2× | 100 → 100 |
109
+ | Busy background | 87 | 10.9 ms | 0.30 ms | 38× | 11.3× | 87 → 87 |
110
+ | At the frame edge | 105 | 10.8 ms | 0.32 ms | 35× | 12.2× | 103 → 105 |
111
+ | Dark background | 105 | 10.3 ms | 0.29 ms | 38× | 12.0× | 91 → 105 |
112
+ | No tag: empty / clutter / decoys | — | 9–15 ms | 0.12–0.31 ms | 19–101× | 5.1× | — |
113
+
114
+ Times are the median detector call. Speedups are per frame: AprilTag's time divided by Napa's on the same frame.
115
+ "Slowest frame" is the smallest of those ratios.
116
+
117
+ **Corners.** For each tag, the benchmark measured the worst corner's distance from the true corner. On the 4,754
118
+ tags both detectors found, Napa's median error was **0.020 px** against AprilTag's 0.099 px, and its 95th percentile
119
+ **0.088 px** against 0.303 px. Napa was more accurate at the median and 95th percentile in every scene. The exception
120
+ is the single worst case under heavy motion blur: with 11 px of blur on a tag about 50 px across, one of Napa's
121
+ corners was 2.5 px off, where AprilTag's worst in that scene was 0.98 px.
122
+
123
+ **False detections.** In 3,000 frames, Napa reported one wrong ID: a tag-shaped decoy whose random payload happened
124
+ to fall within 2 bits of a real code, the same tolerance standard AprilTag uses. Standard AprilTag reported none,
125
+ and full-resolution AprilTag 3. On a separate test of 80,000 such decoys, Napa reported none.
126
+
127
+ ## Every tag family
128
+
129
+ Napa reads all nine of upstream AprilTag's families: tag36h11 (the default), tag16h5, tag25h9, tag36h10,
130
+ tagStandard41h12, tagStandard52h13, tagCircle21h7, tagCircle49h12, and tagCustom48h12. Pass the family when you create
131
+ a detector: `NapaDetector(family="tagStandard41h12")`. tag36h11's detection is unchanged by the other families: it
132
+ gives exactly the same results as before they were added.
133
+
134
+ A family-by-family run over the same 20 scenes (each with its own family's tags, spaced by their printed size) gave:
135
+
136
+ | Family | Tags | Found: AprilTag → Napa | False IDs: AprilTag → Napa | Median corner error: AprilTag → Napa | Speedup: median (slowest frame) |
137
+ | --- | ---: | ---: | ---: | ---: | ---: |
138
+ | tag36h11 | 289 | 288 → 287 | 0 → 0 | 0.097 → 0.022 px | 40× (7.9×) |
139
+ | tag16h5 | 282 | 282 → 280 | **581 → 0** | 0.107 → 0.022 px | 38× (5.5×) |
140
+ | tag25h9 | 287 | 286 → 286 | 3 → 0 | 0.098 → 0.021 px | 36× (7.2×) |
141
+ | tagStandard41h12 | 232 | 232 → 231 | 0 → 0 | 0.097 → 0.025 px | 42× (8.5×) |
142
+ | tagCircle21h7 | 231 | 231 → 230 | 9 → 0 | 0.097 → 0.025 px | 42× (9.3×) |
143
+
144
+ Frames were 640×480 and 1280×720. Speedups are per frame, from a separate timing pass over 80 frames per family on
145
+ one pinned core.
146
+
147
+ Every tag that only AprilTag found was small (40–60 px) and steeply tilted, where the white quiet zone narrows to a
148
+ pixel or two: either its narrow side was under `min_tag_px`, or a dark line in the background touched the tag and
149
+ joined its border (see [Limits](#limits)). On tagStandard52h13, tagCircle49h12, tagCustom48h12, and tag36h10 (which the reference
150
+ Python binding doesn't include), Napa found 99.2–99.6% of tags with no false IDs. Upstream's decode table for the
151
+ three big families needs several gigabytes, which is one more reason to prefer Napa for them: its tables take
152
+ megabytes.
153
+
154
+ tag16h5's 30 codes are only 5 bits apart, so a decoder that corrects two wrong bits, as AprilTag does by default,
155
+ accepts about one in four random tag-shaped patterns. Napa sets each family's default correction so that chance
156
+ matches stay as rare as they are for tag36h11, and adds a "soft" correction for families like tag16h5: a few bits
157
+ may still be corrected, but only if they're exactly the payload's least certain readings. [The API
158
+ reference](docs/api.md#tag-families) has the details.
159
+
160
+ These are synthetic frames on one laptop CPU. Measure on your own camera and hardware before relying on a specific
161
+ number: `napa-benchmark` scores Napa on your own annotated images (see [the guide](docs/guide.md)).
162
+
163
+ ## Why it's faster
164
+
165
+ Standard AprilTag treats each frame as a large image-processing job. It thresholds every pixel, segments the whole
166
+ image into regions, and fits quadrilaterals to every region's boundary before it asks whether anything is a tag.
167
+ Napa asks that question as early and as cheaply as it can.
168
+
169
+ ![Napa on one frame, stage by stage: the rake keeps every third pixel, flat tiles are skipped, dark samples become runs and blobs, shapes that can't be tags are dropped, and only the tags get edge fitting and decoding at full resolution.](docs/napa-pipeline.webp)
170
+
171
+ *One synthetic frame, stage by stage. Every overlay is computed from the frame by a port of Napa's pipeline.*
172
+
173
+ ### 1. The rake: sample sparsely, with a guarantee
174
+
175
+ A tag's black border is one cell wide, and a tag is 8 cells across. Sample every few pixels and you're guaranteed to
176
+ land on the border of every tag above a minimum size, so skipping the pixels in between can't skip a tag.
177
+
178
+ Napa drags a "rake" across the frame, taking one sample every ¾ of the smallest cell in each direction. Blur greys a
179
+ cell's edges but leaves its core dark, and ¾-cell spacing always puts a sample in that core. For the default 32 px
180
+ minimum tag, that's one pixel in nine. Coarser copies of the rake (2×, 4×, 8×, …) catch larger tags; each is built
181
+ from the one before, not from the image, and together they add only a third again to the cost.
182
+
183
+ Each tag is handled at the level where it's between about 16 and 64 samples across. Below that, its one-cell white
184
+ margin can fall between samples and the tag merges with a dark background; above it, the middle of its border is
185
+ too far from any white to stand out. Every tag size has such a level.
186
+
187
+ ### 2. String search on a matrix: skip what can't match
188
+
189
+ Fast string search (Boyer–Moore) doesn't compare every character. One cheap check can prove that no match starts in
190
+ the next several positions, so the search jumps past them.
191
+
192
+ Napa applies the same idea to the image. It splits the raked samples into small tiles and records each tile's
193
+ darkest and brightest value. A tile without contrast can't contain a tag's edge, so Napa skips it and does no work
194
+ on its pixels. The contrast needed rises automatically with sensor noise, measured from the frame itself and region
195
+ by region, so a noisy or textured part of the frame doesn't blind the rest. On a very noisy frame, each rake sample
196
+ averages the block of pixels it stands for instead, which divides the noise by the stride.
197
+
198
+ The same idea speeds up decoding. A tag's 36 payload bits must be matched against 587 codes in 4 rotations, allowing
199
+ up to 3 wrong bits. Any code within 3 bits of the payload must match at least one of four 9-bit blocks *exactly* (the
200
+ pigeonhole principle). So Napa looks those blocks up in a table and compares only the handful of codes that share
201
+ one, instead of all 2,348.
202
+
203
+ ### 3. Old-school vector math
204
+
205
+ Every pass over the samples is written as whole-row vector operations, the classic Apple AltiVec/vDSP approach. One
206
+ SSE2 or NEON instruction compares 16 samples with their thresholds and packs the results into a bit mask. Runs of
207
+ dark samples then fall out of the masks with a count-trailing-zeros instruction per run, not a test per pixel.
208
+ Runs that touch from row to row are joined into blobs with union-find, so the cost follows the number of runs, not
209
+ the number of dark pixels. Gathering every third pixel uses a byte shuffle (SSSE3, chosen at run time) or NEON's
210
+ de-interleaving load.
211
+
212
+ ### 4. Reject early, measure carefully
213
+
214
+ Each blob of plausible size gets a rough quadrilateral from its extreme points. A blob can't be a tag if it's darker
215
+ overall than any real code allows: every tag36h11 code has at least 10 white cells. Then Napa:
216
+
217
+ 1. Fits each side to its edge at full resolution.
218
+ 2. Decodes straight away with a loose tolerance, reading the border first. Text, texture, and checkerboards are
219
+ thrown out here, before any expensive refinement.
220
+ 3. Refines each side coarse to fine, then with a final pass that measures the edge every 1.5 px along it. Each
221
+ measurement is the centre of the edge's gradient, which is exact for real, pixel-integrated edges. Where blur is
222
+ wider than about half a cell, the edge is located instead where the image crosses halfway between the black and
223
+ white on either side.
224
+ 4. Intersects the four lines to get the corners, and decodes the payload.
225
+
226
+ Some images fool a plain edge fit, so Napa checks for them:
227
+
228
+ - **A grey quiet zone on a bright background** makes the margin look dark, so the first outline is the margin's.
229
+ If the decode fails, Napa tries the outline's inner 8/10, where the black square would be.
230
+ - **A side fitted one cell out**, on the margin's outer edge, shows up as border cells that read white. Napa moves
231
+ that side in by a cell and refits it.
232
+ - **Clipped (saturated) exposure** shrinks every black region by the same amount. The black border must be exactly
233
+ one cell wide, so Napa measures that shrinkage on the tag itself and undoes it.
234
+
235
+ ## Obstructions
236
+
237
+ Napa corrects up to 3 wrong payload bits (standard AprilTag defaults to 2), so a hand, cable, or glare spot over part
238
+ of a tag often still decodes. It accepts a third corrected bit only when the three wrong cells touch, as they do
239
+ under a real obstruction: a random pattern that happens to fall within three bits of a code has its errors
240
+ scattered, so this keeps chance matches out. In the benchmark's obstruction sweep (864 tags, each partly covered by
241
+ a white or grey patch), Napa recovered 357 tags and standard AprilTag 330.
242
+
243
+ No AprilTag detector can find a tag whose black border or corners are hidden, because the border is found before any
244
+ bits are read. Obstruction tolerance is best-effort, with no guaranteed percentage.
245
+
246
+ ## Limits
247
+
248
+ - Tags smaller than `min_tag_px` (default 32 px across the black square) may be missed. Lower it to find smaller tags,
249
+ at some cost in speed. A steeply tilted tag counts by its narrowest side.
250
+ - Tags need their white quiet zone, as printed tags have. A tag whose quiet zone runs straight into dark, busy
251
+ texture can merge with it and be missed. Steep tilt makes this likelier, because it narrows the quiet zone: a small
252
+ tag tilted 60° can join a dark line that touches it.
253
+ - Tags with less than `min_contrast` (default 20) grey levels between their black and white are skipped. Lower it for
254
+ washed-out scenes, at a cost in speed on every frame.
255
+ - Motion blur longer than a cell (an eighth of the tag) is the hardest case for corner accuracy: most corners stay
256
+ within a tenth of a pixel, but the worst can be off by a few pixels.
257
+ - Prebuilt wheels cover Linux (x86-64, ARM64), macOS (Apple silicon, Intel), and Windows x64. Elsewhere, pip builds
258
+ from source, which needs a C99 compiler.
259
+
260
+ ## Quick start
261
+
262
+ Requires Python 3.11 or newer.
263
+
264
+ ```bash
265
+ python -m pip install napa-vision # the detector: needs only numpy
266
+ python -m pip install "napa-vision[headless]" # adds OpenCV, for poses, calibration, and napa-benchmark
267
+ ```
268
+
269
+ ```python
270
+ from napa_vision import NapaDetector
271
+
272
+ detector = NapaDetector()
273
+ for tag in detector.detect(image): # uint8 grayscale, BGR, or BGRA
274
+ print(tag.tag_id, tag.corners)
275
+ ```
276
+
277
+ Each detection also has `tag.center` and `tag.homography`, as upstream AprilTag reports them.
278
+
279
+ Already using `pupil_apriltags` or `dt-apriltags`? Change the import and keep the rest of your code:
280
+
281
+ ```python
282
+ from napa_vision.compat import Detector # was: from pupil_apriltags import Detector
283
+
284
+ detector = Detector(families="tag36h11")
285
+ detections = detector.detect(gray, estimate_tag_pose=True, camera_params=(fx, fy, cx, cy), tag_size=0.1651)
286
+ ```
287
+
288
+ Results keep upstream's conventions (corner order, pixel origin, and pose frames), so code written against those
289
+ libraries works unchanged.
290
+
291
+ For metric poses, use `VisionProcessor(calibration=load_calibration_data("camera.json"), tag_size_m=...)`. Every pose
292
+ comes with a covariance, so it can go straight into a Kalman filter, including WPILib's pose estimators. Use one
293
+ detector per camera stream. The native call releases the GIL, so separate streams can run on separate threads.
294
+
295
+ The C library has a plain C API (`src/napa_vision/_native/napa_core.h`), so other languages can call it directly.
296
+ C and C++ projects can build and install it with CMake and link `napa::napa`; see
297
+ [the guide](docs/guide.md#from-c-or-another-language).
298
+ [The guide](docs/guide.md) covers tuning, poses, Kalman filters, coordinate conventions, and calibration, and
299
+ [the API reference](docs/api.md) lists every class, method, and field.
300
+
301
+ ## Command-line tools
302
+
303
+ | Command | Purpose |
304
+ | --- | --- |
305
+ | `napa-tag` | Write tags to print: SVG at an exact printed size, or PNG |
306
+ | `napa-calibrate` | Measure camera intrinsics from checkerboard photographs |
307
+ | `napa-benchmark` | Score Napa on your own annotated images, with optional recall, precision, and latency gates |
308
+
309
+ ## Development
310
+
311
+ ```bash
312
+ python -m pip install -e ".[headless,dev,benchmark]"
313
+ ruff format --check . && ruff check .
314
+ pytest --cov=napa_vision
315
+ ```
316
+
317
+ ## License
318
+
319
+ Copyright © 2026 Zachary Wilson. Napa is free for noncommercial use under the
320
+ [PolyForm Noncommercial License 1.0.0](LICENSE): personal projects, research, education, student robotics teams,
321
+ and other noncommercial organizations can use, modify, and share it. Copies, forks, and works based on Napa must
322
+ include the license and its notices crediting Zachary Wilson. Commercial use, including selling Napa or products
323
+ built on it, needs a separate license; ask through [the repository](https://github.com/zwatx/NAPA).
324
+
325
+ The tag families' codewords come from [AprilTag](https://github.com/AprilRobotics/apriltag) under its BSD license; see
326
+ [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES).