Update Raspberry Pi 5 reproduction package
Browse filesReplace the earlier prototype package with the public runtime layout, converter and Python API; add the patched Ultralytics benchmark workflow, licensed validation dataset integration, fresh Marvik metadata, and verified reference results. The PyTorch checkpoint is no longer redistributed.
- .gitattributes +1 -0
- LICENSE +201 -0
- README.md +59 -32
- compiler/{minimal_runtime/compiler → converter}/__init__.py +0 -0
- compiler/{minimal_runtime/compiler → converter}/checkpoint_decoder.py +1 -1
- compiler/{minimal_runtime/compiler → converter}/checkpoint_intake.py +1 -1
- compiler/{minimal_runtime/compiler/compiler_io.py → converter/converter_io.py} +2 -2
- compiler/{minimal_runtime/compiler → converter}/export_yolo26n.py +1 -2
- compiler/{minimal_runtime/compiler → converter}/model_contract.py +3 -7
- compiler/{minimal_runtime/compiler → converter}/model_format.py +21 -66
- compiler/{minimal_runtime/compiler → converter}/model_validation.py +1 -1
- compiler/{minimal_runtime/compiler → converter}/validate.py +1 -1
- compiler/{minimal_runtime/compiler → converter}/weight_contract.py +0 -0
- compiler/{minimal_runtime/compiler → converter}/weight_lowering.py +1 -1
- compiler/minimal_runtime/__init__.py +0 -1
- metadata.yaml +5 -5
- model/README.md +6 -0
- model/yolo26n.pt +0 -3
- python-api/__init__.py +0 -10
- python-api/arm_yolo26n/__init__.py +23 -0
- python-api/arm_yolo26n/__pycache__/__init__.cpython-313.pyc +0 -0
- python-api/arm_yolo26n/__pycache__/_model.cpython-313.pyc +0 -0
- python-api/arm_yolo26n/__pycache__/detector.cpython-313.pyc +0 -0
- python-api/arm_yolo26n/__pycache__/errors.cpython-313.pyc +0 -0
- python-api/arm_yolo26n/__pycache__/yolo26n.cpython-313.pyc +0 -0
- python-api/arm_yolo26n/_model.py +174 -0
- python-api/arm_yolo26n/detector.py +469 -0
- python-api/arm_yolo26n/errors.py +7 -0
- python-api/{yolo26n.py → arm_yolo26n/yolo26n.py} +129 -222
- python-api/errors.py +0 -7
- repro.py +92 -90
- results/REFERENCE.md +23 -11
- runtime/arm_yolo26n_pi5_model/arm-yolo26n-pi5.yaml +3 -0
- runtime/arm_yolo26n_pi5_model/bundle/licenses/libmd/COPYRIGHT.debian +0 -177
- runtime/arm_yolo26n_pi5_model/bundle/manifest.json +0 -104
- runtime/arm_yolo26n_pi5_model/{bundle/native-runtime.so → lib/libarm-yolo26n.so.0.1.0} +2 -2
- runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/cpuinfo/LICENSE +0 -0
- runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/fxdiv/LICENSE +0 -0
- runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/pthreadpool/LICENSE +0 -0
- runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/xnnpack/LICENSE +0 -0
- runtime/arm_yolo26n_pi5_model/model/manifest.json +1 -0
- runtime/arm_yolo26n_pi5_model/{bundle → model}/model.bin +2 -2
- ultralytics/arm_yolo26_pi5_backend.py +0 -51
- ultralytics/arm_yolo26n_pi5_backend.py +81 -0
- ultralytics/benchmark.py +16 -11
- ultralytics/ultralytics-8.4.108-arm-yolo26-pi5.patch +4 -4
.gitattributes
CHANGED
|
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
runtime/arm_yolo26n_pi5_model/bundle/native-runtime.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
runtime/arm_yolo26n_pi5_model/bundle/native-runtime.so filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
runtime/arm_yolo26n_pi5_model/lib/libarm-yolo26n.so.0.1.0 filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
CHANGED
|
@@ -1,57 +1,84 @@
|
|
| 1 |
---
|
| 2 |
-
license:
|
| 3 |
---
|
| 4 |
|
| 5 |
# YOLO26n Raspberry Pi 5 benchmark reproduction
|
| 6 |
|
| 7 |
-
This package contains
|
| 8 |
-
|
| 9 |
-
It targets a Raspberry Pi 5 running 64-bit
|
|
|
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
```sh
|
| 19 |
python3 repro.py setup
|
| 20 |
python3 repro.py benchmark
|
| 21 |
```
|
| 22 |
|
| 23 |
-
`repro.py setup` installs
|
| 24 |
-
|
| 25 |
-
backend.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
|
| 29 |
-
Before starting,
|
| 30 |
that could distort performance, including RAM-backed model or dataset paths,
|
| 31 |
-
low available memory, dirty
|
| 32 |
-
scanning, and swap activity.
|
| 33 |
|
| 34 |
-
The benchmark downloads `Arm/coco-128-by-sa` at
|
| 35 |
-
`
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
per-image CC BY 2.0 or CC BY-SA 2.0 license records remain in the downloaded
|
| 39 |
-
`LICENSE_MANIFEST.csv`.
|
| 40 |
|
| 41 |
-
The result is written
|
| 42 |
-
`results/REFERENCE.md`.
|
| 43 |
|
| 44 |
-
## Export your own model
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
|
| 49 |
```sh
|
| 50 |
python3 repro.py export
|
| 51 |
python3 repro.py benchmark
|
| 52 |
```
|
| 53 |
|
| 54 |
-
|
| 55 |
-
`build/arm_yolo26n_pi5_model/`
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
license: other
|
| 3 |
---
|
| 4 |
|
| 5 |
# YOLO26n Raspberry Pi 5 benchmark reproduction
|
| 6 |
|
| 7 |
+
This private package contains Arm's compiled YOLO26n runtime, its Python API,
|
| 8 |
+
the model converter, and the integration needed to run the converted model
|
| 9 |
+
through Ultralytics `benchmark()`. It targets a Raspberry Pi 5 running 64-bit
|
| 10 |
+
Raspberry Pi OS Trixie.
|
| 11 |
|
| 12 |
+
The runtime, converter, Python API, and Ultralytics adapter correspond to
|
| 13 |
+
commit `9056a527deaf2adc6123f43424d07672753a9524` of
|
| 14 |
+
[Arm-Runtime-for-YOLO26-on-Raspberry-Pi-5](https://github.com/arm/Arm-Runtime-for-YOLO26-on-Raspberry-Pi-5).
|
| 15 |
|
| 16 |
+
## Benchmark the supplied converted model
|
| 17 |
+
|
| 18 |
+
Internet access is required during setup and while downloading the official
|
| 19 |
+
YOLO26n checkpoint and the validation dataset. Authenticate with `hf auth
|
| 20 |
+
login`, or provide an `HF_TOKEN` that can read the private dataset repository.
|
| 21 |
|
| 22 |
```sh
|
| 23 |
python3 repro.py setup
|
| 24 |
python3 repro.py benchmark
|
| 25 |
```
|
| 26 |
|
| 27 |
+
`repro.py setup` installs Ultralytics 8.4.108 and applies the included patch
|
| 28 |
+
that registers this runtime as an Ultralytics export format and inference
|
| 29 |
+
backend. The benchmark therefore uses Ultralytics `benchmark()`, with an
|
| 30 |
+
explicit integration patch because upstream Ultralytics does not natively know
|
| 31 |
+
how to load this runtime.
|
| 32 |
|
| 33 |
+
Before starting, the benchmark checks for filesystem and memory conditions
|
| 34 |
that could distort performance, including RAM-backed model or dataset paths,
|
| 35 |
+
low available memory, dirty or writeback pressure, page-cache refaults,
|
| 36 |
+
reclaim scanning, and swap activity.
|
| 37 |
|
| 38 |
+
The benchmark downloads `Arm/coco-128-by-sa` at revision
|
| 39 |
+
`f8af0a317c0b54c3dbd38c70b9757dccb8b02460` and uses its `data.yaml` file.
|
| 40 |
+
The dataset, its image licenses, and its per-image attribution records remain
|
| 41 |
+
in the dataset repository and are not redistributed here.
|
|
|
|
|
|
|
| 42 |
|
| 43 |
+
The benchmark result is written under `results/`. The supplied reference
|
| 44 |
+
measurement is in `results/REFERENCE.md`.
|
| 45 |
|
| 46 |
+
## Export your own compatible model
|
| 47 |
|
| 48 |
+
After setup, place a compatible YOLO26n checkpoint at `model/yolo26n.pt`, then
|
| 49 |
+
run:
|
| 50 |
|
| 51 |
```sh
|
| 52 |
python3 repro.py export
|
| 53 |
python3 repro.py benchmark
|
| 54 |
```
|
| 55 |
|
| 56 |
+
Export creates a runnable model directory under
|
| 57 |
+
`build/arm_yolo26n_pi5_model/`. Benchmark automatically selects it and checks
|
| 58 |
+
that the source checkpoint matches the converted model. No PyTorch checkpoint
|
| 59 |
+
is distributed in this repository; the stock benchmark downloads and verifies
|
| 60 |
+
the official checkpoint when needed.
|
| 61 |
+
|
| 62 |
+
## About this version
|
| 63 |
+
|
| 64 |
+
Original model: YOLO26n by Ultralytics.
|
| 65 |
+
|
| 66 |
+
Optimization and conversion: Arm converted the original model into the fixed
|
| 67 |
+
model artifact used by a purpose-built native runtime for Raspberry Pi 5. No
|
| 68 |
+
retraining or fine-tuning was performed as part of this conversion.
|
| 69 |
+
|
| 70 |
+
The runtime software is provided under Apache-2.0 as described in `LICENSE`.
|
| 71 |
+
The original model and converted model artifact are subject to AGPL-3.0 unless
|
| 72 |
+
you have a separate written license agreement with Ultralytics. Third-party
|
| 73 |
+
runtime components retain the licenses included beside the compiled runtime.
|
| 74 |
+
|
| 75 |
+
For information about the original model, including its development, training
|
| 76 |
+
data, intended uses, and limitations, refer to the Ultralytics repository.
|
| 77 |
+
Information supplied there by Ultralytics or other third parties has not been
|
| 78 |
+
independently verified by Arm unless expressly stated otherwise.
|
| 79 |
+
|
| 80 |
+
This package is a reference implementation for evaluation on Arm-based
|
| 81 |
+
systems. It is not a production-ready or supported solution. It is provided
|
| 82 |
+
as is, without representations or warranties. You are responsible for
|
| 83 |
+
reviewing the applicable license terms and independently evaluating the model,
|
| 84 |
+
its outputs, and its suitability for your intended use.
|
compiler/{minimal_runtime/compiler → converter}/__init__.py
RENAMED
|
File without changes
|
compiler/{minimal_runtime/compiler → converter}/checkpoint_decoder.py
RENAMED
|
@@ -13,7 +13,7 @@ from dataclasses import dataclass
|
|
| 13 |
from pathlib import PurePosixPath
|
| 14 |
from typing import Never
|
| 15 |
|
| 16 |
-
from .
|
| 17 |
|
| 18 |
# Torch's ZIP format can use several pickle protocols for the same object
|
| 19 |
# graph. Accept their passive container and memoization opcodes; find_class
|
|
|
|
| 13 |
from pathlib import PurePosixPath
|
| 14 |
from typing import Never
|
| 15 |
|
| 16 |
+
from .converter_io import RejectedInput
|
| 17 |
|
| 18 |
# Torch's ZIP format can use several pickle protocols for the same object
|
| 19 |
# graph. Accept their passive container and memoization opcodes; find_class
|
compiler/{minimal_runtime/compiler → converter}/checkpoint_intake.py
RENAMED
|
@@ -6,7 +6,7 @@ from __future__ import annotations
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from .checkpoint_decoder import DecodedModule, decode_checkpoint_archive
|
| 9 |
-
from .
|
| 10 |
from .model_contract import ValidatedYolo26n
|
| 11 |
from .model_validation import validate_yolo26n_model
|
| 12 |
|
|
|
|
| 6 |
from pathlib import Path
|
| 7 |
|
| 8 |
from .checkpoint_decoder import DecodedModule, decode_checkpoint_archive
|
| 9 |
+
from .converter_io import RejectedInput
|
| 10 |
from .model_contract import ValidatedYolo26n
|
| 11 |
from .model_validation import validate_yolo26n_model
|
| 12 |
|
compiler/{minimal_runtime/compiler/compiler_io.py → converter/converter_io.py}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Standard-library JSON, hashing, and atomic writes for
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
@@ -14,7 +14,7 @@ type JsonObject = dict[str, JsonValue]
|
|
| 14 |
|
| 15 |
|
| 16 |
class RejectedInput(RuntimeError):
|
| 17 |
-
"""A caller-controlled
|
| 18 |
|
| 19 |
|
| 20 |
def sha256_bytes(data: bytes) -> str:
|
|
|
|
| 1 |
+
"""Standard-library JSON, hashing, and atomic writes for converted artifacts."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 14 |
|
| 15 |
|
| 16 |
class RejectedInput(RuntimeError):
|
| 17 |
+
"""A caller-controlled converter input failed validation."""
|
| 18 |
|
| 19 |
|
| 20 |
def sha256_bytes(data: bytes) -> str:
|
compiler/{minimal_runtime/compiler → converter}/export_yolo26n.py
RENAMED
|
@@ -14,7 +14,7 @@ from .checkpoint_intake import (
|
|
| 14 |
decode_checkpoint,
|
| 15 |
read_checkpoint,
|
| 16 |
)
|
| 17 |
-
from .
|
| 18 |
from .model_contract import ModelResolution
|
| 19 |
from .model_format import pack_model
|
| 20 |
from .weight_lowering import lower_yolo26n_weights
|
|
@@ -77,7 +77,6 @@ def export_model(
|
|
| 77 |
lower_yolo26n_weights(model),
|
| 78 |
model_resolution.resolution,
|
| 79 |
model.class_count,
|
| 80 |
-
model.classification_width,
|
| 81 |
)
|
| 82 |
except ValueError as error:
|
| 83 |
raise RejectedInput(f"could not prepare YOLO26n runtime weights: {error}") from error
|
|
|
|
| 14 |
decode_checkpoint,
|
| 15 |
read_checkpoint,
|
| 16 |
)
|
| 17 |
+
from .converter_io import JsonObject, RejectedInput, atomic_write, canonical_bytes, sha256_bytes
|
| 18 |
from .model_contract import ModelResolution
|
| 19 |
from .model_format import pack_model
|
| 20 |
from .weight_lowering import lower_yolo26n_weights
|
|
|
|
| 77 |
lower_yolo26n_weights(model),
|
| 78 |
model_resolution.resolution,
|
| 79 |
model.class_count,
|
|
|
|
| 80 |
)
|
| 81 |
except ValueError as error:
|
| 82 |
raise RejectedInput(f"could not prepare YOLO26n runtime weights: {error}") from error
|
compiler/{minimal_runtime/compiler → converter}/model_contract.py
RENAMED
|
@@ -1,13 +1,12 @@
|
|
| 1 |
-
"""Typed contracts between YOLO26n checkpoint intake and
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import hashlib
|
| 6 |
import math
|
| 7 |
from dataclasses import dataclass
|
| 8 |
from enum import Enum
|
| 9 |
|
| 10 |
-
from .
|
| 11 |
|
| 12 |
# The native schedule accepts these square inputs and derives all intermediate
|
| 13 |
# tensor extents from them. Values outside this closed set are rejected by both
|
|
@@ -162,11 +161,8 @@ def _runtime_weight_names() -> tuple[str, ...]:
|
|
| 162 |
|
| 163 |
RUNTIME_WEIGHT_NAMES = _runtime_weight_names()
|
| 164 |
# Stable slot numbers are assigned by semantic name. The fixed topology above
|
| 165 |
-
# therefore determines
|
| 166 |
RUNTIME_WEIGHT_SLOT_COUNT = len(RUNTIME_WEIGHT_NAMES)
|
| 167 |
-
RUNTIME_WEIGHT_SCHEMA_SHA256 = hashlib.sha256(
|
| 168 |
-
b"\0".join(name.encode("utf-8") for name in RUNTIME_WEIGHT_NAMES)
|
| 169 |
-
).hexdigest()
|
| 170 |
|
| 171 |
_RUNTIME_TARGET = "linux-aarch64-raspberry-pi-5"
|
| 172 |
_RUNTIME_THREAD_COUNT = 4
|
|
|
|
| 1 |
+
"""Typed contracts between YOLO26n checkpoint intake and model conversion."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import math
|
| 6 |
from dataclasses import dataclass
|
| 7 |
from enum import Enum
|
| 8 |
|
| 9 |
+
from .converter_io import JsonObject
|
| 10 |
|
| 11 |
# The native schedule accepts these square inputs and derives all intermediate
|
| 12 |
# tensor extents from them. Values outside this closed set are rejected by both
|
|
|
|
| 161 |
|
| 162 |
RUNTIME_WEIGHT_NAMES = _runtime_weight_names()
|
| 163 |
# Stable slot numbers are assigned by semantic name. The fixed topology above
|
| 164 |
+
# therefore determines the slot count and order used by both exporter and runtime.
|
| 165 |
RUNTIME_WEIGHT_SLOT_COUNT = len(RUNTIME_WEIGHT_NAMES)
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
_RUNTIME_TARGET = "linux-aarch64-raspberry-pi-5"
|
| 168 |
_RUNTIME_THREAD_COUNT = 4
|
compiler/{minimal_runtime/compiler → converter}/model_format.py
RENAMED
|
@@ -2,19 +2,14 @@
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
-
import hashlib
|
| 6 |
import struct
|
| 7 |
from dataclasses import dataclass
|
| 8 |
|
| 9 |
-
from .compiler_io import sha256_bytes
|
| 10 |
from .model_contract import (
|
| 11 |
BOX_COORDINATE_CHANNELS,
|
| 12 |
RUNTIME_WEIGHT_NAMES,
|
| 13 |
-
RUNTIME_WEIGHT_SCHEMA_SHA256,
|
| 14 |
RUNTIME_WEIGHT_SLOT_COUNT,
|
| 15 |
SUPPORTED_RESOLUTIONS,
|
| 16 |
-
classification_width_for_classes,
|
| 17 |
-
detection_output_channels,
|
| 18 |
)
|
| 19 |
from .weight_contract import LoweredWeight
|
| 20 |
|
|
@@ -27,16 +22,18 @@ from .weight_contract import LoweredWeight
|
|
| 27 |
_MAGIC = b"Y26MOD01"
|
| 28 |
_VERSION = 1
|
| 29 |
|
| 30 |
-
# XNNPACK uses 64-byte-aligned static buffers.
|
| 31 |
-
#
|
| 32 |
-
#
|
| 33 |
-
# bytearray initialization supplies the required zero padding.
|
| 34 |
_ALIGNMENT = 64
|
| 35 |
-
_HEADER_BYTES =
|
| 36 |
-
_ENTRY_BYTES =
|
| 37 |
-
_HEADER = struct.Struct("<
|
| 38 |
-
_ENTRY = struct.Struct("<
|
| 39 |
_UINT32_MAX = (1 << 32) - 1
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
@dataclass(frozen=True)
|
|
@@ -45,12 +42,8 @@ class RuntimeModelHeader:
|
|
| 45 |
|
| 46 |
resolution: int
|
| 47 |
class_count: int
|
| 48 |
-
classification_width: int
|
| 49 |
slot_count: int
|
| 50 |
-
directory_offset: int
|
| 51 |
payload_offset: int
|
| 52 |
-
file_bytes: int
|
| 53 |
-
slot_schema_sha256: str
|
| 54 |
|
| 55 |
|
| 56 |
@dataclass(frozen=True)
|
|
@@ -97,66 +90,46 @@ def _round_up_to_alignment(value: int) -> int:
|
|
| 97 |
return ((value + _ALIGNMENT - 1) // _ALIGNMENT) * _ALIGNMENT
|
| 98 |
|
| 99 |
|
| 100 |
-
def
|
| 101 |
-
"""
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
| 106 |
|
| 107 |
|
| 108 |
def read_runtime_model_header(payload: bytes) -> RuntimeModelHeader:
|
| 109 |
"""Read and validate the fixed header of an exported runtime ``model.bin``."""
|
| 110 |
if len(payload) < _HEADER_BYTES:
|
| 111 |
-
raise ValueError("model file is smaller than its required
|
| 112 |
(
|
| 113 |
magic,
|
| 114 |
version,
|
| 115 |
-
header_bytes,
|
| 116 |
resolution,
|
| 117 |
class_count,
|
| 118 |
slot_count,
|
| 119 |
-
classification_width,
|
| 120 |
-
directory_offset,
|
| 121 |
payload_offset,
|
| 122 |
-
file_bytes,
|
| 123 |
-
slot_schema,
|
| 124 |
) = _HEADER.unpack_from(payload)
|
| 125 |
maximum_directory_entries = (len(payload) - _HEADER_BYTES) // _ENTRY_BYTES
|
| 126 |
expected_payload_offset = _round_up_to_alignment(_HEADER_BYTES + _ENTRY_BYTES * slot_count)
|
| 127 |
if (
|
| 128 |
magic != _MAGIC
|
| 129 |
or version != _VERSION
|
| 130 |
-
or header_bytes != _HEADER_BYTES
|
| 131 |
or resolution not in SUPPORTED_RESOLUTIONS
|
| 132 |
-
or
|
| 133 |
-
or class_count > _UINT32_MAX - BOX_COORDINATE_CHANNELS
|
| 134 |
or slot_count == 0
|
| 135 |
or slot_count != RUNTIME_WEIGHT_SLOT_COUNT
|
| 136 |
or slot_count > maximum_directory_entries
|
| 137 |
-
or directory_offset != _HEADER_BYTES
|
| 138 |
or payload_offset != expected_payload_offset
|
| 139 |
-
or
|
| 140 |
-
or payload[88:_HEADER_BYTES] != bytes(_HEADER_BYTES - 88)
|
| 141 |
):
|
| 142 |
raise ValueError("model header does not match the fixed runtime format")
|
| 143 |
-
if classification_width != classification_width_for_classes(class_count):
|
| 144 |
-
raise ValueError("model header has incompatible class-head dimensions")
|
| 145 |
-
schema = slot_schema.hex()
|
| 146 |
-
if schema != RUNTIME_WEIGHT_SCHEMA_SHA256:
|
| 147 |
-
raise ValueError(
|
| 148 |
-
"model header weight schema does not match the fixed runtime: "
|
| 149 |
-
f"expected {RUNTIME_WEIGHT_SCHEMA_SHA256}, received {schema}"
|
| 150 |
-
)
|
| 151 |
return RuntimeModelHeader(
|
| 152 |
resolution=resolution,
|
| 153 |
class_count=class_count,
|
| 154 |
-
classification_width=classification_width,
|
| 155 |
slot_count=slot_count,
|
| 156 |
-
directory_offset=directory_offset,
|
| 157 |
payload_offset=payload_offset,
|
| 158 |
-
file_bytes=file_bytes,
|
| 159 |
-
slot_schema_sha256=schema,
|
| 160 |
)
|
| 161 |
|
| 162 |
|
|
@@ -164,7 +137,6 @@ def _pack_model_weights(
|
|
| 164 |
weights: tuple[_ModelWeight, ...],
|
| 165 |
resolution: int,
|
| 166 |
class_count: int,
|
| 167 |
-
classification_width: int,
|
| 168 |
) -> bytes:
|
| 169 |
"""Pack validated slot records into the binary model container."""
|
| 170 |
if len(weights) != RUNTIME_WEIGHT_SLOT_COUNT or [weight.slot for weight in weights] != list(
|
|
@@ -176,14 +148,9 @@ def _pack_model_weights(
|
|
| 176 |
if (
|
| 177 |
resolution not in SUPPORTED_RESOLUTIONS
|
| 178 |
or type(class_count) is not int
|
| 179 |
-
or
|
| 180 |
-
or detection_output_channels(class_count) > _UINT32_MAX
|
| 181 |
-
or type(classification_width) is not int
|
| 182 |
-
or classification_width > _UINT32_MAX
|
| 183 |
-
or classification_width != classification_width_for_classes(class_count)
|
| 184 |
):
|
| 185 |
raise ValueError("invalid model dimensions")
|
| 186 |
-
slot_schema = bytes.fromhex(_slot_schema_sha256(tuple(weight.name for weight in weights)))
|
| 187 |
|
| 188 |
directory_offset = _HEADER_BYTES
|
| 189 |
payload_offset = _round_up_to_alignment(directory_offset + _ENTRY_BYTES * len(weights))
|
|
@@ -193,16 +160,11 @@ def _pack_model_weights(
|
|
| 193 |
packed.append(0)
|
| 194 |
offset = len(packed)
|
| 195 |
packed.extend(weight.payload)
|
| 196 |
-
digest = bytes.fromhex(sha256_bytes(weight.payload))
|
| 197 |
_ENTRY.pack_into(
|
| 198 |
packed,
|
| 199 |
directory_offset + weight.slot * _ENTRY_BYTES,
|
| 200 |
-
weight.slot,
|
| 201 |
-
0,
|
| 202 |
offset,
|
| 203 |
len(weight.payload),
|
| 204 |
-
digest,
|
| 205 |
-
0,
|
| 206 |
)
|
| 207 |
|
| 208 |
_HEADER.pack_into(
|
|
@@ -210,15 +172,10 @@ def _pack_model_weights(
|
|
| 210 |
0,
|
| 211 |
_MAGIC,
|
| 212 |
_VERSION,
|
| 213 |
-
_HEADER_BYTES,
|
| 214 |
resolution,
|
| 215 |
class_count,
|
| 216 |
len(weights),
|
| 217 |
-
classification_width,
|
| 218 |
-
directory_offset,
|
| 219 |
payload_offset,
|
| 220 |
-
len(packed),
|
| 221 |
-
slot_schema,
|
| 222 |
)
|
| 223 |
return bytes(packed)
|
| 224 |
|
|
@@ -227,12 +184,10 @@ def pack_model(
|
|
| 227 |
lowered_weights: tuple[LoweredWeight, ...],
|
| 228 |
resolution: int,
|
| 229 |
class_count: int,
|
| 230 |
-
classification_width: int,
|
| 231 |
) -> bytes:
|
| 232 |
"""Validate stable weight identities and serialize the runtime model file."""
|
| 233 |
return _pack_model_weights(
|
| 234 |
_build_model_weight_table(lowered_weights),
|
| 235 |
resolution,
|
| 236 |
class_count,
|
| 237 |
-
classification_width,
|
| 238 |
)
|
|
|
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
|
|
|
| 5 |
import struct
|
| 6 |
from dataclasses import dataclass
|
| 7 |
|
|
|
|
| 8 |
from .model_contract import (
|
| 9 |
BOX_COORDINATE_CHANNELS,
|
| 10 |
RUNTIME_WEIGHT_NAMES,
|
|
|
|
| 11 |
RUNTIME_WEIGHT_SLOT_COUNT,
|
| 12 |
SUPPORTED_RESOLUTIONS,
|
|
|
|
|
|
|
| 13 |
)
|
| 14 |
from .weight_contract import LoweredWeight
|
| 15 |
|
|
|
|
| 22 |
_MAGIC = b"Y26MOD01"
|
| 23 |
_VERSION = 1
|
| 24 |
|
| 25 |
+
# XNNPACK uses 64-byte-aligned static buffers. The header occupies one aligned
|
| 26 |
+
# block. Directory position identifies the semantic slot, so each entry needs
|
| 27 |
+
# only the payload offset and byte count.
|
|
|
|
| 28 |
_ALIGNMENT = 64
|
| 29 |
+
_HEADER_BYTES = _ALIGNMENT
|
| 30 |
+
_ENTRY_BYTES = 16
|
| 31 |
+
_HEADER = struct.Struct("<8sIIIIQ")
|
| 32 |
+
_ENTRY = struct.Struct("<QQ")
|
| 33 |
_UINT32_MAX = (1 << 32) - 1
|
| 34 |
+
_UINT64_MAX = (1 << 64) - 1
|
| 35 |
+
_FP32_BYTES = 4
|
| 36 |
+
_CLASSIFICATION_CHANNEL_FLOOR = 64
|
| 37 |
|
| 38 |
|
| 39 |
@dataclass(frozen=True)
|
|
|
|
| 42 |
|
| 43 |
resolution: int
|
| 44 |
class_count: int
|
|
|
|
| 45 |
slot_count: int
|
|
|
|
| 46 |
payload_offset: int
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
@dataclass(frozen=True)
|
|
|
|
| 90 |
return ((value + _ALIGNMENT - 1) // _ALIGNMENT) * _ALIGNMENT
|
| 91 |
|
| 92 |
|
| 93 |
+
def _class_dependent_weight_bytes_fit(class_count: int) -> bool:
|
| 94 |
+
"""Return whether the target runtime can size its largest learned tensor."""
|
| 95 |
+
width = max(_CLASSIFICATION_CHANNEL_FLOOR, class_count)
|
| 96 |
+
return (
|
| 97 |
+
0 < class_count <= _UINT32_MAX - BOX_COORDINATE_CHANNELS
|
| 98 |
+
and class_count <= _UINT64_MAX // _FP32_BYTES // width
|
| 99 |
+
)
|
| 100 |
|
| 101 |
|
| 102 |
def read_runtime_model_header(payload: bytes) -> RuntimeModelHeader:
|
| 103 |
"""Read and validate the fixed header of an exported runtime ``model.bin``."""
|
| 104 |
if len(payload) < _HEADER_BYTES:
|
| 105 |
+
raise ValueError("model file is smaller than its required 64-byte header")
|
| 106 |
(
|
| 107 |
magic,
|
| 108 |
version,
|
|
|
|
| 109 |
resolution,
|
| 110 |
class_count,
|
| 111 |
slot_count,
|
|
|
|
|
|
|
| 112 |
payload_offset,
|
|
|
|
|
|
|
| 113 |
) = _HEADER.unpack_from(payload)
|
| 114 |
maximum_directory_entries = (len(payload) - _HEADER_BYTES) // _ENTRY_BYTES
|
| 115 |
expected_payload_offset = _round_up_to_alignment(_HEADER_BYTES + _ENTRY_BYTES * slot_count)
|
| 116 |
if (
|
| 117 |
magic != _MAGIC
|
| 118 |
or version != _VERSION
|
|
|
|
| 119 |
or resolution not in SUPPORTED_RESOLUTIONS
|
| 120 |
+
or not _class_dependent_weight_bytes_fit(class_count)
|
|
|
|
| 121 |
or slot_count == 0
|
| 122 |
or slot_count != RUNTIME_WEIGHT_SLOT_COUNT
|
| 123 |
or slot_count > maximum_directory_entries
|
|
|
|
| 124 |
or payload_offset != expected_payload_offset
|
| 125 |
+
or payload[_HEADER.size : _HEADER_BYTES] != bytes(_HEADER_BYTES - _HEADER.size)
|
|
|
|
| 126 |
):
|
| 127 |
raise ValueError("model header does not match the fixed runtime format")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
return RuntimeModelHeader(
|
| 129 |
resolution=resolution,
|
| 130 |
class_count=class_count,
|
|
|
|
| 131 |
slot_count=slot_count,
|
|
|
|
| 132 |
payload_offset=payload_offset,
|
|
|
|
|
|
|
| 133 |
)
|
| 134 |
|
| 135 |
|
|
|
|
| 137 |
weights: tuple[_ModelWeight, ...],
|
| 138 |
resolution: int,
|
| 139 |
class_count: int,
|
|
|
|
| 140 |
) -> bytes:
|
| 141 |
"""Pack validated slot records into the binary model container."""
|
| 142 |
if len(weights) != RUNTIME_WEIGHT_SLOT_COUNT or [weight.slot for weight in weights] != list(
|
|
|
|
| 148 |
if (
|
| 149 |
resolution not in SUPPORTED_RESOLUTIONS
|
| 150 |
or type(class_count) is not int
|
| 151 |
+
or not _class_dependent_weight_bytes_fit(class_count)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
):
|
| 153 |
raise ValueError("invalid model dimensions")
|
|
|
|
| 154 |
|
| 155 |
directory_offset = _HEADER_BYTES
|
| 156 |
payload_offset = _round_up_to_alignment(directory_offset + _ENTRY_BYTES * len(weights))
|
|
|
|
| 160 |
packed.append(0)
|
| 161 |
offset = len(packed)
|
| 162 |
packed.extend(weight.payload)
|
|
|
|
| 163 |
_ENTRY.pack_into(
|
| 164 |
packed,
|
| 165 |
directory_offset + weight.slot * _ENTRY_BYTES,
|
|
|
|
|
|
|
| 166 |
offset,
|
| 167 |
len(weight.payload),
|
|
|
|
|
|
|
| 168 |
)
|
| 169 |
|
| 170 |
_HEADER.pack_into(
|
|
|
|
| 172 |
0,
|
| 173 |
_MAGIC,
|
| 174 |
_VERSION,
|
|
|
|
| 175 |
resolution,
|
| 176 |
class_count,
|
| 177 |
len(weights),
|
|
|
|
|
|
|
| 178 |
payload_offset,
|
|
|
|
|
|
|
| 179 |
)
|
| 180 |
return bytes(packed)
|
| 181 |
|
|
|
|
| 184 |
lowered_weights: tuple[LoweredWeight, ...],
|
| 185 |
resolution: int,
|
| 186 |
class_count: int,
|
|
|
|
| 187 |
) -> bytes:
|
| 188 |
"""Validate stable weight identities and serialize the runtime model file."""
|
| 189 |
return _pack_model_weights(
|
| 190 |
_build_model_weight_table(lowered_weights),
|
| 191 |
resolution,
|
| 192 |
class_count,
|
|
|
|
| 193 |
)
|
compiler/{minimal_runtime/compiler → converter}/model_validation.py
RENAMED
|
@@ -6,7 +6,7 @@ import math
|
|
| 6 |
from dataclasses import dataclass
|
| 7 |
|
| 8 |
from .checkpoint_decoder import DecodedModule, DecodedTensor
|
| 9 |
-
from .
|
| 10 |
from .model_contract import (
|
| 11 |
DETECTION_BRANCH_COUNT,
|
| 12 |
DETECTION_STRIDES,
|
|
|
|
| 6 |
from dataclasses import dataclass
|
| 7 |
|
| 8 |
from .checkpoint_decoder import DecodedModule, DecodedTensor
|
| 9 |
+
from .converter_io import RejectedInput
|
| 10 |
from .model_contract import (
|
| 11 |
DETECTION_BRANCH_COUNT,
|
| 12 |
DETECTION_STRIDES,
|
compiler/{minimal_runtime/compiler → converter}/validate.py
RENAMED
|
@@ -7,7 +7,7 @@ import argparse
|
|
| 7 |
import json
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
-
from .
|
| 11 |
from .export_yolo26n import ExportedModel, export_model
|
| 12 |
|
| 13 |
_VALIDATION_VERSION = 1
|
|
|
|
| 7 |
import json
|
| 8 |
from pathlib import Path
|
| 9 |
|
| 10 |
+
from .converter_io import JsonObject, RejectedInput, atomic_write, canonical_bytes, sha256_bytes
|
| 11 |
from .export_yolo26n import ExportedModel, export_model
|
| 12 |
|
| 13 |
_VALIDATION_VERSION = 1
|
compiler/{minimal_runtime/compiler → converter}/weight_contract.py
RENAMED
|
File without changes
|
compiler/{minimal_runtime/compiler → converter}/weight_lowering.py
RENAMED
|
@@ -10,7 +10,7 @@ import struct
|
|
| 10 |
from dataclasses import dataclass
|
| 11 |
from typing import cast
|
| 12 |
|
| 13 |
-
from .
|
| 14 |
from .model_contract import (
|
| 15 |
BOX_COORDINATE_CHANNELS,
|
| 16 |
DETECTION_BRANCH_COUNT,
|
|
|
|
| 10 |
from dataclasses import dataclass
|
| 11 |
from typing import cast
|
| 12 |
|
| 13 |
+
from .converter_io import RejectedInput
|
| 14 |
from .model_contract import (
|
| 15 |
BOX_COORDINATE_CHANNELS,
|
| 16 |
DETECTION_BRANCH_COUNT,
|
compiler/minimal_runtime/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
"""Purpose-built YOLO26n export and native runtime sources."""
|
|
|
|
|
|
metadata.yaml
CHANGED
|
@@ -1,16 +1,16 @@
|
|
| 1 |
# Metadata generated by ARM Model Optimization Pipeline
|
| 2 |
version: 1.0.0
|
| 3 |
task: vision-detection
|
| 4 |
-
created_at: '2026-09-
|
| 5 |
context:
|
| 6 |
model:
|
| 7 |
id: yolo26n
|
| 8 |
base_model_id: yolo26n
|
| 9 |
profile: Arm-Optimized
|
| 10 |
weight_dtype: fp32
|
| 11 |
-
model_size_mb:
|
| 12 |
parameter_count: 2572280
|
| 13 |
-
format:
|
| 14 |
target:
|
| 15 |
name: Raspberry Pi 5
|
| 16 |
hardware_class: Edge Linux
|
|
@@ -22,7 +22,7 @@ context:
|
|
| 22 |
os: linux
|
| 23 |
os_version: Raspberry Pi OS Bookworm
|
| 24 |
runtime:
|
| 25 |
-
name:
|
| 26 |
execution_backend: cpu
|
| 27 |
config:
|
| 28 |
optimisations:
|
|
@@ -38,6 +38,6 @@ context:
|
|
| 38 |
num_runs: 50
|
| 39 |
warmup_runs: 10
|
| 40 |
accuracy:
|
| 41 |
-
mAP_50_95_pct: 46.
|
| 42 |
mAP_50_pct: 49.47
|
| 43 |
mAP_75_pct: 42.84
|
|
|
|
| 1 |
# Metadata generated by ARM Model Optimization Pipeline
|
| 2 |
version: 1.0.0
|
| 3 |
task: vision-detection
|
| 4 |
+
created_at: '2026-09-23T04:33:02Z'
|
| 5 |
context:
|
| 6 |
model:
|
| 7 |
id: yolo26n
|
| 8 |
base_model_id: yolo26n
|
| 9 |
profile: Arm-Optimized
|
| 10 |
weight_dtype: fp32
|
| 11 |
+
model_size_mb: 10.55
|
| 12 |
parameter_count: 2572280
|
| 13 |
+
format: arm-yolo26n-bundle
|
| 14 |
target:
|
| 15 |
name: Raspberry Pi 5
|
| 16 |
hardware_class: Edge Linux
|
|
|
|
| 22 |
os: linux
|
| 23 |
os_version: Raspberry Pi OS Bookworm
|
| 24 |
runtime:
|
| 25 |
+
name: arm-yolo26n
|
| 26 |
execution_backend: cpu
|
| 27 |
config:
|
| 28 |
optimisations:
|
|
|
|
| 38 |
num_runs: 50
|
| 39 |
warmup_runs: 10
|
| 40 |
accuracy:
|
| 41 |
+
mAP_50_95_pct: 46.15
|
| 42 |
mAP_50_pct: 49.47
|
| 43 |
mAP_75_pct: 42.84
|
model/README.md
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Model input
|
| 2 |
+
|
| 3 |
+
To export another compatible YOLO26n model, place its checkpoint here as
|
| 4 |
+
`yolo26n.pt`, then run `python3 repro.py export` from the repository root.
|
| 5 |
+
|
| 6 |
+
No PyTorch checkpoint is distributed in this repository.
|
model/yolo26n.pt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:9b09cc8bf347f0fc8a5f7657480587f25db09b34bf33b0652110fb03a8ad4fef
|
| 3 |
-
size 5544453
|
|
|
|
|
|
|
|
|
|
|
|
python-api/__init__.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
"""Python access to the native YOLO26n runtime."""
|
| 2 |
-
|
| 3 |
-
from .errors import Yolo26RuntimeError
|
| 4 |
-
from .yolo26n import Yolo26nRuntime
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
__all__ = [
|
| 8 |
-
"Yolo26nRuntime",
|
| 9 |
-
"Yolo26RuntimeError",
|
| 10 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
python-api/arm_yolo26n/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tensor and image APIs for the purpose-built YOLO26n runtime."""
|
| 2 |
+
|
| 3 |
+
from .detector import (
|
| 4 |
+
Detection,
|
| 5 |
+
DetectionResult,
|
| 6 |
+
DetectorConfig,
|
| 7 |
+
DetectorTimings,
|
| 8 |
+
PixelFormat,
|
| 9 |
+
Yolo26Detector,
|
| 10 |
+
)
|
| 11 |
+
from .errors import Yolo26nError
|
| 12 |
+
from .yolo26n import Yolo26nTensorSession
|
| 13 |
+
|
| 14 |
+
__all__ = [
|
| 15 |
+
"Detection",
|
| 16 |
+
"DetectionResult",
|
| 17 |
+
"DetectorConfig",
|
| 18 |
+
"DetectorTimings",
|
| 19 |
+
"PixelFormat",
|
| 20 |
+
"Yolo26Detector",
|
| 21 |
+
"Yolo26nTensorSession",
|
| 22 |
+
"Yolo26nError",
|
| 23 |
+
]
|
python-api/arm_yolo26n/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (643 Bytes). View file
|
|
|
python-api/arm_yolo26n/__pycache__/_model.cpython-313.pyc
ADDED
|
Binary file (9.82 kB). View file
|
|
|
python-api/arm_yolo26n/__pycache__/detector.cpython-313.pyc
ADDED
|
Binary file (23.6 kB). View file
|
|
|
python-api/arm_yolo26n/__pycache__/errors.cpython-313.pyc
ADDED
|
Binary file (612 Bytes). View file
|
|
|
python-api/arm_yolo26n/__pycache__/yolo26n.cpython-313.pyc
ADDED
|
Binary file (19 kB). View file
|
|
|
python-api/arm_yolo26n/_model.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Load and validate one converted YOLO26n model directory."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import hashlib
|
| 6 |
+
import json
|
| 7 |
+
from collections.abc import Mapping
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import cast
|
| 11 |
+
|
| 12 |
+
from .errors import Yolo26nError
|
| 13 |
+
|
| 14 |
+
# These values define the converted-model/runtime contract. They describe the
|
| 15 |
+
# only model family and native target this purpose-built runtime accepts.
|
| 16 |
+
_MODEL_KIND = "yolo26n-exported-model"
|
| 17 |
+
_MODEL_VERSION = 1
|
| 18 |
+
_TARGET = "linux-aarch64-raspberry-pi-5"
|
| 19 |
+
|
| 20 |
+
# The Raspberry Pi 5 runtime owns a fixed four-worker pool, matching its four
|
| 21 |
+
# CPU cores, so the thread count is part of model/runtime compatibility.
|
| 22 |
+
_TARGET_THREAD_COUNT = 4
|
| 23 |
+
_BOX_COORDINATE_CHANNELS = 4
|
| 24 |
+
|
| 25 |
+
# Graph sizing and memory planning are implemented only for these profiles.
|
| 26 |
+
_SUPPORTED_RESOLUTIONS = (320, 480, 640, 800, 960, 1280)
|
| 27 |
+
|
| 28 |
+
# YOLO26n predicts at its P3, P4, and P5 feature levels.
|
| 29 |
+
_DETECTION_STRIDES = (8, 16, 32)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass(frozen=True, slots=True)
|
| 33 |
+
class _LoadedModel:
|
| 34 |
+
"""Validated metadata and serialized weights required by a session."""
|
| 35 |
+
|
| 36 |
+
input_shape: tuple[int, int, int, int]
|
| 37 |
+
output_shape: tuple[int, int, int]
|
| 38 |
+
class_names: tuple[str, ...]
|
| 39 |
+
model_bytes: bytes
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass(frozen=True, slots=True)
|
| 43 |
+
class _ArtifactRecord:
|
| 44 |
+
relative_path: Path
|
| 45 |
+
size: int
|
| 46 |
+
sha256: str
|
| 47 |
+
|
| 48 |
+
@classmethod
|
| 49 |
+
def from_manifest(cls, value: object) -> _ArtifactRecord:
|
| 50 |
+
"""Parse the model artifact without accepting unknown fields."""
|
| 51 |
+
record = _require_mapping(value, "artifact")
|
| 52 |
+
if set(record) != {"bytes", "path", "sha256"}:
|
| 53 |
+
raise Yolo26nError("invalid artifact in YOLO26n model manifest")
|
| 54 |
+
|
| 55 |
+
path_value = record["path"]
|
| 56 |
+
size = record["bytes"]
|
| 57 |
+
digest = record["sha256"]
|
| 58 |
+
if (
|
| 59 |
+
not isinstance(path_value, str)
|
| 60 |
+
or type(size) is not int
|
| 61 |
+
or size < 0
|
| 62 |
+
or not isinstance(digest, str)
|
| 63 |
+
or len(digest) != 64
|
| 64 |
+
or any(character not in "0123456789abcdef" for character in digest)
|
| 65 |
+
):
|
| 66 |
+
raise Yolo26nError("invalid artifact in YOLO26n model manifest")
|
| 67 |
+
|
| 68 |
+
relative_path = Path(path_value)
|
| 69 |
+
if relative_path.is_absolute() or ".." in relative_path.parts:
|
| 70 |
+
raise Yolo26nError("invalid artifact path in YOLO26n model manifest")
|
| 71 |
+
return cls(relative_path=relative_path, size=size, sha256=digest)
|
| 72 |
+
|
| 73 |
+
def load(self, model_directory: Path) -> bytes:
|
| 74 |
+
"""Read the model bytes and verify their manifest record."""
|
| 75 |
+
path = model_directory / self.relative_path
|
| 76 |
+
try:
|
| 77 |
+
model_bytes = path.read_bytes()
|
| 78 |
+
except OSError as error:
|
| 79 |
+
raise Yolo26nError(
|
| 80 |
+
f"YOLO26n model artifact {self.relative_path.as_posix()!r} cannot be read"
|
| 81 |
+
) from error
|
| 82 |
+
if len(model_bytes) != self.size or hashlib.sha256(model_bytes).hexdigest() != self.sha256:
|
| 83 |
+
raise Yolo26nError(
|
| 84 |
+
f"YOLO26n model artifact {self.relative_path.as_posix()!r} "
|
| 85 |
+
"does not match its manifest"
|
| 86 |
+
)
|
| 87 |
+
return model_bytes
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def _load_model(model_directory: str | Path) -> _LoadedModel:
|
| 91 |
+
"""Validate one converted model before native code receives its weights."""
|
| 92 |
+
directory = Path(model_directory).expanduser().resolve()
|
| 93 |
+
if not directory.is_dir():
|
| 94 |
+
raise FileNotFoundError(f"YOLO26n model directory does not exist: {directory}")
|
| 95 |
+
|
| 96 |
+
manifest = _read_manifest(directory / "manifest.json")
|
| 97 |
+
if (manifest.get("kind"), manifest.get("version")) != (_MODEL_KIND, _MODEL_VERSION):
|
| 98 |
+
raise Yolo26nError("unsupported YOLO26n model manifest kind or version")
|
| 99 |
+
|
| 100 |
+
model = _require_mapping(manifest.get("model"), "model record")
|
| 101 |
+
input_shape = _require_shape(model.get("input_shape"), "input shape")
|
| 102 |
+
output_shape = _require_shape(model.get("output_shape"), "output shape")
|
| 103 |
+
resolution = input_shape[2] if len(input_shape) == 4 else None
|
| 104 |
+
if (
|
| 105 |
+
len(input_shape) != 4
|
| 106 |
+
or input_shape[:2] != (1, 3)
|
| 107 |
+
or input_shape[2] != input_shape[3]
|
| 108 |
+
or resolution not in _SUPPORTED_RESOLUTIONS
|
| 109 |
+
):
|
| 110 |
+
raise Yolo26nError("unsupported YOLO26n input shape")
|
| 111 |
+
if (
|
| 112 |
+
len(output_shape) != 3
|
| 113 |
+
or output_shape[0] != 1
|
| 114 |
+
or output_shape[1] <= _BOX_COORDINATE_CHANNELS
|
| 115 |
+
or output_shape[2] != _detection_positions(resolution)
|
| 116 |
+
):
|
| 117 |
+
raise Yolo26nError("unsupported YOLO26n output shape")
|
| 118 |
+
|
| 119 |
+
expected_target = {
|
| 120 |
+
"version": 1,
|
| 121 |
+
"target": _TARGET,
|
| 122 |
+
"input_shape": list(input_shape),
|
| 123 |
+
"input_dtype": "fp32",
|
| 124 |
+
"output_dtype": "fp32",
|
| 125 |
+
"thread_count": _TARGET_THREAD_COUNT,
|
| 126 |
+
}
|
| 127 |
+
if model.get("target") != expected_target:
|
| 128 |
+
raise Yolo26nError("unsupported YOLO26n target contract")
|
| 129 |
+
|
| 130 |
+
names = model.get("class_names")
|
| 131 |
+
if (
|
| 132 |
+
not isinstance(names, list)
|
| 133 |
+
or len(names) != output_shape[1] - _BOX_COORDINATE_CHANNELS
|
| 134 |
+
or any(not isinstance(name, str) or not name for name in names)
|
| 135 |
+
):
|
| 136 |
+
raise Yolo26nError("invalid class names in YOLO26n model manifest")
|
| 137 |
+
|
| 138 |
+
artifact = _ArtifactRecord.from_manifest(manifest.get("artifact"))
|
| 139 |
+
return _LoadedModel(
|
| 140 |
+
input_shape=input_shape,
|
| 141 |
+
output_shape=output_shape,
|
| 142 |
+
class_names=tuple(cast(list[str], names)),
|
| 143 |
+
model_bytes=artifact.load(directory),
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _read_manifest(path: Path) -> Mapping[str, object]:
|
| 148 |
+
try:
|
| 149 |
+
value: object = json.loads(path.read_text(encoding="utf-8"))
|
| 150 |
+
except FileNotFoundError:
|
| 151 |
+
raise FileNotFoundError(f"YOLO26n model manifest does not exist: {path}") from None
|
| 152 |
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
| 153 |
+
raise Yolo26nError(f"invalid YOLO26n model manifest: {path}") from error
|
| 154 |
+
return _require_mapping(value, "manifest")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def _require_mapping(value: object, name: str) -> Mapping[str, object]:
|
| 158 |
+
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
| 159 |
+
raise Yolo26nError(f"invalid {name} in YOLO26n model manifest")
|
| 160 |
+
return cast(Mapping[str, object], value)
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def _require_shape(value: object, name: str) -> tuple[int, ...]:
|
| 164 |
+
if (
|
| 165 |
+
not isinstance(value, list)
|
| 166 |
+
or not value
|
| 167 |
+
or any(type(dimension) is not int or dimension <= 0 for dimension in value)
|
| 168 |
+
):
|
| 169 |
+
raise Yolo26nError(f"invalid {name} in YOLO26n model manifest")
|
| 170 |
+
return tuple(value)
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _detection_positions(resolution: int) -> int:
|
| 174 |
+
return sum((resolution // stride) ** 2 for stride in _DETECTION_STRIDES)
|
python-api/arm_yolo26n/detector.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""High-level YOLO26n detection for uint8 NumPy image frames."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import threading
|
| 6 |
+
from collections.abc import Callable
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from time import perf_counter
|
| 10 |
+
from types import TracebackType
|
| 11 |
+
from typing import Literal, Protocol, Self, cast
|
| 12 |
+
|
| 13 |
+
import cv2
|
| 14 |
+
import numpy as np
|
| 15 |
+
from numpy.typing import NDArray
|
| 16 |
+
|
| 17 |
+
from .errors import Yolo26nError
|
| 18 |
+
from .yolo26n import Yolo26nTensorSession
|
| 19 |
+
|
| 20 |
+
__all__ = [
|
| 21 |
+
"Detection",
|
| 22 |
+
"DetectionResult",
|
| 23 |
+
"DetectorConfig",
|
| 24 |
+
"DetectorTimings",
|
| 25 |
+
"PixelFormat",
|
| 26 |
+
"Yolo26Detector",
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
PixelFormat = Literal["BGR", "RGB", "BGRX"]
|
| 30 |
+
_BOX_COORDINATE_CHANNELS = 4
|
| 31 |
+
_LETTERBOX_VALUE = 114
|
| 32 |
+
_MAX_NMS_CANDIDATES = 30_000
|
| 33 |
+
_CLASS_BOX_OFFSET = 7680
|
| 34 |
+
type _FloatArray = NDArray[np.float32]
|
| 35 |
+
type _IndexArray = NDArray[np.int64]
|
| 36 |
+
type _Uint8Array = NDArray[np.uint8]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class _TensorSession(Protocol):
|
| 40 |
+
input_shape: tuple[int, int, int, int]
|
| 41 |
+
output_shape: tuple[int, int, int]
|
| 42 |
+
class_names: tuple[str, ...]
|
| 43 |
+
|
| 44 |
+
@property
|
| 45 |
+
def closed(self) -> bool: ...
|
| 46 |
+
|
| 47 |
+
def execute(self, tensor: _FloatArray) -> _FloatArray: ...
|
| 48 |
+
|
| 49 |
+
def _prepare_image_without_resize(
|
| 50 |
+
self,
|
| 51 |
+
frame: _Uint8Array,
|
| 52 |
+
*,
|
| 53 |
+
pixel_format: str,
|
| 54 |
+
output: _FloatArray,
|
| 55 |
+
) -> None: ...
|
| 56 |
+
|
| 57 |
+
def close(self) -> None: ...
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@dataclass(frozen=True, slots=True)
|
| 61 |
+
class DetectorConfig:
|
| 62 |
+
"""Postprocessing settings for one detector instance."""
|
| 63 |
+
|
| 64 |
+
confidence: float = 0.25
|
| 65 |
+
iou: float = 0.45
|
| 66 |
+
max_detections: int = 300
|
| 67 |
+
multi_label: bool = True
|
| 68 |
+
|
| 69 |
+
def __post_init__(self) -> None:
|
| 70 |
+
if not 0.0 <= self.confidence <= 1.0:
|
| 71 |
+
raise ValueError("confidence must be between 0 and 1")
|
| 72 |
+
if not 0.0 <= self.iou <= 1.0:
|
| 73 |
+
raise ValueError("iou must be between 0 and 1")
|
| 74 |
+
if self.max_detections <= 0:
|
| 75 |
+
raise ValueError("max_detections must be positive")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
@dataclass(frozen=True, slots=True)
|
| 79 |
+
class Detection:
|
| 80 |
+
"""One detection in source-image pixel coordinates (x1, y1, x2, y2)."""
|
| 81 |
+
|
| 82 |
+
box: tuple[float, float, float, float]
|
| 83 |
+
confidence: float
|
| 84 |
+
class_id: int
|
| 85 |
+
class_name: str
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
@dataclass(frozen=True, slots=True)
|
| 89 |
+
class DetectorTimings:
|
| 90 |
+
"""Wall-clock time spent in each detector stage."""
|
| 91 |
+
|
| 92 |
+
preprocess_ms: float
|
| 93 |
+
inference_ms: float
|
| 94 |
+
postprocess_ms: float
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def total_ms(self) -> float:
|
| 98 |
+
return self.preprocess_ms + self.inference_ms + self.postprocess_ms
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@dataclass(frozen=True, slots=True)
|
| 102 |
+
class DetectionResult:
|
| 103 |
+
"""Detections and timing information for one source frame."""
|
| 104 |
+
|
| 105 |
+
detections: tuple[Detection, ...]
|
| 106 |
+
source_shape: tuple[int, int]
|
| 107 |
+
timings: DetectorTimings
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class Yolo26Detector:
|
| 111 |
+
"""Convert image frames into labelled detections in source coordinates.
|
| 112 |
+
|
| 113 |
+
The detector applies letterboxing, normalization, native inference,
|
| 114 |
+
non-maximum suppression, and coordinate scaling. Each result also reports
|
| 115 |
+
time spent preprocessing, executing the runtime, and postprocessing. One
|
| 116 |
+
input tensor is reused between calls, so ``detect`` and ``close`` are
|
| 117 |
+
deliberately non-reentrant; use separate processes for concurrent
|
| 118 |
+
isolation. The runtime and model paths are keyword-only so callers cannot
|
| 119 |
+
silently reverse two path-like arguments.
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
def __init__(
|
| 123 |
+
self,
|
| 124 |
+
*,
|
| 125 |
+
runtime: str | Path,
|
| 126 |
+
model: str | Path,
|
| 127 |
+
config: DetectorConfig | None = None,
|
| 128 |
+
) -> None:
|
| 129 |
+
self.config = config or DetectorConfig()
|
| 130 |
+
self._session: _TensorSession = Yolo26nTensorSession(runtime=runtime, model=model)
|
| 131 |
+
self._lifecycle_lock = threading.Lock()
|
| 132 |
+
try:
|
| 133 |
+
class_names = tuple(self._session.class_names)
|
| 134 |
+
input_shape = tuple(self._session.input_shape)
|
| 135 |
+
output_shape = tuple(self._session.output_shape)
|
| 136 |
+
if (
|
| 137 |
+
len(input_shape) != 4
|
| 138 |
+
or input_shape[:2] != (1, 3)
|
| 139 |
+
or input_shape[2] <= 0
|
| 140 |
+
or input_shape[2] != input_shape[3]
|
| 141 |
+
):
|
| 142 |
+
raise ValueError(
|
| 143 |
+
f"detector requires a square [1, 3, S, S] input, got {input_shape}"
|
| 144 |
+
)
|
| 145 |
+
if (
|
| 146 |
+
len(output_shape) != 3
|
| 147 |
+
or output_shape[0] != 1
|
| 148 |
+
or output_shape[1] <= _BOX_COORDINATE_CHANNELS
|
| 149 |
+
or output_shape[2] <= 0
|
| 150 |
+
):
|
| 151 |
+
raise ValueError(
|
| 152 |
+
"detector requires output shape [1, 4 + classes, positions], "
|
| 153 |
+
f"got {output_shape}"
|
| 154 |
+
)
|
| 155 |
+
output_class_count = output_shape[1] - _BOX_COORDINATE_CHANNELS
|
| 156 |
+
if len(class_names) != output_class_count or any(
|
| 157 |
+
not isinstance(name, str) or not name for name in class_names
|
| 158 |
+
):
|
| 159 |
+
raise ValueError(
|
| 160 |
+
"detector class names must be non-empty strings matching the "
|
| 161 |
+
f"{output_class_count} output classes; got {len(class_names)}"
|
| 162 |
+
)
|
| 163 |
+
except BaseException:
|
| 164 |
+
self._session.close()
|
| 165 |
+
raise
|
| 166 |
+
self.class_names = class_names
|
| 167 |
+
self.input_shape = input_shape
|
| 168 |
+
self.output_shape = output_shape
|
| 169 |
+
self._input_hw = (input_shape[2], input_shape[3])
|
| 170 |
+
self._input_tensor = np.empty(input_shape, dtype=np.float32)
|
| 171 |
+
|
| 172 |
+
@property
|
| 173 |
+
def closed(self) -> bool:
|
| 174 |
+
return self._session.closed
|
| 175 |
+
|
| 176 |
+
def detect(
|
| 177 |
+
self,
|
| 178 |
+
frame: _Uint8Array,
|
| 179 |
+
*,
|
| 180 |
+
pixel_format: PixelFormat,
|
| 181 |
+
on_frame_consumed: Callable[[], None] | None = None,
|
| 182 |
+
) -> DetectionResult:
|
| 183 |
+
"""Detect objects in one uint8 HWC BGR, RGB, or BGRX frame.
|
| 184 |
+
|
| 185 |
+
Pixel-order conversion, letterboxing, normalization, native execution,
|
| 186 |
+
non-maximum suppression, and box scaling are all included. Frames whose
|
| 187 |
+
longest side already matches the square model input take the native
|
| 188 |
+
no-resize path: centered value-114 padding, channel conversion,
|
| 189 |
+
HWC-to-CHW conversion, and FP32 normalization are fused into the reused
|
| 190 |
+
input tensor. Frames requiring interpolation use centered letterboxing.
|
| 191 |
+
If provided, ``on_frame_consumed`` is called exactly once after the
|
| 192 |
+
input tensor no longer refers to the source frame and before inference.
|
| 193 |
+
"""
|
| 194 |
+
source_shape = self._validate_frame(frame, pixel_format)
|
| 195 |
+
if not self._lifecycle_lock.acquire(blocking=False):
|
| 196 |
+
raise Yolo26nError("concurrent detection or close is not supported")
|
| 197 |
+
|
| 198 |
+
try:
|
| 199 |
+
if self.closed:
|
| 200 |
+
raise Yolo26nError("detector is closed")
|
| 201 |
+
|
| 202 |
+
preprocess_started_at = perf_counter()
|
| 203 |
+
self._prepare_input(frame, source_shape, pixel_format)
|
| 204 |
+
preprocess_ms = (perf_counter() - preprocess_started_at) * 1000
|
| 205 |
+
if on_frame_consumed is not None:
|
| 206 |
+
on_frame_consumed()
|
| 207 |
+
|
| 208 |
+
inference_started_at = perf_counter()
|
| 209 |
+
raw = self._run_inference()
|
| 210 |
+
inference_ms = (perf_counter() - inference_started_at) * 1000
|
| 211 |
+
|
| 212 |
+
postprocess_started_at = perf_counter()
|
| 213 |
+
detections = self._postprocess(raw, source_shape)
|
| 214 |
+
postprocess_ms = (perf_counter() - postprocess_started_at) * 1000
|
| 215 |
+
return DetectionResult(
|
| 216 |
+
detections=detections,
|
| 217 |
+
source_shape=source_shape,
|
| 218 |
+
timings=DetectorTimings(
|
| 219 |
+
preprocess_ms=preprocess_ms,
|
| 220 |
+
inference_ms=inference_ms,
|
| 221 |
+
postprocess_ms=postprocess_ms,
|
| 222 |
+
),
|
| 223 |
+
)
|
| 224 |
+
finally:
|
| 225 |
+
self._lifecycle_lock.release()
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
def _validate_frame(
|
| 229 |
+
frame: _Uint8Array,
|
| 230 |
+
pixel_format: PixelFormat,
|
| 231 |
+
) -> tuple[int, int]:
|
| 232 |
+
if not isinstance(frame, np.ndarray):
|
| 233 |
+
raise TypeError("frame must be a numpy.ndarray")
|
| 234 |
+
if frame.dtype != np.uint8:
|
| 235 |
+
raise TypeError("frame must have dtype uint8")
|
| 236 |
+
if pixel_format not in ("BGR", "RGB", "BGRX"):
|
| 237 |
+
raise ValueError("pixel_format must be 'BGR', 'RGB', or 'BGRX'")
|
| 238 |
+
expected_channels = 4 if pixel_format == "BGRX" else 3
|
| 239 |
+
if frame.ndim != 3 or frame.shape[2] != expected_channels:
|
| 240 |
+
raise ValueError(
|
| 241 |
+
f"{pixel_format} frame must have shape [height, width, {expected_channels}]"
|
| 242 |
+
)
|
| 243 |
+
if frame.shape[0] == 0 or frame.shape[1] == 0:
|
| 244 |
+
raise ValueError("frame dimensions must be positive")
|
| 245 |
+
return (int(frame.shape[0]), int(frame.shape[1]))
|
| 246 |
+
|
| 247 |
+
def _prepare_input(
|
| 248 |
+
self,
|
| 249 |
+
frame: _Uint8Array,
|
| 250 |
+
source_shape: tuple[int, int],
|
| 251 |
+
pixel_format: PixelFormat,
|
| 252 |
+
) -> None:
|
| 253 |
+
if max(source_shape) == self._input_hw[0]:
|
| 254 |
+
self._session._prepare_image_without_resize(
|
| 255 |
+
frame,
|
| 256 |
+
pixel_format=pixel_format,
|
| 257 |
+
output=self._input_tensor,
|
| 258 |
+
)
|
| 259 |
+
return
|
| 260 |
+
|
| 261 |
+
three_channel_frame = frame[..., :3] if pixel_format == "BGRX" else frame
|
| 262 |
+
image = _letterbox(three_channel_frame, self._input_hw)
|
| 263 |
+
image_rgb = image[..., ::-1] if pixel_format in ("BGR", "BGRX") else image
|
| 264 |
+
np.copyto(
|
| 265 |
+
self._input_tensor[0],
|
| 266 |
+
image_rgb.transpose(2, 0, 1),
|
| 267 |
+
casting="unsafe",
|
| 268 |
+
)
|
| 269 |
+
np.divide(self._input_tensor, np.float32(255.0), out=self._input_tensor)
|
| 270 |
+
|
| 271 |
+
def _run_inference(self) -> _FloatArray:
|
| 272 |
+
return self._session.execute(self._input_tensor)
|
| 273 |
+
|
| 274 |
+
def _postprocess(
|
| 275 |
+
self,
|
| 276 |
+
raw: _FloatArray,
|
| 277 |
+
source_shape: tuple[int, int],
|
| 278 |
+
) -> tuple[Detection, ...]:
|
| 279 |
+
processed = _non_max_suppression(
|
| 280 |
+
raw[0],
|
| 281 |
+
confidence=self.config.confidence,
|
| 282 |
+
iou=self.config.iou,
|
| 283 |
+
max_detections=self.config.max_detections,
|
| 284 |
+
multi_label=self.config.multi_label,
|
| 285 |
+
)
|
| 286 |
+
_scale_boxes(processed[:, :4], self._input_hw, source_shape)
|
| 287 |
+
detections = []
|
| 288 |
+
for row in processed:
|
| 289 |
+
class_id = int(row[5])
|
| 290 |
+
if not 0 <= class_id < len(self.class_names):
|
| 291 |
+
raise Yolo26nError(f"postprocessing returned unknown class ID {class_id}")
|
| 292 |
+
x1, y1, x2, y2 = (float(value) for value in row[:4])
|
| 293 |
+
if x2 <= x1 or y2 <= y1:
|
| 294 |
+
continue
|
| 295 |
+
detections.append(
|
| 296 |
+
Detection(
|
| 297 |
+
box=(x1, y1, x2, y2),
|
| 298 |
+
confidence=float(row[4]),
|
| 299 |
+
class_id=class_id,
|
| 300 |
+
class_name=self.class_names[class_id],
|
| 301 |
+
)
|
| 302 |
+
)
|
| 303 |
+
return tuple(detections)
|
| 304 |
+
|
| 305 |
+
def close(self) -> None:
|
| 306 |
+
"""Release the detector's tensor session; repeated calls are safe."""
|
| 307 |
+
with self._lifecycle_lock:
|
| 308 |
+
self._session.close()
|
| 309 |
+
|
| 310 |
+
def __enter__(self) -> Self:
|
| 311 |
+
if self.closed:
|
| 312 |
+
raise Yolo26nError("detector is closed")
|
| 313 |
+
return self
|
| 314 |
+
|
| 315 |
+
def __exit__(
|
| 316 |
+
self,
|
| 317 |
+
_exc_type: type[BaseException] | None,
|
| 318 |
+
_exc_value: BaseException | None,
|
| 319 |
+
_traceback: TracebackType | None,
|
| 320 |
+
) -> None:
|
| 321 |
+
self.close()
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def _letterbox(image: _Uint8Array, target_shape: tuple[int, int]) -> _Uint8Array:
|
| 325 |
+
"""Resize without distortion, then center the image on a square canvas."""
|
| 326 |
+
source_height, source_width = image.shape[:2]
|
| 327 |
+
target_height, target_width = target_shape
|
| 328 |
+
scale = min(target_height / source_height, target_width / source_width)
|
| 329 |
+
resized_width = round(source_width * scale)
|
| 330 |
+
resized_height = round(source_height * scale)
|
| 331 |
+
if (source_width, source_height) != (resized_width, resized_height):
|
| 332 |
+
image = cast(
|
| 333 |
+
_Uint8Array,
|
| 334 |
+
cv2.resize(
|
| 335 |
+
image,
|
| 336 |
+
(resized_width, resized_height),
|
| 337 |
+
interpolation=cv2.INTER_LINEAR,
|
| 338 |
+
),
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
horizontal_padding = (target_width - resized_width) / 2
|
| 342 |
+
vertical_padding = (target_height - resized_height) / 2
|
| 343 |
+
left = round(horizontal_padding - 0.1)
|
| 344 |
+
right = round(horizontal_padding + 0.1)
|
| 345 |
+
top = round(vertical_padding - 0.1)
|
| 346 |
+
bottom = round(vertical_padding + 0.1)
|
| 347 |
+
return cast(
|
| 348 |
+
_Uint8Array,
|
| 349 |
+
cv2.copyMakeBorder(
|
| 350 |
+
image,
|
| 351 |
+
top,
|
| 352 |
+
bottom,
|
| 353 |
+
left,
|
| 354 |
+
right,
|
| 355 |
+
cv2.BORDER_CONSTANT,
|
| 356 |
+
value=(_LETTERBOX_VALUE,) * 3,
|
| 357 |
+
),
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def _xywh_to_xyxy(boxes: _FloatArray) -> _FloatArray:
|
| 362 |
+
converted = np.empty_like(boxes)
|
| 363 |
+
half_size = boxes[:, 2:] / np.float32(2.0)
|
| 364 |
+
converted[:, :2] = boxes[:, :2] - half_size
|
| 365 |
+
converted[:, 2:] = boxes[:, :2] + half_size
|
| 366 |
+
return converted
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
def _candidate_detections(
|
| 370 |
+
prediction: _FloatArray,
|
| 371 |
+
confidence: float,
|
| 372 |
+
multi_label: bool,
|
| 373 |
+
) -> _FloatArray:
|
| 374 |
+
class_scores = prediction[_BOX_COORDINATE_CHANNELS:].T
|
| 375 |
+
candidate_positions = np.max(class_scores, axis=1) > confidence
|
| 376 |
+
if not np.any(candidate_positions):
|
| 377 |
+
return np.empty((0, 6), dtype=np.float32)
|
| 378 |
+
|
| 379 |
+
boxes = _xywh_to_xyxy(prediction[:_BOX_COORDINATE_CHANNELS].T[candidate_positions])
|
| 380 |
+
scores = class_scores[candidate_positions]
|
| 381 |
+
if multi_label and scores.shape[1] > 1:
|
| 382 |
+
box_indices, class_ids = np.nonzero(scores > confidence)
|
| 383 |
+
return np.column_stack(
|
| 384 |
+
(boxes[box_indices], scores[box_indices, class_ids], class_ids.astype(np.float32))
|
| 385 |
+
).astype(np.float32, copy=False)
|
| 386 |
+
|
| 387 |
+
class_ids = np.argmax(scores, axis=1)
|
| 388 |
+
confidences = scores[np.arange(len(scores)), class_ids]
|
| 389 |
+
accepted = confidences > confidence
|
| 390 |
+
return np.column_stack(
|
| 391 |
+
(boxes[accepted], confidences[accepted], class_ids[accepted].astype(np.float32))
|
| 392 |
+
).astype(np.float32, copy=False)
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def _nms_indices(
|
| 396 |
+
boxes: _FloatArray,
|
| 397 |
+
scores: _FloatArray,
|
| 398 |
+
iou: float,
|
| 399 |
+
) -> _IndexArray:
|
| 400 |
+
"""Return descending-confidence indices after class-aware box suppression."""
|
| 401 |
+
if not len(boxes):
|
| 402 |
+
return np.empty(0, dtype=np.int64)
|
| 403 |
+
x1, y1, x2, y2 = boxes.T
|
| 404 |
+
areas = (x2 - x1) * (y2 - y1)
|
| 405 |
+
# Reversing a stable ascending order makes exact score ties deterministic:
|
| 406 |
+
# the later model candidate takes precedence.
|
| 407 |
+
order = np.argsort(scores, kind="stable")[::-1]
|
| 408 |
+
kept: list[int] = []
|
| 409 |
+
while len(order):
|
| 410 |
+
current = int(order[0])
|
| 411 |
+
kept.append(current)
|
| 412 |
+
if len(order) == 1:
|
| 413 |
+
break
|
| 414 |
+
remaining = order[1:]
|
| 415 |
+
intersection_width = np.maximum(
|
| 416 |
+
np.minimum(x2[current], x2[remaining]) - np.maximum(x1[current], x1[remaining]),
|
| 417 |
+
np.float32(0.0),
|
| 418 |
+
)
|
| 419 |
+
intersection_height = np.maximum(
|
| 420 |
+
np.minimum(y2[current], y2[remaining]) - np.maximum(y1[current], y1[remaining]),
|
| 421 |
+
np.float32(0.0),
|
| 422 |
+
)
|
| 423 |
+
intersection = intersection_width * intersection_height
|
| 424 |
+
overlap = intersection / (areas[current] + areas[remaining] - intersection)
|
| 425 |
+
order = remaining[overlap <= iou]
|
| 426 |
+
return np.asarray(kept, dtype=np.int64)
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def _non_max_suppression(
|
| 430 |
+
prediction: _FloatArray,
|
| 431 |
+
*,
|
| 432 |
+
confidence: float,
|
| 433 |
+
iou: float,
|
| 434 |
+
max_detections: int,
|
| 435 |
+
multi_label: bool,
|
| 436 |
+
) -> _FloatArray:
|
| 437 |
+
detections = _candidate_detections(prediction, confidence, multi_label)
|
| 438 |
+
if not len(detections):
|
| 439 |
+
return detections
|
| 440 |
+
if len(detections) > _MAX_NMS_CANDIDATES:
|
| 441 |
+
highest_confidence = np.argsort(detections[:, 4], kind="stable")[::-1][:_MAX_NMS_CANDIDATES]
|
| 442 |
+
detections = detections[highest_confidence]
|
| 443 |
+
|
| 444 |
+
# Offsetting coordinates by class prevents boxes from different classes
|
| 445 |
+
# suppressing one another while retaining a single vectorized NMS pass.
|
| 446 |
+
class_offsets = detections[:, 5:6] * np.float32(_CLASS_BOX_OFFSET)
|
| 447 |
+
boxes = detections[:, :4] + class_offsets
|
| 448 |
+
kept = _nms_indices(boxes, detections[:, 4], iou)[:max_detections]
|
| 449 |
+
return detections[kept]
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def _scale_boxes(
|
| 453 |
+
boxes: _FloatArray,
|
| 454 |
+
model_shape: tuple[int, int],
|
| 455 |
+
source_shape: tuple[int, int],
|
| 456 |
+
) -> None:
|
| 457 |
+
"""Map letterboxed model coordinates back to source-image pixels."""
|
| 458 |
+
if not len(boxes):
|
| 459 |
+
return
|
| 460 |
+
model_height, model_width = model_shape
|
| 461 |
+
source_height, source_width = source_shape
|
| 462 |
+
scale = min(model_height / source_height, model_width / source_width)
|
| 463 |
+
horizontal_padding = round((model_width - round(source_width * scale)) / 2 - 0.1)
|
| 464 |
+
vertical_padding = round((model_height - round(source_height * scale)) / 2 - 0.1)
|
| 465 |
+
boxes[:, (0, 2)] -= horizontal_padding
|
| 466 |
+
boxes[:, (1, 3)] -= vertical_padding
|
| 467 |
+
boxes /= np.float32(scale)
|
| 468 |
+
boxes[:, (0, 2)] = boxes[:, (0, 2)].clip(0, source_width)
|
| 469 |
+
boxes[:, (1, 3)] = boxes[:, (1, 3)].clip(0, source_height)
|
python-api/arm_yolo26n/errors.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exceptions raised by the YOLO26n Python APIs."""
|
| 2 |
+
|
| 3 |
+
__all__ = ["Yolo26nError"]
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Yolo26nError(RuntimeError):
|
| 7 |
+
"""A YOLO26n operation could not be completed."""
|
python-api/{yolo26n.py → arm_yolo26n/yolo26n.py}
RENAMED
|
@@ -1,38 +1,46 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import ctypes
|
| 6 |
-
import hashlib
|
| 7 |
-
import json
|
| 8 |
import os
|
| 9 |
import threading
|
| 10 |
-
from collections.abc import Mapping
|
| 11 |
from dataclasses import dataclass
|
| 12 |
from pathlib import Path
|
| 13 |
from types import TracebackType
|
| 14 |
-
from typing import Self
|
| 15 |
|
| 16 |
import numpy as np
|
| 17 |
-
import
|
| 18 |
|
| 19 |
-
from .
|
|
|
|
| 20 |
|
| 21 |
-
__all__ = ["
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
_PIXEL_FORMATS = {"BGR": (0, 3), "RGB": (1, 3), "BGRX": (2, 4)}
|
| 32 |
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def _elements(shape: tuple[int, ...]) -> int:
|
|
@@ -42,148 +50,10 @@ def _elements(shape: tuple[int, ...]) -> int:
|
|
| 42 |
return result
|
| 43 |
|
| 44 |
|
| 45 |
-
def _sha256(path: Path) -> str:
|
| 46 |
-
digest = hashlib.sha256()
|
| 47 |
-
with path.open("rb") as stream:
|
| 48 |
-
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
| 49 |
-
digest.update(block)
|
| 50 |
-
return digest.hexdigest()
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def _read_manifest(path: Path) -> Mapping[str, object]:
|
| 54 |
-
try:
|
| 55 |
-
value: object = json.loads(path.read_text(encoding="utf-8"))
|
| 56 |
-
except FileNotFoundError:
|
| 57 |
-
raise FileNotFoundError(f"YOLO26n runtime manifest does not exist: {path}") from None
|
| 58 |
-
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
| 59 |
-
raise Yolo26RuntimeError(f"invalid YOLO26n runtime manifest: {path}") from error
|
| 60 |
-
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
| 61 |
-
raise Yolo26RuntimeError("YOLO26n runtime manifest must be an object")
|
| 62 |
-
return cast(Mapping[str, object], value)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def _mapping(value: object, name: str) -> Mapping[str, object]:
|
| 66 |
-
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
|
| 67 |
-
raise Yolo26RuntimeError(f"invalid {name} in YOLO26n runtime manifest")
|
| 68 |
-
return cast(Mapping[str, object], value)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def _shape(value: object, name: str) -> tuple[int, ...]:
|
| 72 |
-
if (
|
| 73 |
-
not isinstance(value, list)
|
| 74 |
-
or not value
|
| 75 |
-
or any(type(dimension) is not int or dimension <= 0 for dimension in value)
|
| 76 |
-
):
|
| 77 |
-
raise Yolo26RuntimeError(f"invalid {name} in YOLO26n runtime manifest")
|
| 78 |
-
return tuple(value)
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
@dataclass(frozen=True, slots=True)
|
| 82 |
-
class _ArtifactIdentity:
|
| 83 |
-
relative_path: Path
|
| 84 |
-
size: int
|
| 85 |
-
sha256: str
|
| 86 |
-
|
| 87 |
-
@classmethod
|
| 88 |
-
def parse(cls, value: object, name: str) -> Self:
|
| 89 |
-
record = _mapping(value, f"{name} identity")
|
| 90 |
-
if set(record) != {"bytes", "path", "sha256"}:
|
| 91 |
-
raise Yolo26RuntimeError(f"invalid {name} identity in YOLO26n runtime manifest")
|
| 92 |
-
path_value = record["path"]
|
| 93 |
-
size = record["bytes"]
|
| 94 |
-
sha256 = record["sha256"]
|
| 95 |
-
if (
|
| 96 |
-
not isinstance(path_value, str)
|
| 97 |
-
or type(size) is not int
|
| 98 |
-
or size < 0
|
| 99 |
-
or not isinstance(sha256, str)
|
| 100 |
-
or len(sha256) != 64
|
| 101 |
-
or any(character not in "0123456789abcdef" for character in sha256)
|
| 102 |
-
):
|
| 103 |
-
raise Yolo26RuntimeError(f"invalid {name} identity in YOLO26n runtime manifest")
|
| 104 |
-
relative_path = Path(path_value)
|
| 105 |
-
if relative_path.is_absolute() or ".." in relative_path.parts:
|
| 106 |
-
raise Yolo26RuntimeError(f"invalid {name} path in YOLO26n runtime manifest")
|
| 107 |
-
return cls(relative_path=relative_path, size=size, sha256=sha256)
|
| 108 |
-
|
| 109 |
-
def resolve(self, bundle: Path, name: str) -> Path:
|
| 110 |
-
path = bundle / self.relative_path
|
| 111 |
-
if not path.is_file() or path.stat().st_size != self.size or _sha256(path) != self.sha256:
|
| 112 |
-
raise Yolo26RuntimeError(f"YOLO26n runtime {name} identity mismatch")
|
| 113 |
-
return path
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
@dataclass(frozen=True, slots=True)
|
| 117 |
-
class _BundleData:
|
| 118 |
-
input_shape: tuple[int, int, int, int]
|
| 119 |
-
output_shape: tuple[int, int, int]
|
| 120 |
-
class_names: tuple[str, ...]
|
| 121 |
-
library_path: Path
|
| 122 |
-
model_path: Path
|
| 123 |
-
|
| 124 |
-
@classmethod
|
| 125 |
-
def load(cls, bundle: str | Path) -> Self:
|
| 126 |
-
directory = Path(bundle).expanduser().resolve()
|
| 127 |
-
if not directory.is_dir():
|
| 128 |
-
raise FileNotFoundError(f"YOLO26n runtime bundle does not exist: {directory}")
|
| 129 |
-
manifest = _read_manifest(directory / "manifest.json")
|
| 130 |
-
if (manifest.get("kind"), manifest.get("version")) != (
|
| 131 |
-
_BUNDLE_KIND,
|
| 132 |
-
_BUNDLE_VERSION,
|
| 133 |
-
):
|
| 134 |
-
raise Yolo26RuntimeError("unsupported YOLO26n runtime bundle kind or version")
|
| 135 |
-
|
| 136 |
-
model = _mapping(manifest.get("model"), "model record")
|
| 137 |
-
input_shape = _shape(model.get("input_shape"), "input shape")
|
| 138 |
-
output_shape = _shape(model.get("output_shape"), "output shape")
|
| 139 |
-
resolution = input_shape[2] if len(input_shape) == 4 else None
|
| 140 |
-
if (
|
| 141 |
-
len(input_shape) != 4
|
| 142 |
-
or input_shape[:2] != (1, 3)
|
| 143 |
-
or input_shape[2] != input_shape[3]
|
| 144 |
-
or resolution not in _SUPPORTED_RESOLUTIONS
|
| 145 |
-
):
|
| 146 |
-
raise Yolo26RuntimeError("unsupported YOLO26n input shape")
|
| 147 |
-
if (
|
| 148 |
-
len(output_shape) != 3
|
| 149 |
-
or output_shape[0] != 1
|
| 150 |
-
or output_shape[1] <= _BOX_COORDINATE_CHANNELS
|
| 151 |
-
or output_shape[2] != _detection_positions(cast(int, resolution))
|
| 152 |
-
):
|
| 153 |
-
raise Yolo26RuntimeError("unsupported YOLO26n output shape")
|
| 154 |
-
|
| 155 |
-
expected_target = {
|
| 156 |
-
"version": 1,
|
| 157 |
-
"target": "linux-aarch64-raspberry-pi-5",
|
| 158 |
-
"input_shape": list(input_shape),
|
| 159 |
-
"input_dtype": "fp32",
|
| 160 |
-
"output_dtype": "fp32",
|
| 161 |
-
"thread_count": _THREAD_COUNT,
|
| 162 |
-
}
|
| 163 |
-
if manifest.get("target") != expected_target:
|
| 164 |
-
raise Yolo26RuntimeError("unsupported YOLO26n target contract")
|
| 165 |
-
|
| 166 |
-
names = model.get("class_names")
|
| 167 |
-
if (
|
| 168 |
-
not isinstance(names, list)
|
| 169 |
-
or len(names) != output_shape[1] - _BOX_COORDINATE_CHANNELS
|
| 170 |
-
or any(not isinstance(name, str) or not name for name in names)
|
| 171 |
-
):
|
| 172 |
-
raise Yolo26RuntimeError("invalid class names in YOLO26n runtime manifest")
|
| 173 |
-
artifacts = _mapping(manifest.get("artifacts"), "artifact records")
|
| 174 |
-
runtime = _ArtifactIdentity.parse(artifacts.get("runtime"), "runtime")
|
| 175 |
-
exported_model = _ArtifactIdentity.parse(artifacts.get("model"), "model")
|
| 176 |
-
return cls(
|
| 177 |
-
input_shape=cast(tuple[int, int, int, int], input_shape),
|
| 178 |
-
output_shape=cast(tuple[int, int, int], output_shape),
|
| 179 |
-
class_names=tuple(cast(list[str], names)),
|
| 180 |
-
library_path=runtime.resolve(directory, "runtime"),
|
| 181 |
-
model_path=exported_model.resolve(directory, "model"),
|
| 182 |
-
)
|
| 183 |
-
|
| 184 |
-
|
| 185 |
@dataclass(frozen=True, slots=True)
|
| 186 |
class _NativeModelContract:
|
|
|
|
|
|
|
| 187 |
graph_count: int
|
| 188 |
resolution: int
|
| 189 |
class_count: int
|
|
@@ -192,12 +62,12 @@ class _NativeModelContract:
|
|
| 192 |
calls: int
|
| 193 |
|
| 194 |
|
| 195 |
-
class
|
| 196 |
"""Bind the YOLO26n C API and translate calls to Python values."""
|
| 197 |
|
| 198 |
def __init__(self, path: Path) -> None:
|
| 199 |
self._raw = ctypes.CDLL(str(path), mode=os.RTLD_NOW | os.RTLD_LOCAL)
|
| 200 |
-
self._raw.y26_native_create.argtypes = [ctypes.
|
| 201 |
self._raw.y26_native_create.restype = ctypes.c_void_p
|
| 202 |
self._raw.y26_native_execute.argtypes = [
|
| 203 |
ctypes.c_void_p,
|
|
@@ -212,6 +82,7 @@ class _Yolo26nLibrary:
|
|
| 212 |
ctypes.c_size_t,
|
| 213 |
ctypes.c_size_t,
|
| 214 |
ctypes.c_size_t,
|
|
|
|
| 215 |
ctypes.c_int,
|
| 216 |
ctypes.c_void_p,
|
| 217 |
ctypes.c_size_t,
|
|
@@ -232,8 +103,9 @@ class _Yolo26nLibrary:
|
|
| 232 |
function.argtypes = [ctypes.c_void_p]
|
| 233 |
function.restype = ctypes.c_size_t
|
| 234 |
|
| 235 |
-
def create(self, model:
|
| 236 |
-
|
|
|
|
| 237 |
|
| 238 |
def model_contract(self, handle: ctypes.c_void_p) -> _NativeModelContract:
|
| 239 |
return _NativeModelContract(
|
|
@@ -248,34 +120,36 @@ class _Yolo26nLibrary:
|
|
| 248 |
def execute(
|
| 249 |
self,
|
| 250 |
handle: ctypes.c_void_p,
|
| 251 |
-
model_input:
|
| 252 |
-
output:
|
| 253 |
) -> int:
|
| 254 |
return int(
|
| 255 |
self._raw.y26_native_execute(
|
| 256 |
handle,
|
| 257 |
-
model_input.
|
| 258 |
-
model_input.
|
| 259 |
-
output.
|
| 260 |
-
output.
|
| 261 |
)
|
| 262 |
)
|
| 263 |
|
| 264 |
def preprocess_image_no_resize(
|
| 265 |
self,
|
| 266 |
-
frame:
|
| 267 |
pixel_format: int,
|
| 268 |
-
output:
|
| 269 |
) -> int:
|
|
|
|
| 270 |
return int(
|
| 271 |
self._raw.y26_native_preprocess_image_no_resize(
|
| 272 |
frame.ctypes.data,
|
|
|
|
| 273 |
frame.shape[0],
|
| 274 |
frame.shape[1],
|
| 275 |
frame.strides[0],
|
| 276 |
pixel_format,
|
| 277 |
-
output.
|
| 278 |
-
output.
|
| 279 |
)
|
| 280 |
)
|
| 281 |
|
|
@@ -283,34 +157,56 @@ class _Yolo26nLibrary:
|
|
| 283 |
self._raw.y26_native_destroy(handle)
|
| 284 |
|
| 285 |
|
| 286 |
-
|
| 287 |
-
"""
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
self._lifecycle_lock = threading.Lock()
|
| 295 |
self._handle = ctypes.c_void_p()
|
| 296 |
try:
|
| 297 |
-
self._native =
|
| 298 |
-
self._handle = self._native.create(
|
| 299 |
if not self._handle.value:
|
| 300 |
-
raise
|
| 301 |
expected_input_bytes = 4 * _elements(self.input_shape)
|
| 302 |
expected_output_bytes = 4 * _elements(self.output_shape)
|
| 303 |
native_contract = self._native.model_contract(self._handle)
|
| 304 |
if (
|
| 305 |
-
native_contract.graph_count !=
|
| 306 |
or native_contract.resolution != self.input_shape[2]
|
| 307 |
or native_contract.class_count != len(self.class_names)
|
| 308 |
or native_contract.input_bytes != expected_input_bytes
|
| 309 |
or native_contract.output_bytes != expected_output_bytes
|
| 310 |
):
|
| 311 |
-
raise
|
| 312 |
if native_contract.calls != 0:
|
| 313 |
-
raise
|
| 314 |
except BaseException:
|
| 315 |
self.close()
|
| 316 |
raise
|
|
@@ -319,79 +215,90 @@ class Yolo26nRuntime:
|
|
| 319 |
def closed(self) -> bool:
|
| 320 |
return not bool(self._handle.value)
|
| 321 |
|
| 322 |
-
def execute(self, tensor:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
if self.closed:
|
| 324 |
-
raise
|
| 325 |
-
if not isinstance(tensor,
|
| 326 |
-
raise TypeError("input must be a
|
| 327 |
-
if tensor.
|
| 328 |
-
raise
|
| 329 |
-
if tensor.dtype is not torch.float32:
|
| 330 |
-
raise TypeError("input tensor must have dtype torch.float32")
|
| 331 |
if tuple(tensor.shape) != self.input_shape:
|
| 332 |
raise ValueError(f"input shape {tuple(tensor.shape)} does not match {self.input_shape}")
|
| 333 |
-
|
| 334 |
-
tensor = tensor.contiguous()
|
| 335 |
if not self._lifecycle_lock.acquire(blocking=False):
|
| 336 |
-
raise
|
| 337 |
try:
|
| 338 |
if self.closed:
|
| 339 |
-
raise
|
| 340 |
-
output =
|
| 341 |
status = self._native.execute(self._handle, tensor, output)
|
| 342 |
-
if status !=
|
| 343 |
-
raise
|
| 344 |
return output
|
| 345 |
finally:
|
| 346 |
self._lifecycle_lock.release()
|
| 347 |
|
| 348 |
-
def
|
| 349 |
self,
|
| 350 |
-
frame:
|
| 351 |
*,
|
| 352 |
pixel_format: str,
|
| 353 |
-
output:
|
| 354 |
) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
if self.closed:
|
| 356 |
-
raise
|
| 357 |
if not isinstance(frame, np.ndarray) or frame.dtype != np.uint8:
|
| 358 |
raise TypeError("frame must be a uint8 numpy.ndarray")
|
| 359 |
try:
|
| 360 |
-
|
| 361 |
except KeyError as error:
|
| 362 |
raise ValueError("pixel_format must be 'BGR', 'RGB', or 'BGRX'") from error
|
| 363 |
-
if frame.ndim != 3 or frame.shape[2] != channels:
|
| 364 |
-
raise ValueError(
|
| 365 |
-
|
|
|
|
|
|
|
| 366 |
raise ValueError("frame must have interleaved, forward uint8 pixels")
|
| 367 |
if (
|
| 368 |
-
not isinstance(output,
|
| 369 |
-
or output.
|
| 370 |
-
or output.dtype is not torch.float32
|
| 371 |
or tuple(output.shape) != self.input_shape
|
| 372 |
-
or not output.
|
| 373 |
):
|
| 374 |
raise ValueError(
|
| 375 |
-
f"output must be a contiguous
|
| 376 |
)
|
| 377 |
if not self._lifecycle_lock.acquire(blocking=False):
|
| 378 |
-
raise
|
| 379 |
try:
|
| 380 |
if self.closed:
|
| 381 |
-
raise
|
| 382 |
status = self._native.preprocess_image_no_resize(
|
| 383 |
frame,
|
| 384 |
-
|
| 385 |
output,
|
| 386 |
)
|
| 387 |
-
if status !=
|
| 388 |
-
raise
|
| 389 |
-
f"YOLO26n runtime preprocessing failed with status {status}"
|
| 390 |
-
)
|
| 391 |
finally:
|
| 392 |
self._lifecycle_lock.release()
|
| 393 |
|
| 394 |
def close(self) -> None:
|
|
|
|
| 395 |
lock = getattr(self, "_lifecycle_lock", None)
|
| 396 |
if lock is None:
|
| 397 |
return
|
|
@@ -404,7 +311,7 @@ class Yolo26nRuntime:
|
|
| 404 |
|
| 405 |
def __enter__(self) -> Self:
|
| 406 |
if self.closed:
|
| 407 |
-
raise
|
| 408 |
return self
|
| 409 |
|
| 410 |
def __exit__(
|
|
|
|
| 1 |
+
"""Low-level tensor access to the native YOLO26n runtime."""
|
| 2 |
|
| 3 |
from __future__ import annotations
|
| 4 |
|
| 5 |
import ctypes
|
|
|
|
|
|
|
| 6 |
import os
|
| 7 |
import threading
|
|
|
|
| 8 |
from dataclasses import dataclass
|
| 9 |
from pathlib import Path
|
| 10 |
from types import TracebackType
|
| 11 |
+
from typing import Self
|
| 12 |
|
| 13 |
import numpy as np
|
| 14 |
+
from numpy.typing import NDArray
|
| 15 |
|
| 16 |
+
from ._model import _load_model
|
| 17 |
+
from .errors import Yolo26nError
|
| 18 |
|
| 19 |
+
__all__ = ["Yolo26nTensorSession"]
|
| 20 |
|
| 21 |
+
# The fixed topology is deliberately partitioned into nineteen XNNPACK
|
| 22 |
+
# subgraphs so the native schedule can run custom work between graph segments.
|
| 23 |
+
_EXPECTED_GRAPH_COUNT = 19
|
| 24 |
+
_NATIVE_STATUS_OK = 0
|
| 25 |
+
type _FloatArray = NDArray[np.float32]
|
| 26 |
+
type _Uint8Array = NDArray[np.uint8]
|
| 27 |
|
| 28 |
+
|
| 29 |
+
@dataclass(frozen=True, slots=True)
|
| 30 |
+
class _PixelFormat:
|
| 31 |
+
"""Python view of one y26_native_pixel_format value."""
|
| 32 |
+
|
| 33 |
+
native_value: int
|
| 34 |
+
channels: int
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
+
# Native values must remain synchronized with y26_native_pixel_format in
|
| 38 |
+
# arm_yolo26n.h. Channel counts validate the interleaved source array.
|
| 39 |
+
_PIXEL_FORMATS = {
|
| 40 |
+
"BGR": _PixelFormat(native_value=0, channels=3),
|
| 41 |
+
"RGB": _PixelFormat(native_value=1, channels=3),
|
| 42 |
+
"BGRX": _PixelFormat(native_value=2, channels=4),
|
| 43 |
+
}
|
| 44 |
|
| 45 |
|
| 46 |
def _elements(shape: tuple[int, ...]) -> int:
|
|
|
|
| 50 |
return result
|
| 51 |
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
@dataclass(frozen=True, slots=True)
|
| 54 |
class _NativeModelContract:
|
| 55 |
+
"""Model facts reported by a newly created native handle."""
|
| 56 |
+
|
| 57 |
graph_count: int
|
| 58 |
resolution: int
|
| 59 |
class_count: int
|
|
|
|
| 62 |
calls: int
|
| 63 |
|
| 64 |
|
| 65 |
+
class _NativeLibrary:
|
| 66 |
"""Bind the YOLO26n C API and translate calls to Python values."""
|
| 67 |
|
| 68 |
def __init__(self, path: Path) -> None:
|
| 69 |
self._raw = ctypes.CDLL(str(path), mode=os.RTLD_NOW | os.RTLD_LOCAL)
|
| 70 |
+
self._raw.y26_native_create.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
| 71 |
self._raw.y26_native_create.restype = ctypes.c_void_p
|
| 72 |
self._raw.y26_native_execute.argtypes = [
|
| 73 |
ctypes.c_void_p,
|
|
|
|
| 82 |
ctypes.c_size_t,
|
| 83 |
ctypes.c_size_t,
|
| 84 |
ctypes.c_size_t,
|
| 85 |
+
ctypes.c_size_t,
|
| 86 |
ctypes.c_int,
|
| 87 |
ctypes.c_void_p,
|
| 88 |
ctypes.c_size_t,
|
|
|
|
| 103 |
function.argtypes = [ctypes.c_void_p]
|
| 104 |
function.restype = ctypes.c_size_t
|
| 105 |
|
| 106 |
+
def create(self, model: bytes) -> ctypes.c_void_p:
|
| 107 |
+
model_pointer = ctypes.cast(ctypes.c_char_p(model), ctypes.c_void_p)
|
| 108 |
+
return ctypes.c_void_p(self._raw.y26_native_create(model_pointer, len(model)))
|
| 109 |
|
| 110 |
def model_contract(self, handle: ctypes.c_void_p) -> _NativeModelContract:
|
| 111 |
return _NativeModelContract(
|
|
|
|
| 120 |
def execute(
|
| 121 |
self,
|
| 122 |
handle: ctypes.c_void_p,
|
| 123 |
+
model_input: _FloatArray,
|
| 124 |
+
output: _FloatArray,
|
| 125 |
) -> int:
|
| 126 |
return int(
|
| 127 |
self._raw.y26_native_execute(
|
| 128 |
handle,
|
| 129 |
+
model_input.ctypes.data,
|
| 130 |
+
model_input.nbytes,
|
| 131 |
+
output.ctypes.data,
|
| 132 |
+
output.nbytes,
|
| 133 |
)
|
| 134 |
)
|
| 135 |
|
| 136 |
def preprocess_image_no_resize(
|
| 137 |
self,
|
| 138 |
+
frame: _Uint8Array,
|
| 139 |
pixel_format: int,
|
| 140 |
+
output: _FloatArray,
|
| 141 |
) -> int:
|
| 142 |
+
"""Invoke native preprocessing while preserving padded source rows."""
|
| 143 |
return int(
|
| 144 |
self._raw.y26_native_preprocess_image_no_resize(
|
| 145 |
frame.ctypes.data,
|
| 146 |
+
_image_span_bytes(frame),
|
| 147 |
frame.shape[0],
|
| 148 |
frame.shape[1],
|
| 149 |
frame.strides[0],
|
| 150 |
pixel_format,
|
| 151 |
+
output.ctypes.data,
|
| 152 |
+
output.nbytes,
|
| 153 |
)
|
| 154 |
)
|
| 155 |
|
|
|
|
| 157 |
self._raw.y26_native_destroy(handle)
|
| 158 |
|
| 159 |
|
| 160 |
+
def _image_span_bytes(frame: _Uint8Array) -> int:
|
| 161 |
+
"""Return bytes through the final pixel, including padding between rows."""
|
| 162 |
+
final_row_offset = (frame.shape[0] - 1) * frame.strides[0]
|
| 163 |
+
visible_row_bytes = frame.shape[1] * frame.shape[2]
|
| 164 |
+
return int(final_row_offset + visible_row_bytes)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class Yolo26nTensorSession:
|
| 168 |
+
"""Execute raw tensors with the native runtime and converted model.
|
| 169 |
+
|
| 170 |
+
This is the low-level Python interface. Use it when the caller already
|
| 171 |
+
owns preprocessing and postprocessing and needs direct FP32 NCHW tensor
|
| 172 |
+
execution. `Yolo26Detector` is the image-oriented API.
|
| 173 |
+
|
| 174 |
+
A session owns one native model handle and its worker resources. Calls are
|
| 175 |
+
deliberately non-reentrant; create independent sessions in separate
|
| 176 |
+
processes when concurrent isolation is required. Prefer using the session
|
| 177 |
+
as a context manager so the native resources are released deterministically.
|
| 178 |
+
The runtime and model paths are keyword-only so callers cannot silently
|
| 179 |
+
reverse two path-like arguments.
|
| 180 |
+
"""
|
| 181 |
+
|
| 182 |
+
def __init__(self, *, runtime: str | Path, model: str | Path) -> None:
|
| 183 |
+
runtime_path = Path(runtime).expanduser().resolve()
|
| 184 |
+
if not runtime_path.is_file():
|
| 185 |
+
raise FileNotFoundError(f"YOLO26n runtime library does not exist: {runtime_path}")
|
| 186 |
+
model_data = _load_model(model)
|
| 187 |
+
self.input_shape = model_data.input_shape
|
| 188 |
+
self.output_shape = model_data.output_shape
|
| 189 |
+
self.class_names = model_data.class_names
|
| 190 |
self._lifecycle_lock = threading.Lock()
|
| 191 |
self._handle = ctypes.c_void_p()
|
| 192 |
try:
|
| 193 |
+
self._native = _NativeLibrary(runtime_path)
|
| 194 |
+
self._handle = self._native.create(model_data.model_bytes)
|
| 195 |
if not self._handle.value:
|
| 196 |
+
raise Yolo26nError("YOLO26n runtime rejected its exported model")
|
| 197 |
expected_input_bytes = 4 * _elements(self.input_shape)
|
| 198 |
expected_output_bytes = 4 * _elements(self.output_shape)
|
| 199 |
native_contract = self._native.model_contract(self._handle)
|
| 200 |
if (
|
| 201 |
+
native_contract.graph_count != _EXPECTED_GRAPH_COUNT
|
| 202 |
or native_contract.resolution != self.input_shape[2]
|
| 203 |
or native_contract.class_count != len(self.class_names)
|
| 204 |
or native_contract.input_bytes != expected_input_bytes
|
| 205 |
or native_contract.output_bytes != expected_output_bytes
|
| 206 |
):
|
| 207 |
+
raise Yolo26nError("YOLO26n runtime and model contracts disagree")
|
| 208 |
if native_contract.calls != 0:
|
| 209 |
+
raise Yolo26nError("YOLO26n runtime call counter was not initialized")
|
| 210 |
except BaseException:
|
| 211 |
self.close()
|
| 212 |
raise
|
|
|
|
| 215 |
def closed(self) -> bool:
|
| 216 |
return not bool(self._handle.value)
|
| 217 |
|
| 218 |
+
def execute(self, tensor: _FloatArray) -> _FloatArray:
|
| 219 |
+
"""Execute one FP32 NCHW tensor and return the raw YOLO26n prediction.
|
| 220 |
+
|
| 221 |
+
The input must match `input_shape`. A non-contiguous array is copied
|
| 222 |
+
into contiguous storage before entering native code. The returned FP32
|
| 223 |
+
array always has `output_shape`.
|
| 224 |
+
"""
|
| 225 |
if self.closed:
|
| 226 |
+
raise Yolo26nError("YOLO26n runtime is closed")
|
| 227 |
+
if not isinstance(tensor, np.ndarray):
|
| 228 |
+
raise TypeError("input must be a numpy.ndarray")
|
| 229 |
+
if tensor.dtype != np.float32:
|
| 230 |
+
raise TypeError("input array must have dtype float32")
|
|
|
|
|
|
|
| 231 |
if tuple(tensor.shape) != self.input_shape:
|
| 232 |
raise ValueError(f"input shape {tuple(tensor.shape)} does not match {self.input_shape}")
|
| 233 |
+
tensor = np.ascontiguousarray(tensor)
|
|
|
|
| 234 |
if not self._lifecycle_lock.acquire(blocking=False):
|
| 235 |
+
raise Yolo26nError("concurrent execution or close is not supported")
|
| 236 |
try:
|
| 237 |
if self.closed:
|
| 238 |
+
raise Yolo26nError("YOLO26n runtime is closed")
|
| 239 |
+
output = np.empty(self.output_shape, dtype=np.float32)
|
| 240 |
status = self._native.execute(self._handle, tensor, output)
|
| 241 |
+
if status != _NATIVE_STATUS_OK:
|
| 242 |
+
raise Yolo26nError(f"YOLO26n runtime execution failed with status {status}")
|
| 243 |
return output
|
| 244 |
finally:
|
| 245 |
self._lifecycle_lock.release()
|
| 246 |
|
| 247 |
+
def _prepare_image_without_resize(
|
| 248 |
self,
|
| 249 |
+
frame: _Uint8Array,
|
| 250 |
*,
|
| 251 |
pixel_format: str,
|
| 252 |
+
output: _FloatArray,
|
| 253 |
) -> None:
|
| 254 |
+
"""Fill a model-input tensor from an image that needs padding but no resize.
|
| 255 |
+
|
| 256 |
+
The detector uses this package-internal fast path after determining
|
| 257 |
+
that the source image's longest side already equals the model
|
| 258 |
+
resolution. Native code fuses centered value-114 padding, BGR/BGRX to
|
| 259 |
+
RGB conversion, HWC-to-NCHW conversion, and FP32 normalization. Callers
|
| 260 |
+
of the public tensor API should prepare their own input instead.
|
| 261 |
+
"""
|
| 262 |
if self.closed:
|
| 263 |
+
raise Yolo26nError("YOLO26n runtime is closed")
|
| 264 |
if not isinstance(frame, np.ndarray) or frame.dtype != np.uint8:
|
| 265 |
raise TypeError("frame must be a uint8 numpy.ndarray")
|
| 266 |
try:
|
| 267 |
+
format_contract = _PIXEL_FORMATS[pixel_format]
|
| 268 |
except KeyError as error:
|
| 269 |
raise ValueError("pixel_format must be 'BGR', 'RGB', or 'BGRX'") from error
|
| 270 |
+
if frame.ndim != 3 or frame.shape[2] != format_contract.channels:
|
| 271 |
+
raise ValueError(
|
| 272 |
+
f"{pixel_format} frame must have shape [height, width, {format_contract.channels}]"
|
| 273 |
+
)
|
| 274 |
+
if frame.strides[1:] != (format_contract.channels, 1) or frame.strides[0] <= 0:
|
| 275 |
raise ValueError("frame must have interleaved, forward uint8 pixels")
|
| 276 |
if (
|
| 277 |
+
not isinstance(output, np.ndarray)
|
| 278 |
+
or output.dtype != np.float32
|
|
|
|
| 279 |
or tuple(output.shape) != self.input_shape
|
| 280 |
+
or not output.flags.c_contiguous
|
| 281 |
):
|
| 282 |
raise ValueError(
|
| 283 |
+
f"output must be a contiguous FP32 array with shape {self.input_shape}"
|
| 284 |
)
|
| 285 |
if not self._lifecycle_lock.acquire(blocking=False):
|
| 286 |
+
raise Yolo26nError("concurrent execution or close is not supported")
|
| 287 |
try:
|
| 288 |
if self.closed:
|
| 289 |
+
raise Yolo26nError("YOLO26n runtime is closed")
|
| 290 |
status = self._native.preprocess_image_no_resize(
|
| 291 |
frame,
|
| 292 |
+
format_contract.native_value,
|
| 293 |
output,
|
| 294 |
)
|
| 295 |
+
if status != _NATIVE_STATUS_OK:
|
| 296 |
+
raise Yolo26nError(f"YOLO26n runtime preprocessing failed with status {status}")
|
|
|
|
|
|
|
| 297 |
finally:
|
| 298 |
self._lifecycle_lock.release()
|
| 299 |
|
| 300 |
def close(self) -> None:
|
| 301 |
+
"""Release the native model handle; repeated calls are safe."""
|
| 302 |
lock = getattr(self, "_lifecycle_lock", None)
|
| 303 |
if lock is None:
|
| 304 |
return
|
|
|
|
| 311 |
|
| 312 |
def __enter__(self) -> Self:
|
| 313 |
if self.closed:
|
| 314 |
+
raise Yolo26nError("YOLO26n runtime is closed")
|
| 315 |
return self
|
| 316 |
|
| 317 |
def __exit__(
|
python-api/errors.py
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
"""Errors shared by the native YOLO26 runtime wrappers."""
|
| 2 |
-
|
| 3 |
-
__all__ = ["Yolo26RuntimeError"]
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
class Yolo26RuntimeError(RuntimeError):
|
| 7 |
-
"""A native YOLO26 runtime rejected an operation."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
repro.py
CHANGED
|
@@ -14,14 +14,24 @@ import subprocess
|
|
| 14 |
import sys
|
| 15 |
import tempfile
|
| 16 |
import time
|
|
|
|
| 17 |
|
| 18 |
|
| 19 |
ROOT = Path(__file__).resolve().parent
|
| 20 |
VENV = ROOT / ".venv"
|
| 21 |
MIB = 1024 * 1024
|
| 22 |
DATASET_REPO = "Arm/coco-128-by-sa"
|
| 23 |
-
DATASET_REVISION = "
|
| 24 |
DATASET_DIR = ROOT / "data/coco-128-by-sa"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
@@ -34,6 +44,9 @@ def setup() -> None:
|
|
| 34 |
model_path = Path("/proc/device-tree/model")
|
| 35 |
if not model_path.is_file() or "Raspberry Pi 5" not in model_path.read_text():
|
| 36 |
raise SystemExit("this package is for Raspberry Pi 5")
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
run([sys.executable, "-m", "venv", str(VENV)])
|
| 39 |
python = VENV / "bin/python"
|
|
@@ -49,7 +62,7 @@ def setup() -> None:
|
|
| 49 |
"torch==2.12.1+cpu",
|
| 50 |
"torchvision==0.27.1+cpu",
|
| 51 |
"ultralytics==8.4.108",
|
| 52 |
-
"numpy==2.
|
| 53 |
"opencv-python==5.0.0.93",
|
| 54 |
"pillow==12.3.0",
|
| 55 |
"PyYAML==6.0.3",
|
|
@@ -63,17 +76,15 @@ def setup() -> None:
|
|
| 63 |
capture_output=True,
|
| 64 |
)
|
| 65 |
site = Path(site_result.stdout.strip())
|
| 66 |
-
api_destination = site / "
|
| 67 |
-
|
| 68 |
-
for source in (ROOT / "python-api").glob("*.py"):
|
| 69 |
-
shutil.copy2(source, api_destination / source.name)
|
| 70 |
shutil.copy2(
|
| 71 |
-
ROOT / "ultralytics/
|
| 72 |
-
site / "ultralytics/nn/backends/
|
| 73 |
)
|
| 74 |
|
| 75 |
patch_path = ROOT / "ultralytics/ultralytics-8.4.108-arm-yolo26-pi5.patch"
|
| 76 |
-
patch_data = patch_path.read_text()
|
| 77 |
patch_command = ["patch", "--forward", "-p1", "-d", str(site)]
|
| 78 |
forward = subprocess.run(
|
| 79 |
[*patch_command, "--dry-run"],
|
|
@@ -100,8 +111,8 @@ def setup() -> None:
|
|
| 100 |
str(python),
|
| 101 |
"-c",
|
| 102 |
"import ultralytics; "
|
| 103 |
-
"from
|
| 104 |
-
"from
|
| 105 |
"assert ultralytics.__version__ == '8.4.108'",
|
| 106 |
]
|
| 107 |
)
|
|
@@ -117,81 +128,61 @@ def sha256(path: Path) -> str:
|
|
| 117 |
return digest.hexdigest()
|
| 118 |
|
| 119 |
|
| 120 |
-
def
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
1,
|
| 126 |
-
):
|
| 127 |
-
raise SystemExit("unsupported exported model")
|
| 128 |
-
|
| 129 |
-
exported_artifact = exported_manifest["artifact"]
|
| 130 |
-
exported_model = exported_dir / exported_artifact["path"]
|
| 131 |
-
if (
|
| 132 |
-
not exported_model.is_file()
|
| 133 |
-
or exported_model.stat().st_size != exported_artifact["bytes"]
|
| 134 |
-
or sha256(exported_model) != exported_artifact["sha256"]
|
| 135 |
-
):
|
| 136 |
-
raise SystemExit("exported model identity mismatch")
|
| 137 |
-
|
| 138 |
-
model_record = dict(exported_manifest["model"])
|
| 139 |
-
target_record = model_record.pop("target")
|
| 140 |
-
bundle_manifest = {
|
| 141 |
-
"version": 1,
|
| 142 |
-
"kind": "yolo26n-native-bundle",
|
| 143 |
-
"model": model_record,
|
| 144 |
-
"target": target_record,
|
| 145 |
-
"artifacts": {
|
| 146 |
-
"runtime": reference_manifest["artifacts"]["runtime"],
|
| 147 |
-
"model": exported_artifact,
|
| 148 |
-
},
|
| 149 |
-
"notices": reference_manifest["notices"],
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
output.parent.mkdir(parents=True, exist_ok=True)
|
| 153 |
-
temporary = Path(tempfile.mkdtemp(prefix=f".{output.name}.", dir=output.parent))
|
| 154 |
try:
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
shutil.rmtree(temporary, ignore_errors=True)
|
| 164 |
-
raise
|
| 165 |
|
| 166 |
|
| 167 |
def export_model() -> None:
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
if
|
| 171 |
-
raise SystemExit(
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
def read_key_values(path: Path, multiplier: int = 1) -> dict[str, int]:
|
|
@@ -267,7 +258,7 @@ def preflight(paths: tuple[Path, ...]) -> None:
|
|
| 267 |
|
| 268 |
|
| 269 |
def ensure_dataset(python: Path) -> Path:
|
| 270 |
-
|
| 271 |
run(
|
| 272 |
[
|
| 273 |
str(python),
|
|
@@ -279,8 +270,19 @@ def ensure_dataset(python: Path) -> Path:
|
|
| 279 |
% (DATASET_REPO, DATASET_REVISION, str(DATASET_DIR)),
|
| 280 |
]
|
| 281 |
)
|
| 282 |
-
if not
|
| 283 |
-
raise SystemExit(f"dataset download did not produce {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
run(["sync", "-f", str(DATASET_DIR)])
|
| 285 |
return dataset_yaml.resolve()
|
| 286 |
|
|
@@ -289,12 +291,12 @@ def benchmark() -> None:
|
|
| 289 |
python = VENV / "bin/python"
|
| 290 |
if not python.is_file():
|
| 291 |
raise SystemExit("run 'python3 repro.py setup' first")
|
| 292 |
-
|
| 293 |
dataset_yaml = ensure_dataset(python)
|
| 294 |
|
| 295 |
-
model =
|
| 296 |
-
|
| 297 |
-
|
|
|
|
| 298 |
preflight((model.resolve(), DATASET_DIR.resolve()))
|
| 299 |
|
| 300 |
governor = Path("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
|
|
@@ -308,7 +310,7 @@ def benchmark() -> None:
|
|
| 308 |
str(python),
|
| 309 |
str(ROOT / "ultralytics/benchmark.py"),
|
| 310 |
"--checkpoint",
|
| 311 |
-
str(
|
| 312 |
"--model",
|
| 313 |
str(model),
|
| 314 |
"--data",
|
|
|
|
| 14 |
import sys
|
| 15 |
import tempfile
|
| 16 |
import time
|
| 17 |
+
from urllib.request import urlopen
|
| 18 |
|
| 19 |
|
| 20 |
ROOT = Path(__file__).resolve().parent
|
| 21 |
VENV = ROOT / ".venv"
|
| 22 |
MIB = 1024 * 1024
|
| 23 |
DATASET_REPO = "Arm/coco-128-by-sa"
|
| 24 |
+
DATASET_REVISION = "f8af0a317c0b54c3dbd38c70b9757dccb8b02460"
|
| 25 |
DATASET_DIR = ROOT / "data/coco-128-by-sa"
|
| 26 |
+
STOCK_CHECKPOINT_URL = (
|
| 27 |
+
"https://github.com/ultralytics/assets/releases/download/v8.4.0/yolo26n.pt"
|
| 28 |
+
)
|
| 29 |
+
STOCK_CHECKPOINT_SHA256 = (
|
| 30 |
+
"9b09cc8bf347f0fc8a5f7657480587f25db09b34bf33b0652110fb03a8ad4fef"
|
| 31 |
+
)
|
| 32 |
+
STOCK_CHECKPOINT = ROOT / "downloads/yolo26n.pt"
|
| 33 |
+
SUPPLIED_MODEL = ROOT / "runtime/arm_yolo26n_pi5_model"
|
| 34 |
+
EXPORTED_MODEL = ROOT / "build/arm_yolo26n_pi5_model"
|
| 35 |
|
| 36 |
|
| 37 |
def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
|
|
| 44 |
model_path = Path("/proc/device-tree/model")
|
| 45 |
if not model_path.is_file() or "Raspberry Pi 5" not in model_path.read_text():
|
| 46 |
raise SystemExit("this package is for Raspberry Pi 5")
|
| 47 |
+
os_release = Path("/etc/os-release").read_text(encoding="utf-8")
|
| 48 |
+
if "VERSION_CODENAME=trixie" not in os_release:
|
| 49 |
+
raise SystemExit("this package is validated on Raspberry Pi OS Trixie")
|
| 50 |
|
| 51 |
run([sys.executable, "-m", "venv", str(VENV)])
|
| 52 |
python = VENV / "bin/python"
|
|
|
|
| 62 |
"torch==2.12.1+cpu",
|
| 63 |
"torchvision==0.27.1+cpu",
|
| 64 |
"ultralytics==8.4.108",
|
| 65 |
+
"numpy==2.2.4",
|
| 66 |
"opencv-python==5.0.0.93",
|
| 67 |
"pillow==12.3.0",
|
| 68 |
"PyYAML==6.0.3",
|
|
|
|
| 76 |
capture_output=True,
|
| 77 |
)
|
| 78 |
site = Path(site_result.stdout.strip())
|
| 79 |
+
api_destination = site / "arm_yolo26n"
|
| 80 |
+
shutil.copytree(ROOT / "python-api/arm_yolo26n", api_destination, dirs_exist_ok=True)
|
|
|
|
|
|
|
| 81 |
shutil.copy2(
|
| 82 |
+
ROOT / "ultralytics/arm_yolo26n_pi5_backend.py",
|
| 83 |
+
site / "ultralytics/nn/backends/arm_yolo26n_pi5.py",
|
| 84 |
)
|
| 85 |
|
| 86 |
patch_path = ROOT / "ultralytics/ultralytics-8.4.108-arm-yolo26-pi5.patch"
|
| 87 |
+
patch_data = patch_path.read_text(encoding="utf-8")
|
| 88 |
patch_command = ["patch", "--forward", "-p1", "-d", str(site)]
|
| 89 |
forward = subprocess.run(
|
| 90 |
[*patch_command, "--dry-run"],
|
|
|
|
| 111 |
str(python),
|
| 112 |
"-c",
|
| 113 |
"import ultralytics; "
|
| 114 |
+
"from arm_yolo26n import Yolo26nTensorSession; "
|
| 115 |
+
"from ultralytics.nn.backends import ArmYolo26nPi5Backend; "
|
| 116 |
"assert ultralytics.__version__ == '8.4.108'",
|
| 117 |
]
|
| 118 |
)
|
|
|
|
| 128 |
return digest.hexdigest()
|
| 129 |
|
| 130 |
|
| 131 |
+
def ensure_stock_checkpoint() -> Path:
|
| 132 |
+
if STOCK_CHECKPOINT.is_file() and sha256(STOCK_CHECKPOINT) == STOCK_CHECKPOINT_SHA256:
|
| 133 |
+
return STOCK_CHECKPOINT
|
| 134 |
+
STOCK_CHECKPOINT.parent.mkdir(parents=True, exist_ok=True)
|
| 135 |
+
temporary = STOCK_CHECKPOINT.with_suffix(".pt.download")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
try:
|
| 137 |
+
with urlopen(STOCK_CHECKPOINT_URL) as response, temporary.open("wb") as output:
|
| 138 |
+
shutil.copyfileobj(response, output)
|
| 139 |
+
if sha256(temporary) != STOCK_CHECKPOINT_SHA256:
|
| 140 |
+
raise SystemExit("downloaded stock checkpoint failed SHA-256 verification")
|
| 141 |
+
temporary.replace(STOCK_CHECKPOINT)
|
| 142 |
+
finally:
|
| 143 |
+
temporary.unlink(missing_ok=True)
|
| 144 |
+
return STOCK_CHECKPOINT
|
|
|
|
|
|
|
| 145 |
|
| 146 |
|
| 147 |
def export_model() -> None:
|
| 148 |
+
python = VENV / "bin/python"
|
| 149 |
+
checkpoint = ROOT / "model/yolo26n.pt"
|
| 150 |
+
if not python.is_file():
|
| 151 |
+
raise SystemExit("run 'python3 repro.py setup' first")
|
| 152 |
+
if not checkpoint.is_file():
|
| 153 |
+
raise SystemExit("place a compatible checkpoint at model/yolo26n.pt")
|
| 154 |
+
if EXPORTED_MODEL.exists():
|
| 155 |
+
raise SystemExit(f"export output already exists: {EXPORTED_MODEL}")
|
| 156 |
+
|
| 157 |
+
build = ROOT / "build"
|
| 158 |
+
build.mkdir(exist_ok=True)
|
| 159 |
+
temporary = Path(tempfile.mkdtemp(prefix=".arm-yolo26n-export.", dir=build))
|
| 160 |
+
try:
|
| 161 |
+
converted_model = temporary / "model"
|
| 162 |
+
environment = os.environ.copy()
|
| 163 |
+
environment["PYTHONPATH"] = str(ROOT / "compiler")
|
| 164 |
+
run(
|
| 165 |
+
[
|
| 166 |
+
str(python),
|
| 167 |
+
"-m",
|
| 168 |
+
"converter.export_yolo26n",
|
| 169 |
+
"--checkpoint",
|
| 170 |
+
str(checkpoint),
|
| 171 |
+
"--resolution",
|
| 172 |
+
"640",
|
| 173 |
+
"--output-dir",
|
| 174 |
+
str(converted_model),
|
| 175 |
+
],
|
| 176 |
+
env=environment,
|
| 177 |
+
)
|
| 178 |
+
assembled = temporary / EXPORTED_MODEL.name
|
| 179 |
+
shutil.copytree(SUPPLIED_MODEL, assembled)
|
| 180 |
+
shutil.rmtree(assembled / "model")
|
| 181 |
+
shutil.copytree(converted_model, assembled / "model")
|
| 182 |
+
assembled.rename(EXPORTED_MODEL)
|
| 183 |
+
finally:
|
| 184 |
+
shutil.rmtree(temporary, ignore_errors=True)
|
| 185 |
+
print(f"exported runnable model to {EXPORTED_MODEL}")
|
| 186 |
|
| 187 |
|
| 188 |
def read_key_values(path: Path, multiplier: int = 1) -> dict[str, int]:
|
|
|
|
| 258 |
|
| 259 |
|
| 260 |
def ensure_dataset(python: Path) -> Path:
|
| 261 |
+
source_yaml = DATASET_DIR / "data.yaml"
|
| 262 |
run(
|
| 263 |
[
|
| 264 |
str(python),
|
|
|
|
| 270 |
% (DATASET_REPO, DATASET_REVISION, str(DATASET_DIR)),
|
| 271 |
]
|
| 272 |
)
|
| 273 |
+
if not source_yaml.is_file():
|
| 274 |
+
raise SystemExit(f"dataset download did not produce {source_yaml}")
|
| 275 |
+
|
| 276 |
+
dataset_yaml = DATASET_DIR / ".benchmark-data.yaml"
|
| 277 |
+
source_text = source_yaml.read_text(encoding="utf-8")
|
| 278 |
+
if "path: .\n" not in source_text:
|
| 279 |
+
raise SystemExit(f"unexpected dataset path in {source_yaml}")
|
| 280 |
+
dataset_yaml.write_text(
|
| 281 |
+
source_text.replace(
|
| 282 |
+
"path: .\n", f"path: {json.dumps(str(DATASET_DIR.resolve()))}\n", 1
|
| 283 |
+
),
|
| 284 |
+
encoding="utf-8",
|
| 285 |
+
)
|
| 286 |
run(["sync", "-f", str(DATASET_DIR)])
|
| 287 |
return dataset_yaml.resolve()
|
| 288 |
|
|
|
|
| 291 |
python = VENV / "bin/python"
|
| 292 |
if not python.is_file():
|
| 293 |
raise SystemExit("run 'python3 repro.py setup' first")
|
|
|
|
| 294 |
dataset_yaml = ensure_dataset(python)
|
| 295 |
|
| 296 |
+
model = EXPORTED_MODEL if EXPORTED_MODEL.is_dir() else SUPPLIED_MODEL
|
| 297 |
+
checkpoint = ROOT / "model/yolo26n.pt" if model == EXPORTED_MODEL else ensure_stock_checkpoint()
|
| 298 |
+
if not checkpoint.is_file():
|
| 299 |
+
raise SystemExit("the exported model requires its source checkpoint at model/yolo26n.pt")
|
| 300 |
preflight((model.resolve(), DATASET_DIR.resolve()))
|
| 301 |
|
| 302 |
governor = Path("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor")
|
|
|
|
| 310 |
str(python),
|
| 311 |
str(ROOT / "ultralytics/benchmark.py"),
|
| 312 |
"--checkpoint",
|
| 313 |
+
str(checkpoint),
|
| 314 |
"--model",
|
| 315 |
str(model),
|
| 316 |
"--data",
|
results/REFERENCE.md
CHANGED
|
@@ -1,14 +1,26 @@
|
|
| 1 |
-
# Reference
|
| 2 |
|
| 3 |
-
Measured
|
|
|
|
| 4 |
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
- Device and batch: CPU, batch 1
|
| 10 |
-
- CPU governor: `ondemand`
|
| 11 |
-
- mAP50-95: 0.4678
|
| 12 |
-
- Inference: 38.54 ms/image
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Reference benchmark
|
| 2 |
|
| 3 |
+
Measured on a Raspberry Pi 5 with Raspberry Pi OS Trixie using the packaged
|
| 4 |
+
workflow:
|
| 5 |
|
| 6 |
+
```sh
|
| 7 |
+
python3 repro.py setup
|
| 8 |
+
python3 repro.py benchmark
|
| 9 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
+
| Metric | Result |
|
| 12 |
+
| --- | ---: |
|
| 13 |
+
| Dataset | `Arm/coco-128-by-sa` (128 images, 1,050 instances) |
|
| 14 |
+
| Input | 640 x 640, batch 1 |
|
| 15 |
+
| mAP50-95 | 0.4866 |
|
| 16 |
+
| Inference time | 39.29 ms/image |
|
| 17 |
+
| Model size | 10.6 MB |
|
| 18 |
+
|
| 19 |
+
The page-cache preflight reported 7,054 MiB available memory, no dirty or
|
| 20 |
+
writeback data, no refaults or reclaim scans, and no swap activity. The model
|
| 21 |
+
and dataset were on ext4. The CPU governor was `ondemand`, and the device
|
| 22 |
+
reported no throttling (`0x0`).
|
| 23 |
+
|
| 24 |
+
The export path was also verified with the same stock checkpoint. It reproduced
|
| 25 |
+
the packaged `model.bin` byte-for-byte and completed the benchmark with the same
|
| 26 |
+
0.4866 mAP50-95 result.
|
runtime/arm_yolo26n_pi5_model/arm-yolo26n-pi5.yaml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
backend: arm_yolo26n_pi5
|
| 2 |
+
runtime: lib/libarm-yolo26n.so.0.1.0
|
| 3 |
+
model: model
|
runtime/arm_yolo26n_pi5_model/bundle/licenses/libmd/COPYRIGHT.debian
DELETED
|
@@ -1,177 +0,0 @@
|
|
| 1 |
-
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
| 2 |
-
|
| 3 |
-
Files:
|
| 4 |
-
*
|
| 5 |
-
Copyright:
|
| 6 |
-
Copyright © 2009, 2011, 2016 Guillem Jover <guillem@hadrons.org>
|
| 7 |
-
License: BSD-3-clause
|
| 8 |
-
Redistribution and use in source and binary forms, with or without
|
| 9 |
-
modification, are permitted provided that the following conditions
|
| 10 |
-
are met:
|
| 11 |
-
1. Redistributions of source code must retain the above copyright
|
| 12 |
-
notice, this list of conditions and the following disclaimer.
|
| 13 |
-
2. Redistributions in binary form must reproduce the above copyright
|
| 14 |
-
notice, this list of conditions and the following disclaimer in the
|
| 15 |
-
documentation and/or other materials provided with the distribution.
|
| 16 |
-
3. The name of the author may not be used to endorse or promote products
|
| 17 |
-
derived from this software without specific prior written permission.
|
| 18 |
-
.
|
| 19 |
-
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
| 20 |
-
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
| 21 |
-
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
| 22 |
-
THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
| 23 |
-
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
| 24 |
-
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
| 25 |
-
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
| 26 |
-
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
| 27 |
-
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
| 28 |
-
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| 29 |
-
|
| 30 |
-
Files:
|
| 31 |
-
include/sha2.h
|
| 32 |
-
src/sha2.c
|
| 33 |
-
Copyright:
|
| 34 |
-
Copyright © 2000-2001, Aaron D. Gifford
|
| 35 |
-
All rights reserved.
|
| 36 |
-
License: BSD-3-clause-Aaron-D-Gifford
|
| 37 |
-
Redistribution and use in source and binary forms, with or without
|
| 38 |
-
modification, are permitted provided that the following conditions
|
| 39 |
-
are met:
|
| 40 |
-
1. Redistributions of source code must retain the above copyright
|
| 41 |
-
notice, this list of conditions and the following disclaimer.
|
| 42 |
-
2. Redistributions in binary form must reproduce the above copyright
|
| 43 |
-
notice, this list of conditions and the following disclaimer in the
|
| 44 |
-
documentation and/or other materials provided with the distribution.
|
| 45 |
-
3. Neither the name of the copyright holder nor the names of contributors
|
| 46 |
-
may be used to endorse or promote products derived from this software
|
| 47 |
-
without specific prior written permission.
|
| 48 |
-
.
|
| 49 |
-
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTOR(S) ``AS IS'' AND
|
| 50 |
-
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 51 |
-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
| 52 |
-
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTOR(S) BE LIABLE
|
| 53 |
-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 54 |
-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
| 55 |
-
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
| 56 |
-
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
| 57 |
-
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
| 58 |
-
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
| 59 |
-
SUCH DAMAGE.
|
| 60 |
-
|
| 61 |
-
Files:
|
| 62 |
-
include/rmd160.h
|
| 63 |
-
src/rmd160.c
|
| 64 |
-
Copyright:
|
| 65 |
-
Copyright © 2001 Markus Friedl. All rights reserved.
|
| 66 |
-
License: BSD-2-clause
|
| 67 |
-
Redistribution and use in source and binary forms, with or without
|
| 68 |
-
modification, are permitted provided that the following conditions
|
| 69 |
-
are met:
|
| 70 |
-
1. Redistributions of source code must retain the above copyright
|
| 71 |
-
notice, this list of conditions and the following disclaimer.
|
| 72 |
-
2. Redistributions in binary form must reproduce the above copyright
|
| 73 |
-
notice, this list of conditions and the following disclaimer in the
|
| 74 |
-
documentation and/or other materials provided with the distribution.
|
| 75 |
-
.
|
| 76 |
-
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
| 77 |
-
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
| 78 |
-
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
| 79 |
-
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
| 80 |
-
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
| 81 |
-
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
| 82 |
-
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
| 83 |
-
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| 84 |
-
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
| 85 |
-
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| 86 |
-
|
| 87 |
-
Files:
|
| 88 |
-
src/md2.c
|
| 89 |
-
Copyright:
|
| 90 |
-
Copyright (c) 2001 The NetBSD Foundation, Inc.
|
| 91 |
-
All rights reserved.
|
| 92 |
-
.
|
| 93 |
-
This code is derived from software contributed to The NetBSD Foundation
|
| 94 |
-
by Andrew Brown.
|
| 95 |
-
License: BSD-2-clause-NetBSD
|
| 96 |
-
Redistribution and use in source and binary forms, with or without
|
| 97 |
-
modification, are permitted provided that the following conditions
|
| 98 |
-
are met:
|
| 99 |
-
1. Redistributions of source code must retain the above copyright
|
| 100 |
-
notice, this list of conditions and the following disclaimer.
|
| 101 |
-
2. Redistributions in binary form must reproduce the above copyright
|
| 102 |
-
notice, this list of conditions and the following disclaimer in the
|
| 103 |
-
documentation and/or other materials provided with the distribution.
|
| 104 |
-
.
|
| 105 |
-
THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
|
| 106 |
-
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
| 107 |
-
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
| 108 |
-
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
|
| 109 |
-
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
| 110 |
-
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
| 111 |
-
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
| 112 |
-
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
| 113 |
-
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
| 114 |
-
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
| 115 |
-
POSSIBILITY OF SUCH DAMAGE.
|
| 116 |
-
|
| 117 |
-
Files:
|
| 118 |
-
man/rmd160.3
|
| 119 |
-
man/sha1.3
|
| 120 |
-
man/sha2.3
|
| 121 |
-
Copyright:
|
| 122 |
-
Copyright © 1997, 2003, 2004 Todd C. Miller <Todd.Miller@courtesan.com>
|
| 123 |
-
License: ISC
|
| 124 |
-
Permission to use, copy, modify, and distribute this software for any
|
| 125 |
-
purpose with or without fee is hereby granted, provided that the above
|
| 126 |
-
copyright notice and this permission notice appear in all copies.
|
| 127 |
-
.
|
| 128 |
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
| 129 |
-
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
| 130 |
-
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
| 131 |
-
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
| 132 |
-
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
| 133 |
-
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
| 134 |
-
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
| 135 |
-
|
| 136 |
-
Files:
|
| 137 |
-
man/mdX.3
|
| 138 |
-
src/helper.c
|
| 139 |
-
Copyright:
|
| 140 |
-
Poul-Henning Kamp <phk@login.dkuug.dk>
|
| 141 |
-
License: Beerware
|
| 142 |
-
"THE BEER-WARE LICENSE" (Revision 42):
|
| 143 |
-
<phk@login.dkuug.dk> wrote this file. As long as you retain this notice you
|
| 144 |
-
can do whatever you want with this stuff. If we meet some day, and you think
|
| 145 |
-
this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
|
| 146 |
-
|
| 147 |
-
Files:
|
| 148 |
-
include/md4.h
|
| 149 |
-
src/md4.c
|
| 150 |
-
Copyright:
|
| 151 |
-
Colin Plumb
|
| 152 |
-
Todd C. Miller
|
| 153 |
-
License: public-domain-md4
|
| 154 |
-
This code implements the MD4 message-digest algorithm.
|
| 155 |
-
The algorithm is due to Ron Rivest. This code was
|
| 156 |
-
written by Colin Plumb in 1993, no copyright is claimed.
|
| 157 |
-
This code is in the public domain; do with it what you wish.
|
| 158 |
-
Todd C. Miller modified the MD5 code to do MD4 based on RFC 1186.
|
| 159 |
-
|
| 160 |
-
Files:
|
| 161 |
-
include/md5.h
|
| 162 |
-
src/md5.c
|
| 163 |
-
Copyright:
|
| 164 |
-
Colin Plumb
|
| 165 |
-
License: public-domain-md5
|
| 166 |
-
This code implements the MD5 message-digest algorithm.
|
| 167 |
-
The algorithm is due to Ron Rivest. This code was
|
| 168 |
-
written by Colin Plumb in 1993, no copyright is claimed.
|
| 169 |
-
This code is in the public domain; do with it what you wish.
|
| 170 |
-
|
| 171 |
-
Files:
|
| 172 |
-
include/sha1.h
|
| 173 |
-
src/sha1.c
|
| 174 |
-
Copyright:
|
| 175 |
-
Steve Reid <steve@edmweb.com>
|
| 176 |
-
License: public-domain-sha1
|
| 177 |
-
100% Public Domain
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
runtime/arm_yolo26n_pi5_model/bundle/manifest.json
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"version": 1,
|
| 3 |
-
"kind": "yolo26n-native-bundle",
|
| 4 |
-
"model": {
|
| 5 |
-
"checkpoint_sha256": "9b09cc8bf347f0fc8a5f7657480587f25db09b34bf33b0652110fb03a8ad4fef",
|
| 6 |
-
"class_names": [
|
| 7 |
-
"person",
|
| 8 |
-
"bicycle",
|
| 9 |
-
"car",
|
| 10 |
-
"motorcycle",
|
| 11 |
-
"airplane",
|
| 12 |
-
"bus",
|
| 13 |
-
"train",
|
| 14 |
-
"truck",
|
| 15 |
-
"boat",
|
| 16 |
-
"traffic light",
|
| 17 |
-
"fire hydrant",
|
| 18 |
-
"stop sign",
|
| 19 |
-
"parking meter",
|
| 20 |
-
"bench",
|
| 21 |
-
"bird",
|
| 22 |
-
"cat",
|
| 23 |
-
"dog",
|
| 24 |
-
"horse",
|
| 25 |
-
"sheep",
|
| 26 |
-
"cow",
|
| 27 |
-
"elephant",
|
| 28 |
-
"bear",
|
| 29 |
-
"zebra",
|
| 30 |
-
"giraffe",
|
| 31 |
-
"backpack",
|
| 32 |
-
"umbrella",
|
| 33 |
-
"handbag",
|
| 34 |
-
"tie",
|
| 35 |
-
"suitcase",
|
| 36 |
-
"frisbee",
|
| 37 |
-
"skis",
|
| 38 |
-
"snowboard",
|
| 39 |
-
"sports ball",
|
| 40 |
-
"kite",
|
| 41 |
-
"baseball bat",
|
| 42 |
-
"baseball glove",
|
| 43 |
-
"skateboard",
|
| 44 |
-
"surfboard",
|
| 45 |
-
"tennis racket",
|
| 46 |
-
"bottle",
|
| 47 |
-
"wine glass",
|
| 48 |
-
"cup",
|
| 49 |
-
"fork",
|
| 50 |
-
"knife",
|
| 51 |
-
"spoon",
|
| 52 |
-
"bowl",
|
| 53 |
-
"banana",
|
| 54 |
-
"apple",
|
| 55 |
-
"sandwich",
|
| 56 |
-
"orange",
|
| 57 |
-
"broccoli",
|
| 58 |
-
"carrot",
|
| 59 |
-
"hot dog",
|
| 60 |
-
"pizza",
|
| 61 |
-
"donut",
|
| 62 |
-
"cake",
|
| 63 |
-
"chair",
|
| 64 |
-
"couch",
|
| 65 |
-
"potted plant",
|
| 66 |
-
"bed",
|
| 67 |
-
"dining table",
|
| 68 |
-
"toilet",
|
| 69 |
-
"tv",
|
| 70 |
-
"laptop",
|
| 71 |
-
"mouse",
|
| 72 |
-
"remote",
|
| 73 |
-
"keyboard",
|
| 74 |
-
"cell phone",
|
| 75 |
-
"microwave",
|
| 76 |
-
"oven",
|
| 77 |
-
"toaster",
|
| 78 |
-
"sink",
|
| 79 |
-
"refrigerator",
|
| 80 |
-
"book",
|
| 81 |
-
"clock",
|
| 82 |
-
"vase",
|
| 83 |
-
"scissors",
|
| 84 |
-
"teddy bear",
|
| 85 |
-
"hair drier",
|
| 86 |
-
"toothbrush"
|
| 87 |
-
],
|
| 88 |
-
"input_shape": [ 1, 3, 640, 640 ],
|
| 89 |
-
"output_shape": [ 1, 84, 8400 ]
|
| 90 |
-
},
|
| 91 |
-
"target": {
|
| 92 |
-
"input_dtype" : "fp32",
|
| 93 |
-
"input_shape" : [ 1, 3, 640, 640 ],
|
| 94 |
-
"output_dtype" : "fp32",
|
| 95 |
-
"target" : "linux-aarch64-raspberry-pi-5",
|
| 96 |
-
"thread_count" : 4,
|
| 97 |
-
"version" : 1
|
| 98 |
-
},
|
| 99 |
-
"artifacts": {
|
| 100 |
-
"runtime": {"path":"native-runtime.so","bytes":1398048,"sha256":"8e48d898de082780b6ebadaffda0e04cb942a80be5911a2621651b30f7bd453b"},
|
| 101 |
-
"model": {"path":"model.bin","bytes":9650112,"sha256":"8d5a617332cdd1bab070f6d60a02e4cb1cd00a5da10381469d2cd613988f895b"}
|
| 102 |
-
},
|
| 103 |
-
"notices": [{"path":"licenses/cpuinfo/LICENSE","bytes":1424,"sha256":"8e7e60636c3aa0cb03571a1a841ce5697f9551ff92b3c426c2561613d15ade70"},{"path":"licenses/fxdiv/LICENSE","bytes":1117,"sha256":"2c511c2ad0f207cefa6d8c1a20a5b6fa1d12c15db96031a608e5bad92ae4053d"},{"path":"licenses/libmd/COPYRIGHT.debian","bytes":8018,"sha256":"4365ef6255ad553fce69dd4bc0e093472c5d0e41b8ea493a545cc926ce171aa6"},{"path":"licenses/pthreadpool/LICENSE","bytes":1377,"sha256":"955604be43dc2c71940b5285e59ba60bd5132953ada8ca2292042817349d9399"},{"path":"licenses/xnnpack/LICENSE","bytes":1560,"sha256":"63f519e15726f4c4f830bd958f694c84fecb4e0a4cacc527d2696bb71ef95ada"}]
|
| 104 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
runtime/arm_yolo26n_pi5_model/{bundle/native-runtime.so → lib/libarm-yolo26n.so.0.1.0}
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6b9978243ed1fd325e5b7b6a2bc616280862ed01edc1f653ee55b088658b60b2
|
| 3 |
+
size 1421264
|
runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/cpuinfo/LICENSE
RENAMED
|
File without changes
|
runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/fxdiv/LICENSE
RENAMED
|
File without changes
|
runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/pthreadpool/LICENSE
RENAMED
|
File without changes
|
runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/xnnpack/LICENSE
RENAMED
|
File without changes
|
runtime/arm_yolo26n_pi5_model/model/manifest.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"artifact":{"bytes":9640064,"path":"model.bin","sha256":"031991352d4c1bcca4ad5b6f7023b6e3d66bdf896b476c66b851b1a29c5bdaff"},"kind":"yolo26n-exported-model","model":{"checkpoint_sha256":"9b09cc8bf347f0fc8a5f7657480587f25db09b34bf33b0652110fb03a8ad4fef","class_names":["person","bicycle","car","motorcycle","airplane","bus","train","truck","boat","traffic light","fire hydrant","stop sign","parking meter","bench","bird","cat","dog","horse","sheep","cow","elephant","bear","zebra","giraffe","backpack","umbrella","handbag","tie","suitcase","frisbee","skis","snowboard","sports ball","kite","baseball bat","baseball glove","skateboard","surfboard","tennis racket","bottle","wine glass","cup","fork","knife","spoon","bowl","banana","apple","sandwich","orange","broccoli","carrot","hot dog","pizza","donut","cake","chair","couch","potted plant","bed","dining table","toilet","tv","laptop","mouse","remote","keyboard","cell phone","microwave","oven","toaster","sink","refrigerator","book","clock","vase","scissors","teddy bear","hair drier","toothbrush"],"input_shape":[1,3,640,640],"output_shape":[1,84,8400],"target":{"input_dtype":"fp32","input_shape":[1,3,640,640],"output_dtype":"fp32","target":"linux-aarch64-raspberry-pi-5","thread_count":4,"version":1}},"version":1}
|
runtime/arm_yolo26n_pi5_model/{bundle → model}/model.bin
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:031991352d4c1bcca4ad5b6f7023b6e3d66bdf896b476c66b851b1a29c5bdaff
|
| 3 |
+
size 9640064
|
ultralytics/arm_yolo26_pi5_backend.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
"""Ultralytics backend for the native YOLO26n Raspberry Pi 5 runtime."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
|
| 7 |
-
import torch
|
| 8 |
-
from ultralytics.nn.backends.base import BaseBackend
|
| 9 |
-
from ultralytics.utils import LOGGER
|
| 10 |
-
from yolo26_runtime import Yolo26nRuntime
|
| 11 |
-
|
| 12 |
-
_YOLO26N_SUFFIX = "arm_yolo26n_pi5_model"
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class ArmYolo26Pi5Backend(BaseBackend):
|
| 16 |
-
"""Run a YOLO26n package through its native runtime."""
|
| 17 |
-
|
| 18 |
-
def load_model(self, weight: str | Path) -> None:
|
| 19 |
-
model_directory = Path(weight).expanduser().resolve()
|
| 20 |
-
if not model_directory.is_dir():
|
| 21 |
-
raise ValueError(
|
| 22 |
-
f"Arm YOLO26 Pi 5 model must be a package directory: {model_directory}"
|
| 23 |
-
)
|
| 24 |
-
if not model_directory.name.endswith(_YOLO26N_SUFFIX):
|
| 25 |
-
raise ValueError(
|
| 26 |
-
"YOLO26n Pi 5 model directory must end with "
|
| 27 |
-
f"{_YOLO26N_SUFFIX}: {model_directory}"
|
| 28 |
-
)
|
| 29 |
-
self._load_yolo26n(model_directory)
|
| 30 |
-
|
| 31 |
-
def _load_yolo26n(self, model_directory: Path) -> None:
|
| 32 |
-
bundle_path = model_directory / "bundle"
|
| 33 |
-
LOGGER.info(f"Loading {model_directory} with ArmYolo26Pi5Backend...")
|
| 34 |
-
self.model = Yolo26nRuntime(bundle_path)
|
| 35 |
-
height, width = self.model.input_shape[2:]
|
| 36 |
-
self.apply_metadata(
|
| 37 |
-
{
|
| 38 |
-
"task": "detect",
|
| 39 |
-
"stride": 32,
|
| 40 |
-
"batch": 1,
|
| 41 |
-
"imgsz": [height, width],
|
| 42 |
-
"names": dict(enumerate(self.model.class_names)),
|
| 43 |
-
}
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
def forward(self, im: torch.Tensor) -> list[torch.Tensor]:
|
| 47 |
-
return [self.model.execute(im)]
|
| 48 |
-
|
| 49 |
-
def close(self) -> None:
|
| 50 |
-
if self.model is not None:
|
| 51 |
-
self.model.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ultralytics/arm_yolo26n_pi5_backend.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ultralytics backend for the Arm-optimized YOLO26n Raspberry Pi 5 runtime."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Mapping
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from arm_yolo26n import Yolo26nTensorSession
|
| 10 |
+
from ultralytics.nn.backends.base import BaseBackend
|
| 11 |
+
from ultralytics.utils import LOGGER, YAML
|
| 12 |
+
|
| 13 |
+
_YOLO26N_SUFFIX = "arm_yolo26n_pi5_model"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ArmYolo26nPi5Backend(BaseBackend):
|
| 17 |
+
"""Run a converted YOLO26n model through the native tensor session."""
|
| 18 |
+
|
| 19 |
+
def load_model(self, weight: str | Path) -> None:
|
| 20 |
+
model_directory = Path(weight).expanduser().resolve()
|
| 21 |
+
if not model_directory.is_dir():
|
| 22 |
+
raise ValueError(f"Arm YOLO26n Pi 5 model must be a directory: {model_directory}")
|
| 23 |
+
if not model_directory.name.endswith(_YOLO26N_SUFFIX):
|
| 24 |
+
raise ValueError(
|
| 25 |
+
f"Arm YOLO26n Pi 5 model directory must end with {_YOLO26N_SUFFIX}: "
|
| 26 |
+
f"{model_directory}"
|
| 27 |
+
)
|
| 28 |
+
self._load_yolo26n(model_directory)
|
| 29 |
+
|
| 30 |
+
@staticmethod
|
| 31 |
+
def _descriptor(model_directory: Path, filename: str) -> tuple[Path, Mapping[str, object]]:
|
| 32 |
+
descriptor_path = model_directory / filename
|
| 33 |
+
if not descriptor_path.is_file():
|
| 34 |
+
raise FileNotFoundError(f"missing model descriptor: {descriptor_path}")
|
| 35 |
+
descriptor = YAML.load(descriptor_path)
|
| 36 |
+
if not isinstance(descriptor, Mapping):
|
| 37 |
+
raise ValueError(f"model descriptor must be a mapping: {descriptor_path}")
|
| 38 |
+
return descriptor_path, descriptor
|
| 39 |
+
|
| 40 |
+
def _load_yolo26n(self, model_directory: Path) -> None:
|
| 41 |
+
descriptor_path, descriptor = self._descriptor(model_directory, "arm-yolo26n-pi5.yaml")
|
| 42 |
+
if descriptor.get("backend") != "arm_yolo26n_pi5" or set(descriptor) != {
|
| 43 |
+
"backend",
|
| 44 |
+
"model",
|
| 45 |
+
"runtime",
|
| 46 |
+
}:
|
| 47 |
+
raise ValueError(f"invalid YOLO26n runtime descriptor: {descriptor_path}")
|
| 48 |
+
model_path = _descriptor_path(model_directory, descriptor.get("model"), "model")
|
| 49 |
+
runtime_path = _descriptor_path(model_directory, descriptor.get("runtime"), "runtime")
|
| 50 |
+
LOGGER.info(f"Loading {model_directory} with ArmYolo26nPi5Backend...")
|
| 51 |
+
self.model = Yolo26nTensorSession(runtime=runtime_path, model=model_path)
|
| 52 |
+
height, width = self.model.input_shape[2:]
|
| 53 |
+
self.apply_metadata(
|
| 54 |
+
{
|
| 55 |
+
"task": "detect",
|
| 56 |
+
"stride": 32,
|
| 57 |
+
"batch": 1,
|
| 58 |
+
"imgsz": [height, width],
|
| 59 |
+
"names": dict(enumerate(self.model.class_names)),
|
| 60 |
+
}
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
def forward(self, im: torch.Tensor) -> list[torch.Tensor]:
|
| 64 |
+
if im.device.type != "cpu":
|
| 65 |
+
raise ValueError("ArmYolo26nPi5Backend requires a CPU input tensor")
|
| 66 |
+
if im.dtype is not torch.float32:
|
| 67 |
+
raise TypeError("ArmYolo26nPi5Backend requires an FP32 input tensor")
|
| 68 |
+
model_input = im.detach().numpy()
|
| 69 |
+
prediction = self.model.execute(model_input)
|
| 70 |
+
return [torch.from_numpy(prediction)]
|
| 71 |
+
|
| 72 |
+
def close(self) -> None:
|
| 73 |
+
if self.model is not None:
|
| 74 |
+
self.model.close()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _descriptor_path(directory: Path, value: object, name: str) -> Path:
|
| 78 |
+
if not isinstance(value, str) or not value:
|
| 79 |
+
raise ValueError(f"YOLO26n descriptor {name} must be a non-empty path")
|
| 80 |
+
path = Path(value).expanduser()
|
| 81 |
+
return path.resolve() if path.is_absolute() else (directory / path).resolve()
|
ultralytics/benchmark.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""Run Ultralytics benchmark() against the
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
|
@@ -18,15 +18,15 @@ MODEL_SUFFIX = "arm_yolo26n_pi5_model"
|
|
| 18 |
def parse_args() -> argparse.Namespace:
|
| 19 |
parser = argparse.ArgumentParser(
|
| 20 |
description=(
|
| 21 |
-
"Inject the
|
| 22 |
-
"then run its
|
| 23 |
)
|
| 24 |
)
|
| 25 |
-
parser.add_argument("--checkpoint", type=Path, required=True
|
| 26 |
-
parser.add_argument("--model", type=Path, required=True
|
| 27 |
-
parser.add_argument("--data", default="coco128.yaml"
|
| 28 |
-
parser.add_argument("--imgsz", type=int, default=640
|
| 29 |
-
parser.add_argument("--device", default="cpu"
|
| 30 |
parser.add_argument("--output-dir", type=Path, default=Path("results"))
|
| 31 |
return parser.parse_args()
|
| 32 |
|
|
@@ -45,13 +45,17 @@ def validate_inputs(checkpoint: Path, model: Path) -> None:
|
|
| 45 |
if not model.is_dir() or model.name != MODEL_SUFFIX:
|
| 46 |
raise ValueError(f"model must be a directory named {MODEL_SUFFIX}: {model}")
|
| 47 |
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 50 |
expected = manifest["model"]["checkpoint_sha256"]
|
| 51 |
actual = sha256(checkpoint)
|
| 52 |
if actual != expected:
|
| 53 |
raise ValueError(
|
| 54 |
-
"checkpoint does not match the
|
| 55 |
f"expected {expected}, got {actual}"
|
| 56 |
)
|
| 57 |
|
|
@@ -71,7 +75,8 @@ def main() -> None:
|
|
| 71 |
registered = dict(zip(formats["Argument"], formats["Suffix"]))
|
| 72 |
if registered.get(NATIVE_FORMAT) != MODEL_SUFFIX:
|
| 73 |
raise RuntimeError(
|
| 74 |
-
f"installed Ultralytics does not register {NATIVE_FORMAT!r}
|
|
|
|
| 75 |
)
|
| 76 |
|
| 77 |
def use_native_model(_self: YOLO, **kwargs: object) -> str:
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""Run Ultralytics benchmark() against the Arm YOLO26n model directory."""
|
| 3 |
|
| 4 |
from __future__ import annotations
|
| 5 |
|
|
|
|
| 18 |
def parse_args() -> argparse.Namespace:
|
| 19 |
parser = argparse.ArgumentParser(
|
| 20 |
description=(
|
| 21 |
+
"Inject the converted Arm model at Ultralytics benchmark()'s export "
|
| 22 |
+
"step, then run its normal prediction, validation, timing, and reporting path."
|
| 23 |
)
|
| 24 |
)
|
| 25 |
+
parser.add_argument("--checkpoint", type=Path, required=True)
|
| 26 |
+
parser.add_argument("--model", type=Path, required=True)
|
| 27 |
+
parser.add_argument("--data", default="coco128.yaml")
|
| 28 |
+
parser.add_argument("--imgsz", type=int, default=640)
|
| 29 |
+
parser.add_argument("--device", default="cpu")
|
| 30 |
parser.add_argument("--output-dir", type=Path, default=Path("results"))
|
| 31 |
return parser.parse_args()
|
| 32 |
|
|
|
|
| 45 |
if not model.is_dir() or model.name != MODEL_SUFFIX:
|
| 46 |
raise ValueError(f"model must be a directory named {MODEL_SUFFIX}: {model}")
|
| 47 |
|
| 48 |
+
descriptor = model / "arm-yolo26n-pi5.yaml"
|
| 49 |
+
runtime = model / "lib/libarm-yolo26n.so.0.1.0"
|
| 50 |
+
manifest_path = model / "model/manifest.json"
|
| 51 |
+
if not descriptor.is_file() or not runtime.is_file() or not manifest_path.is_file():
|
| 52 |
+
raise FileNotFoundError(f"incomplete Arm YOLO26n model directory: {model}")
|
| 53 |
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
| 54 |
expected = manifest["model"]["checkpoint_sha256"]
|
| 55 |
actual = sha256(checkpoint)
|
| 56 |
if actual != expected:
|
| 57 |
raise ValueError(
|
| 58 |
+
"checkpoint does not match the converted model: "
|
| 59 |
f"expected {expected}, got {actual}"
|
| 60 |
)
|
| 61 |
|
|
|
|
| 75 |
registered = dict(zip(formats["Argument"], formats["Suffix"]))
|
| 76 |
if registered.get(NATIVE_FORMAT) != MODEL_SUFFIX:
|
| 77 |
raise RuntimeError(
|
| 78 |
+
f"installed Ultralytics does not register {NATIVE_FORMAT!r} "
|
| 79 |
+
f"with suffix {MODEL_SUFFIX!r}"
|
| 80 |
)
|
| 81 |
|
| 82 |
def use_native_model(_self: YOLO, **kwargs: object) -> str:
|
ultralytics/ultralytics-8.4.108-arm-yolo26-pi5.patch
CHANGED
|
@@ -1,13 +1,13 @@
|
|
| 1 |
--- a/ultralytics/nn/backends/__init__.py
|
| 2 |
+++ b/ultralytics/nn/backends/__init__.py
|
| 3 |
@@ -9,3 +9,4 @@
|
| 4 |
-
+from .
|
| 5 |
from .ascend import AscendBackend
|
| 6 |
from .axelera import AxeleraBackend
|
| 7 |
from .base import BaseBackend
|
| 8 |
@@ -28,4 +29,5 @@
|
| 9 |
__all__ = [
|
| 10 |
-
+ "
|
| 11 |
"AscendBackend",
|
| 12 |
"AxeleraBackend",
|
| 13 |
"BaseBackend",
|
|
@@ -15,13 +15,13 @@
|
|
| 15 |
+++ b/ultralytics/nn/autobackend.py
|
| 16 |
@@ -5,4 +5,5 @@
|
| 17 |
from .backends import (
|
| 18 |
-
+
|
| 19 |
AscendBackend,
|
| 20 |
AxeleraBackend,
|
| 21 |
CoreMLBackend,
|
| 22 |
@@ -145,4 +146,5 @@
|
| 23 |
_BACKEND_MAP = {
|
| 24 |
-
+ "arm_yolo26n_pi5":
|
| 25 |
"pt": PyTorchBackend,
|
| 26 |
"torchscript": TorchScriptBackend,
|
| 27 |
"onnx": ONNXBackend,
|
|
|
|
| 1 |
--- a/ultralytics/nn/backends/__init__.py
|
| 2 |
+++ b/ultralytics/nn/backends/__init__.py
|
| 3 |
@@ -9,3 +9,4 @@
|
| 4 |
+
+from .arm_yolo26n_pi5 import ArmYolo26nPi5Backend
|
| 5 |
from .ascend import AscendBackend
|
| 6 |
from .axelera import AxeleraBackend
|
| 7 |
from .base import BaseBackend
|
| 8 |
@@ -28,4 +29,5 @@
|
| 9 |
__all__ = [
|
| 10 |
+
+ "ArmYolo26nPi5Backend",
|
| 11 |
"AscendBackend",
|
| 12 |
"AxeleraBackend",
|
| 13 |
"BaseBackend",
|
|
|
|
| 15 |
+++ b/ultralytics/nn/autobackend.py
|
| 16 |
@@ -5,4 +5,5 @@
|
| 17 |
from .backends import (
|
| 18 |
+
+ ArmYolo26nPi5Backend,
|
| 19 |
AscendBackend,
|
| 20 |
AxeleraBackend,
|
| 21 |
CoreMLBackend,
|
| 22 |
@@ -145,4 +146,5 @@
|
| 23 |
_BACKEND_MAP = {
|
| 24 |
+
+ "arm_yolo26n_pi5": ArmYolo26nPi5Backend,
|
| 25 |
"pt": PyTorchBackend,
|
| 26 |
"torchscript": TorchScriptBackend,
|
| 27 |
"onnx": ONNXBackend,
|