cubexpress 0.1.10__tar.gz → 0.1.12__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.
Potentially problematic release.
This version of cubexpress might be problematic. Click here for more details.
- {cubexpress-0.1.10 → cubexpress-0.1.12}/PKG-INFO +2 -3
- {cubexpress-0.1.10 → cubexpress-0.1.12}/README.md +1 -1
- {cubexpress-0.1.10 → cubexpress-0.1.12}/cubexpress/__init__.py +7 -6
- {cubexpress-0.1.10 → cubexpress-0.1.12}/cubexpress/cloud_utils.py +68 -58
- {cubexpress-0.1.10 → cubexpress-0.1.12}/cubexpress/cube.py +34 -53
- cubexpress-0.1.12/cubexpress/downloader.py +111 -0
- cubexpress-0.1.12/cubexpress/geospatial.py +119 -0
- {cubexpress-0.1.10 → cubexpress-0.1.12}/cubexpress/geotyping.py +2 -7
- {cubexpress-0.1.10 → cubexpress-0.1.12}/cubexpress/request.py +24 -17
- {cubexpress-0.1.10 → cubexpress-0.1.12}/pyproject.toml +2 -3
- cubexpress-0.1.10/cubexpress/downloader.py +0 -135
- cubexpress-0.1.10/cubexpress/geospatial.py +0 -55
- {cubexpress-0.1.10 → cubexpress-0.1.12}/LICENSE +0 -0
- {cubexpress-0.1.10 → cubexpress-0.1.12}/cubexpress/cache.py +0 -0
- {cubexpress-0.1.10 → cubexpress-0.1.12}/cubexpress/conversion.py +0 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cubexpress
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.12
|
|
4
4
|
Summary: Efficient processing of cubic Earth-observation (EO) data.
|
|
5
5
|
Home-page: https://github.com/andesdatacube/cubexpress
|
|
6
|
-
License: MIT
|
|
7
6
|
Keywords: earth-engine,sentinel-2,geospatial,eo,cube
|
|
8
7
|
Author: Julio Contreras
|
|
9
8
|
Author-email: contrerasnetk@gmail.com
|
|
@@ -32,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
|
32
31
|
<h1></h1>
|
|
33
32
|
|
|
34
33
|
<p align="center">
|
|
35
|
-
<img src="
|
|
34
|
+
<img src="https://raw.githubusercontent.com/andesdatacube/cubexpress/refs/heads/main/docs/logo_cubexpress.png" width="39%">
|
|
36
35
|
</p>
|
|
37
36
|
|
|
38
37
|
<p align="center">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from cubexpress.conversion import lonlat2rt, geo2utm
|
|
2
|
-
from cubexpress.geotyping import RasterTransform, Request, RequestSet
|
|
3
|
-
from cubexpress.cloud_utils import
|
|
2
|
+
from cubexpress.geotyping import RasterTransform, Request, RequestSet, GeotransformDict
|
|
3
|
+
from cubexpress.cloud_utils import s2_table
|
|
4
4
|
from cubexpress.cube import get_cube
|
|
5
5
|
from cubexpress.request import table_to_requestset
|
|
6
6
|
|
|
@@ -11,15 +11,16 @@ from cubexpress.request import table_to_requestset
|
|
|
11
11
|
__all__ = [
|
|
12
12
|
"lonlat2rt",
|
|
13
13
|
"RasterTransform",
|
|
14
|
+
"GeotransformDict",
|
|
14
15
|
"Request",
|
|
15
16
|
"RequestSet",
|
|
16
17
|
"geo2utm",
|
|
17
18
|
"get_cube",
|
|
18
|
-
"
|
|
19
|
+
"s2_table",
|
|
19
20
|
"table_to_requestset"
|
|
20
21
|
]
|
|
21
22
|
|
|
22
|
-
# Dynamic version import
|
|
23
|
-
import importlib.metadata
|
|
23
|
+
# # Dynamic version import
|
|
24
|
+
# import importlib.metadata
|
|
24
25
|
|
|
25
|
-
__version__ = importlib.metadata.version("cubexpress")
|
|
26
|
+
# __version__ = importlib.metadata.version("cubexpress")
|
|
@@ -15,9 +15,11 @@ from __future__ import annotations
|
|
|
15
15
|
import datetime as dt
|
|
16
16
|
import ee
|
|
17
17
|
import pandas as pd
|
|
18
|
-
|
|
19
18
|
from cubexpress.cache import _cache_key
|
|
19
|
+
import datetime as dt
|
|
20
20
|
from cubexpress.geospatial import _square_roi
|
|
21
|
+
import warnings
|
|
22
|
+
warnings.filterwarnings('ignore', category=DeprecationWarning)
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def _cloud_table_single_range(
|
|
@@ -55,58 +57,64 @@ def _cloud_table_single_range(
|
|
|
55
57
|
|
|
56
58
|
center = ee.Geometry.Point([lon, lat])
|
|
57
59
|
roi = _square_roi(lon, lat, edge_size, 10)
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
s2 = (
|
|
60
|
-
ee.ImageCollection("COPERNICUS/
|
|
62
|
+
ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
|
61
63
|
.filterBounds(roi)
|
|
62
64
|
.filterDate(start, end)
|
|
63
65
|
)
|
|
64
|
-
|
|
65
|
-
csp = ee.ImageCollection("GOOGLE/CLOUD_SCORE_PLUS/V1/S2_HARMONIZED")
|
|
66
|
-
|
|
67
66
|
ic = (
|
|
68
67
|
s2
|
|
69
|
-
.linkCollection(
|
|
68
|
+
.linkCollection(
|
|
69
|
+
ee.ImageCollection("GOOGLE/CLOUD_SCORE_PLUS/V1/S2_HARMONIZED"),
|
|
70
|
+
["cs_cdf"]
|
|
71
|
+
)
|
|
70
72
|
.select(["cs_cdf"])
|
|
71
73
|
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
ids_inside = (
|
|
75
|
+
ic
|
|
76
|
+
.map(
|
|
77
|
+
lambda img: img.set(
|
|
78
|
+
'roi_inside_scene',
|
|
79
|
+
img.geometry().contains(roi, maxError=10)
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
.filter(ee.Filter.eq('roi_inside_scene', True))
|
|
83
|
+
.aggregate_array('system:index')
|
|
84
|
+
.getInfo()
|
|
85
|
+
)
|
|
86
|
+
|
|
81
87
|
try:
|
|
82
|
-
raw = ic.getRegion(
|
|
88
|
+
raw = ic.getRegion(
|
|
89
|
+
geometry=center,
|
|
90
|
+
scale=(edge_size) * 11
|
|
91
|
+
).getInfo()
|
|
83
92
|
except ee.ee_exception.EEException as e:
|
|
84
93
|
if "No bands in collection" in str(e):
|
|
85
94
|
return pd.DataFrame(
|
|
86
|
-
columns=["id", "
|
|
95
|
+
columns=["id", "longitude", "latitude", "time", "cs_cdf", "inside"]
|
|
87
96
|
)
|
|
88
|
-
raise
|
|
89
|
-
|
|
90
|
-
df_raw =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
df = (
|
|
94
|
-
df_ids
|
|
95
|
-
.merge(df_raw, on="id", how="left")
|
|
97
|
+
raise e
|
|
98
|
+
|
|
99
|
+
df_raw = (
|
|
100
|
+
pd.DataFrame(raw[1:], columns=raw[0])
|
|
101
|
+
.drop(columns=["longitude", "latitude"])
|
|
96
102
|
.assign(
|
|
97
|
-
date=lambda d: pd.to_datetime(d["id"].str[:8], format="%Y%m%d").dt.strftime("%Y-%m-%d")
|
|
98
|
-
null_flag=lambda d: d["cs_cdf"].isna().astype(int),
|
|
103
|
+
date=lambda d: pd.to_datetime(d["id"].str[:8], format="%Y%m%d").dt.strftime("%Y-%m-%d")
|
|
99
104
|
)
|
|
100
|
-
.drop(columns=["longitude", "latitude", "time"])
|
|
101
105
|
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
df_raw["inside"] = df_raw["id"].isin(set(ids_inside)).astype(int)
|
|
107
|
+
df_raw['cs_cdf'] = df_raw.groupby('date').apply(
|
|
108
|
+
lambda group: group['cs_cdf'].transform(
|
|
109
|
+
lambda _: group[group['inside'] == 1]['cs_cdf'].iloc[0]
|
|
110
|
+
if (group['inside'] == 1).any()
|
|
111
|
+
else group['cs_cdf'].mean()
|
|
112
|
+
)
|
|
113
|
+
).reset_index(drop=True)
|
|
114
|
+
|
|
115
|
+
return df_raw
|
|
116
|
+
|
|
117
|
+
def s2_table(
|
|
110
118
|
lon: float,
|
|
111
119
|
lat: float,
|
|
112
120
|
edge_size: int,
|
|
@@ -114,8 +122,7 @@ def s2_cloud_table(
|
|
|
114
122
|
end: str,
|
|
115
123
|
max_cscore: float = 1.0,
|
|
116
124
|
min_cscore: float = 0.0,
|
|
117
|
-
cache: bool = False
|
|
118
|
-
verbose: bool = True,
|
|
125
|
+
cache: bool = False
|
|
119
126
|
) -> pd.DataFrame:
|
|
120
127
|
"""Build (and cache) a per-day cloud-table for the requested ROI.
|
|
121
128
|
|
|
@@ -144,9 +151,7 @@ def s2_cloud_table(
|
|
|
144
151
|
Downstream path hint stored in ``result.attrs``; not used internally.
|
|
145
152
|
cache
|
|
146
153
|
Toggle parquet caching.
|
|
147
|
-
|
|
148
|
-
If *True* prints cache info/progress.
|
|
149
|
-
|
|
154
|
+
|
|
150
155
|
Returns
|
|
151
156
|
-------
|
|
152
157
|
pandas.DataFrame
|
|
@@ -158,10 +163,9 @@ def s2_cloud_table(
|
|
|
158
163
|
scale = 10
|
|
159
164
|
cache_file = _cache_key(lon, lat, edge_size, scale, collection)
|
|
160
165
|
|
|
161
|
-
#
|
|
166
|
+
# Load cached data if present
|
|
162
167
|
if cache and cache_file.exists():
|
|
163
|
-
|
|
164
|
-
print("📂 Loading cached metadata …")
|
|
168
|
+
print("📂 Loading cached metadata …")
|
|
165
169
|
df_cached = pd.read_parquet(cache_file)
|
|
166
170
|
have_idx = pd.to_datetime(df_cached["date"], errors="coerce").dropna()
|
|
167
171
|
|
|
@@ -172,8 +176,7 @@ def s2_cloud_table(
|
|
|
172
176
|
dt.date.fromisoformat(start) >= cached_start
|
|
173
177
|
and dt.date.fromisoformat(end) <= cached_end
|
|
174
178
|
):
|
|
175
|
-
|
|
176
|
-
print("✅ Served entirely from metadata.")
|
|
179
|
+
print("✅ Served entirely from metadata.")
|
|
177
180
|
df_full = df_cached
|
|
178
181
|
else:
|
|
179
182
|
# Identify missing segments and fetch only those.
|
|
@@ -182,14 +185,22 @@ def s2_cloud_table(
|
|
|
182
185
|
a1, b1 = start, cached_start.isoformat()
|
|
183
186
|
df_new_parts.append(
|
|
184
187
|
_cloud_table_single_range(
|
|
185
|
-
lon,
|
|
188
|
+
lon=lon,
|
|
189
|
+
lat=lat,
|
|
190
|
+
edge_size=edge_size,
|
|
191
|
+
start=a1,
|
|
192
|
+
end=b1
|
|
186
193
|
)
|
|
187
194
|
)
|
|
188
195
|
if dt.date.fromisoformat(end) > cached_end:
|
|
189
196
|
a2, b2 = cached_end.isoformat(), end
|
|
190
197
|
df_new_parts.append(
|
|
191
198
|
_cloud_table_single_range(
|
|
192
|
-
lon,
|
|
199
|
+
lon=lon,
|
|
200
|
+
lat=lat,
|
|
201
|
+
edge_size=edge_size,
|
|
202
|
+
start=a2,
|
|
203
|
+
end=b2
|
|
193
204
|
)
|
|
194
205
|
)
|
|
195
206
|
df_new_parts = [df for df in df_new_parts if not df.empty]
|
|
@@ -204,21 +215,20 @@ def s2_cloud_table(
|
|
|
204
215
|
else:
|
|
205
216
|
df_full = df_cached
|
|
206
217
|
else:
|
|
207
|
-
|
|
208
|
-
if verbose:
|
|
209
|
-
msg = "Generating metadata (no cache found)…" if cache else "Generating metadata…"
|
|
210
|
-
print("⏳", msg)
|
|
218
|
+
print("⏳ Generating metadata…")
|
|
211
219
|
df_full = _cloud_table_single_range(
|
|
212
|
-
lon,
|
|
220
|
+
lon=lon,
|
|
221
|
+
lat=lat,
|
|
222
|
+
edge_size=edge_size,
|
|
223
|
+
start=start,
|
|
224
|
+
end=end
|
|
213
225
|
)
|
|
214
|
-
|
|
215
226
|
|
|
216
|
-
#
|
|
227
|
+
# Save cache
|
|
217
228
|
if cache:
|
|
218
229
|
df_full.to_parquet(cache_file, compression="zstd")
|
|
219
230
|
|
|
220
|
-
#
|
|
221
|
-
|
|
231
|
+
# Filter by cloud cover and requested date window
|
|
222
232
|
result = (
|
|
223
233
|
df_full.query("@start <= date <= @end")
|
|
224
234
|
.query("@min_cscore <= cs_cdf <= @max_cscore")
|
|
@@ -14,23 +14,23 @@ The core download/split logic lives in *cubexpress.downloader* and
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import pathlib
|
|
17
|
-
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
18
18
|
from typing import Dict, Any
|
|
19
19
|
import ee
|
|
20
|
+
from tqdm import tqdm
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
from cubexpress.downloader import download_manifest, download_manifests
|
|
23
24
|
from cubexpress.geospatial import quadsplit_manifest, calculate_cell_size
|
|
24
25
|
from cubexpress.request import table_to_requestset
|
|
25
26
|
import pandas as pd
|
|
27
|
+
from cubexpress.geotyping import RequestSet
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
def get_geotiff(
|
|
29
31
|
manifest: Dict[str, Any],
|
|
30
32
|
full_outname: pathlib.Path | str,
|
|
31
|
-
|
|
32
|
-
nworks: int = 4,
|
|
33
|
-
verbose: bool = True,
|
|
33
|
+
nworks: int = 4
|
|
34
34
|
) -> None:
|
|
35
35
|
"""Download *manifest* to *full_outname*, retrying with tiled requests.
|
|
36
36
|
|
|
@@ -43,28 +43,27 @@ def get_geotiff(
|
|
|
43
43
|
nworks
|
|
44
44
|
Maximum worker threads when the image must be split; default **4**.
|
|
45
45
|
"""
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
try:
|
|
48
|
-
download_manifest(
|
|
48
|
+
download_manifest(
|
|
49
|
+
ulist=manifest,
|
|
50
|
+
full_outname=full_outname
|
|
51
|
+
)
|
|
49
52
|
except ee.ee_exception.EEException as err:
|
|
50
|
-
|
|
51
|
-
size = manifest["grid"]["dimensions"]["width"] # square images assumed
|
|
53
|
+
size = manifest["grid"]["dimensions"]["width"]
|
|
52
54
|
cell_w, cell_h, power = calculate_cell_size(str(err), size)
|
|
53
55
|
tiled = quadsplit_manifest(manifest, cell_w, cell_h, power)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
|
|
57
|
+
download_manifests(
|
|
58
|
+
manifests=tiled,
|
|
59
|
+
full_outname=full_outname,
|
|
60
|
+
max_workers=nworks
|
|
61
|
+
)
|
|
59
62
|
|
|
60
63
|
def get_cube(
|
|
61
|
-
|
|
64
|
+
requests: pd.DataFrame | RequestSet,
|
|
62
65
|
outfolder: pathlib.Path | str,
|
|
63
|
-
|
|
64
|
-
join: bool = True,
|
|
65
|
-
nworks: int = 4,
|
|
66
|
-
verbose: bool = True,
|
|
67
|
-
cache: bool = True
|
|
66
|
+
nworks: int = 4
|
|
68
67
|
) -> None:
|
|
69
68
|
"""Download every request in *requests* to *outfolder* using a thread pool.
|
|
70
69
|
|
|
@@ -80,40 +79,22 @@ def get_cube(
|
|
|
80
79
|
nworks
|
|
81
80
|
Pool size for concurrent downloads; default **4**.
|
|
82
81
|
"""
|
|
83
|
-
|
|
84
|
-
requests = table_to_requestset(
|
|
85
|
-
table=table,
|
|
86
|
-
mosaic=mosaic
|
|
87
|
-
)
|
|
88
82
|
|
|
89
83
|
outfolder = pathlib.Path(outfolder).expanduser().resolve()
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
join,
|
|
104
|
-
nworks,
|
|
105
|
-
verbose
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
for fut in concurrent.futures.as_completed(futures):
|
|
84
|
+
outfolder.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
dataframe = requests._dataframe if isinstance(requests, RequestSet) else requests
|
|
86
|
+
|
|
87
|
+
with ThreadPoolExecutor(max_workers=nworks) as executor:
|
|
88
|
+
futures = {
|
|
89
|
+
executor.submit(
|
|
90
|
+
get_geotiff,
|
|
91
|
+
manifest=row.manifest,
|
|
92
|
+
full_outname=pathlib.Path(outfolder) / f"{row.id}.tif",
|
|
93
|
+
nworks=nworks
|
|
94
|
+
): row.id for _, row in dataframe.iterrows()
|
|
95
|
+
}
|
|
96
|
+
for future in tqdm(as_completed(futures), total=len(futures)):
|
|
110
97
|
try:
|
|
111
|
-
|
|
112
|
-
except Exception as exc:
|
|
113
|
-
print(f"Download error: {exc}")
|
|
114
|
-
|
|
115
|
-
download_df = requests._dataframe[["outname", "cs_cdf", "date"]].copy()
|
|
116
|
-
download_df["outname"] = outfolder / requests._dataframe["outname"]
|
|
117
|
-
download_df.rename(columns={"outname": "full_outname"}, inplace=True)
|
|
118
|
-
|
|
119
|
-
return download_df
|
|
98
|
+
future.result()
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
print(f"Download error for {futures[future]}: {exc}")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Low-level download helpers for Earth Engine manifests.
|
|
2
|
+
|
|
3
|
+
Only two public callables are exposed:
|
|
4
|
+
|
|
5
|
+
* :func:`download_manifest` – fetch a single manifest and write one GeoTIFF.
|
|
6
|
+
* :func:`download_manifests` – convenience wrapper to parallel-download a list
|
|
7
|
+
of manifests with a thread pool.
|
|
8
|
+
|
|
9
|
+
Both functions are fully I/O bound; no return value is expected.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import pathlib
|
|
16
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
17
|
+
from copy import deepcopy
|
|
18
|
+
from typing import Any, Dict
|
|
19
|
+
|
|
20
|
+
import ee
|
|
21
|
+
import rasterio as rio
|
|
22
|
+
from rasterio.io import MemoryFile
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import tempfile
|
|
27
|
+
from cubexpress.geospatial import merge_tifs
|
|
28
|
+
|
|
29
|
+
os.environ['CPL_LOG_ERRORS'] = 'OFF'
|
|
30
|
+
logging.getLogger('rasterio._env').setLevel(logging.ERROR)
|
|
31
|
+
|
|
32
|
+
def download_manifest(
|
|
33
|
+
ulist: Dict[str, Any],
|
|
34
|
+
full_outname: pathlib.Path
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Download *ulist* and save it as *full_outname*.
|
|
37
|
+
|
|
38
|
+
The manifest must include either an ``assetId`` or an ``expression``
|
|
39
|
+
(serialized EE image). RasterIO is used to write a tiled, compressed
|
|
40
|
+
GeoTIFF; the function is silent apart from the final ``print``.
|
|
41
|
+
"""
|
|
42
|
+
if "assetId" in ulist:
|
|
43
|
+
images_bytes = ee.data.getPixels(ulist)
|
|
44
|
+
elif "expression" in ulist:
|
|
45
|
+
ee_image = ee.deserializer.decode(json.loads(ulist["expression"]))
|
|
46
|
+
ulist_deep = deepcopy(ulist)
|
|
47
|
+
ulist_deep["expression"] = ee_image
|
|
48
|
+
images_bytes = ee.data.computePixels(ulist_deep)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError("Manifest does not contain 'assetId' or 'expression'")
|
|
51
|
+
|
|
52
|
+
with open(full_outname, "wb") as src:
|
|
53
|
+
src.write(images_bytes)
|
|
54
|
+
|
|
55
|
+
# with MemoryFile(images_bytes) as memfile:
|
|
56
|
+
# with memfile.open() as src:
|
|
57
|
+
# profile = src.profile
|
|
58
|
+
# profile.update(
|
|
59
|
+
# driver="GTiff",
|
|
60
|
+
# tiled=True,
|
|
61
|
+
# interleave="band",
|
|
62
|
+
# blockxsize=256,
|
|
63
|
+
# blockysize=256,
|
|
64
|
+
# compress="ZSTD",
|
|
65
|
+
# zstd_level=13,
|
|
66
|
+
# predictor=2,
|
|
67
|
+
# num_threads=20,
|
|
68
|
+
# nodata=65535,
|
|
69
|
+
# dtype="uint16",
|
|
70
|
+
# count=12,
|
|
71
|
+
# photometric="MINISBLACK"
|
|
72
|
+
# )
|
|
73
|
+
|
|
74
|
+
# with rio.open(full_outname, "w", **profile) as dst:
|
|
75
|
+
# dst.write(src.read())
|
|
76
|
+
|
|
77
|
+
def download_manifests(
|
|
78
|
+
manifests: list[Dict[str, Any]],
|
|
79
|
+
full_outname: pathlib.Path,
|
|
80
|
+
max_workers: int,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Download every manifest in *manifests* concurrently.
|
|
83
|
+
|
|
84
|
+
Each output file is saved in the folder
|
|
85
|
+
``full_outname.parent/full_outname.stem`` with names ``000000.tif``,
|
|
86
|
+
``000001.tif`` … according to the list order.
|
|
87
|
+
"""
|
|
88
|
+
tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="cubexpress_"))
|
|
89
|
+
full_outname_temp = tmp_dir / full_outname.stem
|
|
90
|
+
full_outname_temp.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
with ThreadPoolExecutor(max_workers=max_workers) as exe: # -
|
|
93
|
+
futures = {
|
|
94
|
+
exe.submit(
|
|
95
|
+
download_manifest,
|
|
96
|
+
ulist=umanifest,
|
|
97
|
+
full_outname=full_outname_temp / f"{index:06d}.tif"
|
|
98
|
+
): umanifest for index, umanifest in enumerate(manifests)
|
|
99
|
+
}
|
|
100
|
+
for future in as_completed(futures):
|
|
101
|
+
try:
|
|
102
|
+
future.result()
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
print(f"Error in one of the downloads: {exc}")
|
|
105
|
+
|
|
106
|
+
if full_outname_temp.exists():
|
|
107
|
+
input_files = sorted(full_outname_temp.glob("*.tif"))
|
|
108
|
+
merge_tifs(input_files, full_outname)
|
|
109
|
+
shutil.rmtree(full_outname_temp)
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError(f"Error in {full_outname}")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import ee
|
|
2
|
+
import re
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Dict
|
|
5
|
+
import pathlib
|
|
6
|
+
import rasterio as rio
|
|
7
|
+
from rasterio.merge import merge
|
|
8
|
+
from rasterio.enums import Resampling
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def quadsplit_manifest(manifest: Dict, cell_width: int, cell_height: int, power: int) -> list[Dict]:
|
|
13
|
+
manifest_copy = deepcopy(manifest)
|
|
14
|
+
|
|
15
|
+
manifest_copy["grid"]["dimensions"]["width"] = cell_width
|
|
16
|
+
manifest_copy["grid"]["dimensions"]["height"] = cell_height
|
|
17
|
+
x = manifest_copy["grid"]["affineTransform"]["translateX"]
|
|
18
|
+
y = manifest_copy["grid"]["affineTransform"]["translateY"]
|
|
19
|
+
scale_x = manifest_copy["grid"]["affineTransform"]["scaleX"]
|
|
20
|
+
scale_y = manifest_copy["grid"]["affineTransform"]["scaleY"]
|
|
21
|
+
|
|
22
|
+
manifests = []
|
|
23
|
+
|
|
24
|
+
for columny in range(2**power):
|
|
25
|
+
for rowx in range(2**power):
|
|
26
|
+
new_x = x + (rowx * cell_width) * scale_x
|
|
27
|
+
new_y = y + (columny * cell_height) * scale_y
|
|
28
|
+
new_manifest = deepcopy(manifest_copy)
|
|
29
|
+
new_manifest["grid"]["affineTransform"]["translateX"] = new_x
|
|
30
|
+
new_manifest["grid"]["affineTransform"]["translateY"] = new_y
|
|
31
|
+
manifests.append(new_manifest)
|
|
32
|
+
|
|
33
|
+
return manifests
|
|
34
|
+
|
|
35
|
+
def calculate_cell_size(ee_error_message: str, size: int) -> tuple[int, int]:
|
|
36
|
+
match = re.findall(r'\d+', ee_error_message)
|
|
37
|
+
image_pixel = int(match[0])
|
|
38
|
+
max_pixel = int(match[1])
|
|
39
|
+
|
|
40
|
+
images = image_pixel / max_pixel
|
|
41
|
+
power = 0
|
|
42
|
+
|
|
43
|
+
while images > 1:
|
|
44
|
+
power += 1
|
|
45
|
+
images = image_pixel / (max_pixel * 4 ** power)
|
|
46
|
+
|
|
47
|
+
cell_width = size // 2 ** power
|
|
48
|
+
cell_height = size // 2 ** power
|
|
49
|
+
|
|
50
|
+
return cell_width, cell_height, power
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _square_roi(lon: float, lat: float, edge_size: int, scale: int) -> ee.Geometry:
|
|
55
|
+
"""Return a square `ee.Geometry` centred on (*lon*, *lat*)."""
|
|
56
|
+
half = edge_size * scale / 2
|
|
57
|
+
point = ee.Geometry.Point([lon, lat])
|
|
58
|
+
return point.buffer(half).bounds()
|
|
59
|
+
|
|
60
|
+
def merge_tifs(
|
|
61
|
+
input_files: list[pathlib.Path],
|
|
62
|
+
output_path: pathlib.Path,
|
|
63
|
+
*,
|
|
64
|
+
nodata: int = 65535,
|
|
65
|
+
gdal_threads: int = 8
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Merge a list of GeoTIFF files into a single mosaic and write it out.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
input_files : list[Path]
|
|
73
|
+
Paths to the GeoTIFF tiles to be merged.
|
|
74
|
+
output_path : Path
|
|
75
|
+
Destination path for the merged GeoTIFF.
|
|
76
|
+
nodata : int, optional
|
|
77
|
+
NoData value to assign in the mosaic (default: 65535).
|
|
78
|
+
gdal_threads : int, optional
|
|
79
|
+
Number of GDAL threads to use for reading/writing (default: 8).
|
|
80
|
+
|
|
81
|
+
Raises
|
|
82
|
+
------
|
|
83
|
+
ValueError
|
|
84
|
+
If `input_files` is empty.
|
|
85
|
+
"""
|
|
86
|
+
if not input_files:
|
|
87
|
+
raise ValueError("The input_files list is empty")
|
|
88
|
+
|
|
89
|
+
# Ensure output path is a Path object
|
|
90
|
+
output_path = pathlib.Path(output_path).expanduser().resolve()
|
|
91
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
# Set GDAL threading environment
|
|
94
|
+
with rio.Env(GDAL_NUM_THREADS=str(gdal_threads), NUM_THREADS=str(gdal_threads)):
|
|
95
|
+
# Open all source datasets
|
|
96
|
+
srcs = [rio.open(fp) for fp in input_files]
|
|
97
|
+
try:
|
|
98
|
+
# Merge sources into one mosaic
|
|
99
|
+
mosaic, out_transform = merge(
|
|
100
|
+
srcs,
|
|
101
|
+
nodata=nodata,
|
|
102
|
+
resampling=Resampling.nearest
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Copy metadata from the first source and update it
|
|
106
|
+
meta = srcs[0].profile.copy()
|
|
107
|
+
meta.update({
|
|
108
|
+
"transform": out_transform,
|
|
109
|
+
"height": mosaic.shape[1],
|
|
110
|
+
"width": mosaic.shape[2]
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
# Write the merged mosaic to disk
|
|
114
|
+
with rio.open(output_path, "w", **meta) as dst:
|
|
115
|
+
dst.write(mosaic)
|
|
116
|
+
finally:
|
|
117
|
+
# Always close all open datasets
|
|
118
|
+
for src in srcs:
|
|
119
|
+
src.close()
|
|
@@ -259,13 +259,8 @@ class RequestSet(BaseModel):
|
|
|
259
259
|
def create_manifests(self) -> pd.DataFrame:
|
|
260
260
|
"""
|
|
261
261
|
Exports the raster metadata to a pandas DataFrame.
|
|
262
|
-
|
|
263
262
|
Returns:
|
|
264
263
|
pd.DataFrame: A DataFrame containing the metadata for all entries.
|
|
265
|
-
|
|
266
|
-
Example:
|
|
267
|
-
>>> df = raster_transform_set.export_df()
|
|
268
|
-
>>> print(df)
|
|
269
264
|
"""
|
|
270
265
|
# Use ProcessPoolExecutor for CPU-bound tasks to convert raster transforms to lon/lat
|
|
271
266
|
with ProcessPoolExecutor(max_workers=None) as executor:
|
|
@@ -306,8 +301,8 @@ class RequestSet(BaseModel):
|
|
|
306
301
|
"crsCode": meta.raster_transform.crs,
|
|
307
302
|
},
|
|
308
303
|
},
|
|
309
|
-
"cs_cdf": int(meta.id.split("_")[-1]) / 100,
|
|
310
|
-
"date": meta.id.split("_")[0],
|
|
304
|
+
# "cs_cdf": int(meta.id.split("_")[-1]) / 100,
|
|
305
|
+
# "date": meta.id.split("_")[0],
|
|
311
306
|
"outname": f"{meta.id}.tif",
|
|
312
307
|
}
|
|
313
308
|
|
|
@@ -11,9 +11,9 @@ from cubexpress.conversion import lonlat2rt
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def table_to_requestset(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
table: pd.DataFrame,
|
|
15
|
+
mosaic: bool = True
|
|
16
|
+
) -> RequestSet:
|
|
17
17
|
"""Return a :class:`RequestSet` built from *df* (cloud_table result).
|
|
18
18
|
|
|
19
19
|
Parameters
|
|
@@ -31,12 +31,11 @@ def table_to_requestset(
|
|
|
31
31
|
If *df* is empty after filtering.
|
|
32
32
|
|
|
33
33
|
"""
|
|
34
|
-
|
|
35
34
|
|
|
36
35
|
df = table.copy()
|
|
37
36
|
|
|
38
37
|
if df.empty:
|
|
39
|
-
raise ValueError("
|
|
38
|
+
raise ValueError("There are no images in the requested period. Please check your dates or your ubication.")
|
|
40
39
|
|
|
41
40
|
rt = lonlat2rt(
|
|
42
41
|
lon=df.attrs["lon"],
|
|
@@ -44,22 +43,30 @@ def table_to_requestset(
|
|
|
44
43
|
edge_size=df.attrs["edge_size"],
|
|
45
44
|
scale=df.attrs["scale"],
|
|
46
45
|
)
|
|
46
|
+
|
|
47
47
|
centre_hash = pgh.encode(df.attrs["lat"], df.attrs["lon"], precision=5)
|
|
48
|
-
reqs
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
reqs = []
|
|
51
49
|
|
|
52
50
|
if mosaic:
|
|
53
51
|
grouped = (
|
|
54
|
-
|
|
52
|
+
df.groupby('date')
|
|
55
53
|
.agg(
|
|
56
|
-
id_list
|
|
57
|
-
|
|
54
|
+
id_list = ('id', list),
|
|
55
|
+
tiles = (
|
|
56
|
+
'id',
|
|
57
|
+
lambda ids: ','.join(
|
|
58
|
+
sorted({i.split('_')[-1][1:] for i in ids})
|
|
59
|
+
)
|
|
60
|
+
),
|
|
61
|
+
cs_cdf_mean = (
|
|
62
|
+
'cs_cdf',
|
|
63
|
+
lambda x: int(round(x.mean(), 2) * 100)
|
|
64
|
+
)
|
|
58
65
|
)
|
|
59
66
|
)
|
|
60
67
|
|
|
61
68
|
for day, row in grouped.iterrows():
|
|
62
|
-
|
|
69
|
+
|
|
63
70
|
img_ids = row["id_list"]
|
|
64
71
|
cdf = row["cs_cdf_mean"]
|
|
65
72
|
|
|
@@ -79,10 +86,11 @@ def table_to_requestset(
|
|
|
79
86
|
)
|
|
80
87
|
else:
|
|
81
88
|
for img_id in img_ids:
|
|
82
|
-
tile = img_id.split("_")[-1][1:]
|
|
89
|
+
# tile = img_id.split("_")[-1][1:]
|
|
83
90
|
reqs.append(
|
|
84
91
|
Request(
|
|
85
|
-
id=f"{day}_{centre_hash}_{tile}_{cdf}",
|
|
92
|
+
# id=f"{day}_{centre_hash}_{tile}_{cdf}",
|
|
93
|
+
id=f"{day}_{centre_hash}_{cdf}",
|
|
86
94
|
raster_transform=rt,
|
|
87
95
|
image=f"{df.attrs['collection']}/{img_id}",
|
|
88
96
|
bands=df.attrs["bands"],
|
|
@@ -94,14 +102,13 @@ def table_to_requestset(
|
|
|
94
102
|
tile = img_id.split("_")[-1][1:]
|
|
95
103
|
day = row["date"]
|
|
96
104
|
cdf = int(round(row["cs_cdf"], 2) * 100)
|
|
97
|
-
|
|
98
105
|
reqs.append(
|
|
99
106
|
Request(
|
|
100
|
-
id=f"{day}_{
|
|
107
|
+
id=f"{day}_{tile}_{cdf}",
|
|
101
108
|
raster_transform=rt,
|
|
102
109
|
image=f"{df.attrs['collection']}/{img_id}",
|
|
103
110
|
bands=df.attrs["bands"],
|
|
104
111
|
)
|
|
105
112
|
)
|
|
106
113
|
|
|
107
|
-
return RequestSet(requestset=reqs)
|
|
114
|
+
return RequestSet(requestset=reqs)
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "cubexpress"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.12"
|
|
4
4
|
description = "Efficient processing of cubic Earth-observation (EO) data."
|
|
5
5
|
authors = [
|
|
6
6
|
"Julio Contreras <contrerasnetk@gmail.com>",
|
|
7
|
-
"Cesar Aybar <csaybar@gmail.com>",
|
|
8
7
|
]
|
|
9
|
-
|
|
8
|
+
|
|
10
9
|
repository = "https://github.com/andesdatacube/cubexpress"
|
|
11
10
|
documentation = "https://andesdatacube.github.io/cubexpress"
|
|
12
11
|
readme = "README.md"
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"""Low-level download helpers for Earth Engine manifests.
|
|
2
|
-
|
|
3
|
-
Only two public callables are exposed:
|
|
4
|
-
|
|
5
|
-
* :func:`download_manifest` – fetch a single manifest and write one GeoTIFF.
|
|
6
|
-
* :func:`download_manifests` – convenience wrapper to parallel-download a list
|
|
7
|
-
of manifests with a thread pool.
|
|
8
|
-
|
|
9
|
-
Both functions are fully I/O bound; no return value is expected.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import json
|
|
15
|
-
import pathlib
|
|
16
|
-
import concurrent.futures
|
|
17
|
-
from copy import deepcopy
|
|
18
|
-
from typing import Any, Dict, List
|
|
19
|
-
|
|
20
|
-
import ee
|
|
21
|
-
import rasterio as rio
|
|
22
|
-
from rasterio.io import MemoryFile
|
|
23
|
-
import logging
|
|
24
|
-
from rasterio.merge import merge
|
|
25
|
-
from rasterio.enums import Resampling
|
|
26
|
-
import os
|
|
27
|
-
import shutil
|
|
28
|
-
import tempfile
|
|
29
|
-
|
|
30
|
-
os.environ['CPL_LOG_ERRORS'] = 'OFF'
|
|
31
|
-
logging.getLogger('rasterio._env').setLevel(logging.ERROR)
|
|
32
|
-
|
|
33
|
-
def download_manifest(ulist: Dict[str, Any], full_outname: pathlib.Path) -> None:
|
|
34
|
-
"""Download *ulist* and save it as *full_outname*.
|
|
35
|
-
|
|
36
|
-
The manifest must include either an ``assetId`` or an ``expression``
|
|
37
|
-
(serialized EE image). RasterIO is used to write a tiled, compressed
|
|
38
|
-
GeoTIFF; the function is silent apart from the final ``print``.
|
|
39
|
-
"""
|
|
40
|
-
if "assetId" in ulist:
|
|
41
|
-
images_bytes = ee.data.getPixels(ulist)
|
|
42
|
-
elif "expression" in ulist:
|
|
43
|
-
ee_image = ee.deserializer.decode(json.loads(ulist["expression"]))
|
|
44
|
-
ulist_deep = deepcopy(ulist)
|
|
45
|
-
ulist_deep["expression"] = ee_image
|
|
46
|
-
images_bytes = ee.data.computePixels(ulist_deep)
|
|
47
|
-
else: # pragma: no cover
|
|
48
|
-
raise ValueError("Manifest does not contain 'assetId' or 'expression'")
|
|
49
|
-
|
|
50
|
-
with MemoryFile(images_bytes) as memfile:
|
|
51
|
-
with memfile.open() as src:
|
|
52
|
-
profile = src.profile
|
|
53
|
-
profile.update(
|
|
54
|
-
driver="GTiff",
|
|
55
|
-
tiled=True,
|
|
56
|
-
interleave="band",
|
|
57
|
-
blockxsize=256, # TODO: Creo que es 128 (por de la superresolucion)
|
|
58
|
-
blockysize=256,
|
|
59
|
-
compress="ZSTD",
|
|
60
|
-
# zstd_level=13,
|
|
61
|
-
predictor=2,
|
|
62
|
-
num_threads=20,
|
|
63
|
-
nodata=65535,
|
|
64
|
-
dtype="uint16",
|
|
65
|
-
count=13,
|
|
66
|
-
photometric="MINISBLACK"
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
with rio.open(full_outname, "w", **profile) as dst:
|
|
70
|
-
dst.write(src.read())
|
|
71
|
-
|
|
72
|
-
def download_manifests(
|
|
73
|
-
manifests: list[Dict[str, Any]],
|
|
74
|
-
full_outname: pathlib.Path,
|
|
75
|
-
join: bool = True,
|
|
76
|
-
max_workers: int = 4,
|
|
77
|
-
) -> None:
|
|
78
|
-
"""Download every manifest in *manifests* concurrently.
|
|
79
|
-
|
|
80
|
-
Each output file is saved in the folder
|
|
81
|
-
``full_outname.parent/full_outname.stem`` with names ``000000.tif``,
|
|
82
|
-
``000001.tif`` … according to the list order.
|
|
83
|
-
"""
|
|
84
|
-
# full_outname = pathlib.Path("/home/contreras/Documents/GitHub/cubexpress/cubexpress_test/2017-08-19_6mfrw_18LVN.tif")
|
|
85
|
-
original_dir = full_outname.parent
|
|
86
|
-
if join:
|
|
87
|
-
tmp_dir = pathlib.Path(tempfile.mkdtemp(prefix="s2tmp_"))
|
|
88
|
-
full_outname = tmp_dir / full_outname.name
|
|
89
|
-
|
|
90
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
91
|
-
futures = []
|
|
92
|
-
|
|
93
|
-
for index, umanifest in enumerate(manifests):
|
|
94
|
-
folder = full_outname.parent / full_outname.stem
|
|
95
|
-
folder.mkdir(parents=True, exist_ok=True)
|
|
96
|
-
outname = folder / f"{index:06d}.tif"
|
|
97
|
-
futures.append(executor.submit(download_manifest, umanifest, outname))
|
|
98
|
-
|
|
99
|
-
for fut in concurrent.futures.as_completed(futures):
|
|
100
|
-
try:
|
|
101
|
-
fut.result()
|
|
102
|
-
except Exception as exc: # noqa: BLE001
|
|
103
|
-
print(f"Error en una de las descargas: {exc}") # noqa: T201
|
|
104
|
-
|
|
105
|
-
dir_path = full_outname.parent / full_outname.stem
|
|
106
|
-
input_files = sorted(dir_path.glob("*.tif"))
|
|
107
|
-
|
|
108
|
-
if dir_path.exists() and len(input_files) > 1:
|
|
109
|
-
|
|
110
|
-
with rio.Env(GDAL_NUM_THREADS="8", NUM_THREADS="8"):
|
|
111
|
-
srcs = [rio.open(fp) for fp in input_files]
|
|
112
|
-
mosaic, out_transform = merge(
|
|
113
|
-
srcs,
|
|
114
|
-
nodata=65535,
|
|
115
|
-
resampling=Resampling.nearest
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
meta = srcs[0].profile.copy()
|
|
119
|
-
meta["transform"] = out_transform
|
|
120
|
-
meta.update(
|
|
121
|
-
height=mosaic.shape[1],
|
|
122
|
-
width=mosaic.shape[2]
|
|
123
|
-
)
|
|
124
|
-
outname = original_dir / full_outname.name
|
|
125
|
-
outname.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
-
with rio.open(outname, "w", **meta) as dst:
|
|
127
|
-
dst.write(mosaic)
|
|
128
|
-
|
|
129
|
-
for src in srcs:
|
|
130
|
-
src.close()
|
|
131
|
-
|
|
132
|
-
# Delete a folder with pathlib
|
|
133
|
-
shutil.rmtree(dir_path)
|
|
134
|
-
else:
|
|
135
|
-
return outname
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import ee
|
|
2
|
-
import re
|
|
3
|
-
from copy import deepcopy
|
|
4
|
-
from typing import Dict
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def quadsplit_manifest(manifest: Dict, cell_width: int, cell_height: int, power: int) -> list[Dict]:
|
|
8
|
-
manifest_copy = deepcopy(manifest)
|
|
9
|
-
|
|
10
|
-
manifest_copy["grid"]["dimensions"]["width"] = cell_width
|
|
11
|
-
manifest_copy["grid"]["dimensions"]["height"] = cell_height
|
|
12
|
-
x = manifest_copy["grid"]["affineTransform"]["translateX"]
|
|
13
|
-
y = manifest_copy["grid"]["affineTransform"]["translateY"]
|
|
14
|
-
scale_x = manifest_copy["grid"]["affineTransform"]["scaleX"]
|
|
15
|
-
scale_y = manifest_copy["grid"]["affineTransform"]["scaleY"]
|
|
16
|
-
|
|
17
|
-
manifests = []
|
|
18
|
-
|
|
19
|
-
for columny in range(2**power):
|
|
20
|
-
for rowx in range(2**power):
|
|
21
|
-
new_x = x + (rowx * cell_width) * scale_x
|
|
22
|
-
new_y = y + (columny * cell_height) * scale_y
|
|
23
|
-
new_manifest = deepcopy(manifest_copy)
|
|
24
|
-
new_manifest["grid"]["affineTransform"]["translateX"] = new_x
|
|
25
|
-
new_manifest["grid"]["affineTransform"]["translateY"] = new_y
|
|
26
|
-
manifests.append(new_manifest)
|
|
27
|
-
|
|
28
|
-
return manifests
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def calculate_cell_size(ee_error_message: str, size: int) -> tuple[int, int]:
|
|
33
|
-
match = re.findall(r'\d+', ee_error_message)
|
|
34
|
-
image_pixel = int(match[0])
|
|
35
|
-
max_pixel = int(match[1])
|
|
36
|
-
|
|
37
|
-
images = image_pixel / max_pixel
|
|
38
|
-
power = 0
|
|
39
|
-
|
|
40
|
-
while images > 1:
|
|
41
|
-
power += 1
|
|
42
|
-
images = image_pixel / (max_pixel * 4 ** power)
|
|
43
|
-
|
|
44
|
-
cell_width = size // 2 ** power
|
|
45
|
-
cell_height = size // 2 ** power
|
|
46
|
-
|
|
47
|
-
return cell_width, cell_height, power
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def _square_roi(lon: float, lat: float, edge_size: int, scale: int) -> ee.Geometry:
|
|
52
|
-
"""Return a square `ee.Geometry` centred on (*lon*, *lat*)."""
|
|
53
|
-
half = edge_size * scale / 2
|
|
54
|
-
point = ee.Geometry.Point([lon, lat])
|
|
55
|
-
return point.buffer(half).bounds()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|