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.
sarkit_convert/csk.py ADDED
@@ -0,0 +1,822 @@
1
+ """
2
+ ===================
3
+ CSK Complex to SICD
4
+ ===================
5
+
6
+ Convert a complex image from the Cosmo-SkyMed HD5 SLC into SICD 1.4
7
+
8
+ During development, the following documents were considered:
9
+
10
+ "COSMO-SkyMed Seconda Generazione: System and Products Description", Revision A
11
+ "COSMO-SkyMed Mission and Products Description", Issue 2
12
+
13
+ In addition, SARPy was consulted on how to use the CSK/CSG to compute SICD
14
+ metadata that would predict the complex data characteristics
15
+
16
+ """
17
+
18
+ import argparse
19
+ import contextlib
20
+ import pathlib
21
+
22
+ import dateutil.parser
23
+ import h5py
24
+ import lxml.builder
25
+ import numpy as np
26
+ import numpy.linalg as npl
27
+ import numpy.polynomial.polynomial as npp
28
+ import sarkit.sicd as sksicd
29
+ import sarkit.verification
30
+ import sarkit.wgs84
31
+ import scipy.constants
32
+ import scipy.optimize
33
+
34
+ from sarkit_convert import _utils as utils
35
+
36
+ NSMAP = {
37
+ "sicd": "urn:SICD:1.4.0",
38
+ }
39
+
40
+ MODE_TYPE_MAP = {
41
+ # CSK
42
+ "HIMAGE": "STRIPMAP",
43
+ "PINGPONG": "STRIPMAP",
44
+ "WIDEREGION": "STRIPMAP",
45
+ "HUGEREGION": "STRIPMAP",
46
+ "ENHANCED SPOTLIGHT": "DYNAMIC STRIPMAP",
47
+ "SMART": "DYNAMIC STRIPMAP",
48
+ # CSG
49
+ "STRIPMAP": "STRIPMAP",
50
+ "QUADPOL": "STRIPMAP",
51
+ # KOMPSAT-5 (rebranded CSK)
52
+ "STANDARD": "STRIPMAP",
53
+ "ENHANCED STANDARD": "STRIPMAP",
54
+ "WIDE SWATH": "STRIPMAP",
55
+ "ENHANCED WIDE SWATH": "STRIPMAP",
56
+ "HIGH RESOLUTION": "DYNAMIC STRIPMAP",
57
+ "ENHANCED HIGH RESOLUTION": "DYNAMIC STRIPMAP",
58
+ "ULTRA HIGH RESOLUTION": "DYNAMIC STRIPMAP",
59
+ }
60
+
61
+ PIXEL_TYPE_MAP = {
62
+ "float32": "RE32F_IM32F",
63
+ "int16": "RE16I_IM16I",
64
+ }
65
+
66
+
67
+ def try_decode(value):
68
+ """Try to decode a value.
69
+
70
+ `h5py` attributes are arrays. Strings get returned as `bytes` and need to be decoded.
71
+ """
72
+ with contextlib.suppress(AttributeError):
73
+ return value.decode()
74
+ return value
75
+
76
+
77
+ def extract_attributes(item, add_dataset_info=False):
78
+ """Recursively extracts a nested dictionary of attributes from an item and its descendants"""
79
+ retval = dict()
80
+ if add_dataset_info:
81
+ if hasattr(item, "shape"):
82
+ retval["__shape__"] = item.shape
83
+ if hasattr(item, "dtype"):
84
+ retval["__dtype__"] = str(item.dtype)
85
+ if hasattr(item, "items"):
86
+ for name, contents in item.items():
87
+ retval[name] = extract_attributes(contents, add_dataset_info)
88
+ retval.update(sorted((key, try_decode(value)) for key, value in item.attrs.items()))
89
+ return retval
90
+
91
+
92
+ def compute_apc_poly(h5_attrs, ref_time, start_time, stop_time):
93
+ """Creates an Aperture Phase Center (APC) poly that orbits the Earth above the equator.
94
+
95
+ Polynomial generates 3D coords in ECF as a function of time from start of collect.
96
+
97
+ Parameters
98
+ ----------
99
+ h5_attrs: dict
100
+ The collection metadata
101
+ ref_time: datetime.datetime
102
+ The time at which the orbit goes through the `apc_pos`.
103
+ start_time: datetime.datetime
104
+ The start time to fit.
105
+ stop_time: datetime.datetime
106
+ The end time to fit.
107
+
108
+ Returns
109
+ -------
110
+ `numpy.ndarray`, shape=(6, 3)
111
+ APC poly
112
+ """
113
+ position = h5_attrs["ECEF Satellite Position"]
114
+ velocity = h5_attrs["ECEF Satellite Velocity"]
115
+ acceleration = h5_attrs["ECEF Satellite Acceleration"]
116
+ times = h5_attrs["State Vectors Times"] - (start_time - ref_time).total_seconds()
117
+ apc_poly = utils.fit_state_vectors(
118
+ (0, (stop_time - start_time).total_seconds()),
119
+ times,
120
+ position,
121
+ velocity,
122
+ acceleration,
123
+ order=5,
124
+ )
125
+
126
+ return apc_poly
127
+
128
+
129
+ def hdf5_to_sicd(
130
+ h5_filename,
131
+ sicd_filename,
132
+ classification,
133
+ ostaid,
134
+ img_str,
135
+ chan_index,
136
+ tx_polarizations,
137
+ tx_rcv_pols,
138
+ ):
139
+ with h5py.File(h5_filename, "r") as h5file:
140
+ h5_attrs = extract_attributes(h5file)
141
+ mission_id = h5_attrs["Mission ID"]
142
+ if mission_id == "CSG":
143
+ dataset_str = "IMG"
144
+ else:
145
+ dataset_str = "SBI"
146
+ sample_data_h5_path = f"{img_str}/{dataset_str}"
147
+ sample_data_shape = h5file[sample_data_h5_path].shape
148
+ sample_data_dtype = h5file[sample_data_h5_path].dtype
149
+
150
+ # Timeline
151
+ ref_time = dateutil.parser.parse(h5_attrs["Reference UTC"])
152
+ collection_start_time = dateutil.parser.parse(h5_attrs["Scene Sensing Start UTC"])
153
+ collection_stop_time = dateutil.parser.parse(h5_attrs["Scene Sensing Stop UTC"])
154
+ collection_duration = (collection_stop_time - collection_start_time).total_seconds()
155
+ prf = h5_attrs[img_str]["PRF"]
156
+ num_pulses = int(np.ceil(collection_duration * prf))
157
+ look = {"LEFT": 1, "RIGHT": -1}[h5_attrs["Look Side"]]
158
+
159
+ # Collection Info
160
+ collector_name = h5_attrs["Satellite ID"]
161
+ date_str = collection_start_time.strftime("%d%b%y").upper()
162
+ time_str = collection_start_time.strftime("%H%M%S") + "Z"
163
+ core_name = f"{date_str}_{h5_attrs['Satellite ID']}_{time_str}"
164
+ acquisition_mode = h5_attrs["Acquisition Mode"]
165
+ if "scan" in acquisition_mode.lower():
166
+ raise ValueError("ScanSar modes not supported")
167
+ radar_mode_type = MODE_TYPE_MAP.get(acquisition_mode, None)
168
+ if not radar_mode_type:
169
+ radar_mode_type = "DYNAMIC STRIPMAP"
170
+ radar_mode_id = h5_attrs["Multi-Beam ID"]
171
+
172
+ # Creation Info
173
+ creation_time = dateutil.parser.parse(h5_attrs["Product Generation UTC"])
174
+ creation_site = h5_attrs["Processing Centre"]
175
+ l0_ver = h5_attrs.get("L0 Software Version", "NONE")
176
+ l1_ver = h5_attrs.get("L1A Software Version", "NONE")
177
+ creation_application = f"L0: {l0_ver}, L1: {l1_ver}"
178
+
179
+ # Image Data
180
+ pixel_type = PIXEL_TYPE_MAP[sample_data_dtype.name]
181
+ num_rows = sample_data_shape[1]
182
+ num_cols = sample_data_shape[0]
183
+ first_row = 0
184
+ first_col = 0
185
+ scp_pixel = np.array([num_rows // 2, num_cols // 2])
186
+
187
+ # Position
188
+ apc_poly = compute_apc_poly(
189
+ h5_attrs, ref_time, collection_start_time, collection_stop_time
190
+ )
191
+
192
+ # Radar Collection
193
+ center_frequency = h5_attrs["Radar Frequency"]
194
+ tx_pulse_length = h5_attrs[img_str]["Range Chirp Length"]
195
+ tx_fm_rate = h5_attrs[img_str]["Range Chirp Rate"]
196
+ tx_rf_bw = np.abs(tx_fm_rate * tx_pulse_length)
197
+ tx_freq_min = center_frequency - 0.5 * tx_rf_bw
198
+ tx_freq_max = center_frequency + 0.5 * tx_rf_bw
199
+ tx_freq_start = center_frequency - (tx_pulse_length / 2 * tx_fm_rate)
200
+ adc_sample_rate = h5_attrs[img_str]["Sampling Rate"]
201
+ rcv_window_length = (
202
+ h5_attrs[img_str]["Echo Sampling Window Length"] / adc_sample_rate
203
+ )
204
+ tx_rcv_polarization = tx_rcv_pols[chan_index - 1]
205
+ tx_polarization = tx_rcv_polarization[0]
206
+
207
+ # Grid
208
+ assert h5_attrs["Lines Order"] == "EARLY-LATE"
209
+ assert h5_attrs["Columns Order"] == "NEAR-FAR"
210
+ spacings = np.array(
211
+ [
212
+ h5_attrs[img_str][dataset_str]["Column Spacing"],
213
+ h5_attrs[img_str][dataset_str]["Line Spacing"],
214
+ ]
215
+ )
216
+ intervals = np.array(
217
+ [
218
+ h5_attrs[img_str][dataset_str]["Column Time Interval"],
219
+ h5_attrs[img_str][dataset_str]["Line Time Interval"],
220
+ ]
221
+ )
222
+ zd_az_0 = h5_attrs[img_str][dataset_str]["Zero Doppler Azimuth First Time"]
223
+ zd_rg_0 = h5_attrs[img_str][dataset_str]["Zero Doppler Range First Time"]
224
+ row_bw = (
225
+ h5_attrs[img_str]["Range Focusing Bandwidth"]
226
+ * 2
227
+ / scipy.constants.speed_of_light
228
+ )
229
+ row_wid = 1 / row_bw
230
+ col_bw = (
231
+ min(
232
+ h5_attrs[img_str]["Azimuth Focusing Transition Bandwidth"] * intervals[1], 1
233
+ )
234
+ / spacings[1]
235
+ )
236
+ col_wid = 1 / col_bw
237
+
238
+ # CA and COA times
239
+ num_grid_pts = 51
240
+ grid_indices = np.stack(
241
+ np.meshgrid(
242
+ np.linspace(0, num_rows - 1, num_grid_pts),
243
+ np.linspace(0, num_cols - 1, num_grid_pts),
244
+ indexing="ij",
245
+ ),
246
+ axis=-1,
247
+ )
248
+ grid_coords = (grid_indices - scp_pixel) * spacings
249
+ time_coords = grid_indices[:, ::-look] * intervals + np.array([zd_rg_0, zd_az_0])
250
+ start_minus_ref = (collection_start_time - ref_time).total_seconds()
251
+
252
+ if mission_id == "CSG":
253
+ range_ref = h5_attrs[img_str]["Range Polynomial Reference Time"]
254
+ azimuth_ref = h5_attrs[img_str]["Azimuth Polynomial Reference Time"]
255
+ azimuth_ref_zd = h5_attrs[img_str]["Azimuth Polynomial Reference Time - ZD"]
256
+ azimuth_first_time = h5_attrs[img_str]["B0001"]["Azimuth First Time"]
257
+ azimuth_last_time = h5_attrs[img_str]["B0001"]["Azimuth Last Time"]
258
+ raw_times = np.linspace(azimuth_first_time, azimuth_last_time, num_grid_pts)
259
+
260
+ centroid_range_poly = h5_attrs[img_str][
261
+ "Doppler Centroid vs Range Time Polynomial"
262
+ ]
263
+ centroid_azimuth_poly = h5_attrs[img_str][
264
+ "Doppler Centroid vs Azimuth Time Polynomial - RAW"
265
+ ]
266
+ raw_doppler_centroid = npp.polyval(
267
+ raw_times - azimuth_ref, centroid_azimuth_poly
268
+ )
269
+
270
+ rate_range_poly = h5_attrs[img_str]["Doppler Rate vs Range Time Polynomial"]
271
+ rate_azimuth_poly = h5_attrs[img_str]["Doppler Rate vs Azimuth Time Polynomial"]
272
+ raw_doppler_rate = npp.polyval(raw_times - azimuth_ref, rate_azimuth_poly)
273
+
274
+ zd_times = raw_times - raw_doppler_centroid / raw_doppler_rate
275
+
276
+ zd_to_az_centroid = npp.polyfit(
277
+ zd_times - azimuth_ref_zd, raw_doppler_centroid, 4
278
+ )
279
+ doppler_centroid = (
280
+ npp.polyval(time_coords[..., 0] - range_ref, centroid_range_poly)
281
+ + npp.polyval(time_coords[..., 1] - azimuth_ref_zd, zd_to_az_centroid)
282
+ - (centroid_range_poly[0] + zd_to_az_centroid[0]) / 2
283
+ )
284
+
285
+ zd_to_az_rate = npp.polyfit(zd_times - azimuth_ref_zd, raw_doppler_rate, 4)
286
+ doppler_rate = (
287
+ npp.polyval(time_coords[..., 0] - range_ref, rate_range_poly)
288
+ + npp.polyval(time_coords[..., 1] - azimuth_ref_zd, zd_to_az_rate)
289
+ - (rate_range_poly[0] + zd_to_az_rate[0]) / 2
290
+ )
291
+ else:
292
+ range_ref = h5_attrs["Range Polynomial Reference Time"]
293
+ azimuth_ref = h5_attrs["Azimuth Polynomial Reference Time"]
294
+
295
+ centroid_range_poly = h5_attrs["Centroid vs Range Time Polynomial"]
296
+ centroid_azimuth_poly = h5_attrs["Centroid vs Azimuth Time Polynomial"]
297
+ doppler_centroid = (
298
+ npp.polyval(time_coords[..., 0] - range_ref, centroid_range_poly)
299
+ + npp.polyval(time_coords[..., 1] - azimuth_ref, centroid_azimuth_poly)
300
+ - (centroid_range_poly[0] + centroid_azimuth_poly[0]) / 2
301
+ )
302
+
303
+ rate_range_poly = h5_attrs["Doppler Rate vs Range Time Polynomial"]
304
+ rate_azimuth_poly = h5_attrs["Doppler Rate vs Azimuth Time Polynomial"]
305
+ doppler_rate = (
306
+ npp.polyval(time_coords[..., 0] - range_ref, rate_range_poly)
307
+ + npp.polyval(time_coords[..., 1] - azimuth_ref, rate_azimuth_poly)
308
+ - (rate_range_poly[0] + rate_azimuth_poly[0]) / 2
309
+ )
310
+
311
+ range_rate_per_hz = -scipy.constants.speed_of_light / (2 * center_frequency)
312
+ range_rate = doppler_centroid * range_rate_per_hz
313
+ range_rate_rate = doppler_rate * range_rate_per_hz
314
+ doppler_centroid_poly = utils.polyfit2d(
315
+ grid_coords[..., 0].flatten(),
316
+ grid_coords[..., 1].flatten(),
317
+ doppler_centroid.flatten(),
318
+ 4,
319
+ 4,
320
+ )
321
+ doppler_rate_poly = utils.polyfit2d(
322
+ grid_coords[..., 0].flatten(),
323
+ grid_coords[..., 1].flatten(),
324
+ doppler_rate.flatten(),
325
+ 4,
326
+ 4,
327
+ )
328
+ time_ca_samps = time_coords[..., 1] - start_minus_ref
329
+ time_ca_poly = npp.polyfit(
330
+ grid_coords[..., 1].flatten(), time_ca_samps.flatten(), 1
331
+ )
332
+ time_coa_samps = time_ca_samps + range_rate / range_rate_rate
333
+ time_coa_poly = utils.polyfit2d(
334
+ grid_coords[..., 0].flatten(),
335
+ grid_coords[..., 1].flatten(),
336
+ time_coa_samps.flatten(),
337
+ 4,
338
+ 4,
339
+ )
340
+
341
+ range_ca = time_coords[..., 0] * scipy.constants.speed_of_light / 2
342
+ speed_ca = npl.norm(
343
+ npp.polyval(time_coords[..., 1] - start_minus_ref, npp.polyder(apc_poly)),
344
+ axis=0,
345
+ )
346
+ drsf = range_rate_rate * range_ca / speed_ca**2
347
+ drsf_poly = utils.polyfit2d(
348
+ grid_coords[..., 0].flatten(),
349
+ grid_coords[..., 1].flatten(),
350
+ drsf.flatten(),
351
+ 4,
352
+ 4,
353
+ )
354
+
355
+ llh_ddm = h5_attrs["Scene Centre Geodetic Coordinates"]
356
+ scp_drsf = drsf_poly[0, 0]
357
+ scp_tca = time_ca_poly[0]
358
+ scp_rca = (
359
+ (zd_rg_0 + scp_pixel[0] * intervals[0]) * scipy.constants.speed_of_light / 2
360
+ )
361
+ scp_tcoa = time_coa_poly[0, 0]
362
+ scp_delta_t_coa = scp_tcoa - scp_tca
363
+ scp_varp_ca_mag = npl.norm(npp.polyval(scp_tca, npp.polyder(apc_poly)))
364
+ scp_rcoa = np.sqrt(scp_rca**2 + scp_drsf * scp_varp_ca_mag**2 * scp_delta_t_coa**2)
365
+ scp_rratecoa = scp_drsf / scp_rcoa * scp_varp_ca_mag**2 * scp_delta_t_coa
366
+
367
+ def obj(hae):
368
+ scene_pos = sarkit.wgs84.geodetic_to_cartesian([llh_ddm[0], llh_ddm[1], hae])
369
+ delta_t = np.linspace(-0.01, 0.01)
370
+ arp_pos = npp.polyval(scp_tca + delta_t, apc_poly).T
371
+ arp_speed = npl.norm(npp.polyval(scp_tca, npp.polyder(apc_poly)), axis=0)
372
+ range_ = npl.norm(arp_pos - scene_pos, axis=1)
373
+ range_poly = npp.polyfit(delta_t, range_, len(apc_poly))
374
+ test_drsf = 2 * range_poly[2] * range_poly[0] / arp_speed**2
375
+ return scp_drsf - test_drsf
376
+
377
+ scp_hae = scipy.optimize.brentq(obj, -30e3, 30e3)
378
+ sc_ecf = sarkit.wgs84.geodetic_to_cartesian(llh_ddm)
379
+ scp_set = sksicd.projection.ProjectionSetsMono(
380
+ t_COA=np.array([scp_tcoa]),
381
+ ARP_COA=np.array([npp.polyval(scp_tcoa, apc_poly)]),
382
+ VARP_COA=np.array([npp.polyval(scp_tcoa, npp.polyder(apc_poly))]),
383
+ R_COA=np.array([scp_rcoa]),
384
+ Rdot_COA=np.array([scp_rratecoa]),
385
+ )
386
+ scp_ecf, _, _ = sksicd.projection.r_rdot_to_constant_hae_surface(
387
+ look, sc_ecf, scp_set, scp_hae
388
+ )
389
+ scp_ecf = scp_ecf[0]
390
+ scp_llh = sarkit.wgs84.cartesian_to_geodetic(scp_ecf)
391
+ scp_ca_pos = npp.polyval(scp_tca, apc_poly)
392
+ scp_ca_vel = npp.polyval(scp_tcoa, npp.polyder(apc_poly))
393
+ los = scp_ecf - scp_ca_pos
394
+ u_row = los / npl.norm(los)
395
+ left = np.cross(scp_ca_pos, scp_ca_vel)
396
+ look = np.sign(np.dot(left, u_row))
397
+ spz = -look * np.cross(u_row, scp_ca_vel)
398
+ uspz = spz / npl.norm(spz)
399
+ u_col = np.cross(uspz, u_row)
400
+
401
+ # Build XML
402
+ sicd = lxml.builder.ElementMaker(
403
+ namespace=NSMAP["sicd"], nsmap={None: NSMAP["sicd"]}
404
+ )
405
+ collection_info = sicd.CollectionInfo(
406
+ sicd.CollectorName(collector_name),
407
+ sicd.CoreName(core_name),
408
+ sicd.CollectType("MONOSTATIC"),
409
+ sicd.RadarMode(sicd.ModeType(radar_mode_type), sicd.ModeID(radar_mode_id)),
410
+ sicd.Classification(classification),
411
+ )
412
+ image_creation = sicd.ImageCreation(
413
+ sicd.Application(creation_application),
414
+ sicd.DateTime(creation_time.isoformat() + "Z"),
415
+ sicd.Site(creation_site),
416
+ )
417
+ image_data = sicd.ImageData(
418
+ sicd.PixelType(pixel_type),
419
+ sicd.NumRows(str(num_rows)),
420
+ sicd.NumCols(str(num_cols)),
421
+ sicd.FirstRow(str(first_row)),
422
+ sicd.FirstCol(str(first_col)),
423
+ sicd.FullImage(sicd.NumRows(str(num_rows)), sicd.NumCols(str(num_cols))),
424
+ sicd.SCPPixel(sicd.Row(str(scp_pixel[0])), sicd.Col(str(scp_pixel[1]))),
425
+ )
426
+
427
+ def make_xyz(arr):
428
+ return [sicd.X(str(arr[0])), sicd.Y(str(arr[1])), sicd.Z(str(arr[2]))]
429
+
430
+ def make_llh(arr):
431
+ return [sicd.Lat(str(arr[0])), sicd.Lon(str(arr[1])), sicd.HAE(str(arr[2]))]
432
+
433
+ def make_ll(arr):
434
+ return [sicd.Lat(str(arr[0])), sicd.Lon(str(arr[1]))]
435
+
436
+ # Placeholder locations
437
+ geo_data = sicd.GeoData(
438
+ sicd.EarthModel("WGS_84"),
439
+ sicd.SCP(sicd.ECF(*make_xyz(scp_ecf)), sicd.LLH(*make_llh(scp_llh))),
440
+ sicd.ImageCorners(
441
+ sicd.ICP({"index": "1:FRFC"}, *make_ll([0, 0])),
442
+ sicd.ICP({"index": "2:FRLC"}, *make_ll([0, 0])),
443
+ sicd.ICP({"index": "3:LRLC"}, *make_ll([0, 0])),
444
+ sicd.ICP({"index": "4:LRFC"}, *make_ll([0, 0])),
445
+ ),
446
+ )
447
+
448
+ dc_sgn = np.sign(-doppler_rate_poly[0, 0])
449
+ col_deltakcoa_poly = (
450
+ -look * dc_sgn * doppler_centroid_poly * intervals[1] / spacings[1]
451
+ )
452
+ vertices = [
453
+ (0, 0),
454
+ (0, num_cols - 1),
455
+ (num_rows - 1, num_cols - 1),
456
+ (num_rows - 1, 0),
457
+ ]
458
+ coords = (vertices - scp_pixel) * spacings
459
+ deltaks = npp.polyval2d(coords[:, 0], coords[:, 1], col_deltakcoa_poly)
460
+ dk1 = deltaks.min() - col_bw / 2
461
+ dk2 = deltaks.max() + col_bw / 2
462
+ if dk1 < -0.5 / spacings[1] or dk2 > 0.5 / spacings[1]:
463
+ dk1 = -0.5 / spacings[1]
464
+ dk2 = -dk1
465
+
466
+ row_window_name = h5_attrs["Range Focusing Weighting Function"]
467
+ row_window_coeff = h5_attrs["Range Focusing Weighting Coefficient"]
468
+ col_window_name = h5_attrs["Azimuth Focusing Weighting Function"]
469
+ col_window_coeff = h5_attrs["Azimuth Focusing Weighting Coefficient"]
470
+
471
+ grid = sicd.Grid(
472
+ sicd.ImagePlane("SLANT"),
473
+ sicd.Type("RGZERO"),
474
+ sicd.TimeCOAPoly(),
475
+ sicd.Row(
476
+ sicd.UVectECF(*make_xyz(u_row)),
477
+ sicd.SS(str(spacings[0])),
478
+ sicd.ImpRespWid(str(row_wid)),
479
+ sicd.Sgn("-1"),
480
+ sicd.ImpRespBW(str(row_bw)),
481
+ sicd.KCtr(str(center_frequency / (scipy.constants.speed_of_light / 2))),
482
+ sicd.DeltaK1(str(-row_bw / 2)),
483
+ sicd.DeltaK2(str(row_bw / 2)),
484
+ sicd.DeltaKCOAPoly(),
485
+ sicd.WgtType(
486
+ sicd.WindowName(row_window_name),
487
+ sicd.Parameter({"name": "COEFFICIENT"}, str(row_window_coeff)),
488
+ ),
489
+ ),
490
+ sicd.Col(
491
+ sicd.UVectECF(*make_xyz(u_col)),
492
+ sicd.SS(str(spacings[1])),
493
+ sicd.ImpRespWid(str(col_wid)),
494
+ sicd.Sgn("-1"),
495
+ sicd.ImpRespBW(str(col_bw)),
496
+ sicd.KCtr("0"),
497
+ sicd.DeltaK1(str(dk1)),
498
+ sicd.DeltaK2(str(dk2)),
499
+ sicd.DeltaKCOAPoly(),
500
+ sicd.WgtType(
501
+ sicd.WindowName(col_window_name),
502
+ sicd.Parameter({"name": "COEFFICIENT"}, str(col_window_coeff)),
503
+ ),
504
+ ),
505
+ )
506
+ sksicd.Poly2dType().set_elem(grid.find("./{*}TimeCOAPoly"), time_coa_poly)
507
+ sksicd.Poly2dType().set_elem(grid.find("./{*}Row/{*}DeltaKCOAPoly"), [[0]])
508
+ sksicd.Poly2dType().set_elem(
509
+ grid.find("./{*}Col/{*}DeltaKCOAPoly"), col_deltakcoa_poly
510
+ )
511
+ rcs_row_sf = None
512
+ rcs_col_sf = None
513
+ if row_window_name == "HAMMING":
514
+ wgts = scipy.signal.windows.general_hamming(512, row_window_coeff, sym=True)
515
+ wgtfunc = sicd.WgtFunct()
516
+ sksicd.TRANSCODERS["Grid/Row/WgtFunct"].set_elem(wgtfunc, wgts)
517
+ grid.find("./{*}Row").append(wgtfunc)
518
+ row_broadening_factor = utils.broadening_from_amp(wgts)
519
+ row_wid = row_broadening_factor / row_bw
520
+ sksicd.DblType().set_elem(grid.find("./{*}Row/{*}ImpRespWid"), row_wid)
521
+ rcs_row_sf = 1 + np.var(wgts) / np.mean(wgts) ** 2
522
+ if col_window_name == "HAMMING":
523
+ wgts = scipy.signal.windows.general_hamming(512, col_window_coeff, sym=True)
524
+ wgtfunc = sicd.WgtFunct()
525
+ sksicd.TRANSCODERS["Grid/Col/WgtFunct"].set_elem(wgtfunc, wgts)
526
+ grid.find("./{*}Col").append(wgtfunc)
527
+ col_broadening_factor = utils.broadening_from_amp(wgts)
528
+ col_wid = col_broadening_factor / col_bw
529
+ sksicd.DblType().set_elem(grid.find("./{*}Col/{*}ImpRespWid"), col_wid)
530
+ rcs_col_sf = 1 + np.var(wgts) / np.mean(wgts) ** 2
531
+
532
+ timeline = sicd.Timeline(
533
+ sicd.CollectStart(collection_start_time.isoformat() + "Z"),
534
+ sicd.CollectDuration(str(collection_duration)),
535
+ sicd.IPP(
536
+ {"size": "1"},
537
+ sicd.Set(
538
+ {"index": "1"},
539
+ sicd.TStart(str(0)),
540
+ sicd.TEnd(str(num_pulses / prf)),
541
+ sicd.IPPStart(str(0)),
542
+ sicd.IPPEnd(str(num_pulses - 1)),
543
+ sicd.IPPPoly(),
544
+ ),
545
+ ),
546
+ )
547
+ sksicd.PolyType().set_elem(timeline.find("./{*}IPP/{*}Set/{*}IPPPoly"), [0, prf])
548
+
549
+ position = sicd.Position(sicd.ARPPoly())
550
+ sksicd.XyzPolyType().set_elem(position.find("./{*}ARPPoly"), apc_poly)
551
+
552
+ rcv_channels = sicd.RcvChannels(
553
+ {"size": str(len(tx_rcv_pols))},
554
+ )
555
+ for ndx, tx_rcv_pol in enumerate(tx_rcv_pols):
556
+ rcv_channels.append(
557
+ sicd.ChanParameters(
558
+ {"index": str(ndx + 1)}, sicd.TxRcvPolarization(tx_rcv_pol)
559
+ )
560
+ )
561
+
562
+ radar_collection = sicd.RadarCollection(
563
+ sicd.TxFrequency(sicd.Min(str(tx_freq_min)), sicd.Max(str(tx_freq_max))),
564
+ sicd.Waveform(
565
+ {"size": "1"},
566
+ sicd.WFParameters(
567
+ {"index": "1"},
568
+ sicd.TxPulseLength(str(tx_pulse_length)),
569
+ sicd.TxRFBandwidth(str(tx_rf_bw)),
570
+ sicd.TxFreqStart(str(tx_freq_start)),
571
+ sicd.TxFMRate(str(tx_fm_rate)),
572
+ sicd.RcvWindowLength(str(rcv_window_length)),
573
+ sicd.ADCSampleRate(str(adc_sample_rate)),
574
+ ),
575
+ ),
576
+ sicd.TxPolarization(tx_polarization),
577
+ rcv_channels,
578
+ )
579
+ if len(tx_polarizations) > 1:
580
+ radar_collection.find("./{*}TxPolarization").text = "SEQUENCE"
581
+ tx_sequence = sicd.TxSequence({"size": str(len(tx_polarizations))})
582
+ for ndx, tx_pol in enumerate(tx_polarizations):
583
+ tx_sequence.append(
584
+ sicd.TxStep({"index": str(ndx + 1)}, sicd.TxPolarization(tx_pol))
585
+ )
586
+ rcv_channels.addprevious(tx_sequence)
587
+
588
+ image_formation = sicd.ImageFormation(
589
+ sicd.RcvChanProc(sicd.NumChanProc("1"), sicd.ChanIndex(str(chan_index))),
590
+ sicd.TxRcvPolarizationProc(tx_rcv_polarization),
591
+ sicd.TStartProc(str(0)),
592
+ sicd.TEndProc(str(collection_duration)),
593
+ sicd.TxFrequencyProc(
594
+ sicd.MinProc(str(tx_freq_min)), sicd.MaxProc(str(tx_freq_max))
595
+ ),
596
+ sicd.ImageFormAlgo("RMA"),
597
+ sicd.STBeamComp("NO"),
598
+ sicd.ImageBeamComp("SV"),
599
+ sicd.AzAutofocus("NO"),
600
+ sicd.RgAutofocus("NO"),
601
+ )
602
+
603
+ rma = sicd.RMA(
604
+ sicd.RMAlgoType("OMEGA_K"),
605
+ sicd.ImageType("INCA"),
606
+ sicd.INCA(
607
+ sicd.TimeCAPoly(),
608
+ sicd.R_CA_SCP(str(scp_rca)),
609
+ sicd.FreqZero(str(center_frequency)),
610
+ sicd.DRateSFPoly(),
611
+ sicd.DopCentroidPoly(),
612
+ ),
613
+ )
614
+ sksicd.PolyType().set_elem(rma.find("./{*}INCA/{*}TimeCAPoly"), time_ca_poly)
615
+ sksicd.Poly2dType().set_elem(rma.find("./{*}INCA/{*}DRateSFPoly"), drsf_poly)
616
+ sksicd.Poly2dType().set_elem(
617
+ rma.find("./{*}INCA/{*}DopCentroidPoly"), doppler_centroid_poly
618
+ )
619
+ sicd_xml_obj = sicd.SICD(
620
+ collection_info,
621
+ image_creation,
622
+ image_data,
623
+ geo_data,
624
+ grid,
625
+ timeline,
626
+ position,
627
+ radar_collection,
628
+ image_formation,
629
+ rma,
630
+ )
631
+
632
+ image_formation.addnext(sksicd.compute_scp_coa(sicd_xml_obj.getroottree()))
633
+
634
+ # Add Radiometric
635
+ if mission_id == "CSK":
636
+ if h5_attrs["Range Spreading Loss Compensation Geometry"] != "NONE":
637
+ slant_range = h5_attrs["Reference Slant Range"]
638
+ exp = h5_attrs["Reference Slant Range Exponent"]
639
+ scale_factor = slant_range ** (2 * exp)
640
+ rescale_factor = h5_attrs["Rescaling Factor"]
641
+ scale_factor /= rescale_factor * rescale_factor
642
+ if h5_attrs.get("Calibration Constant Compensation Flag", None) == 0:
643
+ cal = h5_attrs[img_str]["Calibration Constant"]
644
+ scale_factor /= cal
645
+ betazero_poly = np.array([[scale_factor]])
646
+ graze = np.deg2rad(float(sicd_xml_obj.findtext("./{*}SCPCOA/{*}GrazeAng")))
647
+ twist = np.deg2rad(float(sicd_xml_obj.findtext("./{*}SCPCOA/{*}TwistAng")))
648
+ sigmazero_poly = betazero_poly * np.cos(graze) * np.cos(twist)
649
+ gammazero_poly = betazero_poly / np.tan(graze) * np.cos(twist)
650
+
651
+ radiometric = sicd.Radiometric(
652
+ sicd.SigmaZeroSFPoly(), sicd.BetaZeroSFPoly(), sicd.GammaZeroSFPoly()
653
+ )
654
+ sksicd.Poly2dType().set_elem(
655
+ radiometric.find("./{*}SigmaZeroSFPoly"), sigmazero_poly
656
+ )
657
+ sksicd.Poly2dType().set_elem(
658
+ radiometric.find("./{*}BetaZeroSFPoly"), betazero_poly
659
+ )
660
+ sksicd.Poly2dType().set_elem(
661
+ radiometric.find("./{*}GammaZeroSFPoly"), gammazero_poly
662
+ )
663
+ if rcs_row_sf and rcs_col_sf:
664
+ rcssf_poly = betazero_poly * (
665
+ rcs_row_sf * rcs_col_sf / (row_bw * col_bw)
666
+ )
667
+ radiometric.find("./{*}SigmaZeroSFPoly").addprevious(sicd.RCSSFPoly())
668
+ sksicd.Poly2dType().set_elem(
669
+ radiometric.find("./{*}RCSSFPoly"), rcssf_poly
670
+ )
671
+ sicd_xml_obj.find("./{*}RMA").addprevious(radiometric)
672
+
673
+ # Add Geodata Corners
674
+ sicd_xmltree = sicd_xml_obj.getroottree()
675
+ image_grid_locations = (
676
+ np.array(
677
+ [[0, 0], [0, num_cols - 1], [num_rows - 1, num_cols - 1], [num_rows - 1, 0]]
678
+ )
679
+ - scp_pixel
680
+ ) * spacings
681
+ icp_ecef, _, _ = sksicd.image_to_ground_plane(
682
+ sicd_xmltree,
683
+ image_grid_locations,
684
+ scp_ecf,
685
+ sarkit.wgs84.up(sarkit.wgs84.cartesian_to_geodetic(scp_ecf)),
686
+ )
687
+ icp_llh = sarkit.wgs84.cartesian_to_geodetic(icp_ecef)
688
+ xml_helper = sksicd.XmlHelper(sicd_xmltree)
689
+ xml_helper.set("./{*}GeoData/{*}ImageCorners", icp_llh[:, :2])
690
+
691
+ # Validate XML
692
+ sicd_con = sarkit.verification.SicdConsistency(sicd_xmltree)
693
+ sicd_con.check()
694
+ sicd_con.print_result(fail_detail=True)
695
+
696
+ # Grab the data
697
+ with h5py.File(h5_filename, "r") as h5file:
698
+ data_arr = np.asarray(h5file[sample_data_h5_path])
699
+ dtype = data_arr.dtype
700
+ view_dtype = sksicd.PIXEL_TYPES[pixel_type]["dtype"].newbyteorder(
701
+ dtype.byteorder
702
+ )
703
+ complex_data_arr = np.squeeze(data_arr.view(view_dtype))
704
+ complex_data_arr = np.transpose(complex_data_arr)
705
+ if look > 0:
706
+ complex_data_arr = complex_data_arr[:, ::-1]
707
+
708
+ metadata = sksicd.NitfMetadata(
709
+ xmltree=sicd_xmltree,
710
+ file_header_part={
711
+ "ostaid": ostaid,
712
+ "ftitle": core_name,
713
+ "security": {
714
+ "clas": classification[0].upper(),
715
+ "clsy": "US",
716
+ },
717
+ },
718
+ im_subheader_part={
719
+ "tgtid": "",
720
+ "iid2": core_name,
721
+ "security": {
722
+ "clas": classification[0].upper(),
723
+ "clsy": "US",
724
+ },
725
+ "isorce": collector_name,
726
+ },
727
+ de_subheader_part={
728
+ "security": {
729
+ "clas": classification[0].upper(),
730
+ "clsy": "US",
731
+ },
732
+ },
733
+ )
734
+
735
+ with sicd_filename.open("wb") as f:
736
+ with sksicd.NitfWriter(f, metadata) as writer:
737
+ writer.write_image(complex_data_arr)
738
+
739
+
740
+ def main(args=None):
741
+ parser = argparse.ArgumentParser(
742
+ description="Converts a CSK SCS HDF5 file into a SICD.",
743
+ fromfile_prefix_chars="@",
744
+ formatter_class=argparse.RawDescriptionHelpFormatter,
745
+ )
746
+
747
+ parser.add_argument(
748
+ "input_h5_file", type=pathlib.Path, help="path of the input HDF5 file"
749
+ )
750
+ parser.add_argument(
751
+ "classification",
752
+ help="content of the /SICD/CollectionInfo/Classification node in the SICD XML",
753
+ )
754
+ parser.add_argument(
755
+ "output_sicd_file",
756
+ type=pathlib.Path,
757
+ help='path of the output SICD file. The string "{pol}" will be replaced with polarization for multiple images',
758
+ )
759
+ parser.add_argument(
760
+ "--ostaid",
761
+ help="content of the originating station ID (OSTAID) field of the NITF header",
762
+ default="Unknown",
763
+ )
764
+ config = parser.parse_args(args)
765
+
766
+ tx_polarizations = []
767
+ with h5py.File(config.input_h5_file, "r") as h5file:
768
+ acquisition_mode = h5file.attrs["Acquisition Mode"].decode()
769
+ if "scan" in acquisition_mode.lower():
770
+ raise ValueError("ScanSar modes not supported")
771
+ mission_id = h5file.attrs["Mission ID"].decode()
772
+ images = dict()
773
+ if mission_id == "CSG":
774
+ img_str = "S01"
775
+ polarization = h5file.attrs["Polarization"].decode()
776
+ filename = pathlib.Path(
777
+ str(config.output_sicd_file).format(pol=polarization)
778
+ )
779
+ images[img_str] = {
780
+ "polarization": polarization,
781
+ "chan_index": 1,
782
+ "filename": filename,
783
+ }
784
+ tx_polarizations.append(polarization[0])
785
+ tx_rcv_pols = [f"{polarization[0]}:{polarization[1]}"]
786
+ else:
787
+ img_ndx = 1
788
+ images = dict()
789
+ tx_rcv_pols = []
790
+ while (img_str := f"S{img_ndx:02}") in h5file:
791
+ polarization = h5file[img_str].attrs["Polarisation"].decode()
792
+ filename = pathlib.Path(
793
+ str(config.output_sicd_file).format(pol=polarization)
794
+ )
795
+ images[img_str] = {
796
+ "polarization": polarization,
797
+ "chan_index": img_ndx,
798
+ "filename": filename,
799
+ }
800
+ tx_rcv_pols.append(f"{polarization[0]}:{polarization[1]}")
801
+ if (tx_polarization := polarization[0]) not in tx_polarizations:
802
+ tx_polarizations.append(tx_polarization)
803
+ img_ndx += 1
804
+
805
+ if len(images) != len(set([image["filename"] for image in images.values()])):
806
+ raise ValueError("Output filename does not include necessary polarization slug")
807
+
808
+ for img_str, img_info in images.items():
809
+ hdf5_to_sicd(
810
+ h5_filename=config.input_h5_file,
811
+ sicd_filename=img_info["filename"],
812
+ classification=config.classification,
813
+ ostaid=config.ostaid,
814
+ img_str=img_str,
815
+ chan_index=img_info["chan_index"],
816
+ tx_polarizations=tx_polarizations,
817
+ tx_rcv_pols=tx_rcv_pols,
818
+ )
819
+
820
+
821
+ if __name__ == "__main__":
822
+ main()