glam-processing 0.2.0__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.
- glam_processing/__init__.py +0 -0
- glam_processing/cli.py +51 -0
- glam_processing/config/__init__.py +0 -0
- glam_processing/config/clms.py +39 -0
- glam_processing/config/settings.py +1 -0
- glam_processing/download.py +654 -0
- glam_processing/earthdata.py +733 -0
- glam_processing/exceptions.py +47 -0
- glam_processing/spectral.py +110 -0
- glam_processing-0.2.0.dist-info/METADATA +23 -0
- glam_processing-0.2.0.dist-info/RECORD +13 -0
- glam_processing-0.2.0.dist-info/WHEEL +4 -0
- glam_processing-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import h5py
|
|
5
|
+
|
|
6
|
+
import rasterio
|
|
7
|
+
from rasterio.io import MemoryFile
|
|
8
|
+
|
|
9
|
+
from rio_cogeo.cogeo import cog_translate
|
|
10
|
+
from rio_cogeo.profiles import cog_profiles
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from .spectral import calc_ndvi, calc_ndwi
|
|
15
|
+
from . import exceptions
|
|
16
|
+
|
|
17
|
+
logging.basicConfig(
|
|
18
|
+
format="%(asctime)s - %(message)s",
|
|
19
|
+
datefmt="%d-%b-%y %H:%M:%S",
|
|
20
|
+
level=logging.INFO,
|
|
21
|
+
)
|
|
22
|
+
log = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
SUPPORTED_DATASETS = [
|
|
25
|
+
# MODIS
|
|
26
|
+
"MOD09Q1",
|
|
27
|
+
"MYD09Q1",
|
|
28
|
+
"MOD13Q1",
|
|
29
|
+
"MYD13Q1",
|
|
30
|
+
"MOD09A1",
|
|
31
|
+
"MYD09A1",
|
|
32
|
+
"MYD09GA",
|
|
33
|
+
"MOD09Q1N",
|
|
34
|
+
"MOD13Q4N",
|
|
35
|
+
"MOD09CMG",
|
|
36
|
+
"MCD12Q1",
|
|
37
|
+
# VIIRS/NPP
|
|
38
|
+
"VNP09H1",
|
|
39
|
+
"VNP09A1",
|
|
40
|
+
"VNP09GA",
|
|
41
|
+
"VNP09CMG",
|
|
42
|
+
"VNP21A2",
|
|
43
|
+
# MERRA-2
|
|
44
|
+
"M2SDNXSLV",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# WKT of default Sinusoidal Projection
|
|
49
|
+
SINUS_WKT = 'PROJCS["unnamed",GEOGCS["Unknown datum based upon the custom spheroid",DATUM["Not_specified_based_on_custom_spheroid",SPHEROID["Custom spheroid",6371007.181,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]]],PROJECTION["Sinusoidal"],PARAMETER["longitude_of_center",0],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH]]'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def authenticate(strategy="interactive", persist=True):
|
|
53
|
+
"""Authenticate with earthaccess"""
|
|
54
|
+
|
|
55
|
+
from earthaccess import Auth
|
|
56
|
+
|
|
57
|
+
auth = Auth()
|
|
58
|
+
auth.login(strategy=strategy, persist=persist)
|
|
59
|
+
return auth.authenticated
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_granules(product, start_date, end_date):
|
|
63
|
+
# get list of granules for a product
|
|
64
|
+
# start_date and end_date should be in YYYY-MM-DD format
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def get_dtype_from_sds_name(sds_name):
|
|
69
|
+
# given name of sds return string representation of dtype
|
|
70
|
+
if "sur_refl_b" in sds_name:
|
|
71
|
+
return "int16"
|
|
72
|
+
elif sds_name in ["sur_refl_qc_500m"]:
|
|
73
|
+
return "uint32"
|
|
74
|
+
elif sds_name in [
|
|
75
|
+
"sur_refl_szen",
|
|
76
|
+
"sur_refl_vzen",
|
|
77
|
+
"sur_refl_raz",
|
|
78
|
+
"RelativeAzimuth",
|
|
79
|
+
"SolarZenith",
|
|
80
|
+
"SensorZenith",
|
|
81
|
+
"SurfReflect_I1",
|
|
82
|
+
"SurfReflect_I2",
|
|
83
|
+
"SurfReflect_I3",
|
|
84
|
+
"SurfReflect_I1_1",
|
|
85
|
+
"SurfReflect_I2_1",
|
|
86
|
+
]:
|
|
87
|
+
return "int16"
|
|
88
|
+
elif sds_name in [
|
|
89
|
+
"sur_refl_state_500m",
|
|
90
|
+
"sur_refl_day_of_year",
|
|
91
|
+
"sur_refl_state_250m",
|
|
92
|
+
"sur_refl_qc_250m",
|
|
93
|
+
"SurfReflect_Day_Of_Year",
|
|
94
|
+
"SurfReflect_State_500m",
|
|
95
|
+
"SurfReflect_QC_500m",
|
|
96
|
+
]:
|
|
97
|
+
return "uint16"
|
|
98
|
+
elif sds_name in ["LC_Type1"]:
|
|
99
|
+
return "uint8"
|
|
100
|
+
elif sds_name in ["HOUR_NO_RAIN", "T2MMAX", "T2MMIN", "T2MMEAN", "TPRECMAX"]:
|
|
101
|
+
return "float32"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_h5_geo_info(file):
|
|
105
|
+
# Get info from the StructMetadata Object
|
|
106
|
+
metadata = file["HDFEOS INFORMATION"]["StructMetadata.0"][()].split()
|
|
107
|
+
# Info returned is of type Byte, must convert to string before using it
|
|
108
|
+
metadata_byte2str = [s.decode("utf-8") for s in metadata]
|
|
109
|
+
# Get upper left points
|
|
110
|
+
ulc = [i for i in metadata_byte2str if "UpperLeftPointMtrs" in i]
|
|
111
|
+
ulc_lon = float(
|
|
112
|
+
ulc[0].replace("=", ",").replace("(", "").replace(")", "").split(",")[1]
|
|
113
|
+
)
|
|
114
|
+
ulc_lat = float(
|
|
115
|
+
ulc[0].replace("=", ",").replace("(", "").replace(")", "").split(",")[2]
|
|
116
|
+
)
|
|
117
|
+
return (ulc_lon, 0.0, ulc_lat, 0.0)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_nrt(product):
|
|
121
|
+
if product[-1] == "N":
|
|
122
|
+
nrt = True
|
|
123
|
+
else:
|
|
124
|
+
nrt = False
|
|
125
|
+
return nrt
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_sds(dataset, name):
|
|
129
|
+
for sds in dataset.subdatasets:
|
|
130
|
+
sds_parts = sds.split(":")
|
|
131
|
+
if sds_parts[0] == "HDF4_EOS":
|
|
132
|
+
if sds_parts[-1] == name:
|
|
133
|
+
band = rasterio.open(sds)
|
|
134
|
+
return (band.read(), band.nodata)
|
|
135
|
+
elif sds_parts[0] == "HDF5":
|
|
136
|
+
if sds_parts[-1].split("/")[-1] == name:
|
|
137
|
+
band = rasterio.open(sds)
|
|
138
|
+
return (band.read(), band.nodata)
|
|
139
|
+
elif sds_parts[0] == "netcdf":
|
|
140
|
+
if sds_parts[-1] == name:
|
|
141
|
+
band = rasterio.open(sds)
|
|
142
|
+
return (band.read(), band.nodata)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_sds_path(dataset, name):
|
|
146
|
+
for sds in dataset.subdatasets:
|
|
147
|
+
sds_parts = sds.split(":")
|
|
148
|
+
if sds_parts[0] == "HDF4_EOS":
|
|
149
|
+
if sds_parts[-1] == name:
|
|
150
|
+
return sds
|
|
151
|
+
elif sds_parts[0] == "HDF5":
|
|
152
|
+
if sds_parts[-1].split("/")[-1] == name:
|
|
153
|
+
return sds
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def apply_mask(in_array, source_dataset, nodata):
|
|
157
|
+
"""
|
|
158
|
+
This function removes non-clear pixels from an input array,
|
|
159
|
+
including clouds, cloud shadow, and water.
|
|
160
|
+
|
|
161
|
+
For M*D CMG files, removes pixels ranked below "8" in
|
|
162
|
+
MOD13Q1 compositing method, as well as water.
|
|
163
|
+
|
|
164
|
+
Returns a cleaned array.
|
|
165
|
+
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
|
|
171
|
+
in_array: numpy.array
|
|
172
|
+
The array to be cleaned. This must have the same dimensions
|
|
173
|
+
as source_dataset, and preferably have been extracted from the
|
|
174
|
+
stack.
|
|
175
|
+
source_dataset: str
|
|
176
|
+
Path to a hierarchical data file containing QA layers with
|
|
177
|
+
which to perform the masking. Currently valid formats include
|
|
178
|
+
MOD09Q1 hdf and VNP09H1 files.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
# get file extension and product suffix
|
|
182
|
+
file_path = source_dataset.name
|
|
183
|
+
file_name, ext = os.path.splitext(os.path.basename(file_path))
|
|
184
|
+
suffix = file_name.split(".")[0]
|
|
185
|
+
|
|
186
|
+
# product-conditional behavior
|
|
187
|
+
|
|
188
|
+
# MODIS pre-generated VI masking
|
|
189
|
+
if suffix in ["MOD13Q1", "MOD13Q4", "MOD13Q4N"]:
|
|
190
|
+
if suffix[-1] == "1":
|
|
191
|
+
pr_arr, pr_nodata = get_sds(
|
|
192
|
+
source_dataset, "250m 16 days pixel reliability"
|
|
193
|
+
)
|
|
194
|
+
qa_arr, qa_nodata = get_sds(source_dataset, "250m 16 days VI Quality")
|
|
195
|
+
else:
|
|
196
|
+
pr_arr, pr_nodata = get_sds(source_dataset, "250m 8 days pixel reliability")
|
|
197
|
+
qa_arr, qa_nodata = get_sds(source_dataset, "250m 8 days VI Quality")
|
|
198
|
+
|
|
199
|
+
# in_array[(pr_arr != 0) & (pr_arr != 1)] = nodata
|
|
200
|
+
|
|
201
|
+
# mask clouds
|
|
202
|
+
in_array[(qa_arr & 0b11) > 1] = nodata # bits 0-1 > 01 = Cloudy
|
|
203
|
+
|
|
204
|
+
# mask Aerosol
|
|
205
|
+
in_array[(qa_arr & 0b11000000) == 0] = nodata # climatology
|
|
206
|
+
in_array[(qa_arr & 0b11000000) == 192] = nodata # high
|
|
207
|
+
|
|
208
|
+
# mask water
|
|
209
|
+
in_array[
|
|
210
|
+
((qa_arr & 0b11100000000000) != 2048)
|
|
211
|
+
& ((qa_arr & 0b11100000000000) != 4096)
|
|
212
|
+
& ((qa_arr & 0b11100000000000) != 8192)
|
|
213
|
+
] = nodata
|
|
214
|
+
# 001 = land, 010 = coastline, 100 = ephemeral water
|
|
215
|
+
|
|
216
|
+
# mask snow/ice
|
|
217
|
+
in_array[(qa_arr & 0b100000000000000) != 0] = nodata # bit 14
|
|
218
|
+
|
|
219
|
+
# mask cloud shadow
|
|
220
|
+
in_array[(qa_arr & 0b1000000000000000) != 0] = nodata # bit 15
|
|
221
|
+
|
|
222
|
+
# mask cloud adjacent pixels
|
|
223
|
+
in_array[(qa_arr & 0b100000000) != 0] = nodata # bit 8
|
|
224
|
+
|
|
225
|
+
# MODIS and VIIRS surface reflectance masking
|
|
226
|
+
# CMG
|
|
227
|
+
elif "CMG" in suffix:
|
|
228
|
+
if ext == ".hdf": # MOD09CMG
|
|
229
|
+
qa_arr, qa_nodata = get_sds(source_dataset, "Coarse Resolution QA")
|
|
230
|
+
state_arr, state_nodata = get_sds(
|
|
231
|
+
source_dataset, "Coarse Resolution State QA"
|
|
232
|
+
)
|
|
233
|
+
vang_arr, vang_nodata = get_sds(
|
|
234
|
+
source_dataset, "Coarse Resolution View Zenith Angle"
|
|
235
|
+
)
|
|
236
|
+
vang_arr[vang_arr <= 0] = 9999
|
|
237
|
+
sang_arr, sang_nodata = get_sds(
|
|
238
|
+
source_dataset, "Coarse Resolution Solar Zenith Angle"
|
|
239
|
+
)
|
|
240
|
+
rank_arr = np.full(qa_arr.shape, 10) # empty rank array
|
|
241
|
+
|
|
242
|
+
# perform the ranking!
|
|
243
|
+
logging.debug("--rank 9: SNOW")
|
|
244
|
+
SNOW = (state_arr & 0b1000000000000) | (
|
|
245
|
+
state_arr & 0b1000000000000000
|
|
246
|
+
) # state bit 12 OR 15
|
|
247
|
+
rank_arr[SNOW > 0] = 9 # snow
|
|
248
|
+
del SNOW
|
|
249
|
+
logging.debug("--rank 8: HIGHAEROSOL")
|
|
250
|
+
HIGHAEROSOL = state_arr & 0b11000000 # state bits 6 AND 7
|
|
251
|
+
rank_arr[HIGHAEROSOL == 192] = 8
|
|
252
|
+
del HIGHAEROSOL
|
|
253
|
+
logging.debug("--rank 7: CLIMAEROSOL")
|
|
254
|
+
CLIMAEROSOL = state_arr & 0b11000000 # state bits 6 & 7
|
|
255
|
+
# CLIMAEROSOL=(cloudMask & 0b100000000000000) # cloudMask bit 14
|
|
256
|
+
rank_arr[CLIMAEROSOL == 0] = 7 # default aerosol level
|
|
257
|
+
del CLIMAEROSOL
|
|
258
|
+
logging.debug("--rank 6: UNCORRECTED")
|
|
259
|
+
UNCORRECTED = qa_arr & 0b11 # qa bits 0 AND 1
|
|
260
|
+
rank_arr[UNCORRECTED == 3] = 6 # flagged uncorrected
|
|
261
|
+
del UNCORRECTED
|
|
262
|
+
logging.debug("--rank 5: SHADOW")
|
|
263
|
+
SHADOW = state_arr & 0b100 # state bit 2
|
|
264
|
+
rank_arr[SHADOW == 4] = 5 # cloud shadow
|
|
265
|
+
del SHADOW
|
|
266
|
+
logging.debug("--rank 4: CLOUDY")
|
|
267
|
+
# set adj to 11 and internal to 12 to verify in qa output
|
|
268
|
+
# state bit 0 OR bit 1 OR bit 10 OR bit 13
|
|
269
|
+
CLOUDY = state_arr & 0b11
|
|
270
|
+
# rank_arr[CLOUDY!=0]=4 # cloud pixel
|
|
271
|
+
del CLOUDY
|
|
272
|
+
CLOUDADJ = state_arr & 0b10000000000000
|
|
273
|
+
# rank_arr[CLOUDADJ>0]=4 # adjacent to cloud
|
|
274
|
+
del CLOUDADJ
|
|
275
|
+
CLOUDINT = state_arr & 0b10000000000
|
|
276
|
+
rank_arr[CLOUDINT > 0] = 4
|
|
277
|
+
del CLOUDINT
|
|
278
|
+
logging.debug("--rank 3: HIGHVIEW")
|
|
279
|
+
rank_arr[sang_arr > (85 / 0.01)] = 3 # HIGHVIEW
|
|
280
|
+
logging.debug("--rank 2: LOWSUN")
|
|
281
|
+
rank_arr[vang_arr > (60 / 0.01)] = 2 # LOWSUN
|
|
282
|
+
# BAD pixels
|
|
283
|
+
# qa bits (2-5 OR 6-9 == 1110)
|
|
284
|
+
logging.debug("--rank 1: BAD pixels")
|
|
285
|
+
BAD = (qa_arr & 0b111100) | (qa_arr & 0b1110000000)
|
|
286
|
+
rank_arr[BAD == 112] = 1
|
|
287
|
+
rank_arr[BAD == 896] = 1
|
|
288
|
+
rank_arr[BAD == 952] = 1
|
|
289
|
+
del BAD
|
|
290
|
+
|
|
291
|
+
logging.debug("-building water mask")
|
|
292
|
+
water = state_arr & 0b111000 # check bits
|
|
293
|
+
water[water == 56] = 1 # deep ocean
|
|
294
|
+
water[water == 48] = 1 # continental/moderate ocean
|
|
295
|
+
water[water == 24] = 1 # shallow inland water
|
|
296
|
+
water[water == 40] = 1 # deep inland water
|
|
297
|
+
water[water == 0] = 1 # shallow ocean
|
|
298
|
+
rank_arr[water == 1] = 0
|
|
299
|
+
vang_arr[water == 32] = 9999 # ephemeral water???
|
|
300
|
+
water[state_arr == 0] = 0
|
|
301
|
+
water[water != 1] = 0 # set non-water to zero
|
|
302
|
+
in_array[rank_arr <= 7] = nodata
|
|
303
|
+
elif ext == ".h5": # VNP09CMG
|
|
304
|
+
qf2, qf2_nodata = get_sds(source_dataset, "SurfReflect_QF2")
|
|
305
|
+
qf4, qf4_nodata = get_sds(source_dataset, "SurfReflect_QF4")
|
|
306
|
+
state_arr, state_nodata = get_sds(source_dataset, "State_QA")
|
|
307
|
+
vang_arr, vang_nodata = get_sds(source_dataset, "SensorZenith")
|
|
308
|
+
vang_arr[vang_arr <= 0] = 9999
|
|
309
|
+
sang_arr, sang_nodata = get_sds(source_dataset, "SolarZenith")
|
|
310
|
+
rank_arr = np.full(state_arr.shape, 10) # empty rank array
|
|
311
|
+
|
|
312
|
+
# perform the ranking!
|
|
313
|
+
logging.debug("--rank 9: SNOW")
|
|
314
|
+
SNOW = state_arr & 0b1000000000000000 # state bit 15
|
|
315
|
+
rank_arr[SNOW > 0] = 9 # snow
|
|
316
|
+
del SNOW
|
|
317
|
+
logging.debug("--rank 8: HIGHAEROSOL")
|
|
318
|
+
HIGHAEROSOL = qf2 & 0b10000 # qf2 bit 4
|
|
319
|
+
rank_arr[HIGHAEROSOL != 0] = 8
|
|
320
|
+
del HIGHAEROSOL
|
|
321
|
+
logging.debug("--rank 7: AEROSOL")
|
|
322
|
+
CLIMAEROSOL = state_arr & 0b1000000 # state bit 6
|
|
323
|
+
# CLIMAEROSOL=(cloudMask & 0b100000000000000) # cloudMask bit 14
|
|
324
|
+
# rank_arr[CLIMAEROSOL==0]=7 # "No"
|
|
325
|
+
del CLIMAEROSOL
|
|
326
|
+
# logging.debug("--rank 6: UNCORRECTED")
|
|
327
|
+
# UNCORRECTED = (qa_arr & 0b11) # qa bits 0 AND 1
|
|
328
|
+
# rank_arr[UNCORRECTED==3]=6 # flagged uncorrected
|
|
329
|
+
# del UNCORRECTED
|
|
330
|
+
logging.debug("--rank 5: SHADOW")
|
|
331
|
+
SHADOW = state_arr & 0b100 # state bit 2
|
|
332
|
+
rank_arr[SHADOW != 0] = 5 # cloud shadow
|
|
333
|
+
del SHADOW
|
|
334
|
+
logging.debug("--rank 4: CLOUDY")
|
|
335
|
+
# set adj to 11 and internal to 12 to verify in qa output
|
|
336
|
+
# CLOUDY = ((state_arr & 0b11)) # state bit 0 OR bit 1 OR bit 10 OR bit 13
|
|
337
|
+
# rank_arr[CLOUDY!=0]=4 # cloud pixel
|
|
338
|
+
# del CLOUDY
|
|
339
|
+
# CLOUDADJ = (state_arr & 0b10000000000) # nonexistent for viirs
|
|
340
|
+
# #rank_arr[CLOUDADJ>0]=4 # adjacent to cloud
|
|
341
|
+
# del CLOUDADJ
|
|
342
|
+
CLOUDINT = state_arr & 0b10000000000 # state bit 10
|
|
343
|
+
rank_arr[CLOUDINT > 0] = 4
|
|
344
|
+
del CLOUDINT
|
|
345
|
+
logging.debug("--rank 3: HIGHVIEW")
|
|
346
|
+
rank_arr[sang_arr > (85 / 0.01)] = 3 # HIGHVIEW
|
|
347
|
+
logging.debug("--rank 2: LOWSUN")
|
|
348
|
+
rank_arr[vang_arr > (60 / 0.01)] = 2 # LOWSUN
|
|
349
|
+
# BAD pixels
|
|
350
|
+
# qa bits (2-5 OR 6-9 == 1110)
|
|
351
|
+
logging.debug("--rank 1: BAD pixels")
|
|
352
|
+
BAD = qf4 & 0b110
|
|
353
|
+
rank_arr[BAD != 0] = 1
|
|
354
|
+
del BAD
|
|
355
|
+
|
|
356
|
+
logging.debug("-building water mask")
|
|
357
|
+
water = state_arr & 0b111000 # check bits 3-5
|
|
358
|
+
water[water == 40] = 0 # "coastal" = 101
|
|
359
|
+
water[water > 8] = 1 # sea water = 011; inland water = 010
|
|
360
|
+
# water[water==16]=1 # inland water = 010
|
|
361
|
+
# water[state_arr==0]=0
|
|
362
|
+
water[water != 1] = 0 # set non-water to zero
|
|
363
|
+
water[water != 0] = 1
|
|
364
|
+
rank_arr[water == 1] = 0
|
|
365
|
+
in_array[rank_arr <= 7] = nodata
|
|
366
|
+
else:
|
|
367
|
+
raise exceptions.FileTypeError("File must be of format .hdf or .h5")
|
|
368
|
+
elif "MERRA2" in suffix:
|
|
369
|
+
pass
|
|
370
|
+
# standard
|
|
371
|
+
else:
|
|
372
|
+
# viirs
|
|
373
|
+
if ext == ".h5":
|
|
374
|
+
qa_arr, qa_nodata = get_sds(source_dataset, "SurfReflect_QC_500m")
|
|
375
|
+
state_arr, state_nodata = get_sds(source_dataset, "SurfReflect_State_500m")
|
|
376
|
+
# MOD09A1
|
|
377
|
+
elif suffix == "MOD09A1":
|
|
378
|
+
qa_arr, qa_nodata = get_sds(source_dataset, "sur_refl_qc_500m")
|
|
379
|
+
state_arr, state_nodata = get_sds(source_dataset, "sur_refl_state_500m")
|
|
380
|
+
elif suffix == "MOD09GA":
|
|
381
|
+
qa_arr, qa_nodata = get_sds(source_dataset, "QC_500m_1")
|
|
382
|
+
state_arr, state_nodata = get_sds(source_dataset, "state_1km_1")
|
|
383
|
+
# all other MODIS products
|
|
384
|
+
elif ext == ".hdf":
|
|
385
|
+
qa_arr, qa_nodata = get_sds(source_dataset, "sur_refl_qc_250m")
|
|
386
|
+
state_arr, state_nodata = get_sds(source_dataset, "sur_refl_state_250m")
|
|
387
|
+
else:
|
|
388
|
+
raise exceptions.FileTypeError("File must be of format .hdf or .h5")
|
|
389
|
+
|
|
390
|
+
# mask clouds
|
|
391
|
+
in_array[(state_arr & 0b11) != 0] = nodata
|
|
392
|
+
in_array[(state_arr & 0b10000000000) != 0] = -3000 # internal cloud mask
|
|
393
|
+
|
|
394
|
+
# mask cloud shadow
|
|
395
|
+
in_array[(state_arr & 0b100) != 0] = nodata
|
|
396
|
+
|
|
397
|
+
# mask cloud adjacent pixels
|
|
398
|
+
in_array[(state_arr & 0b10000000000000) != 0] = nodata
|
|
399
|
+
|
|
400
|
+
# mask aerosols
|
|
401
|
+
in_array[(state_arr & 0b11000000) == 0] = nodata # climatology
|
|
402
|
+
# high; known to be an unreliable flag in MODIS collection 6
|
|
403
|
+
in_array[(state_arr & 0b11000000) == 192] = nodata
|
|
404
|
+
|
|
405
|
+
# mask snow/ice
|
|
406
|
+
in_array[(state_arr & 0b1000000000000) != 0] = nodata
|
|
407
|
+
|
|
408
|
+
# mask water
|
|
409
|
+
# checks against three 'allowed' land/water classes and excludes pixels that don't match
|
|
410
|
+
in_array[
|
|
411
|
+
((state_arr & 0b111000) != 8)
|
|
412
|
+
& ((state_arr & 0b111000) != 16)
|
|
413
|
+
& ((state_arr & 0b111000) != 32)
|
|
414
|
+
] = nodata
|
|
415
|
+
|
|
416
|
+
# mask bad solar zenith
|
|
417
|
+
# in_array[(qa_arr & 0b11100000) != 0] = nodata
|
|
418
|
+
|
|
419
|
+
# return output
|
|
420
|
+
return in_array
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def get_ndvi_array(dataset):
|
|
424
|
+
file_path = dataset.name
|
|
425
|
+
file_name = os.path.basename(file_path)
|
|
426
|
+
suffix = file_name.split(".")[0][3:]
|
|
427
|
+
f, ext = os.path.splitext(file_name)
|
|
428
|
+
|
|
429
|
+
if suffix in ["09Q4", "13Q4", "13Q4N"]:
|
|
430
|
+
band_name = "250m 8 days NDVI"
|
|
431
|
+
ndvi_array, ndvi_nodata = get_sds(dataset, band_name)
|
|
432
|
+
return (ndvi_array, ndvi_nodata)
|
|
433
|
+
elif suffix == "13Q1":
|
|
434
|
+
band_name = "250m 16 days NDVI"
|
|
435
|
+
ndvi_array, ndvi_nodata = get_sds(dataset, band_name)
|
|
436
|
+
return (ndvi_array, ndvi_nodata)
|
|
437
|
+
elif suffix == "09CM":
|
|
438
|
+
if ext == ".hdf":
|
|
439
|
+
red_name = "Coarse Resolution Surface Reflectance Band 1"
|
|
440
|
+
nir_name = "Coarse Resolution Surface Reflectance Band 2"
|
|
441
|
+
elif ext == ".h5":
|
|
442
|
+
red_name = "SurfReflect_I1"
|
|
443
|
+
nir_name = "SurfReflect_I2"
|
|
444
|
+
|
|
445
|
+
red_band, red_nodata = get_sds(dataset, red_name)
|
|
446
|
+
nir_band, nir_nodata = get_sds(dataset, nir_name)
|
|
447
|
+
|
|
448
|
+
ndvi_array = calc_ndvi(red_band, nir_band)
|
|
449
|
+
return (ndvi_array, red_nodata)
|
|
450
|
+
elif suffix == "09GA":
|
|
451
|
+
if ext == ".hdf":
|
|
452
|
+
red_name = "sur_refl_b01_1"
|
|
453
|
+
nir_name = "sur_refl_b02_1"
|
|
454
|
+
elif ext == ".h5":
|
|
455
|
+
red_name = "SurfReflect_I1_1"
|
|
456
|
+
nir_name = "SurfReflect_I2_1"
|
|
457
|
+
else:
|
|
458
|
+
raise exceptions.FileTypeError("File must be of type .hdf or .h5")
|
|
459
|
+
|
|
460
|
+
# Discovered negative surface reflectance values in MOD09 & MYD09
|
|
461
|
+
# that threw off NDVI calculations
|
|
462
|
+
# clip values to (0,10000)
|
|
463
|
+
|
|
464
|
+
# get numpy array from red band dataset
|
|
465
|
+
red_band, red_nodata = get_sds(dataset, red_name)
|
|
466
|
+
# dont clip nodata values
|
|
467
|
+
red_band[red_band != red_nodata] = np.clip(
|
|
468
|
+
red_band[red_band != red_nodata], 0, 10000
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# get numpy array from nir band dataset
|
|
472
|
+
nir_band, nir_nodata = get_sds(dataset, nir_name)
|
|
473
|
+
nir_band[nir_band != nir_nodata] = np.clip(
|
|
474
|
+
nir_band[nir_band != nir_nodata], 0, 10000
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
ndvi_array = calc_ndvi(red_band, nir_band)
|
|
478
|
+
|
|
479
|
+
return (ndvi_array, red_nodata)
|
|
480
|
+
|
|
481
|
+
else:
|
|
482
|
+
if ext == ".hdf":
|
|
483
|
+
red_name = "sur_refl_b01"
|
|
484
|
+
nir_name = "sur_refl_b02"
|
|
485
|
+
elif ext == ".h5":
|
|
486
|
+
red_name = "SurfReflect_I1"
|
|
487
|
+
nir_name = "SurfReflect_I2"
|
|
488
|
+
else:
|
|
489
|
+
raise exceptions.FileTypeError("File must be of type .hdf or .h5")
|
|
490
|
+
|
|
491
|
+
# Discovered negative surface reflectance values in MOD09 & MYD09
|
|
492
|
+
# that threw off NDVI calculations
|
|
493
|
+
# clip values to (0,10000)
|
|
494
|
+
|
|
495
|
+
# get numpy array from red band dataset
|
|
496
|
+
red_band, red_nodata = get_sds(dataset, red_name)
|
|
497
|
+
# dont clip nodata values
|
|
498
|
+
red_band[red_band != red_nodata] = np.clip(
|
|
499
|
+
red_band[red_band != red_nodata], 0, 10000
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# get numpy array from nir band dataset
|
|
503
|
+
nir_band, nir_nodata = get_sds(dataset, nir_name)
|
|
504
|
+
nir_band[nir_band != nir_nodata] = np.clip(
|
|
505
|
+
nir_band[nir_band != nir_nodata], 0, 10000
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
ndvi_array = calc_ndvi(red_band, nir_band)
|
|
509
|
+
|
|
510
|
+
return (ndvi_array, red_nodata)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def get_ndwi_array(dataset):
|
|
514
|
+
file_path = dataset.name
|
|
515
|
+
file_name = os.path.basename(file_path)
|
|
516
|
+
suffix = file_name.split(".")[0][3:]
|
|
517
|
+
f, ext = os.path.splitext(file_name)
|
|
518
|
+
|
|
519
|
+
if suffix == "09A1":
|
|
520
|
+
nir_name = "sur_refl_b02"
|
|
521
|
+
swir_name = "sur_refl_b06"
|
|
522
|
+
|
|
523
|
+
# Discovered negative surface reflectance values in MOD09 & MYD09
|
|
524
|
+
# that threw off NDVI calculations
|
|
525
|
+
# clip values to (0,10000)
|
|
526
|
+
|
|
527
|
+
nir_band, nir_nodata = get_sds(dataset, nir_name)
|
|
528
|
+
nir_band[nir_band != nir_nodata] = np.clip(
|
|
529
|
+
nir_band[nir_band != nir_nodata], 0, 10000
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
swir_band, swir_nodata = get_sds(dataset, swir_name)
|
|
533
|
+
swir_band[swir_band != swir_nodata] = np.clip(
|
|
534
|
+
swir_band[swir_band != swir_nodata], 0, 10000
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
ndwi_array = calc_ndwi(nir_band, swir_band)
|
|
538
|
+
else:
|
|
539
|
+
raise exceptions.UnsupportedError(
|
|
540
|
+
"Only MOD09A1 imagery is supported for NDWI generation"
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
return (ndwi_array, nir_nodata)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def create_ndvi_geotiff(file, out_dir):
|
|
547
|
+
dataset = rasterio.open(file)
|
|
548
|
+
|
|
549
|
+
file_path = dataset.name
|
|
550
|
+
file_name = os.path.basename(file_path)
|
|
551
|
+
f, ext = os.path.splitext(file_name)
|
|
552
|
+
|
|
553
|
+
# calculate ndvi and export to geotiff
|
|
554
|
+
ndvi_array, ndvi_nodata = get_ndvi_array(dataset)
|
|
555
|
+
|
|
556
|
+
# apply mask
|
|
557
|
+
ndvi_array = apply_mask(ndvi_array, dataset, ndvi_nodata)
|
|
558
|
+
|
|
559
|
+
out_name = file_name.replace(ext, ".ndvi.tif")
|
|
560
|
+
output = os.path.join(out_dir, out_name)
|
|
561
|
+
|
|
562
|
+
# coerce dtype to int16
|
|
563
|
+
dtype = "int16"
|
|
564
|
+
|
|
565
|
+
ndvi_array = ndvi_array.astype(dtype)
|
|
566
|
+
|
|
567
|
+
profile = rasterio.open(dataset.subdatasets[0]).profile.copy()
|
|
568
|
+
profile.update({"driver": "GTiff", "dtype": dtype, "nodata": ndvi_nodata})
|
|
569
|
+
|
|
570
|
+
if "VNP" in file_name:
|
|
571
|
+
f = h5py.File(file_path, "r")
|
|
572
|
+
geo_info = get_h5_geo_info(f)
|
|
573
|
+
if profile["height"] == 1200: # VIIRS VNP09A1, VNP09GA - 1km
|
|
574
|
+
yRes = -926.6254330555555
|
|
575
|
+
xRes = 926.6254330555555
|
|
576
|
+
elif profile["height"] == 2400: # VIIRS VNP09H1, VNP09GA - 500m
|
|
577
|
+
yRes = -463.31271652777775
|
|
578
|
+
xRes = 463.31271652777775
|
|
579
|
+
new_transform = rasterio.Affine(
|
|
580
|
+
xRes, geo_info[1], geo_info[0], geo_info[3], yRes, geo_info[2]
|
|
581
|
+
)
|
|
582
|
+
profile.update({"transform": new_transform})
|
|
583
|
+
|
|
584
|
+
# assign CRS if None
|
|
585
|
+
if profile["crs"] == None:
|
|
586
|
+
profile.update({"crs": SINUS_WKT})
|
|
587
|
+
|
|
588
|
+
# create cog
|
|
589
|
+
with MemoryFile() as memfile:
|
|
590
|
+
with memfile.open(**profile) as mem:
|
|
591
|
+
mem.write(ndvi_array)
|
|
592
|
+
dst_profile = cog_profiles.get("deflate")
|
|
593
|
+
cog_translate(
|
|
594
|
+
mem,
|
|
595
|
+
output,
|
|
596
|
+
dst_profile,
|
|
597
|
+
in_memory=True,
|
|
598
|
+
quiet=True,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
return output
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def create_ndwi_geotiff(file, out_dir):
|
|
605
|
+
dataset = rasterio.open(file)
|
|
606
|
+
|
|
607
|
+
file_path = dataset.name
|
|
608
|
+
file_name = os.path.basename(file_path)
|
|
609
|
+
f, ext = os.path.splitext(file_name)
|
|
610
|
+
|
|
611
|
+
# calculate ndvi and export to geotiff
|
|
612
|
+
ndwi_array, ndwi_nodata = get_ndwi_array(dataset)
|
|
613
|
+
|
|
614
|
+
# apply mask
|
|
615
|
+
ndwi_array = apply_mask(ndwi_array, dataset, ndwi_nodata)
|
|
616
|
+
|
|
617
|
+
out_name = file_name.replace(ext, ".ndwi.tif")
|
|
618
|
+
output = os.path.join(out_dir, out_name)
|
|
619
|
+
|
|
620
|
+
# coerce dtype to int16
|
|
621
|
+
dtype = "int16"
|
|
622
|
+
|
|
623
|
+
ndwi_array = ndwi_array.astype(dtype)
|
|
624
|
+
|
|
625
|
+
profile = rasterio.open(dataset.subdatasets[0]).profile.copy()
|
|
626
|
+
profile.update({"driver": "GTiff", "dtype": dtype, "nodata": ndwi_nodata})
|
|
627
|
+
|
|
628
|
+
if "VNP" in file_name:
|
|
629
|
+
f = h5py.File(file_path, "r")
|
|
630
|
+
geo_info = get_h5_geo_info(f)
|
|
631
|
+
if profile["height"] == 1200: # VIIRS VNP09A1, VNP09GA - 1km
|
|
632
|
+
yRes = -926.6254330555555
|
|
633
|
+
xRes = 926.6254330555555
|
|
634
|
+
elif profile["height"] == 2400: # VIIRS VNP09H1, VNP09GA - 500m
|
|
635
|
+
yRes = -463.31271652777775
|
|
636
|
+
xRes = 463.31271652777775
|
|
637
|
+
new_transform = rasterio.Affine(
|
|
638
|
+
xRes, geo_info[1], geo_info[0], geo_info[3], yRes, geo_info[2]
|
|
639
|
+
)
|
|
640
|
+
profile.update({"transform": new_transform})
|
|
641
|
+
|
|
642
|
+
# assign CRS if None
|
|
643
|
+
if profile["crs"] == None:
|
|
644
|
+
profile.update({"crs": SINUS_WKT})
|
|
645
|
+
|
|
646
|
+
# create cog
|
|
647
|
+
with MemoryFile() as memfile:
|
|
648
|
+
with memfile.open(**profile) as mem:
|
|
649
|
+
mem.write(ndwi_array)
|
|
650
|
+
dst_profile = cog_profiles.get("deflate")
|
|
651
|
+
cog_translate(
|
|
652
|
+
mem,
|
|
653
|
+
output,
|
|
654
|
+
dst_profile,
|
|
655
|
+
in_memory=True,
|
|
656
|
+
quiet=True,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
return output
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def create_sds_geotiff(file, product, sds_name, out_dir, mask=True):
|
|
663
|
+
filename, ext = os.path.splitext(file)
|
|
664
|
+
if ext in [".nc4", ".nc"]:
|
|
665
|
+
dataset = rasterio.open(f"netcdf:{file}")
|
|
666
|
+
else:
|
|
667
|
+
dataset = rasterio.open(file)
|
|
668
|
+
|
|
669
|
+
file_path = dataset.name
|
|
670
|
+
file_name = os.path.basename(file_path)
|
|
671
|
+
|
|
672
|
+
# get sds array and nodata value
|
|
673
|
+
sds_array, sds_nodata = get_sds(dataset, sds_name)
|
|
674
|
+
|
|
675
|
+
# apply mask
|
|
676
|
+
if mask:
|
|
677
|
+
sds_array = apply_mask(sds_array, dataset, sds_nodata)
|
|
678
|
+
|
|
679
|
+
# if band is surface reflectance then clip values (exclude nodata)
|
|
680
|
+
if sds_name.lower().find("refl") > -1 and product != "VNP09A1":
|
|
681
|
+
sds_array[sds_array != sds_nodata] = np.clip(
|
|
682
|
+
sds_array[sds_array != sds_nodata], 0, 10000
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
out_name = file_name.replace(ext, f".{sds_name}.tif")
|
|
686
|
+
output = os.path.join(out_dir, out_name)
|
|
687
|
+
|
|
688
|
+
dtype = get_dtype_from_sds_name(sds_name)
|
|
689
|
+
|
|
690
|
+
sds_array = sds_array.astype(dtype)
|
|
691
|
+
|
|
692
|
+
profile = rasterio.open(dataset.subdatasets[0]).profile.copy()
|
|
693
|
+
profile.update({"driver": "GTiff", "dtype": dtype, "nodata": sds_nodata})
|
|
694
|
+
|
|
695
|
+
if "VNP" in product:
|
|
696
|
+
f = h5py.File(file_path, "r")
|
|
697
|
+
geo_info = get_h5_geo_info(f)
|
|
698
|
+
out_name = file_name.replace(".h5", f".{sds_name}.tif")
|
|
699
|
+
output = os.path.join(out_dir, out_name)
|
|
700
|
+
if profile["height"] == 1200: # VIIRS VNP09A1, VNP09GA - 1km
|
|
701
|
+
yRes = -926.6254330555555
|
|
702
|
+
xRes = 926.6254330555555
|
|
703
|
+
elif profile["height"] == 2400: # VIIRS VNP09H1, VNP09GA - 500m
|
|
704
|
+
yRes = -463.31271652777775
|
|
705
|
+
xRes = 463.31271652777775
|
|
706
|
+
new_transform = rasterio.Affine(
|
|
707
|
+
xRes, geo_info[1], geo_info[0], geo_info[3], yRes, geo_info[2]
|
|
708
|
+
)
|
|
709
|
+
profile.update({"transform": new_transform})
|
|
710
|
+
tags = dataset.tags()
|
|
711
|
+
for tag in tags:
|
|
712
|
+
if sds_name + "__FillValue" in tag:
|
|
713
|
+
sds_nodata = tags[tag]
|
|
714
|
+
profile.update({"nodata": int(sds_nodata)})
|
|
715
|
+
|
|
716
|
+
# assign CRS if None
|
|
717
|
+
if profile["crs"] == None:
|
|
718
|
+
profile.update({"crs": SINUS_WKT})
|
|
719
|
+
|
|
720
|
+
# create cog
|
|
721
|
+
with MemoryFile() as memfile:
|
|
722
|
+
with memfile.open(**profile) as mem:
|
|
723
|
+
mem.write(sds_array)
|
|
724
|
+
dst_profile = cog_profiles.get("deflate")
|
|
725
|
+
cog_translate(
|
|
726
|
+
mem,
|
|
727
|
+
output,
|
|
728
|
+
dst_profile,
|
|
729
|
+
in_memory=True,
|
|
730
|
+
quiet=True,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
return output
|