Skip to content

Commit cb10da4

Browse files
committed
Added ERS1 & ERS2 support with the external file.
Calibration vector now properly uses the antenna gain information.
2 parents 2755bb0 + a35e881 commit cb10da4

4 files changed

Lines changed: 168 additions & 120 deletions

File tree

asar_xarray/asar.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ def get_metadata(gdal_dataset: gdal.Dataset) -> Dict[str, Any]:
3535
return attributes
3636

3737

38-
def open_asar_dataset(
39-
filepath: str | os.PathLike[Any] | ReadBuffer[Any] | bytes | memoryview | AbstractDataStore) -> xr.Dataset:
38+
def open_asar_dataset(filepath: str | os.PathLike[Any] | ReadBuffer[Any] | AbstractDataStore) -> xr.Dataset:
4039
"""
4140
Open an ASAR dataset and converts it into an xarray Dataset.
4241
@@ -107,8 +106,8 @@ def create_dataset(metadata: dict[str, Any], filepath: str) -> xr.Dataset:
107106
"product_type": product_type,
108107
"start_time": product_first_line_utc_time,
109108
"stop_time": product_last_line_utc_time,
110-
"range_pixel_spacing": metadata["range_spacing"],
111-
"azimuth_spacing": metadata["azimuth_spacing"],
109+
"range_pixel_spacing" : metadata["range_spacing"],
110+
"azimuth_pixel_spacing" : metadata["azimuth_spacing"],
112111
"radar_frequency": metadata["records"]["main_processing_params"]["radar_freq"] / 1e9,
113112
"ascending_node_time": "",
114113
"azimuth_pixel_spacing": metadata["records"]["main_processing_params"]["azimuth_spacing"],
@@ -117,14 +116,15 @@ def create_dataset(metadata: dict[str, Any], filepath: str) -> xr.Dataset:
117116
"azimuth_time_interval": azimuth_time_interval,
118117
"image_slant_range_time": image_slant_range_time,
119118
"range_sampling_rate": range_sampling_rate,
120-
"incidence_angle_mid_swath": metadata["direct_parse"]["incidence_angle_center"] * 2 * math.pi / 360,
119+
"incidence_angle_mid_swath": metadata["direct_parse"]["incidence_angle_center"] * 2 * math.pi / 360 ,
121120
"metadata": metadata
122121
}
123122

124123
azimuth_time = compute_azimuth_time(
125124
product_first_line_utc_time, product_last_line_utc_time, number_of_lines
126125
)
127126

127+
128128
swap_dims = {"line": "azimuth_time", "pixel": "slant_range_time"}
129129

130130
coords: dict[str, Any] = {
@@ -180,6 +180,7 @@ def get_chirp_parameters(dataset: gdal.Dataset) -> dict[str, Any]:
180180
new_key = key.replace('CHIRP_PARAMS_ADS_CHIRP_', '').lower()
181181
params['chirp'][new_key] = float(value)
182182

183+
#params['elev_corr_factor'] = float(metadata.get('CHIRP_PARAMS_ADS_ELEV_CORR_FACTOR'))
183184
params['cal_pulse_info'] = get_chirp_cal_pulse_info(metadata)
184185

185186
return params
Binary file not shown.
Binary file not shown.

asar_xarray/envisat_direct.py

Lines changed: 162 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import math
66
import pathlib
7+
import numpy as np
78

89

910
def parse_int(s: str) -> int:
@@ -14,6 +15,35 @@ def parse_int(s: str) -> int:
1415
return int(s)
1516

1617

18+
19+
def calc_distance(latitude, longitude, altitude=0.0):
20+
"""Lat/Lon to distance from earth center"""
21+
DTOR = math.pi / 180.0
22+
RTOD = 180 / math.pi
23+
A = 6378137.0
24+
B = 6356752.3142451794975639665996337
25+
FLAT_EARTH_COEF = 1.0 / ((A - B) / A)
26+
E2 = 2.0 / FLAT_EARTH_COEF - 1.0 / (FLAT_EARTH_COEF * FLAT_EARTH_COEF)
27+
EP2 = E2 / (1 - E2)
28+
29+
lat = latitude * DTOR
30+
lon = longitude * DTOR
31+
32+
sin_lat = math.sin(lat)
33+
cos_lat = math.cos(lat)
34+
35+
N = (A / math.sqrt(1.0 - E2 * sin_lat * sin_lat))
36+
NcosLat = (N + altitude) * cos_lat
37+
38+
sin_lon = math.sin(lon)
39+
cos_lon = math.cos(lon)
40+
41+
x_pos = NcosLat * cos_lon
42+
y_pos = NcosLat * sin_lon
43+
z_pos = (N + altitude - E2 * N) * sin_lat
44+
return math.sqrt(x_pos**2 + y_pos**2 + z_pos**2)
45+
46+
1747
class EnvisatADS:
1848
"""Class representing an Envisat Annotation Data Set (ADS) descriptor."""
1949

@@ -32,7 +62,7 @@ def __str__(self) -> str:
3262
return "Envisat ADS: \"{}\" {} {} {}".format(self.name, self.offset, self.size, self.num)
3363

3464

35-
def parse_direct(path: str, gdal_metadata: dict[str, Any]) -> dict[str, Any]:
65+
def parse_direct(path: str, gdal_metadata) -> dict[str, Any]:
3666
"""
3767
Parse an Envisat product file and extract relevant metadata fields.
3868
@@ -42,7 +72,8 @@ def parse_direct(path: str, gdal_metadata: dict[str, Any]) -> dict[str, Any]:
4272
4373
returns: Dictionary containing extracted metadata fields.
4474
"""
45-
metadata: dict[str, Any] = {}
75+
metadata = {}
76+
file_buffer = None
4677
with open(path, "rb") as fp:
4778
file_buffer = fp.read()
4879

@@ -59,145 +90,161 @@ def parse_direct(path: str, gdal_metadata: dict[str, Any]) -> dict[str, Any]:
5990

6091
for i in range(dsd_num):
6192
ads = EnvisatADS(dsd_buf[i * dsd_size:(i + 1) * dsd_size])
93+
#print(ads.name)
6294
if ads.name == "GEOLOCATION GRID ADS":
63-
extract_geolocation_metadata(ads, file_buffer, metadata)
95+
rec_size = 521
96+
assert ((ads.size // ads.num) == rec_size)
97+
geoloc_buf = file_buffer[ads.offset:ads.offset + ads.size]
98+
middle_idx = ads.num // 2
99+
geoloc_record = geoloc_buf[middle_idx * rec_size: (middle_idx + 1) * rec_size]
100+
# Geolocation Grid ADSRs header
101+
header_size = 12 + 1 + 4 + 4 + 4
102+
103+
lats_buffer = geoloc_buf[header_size + 11 * 4 * 3 : header_size + 11 * 4 * 4]
104+
lons_buffer = geoloc_buf[header_size + 11 * 4 * 4: header_size + 11 * 4 * 5]
105+
106+
107+
lats = list(struct.unpack(">11i", lats_buffer))
108+
lons = list(struct.unpack(">11i", lons_buffer))
109+
lats = [e * 1e-6 for e in lats]
110+
lons = [e * 1e-6 for e in lons]
111+
112+
113+
# tiepoints, 11 of big endian floats for each of the following:
114+
# samp numbers, slant range times, angles, lats, longs
115+
block_size = 11 * 4
116+
slant_time_offset = header_size + 1 * block_size
117+
incidence_angle_offset = header_size + 2 * block_size
118+
# adjust to middle of 11
119+
incidence_angle_offset += 5 * 4
120+
121+
slant_time_first = struct.unpack(">f", geoloc_record[slant_time_offset:slant_time_offset + 4])[0]
122+
incidence_angle_middle = \
123+
struct.unpack(">f", geoloc_record[incidence_angle_offset:incidence_angle_offset + 4])[0]
124+
125+
64126

127+
metadata["slant_time_first"] = slant_time_first
128+
metadata["incidence_angle_center"] = incidence_angle_middle
129+
130+
131+
132+
ext_cal_buf = None
65133
if ads.name == "EXTERNAL CALIBRATION":
66-
extract_external_calibration_metadata(ads, gdal_metadata, metadata)
67134

68-
if ads.name == "SR GR ADS" and ads.size > 0:
69-
srgr_coeffs = extract_srgr_coeffs(ads, file_buffer)
135+
auxfolder = pathlib.Path(os.path.abspath(__file__)).parent
136+
prod_name = gdal_metadata["product"]
137+
auxfolder /= "aux"
138+
if ".N1" in prod_name:
139+
auxfolder /= "ASAR_Auxiliary_Files"
140+
auxfolder /= "ASA_XCA_AX"
141+
elif ".E1" in prod_name:
142+
auxfolder /= "ERS1"
143+
elif ".E2" in prod_name:
144+
auxfolder /= "ERS2"
145+
146+
for p in os.scandir(auxfolder):
147+
if ads.filename == p.name:
148+
with open(p.path, "rb") as fp:
149+
ext_cal_buf = fp.read()
150+
pol = gdal_metadata["mds1_tx_rx_polar"]
151+
swath = gdal_metadata["swath"]
152+
swath_offset = ord(swath[2]) - ord("1")
153+
pol_offset = {"H/H" : 0, "V/V" :1, "H/V" : 2, "V/H":3}[pol]
154+
155+
# Envisat_Product_Spec_Vol8.pdf
156+
# 8.6.2 External Calibration Data
157+
offset = 1247 + 378
158+
159+
offset += 12
160+
offset += 4
161+
offset += 26 * 28 + 4 * 4
162+
163+
mid_angles = struct.unpack(">7f", ext_cal_buf[offset:offset+4*7])
164+
165+
offset += 8 * 4
166+
167+
offset += 804 * 4 * swath_offset
168+
offset += 201 * 4 * pol_offset
169+
170+
antenna_gains = struct.unpack(">201f", ext_cal_buf[offset:offset + 4 * 201])
171+
metadata["antenna_ref_elev_angle"] = mid_angles[swath_offset]
172+
173+
if ads.name == "SR GR ADS" and ads.size > 0:
174+
srgr_buf = file_buffer[ads.offset:ads.offset + ads.size]
175+
176+
r = struct.unpack(">ff5f", srgr_buf[13:41])
177+
178+
179+
srgr_coeffs = list(r[2:])
70180
metadata["srgr_coeffs"] = srgr_coeffs
71181

72-
# calculate spreading loss compensation
73-
c = 299792458
74182

183+
184+
185+
186+
187+
# antenna gain
188+
c = 299792458
75189
n_samp = gdal_metadata["line_length"]
190+
R_first = c * slant_time_first * 1e-9 / 2
76191
range_spacing = c / (2 * gdal_metadata["records"]["main_processing_params"]["range_samp_rate"])
77192
range_ref = gdal_metadata["records"]["main_processing_params"]["range_ref"]
78-
r_first = c * metadata["slant_time_first"] * 1e-9 / 2
193+
R_first = c * slant_time_first * 1e-9 / 2
79194

80-
spreading_loss = []
81-
for n in range(n_samp):
82-
r = r_first + n * range_spacing
83-
factor = math.sqrt((range_ref / r) ** 3)
84-
spreading_loss.append(1 / factor)
195+
osv = gdal_metadata["records"]["main_processing_params"]["orbit_state_vectors"]
85196

86-
cal_factor = gdal_metadata["records"]["main_processing_params"]["calibration_factors"][0]["ext_cal_fact"]
197+
sat_x = osv[0]["x_pos_1"] * 1e-2
198+
sat_y = osv[0]["y_pos_1"] * 1e-2
199+
sat_z = osv[0]["z_pos_1"] * 1e-2
87200

88-
metadata["cal_factor"] = cal_factor
89-
metadata["cal_vector"] = spreading_loss
201+
gain_arr = []
202+
for n in range(n_samp):
203+
# https://github.com/senbox-org/microwave-toolbox/blob/master/sar-op-calibration/src/main/java/eu/esa/sar/calibration/gpf/calibrators/ASARCalibrator.java
204+
R = R_first + n * range_spacing
205+
n_geogrid = len(lats)
206+
geo_interp_idx = (n / n_samp) * (len(lats) - 1)
207+
idx = int(geo_interp_idx)
208+
fract = geo_interp_idx % 1
209+
#interpolate geogrid to match first line geogrid to first OSV
210+
lat = lats[idx] + (lats[idx+1] - lats[idx]) * fract
211+
lon = lons[idx] + (lons[idx+1] - lons[idx]) * fract
90212

91-
return metadata
213+
sar_dis = math.sqrt(sat_x**2 + sat_y**2 + sat_z**2)
92214

215+
distance = calc_distance(lat, lon)
93216

94-
def extract_geolocation_metadata(ads: EnvisatADS, file_buffer: bytes, metadata: dict[str, Any]) -> None:
95-
"""
96-
Extract geolocation metadata from the GEOLOCATION GRID ADS record.
217+
# elevation angle, cosine law from three sides, slant range, earth center to satellite, earth center to target
218+
angle_cos = (R * R + sar_dis * sar_dis - distance * distance) / (2 * R * sar_dis)
219+
elev_angle = np.rad2deg(math.acos(angle_cos))
97220

