hilaus-arm commited on
Commit
d31e060
·
verified ·
1 Parent(s): d126fbf

Update Raspberry Pi 5 reproduction package

Browse files

Replace 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.

Files changed (46) hide show
  1. .gitattributes +1 -0
  2. LICENSE +201 -0
  3. README.md +59 -32
  4. compiler/{minimal_runtime/compiler → converter}/__init__.py +0 -0
  5. compiler/{minimal_runtime/compiler → converter}/checkpoint_decoder.py +1 -1
  6. compiler/{minimal_runtime/compiler → converter}/checkpoint_intake.py +1 -1
  7. compiler/{minimal_runtime/compiler/compiler_io.py → converter/converter_io.py} +2 -2
  8. compiler/{minimal_runtime/compiler → converter}/export_yolo26n.py +1 -2
  9. compiler/{minimal_runtime/compiler → converter}/model_contract.py +3 -7
  10. compiler/{minimal_runtime/compiler → converter}/model_format.py +21 -66
  11. compiler/{minimal_runtime/compiler → converter}/model_validation.py +1 -1
  12. compiler/{minimal_runtime/compiler → converter}/validate.py +1 -1
  13. compiler/{minimal_runtime/compiler → converter}/weight_contract.py +0 -0
  14. compiler/{minimal_runtime/compiler → converter}/weight_lowering.py +1 -1
  15. compiler/minimal_runtime/__init__.py +0 -1
  16. metadata.yaml +5 -5
  17. model/README.md +6 -0
  18. model/yolo26n.pt +0 -3
  19. python-api/__init__.py +0 -10
  20. python-api/arm_yolo26n/__init__.py +23 -0
  21. python-api/arm_yolo26n/__pycache__/__init__.cpython-313.pyc +0 -0
  22. python-api/arm_yolo26n/__pycache__/_model.cpython-313.pyc +0 -0
  23. python-api/arm_yolo26n/__pycache__/detector.cpython-313.pyc +0 -0
  24. python-api/arm_yolo26n/__pycache__/errors.cpython-313.pyc +0 -0
  25. python-api/arm_yolo26n/__pycache__/yolo26n.cpython-313.pyc +0 -0
  26. python-api/arm_yolo26n/_model.py +174 -0
  27. python-api/arm_yolo26n/detector.py +469 -0
  28. python-api/arm_yolo26n/errors.py +7 -0
  29. python-api/{yolo26n.py → arm_yolo26n/yolo26n.py} +129 -222
  30. python-api/errors.py +0 -7
  31. repro.py +92 -90
  32. results/REFERENCE.md +23 -11
  33. runtime/arm_yolo26n_pi5_model/arm-yolo26n-pi5.yaml +3 -0
  34. runtime/arm_yolo26n_pi5_model/bundle/licenses/libmd/COPYRIGHT.debian +0 -177
  35. runtime/arm_yolo26n_pi5_model/bundle/manifest.json +0 -104
  36. runtime/arm_yolo26n_pi5_model/{bundle/native-runtime.so → lib/libarm-yolo26n.so.0.1.0} +2 -2
  37. runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/cpuinfo/LICENSE +0 -0
  38. runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/fxdiv/LICENSE +0 -0
  39. runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/pthreadpool/LICENSE +0 -0
  40. runtime/arm_yolo26n_pi5_model/{bundle/licenses → licenses}/xnnpack/LICENSE +0 -0
  41. runtime/arm_yolo26n_pi5_model/model/manifest.json +1 -0
  42. runtime/arm_yolo26n_pi5_model/{bundle → model}/model.bin +2 -2
  43. ultralytics/arm_yolo26_pi5_backend.py +0 -51
  44. ultralytics/arm_yolo26n_pi5_backend.py +81 -0
  45. ultralytics/benchmark.py +16 -11
  46. 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: apache-2.0
3
  ---
4
 
