Skip to content

Commit 428328d

Browse files
authored
add a --bbox option to s1_info for frame bounds (#88)
* add a --bbox option to s1_info for frame bounds * add method to get bounds from the preview, if available * get preview kml bounds working * update usage for bbox and correct entry point * add __init__.py to utils subpackage * try loading any polarization for the bounds, accounting for hh/hv * reorder kml parsing logic * separate options in frame-bbox and burst-bbox * remove unused imports
1 parent a29a422 commit 428328d

File tree

2 files changed

+133
-14
lines changed

2 files changed

+133
-14
lines changed

src/s1reader/s1_info.py

Lines changed: 133 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
"""Extract the burst ID information from a Sentinel-1 SLC product."""
22
import argparse
3+
import sys
4+
import warnings
5+
import zipfile
36
from itertools import chain
47
from pathlib import Path
58
from typing import List, Optional, Union
6-
import warnings
9+
10+
import lxml.etree as ET
11+
import shapely.geometry
12+
import shapely.ops
713

814
import s1reader
915

@@ -22,7 +28,7 @@ def get_bursts(
2228
return list(chain.from_iterable(burst_nested_list))
2329

2430

25-
def _is_safe_dir(path):
31+
def _is_safe_dir(path: Union[Path, str]) -> bool:
2632
# Rather than matching the name, we just check for the existence of the
2733
# manifest.safe file and annotation files
2834
if not (path / "manifest.safe").is_file():
@@ -35,7 +41,7 @@ def _is_safe_dir(path):
3541
return True
3642

3743

38-
def _plot_bursts(safe_path, output_dir="burst_maps"):
44+
def _plot_bursts(safe_path: Union[Path, str], output_dir="burst_maps") -> None:
3945
from s1reader.utils import plot_bursts
4046

4147
orbit_dir = None
@@ -48,27 +54,121 @@ def _plot_bursts(safe_path, output_dir="burst_maps"):
4854
plot_bursts.burst_map(safe_path, orbit_dir, xs, ys, epsg, output_filename)
4955

5056

57+
def get_frame_bounds(safe_path: Union[Path, str]) -> List[float]:
58+
"""Get the bounding box of the frame from the union of all burst bounds.
59+
60+
bounding box format is [lonmin, latmin, lonmax, latmax]
61+
62+
Notes
63+
-----
64+
Will use the preview/map-overlay.kml file if it exists, otherwise will
65+
use the union of all burst bounds (which is slower).
66+
67+
Parameters
68+
----------
69+
safe_path : Union[Path, str]
70+
Path to the SAFE directory or zip file.
71+
72+
Returns
73+
-------
74+
List[float]
75+
[lonmin, latmin, lonmax, latmax]
76+
"""
77+
try:
78+
return _bounds_from_preview(safe_path)
79+
except Exception as e:
80+
warnings.warn(f"Could not get bounds for {safe_path}: {e}")
81+
return _bounds_from_bursts(safe_path)
82+
83+
84+
def _bounds_from_preview(safe_path: Union[Path, str]) -> List[float]:
85+
"""Get the bounding box of the frame from the preview/map-overlay.kml."""
86+
# looking for:
87+
# S1A_IW_SLC__1SDV_20221005T125539_20221005T125606_045307_056AA5_CB45.SAFE/preview/map-overlay.kml
88+
if _is_safe_dir(safe_path):
89+
overlay_path = Path(safe_path) / "preview" / "map-overlay.kml"
90+
root = ET.parse(overlay_path).getroot()
91+
else:
92+
# The name of the unzipped .SAFE directory (with .zip stripped)
93+
with zipfile.ZipFile(safe_path, "r") as zip_ref:
94+
zname = [
95+
zi
96+
for zi in zip_ref.infolist()
97+
if "preview/map-overlay.kml" in zi.filename
98+
]
99+
if len(zname) > 0:
100+
with zip_ref.open(zname[0].filename, "r") as kml_in:
101+
root = ET.parse(kml_in).getroot()
102+
else:
103+
root = None
104+
105+
if root is None:
106+
raise ValueError(f"map-overlay.kml does not exist in {safe_path}.")
107+
108+
# point_str looks like:
109+
# <coordinates>-102.552971,31.482372 -105.191353,31.887299...
110+
point_str = list(elem.text for elem in root.iter("coordinates"))[0]
111+
coords = [p.split(",") for p in point_str.split()]
112+
lons, lats = zip(*[(float(lon), float(lat)) for lon, lat in coords])
113+
return [min(lons), min(lats), max(lons), max(lats)]
114+
115+
116+
def _bounds_from_bursts(safe_path: Union[Path, str]) -> List[float]:
117+
"""Get the bounding box of the frame from the union of all burst bounds."""
118+
# Get all the bursts from subswath 1, 2, 3
119+
bursts = None
120+
for pol in ["vv", "hh", "hv", "vh"]:
121+
try:
122+
bursts = get_bursts(safe_path, pol=pol)
123+
break
124+
except ValueError:
125+
# This is raised if the product doesn't have the specified polarization
126+
continue
127+
if bursts is None:
128+
raise ValueError("Could not load any polarizations in {safe_path}.")
129+
130+
# Convert the border (list of polygons) into a MultiPolygon
131+
all_borders = [shapely.geometry.MultiPolygon(b.border) for b in bursts]
132+
# Perform a union to get one shape for the entire frame
133+
border_geom = shapely.ops.unary_union(all_borders)
134+
# grab the bounds and pad as needed
135+
return list(border_geom.bounds)
136+
137+
51138
EXAMPLE = """
52139
Example usage:
53140
54141
# Print all bursts in a Sentinel-1 SLC product
55-
s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip
142+
s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip
56143
57144
# Print only the burst IDs
58-
s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE --burst-id
145+
s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE --burst-id
146+
147+
# Print the burst IDs, and the bounding box for each burst
148+
s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE -b --burst-bbox
59149
60150
# Print burst ids for all files matching the pattern
61-
s1_info.py -b S1A_IW_SLC__1SDV_2018*
151+
s1_info -b S1A_IW_SLC__1SDV_2018*
62152
63153
# Print only from subswath IW1, and "vv" polarization
64-
s1_info.py -b S1A_IW_SLC__1SDV_2018* --iw 1 --pol vv
154+
s1_info -b S1A_IW_SLC__1SDV_2018* --iw 1 --pol vv
65155
66156
# Get info for all products in the 'data/' directory
67-
s1_info.py data/
68-
157+
s1_info data/
158+
159+
# Print the bounding box of the full frame for each product
160+
s1_info --frame-bbox data/
161+
162+
# Using https://github.com/scottstanie/sardem , create a DEM covering the SLC product
163+
s1_info --frame-bbox S1A_IW_SLC__1SDV_20220226T124745_20220226T124812_042084_050378_F69A.zip |
164+
cut -d':' -f2 | # separate the bbox from the label
165+
tr -d ',[]' | # remove brackets and commas
166+
xargs sardem --data cop --bbox # pass the bbox to sardem as an argument
167+
168+
69169
# Plot the burst map, saving files into the 'burst_maps/' directory
70-
s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE/ --plot
71-
s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip -p -o my_burst_maps
170+
s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE/ --plot
171+
s1_info S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip -p -o my_burst_maps
72172
"""
73173

74174

@@ -102,6 +202,16 @@ def get_cli_args():
102202
action="store_true",
103203
help="Print only the burst IDs for all bursts.",
104204
)
205+
parser.add_argument(
206+
"--frame-bbox",
207+
action="store_true",
208+
help="Print the frame bounding box (lonmin, latmin, lonmax, latmax) of the S1 product.",
209+
)
210+
parser.add_argument(
211+
"--burst-bbox",
212+
action="store_true",
213+
help="Print each burst's bounding box (lonmin, latmin, lonmax, latmax).",
214+
)
105215
parser.add_argument(
106216
"-p",
107217
"--plot",
@@ -134,19 +244,28 @@ def main():
134244
else:
135245
warnings.warn(f"{path} is not a file or directory. Skipping.")
136246

137-
print(f"Found {len(all_files)} Sentinel-1 SLC products.")
247+
print(f"Found {len(all_files)} Sentinel-1 SLC products.", file=sys.stderr)
138248
for path in all_files:
139249
if args.plot:
140250
_plot_bursts(path)
141251
continue
252+
elif args.frame_bbox:
253+
msg = f"{path}: {get_frame_bounds(path)}"
254+
print(msg)
255+
continue
256+
142257
print(f"Bursts in {path}:")
143258
print("-" * 80)
144259
# Do we want to pretty-print this with rich?
145260
for burst in get_bursts(path, args.pol, args.iw):
146261
if args.burst_id:
147-
print(burst.burst_id)
262+
print(burst.burst_id, end=" ")
148263
else:
149-
print(burst)
264+
print(burst, end=" ")
265+
266+
if args.burst_bbox:
267+
print(list(shapely.geometry.MultiPolygon(burst.border).bounds), end=" ")
268+
print()
150269

151270

152271
if __name__ == "__main__":

src/s1reader/utils/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)