satcube 0.1.17__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/__init__.py +2 -4
- satcube/align.py +85 -44
- satcube/archive_cloud_detection.py +23 -0
- satcube/archive_dataclass.py +39 -0
- satcube/archive_main.py +453 -0
- satcube/archive_utils.py +1087 -0
- satcube/{cloud_detection.py → cloud.py} +100 -95
- satcube/composite.py +85 -0
- satcube/download.py +2 -5
- satcube/gapfill.py +216 -0
- satcube/objects.py +208 -36
- satcube/smooth.py +46 -0
- {satcube-0.1.17.dist-info → satcube-0.1.18.dist-info}/METADATA +1 -1
- satcube-0.1.18.dist-info/RECORD +17 -0
- satcube-0.1.17.dist-info/RECORD +0 -10
- {satcube-0.1.17.dist-info → satcube-0.1.18.dist-info}/LICENSE +0 -0
- {satcube-0.1.17.dist-info → satcube-0.1.18.dist-info}/WHEEL +0 -0
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
|
|
8
|
-
from satcube.
|
|
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
|
|
13
|
-
raw_dir: pathlib.Path
|
|
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
|
-
|
|
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=
|
|
47
|
+
output_dir=self.aligned_dir,
|
|
40
48
|
nworks=nworks,
|
|
41
49
|
cache=cache
|
|
42
50
|
)
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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,,
|
satcube-0.1.17.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
satcube/__init__.py,sha256=1q5c8Kx1dePuPBo29O7-1MA0L_GdBhi56gbxjQAa1_o,317
|
|
2
|
-
satcube/align.py,sha256=_WCNgI0KhrI8b_aEnR-ecnqaILBV_eCdTuR0V18FwPA,3007
|
|
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.17.dist-info/LICENSE,sha256=YdB4BQMkMzWuKvXRIpQR4g91IQ_pwA5PSH2lNM97zFI,1070
|
|
8
|
-
satcube-0.1.17.dist-info/METADATA,sha256=JH4s1yC_43FND-i42X5kXA5W-3UdGa62htVk-Hq-_l8,6559
|
|
9
|
-
satcube-0.1.17.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
10
|
-
satcube-0.1.17.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|