Skip to content

Commit 6f141a6

Browse files
committed
retrained models, support for l2a, refactoring
1 parent bdf735d commit 6f141a6

14 files changed

+367
-110
lines changed

CHANGELOG.rst

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
Changelog
22
=========
33

4+
[1.0.0] (2024-11-XX)
5+
--------------------
6+
Added
7+
*******
8+
-support for L2A product level
9+
10+
Changed
11+
*******
12+
- retrained all models with new architecture and training data
13+
- removed backwards compatibility with old band naming scheme
14+
415
[0.2.2] (2024-10-21)
516
--------------------
617
Added

MANIFEST.in

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
include requirements.txt
22
include README.md
33
include LICENSE
4-
include ukis_csmask/model_4b.onnx
5-
include ukis_csmask/model_6b.onnx
4+
include ukis_csmask/model_4b_l1c.onnx
5+
include ukis_csmask/model_4b_l1c.json
6+
include ukis_csmask/model_4b_l2a.onnx
7+
include ukis_csmask/model_4b_l2a.json
8+
include ukis_csmask/model_6b_l1c.onnx
9+
include ukis_csmask/model_6b_l1c.json
10+
include ukis_csmask/model_6b_l2a.onnx
11+
include ukis_csmask/model_6b_l2a.json

README.md

