onboard-methane-detection 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.
- onboard_methane_detection-0.1.0/LICENSE.md +57 -0
- onboard_methane_detection-0.1.0/MANIFEST.in +1 -0
- onboard_methane_detection-0.1.0/PKG-INFO +234 -0
- onboard_methane_detection-0.1.0/README.md +156 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/__init__.py +17 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/inference/__init__.py +0 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/inference/pipeline.py +67 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/inference/utils.py +129 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/mag1c_sas_base.py +204 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/onground/__init__.py +0 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/onground/utils.py +256 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/processing/__init__.py +0 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/processing/pipeline.py +69 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection/processing/utils.py +173 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection.egg-info/PKG-INFO +234 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection.egg-info/SOURCES.txt +19 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection.egg-info/dependency_links.txt +1 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection.egg-info/requires.txt +2 -0
- onboard_methane_detection-0.1.0/onboard_methane_detection.egg-info/top_level.txt +1 -0
- onboard_methane_detection-0.1.0/pyproject.toml +37 -0
- onboard_methane_detection-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
The majority of code is licensed under:
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025 Jonáš Herec, Zaitra s.r.o.
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
23
|
+
|
|
24
|
+
However, the onboard_methane_detection/mag1c_sas_base.py file and onboard_methane_detection/onground/utils.py contains Mag1c edited code and use files from Mag1c, which is licensed under the BSD 3-Clause License.
|
|
25
|
+
The original code can be found at https://github.com/markusfoote/mag1c
|
|
26
|
+
It is licensed under this license and copyright:
|
|
27
|
+
BSD 3-Clause License
|
|
28
|
+
|
|
29
|
+
Copyright (c) 2019,
|
|
30
|
+
Scientific Computing and Imaging Institute and
|
|
31
|
+
Utah Remote Sensing Applications Lab
|
|
32
|
+
All rights reserved.
|
|
33
|
+
|
|
34
|
+
Redistribution and use in source and binary forms, with or without
|
|
35
|
+
modification, are permitted provided that the following conditions are met:
|
|
36
|
+
|
|
37
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
38
|
+
list of conditions and the following disclaimer.
|
|
39
|
+
|
|
40
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
41
|
+
this list of conditions and the following disclaimer in the documentation
|
|
42
|
+
and/or other materials provided with the distribution.
|
|
43
|
+
|
|
44
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
45
|
+
contributors may be used to endorse or promote products derived from
|
|
46
|
+
this software without specific prior written permission.
|
|
47
|
+
|
|
48
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
49
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
50
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
51
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
52
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
53
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
54
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
55
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
56
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
57
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exclude showcase.ipynb
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: onboard-methane-detection
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Onboard methane detection
|
|
5
|
+
Author-email: Jonáš Herec <jonas.herec@zaitra.io>
|
|
6
|
+
License: The majority of code is licensed under:
|
|
7
|
+
MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2025 Jonáš Herec, Zaitra s.r.o.
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
However, the onboard_methane_detection/mag1c_sas_base.py file and onboard_methane_detection/onground/utils.py contains Mag1c edited code and use files from Mag1c, which is licensed under the BSD 3-Clause License.
|
|
30
|
+
The original code can be found at https://github.com/markusfoote/mag1c
|
|
31
|
+
It is licensed under this license and copyright:
|
|
32
|
+
BSD 3-Clause License
|
|
33
|
+
|
|
34
|
+
Copyright (c) 2019,
|
|
35
|
+
Scientific Computing and Imaging Institute and
|
|
36
|
+
Utah Remote Sensing Applications Lab
|
|
37
|
+
All rights reserved.
|
|
38
|
+
|
|
39
|
+
Redistribution and use in source and binary forms, with or without
|
|
40
|
+
modification, are permitted provided that the following conditions are met:
|
|
41
|
+
|
|
42
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
43
|
+
list of conditions and the following disclaimer.
|
|
44
|
+
|
|
45
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
46
|
+
this list of conditions and the following disclaimer in the documentation
|
|
47
|
+
and/or other materials provided with the distribution.
|
|
48
|
+
|
|
49
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
50
|
+
contributors may be used to endorse or promote products derived from
|
|
51
|
+
this software without specific prior written permission.
|
|
52
|
+
|
|
53
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
54
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
55
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
56
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
57
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
58
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
59
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
60
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
61
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
62
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
63
|
+
|
|
64
|
+
Project-URL: Homepage, https://github.com/zaitra/onboard-methane-detection
|
|
65
|
+
Project-URL: Repository, https://github.com/zaitra/onboard-methane-detection
|
|
66
|
+
Keywords: methane,detection,hyperspectral,remote-sensing,satellite
|
|
67
|
+
Classifier: Programming Language :: Python :: 3
|
|
68
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
69
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
70
|
+
Classifier: Operating System :: OS Independent
|
|
71
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
72
|
+
Requires-Python: >=3.9
|
|
73
|
+
Description-Content-Type: text/markdown
|
|
74
|
+
License-File: LICENSE.md
|
|
75
|
+
Requires-Dist: numpy
|
|
76
|
+
Requires-Dist: onnxruntime
|
|
77
|
+
Dynamic: license-file
|
|
78
|
+
|
|
79
|
+
# Onboard Methane Detection
|
|
80
|
+
|
|
81
|
+
This library provides an end-to-end pipeline for processing hyperspectral imagery to detect methane emissions. It includes tools for band selection, methane spectrum generation, preprocessing via Mag1c SAS, and model inference.
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install onboard-methane-detection
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Dependencies and Performance
|
|
90
|
+
|
|
91
|
+
Onboard Methane Detection is lightweight and depends only on NumPy for processing. ONNX Runtime is required only if you want inference. You can omit ONNX Runtime by using the lightweight build (see `Lightweight Onboard Package Build` below).
|
|
92
|
+
|
|
93
|
+
Important: NumPy can be very slow without an acceleration library, so make sure one is installed on your device. We tested with [OpenBLAS](https://github.com/OpenMathLib/OpenBLAS); the build was about 3.9 MB on ARM Cortex-A53 when compiled without LAPACK. After installing OpenBLAS, reinstall NumPy via `pip` so it can be detected and used.
|
|
94
|
+
|
|
95
|
+
## End-to-End Example
|
|
96
|
+
|
|
97
|
+
The following example demonstrates a complete workflow, divided into distinct stages: on-ground preparation, and onboard session execution. The same example is available in
|
|
98
|
+
<a href="https://colab.research.google.com/github/zaitra/onboard-methane-detection/blob/main/showcase.ipynb">showcase.ipynb <img src="https://colab.research.google.com/assets/colab-badge.svg" height=16px></a>.
|
|
99
|
+
|
|
100
|
+
### On-Ground
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# Core imports for on-ground processing
|
|
104
|
+
import numpy as np
|
|
105
|
+
from onboard_methane_detection import (
|
|
106
|
+
select_the_bands_by_transmittance,
|
|
107
|
+
generate_methane_spectrum,
|
|
108
|
+
select_rgb_bands
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# 1. Define or load sensor specifications
|
|
112
|
+
# wavelengths = [...] # List of all available sensor wavelengths
|
|
113
|
+
# fwhms = [...] # List of corresponding FWHMs
|
|
114
|
+
|
|
115
|
+
# 2. Select RGB bands
|
|
116
|
+
rgb_wavelengths, rgb_indices = select_rgb_bands(wavelengths)
|
|
117
|
+
|
|
118
|
+
# 3. Select bands specifically for Methane detection
|
|
119
|
+
# First, generate the baseline CH4 spectrum across all sensor bands
|
|
120
|
+
ch4_spectrum_full = generate_methane_spectrum(wavelengths, fwhms)
|
|
121
|
+
|
|
122
|
+
# Then, select the most informative bands based on transmittance
|
|
123
|
+
selected_wvs_ch4, selected_ch4_spectrum = select_the_bands_by_transmittance(
|
|
124
|
+
wavelengths, ch4_spectrum_full, N=50, strategy="highest-variance"
|
|
125
|
+
)
|
|
126
|
+
selected_indices_ch4 = [wavelengths.tolist().index(w) for w in selected_wvs_ch4]
|
|
127
|
+
|
|
128
|
+
# 4. Export artifacts for the onboard inference session
|
|
129
|
+
np.save('rgb_indices.npy', np.array(rgb_indices))
|
|
130
|
+
np.save('selected_indices_ch4.npy', np.array(selected_indices_ch4))
|
|
131
|
+
np.save('selected_ch4_spectrum.npy', selected_ch4_spectrum)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Onboard
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
import numpy as np
|
|
138
|
+
|
|
139
|
+
# Core imports for onboard execution
|
|
140
|
+
from onboard_methane_detection import (
|
|
141
|
+
mag1c_sas,
|
|
142
|
+
initialize_model,
|
|
143
|
+
normalize_image,
|
|
144
|
+
model_inference
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 1. Load exported artifacts (indices and spectrum) from On-Ground preparation
|
|
148
|
+
rgb_indices = np.load('rgb_indices.npy')
|
|
149
|
+
selected_indices_ch4 = np.load('selected_indices_ch4.npy')
|
|
150
|
+
selected_ch4_spectrum = np.load('selected_ch4_spectrum.npy')
|
|
151
|
+
|
|
152
|
+
# 2. Session Initialization
|
|
153
|
+
# Initialize the inference model before the processing loop begins
|
|
154
|
+
session, input_name = initialize_model(dynamic_output_size=True)
|
|
155
|
+
|
|
156
|
+
# 3. Processing Images (Onboard Loop)
|
|
157
|
+
# acquired_images: list of images in shape (C, H, W)
|
|
158
|
+
|
|
159
|
+
for image in acquired_images:
|
|
160
|
+
# Extract RGB channels using previously found indices
|
|
161
|
+
rgb_image = image[rgb_indices, :, :]
|
|
162
|
+
|
|
163
|
+
# Extract the CH4 detection bands from the image
|
|
164
|
+
methane_bands_image = image[selected_indices_ch4, :, :]
|
|
165
|
+
|
|
166
|
+
# Apply Mag1c SAS preprocessing.
|
|
167
|
+
# Optional tiling args (`tiling`, `tile_size`) can be used if the image is too large.
|
|
168
|
+
mag1c_output = mag1c_sas(methane_bands_image, selected_ch4_spectrum, tiling=False)
|
|
169
|
+
|
|
170
|
+
# Concatenate Mag1c SAS output with local RGB context.
|
|
171
|
+
# Correct channel order is R, G, B, Mag1c-SAS.
|
|
172
|
+
model_input = np.concatenate([rgb_image, mag1c_output], axis=0)
|
|
173
|
+
|
|
174
|
+
# Normalize input: sensor_or_factors can be "emit", "aviris_ng",
|
|
175
|
+
# or a custom array of 4 division factors for a different sensor.
|
|
176
|
+
model_input = normalize_image(model_input, sensor_or_factors='emit')
|
|
177
|
+
|
|
178
|
+
# Run model inference.
|
|
179
|
+
# Tiling arguments can also be provided here if the image is too large.
|
|
180
|
+
predictions = model_inference(session, input_name, model_input, use_tiling=False, logits_to_probs=True)
|
|
181
|
+
|
|
182
|
+
print("Processed image and generated predictions.")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Lightweight Onboard Package Build
|
|
186
|
+
|
|
187
|
+
For deployment on resource-constrained satellite hardware, you may want to build a minimal version of this library containing only the necessary modules. A deployment script `build_package_for_deployment.py` is provided for this purpose. The lightweight build depends only on NumPy, and adds ONNX Runtime only if you include `inference`.
|
|
188
|
+
|
|
189
|
+
### Usage
|
|
190
|
+
|
|
191
|
+
Run the script to generate a lightweight installable package:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
# Base-only build (no optional subpackages):
|
|
195
|
+
# Includes only `compute_base_mag1c_SAS` (Mag1c-SAS core math).
|
|
196
|
+
python build_package_for_deployment.py --output satellite_pkg
|
|
197
|
+
|
|
198
|
+
# Add optional subpackages as needed:
|
|
199
|
+
python build_package_for_deployment.py --parts processing inference --output satellite_pkg
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### What Gets Included
|
|
203
|
+
|
|
204
|
+
The build script always includes the base Mag1c-SAS implementation and then optionally adds subpackages. You can omit `--parts` entirely to keep the package as small as possible.
|
|
205
|
+
|
|
206
|
+
- Base (always included):
|
|
207
|
+
- Exports `compute_base_mag1c_SAS(...)` which expects batched, flattened radiance data shaped like `(B, N, C)`.
|
|
208
|
+
- Import path: `from onboard_methane_detection import compute_base_mag1c_SAS`
|
|
209
|
+
- This is the minimal dependency footprint and is suitable if you already handle reshaping/masking/sampling in your own pipeline.
|
|
210
|
+
- `processing` (optional):
|
|
211
|
+
- Adds `mag1c_sas(image, methane_spectrum, ...)`, a more convenient Mag1c-SAS wrapper for CHW hyperspectral cubes.
|
|
212
|
+
- Automatically handles masking invalid pixels, reshaping CHW -> `(B, N, C)`, sampling pixels for covariance estimation, and reshaping the output back.
|
|
213
|
+
- `inference` (optional):
|
|
214
|
+
- Adds LinkNet ONNX inference utilities: `initialize_model(...)`, `normalize_image(...)`, and `model_inference(...)`.
|
|
215
|
+
- `model_inference(...)` can use tiling, but tiling utilities live in `processing`, so include both `inference` and `processing` if you want tiled inference.
|
|
216
|
+
- Use this when you want to run the segmentation model onboard.
|
|
217
|
+
- `onground` (optional):
|
|
218
|
+
- Adds on-ground preparation helpers: `generate_methane_spectrum(...)`, `select_the_bands_by_transmittance(...)`, and `select_rgb_bands(...)`.
|
|
219
|
+
- Use this to generate the methane spectrum template and select bands/indices to export for the onboard session.
|
|
220
|
+
|
|
221
|
+
### Arguments
|
|
222
|
+
|
|
223
|
+
- `--parts`: A space-separated list of optional subpackages to include in the build. Available options are `inference`, `processing`, and `onground`. If you want the smallest possible package, omit `--parts` entirely (base-only Mag1c-SAS).
|
|
224
|
+
- `--output`: The destination directory where the lightweight package will be generated (default: `satellite_package`).
|
|
225
|
+
- `--source`: The directory of the source package (default: `onboard_methane_detection`).
|
|
226
|
+
|
|
227
|
+
Once the package is generated, you can install it on your target hardware using:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
pip install ./satellite_pkg
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
# Acknowledgments
|
|
234
|
+
A huge thank you to the creators of [Mag1c](https://github.com/markusfoote/mag1c). This project uses core files and edited code from their original repository, and it wouldn't have been possible without their foundational work!
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Onboard Methane Detection
|
|
2
|
+
|
|
3
|
+
This library provides an end-to-end pipeline for processing hyperspectral imagery to detect methane emissions. It includes tools for band selection, methane spectrum generation, preprocessing via Mag1c SAS, and model inference.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install onboard-methane-detection
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Dependencies and Performance
|
|
12
|
+
|
|
13
|
+
Onboard Methane Detection is lightweight and depends only on NumPy for processing. ONNX Runtime is required only if you want inference. You can omit ONNX Runtime by using the lightweight build (see `Lightweight Onboard Package Build` below).
|
|
14
|
+
|
|
15
|
+
Important: NumPy can be very slow without an acceleration library, so make sure one is installed on your device. We tested with [OpenBLAS](https://github.com/OpenMathLib/OpenBLAS); the build was about 3.9 MB on ARM Cortex-A53 when compiled without LAPACK. After installing OpenBLAS, reinstall NumPy via `pip` so it can be detected and used.
|
|
16
|
+
|
|
17
|
+
## End-to-End Example
|
|
18
|
+
|
|
19
|
+
The following example demonstrates a complete workflow, divided into distinct stages: on-ground preparation, and onboard session execution. The same example is available in
|
|
20
|
+
<a href="https://colab.research.google.com/github/zaitra/onboard-methane-detection/blob/main/showcase.ipynb">showcase.ipynb <img src="https://colab.research.google.com/assets/colab-badge.svg" height=16px></a>.
|
|
21
|
+
|
|
22
|
+
### On-Ground
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# Core imports for on-ground processing
|
|
26
|
+
import numpy as np
|
|
27
|
+
from onboard_methane_detection import (
|
|
28
|
+
select_the_bands_by_transmittance,
|
|
29
|
+
generate_methane_spectrum,
|
|
30
|
+
select_rgb_bands
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# 1. Define or load sensor specifications
|
|
34
|
+
# wavelengths = [...] # List of all available sensor wavelengths
|
|
35
|
+
# fwhms = [...] # List of corresponding FWHMs
|
|
36
|
+
|
|
37
|
+
# 2. Select RGB bands
|
|
38
|
+
rgb_wavelengths, rgb_indices = select_rgb_bands(wavelengths)
|
|
39
|
+
|
|
40
|
+
# 3. Select bands specifically for Methane detection
|
|
41
|
+
# First, generate the baseline CH4 spectrum across all sensor bands
|
|
42
|
+
ch4_spectrum_full = generate_methane_spectrum(wavelengths, fwhms)
|
|
43
|
+
|
|
44
|
+
# Then, select the most informative bands based on transmittance
|
|
45
|
+
selected_wvs_ch4, selected_ch4_spectrum = select_the_bands_by_transmittance(
|
|
46
|
+
wavelengths, ch4_spectrum_full, N=50, strategy="highest-variance"
|
|
47
|
+
)
|
|
48
|
+
selected_indices_ch4 = [wavelengths.tolist().index(w) for w in selected_wvs_ch4]
|
|
49
|
+
|
|
50
|
+
# 4. Export artifacts for the onboard inference session
|
|
51
|
+
np.save('rgb_indices.npy', np.array(rgb_indices))
|
|
52
|
+
np.save('selected_indices_ch4.npy', np.array(selected_indices_ch4))
|
|
53
|
+
np.save('selected_ch4_spectrum.npy', selected_ch4_spectrum)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Onboard
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import numpy as np
|
|
60
|
+
|
|
61
|
+
# Core imports for onboard execution
|
|
62
|
+
from onboard_methane_detection import (
|
|
63
|
+
mag1c_sas,
|
|
64
|
+
initialize_model,
|
|
65
|
+
normalize_image,
|
|
66
|
+
model_inference
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# 1. Load exported artifacts (indices and spectrum) from On-Ground preparation
|
|
70
|
+
rgb_indices = np.load('rgb_indices.npy')
|
|
71
|
+
selected_indices_ch4 = np.load('selected_indices_ch4.npy')
|
|
72
|
+
selected_ch4_spectrum = np.load('selected_ch4_spectrum.npy')
|
|
73
|
+
|
|
74
|
+
# 2. Session Initialization
|
|
75
|
+
# Initialize the inference model before the processing loop begins
|
|
76
|
+
session, input_name = initialize_model(dynamic_output_size=True)
|
|
77
|
+
|
|
78
|
+
# 3. Processing Images (Onboard Loop)
|
|
79
|
+
# acquired_images: list of images in shape (C, H, W)
|
|
80
|
+
|
|
81
|
+
for image in acquired_images:
|
|
82
|
+
# Extract RGB channels using previously found indices
|
|
83
|
+
rgb_image = image[rgb_indices, :, :]
|
|
84
|
+
|
|
85
|
+
# Extract the CH4 detection bands from the image
|
|
86
|
+
methane_bands_image = image[selected_indices_ch4, :, :]
|
|
87
|
+
|
|
88
|
+
# Apply Mag1c SAS preprocessing.
|
|
89
|
+
# Optional tiling args (`tiling`, `tile_size`) can be used if the image is too large.
|
|
90
|
+
mag1c_output = mag1c_sas(methane_bands_image, selected_ch4_spectrum, tiling=False)
|
|
91
|
+
|
|
92
|
+
# Concatenate Mag1c SAS output with local RGB context.
|
|
93
|
+
# Correct channel order is R, G, B, Mag1c-SAS.
|
|
94
|
+
model_input = np.concatenate([rgb_image, mag1c_output], axis=0)
|
|
95
|
+
|
|
96
|
+
# Normalize input: sensor_or_factors can be "emit", "aviris_ng",
|
|
97
|
+
# or a custom array of 4 division factors for a different sensor.
|
|
98
|
+
model_input = normalize_image(model_input, sensor_or_factors='emit')
|
|
99
|
+
|
|
100
|
+
# Run model inference.
|
|
101
|
+
# Tiling arguments can also be provided here if the image is too large.
|
|
102
|
+
predictions = model_inference(session, input_name, model_input, use_tiling=False, logits_to_probs=True)
|
|
103
|
+
|
|
104
|
+
print("Processed image and generated predictions.")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Lightweight Onboard Package Build
|
|
108
|
+
|
|
109
|
+
For deployment on resource-constrained satellite hardware, you may want to build a minimal version of this library containing only the necessary modules. A deployment script `build_package_for_deployment.py` is provided for this purpose. The lightweight build depends only on NumPy, and adds ONNX Runtime only if you include `inference`.
|
|
110
|
+
|
|
111
|
+
### Usage
|
|
112
|
+
|
|
113
|
+
Run the script to generate a lightweight installable package:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# Base-only build (no optional subpackages):
|
|
117
|
+
# Includes only `compute_base_mag1c_SAS` (Mag1c-SAS core math).
|
|
118
|
+
python build_package_for_deployment.py --output satellite_pkg
|
|
119
|
+
|
|
120
|
+
# Add optional subpackages as needed:
|
|
121
|
+
python build_package_for_deployment.py --parts processing inference --output satellite_pkg
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### What Gets Included
|
|
125
|
+
|
|
126
|
+
The build script always includes the base Mag1c-SAS implementation and then optionally adds subpackages. You can omit `--parts` entirely to keep the package as small as possible.
|
|
127
|
+
|
|
128
|
+
- Base (always included):
|
|
129
|
+
- Exports `compute_base_mag1c_SAS(...)` which expects batched, flattened radiance data shaped like `(B, N, C)`.
|
|
130
|
+
- Import path: `from onboard_methane_detection import compute_base_mag1c_SAS`
|
|
131
|
+
- This is the minimal dependency footprint and is suitable if you already handle reshaping/masking/sampling in your own pipeline.
|
|
132
|
+
- `processing` (optional):
|
|
133
|
+
- Adds `mag1c_sas(image, methane_spectrum, ...)`, a more convenient Mag1c-SAS wrapper for CHW hyperspectral cubes.
|
|
134
|
+
- Automatically handles masking invalid pixels, reshaping CHW -> `(B, N, C)`, sampling pixels for covariance estimation, and reshaping the output back.
|
|
135
|
+
- `inference` (optional):
|
|
136
|
+
- Adds LinkNet ONNX inference utilities: `initialize_model(...)`, `normalize_image(...)`, and `model_inference(...)`.
|
|
137
|
+
- `model_inference(...)` can use tiling, but tiling utilities live in `processing`, so include both `inference` and `processing` if you want tiled inference.
|
|
138
|
+
- Use this when you want to run the segmentation model onboard.
|
|
139
|
+
- `onground` (optional):
|
|
140
|
+
- Adds on-ground preparation helpers: `generate_methane_spectrum(...)`, `select_the_bands_by_transmittance(...)`, and `select_rgb_bands(...)`.
|
|
141
|
+
- Use this to generate the methane spectrum template and select bands/indices to export for the onboard session.
|
|
142
|
+
|
|
143
|
+
### Arguments
|
|
144
|
+
|
|
145
|
+
- `--parts`: A space-separated list of optional subpackages to include in the build. Available options are `inference`, `processing`, and `onground`. If you want the smallest possible package, omit `--parts` entirely (base-only Mag1c-SAS).
|
|
146
|
+
- `--output`: The destination directory where the lightweight package will be generated (default: `satellite_package`).
|
|
147
|
+
- `--source`: The directory of the source package (default: `onboard_methane_detection`).
|
|
148
|
+
|
|
149
|
+
Once the package is generated, you can install it on your target hardware using:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install ./satellite_pkg
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
# Acknowledgments
|
|
156
|
+
A huge thank you to the creators of [Mag1c](https://github.com/markusfoote/mag1c). This project uses core files and edited code from their original repository, and it wouldn't have been possible without their foundational work!
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .mag1c_sas_base import compute_base_mag1c_SAS
|
|
2
|
+
# Optional imports.
|
|
3
|
+
try:
|
|
4
|
+
from .processing.pipeline import mag1c_sas
|
|
5
|
+
except ImportError:
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from .inference.utils import initialize_model, normalize_image
|
|
10
|
+
from .inference.pipeline import model_inference
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from .onground.utils import select_the_bands_by_transmittance, generate_methane_spectrum, select_rgb_bands
|
|
16
|
+
except ImportError:
|
|
17
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .utils import run_inference_full, pad_to_32, reverse_32_padding
|
|
3
|
+
from ..processing.utils import tile_image, stitch_tiles
|
|
4
|
+
|
|
5
|
+
def model_inference(session, input_name, image, use_tiling=True, tile_size=512, logits_to_probs=False):
|
|
6
|
+
"""
|
|
7
|
+
Pipeline for performing model inference on a CHW image with optional tiling.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
session: Initialized ONNX inference session.
|
|
11
|
+
input_name: Input node name for the ONNX session.
|
|
12
|
+
image: Input image of shape (C, H, W) or (B, C, H, W).
|
|
13
|
+
use_tiling: If True, process the image in tiles. If False, process the whole image at once.
|
|
14
|
+
tile_size: Size of tiles (default 512).
|
|
15
|
+
logits_to_probs: If True, apply sigmoid to convert logits to probabilities.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Model inference result of shape (1, H, W).
|
|
19
|
+
"""
|
|
20
|
+
is_chw = image.ndim == 3
|
|
21
|
+
if is_chw:
|
|
22
|
+
image = np.expand_dims(image, axis=0)
|
|
23
|
+
|
|
24
|
+
if not use_tiling:
|
|
25
|
+
padded_image, pad_info = pad_to_32(image)
|
|
26
|
+
result = _process_tile_inference(session, input_name, padded_image, logits_to_probs)
|
|
27
|
+
result_unpadded = reverse_32_padding(result, pad_info)
|
|
28
|
+
return result_unpadded
|
|
29
|
+
|
|
30
|
+
# Tile the image
|
|
31
|
+
tiles, tiling_info = tile_image(image, tile_size)
|
|
32
|
+
|
|
33
|
+
# Process each tile
|
|
34
|
+
result_tiles = []
|
|
35
|
+
for tile in tiles:
|
|
36
|
+
tile_result = _process_tile_inference(session, input_name, tile, logits_to_probs)
|
|
37
|
+
result_tiles.append(tile_result)
|
|
38
|
+
|
|
39
|
+
# Stitch results back together
|
|
40
|
+
# Update tiling_info for single-channel output
|
|
41
|
+
orig_bchw = tiling_info['original_shape']
|
|
42
|
+
orig_h, orig_w = orig_bchw[-2:]
|
|
43
|
+
tiling_info['original_shape'] = (1, orig_h, orig_w)
|
|
44
|
+
|
|
45
|
+
result = stitch_tiles(result_tiles, tiling_info)
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _process_tile_inference(session, input_name, image_batched, logits_to_probs):
|
|
50
|
+
"""
|
|
51
|
+
Process a single tile/image through model inference.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
session: Initialized ONNX inference session.
|
|
55
|
+
input_name: Input node name for the ONNX session.
|
|
56
|
+
image_batched: Input image of shape (B, C, H, W).
|
|
57
|
+
logits_to_probs: If True, apply sigmoid to convert logits to probabilities.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Model inference result of shape (1, H, W).
|
|
61
|
+
"""
|
|
62
|
+
image_batched = image_batched.astype(np.float32)
|
|
63
|
+
|
|
64
|
+
# Run inference
|
|
65
|
+
result = run_inference_full(session, input_name, image_batched, logits_to_probs=logits_to_probs)
|
|
66
|
+
|
|
67
|
+
return result
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import onnxruntime as ort
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def initialize_model(model_path: str = None, dynamic_output_size: bool = False) -> tuple[ort.InferenceSession, str]:
|
|
7
|
+
"""
|
|
8
|
+
Initialize and return the ONNX inference session and input name.
|
|
9
|
+
"""
|
|
10
|
+
if model_path is None:
|
|
11
|
+
model_filename = "linknet_exported_dynamic.onnx" if dynamic_output_size else "linknet_exported.onnx"
|
|
12
|
+
print(f"Using model: {model_filename}")
|
|
13
|
+
model_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), model_filename)
|
|
14
|
+
session = ort.InferenceSession(model_path)
|
|
15
|
+
input_name = session.get_inputs()[0].name
|
|
16
|
+
return session, input_name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_sole_inference(session: ort.InferenceSession, input_name: str, image: np.ndarray) -> np.ndarray:
|
|
20
|
+
"""
|
|
21
|
+
Run the inference session with the given image.
|
|
22
|
+
"""
|
|
23
|
+
outputs = session.run(None, {input_name: image})
|
|
24
|
+
return outputs[0]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def postprocess_result(result: np.ndarray, logits_to_probs: bool = False) -> np.ndarray:
|
|
28
|
+
"""
|
|
29
|
+
Postprocess the inference results, converting logits to probabilities if requested.
|
|
30
|
+
"""
|
|
31
|
+
if logits_to_probs:
|
|
32
|
+
result = 1 / (1 + np.exp(-result)) # Sigmoid function
|
|
33
|
+
return result[0] # Remove batch dimension if present
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_image(image: np.ndarray, sensor_or_factors: str | list | np.ndarray = 'emit') -> np.ndarray:
|
|
37
|
+
"""
|
|
38
|
+
Normalize a CHW image and return BCHW.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
image: Numpy array of shape (C, H, W). If BCHW is provided, it is used as-is.
|
|
42
|
+
sensor_or_factors: 'emit', 'aviris_ng', or a list/array of length 4 containing custom division factors.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(sensor_or_factors, str):
|
|
45
|
+
if sensor_or_factors.lower() == 'emit':
|
|
46
|
+
factors = np.array([98.91703491210939, 112.33824462890625, 119.84940185546876, 2436.562744140625], dtype=image.dtype)
|
|
47
|
+
elif sensor_or_factors.lower() == 'aviris_ng':
|
|
48
|
+
factors = np.array([247.29258728, 280.84561157, 299.62350464, 1218.28137207], dtype=image.dtype)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError("String must be 'emit' or 'aviris_ng'")
|
|
51
|
+
elif isinstance(sensor_or_factors, (list, np.ndarray)) and len(sensor_or_factors) == 4:
|
|
52
|
+
factors = np.array(sensor_or_factors, dtype=image.dtype)
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("Must be 'emit', 'aviris_ng', or a custom list/array of length 4")
|
|
55
|
+
|
|
56
|
+
if image.ndim == 3:
|
|
57
|
+
image_bchw = image[None, :, :, :]
|
|
58
|
+
elif image.ndim == 4:
|
|
59
|
+
image_bchw = image
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError("Image must be CHW or BCHW")
|
|
62
|
+
|
|
63
|
+
factors = factors[None, :, None, None]
|
|
64
|
+
normalized = image_bchw / factors
|
|
65
|
+
return np.clip(normalized, 0, 2)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def run_inference_full(session: ort.InferenceSession, input_name: str, image: np.ndarray, logits_to_probs: bool = False) -> np.ndarray:
|
|
69
|
+
"""
|
|
70
|
+
Load an ONNX model and perform inference on a numpy image.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
session: Initialized ONNX inference session.
|
|
74
|
+
input_name: Input node name for the ONNX session.
|
|
75
|
+
image: Input numpy image array of shape (B, C, H, W) where:
|
|
76
|
+
- B is batch size
|
|
77
|
+
- C is number of channels (4)
|
|
78
|
+
- H is image height
|
|
79
|
+
- W is image width
|
|
80
|
+
logits_to_probs: If True, apply sigmoid to convert logits to probabilities
|
|
81
|
+
(for binary segmentation).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Inference output as numpy array of shape (B, 1, H, W).
|
|
85
|
+
Contains probabilities if logits_to_probs=True, else raw logits.
|
|
86
|
+
"""
|
|
87
|
+
result = run_sole_inference(session, input_name, image)
|
|
88
|
+
return postprocess_result(result, logits_to_probs)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def pad_to_32(image: np.ndarray) -> tuple[np.ndarray, dict]:
|
|
92
|
+
"""
|
|
93
|
+
Pad a BCHW image so its spatial dimensions (H, W) are divisible by 32.
|
|
94
|
+
Returns the padded image and a dictionary with padding information.
|
|
95
|
+
"""
|
|
96
|
+
b, c, orig_h, orig_w = image.shape
|
|
97
|
+
pad_h = (32 - (orig_h % 32)) % 32
|
|
98
|
+
pad_w = (32 - (orig_w % 32)) % 32
|
|
99
|
+
|
|
100
|
+
pad_top = pad_h // 2
|
|
101
|
+
pad_bottom = pad_h - pad_top
|
|
102
|
+
pad_left = pad_w // 2
|
|
103
|
+
pad_right = pad_w - pad_left
|
|
104
|
+
|
|
105
|
+
padded_image = np.pad(image, ((0, 0), (0, 0), (pad_top, pad_bottom), (pad_left, pad_right)), mode='constant')
|
|
106
|
+
|
|
107
|
+
pad_info = {
|
|
108
|
+
'orig_h': orig_h,
|
|
109
|
+
'orig_w': orig_w,
|
|
110
|
+
'pad_top': pad_top,
|
|
111
|
+
'pad_left': pad_left,
|
|
112
|
+
'pad_h': pad_h,
|
|
113
|
+
'pad_w': pad_w
|
|
114
|
+
}
|
|
115
|
+
return padded_image, pad_info
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def reverse_32_padding(result_image: np.ndarray, pad_info: dict) -> np.ndarray:
|
|
119
|
+
"""
|
|
120
|
+
Crop the padded inference result back to its original shape.
|
|
121
|
+
Assumes result_image is of shape (1, H, W).
|
|
122
|
+
"""
|
|
123
|
+
if pad_info['pad_h'] > 0 or pad_info['pad_w'] > 0:
|
|
124
|
+
pad_top = pad_info['pad_top']
|
|
125
|
+
pad_left = pad_info['pad_left']
|
|
126
|
+
orig_h = pad_info['orig_h']
|
|
127
|
+
orig_w = pad_info['orig_w']
|
|
128
|
+
return result_image[:, pad_top:pad_top+orig_h, pad_left:pad_left+orig_w]
|
|
129
|
+
return result_image
|