Skip to content

Commit a6596c0

Browse files
authored
Merge pull request #57 from ASFHyP3/develop
Release v0.5.0
2 parents 2ab7ad5 + f264201 commit a6596c0

14 files changed

Lines changed: 440 additions & 305 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,4 @@ tags
244244
# Data
245245
*.zarr.zip
246246
*tif*
247+
integration_test/

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ 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.5.0]
10+
11+
### Changed
12+
* Format and layout of chips to more closely match the TerraMesh dataset.
13+
914
## [0.4.0]
1015

1116
### Changed

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,38 @@ A package for satellite image AI data prep. This package "chips" data labels and
66
`SatChip` relies on a two-step process; chip your label train data inputs, then create corresponding chips for different remote sensing data sources.
77

88
### Step 1: Chip labels
9-
The `chiplabel` CLI tool takes a GDAL-compatible image, a collection date, and an optional output directory as input using the following format:
9+
The `chiplabel` CLI tool takes a GDAL-compatible image, a collection date, and an optional chip directory as input using the following format:
1010

1111
```bash
12-
chiplabel PATH/TO/LABELS.tif DATE(UTC FORMAT) --outdir OUTPUT_DIR
12+
chiplabel PATH/TO/LABELS.tif DATE(UTC FORMAT) --chipdir CHIP_DIR
1313
```
1414
For example:
1515
```bash
16-
chiplabel LA_damage_20250113_v0.tif 2024-01-01T01:01:01 --outdir chips
16+
chiplabel LA_damage_20250113_v0.tif 2024-01-01T01:01:01 --chipdir chips
1717
```
18-
This will produce an output zipped Zarr store label dataset with the name `{LABELS}.zarr.zip` in the specified output directory (`--outdir`). This file will be the input to the remote sensing data chipping step.
18+
This will produce an output zipped Zarr store label dataset with the name `{LABEL}_{SAMPLE}.zarr.zip` (see the (Tiling Schema)[#tiling_schema] section for details on the `SAMPLE` name) to the `LABEL` directory in the specified chip directory (`--chipdir`). This file will be the input to the remote sensing data chipping step.
1919

2020
For more information on usage see `chiplabel --help`
2121

2222
### Step 2: Chip remote sensing data
23-
The `chipdata` CLI tool takes a label zipped Zarr store, a dataset name, a date range and a set of optional parameters using the following format:
23+
The `chipdata` CLI tool takes a path to a directory containing chip labels, a dataset name, a date range and a set of optional parameters using the following format:
2424
```bash
25-
chipdata PATH/TO/LABELS.zarr.zip DATASET Ymd-Ymd \
25+
chipdata PATH/TO/LABEL DATASET Ymd-Ymd \
2626
--maxcloudpct MAX_CLOUD_PCT --strategy STRATEGY \
27-
--outdir OUTPUT_DIR --scratchdir SCRATCH_DIR
27+
--chipdir CHIPPUT_DIR --imagedir IMAGE_DIR
2828
```
2929
For example:
3030
```bash
31-
chipdata LA_damage_20250113_v0.zarr.zip S2L2A 20250112-20250212 --maxcloudpct 20 --outdir chips --scratchdir images
31+
chipdata LABEL S2L2A 20250112-20250212 --maxcloudpct 20 --chipdir CHIP_DIR --imagedir IMAGES
3232
```
33-
Similarly to step 1, this will produce an output zipped Zarr store that contains chipped data for your chosen dataset with the name `{LABELS}_{DATASET}.zarr.zip`. The arguments are as follows:
34-
- `PATH/TO/LABELS.zarr.zip`: the path to your training lables.
33+
Similarly to step 1, this will produce an output zipped Zarr store that contains chipped data for your chosen dataset with the name `{LABELS_{SAMPLE}_{DATASET}.zarr.zip`. The arguments are as follows:
34+
- `PATH/TO/LABEL`: the path to your training labels
3535
- `DATASET`: The satellite imagery dataset you would like to create labels for. See the list below for all current options.
3636
- `Ymd-Ymd`: The date range to select imagery from. For example, `20250112-20250212` selects imagery between January 12 and February 12, 2025.
3737
- `MAX_CLOUD_PCT`: For optical data, this optional parameter lets you set the maximum amount of cloud coverage allowed in a chip. Values between 0 and 100 are allowed. Cloud coverage is calculated on a per-chip basis. The default is 100 i.e., no limit.
3838
- `STRATEGY`: Lets you selected what data inside your date range will be used to create chips. Specifying `BEST` (the default) will create a chip for the image closest to the beginning of your date range that has at least 95% spatial coverage. Specifying `ALL` will create chips for all images within your date range that have at least 95% spatial coverage.
39-
- `OUTPUT_DIR`: Specifies the directory where the image chips will be saved. If not specified, this defaults to your current directory.
40-
- `SCRATCH_DIR`: Specifies the directory where the full-size satellite images will be downloaded to. If this argument is not provided, the images will be stored in a scratch directory that will be deleted when the `chipdata` call finishes.
39+
- `CHIP_DIR`: Specifies the directory where the image chips will be saved. If not specified, this defaults to your current directory.
40+
- `IMAGE_DIR`: Specifies the directory where the full-size satellite images will be downloaded to. If this argument is not provided, the images will be stored in the `IMAGES` directory within `CHIP_DIR`.
4141

4242
Currently supported datasets include:
4343
- `S2L2A`: Sentinel-2 L2A data sourced from the [Sentinel-2 AWS Open Data Archive](https://registry.opendata.aws/sentinel-2/)
@@ -65,9 +65,9 @@ For instance, the bottom-left subgrid of MajorTOM tile `434U_876L` is named `434
6565
## Viewing Chips
6666
Assessing chips after their creation can be challenging due to the large number of small images created. To address this issue, SatChip includes a `chipview` CLI tool that uses Matplotlib to quickly visualize the data included within the created zipped Zarr stores:
6767
```bash
68-
chipview PATH/TO/CHIPS.zarr.zip BAND --idx IDX
68+
chipview PATH/TO/CHIP.zarr.zip --band BAND
6969
```
70-
Where `PATH/TO/CHIPS.zarr.zip` is the path to the chip file (labels or image data), `BAND` is the name of the band you would like to view, and `IDX` is an optional integer index of which dataset you would like to initially view.
70+
Where `PATH/TO/CHIPS.zarr.zip` is the path to the chip file (labels or image data), and `BAND` is an OPTIONAL name of the band you would like to view. If no band is specified, an OPERA-style RGB decomposition will be shown for RTC data, and an RGB composite will be shown for optical data.
7171

7272
## License
7373
`SatChip` is licensed under the BSD-3-Clause open source license. See the LICENSE file for more details.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ chipview = "satchip.chip_view:main"
6363
[tool.pytest.ini_options]
6464
testpaths = ["tests"]
6565
script_launch_mode = "subprocess"
66+
addopts = '-ra -q -m "not integration"'
67+
markers = ["integration"]
6668

6769
[tool.setuptools]
6870
include-package-data = true

scripts/open_chips.py

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/satchip/chip_data.py

Lines changed: 64 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import argparse
2+
from collections import Counter
23
from datetime import datetime
34
from pathlib import Path
4-
from tempfile import TemporaryDirectory
55

66
import numpy as np
77
import xarray as xr
8+
from shapely.geometry import box
89
from tqdm import tqdm
910

10-
import satchip
1111
from satchip import utils
1212
from satchip.chip_hls import get_hls_data
1313
from satchip.chip_sentinel1rtc import get_rtc_paths_for_chips, get_s1rtc_chip_data
1414
from satchip.chip_sentinel2 import get_s2l2a_data
15-
from satchip.terra_mind_grid import TerraMindGrid
15+
from satchip.terra_mind_grid import TerraMindChip, TerraMindGrid
1616

1717

1818
def fill_missing_times(data_chip: xr.DataArray, times: np.ndarray) -> xr.DataArray:
@@ -31,65 +31,80 @@ def fill_missing_times(data_chip: xr.DataArray, times: np.ndarray) -> xr.DataArr
3131
return xr.concat([data_chip, missing_data], dim='time').sortby('time')
3232

3333

34+
def get_chip(label_path: Path) -> TerraMindChip:
35+
label_dataset = utils.load_chip(label_path)
36+
buffered = box(*label_dataset.bounds).buffer(0.1).bounds
37+
grid = TerraMindGrid([buffered[1], buffered[3]], [buffered[0], buffered[2]]) # type: ignore
38+
label_chip_name = label_dataset.sample.item()
39+
chip = [c for c in grid.terra_mind_chips if c.name == label_chip_name]
40+
assert len(chip) == 1, f'No TerraMind chip found for label {label_chip_name}'
41+
return chip[0]
42+
43+
3444
def chip_data(
35-
label_path: Path,
45+
chip: TerraMindChip,
46+
platform: str,
47+
opts: utils.ChipDataOpts,
48+
image_dir: Path,
49+
) -> xr.Dataset:
50+
if platform == 'S1RTC':
51+
rtc_paths = opts['local_hyp3_paths'][chip.name]
52+
chip_dataset = get_s1rtc_chip_data(chip, rtc_paths)
53+
elif platform == 'S2L2A':
54+
chip_dataset = get_s2l2a_data(chip, image_dir, opts=opts)
55+
elif platform == 'HLS':
56+
chip_dataset = get_hls_data(chip, image_dir, opts=opts)
57+
else:
58+
raise Exception(f'Unknown platform {platform}')
59+
60+
return chip_dataset
61+
62+
63+
def create_chips(
64+
label_paths: list[Path],
3665
platform: str,
3766
date_start: datetime,
3867
date_end: datetime,
3968
strategy: str,
4069
max_cloud_pct: int,
41-
output_dir: Path,
42-
scratch_dir: Path,
43-
) -> xr.Dataset:
44-
labels = utils.load_chip(label_path)
45-
date = labels.time.data[0].astype('M8[ms]').astype(datetime)
46-
bounds = labels.attrs['bounds']
47-
48-
grid = TerraMindGrid([bounds[1] - 1, bounds[3] + 1], [bounds[0] - 1, bounds[2] + 1]) # type: ignore
49-
terra_mind_chips = [c for c in grid.terra_mind_chips if c.name in list(labels.sample.data)]
70+
chip_dir: Path,
71+
image_dir: Path,
72+
) -> list[Path]:
73+
platform_dir = chip_dir / platform
74+
platform_dir.mkdir(parents=True, exist_ok=True)
5075

5176
opts: utils.ChipDataOpts = {'strategy': strategy, 'date_start': date_start, 'date_end': date_end}
5277
if platform in ['S2L2A', 'HLS']:
5378
opts['max_cloud_pct'] = max_cloud_pct
5479

80+
chips = [get_chip(p) for p in label_paths]
81+
chip_names = [c.name for c in chips]
82+
if len(chip_names) != len(set(chip_names)):
83+
duplicates = [name for name, count in Counter(chip_names).items() if count > 1]
84+
msg = f'Duplicate sample locations not supported. Duplicate chips: {", ".join(duplicates)}'
85+
raise NotImplementedError(msg)
86+
chip_paths = [
87+
platform_dir / (x.with_suffix('').with_suffix('').name + f'_{platform}.zarr.zip') for x in label_paths
88+
]
5589
if platform == 'S1RTC':
56-
rtc_paths_for_chips = get_rtc_paths_for_chips(terra_mind_chips, bounds, scratch_dir, opts)
57-
58-
data_chips = []
59-
for chip in tqdm(terra_mind_chips):
60-
if platform == 'S1RTC':
61-
rtc_paths = rtc_paths_for_chips[chip.name]
62-
chip_data = get_s1rtc_chip_data(chip, rtc_paths, scratch_dir, opts=opts)
63-
elif platform == 'S2L2A':
64-
chip_data = get_s2l2a_data(chip, scratch_dir, opts=opts)
65-
elif platform == 'HLS':
66-
chip_data = get_hls_data(chip, scratch_dir, opts=opts)
67-
else:
68-
raise Exception(f'Unknown platform {platform}')
69-
70-
data_chips.append(chip_data)
71-
72-
times = np.unique(np.concatenate([dc.time.data for dc in data_chips]))
73-
for i, data_chip in enumerate(data_chips):
74-
if len(data_chip.time) < len(times):
75-
data_chips[i] = fill_missing_times(data_chip, times)
76-
attrs = {'date_created': date.isoformat(), 'satchip_version': satchip.__version__, 'bounds': labels.attrs['bounds']}
77-
dataset = xr.Dataset(attrs=attrs)
78-
dataset['data'] = xr.combine_by_coords(data_chips, join='override')
79-
output_path = output_dir / (label_path.with_suffix('').with_suffix('').name + f'_{platform}.zarr.zip')
80-
utils.save_chip(dataset, output_path)
81-
return labels
90+
rtc_paths_for_chips = get_rtc_paths_for_chips(chips, image_dir, opts)
91+
opts['local_hyp3_paths'] = rtc_paths_for_chips
92+
93+
for chip, chip_path in tqdm(zip(chips, chip_paths), desc='Chipping labels'):
94+
dataset = chip_data(chip, platform, opts, image_dir)
95+
utils.save_chip(dataset, chip_path)
96+
return chip_paths
8297

8398

8499
def main() -> None:
85100
parser = argparse.ArgumentParser(description='Chip a label image')
86-
parser.add_argument('labelpath', type=Path, help='Path to the label image')
101+
parser.add_argument('labelpath', type=Path, help='Path to the label directory')
87102
parser.add_argument('platform', choices=['S2L2A', 'S1RTC', 'HLS'], type=str, help='Dataset to create chips for')
88103
parser.add_argument('daterange', type=str, help='Inclusive date range to search for data in the format Ymd-Ymd')
89104
parser.add_argument('--maxcloudpct', default=100, type=int, help='Maximum percent cloud cover for a data chip')
90-
parser.add_argument('--outdir', default='.', type=Path, help='Output directory for the chips')
105+
parser.add_argument('--chipdir', default='.', type=Path, help='Output directory for the chips')
91106
parser.add_argument(
92-
'--scratchdir', default=None, type=Path, help='Output directory for scratch files if you want to keep them'
107+
'--imagedir', default=None, type=Path, help='Output directory for image files. Defaults to chipdir/IMAGES'
93108
)
94109
parser.add_argument(
95110
'--strategy',
@@ -103,23 +118,15 @@ def main() -> None:
103118
assert 0 <= args.maxcloudpct <= 100, 'maxcloudpct must be between 0 and 100'
104119
date_start, date_end = [datetime.strptime(d, '%Y%m%d') for d in args.daterange.split('-')]
105120
assert date_start < date_end, 'start date must be before end date'
121+
label_paths = list(args.labelpath.glob('*.zarr.zip'))
122+
assert len(label_paths) > 0, f'No label files found in {args.labelpath}'
106123

107-
params = (
108-
args.labelpath,
109-
args.platform,
110-
date_start,
111-
date_end,
112-
args.strategy,
113-
args.maxcloudpct,
114-
args.outdir,
115-
)
124+
if args.imagedir is None:
125+
args.imagedir = args.chipdir / 'IMAGES'
116126

117-
if args.scratchdir is not None:
118-
chip_data(*params, args.scratchdir)
119-
else:
120-
with TemporaryDirectory() as tmp_dir:
121-
scratch_dir = Path(tmp_dir)
122-
chip_data(*params, scratch_dir)
127+
create_chips(
128+
label_paths, args.platform, date_start, date_end, args.strategy, args.maxcloudpct, args.chipdir, args.imagedir
129+
)
123130

124131

125132
if __name__ == '__main__':

0 commit comments

Comments
 (0)