+3-46
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://black.readthedocs.io/en/stable/)
99
[![DOI](https://zenodo.org/badge/328616234.svg)](https://zenodo.org/badge/latestdoi/328616234)
1010

11-
UKIS Cloud Shadow MASK (ukis-csmask) package masks clouds and cloud shadows in Sentinel-2, Landsat-9, Landsat-8, Landsat-7 and Landsat-5 images. Masking is performed with a pre-trained convolution neural network. It is fast and works directly on Level-1C data (no atmospheric correction required). Images just need to be in Top Of Atmosphere (TOA) reflectance and include at least the "blue", "green", "red" and "nir" spectral bands. Best performance (in terms of accuracy and speed) is achieved when images also include "swir16" and "swir22" spectral bands and are resampled to approximately 30 m spatial resolution.
11+
UKIS Cloud Shadow MASK (ukis-csmask) package masks clouds and cloud shadows in Sentinel-2, Landsat-9, Landsat-8, Landsat-7 and Landsat-5 images. Masking is performed with a pre-trained convolution neural network. It is fast and works with both Level-1C (no atmospheric correction) and Level-2A (atmospherically corrected) data. Images just need to be in reflectance and include at least the "blue", "green", "red" and "nir" spectral bands. Best performance (in terms of accuracy and speed) is achieved when images also include "swir16" and "swir22" spectral bands and are resampled to approximately 30 m spatial resolution.
1212

1313
This [publication](https://doi.org/10.1016/j.rse.2019.05.022) provides further insight into the underlying algorithm and compares it to the widely used [Fmask](http://www.pythonfmask.org/en/latest/) algorithm across a heterogeneous test dataset.
1414

@@ -23,8 +23,8 @@ If you use ukis-csmask in your work, please consider citing one of the above pub
2323

2424
![Examples](img/examples.png)
2525

26-
## Example (Sentinel 2)
27-
Here's an example on how to compute a cloud and cloud shadow mask from an image. Please note that here we use [ukis-pysat](https://github.com/dlr-eoc/ukis-pysat) for convencience image handling, but you can also work directly with [numpy](https://numpy.org/) arrays.
26+
## Example (Sentinel-2 Level-1C)
27+
Here's an example on how to compute a cloud and cloud shadow mask from an image. Please note that here we use [ukis-pysat](https://github.com/dlr-eoc/ukis-pysat) for convencience image handling, but you can also work directly with [numpy](https://numpy.org/) arrays. Further examples can be found [here](examples).
2828

2929
````python
3030
from ukis_csmask.mask import CSmask
@@ -64,49 +64,6 @@ csmask_csm.write_to_file("sentinel2_csm.tif", dtype="uint8", compress="PACKBITS"
6464
csmask_valid.write_to_file("sentinel2_valid.tif", dtype="uint8", compress="PACKBITS", kwargs={"nbits":2})
6565
````
6666

67-
## Example (Landsat 8)
68-
Here's a similar example based on Landsat 8.
69-
70-
````python
71-
import rasterio
72-
import numpy as np
73-
from ukis_csmask.mask import CSmask
74-
from ukis_pysat.raster import Image, Platform
75-
76-
# set Landsat 8 source path and prefix (example)
77-
data_path = "/your_data_path/"
78-
L8_file_prefix = "LC08_L1TP_191015_20210428_20210507_02_T1"
79-
80-
data_path = data_path+L8_file_prefix+"/"
81-
mtl_file = data_path+L8_file_prefix+"_MTL.txt"
82-
83-
# stack [B2:'Blue', B3:'Green', B4:'Red', B5:'NIR', B6:'SWIR1', B7:'SWIR2'] as numpy array
84-
L8_band_files = [data_path+L8_file_prefix+'_B'+ x + '.TIF' for x in [str(x+2) for x in range(6)]]
85-
86-
# >> adopted from https://gis.stackexchange.com/questions/223910/using-rasterio-or-gdal-to-stack-multiple-bands-without-using-subprocess-commands
87-
# read metadata of first file
88-
with rasterio.open(L8_band_files[0]) as src0:
89-
meta = src0.meta
90-
# update meta to reflect the number of layers
91-
meta.update(count = len(L8_band_files))
92-
# read each layer and append it to numpy array
93-
L8_bands = []
94-
for id, layer in enumerate(L8_band_files, start=1):
95-
with rasterio.open(layer) as src1:
96-
L8_bands.append(src1.read(1))
97-
L8_bands = np.stack(L8_bands,axis=2)
98-
# <<
99-
100-
img = Image(data=L8_bands, crs = meta['crs'], transform = meta['transform'], dimorder="last")
101-
102-
img.dn2toa(
103-
platform=Platform.Landsat8,
104-
mtl_file=mtl_file,
105-
wavelengths = ["blue", "green", "red", "nir", "swir16", "swir22"]
106-
)
107-
# >> proceed by analogy with Sentinel 2 example
108-
````
109-
11067
## Installation
11168
The easiest way to install ukis-csmask is through pip. To install ukis-csmask with [default CPU provider](https://onnxruntime.ai/docs/execution-providers/) run the following.
11269

examples/landsat_l2a.ipynb

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Segment clouds and cloud shadows in Landsat images (L2A)\n",
8+
"This notebook shows an example on how to use [ukis-csmask]() to segment clouds and cloud shadows in Level-2A images from Landsat-9, Landsat-8, Landsat-7 and Landsat-5 satellites. Images are acquired from [Planetary Computer]() and are prepared according to [ukis-csmask]() requirements.\n",
9+
"\n",
10+
"> NOTE: to run this notebook, we first need to install some additional dependencies for work with Planetary Computer\n",
11+
"```shell\n",
12+
"pip install planetary_computer rasterio rioxarray pystac-client odc-stac tqdm\n",
13+
"```"
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"id": "703f3744-902d-470b-a80f-9a8d3ea08dfa",
20+
"metadata": {},
21+
"outputs": [],
22+
"source": [
23+
"import numpy as np\n",
24+
"import planetary_computer as pc\n",
25+
"\n",
26+
"from odc.stac import load\n",
27+
"from pystac_client import Client\n",
28+
"from tqdm import tqdm\n",
29+
"from ukis_csmask.mask import CSmask"
30+
]
31+
},
32+
{
33+
"cell_type": "code",
34+
"execution_count": null,
35+
"id": "8ca03c78-1e24-479c-9786-a1b43206a08b",
36+
"metadata": {},
37+
"outputs": [],
38+
"source": [
39+
"stac_api_endpoint = \"https://planetarycomputer.microsoft.com/api/stac/v1\"\n",
40+
"collections = [\"landsat-c2-l2\"]\n",
41+
"# TODO: add ids for Landsat9, Landsat7, Landsat5\n",
42+
"ids = [\"LC08_L2SP_221081_20240508_02_T1\"]\n",
43+
"product_level = \"l2a\"\n",
44+
"band_order = [\"blue\", \"green\", \"red\", \"nir\", \"swir16\", \"swir22\"]"
45+
]
46+
},
47+
{
48+
"cell_type": "code",
49+
"execution_count": null,
50+
"id": "7b568942-84e6-4baf-b490-a213b3787f80",
51+
"metadata": {},
52+
"outputs": [],
53+
"source": [
54+
"# search catalog by scene id\n",
55+
"catalog = Client.open(stac_api_endpoint)\n",
56+
"search = catalog.search(collections=collections, ids=ids)\n",
57+
"items = [item for item in search.items()]\n",
58+
"items_cnt = len(items)\n",
59+
"print(f\"Search returned {items_cnt} item(s)\")"
60+
]
61+
},
62+
{
63+
"cell_type": "code",
64+
"execution_count": null,
65+
"id": "68eb9c30-06f7-409e-914d-21e00f45de99",
66+
"metadata": {},
67+
"outputs": [],
68+
"source": [
69+
"for item in tqdm(items, total=items_cnt, desc=\"Predict images\"):\n",
70+
" # near infrared band has different alias in landsat collections\n",
71+
" bands = [b.replace(\"nir\", \"nir08\") for b in band_order]\n",
72+
"\n",
73+
" # load and preprocess image\n",
74+
" arr = (\n",
75+
" load(\n",
76+
" items=[item],\n",
77+
" bands=bands,\n",
78+
" resolution=30,\n",
79+
" dtype=\"float32\",\n",
80+
" patch_url=pc.sign,\n",
81+
" )\n",
82+
" .to_dataarray()\n",
83+
" .squeeze()\n",
84+
" .drop_vars(\"time\")\n",
85+
" )\n",
86+
" arr = arr.rename({\"variable\": \"band\"})\n",
87+
"\n",
88+
" # use band-specific rescale factors to convert DN to reflectance\n",
89+
" for idx, band_name in enumerate(bands):\n",
90+
" band_info = item.assets[band_name].extra_fields[\"raster:bands\"][0]\n",
91+
" arr[idx, :, :] = arr.sel(band=str(band_name)).astype(np.float32) * band_info[\"scale\"]\n",
92+
" arr[idx, :, :] += band_info[\"offset\"]\n",
93+
" arr[idx, :, :] = arr[idx, :, :].clip(min=0.0, max=1.0)\n",
94+
"\n",
95+
" # compute cloud and cloud shadow mask\n",
96+
" csmask = CSmask(\n",
97+
" img=np.moveaxis(arr, 0, -1),\n",
98+
" band_order=band_order,\n",
99+
" product_level=product_level,\n",
100+
" nodata_value=0,\n",
101+
" invalid_buffer=4,\n",
102+
" intra_op_num_threads=0,\n",
103+
" inter_op_num_threads=0,\n",
104+
" providers=None,\n",
105+
" batch_size=1,\n",
106+
" )\n",
107+
"\n",
108+
" # access cloud and cloud shadow mask\n",
109+
" csmask_csm = csmask.csm\n",
110+
"\n",
111+
" # access valid mask\n",
112+
" csmask_valid = csmask.valid"
113+
]
114+
}
115+
],
116+
"metadata": {
117+
"kernelspec": {
118+
"display_name": "Python 3 (ipykernel)",
119+
"language": "python",
120+
"name": "python3"
121+
},
122+
"language_info": {
123+
"codemirror_mode": {
124+
"name": "ipython",
125+
"version": 3
126+
},
127+
"file_extension": ".py",
128+
"mimetype": "text/x-python",
129+
"name": "python",
130+
"nbconvert_exporter": "python",
131+
"pygments_lexer": "ipython3",
132+
"version": "3.11.10"
133+
}
134+
},
135+
"nbformat": 4,
136+
"nbformat_minor": 5
137+
}

src/ukis-csmask

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit bdf735dd55d4f5b5ce27caaed1836ac9327e30e1

tests/test_mask.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
@pytest.mark.parametrize(
1515
"img, band_order, nodata_value",
1616
[
17-
(np.empty((256, 256, 6), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"], None),
18-
(np.empty((256, 256, 8), np.float32), ["Green", "Red", "Blue", "NIR", "SWIR1", "SWIR2", "NIR2", "ETC"], None),
19-
(np.empty((256, 256, 6), np.float32), ["Green", "Red", "Blue", "NIR", "SWIR1", "SWIR2"], -666),
17+
(np.empty((256, 256, 6), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"], None),
18+
(np.empty((256, 256, 8), np.float32), ["Green", "Red", "Blue", "NIR", "SWIR16", "SWIR22", "NIR2", "ETC"], None),
19+
(np.empty((256, 256, 6), np.float32), ["Green", "Red", "Blue", "NIR", "SWIR16", "SWIR22"], -666),
2020
(np.empty((256, 256, 4), np.float32), ["Red", "Green", "Blue", "NIR"], 0),
21-
(np.empty((256, 256, 5), np.float32), ["Red", "Green", "Blue", "NIR", "SWIR2"], 0),
21+
(np.empty((256, 256, 5), np.float32), ["Red", "Green", "Blue", "NIR", "SWIR22"], 0),
2222
],
2323
)
2424
def test_csmask_init(img, band_order, nodata_value):
@@ -28,11 +28,11 @@ def test_csmask_init(img, band_order, nodata_value):
2828
@pytest.mark.parametrize(
2929
"img, band_order, nodata_value",
3030
[
31-
(3, ["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"], None),
32-
(np.empty((256, 256), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"], None),
33-
(np.empty((256, 256, 3), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"], None),
34-
(np.empty((256, 256, 6), np.uint8), ["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"], None),
35-
(np.empty((256, 256, 6), np.float32), ["Blue", "Green", "Yellow", "NIR", "SWIR1", "SWIR2"], None),
31+
(3, ["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"], None),
32+
(np.empty((256, 256), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"], None),
33+
(np.empty((256, 256, 3), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"], None),
34+
(np.empty((256, 256, 6), np.uint8), ["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"], None),
35+
(np.empty((256, 256, 6), np.float32), ["Blue", "Green", "Yellow", "NIR", "SWIR16", "SWIR22"], None),
3636
(np.empty((256, 256, 6), np.float32), None, None),
3737
],
3838
)
@@ -44,8 +44,8 @@ def test_csmask_init_raises(img, band_order, nodata_value):
4444
@pytest.mark.parametrize(
4545
"img, band_order, nodata_value",
4646
[
47-
(np.empty((128, 128, 6), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"], None),
48-
(np.empty((64, 64, 6), np.float32), ["Green", "Red", "Blue", "NIR", "SWIR1", "SWIR2"], -666),
47+
(np.empty((128, 128, 6), np.float32), ["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"], None),
48+
(np.empty((64, 64, 6), np.float32), ["Green", "Red", "Blue", "NIR", "SWIR16", "SWIR22"], -666),
4949
],
5050
)
5151
def test_csmask_init_warns(img, band_order, nodata_value):
@@ -64,7 +64,7 @@ def test_csmask_init_warns(img, band_order, nodata_value):
6464
],
6565
)
6666
def test_csmask_csm_6band(data):
67-
csmask = CSmask(img=data["img"], band_order=["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"])
67+
csmask = CSmask(img=data["img"], band_order=["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"])
6868
y_pred = csmask.csm
6969
y_true = reclassify(data["msk"], {"reclass_value_from": [0, 1, 2, 3, 4], "reclass_value_to": [2, 0, 0, 0, 1]})
7070
y_true = y_true.ravel()
@@ -84,7 +84,7 @@ def test_csmask_csm_6band(data):
8484
],
8585
)
8686
def test_csmask_valid_6band(data):
87-
csmask = CSmask(img=data["img"], band_order=["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2"])
87+
csmask = CSmask(img=data["img"], band_order=["Blue", "Green", "Red", "NIR", "SWIR16", "SWIR22"])
8888
y_pred = csmask.valid
8989
y_true = reclassify(data["msk"], {"reclass_value_from": [0, 1, 2, 3, 4], "reclass_value_to": [0, 1, 1, 1, 0]})
9090
y_true_inverted = ~y_true.astype(bool)
File renamed without changes.
File renamed without changes.

ukis_csmask/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.2"
1+
__version__ = "1.0.0"

0 commit comments

Comments
 (0)