sarpyx 0.1.5__py3-none-any.whl → 0.1.6__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.
- docs/examples/advanced/batch_processing.py +1 -1
- docs/examples/advanced/custom_processing_chains.py +1 -1
- docs/examples/advanced/performance_optimization.py +1 -1
- docs/examples/basic/snap_integration.py +1 -1
- docs/examples/intermediate/quality_assessment.py +1 -1
- outputs/baseline/20260205-234828/__init__.py +33 -0
- outputs/baseline/20260205-234828/main.py +493 -0
- outputs/final/20260205-234851/__init__.py +33 -0
- outputs/final/20260205-234851/main.py +493 -0
- sarpyx/__init__.py +2 -2
- sarpyx/algorithms/__init__.py +2 -2
- sarpyx/cli/__init__.py +1 -1
- sarpyx/cli/focus.py +3 -5
- sarpyx/cli/main.py +106 -7
- sarpyx/cli/shipdet.py +1 -1
- sarpyx/cli/worldsar.py +549 -0
- sarpyx/processor/__init__.py +1 -1
- sarpyx/processor/core/decode.py +43 -8
- sarpyx/processor/core/focus.py +104 -57
- sarpyx/science/__init__.py +1 -1
- sarpyx/sla/__init__.py +8 -0
- sarpyx/sla/metrics.py +101 -0
- sarpyx/{snap → snapflow}/__init__.py +1 -1
- sarpyx/snapflow/engine.py +6165 -0
- sarpyx/{snap → snapflow}/op.py +0 -1
- sarpyx/utils/__init__.py +1 -1
- sarpyx/utils/geos.py +652 -0
- sarpyx/utils/grid.py +285 -0
- sarpyx/utils/io.py +77 -9
- sarpyx/utils/meta.py +55 -0
- sarpyx/utils/nisar_utils.py +652 -0
- sarpyx/utils/rfigen.py +108 -0
- sarpyx/utils/wkt_utils.py +109 -0
- sarpyx/utils/zarr_utils.py +55 -37
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/METADATA +9 -5
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/RECORD +41 -32
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/WHEEL +1 -1
- sarpyx-0.1.6.dist-info/licenses/LICENSE +201 -0
- sarpyx-0.1.6.dist-info/top_level.txt +4 -0
- tests/test_zarr_compat.py +35 -0
- sarpyx/processor/core/decode_v0.py +0 -0
- sarpyx/processor/core/decode_v1.py +0 -849
- sarpyx/processor/core/focus_old.py +0 -1550
- sarpyx/processor/core/focus_v1.py +0 -1566
- sarpyx/processor/core/focus_v2.py +0 -1625
- sarpyx/snap/engine.py +0 -633
- sarpyx-0.1.5.dist-info/top_level.txt +0 -2
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/entry_points.txt +0 -0
sarpyx/utils/grid.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# FROM MAJOR TOM
|
|
2
|
+
import numpy as np
|
|
3
|
+
import math
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import geopandas as gpd
|
|
6
|
+
from shapely.geometry import LineString, Polygon
|
|
7
|
+
from tqdm import tqdm
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Grid():
|
|
13
|
+
|
|
14
|
+
RADIUS_EQUATOR = 6378.137 # km
|
|
15
|
+
|
|
16
|
+
def __init__(self,dist,latitude_range=(-85,85),longitude_range=(-180,180),utm_definition='bottomleft'):
|
|
17
|
+
self.dist = dist
|
|
18
|
+
self.latitude_range = latitude_range
|
|
19
|
+
self.longitude_range = longitude_range
|
|
20
|
+
self.utm_definition = utm_definition
|
|
21
|
+
self.rows,self.lats = self.get_rows()
|
|
22
|
+
self.points, self.points_by_row = self.get_points()
|
|
23
|
+
|
|
24
|
+
def get_rows(self):
|
|
25
|
+
|
|
26
|
+
# Define set of latitudes to use, based on the grid distance
|
|
27
|
+
arc_pole_to_pole = math.pi * self.RADIUS_EQUATOR
|
|
28
|
+
num_divisions_in_hemisphere = math.ceil(arc_pole_to_pole / self.dist)
|
|
29
|
+
|
|
30
|
+
latitudes = np.linspace(-90, 90, num_divisions_in_hemisphere+1)[:-1]
|
|
31
|
+
latitudes = np.mod(latitudes, 180) - 90
|
|
32
|
+
|
|
33
|
+
# order should be from south to north
|
|
34
|
+
latitudes = np.sort(latitudes)
|
|
35
|
+
|
|
36
|
+
zeroth_row = np.searchsorted(latitudes,0)
|
|
37
|
+
|
|
38
|
+
# From 0U-NU and 1D-ND
|
|
39
|
+
rows = [None] * len(latitudes)
|
|
40
|
+
rows[zeroth_row:] = [f'{i}U' for i in range(len(latitudes)-zeroth_row)]
|
|
41
|
+
rows[:zeroth_row] = [f'{abs(i-zeroth_row)}D' for i in range(zeroth_row)]
|
|
42
|
+
|
|
43
|
+
# bound to range
|
|
44
|
+
idxs = (latitudes>=self.latitude_range[0]) * (latitudes<=self.latitude_range[1])
|
|
45
|
+
rows,latitudes = np.array(rows), np.array(latitudes)
|
|
46
|
+
rows,latitudes = rows[idxs],latitudes[idxs]
|
|
47
|
+
|
|
48
|
+
return rows,latitudes
|
|
49
|
+
|
|
50
|
+
def get_circumference_at_latitude(self,lat):
|
|
51
|
+
|
|
52
|
+
# Circumference of the cross-section of a sphere at a given latitude
|
|
53
|
+
|
|
54
|
+
radius_at_lat = self.RADIUS_EQUATOR * math.cos(lat * math.pi / 180)
|
|
55
|
+
circumference = 2 * math.pi * radius_at_lat
|
|
56
|
+
|
|
57
|
+
return circumference
|
|
58
|
+
|
|
59
|
+
def subdivide_circumference(self,lat,return_cols=False):
|
|
60
|
+
# Provide a list of longitudes that subdivide the circumference of the earth at a given latitude
|
|
61
|
+
# into equal parts as close as possible to dist
|
|
62
|
+
|
|
63
|
+
circumference = self.get_circumference_at_latitude(lat)
|
|
64
|
+
num_divisions = math.ceil(circumference / self.dist)
|
|
65
|
+
longitudes = np.linspace(-180,180, num_divisions+1)[:-1]
|
|
66
|
+
longitudes = np.mod(longitudes, 360) - 180
|
|
67
|
+
longitudes = np.sort(longitudes)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if return_cols:
|
|
71
|
+
cols = [None] * len(longitudes)
|
|
72
|
+
zeroth_idx = np.where(longitudes==0)[0][0]
|
|
73
|
+
cols[zeroth_idx:] = [f'{i}R' for i in range(len(longitudes)-zeroth_idx)]
|
|
74
|
+
cols[:zeroth_idx] = [f'{abs(i-zeroth_idx)}L' for i in range(zeroth_idx)]
|
|
75
|
+
return np.array(cols),np.array(longitudes)
|
|
76
|
+
|
|
77
|
+
return np.array(longitudes)
|
|
78
|
+
|
|
79
|
+
def get_points(self):
|
|
80
|
+
|
|
81
|
+
r_idx = 0
|
|
82
|
+
points_by_row = [None]*len(self.rows)
|
|
83
|
+
for r,lat in zip(self.rows,self.lats):
|
|
84
|
+
point_names,grid_row_names,grid_col_names,grid_row_idx,grid_col_idx,grid_lats,grid_lons,utm_zones,epsgs = [],[],[],[],[],[],[],[],[]
|
|
85
|
+
cols,lons = self.subdivide_circumference(lat,return_cols=True)
|
|
86
|
+
|
|
87
|
+
cols,lons = self.filter_longitude(cols,lons)
|
|
88
|
+
c_idx = 0
|
|
89
|
+
for c,lon in zip(cols,lons):
|
|
90
|
+
point_names.append(f'{r}_{c}')
|
|
91
|
+
grid_row_names.append(r)
|
|
92
|
+
grid_col_names.append(c)
|
|
93
|
+
grid_row_idx.append(r_idx)
|
|
94
|
+
grid_col_idx.append(c_idx)
|
|
95
|
+
grid_lats.append(lat)
|
|
96
|
+
grid_lons.append(lon)
|
|
97
|
+
if self.utm_definition == 'bottomleft':
|
|
98
|
+
utm_zones.append(get_utm_zone_from_latlng([lat,lon]))
|
|
99
|
+
elif self.utm_definition == 'center':
|
|
100
|
+
center_lat = lat + (1000*self.dist/2)/111_120
|
|
101
|
+
center_lon = lon + (1000*self.dist/2)/(111_120*math.cos(center_lat*math.pi/180))
|
|
102
|
+
utm_zones.append(get_utm_zone_from_latlng([center_lat,center_lon]))
|
|
103
|
+
else:
|
|
104
|
+
raise ValueError(f'Invalid utm_definition {self.utm_definition}')
|
|
105
|
+
epsgs.append(f'EPSG:{utm_zones[-1]}')
|
|
106
|
+
|
|
107
|
+
c_idx += 1
|
|
108
|
+
points_by_row[r_idx] = gpd.GeoDataFrame({
|
|
109
|
+
'name':point_names,
|
|
110
|
+
'row':grid_row_names,
|
|
111
|
+
'col':grid_col_names,
|
|
112
|
+
'row_idx':grid_row_idx,
|
|
113
|
+
'col_idx':grid_col_idx,
|
|
114
|
+
'utm_zone':utm_zones,
|
|
115
|
+
'epsg':epsgs
|
|
116
|
+
},geometry=gpd.points_from_xy(grid_lons,grid_lats))
|
|
117
|
+
r_idx += 1
|
|
118
|
+
points = gpd.GeoDataFrame(pd.concat(points_by_row))
|
|
119
|
+
# points.reset_index(inplace=True,drop=True)
|
|
120
|
+
return points, points_by_row
|
|
121
|
+
|
|
122
|
+
def group_points_by_row(self):
|
|
123
|
+
# Make list of different gdfs for each row
|
|
124
|
+
points_by_row = [None]*len(self.rows)
|
|
125
|
+
for i,row in enumerate(self.rows):
|
|
126
|
+
points_by_row[i] = self.points[self.points.row==row]
|
|
127
|
+
return points_by_row
|
|
128
|
+
|
|
129
|
+
def filter_longitude(self,cols,lons):
|
|
130
|
+
idxs = (lons>=self.longitude_range[0]) * (lons<=self.longitude_range[1])
|
|
131
|
+
cols,lons = cols[idxs],lons[idxs]
|
|
132
|
+
return cols,lons
|
|
133
|
+
|
|
134
|
+
def latlon2rowcol(self,lats,lons,return_idx=False,integer=False):
|
|
135
|
+
"""
|
|
136
|
+
Convert latitude and longitude to row and column number from the grid
|
|
137
|
+
"""
|
|
138
|
+
# Always take bottom left corner of grid cell
|
|
139
|
+
rows = np.searchsorted(self.lats,lats)-1
|
|
140
|
+
|
|
141
|
+
# Get the possible points of the grid cells at the given latitude
|
|
142
|
+
possible_points = [self.points_by_row[row] for row in rows]
|
|
143
|
+
|
|
144
|
+
# For each point, find the rightmost point that is still to the left of the given longitude
|
|
145
|
+
cols = [poss_points.iloc[np.searchsorted(poss_points.geometry.x,lon)-1].col for poss_points,lon in zip(possible_points,lons)]
|
|
146
|
+
rows = self.rows[rows].tolist()
|
|
147
|
+
|
|
148
|
+
outputs = [rows, cols]
|
|
149
|
+
if return_idx:
|
|
150
|
+
# Get the table index for self.points with each row,col pair in rows, cols
|
|
151
|
+
idx = [self.points[(self.points.row==row) & (self.points.col==col)].index.values[0] for row,col in zip(rows,cols)]
|
|
152
|
+
outputs.append(idx)
|
|
153
|
+
|
|
154
|
+
# return raw numbers
|
|
155
|
+
if integer:
|
|
156
|
+
outputs[0] = [int(el[:-1]) if el[-1] == 'U' else -int(el[:-1]) for el in outputs[0]]
|
|
157
|
+
outputs[1] = [int(el[:-1]) if el[-1] == 'R' else -int(el[:-1]) for el in outputs[1]]
|
|
158
|
+
|
|
159
|
+
return outputs
|
|
160
|
+
|
|
161
|
+
def rowcol2latlon(self,rows,cols):
|
|
162
|
+
point_geoms = [self.points.loc[(self.points.row==row) & (self.points.col==col),'geometry'].values[0] for row,col in zip(rows,cols)]
|
|
163
|
+
lats = [point.y for point in point_geoms]
|
|
164
|
+
lons = [point.x for point in point_geoms]
|
|
165
|
+
return lats,lons
|
|
166
|
+
|
|
167
|
+
def get_bounded_footprint(self,point,buffer_ratio=0):
|
|
168
|
+
# Gets the polygon footprint of the grid cell for a given point, bounded by the other grid points' cells.
|
|
169
|
+
# Grid point defined as bottom-left corner of polygon. Buffer ratio is the ratio of the grid cell's width/height to buffer by.
|
|
170
|
+
|
|
171
|
+
bottom,left = point.geometry.y,point.geometry.x
|
|
172
|
+
row_idx = point.row_idx
|
|
173
|
+
col_idx = point.col_idx
|
|
174
|
+
next_row_idx = row_idx+1
|
|
175
|
+
next_col_idx = col_idx+1
|
|
176
|
+
|
|
177
|
+
if next_row_idx >= len(self.lats): # If at top row, use difference between top and second-to-top row for height
|
|
178
|
+
height = (self.lats[row_idx] - self.lats[row_idx-1])
|
|
179
|
+
top = self.lats[row_idx] + height
|
|
180
|
+
else:
|
|
181
|
+
top = self.lats[next_row_idx]
|
|
182
|
+
|
|
183
|
+
max_col = len(self.points_by_row[row_idx].col_idx)-1
|
|
184
|
+
if next_col_idx > max_col: # If at rightmost column, use difference between rightmost and second-to-rightmost column for width
|
|
185
|
+
width = (self.points_by_row[row_idx].iloc[col_idx].geometry.x - self.points_by_row[row_idx].iloc[col_idx-1].geometry.x)
|
|
186
|
+
right = self.points_by_row[row_idx].iloc[col_idx].geometry.x + width
|
|
187
|
+
else:
|
|
188
|
+
right = self.points_by_row[row_idx].iloc[next_col_idx].geometry.x
|
|
189
|
+
|
|
190
|
+
# Buffer the polygon by the ratio of the grid cell's width/height
|
|
191
|
+
width = right - left
|
|
192
|
+
height = top - bottom
|
|
193
|
+
|
|
194
|
+
buffer_horizontal = width * buffer_ratio
|
|
195
|
+
buffer_vertical = height * buffer_ratio
|
|
196
|
+
|
|
197
|
+
new_left = left - buffer_horizontal
|
|
198
|
+
new_right = right + buffer_horizontal
|
|
199
|
+
|
|
200
|
+
new_bottom = bottom - buffer_vertical
|
|
201
|
+
new_top = top + buffer_vertical
|
|
202
|
+
|
|
203
|
+
bbox = Polygon([(new_left,new_bottom),(new_left,new_top),(new_right,new_top),(new_right,new_bottom)])
|
|
204
|
+
|
|
205
|
+
return bbox
|
|
206
|
+
|
|
207
|
+
def get_utm_zone_from_latlng(latlng):
|
|
208
|
+
"""
|
|
209
|
+
Get the UTM zone from a latlng list and return the corresponding EPSG code.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
latlng : List[Union[int, float]]
|
|
214
|
+
The latlng list to get the UTM zone from.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
str
|
|
219
|
+
The EPSG code for the UTM zone.
|
|
220
|
+
"""
|
|
221
|
+
assert isinstance(latlng, (list, tuple)), "latlng must be in the form of a list or tuple."
|
|
222
|
+
|
|
223
|
+
longitude = latlng[1]
|
|
224
|
+
latitude = latlng[0]
|
|
225
|
+
|
|
226
|
+
zone_number = (math.floor((longitude + 180) / 6)) % 60 + 1
|
|
227
|
+
|
|
228
|
+
# Special zones for Svalbard and Norway
|
|
229
|
+
if latitude >= 56.0 and latitude < 64.0 and longitude >= 3.0 and longitude < 12.0:
|
|
230
|
+
zone_number = 32
|
|
231
|
+
elif latitude >= 72.0 and latitude < 84.0:
|
|
232
|
+
if longitude >= 0.0 and longitude < 9.0:
|
|
233
|
+
zone_number = 31
|
|
234
|
+
elif longitude >= 9.0 and longitude < 21.0:
|
|
235
|
+
zone_number = 33
|
|
236
|
+
elif longitude >= 21.0 and longitude < 33.0:
|
|
237
|
+
zone_number = 35
|
|
238
|
+
elif longitude >= 33.0 and longitude < 42.0:
|
|
239
|
+
zone_number = 37
|
|
240
|
+
|
|
241
|
+
# Determine the hemisphere and construct the EPSG code
|
|
242
|
+
if latitude < 0:
|
|
243
|
+
epsg_code = f"327{zone_number:02d}"
|
|
244
|
+
else:
|
|
245
|
+
epsg_code = f"326{zone_number:02d}"
|
|
246
|
+
if not re.match(r"32[6-7](0[1-9]|[1-5][0-9]|60)",epsg_code):
|
|
247
|
+
print(f"latlng: {latlng}, epsg_code: {epsg_code}")
|
|
248
|
+
raise ValueError(f"out of bound latlng resulted in incorrect EPSG code for the point")
|
|
249
|
+
|
|
250
|
+
return epsg_code
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == '__main__':
|
|
254
|
+
|
|
255
|
+
assert get_utm_zone_from_latlng([-1,-174.34]) == "32701"
|
|
256
|
+
assert get_utm_zone_from_latlng([48,-4]) == "32630"
|
|
257
|
+
assert get_utm_zone_from_latlng([78,13]) == "32633"
|
|
258
|
+
assert get_utm_zone_from_latlng([-34,19.7]) == "32734"
|
|
259
|
+
assert get_utm_zone_from_latlng([-36,175.7]) == "32760"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
dist = 10 # 10 KM
|
|
263
|
+
grid = Grid(dist)
|
|
264
|
+
|
|
265
|
+
np.random.seed(0)
|
|
266
|
+
test_lons = np.random.uniform(-20,20,size=(1000)) % 180 # Checks edge-case of crossing 180th meridian
|
|
267
|
+
test_lats = np.random.uniform(-20,68,size=(1000))
|
|
268
|
+
|
|
269
|
+
test_rows,test_cols = grid.latlon2rowcol(test_lats,test_lons)
|
|
270
|
+
test_lats2,test_lons2 = grid.rowcol2latlon(test_rows,test_cols)
|
|
271
|
+
|
|
272
|
+
print(test_lons[:10])
|
|
273
|
+
print(test_lats[:10])
|
|
274
|
+
print(test_rows[:10])
|
|
275
|
+
print(test_cols[:10])
|
|
276
|
+
|
|
277
|
+
# Make line segments from the points to their corresponding grid points
|
|
278
|
+
lines = []
|
|
279
|
+
for i in range(len(test_lats)):
|
|
280
|
+
lines.append([(test_lons[i],test_lats[i]),(test_lons2[i],test_lats2[i])])
|
|
281
|
+
|
|
282
|
+
lines = gpd.GeoDataFrame(geometry=gpd.GeoSeries([LineString(line) for line in lines]))
|
|
283
|
+
|
|
284
|
+
# lines.to_file(f'testlines_{dist}km.geojson',driver='GeoJSON')
|
|
285
|
+
grid.points.to_file(f'grid_{dist}km.geojson',driver='GeoJSON')
|
sarpyx/utils/io.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import shutil
|
|
2
2
|
import subprocess
|
|
3
|
+
import h5py
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Any, Union
|
|
5
6
|
from zipfile import ZipFile
|
|
@@ -9,6 +10,8 @@ from typing import Optional, Tuple, Union, Dict, Any, List, Callable
|
|
|
9
10
|
import numpy as np
|
|
10
11
|
import matplotlib.pyplot as plt
|
|
11
12
|
|
|
13
|
+
from .meta import extract_core_metadata_sentinel
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
# ------- Functions for memory efficiency -------
|
|
14
17
|
def gc_collect(func: Callable) -> Callable:
|
|
@@ -184,12 +187,8 @@ class ArraySlicer:
|
|
|
184
187
|
plt.show()
|
|
185
188
|
|
|
186
189
|
|
|
187
|
-
# ------- Functions for file operations -------
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
190
|
|
|
191
|
+
# ------- Functions for file operations -------
|
|
193
192
|
|
|
194
193
|
def calculate_slice_indices(array_height: int, slice_height: int) -> List[dict]:
|
|
195
194
|
"""Calculate slice indices and drop information for array slicing.
|
|
@@ -346,10 +345,31 @@ def calculate_slice_indices(array_height: int, slice_height: int) -> List[dict]:
|
|
|
346
345
|
return slice_info_list
|
|
347
346
|
|
|
348
347
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
348
|
+
def _identify_mission_type(filename: str) -> str:
|
|
349
|
+
"""Identify the mission type based on filename.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
filename: Name of the product file.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
Product type string.
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
ValueError: If product type cannot be determined.
|
|
359
|
+
"""
|
|
360
|
+
# TODO: double check naming schemes
|
|
361
|
+
if 'BIO' in filename:
|
|
362
|
+
return 'BIOMASS'
|
|
363
|
+
if 'TSX' in filename:
|
|
364
|
+
return 'TerraSAR'
|
|
365
|
+
if 'S1' in filename:
|
|
366
|
+
return 'Sentinel-1'
|
|
367
|
+
elif 'CSK' in filename:
|
|
368
|
+
return 'COSMO-SkyMed'
|
|
369
|
+
elif 'SAO' in filename:
|
|
370
|
+
return 'SAOCOM'
|
|
371
|
+
else:
|
|
372
|
+
raise ValueError(f'Unknown product type for file: {filename}')
|
|
353
373
|
|
|
354
374
|
|
|
355
375
|
def save_matlab_mat(data_object: Any, filename: str, filepath: Union[str, Path]) -> bool:
|
|
@@ -378,6 +398,7 @@ def save_matlab_mat(data_object: Any, filename: str, filepath: Union[str, Path])
|
|
|
378
398
|
print(f"Could not save MATLAB file to {savename}: {e}")
|
|
379
399
|
return False
|
|
380
400
|
|
|
401
|
+
|
|
381
402
|
def delete(path_to_delete: Union[str, Path]):
|
|
382
403
|
"""Deletes a file or directory.
|
|
383
404
|
|
|
@@ -393,6 +414,7 @@ def delete(path_to_delete: Union[str, Path]):
|
|
|
393
414
|
else:
|
|
394
415
|
path_to_delete.unlink() # Use unlink for files
|
|
395
416
|
|
|
417
|
+
|
|
396
418
|
def unzip(path_to_zip_file: Union[str, Path]):
|
|
397
419
|
"""Unzips a file to its parent directory.
|
|
398
420
|
|
|
@@ -404,6 +426,7 @@ def unzip(path_to_zip_file: Union[str, Path]):
|
|
|
404
426
|
with ZipFile(zip_path, 'r') as zip_ref:
|
|
405
427
|
zip_ref.extractall(output_dir)
|
|
406
428
|
|
|
429
|
+
|
|
407
430
|
def delProd(prodToDelete: Union[str, Path]):
|
|
408
431
|
"""Deletes a SNAP product (.dim file and associated .data directory).
|
|
409
432
|
|
|
@@ -419,6 +442,7 @@ def delProd(prodToDelete: Union[str, Path]):
|
|
|
419
442
|
delete(dim_file)
|
|
420
443
|
delete(data_dir)
|
|
421
444
|
|
|
445
|
+
|
|
422
446
|
def command_line(cmd: str):
|
|
423
447
|
"""Executes a command line process and prints its output.
|
|
424
448
|
|
|
@@ -437,6 +461,7 @@ def command_line(cmd: str):
|
|
|
437
461
|
except FileNotFoundError:
|
|
438
462
|
print(f"Error: Command not found - ensure the executable is in the system's PATH or provide the full path.")
|
|
439
463
|
|
|
464
|
+
|
|
440
465
|
def iterNodes(root, val_dict: dict) -> dict:
|
|
441
466
|
"""Recursively iterates through XML nodes and extracts tag/text pairs.
|
|
442
467
|
|
|
@@ -461,6 +486,7 @@ def iterNodes(root, val_dict: dict) -> dict:
|
|
|
461
486
|
|
|
462
487
|
return val_dict
|
|
463
488
|
|
|
489
|
+
|
|
464
490
|
def find_dat_file(folder: Path, pol: str) -> Path:
|
|
465
491
|
"""
|
|
466
492
|
Find the .dat file in a SAFE folder for a specific polarization using recursive search.
|
|
@@ -483,3 +509,45 @@ def find_dat_file(folder: Path, pol: str) -> Path:
|
|
|
483
509
|
return file
|
|
484
510
|
|
|
485
511
|
raise FileNotFoundError(f'No valid .dat file found in {folder} for polarization {pol}')
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# =====================================================================
|
|
515
|
+
# Product Readers
|
|
516
|
+
def read_h5(file_path: str) -> tuple[dict, dict]:
|
|
517
|
+
"""Read an HDF5 file and return its contents and metadata as dictionaries.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
file_path: Path to the HDF5 file to read.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Tuple containing:
|
|
524
|
+
- Dictionary with datasets and their values from the HDF5 file
|
|
525
|
+
- Dictionary with metadata (attributes) from the HDF5 file
|
|
526
|
+
|
|
527
|
+
Raises:
|
|
528
|
+
FileNotFoundError: If the file doesn't exist.
|
|
529
|
+
OSError: If the file cannot be opened or read.
|
|
530
|
+
"""
|
|
531
|
+
data = {}
|
|
532
|
+
metadata = {}
|
|
533
|
+
|
|
534
|
+
with h5py.File(file_path, 'r') as h5_file:
|
|
535
|
+
# Extract root attributes
|
|
536
|
+
metadata['root'] = dict(h5_file.attrs)
|
|
537
|
+
|
|
538
|
+
def extract_data(name, obj):
|
|
539
|
+
if isinstance(obj, h5py.Dataset):
|
|
540
|
+
data[name] = obj[()]
|
|
541
|
+
# Extract dataset attributes
|
|
542
|
+
if obj.attrs:
|
|
543
|
+
metadata[name] = dict(obj.attrs)
|
|
544
|
+
elif isinstance(obj, h5py.Group):
|
|
545
|
+
# Extract group attributes
|
|
546
|
+
if obj.attrs:
|
|
547
|
+
metadata[name] = dict(obj.attrs)
|
|
548
|
+
|
|
549
|
+
h5_file.visititems(extract_data)
|
|
550
|
+
|
|
551
|
+
quickinfo = extract_core_metadata_sentinel(metadata.get('metadata/Abstracted_Metadata', {}))
|
|
552
|
+
metadata['quickinfo'] = quickinfo
|
|
553
|
+
return data, metadata
|
sarpyx/utils/meta.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# =====================================================================
|
|
2
|
+
# Sentinel-1 core metadata extraction
|
|
3
|
+
# =====================================================================
|
|
4
|
+
def extract_core_metadata_sentinel(md: dict) -> dict:
|
|
5
|
+
"""
|
|
6
|
+
Extract a minimal, cross-mission-relevant SAR metadata subset
|
|
7
|
+
for geospatial foundation models.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
md (dict): Metadata dictionary containing SAR metadata.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
dict: A dictionary containing the extracted metadata subset with the following keys:
|
|
14
|
+
- MISSION
|
|
15
|
+
- ACQUISITION_MODE
|
|
16
|
+
- PRODUCT_TYPE
|
|
17
|
+
- radar_frequency
|
|
18
|
+
- pulse_repetition_frequency
|
|
19
|
+
- range_spacing
|
|
20
|
+
- azimuth_spacing
|
|
21
|
+
- range_bandwidth
|
|
22
|
+
- azimuth_bandwidth
|
|
23
|
+
- PASS
|
|
24
|
+
- avg_scene_height
|
|
25
|
+
"""
|
|
26
|
+
def _decode(v):
|
|
27
|
+
# SNAP often stores strings as bytes
|
|
28
|
+
if isinstance(v, (bytes, bytearray)):
|
|
29
|
+
return v.decode('utf-8')
|
|
30
|
+
return v
|
|
31
|
+
|
|
32
|
+
keys = [
|
|
33
|
+
'MISSION',
|
|
34
|
+
'ACQUISITION_MODE',
|
|
35
|
+
'PRODUCT_TYPE',
|
|
36
|
+
'radar_frequency',
|
|
37
|
+
'pulse_repetition_frequency',
|
|
38
|
+
'range_spacing',
|
|
39
|
+
'azimuth_spacing',
|
|
40
|
+
'range_bandwidth',
|
|
41
|
+
'azimuth_bandwidth',
|
|
42
|
+
'antenna_pointing',
|
|
43
|
+
'PASS',
|
|
44
|
+
'avg_scene_height',
|
|
45
|
+
'PRODUCT',
|
|
46
|
+
'mds1_tx_rx_polar',
|
|
47
|
+
'mds2_tx_rx_polar',
|
|
48
|
+
'first_line_time',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
k: _decode(md.get(k))
|
|
53
|
+
for k in keys
|
|
54
|
+
if k in md
|
|
55
|
+
}
|