5
  # YOLO26n Raspberry Pi 5 benchmark reproduction
6
 
7
- This package contains a compiled YOLO26n runtime, its Python API, the model
8
- exporter, and the integration needed to run it through the Ultralytics benchmark.
9
- It targets a Raspberry Pi 5 running 64-bit Raspberry Pi OS Trixie.
 
10
 
11
- ## Benchmark the supplied model
 
 
12
 
13
- Internet access is required during setup and while the benchmark downloads the
14
- licensed validation dataset from Hugging Face. The dataset repository is
15
- currently private, so authenticate first with `hf auth login` or provide an
16
- `HF_TOKEN` that can read it.
 
17
 
18
  ```sh
19
  python3 repro.py setup
20
  python3 repro.py benchmark
21
  ```
22
 
23
- `repro.py setup` installs upstream Ultralytics 8.4.108 and then applies the included
24
- patch that registers this runtime as an Ultralytics export format and inference
25
- backend. Therefore, this is the real Ultralytics `benchmark()` flow, but it is
26
- not an unmodified Ultralytics installation. The patch is required because
27
- upstream Ultralytics does not know how to load this runtime.
28
 
29
- Before starting, `repro.py benchmark` checks for filesystem and memory conditions
30
  that could distort performance, including RAM-backed model or dataset paths,
31
- low available memory, dirty/writeback pressure, page-cache refaults, reclaim
32
- scanning, and swap activity.
33
 
34
- The benchmark downloads `Arm/coco-128-by-sa` at the pinned revision
35
- `cb1edb8da4ff3cb54d40ecb12d3efecc116ff3de` into `data/coco-128-by-sa/` and
36
- uses `data-validation-all.yaml` to validate all 128 images. It does not include
37
- or redistribute those images in this package. Dataset attribution and the
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 to `results/`. The supplied reference measurement is in
42
- `results/REFERENCE.md`.
43
 
44
- ## Export your own model
45
 
46
- Replace `model/yolo26n.pt` with your own compatible YOLO26n checkpoint, keeping
47
- that filename, then run:
48
 
49
  ```sh
50
  python3 repro.py export
51
  python3 repro.py benchmark
52
  ```
53
 
