Skip to content

Commit 6cf17b1

Browse files
Merge pull request #86 from TUDelftGeodesy/64-cropping
64 Native cropping in radar coordinates via polygon or bounding box
2 parents bec40cb + 0146f22 commit 6cf17b1

3 files changed

Lines changed: 132 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies = [
1212
"numpy",
1313
"distributed",
1414
"zarr",
15+
"shapely",
1516
]
1617
description = "Xarray extension for Synthetic Aperture Radar (SAR) data"
1718
readme = "README.md"

sarxarray/utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
import shapely.geometry as sg
23
import xarray as xr
34
from dask.delayed import Delayed, delayed
45

@@ -154,6 +155,59 @@ def _compute_coherence(numerator, denominator):
154155
return coherence
155156

156157

158+
def crop(data: xr.Dataset | xr.DataArray, geom: sg.Polygon | tuple) -> xr.Dataset:
159+
"""Crop a radar image or stack of radar images to the bounding box of a polygon.
160+
161+
Parameters
162+
----------
163+
data: xr.Dataset | xr.DataArray
164+
The dataset or data array to be cropped in azimuth and range
165+
geom: sg.Polygon | tuple
166+
shapely.geometry.Polygon in radar coordinates of the area that should be
167+
kept, in [azimuth, range] format, OR a tuple of the bounding box of the crop in
168+
(min_azimuth, min_range, max_azimuth, max_range) format
169+
170+
Returns
171+
-------
172+
xr.Dataset | xr.DataArray
173+
The dataset or data array cropped to the area of interest
174+
175+
Raises
176+
------
177+
ValueError
178+
- If the azimuth or range coordinate does not exist in `data`
179+
- If `geom` is not `shapely.geometry.Polygon` or `tuple`
180+
181+
AssertionError
182+
- When a tuple is passed to `geom` that falls in one of three categories:
183+
- The tuple does not have exactly 4 entries
184+
- The minimum azimuth coordinate is larger than or equal
185+
to the maximum azimuth coordinate
186+
- The minimum range coordinate is larger than or equal
187+
to the maximum range coordinate
188+
"""
189+
if not {"azimuth", "range"}.issubset(data.dims):
190+
raise ValueError("The data must have azimuth and range dimensions.")
191+
192+
if isinstance(geom, sg.Polygon):
193+
bounding_box = geom.bounds # returns (min_az, min_r, max_az, max_r)
194+
elif isinstance(geom, tuple):
195+
assert len(geom) == 4, f"geom as tuple must have four entries, got {len(geom)}"
196+
assert geom[0] < geom[2], f"geom Azimuth wrong: min {geom[0]} >= max {geom[2]}"
197+
assert geom[1] < geom[3], f"geom Range wrong: min {geom[1]} >= max {geom[3]}"
198+
bounding_box = geom
199+
else:
200+
raise ValueError(
201+
f"geom must be tuple or shapely.geometry.Polygon, is {type(geom)}."
202+
)
203+
data = data.sel(
204+
azimuth=range(int(bounding_box[0]), int(np.ceil(bounding_box[2]))+1),
205+
range=range(int(bounding_box[1]), int(np.ceil(bounding_box[3]))+1)
206+
)
207+
208+
return data
209+
210+
157211
def _validate_multi_look_inputs(data, window_size, method, statistics):
158212
# check if data is xarray
159213
if not isinstance(data, xr.Dataset | xr.DataArray):

tests/test_utils.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
import pytest
33
import xarray as xr
44
from dask.delayed import Delayed
5+
from shapely.geometry import Polygon
56

67
from sarxarray.utils import (
78
_get_chunks,
89
_validate_multi_look_inputs,
910
complex_coherence,
11+
crop,
1012
multi_look,
1113
)
1214

@@ -27,6 +29,22 @@ def synthetic_dataarray():
2729
},
2830
)
2931

32+
# Create a polygon for cropping
33+
@pytest.fixture
34+
def crop_geometry():
35+
return Polygon([
36+
[601.9, 1405],
37+
[605, 1407],
38+
[606, 1408],
39+
[606, 1409],
40+
[603, 1409],
41+
[601.9, 1405]
42+
])
43+
44+
# Create a bounding box for cropping
45+
@pytest.fixture
46+
def crop_geometry_bbox():
47+
return (601.9, 1405, 606, 1409)
3048

3149
# this class tests multi_look with dataarray. For testing with dataset, see
3250
# test_stack.py
@@ -276,3 +294,62 @@ def test_complex_coherence_bad_args(
276294
complex_coherence(reference, other1, window_size=(2, 2), compute=True)
277295
with pytest.raises(ValueError):
278296
complex_coherence(reference, other2, window_size=(2, 2), compute=True)
297+
298+
299+
class TestUtilsCrop:
300+
def test_crop_poly(self, synthetic_dataarray, crop_geometry):
301+
da = synthetic_dataarray
302+
geom = crop_geometry
303+
da_crop = crop(da, geom)
304+
assert da_crop.azimuth.size == 6
305+
assert da_crop.range.size == 5
306+
assert da_crop.time.size == da.time.size
307+
308+
def test_crop_bbox(self, synthetic_dataarray, crop_geometry_bbox):
309+
da = synthetic_dataarray
310+
geom = crop_geometry_bbox
311+
da_crop = crop(da, geom)
312+
assert da_crop.azimuth.size == 6
313+
assert da_crop.range.size == 5
314+
assert da_crop.time.size == da.time.size
315+
316+
def test_crop_wrong_dimname(self, synthetic_dataarray, crop_geometry):
317+
da = synthetic_dataarray
318+
da = da.rename({"azimuth": "az"}) # rename azimuth to a wrong name
319+
geom = crop_geometry
320+
with pytest.raises(ValueError):
321+
_ = crop(da, geom)
322+
323+
def test_crop_bbox_wrong_length(self, synthetic_dataarray, crop_geometry_bbox):
324+
da = synthetic_dataarray
325+
geom = crop_geometry_bbox[:3]
326+
with pytest.raises(AssertionError):
327+
_ = crop(da, geom)
328+
329+
def test_crop_bbox_wrong_az(self, synthetic_dataarray, crop_geometry_bbox):
330+
da = synthetic_dataarray
331+
geom = ( # swap min and max azimuth
332+
crop_geometry_bbox[2],
333+
crop_geometry_bbox[1],
334+
crop_geometry_bbox[0],
335+
crop_geometry_bbox[3]
336+
)
337+
with pytest.raises(AssertionError):
338+
_ = crop(da, geom)
339+
340+
def test_crop_bbox_wrong_r(self, synthetic_dataarray, crop_geometry_bbox):
341+
da = synthetic_dataarray
342+
geom = ( # swap min and max range
343+
crop_geometry_bbox[0],
344+
crop_geometry_bbox[3],
345+
crop_geometry_bbox[2],
346+
crop_geometry_bbox[1]
347+
)
348+
with pytest.raises(AssertionError):
349+
_ = crop(da, geom)
350+
351+
def test_crop_bbox_wrong_geom_type(self, synthetic_dataarray, crop_geometry_bbox):
352+
da = synthetic_dataarray
353+
geom = list(crop_geometry_bbox)
354+
with pytest.raises(ValueError):
355+
_ = crop(da, geom)

0 commit comments

Comments
 (0)