98-
Args:
99-
----
100-
ads: EnvisatADS object containing ADS descriptor information.
101-
file_buffer: The full file buffer as bytes.
102-
metadata: Dictionary to store extracted metadata.
221+
# find the gain from LUT, with 201 gains against reference elevation angle with 0.05 degree steps
222+
elev_idx = int((elev_angle - metadata["antenna_ref_elev_angle"]) / 0.05)
223+
elev_idx += 100
224+
gain = 1.0
225+
if elev_idx >= 0 and elev_idx <= len(antenna_gains):
226+
# dB -> linear
227+
gain = math.pow(10, antenna_gains[elev_idx] / 10)
103228

104-
Populates the metadata dictionary with:
105-
- slant_time_first: First slant range time (float).
106-
- incidence_angle_center: Incidence angle at the center (float).
107-
"""
108-
rec_size = 521
109-
assert ((ads.size // ads.num) == rec_size)
110-
geoloc_buf = file_buffer[ads.offset:ads.offset + ads.size]
111-
middle_idx = ads.num // 2
112-
geoloc_record = geoloc_buf[middle_idx * rec_size: (middle_idx + 1) * rec_size]
113-
# Geolocation Grid ADSRs header
114-
header_size = 12 + 1 + 4 + 4 + 4
115-
116-
lats_buffer = geoloc_buf[header_size + 11 * 4 * 3: header_size + 11 * 4 * 4]
117-
lons_buffer = geoloc_buf[header_size + 11 * 4 * 4: header_size + 11 * 4 * 5]
118-
119-
lats = list(struct.unpack(">11i", lats_buffer))
120-
lons = list(struct.unpack(">11i", lons_buffer))
121-
lats = [e * 1e-6 for e in lats]
122-
lons = [e * 1e-6 for e in lons]
123-
124-
# tiepoints, 11 of big endian floats for each of the following:
125-
# samp numbers, slant range times, angles, lats, longs
126-
block_size = 11 * 4
127-
slant_time_offset = header_size + 1 * block_size
128-
incidence_angle_offset = header_size + 2 * block_size
129-
# adjust to middle of 11
130-
incidence_angle_offset += 5 * 4
131-
132-
slant_time_first = struct.unpack(">f", geoloc_record[slant_time_offset:slant_time_offset + 4])[0]
133-
incidence_angle_middle = \
134-
struct.unpack(">f", geoloc_record[incidence_angle_offset:incidence_angle_offset + 4])[0]
135-
136-
metadata["slant_time_first"] = slant_time_first
137-
metadata["incidence_angle_center"] = incidence_angle_middle
138-
139-
140-
def extract_external_calibration_metadata(ads: EnvisatADS, gdal_metadata: dict[str, Any],
141-
metadata: dict[str, Any]) -> None:
142-
"""
143-
Extract external calibration metadata from the EXTERNAL CALIBRATION ADS record.
229+
gain_arr.append(gain)
144230