54
- `repro.py export` creates a runnable model package under
55
- `build/arm_yolo26n_pi5_model/` using the supplied model-independent compiled
56
- runtime. When that directory exists, `repro.py benchmark` automatically benchmarks
57
- the newly exported model instead of the supplied prebuilt model.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 .compiler_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
 
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 .compiler_io import RejectedInput
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 compiler artifacts."""
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 compiler input failed validation."""
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 .compiler_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,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 compiler lowering."""
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 .compiler_io import JsonObject
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 both the slot count and schema identity.
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. A 128-byte header and 64-byte
31
- # directory records preserve that alignment while leaving zeroed reserved space
32
- # for corruption detection. The packed structs describe only the used prefix;
33
- # bytearray initialization supplies the required zero padding.
34
  _ALIGNMENT = 64
35
- _HEADER_BYTES = 2 * _ALIGNMENT
36
- _ENTRY_BYTES = _ALIGNMENT
37
- _HEADER = struct.Struct("<8sIIIIIIQQQ32s")
38
- _ENTRY = struct.Struct("<IIQQ32sQ")
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 _slot_schema_sha256(names: tuple[str, ...]) -> str:
101
- """Identify the ordered semantic names that define runtime slot numbers."""
102
- if not names or any(not name or "\0" in name for name in names):
103
- raise ValueError("invalid model weight names")
104
- payload = b"\0".join(name.encode("utf-8") for name in names)
105
- return hashlib.sha256(payload).hexdigest()
 
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 128-byte header")
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 class_count == 0
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 file_bytes != len(payload)
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 class_count <= 0
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 .compiler_io import RejectedInput
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 .compiler_io import JsonObject, RejectedInput, atomic_write, canonical_bytes, sha256_bytes
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 .compiler_io import RejectedInput
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-12T01:01:35Z'
5
  context:
6
  model:
7
  id: yolo26n
8
  base_model_id: yolo26n
9
  profile: Arm-Optimized
10
  weight_dtype: fp32
11
- model_size_mb: 11.287
12
  parameter_count: 2572280
13
- format: armrt-bundle
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: armrt
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.16
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
- """Python tensor boundary for an exported YOLO26n runtime bundle."""
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, cast
15
 
16
  import numpy as np
17
- import torch
18
 
19
- from .errors import Yolo26RuntimeError
 
20
 
21
- __all__ = ["Yolo26nRuntime"]
22
 
 
 
 
 
 
 
23
 
24
- _BUNDLE_KIND = "yolo26n-native-bundle"
25
- _BUNDLE_VERSION = 1
26
- _THREAD_COUNT = 4
27
- _GRAPH_COUNT = 19
28
- _OK = 0
29
- _BOX_COORDINATE_CHANNELS = 4
30
- _SUPPORTED_RESOLUTIONS = (320, 480, 640, 800, 960, 1280)
31
- _PIXEL_FORMATS = {"BGR": (0, 3), "RGB": (1, 3), "BGRX": (2, 4)}
32
 
33
 
34
- def _detection_positions(resolution: int) -> int:
35
- return sum((resolution // stride) ** 2 for stride in (8, 16, 32))
 
 
 
 
 
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 _Yolo26nLibrary:
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.c_char_p]
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: Path) -> ctypes.c_void_p:
236
- return ctypes.c_void_p(self._raw.y26_native_create(os.fsencode(model)))
 
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: torch.Tensor,
252
- output: torch.Tensor,
253
  ) -> int:
254
  return int(
255
  self._raw.y26_native_execute(
256
  handle,
257
- model_input.data_ptr(),
258
- model_input.numel() * model_input.element_size(),
259
- output.data_ptr(),
260
- output.numel() * output.element_size(),
261
  )
262
  )
263
 
264
  def preprocess_image_no_resize(
265
  self,
266
- frame: np.ndarray,
267
  pixel_format: int,
268
- output: torch.Tensor,
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.data_ptr(),
278
- output.numel() * output.element_size(),
279
  )
280
  )
281
 
@@ -283,34 +157,56 @@ class _Yolo26nLibrary:
283
  self._raw.y26_native_destroy(handle)
284
 
285
 
286
- class Yolo26nRuntime:
287
- """Own one validated, fixed-contract YOLO26n native runtime."""
288
-
289
- def __init__(self, bundle: str | Path) -> None:
290
- bundle_data = _BundleData.load(bundle)
291
- self.input_shape = bundle_data.input_shape
292
- self.output_shape = bundle_data.output_shape
293
- self.class_names = bundle_data.class_names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  self._lifecycle_lock = threading.Lock()
295
  self._handle = ctypes.c_void_p()
296
  try:
297
- self._native = _Yolo26nLibrary(bundle_data.library_path)
298
- self._handle = self._native.create(bundle_data.model_path)
299
  if not self._handle.value:
300
- raise Yolo26RuntimeError("YOLO26n runtime rejected its exported model")
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 != _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 Yolo26RuntimeError("YOLO26n runtime and bundle contracts disagree")
312
  if native_contract.calls != 0:
313
- raise Yolo26RuntimeError("YOLO26n runtime call counter was not initialized")
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: torch.Tensor) -> torch.Tensor:
 
 
 
 
 
 
323
  if self.closed:
324
- raise Yolo26RuntimeError("YOLO26n runtime is closed")
325
- if not isinstance(tensor, torch.Tensor):
326
- raise TypeError("input must be a torch.Tensor")
327
- if tensor.device.type != "cpu":
328
- raise ValueError("input tensor must be on the CPU")
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
- if not tensor.is_contiguous():
334
- tensor = tensor.contiguous()
335
  if not self._lifecycle_lock.acquire(blocking=False):
336
- raise Yolo26RuntimeError("concurrent execution or close is not supported")
337
  try:
338
  if self.closed:
339
- raise Yolo26RuntimeError("YOLO26n runtime is closed")
340
- output = torch.empty(self.output_shape, dtype=torch.float32, device="cpu")
341
  status = self._native.execute(self._handle, tensor, output)
342
- if status != _OK:
343
- raise Yolo26RuntimeError(f"YOLO26n runtime execution failed with status {status}")
344
  return output
345
  finally:
346
  self._lifecycle_lock.release()
347
 
348
- def preprocess_image_no_resize(
349
  self,
350
- frame: np.ndarray,
351
  *,
352
  pixel_format: str,
353
- output: torch.Tensor,
354
  ) -> None:
 
 
 
 
 
 
 
 
355
  if self.closed:
356
- raise Yolo26RuntimeError("YOLO26n runtime is closed")
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
- native_pixel_format, channels = _PIXEL_FORMATS[pixel_format]
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(f"{pixel_format} frame must have shape [height, width, {channels}]")
365
- if frame.strides[1:] != (channels, 1) or frame.strides[0] <= 0:
 
 
366
  raise ValueError("frame must have interleaved, forward uint8 pixels")
367
  if (
368
- not isinstance(output, torch.Tensor)
369
- or output.device.type != "cpu"
370
- or output.dtype is not torch.float32
371
  or tuple(output.shape) != self.input_shape
372
- or not output.is_contiguous()
373
  ):
374
  raise ValueError(
375
- f"output must be a contiguous CPU FP32 tensor with shape {self.input_shape}"
376
  )
377
  if not self._lifecycle_lock.acquire(blocking=False):
378
- raise Yolo26RuntimeError("concurrent execution or close is not supported")
379
  try:
380
  if self.closed:
381
- raise Yolo26RuntimeError("YOLO26n runtime is closed")
382
  status = self._native.preprocess_image_no_resize(
383
  frame,
384
- native_pixel_format,
385
  output,
386
  )
387
- if status != _OK:
388
- raise Yolo26RuntimeError(
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 Yolo26RuntimeError("YOLO26n runtime is closed")
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 = "cb1edb8da4ff3cb54d40ecb12d3efecc116ff3de"
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.3.5",
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 / "yolo26_runtime"
67
- api_destination.mkdir(parents=True, exist_ok=True)
68
- for source in (ROOT / "python-api").glob("*.py"):
69
- shutil.copy2(source, api_destination / source.name)
70
  shutil.copy2(
71
- ROOT / "ultralytics/arm_yolo26_pi5_backend.py",
72
- site / "ultralytics/nn/backends/arm_yolo26_pi5.py",
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 ultralytics.nn.backends import ArmYolo26Pi5Backend; "
104
- "from yolo26_runtime import Yolo26nRuntime; "
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 assemble_bundle(exported_dir: Path, reference_bundle: Path, output: Path) -> None:
121
- exported_manifest = json.loads((exported_dir / "manifest.json").read_text())
122
- reference_manifest = json.loads((reference_bundle / "manifest.json").read_text())
123
- if (exported_manifest.get("kind"), exported_manifest.get("version")) != (
124
- "yolo26n-exported-model",
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
- bundle = temporary / "bundle"
156
- shutil.copytree(reference_bundle, bundle)
157
- shutil.copy2(exported_model, bundle / "model.bin")
158
- (bundle / "manifest.json").write_text(
159
- json.dumps(bundle_manifest, indent=2) + "\n", encoding="utf-8"
160
- )
161
- os.rename(temporary, output)
162
- except BaseException:
163
- shutil.rmtree(temporary, ignore_errors=True)
164
- raise
165
 
166
 
167
  def export_model() -> None:
168
- exported = ROOT / "build/exported-model"
169
- output = ROOT / "build/arm_yolo26n_pi5_model"
170
- if exported.exists() or output.exists():
171
- raise SystemExit(f"export output already exists under {ROOT / 'build'}")
172
-
173
- environment = os.environ.copy()
174
- environment["PYTHONPATH"] = str(ROOT / "compiler")
175
- run(
176
- [
177
- sys.executable,
178
- "-m",
179
- "minimal_runtime.compiler.export_yolo26n",
180
- "--checkpoint",
181
- str(ROOT / "model/yolo26n.pt"),
182
- "--resolution",
183
- "640",
184
- "--output-dir",
185
- str(exported),
186
- ],
187
- env=environment,
188
- )
189
- assemble_bundle(
190
- exported,
191
- ROOT / "runtime/arm_yolo26n_pi5_model/bundle",
192
- output,
193
- )
194
- print(f"exported runnable model to {output}")
 
 
 
 
 
 
 
 
 
 
 
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
- dataset_yaml = DATASET_DIR / "data-validation-all.yaml"
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 dataset_yaml.is_file():
283
- raise SystemExit(f"dataset download did not produce {dataset_yaml}")
 
 
 
 
 
 
 
 
 
 
 
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 = ROOT / "build/arm_yolo26n_pi5_model"
296
- if not model.is_dir():
297
- model = ROOT / "runtime/arm_yolo26n_pi5_model"
 
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(ROOT / "model/yolo26n.pt"),
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 result
2
 
3
- Measured from a fresh extraction of this package on Raspberry Pi 5:
 
4
 
5
- - Dataset: `Arm/coco-128-by-sa` (128 images)
6
- - Dataset revision: `cb1edb8da4ff3cb54d40ecb12d3efecc116ff3de`
7
- - Validation definition: `data-validation-all.yaml`
8
- - Image size: 640
9
- - Device and batch: CPU, batch 1
10
- - CPU governor: `ondemand`
11
- - mAP50-95: 0.4678
12
- - Inference: 38.54 ms/image
13
 
14
- Running the included exporter produced a byte-identical `model.bin`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:8e48d898de082780b6ebadaffda0e04cb942a80be5911a2621651b30f7bd453b
3
- size 1398048
 
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:8d5a617332cdd1bab070f6d60a02e4cb1cd00a5da10381469d2cd613988f895b
3
- size 9650112
 
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 native YOLO26n model package."""
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 native model package at Ultralytics benchmark()'s export step, "
22
- "then run its unmodified prediction, validation, timing, and reporting path."
23
  )
