izisat 0.1.0__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.
- izisat/__init__.py +0 -0
- izisat/izisentinel.py +161 -0
- izisat/misc/__init__.py +0 -0
- izisat/misc/connections.py +554 -0
- izisat/misc/dates.py +24 -0
- izisat/misc/files.py +135 -0
- izisat/misc/raster_processing.py +373 -0
- izisat/misc/utils.py +162 -0
- izisat-0.1.0.dist-info/METADATA +151 -0
- izisat-0.1.0.dist-info/RECORD +13 -0
- izisat-0.1.0.dist-info/WHEEL +5 -0
- izisat-0.1.0.dist-info/licenses/LICENSE +21 -0
- izisat-0.1.0.dist-info/top_level.txt +1 -0
izisat/misc/files.py
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
import os
|
2
|
+
from loguru import logger
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
|
6
|
+
class Files:
|
7
|
+
def __init__(self):
|
8
|
+
pass
|
9
|
+
|
10
|
+
|
11
|
+
def save_path_sentinel2_data(self, dir_out, etc, date, type, tile, band=None):
|
12
|
+
"""
|
13
|
+
Generate a directory path for Sentinel-2 data and create it if it doesn't exist.
|
14
|
+
|
15
|
+
Parameters:
|
16
|
+
------------
|
17
|
+
dir_out: str
|
18
|
+
The base output directory.
|
19
|
+
date: str
|
20
|
+
The date in the format '%Y-%m-%dT%H:%M:%S.%fZ'.
|
21
|
+
etc: str
|
22
|
+
Additional information for the directory path.
|
23
|
+
tile: str
|
24
|
+
The tile information for the directory path.
|
25
|
+
band: str, optional
|
26
|
+
The band information for the directory path.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
----------
|
30
|
+
str
|
31
|
+
The path of the generated or existing directory.
|
32
|
+
"""
|
33
|
+
dt_object = datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ')
|
34
|
+
# Format the datetime object as 'YYYY/MM/DD'
|
35
|
+
date = dt_object.strftime('%Y/%m/%d')
|
36
|
+
|
37
|
+
if band is not None:
|
38
|
+
directory = os.path.join(dir_out, etc, date, type, tile, band)
|
39
|
+
else:
|
40
|
+
directory = os.path.join(dir_out, etc, date, type, tile)
|
41
|
+
|
42
|
+
if os.path.exists(directory):
|
43
|
+
logger.warning(f"Directory {directory} already exists.")
|
44
|
+
return directory
|
45
|
+
else:
|
46
|
+
os.makedirs(directory, exist_ok=True)
|
47
|
+
logger.success(f"Directory {directory} created!")
|
48
|
+
return directory
|
49
|
+
|
50
|
+
|
51
|
+
def create_output_folders(self, output_base_path, products_info, bands_dict):
|
52
|
+
"""
|
53
|
+
Create, if necessary, folders to save the downloaded bands.
|
54
|
+
|
55
|
+
Parameters:
|
56
|
+
-----------
|
57
|
+
output_base_path: str
|
58
|
+
The base path where the output folders will be created.
|
59
|
+
products_info: list
|
60
|
+
A list of product information, where each item contains:
|
61
|
+
[product_id, product_name, product_path, product_origin_date, product_tile, product_platform_name, product_type]
|
62
|
+
bands_dict: dict
|
63
|
+
A dictionary containing information about bands for different product types and resolutions.
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
--------
|
67
|
+
created_directories: list
|
68
|
+
A list of paths to the created output directories.
|
69
|
+
"""
|
70
|
+
logger.info("Creating, if necessary, folders to save the downloaded bands...")
|
71
|
+
created_directories = []
|
72
|
+
for product_info in products_info:
|
73
|
+
product_type = product_info[6]
|
74
|
+
product_tile = product_info[4]
|
75
|
+
product_date = product_info[3]
|
76
|
+
product_plataform = product_info[5]
|
77
|
+
if product_type == "L1C":
|
78
|
+
directory_path = self.save_path_sentinel2_data(output_base_path, product_plataform, product_date, product_tile, product_type)
|
79
|
+
created_directories.append(directory_path)
|
80
|
+
elif product_type == "L2A":
|
81
|
+
for resolution, _ in bands_dict.items():
|
82
|
+
if resolution == "L1C":
|
83
|
+
pass
|
84
|
+
else:
|
85
|
+
l2a_dict = bands_dict.get("L2A", {})
|
86
|
+
for resolution_key in l2a_dict:
|
87
|
+
directory_path = self.save_path_sentinel2_data(output_base_path, product_plataform, product_date, product_tile, product_type, resolution_key)
|
88
|
+
created_directories.append(directory_path)
|
89
|
+
|
90
|
+
return created_directories
|
91
|
+
|
92
|
+
|
93
|
+
def check_file_exist(self, base_dir, etc, date, tile, type, filename, resolution = None):
|
94
|
+
"""
|
95
|
+
Check if a file exists based on the specified parameters.
|
96
|
+
|
97
|
+
Parameters:
|
98
|
+
-----------
|
99
|
+
base_dir: str
|
100
|
+
The base directory where the file is expected to exist.
|
101
|
+
etc: str
|
102
|
+
Additional subdirectory for categorization (e.g., product platform or other metadata).
|
103
|
+
date: str
|
104
|
+
The date of the product in 'YYYY-MM-DDTHH:MM:SS.sssZ' format.
|
105
|
+
tile: str
|
106
|
+
The tile identifier associated with the product.
|
107
|
+
type: str
|
108
|
+
The type of the product (e.g., 'L1C', 'L2A').
|
109
|
+
filename: str
|
110
|
+
The name of the file to check.
|
111
|
+
resolution: str, optional
|
112
|
+
The resolution category (e.g., 'resolution10m', 'resolution20m').
|
113
|
+
If provided, the file path will include this resolution subdirectory.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
--------
|
117
|
+
exists: bool
|
118
|
+
True if the file exists, False otherwise.
|
119
|
+
file_path: str
|
120
|
+
The full path to the checked file.
|
121
|
+
|
122
|
+
"""
|
123
|
+
date_obj = datetime.strptime(date, '%Y-%m-%dT%H:%M:%S.%fZ')
|
124
|
+
# Format datetime object as 'YYYY/MM/DD'
|
125
|
+
date = date_obj.strftime('%Y/%m/%d')
|
126
|
+
|
127
|
+
if resolution is None:
|
128
|
+
file = os.path.join(base_dir, etc, date, tile, type, filename)
|
129
|
+
return os.path.exists(file), file
|
130
|
+
else:
|
131
|
+
file = os.path.join(base_dir, etc, date, tile, type, resolution, filename)
|
132
|
+
return os.path.exists(file), file
|
133
|
+
|
134
|
+
|
135
|
+
|
@@ -0,0 +1,373 @@
|
|
1
|
+
import os
|
2
|
+
import rasterio
|
3
|
+
import rasterio.mask
|
4
|
+
from shapely import wkt
|
5
|
+
from shapely.geometry import mapping
|
6
|
+
from typing import Union
|
7
|
+
from pathlib import Path
|
8
|
+
import geopandas as gpd
|
9
|
+
import numpy as np
|
10
|
+
from rasterio.crs import CRS
|
11
|
+
from rasterio.transform import from_origin
|
12
|
+
from rasterio.merge import merge
|
13
|
+
import numpy as np
|
14
|
+
import rasterio
|
15
|
+
from rasterio.io import MemoryFile
|
16
|
+
from rasterio.warp import reproject, Resampling, calculate_default_transform
|
17
|
+
|
18
|
+
|
19
|
+
class RasterProcessing:
|
20
|
+
def __init__(self):
|
21
|
+
pass
|
22
|
+
|
23
|
+
def crop_band_by_footprint_wkt(self,
|
24
|
+
jp2_path: str,
|
25
|
+
footprint_wkt: str,
|
26
|
+
output_path: str = None
|
27
|
+
):
|
28
|
+
"""
|
29
|
+
Crops a georeferenced JP2 image using a WKT footprint (MULTIPOLYGON).
|
30
|
+
|
31
|
+
Args:
|
32
|
+
jp2_path (str): Path to the .jp2 image (satellite band).
|
33
|
+
footprint_wkt (str): WKT string of the footprint polygon (e.g. 'MULTIPOLYGON (...)').
|
34
|
+
output_path (str, optional): Output .tif file path or directory. If None, nothing is saved.
|
35
|
+
suffix (str): Suffix to append to the output file name if saving to a directory.
|
36
|
+
|
37
|
+
Returns:
|
38
|
+
dict: {
|
39
|
+
"image": Cropped image as a NumPy array,
|
40
|
+
"profile": Raster metadata dictionary,
|
41
|
+
"output_name": Full output path if saved, else None
|
42
|
+
}
|
43
|
+
"""
|
44
|
+
# Convert WKT string to geometry
|
45
|
+
geometry = [mapping(wkt.loads(footprint_wkt))]
|
46
|
+
|
47
|
+
with rasterio.open(jp2_path) as src:
|
48
|
+
cropped_image, transform = rasterio.mask.mask(src, geometry, crop=True)
|
49
|
+
cropped_image = cropped_image.squeeze()
|
50
|
+
profile = src.profile.copy()
|
51
|
+
profile.update({
|
52
|
+
"height": cropped_image.shape[0],
|
53
|
+
"width": cropped_image.shape[1],
|
54
|
+
"transform": transform
|
55
|
+
})
|
56
|
+
if output_path is not None:
|
57
|
+
with rasterio.open(output_path, "w", **profile) as dst:
|
58
|
+
dst.write(cropped_image)
|
59
|
+
return cropped_image, profile
|
60
|
+
|
61
|
+
def ensure_array_in_epsg(self, array: np.ndarray, profile: dict, target_epsg: int):
|
62
|
+
import numpy as np
|
63
|
+
import rasterio
|
64
|
+
from rasterio.io import MemoryFile
|
65
|
+
from rasterio.warp import reproject, Resampling, calculate_default_transform
|
66
|
+
from rasterio.transform import array_bounds
|
67
|
+
"""
|
68
|
+
Garante que o array raster esteja no EPSG desejado. Se necessário, reprojeta.
|
69
|
+
|
70
|
+
Parâmetros:
|
71
|
+
- array: np.ndarray (2D)
|
72
|
+
- profile: dict com as chaves: 'transform', 'crs', 'width', 'height'
|
73
|
+
- target_epsg: int
|
74
|
+
|
75
|
+
Retorna:
|
76
|
+
- dst_array: np.ndarray reprojetado ou original
|
77
|
+
- new_profile: dict atualizado
|
78
|
+
"""
|
79
|
+
src_crs = profile.get("crs")
|
80
|
+
dst_crs = rasterio.crs.CRS.from_epsg(target_epsg)
|
81
|
+
|
82
|
+
if src_crs == dst_crs:
|
83
|
+
new_profile = profile.copy()
|
84
|
+
new_profile["crs"] = dst_crs
|
85
|
+
return array, new_profile
|
86
|
+
|
87
|
+
# Calcula os bounds a partir do transform
|
88
|
+
bounds = array_bounds(profile["height"], profile["width"], profile["transform"])
|
89
|
+
|
90
|
+
transform, width, height = calculate_default_transform(
|
91
|
+
src_crs, dst_crs,
|
92
|
+
profile["width"], profile["height"],
|
93
|
+
*bounds
|
94
|
+
)
|
95
|
+
|
96
|
+
dst_array = np.empty((height, width), dtype=array.dtype)
|
97
|
+
|
98
|
+
reproject(
|
99
|
+
source=array,
|
100
|
+
destination=dst_array,
|
101
|
+
src_transform=profile["transform"],
|
102
|
+
src_crs=src_crs,
|
103
|
+
dst_transform=transform,
|
104
|
+
dst_crs=dst_crs,
|
105
|
+
resampling=Resampling.nearest
|
106
|
+
)
|
107
|
+
|
108
|
+
new_profile = profile.copy()
|
109
|
+
new_profile.update({
|
110
|
+
"crs": dst_crs,
|
111
|
+
"transform": transform,
|
112
|
+
"width": width,
|
113
|
+
"height": height
|
114
|
+
})
|
115
|
+
|
116
|
+
return dst_array, new_profile
|
117
|
+
|
118
|
+
def get_cropped_bands_no_merge(self, gdf:gpd.GeoDataFrame, band_paths):
|
119
|
+
utm_zones = {
|
120
|
+
19:31979,
|
121
|
+
20:31980,
|
122
|
+
21:31981,
|
123
|
+
22:31982,
|
124
|
+
23:31983,
|
125
|
+
24:31984,
|
126
|
+
25:31985,
|
127
|
+
}
|
128
|
+
bands_list = ['B02','B03','B04','B08']
|
129
|
+
cropped_bands = []
|
130
|
+
|
131
|
+
epsg = utm_zones[int(os.path.basename(band_paths['B02']).split('_')[0][1:3])]
|
132
|
+
gdf = gdf.to_crs(epsg)
|
133
|
+
footprint_wkt = gdf.iloc[0].geometry.wkt
|
134
|
+
|
135
|
+
for band in bands_list:
|
136
|
+
path = band_paths.get(band)
|
137
|
+
if path:
|
138
|
+
cropped, profile = self.crop_band_by_footprint_wkt(path, footprint_wkt)
|
139
|
+
cropped_bands.append(cropped)
|
140
|
+
|
141
|
+
return (*cropped_bands, profile)
|
142
|
+
|
143
|
+
def get_cropped_bands_with_merge(self, gdf: gpd.GeoDataFrame, band_dicts: list):
|
144
|
+
"""
|
145
|
+
Recorta e mescla múltiplas bandas de entrada organizadas como uma lista de dicionários.
|
146
|
+
|
147
|
+
Parâmetros:
|
148
|
+
- gdf: GeoDataFrame com a geometria para recorte.
|
149
|
+
- band_dicts: lista de dicionários, cada um contendo os caminhos das bandas 'B02', 'B03', 'B04' e 'B08'.
|
150
|
+
|
151
|
+
Retorna:
|
152
|
+
- Tupla com arrays mesclados para cada banda: B02, B03, B04, B08 e o profile do merge.
|
153
|
+
"""
|
154
|
+
|
155
|
+
utm_zones = {
|
156
|
+
19: 31979,
|
157
|
+
20: 31980,
|
158
|
+
21: 31981,
|
159
|
+
22: 31982,
|
160
|
+
23: 31983,
|
161
|
+
24: 31984,
|
162
|
+
25: 31985,
|
163
|
+
}
|
164
|
+
bands_list = ['B02', 'B03', 'B04', 'B08']
|
165
|
+
cropped_bands_by_type = {band: [] for band in bands_list}
|
166
|
+
final_profile = None
|
167
|
+
final_crs = gdf.estimate_utm_crs('SIRGAS 2000').to_epsg()
|
168
|
+
|
169
|
+
# Recorta todas as bandas e armazena por tipo
|
170
|
+
for band_paths in band_dicts:
|
171
|
+
try:
|
172
|
+
for band in bands_list:
|
173
|
+
path = band_paths.get(band)
|
174
|
+
if path:
|
175
|
+
epsg = utm_zones[int(os.path.basename(path).split('_')[0][1:3])]
|
176
|
+
gdf = gdf.to_crs(epsg)
|
177
|
+
footprint_wkt = gdf.iloc[0].geometry.wkt
|
178
|
+
cropped, profile = self.crop_band_by_footprint_wkt(path, footprint_wkt)
|
179
|
+
cropped_bands_by_type[band].append((cropped, profile))
|
180
|
+
except:
|
181
|
+
pass
|
182
|
+
|
183
|
+
# Faz merge por banda
|
184
|
+
merged_bands = []
|
185
|
+
for band in bands_list:
|
186
|
+
datasets = []
|
187
|
+
for cropped, profile in cropped_bands_by_type[band]:
|
188
|
+
cropped_epsg, profile_epsg = self.ensure_array_in_epsg(cropped, profile, final_crs)
|
189
|
+
profile_epsg.update({
|
190
|
+
'driver': 'GTiff',
|
191
|
+
'dtype': 'uint16',
|
192
|
+
'compress': None,
|
193
|
+
'tiled': False,
|
194
|
+
'nodata': 0
|
195
|
+
})
|
196
|
+
memfile = rasterio.io.MemoryFile()
|
197
|
+
with memfile.open(**profile_epsg) as tmp:
|
198
|
+
tmp.write(cropped_epsg, 1)
|
199
|
+
datasets.append(memfile.open())
|
200
|
+
|
201
|
+
merged, out_transform = merge(datasets, method='first')
|
202
|
+
merged_bands.append(merged.squeeze())
|
203
|
+
|
204
|
+
# Pega profile de um dos arquivos e atualiza
|
205
|
+
if final_profile is None:
|
206
|
+
final_profile = datasets[0].profile.copy()
|
207
|
+
final_profile.update({
|
208
|
+
"height": merged.shape[1],
|
209
|
+
"width": merged.shape[2],
|
210
|
+
"transform": out_transform,
|
211
|
+
"count": 1
|
212
|
+
})
|
213
|
+
|
214
|
+
# Fecha arquivos temporários
|
215
|
+
|
216
|
+
return (*merged_bands, final_profile)
|
217
|
+
|
218
|
+
|
219
|
+
def create_rgb(self, b02_array, b03_array, b04_array, reference_profile=None, output_path=None):
|
220
|
+
"""
|
221
|
+
Cria uma imagem RGB a partir das bandas B02 (Blue), B03 (Green) e B04 (Red) do Sentinel-2.
|
222
|
+
|
223
|
+
Parâmetros:
|
224
|
+
- b02_array (np.ndarray): Banda 2 (azul)
|
225
|
+
- b03_array (np.ndarray): Banda 3 (verde)
|
226
|
+
- b04_array (np.ndarray): Banda 4 (vermelho)
|
227
|
+
- reference_profile (dict): Perfil de metadados de referência de uma das bandas originais
|
228
|
+
- output_path (str, opcional): Caminho para salvar o arquivo. Se None, apenas retorna o array.
|
229
|
+
|
230
|
+
Retorna:
|
231
|
+
- rgb_array (np.ndarray): Array RGB com shape (3, height, width)
|
232
|
+
"""
|
233
|
+
|
234
|
+
# Verifica se as dimensões batem
|
235
|
+
if not (b02_array.shape == b03_array.shape == b04_array.shape):
|
236
|
+
raise ValueError("As bandas devem ter as mesmas dimensões")
|
237
|
+
|
238
|
+
# Cria array RGB no formato (3, altura, largura)
|
239
|
+
rgb_array = np.stack([b04_array, b03_array, b02_array], axis=0) # (R, G, B)
|
240
|
+
|
241
|
+
# Atualiza o perfil para refletir 3 bandas
|
242
|
+
profile = reference_profile.copy()
|
243
|
+
profile.update({
|
244
|
+
'count': 3,
|
245
|
+
'dtype': rgb_array.dtype,
|
246
|
+
'driver': 'GTiff'
|
247
|
+
})
|
248
|
+
|
249
|
+
# Salva ou retorna
|
250
|
+
if output_path:
|
251
|
+
with rasterio.open(output_path, 'w', **profile) as dst:
|
252
|
+
dst.write(rgb_array)
|
253
|
+
return rgb_array
|
254
|
+
|
255
|
+
def create_ndvi(self, b04_array, b08_array, reference_profile, output_path=None):
|
256
|
+
"""
|
257
|
+
Calcula o NDVI (Normalized Difference Vegetation Index) usando as bandas B04 (Red) e B08 (NIR) do Sentinel-2.
|
258
|
+
|
259
|
+
Parâmetros:
|
260
|
+
- b04_array (np.ndarray): Banda 4 (vermelho)
|
261
|
+
- b08_array (np.ndarray): Banda 8 (infravermelho próximo)
|
262
|
+
- reference_profile (dict): Perfil de metadados de referência de uma das bandas originais
|
263
|
+
- output_path (str, opcional): Caminho para salvar o arquivo. Se None, apenas retorna o array.
|
264
|
+
|
265
|
+
Retorna:
|
266
|
+
- ndvi (np.ndarray): Array 2D com valores de NDVI
|
267
|
+
"""
|
268
|
+
|
269
|
+
# Verifica se as dimensões batem
|
270
|
+
if b04_array.shape != b08_array.shape:
|
271
|
+
raise ValueError("As bandas devem ter as mesmas dimensões")
|
272
|
+
|
273
|
+
# Converte para float32 para evitar problemas de divisão inteira
|
274
|
+
b04 = b04_array.astype('float32')
|
275
|
+
b08 = b08_array.astype('float32')
|
276
|
+
|
277
|
+
# NDVI = (NIR - Red) / (NIR + Red)
|
278
|
+
denominator = b08 + b04
|
279
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
280
|
+
ndvi = (b08 - b04) / denominator
|
281
|
+
ndvi[np.isnan(ndvi)] = -9999 # valor nodata
|
282
|
+
|
283
|
+
# Atualiza o perfil
|
284
|
+
profile = reference_profile.copy()
|
285
|
+
profile.update({
|
286
|
+
'count': 1,
|
287
|
+
'dtype': 'float32',
|
288
|
+
'driver': 'GTiff',
|
289
|
+
'nodata': -9999
|
290
|
+
})
|
291
|
+
|
292
|
+
# Salva ou retorna
|
293
|
+
if output_path:
|
294
|
+
with rasterio.open(output_path, 'w', **profile) as dst:
|
295
|
+
dst.write(ndvi, 1)
|
296
|
+
return ndvi
|
297
|
+
|
298
|
+
def create_evi(self, b02_array, b04_array, b08_array, reference_profile, output_path=None):
|
299
|
+
"""
|
300
|
+
Calcula o EVI (Enhanced Vegetation Index) usando as bandas B02 (Blue), B04 (Red) e B08 (NIR) do Sentinel-2.
|
301
|
+
|
302
|
+
Parâmetros:
|
303
|
+
- b02_array (np.ndarray): Banda 2 (azul)
|
304
|
+
- b04_array (np.ndarray): Banda 4 (vermelho)
|
305
|
+
- b08_array (np.ndarray): Banda 8 (infravermelho próximo)
|
306
|
+
- reference_profile (dict): Perfil de metadados de referência de uma das bandas originais
|
307
|
+
- output_path (str, opcional): Caminho para salvar o arquivo. Se None, apenas retorna o array.
|
308
|
+
|
309
|
+
Retorna:
|
310
|
+
- evi (np.ndarray): Array 2D com valores de EVI
|
311
|
+
"""
|
312
|
+
|
313
|
+
# Verifica se as dimensões batem
|
314
|
+
if not (b02_array.shape == b04_array.shape == b08_array.shape):
|
315
|
+
raise ValueError("As bandas devem ter as mesmas dimensões")
|
316
|
+
|
317
|
+
# Converte para float32
|
318
|
+
b02 = b02_array.astype('float32') / 10000 # Blue
|
319
|
+
b04 = b04_array.astype('float32') / 10000 # Red
|
320
|
+
b08 = b08_array.astype('float32') / 10000 # NIR
|
321
|
+
|
322
|
+
# Parâmetros do EVI
|
323
|
+
G = 2.5
|
324
|
+
C1 = 6.0
|
325
|
+
C2 = 7.5
|
326
|
+
L = 1.0
|
327
|
+
|
328
|
+
# Fórmula do EVI
|
329
|
+
denominator = b08 + C1 * b04 - C2 * b02 + L
|
330
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
331
|
+
evi = G * ((b08 - b04) / denominator)
|
332
|
+
evi[np.isnan(evi)] = -9999 # valor nodata
|
333
|
+
|
334
|
+
# Atualiza o perfil
|
335
|
+
profile = reference_profile.copy()
|
336
|
+
profile.update({
|
337
|
+
'count': 1,
|
338
|
+
'dtype': 'float32',
|
339
|
+
'driver': 'GTiff',
|
340
|
+
'nodata': -9999
|
341
|
+
})
|
342
|
+
|
343
|
+
# Salva ou retorna
|
344
|
+
if output_path:
|
345
|
+
with rasterio.open(output_path, 'w', **profile) as dst:
|
346
|
+
dst.write(evi, 1)
|
347
|
+
return evi
|
348
|
+
|
349
|
+
if __name__ == '__main__':
|
350
|
+
import rasterio
|
351
|
+
b02 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B02_10m.jp2'
|
352
|
+
b03 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B03_10m.jp2'
|
353
|
+
b04 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B04_10m.jp2'
|
354
|
+
b08 = '/home/ppz/Documentos/coding/izisat/auxiliary/Sentinel-2/2025/04/06/T22KGD/L2A/10m/T22KGD_20250406T133211_B08_10m.jp2'
|
355
|
+
|
356
|
+
# Lê as bandas como arrays e pega o perfil de uma delas
|
357
|
+
with rasterio.open(b02) as b2_src:
|
358
|
+
b02 = b2_src.read(1)
|
359
|
+
profile = b2_src.profile
|
360
|
+
|
361
|
+
with rasterio.open(b03) as b3_src:
|
362
|
+
b03 = b3_src.read(1)
|
363
|
+
|
364
|
+
with rasterio.open(b04) as b4_src:
|
365
|
+
b04 = b4_src.read(1)
|
366
|
+
|
367
|
+
with rasterio.open(b08) as b8_src:
|
368
|
+
b08 = b8_src.read(1)
|
369
|
+
|
370
|
+
# Chama a função
|
371
|
+
instance = RasterProcessing()
|
372
|
+
rgb_array = instance.create_rgb(b02, b03, b04, reference_profile=profile, output_path='rgb_composite.tif')
|
373
|
+
ndvi_array = instance.create_ndvi(b04, b08, profile, output_path='ndvi_composite.tif')
|
izisat/misc/utils.py
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
from loguru import logger
|
2
|
+
from pathlib import Path
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
class Utils:
|
6
|
+
def __init__(self):
|
7
|
+
pass
|
8
|
+
|
9
|
+
|
10
|
+
def construct_query_for_sentinel2_products(self, footprint: str, start_date: str, end_date: str, cloud_cover_percentage: str, type,
|
11
|
+
platform_name: str):
|
12
|
+
"""
|
13
|
+
Create a query for downloading Sentinel data based on specified parameters.
|
14
|
+
|
15
|
+
Parameters:
|
16
|
+
------------
|
17
|
+
footprint: str
|
18
|
+
The spatial geometry (POLYGON) of the area of interest.
|
19
|
+
start_date: str
|
20
|
+
The start date for the time range of interest in the format 'YYYY-MM-DD'.
|
21
|
+
end_date: str
|
22
|
+
The end date for the time range of interest in the format 'YYYY-MM-DD'.
|
23
|
+
cloud_cover_percentage: str
|
24
|
+
The maximum allowable cloud cover percentage.
|
25
|
+
type: str or list
|
26
|
+
Type of MSI to download
|
27
|
+
platform_name: str, optional
|
28
|
+
The name of the Sentinel platform (default: 'SENTINEL-2').
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
params: str
|
32
|
+
The query string for downloading Sentinel data.
|
33
|
+
"""
|
34
|
+
|
35
|
+
logger.info("Creating query to download Sentinel data...")
|
36
|
+
try:
|
37
|
+
if footprint and start_date and end_date and platform_name and cloud_cover_percentage and type:
|
38
|
+
footprint = footprint.replace(" ", "%20")
|
39
|
+
if isinstance(type, str):
|
40
|
+
params = f"?&$filter=(Collection/Name%20eq%20%27{platform_name}%27%20and%20(Attributes/OData.CSC.StringAttribute/any(att:att/Name%20eq%20%27instrumentShortName%27%20and%20att/OData.CSC.StringAttribute/Value%20eq%20%27MSI%27)%20and%20Attributes/OData.CSC.DoubleAttribute/any(att:att/Name%20eq%20%27cloudCover%27%20and%20att/OData.CSC.DoubleAttribute/Value%20le%20{cloud_cover_percentage})%20and%20(contains(Name,%27{type}%27)%20and%20OData.CSC.Intersects(area=geography%27SRID=4326;{footprint}%27)))%20and%20Online%20eq%20true)%20and%20ContentDate/Start%20ge%20{start_date}T00:00:00.000Z%20and%20ContentDate/Start%20lt%20{end_date}T23:59:59.999Z&$orderby=ContentDate/Start%20desc&$expand=Attributes&$count=True&$top=50&$expand=Assets&$skip=0"
|
41
|
+
logger.success("Query created successfully")
|
42
|
+
return params
|
43
|
+
elif isinstance(type, list):
|
44
|
+
params = f"?&$filter=(Collection/Name%20eq%20%27{platform_name}%27%20and%20(Attributes/OData.CSC.StringAttribute/any(att:att/Name%20eq%20%27instrumentShortName%27%20and%20att/OData.CSC.StringAttribute/Value%20eq%20%27MSI%27)%20and%20Attributes/OData.CSC.DoubleAttribute/any(att:att/Name%20eq%20%27cloudCover%27%20and%20att/OData.CSC.DoubleAttribute/Value%20le%20{cloud_cover_percentage})%20and%20((contains(Name,%27{type[0]}%27)%20and%20OData.CSC.Intersects(area=geography%27SRID=4326;{footprint}%27))%20or%20(contains(Name,%27{type[1]}%27)%20and%20OData.CSC.Intersects(area=geography%27SRID=4326;{footprint}%27))))%20and%20Online%20eq%20true)%20and%20ContentDate/Start%20ge%20{start_date}T00:00:00.000Z%20and%20ContentDate/Start%20lt%20{end_date}T23:59:59.999Z&$orderby=ContentDate/Start%20desc&$expand=Attributes&$count=True&$top=50&$expand=Assets&$skip=0"
|
45
|
+
logger.success("Query created successfully")
|
46
|
+
return params
|
47
|
+
else:
|
48
|
+
raise ValueError("Please provide valid values for type.")
|
49
|
+
else:
|
50
|
+
raise ValueError("Please provide valid values for all required parameters.")
|
51
|
+
except Exception as e:
|
52
|
+
logger.error(f"An error occurred: {str(e)}")
|
53
|
+
raise
|
54
|
+
|
55
|
+
def retrieve_products_info(self, products):
|
56
|
+
"""
|
57
|
+
Retrieve information about Sentinel products.
|
58
|
+
|
59
|
+
Parameters:
|
60
|
+
-----------
|
61
|
+
products: list
|
62
|
+
List of Sentinel products, each represented as a dictionary.
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
---------
|
66
|
+
list:
|
67
|
+
A list containing information about each product. Each element in the list is a sublist with the following format:
|
68
|
+
[product_id, product_name, product_s3path, product_origin_date, product_tile, product_platform_name, product_type]
|
69
|
+
"""
|
70
|
+
result_list = []
|
71
|
+
logger.info(f"Retrieving information about {len(products)} products...")
|
72
|
+
|
73
|
+
for i, product in enumerate(products):
|
74
|
+
logger.info(f"Retrieving information about product {i+1}")
|
75
|
+
product_id = product['Id']
|
76
|
+
product_name = product['Name']
|
77
|
+
product_s3path = product["S3Path"]
|
78
|
+
product_origin_date = product['ContentDate']['End']
|
79
|
+
|
80
|
+
# Creating output directory to save downloaded data
|
81
|
+
product_tile = product_name.split("_")[5]
|
82
|
+
product_platform_name = product_s3path.split('/')[2]
|
83
|
+
product_type = product_name.split('_')[1][-3:]
|
84
|
+
|
85
|
+
# Append product information to the result list
|
86
|
+
result_list.append([product_id, product_name, product_s3path, product_origin_date, product_tile, product_platform_name, product_type])
|
87
|
+
logger.success(f"ID, Name, S3Path, Origin Date, Tile, Plataform Name and Type successfully retrieved for product {i+1}")
|
88
|
+
|
89
|
+
logger.success("Information successfully retrieved for all products. A list with these informations were created!")
|
90
|
+
return result_list
|
91
|
+
|
92
|
+
def modify_string(self, url):
|
93
|
+
"""
|
94
|
+
Modify a URL by replacing the last occurrence of "Nodes" with "$value".
|
95
|
+
|
96
|
+
Parameters:
|
97
|
+
-----------
|
98
|
+
url: str
|
99
|
+
The input URL to be modified.
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
---------
|
103
|
+
modified_url: str
|
104
|
+
The modified URL with the last "Nodes" replaced by "$value".
|
105
|
+
ValueError
|
106
|
+
If "Nodes" is not found in the input URL.
|
107
|
+
"""
|
108
|
+
last_nodes_index = url.rfind("Nodes")
|
109
|
+
|
110
|
+
if last_nodes_index != -1:
|
111
|
+
# Remove the last "Nodes" and replace it with "/$value"
|
112
|
+
modified_url = url[:last_nodes_index] + "$value"
|
113
|
+
return modified_url
|
114
|
+
else:
|
115
|
+
raise ValueError("Error: 'Nodes' not found in the URL")
|
116
|
+
|
117
|
+
def generate_sentinel2_band_paths_nested_by_tile(self, download_folder: str, products: list[list[str]]) -> dict[str, dict[str, str]]:
|
118
|
+
"""
|
119
|
+
Gera dicionário aninhado com caminhos das bandas Sentinel-2, agrupados por data e tile (fuso).
|
120
|
+
|
121
|
+
Retorna:
|
122
|
+
{
|
123
|
+
"2025-04-13_T23": {
|
124
|
+
"B02": "/.../T23KKV_20250413T132251_B02_10m.jp2",
|
125
|
+
"B03": "/.../T23KKV_..._B03_10m.jp2",
|
126
|
+
...
|
127
|
+
},
|
128
|
+
...
|
129
|
+
}
|
130
|
+
"""
|
131
|
+
from collections import defaultdict
|
132
|
+
result = {}
|
133
|
+
grouped = defaultdict(list)
|
134
|
+
for variables in products:
|
135
|
+
date_obj = datetime.fromisoformat(variables[3].replace("Z", ""))
|
136
|
+
date_str = date_obj.strftime("%Y-%m-%d")
|
137
|
+
tile = variables[4] # Ex: T23KKV
|
138
|
+
tile_id = tile[1:3] # Pega apenas o fuso "23"
|
139
|
+
sensor = variables[5]
|
140
|
+
product_level = variables[6]
|
141
|
+
year = f"{date_obj.year}"
|
142
|
+
month = f"{date_obj.month:02}"
|
143
|
+
day = f"{date_obj.day:02}"
|
144
|
+
timestamp_part = variables[1].split("_")[2] # Ex: 20250413T132251
|
145
|
+
|
146
|
+
bands = ["B02", "B03", "B04", "B08"]
|
147
|
+
base_path = Path(download_folder) / sensor / year / month / day / tile / product_level / "10m"
|
148
|
+
|
149
|
+
band_paths = {
|
150
|
+
band: str(base_path / f"{tile}_{timestamp_part}_{band}_10m.jp2")
|
151
|
+
for band in bands
|
152
|
+
}
|
153
|
+
|
154
|
+
key = f"{date_str}_T{tile_id}" # Ex: 2025-04-13_T23
|
155
|
+
result[key] = band_paths
|
156
|
+
|
157
|
+
for key, bands in result.items():
|
158
|
+
date = key.split('_')[0]
|
159
|
+
grouped[date].append(bands)
|
160
|
+
|
161
|
+
return grouped
|
162
|
+
|