phenolapseuav 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ {
2
+ "title": "phenolapseUAV: interactive registration of multispectral UAV time series",
3
+ "description": "An open-source napari plugin for interactive and automatic registration of georeferenced multispectral and infrared UAV orthomosaics.",
4
+ "license": "GPL-3.0-only",
5
+ "upload_type": "software",
6
+ "access_right": "open",
7
+ "creators": [
8
+ {"name": "Fernandez, Romain"},
9
+ {"name": "Diene, Serigne Mansour"},
10
+ {"name": "Audebert, Alain"}
11
+ ],
12
+ "keywords": [
13
+ "napari",
14
+ "image registration",
15
+ "UAV",
16
+ "multispectral imaging",
17
+ "infrared imaging",
18
+ "phenotyping"
19
+ ],
20
+ "related_identifiers": [
21
+ {
22
+ "identifier": "https://github.com/Rocsg/phenolapseUAV",
23
+ "relation": "isSupplementTo",
24
+ "scheme": "url"
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to phenolapseUAV are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and releases follow
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-08-28
10
+
11
+ ### Added
12
+
13
+ - npe2 napari plugin for interactive UAV orthomosaic registration.
14
+ - Georeference-first residual similarity, affine, and TPS model.
15
+ - Directed registration graph for MS/IR image series.
16
+ - Per-band full-resolution GeoTIFF export.
17
+ - 512 × 512 multi-image validation stack export.
18
+ - Automatic pairwise and series registration command-line tools.
19
+
20
+ [Unreleased]: https://github.com/Rocsg/phenolapseUAV/compare/v0.1.0...HEAD
21
+ [0.1.0]: https://github.com/Rocsg/phenolapseUAV/releases/tag/v0.1.0
@@ -0,0 +1,23 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use phenolapseUAV in your research, please cite this software."
3
+ title: "phenolapseUAV: interactive registration of multispectral UAV time series"
4
+ type: software
5
+ version: 0.1.0
6
+ date-released: 2026-08-28
7
+ license: GPL-3.0-only
8
+ repository-code: "https://github.com/Rocsg/phenolapseUAV"
9
+ url: "https://github.com/Rocsg/phenolapseUAV"
10
+ authors:
11
+ - family-names: Fernandez
12
+ given-names: Romain
13
+ - family-names: Diene
14
+ given-names: Serigne Mansour
15
+ - family-names: Audebert
16
+ given-names: Alain
17
+ keywords:
18
+ - image registration
19
+ - UAV
20
+ - multispectral imaging
21
+ - infrared imaging
22
+ - phenotyping
23
+ - napari
@@ -0,0 +1,14 @@
1
+ # Code of Conduct
2
+
3
+ phenolapseUAV follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
4
+
5
+ Participants must provide a welcoming, harassment-free environment, use
6
+ inclusive language, respect differing viewpoints, accept constructive feedback,
7
+ and focus on what is best for the community. Unacceptable behavior includes
8
+ harassment, discriminatory language, personal attacks, publishing private
9
+ information, and other conduct inappropriate in a professional setting.
10
+
11
+ Report conduct issues privately to the repository owner through the contact
12
+ method listed on the GitHub profile. Reports will be reviewed promptly and
13
+ confidentially. Maintainers may remove content or temporarily or permanently ban
14
+ contributors whose behavior violates these expectations.
@@ -0,0 +1,17 @@
1
+ # Contributing to phenolapseUAV
2
+
3
+ Contributions are welcome through GitHub issues and pull requests.
4
+
5
+ ## Development setup
6
+
7
+ 1. Fork and clone the repository.
8
+ 2. Create and activate a Python 3.10–3.12 virtual environment.
9
+ 3. Install development dependencies with `python -m pip install -e ".[dev]"`.
10
+ 4. Run `python -m pytest` and `python -m ruff check .` before submitting.
11
+
12
+ For automatic AROSICS/OpenCV workflows, install `.[automatic,dev]` instead.
13
+ Keep UI strings, comments, documentation, and public APIs in English. Add tests
14
+ for fixes and new behavior. Do not include private imagery or generated outputs.
15
+
16
+ By submitting a contribution, you agree that it is distributed under the
17
+ GNU General Public License version 3.
@@ -0,0 +1,15 @@
1
+ phenolapseUAV
2
+ Copyright (C) 2026 Romain Fernandez, Serigne Mansour Diene, and Alain Audebert
3
+
4
+ This program is free software: you can redistribute it and/or modify it under
5
+ the terms of the GNU General Public License as published by the Free Software
6
+ Foundation, version 3 of the License.
7
+
8
+ This program is distributed in the hope that it will be useful, but WITHOUT
9
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10
+ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
11
+
12
+ The complete, unmodified GNU General Public License version 3 is available at:
13
+ https://www.gnu.org/licenses/gpl-3.0.txt
14
+
15
+ SPDX-License-Identifier: GPL-3.0-only
@@ -0,0 +1,3 @@
1
+ include README.md LICENSE CITATION.cff CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md .zenodo.json
2
+ include src/phenolapseuav/napari.yaml
3
+ recursive-include paper *.md *.bib
@@ -0,0 +1,292 @@
1
+ Metadata-Version: 2.4
2
+ Name: phenolapseuav
3
+ Version: 0.1.0
4
+ Summary: Interactive landmark-based registration of multispectral and infrared UAV orthomosaics in napari
5
+ Author: Romain Fernandez, Serigne Mansour Diene, Alain Audebert
6
+ License-Expression: GPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/Rocsg/phenolapseUAV
8
+ Project-URL: Repository, https://github.com/Rocsg/phenolapseUAV
9
+ Project-URL: Issues, https://github.com/Rocsg/phenolapseUAV/issues
10
+ Project-URL: Documentation, https://github.com/Rocsg/phenolapseUAV#readme
11
+ Keywords: napari,image registration,UAV,orthomosaic,multispectral,infrared,thin-plate spline,phenotyping
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: napari
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
22
+ Requires-Python: >=3.10
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: affine>=2.4
26
+ Requires-Dist: napari>=0.5.6
27
+ Requires-Dist: numpy>=1.26
28
+ Requires-Dist: pyproj>=3.5
29
+ Requires-Dist: qtpy>=2.4
30
+ Requires-Dist: rasterio>=1.3
31
+ Requires-Dist: scikit-image>=0.26
32
+ Provides-Extra: automatic
33
+ Requires-Dist: arosics>=1.4; extra == "automatic"
34
+ Requires-Dist: matplotlib>=3.8; extra == "automatic"
35
+ Requires-Dist: opencv-python-headless>=4.9; extra == "automatic"
36
+ Provides-Extra: dev
37
+ Requires-Dist: build>=1.2; extra == "dev"
38
+ Requires-Dist: npe2>=0.7.7; extra == "dev"
39
+ Requires-Dist: pytest>=8; extra == "dev"
40
+ Requires-Dist: pytest-cov>=5; extra == "dev"
41
+ Requires-Dist: ruff>=0.6; extra == "dev"
42
+ Requires-Dist: twine>=5; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # phenolapseUAV
46
+
47
+ [![Tests](https://github.com/Rocsg/phenolapseUAV/actions/workflows/tests.yml/badge.svg)](https://github.com/Rocsg/phenolapseUAV/actions/workflows/tests.yml)
48
+ [![PyPI](https://img.shields.io/pypi/v/phenolapseUAV.svg)](https://pypi.org/project/phenolapseUAV/)
49
+ [![Python](https://img.shields.io/pypi/pyversions/phenolapseUAV.svg)](https://pypi.org/project/phenolapseUAV/)
50
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
51
+
52
+ **phenolapseUAV** is an open-source napari plugin for interactive and automatic
53
+ registration of large georeferenced multispectral and infrared UAV
54
+ orthomosaics. It is designed for temporal phenotyping series in which image
55
+ resolution, pixel size, contrast, and reconstruction quality vary between
56
+ acquisitions or sensors.
57
+
58
+ ## Installation for napari users
59
+
60
+ After the first PyPI release, no terminal is required:
61
+
62
+ 1. Install and launch the official napari application.
63
+ 2. Open **Plugins > Install/Uninstall Plugins**.
64
+ 3. Search for **phenolapseUAV**.
65
+ 4. Select **Install**.
66
+ 5. Open **Plugins > phenolapseUAV Registration**.
67
+
68
+ Alternatively, install into an existing napari Python environment:
69
+
70
+ ```bash
71
+ python -m pip install phenolapseUAV
72
+ ```
73
+
74
+ Automatic command-line registration is optional:
75
+
76
+ ```bash
77
+ python -m pip install "phenolapseUAV[automatic]"
78
+ ```
79
+
80
+ AROSICS and GDAL availability depends on the operating system. The interactive
81
+ napari plugin does not require AROSICS.
82
+
83
+ ## Quick start
84
+
85
+ Open **Plugins > phenolapseUAV Registration**, then either select one FIXED and
86
+ one MOVING GeoTIFF or open a directory following the
87
+ `<scene>_<MS|IR>.tif` naming convention. All controls and full-resolution
88
+ exports are available from the dock widget.
89
+
90
+ ## Pairwise automatic registration
91
+
92
+ ```bash
93
+ phenolapseUAV-register-pair --ref reference.tif --mov moving.tif --outdir output
94
+ ```
95
+
96
+ ### Options
97
+
98
+ | Option | Description |
99
+ |---|---|
100
+ | `--ref PATH` | Reference image |
101
+ | `--mov PATH` | Moving image |
102
+ | `--outdir PATH` | Output directory |
103
+ | `--band INT` | 1-based band index; defaults to a robust 2nd–98th percentile mean |
104
+ | `--method {arosics,fallback,auto}` | Registration method; `auto` tries AROSICS, then the fallback |
105
+ | `--debug` | Enable verbose logging |
106
+ | `--overwrite` | Allow existing outputs to be overwritten |
107
+
108
+ ### Outputs
109
+
110
+ - `<moving>_registered.tif`: registered GeoTIFF with all spectral bands preserved
111
+ - `registration_report.txt`: detailed text report
112
+ - `registration_vectors.geojson`: displacement vectors when available
113
+ - `quicklook_before.png` and `quicklook_after.png`
114
+ - `difference_before.png` and `difference_after.png`
115
+
116
+ ### Methods
117
+
118
+ 1. **AROSICS**: global `COREG`, followed by `COREG_LOCAL` when successful.
119
+ 2. **Fallback**: ORB keypoint extraction and matching, RANSAC estimation of a conservative translation or Euclidean model, then band-by-band reprojection with rasterio.
120
+
121
+ Registration is intentionally conservative to preserve the geometry of biological field plots; no unconstrained deformation is applied.
122
+
123
+ ## Manual landmark registration in napari
124
+
125
+ The phenolapseUAV napari widget registers two large GeoTIFF files from explicit landmark pairs. During editing, it loads only the displayed band and a downsampled preview (factor 4 by default).
126
+
127
+ ```bash
128
+ phenolapseUAV \
129
+ --fixed /path/to/reference_MS.tif \
130
+ --moving /path/to/moving_MS.tif \
131
+ --preview-factor 4
132
+ ```
133
+
134
+ Open a complete series and its registration graph directly with:
135
+
136
+ ```bash
137
+ phenolapseUAV \
138
+ --dir /path/to/Input/STR \
139
+ --preview-factor 6
140
+ ```
141
+
142
+ `--dir` and the `--fixed`/`--moving` pair are mutually exclusive.
143
+
144
+ ### Landmark input
145
+
146
+ Input works anywhere on the canvas, independently of the displayed view or active layer:
147
+
148
+ - **FIXED is red** and uses the left mouse button.
149
+ - **MOVING is green** and uses the right mouse button.
150
+ - The recommended order is FIXED first, then MOVING.
151
+ - Landmark clicks never switch views automatically; the buttons and `F`, `M`, and `O` shortcuts remain under user control.
152
+ - A middle click on a FIXED or MOVING landmark immediately deletes its entire pair and table row.
153
+ - A click without movement adds a landmark; a drag retains normal canvas navigation.
154
+ - Holding **Space** completely disables left- and right-click landmark input so those buttons can pan the napari view.
155
+ - Hovering over a landmark displays **Click point XX to delete the pair**. A left click on that point removes the complete pair, refreshes the table, and recomputes the preview.
156
+ - **Delete latest pair**, `Delete`, or `Shift+Delete` always removes the newest pair, regardless of the current view, active layer, or selection.
157
+ - In the opposite view or overlay, clicks are mapped back through the current transformation, or through GeoTIFF georeferencing before the first registration.
158
+ - Right-button dragging never zooms; use the mouse wheel for zooming.
159
+
160
+ ### Shortcuts
161
+
162
+ - `N`: explicitly reserve the next pair
163
+ - `F`: FIXED-only view
164
+ - `M`: MOVING-only view
165
+ - `O`: additive red FIXED / green registered MOVING overlay
166
+
167
+ The application starts in the red/green overlay view.
168
+
169
+ Every landmark has a stable visible ID. FIXED markers are slightly red-tinted white and MOVING markers are slightly green-tinted white. Both are semi-transparent and include a fully saturated red or green center. Their initial diameter is 3 and can be adjusted with **Landmark size** for precise placement.
170
+
171
+ Deleting a landmark removes its complete pair without silently reassigning later IDs. Moving, deleting, or disabling a complete pair starts an asynchronous preview update. An isolated FIXED click does not trigger registration: computation starts only after a complete pair is validated. A large **UPDATING VIEW…** message appears at the center of the canvas during computation. Landmark placement and movement are locked during this update, while view navigation remains available. Stale worker results are ignored. The point table automatically scrolls to the latest coordinates.
172
+
173
+ Stored MOVING coordinates always refer to the raw image. In the overlay, green MOVING pixels have already been warped onto the FIXED grid; right clicks are therefore inverse-mapped to the original MOVING coordinates before updating the model. Once a registration is available, the MOVING-only view also displays the registered image on the FIXED grid. Clicks and moved landmarks in that view are mapped back to raw MOVING coordinates.
174
+
175
+ Before the first pair, the MOVING preview is reprojected onto the geographic FIXED grid using GeoTIFF georeferencing and pixel sizes. Lower-resolution IR images therefore retain their correct geographic extent instead of appearing artificially smaller. The green-only view and red/green overlay use the exact same registered MOVING array and FIXED grid, both before and after landmark fitting; switching views cannot change image placement.
176
+
177
+ Because scikit-image does not provide an analytical TPS inverse, the application explicitly estimates two TPS models: FIXED→MOVING for resampling and MOVING→FIXED for interactive landmark display.
178
+
179
+ ### Registration-series graph
180
+
181
+ The **Manual registration series** panel below the landmark table manages multiple registrations in one session:
182
+
183
+ 1. **Open directory** discovers GeoTIFF files and automatically reloads existing state.
184
+ 2. Filenames are parsed explicitly as `<scene>_<MS|IR>.tif`, for example `2022_13h_STR_MS.tif`.
185
+ 3. Scenes are displayed as rows and MS/IR modalities as columns.
186
+ 4. The first selected node becomes FIXED and receives a red frame.
187
+ 5. The second becomes MOVING, receives a green frame, and immediately loads the pair into the main viewer.
188
+ 6. Switching edges stores the previous edge's landmarks in memory and restores the new edge if it was edited earlier.
189
+
190
+ Potential edges are dark. An edited edge changes color according to its number of complete pairs: 1, 4 (similarity), 7 (residual affine), and 10 (residual TPS). Every landmark edit and edge change automatically saves the complete state. **Save state** forces the same write without asking for a filename.
191
+
192
+ For an `Input/STR` directory, state is stored automatically in `Output/STR/manual_registration_state.json`, or in an `OUTPUT` directory created next to `INPUT`. The file contains image metadata, directed FIXED→MOVING edges, and full-resolution coordinates. These directed edges form the graph required to compose transformations along a chain without losing original coordinates.
193
+
194
+ Manual registration always starts with GeoTIFF georeferencing and pixel sizes. Landmark corrections are then fitted successively on the remaining residuals: a fallback translation for 1–3 pairs, similarity from 4 pairs, affine after similarity from 7 pairs, and TPS after similarity and affine from 10 pairs. The same composed mapping is used for preview, interaction, and full-resolution export. Full-resolution export requires 10 pairs and processes spectral bands separately with one shared spatial transformation and no interpolation between channels.
195
+
196
+ **Apply / Export TPS** first displays the complete image list and asks which image must define the series reference geometry, with the current FIXED image preselected. It then asks for an output directory. In series mode it follows complete directed registration edges from the selected reference and exports every image onto that grid. Export is refused if any series image is unreachable or if a required edge has fewer than 10 complete enabled pairs. Every source band is saved as an independent single-band, two-dimensional GeoTIFF in `BANDS/`; one-band source images therefore remain 2-D rasters rather than one-item stacks. In standalone mode, the FIXED and MOVING images are exported in the same per-band format.
197
+
198
+ After a successful export, **Create 512×512 sample** becomes available. Clicking it displays a preview of the selected series reference and asks for a crop center. The next left click creates `SAMPLE_RESULT.tif` in the selected output directory. This file contains one 512×512 band per exported image, using the first source band, and is immediately opened as a stack in napari for visual inspection across the complete series and all modalities. Crops touching an image boundary are padded with nodata.
199
+
200
+ Contrast limits are estimated from raw source-pixel samples. Non-finite values, zero, and exact saturation are excluded. The default 0.5th and 99.5th percentiles can be changed in the panel. Landmarks and session metadata can be saved and reloaded as JSON.
201
+
202
+ ### Future synchronized three-view layout
203
+
204
+ napari currently has no stable public API for inserting three independent canvases into one `Viewer`. Modifying private Qt/Vispy viewer objects would be fragile. A safe future implementation would use three public `Viewer` instances (FIXED, MOVING, and OVERLAY), share read-only preview arrays, and synchronize their cameras through public `camera.events` with a recursion guard. Qt can place the windows side by side without duplicating full-resolution rasters. The current prototype intentionally retains one canvas with instant view switching until this feature can be isolated and tested.
205
+
206
+ ## Series registration
207
+
208
+ The optional `phenolapseUAV-register-series` command registers an entire orthomosaic series, manages matching multispectral (MS) and infrared (IR) acquisitions, and exports every image in the geometry and native resolution of the reference MS image. IR images are therefore upsampled onto the reference MS grid.
209
+
210
+ Expected naming is `<prefix>_MS.tif` and `<prefix>_IR.tif`, for example `2022_13h_STR_MS.tif` and `2022_13h_STR_IR.tif`. Each shared prefix identifies one scene. MS images are matched to MS images, and IR images to IR images.
211
+
212
+ ```bash
213
+ phenolapseUAV-register-series --indir /path/to/input --outdir /path/to/output
214
+ ```
215
+
216
+ ### Options
217
+
218
+ | Option | Description |
219
+ |---|---|
220
+ | `--indir PATH` | Directory containing the GeoTIFF series |
221
+ | `--outdir PATH` | Output directory |
222
+ | `--reference NAME.tif` | Reference **MS** filename; when omitted, select it from an interactive downsampled montage |
223
+ | `--band INT` | 1-based MS matching band; when omitted, select a channel interactively or use the robust mean |
224
+ | `--subsample {1,2,3,4}` | Downsampling for a single pass; ignored with `--pyramid` |
225
+ | `--pyramid 4,2,1` | Coarse-to-fine registration with composed corrections and one final native-resolution resampling |
226
+ | `--warp {rigid,affine,poly2,tps}` | Warp model; defaults to a smooth, lightweight TPS with automatic fallback when landmarks are insufficient |
227
+ | `--resample {cubic,linear,nearest}` | Resampling interpolation |
228
+ | `--limit N` | Process only the first N moving scenes; 0 processes all scenes |
229
+ | `--debug` | Enable verbose logging |
230
+
231
+ Existing files with the same output names are overwritten automatically.
232
+
233
+ ### Optional `Reference_point` file
234
+
235
+ When `Reference_point` or `Reference_point.txt` exists in the input directory, each `image_name X Y` line specifies a full-resolution support point in column/row coordinates. The point is added as an anchor to the MS tie points before automatic optimization.
236
+
237
+ ### MS/IR registration topology
238
+
239
+ 1. Register every **MS** image to the reference **MS** image.
240
+ 2. Register every **IR** image to the reference scene's **IR** image.
241
+ 3. Place the reference **IR** image on the reference **MS** image using georeferencing only; no cross-modality IR↔MS content matching is performed.
242
+ 4. Compose each IR transformation as `IR → IR_ref → MS_ref`.
243
+
244
+ Homogeneous **MS↔MS** and **IR↔IR** matching uses SIFT and RANSAC. With `--pyramid 4,2,1`, level 4 estimates a similarity model, level 2 estimates a lightweight affine model, and level 1 estimates the requested final model (TPS by default). Refinements with overly concentrated tie points are rejected. Transformations are composed, and each original GeoTIFF is resampled only once at full resolution onto the reference MS grid.
245
+
246
+ ### Outputs
247
+
248
+ - All rasters are written to the `BANDS/` subdirectory.
249
+ - Each MS image produces one single-band file per channel: `<name>_B01_registered.tif`, `<name>_B02_registered.tif`, and so on.
250
+ - Each IR image produces `<name>_registered.tif`.
251
+ - Reference MS bands are extracted without resampling.
252
+ - A safety guard rejects transformations covering less than 1% of the reference grid.
253
+ - `series_report.txt` records the model, tie-point count, and processing time for each image.
254
+
255
+ ## Development
256
+
257
+ Clone the repository and install it in editable mode:
258
+
259
+ ```bash
260
+ git clone https://github.com/Rocsg/phenolapseUAV.git
261
+ cd phenolapseUAV
262
+ python -m pip install -e ".[dev]"
263
+ python -m pytest
264
+ ```
265
+
266
+ Use `.[automatic,dev]` to include the optional automatic-registration stack.
267
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution rules.
268
+
269
+ ## Release process
270
+
271
+ 1. Update the version in `pyproject.toml`, `CITATION.cff`, and `CHANGELOG.md`.
272
+ 2. Run the test suite and `python -m build`.
273
+ 3. Validate artifacts with `python -m twine check dist/*`.
274
+ 4. Push a version tag and create a GitHub release.
275
+ 5. The release workflow publishes the wheel and source distribution to PyPI
276
+ using trusted publishing.
277
+ 6. Connect the GitHub repository to Zenodo before the first public release to
278
+ obtain both release and concept DOIs.
279
+
280
+ The PyPI project owner must configure a trusted publisher for repository
281
+ `Rocsg/phenolapseUAV`, workflow `publish.yml`, environment `pypi`.
282
+
283
+ ## Citation
284
+
285
+ Citation metadata are provided in [CITATION.cff](CITATION.cff). A JOSS manuscript
286
+ is available under [paper/paper.md](paper/paper.md). Once a DOI or paper citation
287
+ exists, update both files before the corresponding release.
288
+
289
+ ## License
290
+
291
+ phenolapseUAV is distributed under the GNU General Public License version 3.0
292
+ only. See [LICENSE](LICENSE).