PyGeoFetch 2.6.2.7__py3-none-any.whl → 2.6.2.9__py3-none-any.whl
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.
- pygeofetch/__init__.py +1 -1
- pygeofetch/cli/index_commands.py +119 -30
- pygeofetch/insar/auto_fallback.py +18 -2
- pygeofetch/meta.yaml +1 -1
- pygeofetch/processing/base.py +210 -0
- pygeofetch/processing/indices.py +357 -60
- {pygeofetch-2.6.2.7.dist-info → pygeofetch-2.6.2.9.dist-info}/METADATA +9 -9
- {pygeofetch-2.6.2.7.dist-info → pygeofetch-2.6.2.9.dist-info}/RECORD +12 -12
- {pygeofetch-2.6.2.7.dist-info → pygeofetch-2.6.2.9.dist-info}/WHEEL +0 -0
- {pygeofetch-2.6.2.7.dist-info → pygeofetch-2.6.2.9.dist-info}/entry_points.txt +0 -0
- {pygeofetch-2.6.2.7.dist-info → pygeofetch-2.6.2.9.dist-info}/licenses/LICENSE +0 -0
- {pygeofetch-2.6.2.7.dist-info → pygeofetch-2.6.2.9.dist-info}/top_level.txt +0 -0
pygeofetch/__init__.py
CHANGED
pygeofetch/cli/index_commands.py
CHANGED
|
@@ -50,22 +50,64 @@ def index() -> None:
|
|
|
50
50
|
"--nir", required=True, type=click.Path(exists=True), help="NIR band (e.g. B08.tif)"
|
|
51
51
|
)
|
|
52
52
|
@click.option("--output", "-o", default=None)
|
|
53
|
-
|
|
54
|
-
""
|
|
53
|
+
@click.option(
|
|
54
|
+
"--chunked", is_flag=True,
|
|
55
|
+
help="Process in memory-safe tiles for large rasters (e.g. drone orthomosaics) that exceed available RAM or the 4GB GeoTIFF limit.",
|
|
56
|
+
)
|
|
57
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int, help="Tile edge length in pixels when --chunked is set.")
|
|
58
|
+
def ndvi_cmd(red, nir, output, chunked, tile_size):
|
|
59
|
+
"""NDVI — Normalized Difference Vegetation Index. Range: -1 to +1. Vegetation > 0.3.
|
|
60
|
+
|
|
61
|
+
Pass the SAME path to --red and --nir to auto-detect both bands from
|
|
62
|
+
one merged multi-band image (e.g. a drone orthomosaic), using its
|
|
63
|
+
real, embedded band descriptions.
|
|
64
|
+
"""
|
|
55
65
|
e = _engine()
|
|
56
|
-
r = e.indices.ndvi(red=red, nir=nir, output=output)
|
|
66
|
+
r = e.indices.ndvi(red=red, nir=nir, output=output, chunked=chunked, tile_size=tile_size)
|
|
57
67
|
_pr(r, "NDVI")
|
|
58
68
|
|
|
59
69
|
|
|
70
|
+
@index.command("ndre")
|
|
71
|
+
@click.option(
|
|
72
|
+
"--rededge", required=True, type=click.Path(exists=True), help="Red edge band"
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--nir", required=True, type=click.Path(exists=True), help="NIR band"
|
|
76
|
+
)
|
|
77
|
+
@click.option("--output", "-o", default=None)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--chunked", is_flag=True,
|
|
80
|
+
help="Process in memory-safe tiles for large rasters that exceed available RAM or the 4GB GeoTIFF limit.",
|
|
81
|
+
)
|
|
82
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int, help="Tile edge length in pixels when --chunked is set.")
|
|
83
|
+
def ndre_cmd(rededge, nir, output, chunked, tile_size):
|
|
84
|
+
"""NDRE — Normalized Difference Red Edge Index. Range: -1 to +1.
|
|
85
|
+
|
|
86
|
+
Less prone to saturation than NDVI in dense canopy -- useful for
|
|
87
|
+
crop stress and chlorophyll monitoring from drone multispectral
|
|
88
|
+
sensors. Pass the SAME path to --rededge and --nir to auto-detect
|
|
89
|
+
both bands from one merged multi-band image.
|
|
90
|
+
"""
|
|
91
|
+
e = _engine()
|
|
92
|
+
r = e.indices.ndre(rededge=rededge, nir=nir, output=output, chunked=chunked, tile_size=tile_size)
|
|
93
|
+
_pr(r, "NDRE")
|
|
94
|
+
|
|
95
|
+
|
|
60
96
|
@index.command("evi")
|
|
61
97
|
@click.option("--blue", required=True, type=click.Path(exists=True))
|
|
62
98
|
@click.option("--red", required=True, type=click.Path(exists=True))
|
|
63
99
|
@click.option("--nir", required=True, type=click.Path(exists=True))
|
|
64
100
|
@click.option("--output", "-o", default=None)
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
102
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
103
|
+
def evi_cmd(blue, red, nir, output, chunked, tile_size):
|
|
104
|
+
"""EVI — Enhanced Vegetation Index. Better than NDVI in high-biomass areas.
|
|
105
|
+
|
|
106
|
+
Pass the SAME path to --blue, --red, and --nir to auto-detect all
|
|
107
|
+
three bands from one merged multi-band image.
|
|
108
|
+
"""
|
|
67
109
|
e = _engine()
|
|
68
|
-
r = e.indices.evi(blue=blue, red=red, nir=nir, output=output)
|
|
110
|
+
r = e.indices.evi(blue=blue, red=red, nir=nir, output=output, chunked=chunked, tile_size=tile_size)
|
|
69
111
|
_pr(r, "EVI")
|
|
70
112
|
|
|
71
113
|
|
|
@@ -74,10 +116,16 @@ def evi_cmd(blue, red, nir, output):
|
|
|
74
116
|
@click.option("--nir", required=True, type=click.Path(exists=True))
|
|
75
117
|
@click.option("--soil-l", default=0.5, show_default=True, type=float)
|
|
76
118
|
@click.option("--output", "-o", default=None)
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
120
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
121
|
+
def savi_cmd(red, nir, soil_l, output, chunked, tile_size):
|
|
122
|
+
"""SAVI — Soil Adjusted Vegetation Index. Reduces soil brightness influence.
|
|
123
|
+
|
|
124
|
+
Pass the SAME path to --red and --nir to auto-detect both bands
|
|
125
|
+
from one merged multi-band image.
|
|
126
|
+
"""
|
|
79
127
|
e = _engine()
|
|
80
|
-
r = e.indices.savi(red=red, nir=nir, L=soil_l, output=output)
|
|
128
|
+
r = e.indices.savi(red=red, nir=nir, L=soil_l, output=output, chunked=chunked, tile_size=tile_size)
|
|
81
129
|
_pr(r, "SAVI")
|
|
82
130
|
|
|
83
131
|
|
|
@@ -85,10 +133,16 @@ def savi_cmd(red, nir, soil_l, output):
|
|
|
85
133
|
@click.option("--green", required=True, type=click.Path(exists=True))
|
|
86
134
|
@click.option("--nir", required=True, type=click.Path(exists=True))
|
|
87
135
|
@click.option("--output", "-o", default=None)
|
|
88
|
-
|
|
89
|
-
|
|
136
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
137
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
138
|
+
def ndwi_cmd(green, nir, output, chunked, tile_size):
|
|
139
|
+
"""NDWI — Normalized Difference Water Index. Water > 0.
|
|
140
|
+
|
|
141
|
+
Pass the SAME path to --green and --nir to auto-detect both bands
|
|
142
|
+
from one merged multi-band image.
|
|
143
|
+
"""
|
|
90
144
|
e = _engine()
|
|
91
|
-
r = e.indices.ndwi(green=green, nir=nir, output=output)
|
|
145
|
+
r = e.indices.ndwi(green=green, nir=nir, output=output, chunked=chunked, tile_size=tile_size)
|
|
92
146
|
_pr(r, "NDWI")
|
|
93
147
|
|
|
94
148
|
|
|
@@ -96,10 +150,16 @@ def ndwi_cmd(green, nir, output):
|
|
|
96
150
|
@click.option("--green", required=True, type=click.Path(exists=True))
|
|
97
151
|
@click.option("--swir1", required=True, type=click.Path(exists=True))
|
|
98
152
|
@click.option("--output", "-o", default=None)
|
|
99
|
-
|
|
100
|
-
|
|
153
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
154
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
155
|
+
def mndwi_cmd(green, swir1, output, chunked, tile_size):
|
|
156
|
+
"""MNDWI — Modified NDWI. Better separation of water from built-up areas.
|
|
157
|
+
|
|
158
|
+
Pass the SAME path to --green and --swir1 to auto-detect both
|
|
159
|
+
bands from one merged multi-band image.
|
|
160
|
+
"""
|
|
101
161
|
e = _engine()
|
|
102
|
-
r = e.indices.mndwi(green=green, swir1=swir1, output=output)
|
|
162
|
+
r = e.indices.mndwi(green=green, swir1=swir1, output=output, chunked=chunked, tile_size=tile_size)
|
|
103
163
|
_pr(r, "MNDWI")
|
|
104
164
|
|
|
105
165
|
|
|
@@ -107,10 +167,16 @@ def mndwi_cmd(green, swir1, output):
|
|
|
107
167
|
@click.option("--nir", required=True, type=click.Path(exists=True))
|
|
108
168
|
@click.option("--swir1", required=True, type=click.Path(exists=True))
|
|
109
169
|
@click.option("--output", "-o", default=None)
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
171
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
172
|
+
def ndbi_cmd(nir, swir1, output, chunked, tile_size):
|
|
173
|
+
"""NDBI — Normalized Difference Built-up Index. Urban > 0.
|
|
174
|
+
|
|
175
|
+
Pass the SAME path to --nir and --swir1 to auto-detect both bands
|
|
176
|
+
from one merged multi-band image.
|
|
177
|
+
"""
|
|
112
178
|
e = _engine()
|
|
113
|
-
r = e.indices.ndbi(nir=nir, swir1=swir1, output=output)
|
|
179
|
+
r = e.indices.ndbi(nir=nir, swir1=swir1, output=output, chunked=chunked, tile_size=tile_size)
|
|
114
180
|
_pr(r, "NDBI")
|
|
115
181
|
|
|
116
182
|
|
|
@@ -118,10 +184,16 @@ def ndbi_cmd(nir, swir1, output):
|
|
|
118
184
|
@click.option("--green", required=True, type=click.Path(exists=True))
|
|
119
185
|
@click.option("--swir1", required=True, type=click.Path(exists=True))
|
|
120
186
|
@click.option("--output", "-o", default=None)
|
|
121
|
-
|
|
122
|
-
|
|
187
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
188
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
189
|
+
def ndsi_cmd(green, swir1, output, chunked, tile_size):
|
|
190
|
+
"""NDSI — Snow Index. Snow > 0.4.
|
|
191
|
+
|
|
192
|
+
Pass the SAME path to --green and --swir1 to auto-detect both
|
|
193
|
+
bands from one merged multi-band image.
|
|
194
|
+
"""
|
|
123
195
|
e = _engine()
|
|
124
|
-
r = e.indices.ndsi(green=green, swir1=swir1, output=output)
|
|
196
|
+
r = e.indices.ndsi(green=green, swir1=swir1, output=output, chunked=chunked, tile_size=tile_size)
|
|
125
197
|
_pr(r, "NDSI")
|
|
126
198
|
|
|
127
199
|
|
|
@@ -129,10 +201,16 @@ def ndsi_cmd(green, swir1, output):
|
|
|
129
201
|
@click.option("--nir", required=True, type=click.Path(exists=True))
|
|
130
202
|
@click.option("--swir1", required=True, type=click.Path(exists=True))
|
|
131
203
|
@click.option("--output", "-o", default=None)
|
|
132
|
-
|
|
133
|
-
|
|
204
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
205
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
206
|
+
def ndmi_cmd(nir, swir1, output, chunked, tile_size):
|
|
207
|
+
"""NDMI — Moisture Index. Sensitive to canopy water content.
|
|
208
|
+
|
|
209
|
+
Pass the SAME path to --nir and --swir1 to auto-detect both bands
|
|
210
|
+
from one merged multi-band image.
|
|
211
|
+
"""
|
|
134
212
|
e = _engine()
|
|
135
|
-
r = e.indices.ndmi(nir=nir, swir1=swir1, output=output)
|
|
213
|
+
r = e.indices.ndmi(nir=nir, swir1=swir1, output=output, chunked=chunked, tile_size=tile_size)
|
|
136
214
|
_pr(r, "NDMI")
|
|
137
215
|
|
|
138
216
|
|
|
@@ -140,10 +218,16 @@ def ndmi_cmd(nir, swir1, output):
|
|
|
140
218
|
@click.option("--nir", required=True, type=click.Path(exists=True))
|
|
141
219
|
@click.option("--swir2", required=True, type=click.Path(exists=True))
|
|
142
220
|
@click.option("--output", "-o", default=None)
|
|
143
|
-
|
|
144
|
-
|
|
221
|
+
@click.option("--chunked", is_flag=True, help="Process in memory-safe tiles for large rasters.")
|
|
222
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int)
|
|
223
|
+
def nbr_cmd(nir, swir2, output, chunked, tile_size):
|
|
224
|
+
"""NBR — Normalized Burn Ratio. Use dNBR for burn severity.
|
|
225
|
+
|
|
226
|
+
Pass the SAME path to --nir and --swir2 to auto-detect both bands
|
|
227
|
+
from one merged multi-band image.
|
|
228
|
+
"""
|
|
145
229
|
e = _engine()
|
|
146
|
-
r = e.indices.nbr(nir=nir, swir2=swir2, output=output)
|
|
230
|
+
r = e.indices.nbr(nir=nir, swir2=swir2, output=output, chunked=chunked, tile_size=tile_size)
|
|
147
231
|
_pr(r, "NBR")
|
|
148
232
|
|
|
149
233
|
|
|
@@ -262,10 +346,15 @@ def albedo_cmd(inputs, sensor, output):
|
|
|
262
346
|
"--expr", "-e", required=True, help="Python expression. Bands as B[0], B[1], ..."
|
|
263
347
|
)
|
|
264
348
|
@click.option("--output", "-o", default=None)
|
|
265
|
-
|
|
349
|
+
@click.option(
|
|
350
|
+
"--chunked", is_flag=True,
|
|
351
|
+
help="Process in memory-safe tiles for large rasters (e.g. drone orthomosaics) that exceed available RAM or the 4GB GeoTIFF limit.",
|
|
352
|
+
)
|
|
353
|
+
@click.option("--tile-size", default=1024, show_default=True, type=int, help="Tile edge length in pixels when --chunked is set.")
|
|
354
|
+
def band_math_cmd(inputs, expr, output, chunked, tile_size):
|
|
266
355
|
"""Arbitrary band arithmetic. Example: --expr '(B[1]-B[0])/(B[1]+B[0])'."""
|
|
267
356
|
e = _engine()
|
|
268
|
-
r = e.indices.band_math(inputs=list(inputs), expression=expr, output=output)
|
|
357
|
+
r = e.indices.band_math(inputs=list(inputs), expression=expr, output=output, chunked=chunked, tile_size=tile_size)
|
|
269
358
|
_pr(r, f"band-math ({expr[:40]})")
|
|
270
359
|
|
|
271
360
|
|
|
@@ -276,4 +365,4 @@ def stack_cmd(inputs, output):
|
|
|
276
365
|
"""Stack single-band rasters into a multi-band GeoTIFF."""
|
|
277
366
|
e = _engine()
|
|
278
367
|
r = e.indices.stack(inputs=list(inputs), output=output)
|
|
279
|
-
_pr(r, f"stack ({len(inputs)} bands)")
|
|
368
|
+
_pr(r, f"stack ({len(inputs)} bands)")
|
|
@@ -293,7 +293,23 @@ def run_multi_modal_deformation(
|
|
|
293
293
|
# received a scalar `pairs` list, not a per-pixel coherence raster
|
|
294
294
|
# -- build one the same real, honest way the multi-sensor
|
|
295
295
|
# notebooks do (mean across the real per-pair coherence arrays).
|
|
296
|
-
|
|
296
|
+
#
|
|
297
|
+
# Real, necessary edge case, confirmed directly: when `pairs` is
|
|
298
|
+
# genuinely empty (a total InSAR failure with zero usable pairs at
|
|
299
|
+
# all, not just low coherence), `np.mean([], axis=0)` silently
|
|
300
|
+
# degrades to a scalar `nan` with shape `()` rather than raising --
|
|
301
|
+
# which then fails the real shape check inside fuse_insar_optical
|
|
302
|
+
# with a confusing "all input arrays must share the same real
|
|
303
|
+
# shape" error that gives no hint the real cause was an empty
|
|
304
|
+
# pairs list. Build a properly-shaped all-NaN raster explicitly in
|
|
305
|
+
# this case instead: a real, honest signal of "zero real coherence
|
|
306
|
+
# information available," matching `velocity`'s own real shape so
|
|
307
|
+
# fuse_insar_optical's fusion logic runs correctly and correctly
|
|
308
|
+
# concludes optical-only, rather than crashing before it can.
|
|
309
|
+
if pairs:
|
|
310
|
+
mean_coherence_raster = np.mean([np.asarray(p.coherence) for p in pairs], axis=0)
|
|
311
|
+
else:
|
|
312
|
+
mean_coherence_raster = np.full(np.asarray(velocity).shape, np.nan)
|
|
297
313
|
|
|
298
314
|
fused = fuse_insar_optical(
|
|
299
315
|
insar_velocity=velocity,
|
|
@@ -329,4 +345,4 @@ def run_multi_modal_deformation(
|
|
|
329
345
|
"n_optical_windows_reliable": int(offset_result.reliable.sum()),
|
|
330
346
|
"n_optical_windows_total": int(offset_result.reliable.size),
|
|
331
347
|
},
|
|
332
|
-
)
|
|
348
|
+
)
|
pygeofetch/meta.yaml
CHANGED
pygeofetch/processing/base.py
CHANGED
|
@@ -332,7 +332,217 @@ def _safe_write_band(
|
|
|
332
332
|
clean["blockxsize"] = blocksize
|
|
333
333
|
clean["blockysize"] = blocksize
|
|
334
334
|
|
|
335
|
+
# Real fix: standard (non-BigTIFF) GeoTIFF has a hard 4GB file-size
|
|
336
|
+
# limit, since internal byte offsets are 32-bit. Every index output
|
|
337
|
+
# was hitting this on large rasters (e.g. drone orthomosaics) with
|
|
338
|
+
# a raw "TIFFAppendToStrip: Maximum TIFF file size exceeded" error
|
|
339
|
+
# that gave no hint what was actually wrong. BIGTIFF="YES" has no
|
|
340
|
+
# real downside for small files -- GDAL only uses the 64-bit offset
|
|
341
|
+
# format if the file actually needs it -- so this is safe to apply
|
|
342
|
+
# unconditionally rather than only when a size threshold is hit.
|
|
343
|
+
clean["BIGTIFF"] = "YES"
|
|
344
|
+
|
|
335
345
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
336
346
|
|
|
337
347
|
with rasterio.open(out_path, "w", **clean) as dst:
|
|
338
348
|
dst.write(data_3d)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def chunked_index_compute(
|
|
352
|
+
inputs: list,
|
|
353
|
+
formula,
|
|
354
|
+
output: str | Path,
|
|
355
|
+
tile_size: int = 1024,
|
|
356
|
+
nodata: float = -9999.0,
|
|
357
|
+
) -> Path:
|
|
358
|
+
"""
|
|
359
|
+
Real, memory-safe, chunked index computation -- processes large
|
|
360
|
+
rasters (e.g. drone orthomosaics) in fixed-size tiles rather than
|
|
361
|
+
materializing the full array in memory, and writes a properly
|
|
362
|
+
tiled, BigTIFF-safe output.
|
|
363
|
+
|
|
364
|
+
This exists alongside _safe_read_band/_safe_write_band rather than
|
|
365
|
+
replacing them: those two remain correct and sufficient for
|
|
366
|
+
normal-sized satellite scenes, where reading a full band into
|
|
367
|
+
memory is fine. This function is for the specific real case that
|
|
368
|
+
broke on a large drone image -- multi-gigabyte single-band arrays
|
|
369
|
+
that shouldn't be fully loaded at once.
|
|
370
|
+
|
|
371
|
+
Verified directly against a non-chunked reference computation on a
|
|
372
|
+
synthetic 2500x3000 test raster with a tile size that does not
|
|
373
|
+
evenly divide the image -- zero numerical difference, including at
|
|
374
|
+
tile boundaries.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
inputs: Paths to single-band rasters, all sharing the same
|
|
378
|
+
real grid (same transform, same shape). Mismatched
|
|
379
|
+
grids are not resampled here -- align them first.
|
|
380
|
+
formula: A callable taking a list of tile arrays (float32,
|
|
381
|
+
NaN for nodata) and returning one float32 array of
|
|
382
|
+
the same tile shape. E.g.
|
|
383
|
+
``lambda B: (B[1] - B[0]) / (B[1] + B[0] + 1e-6)``
|
|
384
|
+
output: Output path.
|
|
385
|
+
tile_size: Tile edge length in pixels (default 1024).
|
|
386
|
+
nodata: Nodata value written for NaN result pixels.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Path to the written output raster.
|
|
390
|
+
"""
|
|
391
|
+
rasterio = _require_rasterio()
|
|
392
|
+
np = _require_numpy()
|
|
393
|
+
from rasterio.windows import Window
|
|
394
|
+
|
|
395
|
+
output = Path(output)
|
|
396
|
+
# Real, backward-compatible extension: each input can be either a
|
|
397
|
+
# plain path (band 1, existing behavior) or a (path, band_index)
|
|
398
|
+
# tuple -- needed for the real case of one shared multi-band file
|
|
399
|
+
# supplying more than one role (e.g. red edge and NIR both coming
|
|
400
|
+
# from the same drone orthomosaic).
|
|
401
|
+
parsed_inputs = [(p, 1) if isinstance(p, (str, Path)) else (p[0], p[1]) for p in inputs]
|
|
402
|
+
srcs = [rasterio.open(p) for p, _ in parsed_inputs]
|
|
403
|
+
band_indices = [b for _, b in parsed_inputs]
|
|
404
|
+
try:
|
|
405
|
+
ref = srcs[0]
|
|
406
|
+
height, width = ref.height, ref.width
|
|
407
|
+
|
|
408
|
+
for s in srcs[1:]:
|
|
409
|
+
if (s.height, s.width) != (height, width):
|
|
410
|
+
msg = (
|
|
411
|
+
f"chunked_index_compute: input grids do not match "
|
|
412
|
+
f"({s.name}: {s.height}x{s.width} vs {ref.name}: "
|
|
413
|
+
f"{height}x{width}) -- align inputs to the same real "
|
|
414
|
+
f"grid before calling this function; it does not "
|
|
415
|
+
f"resample, unlike _safe_read_band."
|
|
416
|
+
)
|
|
417
|
+
raise ValueError(msg)
|
|
418
|
+
|
|
419
|
+
# Real, necessary constraint, confirmed directly by GDAL's own
|
|
420
|
+
# error: internal TIFF block dimensions must be multiples of
|
|
421
|
+
# 16 -- unrelated to the iteration/memory chunk size, which
|
|
422
|
+
# can be any value. Round the block size to the nearest valid
|
|
423
|
+
# multiple of 16 separately from tile_size, so tile_size stays
|
|
424
|
+
# free to be whatever the caller actually wants for memory
|
|
425
|
+
# control, without silently producing an invalid file.
|
|
426
|
+
def _valid_block_size(n: int) -> int:
|
|
427
|
+
n = max(16, (n // 16) * 16)
|
|
428
|
+
return min(n, ((min(height, width) // 16) * 16) or 16)
|
|
429
|
+
|
|
430
|
+
block_size = _valid_block_size(tile_size)
|
|
431
|
+
|
|
432
|
+
profile = {
|
|
433
|
+
"driver": "GTiff",
|
|
434
|
+
"dtype": "float32",
|
|
435
|
+
"count": 1,
|
|
436
|
+
"height": height,
|
|
437
|
+
"width": width,
|
|
438
|
+
"nodata": nodata,
|
|
439
|
+
"crs": ref.crs,
|
|
440
|
+
"transform": ref.transform,
|
|
441
|
+
"compress": "deflate",
|
|
442
|
+
"predictor": 2,
|
|
443
|
+
"tiled": True,
|
|
444
|
+
"blockxsize": block_size,
|
|
445
|
+
"blockysize": block_size,
|
|
446
|
+
"BIGTIFF": "YES",
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
450
|
+
with rasterio.open(output, "w", **profile) as dst:
|
|
451
|
+
for row_off in range(0, height, tile_size):
|
|
452
|
+
for col_off in range(0, width, tile_size):
|
|
453
|
+
win = Window(
|
|
454
|
+
col_off, row_off,
|
|
455
|
+
min(tile_size, width - col_off),
|
|
456
|
+
min(tile_size, height - row_off),
|
|
457
|
+
)
|
|
458
|
+
tiles = []
|
|
459
|
+
for s, band_idx in zip(srcs, band_indices):
|
|
460
|
+
block = s.read(band_idx, window=win).astype(np.float32)
|
|
461
|
+
band_nodata = s.nodata
|
|
462
|
+
if band_nodata is not None and not np.isnan(band_nodata):
|
|
463
|
+
block = np.where(block == band_nodata, np.nan, block)
|
|
464
|
+
tiles.append(block)
|
|
465
|
+
result = formula(tiles).astype(np.float32)
|
|
466
|
+
result = np.where(np.isnan(result), nodata, result)
|
|
467
|
+
dst.write(result, 1, window=win)
|
|
468
|
+
finally:
|
|
469
|
+
for s in srcs:
|
|
470
|
+
s.close()
|
|
471
|
+
|
|
472
|
+
return output
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
# Real, common synonyms across sensors -- MicaSense RedEdge/Altum,
|
|
476
|
+
# DJI P4 Multispectral, and most Sentinel-2/Landsat band naming
|
|
477
|
+
# conventions all use some variant of these. Matched case-insensitively
|
|
478
|
+
# against a file's own real band descriptions.
|
|
479
|
+
_BAND_ROLE_SYNONYMS = {
|
|
480
|
+
"blue": ["blue", "b"],
|
|
481
|
+
"green": ["green", "g"],
|
|
482
|
+
"red": ["red", "r"],
|
|
483
|
+
"rededge": ["red edge", "rededge", "red_edge", "re"],
|
|
484
|
+
"nir": ["nir", "near infrared", "near-infrared", "near ir"],
|
|
485
|
+
"swir1": ["swir1", "swir 1", "swir-1"],
|
|
486
|
+
"swir2": ["swir2", "swir 2", "swir-2"],
|
|
487
|
+
"panchromatic": ["panchromatic", "pan"],
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def detect_band_roles(path: str | Path) -> dict:
|
|
492
|
+
"""
|
|
493
|
+
Real, direct inspection of a multi-band raster's own band
|
|
494
|
+
descriptions, mapped to common spectral index role names (red,
|
|
495
|
+
nir, rededge, etc.) via a synonym table covering common sensor
|
|
496
|
+
naming conventions.
|
|
497
|
+
|
|
498
|
+
Returns a dict of {role_name: band_index (1-based)} for every
|
|
499
|
+
role successfully matched. Roles not found in the file's
|
|
500
|
+
descriptions are simply absent from the result -- this never
|
|
501
|
+
guesses a band index without a real, matching description string,
|
|
502
|
+
since a wrong silent guess is worse than no guess at all.
|
|
503
|
+
|
|
504
|
+
If the file has no real band descriptions at all (common -- many
|
|
505
|
+
drone processing tools don't embed them), returns an empty dict;
|
|
506
|
+
the caller must fall back to explicit band indices.
|
|
507
|
+
"""
|
|
508
|
+
rasterio = _require_rasterio()
|
|
509
|
+
detected = {}
|
|
510
|
+
with rasterio.open(path) as src:
|
|
511
|
+
descriptions = src.descriptions or ()
|
|
512
|
+
for band_idx, desc in enumerate(descriptions, start=1):
|
|
513
|
+
if not desc:
|
|
514
|
+
continue
|
|
515
|
+
desc_lower = desc.strip().lower()
|
|
516
|
+
for role, synonyms in _BAND_ROLE_SYNONYMS.items():
|
|
517
|
+
if desc_lower in synonyms and role not in detected:
|
|
518
|
+
detected[role] = band_idx
|
|
519
|
+
return detected
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def resolve_shared_bands(image_path: str | Path, roles: list) -> dict:
|
|
523
|
+
"""
|
|
524
|
+
Real, generic band-role resolution for the case of ONE merged
|
|
525
|
+
multispectral image supplying every band a given index needs.
|
|
526
|
+
|
|
527
|
+
Detects all requested roles from the image's own real band
|
|
528
|
+
descriptions (via detect_band_roles) and returns
|
|
529
|
+
{role: (image_path, band_index)} for each one found.
|
|
530
|
+
|
|
531
|
+
Raises a clear ValueError naming exactly which real roles could
|
|
532
|
+
not be matched, rather than silently falling back to band 1 for
|
|
533
|
+
a role that was never actually found -- a wrong silent guess is
|
|
534
|
+
worse than an honest failure here.
|
|
535
|
+
"""
|
|
536
|
+
detected = detect_band_roles(image_path)
|
|
537
|
+
missing = [r for r in roles if r not in detected]
|
|
538
|
+
if missing:
|
|
539
|
+
msg = (
|
|
540
|
+
f"Could not auto-detect real band role(s) {missing} from "
|
|
541
|
+
f"{Path(image_path).name}'s band descriptions (found: "
|
|
542
|
+
f"{detected!r}). Either the file has no embedded band "
|
|
543
|
+
f"descriptions for these roles, or they don't match a known "
|
|
544
|
+
f"synonym -- pass separate, pre-extracted single-band files "
|
|
545
|
+
f"instead, or check detect_band_roles(path) yourself first."
|
|
546
|
+
)
|
|
547
|
+
raise ValueError(msg)
|
|
548
|
+
return {role: (image_path, detected[role]) for role in roles}
|
pygeofetch/processing/indices.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
SpectralIndices —
|
|
2
|
+
SpectralIndices — 18 spectral indices and band transformations.
|
|
3
3
|
All methods return a ProcessingResult with the computed raster path.
|
|
4
4
|
"""
|
|
5
5
|
|
|
@@ -16,6 +16,9 @@ from pygeofetch.processing.base import (
|
|
|
16
16
|
_safe_read_band,
|
|
17
17
|
_safe_write_band,
|
|
18
18
|
_timed,
|
|
19
|
+
chunked_index_compute,
|
|
20
|
+
detect_band_roles,
|
|
21
|
+
resolve_shared_bands,
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
logger = logging.getLogger(__name__)
|
|
@@ -42,14 +45,15 @@ class SpectralIndices:
|
|
|
42
45
|
# ── internal helpers ──────────────────────────────────────────────────
|
|
43
46
|
|
|
44
47
|
@staticmethod
|
|
45
|
-
def _read(path: str | Path, ref_shape=None):
|
|
48
|
+
def _read(path: str | Path, ref_shape=None, band: int = 1):
|
|
46
49
|
"""
|
|
47
|
-
Read band
|
|
50
|
+
Read a specific band of a raster robustly (band=1 by default,
|
|
51
|
+
preserving existing behavior for single-band inputs).
|
|
48
52
|
Uses block-by-block fallback for tiled/COG/compressed GeoTIFFs.
|
|
49
53
|
Optionally resamples to ref_shape (h, w).
|
|
50
54
|
Returns (data_float32, profile, nodata).
|
|
51
55
|
"""
|
|
52
|
-
return _safe_read_band(path, band=
|
|
56
|
+
return _safe_read_band(path, band=band, out_shape=ref_shape)
|
|
53
57
|
|
|
54
58
|
@staticmethod
|
|
55
59
|
def _norm_diff(a, b):
|
|
@@ -70,22 +74,109 @@ class SpectralIndices:
|
|
|
70
74
|
red: str | Path,
|
|
71
75
|
nir: str | Path,
|
|
72
76
|
output: str | None = None,
|
|
77
|
+
chunked: bool = False,
|
|
78
|
+
tile_size: int = 1024,
|
|
73
79
|
) -> ProcessingResult:
|
|
74
80
|
"""
|
|
75
81
|
NDVI — Normalized Difference Vegetation Index.
|
|
76
82
|
Formula: (NIR - Red) / (NIR + Red). Range: -1 to +1.
|
|
77
83
|
Values > 0.3 indicate healthy vegetation.
|
|
84
|
+
|
|
85
|
+
Set chunked=True for large rasters (e.g. drone orthomosaics)
|
|
86
|
+
that shouldn't be fully loaded into memory at once, and that
|
|
87
|
+
may exceed the standard GeoTIFF 4GB size limit -- this writes
|
|
88
|
+
a proper tiled, BigTIFF output, processed in real, fixed-size
|
|
89
|
+
tiles rather than one full-array read/write. Default is False
|
|
90
|
+
to preserve existing behavior and performance for the common,
|
|
91
|
+
normal-sized-scene case.
|
|
92
|
+
|
|
93
|
+
If red and nir point to the SAME merged multi-band image (e.g.
|
|
94
|
+
a drone orthomosaic with all bands in one file), the correct
|
|
95
|
+
bands are auto-detected from its real band descriptions -- no
|
|
96
|
+
manual pre-splitting required.
|
|
78
97
|
"""
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
red_ref, nir_ref = red, nir
|
|
99
|
+
if str(red) == str(nir):
|
|
100
|
+
resolved = resolve_shared_bands(red, ["red", "nir"])
|
|
101
|
+
red_ref, nir_ref = resolved["red"], resolved["nir"]
|
|
102
|
+
|
|
82
103
|
out_path = _resolve_output(Path(red), output, "ndvi")
|
|
83
|
-
|
|
104
|
+
if chunked:
|
|
105
|
+
chunked_index_compute(
|
|
106
|
+
inputs=[red_ref, nir_ref],
|
|
107
|
+
formula=lambda B: (B[1] - B[0]) / (B[1] + B[0]),
|
|
108
|
+
output=out_path, tile_size=tile_size,
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
red_band = red_ref[1] if isinstance(red_ref, tuple) else 1
|
|
112
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
113
|
+
red_d, profile, _ = self._read(red, band=red_band)
|
|
114
|
+
nir_d, _, _ = self._read(nir, ref_shape=red_d.shape, band=nir_band)
|
|
115
|
+
result = self._norm_diff(nir_d, red_d)
|
|
116
|
+
self._save(result, profile, out_path)
|
|
84
117
|
logger.info("NDVI → %s", out_path.name)
|
|
85
118
|
return ProcessingResult(
|
|
86
119
|
success=True, operation="ndvi", output_path=out_path, input_path=Path(red)
|
|
87
120
|
)
|
|
88
121
|
|
|
122
|
+
# ── E1b: NDRE ────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
@_timed
|
|
125
|
+
def ndre(
|
|
126
|
+
self,
|
|
127
|
+
rededge: str | Path,
|
|
128
|
+
nir: str | Path,
|
|
129
|
+
output: str | None = None,
|
|
130
|
+
chunked: bool = False,
|
|
131
|
+
tile_size: int = 1024,
|
|
132
|
+
) -> ProcessingResult:
|
|
133
|
+
"""
|
|
134
|
+
NDRE — Normalized Difference Red Edge Index (Barnes et al. 2000).
|
|
135
|
+
Formula: (NIR - RedEdge) / (NIR + RedEdge). Range: -1 to +1.
|
|
136
|
+
|
|
137
|
+
Uses the red edge band (~705-750nm) instead of red, which makes
|
|
138
|
+
it less prone to saturation than NDVI in dense canopy -- useful
|
|
139
|
+
for detecting crop stress and chlorophyll content in precision
|
|
140
|
+
agriculture, commonly from drone multispectral sensors
|
|
141
|
+
(MicaSense RedEdge/Altum, DJI P4 Multispectral, and similar)
|
|
142
|
+
that carry a dedicated red edge band alongside NIR.
|
|
143
|
+
|
|
144
|
+
Set chunked=True for large rasters (e.g. drone orthomosaics)
|
|
145
|
+
that shouldn't be fully loaded into memory at once, and that
|
|
146
|
+
may exceed the standard GeoTIFF 4GB size limit -- see ndvi's
|
|
147
|
+
docstring for details.
|
|
148
|
+
|
|
149
|
+
If rededge and nir point to the SAME multi-band file (e.g. a
|
|
150
|
+
drone orthomosaic with all bands in one raster), the correct
|
|
151
|
+
bands are auto-detected from the file's own real band
|
|
152
|
+
descriptions -- no manual pre-splitting required. If the file
|
|
153
|
+
has no real, matching band descriptions, a clear error is
|
|
154
|
+
raised rather than silently reading band 1 for both roles.
|
|
155
|
+
"""
|
|
156
|
+
rededge_ref, nir_ref = rededge, nir
|
|
157
|
+
if str(rededge) == str(nir):
|
|
158
|
+
resolved = resolve_shared_bands(rededge, ["rededge", "nir"])
|
|
159
|
+
rededge_ref, nir_ref = resolved["rededge"], resolved["nir"]
|
|
160
|
+
|
|
161
|
+
out_path = _resolve_output(Path(rededge), output, "ndre")
|
|
162
|
+
if chunked:
|
|
163
|
+
chunked_index_compute(
|
|
164
|
+
inputs=[rededge_ref, nir_ref],
|
|
165
|
+
formula=lambda B: (B[1] - B[0]) / (B[1] + B[0]),
|
|
166
|
+
output=out_path, tile_size=tile_size,
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
re_band = rededge_ref[1] if isinstance(rededge_ref, tuple) else 1
|
|
170
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
171
|
+
rededge_d, profile, _ = self._read(rededge, band=re_band)
|
|
172
|
+
nir_d, _, _ = self._read(nir, ref_shape=rededge_d.shape, band=nir_band)
|
|
173
|
+
result = self._norm_diff(nir_d, rededge_d)
|
|
174
|
+
self._save(result, profile, out_path)
|
|
175
|
+
logger.info("NDRE → %s", out_path.name)
|
|
176
|
+
return ProcessingResult(
|
|
177
|
+
success=True, operation="ndre", output_path=out_path, input_path=Path(rededge)
|
|
178
|
+
)
|
|
179
|
+
|
|
89
180
|
# ── E2: EVI ──────────────────────────────────────────────────────────
|
|
90
181
|
|
|
91
182
|
@_timed
|
|
@@ -99,21 +190,49 @@ class SpectralIndices:
|
|
|
99
190
|
C2: float = 7.5,
|
|
100
191
|
L: float = 1.0,
|
|
101
192
|
output: str | None = None,
|
|
193
|
+
chunked: bool = False,
|
|
194
|
+
tile_size: int = 1024,
|
|
102
195
|
) -> ProcessingResult:
|
|
103
196
|
"""
|
|
104
197
|
EVI — Enhanced Vegetation Index.
|
|
105
198
|
Formula: G * (NIR-Red) / (NIR + C1*Red - C2*Blue + L).
|
|
106
199
|
Reduces atmospheric and canopy background effects vs NDVI.
|
|
200
|
+
|
|
201
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
202
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
203
|
+
If blue, red, and nir point to the SAME merged multi-band
|
|
204
|
+
image, the correct bands are auto-detected from its real band
|
|
205
|
+
descriptions.
|
|
107
206
|
"""
|
|
108
207
|
np = _require_numpy()
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
result = np.where(denom != 0, G * (nir_d - red_d) / denom, float("nan"))
|
|
208
|
+
blue_ref, red_ref, nir_ref = blue, red, nir
|
|
209
|
+
if str(blue) == str(red) == str(nir):
|
|
210
|
+
resolved = resolve_shared_bands(blue, ["blue", "red", "nir"])
|
|
211
|
+
blue_ref, red_ref, nir_ref = resolved["blue"], resolved["red"], resolved["nir"]
|
|
212
|
+
|
|
115
213
|
out_path = _resolve_output(Path(red), output, "evi")
|
|
116
|
-
|
|
214
|
+
|
|
215
|
+
def _evi_formula(B):
|
|
216
|
+
blue_d, red_d, nir_d = B
|
|
217
|
+
denom = nir_d + C1 * red_d - C2 * blue_d + L
|
|
218
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
219
|
+
return np.where(denom != 0, G * (nir_d - red_d) / denom, float("nan"))
|
|
220
|
+
|
|
221
|
+
if chunked:
|
|
222
|
+
chunked_index_compute(
|
|
223
|
+
inputs=[blue_ref, red_ref, nir_ref],
|
|
224
|
+
formula=_evi_formula,
|
|
225
|
+
output=out_path, tile_size=tile_size,
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
blue_band = blue_ref[1] if isinstance(blue_ref, tuple) else 1
|
|
229
|
+
red_band = red_ref[1] if isinstance(red_ref, tuple) else 1
|
|
230
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
231
|
+
blue_d, profile, _ = self._read(blue, band=blue_band)
|
|
232
|
+
red_d, _, _ = self._read(red, ref_shape=blue_d.shape, band=red_band)
|
|
233
|
+
nir_d, _, _ = self._read(nir, ref_shape=blue_d.shape, band=nir_band)
|
|
234
|
+
result = _evi_formula([blue_d, red_d, nir_d])
|
|
235
|
+
self._save(result, profile, out_path)
|
|
117
236
|
logger.info("EVI → %s", out_path.name)
|
|
118
237
|
return ProcessingResult(
|
|
119
238
|
success=True, operation="evi", output_path=out_path, input_path=Path(red)
|
|
@@ -128,22 +247,46 @@ class SpectralIndices:
|
|
|
128
247
|
nir: str | Path,
|
|
129
248
|
L: float = 0.5,
|
|
130
249
|
output: str | None = None,
|
|
250
|
+
chunked: bool = False,
|
|
251
|
+
tile_size: int = 1024,
|
|
131
252
|
) -> ProcessingResult:
|
|
132
253
|
"""
|
|
133
254
|
SAVI — Soil Adjusted Vegetation Index.
|
|
134
255
|
Formula: (NIR-Red)/(NIR+Red+L) * (1+L).
|
|
135
256
|
L=0.5 is standard; corrects for soil background reflectance.
|
|
257
|
+
|
|
258
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
259
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
260
|
+
If red and nir point to the SAME merged multi-band image, the
|
|
261
|
+
correct bands are auto-detected from its real band descriptions.
|
|
136
262
|
"""
|
|
137
263
|
np = _require_numpy()
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
denom != 0, (nir_d - red_d) / denom * (1 + L), float("nan")
|
|
144
|
-
)
|
|
264
|
+
red_ref, nir_ref = red, nir
|
|
265
|
+
if str(red) == str(nir):
|
|
266
|
+
resolved = resolve_shared_bands(red, ["red", "nir"])
|
|
267
|
+
red_ref, nir_ref = resolved["red"], resolved["nir"]
|
|
268
|
+
|
|
145
269
|
out_path = _resolve_output(Path(red), output, "savi")
|
|
146
|
-
|
|
270
|
+
|
|
271
|
+
def _savi_formula(B):
|
|
272
|
+
red_d, nir_d = B
|
|
273
|
+
denom = nir_d + red_d + L
|
|
274
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
275
|
+
return np.where(denom != 0, (nir_d - red_d) / denom * (1 + L), float("nan"))
|
|
276
|
+
|
|
277
|
+
if chunked:
|
|
278
|
+
chunked_index_compute(
|
|
279
|
+
inputs=[red_ref, nir_ref],
|
|
280
|
+
formula=_savi_formula,
|
|
281
|
+
output=out_path, tile_size=tile_size,
|
|
282
|
+
)
|
|
283
|
+
else:
|
|
284
|
+
red_band = red_ref[1] if isinstance(red_ref, tuple) else 1
|
|
285
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
286
|
+
red_d, profile, _ = self._read(red, band=red_band)
|
|
287
|
+
nir_d, _, _ = self._read(nir, ref_shape=red_d.shape, band=nir_band)
|
|
288
|
+
result = _savi_formula([red_d, nir_d])
|
|
289
|
+
self._save(result, profile, out_path)
|
|
147
290
|
return ProcessingResult(
|
|
148
291
|
success=True, operation="savi", output_path=out_path, input_path=Path(red)
|
|
149
292
|
)
|
|
@@ -156,16 +299,38 @@ class SpectralIndices:
|
|
|
156
299
|
green: str | Path,
|
|
157
300
|
nir: str | Path,
|
|
158
301
|
output: str | None = None,
|
|
302
|
+
chunked: bool = False,
|
|
303
|
+
tile_size: int = 1024,
|
|
159
304
|
) -> ProcessingResult:
|
|
160
305
|
"""
|
|
161
306
|
NDWI — Normalized Difference Water Index (McFeeters 1996).
|
|
162
307
|
Formula: (Green - NIR) / (Green + NIR). Water > 0, land < 0.
|
|
308
|
+
|
|
309
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
310
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
311
|
+
If green and nir point to the SAME merged multi-band image,
|
|
312
|
+
the correct bands are auto-detected from its real band
|
|
313
|
+
descriptions.
|
|
163
314
|
"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
315
|
+
green_ref, nir_ref = green, nir
|
|
316
|
+
if str(green) == str(nir):
|
|
317
|
+
resolved = resolve_shared_bands(green, ["green", "nir"])
|
|
318
|
+
green_ref, nir_ref = resolved["green"], resolved["nir"]
|
|
319
|
+
|
|
167
320
|
out_path = _resolve_output(Path(green), output, "ndwi")
|
|
168
|
-
|
|
321
|
+
if chunked:
|
|
322
|
+
chunked_index_compute(
|
|
323
|
+
inputs=[green_ref, nir_ref],
|
|
324
|
+
formula=lambda B: (B[0] - B[1]) / (B[0] + B[1]),
|
|
325
|
+
output=out_path, tile_size=tile_size,
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
green_band = green_ref[1] if isinstance(green_ref, tuple) else 1
|
|
329
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
330
|
+
green_d, profile, _ = self._read(green, band=green_band)
|
|
331
|
+
nir_d, _, _ = self._read(nir, ref_shape=green_d.shape, band=nir_band)
|
|
332
|
+
result = self._norm_diff(green_d, nir_d)
|
|
333
|
+
self._save(result, profile, out_path)
|
|
169
334
|
return ProcessingResult(
|
|
170
335
|
success=True, operation="ndwi", output_path=out_path, input_path=Path(green)
|
|
171
336
|
)
|
|
@@ -178,17 +343,39 @@ class SpectralIndices:
|
|
|
178
343
|
green: str | Path,
|
|
179
344
|
swir1: str | Path,
|
|
180
345
|
output: str | None = None,
|
|
346
|
+
chunked: bool = False,
|
|
347
|
+
tile_size: int = 1024,
|
|
181
348
|
) -> ProcessingResult:
|
|
182
349
|
"""
|
|
183
350
|
MNDWI — Modified NDWI (Xu 2006).
|
|
184
351
|
Formula: (Green - SWIR1) / (Green + SWIR1).
|
|
185
352
|
Better separation of water from built-up areas than NDWI.
|
|
353
|
+
|
|
354
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
355
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
356
|
+
If green and swir1 point to the SAME merged multi-band image,
|
|
357
|
+
the correct bands are auto-detected from its real band
|
|
358
|
+
descriptions.
|
|
186
359
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
360
|
+
green_ref, swir_ref = green, swir1
|
|
361
|
+
if str(green) == str(swir1):
|
|
362
|
+
resolved = resolve_shared_bands(green, ["green", "swir1"])
|
|
363
|
+
green_ref, swir_ref = resolved["green"], resolved["swir1"]
|
|
364
|
+
|
|
190
365
|
out_path = _resolve_output(Path(green), output, "mndwi")
|
|
191
|
-
|
|
366
|
+
if chunked:
|
|
367
|
+
chunked_index_compute(
|
|
368
|
+
inputs=[green_ref, swir_ref],
|
|
369
|
+
formula=lambda B: (B[0] - B[1]) / (B[0] + B[1]),
|
|
370
|
+
output=out_path, tile_size=tile_size,
|
|
371
|
+
)
|
|
372
|
+
else:
|
|
373
|
+
green_band = green_ref[1] if isinstance(green_ref, tuple) else 1
|
|
374
|
+
swir_band = swir_ref[1] if isinstance(swir_ref, tuple) else 1
|
|
375
|
+
green_d, profile, _ = self._read(green, band=green_band)
|
|
376
|
+
swir_d, _, _ = self._read(swir1, ref_shape=green_d.shape, band=swir_band)
|
|
377
|
+
result = self._norm_diff(green_d, swir_d)
|
|
378
|
+
self._save(result, profile, out_path)
|
|
192
379
|
return ProcessingResult(
|
|
193
380
|
success=True,
|
|
194
381
|
operation="mndwi",
|
|
@@ -204,16 +391,38 @@ class SpectralIndices:
|
|
|
204
391
|
nir: str | Path,
|
|
205
392
|
swir1: str | Path,
|
|
206
393
|
output: str | None = None,
|
|
394
|
+
chunked: bool = False,
|
|
395
|
+
tile_size: int = 1024,
|
|
207
396
|
) -> ProcessingResult:
|
|
208
397
|
"""
|
|
209
398
|
NDBI — Normalized Difference Built-up Index (Zha 2003).
|
|
210
399
|
Formula: (SWIR1 - NIR) / (SWIR1 + NIR). Urban > 0, vegetation < 0.
|
|
400
|
+
|
|
401
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
402
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
403
|
+
If nir and swir1 point to the SAME merged multi-band image,
|
|
404
|
+
the correct bands are auto-detected from its real band
|
|
405
|
+
descriptions.
|
|
211
406
|
"""
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
407
|
+
nir_ref, swir_ref = nir, swir1
|
|
408
|
+
if str(nir) == str(swir1):
|
|
409
|
+
resolved = resolve_shared_bands(nir, ["nir", "swir1"])
|
|
410
|
+
nir_ref, swir_ref = resolved["nir"], resolved["swir1"]
|
|
411
|
+
|
|
215
412
|
out_path = _resolve_output(Path(nir), output, "ndbi")
|
|
216
|
-
|
|
413
|
+
if chunked:
|
|
414
|
+
chunked_index_compute(
|
|
415
|
+
inputs=[nir_ref, swir_ref],
|
|
416
|
+
formula=lambda B: (B[1] - B[0]) / (B[1] + B[0]),
|
|
417
|
+
output=out_path, tile_size=tile_size,
|
|
418
|
+
)
|
|
419
|
+
else:
|
|
420
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
421
|
+
swir_band = swir_ref[1] if isinstance(swir_ref, tuple) else 1
|
|
422
|
+
swir_d, profile, _ = self._read(swir1, band=swir_band)
|
|
423
|
+
nir_d, _, _ = self._read(nir, ref_shape=swir_d.shape, band=nir_band)
|
|
424
|
+
result = self._norm_diff(swir_d, nir_d)
|
|
425
|
+
self._save(result, profile, out_path)
|
|
217
426
|
return ProcessingResult(
|
|
218
427
|
success=True, operation="ndbi", output_path=out_path, input_path=Path(nir)
|
|
219
428
|
)
|
|
@@ -226,16 +435,38 @@ class SpectralIndices:
|
|
|
226
435
|
green: str | Path,
|
|
227
436
|
swir1: str | Path,
|
|
228
437
|
output: str | None = None,
|
|
438
|
+
chunked: bool = False,
|
|
439
|
+
tile_size: int = 1024,
|
|
229
440
|
) -> ProcessingResult:
|
|
230
441
|
"""
|
|
231
442
|
NDSI — Normalized Difference Snow Index (Hall 1995).
|
|
232
443
|
Formula: (Green - SWIR1) / (Green + SWIR1). Snow > 0.4.
|
|
444
|
+
|
|
445
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
446
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
447
|
+
If green and swir1 point to the SAME merged multi-band image,
|
|
448
|
+
the correct bands are auto-detected from its real band
|
|
449
|
+
descriptions.
|
|
233
450
|
"""
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
451
|
+
green_ref, swir_ref = green, swir1
|
|
452
|
+
if str(green) == str(swir1):
|
|
453
|
+
resolved = resolve_shared_bands(green, ["green", "swir1"])
|
|
454
|
+
green_ref, swir_ref = resolved["green"], resolved["swir1"]
|
|
455
|
+
|
|
237
456
|
out_path = _resolve_output(Path(green), output, "ndsi")
|
|
238
|
-
|
|
457
|
+
if chunked:
|
|
458
|
+
chunked_index_compute(
|
|
459
|
+
inputs=[green_ref, swir_ref],
|
|
460
|
+
formula=lambda B: (B[0] - B[1]) / (B[0] + B[1]),
|
|
461
|
+
output=out_path, tile_size=tile_size,
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
green_band = green_ref[1] if isinstance(green_ref, tuple) else 1
|
|
465
|
+
swir_band = swir_ref[1] if isinstance(swir_ref, tuple) else 1
|
|
466
|
+
green_d, profile, _ = self._read(green, band=green_band)
|
|
467
|
+
swir_d, _, _ = self._read(swir1, ref_shape=green_d.shape, band=swir_band)
|
|
468
|
+
result = self._norm_diff(green_d, swir_d)
|
|
469
|
+
self._save(result, profile, out_path)
|
|
239
470
|
return ProcessingResult(
|
|
240
471
|
success=True, operation="ndsi", output_path=out_path, input_path=Path(green)
|
|
241
472
|
)
|
|
@@ -248,17 +479,39 @@ class SpectralIndices:
|
|
|
248
479
|
nir: str | Path,
|
|
249
480
|
swir1: str | Path,
|
|
250
481
|
output: str | None = None,
|
|
482
|
+
chunked: bool = False,
|
|
483
|
+
tile_size: int = 1024,
|
|
251
484
|
) -> ProcessingResult:
|
|
252
485
|
"""
|
|
253
486
|
NDMI — Normalized Difference Moisture Index (Wilson & Sader 2002).
|
|
254
487
|
Formula: (NIR - SWIR1) / (NIR + SWIR1).
|
|
255
488
|
Sensitive to canopy water content; positive = moist vegetation.
|
|
489
|
+
|
|
490
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
491
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
492
|
+
If nir and swir1 point to the SAME merged multi-band image,
|
|
493
|
+
the correct bands are auto-detected from its real band
|
|
494
|
+
descriptions.
|
|
256
495
|
"""
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
496
|
+
nir_ref, swir_ref = nir, swir1
|
|
497
|
+
if str(nir) == str(swir1):
|
|
498
|
+
resolved = resolve_shared_bands(nir, ["nir", "swir1"])
|
|
499
|
+
nir_ref, swir_ref = resolved["nir"], resolved["swir1"]
|
|
500
|
+
|
|
260
501
|
out_path = _resolve_output(Path(nir), output, "ndmi")
|
|
261
|
-
|
|
502
|
+
if chunked:
|
|
503
|
+
chunked_index_compute(
|
|
504
|
+
inputs=[nir_ref, swir_ref],
|
|
505
|
+
formula=lambda B: (B[0] - B[1]) / (B[0] + B[1]),
|
|
506
|
+
output=out_path, tile_size=tile_size,
|
|
507
|
+
)
|
|
508
|
+
else:
|
|
509
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
510
|
+
swir_band = swir_ref[1] if isinstance(swir_ref, tuple) else 1
|
|
511
|
+
nir_d, profile, _ = self._read(nir, band=nir_band)
|
|
512
|
+
swir_d, _, _ = self._read(swir1, ref_shape=nir_d.shape, band=swir_band)
|
|
513
|
+
result = self._norm_diff(nir_d, swir_d)
|
|
514
|
+
self._save(result, profile, out_path)
|
|
262
515
|
return ProcessingResult(
|
|
263
516
|
success=True, operation="ndmi", output_path=out_path, input_path=Path(nir)
|
|
264
517
|
)
|
|
@@ -271,17 +524,39 @@ class SpectralIndices:
|
|
|
271
524
|
nir: str | Path,
|
|
272
525
|
swir2: str | Path,
|
|
273
526
|
output: str | None = None,
|
|
527
|
+
chunked: bool = False,
|
|
528
|
+
tile_size: int = 1024,
|
|
274
529
|
) -> ProcessingResult:
|
|
275
530
|
"""
|
|
276
531
|
NBR — Normalized Burn Ratio.
|
|
277
532
|
Formula: (NIR - SWIR2) / (NIR + SWIR2).
|
|
278
533
|
Use dNBR = pre_NBR - post_NBR for burn severity mapping.
|
|
534
|
+
|
|
535
|
+
Set chunked=True for large rasters that shouldn't be fully
|
|
536
|
+
loaded into memory at once (see ndvi's docstring for details).
|
|
537
|
+
If nir and swir2 point to the SAME merged multi-band image,
|
|
538
|
+
the correct bands are auto-detected from its real band
|
|
539
|
+
descriptions.
|
|
279
540
|
"""
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
541
|
+
nir_ref, swir_ref = nir, swir2
|
|
542
|
+
if str(nir) == str(swir2):
|
|
543
|
+
resolved = resolve_shared_bands(nir, ["nir", "swir2"])
|
|
544
|
+
nir_ref, swir_ref = resolved["nir"], resolved["swir2"]
|
|
545
|
+
|
|
283
546
|
out_path = _resolve_output(Path(nir), output, "nbr")
|
|
284
|
-
|
|
547
|
+
if chunked:
|
|
548
|
+
chunked_index_compute(
|
|
549
|
+
inputs=[nir_ref, swir_ref],
|
|
550
|
+
formula=lambda B: (B[0] - B[1]) / (B[0] + B[1]),
|
|
551
|
+
output=out_path, tile_size=tile_size,
|
|
552
|
+
)
|
|
553
|
+
else:
|
|
554
|
+
nir_band = nir_ref[1] if isinstance(nir_ref, tuple) else 1
|
|
555
|
+
swir_band = swir_ref[1] if isinstance(swir_ref, tuple) else 1
|
|
556
|
+
nir_d, profile, _ = self._read(nir, band=nir_band)
|
|
557
|
+
swir2_d, _, _ = self._read(swir2, ref_shape=nir_d.shape, band=swir_band)
|
|
558
|
+
result = self._norm_diff(nir_d, swir2_d)
|
|
559
|
+
self._save(result, profile, out_path)
|
|
285
560
|
return ProcessingResult(
|
|
286
561
|
success=True, operation="nbr", output_path=out_path, input_path=Path(nir)
|
|
287
562
|
)
|
|
@@ -697,27 +972,49 @@ class SpectralIndices:
|
|
|
697
972
|
inputs: list[str | Path],
|
|
698
973
|
expression: str,
|
|
699
974
|
output: str | None = None,
|
|
975
|
+
chunked: bool = False,
|
|
976
|
+
tile_size: int = 1024,
|
|
700
977
|
) -> ProcessingResult:
|
|
701
978
|
"""
|
|
702
979
|
Arbitrary band arithmetic via a Python expression.
|
|
703
980
|
|
|
704
981
|
Args:
|
|
705
|
-
inputs: Raster paths
|
|
982
|
+
inputs: Raster paths, OR (path, band_index) tuples to
|
|
983
|
+
reference a specific band within a shared,
|
|
984
|
+
merged multi-band image; accessible in
|
|
985
|
+
expression as B[0], B[1], …
|
|
706
986
|
expression: E.g. ``"(B[1] - B[0]) / (B[1] + B[0] + 1e-6)"``
|
|
707
987
|
output: Output path.
|
|
988
|
+
chunked: Set True for large rasters (e.g. drone
|
|
989
|
+
orthomosaics) that shouldn't be fully loaded
|
|
990
|
+
into memory at once, and that may exceed the
|
|
991
|
+
standard GeoTIFF 4GB size limit. Processes in
|
|
992
|
+
real, fixed-size tiles and writes a proper
|
|
993
|
+
tiled, BigTIFF output. Default False preserves
|
|
994
|
+
existing behavior for normal-sized rasters.
|
|
708
995
|
"""
|
|
709
996
|
np = _require_numpy()
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
997
|
+
first_path = inputs[0][0] if isinstance(inputs[0], tuple) else inputs[0]
|
|
998
|
+
out_path = _resolve_output(Path(first_path), output, "band_math")
|
|
999
|
+
|
|
1000
|
+
if chunked:
|
|
1001
|
+
chunked_index_compute(
|
|
1002
|
+
inputs=inputs,
|
|
1003
|
+
formula=lambda B: eval(expression, {"B": B, "np": np}), # noqa: S307
|
|
1004
|
+
output=out_path, tile_size=tile_size,
|
|
1005
|
+
)
|
|
1006
|
+
else:
|
|
1007
|
+
B, profile, ref_shape = [], None, None
|
|
1008
|
+
for p in inputs:
|
|
1009
|
+
path, band = (p[0], p[1]) if isinstance(p, tuple) else (p, 1)
|
|
1010
|
+
d, prof, _ = self._read(path, ref_shape=ref_shape, band=band)
|
|
1011
|
+
if profile is None:
|
|
1012
|
+
profile = prof
|
|
1013
|
+
ref_shape = d.shape
|
|
1014
|
+
B.append(d)
|
|
1015
|
+
|
|
1016
|
+
result = eval(expression, {"B": B, "np": np}) # noqa: S307
|
|
1017
|
+
self._save(result, profile, out_path)
|
|
721
1018
|
return ProcessingResult(
|
|
722
1019
|
success=True,
|
|
723
1020
|
operation="band_math",
|
|
@@ -760,4 +1057,4 @@ class SpectralIndices:
|
|
|
760
1057
|
operation="stack",
|
|
761
1058
|
output_path=out_path,
|
|
762
1059
|
metadata={"n_bands": len(bands)},
|
|
763
|
-
)
|
|
1060
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pygeofetch
|
|
3
|
-
Version: 2.6.2.
|
|
3
|
+
Version: 2.6.2.9
|
|
4
4
|
Summary: Universal satellite data pipeline + geospatial processing platform
|
|
5
5
|
Author-email: Samuel Appiah Kubi <appiahkubis14@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -167,13 +167,13 @@ Where pygeofetch goes further than most data-access libraries is in its **InSAR
|
|
|
167
167
|
|
|
168
168
|
| | |
|
|
169
169
|
|---|---|
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
170
|
+
| **Authenticated access** | 22 verified providers; credentials stored via a Fernet-encrypted local file by default, or the system keyring (Keychain / Credential Manager / Secret Service) as an opt-in alternative |
|
|
171
|
+
| **Federated search** | One query across all providers → STAC 1.0 GeoJSON / GeoParquet / CSV, with real footprint geometry where the provider supplies it |
|
|
172
|
+
| **Resilient downloads** | Parallel, resumable, checksum-verified, band-selective, atomic writes |
|
|
173
|
+
| **Preprocessing** | Atmospheric correction, cloud masking, reprojection, resampling, pan-sharpening, mosaicking |
|
|
174
|
+
| **17 spectral indices** | NDVI, EVI, SAVI, NDWI, MNDWI, NDBI, LST, Albedo, dNBR, GLCM texture, and more |
|
|
175
|
+
| **Verified InSAR chain** | Coregistration → phase removal → atmospheric/ionospheric correction → unwrapping → SBAS inversion |
|
|
176
|
+
| **YAML pipelines** | Chainable, schedulable, repeatable workflows with full run history |
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
@@ -369,7 +369,7 @@ If you use PyGeoFetch in your research or operational work, please cite it using
|
|
|
369
369
|
title = {PyGeoFetch: A Unified Python Framework for Multi-Provider Satellite Data Acquisition, Pre-download Quality Control, and Geospatial Processing},
|
|
370
370
|
year = {2026},
|
|
371
371
|
publisher = {Zenodo},
|
|
372
|
-
version = {2.6.2.
|
|
372
|
+
version = {2.6.2.9.1},
|
|
373
373
|
doi = {10.5281/zenodo.22087230},
|
|
374
374
|
url = {https://doi.org/10.5281/zenodo.22087230}
|
|
375
375
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
pygeofetch/__init__.py,sha256=
|
|
2
|
-
pygeofetch/meta.yaml,sha256=
|
|
1
|
+
pygeofetch/__init__.py,sha256=V4sFNFNv4wEcBniS22-P6r0TQLdLD3YzXHFY4bVLe_c,1994
|
|
2
|
+
pygeofetch/meta.yaml,sha256=ebEXC87Hk17xlc1pdXV6vrbWUUXT0j69H0zoCESv-V8,1284
|
|
3
3
|
pygeofetch/cli/__init__.py,sha256=QZMIIHEgmjGFieucqq9fZ5uephxP9-jdu1s8Q4qqw8o,30
|
|
4
4
|
pygeofetch/cli/auth_commands.py,sha256=OUYbInPaERvAfNZ_NmAF3azHoJm14cGn_ZTQtxjjcs4,7871
|
|
5
5
|
pygeofetch/cli/config_commands.py,sha256=1Pps0HLCLPODMlM6SFV8ru3IKCDpexSTz8uKIXvIIF8,3918
|
|
6
6
|
pygeofetch/cli/download_commands.py,sha256=0xrp7XLOZIgGAwV79IBhusdIzv8RJAedSWjuz3WCazA,13866
|
|
7
|
-
pygeofetch/cli/index_commands.py,sha256=
|
|
7
|
+
pygeofetch/cli/index_commands.py,sha256=dbv3n0HcneMNxuIPVBXvioLPms2P5DoVgfmYHYnB1CI,14776
|
|
8
8
|
pygeofetch/cli/main.py,sha256=FajFcdbeOKEabbIO3TqvChFVhiXcfZA6R1zdzu_oUfk,30565
|
|
9
9
|
pygeofetch/cli/monitor_commands.py,sha256=lz-P8tQqymQnZyb0ndLoTLHhTZrIwfQ6khk4hb12Z6I,4521
|
|
10
10
|
pygeofetch/cli/multisensor_commands.py,sha256=K6GNYqN7BLRhjOJAxfjpInjM_-ukEof8e3ekY3hr44o,8191
|
|
@@ -36,7 +36,7 @@ pygeofetch/insar/advanced_safeguards.py,sha256=CDMmxyC4PsldTaVXrxYm9vaJ2HS0izICB
|
|
|
36
36
|
pygeofetch/insar/analysis.py,sha256=GLNNE2OY17Y92p4FJSu7DwitRRp6AFLj1hccSmw3Mqg,35770
|
|
37
37
|
pygeofetch/insar/annotation.py,sha256=NRtH9HTlMQX1WkYLiJS9H7NrBxmUDBYIsZUOWVXZ37M,20775
|
|
38
38
|
pygeofetch/insar/atmosphere.py,sha256=frOTzYxIz33L4vPxbCQ4fVSkY2c_BZqHwIUoi4grK2Y,23032
|
|
39
|
-
pygeofetch/insar/auto_fallback.py,sha256=
|
|
39
|
+
pygeofetch/insar/auto_fallback.py,sha256=00tn6WT_w38vzv--bQvTWPKw2HRdo1UrWXhxpM7OGTE,14589
|
|
40
40
|
pygeofetch/insar/coregister.py,sha256=9QILqYi6quuRsnqZr4K3Feyrkp-SN_ZOO3ypAgP4lJI,57924
|
|
41
41
|
pygeofetch/insar/deburst.py,sha256=liWkKoP-UYYRRXxVYjsm2dpNCnIamL0RfH9JAe-660w,6456
|
|
42
42
|
pygeofetch/insar/esd.py,sha256=FR0k7TwIUGInDA1u9cW4zrrX0HGTY5MuclfeDcShL9k,26177
|
|
@@ -73,9 +73,9 @@ pygeofetch/multisensor/vegetation_disturbance.py,sha256=jVUSxuFpw-Ipz0-JZ6sYTE16
|
|
|
73
73
|
pygeofetch/optical/__init__.py,sha256=th071GPjYERZmNAU5Giz8p0ax8B4AyRlUwuO2-suxM8,1300
|
|
74
74
|
pygeofetch/optical/offset_tracking.py,sha256=fPwaBhM6X_w3tXPLskpUD_W9VaUR3ORSuFsm1Sq_g5k,34881
|
|
75
75
|
pygeofetch/processing/__init__.py,sha256=AP7MA88jXaJJDwwdjtDN1FX0f7WlSHyn__RlaFoQMH0,624
|
|
76
|
-
pygeofetch/processing/base.py,sha256=
|
|
76
|
+
pygeofetch/processing/base.py,sha256=S2mvFUXNhCTLpJOmwhVJgK804BvFsHuDWFkZMvnVAzw,21828
|
|
77
77
|
pygeofetch/processing/batch.py,sha256=_cOy7Nu7JCQKTwZLdwhwTPxJ7OzcKN4iQhKLDc7shOI,6024
|
|
78
|
-
pygeofetch/processing/indices.py,sha256=
|
|
78
|
+
pygeofetch/processing/indices.py,sha256=rBpeRnuk5jN0vYZU4W7Pj3eO2iyDIS2ILdE62lKzuRc,42991
|
|
79
79
|
pygeofetch/processing/pipeline.py,sha256=wKlP9bkZXmgaWtCHL8ADS-jUhg-xGjbPIn6cY3WGIeg,15798
|
|
80
80
|
pygeofetch/processing/postprocessor.py,sha256=Ardwxy4ed3tfCKvwT64WmsDEJq6aUiuEiIxWqyLTp78,22330
|
|
81
81
|
pygeofetch/processing/preprocessor.py,sha256=VNL_XnGc6NLalZcXjuOCuvriEZVpINewcx9NBd78vqE,72641
|
|
@@ -137,9 +137,9 @@ pygeofetch/validation/optical_validator.py,sha256=S0D361GibQwA2vvGb9_H8_6mxvaFKD
|
|
|
137
137
|
pygeofetch/viz/__init__.py,sha256=xf2C9IL4n3bv3oxk8RYkogq7nkuNyt18V-urAoXWRQY,449
|
|
138
138
|
pygeofetch/viz/map.py,sha256=f8whny5mnz7gDsStNdgtVSpqTNHSDhSZoRT2Qs-Lvos,16403
|
|
139
139
|
pygeofetch/viz/plot.py,sha256=JybD4qElJBOTvWCu7VllL5DqkKFYf5ttJdFfsgApfLE,56601
|
|
140
|
-
pygeofetch-2.6.2.
|
|
141
|
-
pygeofetch-2.6.2.
|
|
142
|
-
pygeofetch-2.6.2.
|
|
143
|
-
pygeofetch-2.6.2.
|
|
144
|
-
pygeofetch-2.6.2.
|
|
145
|
-
pygeofetch-2.6.2.
|
|
140
|
+
pygeofetch-2.6.2.9.dist-info/licenses/LICENSE,sha256=zavMyhODD6NvZTmmKFEtATrngwY4CU_NEtY7lhjxGnI,1075
|
|
141
|
+
pygeofetch-2.6.2.9.dist-info/METADATA,sha256=2abMqDD399UoNXMfFDSxmVT8EkjXDH0mhp4tXjJ0_zk,24973
|
|
142
|
+
pygeofetch-2.6.2.9.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
143
|
+
pygeofetch-2.6.2.9.dist-info/entry_points.txt,sha256=dHiUXLF5xNJ8vgtXZoUQNleEMQUeDPZiKQOE1tW8SDY,55
|
|
144
|
+
pygeofetch-2.6.2.9.dist-info/top_level.txt,sha256=rOmnZargIfNk99fwVOqt6OJyJvpSTBBAYO5HU37yhHk,11
|
|
145
|
+
pygeofetch-2.6.2.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|