satcube 0.1.16__py3-none-any.whl → 0.1.18__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.

Potentially problematic release.


This version of satcube might be problematic. Click here for more details.

satcube/objects.py CHANGED
@@ -1,26 +1,33 @@
1
- # satcube/objects.py
2
1
  from __future__ import annotations
3
2
  from dataclasses import dataclass, field
4
3
  import pathlib
5
4
  import pandas as pd
5
+ from typing import Tuple
6
+ from datetime import datetime
7
+ import numpy as np
8
+ import xarray as xr
9
+ import rasterio as rio
10
+ import torch
6
11
 
7
- from satcube.align import align as _align_fn
8
- from satcube.cloud_detection import cloud_masking as _cloud_fn
12
+ from satcube.align import align_fn
13
+ from satcube.cloud import cloud_fn
14
+ from satcube.gapfill import gapfill_fn
15
+ from satcube.composite import monthly_composites_s2
16
+ from satcube.smooth import gaussian_smooth
9
17
 
10
18
  @dataclass
11
19
  class SatCubeMetadata:
12
- df: pd.DataFrame = field(repr=False)
13
- raw_dir: pathlib.Path = field(repr=False)
20
+ df: pd.DataFrame
21
+ raw_dir: pathlib.Path
22
+ _current_dir: pathlib.Path | None = None
14
23
 
15
- def __repr__(self) -> str:
16
- return self.df.__repr__()
17
-
18
- __str__ = __repr__
19
-
20
- def _repr_html_(self) -> str:
21
- html = getattr(self.df, "_repr_html_", None)
22
- return html() if callable(html) else self.df.__repr__()
23
24
 
25
+ def _dir(self) -> pathlib.Path:
26
+ return self._current_dir or self.raw_dir
27
+
28
+ def _spawn(self, *, df: pd.DataFrame, current_dir: pathlib.Path) -> "SatCubeMetadata":
29
+ return SatCubeMetadata(df=df, raw_dir=self.raw_dir, _current_dir=current_dir)
30
+
24
31
  def align(
25
32
  self,
26
33
  input_dir: str | pathlib.Path | None = None,
@@ -28,44 +35,209 @@ class SatCubeMetadata:
28
35
  nworks: int = 4,
29
36
  cache: bool = False
30
37
  ) -> "SatCubeMetadata":
31
-
32
- if input_dir is None:
33
- input_dir = self.raw_dir
34
- else:
35
- input_dir = pathlib.Path(input_dir).expanduser().resolve()
36
38
 
