Skip to content

Commit 4a6291a

Browse files
authored
initial version of s1_info cli tool (#83)
* initial version of s1_info cli tool * print output name * fix annotation, print path * add example, account for possible unzipped SAFE dirs * vary examples to show SAFE dir is okay
1 parent 578d936 commit 4a6291a

File tree

3 files changed

+200
-50
lines changed

3 files changed

+200
-50
lines changed

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,6 @@ install_requires =
2525
[options.packages.find]
2626
where = src
2727

28+
[options.entry_points]
29+
console_scripts =
30+
s1_info = s1reader.s1_info:main

src/s1reader/s1_info.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Extract the burst ID information from a Sentinel-1 SLC product."""
2+
import argparse
3+
from itertools import chain
4+
from pathlib import Path
5+
from typing import List, Optional, Union
6+
import warnings
7+
8+
import s1reader
9+
10+
11+
def get_bursts(
12+
filename: Union[Path, str], pol: str = "vv", iw: Optional[int] = None
13+
) -> List[s1reader.Sentinel1BurstSlc]:
14+
if iw is not None:
15+
iws = [iw]
16+
else:
17+
iws = [1, 2, 3]
18+
burst_nested_list = [
19+
s1reader.load_bursts(filename, None, iw, pol, flag_apply_eap=False)
20+
for iw in iws
21+
]
22+
return list(chain.from_iterable(burst_nested_list))
23+
24+
25+
def _is_safe_dir(path):
26+
# Rather than matching the name, we just check for the existence of the
27+
# manifest.safe file and annotation files
28+
if not (path / "manifest.safe").is_file():
29+
return False
30+
annotation_dir = path / "annotation"
31+
if not annotation_dir.is_dir():
32+
return False
33+
if len(list(annotation_dir.glob("*.xml"))) == 0:
34+
return False
35+
return True
36+
37+
38+
def _plot_bursts(safe_path, output_dir="burst_maps"):
39+
from s1reader.utils import plot_bursts
40+
41+
orbit_dir = None
42+
xs, ys = 5, 10
43+
epsg = 4326
44+
d = Path(output_dir).resolve()
45+
d.mkdir(exist_ok=True, parents=True)
46+
print(f"Output directory: {d}")
47+
output_filename = d / safe_path.stem
48+
plot_bursts.burst_map(safe_path, orbit_dir, xs, ys, epsg, output_filename)
49+
50+
51+
EXAMPLE = """
52+
Example usage:
53+
54+
# Print all bursts in a Sentinel-1 SLC product
55+
s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.zip
56+
57+
# Print only the burst IDs
58+
s1_info.py S1A_IW_SLC__1SDV_20180601T000000_20180601T000025_021873_025F3D_9E9E.SAFE --burst-id
59+
60+
# Print burst ids for all files matching the pattern
61+
s1_info.py -b S1A_IW_SLC__1SDV_2018*
62+
63+
# Print only from subswath IW1, and "vv" polarization
64+
s1_info.py -b S1A_IW_SLC__1SDV_2018* --iw 1 --pol vv
65+
66+
# Get info for all products in the 'data/' directory
67+
s1_info.py data/
68+
69+
# 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
72+
"""
73+
74+
75+
def get_cli_args():
76+
parser = argparse.ArgumentParser(
77+
formatter_class=argparse.RawDescriptionHelpFormatter,
78+
description="Extract the burst ID information from a Sentinel-1 SLC product.",
79+
epilog=EXAMPLE,
80+
)
81+
parser.add_argument(
82+
"paths",
83+
help="Path to the Sentinel-1 SLC product(s), or directory containing products.",
84+
nargs="+",
85+
)
86+
parser.add_argument(
87+
"--pol",
88+
default="vv",
89+
choices=["vv", "vh", "hh", "hv"],
90+
help="Polarization to use.",
91+
)
92+
parser.add_argument(
93+
"-i",
94+
"--iw",
95+
type=int,
96+
choices=[1, 2, 3],
97+
help="Print only the burst IDs for the given IW.",
98+
)
99+
parser.add_argument(
100+
"-b",
101+
"--burst-id",
102+
action="store_true",
103+
help="Print only the burst IDs for all bursts.",
104+
)
105+
parser.add_argument(
106+
"-p",
107+
"--plot",
108+
action="store_true",
109+
help="Plot the burst map for all bursts.",
110+
)
111+
parser.add_argument(
112+
"-o",
113+
"--output-dir",
114+
default="burst_maps",
115+
help=(
116+
"Name of the output directory for the burst maps (if plotting),"
117+
" with files named for each S1 product (default= %(default)s)."
118+
),
119+
)
120+
return parser.parse_args()
121+
122+
123+
def main():
124+
args = get_cli_args()
125+
paths = [Path(p) for p in args.paths]
126+
all_files = []
127+
for path in paths:
128+
if path.is_file() or _is_safe_dir(path):
129+
all_files.append(path)
130+
elif path.is_dir():
131+
# Get all matching files within the directory
132+
files = path.glob("S1[AB]_IW*")
133+
all_files.extend(list(sorted(files)))
134+
else:
135+
warnings.warn(f"{path} is not a file or directory. Skipping.")
136+
137+
print(f"Found {len(all_files)} Sentinel-1 SLC products.")
138+
for path in all_files:
139+
if args.plot:
140+
_plot_bursts(path)
141+
continue
142+
print(f"Bursts in {path}:")
143+
print("-" * 80)
144+
# Do we want to pretty-print this with rich?
145+
for burst in get_bursts(path, args.pol, args.iw):
146+
if args.burst_id:
147+
print(burst.burst_id)
148+
else:
149+
print(burst)
150+
151+
152+
if __name__ == "__main__":
153+
main()

src/s1reader/utils/plot_bursts.py

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import argparse
22
import os
3-
import sys
4-
from importlib import import_module
3+
from pathlib import Path
54

6-
named_libs = [('fiona', 'fiona'), ('geopandas', 'gpd'), ('pandas', 'pd')]
7-
8-
for (name, short) in named_libs:
9-
try:
10-
lib = import_module(name)
11-
except:
12-
print(sys.exc_info())
13-
else:
14-
globals()[short] = lib
5+
try:
6+
import fiona
7+
import geopandas as gpd
8+
import pandas as pd
9+
except ImportError:
10+
print("ERROR: fiona, geopandas, and pandas are required for this script.")
11+
raise
1512

1613
from osgeo import osr
1714
from shapely.geometry import Polygon
@@ -25,34 +22,27 @@ def command_line_parser():
2522
'''
2623
Command line parser
2724
'''
28-
parser = argparse.ArgumentParser(description="""
29-
Create a burst map for a single slc""",
30-
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
31-
parser.add_argument('-s', '--slc', type=str, action='store',
32-
dest='slc',
33-
help="slc to map")
34-
parser.add_argument('-d', '--orbit-dir', type=str, dest='orbit_dir',
35-
help="Directory containing orbit files")
25+
parser = argparse.ArgumentParser(
26+
description="Create a burst map for a single slc",
27+
formatter_class=argparse.ArgumentDefaultsHelpFormatter
28+
)
29+
parser.add_argument('-s', '--slc', help="Sentinel-1 product to load.")
30+
parser.add_argument('-d', '--orbit-dir', help="Directory containing orbit files")
3631
parser.add_argument('-x', '--x-spacing', type=float, default=5,
37-
dest='x_spacing',
3832
help='Spacing of the geogrid in x direction')
3933
parser.add_argument('-y', '--y-spacing', type=float, default=10,
40-
dest='y_spacing',
4134
help='Spacing of the geogrid in y direction')
42-
parser.add_argument('-e', '--epsg', type=int, dest='epsg',
43-
help='EPSG for output coordinates')
44-
parser.add_argument('-o', '--output', type=str, default='burst_map.gpkg',
45-
dest='output',
35+
parser.add_argument('-e', '--epsg', type=int, help='EPSG for output coordinates')
36+
parser.add_argument('-o', '--output', default='burst_map.gpkg', dest='output',
4637
help='Base filename for all output burst map products')
4738
return parser.parse_args()
4839

4940

50-
def burst_map(slc, orbit_dir, x_spacing,
51-
y_spacing, epsg,
52-
output_filename):
53-
"""
54-
Create a CSV of SLC metadata and plot bursts
55-
Parameters:
41+
def burst_map(slc, orbit_dir, x_spacing, y_spacing, epsg, output_filename):
42+
"""Create a CSV of SLC metadata and plot bursts.
43+
44+
Parameters
45+
----------
5646
slc: str
5747
Path to SLC file
5848
orbit_dir: str
@@ -65,11 +55,11 @@ def burst_map(slc, orbit_dir, x_spacing,
6555
EPSG code for the output coodrdinates
6656
output_filename: str
6757
Filename used for the output CSV, shp, html, and kml files
68-
69-
Returns:
58+
59+
Returns
60+
-------
7061
output_filename.csv, output_filename.shp, output_filename.html, output_filename.kml
7162
"""
72-
7363
# Initialize dictionary that will contain all the info for geocoding
7464
burst_map = {'burst_id':[], 'length': [], 'width': [],
7565
'spacing_x': [], 'spacing_y':[], 'min_x': [],
@@ -78,7 +68,7 @@ def burst_map(slc, orbit_dir, x_spacing,
7868
'border':[]}
7969
i_subswath = [1, 2, 3]
8070
pol = 'vv'
81-
orbit_path = get_orbit_file_from_dir(slc, orbit_dir)
71+
orbit_path = get_orbit_file_from_dir(slc, orbit_dir) if orbit_dir else None
8272

8373
for subswath in i_subswath:
8474
ref_bursts = load_bursts(slc, orbit_path, subswath, pol)
@@ -93,6 +83,8 @@ def burst_map(slc, orbit_dir, x_spacing,
9383
burst_map['first_valid_sample'].append(burst.first_valid_sample)
9484
burst_map['last_valid_sample'].append(burst.last_valid_sample)
9585

86+
# TODO: this will ignore the other border for bursts on the antimeridian.
87+
# Should probably turn into a MultiPolygon
9688
poly = burst.border[0]
9789
# Give some margin to the polygon
9890
margin = 0.001
@@ -113,6 +105,7 @@ def burst_map(slc, orbit_dir, x_spacing,
113105
tgt_x.append(dummy_x)
114106
tgt_y.append(dummy_y)
115107

108+
# TODO: Get the min/max from the burst database
116109
if epsg == 4326:
117110
x_min = x_spacing * (min(tgt_x) / x_spacing)
118111
y_min = y_spacing * (min(tgt_y) / y_spacing)
@@ -130,28 +123,29 @@ def burst_map(slc, orbit_dir, x_spacing,
130123
burst_map['max_x'].append(x_max)
131124
burst_map['max_y'].append(y_max)
132125

126+
out_path = Path(output_filename)
127+
133128
# Save generated burst map as csv
134129
data = pd.DataFrame.from_dict(burst_map)
135-
data.to_csv(f'{output_filename}.csv')
130+
data.to_csv(out_path.with_suffix('.csv'))
136131

137132
# Create GeoDataFrame to plot bursts on a map
138133
df = data
139134
df['border'] = df['border'].apply(wkt.loads)
140-
gdf = gpd.GeoDataFrame(df, crs='epsg:4326')
141-
gdf = gdf.rename(columns={'border': 'geometry'}).set_geometry('geometry')
142-
135+
gdf = gpd.GeoDataFrame(df.rename(columns={'border': 'geometry'}), crs='epsg:4326')
136+
137+
gdf.to_file(out_path.with_suffix('.gpkg'), driver='GPKG')
143138
# Save the GeoDataFrame as a shapefile (some people may prefer the format)
144-
gdf.to_file(f'{output_filename}.shp')
145-
139+
gdf.to_file(out_path.with_suffix('.shp'))
140+
146141
# Save the GeoDataFrame as a kml
147-
kml_path = f'{output_filename}.kml'
148-
if os.path.isfile(kml_path):
149-
os.remove(kml_path)
150-
142+
kml_path = out_path.with_suffix('.kml')
143+
if kml_path.exists():
144+
kml_path.unlink()
145+
151146
fiona.supported_drivers['KML'] = 'rw'
152-
gdf.to_file(f'{output_filename}.kml', driver='KML')
153-
154-
147+
gdf.to_file(kml_path, driver='KML')
148+
155149
# Plot bursts on an interactive map
156150
m = gdf.explore(
157151
column="burst_id", # make choropleth based on "Burst ID" column
@@ -162,8 +156,8 @@ def burst_map(slc, orbit_dir, x_spacing,
162156
style_kwds=dict(color="black") # use black outline
163157
)
164158

165-
m.save(f'{output_filename}.html')
166-
159+
m.save(out_path.with_suffix('.html'))
160+
167161

168162
if __name__ == '__main__':
169163
cmd = command_line_parser()

0 commit comments

Comments
 (0)