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/tsx.py ADDED
@@ -0,0 +1,906 @@
1
+ """
2
+ ===================
3
+ TSX Complex to SICD
4
+ ===================
5
+
6
+ Convert a complex image from the TerraSAR-X COSAR into SICD 1.4
7
+
8
+ During development, the following documents were considered:
9
+
10
+ "Level 1b Product Format Specification", Issue 1.3
11
+
12
+ In addition, SARPy was consulted on how to use the TSX metadata to compute SICD
13
+ metadata that would predict the complex data characteristics
14
+
15
+ """
16
+
17
+ import argparse
18
+ import pathlib
19
+
20
+ import dateutil.parser
21
+ import lxml.builder
22
+ import numpy as np
23
+ import numpy.linalg as npl
24
+ import numpy.polynomial.polynomial as npp
25
+ import sarkit.sicd as sksicd
26
+ import sarkit.verification
27
+ import sarkit.wgs84
28
+ import scipy.constants
29
+ from lxml import etree
30
+
31
+ from sarkit_convert import _utils as utils
32
+
33
+ NSMAP = {
34
+ "sicd": "urn:SICD:1.4.0",
35
+ }
36
+
37
+ COSAR_PIXEL_TYPE = "RE16I_IM16I"
38
+
39
+ MODE_TYPE_MAP = {
40
+ "ST": "SPOTLIGHT",
41
+ "SL": "DYNAMIC STRIPMAP",
42
+ "HS": "DYNAMIC STRIPMAP",
43
+ "SM": "STRIPMAP",
44
+ }
45
+
46
+ INSTRUMENT_CONVERSION_TO_SECONDS = 32 / 3.29658384e8
47
+ INSTRUMENT_CONVERSION_TO_HZ = 1.25e6
48
+
49
+
50
+ def _get_xyz(root, prefix):
51
+ return [
52
+ float(root.findtext(f"{prefix}{component}")) for component in ("X", "Y", "Z")
53
+ ]
54
+
55
+
56
+ def _parse_to_naive(timestamp_str):
57
+ return dateutil.parser.parse(timestamp_str).replace(tzinfo=None)
58
+
59
+
60
+ def _naive_to_sicd_str(timestamp):
61
+ return timestamp.replace(tzinfo=None).isoformat() + "Z"
62
+
63
+
64
+ def _boolstr_to_bool(text):
65
+ return text in ("1", "true")
66
+
67
+
68
+ def _compute_apc_poly(tsx_xml, start_time, stop_time):
69
+ """Creates an Aperture Phase Center (APC) poly that best fits the provided state vectors
70
+
71
+ Polynomial generates 3D coords in ECF as a function of time from start of collect.
72
+
73
+ Parameters
74
+ ----------
75
+ tsx_xml: lxml.Element
76
+ The TSX xml
77
+ start_time: datetime.datetime
78
+ The start time to fit
79
+ stop_time: datetime.datetime
80
+ The end time to fit
81
+
82
+ Returns
83
+ -------
84
+ `numpy.ndarray`, shape=(6, 3)
85
+ APC poly
86
+ """
87
+
88
+ times = []
89
+ positions = []
90
+ velocities = []
91
+ for state_vec in tsx_xml.findall("./platform/orbit/stateVec"):
92
+ sv_time = _parse_to_naive(state_vec.findtext("./timeUTC"))
93
+ times.append((sv_time - start_time).total_seconds())
94
+ positions.append(_get_xyz(state_vec, "pos"))
95
+ velocities.append(_get_xyz(state_vec, "vel"))
96
+
97
+ start = 0.0
98
+ stop = (stop_time - start_time).total_seconds()
99
+ apc_poly = utils.fit_state_vectors(
100
+ (start, stop),
101
+ times,
102
+ positions,
103
+ velocities,
104
+ order=5,
105
+ )
106
+
107
+ return apc_poly
108
+
109
+
110
+ def read_cosar(cosar_file):
111
+ burst_annot_dtype = np.dtype(
112
+ [
113
+ ("BIB", ">i4"),
114
+ ("RSRI", ">i4"),
115
+ ("RS", ">i4"),
116
+ ("AS", ">i4"),
117
+ ("BI", ">i4"),
118
+ ("RTNB", ">i4"),
119
+ ("TNL", ">i4"),
120
+ ]
121
+ )
122
+ burst_annot = np.fromfile(cosar_file, dtype=burst_annot_dtype, count=1)
123
+ assert (
124
+ burst_annot["BIB"] == burst_annot["RTNB"] * burst_annot["TNL"]
125
+ ) # TODO Handle only one burst for now
126
+ rtnb = burst_annot["RTNB"][0]
127
+ n_rg = burst_annot["RS"][0]
128
+ n_az = burst_annot["AS"][0]
129
+ azim_annot_shape = (3, n_rg + 2)
130
+ azim_info = np.fromfile(
131
+ cosar_file, dtype=">i4", count=np.prod(azim_annot_shape), offset=rtnb
132
+ ).reshape(azim_annot_shape)[:, 2:]
133
+ if not np.array_equiv(azim_info[0, 2:], 0):
134
+ raise ValueError("COSAR image data is not deskewed")
135
+
136
+ range_lines_shape = (n_az, n_rg + 2)
137
+ range_lines = np.fromfile(
138
+ cosar_file, dtype=">i4", count=np.prod(range_lines_shape), offset=4 * rtnb
139
+ ).reshape(range_lines_shape)
140
+
141
+ range_info = range_lines[:, :2]
142
+ assert np.all(range_info[:, 0] <= range_info[:, 1])
143
+
144
+ view_dtype = sksicd.PIXEL_TYPES[COSAR_PIXEL_TYPE]["dtype"].newbyteorder("big")
145
+ data_arr = range_lines[:, 2:]
146
+ complex_data_arr = np.squeeze(data_arr.view(view_dtype))
147
+
148
+ return complex_data_arr
149
+
150
+
151
+ def cosar_to_sicd(
152
+ tsx_xml,
153
+ layer_index,
154
+ cosar_file,
155
+ sicd_file,
156
+ classification,
157
+ ostaid,
158
+ chan_index,
159
+ tx_polarizations,
160
+ tx_rcv_pols,
161
+ ):
162
+ cal_const_elem = tsx_xml.find(
163
+ f'./calibration/calibrationConstant[@layerIndex="{layer_index}"]'
164
+ )
165
+ pol_layer = cal_const_elem.findtext("./polLayer")
166
+ settings_elem = tsx_xml.xpath(
167
+ f'./instrument/settings/polLayer[text()="{pol_layer}"]/..'
168
+ )[0]
169
+
170
+ # Timeline
171
+ proc_param = tsx_xml.find("./processing/processingParameter")
172
+ collection_start_time = _parse_to_naive(
173
+ proc_param.findtext("./rangeCompression/segmentInfo/dataSegment/startTimeUTC")
174
+ )
175
+ collection_stop_time = _parse_to_naive(
176
+ proc_param.findtext("./rangeCompression/segmentInfo/dataSegment/stopTimeUTC")
177
+ )
178
+ collection_duration = (collection_stop_time - collection_start_time).total_seconds()
179
+ prf = float(settings_elem.findtext("./settingRecord/PRF"))
180
+ num_pulses = int(np.ceil(collection_duration * prf))
181
+ look = {"LEFT": 1, "RIGHT": -1}[
182
+ tsx_xml.findtext("./productInfo/acquisitionInfo/lookDirection")
183
+ ]
184
+
185
+ # Collection Info
186
+ collector_name = tsx_xml.findtext("./productInfo/missionInfo/mission")
187
+ core_name = tsx_xml.findtext("./productInfo/sceneInfo/sceneID")
188
+ radar_mode_id = tsx_xml.findtext("./productInfo/acquisitionInfo/imagingMode")
189
+ if radar_mode_id not in MODE_TYPE_MAP:
190
+ raise ValueError(f"Unsupported mode {radar_mode_id}")
191
+ radar_mode_type = MODE_TYPE_MAP[radar_mode_id]
192
+
193
+ # Creation Info
194
+ creation_time = _parse_to_naive(tsx_xml.findtext("./generalHeader/generationTime"))
195
+ generation_system = tsx_xml.find("./generalHeader/generationSystem")
196
+ application = generation_system.text
197
+ version = generation_system.attrib["version"]
198
+ creation_application = f"{application} version {version}"
199
+ creation_site = tsx_xml.findtext(
200
+ "./productInfo/generationInfo/level1ProcessingFacility"
201
+ )
202
+
203
+ # COSAR data has range in the fast dimension, so this is a deliberate transposition
204
+ # Image Data
205
+ num_rows = int(
206
+ tsx_xml.findtext("./productInfo/imageDataInfo/imageRaster/numberOfColumns")
207
+ )
208
+ num_cols = int(
209
+ tsx_xml.findtext("./productInfo/imageDataInfo/imageRaster/numberOfRows")
210
+ )
211
+ first_row = 0
212
+ first_col = 0
213
+ scp_row = int(
214
+ tsx_xml.findtext("./productInfo/sceneInfo/sceneCenterCoord/refColumn")
215
+ )
216
+ ref_col = int(tsx_xml.findtext("./productInfo/sceneInfo/sceneCenterCoord/refRow"))
217
+ if look > 0:
218
+ scp_col = num_cols - 1 - ref_col
219
+ else:
220
+ scp_col = ref_col
221
+ scp_pixel = np.array([scp_row, scp_col])
222
+
223
+ # Position
224
+ apc_poly = _compute_apc_poly(tsx_xml, collection_start_time, collection_stop_time)
225
+
226
+ # Image Formation
227
+ st_beam_comp = "NONE"
228
+ procflags_elem = tsx_xml.find("./processing/processingFlags")
229
+ if _boolstr_to_bool(procflags_elem.findtext("./azimuthPatternCorrectedFlag")):
230
+ st_beam_comp = "GLOBAL"
231
+ if _boolstr_to_bool(
232
+ procflags_elem.findtext("./spotLightBeamCorrectedFlag")
233
+ ) or _boolstr_to_bool(procflags_elem.findtext("./scanSARBeamCorrectedFlag")):
234
+ st_beam_comp = "SV"
235
+
236
+ # Radar Collection
237
+ center_frequency = float(
238
+ tsx_xml.findtext("./instrument/radarParameters/centerFrequency")
239
+ )
240
+ chirp_elem = proc_param.find("./rangeCompression/chirps/referenceChirp")
241
+ tx_pulse_length = (
242
+ float(chirp_elem.findtext("./pulseLength")) * INSTRUMENT_CONVERSION_TO_SECONDS
243
+ )
244
+ tx_rf_bw = (
245
+ float(chirp_elem.findtext("./pulseBandwidth")) * INSTRUMENT_CONVERSION_TO_HZ
246
+ )
247
+ tx_fm_rate = (
248
+ {"UP": 1, "DOWN": -1}[chirp_elem.findtext("./chirpSlope")]
249
+ * tx_rf_bw
250
+ / tx_pulse_length
251
+ )
252
+ tx_freq_min = center_frequency - 0.5 * tx_rf_bw
253
+ tx_freq_max = center_frequency + 0.5 * tx_rf_bw
254
+ tx_freq_start = center_frequency - (tx_pulse_length / 2 * tx_fm_rate)
255
+ adc_sample_rate = float(settings_elem.findtext("./RSF"))
256
+ rcv_window_length = (
257
+ float(settings_elem.findtext("./settingRecord/echowindowLength"))
258
+ / adc_sample_rate
259
+ )
260
+ tx_rcv_polarization = tx_rcv_pols[chan_index - 1]
261
+ tx_polarization = tx_rcv_polarization[0]
262
+
263
+ # Grid
264
+ orientation = tsx_xml.findtext(
265
+ "./productSpecific/complexImageInfo/imageDataStartWith"
266
+ )
267
+ if orientation != "EARLYAZNEARRG":
268
+ raise ValueError("Image data not in expected orientation")
269
+ # These spacings are within a row/col, so effectively cancel the raster transpose.
270
+ intervals = np.array(
271
+ [
272
+ float(
273
+ tsx_xml.findtext("./productInfo/imageDataInfo/imageRaster/rowSpacing")
274
+ ),
275
+ float(
276
+ tsx_xml.findtext(
277
+ "./productInfo/imageDataInfo/imageRaster/columnSpacing"
278
+ )
279
+ ),
280
+ ]
281
+ )
282
+ spacings = np.array(
283
+ [
284
+ intervals[0] * scipy.constants.speed_of_light / 2,
285
+ float(
286
+ tsx_xml.findtext(
287
+ "./productSpecific/complexImageInfo/projectedSpacingAzimuth"
288
+ )
289
+ ),
290
+ ]
291
+ )
292
+ row_bw = (
293
+ float(proc_param.findtext("./rangeLookBandwidth"))
294
+ * 2
295
+ / scipy.constants.speed_of_light
296
+ )
297
+ col_bw = (
298
+ float(proc_param.findtext("./azimuthLookBandwidth"))
299
+ * intervals[1]
300
+ / spacings[1]
301
+ )
302
+ zd_0_utc = _parse_to_naive(
303
+ tsx_xml.findtext("./productInfo/sceneInfo/start/timeUTC")[:-1]
304
+ )
305
+ zd_az_0 = (zd_0_utc - collection_start_time).total_seconds()
306
+ zd_rg_0 = float(tsx_xml.findtext("./productInfo/sceneInfo/rangeTime/firstPixel"))
307
+ row_wid = 1 / row_bw
308
+ col_wid = 1 / col_bw
309
+
310
+ # Create doppler rate and centroid
311
+ num_grid_pts = 51
312
+ range_times = np.linspace(0, num_rows - 1, num_grid_pts) * intervals[0] + zd_rg_0
313
+ xrow_vals = ((range_times - zd_rg_0) / intervals[0] - scp_pixel[0]) * spacings[0]
314
+
315
+ def get_poly_vals(base_path, rel_poly_path):
316
+ values = []
317
+ azimuth_times = []
318
+ for base_node in tsx_xml.findall(base_path):
319
+ utc_time = _parse_to_naive(base_node.findtext("./timeUTC"))
320
+ azimuth_times.append((utc_time - collection_start_time).total_seconds())
321
+ poly_node = base_node.find(rel_poly_path)
322
+ ref_range_time = float(poly_node.findtext("referencePoint"))
323
+ degree = int(poly_node.findtext("polynomialDegree"))
324
+ poly = np.zeros(degree + 1)
325
+ for coef in poly_node.findall("coefficient"):
326
+ poly[int(coef.attrib["exponent"])] = float(coef.text)
327
+ current_values = npp.polyval(range_times - ref_range_time, poly)
328
+ values.append(current_values)
329
+ values = np.stack(values, axis=1)
330
+ return np.array(azimuth_times), values
331
+
332
+ def zd_times_to_ycol(times):
333
+ indices = (np.asarray(times) - zd_az_0) / intervals[1]
334
+ if look > 0:
335
+ ycol_vals = (num_cols - 1 - indices - scp_pixel[1]) * spacings[1]
336
+ else:
337
+ ycol_vals = (indices - scp_pixel[1]) * spacings[1]
338
+ return ycol_vals
339
+
340
+ drate_times, doppler_rate = get_poly_vals(
341
+ "./processing/geometry/dopplerRate", "dopplerRatePolynomial"
342
+ )
343
+ drate_ycol_vals = zd_times_to_ycol(drate_times)
344
+ drate_grid_coords = np.stack(
345
+ np.meshgrid(xrow_vals, drate_ycol_vals, indexing="ij"), axis=-1
346
+ )
347
+ doppler_rate_poly = utils.polyfit2d_tol(
348
+ drate_grid_coords[..., 0].flatten(),
349
+ drate_grid_coords[..., 1].flatten(),
350
+ doppler_rate.flatten(),
351
+ 4,
352
+ 4,
353
+ 1e-3,
354
+ )
355
+
356
+ centroid_coord_type = tsx_xml.findtext(
357
+ "./processing/doppler/dopplerCentroidCoordinateType"
358
+ )
359
+ dopp_cent_path = (
360
+ f'./processing/doppler/dopplerCentroid[@layerIndex="{layer_index}"]'
361
+ )
362
+ if radar_mode_type == "SPOTLIGHT":
363
+ coa_time = collection_duration / 2
364
+ delta_time = coa_time - np.array(drate_times)
365
+ doppler_centroid = (
366
+ npp.polyval2d(
367
+ drate_grid_coords[..., 0], drate_grid_coords[..., 1], doppler_rate_poly
368
+ )
369
+ * delta_time[np.newaxis, :]
370
+ )
371
+ doppler_centroid_poly = utils.polyfit2d_tol(
372
+ drate_grid_coords[..., 0].flatten(),
373
+ drate_grid_coords[..., 1].flatten(),
374
+ doppler_centroid.flatten(),
375
+ 4,
376
+ 4,
377
+ 1e-2,
378
+ )
379
+ time_coa_poly = np.array([[coa_time]])
380
+ else:
381
+ if centroid_coord_type == "RAW":
382
+ coa_times, doppler_centroid = get_poly_vals(
383
+ dopp_cent_path + "/dopplerEstimate", "./combinedDoppler"
384
+ )
385
+ coa_times = np.tile(coa_times, (doppler_centroid.shape[0], 1))
386
+ zd_times = coa_times - doppler_centroid / doppler_rate_poly[0, 0]
387
+ dcent_ycol_vals = zd_times_to_ycol(zd_times)
388
+ dcent_xrow_vals = np.tile(
389
+ xrow_vals[:, np.newaxis], (1, dcent_ycol_vals.shape[1])
390
+ )
391
+ dcent_grid_coords = np.stack((dcent_xrow_vals, dcent_ycol_vals), axis=-1)
392
+ doppler_centroid_poly = utils.polyfit2d_tol(
393
+ dcent_grid_coords[..., 0].flatten(),
394
+ dcent_grid_coords[..., 1].flatten(),
395
+ doppler_centroid.flatten(),
396
+ 4,
397
+ 4,
398
+ 1e-2,
399
+ )
400
+ elif centroid_coord_type == "ZERODOPPLER":
401
+ zd_times, doppler_centroid = get_poly_vals(
402
+ dopp_cent_path + "/dopplerEstimate", "./combinedDoppler"
403
+ )
404
+ zd_times = np.tile(zd_times, (doppler_centroid.shape[0], 1))
405
+ coa_times = zd_times + doppler_centroid / doppler_rate_poly[0, 0]
406
+ dcent_ycol_vals = zd_times_to_ycol(zd_times)
407
+ dcent_xrow_vals = np.tile(
408
+ xrow_vals[:, np.newaxis], (1, dcent_ycol_vals.shape[1])
409
+ )
410
+ dcent_grid_coords = np.stack((dcent_xrow_vals, dcent_ycol_vals), axis=-1)
411
+ doppler_centroid_poly = utils.polyfit2d_tol(
412
+ dcent_grid_coords[..., 0].flatten(),
413
+ dcent_grid_coords[..., 1].flatten(),
414
+ doppler_centroid.flatten(),
415
+ 4,
416
+ 4,
417
+ 1e-2,
418
+ )
419
+ else:
420
+ raise ValueError(
421
+ f"Unknown dopplerCentroidCoordinateType: {centroid_coord_type}"
422
+ )
423
+ time_coa_poly = utils.polyfit2d_tol(
424
+ dcent_grid_coords[..., 0].flatten(),
425
+ dcent_grid_coords[..., 1].flatten(),
426
+ coa_times.flatten(),
427
+ 4,
428
+ 4,
429
+ 1e-3,
430
+ )
431
+
432
+ range_rate_per_hz = -scipy.constants.speed_of_light / (2 * center_frequency)
433
+ range_rate_rate = doppler_rate * range_rate_per_hz
434
+
435
+ if look > 0:
436
+ scp_tca = (num_cols - 1 - scp_col) * intervals[1] + zd_az_0
437
+ else:
438
+ scp_tca = scp_col * intervals[1] + zd_az_0
439
+ scp_rca = (
440
+ (zd_rg_0 + scp_pixel[0] * intervals[0]) * scipy.constants.speed_of_light / 2
441
+ )
442
+ time_ca_poly = np.array([scp_tca, -look * intervals[1] / spacings[1]])
443
+ range_ca = range_times * scipy.constants.speed_of_light / 2
444
+ speed_ca = npl.norm(npp.polyval(drate_times, npp.polyder(apc_poly)), axis=0)
445
+ drsf = range_rate_rate * range_ca[:, np.newaxis] / speed_ca[np.newaxis, :] ** 2
446
+ drsf_poly = utils.polyfit2d_tol(
447
+ drate_grid_coords[..., 0].flatten(),
448
+ drate_grid_coords[..., 1].flatten(),
449
+ drsf.flatten(),
450
+ 4,
451
+ 4,
452
+ 1e-6,
453
+ )
454
+
455
+ si_elem = tsx_xml.find("./productInfo/sceneInfo")
456
+ scc_llh = np.array(
457
+ [
458
+ float(si_elem.findtext("./sceneCenterCoord/lat")),
459
+ float(si_elem.findtext("./sceneCenterCoord/lon")),
460
+ float(si_elem.findtext("./sceneAverageHeight")),
461
+ ]
462
+ )
463
+ scc_ecf = sarkit.wgs84.geodetic_to_cartesian(scc_llh)
464
+
465
+ scp_drsf = drsf_poly[0, 0]
466
+ scp_tcoa = time_coa_poly[0, 0]
467
+ scp_delta_t_coa = scp_tcoa - scp_tca
468
+ scp_varp_ca_mag = npl.norm(npp.polyval(scp_tca, npp.polyder(apc_poly)))
469
+ scp_rcoa = np.sqrt(scp_rca**2 + scp_drsf * scp_varp_ca_mag**2 * scp_delta_t_coa**2)
470
+ scp_rratecoa = scp_drsf / scp_rcoa * scp_varp_ca_mag**2 * scp_delta_t_coa
471
+
472
+ scp_set = sksicd.projection.ProjectionSetsMono(
473
+ t_COA=np.array([scp_tcoa]),
474
+ ARP_COA=np.array([npp.polyval(scp_tcoa, apc_poly)]),
475
+ VARP_COA=np.array([npp.polyval(scp_tcoa, npp.polyder(apc_poly))]),
476
+ R_COA=np.array([scp_rcoa]),
477
+ Rdot_COA=np.array([scp_rratecoa]),
478
+ )
479
+ scp_ecf, _, _ = sksicd.projection.r_rdot_to_constant_hae_surface(
480
+ look, scc_ecf, scp_set, scc_llh[2]
481
+ )
482
+ scp_ecf = scp_ecf[0]
483
+ scp_llh = sarkit.wgs84.cartesian_to_geodetic(scp_ecf)
484
+ scp_ca_pos = npp.polyval(scp_tca, apc_poly)
485
+ scp_ca_vel = npp.polyval(scp_tca, npp.polyder(apc_poly))
486
+ los = scp_ecf - scp_ca_pos
487
+ u_row = los / npl.norm(los)
488
+ left = np.cross(scp_ca_pos, scp_ca_vel)
489
+ look = np.sign(np.dot(left, u_row))
490
+ spz = -look * np.cross(u_row, scp_ca_vel)
491
+ uspz = spz / npl.norm(spz)
492
+ u_col = np.cross(uspz, u_row)
493
+
494
+ # Build XML
495
+ sicd = lxml.builder.ElementMaker(
496
+ namespace=NSMAP["sicd"], nsmap={None: NSMAP["sicd"]}
497
+ )
498
+ collection_info = sicd.CollectionInfo(
499
+ sicd.CollectorName(collector_name),
500
+ sicd.CoreName(core_name),
501
+ sicd.CollectType("MONOSTATIC"),
502
+ sicd.RadarMode(sicd.ModeType(radar_mode_type), sicd.ModeID(radar_mode_id)),
503
+ sicd.Classification(classification),
504
+ )
505
+ image_creation = sicd.ImageCreation(
506
+ sicd.Application(creation_application),
507
+ sicd.DateTime(_naive_to_sicd_str(creation_time)),
508
+ sicd.Site(creation_site),
509
+ )
510
+ image_data = sicd.ImageData(
511
+ sicd.PixelType(COSAR_PIXEL_TYPE),
512
+ sicd.NumRows(str(num_rows)),
513
+ sicd.NumCols(str(num_cols)),
514
+ sicd.FirstRow(str(first_row)),
515
+ sicd.FirstCol(str(first_col)),
516
+ sicd.FullImage(sicd.NumRows(str(num_rows)), sicd.NumCols(str(num_cols))),
517
+ sicd.SCPPixel(sicd.Row(str(scp_pixel[0])), sicd.Col(str(scp_pixel[1]))),
518
+ )
519
+
520
+ def make_xyz(arr):
521
+ return [sicd.X(str(arr[0])), sicd.Y(str(arr[1])), sicd.Z(str(arr[2]))]
522
+
523
+ def make_llh(arr):
524
+ return [sicd.Lat(str(arr[0])), sicd.Lon(str(arr[1])), sicd.HAE(str(arr[2]))]
525
+
526
+ # Will add ImageCorners later
527
+ geo_data = sicd.GeoData(
528
+ sicd.EarthModel("WGS_84"),
529
+ sicd.SCP(sicd.ECF(*make_xyz(scp_ecf)), sicd.LLH(*make_llh(scp_llh))),
530
+ )
531
+
532
+ dc_sgn = np.sign(-doppler_rate_poly[0, 0])
533
+ col_deltakcoa_poly = (
534
+ -look * dc_sgn * doppler_centroid_poly * intervals[1] / spacings[1]
535
+ )
536
+ vertices = [
537
+ (0, 0),
538
+ (0, num_cols - 1),
539
+ (num_rows - 1, num_cols - 1),
540
+ (num_rows - 1, 0),
541
+ ]
542
+ coords = (vertices - scp_pixel) * spacings
543
+ deltaks = npp.polyval2d(coords[:, 0], coords[:, 1], col_deltakcoa_poly)
544
+ dk1 = deltaks.min() - col_bw / 2
545
+ dk2 = deltaks.max() + col_bw / 2
546
+ if dk1 < -0.5 / spacings[1] or dk2 > 0.5 / spacings[1]:
547
+ dk1 = -0.5 / spacings[1]
548
+ dk2 = -dk1
549
+
550
+ row_window_name = proc_param.findtext("./rangeWindowID")
551
+ row_window_coeff = float(proc_param.findtext("./rangeWindowCoefficient"))
552
+ col_window_name = proc_param.findtext("./azimuthWindowID")
553
+ col_window_coeff = float(proc_param.findtext("./azimuthWindowCoefficient"))
554
+
555
+ grid = sicd.Grid(
556
+ sicd.ImagePlane("SLANT"),
557
+ sicd.Type("RGZERO"),
558
+ sicd.TimeCOAPoly(),
559
+ sicd.Row(
560
+ sicd.UVectECF(*make_xyz(u_row)),
561
+ sicd.SS(str(spacings[0])),
562
+ sicd.ImpRespWid(str(row_wid)),
563
+ sicd.Sgn("-1"),
564
+ sicd.ImpRespBW(str(row_bw)),
565
+ sicd.KCtr(str(center_frequency / (scipy.constants.speed_of_light / 2))),
566
+ sicd.DeltaK1(str(-row_bw / 2)),
567
+ sicd.DeltaK2(str(row_bw / 2)),
568
+ sicd.DeltaKCOAPoly(),
569
+ sicd.WgtType(
570
+ sicd.WindowName(row_window_name),
571
+ sicd.Parameter({"name": "COEFFICIENT"}, str(row_window_coeff)),
572
+ ),
573
+ ),
574
+ sicd.Col(
575
+ sicd.UVectECF(*make_xyz(u_col)),
576
+ sicd.SS(str(spacings[1])),
577
+ sicd.ImpRespWid(str(col_wid)),
578
+ sicd.Sgn("-1"),
579
+ sicd.ImpRespBW(str(col_bw)),
580
+ sicd.KCtr("0"),
581
+ sicd.DeltaK1(str(dk1)),
582
+ sicd.DeltaK2(str(dk2)),
583
+ sicd.DeltaKCOAPoly(),
584
+ sicd.WgtType(
585
+ sicd.WindowName(col_window_name),
586
+ sicd.Parameter({"name": "COEFFICIENT"}, str(col_window_coeff)),
587
+ ),
588
+ ),
589
+ )
590
+ sksicd.Poly2dType().set_elem(grid.find("./{*}TimeCOAPoly"), time_coa_poly)
591
+ sksicd.Poly2dType().set_elem(grid.find("./{*}Row/{*}DeltaKCOAPoly"), [[0]])
592
+ sksicd.Poly2dType().set_elem(
593
+ grid.find("./{*}Col/{*}DeltaKCOAPoly"), col_deltakcoa_poly
594
+ )
595
+ rcs_row_sf = None
596
+ rcs_col_sf = None
597
+ if row_window_name == "Hamming":
598
+ wgts = scipy.signal.windows.general_hamming(512, row_window_coeff, sym=True)
599
+ wgtfunc = sicd.WgtFunct()
600
+ sksicd.TRANSCODERS["Grid/Row/WgtFunct"].set_elem(wgtfunc, wgts)
601
+ grid.find("./{*}Row").append(wgtfunc)
602
+ row_broadening_factor = utils.broadening_from_amp(wgts)
603
+ row_wid = row_broadening_factor / row_bw
604
+ sksicd.DblType().set_elem(grid.find("./{*}Row/{*}ImpRespWid"), row_wid)
605
+ rcs_row_sf = 1 + np.var(wgts) / np.mean(wgts) ** 2
606
+ if col_window_name == "Hamming":
607
+ wgts = scipy.signal.windows.general_hamming(512, col_window_coeff, sym=True)
608
+ wgtfunc = sicd.WgtFunct()
609
+ sksicd.TRANSCODERS["Grid/Col/WgtFunct"].set_elem(wgtfunc, wgts)
610
+ grid.find("./{*}Col").append(wgtfunc)
611
+ col_broadening_factor = utils.broadening_from_amp(wgts)
612
+ col_wid = col_broadening_factor / col_bw
613
+ sksicd.DblType().set_elem(grid.find("./{*}Col/{*}ImpRespWid"), col_wid)
614
+ rcs_col_sf = 1 + np.var(wgts) / np.mean(wgts) ** 2
615
+
616
+ timeline = sicd.Timeline(
617
+ sicd.CollectStart(_naive_to_sicd_str(collection_start_time)),
618
+ sicd.CollectDuration(str(collection_duration)),
619
+ sicd.IPP(
620
+ {"size": "1"},
621
+ sicd.Set(
622
+ {"index": "1"},
623
+ sicd.TStart(str(0)),
624
+ sicd.TEnd(str(num_pulses / prf)),
625
+ sicd.IPPStart(str(0)),
626
+ sicd.IPPEnd(str(num_pulses - 1)),
627
+ sicd.IPPPoly(),
628
+ ),
629
+ ),
630
+ )
631
+ sksicd.PolyType().set_elem(timeline.find("./{*}IPP/{*}Set/{*}IPPPoly"), [0, prf])
632
+
633
+ position = sicd.Position(sicd.ARPPoly())
634
+ sksicd.XyzPolyType().set_elem(position.find("./{*}ARPPoly"), apc_poly)
635
+
636
+ rcv_channels = sicd.RcvChannels(
637
+ {"size": str(len(tx_rcv_pols))},
638
+ )
639
+ for ndx, tx_rcv_pol in enumerate(tx_rcv_pols):
640
+ rcv_channels.append(
641
+ sicd.ChanParameters(
642
+ {"index": str(ndx + 1)}, sicd.TxRcvPolarization(tx_rcv_pol)
643
+ )
644
+ )
645
+
646
+ radar_collection = sicd.RadarCollection(
647
+ sicd.TxFrequency(sicd.Min(str(tx_freq_min)), sicd.Max(str(tx_freq_max))),
648
+ sicd.Waveform(
649
+ {"size": "1"},
650
+ sicd.WFParameters(
651
+ {"index": "1"},
652
+ sicd.TxPulseLength(str(tx_pulse_length)),
653
+ sicd.TxRFBandwidth(str(tx_rf_bw)),
654
+ sicd.TxFreqStart(str(tx_freq_start)),
655
+ sicd.TxFMRate(str(tx_fm_rate)),
656
+ sicd.RcvWindowLength(str(rcv_window_length)),
657
+ sicd.ADCSampleRate(str(adc_sample_rate)),
658
+ ),
659
+ ),
660
+ sicd.TxPolarization(tx_polarization),
661
+ rcv_channels,
662
+ )
663
+ if len(tx_polarizations) > 1:
664
+ radar_collection.find("./{*}TxPolarization").text = "SEQUENCE"
665
+ tx_sequence = sicd.TxSequence({"size": str(len(tx_polarizations))})
666
+ for ndx, tx_pol in enumerate(tx_polarizations):
667
+ tx_sequence.append(
668
+ sicd.TxStep({"index": str(ndx + 1)}, sicd.TxPolarization(tx_pol))
669
+ )
670
+ rcv_channels.addprevious(tx_sequence)
671
+
672
+ image_formation = sicd.ImageFormation(
673
+ sicd.RcvChanProc(sicd.NumChanProc("1"), sicd.ChanIndex(str(chan_index))),
674
+ sicd.TxRcvPolarizationProc(tx_rcv_polarization),
675
+ sicd.TStartProc(str(0)),
676
+ sicd.TEndProc(str(collection_duration)),
677
+ sicd.TxFrequencyProc(
678
+ sicd.MinProc(str(tx_freq_min)), sicd.MaxProc(str(tx_freq_max))
679
+ ),
680
+ sicd.ImageFormAlgo("RMA"),
681
+ sicd.STBeamComp(st_beam_comp),
682
+ sicd.ImageBeamComp("SV"),
683
+ sicd.AzAutofocus("NO"),
684
+ sicd.RgAutofocus("NO"),
685
+ )
686
+
687
+ rma = sicd.RMA(
688
+ sicd.RMAlgoType("OMEGA_K"),
689
+ sicd.ImageType("INCA"),
690
+ sicd.INCA(
691
+ sicd.TimeCAPoly(),
692
+ sicd.R_CA_SCP(str(scp_rca)),
693
+ sicd.FreqZero(str(center_frequency)),
694
+ sicd.DRateSFPoly(),
695
+ sicd.DopCentroidPoly(),
696
+ ),
697
+ )
698
+ sksicd.PolyType().set_elem(rma.find("./{*}INCA/{*}TimeCAPoly"), time_ca_poly)
699
+ sksicd.Poly2dType().set_elem(rma.find("./{*}INCA/{*}DRateSFPoly"), drsf_poly)
700
+ sksicd.Poly2dType().set_elem(
701
+ rma.find("./{*}INCA/{*}DopCentroidPoly"), doppler_centroid_poly
702
+ )
703
+ sicd_xml_obj = sicd.SICD(
704
+ collection_info,
705
+ image_creation,
706
+ image_data,
707
+ geo_data,
708
+ grid,
709
+ timeline,
710
+ position,
711
+ radar_collection,
712
+ image_formation,
713
+ rma,
714
+ )
715
+
716
+ image_formation.addnext(sksicd.compute_scp_coa(sicd_xml_obj.getroottree()))
717
+
718
+ # Add Radiometric
719
+ cal_constant = float(cal_const_elem.findtext("./calFactor"))
720
+ betazero_poly = np.array([[cal_constant]])
721
+ graze = np.deg2rad(float(sicd_xml_obj.findtext("./{*}SCPCOA/{*}GrazeAng")))
722
+ twist = np.deg2rad(float(sicd_xml_obj.findtext("./{*}SCPCOA/{*}TwistAng")))
723
+ sigmazero_poly = betazero_poly * np.cos(graze) * np.cos(twist)
724
+ gammazero_poly = betazero_poly / np.tan(graze) * np.cos(twist)
725
+
726
+ noise_path = f'./noise[@layerIndex="{layer_index}"]'
727
+ noise_times, noise_vals = get_poly_vals(f"{noise_path}/imageNoise", "noiseEstimate")
728
+ noise_ycol_vals = zd_times_to_ycol(np.tile(noise_times, (noise_vals.shape[0], 1)))
729
+ noise_xrow_vals = np.tile(xrow_vals[:, np.newaxis], (1, noise_ycol_vals.shape[1]))
730
+ noise_grid_coords = np.stack((noise_xrow_vals, noise_ycol_vals), axis=-1)
731
+ fit_noise_vals = 10 * np.log10(noise_vals)
732
+ noise_poly = utils.polyfit2d_tol(
733
+ noise_grid_coords[..., 0].flatten(),
734
+ noise_grid_coords[..., 1].flatten(),
735
+ fit_noise_vals.flatten(),
736
+ min(fit_noise_vals.shape[0] - 1, 4),
737
+ min(fit_noise_vals.shape[1] - 1, 4),
738
+ 1e-2,
739
+ )
740
+
741
+ radiometric = sicd.Radiometric(
742
+ sicd.NoiseLevel(sicd.NoiseLevelType("ABSOLUTE"), sicd.NoisePoly()),
743
+ sicd.SigmaZeroSFPoly(),
744
+ sicd.BetaZeroSFPoly(),
745
+ sicd.GammaZeroSFPoly(),
746
+ )
747
+ sksicd.Poly2dType().set_elem(
748
+ radiometric.find("./{*}NoiseLevel/{*}NoisePoly"), noise_poly
749
+ )
750
+ sksicd.Poly2dType().set_elem(
751
+ radiometric.find("./{*}SigmaZeroSFPoly"), sigmazero_poly
752
+ )
753
+ sksicd.Poly2dType().set_elem(radiometric.find("./{*}BetaZeroSFPoly"), betazero_poly)
754
+ sksicd.Poly2dType().set_elem(
755
+ radiometric.find("./{*}GammaZeroSFPoly"), gammazero_poly
756
+ )
757
+ if rcs_row_sf and rcs_col_sf:
758
+ rcssf_poly = betazero_poly * (rcs_row_sf * rcs_col_sf / (row_bw * col_bw))
759
+ radiometric.find("./{*}SigmaZeroSFPoly").addprevious(sicd.RCSSFPoly())
760
+ sksicd.Poly2dType().set_elem(radiometric.find("./{*}RCSSFPoly"), rcssf_poly)
761
+
762
+ sicd_xml_obj.find("./{*}RMA").addprevious(radiometric)
763
+
764
+ # Add Geodata Corners
765
+ sicd_xmltree = sicd_xml_obj.getroottree()
766
+ image_grid_locations = (
767
+ np.array(
768
+ [[0, 0], [0, num_cols - 1], [num_rows - 1, num_cols - 1], [num_rows - 1, 0]]
769
+ )
770
+ - scp_pixel
771
+ ) * spacings
772
+ icp_ecef, _, _ = sksicd.image_to_ground_plane(
773
+ sicd_xmltree,
774
+ image_grid_locations,
775
+ scp_ecf,
776
+ sarkit.wgs84.up(sarkit.wgs84.cartesian_to_geodetic(scp_ecf)),
777
+ )
778
+ icp_llh = sarkit.wgs84.cartesian_to_geodetic(icp_ecef)
779
+ image_corners = sicd.ImageCorners()
780
+ sksicd.ImageCornersType().set_elem(image_corners, icp_llh[:, :2])
781
+ geo_data.append(image_corners)
782
+
783
+ # Add RNIIRS
784
+ xml_helper = sksicd.XmlHelper(sicd_xmltree)
785
+ inf_density, pred_rniirs = utils.get_rniirs_estimate(xml_helper)
786
+ collection_info.append(
787
+ sicd.Parameter({"name": "INFORMATION_DENSITY"}, f"{inf_density:.2g}")
788
+ )
789
+ collection_info.append(
790
+ sicd.Parameter({"name": "PREDICTED_RNIIRS"}, f"{pred_rniirs:.2g}")
791
+ )
792
+
793
+ # Validate XML
794
+ sicd_con = sarkit.verification.SicdConsistency(sicd_xmltree)
795
+ sicd_con.check()
796
+ sicd_con.print_result(fail_detail=True)
797
+
798
+ # Grab the data
799
+ complex_data_arr = np.transpose(read_cosar(cosar_file))
800
+ if look > 0:
801
+ complex_data_arr = complex_data_arr[:, ::-1]
802
+
803
+ metadata = sksicd.NitfMetadata(
804
+ xmltree=sicd_xmltree,
805
+ file_header_part={
806
+ "ostaid": ostaid,
807
+ "ftitle": core_name,
808
+ "security": {
809
+ "clas": classification[0].upper(),
810
+ "clsy": "US",
811
+ },
812
+ },
813
+ im_subheader_part={
814
+ "tgtid": "",
815
+ "iid2": core_name,
816
+ "security": {
817
+ "clas": classification[0].upper(),
818
+ "clsy": "US",
819
+ },
820
+ "isorce": collector_name,
821
+ },
822
+ de_subheader_part={
823
+ "security": {
824
+ "clas": classification[0].upper(),
825
+ "clsy": "US",
826
+ },
827
+ },
828
+ )
829
+
830
+ with sicd_file.open("wb") as f:
831
+ with sksicd.NitfWriter(f, metadata) as writer:
832
+ writer.write_image(complex_data_arr)
833
+
834
+
835
+ def main(args=None):
836
+ parser = argparse.ArgumentParser(description="Converts a TSX dataset into a SICD.")
837
+
838
+ parser.add_argument(
839
+ "input_xml_file", type=pathlib.Path, help="path of the input XML file"
840
+ )
841
+ parser.add_argument(
842
+ "classification",
843
+ help="content of the /SICD/CollectionInfo/Classification node in the SICD XML",
844
+ )
845
+ parser.add_argument(
846
+ "output_sicd_file",
847
+ type=pathlib.Path,
848
+ help='path of the output SICD file. The string "{pol}" will be replaced with polarization for multiple images',
849
+ )
850
+ parser.add_argument(
851
+ "--ostaid",
852
+ help="content of the originating station ID (OSTAID) field of the NITF header",
853
+ default="Unknown",
854
+ )
855
+ config = parser.parse_args(args)
856
+
857
+ if not config.input_xml_file.is_file():
858
+ raise ValueError(f"Input XML file {str(config.input_xml_file)} is not a file")
859
+
860
+ tsx_xml = etree.parse(config.input_xml_file).getroot()
861
+
862
+ images = dict()
863
+ img_ndx = 1
864
+ tx_polarizations = []
865
+ tx_rcv_pols = []
866
+ for imagedata_elem in tsx_xml.findall("./productComponents/imageData"):
867
+ layer_index = imagedata_elem.attrib["layerIndex"]
868
+ pol_layer = imagedata_elem.findtext("./polLayer")
869
+ assert imagedata_elem.findtext("./file/location/host") == "."
870
+ path = imagedata_elem.findtext("./file/location/path")
871
+ fname = imagedata_elem.findtext("./file/location/filename")
872
+ cosar_filename = config.input_xml_file.parent / path / fname
873
+ if not cosar_filename.is_file():
874
+ raise ValueError(f"Input COSAR file {str(cosar_filename)} is not a file")
875
+
876
+ sicd_filename = pathlib.Path(str(config.output_sicd_file).format(pol=pol_layer))
877
+ images[layer_index] = {
878
+ "pol_layer": pol_layer,
879
+ "chan_index": img_ndx,
880
+ "cosar_filename": cosar_filename,
881
+ "sicd_filename": sicd_filename,
882
+ }
883
+ tx_rcv_pols.append(f"{pol_layer[0]}:{pol_layer[1]}")
884
+ if (tx_polarization := pol_layer[0]) not in tx_polarizations:
885
+ tx_polarizations.append(tx_polarization)
886
+ img_ndx += 1
887
+
888
+ if len(images) != len(set([image["sicd_filename"] for image in images.values()])):
889
+ raise ValueError("Output filename does not include necessary polarization slug")
890
+
891
+ for layer_index, img_info in images.items():
892
+ cosar_to_sicd(
893
+ tsx_xml=tsx_xml,
894
+ layer_index=layer_index,
895
+ cosar_file=img_info["cosar_filename"],
896
+ sicd_file=img_info["sicd_filename"],
897
+ classification=config.classification,
898
+ ostaid=config.ostaid,
899
+ chan_index=img_info["chan_index"],
900
+ tx_polarizations=tx_polarizations,
901
+ tx_rcv_pols=tx_rcv_pols,
902
+ )
903
+
904
+
905
+ if __name__ == "__main__":
906
+ main()