37
- _align_fn(
39
+ self.aligned_dir = pathlib.Path(output_dir).resolve()
40
+
41
+ input_dir = pathlib.Path(input_dir).expanduser().resolve() if input_dir else self._dir()
42
+ output_dir = pathlib.Path(output_dir).resolve()
43
+
44
+ new_df = align_fn(
45
+ metadata=self.df,
38
46
  input_dir=input_dir,
39
- output_dir=output_dir,
47
+ output_dir=self.aligned_dir,
40
48
  nworks=nworks,
41
49
  cache=cache
42
50
  )
43
- self.aligned_dir = pathlib.Path(output_dir).resolve()
44
- return self
51
+
52
+ return self._spawn(df=new_df, current_dir=output_dir)
45
53
 
46
54
  def cloud_masking(
47
55
  self,
56
+ input_dir: str | pathlib.Path | None = None,
48
57
  output_dir: str | pathlib.Path = "masked",
49
- model_path: str | pathlib.Path = "SEN2CloudEnsemble",
50
- device: str = "cpu"
58
+ device: str = "cpu",
59
+ cache: bool = False,
60
+ nworks: int = 4,
61
+ ) -> "SatCubeMetadata":
62
+
63
+ input_dir = pathlib.Path(input_dir).expanduser().resolve() if input_dir else self._dir()
64
+ output_dir = pathlib.Path(output_dir).resolve()
65
+
66
+
67
+ new_df = cloud_fn(
68
+ metadata=self.df,
69
+ input_dir=input_dir,
70
+ output_dir=output_dir,
71
+ device=device,
72
+ nworks=nworks,
73
+ cache=cache
74
+ )
75
+
76
+ return self._spawn(df=new_df, current_dir=output_dir)
77
+
78
+ def gap_fill(
79
+ self,
80
+ *,
81
+ input_dir: str | pathlib.Path | None = None,
82
+ output_dir: str | pathlib.Path = "gapfilled",
83
+ method: str = "histogram_matching",
84
+ quiet: bool = False
85
+ ) -> "SatCubeMetadata":
86
+ """Fill small cloud/shadow gaps on top of current stack."""
87
+ in_dir = pathlib.Path(input_dir).expanduser().resolve() if input_dir else self._dir()
88
+ out_dir = pathlib.Path(output_dir).resolve()
89
+
90
+ new_df = gapfill_fn(
91
+ metadata=self.df,
92
+ input_dir=in_dir,
93
+ output_dir=out_dir,
94
+ method=method,
95
+ quiet=quiet,
96
+ )
97
+
98
+ return self._spawn(df=new_df, current_dir=out_dir)
99
+
100
+ def monthly_composites(
101
+ self,
102
+ input_dir: str | pathlib.Path | None = None,
103
+ output_dir: str | pathlib.Path = "monthly_composites",
104
+ agg_method: str = "median"
105
+ ) -> "SatCubeMetadata":
106
+
107
+ input_dir = pathlib.Path(input_dir).expanduser().resolve() if input_dir else self._dir()
108
+ output_dir = pathlib.Path(output_dir).resolve()
109
+
110
+ date_range = (self.df["date"].min(), self.df["date"].max())
111
+
112
+ out_table = monthly_composites_s2(
113
+ metadata=self.df,
114
+ input_dir=input_dir,
115
+ output_dir=output_dir,
116
+ date_range=date_range,
117
+ agg_method=agg_method
118
+ )
119
+
120
+ return self._spawn(df=out_table, current_dir=output_dir)
121
+
122
+ def interpolate(
123
+ self,
124
+ input_dir: str | pathlib.Path | None = None,
125
+ output_dir: str | pathlib.Path = "interpolated",
51
126
  ) -> "SatCubeMetadata":
52
- if not hasattr(self, "aligned_dir"):
53
- raise RuntimeError("You must run .align() first")
54
- _cloud_fn(
55
- input=self.aligned_dir,
56
- output=output_dir,
57
- model_path=model_path,
58
- device=device
127
+
128
+
129
+ input_dir = pathlib.Path(input_dir).expanduser().resolve() if input_dir else self._dir()
130
+ output_dir = pathlib.Path(output_dir).resolve()
131
+ output_dir.mkdir(parents=True, exist_ok=True)
132
+
133
+ all_raw_files = [input_dir / f for f in input_dir.glob("*.tif") if f.is_file()]
134
+
135
+ all_raw_files.sort()
136
+ all_raw_dates = pd.to_datetime(self.df["date"])
137
+
138
+ # Create a data cube with xarray
139
+ profile = rio.open(all_raw_files[0]).profile
140
+ data_np = np.array([rio.open(file).read() for file in all_raw_files]) / 10000 # Normalize
141
+ data_np[data_np == 6.5535] = np.nan # Set NoData values to NaN
142
+ data_np = xr.DataArray(
143
+ data=data_np,
144
+ dims=("time", "band", "y", "x"),
145
+ coords={"time": all_raw_dates, "band": range(13)},
146
+ )
147
+
148
+ data_np = data_np.interpolate_na(dim="time", method="linear")
149
+
150
+ # Save the interpolated images
151
+ for idx, file in enumerate(all_raw_files):
152
+ current_data = data_np[idx].values
153
+ date = pd.to_datetime(self.df["date"].iloc[idx]).strftime("%Y-%m-%d")
154
+ with rio.open(output_dir / f"{date}.tif", "w", **profile) as dst:
155
+ dst.write((current_data * 10000).astype(np.uint16)) # Save the interpolated image
156
+
157
+ return self._spawn(df=self.df, current_dir=output_dir)
158
+
159
+ def smooth(
160
+ self,
161
+ input_dir: str | pathlib.Path | None = None,
162
+ output_dir: str | pathlib.Path = "smoothed",
163
+ smooth_w: int = 7,
164
+ smooth_p: int = 2,
165
+ device: str | torch.device = "cpu",
166
+ ) -> "SatCubeMetadata":
167
+
168
+ input_dir = pathlib.Path(input_dir).expanduser().resolve() if input_dir else self._dir()
169
+ output_dir = pathlib.Path(output_dir).resolve()
170
+ output_dir.mkdir(parents=True, exist_ok=True)
171
+
172
+ all_raw_files = list(input_dir / self.df["outname"])
173
+ out_files = [output_dir / file.name for file in all_raw_files]
174
+
175
+ profile = rio.open(all_raw_files[0]).profile
176
+ data_np = (np.array([rio.open(file).read() for file in all_raw_files]) / 10000).astype(np.float32)
177
+
178
+ data_month = pd.to_datetime(self.df["date"]).dt.month
179
+ data_clim = []
180
+ for month in range(1, 13):
181
+ data_clim.append(data_np[data_month == month].mean(axis=0) / 10000)
182
+ data_clim = np.array(data_clim)
183
+
184
+ for idx, month in enumerate(data_month):
185
+ data_np[idx] = data_np[idx] - data_clim[month - 1]
186
+
187
+ # Smooth the residuals using Gaussian smoothing
188
+ data_np = torch.from_numpy(data_np).float().to(device)
189
+ try:
190
+ data_np = (
191
+ gaussian_smooth(
192
+ data_np, kernel_size=smooth_w, sigma=smooth_p
193
+ ).cpu().numpy()
194
+ )
195
+ except Exception as e:
196
+ print(e)
197
+ data_np = data_np.cpu().numpy()
198
+
199
+ # Add the residuals back to the climatology
200
+ for idx, month in enumerate(data_month):
201
+ data_np[idx] = data_np[idx] + data_clim[month - 1]
202
+
203
+ # Save the smoothed images
204
+ for idx, file in enumerate(out_files):
205
+ with rio.open(file, "w", **profile) as dst:
206
+ dst.write((data_np[idx] * 10000).astype(np.uint16))
207
+
208
+ # Prepare the new table
209
+ new_table = pd.DataFrame(
210
+ {
211
+ "date": self.df["date"],
212
+ "outname": self.df["outname"],
213
+ }
59
214
  )
60
- self.masked_dir = pathlib.Path(output_dir).resolve()
61
- return self
215
+
216
+ return self._spawn(df=new_table, current_dir=output_dir)
217
+
218
+ def filter_metadata(self, condition) -> "SatCubeMetadata":
219
+ filtered_df = self.df[condition(self.df)]
220
+ return self._spawn(df=filtered_df, current_dir=self._current_dir)
221
+
222
+ def __repr__(self) -> str:
223
+ return self.df.__repr__()
224
+
225
+ __str__ = __repr__
226
+
227
+ def _repr_html_(self) -> str:
228
+ html = getattr(self.df, "_repr_html_", None)
229
+ return html() if callable(html) else self.df.__repr__()
62
230
 
63
231
  def __getattr__(self, item):
64
232
  return getattr(self.df, item)
65
233
 
66
234
  def __getitem__(self, key):
67
- return self.df.__getitem__(key)
68
-
235
+ if isinstance(key, pd.Series) or isinstance(key, pd.DataFrame):
236
+ filtered_df = self.df[key]
237
+ return self._spawn(df=filtered_df, current_dir=self._current_dir)
238
+ return self.df[key]
69
239
  def __len__(self):
70
240
  return len(self.df)
71
-
241
+
242
+ def update_metadata(self, new_df: pd.DataFrame) -> None:
243
+ self.df = new_df
satcube/smooth.py ADDED
@@ -0,0 +1,46 @@
1
+ import torch.nn.functional as F
2
+ import torch
3
+ import numpy as np
4
+
5
+ def gaussian_kernel1d(kernel_size: int, sigma: float):
6
+ """
7
+ Returns a 1D Gaussian kernel.
8
+ """
9
+ # Create a tensor with evenly spaced values centered at 0
10
+ x = torch.linspace(-(kernel_size // 2), kernel_size // 2, kernel_size)
11
+ # Calculate the Gaussian function
12
+ kernel = torch.exp(-(x**2) / (2 * sigma**2))
13
+ # Normalize the kernel to ensure the sum of all elements is 1
14
+ kernel = kernel / kernel.sum()
15
+ return kernel
16
+
17
+
18
+ def gaussian_smooth(tensor, kernel_size: int, sigma: float):
19
+ """
20
+ Apply Gaussian smoothing on the time dimension (first dimension) of the input tensor.
21
+
22
+ Args:
23
+ - tensor (torch.Tensor): Input tensor of shape (T, C, H, W) where T is the time dimension.
24
+ - kernel_size (int): Size of the Gaussian kernel.
25
+ - sigma (float): Standard deviation of the Gaussian kernel.
26
+
27
+ Returns:
28
+ - smoothed_tensor (torch.Tensor): Smoothed tensor.
29
+ """
30
+ if isinstance(tensor, np.ndarray):
31
+ tensor = torch.from_numpy(tensor)
32
+ # Get the Gaussian kernel
33
+ kernel = gaussian_kernel1d(kernel_size, sigma).to(tensor.device).view(1, 1, -1)
34
+
35
+ # Prepare the tensor for convolution: (B, C, T) where B = C*H*W, C=1, T=102
36
+ T, C, H, W = tensor.shape
37
+ tensor = tensor.view(T, -1).permute(1, 0).unsqueeze(1) # Shape: (C*H*W, 1, T)
38
+
39
+ # Apply convolution
40
+ padding = kernel_size // 2
41
+ smoothed = F.conv1d(tensor, kernel, padding=padding, groups=1)
42
+
43
+ # Reshape back to original shape
44
+ smoothed = smoothed.squeeze(1).permute(1, 0).view(T, C, H, W)
45
+
46
+ return smoothed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satcube
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: A Python package to create cloud-free monthly composites by fusing Landsat and Sentinel-2 data.
5
5
  Home-page: https://github.com/IPL-UV/satcube
6
6
  Author: Cesar Aybar
@@ -0,0 +1,17 @@
1
+ satcube/__init__.py,sha256=LtMqWBMAhz_QJQAQweBmKKJJoSqCFZ0ptCuxZ0WUPRs,209
2
+ satcube/align.py,sha256=Ej5v2lLMtbAFQnme6GCOiTHVNCni2WDVGLK5BEvTYO8,3674
3
+ satcube/archive_cloud_detection.py,sha256=HK8hTmxHziFGux6S91Rvob5FA0tuaKyES6FdG7Wiupo,749
4
+ satcube/archive_dataclass.py,sha256=v_ReYzzwAK7bQVBXLhdcCHZZJ1-Vbibx5vtZ11SYDK0,933
5
+ satcube/archive_main.py,sha256=QfdVyJcwCXMMtIFCOlVU7FoilDpkx9Oa6jN2MamFPhI,14852
6
+ satcube/archive_utils.py,sha256=hnmUF5052PK4NJxUZCHSIWvekU6AllvHc2bxpF2TaXI,35064
7
+ satcube/cloud.py,sha256=PL_wFH2HkfYXOELbxLcY-L5KCwBq0qX0BKS9X4w4t5s,7910
8
+ satcube/composite.py,sha256=H0cZyEgiAGqB2KjHYrkW8HFSyaxieDGCd8h0tS-D36E,2773
9
+ satcube/download.py,sha256=zWnsmxwDhg8TtLiRpPVPmc8vcceLH1sVMemx3aLQuwE,1453
10
+ satcube/gapfill.py,sha256=Wpdx7JejxFgE3pdyV53-hqsT05-5IOPfEEuDhBhG8Dc,6763
11
+ satcube/objects.py,sha256=B9v0LXzdd6Hg35Gxugd7Gv8pPH3gMYbwrv9fVPydV28,8385
12
+ satcube/smooth.py,sha256=B7laeb3Z3QQLzGx22wIsCEt_F6PZevOXG59ZzzYgUg8,1614
13
+ satcube/utils.py,sha256=QBdmSg6_4Vy-4mXH1Z3Z2AvbIKAXBYi5g3-IgWSE1MY,3660
14
+ satcube-0.1.18.dist-info/LICENSE,sha256=YdB4BQMkMzWuKvXRIpQR4g91IQ_pwA5PSH2lNM97zFI,1070
15
+ satcube-0.1.18.dist-info/METADATA,sha256=5dI7dilinMGbBhWCJ2R65yRKrJGofng6yBzMMvD03eU,6559
16
+ satcube-0.1.18.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
17
+ satcube-0.1.18.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- satcube/__init__.py,sha256=1q5c8Kx1dePuPBo29O7-1MA0L_GdBhi56gbxjQAa1_o,317
2
- satcube/align.py,sha256=0QOzX9f-r05F8niI91xlvifmyHaAQjWgBit_4Jc6OeM,2979
3
- satcube/cloud_detection.py,sha256=05OifUtWjE5jco8Q1jf_7qoQ45qrnJQgRc6LrPlMkLI,7898
4
- satcube/download.py,sha256=o8kwPzipU1f_pV0KNPLmLmjUEsoWb6yhkCt7kerDViU,1545
5
- satcube/objects.py,sha256=zhiYu-rUTm65TuEl2qIo7kLfpdam17_6CZl2KBCT9Jw,1988
6
- satcube/utils.py,sha256=QBdmSg6_4Vy-4mXH1Z3Z2AvbIKAXBYi5g3-IgWSE1MY,3660
7
- satcube-0.1.16.dist-info/LICENSE,sha256=YdB4BQMkMzWuKvXRIpQR4g91IQ_pwA5PSH2lNM97zFI,1070
8
- satcube-0.1.16.dist-info/METADATA,sha256=pjBSFmFAtEkGJfNxoDQTWtPTaLP_ilYK3Mp8slwS9uU,6559
9
- satcube-0.1.16.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
10
- satcube-0.1.16.dist-info/RECORD,,