sarkit-convert 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.
@@ -0,0 +1,885 @@
1
+ """
2
+ =====================
3
+ Iceye Complex to SICD
4
+ =====================
5
+
6
+ Convert a complex image from the Iceye HD5 SLC into SICD.
7
+
8
+ Note: In the development of this converter "Iceye Product Metadata" description (v2.1, v2.2, v2.4, v2.5) was considered.
9
+
10
+ """
11
+
12
+ import argparse
13
+ import copy
14
+ import pathlib
15
+
16
+ import dateutil.parser
17
+ import h5py
18
+ import lxml.builder
19
+ import numpy as np
20
+ import numpy.linalg as npl
21
+ import numpy.polynomial.polynomial as npp
22
+ import sarkit.sicd as sksicd
23
+ import sarkit.wgs84
24
+ from sarkit import _constants
25
+ from sarkit.verification import SicdConsistency
26
+
27
+ from sarkit_convert import _utils as utils
28
+
29
+ NSMAP = {
30
+ "sicd": "urn:SICD:1.4.0",
31
+ }
32
+
33
+ PIXEL_TYPE_MAP = {
34
+ "float32": "RE32F_IM32F",
35
+ "int16": "RE16I_IM16I",
36
+ }
37
+
38
+
39
+ def _extract_attributes(h5_obj):
40
+ """Recursively extract all dataset names and values into a dictionary, skipping specified keys and decoding byte strings."""
41
+ result = {}
42
+
43
+ for key in h5_obj: # Iterate over keys in the HDF5 group
44
+ item = h5_obj[key]
45
+ if isinstance(item, h5py.Dataset):
46
+ value = item[...]
47
+ if isinstance(value, bytes):
48
+ value = value.decode("utf-8") # Decode byte string
49
+ elif isinstance(value, np.ndarray) and value.dtype.kind == "S":
50
+ value = value.astype(str).tolist() # Handle ndarrays with type string
51
+ elif isinstance(value, np.ndarray) and value.dtype.kind == "O":
52
+ value = value.item().decode("utf-8") # Handle ndarrays with type object
53
+ elif isinstance(value, np.ndarray) and value.size == 1:
54
+ value = value.item() # Handle single value arrays
55
+
56
+ result[key] = value
57
+ elif isinstance(item, h5py.Group): # If it is a group, recurse into it
58
+ if np.array_equal(item.attrs.get("type"), [b"hickle"]):
59
+ result[key] = None
60
+ else:
61
+ result[key] = _extract_attributes(item)
62
+
63
+ return result
64
+
65
+
66
+ def compute_apc_poly(h5_attrs, start_time, stop_time):
67
+ """Creates an Aperture Phase Center (APC) poly that orbits the Earth above the equator.
68
+
69
+ Polynomial generates 3D coords in ECF as a function of time from start of collect.
70
+
71
+ Parameters
72
+ ----------
73
+ h5_attrs: dict
74
+ The collection metadata
75
+ start_time: float
76
+ The start time to fit.
77
+ stop_time: float
78
+ The end time to fit.
79
+
80
+ Returns
81
+ -------
82
+ `numpy.ndarray`, shape=(6, 3)
83
+ APC poly
84
+ """
85
+ times_str = np.array(h5_attrs["state_vector_time_utc"]).flatten()
86
+ state_times = np.asarray(
87
+ [dateutil.parser.parse(entry) for entry in times_str], dtype=np.datetime64
88
+ )
89
+ times = (state_times - np.datetime64(start_time)) / np.timedelta64(1, "s")
90
+ positions = np.zeros((times.size, 3), dtype="float64")
91
+ velocities = np.zeros((times.size, 3), dtype="float64")
92
+
93
+ positions[:, :] = np.stack(
94
+ (h5_attrs["posX"], h5_attrs["posY"], h5_attrs["posZ"]), axis=1
95
+ )
96
+ velocities[:, :] = np.stack(
97
+ (h5_attrs["velX"], h5_attrs["velY"], h5_attrs["velZ"]), axis=1
98
+ )
99
+
100
+ apc_poly = utils.fit_state_vectors(
101
+ (0, (stop_time - start_time).total_seconds()),
102
+ times,
103
+ positions,
104
+ velocities,
105
+ order=5,
106
+ )
107
+
108
+ return apc_poly
109
+
110
+
111
+ def _update_radiometric_node(sicd_xmltree):
112
+ """Use existing metadata to populate the Radiometric XML node."""
113
+ xmlhelp = sksicd.XmlHelper(copy.deepcopy(sicd_xmltree))
114
+
115
+ def get_slant_plane_area(xmlhelp):
116
+ row_imp_resp_bw = xmlhelp.load("./{*}Grid/{*}Row/{*}ImpRespBW")
117
+ col_imp_resp_bw = xmlhelp.load("./{*}Grid/{*}Col/{*}ImpRespBW")
118
+ range_weight_f = azimuth_weight_f = 1.0
119
+ row_wgt_funct = xmlhelp.load("./{*}Grid/{*}Row/{*}WgtFunct")
120
+ if row_wgt_funct is not None:
121
+ var = np.var(row_wgt_funct)
122
+ mean = np.mean(row_wgt_funct)
123
+ range_weight_f += var / (mean * mean)
124
+ col_wgt_funct = xmlhelp.load("./{*}Grid/{*}Col/{*}WgtFunct")
125
+ if col_wgt_funct is not None:
126
+ var = np.var(col_wgt_funct)
127
+ mean = np.mean(col_wgt_funct)
128
+ azimuth_weight_f += var / (mean * mean)
129
+ return (range_weight_f * azimuth_weight_f) / (row_imp_resp_bw * col_imp_resp_bw)
130
+
131
+ sp_area = get_slant_plane_area(xmlhelp)
132
+ radiometric_node = xmlhelp.element_tree.find("./{*}Radiometric")
133
+ scpcoa_slope_ang = xmlhelp.load("./{*}SCPCOA/{*}SlopeAng")
134
+ scpcoa_graze_ang = xmlhelp.load("./{*}SCPCOA/{*}GrazeAng")
135
+ if radiometric_node.find("{*}BetaZeroSFPoly") is None:
136
+ if radiometric_node.find("{*}RCSSFPoly") is not None:
137
+ beta_zero_sf_poly_coefs = (
138
+ xmlhelp.load_elem(radiometric_node.find("{*}RCSSFPoly")) / sp_area
139
+ )
140
+ elif radiometric_node.find("{*}SigmaZeroSFPoly") is not None:
141
+ beta_zero_sf_poly_coefs = xmlhelp.load_elem(
142
+ radiometric_node.find("{*}SigmaZeroSFPoly")
143
+ ) / np.cos(np.deg2rad(scpcoa_slope_ang))
144
+ elif radiometric_node.find("{*}GammaZeroSFPoly") is not None:
145
+ beta_zero_sf_poly_coefs = xmlhelp.load_elem(
146
+ radiometric_node.find("{*}GammaZeroSFPoly")
147
+ ) * (
148
+ np.sin(np.deg2rad(scpcoa_graze_ang))
149
+ / np.cos(np.deg2rad(scpcoa_slope_ang))
150
+ )
151
+ else:
152
+ beta_zero_sf_poly_coefs = xmlhelp.load_elem(
153
+ radiometric_node.find("{*}BetaZeroSFPoly")
154
+ )
155
+
156
+ if beta_zero_sf_poly_coefs is not None:
157
+ # In other words, none of the SF polynomials are populated.
158
+ if radiometric_node.find("{*}RCSSFPoly") is None:
159
+ rcs_sf_poly_coefs = beta_zero_sf_poly_coefs * sp_area
160
+ if radiometric_node.find("{*}SigmaZeroSFPoly") is None:
161
+ sigma_zero_sf_poly_coefs = beta_zero_sf_poly_coefs * np.cos(
162
+ np.deg2rad(scpcoa_slope_ang)
163
+ )
164
+ if radiometric_node.find("{*}GammaZeroSFPoly") is None:
165
+ gamma_zero_sf_poly_coefs = beta_zero_sf_poly_coefs * (
166
+ np.cos(np.deg2rad(scpcoa_slope_ang))
167
+ / np.sin(np.deg2rad(scpcoa_graze_ang))
168
+ )
169
+
170
+ sicd = lxml.builder.ElementMaker(
171
+ namespace=NSMAP["sicd"], nsmap={None: NSMAP["sicd"]}
172
+ )
173
+ new_radiometric_node = sicd.Radiometric(
174
+ sicd.RCSSFPoly(),
175
+ sicd.SigmaZeroSFPoly(),
176
+ sicd.BetaZeroSFPoly(),
177
+ sicd.GammaZeroSFPoly(),
178
+ )
179
+ sksicd.Poly2dType().set_elem(
180
+ new_radiometric_node.find("./{*}RCSSFPoly"), rcs_sf_poly_coefs
181
+ )
182
+ sksicd.Poly2dType().set_elem(
183
+ new_radiometric_node.find("./{*}SigmaZeroSFPoly"), sigma_zero_sf_poly_coefs
184
+ )
185
+ sksicd.Poly2dType().set_elem(
186
+ new_radiometric_node.find("./{*}BetaZeroSFPoly"), beta_zero_sf_poly_coefs
187
+ )
188
+ sksicd.Poly2dType().set_elem(
189
+ new_radiometric_node.find("./{*}GammaZeroSFPoly"), gamma_zero_sf_poly_coefs
190
+ )
191
+
192
+ return new_radiometric_node
193
+
194
+
195
+ def _get_x_y_coords(num_row_col, spacing_row_col, scp_pixel, start_row_col):
196
+ """Create the X, Y coordinates of the full image"""
197
+ full_img_verticies = np.array(
198
+ [
199
+ [0, 0],
200
+ [0, num_row_col[1] - 1],
201
+ [num_row_col[0] - 1, num_row_col[1] - 1],
202
+ [num_row_col[0] - 1, 0],
203
+ ],
204
+ )
205
+
206
+ x_coords = spacing_row_col[0] * (
207
+ full_img_verticies[:, 0] - (scp_pixel[0] - start_row_col[0])
208
+ )
209
+ y_coords = spacing_row_col[1] * (
210
+ full_img_verticies[:, 1] - (scp_pixel[1] - start_row_col[1])
211
+ )
212
+
213
+ return x_coords, y_coords
214
+
215
+
216
+ def _calc_deltaks(x_coords, y_coords, deltak_coa_poly, imp_resp_bw, spacing):
217
+ """Calculate the minimum and maximum DeltaK values"""
218
+ deltaks = npp.polyval2d(x_coords, y_coords, deltak_coa_poly)
219
+ min_deltak = np.amin(deltaks) - 0.5 * imp_resp_bw
220
+ max_deltak = np.amax(deltaks) + 0.5 * imp_resp_bw
221
+
222
+ if (min_deltak < -0.5 / abs(spacing)) or (max_deltak > 0.5 / abs(spacing)):
223
+ min_deltak = -0.5 / abs(spacing)
224
+ max_deltak = -min_deltak
225
+
226
+ return min_deltak, max_deltak
227
+
228
+
229
+ def hdf5_to_sicd(h5_filename, sicd_filename, classification, ostaid="Unknown"):
230
+ """Converts Iceye native SLC h5 files to NGA standard SICD files.
231
+
232
+ Parameters
233
+ ----------
234
+ h5_filename: str
235
+ path of the input HDF5 file
236
+ sicd_filename: str
237
+ path of the output SICD file.
238
+ classification: str
239
+ content of the /SICD/CollectionInfo/Classification node in the SICD XML.
240
+ ostaid: str, optional
241
+ content of the originating station ID (OSTAID) field of the NITF header.
242
+
243
+ """
244
+ with h5py.File(h5_filename, "r") as h5file:
245
+ h5_attrs = _extract_attributes(h5file)
246
+
247
+ # Timeline
248
+ collect_start = dateutil.parser.parse(h5_attrs["acquisition_start_utc"])
249
+ collect_stop = dateutil.parser.parse(h5_attrs["acquisition_end_utc"])
250
+ collect_duration = (collect_stop - collect_start).total_seconds()
251
+ acq_prf = h5_attrs["acquisition_prf"]
252
+ num_pulses = int(np.round(collect_duration * acq_prf))
253
+ t_start = 0
254
+ t_end = collect_duration
255
+ ipp_start = 0
256
+ ipp_end = int(num_pulses - 1)
257
+ ipp_poly = [0, acq_prf]
258
+ look = {"left": 1, "right": -1}[h5_attrs["look_side"].lower()]
259
+
260
+ # Collection Info
261
+ collector_name = h5_attrs["satellite_name"]
262
+ core_name = h5_attrs["product_name"]
263
+ collect_type = "MONOSTATIC"
264
+ mode_id = h5_attrs["product_type"]
265
+ mode_type = h5_attrs["acquisition_mode"].upper()
266
+ if not mode_type:
267
+ mode_type = "DYNAMIC STRIPMAP"
268
+
269
+ # Creation Info
270
+ creation_application = f"ICEYE_P_{h5_attrs['processor_version']}"
271
+ creation_date_time = dateutil.parser.parse(h5_attrs["processing_time"])
272
+
273
+ # Image Data
274
+ samp_prec = h5_attrs["sample_precision"]
275
+ pixel_type = PIXEL_TYPE_MAP[samp_prec]
276
+ num_rows = int(h5_attrs["number_of_range_samples"])
277
+ num_cols = int(h5_attrs["number_of_azimuth_samples"])
278
+ first_row = 0
279
+ first_col = 0
280
+ scp_pixel = (num_rows // 2, num_cols // 2)
281
+
282
+ # # Geo Data
283
+ coord_center = h5_attrs["coord_center"]
284
+ avg_scene_height = float(h5_attrs["avg_scene_height"])
285
+ init_scp_llh = [coord_center[2], coord_center[3], avg_scene_height]
286
+
287
+ # Position
288
+ apc_poly = compute_apc_poly(h5_attrs, collect_start, collect_stop)
289
+
290
+ # Radar Collection
291
+ center_frequency = h5_attrs["carrier_frequency"]
292
+ tx_rf_bw = h5_attrs["chirp_bandwidth"]
293
+ tx_freq_min = center_frequency - 0.5 * tx_rf_bw
294
+ tx_freq_max = center_frequency + 0.5 * tx_rf_bw
295
+ tx_pulse_length = h5_attrs["chirp_duration"]
296
+ rcv_demod_type = "CHIRP"
297
+ adc_sample_rate = h5_attrs["range_sampling_rate"]
298
+ tx_fm_rate = tx_rf_bw / tx_pulse_length
299
+ tx_polarization = h5_attrs["polarization"][0]
300
+ rcv_polarization = h5_attrs["polarization"][1]
301
+ tx_rcv_polarization = f"{tx_polarization}:{rcv_polarization}"
302
+
303
+ row_bw = 2 * tx_rf_bw / _constants.speed_of_light
304
+
305
+ # Image Formation
306
+ tx_rcv_polarization_proc = tx_rcv_polarization
307
+ image_form_algo = "RMA"
308
+ t_start_proc = 0
309
+ t_end_proc = collect_duration
310
+ tx_freq_proc = (tx_freq_min, tx_freq_max)
311
+ st_beam_comp = "NO"
312
+ image_beam_comp = "SV"
313
+ az_autofocus = "NO"
314
+ rg_autofocus = "NO"
315
+
316
+ def calculate_drate_polys():
317
+ r_ca_coefs = np.array([r_ca_scp, 1], dtype="float64")
318
+ doppler_rate_coefs = h5_attrs["doppler_rate_coeffs"]
319
+ # Prior to ICEYE 1.14 processor, absolute value of Doppler rate was
320
+ # provided, not true Doppler rate. Doppler rate should always be negative
321
+ if doppler_rate_coefs[0] > 0:
322
+ doppler_rate_coefs *= -1
323
+ dop_rate_poly = doppler_rate_coefs
324
+
325
+ def shift(coefs, t_0: float, alpha: float = 1):
326
+ # prepare array workspace
327
+ out = np.copy(coefs)
328
+ if t_0 != 0 and out.size > 1:
329
+ siz = out.size
330
+ for i in range(siz):
331
+ index = siz - i - 1
332
+ if i > 0:
333
+ out[index : siz - 1] -= t_0 * out[index + 1 : siz]
334
+
335
+ if alpha != 1 and out.size > 1:
336
+ out *= np.power(alpha, np.arange(out.size))
337
+
338
+ return out
339
+
340
+ drate_ca_poly_coefs = shift(
341
+ dop_rate_poly,
342
+ t_0=zd_ref_time - rg_time_scp,
343
+ alpha=2 / _constants.speed_of_light,
344
+ )
345
+
346
+ drsf_poly_coefs = (
347
+ -npp.polymul(drate_ca_poly_coefs, r_ca_coefs)
348
+ * _constants.speed_of_light
349
+ / (2 * center_frequency * vm_ca_sq)
350
+ )
351
+
352
+ return drate_ca_poly_coefs, drsf_poly_coefs
353
+
354
+ def calculate_doppler_polys():
355
+ # define and fit the time coa array
356
+ if mode_type == "SPOTLIGHT":
357
+ coa_time = collect_duration / 2
358
+ alpha = 2.0 / _constants.speed_of_light
359
+ pos = npp.polyval(coa_time, apc_poly)
360
+ vel = npp.polyval(coa_time, npp.polyder(apc_poly))
361
+ speed = np.linalg.norm(vel)
362
+ vel_hat = vel / speed
363
+ scp = sarkit.wgs84.geodetic_to_cartesian(
364
+ [coord_center[2], coord_center[3], avg_scene_height]
365
+ )
366
+ los = scp - pos
367
+
368
+ time_coa_poly_coefs = np.array(
369
+ [
370
+ [
371
+ coa_time,
372
+ ],
373
+ ]
374
+ )
375
+ dop_centroid_poly_coefs = np.zeros((2, 2), dtype=np.float64)
376
+ dop_centroid_poly_coefs[0, 1] = (
377
+ -look * center_frequency * alpha * speed / r_ca_scp
378
+ )
379
+ dop_centroid_poly_coefs[1, 1] = (
380
+ look * center_frequency * alpha * speed / (r_ca_scp**2)
381
+ )
382
+ dop_centroid_poly_coefs[:, 0] = -look * (
383
+ dop_centroid_poly_coefs[:, 1] * np.dot(los, vel_hat)
384
+ )
385
+ else:
386
+ # extract doppler centroid coefficients
387
+ dc_estimate_coefs = h5_attrs["dc_estimate_coeffs"]
388
+ dc_time_str = h5_attrs["dc_estimate_time_utc"]
389
+ dc_zd_times = np.zeros((len(dc_time_str),), dtype="float64")
390
+ for i, entry in enumerate(dc_time_str):
391
+ dc_zd_times[i] = (
392
+ dateutil.parser.parse(entry[0]) - collect_start
393
+ ).total_seconds()
394
+ # create a sampled doppler centroid
395
+ samples = 51
396
+ # create doppler time samples
397
+ diff_time_rg = (
398
+ first_pixel_time
399
+ - zd_ref_time
400
+ + np.linspace(0, num_rows / adc_sample_rate, samples)
401
+ )
402
+ # doppler centroid samples definition
403
+ dc_sample_array = np.zeros((samples, dc_zd_times.size), dtype="float64")
404
+ for i, coefs in enumerate(dc_estimate_coefs):
405
+ dc_sample_array[:, i] = npp.polyval(diff_time_rg, coefs)
406
+ # create arrays for range/azimuth from scp in meters
407
+ azimuth_scp_m, range_scp_m = np.meshgrid(
408
+ col_ss * (dc_zd_times - zd_time_scp) / ss_zd_s,
409
+ (diff_time_rg + zd_ref_time - rg_time_scp)
410
+ * _constants.speed_of_light
411
+ / 2,
412
+ )
413
+
414
+ x_order = min(3, range_scp_m.shape[0] - 1)
415
+ y_order = min(3, range_scp_m.shape[1] - 1)
416
+
417
+ # fit the doppler centroid sample array
418
+ dop_centroid_poly_coefs = utils.polyfit2d(
419
+ range_scp_m.flatten(),
420
+ azimuth_scp_m.flatten(),
421
+ dc_sample_array.flatten(),
422
+ x_order,
423
+ y_order,
424
+ )
425
+ doppler_rate_sampled = npp.polyval(azimuth_scp_m, drca_poly_coefs)
426
+ time_coa = dc_zd_times + dc_sample_array / doppler_rate_sampled
427
+ time_coa_poly_coefs = utils.polyfit2d(
428
+ range_scp_m.flatten(),
429
+ azimuth_scp_m.flatten(),
430
+ time_coa.flatten(),
431
+ x_order,
432
+ y_order,
433
+ )
434
+
435
+ return dop_centroid_poly_coefs, time_coa_poly_coefs
436
+
437
+ ss_zd_s = float(h5_attrs["azimuth_time_interval"])
438
+ if look == 1:
439
+ ss_zd_s *= -1
440
+ zero_doppler_left = dateutil.parser.parse(h5_attrs["zerodoppler_end_utc"])
441
+ else:
442
+ zero_doppler_left = dateutil.parser.parse(h5_attrs["zerodoppler_start_utc"])
443
+ dop_bw = h5_attrs["total_processed_bandwidth_azimuth"]
444
+ zd_time_scp = (zero_doppler_left - collect_start).total_seconds() + scp_pixel[
445
+ 1
446
+ ] * ss_zd_s
447
+ first_pixel_time = float(h5_attrs["first_pixel_time"])
448
+ zd_ref_time = first_pixel_time + num_rows / (2 * adc_sample_rate)
449
+ vel_scp = npp.polyval(zd_time_scp, npp.polyder(apc_poly))
450
+ vm_ca_sq = np.sum(vel_scp * vel_scp)
451
+ rg_time_scp = first_pixel_time + scp_pixel[0] / adc_sample_rate
452
+ r_ca_scp = rg_time_scp * _constants.speed_of_light / 2
453
+ drca_poly_coefs, drsf_poly_coefs = calculate_drate_polys()
454
+
455
+ # calculate some doppler dependent grid parameters
456
+ col_ss = float(np.sqrt(vm_ca_sq) * abs(ss_zd_s) * drsf_poly_coefs[0])
457
+ col_bw = dop_bw * abs(ss_zd_s) / col_ss
458
+ time_ca_poly_coefs = [zd_time_scp, ss_zd_s / col_ss]
459
+
460
+ # RMA
461
+ dop_centroid_poly_coefs, time_coa_poly_coefs = calculate_doppler_polys()
462
+ if mode_type == "SPOTLIGHT":
463
+ dop_centroid_poly = np.array([[0]])
464
+ dop_centroid_coa = "false"
465
+ else:
466
+ dop_centroid_poly = dop_centroid_poly_coefs
467
+ dop_centroid_coa = "true"
468
+ first_pixel_time = h5_attrs["first_pixel_time"]
469
+ rg_time_scp = first_pixel_time + adc_sample_rate
470
+ freq_zero = center_frequency
471
+ dr_sf_poly = drsf_poly_coefs[:, np.newaxis]
472
+ time_ca_poly = time_ca_poly_coefs
473
+ rma_algo_type = "OMEGA_K"
474
+ image_type = "INCA"
475
+
476
+ # Grid
477
+ image_plane = "SLANT"
478
+ grid_type = "RGZERO"
479
+ row_ss = _constants.speed_of_light / (2 * adc_sample_rate)
480
+ row_imp_res_bw = row_bw
481
+ row_sgn = -1
482
+ row_kctr = str(center_frequency / (_constants.speed_of_light / 2))
483
+ row_deltak_coa_poly = np.array([[0]])
484
+
485
+ col_ss = col_ss
486
+ col_imp_res_bw = col_bw
487
+ col_sgn = -1
488
+ col_kctr = 0
489
+ time_coa_poly = time_coa_poly_coefs
490
+ col_deltak_coa_poly = dop_centroid_poly_coefs * ss_zd_s / col_ss
491
+
492
+ row_win = h5_attrs["window_function_range"]
493
+ col_win = h5_attrs["window_function_azimuth"]
494
+ if row_win == "NONE":
495
+ row_win = "UNIFORM"
496
+ if col_win == "NONE":
497
+ col_win = "UNIFORM"
498
+ row_brodening_factor = utils.broadening_from_amp(np.ones(256))
499
+ col_brodening_factor = utils.broadening_from_amp(np.ones(256))
500
+ row_imp_res_wid = row_brodening_factor / row_imp_res_bw
501
+ col_imp_res_wid = col_brodening_factor / col_imp_res_bw
502
+
503
+ x_coords, y_coords = _get_x_y_coords(
504
+ [num_rows, num_cols], [row_ss, col_ss], scp_pixel, [first_row, first_col]
505
+ )
506
+
507
+ row_delta_k1, row_delta_k2 = _calc_deltaks(
508
+ x_coords, y_coords, row_deltak_coa_poly, row_imp_res_bw, row_ss
509
+ )
510
+ col_delta_k1, col_delta_k2 = _calc_deltaks(
511
+ x_coords, y_coords, col_deltak_coa_poly, col_imp_res_bw, col_ss
512
+ )
513
+
514
+ # Adjust SCP
515
+ scp_drsf = dr_sf_poly[0, 0]
516
+ scp_tca = time_ca_poly[0]
517
+ scp_tcoa = time_coa_poly[0, 0]
518
+ scp_delta_t_coa = scp_tcoa - scp_tca
519
+ scp_varp_ca_mag = npl.norm(npp.polyval(scp_tca, npp.polyder(apc_poly)))
520
+ scp_rcoa = np.sqrt(r_ca_scp**2 + scp_drsf * scp_varp_ca_mag**2 * scp_delta_t_coa**2)
521
+ scp_rratecoa = scp_drsf / scp_rcoa * scp_varp_ca_mag**2 * scp_delta_t_coa
522
+ scp_set = sksicd.projection.ProjectionSetsMono(
523
+ t_COA=np.array([scp_tcoa]),
524
+ ARP_COA=np.array([npp.polyval(scp_tcoa, apc_poly)]),
525
+ VARP_COA=np.array([npp.polyval(scp_tcoa, npp.polyder(apc_poly))]),
526
+ R_COA=np.array([scp_rcoa]),
527
+ Rdot_COA=np.array([scp_rratecoa]),
528
+ )
529
+ scp_ecf = sksicd.projection.r_rdot_to_ground_plane_mono(
530
+ look,
531
+ scp_set,
532
+ sarkit.wgs84.geodetic_to_cartesian(init_scp_llh),
533
+ sarkit.wgs84.up(init_scp_llh),
534
+ )[0]
535
+ scp_llh = sarkit.wgs84.cartesian_to_geodetic(scp_ecf)
536
+
537
+ # Calc unit vectors
538
+ scp_ca_pos = npp.polyval(scp_tca, apc_poly)
539
+ scp_ca_vel = npp.polyval(scp_tca, npp.polyder(apc_poly))
540
+ los = scp_ecf - scp_ca_pos
541
+ row_uvect_ecf = los / npl.norm(los)
542
+ left = np.cross(scp_ca_pos, scp_ca_vel)
543
+ look = np.sign(np.dot(left, row_uvect_ecf))
544
+ spz = -look * np.cross(row_uvect_ecf, scp_ca_vel)
545
+ uspz = spz / npl.norm(spz)
546
+ col_uvect_ecf = np.cross(uspz, row_uvect_ecf)
547
+
548
+ # Radiometric
549
+ beta_zero_sf_poly = [
550
+ [
551
+ float(h5_attrs["calibration_factor"]),
552
+ ],
553
+ ]
554
+
555
+ # Build XML
556
+ sicd = lxml.builder.ElementMaker(
557
+ namespace=NSMAP["sicd"], nsmap={None: NSMAP["sicd"]}
558
+ )
559
+ collection_info = sicd.CollectionInfo(
560
+ sicd.CollectorName(collector_name),
561
+ sicd.CoreName(core_name),
562
+ sicd.CollectType(collect_type),
563
+ sicd.RadarMode(sicd.ModeType(mode_type), sicd.ModeID(mode_id)),
564
+ sicd.Classification(classification),
565
+ )
566
+ image_creation = sicd.ImageCreation(
567
+ sicd.Application(creation_application),
568
+ sicd.DateTime(creation_date_time.isoformat() + "Z"),
569
+ sicd.Site(ostaid),
570
+ )
571
+ image_data = sicd.ImageData(
572
+ sicd.PixelType(pixel_type),
573
+ sicd.NumRows(str(num_rows)),
574
+ sicd.NumCols(str(num_cols)),
575
+ sicd.FirstRow(str(first_row)),
576
+ sicd.FirstCol(str(first_col)),
577
+ sicd.FullImage(sicd.NumRows(str(num_rows)), sicd.NumCols(str(num_cols))),
578
+ sicd.SCPPixel(sicd.Row(str(scp_pixel[0])), sicd.Col(str(scp_pixel[1]))),
579
+ )
580
+
581
+ def make_xyz(arr):
582
+ return [sicd.X(str(arr[0])), sicd.Y(str(arr[1])), sicd.Z(str(arr[2]))]
583
+
584
+ def make_llh(arr):
585
+ return [sicd.Lat(str(arr[0])), sicd.Lon(str(arr[1])), sicd.HAE(str(arr[2]))]
586
+
587
+ def make_ll(arr):
588
+ return [sicd.Lat(str(arr[0])), sicd.Lon(str(arr[1]))]
589
+
590
+ # Add GeoData with placeholder corners
591
+ geo_data = sicd.GeoData(
592
+ sicd.EarthModel("WGS_84"),
593
+ sicd.SCP(sicd.ECF(*make_xyz(scp_ecf)), sicd.LLH(*make_llh(scp_llh))),
594
+ sicd.ImageCorners(
595
+ sicd.ICP({"index": "1:FRFC"}, *make_ll([0, 0])),
596
+ sicd.ICP({"index": "2:FRLC"}, *make_ll([0, 0])),
597
+ sicd.ICP({"index": "3:LRLC"}, *make_ll([0, 0])),
598
+ sicd.ICP({"index": "4:LRFC"}, *make_ll([0, 0])),
599
+ ),
600
+ )
601
+
602
+ grid = sicd.Grid(
603
+ sicd.ImagePlane(image_plane),
604
+ sicd.Type(grid_type),
605
+ sicd.TimeCOAPoly(),
606
+ sicd.Row(
607
+ sicd.UVectECF(*make_xyz(row_uvect_ecf)),
608
+ sicd.SS(str(row_ss)),
609
+ sicd.ImpRespWid(str(row_imp_res_wid)),
610
+ sicd.Sgn(str(row_sgn)),
611
+ sicd.ImpRespBW(str(row_imp_res_bw)),
612
+ sicd.KCtr(str(row_kctr)),
613
+ sicd.DeltaK1(str(row_delta_k1)),
614
+ sicd.DeltaK2(str(row_delta_k2)),
615
+ sicd.DeltaKCOAPoly(),
616
+ sicd.WgtType(
617
+ sicd.WindowName(
618
+ str(row_win),
619
+ )
620
+ ),
621
+ ),
622
+ sicd.Col(
623
+ sicd.UVectECF(*make_xyz(col_uvect_ecf)),
624
+ sicd.SS(str(col_ss)),
625
+ sicd.ImpRespWid(str(col_imp_res_wid)),
626
+ sicd.Sgn(str(col_sgn)),
627
+ sicd.ImpRespBW(str(col_imp_res_bw)),
628
+ sicd.KCtr(str(col_kctr)),
629
+ sicd.DeltaK1(str(col_delta_k1)),
630
+ sicd.DeltaK2(str(col_delta_k2)),
631
+ sicd.DeltaKCOAPoly(),
632
+ sicd.WgtType(
633
+ sicd.WindowName(
634
+ str(col_win),
635
+ )
636
+ ),
637
+ ),
638
+ )
639
+ sksicd.Poly2dType().set_elem(grid.find("./{*}TimeCOAPoly"), time_coa_poly)
640
+ sksicd.Poly2dType().set_elem(
641
+ grid.find("./{*}Row/{*}DeltaKCOAPoly"), row_deltak_coa_poly
642
+ )
643
+ sksicd.Poly2dType().set_elem(
644
+ grid.find("./{*}Col/{*}DeltaKCOAPoly"), col_deltak_coa_poly
645
+ )
646
+
647
+ timeline = sicd.Timeline(
648
+ sicd.CollectStart(collect_start.isoformat() + "Z"),
649
+ sicd.CollectDuration(str(collect_duration)),
650
+ sicd.IPP(
651
+ {"size": "1"},
652
+ sicd.Set(
653
+ {"index": "1"},
654
+ sicd.TStart(str(t_start)),
655
+ sicd.TEnd(str(t_end)),
656
+ sicd.IPPStart(str(ipp_start)),
657
+ sicd.IPPEnd(str(ipp_end)),
658
+ sicd.IPPPoly(),
659
+ ),
660
+ ),
661
+ )
662
+ sksicd.PolyType().set_elem(timeline.find("./{*}IPP/{*}Set/{*}IPPPoly"), ipp_poly)
663
+
664
+ position = sicd.Position(sicd.ARPPoly())
665
+ sksicd.XyzPolyType().set_elem(position.find("./{*}ARPPoly"), apc_poly)
666
+
667
+ radar_collection = sicd.RadarCollection(
668
+ sicd.TxFrequency(sicd.Min(str(tx_freq_min)), sicd.Max(str(tx_freq_max))),
669
+ sicd.Waveform(
670
+ {"size": "1"},
671
+ sicd.WFParameters(
672
+ {"index": "1"},
673
+ sicd.TxPulseLength(str(tx_pulse_length)),
674
+ sicd.TxRFBandwidth(str(tx_rf_bw)),
675
+ sicd.TxFreqStart(str(tx_freq_min)),
676
+ sicd.TxFMRate(str(tx_fm_rate)),
677
+ sicd.RcvDemodType(rcv_demod_type),
678
+ sicd.ADCSampleRate(str(adc_sample_rate)),
679
+ sicd.RcvFMRate(str(0)),
680
+ ),
681
+ ),
682
+ sicd.TxPolarization(tx_polarization),
683
+ sicd.RcvChannels(
684
+ {"size": "1"},
685
+ sicd.ChanParameters(
686
+ {"index": "1"},
687
+ sicd.TxRcvPolarization(tx_rcv_polarization),
688
+ ),
689
+ ),
690
+ )
691
+
692
+ image_formation = sicd.ImageFormation(
693
+ sicd.RcvChanProc(sicd.NumChanProc("1"), sicd.ChanIndex("1")),
694
+ sicd.TxRcvPolarizationProc(tx_rcv_polarization_proc),
695
+ sicd.TStartProc(str(t_start_proc)),
696
+ sicd.TEndProc(str(t_end_proc)),
697
+ sicd.TxFrequencyProc(
698
+ sicd.MinProc(str(tx_freq_proc[0])), sicd.MaxProc(str(tx_freq_proc[1]))
699
+ ),
700
+ sicd.ImageFormAlgo(image_form_algo),
701
+ sicd.STBeamComp(st_beam_comp),
702
+ sicd.ImageBeamComp(image_beam_comp),
703
+ sicd.AzAutofocus(az_autofocus),
704
+ sicd.RgAutofocus(rg_autofocus),
705
+ )
706
+
707
+ radiometric = sicd.Radiometric(
708
+ sicd.BetaZeroSFPoly(),
709
+ )
710
+ sksicd.Poly2dType().set_elem(
711
+ radiometric.find("./{*}BetaZeroSFPoly"), beta_zero_sf_poly
712
+ )
713
+
714
+ rma = sicd.RMA(
715
+ sicd.RMAlgoType(rma_algo_type),
716
+ sicd.ImageType(image_type),
717
+ sicd.INCA(
718
+ sicd.TimeCAPoly(),
719
+ sicd.R_CA_SCP(str(r_ca_scp)),
720
+ sicd.FreqZero(str(freq_zero)),
721
+ sicd.DRateSFPoly(),
722
+ sicd.DopCentroidPoly(),
723
+ sicd.DopCentroidCOA(dop_centroid_coa),
724
+ ),
725
+ )
726
+ sksicd.PolyType().set_elem(rma.find("./{*}INCA/{*}TimeCAPoly"), time_ca_poly)
727
+ sksicd.Poly2dType().set_elem(rma.find("./{*}INCA/{*}DRateSFPoly"), dr_sf_poly)
728
+ sksicd.Poly2dType().set_elem(
729
+ rma.find("./{*}INCA/{*}DopCentroidPoly"), dop_centroid_poly
730
+ )
731
+
732
+ sicd_xml_obj = sicd.SICD(
733
+ collection_info,
734
+ image_creation,
735
+ image_data,
736
+ geo_data,
737
+ grid,
738
+ timeline,
739
+ position,
740
+ radar_collection,
741
+ image_formation,
742
+ rma,
743
+ )
744
+
745
+ scp_coa = sksicd.compute_scp_coa(sicd_xml_obj.getroottree())
746
+ sicd_xml_obj = sicd.SICD(
747
+ collection_info,
748
+ image_creation,
749
+ image_data,
750
+ geo_data,
751
+ grid,
752
+ timeline,
753
+ position,
754
+ radar_collection,
755
+ image_formation,
756
+ scp_coa,
757
+ radiometric,
758
+ rma,
759
+ )
760
+
761
+ new_radiometric = _update_radiometric_node(sicd_xml_obj.getroottree())
762
+ sicd_xml_obj = sicd.SICD(
763
+ collection_info,
764
+ image_creation,
765
+ image_data,
766
+ geo_data,
767
+ grid,
768
+ timeline,
769
+ position,
770
+ radar_collection,
771
+ image_formation,
772
+ scp_coa,
773
+ new_radiometric,
774
+ rma,
775
+ )
776
+
777
+ sicd_xmltree = sicd_xml_obj.getroottree()
778
+
779
+ # Update ImageCorners
780
+ image_grid_locations = (
781
+ np.array(
782
+ [[0, 0], [0, num_cols - 1], [num_rows - 1, num_cols - 1], [num_rows - 1, 0]]
783
+ )
784
+ - scp_pixel
785
+ ) * [row_ss, col_ss]
786
+ icp_ecef, _, _ = sksicd.image_to_ground_plane(
787
+ sicd_xmltree,
788
+ image_grid_locations,
789
+ scp_ecf,
790
+ sarkit.wgs84.up(sarkit.wgs84.cartesian_to_geodetic(scp_ecf)),
791
+ )
792
+ icp_llh = sarkit.wgs84.cartesian_to_geodetic(icp_ecef)
793
+ xml_helper = sksicd.XmlHelper(sicd_xmltree)
794
+ xml_helper.set("./{*}GeoData/{*}ImageCorners", icp_llh[:, :2])
795
+
796
+ # Check for XML consistency
797
+ sicd_con = SicdConsistency(sicd_xmltree)
798
+ sicd_con.check()
799
+ sicd_con.print_result(fail_detail=True)
800
+
801
+ # Grab the data
802
+ real_part = h5_attrs["s_i"]
803
+ imag_part = h5_attrs["s_q"]
804
+ complex_data_arr = np.dstack((real_part, imag_part))
805
+ dtype = complex_data_arr.dtype
806
+ view_dtype = sksicd.PIXEL_TYPES[pixel_type]["dtype"].newbyteorder(dtype.byteorder)
807
+ complex_data_arr = complex_data_arr.view(dtype=view_dtype).reshape(
808
+ complex_data_arr.shape[:2]
809
+ )
810
+
811
+ complex_data_arr = np.transpose(complex_data_arr)
812
+ if look > 0:
813
+ complex_data_arr = np.fliplr(complex_data_arr)
814
+
815
+ metadata = sksicd.NitfMetadata(
816
+ xmltree=sicd_xmltree,
817
+ file_header_part={
818
+ "ostaid": ostaid,
819
+ "ftitle": core_name,
820
+ "security": {
821
+ "clas": classification[0].upper(),
822
+ "clsy": "US",
823
+ },
824
+ },
825
+ im_subheader_part={
826
+ "iid2": core_name,
827
+ "security": {
828
+ "clas": classification[0].upper(),
829
+ "clsy": "US",
830
+ },
831
+ "isorce": collector_name,
832
+ },
833
+ de_subheader_part={
834
+ "security": {
835
+ "clas": classification[0].upper(),
836
+ "clsy": "US",
837
+ },
838
+ },
839
+ )
840
+
841
+ with sicd_filename.open("wb") as f:
842
+ with sksicd.NitfWriter(f, metadata) as writer:
843
+ writer.write_image(complex_data_arr)
844
+
845
+
846
+ def main(args=None):
847
+ """CLI for converting Iceye SLC to SICD"""
848
+ parser = argparse.ArgumentParser(
849
+ description="Converts an Iceye HDF5 file into a SICD.",
850
+ fromfile_prefix_chars="@",
851
+ formatter_class=argparse.RawDescriptionHelpFormatter,
852
+ )
853
+ parser.add_argument(
854
+ "input_h5_file",
855
+ type=pathlib.Path,
856
+ help="path of the input HDF5 file",
857
+ )
858
+ parser.add_argument(
859
+ "classification",
860
+ type=str,
861
+ help="content of the /SICD/CollectionInfo/Classification node in the SICD XML",
862
+ )
863
+ parser.add_argument(
864
+ "output_sicd_file",
865
+ type=pathlib.Path,
866
+ help="path of the output SICD file",
867
+ )
868
+ parser.add_argument(
869
+ "--ostaid",
870
+ type=str,
871
+ help="content of the originating station ID (OSTAID) field of the NITF header",
872
+ default="Unknown",
873
+ )
874
+ config = parser.parse_args(args)
875
+
876
+ hdf5_to_sicd(
877
+ config.input_h5_file,
878
+ config.output_sicd_file,
879
+ classification=config.classification,
880
+ ostaid=config.ostaid,
881
+ )
882
+
883
+
884
+ if __name__ == "__main__":
885
+ main()