Skip to content

Commit 8466baf

Browse files
Merge pull request #23 from forrestfwilliams/develop
Release v0.3.1
2 parents 8671e57 + 66ef190 commit 8466baf

File tree

4 files changed

+87
-64
lines changed

4 files changed

+87
-64
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/)
77
and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
88

9+
## [0.3.1]
10+
11+
### Changed
12+
* Loading of SICD data during beta0/sigma0 creation to a chunked strategy to reduce memory requirements
13+
14+
### Fixed
15+
* Geolocation issue for prototype Umbra workflow related to switching to local UTM zone during processing
16+
917
## [0.3.0]
1018

1119
### Changed

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
A python library for creating ISCE3-based RTCs for multiple SAR data sources
44

5-
**ALL CREDIT FOR THIS LIBRARY'S RTC ALGORITHM GOES TO GUSTAVO SHIROMA AND THE JPL [OPERA](https://www.jpl.nasa.gov/go/opera/about-opera/) AND [ISCE3](https://github.com/isce-framework/isce3) TEAMS. THIS PLUGIN MERELY ALLOWS OTHERS TO USE THEIR ALGORITHM WITH MULTIPLE SENSORS.**
5+
> [!WARNING]
6+
> This package is still in early development. Users are encouraged to not use this package in production or other critical contexts until the v1.0.0 release.
67
7-
The RTC algorithm utilized by this library is described in [Shiroma et al., 2023](https://doi.org/10.1109/TGRS.2022.3147472).
8+
> [!IMPORTANT]
9+
> All credit for this library's RTC algorithm goes to Gustavo Shiroma and the JPL [OPERA](https://www.jpl.nasa.gov/go/opera/about-opera/) and [ISCE3](https://github.com/isce-framework/isce3) teams. This package merely allows others to use their algorithm with a wider set of SAR data sources. The RTC algorithm utilized by this package is described in [Shiroma et al., 2023](https://doi.org/10.1109/TGRS.2022.3147472).
810
911
## Usage
1012
MultiRTC allows users to create RTC products from SLC data for multiple SAR sensor platforms. Currently this list includes:
@@ -23,14 +25,21 @@ multirtc PLATFORM SLC-GRANULE --resolution RESOLUTION --work-dir WORK-DIR
2325
```
2426
Where `PLATFORM` is the name of the satellite platform (currently `S1`, `CAPELLA` or `UMBRA`), `SLC-GRANULE` is the name of the SLC granule, `RESOLUTION` is the desired output resolution of the RTC image in meters, and `WORK-DIR` is the name of the working directory to perform processing in. Inputs such as the SLC data, DEM, and external orbit information are stored in `WORK-DIR/input`, while the RTC image and associated outputs are stored in `WORK-DIR/output` once processing is complete. SLC data that is available in the [Alaska Satellite Facility's data archive](https://search.asf.alaska.edu/#/?maxResults=250) (such as Sentinel-1 Burst SLCs) will be automatically downloaded to the input directory, but data not available in this archive (commercial datasets) are required to be staged in the input directory prior to processing.
2527

26-
Output RTC products are in Gamma0 radiometry.
28+
Output RTC pixel values represent gamma0 power.
2729

2830
### Current Umbra Implementation
29-
Currently, the Umbra processor only supports basic geocoding and not full RTC processing. ISCE3's RTC algorithm is only designed to work with Range Migration Algorithm (RMA) focused SLC products, but Umbra creates their data using the Polar Format Algorithm (PFA). Using an [approach detailed by Piyush Agram](https://arxiv.org/abs/2503.07889v1) to adapt RMA approaches to the PFA image geometry, we have developed a workflow to geocode an Umbra SLC but there is more work to be done to implement full RTC processing. Since full RTC is not yet implemented, Umbra geocoded products are in Sigma0 radiometry.
31+
Currently, the Umbra processor only supports basic geocoding and not full RTC processing. ISCE3's RTC algorithm is only designed to work with Range Migration Algorithm (RMA) focused SLC products, but Umbra creates their data using the Polar Format Algorithm (PFA). Using an [approach detailed by Piyush Agram](https://arxiv.org/abs/2503.07889v1) to adapt RMA approaches to the PFA image geometry, we have developed a workflow to geocode an Umbra SLC but there is more work to be done to implement full RTC processing. Since full RTC is not yet implemented, Umbra geocoded pixel values represent sigma0 power.
3032

3133
### DEM options
3234
Currently, only the OPERA DEM is supported. This is a global Height Above Ellipsoid DEM sourced from the [COP-30 DEM](https://portal.opentopography.org/raster?opentopoID=OTSDEM.032021.4326.3). In the future, we hope to support a wider variety of automatically retrieved and user provided DEMs.
3335

36+
## When will support for [insert SAR provider here] products be added?
37+
We're currently working on this package on a "best effort" basis with no specific timeline for any particular dataset. We would love to add support for every SAR dataset ASAP, but we only have so much time to devote to this package. If you want a particular dataset to be prioritized there are several things you can do:
38+
39+
- [Open an issue](https://github.com/forrestfwilliams/multirtc/issues/new) requesting support for your dataset and encourage others to like or comment on it.
40+
- Provides links to example datasets over the Rosamond, California corner reflector site (Lat/Lon 34.799,-118.095) for performing cal/val.
41+
- Reach out to us about funding the development required to add your dataset.
42+
3443
## Developer Setup
3544
1. Ensure that conda is installed on your system (we recommend using [mambaforge](https://github.com/conda-forge/miniforge#mambaforge) to reduce setup times).
3645
2. Download a local version of the `multirtc` repository (`git clone https://github.com/forrestfwilliams/multirtc.git`)

src/multirtc/create_rtc.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from scipy import ndimage
1111
from tqdm import tqdm
1212

13+
from multirtc.define_geogrid import get_point_epsg
1314
from multirtc.prep_burst import S1BurstSlc
1415
from multirtc.s1burst_corrections import compute_correction_lut
1516
from multirtc.sicd import SicdSlc
@@ -360,13 +361,11 @@ def rtc(slc, geogrid, opts):
360361

361362
if isinstance(slc, SicdSlc):
362363
input_filename = slc.filepath.parent / (slc.filepath.stem + '_beta0.tif')
363-
if not input_filename.exists():
364-
slc.create_complex_beta0(input_filename)
364+
slc.create_complex_beta0(input_filename)
365365
input_filename = str(input_filename)
366366
elif isinstance(slc, S1BurstSlc):
367367
input_filename = slc.filepath.parent / (slc.filepath.stem + '_beta0.tif')
368-
if not input_filename.exists():
369-
slc.create_complex_beta0(input_filename, flag_thermal_correction=opts.apply_thermal_noise)
368+
slc.create_complex_beta0(input_filename, flag_thermal_correction=opts.apply_thermal_noise)
370369
input_filename = str(input_filename)
371370
sub_swaths = slc.apply_valid_data_masking()
372371
geocode_kwargs['sub_swaths'] = sub_swaths
@@ -522,8 +521,8 @@ def pfa_prototype_geocode(sicd, geogrid, dem_path, output_dir):
522521
slc_lut = isce3.core.LUT2d(
523522
np.arange(sigma0_data.shape[1]), np.arange(sigma0_data.shape[0]), sigma0_data, interp_method
524523
)
525-
local2ecef = pyproj.Transformer.from_crs(f'EPSG:{geogrid.epsg}', 'EPSG:4978', always_xy=True)
526-
local2ll = pyproj.Transformer.from_crs(f'EPSG:{geogrid.epsg}', 'EPSG:4326', always_xy=True)
524+
assert geogrid.epsg == 4326, 'Only EPSG:4326 is supported for PFA prototype geocoding'
525+
ll2ecef = pyproj.Transformer.from_crs('EPSG:4326', 'EPSG:4978', always_xy=True)
527526
dem_raster = isce3.io.Raster(str(dem_path))
528527
dem = isce3.geometry.DEMInterpolator()
529528
dem.load_dem(dem_raster)
@@ -535,9 +534,8 @@ def pfa_prototype_geocode(sicd, geogrid, dem_path, output_dir):
535534
for i, j in tqdm(itertools.product(range(geogrid.width), range(geogrid.length)), total=n_iters):
536535
x = geogrid.start_x + (i * geogrid.spacing_x)
537536
y = geogrid.start_y + (j * geogrid.spacing_y)
538-
lon, lat = local2ll.transform(x, y)
539537
hae = dem.interpolate_lonlat(np.deg2rad(x), np.deg2rad(y)) # ISCE3 expects lat/lon to be in radians!
540-
ecef_x, ecef_y, ecef_z = local2ecef.transform(x, y, hae)
538+
ecef_x, ecef_y, ecef_z = ll2ecef.transform(x, y, hae)
541539
row, col = sicd.geo2rowcol(np.array([(ecef_x, ecef_y, ecef_z)]))[0]
542540
if slc_lut.contains(row, col):
543541
output[j, i] = slc_lut.eval(row, col)
@@ -556,3 +554,6 @@ def pfa_prototype_geocode(sicd, geogrid, dem_path, output_dir):
556554
out_ds.GetRasterBand(1).SetNoDataValue(np.nan)
557555
out_ds.SetMetadata({'AREA_OR_POINT': 'Area'})
558556
out_ds = None
557+
558+
local_epsg = get_point_epsg(geogrid.start_y, geogrid.start_x)
559+
gdal.Warp(str(output_path), str(output_path), dstSRS=f'EPSG:{local_epsg}', format='GTiff')

src/multirtc/sicd.py

Lines changed: 57 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from sarpy.io.complex.sicd import SICDReader
1010
from shapely.geometry import Point, Polygon
1111

12-
from multirtc import define_geogrid
1312
from multirtc.base import SlcTemplate, to_isce_datetime
13+
from multirtc.define_geogrid import get_point_epsg
1414

1515

1616
def check_poly_order(poly):
@@ -19,8 +19,8 @@ def check_poly_order(poly):
1919

2020
class SicdSlc:
2121
def __init__(self, sicd_path):
22-
reader = SICDReader(str(sicd_path.expanduser().resolve()))
23-
sicd = reader.get_sicds_as_tuple()[0]
22+
self.reader = SICDReader(str(sicd_path.expanduser().resolve()))
23+
sicd = self.reader.get_sicds_as_tuple()[0]
2424
self.source = sicd
2525
self.id = Path(sicd_path).with_suffix('').name
2626
self.filepath = Path(sicd_path)
@@ -50,7 +50,7 @@ def __init__(self, sicd_path):
5050
self.raw_time_coa_poly = sicd.Grid.TimeCOAPoly
5151
last_line_time = self.raw_time_coa_poly(0, self.shape[1] - self.shift[1])
5252
first_line_time = self.raw_time_coa_poly(0, -self.shift[1])
53-
self.az_reversed = last_line_time > first_line_time
53+
self.az_reversed = last_line_time < first_line_time
5454
self.arp_pos = sicd.SCPCOA.ARPPos.get_array()
5555
self.scp_pos = sicd.GeoData.SCP.ECF.get_array()
5656
azimuth_angle, elevation_angle = self.calculate_look_angles()
@@ -88,71 +88,69 @@ def calculate_look_angles(self):
8888
elevation = np.arcsin(up / np.linalg.norm(topocentric))
8989
return np.rad2deg(azimuth), np.rad2deg(elevation)
9090

91-
def get_xrow_ycol(self) -> np.ndarray:
91+
def get_xrow_ycol(self, rowrange=None, colrange=None) -> np.ndarray:
9292
"""Calculate xrow and ycol SICD."""
93-
irow = np.tile(np.arange(self.shape[0]), (self.shape[1], 1)).T
94-
irow -= self.scp_index[0]
93+
rowlen = self.shape[0] if rowrange is None else rowrange[1] - rowrange[0]
94+
collen = self.shape[1] if colrange is None else colrange[1] - colrange[0]
95+
rowoffset = self.scp_index[0] if rowrange is None else self.scp_index[0] + rowrange[0]
96+
coloffset = self.scp_index[1] if colrange is None else self.scp_index[1] + colrange[0]
97+
98+
irow = np.tile(np.arange(rowlen), (collen, 1)).T
99+
irow -= rowoffset
95100
xrow = irow * self.spacing[0]
96101

97-
icol = np.tile(np.arange(self.shape[1]), (self.shape[0], 1))
98-
icol -= self.scp_index[1]
102+
icol = np.tile(np.arange(collen), (rowlen, 1))
103+
icol -= coloffset
99104
ycol = icol * self.spacing[1]
100105
return xrow, ycol
101106

102-
def load_data(self):
103-
reader = SICDReader(str(self.filepath))
104-
data = reader[:, :]
105-
return data
106-
107-
def load_scaled_data(self, scale, power=False):
107+
def load_scaled_data(self, scale, power=False, rowrange=None, colrange=None):
108108
if scale == 'beta0':
109109
coeff = self.beta0.Coefs
110110
elif scale == 'sigma0':
111111
coeff = self.sigma0.Coefs
112112
else:
113113
raise ValueError(f'Scale must be either "beta0" or "sigma0", got {scale}')
114114

115-
xrow, ycol = self.get_xrow_ycol()
115+
xrow, ycol = self.get_xrow_ycol(rowrange=rowrange, colrange=colrange)
116+
if colrange is not None and rowrange is not None:
117+
data = self.reader[rowrange[0] : rowrange[1], colrange[0] : colrange[1]]
118+
elif colrange is None and rowrange is None:
119+
data = self.reader[:, :]
120+
else:
121+
raise ValueError('Both xrange and yrange must be provided or neither.')
122+
116123
scale_factor = polyval2d(xrow, ycol, coeff)
117-
data = self.load_data()
124+
del xrow, ycol # deleting for memory management
125+
118126
if power:
119-
scaled_data = (data.real**2 + data.imag**2) * scale_factor
127+
data = (data.real**2 + data.imag**2) * scale_factor
120128
else:
121-
scaled_data = data * np.sqrt(scale_factor)
122-
return scaled_data
123-
124-
def write_complex_beta0(self, outpath, isce_format=True):
125-
scaled_data = self.load_scaled_data('beta0', power=False)
126-
if isce_format:
127-
if self.az_reversed:
128-
scaled_data = scaled_data[:, ::-1].T
129-
else:
130-
scaled_data = scaled_data.T
129+
data = data * np.sqrt(scale_factor)
130+
return data
131131

132+
def create_complex_beta0(self, outpath, row_iter=256):
132133
driver = gdal.GetDriverByName('GTiff')
133-
ds = driver.Create(str(outpath), scaled_data.shape[1], scaled_data.shape[0], 1, gdal.GDT_CFloat32)
134+
# Shape transposed for ISCE3 expectations
135+
ds = driver.Create(str(outpath), self.shape[0], self.shape[1], 1, gdal.GDT_CFloat32)
134136
band = ds.GetRasterBand(1)
135-
band.WriteArray(scaled_data)
136-
band.FlushCache()
137-
ds = None
138-
139-
def create_complex_beta0(self, outpath, isce_format=True):
140-
xrow, ycol = self.get_xrow_ycol()
141-
scale_factor = np.sqrt(polyval2d(xrow, ycol, self.beta0_coeff))
142-
data = self.load_data()
143-
scaled_data = data * scale_factor
144-
145-
if isce_format:
137+
n_chunks = int(np.floor(self.shape[0] // row_iter)) + 1
138+
for i in range(n_chunks):
139+
start_row = i * row_iter
140+
end_row = min((i + 1) * row_iter, self.shape[0])
141+
rowrange = [start_row, end_row]
142+
colrange = [0, self.shape[1]]
143+
scaled_data = self.load_scaled_data('beta0', power=False, rowrange=rowrange, colrange=colrange)
144+
# Shape transposed for ISCE3 expectations
146145
if self.az_reversed:
147146
scaled_data = scaled_data[:, ::-1].T
148147
else:
149148
scaled_data = scaled_data.T
149+
# Offset transposed to match ISCE3 expectations
150+
band.WriteArray(scaled_data, xoff=start_row, yoff=0)
150151

151-
driver = gdal.GetDriverByName('GTiff')
152-
ds = driver.Create(str(outpath), scaled_data.shape[1], scaled_data.shape[0], 1, gdal.GDT_CFloat32)
153-
band = ds.GetRasterBand(1)
154-
band.WriteArray(scaled_data)
155152
band.FlushCache()
153+
ds.FlushCache()
156154
ds = None
157155

158156

@@ -304,17 +302,24 @@ def geo2rowcol(self, xyz: np.ndarray) -> np.ndarray:
304302
return row_col
305303

306304
def create_geogrid(self, spacing_meters):
307-
epsg_local = define_geogrid.get_point_epsg(self.center.y, self.center.x)
308305
ecef = pyproj.CRS(4978) # ECEF on WGS84 Ellipsoid
309-
local = pyproj.CRS(epsg_local)
310-
ecef2local = pyproj.Transformer.from_crs(ecef, local, always_xy=True)
311-
x_spacing = spacing_meters
312-
y_spacing = -1 * spacing_meters
306+
lla = pyproj.CRS(4979) # WGS84 lat/lon/ellipsoid height
307+
local_utm = pyproj.CRS(get_point_epsg(self.center.y, self.center.x))
308+
lla2utm = pyproj.Transformer.from_crs(lla, local_utm, always_xy=True)
309+
utm2lla = pyproj.Transformer.from_crs(local_utm, lla, always_xy=True)
310+
ecef2lla = pyproj.Transformer.from_crs(ecef, lla, always_xy=True)
311+
312+
lla_point = (self.center.x, self.center.y)
313+
utm_point = lla2utm.transform(*lla_point)
314+
utm_point_shift = (utm_point[0] + spacing_meters, utm_point[1])
315+
lla_point_shift = utm2lla.transform(*utm_point_shift)
316+
x_spacing = lla_point_shift[0] - lla_point[0]
317+
y_spacing = -1 * x_spacing
313318

314319
points = np.array([(0, 0), (0, self.shape[1]), self.shape, (self.shape[0], 0)])
315-
geos = self.rowcol2geo(points, hae=self.scp_hae)
320+
geos = self.rowcol2geo(points, self.scp_hae)
316321

317-
points = np.vstack(ecef2local.transform(geos[:, 0], geos[:, 1], geos[:, 2])).T
322+
points = np.vstack(ecef2lla.transform(geos[:, 0], geos[:, 1], geos[:, 2])).T
318323
minx, maxx = np.min(points[:, 0]), np.max(points[:, 0])
319324
miny, maxy = np.min(points[:, 1]), np.max(points[:, 1])
320325

@@ -327,6 +332,6 @@ def create_geogrid(self, spacing_meters):
327332
spacing_y=float(y_spacing),
328333
length=int(length),
329334
width=int(width),
330-
epsg=epsg_local,
335+
epsg=4326,
331336
)
332337
return geogrid

0 commit comments

Comments
 (0)