145-
Args:
146-
----
147-
ads: EnvisatADS object containing ADS descriptor information.
148-
gdal_metadata: Dictionary with GDAL metadata required for extraction.
149-
metadata: Dictionary to store extracted metadata.
150-
151-
Populates the metadata dictionary with:
152-
- antenna_ref_elev_angle: Reference elevation angle for the antenna (float).
153-
- antenna_elev_gains: List of antenna elevation gain values (tuple of 201 floats).
154-
"""
155-
aux_folder = pathlib.Path(os.path.abspath(__file__)).parent
156-
aux_folder /= "aux"
157-
aux_folder /= "ASAR_Auxiliary_Files"
158-
aux_folder /= "ASA_XCA_AX"
159231

160-
for p in os.scandir(aux_folder):
161-
if ads.filename == p.name:
162-
with open(p.path, "rb") as fp:
163-
ext_cal_buf = fp.read()
164-
pol = gdal_metadata["mds1_tx_rx_polar"]
165-
swath = gdal_metadata["swath"]
166-
swath_offset = ord(swath[2]) - ord("1")
167-
pol_offset = {"H/H": 0, "V/V": 1, "H/V": 2, "V/H": 3}[pol]
168232

169-
# Envisat_Product_Spec_Vol8.pdf
170-
# 8.6.2 External Calibration Data
171-
offset = 1247 + 378
172233

173-
offset += 12
174-
offset += 4
175-
offset += 26 * 28 + 4 * 4
176234

177-
mid_angles = struct.unpack(">7f", ext_cal_buf[offset:offset + 4 * 7])
178235

179-
offset += 8 * 4
180-
181-
offset += 804 * 4 * swath_offset
182-
offset += 201 * 4 * pol_offset
236+
# calculate spreading loss compensation
183237

184-
antenna_gains = struct.unpack(">201f", ext_cal_buf[offset:offset + 4 * 201])
185-
metadata["antenna_ref_elev_angle"] = mid_angles[swath_offset]
186-
metadata["antenna_elev_gains"] = antenna_gains
238+
spreading_loss = []
239+
for n in range(n_samp):
240+
R = R_first + n * range_spacing
241+
factor = math.sqrt((range_ref / R) ** 3)
242+
spreading_loss.append(1/factor)
187243

244+
cal_factor = gdal_metadata["records"]["main_processing_params"]["calibration_factors"][0]["ext_cal_fact"]
188245

189-
def extract_srgr_coeffs(ads: EnvisatADS, file_buffer: bytes) -> list[float]:
190-
"""
191-
Extract SRGR (Slant Range to Ground Range) coefficients from the SR GR ADS record.
246+
metadata["cal_factor"] = cal_factor
247+
metadata["cal_vector"] = np.array(spreading_loss) * np.array(gain_arr)
192248

193-
Args:
194-
----
195-
ads: EnvisatADS object containing ADS descriptor information.
196-
file_buffer: The full file buffer as bytes.
197249

198-
Returns: List of SRGR coefficients (list of 5 floats).
199-
"""
200-
srgr_buf = file_buffer[ads.offset:ads.offset + ads.size]
201-
r = struct.unpack(">ff5f", srgr_buf[13:41])
202-
srgr_coeffs = list(r[2:])
203-
return srgr_coeffs
250+
return metadata

0 commit comments

Comments
 (0)