RadGEEToolbox 1.7.0__tar.gz → 1.7.2__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.
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/PKG-INFO +8 -6
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/README.md +7 -5
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/GenericCollection.py +123 -32
- radgeetoolbox-1.7.2/RadGEEToolbox/GetPalette.py +169 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/LandsatCollection.py +465 -37
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/Sentinel1Collection.py +607 -19
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/Sentinel2Collection.py +468 -31
- radgeetoolbox-1.7.2/RadGEEToolbox/VisParams.py +139 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/__init__.py +1 -1
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/PKG-INFO +8 -6
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/pyproject.toml +1 -1
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/setup.py +1 -1
- radgeetoolbox-1.7.0/RadGEEToolbox/GetPalette.py +0 -120
- radgeetoolbox-1.7.0/RadGEEToolbox/VisParams.py +0 -221
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/LICENSE.txt +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox/CollectionStitch.py +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/SOURCES.txt +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/dependency_links.txt +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/requires.txt +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/RadGEEToolbox.egg-info/top_level.txt +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/setup.cfg +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/tests/test_landsat_collection.py +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/tests/test_sentinel1_collection.py +0 -0
- {radgeetoolbox-1.7.0 → radgeetoolbox-1.7.2}/tests/test_sentinel2_collection.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: RadGEEToolbox
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.2
|
|
4
4
|
Summary: Streamlined Multispectral & SAR Analysis for Google Earth Engine Python API
|
|
5
5
|
Home-page: https://github.com/radwinskis/RadGEEToolbox
|
|
6
6
|
Author: Mark Radwin
|
|
@@ -43,7 +43,7 @@ Designed for both new and advanced users of Google Earth Engine, RadGEEToolbox m
|
|
|
43
43
|
|
|
44
44
|
Although similar packages exist (eemont, geetools, etc.), `RadGEEToolbox` extends functionality and provides cohesive, chainable methods for research oriented projects working with Landsat TM & OLI, Sentinel-1 SAR, and/or Sentinel-2 MSI datasets (Table 1). The ultimate goal of `RadGEEToolbox` is to make satellite image processing easier and faster for real world applications relying on the most commonly utilized remote sensing platforms.
|
|
45
45
|
|
|
46
|
-
As of version `1.7.
|
|
46
|
+
As of version `1.7.2`, `RadGEEToolbox` supports any generic image collection via the `GenericCollection` module which allows for utilization of the same data management, temporal reduction, zonal statistics, and data export tools available for the `LandsatCollection`, `Sentinel1Collection`, and `Sentinel2Collection` modules. This allows users to provide their own image collection of choice, such as PRISM or MODIS data, to benefit from the tools available with `RadGEEToolbox`.
|
|
47
47
|
|
|
48
48
|
***Table 1.*** *Comparison of functionality between RadGEEToolbox, eemont, and geetools.*
|
|
49
49
|
|
|
@@ -181,15 +181,15 @@ _____________
|
|
|
181
181
|
|
|
182
182
|
### Installing via pip
|
|
183
183
|
|
|
184
|
-
To install `RadGEEToolbox` version 1.7.
|
|
184
|
+
To install `RadGEEToolbox` version 1.7.2 using pip (NOTE: it is recommended to create a new virtual environment):
|
|
185
185
|
|
|
186
186
|
```bash
|
|
187
|
-
pip install RadGEEToolbox==1.7.
|
|
187
|
+
pip install RadGEEToolbox==1.7.2
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
### Installing via Conda
|
|
191
191
|
|
|
192
|
-
To install `RadGEEToolbox` version 1.7.
|
|
192
|
+
To install `RadGEEToolbox` version 1.7.2 using conda-forge (NOTE: it is recommended to create a new virtual environment):
|
|
193
193
|
|
|
194
194
|
```bash
|
|
195
195
|
conda install conda-forge::radgeetoolbox
|
|
@@ -220,7 +220,7 @@ To verify that `RadGEEToolbox` was installed correctly:
|
|
|
220
220
|
python -c "import RadGEEToolbox; print(RadGEEToolbox.__version__)"
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
You should see `1.7.
|
|
223
|
+
You should see `1.7.2` printed as the version number.
|
|
224
224
|
|
|
225
225
|
### Want to Visualize Data? Install These Too
|
|
226
226
|
|
|
@@ -322,6 +322,8 @@ Then, directly plot `water_area_time_series` using Matplotlib as shown below
|
|
|
322
322
|
|
|
323
323
|
For details about Sentinel-1 SAR and Sentinel-2 MSI modules, and all other available Landsat or cross-module functions, please refer to the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/). You can also explore [`/Example Notebooks`](https://github.com/radwinskis/RadGEEToolbox/tree/main/Example%20Notebooks) for more usage examples.
|
|
324
324
|
|
|
325
|
+
> NOTE for those newer to Python: when you see ***`property`*** next to the function name in the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/), you do not need to add parentheses at the end of the function when calling the function. For example: ```my_image_collection.add_date_property``` calls the `add_date_property` property function upon the `my_image_collection` image collection - no parentheses needed. All other non-property functions require one or more arguments to be specified by the user, and will require the use of parentheses to define the function arguments.
|
|
326
|
+
|
|
325
327
|
________
|
|
326
328
|
|
|
327
329
|
|
|
@@ -14,7 +14,7 @@ Designed for both new and advanced users of Google Earth Engine, RadGEEToolbox m
|
|
|
14
14
|
|
|
15
15
|
Although similar packages exist (eemont, geetools, etc.), `RadGEEToolbox` extends functionality and provides cohesive, chainable methods for research oriented projects working with Landsat TM & OLI, Sentinel-1 SAR, and/or Sentinel-2 MSI datasets (Table 1). The ultimate goal of `RadGEEToolbox` is to make satellite image processing easier and faster for real world applications relying on the most commonly utilized remote sensing platforms.
|
|
16
16
|
|
|
17
|
-
As of version `1.7.
|
|
17
|
+
As of version `1.7.2`, `RadGEEToolbox` supports any generic image collection via the `GenericCollection` module which allows for utilization of the same data management, temporal reduction, zonal statistics, and data export tools available for the `LandsatCollection`, `Sentinel1Collection`, and `Sentinel2Collection` modules. This allows users to provide their own image collection of choice, such as PRISM or MODIS data, to benefit from the tools available with `RadGEEToolbox`.
|
|
18
18
|
|
|
19
19
|
***Table 1.*** *Comparison of functionality between RadGEEToolbox, eemont, and geetools.*
|
|
20
20
|
|
|
@@ -152,15 +152,15 @@ _____________
|
|
|
152
152
|
|
|
153
153
|
### Installing via pip
|
|
154
154
|
|
|
155
|
-
To install `RadGEEToolbox` version 1.7.
|
|
155
|
+
To install `RadGEEToolbox` version 1.7.2 using pip (NOTE: it is recommended to create a new virtual environment):
|
|
156
156
|
|
|
157
157
|
```bash
|
|
158
|
-
pip install RadGEEToolbox==1.7.
|
|
158
|
+
pip install RadGEEToolbox==1.7.2
|
|
159
159
|
```
|
|
160
160
|
|
|
161
161
|
### Installing via Conda
|
|
162
162
|
|
|
163
|
-
To install `RadGEEToolbox` version 1.7.
|
|
163
|
+
To install `RadGEEToolbox` version 1.7.2 using conda-forge (NOTE: it is recommended to create a new virtual environment):
|
|
164
164
|
|
|
165
165
|
```bash
|
|
166
166
|
conda install conda-forge::radgeetoolbox
|
|
@@ -191,7 +191,7 @@ To verify that `RadGEEToolbox` was installed correctly:
|
|
|
191
191
|
python -c "import RadGEEToolbox; print(RadGEEToolbox.__version__)"
|
|
192
192
|
```
|
|
193
193
|
|
|
194
|
-
You should see `1.7.
|
|
194
|
+
You should see `1.7.2` printed as the version number.
|
|
195
195
|
|
|
196
196
|
### Want to Visualize Data? Install These Too
|
|
197
197
|
|
|
@@ -293,6 +293,8 @@ Then, directly plot `water_area_time_series` using Matplotlib as shown below
|
|
|
293
293
|
|
|
294
294
|
For details about Sentinel-1 SAR and Sentinel-2 MSI modules, and all other available Landsat or cross-module functions, please refer to the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/). You can also explore [`/Example Notebooks`](https://github.com/radwinskis/RadGEEToolbox/tree/main/Example%20Notebooks) for more usage examples.
|
|
295
295
|
|
|
296
|
+
> NOTE for those newer to Python: when you see ***`property`*** next to the function name in the [RadGEEToolbox documentation](https://radgeetoolbox.readthedocs.io/en/latest/), you do not need to add parentheses at the end of the function when calling the function. For example: ```my_image_collection.add_date_property``` calls the `add_date_property` property function upon the `my_image_collection` image collection - no parentheses needed. All other non-property functions require one or more arguments to be specified by the user, and will require the use of parentheses to define the function arguments.
|
|
297
|
+
|
|
296
298
|
________
|
|
297
299
|
|
|
298
300
|
|
|
@@ -104,7 +104,7 @@ class GenericCollection:
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
@staticmethod
|
|
107
|
-
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
107
|
+
def anomaly_fn(image, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=None):
|
|
108
108
|
"""
|
|
109
109
|
Calculates the anomaly of a singleband image compared to the mean of the singleband image.
|
|
110
110
|
|
|
@@ -132,16 +132,25 @@ class GenericCollection:
|
|
|
132
132
|
|
|
133
133
|
image_to_process = image.select([band_name])
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
image_scale = image_to_process.projection().nominalScale()
|
|
136
|
+
|
|
137
|
+
# If the user supplies a numeric scale (int/float), keep it; otherwise default to image projection scale.
|
|
138
|
+
scale_value = scale if scale is not None else image_scale # Can be Python number or ee.Number
|
|
139
|
+
|
|
140
|
+
# Compute mean over geometry at chosen scale (scale_value may be ee.Number).
|
|
136
141
|
mean_image = image_to_process.reduceRegion(
|
|
137
142
|
reducer=ee.Reducer.mean(),
|
|
138
143
|
geometry=geometry,
|
|
139
|
-
scale=
|
|
144
|
+
scale=scale_value,
|
|
140
145
|
maxPixels=1e13
|
|
141
146
|
).toImage()
|
|
142
147
|
|
|
143
148
|
# Compute the anomaly by subtracting the mean image from the input image.
|
|
144
|
-
|
|
149
|
+
if scale is None:
|
|
150
|
+
anomaly_image = image_to_process.subtract(mean_image)
|
|
151
|
+
else:
|
|
152
|
+
anomaly_image = image_to_process.reproject(crs=image_to_process.projection(), scale=scale_value).subtract(mean_image)
|
|
153
|
+
|
|
145
154
|
if anomaly_band_name is None:
|
|
146
155
|
if band_name:
|
|
147
156
|
anomaly_image = anomaly_image.rename(band_name)
|
|
@@ -152,9 +161,9 @@ class GenericCollection:
|
|
|
152
161
|
anomaly_image = anomaly_image.rename(anomaly_band_name)
|
|
153
162
|
# return anomaly_image
|
|
154
163
|
if replace:
|
|
155
|
-
return anomaly_image.copyProperties(image)
|
|
164
|
+
return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
156
165
|
else:
|
|
157
|
-
return image.addBands(anomaly_image, overwrite=True)
|
|
166
|
+
return image.addBands(anomaly_image, overwrite=True).copyProperties(image)
|
|
158
167
|
|
|
159
168
|
@staticmethod
|
|
160
169
|
def mask_via_band_fn(image, band_to_mask, band_for_mask, threshold, mask_above=False, add_band_to_original_image=False):
|
|
@@ -315,7 +324,7 @@ class GenericCollection:
|
|
|
315
324
|
|
|
316
325
|
# Call to iterate the calculate_and_set_area function over the list of bands, starting with the original image
|
|
317
326
|
final_image = ee.Image(bands.iterate(calculate_and_set_area, image))
|
|
318
|
-
return final_image
|
|
327
|
+
return final_image #.set('system:time_start', image.get('system:time_start'))
|
|
319
328
|
|
|
320
329
|
def PixelAreaSumCollection(
|
|
321
330
|
self, band_name, geometry, threshold=-1, scale=30, maxPixels=1e12, output_type='ImageCollection', area_data_export_path=None
|
|
@@ -329,11 +338,11 @@ class GenericCollection:
|
|
|
329
338
|
|
|
330
339
|
Args:
|
|
331
340
|
band_name (string or list of strings): name of band(s) (string) for calculating area. If providing multiple band names, pass as a list of strings.
|
|
332
|
-
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation
|
|
341
|
+
geometry (ee.Geometry): ee.Geometry object denoting area to clip to for area calculation.
|
|
333
342
|
threshold (float): integer threshold to specify masking of pixels below threshold (defaults to -1). If providing multiple band names, the same threshold will be applied to all bands. Best practice in this case is to mask the bands prior to passing to this function and leave threshold at default of -1.
|
|
334
|
-
scale (int): integer scale of image resolution (meters) (defaults to 30)
|
|
335
|
-
maxPixels (int): integer denoting maximum number of pixels for calculations
|
|
336
|
-
output_type (str): 'ImageCollection' to return an ee.ImageCollection, 'GenericCollection' to return a GenericCollection object (defaults to 'ImageCollection')
|
|
343
|
+
scale (int): integer scale of image resolution (meters) (defaults to 30).
|
|
344
|
+
maxPixels (int): integer denoting maximum number of pixels for calculations.
|
|
345
|
+
output_type (str): 'ImageCollection' or 'ee.ImageCollection' to return an ee.ImageCollection, 'GenericCollection' to return a GenericCollection object, or 'DataFrame', 'Pandas', 'pd', 'dataframe', 'df' to return a pandas DataFrame (defaults to 'ImageCollection').
|
|
337
346
|
area_data_export_path (str, optional): If provided, the function will save the resulting area data to a CSV file at the specified path.
|
|
338
347
|
|
|
339
348
|
Returns:
|
|
@@ -356,17 +365,44 @@ class GenericCollection:
|
|
|
356
365
|
# Storing the result in the instance variable to avoid redundant calculations
|
|
357
366
|
self._PixelAreaSumCollection = AreaCollection
|
|
358
367
|
|
|
368
|
+
prop_names = band_name if isinstance(band_name, list) else [band_name]
|
|
369
|
+
|
|
359
370
|
# If an export path is provided, the area data will be exported to a CSV file
|
|
360
371
|
if area_data_export_path:
|
|
361
|
-
GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=
|
|
362
|
-
|
|
372
|
+
GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
|
|
363
373
|
# Returning the result in the desired format based on output_type argument or raising an error for invalid input
|
|
364
|
-
if output_type == 'ImageCollection':
|
|
374
|
+
if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
|
|
365
375
|
return self._PixelAreaSumCollection
|
|
366
376
|
elif output_type == 'GenericCollection':
|
|
367
377
|
return GenericCollection(collection=self._PixelAreaSumCollection)
|
|
378
|
+
elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
|
|
379
|
+
return GenericCollection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
|
|
368
380
|
else:
|
|
369
|
-
raise ValueError("output_type must be 'ImageCollection'
|
|
381
|
+
raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'GenericCollection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def add_month_property_fn(image):
|
|
385
|
+
"""
|
|
386
|
+
Adds a numeric 'month' property to the image based on its date.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
image (ee.Image): Input image.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
ee.Image: Image with the 'month' property added.
|
|
393
|
+
"""
|
|
394
|
+
return image.set('month', image.date().get('month'))
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def add_month_property(self):
|
|
398
|
+
"""
|
|
399
|
+
Adds a numeric 'month' property to each image in the collection.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
GenericCollection: A GenericCollection image collection with the 'month' property added to each image.
|
|
403
|
+
"""
|
|
404
|
+
col = self.collection.map(GenericCollection.add_month_property_fn)
|
|
405
|
+
return GenericCollection(collection=col)
|
|
370
406
|
|
|
371
407
|
def combine(self, other):
|
|
372
408
|
"""
|
|
@@ -480,6 +516,28 @@ class GenericCollection:
|
|
|
480
516
|
dates = self._dates_list.getInfo()
|
|
481
517
|
self._dates = dates
|
|
482
518
|
return self._dates
|
|
519
|
+
|
|
520
|
+
def remove_duplicate_dates(self, sort_by='system:time_start', ascending=True):
|
|
521
|
+
"""
|
|
522
|
+
Removes duplicate images that share the same date, keeping only the first one encountered.
|
|
523
|
+
Useful for handling duplicate acquisitions or overlapping path/rows.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
sort_by (str): Property to sort by before filtering distinct dates. Defaults to 'system:time_start' which is a global property.
|
|
527
|
+
Take care to provide a property that exists in all images if using a custom property.
|
|
528
|
+
ascending (bool): Sort order. Defaults to True.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
GenericCollection: A new GenericCollection object with distinct dates.
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
# Sort the collection to ensure the "best" image comes first (e.g. least cloudy)
|
|
535
|
+
sorted_col = self.collection.sort(sort_by, ascending)
|
|
536
|
+
|
|
537
|
+
# distinct() retains the first image for each unique value of the specified property
|
|
538
|
+
distinct_col = sorted_col.distinct('Date_Filter')
|
|
539
|
+
|
|
540
|
+
return GenericCollection(collection=distinct_col)
|
|
483
541
|
|
|
484
542
|
def ExportProperties(self, property_names, file_path=None):
|
|
485
543
|
"""
|
|
@@ -495,6 +553,8 @@ class GenericCollection:
|
|
|
495
553
|
# Ensure property_names is a list for consistent processing
|
|
496
554
|
if isinstance(property_names, str):
|
|
497
555
|
property_names = [property_names]
|
|
556
|
+
elif isinstance(property_names, list):
|
|
557
|
+
property_names = property_names
|
|
498
558
|
|
|
499
559
|
# Ensure properties are included without duplication, including 'Date_Filter'
|
|
500
560
|
all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
|
|
@@ -1565,24 +1625,24 @@ class GenericCollection:
|
|
|
1565
1625
|
if classify_above_threshold:
|
|
1566
1626
|
if mask_zeros:
|
|
1567
1627
|
col = self.collection.map(
|
|
1568
|
-
lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
|
|
1628
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
1569
1629
|
)
|
|
1570
1630
|
else:
|
|
1571
1631
|
col = self.collection.map(
|
|
1572
|
-
lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image)
|
|
1632
|
+
lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
1573
1633
|
)
|
|
1574
1634
|
else:
|
|
1575
1635
|
if mask_zeros:
|
|
1576
1636
|
col = self.collection.map(
|
|
1577
|
-
lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image)
|
|
1637
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name).updateMask(image.select(band_name).gt(0)).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
1578
1638
|
)
|
|
1579
1639
|
else:
|
|
1580
1640
|
col = self.collection.map(
|
|
1581
|
-
lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image)
|
|
1641
|
+
lambda image: image.select(band_name).lte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
|
|
1582
1642
|
)
|
|
1583
1643
|
return GenericCollection(collection=col)
|
|
1584
1644
|
|
|
1585
|
-
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True):
|
|
1645
|
+
def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=None):
|
|
1586
1646
|
"""
|
|
1587
1647
|
Calculates the anomaly of each image in a collection compared to the mean of each image.
|
|
1588
1648
|
|
|
@@ -1596,6 +1656,7 @@ class GenericCollection:
|
|
|
1596
1656
|
band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
1597
1657
|
anomaly_band_name (str, optional): A string representing the band name to be used for the output anomaly image. If not provided, the band name of the first band of the input image will be used.
|
|
1598
1658
|
replace (bool, optional): A boolean indicating whether to replace the original band with the anomaly band. If True, the output image will only contain the anomaly band. If False, the output image will retain all original bands and add the anomaly band. Default is True.
|
|
1659
|
+
scale (int, optional): The scale (in meters) to use for the image reduction. If not provided, the nominal scale of the image will be used.
|
|
1599
1660
|
|
|
1600
1661
|
Returns:
|
|
1601
1662
|
GenericCollection: A GenericCollection where each image represents the anomaly (deviation from
|
|
@@ -1614,7 +1675,7 @@ class GenericCollection:
|
|
|
1614
1675
|
else:
|
|
1615
1676
|
band_name = band_names.get(0).getInfo()
|
|
1616
1677
|
|
|
1617
|
-
col = self.collection.map(lambda image: GenericCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace))
|
|
1678
|
+
col = self.collection.map(lambda image: GenericCollection.anomaly_fn(image, geometry=geometry, band_name=band_name, anomaly_band_name=anomaly_band_name, replace=replace, scale=scale))
|
|
1618
1679
|
return GenericCollection(collection=col)
|
|
1619
1680
|
|
|
1620
1681
|
def mask_via_band(self, band_to_mask, band_for_mask, threshold=-1, mask_above=True, add_band_to_original_image=False):
|
|
@@ -2434,24 +2495,30 @@ class GenericCollection:
|
|
|
2434
2495
|
ValueError: If input parameters are invalid.
|
|
2435
2496
|
TypeError: If geometries input type is unsupported.
|
|
2436
2497
|
"""
|
|
2498
|
+
# Create a local reference to the collection object to allow for modifications (like band selection) without altering the original instance
|
|
2437
2499
|
img_collection_obj = self
|
|
2500
|
+
|
|
2501
|
+
# If a specific band is requested, select only that band
|
|
2438
2502
|
if band:
|
|
2439
2503
|
img_collection_obj = GenericCollection(collection=img_collection_obj.collection.select(band))
|
|
2440
2504
|
else:
|
|
2505
|
+
# If no band is specified, default to using the first band of the first image in the collection
|
|
2441
2506
|
first_image = img_collection_obj.image_grab(0)
|
|
2442
2507
|
first_band = first_image.bandNames().get(0)
|
|
2443
2508
|
img_collection_obj = GenericCollection(collection=img_collection_obj.collection.select([first_band]))
|
|
2444
|
-
|
|
2509
|
+
|
|
2510
|
+
# If a list of dates is provided, filter the collection to include only images matching those dates
|
|
2445
2511
|
if dates:
|
|
2446
2512
|
img_collection_obj = GenericCollection(
|
|
2447
2513
|
collection=self.collection.filter(ee.Filter.inList('Date_Filter', dates))
|
|
2448
2514
|
)
|
|
2449
2515
|
|
|
2450
|
-
# Initialize variables
|
|
2516
|
+
# Initialize variables to hold the standardized feature collection and coordinates
|
|
2451
2517
|
features = None
|
|
2452
2518
|
validated_coordinates = []
|
|
2453
2519
|
|
|
2454
|
-
#
|
|
2520
|
+
# Define a helper function to ensure every feature has a standardized 'geo_name' property
|
|
2521
|
+
# This handles features that might have different existing name properties or none at all
|
|
2455
2522
|
def set_standard_name(feature):
|
|
2456
2523
|
has_geo_name = feature.get('geo_name')
|
|
2457
2524
|
has_name = feature.get('name')
|
|
@@ -2462,33 +2529,38 @@ class GenericCollection:
|
|
|
2462
2529
|
ee.Algorithms.If(has_index, has_index, 'unnamed_geometry')))
|
|
2463
2530
|
return feature.set({'geo_name': new_name})
|
|
2464
2531
|
|
|
2532
|
+
# Handle input: FeatureCollection or single Feature
|
|
2465
2533
|
if isinstance(geometries, (ee.FeatureCollection, ee.Feature)):
|
|
2466
2534
|
features = ee.FeatureCollection(geometries)
|
|
2467
2535
|
if geometry_names:
|
|
2468
2536
|
print("Warning: 'geometry_names' are ignored when the input is an ee.Feature or ee.FeatureCollection.")
|
|
2469
2537
|
|
|
2538
|
+
# Handle input: Single ee.Geometry
|
|
2470
2539
|
elif isinstance(geometries, ee.Geometry):
|
|
2471
2540
|
name = geometry_names[0] if (geometry_names and geometry_names[0]) else 'unnamed_geometry'
|
|
2472
2541
|
features = ee.FeatureCollection([ee.Feature(geometries).set('geo_name', name)])
|
|
2473
2542
|
|
|
2543
|
+
# Handle input: List (could be coordinates or ee.Geometry objects)
|
|
2474
2544
|
elif isinstance(geometries, list):
|
|
2475
2545
|
if not geometries: # Handle empty list case
|
|
2476
2546
|
raise ValueError("'geometries' list cannot be empty.")
|
|
2477
2547
|
|
|
2478
|
-
# Case: List of coordinates
|
|
2548
|
+
# Case: List of tuples (coordinates)
|
|
2479
2549
|
if all(isinstance(i, tuple) for i in geometries):
|
|
2480
2550
|
validated_coordinates = geometries
|
|
2551
|
+
# Generate default names if none provided
|
|
2481
2552
|
if geometry_names is None:
|
|
2482
2553
|
geometry_names = [f"Location_{i+1}" for i in range(len(validated_coordinates))]
|
|
2483
2554
|
elif len(geometry_names) != len(validated_coordinates):
|
|
2484
2555
|
raise ValueError("geometry_names must have the same length as the coordinates list.")
|
|
2556
|
+
# Create features with buffers around the coordinates
|
|
2485
2557
|
points = [
|
|
2486
2558
|
ee.Feature(ee.Geometry.Point(coord).buffer(buffer_size), {'geo_name': str(name)})
|
|
2487
2559
|
for coord, name in zip(validated_coordinates, geometry_names)
|
|
2488
2560
|
]
|
|
2489
2561
|
features = ee.FeatureCollection(points)
|
|
2490
2562
|
|
|
2491
|
-
# Case: List of
|
|
2563
|
+
# Case: List of ee.Geometry objects
|
|
2492
2564
|
elif all(isinstance(i, ee.Geometry) for i in geometries):
|
|
2493
2565
|
if geometry_names is None:
|
|
2494
2566
|
geometry_names = [f"Geometry_{i+1}" for i in range(len(geometries))]
|
|
@@ -2503,6 +2575,7 @@ class GenericCollection:
|
|
|
2503
2575
|
else:
|
|
2504
2576
|
raise TypeError("Input list must be a list of (lon, lat) tuples OR a list of ee.Geometry objects.")
|
|
2505
2577
|
|
|
2578
|
+
# Handle input: Single tuple (coordinate)
|
|
2506
2579
|
elif isinstance(geometries, tuple) and len(geometries) == 2:
|
|
2507
2580
|
name = geometry_names[0] if geometry_names else 'Location_1'
|
|
2508
2581
|
features = ee.FeatureCollection([
|
|
@@ -2511,39 +2584,48 @@ class GenericCollection:
|
|
|
2511
2584
|
else:
|
|
2512
2585
|
raise TypeError("Unsupported type for 'geometries'.")
|
|
2513
2586
|
|
|
2587
|
+
# Apply the naming standardization to the created FeatureCollection
|
|
2514
2588
|
features = features.map(set_standard_name)
|
|
2515
2589
|
|
|
2590
|
+
# Dynamically retrieve the Earth Engine reducer based on the string name provided
|
|
2516
2591
|
try:
|
|
2517
2592
|
reducer = getattr(ee.Reducer, reducer_type)()
|
|
2518
2593
|
except AttributeError:
|
|
2519
2594
|
raise ValueError(f"Unknown reducer_type: '{reducer_type}'.")
|
|
2520
2595
|
|
|
2596
|
+
# Define the function to map over the image collection
|
|
2521
2597
|
def calculate_stats_for_image(image):
|
|
2522
2598
|
image_date = image.get('Date_Filter')
|
|
2599
|
+
# Calculate statistics for all geometries in 'features' for this specific image
|
|
2523
2600
|
stats_fc = image.reduceRegions(
|
|
2524
2601
|
collection=features, reducer=reducer, scale=scale, tileScale=tileScale
|
|
2525
2602
|
)
|
|
2526
2603
|
|
|
2604
|
+
# Helper to ensure the result has the reducer property, even if masked
|
|
2605
|
+
# If the property is missing (e.g., all pixels masked), set it to a sentinel value (-9999)
|
|
2527
2606
|
def guarantee_reducer_property(f):
|
|
2528
2607
|
has_property = f.propertyNames().contains(reducer_type)
|
|
2529
2608
|
return ee.Algorithms.If(has_property, f, f.set(reducer_type, -9999))
|
|
2609
|
+
|
|
2610
|
+
# Apply the guarantee check
|
|
2530
2611
|
fixed_stats_fc = stats_fc.map(guarantee_reducer_property)
|
|
2531
2612
|
|
|
2613
|
+
# Attach the image date to every feature in the result so we know which image it came from
|
|
2532
2614
|
return fixed_stats_fc.map(lambda f: f.set('image_date', image_date))
|
|
2533
2615
|
|
|
2616
|
+
# Map the calculation over the image collection and flatten the resulting FeatureCollections into one
|
|
2534
2617
|
results_fc = ee.FeatureCollection(img_collection_obj.collection.map(calculate_stats_for_image)).flatten()
|
|
2618
|
+
|
|
2619
|
+
# Convert the Earth Engine FeatureCollection to a pandas DataFrame (client-side operation)
|
|
2535
2620
|
df = GenericCollection.ee_to_df(results_fc, remove_geom=True)
|
|
2536
2621
|
|
|
2537
|
-
#
|
|
2622
|
+
# Check for empty results or missing columns
|
|
2538
2623
|
if df.empty:
|
|
2539
|
-
# print("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
2540
|
-
# return df
|
|
2541
2624
|
raise ValueError("No results found for the given parameters. Check if the geometries intersect with the images, if the dates filter is too restrictive, or if the provided bands are empty.")
|
|
2542
2625
|
if reducer_type not in df.columns:
|
|
2543
2626
|
print(f"Warning: Reducer '{reducer_type}' not found in results.")
|
|
2544
|
-
# return df
|
|
2545
2627
|
|
|
2546
|
-
#
|
|
2628
|
+
# Filter out the sentinel values (-9999) which indicate failed reductions/masked pixels
|
|
2547
2629
|
initial_rows = len(df)
|
|
2548
2630
|
df.dropna(subset=[reducer_type], inplace=True)
|
|
2549
2631
|
df = df[df[reducer_type] != -9999]
|
|
@@ -2551,9 +2633,18 @@ class GenericCollection:
|
|
|
2551
2633
|
if dropped_rows > 0:
|
|
2552
2634
|
print(f"Warning: Discarded {dropped_rows} results due to failed reductions (e.g., no valid pixels in geometry).")
|
|
2553
2635
|
|
|
2554
|
-
#
|
|
2636
|
+
# Pivot the DataFrame so that each row represents a date and each column represents a geometry location
|
|
2555
2637
|
pivot_df = df.pivot(index='image_date', columns='geo_name', values=reducer_type)
|
|
2638
|
+
# Rename the column headers (geometry names) to include the reducer type
|
|
2639
|
+
pivot_df.columns = [f"{col}_{reducer_type}" for col in pivot_df.columns]
|
|
2640
|
+
# Rename the index axis to 'Date' so it is correctly labeled when moved to a column later
|
|
2556
2641
|
pivot_df.index.name = 'Date'
|
|
2642
|
+
# Remove the name of the columns axis (which defaults to 'geo_name') so it doesn't appear as a confusing label in the final output
|
|
2643
|
+
pivot_df.columns.name = None
|
|
2644
|
+
# Reset the index to move the 'Date' index into a regular column and create a standard numerical index (0, 1, 2...)
|
|
2645
|
+
pivot_df = pivot_df.reset_index(drop=False)
|
|
2646
|
+
|
|
2647
|
+
# If a file path is provided, save the resulting DataFrame to CSV
|
|
2557
2648
|
if file_path:
|
|
2558
2649
|
# Check if file_path ends with .csv and remove it if so for consistency
|
|
2559
2650
|
if file_path.endswith('.csv'):
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
def get_palette(name):
|
|
2
|
+
"""
|
|
3
|
+
Returns the color palette associated with the given name.
|
|
4
|
+
|
|
5
|
+
Args:
|
|
6
|
+
name (str): Options include:
|
|
7
|
+
- Scientific: 'viridis', 'plasma', 'inferno', 'magma', 'cividis', 'turbo', 'coolwarm', 'spectral'
|
|
8
|
+
- Sequential: 'blues', 'greens', 'reds', 'greys', 'oranges', 'purples'
|
|
9
|
+
- Diverging: 'rdylgn' (Red-Yellow-Green), 'rdylbu' (Red-Yellow-Blue), 'rdbu' (Red-White-Blue), 'brbg' (Brown-Blue-Green), 'piyg' (Pink-Yellow-Green)
|
|
10
|
+
- Domain Specific: 'dem' (Elevation), 'terrain' (Topography), 'ndvi' (Vegetation), 'ndwi' (Water), 'precipitation' (Rain/Snow), 'thermal' (Temperature), 'evapotranspiration' (ET)
|
|
11
|
+
- Custom/Legacy: 'algae', 'dense', 'haline', 'jet', 'matter', 'pubu', 'soft_blue_green_red', 'turbid', 'ylord', 'ocean'
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
list: list of colors to be used for image visualization in GEE vis params
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
palettes = {
|
|
18
|
+
# --- Scientific / Perceptually Uniform ---
|
|
19
|
+
"viridis": [
|
|
20
|
+
"#440154", "#482475", "#414487", "#355f8d", "#2a788e",
|
|
21
|
+
"#21918c", "#22a884", "#44bf70", "#7ad151", "#bddf26", "#fde725"
|
|
22
|
+
],
|
|
23
|
+
"plasma": [
|
|
24
|
+
"#0d0887", "#46039f", "#7201a8", "#9c179e", "#bd3786",
|
|
25
|
+
"#d8576b", "#ed7953", "#fb9f3a", "#fdc924", "#f0f921"
|
|
26
|
+
],
|
|
27
|
+
"inferno": [
|
|
28
|
+
"#000004", "#160b39", "#420a68", "#6a176e", "#932667",
|
|
29
|
+
"#bc3754", "#dd513a", "#f37819", "#fca50a", "#f6d746", "#fcffa4"
|
|
30
|
+
],
|
|
31
|
+
"magma": [
|
|
32
|
+
"#000004", "#140e36", "#3b0f70", "#641a80", "#8c2981",
|
|
33
|
+
"#b73779", "#de4968", "#f7705c", "#fe9f6d", "#fecf92", "#fcfdbf"
|
|
34
|
+
],
|
|
35
|
+
"cividis": [
|
|
36
|
+
"#00204d", "#002c69", "#003989", "#184a8c", "#3f5b8a",
|
|
37
|
+
"#5d6d85", "#78807f", "#969576", "#b4ab6a", "#d4c359", "#fdea45"
|
|
38
|
+
],
|
|
39
|
+
"turbo": [
|
|
40
|
+
"#30123b", "#466be3", "#28bbec", "#32f298", "#a2fc3c",
|
|
41
|
+
"#f2ea33", "#fe9b2d", "#e4460a", "#7a0403"
|
|
42
|
+
],
|
|
43
|
+
"coolwarm": [
|
|
44
|
+
"#3d4c8a", "#6282ea", "#99baff", "#cdd9ec", "#eaf0e4",
|
|
45
|
+
"#f4dcb8", "#e8ac80", "#d4654d", "#b2182b"
|
|
46
|
+
],
|
|
47
|
+
|
|
48
|
+
# --- Sequential (ColorBrewer & Standard) ---
|
|
49
|
+
"blues": [
|
|
50
|
+
"#f7fbff", "#deebf7", "#c6dbef", "#9ecae1", "#6baed6",
|
|
51
|
+
"#4292c6", "#2171b5", "#08519c", "#08306b"
|
|
52
|
+
],
|
|
53
|
+
"greens": [
|
|
54
|
+
"#f7fcf5", "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476",
|
|
55
|
+
"#41ab5d", "#238b45", "#006d2c", "#00441b"
|
|
56
|
+
],
|
|
57
|
+
"reds": [
|
|
58
|
+
"#fff5f0", "#fee0d2", "#fcbba1", "#fc9272", "#fb6a4a",
|
|
59
|
+
"#ef3b2c", "#cb181d", "#a50f15", "#67000d"
|
|
60
|
+
],
|
|
61
|
+
"greys": [
|
|
62
|
+
"#ffffff", "#f0f0f0", "#d9d9d9", "#bdbdbd", "#969696",
|
|
63
|
+
"#737373", "#525252", "#252525", "#000000"
|
|
64
|
+
],
|
|
65
|
+
"oranges": [
|
|
66
|
+
"#fff5eb", "#fee6ce", "#fdd0a2", "#fdae6b", "#fd8d3c",
|
|
67
|
+
"#f16913", "#d94801", "#a63603", "#7f2704"
|
|
68
|
+
],
|
|
69
|
+
"purples": [
|
|
70
|
+
"#fcfbfd", "#efedf5", "#dadaeb", "#bcbddc", "#9e9ac8",
|
|
71
|
+
"#807dba", "#6a51a3", "#54278f", "#3f007d"
|
|
72
|
+
],
|
|
73
|
+
|
|
74
|
+
# --- Diverging ---
|
|
75
|
+
"spectral": [
|
|
76
|
+
"#9e0142", "#d53e4f", "#f46d43", "#fdae61", "#fee08b",
|
|
77
|
+
"#ffffbf", "#e6f598", "#abdda4", "#66c2a5", "#3288bd", "#5e4fa2"
|
|
78
|
+
],
|
|
79
|
+
"rdylgn": [
|
|
80
|
+
"#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b",
|
|
81
|
+
"#ffffbf", "#d9ef8b", "#a6d96a", "#66bd63", "#1a9850", "#006837"
|
|
82
|
+
],
|
|
83
|
+
"rdylbu": [
|
|
84
|
+
"#a50026", "#d73027", "#f46d43", "#fdae61", "#fee08b",
|
|
85
|
+
"#ffffbf", "#e0f3f8", "#abd9e9", "#74add1", "#4575b4", "#313695"
|
|
86
|
+
],
|
|
87
|
+
"rdbu": [
|
|
88
|
+
"#67001f", "#b2182b", "#d6604d", "#f4a582", "#fddbc7",
|
|
89
|
+
"#f7f7f7", "#d1e5f0", "#92c5de", "#4393c3", "#2166ac", "#053061"
|
|
90
|
+
],
|
|
91
|
+
"brbg": [
|
|
92
|
+
"#543005", "#8c510a", "#bf812d", "#dfc27d", "#f6e8c3",
|
|
93
|
+
"#f5f5f5", "#c7eae5", "#80cdc1", "#35978f", "#01665e", "#003c30"
|
|
94
|
+
],
|
|
95
|
+
"piyg": [
|
|
96
|
+
"#8e0152", "#c51b7d", "#de77ae", "#f1b6da", "#fde0ef",
|
|
97
|
+
"#f7f7f7", "#e6f5d0", "#b8e186", "#7fbc41", "#4d9221", "#276419"
|
|
98
|
+
],
|
|
99
|
+
|
|
100
|
+
# --- Domain Specific ---
|
|
101
|
+
"dem": [
|
|
102
|
+
"#006600", "#002200", "#fff700", "#ab7634", "#c4d0ff", "#ffffff"
|
|
103
|
+
], # Classic Green-Brown-White Elevation
|
|
104
|
+
"terrain": [
|
|
105
|
+
"#00A600", "#63C600", "#E6E600", "#E9BD3A", "#ECB176",
|
|
106
|
+
"#EFC2B3", "#F2F2F2"
|
|
107
|
+
], # Alternative Terrain
|
|
108
|
+
"ndvi": [
|
|
109
|
+
"#FFFFFF", "#CE7E45", "#DF923D", "#F1B555", "#FCD163", "#99B718",
|
|
110
|
+
"#74A901", "#66A000", "#529400", "#3E8601", "#207401", "#056201",
|
|
111
|
+
"#004C00", "#023B01", "#012E01", "#011D01", "#011301"
|
|
112
|
+
], # Standard MODIS/Landsat NDVI ramp
|
|
113
|
+
"ndwi": [
|
|
114
|
+
"#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf", "#3690c0",
|
|
115
|
+
"#0570b0", "#045a8d", "#023858"
|
|
116
|
+
], # Blue ramp for water
|
|
117
|
+
"precipitation": [
|
|
118
|
+
"#ffffff", "#00ffff", "#0000ff", "#00ff00", "#ffff00",
|
|
119
|
+
"#ff0000", "#ff00ff"
|
|
120
|
+
], # Classic Precip: White-Blue-Green-Yellow-Red-Purple
|
|
121
|
+
"evapotranspiration": [
|
|
122
|
+
"#ffffff", "#fcd163", "#99b718", "#74a901", "#66a000",
|
|
123
|
+
"#529400", "#3e8601", "#207401", "#056201", "#004c00"
|
|
124
|
+
], # Modeled on NDVI/Vegetation water use
|
|
125
|
+
"thermal": [
|
|
126
|
+
"#042333", "#2c3395", "#744992", "#b15f82", "#eb7958",
|
|
127
|
+
"#fbb43d", "#e8fa5b"
|
|
128
|
+
],
|
|
129
|
+
|
|
130
|
+
# --- Custom / Legacy from Original ---
|
|
131
|
+
"jet": [
|
|
132
|
+
"#00007F", "#002AFF", "#00D4FF", "#7FFF7F", "#FFD400",
|
|
133
|
+
"#FF2A00", "#7F0000"
|
|
134
|
+
],
|
|
135
|
+
"soft_blue_green_red": ["#deeaee", "#b1cbbb", "#eea29a", "#c94c4c"],
|
|
136
|
+
"algae": [
|
|
137
|
+
"#d7f9d0", "#a2d595", "#64b463", "#129450", "#126e45",
|
|
138
|
+
"#1a482f", "#122414"
|
|
139
|
+
],
|
|
140
|
+
"turbid": [
|
|
141
|
+
"#e9f6ab", "#d3c671", "#bf9747", "#a1703b", "#795338",
|
|
142
|
+
"#4d392d", "#221f1b"
|
|
143
|
+
],
|
|
144
|
+
"dense": [
|
|
145
|
+
"#e6f1f1", "#a2cee2", "#76a4e5", "#7871d5", "#7642a5",
|
|
146
|
+
"#621d62", "#360e24"
|
|
147
|
+
],
|
|
148
|
+
"matter": [
|
|
149
|
+
"#feedb0", "#f7b37c", "#eb7858", "#ce4356", "#9f2462",
|
|
150
|
+
"#66185c", "#2f0f3e"
|
|
151
|
+
],
|
|
152
|
+
"haline": [
|
|
153
|
+
"#2a186c", "14439c", "#206e8b", "#3c9387", "#5ab978",
|
|
154
|
+
"#aad85c", "#fdef9a"
|
|
155
|
+
],
|
|
156
|
+
"ylord": [
|
|
157
|
+
"#ffffcc", "#ffeda0", "#fed976", "#feb24c", "#fd8d3c",
|
|
158
|
+
"#fc4e2a", "#e31a1c", "#bd0026", "#800026"
|
|
159
|
+
],
|
|
160
|
+
"pubu": [
|
|
161
|
+
"#fff7fb", "#ece7f2", "#d0d1e6", "#a6bddb", "#74a9cf",
|
|
162
|
+
"#3690c0", "#0570b0", "#045a8d", "#023858"
|
|
163
|
+
][::-1],
|
|
164
|
+
"ocean": [
|
|
165
|
+
"#ffffd9", "#edf8b1", "#c7e9b4", "#7fcdbb", "#41b6c4",
|
|
166
|
+
"#1d91c0", "#225ea8", "#253494", "#081d58"
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
return palettes.get(name, None)
|