RadGEEToolbox 1.7.1__py3-none-any.whl → 1.7.3__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.
@@ -218,6 +218,11 @@ class Sentinel1Collection:
218
218
  self._monthly_max = None
219
219
  self._monthly_min = None
220
220
  self._monthly_sum = None
221
+ self._yearly_median = None
222
+ self._yearly_mean = None
223
+ self._yearly_max = None
224
+ self._yearly_min = None
225
+ self._yearly_sum = None
221
226
  self._MosaicByDate = None
222
227
  self._PixelAreaSumCollection = None
223
228
  self._speckle_filter = None
@@ -339,17 +344,18 @@ class Sentinel1Collection:
339
344
  # Storing the result in the instance variable to avoid redundant calculations
340
345
  self._PixelAreaSumCollection = AreaCollection
341
346
 
347
+ prop_names = band_name if isinstance(band_name, list) else [band_name]
348
+
342
349
  # If an export path is provided, the area data will be exported to a CSV file
343
350
  if area_data_export_path:
344
- Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name], file_path=area_data_export_path+'.csv')
345
-
351
+ Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names, file_path=area_data_export_path+'.csv')
346
352
  # Returning the result in the desired format based on output_type argument or raising an error for invalid input
347
353
  if output_type == 'ImageCollection' or output_type == 'ee.ImageCollection':
348
354
  return self._PixelAreaSumCollection
349
355
  elif output_type == 'Sentinel1Collection':
350
356
  return Sentinel1Collection(collection=self._PixelAreaSumCollection)
351
357
  elif output_type == 'DataFrame' or output_type == 'Pandas' or output_type == 'pd' or output_type == 'dataframe' or output_type == 'df':
352
- return Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=[band_name])
358
+ return Sentinel1Collection(collection=self._PixelAreaSumCollection).ExportProperties(property_names=prop_names)
353
359
  else:
354
360
  raise ValueError("Incorrect `output_type`. The `output_type` argument must be one of the following: 'ImageCollection', 'ee.ImageCollection', 'Sentinel1Collection', 'DataFrame', 'Pandas', 'pd', 'dataframe', or 'df'.")
355
361
 
@@ -426,7 +432,7 @@ class Sentinel1Collection:
426
432
  # Overwrite on name collision
427
433
  merged = a.addBands(b, None, True)
428
434
  # Keep parent props + date key
429
- merged = merged.copyProperties(a, a.propertyNames())
435
+ merged = merged.copyProperties(a, a.propertyNames()).set('system:time_start', a.get('system:time_start'))
430
436
  merged = merged.set(date_key, a.get(date_key))
431
437
  return ee.Image(merged)
432
438
 
@@ -454,7 +460,7 @@ class Sentinel1Collection:
454
460
  # Add the single band; overwrite if the name already exists in parent
455
461
  merged = parent.addBands(sb.select([bname]).rename([bname]), None, True)
456
462
  # Preserve parent props + date key
457
- merged = merged.copyProperties(parent, parent.propertyNames())
463
+ merged = merged.copyProperties(parent, parent.propertyNames()).set('system:time_start', parent.get('system:time_start'))
458
464
  merged = merged.set(date_key, parent.get(date_key))
459
465
  return ee.Image(merged)
460
466
 
@@ -669,7 +675,7 @@ class Sentinel1Collection:
669
675
  xHat = image.select(bandNames).updateMask(retainPixel).unmask(xHat)
670
676
  output = ee.Image(xHat).rename(bandNames)
671
677
  # return image.addBands(output, None, True)
672
- return output.copyProperties(image)
678
+ return output.copyProperties(image).set('system:time_start', image.get('system:time_start'))
673
679
 
674
680
  def speckle_filter(self, KERNEL_SIZE, geometry=None, Tk=7, sigma=0.9, looks=1):