24
  )
25
- parser.add_argument("--checkpoint", type=Path, required=True, help="Source YOLO26n .pt checkpoint")
26
- parser.add_argument("--model", type=Path, required=True, help="Native model package directory")
27
- parser.add_argument("--data", default="coco128.yaml", help="Ultralytics validation dataset")
28
- parser.add_argument("--imgsz", type=int, default=640, help="Square validation image size")
29
- parser.add_argument("--device", default="cpu", help="Ultralytics validation device")
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
- manifest_path = model / "bundle" / "manifest.json"
 
 
 
 
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 weights used to build the native package: "
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} with suffix {MODEL_SUFFIX!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 .arm_yolo26_pi5 import ArmYolo26Pi5Backend
5
  from .ascend import AscendBackend
6
  from .axelera import AxeleraBackend
7
  from .base import BaseBackend
8
  @@ -28,4 +29,5 @@
9
  __all__ = [
10
- + "ArmYolo26Pi5Backend",
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
- + ArmYolo26Pi5Backend,
19
  AscendBackend,
20
  AxeleraBackend,
21
  CoreMLBackend,
22
  @@ -145,4 +146,5 @@
23
  _BACKEND_MAP = {
24
- + "arm_yolo26n_pi5": ArmYolo26Pi5Backend,
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,