675
681
  """
@@ -717,6 +723,7 @@ class Sentinel1Collection:
717
723
  .pow(image.divide(ee.Image(10)))
718
724
  .rename(band_names)
719
725
  .copyProperties(image)
726
+ .set('system:time_start', image.get('system:time_start'))
720
727
  )
721
728
  return sigma_nought
722
729
 
@@ -743,6 +750,7 @@ class Sentinel1Collection:
743
750
  .multiply(image.log10())
744
751
  .rename(band_names)
745
752
  .copyProperties(image)
753
+ .set('system:time_start', image.get('system:time_start'))
746
754
  )
747
755
  return dB
748
756
 
@@ -808,7 +816,7 @@ class Sentinel1Collection:
808
816
  if replace:
809
817
  return anomaly_image.copyProperties(image).set('system:time_start', image.get('system:time_start'))
810
818
  else:
811
- return image.addBands(anomaly_image, overwrite=True)
819
+ return image.addBands(anomaly_image, overwrite=True).copyProperties(image).set('system:time_start', image.get('system:time_start'))
812
820
 
813
821
  @property
814
822
  def dates_list(self):
@@ -853,6 +861,8 @@ class Sentinel1Collection:
853
861
  # Ensure property_names is a list for consistent processing
854
862
  if isinstance(property_names, str):
855
863
  property_names = [property_names]
864
+ elif isinstance(property_names, list):
865
+ property_names = property_names
856
866
 
857
867
  # Ensure properties are included without duplication, including 'Date_Filter'
858
868
  all_properties_to_fetch = list(set(['Date_Filter'] + property_names))
@@ -1460,6 +1470,391 @@ class Sentinel1Collection:
1460
1470
 
1461
1471
  return self._monthly_min
1462
1472
 
1473
+ def yearly_mean_collection(self, start_month=1, end_month=12):
1474
+ """
1475
+ Creates a yearly mean composite from the collection, with optional monthly filtering.
1476
+
1477
+ This function computes the mean for each year within the collection's date range.
1478
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1479
+ to calculate the mean only using imagery from that specific season for each year.
1480
+
1481
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1482
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1483
+
1484
+ Args:
1485
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1486
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1487
+
1488
+ Returns:
1489
+ Object: A new instance of the same class (e.g., Sentinel1Collection) containing the yearly mean composites.
1490
+ """
1491
+ if self._yearly_mean is None:
1492
+
1493
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1494
+ start_date_full = ee.Date(date_range.get('min'))
1495
+ end_date_full = ee.Date(date_range.get('max'))
1496
+
1497
+ start_year = start_date_full.get('year')
1498
+ end_year = end_date_full.get('year')
1499
+
1500
+ if start_month != 1 or end_month != 12:
1501
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1502
+ else:
1503
+ processing_collection = self.collection
1504
+
1505
+ # Capture projection from the first image to restore it after reduction
1506
+ target_proj = self.collection.first().projection()
1507
+
1508
+ years = ee.List.sequence(start_year, end_year)
1509
+
1510
+ def create_yearly_composite(year):
1511
+ year = ee.Number(year)
1512
+ # Define the full calendar year range
1513
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1514
+ end_of_year = start_of_year.advance(1, 'year')
1515
+
1516
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1517
+
1518
+ # Calculate stats
1519
+ image_count = yearly_subset.size()
1520
+ yearly_reduction = yearly_subset.mean()
1521
+
1522
+ # Define the timestamp for the composite.
1523
+ # We use the start_month of that year to accurately reflect the data start time.
1524
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1525
+
1526
+ return yearly_reduction.set({
1527
+ 'system:time_start': composite_date.millis(),
1528
+ 'year': year,
1529
+ 'month': start_month,
1530
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1531
+ 'image_count': image_count,
1532
+ 'season_start': start_month,
1533
+ 'season_end': end_month
1534
+ }).reproject(target_proj)
1535
+
1536
+ # Map the function over the years list
1537
+ yearly_composites_list = years.map(create_yearly_composite)
1538
+
1539
+ # Convert to Collection
1540
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1541
+
1542
+ # Filter out any composites that were created from zero images.
1543
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1544
+
1545
+ self._yearly_mean = Sentinel1Collection(collection=final_collection)
1546
+ else:
1547
+ pass
1548
+ return self._yearly_mean
1549
+
1550
+ def yearly_median_collection(self, start_month=1, end_month=12):
1551
+ """
1552
+ Creates a yearly median composite from the collection, with optional monthly filtering.
1553
+
1554
+ This function computes the median for each year within the collection's date range.
1555
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1556
+ to calculate the median only using imagery from that specific season for each year.
1557
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1558
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1559
+
1560
+ Args:
1561
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1562
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1563
+
1564
+ Returns:
1565
+ Object: A new instance of the same class (e.g., Sentinel1Collection) containing the yearly median composites.
1566
+ """
1567
+ if self._yearly_median is None:
1568
+
1569
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1570
+ start_date_full = ee.Date(date_range.get('min'))
1571
+ end_date_full = ee.Date(date_range.get('max'))
1572
+
1573
+ start_year = start_date_full.get('year')
1574
+ end_year = end_date_full.get('year')
1575
+
1576
+ if start_month != 1 or end_month != 12:
1577
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1578
+ else:
1579
+ processing_collection = self.collection
1580
+
1581
+ # Capture projection from the first image to restore it after reduction
1582
+ target_proj = self.collection.first().projection()
1583
+
1584
+ years = ee.List.sequence(start_year, end_year)
1585
+
1586
+ def create_yearly_composite(year):
1587
+ year = ee.Number(year)
1588
+ # Define the full calendar year range
1589
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1590
+ end_of_year = start_of_year.advance(1, 'year')
1591
+
1592
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1593
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1594
+
1595
+ # Calculate stats
1596
+ image_count = yearly_subset.size()
1597
+ yearly_reduction = yearly_subset.median()
1598
+
1599
+ # Define the timestamp for the composite.
1600
+ # We use the start_month of that year to accurately reflect the data start time.
1601
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1602
+
1603
+ return yearly_reduction.set({
1604
+ 'system:time_start': composite_date.millis(),
1605
+ 'year': year,
1606
+ 'month': start_month,
1607
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1608
+ 'image_count': image_count,
1609
+ 'season_start': start_month,
1610
+ 'season_end': end_month
1611
+ }).reproject(target_proj)
1612
+
1613
+ # Map the function over the years list
1614
+ yearly_composites_list = years.map(create_yearly_composite)
1615
+
1616
+ # Convert to Collection
1617
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1618
+
1619
+ # Filter out any composites that were created from zero images.
1620
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1621
+
1622
+ self._yearly_median = Sentinel1Collection(collection=final_collection)
1623
+ else:
1624
+ pass
1625
+ return self._yearly_median
1626
+
1627
+ def yearly_max_collection(self, start_month=1, end_month=12):
1628
+ """
1629
+ Creates a yearly max composite from the collection, with optional monthly filtering.
1630
+
1631
+ This function computes the max for each year within the collection's date range.
1632
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1633
+ to calculate the max only using imagery from that specific season for each year.
1634
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1635
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1636
+
1637
+ Args:
1638
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1639
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1640
+
1641
+ Returns:
1642
+ Object: A new instance of the same class (e.g., Sentinel1Collection) containing the yearly max composites.
1643
+ """
1644
+ if self._yearly_max is None:
1645
+
1646
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1647
+ start_date_full = ee.Date(date_range.get('min'))
1648
+ end_date_full = ee.Date(date_range.get('max'))
1649
+
1650
+ start_year = start_date_full.get('year')
1651
+ end_year = end_date_full.get('year')
1652
+
1653
+ if start_month != 1 or end_month != 12:
1654
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1655
+ else:
1656
+ processing_collection = self.collection
1657
+
1658
+ # Capture projection from the first image to restore it after reduction
1659
+ target_proj = self.collection.first().projection()
1660
+
1661
+ years = ee.List.sequence(start_year, end_year)
1662
+
1663
+ def create_yearly_composite(year):
1664
+ year = ee.Number(year)
1665
+ # Define the full calendar year range
1666
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1667
+ end_of_year = start_of_year.advance(1, 'year')
1668
+
1669
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1670
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1671
+
1672
+ # Calculate stats
1673
+ image_count = yearly_subset.size()
1674
+ yearly_reduction = yearly_subset.max()
1675
+
1676
+ # Define the timestamp for the composite.
1677
+ # We use the start_month of that year to accurately reflect the data start time.
1678
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1679
+
1680
+ return yearly_reduction.set({
1681
+ 'system:time_start': composite_date.millis(),
1682
+ 'year': year,
1683
+ 'month': start_month,
1684
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1685
+ 'image_count': image_count,
1686
+ 'season_start': start_month,
1687
+ 'season_end': end_month
1688
+ }).reproject(target_proj)
1689
+
1690
+ # Map the function over the years list
1691
+ yearly_composites_list = years.map(create_yearly_composite)
1692
+
1693
+ # Convert to Collection
1694
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1695
+
1696
+ # Filter out any composites that were created from zero images.
1697
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1698
+
1699
+ self._yearly_max = Sentinel1Collection(collection=final_collection)
1700
+ else:
1701
+ pass
1702
+ return self._yearly_max
1703
+
1704
+ def yearly_min_collection(self, start_month=1, end_month=12):
1705
+ """
1706
+ Creates a yearly min composite from the collection, with optional monthly filtering.
1707
+
1708
+ This function computes the min for each year within the collection's date range.
1709
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1710
+ to calculate the min only using imagery from that specific season for each year.
1711
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1712
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1713
+
1714
+ Args:
1715
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1716
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1717
+
1718
+ Returns:
1719
+ Object: A new instance of the same class (e.g., Sentinel1Collection) containing the yearly min composites.
1720
+ """
1721
+ if self._yearly_min is None:
1722
+
1723
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1724
+ start_date_full = ee.Date(date_range.get('min'))
1725
+ end_date_full = ee.Date(date_range.get('max'))
1726
+
1727
+ start_year = start_date_full.get('year')
1728
+ end_year = end_date_full.get('year')
1729
+
1730
+ if start_month != 1 or end_month != 12:
1731
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1732
+ else:
1733
+ processing_collection = self.collection
1734
+
1735
+ # Capture projection from the first image to restore it after reduction
1736
+ target_proj = self.collection.first().projection()
1737
+
1738
+ years = ee.List.sequence(start_year, end_year)
1739
+
1740
+ def create_yearly_composite(year):
1741
+ year = ee.Number(year)
1742
+ # Define the full calendar year range
1743
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1744
+ end_of_year = start_of_year.advance(1, 'year')
1745
+
1746
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1747
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1748
+
1749
+ # Calculate stats
1750
+ image_count = yearly_subset.size()
1751
+ yearly_reduction = yearly_subset.min()
1752
+
1753
+ # Define the timestamp for the composite.
1754
+ # We use the start_month of that year to accurately reflect the data start time.
1755
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1756
+
1757
+ return yearly_reduction.set({
1758
+ 'system:time_start': composite_date.millis(),
1759
+ 'year': year,
1760
+ 'month': start_month,
1761
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1762
+ 'image_count': image_count,
1763
+ 'season_start': start_month,
1764
+ 'season_end': end_month
1765
+ }).reproject(target_proj)
1766
+
1767
+ # Map the function over the years list
1768
+ yearly_composites_list = years.map(create_yearly_composite)
1769
+
1770
+ # Convert to Collection
1771
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1772
+
1773
+ # Filter out any composites that were created from zero images.
1774
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1775
+
1776
+ self._yearly_min = Sentinel1Collection(collection=final_collection)
1777
+ else:
1778
+ pass
1779
+ return self._yearly_min
1780
+
1781
+ def yearly_sum_collection(self, start_month=1, end_month=12):
1782
+ """
1783
+ Creates a yearly sum composite from the collection, with optional monthly filtering.
1784
+
1785
+ This function computes the sum for each year within the collection's date range.
1786
+ You can specify a range of months (e.g., start_month=6, end_month=10 for June-October)
1787
+ to calculate the sum only using imagery from that specific season for each year.
1788
+ The resulting images have 'system:time_start', 'year', 'image_count', 'season_start',
1789
+ 'season_end', and 'Date_Filter' properties. Years with no images (after filtering) are excluded.
1790
+
1791
+ Args:
1792
+ start_month (int): The starting month (1-12) for the filter. Defaults to 1 (January).
1793
+ end_month (int): The ending month (1-12) for the filter. Defaults to 12 (December).
1794
+
1795
+ Returns:
1796
+ Object: A new instance of the same class (e.g., Sentinel1Collection) containing the yearly sum composites.
1797
+ """
1798
+ if self._yearly_sum is None:
1799
+
1800
+ date_range = self.collection.reduceColumns(ee.Reducer.minMax(), ["system:time_start"])
1801
+ start_date_full = ee.Date(date_range.get('min'))
1802
+ end_date_full = ee.Date(date_range.get('max'))
1803
+
1804
+ start_year = start_date_full.get('year')
1805
+ end_year = end_date_full.get('year')
1806
+
1807
+ if start_month != 1 or end_month != 12:
1808
+ processing_collection = self.collection.filter(ee.Filter.calendarRange(start_month, end_month, 'month'))
1809
+ else:
1810
+ processing_collection = self.collection
1811
+
1812
+ # Capture projection from the first image to restore it after reduction
1813
+ target_proj = self.collection.first().projection()
1814
+
1815
+ years = ee.List.sequence(start_year, end_year)
1816
+
1817
+ def create_yearly_composite(year):
1818
+ year = ee.Number(year)
1819
+ # Define the full calendar year range
1820
+ start_of_year = ee.Date.fromYMD(year, 1, 1)
1821
+ end_of_year = start_of_year.advance(1, 'year')
1822
+
1823
+ # Filter to the specific year using the PRE-FILTERED seasonal collection
1824
+ yearly_subset = processing_collection.filterDate(start_of_year, end_of_year)
1825
+
1826
+ # Calculate stats
1827
+ image_count = yearly_subset.size()
1828
+ yearly_reduction = yearly_subset.sum()
1829
+
1830
+ # Define the timestamp for the composite.
1831
+ # We use the start_month of that year to accurately reflect the data start time.
1832
+ composite_date = ee.Date.fromYMD(year, start_month, 1)
1833
+
1834
+ return yearly_reduction.set({
1835
+ 'system:time_start': composite_date.millis(),
1836
+ 'year': year,
1837
+ 'month': start_month,
1838
+ 'Date_Filter': composite_date.format('YYYY-MM-dd'),
1839
+ 'image_count': image_count,
1840
+ 'season_start': start_month,
1841
+ 'season_end': end_month
1842
+ }).reproject(target_proj)
1843
+
1844
+ # Map the function over the years list
1845
+ yearly_composites_list = years.map(create_yearly_composite)
1846
+
1847
+ # Convert to Collection
1848
+ yearly_collection = ee.ImageCollection.fromImages(yearly_composites_list)
1849
+
1850
+ # Filter out any composites that were created from zero images.
1851
+ final_collection = yearly_collection.filter(ee.Filter.gt('image_count', 0))
1852
+
1853
+ self._yearly_sum = Sentinel1Collection(collection=final_collection)
1854
+ else:
1855
+ pass
1856
+ return self._yearly_sum
1857
+
1463
1858
  def anomaly(self, geometry, band_name=None, anomaly_band_name=None, replace=True, scale=10):
1464
1859
  """
1465
1860
  Calculates the anomaly of each image in a collection compared to the mean of each image.
@@ -1523,9 +1918,237 @@ class Sentinel1Collection:
1523
1918
  raise ValueError("Threshold must be specified for binary masking.")
1524
1919
 
1525
1920
  col = self.collection.map(
1526
- lambda image: image.select(band_name).gte(threshold).rename(band_name)
1921
+ lambda image: image.select(band_name).gte(threshold).rename(band_name).copyProperties(image).set('system:time_start', image.get('system:time_start'))
1527
1922
  )
1528
1923
  return Sentinel1Collection(collection=col)
1924
+
1925
+ def mann_kendall_trend(self, target_band=None, join_method='system:time_start', geometry=None):
1926
+ """
1927
+ Calculates the Mann-Kendall S-value, Variance, Z-Score, and Confidence Level for each pixel in the image collection, in addition to calculating
1928
+ the Sen's slope for each pixel in the image collection. The output is an image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
1929
+
1930
+ This function can be used to identify trends in the image collection over time, such as increasing or decreasing values in the target band, and can be used to assess the significance of these trends.
1931
+ Note that this function is computationally intensive and may take a long time to run for large image collections or high-resolution images.
1932
+
1933
+ The 's_statistic' band represents the Mann-Kendall S-value, which is a measure of the strength and direction of the trend.
1934
+ The 'variance' band represents the variance of the S-value, which is a measure of the variability of the S-value.
1935
+ The 'z_score' band represents the Z-Score, which is a measure of the significance of the trend.
1936
+ The 'confidence' band represents the confidence level of the trend based on the z_score, which is a probabilistic measure of the confidence in the trend (percentage).
1937
+ The 'slope' band represents the Sen's slope, which is a measure of the rate of change in the target band over time. This value can be small as multispectral indices commonly range from -1 to 1, so a slope may have values of <0.2 for most cases.
1938
+
1939
+ Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
1940
+ You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
1941
+ The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
1942
+
1943
+ Args:
1944
+ image_collection (Sentinel1Collection or ee.ImageCollection): The input image collection for which the Mann-Kendall and Sen's slope trend statistics will be calculated.
1945
+ target_band (str): The band name to be used for the output anomaly image. e.g. 'ndvi'
1946
+ join_method (str, optional): The method used to join images in the collection. Options are 'system:time_start' or 'Date_Filter'. Default is 'system:time_start'.
1947
+ geometry (ee.Geometry, optional): An ee.Geometry object to limit the area over which the trend statistics are calculated and mask the output image. Default is None.
1948
+
1949
+ Returns:
1950
+ ee.Image: An image with the following bands: 's_statistic', 'variance', 'z_score', 'confidence', and 'slope'.
1951
+ """
1952
+ ########## PART 1 - S-VALUE CALCULATION ##########
1953
+ ##### https://vsp.pnnl.gov/help/vsample/design_trend_mann_kendall.htm #####
1954
+ image_collection = self
1955
+ if isinstance(image_collection, Sentinel1Collection):
1956
+ image_collection = image_collection.collection
1957
+ elif isinstance(image_collection, ee.ImageCollection):
1958
+ pass
1959
+ else:
1960
+ raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid Sentinel1Collection or ee.ImageCollection object.')
1961
+
1962
+ if target_band is None:
1963
+ raise ValueError('The `target_band` parameter must be specified.')
1964
+ if not isinstance(target_band, str):
1965
+ raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
1966
+
1967
+ if geometry is not None and not isinstance(geometry, ee.Geometry):
1968
+ raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
1969
+ # define the join, which will join all images newer than the current image
1970
+ # use system:time_start if the image does not have a Date_Filter property
1971
+ if join_method == 'system:time_start':
1972
+ # get all images where the leftField value is less than (before) the rightField value
1973
+ time_filter = ee.Filter.lessThan(leftField='system:time_start',
1974
+ rightField='system:time_start')
1975
+ elif join_method == 'Date_Filter':
1976
+ # get all images where the leftField value is less than (before) the rightField value
1977
+ time_filter = ee.Filter.lessThan(leftField='Date_Filter',
1978
+ rightField='Date_Filter')
1979
+ else:
1980
+ raise ValueError(f'The chosen `join_method`: {join_method} does not match the options of "system:time_start" or "Date_Filter".')
1981
+
1982
+ # for any matches during a join, set image as a property key called 'future_image'
1983
+ join = ee.Join.saveAll(matchesKey='future_image')
1984
+
1985
+ # apply the join on the input collection
1986
+ # joining all images newer than the current image with the current image
1987
+ joined_collection = ee.ImageCollection(join.apply(primary=image_collection,
1988
+ secondary=image_collection, condition=time_filter))
1989
+
1990
+ # defining a collection to calculate the partial S value for each match in the join
1991
+ # e.g. t4-t1, t3-t1, t2-1 if there are 4 images
1992
+ def calculate_partial_s(current_image):
1993
+ # select the target band for arithmetic
1994
+ current_val = current_image.select(target_band)
1995
+ # get the joined images from the current image properties and cast the joined images as a list
1996
+ future_image_list = ee.List(current_image.get('future_image'))
1997
+ # convert the joined list to an image collection
1998
+ future_image_collection = ee.ImageCollection(future_image_list)
1999
+
2000
+ # define a function that will calculate the difference between the joined images and the current image,
2001
+ # then calculate the partial S sign based on the value of the difference calculation
2002
+ def get_sign(future_image):
2003
+ # select the target band for arithmetic from the future image
2004
+ future_val = future_image.select(target_band)
2005
+ # calculate the difference, i.e. t2-t1
2006
+ difference = future_val.subtract(current_val)
2007
+ # determine the sign of the difference value (1 if diff > 0, 0 if 0, and -1 if diff < 0)
2008
+ # use .unmask(0) to set any masked pixels as 0 to avoid
2009
+
2010
+ sign = difference.signum().unmask(0)
2011
+
2012
+ return sign
2013
+
2014
+ # map the get_sign() function along the future image col
2015
+ # then sum the values for each pixel to get the partial S value
2016
+ return future_image_collection.map(get_sign).sum()
2017
+
2018
+ # calculate the partial s value for each image in the joined/input image collection
2019
+ partial_s_col = joined_collection.map(calculate_partial_s)
2020
+
2021
+ # convert the image collection to an image of s_statistic values per pixel
2022
+ # where the s_statistic is the sum of partial s values
2023
+ # renaming the band as 's_statistic' for later usage
2024
+ final_s_image = partial_s_col.sum().rename('s_statistic')
2025
+
2026
+
2027
+ ########## PART 2 - VARIANCE and Z-SCORE ##########
2028
+ # to calculate variance we need to know how many pixels were involved in the partial_s calculations per pixel
2029
+ # we do this by using count() and turn the value to a float for later arithmetic
2030
+ n = image_collection.select(target_band).count().toFloat()
2031
+
2032
+ ##### VARIANCE CALCULATION #####
2033
+ # as we are using floating point values with high precision, it is HIGHLY
2034
+ # unlikely that there will be multiple pixel values with the same value.
2035
+ # Thus, we opt to use the simplified variance calculation approach as the
2036
+ # impacts to the output value are negligible and the processing benefits are HUGE
2037
+ # variance = (n * (n - 1) * (2n + 5)) / 18
2038
+ var_s = n.multiply(n.subtract(1))\
2039
+ .multiply(n.multiply(2).add(5))\
2040
+ .divide(18).rename('variance')
2041
+
2042
+ z_score = ee.Image().expression(
2043
+ """
2044
+ (s > 0) ? (s - 1) / sqrt(var) :
2045
+ (s < 0) ? (s + 1) / sqrt(var) :
2046
+ 0
2047
+ """,
2048
+ {'s': final_s_image, 'var': var_s}
2049
+ ).rename('z_score')
2050
+
2051
+ confidence = z_score.abs().divide(ee.Number(2).sqrt()).erf().rename('confidence')
2052
+
2053
+ stat_bands = ee.Image([var_s, z_score, confidence])
2054
+
2055
+ mk_stats_image = final_s_image.addBands(stat_bands)
2056
+
2057
+ ########## PART 3 - Sen's Slope ##########
2058
+ def add_year_band(image):
2059
+ if join_method == 'Date_Filter':
2060
+ # Get the string 'YYYY-MM-DD'
2061
+ date_string = image.get('Date_Filter')
2062
+ # Parse it into an ee.Date object (handles the conversion to time math)
2063
+ date = ee.Date.parse('YYYY-MM-dd', date_string)
2064
+ else:
2065
+ # Standard way: assumes system:time_start exists
2066
+ date = image.date()
2067
+ years = date.difference(ee.Date('1970-01-01'), 'year')
2068
+ return image.addBands(ee.Image(years).float().rename('year'))
2069
+
2070
+ slope_input = image_collection.map(add_year_band).select(['year', target_band])
2071
+
2072
+ sens_slope = slope_input.reduce(ee.Reducer.sensSlope())
2073
+
2074
+ slope_band = sens_slope.select('slope')
2075
+
2076
+ # add a mask to the final image to remove pixels with less than min_observations
2077
+ # mainly an effort to mask pixels outside of the boundary of the input image collection
2078
+ min_observations = 1
2079
+ valid_mask = n.gte(min_observations)
2080
+
2081
+ final_image = mk_stats_image.addBands(slope_band).updateMask(valid_mask)
2082
+
2083
+ if geometry is not None:
2084
+ mask = ee.Image(1).clip(geometry)
2085
+ final_image = final_image.updateMask(mask)
2086
+
2087
+ return final_image
2088
+
2089
+ def sens_slope_trend(self, target_band=None, join_method='system:time_start', geometry=None):
2090
+ """
2091
+ Calculates Sen's Slope (trend magnitude) for the collection.
2092
+ This is a lighter-weight alternative to the full `mann_kendall_trend` function if only
2093
+ the direction and magnitude of the trend are needed.
2094
+
2095
+ Be sure to select the correct band for the `target_band` parameter, as this will be used to calculate the trend statistics.
2096
+ You may optionally provide an ee.Geometry object for the `geometry` parameter to limit the area over which the trend statistics are calculated.
2097
+ The `geometry` parameter is optional and defaults to None, which means that the trend statistics will be calculated over the entire footprint of the image collection.
2098
+
2099
+ Args:
2100
+ target_band (str): The name of the band to analyze. Defaults to 'ndvi'.
2101
+ join_method (str): Property to use for time sorting ('system:time_start' or 'Date_Filter').
2102
+ geometry (ee.Geometry, optional): Geometry to mask the final output.
2103
+
2104
+ Returns:
2105
+ ee.Image: An image containing the 'slope' band.
2106
+ """
2107
+ image_collection = self
2108
+ if isinstance(image_collection, Sentinel1Collection):
2109
+ image_collection = image_collection.collection
2110
+ elif isinstance(image_collection, ee.ImageCollection):
2111
+ pass
2112
+ else:
2113
+ raise ValueError(f'The chosen `image_collection`: {image_collection} is not a valid Sentinel1Collection or ee.ImageCollection object.')
2114
+
2115
+ if target_band is None:
2116
+ raise ValueError('The `target_band` parameter must be specified.')
2117
+ if not isinstance(target_band, str):
2118
+ raise ValueError(f'The chosen `target_band`: {target_band} is not a valid string.')
2119
+
2120
+ if geometry is not None and not isinstance(geometry, ee.Geometry):
2121
+ raise ValueError(f'The chosen `geometry`: {geometry} is not a valid ee.Geometry object.')
2122
+
2123
+ # Add Year Band (Time X-Axis)
2124
+ def add_year_band(image):
2125
+ # Handle user-defined date strings vs system time
2126
+ if join_method == 'Date_Filter':
2127
+ date_string = image.get('Date_Filter')
2128
+ date = ee.Date.parse('YYYY-MM-dd', date_string)
2129
+ else:
2130
+ date = image.date()
2131
+
2132
+ # Convert to fractional years relative to epoch
2133
+ years = date.difference(ee.Date('1970-01-01'), 'year')
2134
+ return image.addBands(ee.Image(years).float().rename('year'))
2135
+
2136
+ # Prepare Collection: Select ONLY [Year, Target]
2137
+ # sensSlope expects Band 0 = Independent (X), Band 1 = Dependent (Y)
2138
+ slope_input = self.collection.map(add_year_band).select(['year', target_band])
2139
+
2140
+ # Run the Native Reducer
2141
+ sens_result = slope_input.reduce(ee.Reducer.sensSlope())
2142
+
2143
+ # Extract and Mask
2144
+ slope_band = sens_result.select('slope')
2145
+
2146
+ if geometry is not None:
2147
+ mask = ee.Image(1).clip(geometry)
2148
+ slope_band = slope_band.updateMask(mask)
2149
+
2150
+ return slope_band
2151
+
1529
2152
 
1530
2153
  def mask_to_polygon(self, polygon):
1531
2154
  """
@@ -1543,7 +2166,7 @@ class Sentinel1Collection:
1543
2166
  mask = ee.Image.constant(1).clip(polygon)
1544
2167
 
1545
2168
  # Update the mask of each image in the collection
1546
- masked_collection = self.collection.map(lambda img: img.updateMask(mask))
2169
+ masked_collection = self.collection.map(lambda img: img.updateMask(mask).copyProperties(img).set('system:time_start', img.get('system:time_start')))
1547
2170
 
1548
2171
  # Update the internal collection state
1549
2172
  self._geometry_masked_collection = Sentinel1Collection(
@@ -1572,7 +2195,7 @@ class Sentinel1Collection:
1572
2195
  area = full_mask.paint(polygon, 0)
1573
2196
 
1574
2197
  # Update the mask of each image in the collection
1575
- masked_collection = self.collection.map(lambda img: img.updateMask(area))
2198
+ masked_collection = self.collection.map(lambda img: img.updateMask(area).copyProperties(img).set('system:time_start', img.get('system:time_start')))
1576
2199
 
1577
2200
  # Update the internal collection state
1578
2201
  self._geometry_masked_out_collection = Sentinel1Collection(