Skip to content

Commit 39de435

Browse files
authored
Release 1.7.1, Merge pull request #76 from sentinel-hub/develop
Release 1.7.1
2 parents 7155920 + ec2e12b commit 39de435

17 files changed

+268
-374
lines changed

.flake8

-8
This file was deleted.

.github/workflows/ci_action.yml

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
python-version:
6767
- "3.9"
6868
- "3.10"
69+
- "3.11"
6970
include:
7071
# A flag marks whether full or partial tests should be run
7172
# We don't run integration tests on pull requests from outside repos, because they don't have secrets

.pre-commit-config.yaml

+6-29
Original file line numberDiff line numberDiff line change
@@ -13,41 +13,18 @@ repos:
1313
- id: debug-statements
1414

1515
- repo: https://github.com/psf/black
16-
rev: 22.12.0
16+
rev: 23.1.0
1717
hooks:
1818
- id: black
1919
language_version: python3
2020

21-
- repo: https://github.com/pycqa/isort
22-
rev: 5.12.0
21+
- repo: https://github.com/charliermarsh/ruff-pre-commit
22+
rev: "v0.0.269"
2323
hooks:
24-
- id: isort
25-
name: isort (python)
26-
27-
- repo: https://github.com/PyCQA/autoflake
28-
rev: v2.0.0
29-
hooks:
30-
- id: autoflake
31-
args:
32-
[
33-
--remove-all-unused-imports,
34-
--in-place,
35-
--ignore-init-module-imports,
36-
]
37-
38-
- repo: https://github.com/pycqa/flake8
39-
rev: 6.0.0
40-
hooks:
41-
- id: flake8
42-
additional_dependencies:
43-
- flake8-bugbear
44-
- flake8-comprehensions
45-
- flake8-simplify
46-
- flake8-typing-imports
24+
- id: ruff
4725

4826
- repo: https://github.com/nbQA-dev/nbQA
49-
rev: 1.6.1
27+
rev: 1.6.3
5028
hooks:
5129
- id: nbqa-black
52-
- id: nbqa-isort
53-
- id: nbqa-flake8
30+
- id: nbqa-ruff

MANIFEST.in

-7
This file was deleted.

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The **s2cloudless** algorithm was part of an international collaborative effort
1919

2020
## Installation
2121

22-
The package requires a Python version >= 3.7. The package is available on
22+
The package requires a Python version >= 3.8. The package is available on
2323
the PyPI package manager and can be installed with
2424

2525
```

examples/sentinel2-cloud-detector-example.ipynb

+49-35
Large diffs are not rendered by default.

pyproject.toml

+137-19
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,142 @@
1+
[build-system]
2+
requires = ['hatchling']
3+
build-backend = 'hatchling.build'
4+
5+
[tool.hatch.version]
6+
path = 's2cloudless/__init__.py'
7+
8+
[tool.hatch.build.targets.sdist]
9+
include = ['/README.md', '/LICENSE.md', '/s2cloudless']
10+
11+
[project]
12+
name = "s2cloudless"
13+
dynamic = ["version"]
14+
description = "Sentinel Hub's cloud detector for Sentinel-2 imagery"
15+
readme = "README.md"
16+
requires-python = ">= 3.8"
17+
license = { file = "LICENSE.md" }
18+
authors = [
19+
{ name = "Sinergise EO research team", email = "[email protected]" },
20+
]
21+
classifiers = [
22+
"Development Status :: 5 - Production/Stable",
23+
"Intended Audience :: Education",
24+
"Intended Audience :: Science/Research",
25+
"Operating System :: MacOS",
26+
"Operating System :: Microsoft :: Windows",
27+
"Operating System :: Unix",
28+
"Programming Language :: Python",
29+
"Programming Language :: Python :: 3",
30+
"Programming Language :: Python :: 3.8",
31+
"Programming Language :: Python :: 3.9",
32+
"Programming Language :: Python :: 3.10",
33+
"Programming Language :: Python :: 3.11",
34+
"Topic :: Scientific/Engineering",
35+
]
36+
dependencies = [
37+
"lightgbm>=2.0.11",
38+
"numpy>=1.13.3",
39+
"opencv-python-headless",
40+
"sentinelhub>=3.9.0",
41+
"typing_extensions",
42+
]
43+
44+
[project.optional-dependencies]
45+
dev = [
46+
"build",
47+
"mypy",
48+
"pre-commit",
49+
"pylint>=2.14.0",
50+
"pytest>=3.0.0",
51+
"pytest-cov",
52+
"twine",
53+
]
54+
55+
[project.urls]
56+
Homepage = "https://github.com/sentinel-hub/sentinel2-cloud-detector"
57+
Issues = "https://github.com/sentinel-hub/sentinel2-cloud-detector/issues"
58+
Source = "https://github.com/sentinel-hub/sentinel2-cloud-detector"
59+
Forum = "https://forum.sentinel-hub.com"
60+
161
[tool.black]
262
line-length = 120
363
preview = true
464

5-
[tool.isort]
6-
profile = "black"
7-
known_first_party = "sentinelhub"
8-
known_absolute = "s2cloudless"
9-
sections = ["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","ABSOLUTE","LOCALFOLDER"]
10-
line_length = 120
65+
[tool.ruff]
66+
line-length = 120
67+
target-version = "py38"
68+
select = [
69+
"F", # pyflakes
70+
"E", # pycodestyle
71+
"W", # pycodestyle
72+
"C90", # mccabe
73+
"N", # naming
74+
"YTT", # flake-2020
75+
"B", # bugbear
76+
"A", # built-ins
77+
"COM", # commas
78+
"C4", # comprehensions
79+
"T10", # debugger statements
80+
"ISC", # implicit string concatenation
81+
"ICN", # import conventions
82+
"G", # logging format
83+
"PIE", # flake8-pie
84+
"T20", # print statements
85+
"PT", # pytest style
86+
"RET", # returns
87+
"SLF", # private member access
88+
"SIM", # simplifications
89+
"ARG", # unused arguments
90+
"PD", # pandas
91+
"PGH", # pygrep hooks (useless noqa comments, eval statements etc.)
92+
"FLY", # flynt
93+
"RUF", # ruff rules
94+
"NPY", # numpy
95+
"I", # isort
96+
"UP", # pyupgrade
97+
"FA", # checks where future import of annotations would make types nicer
98+
]
99+
fix = true
100+
fixable = [
101+
"I", # sort imports
102+
"F401", # remove redundant imports
103+
"UP007", # use new-style union type annotations
104+
"UP006", # use new-style built-in type annotations
105+
"UP037", # remove quotes around types when not necessary
106+
"FA100", # import future annotations where necessary (not autofixable ATM)
107+
]
108+
ignore = [
109+
"SIM108", # tries to aggresively inline `if`, not always readable
110+
"COM812", # trailing comma missing, fights with black
111+
"PD011", # suggests `.to_numpy` instead of `.values`, also does this for non-pandas objects...
112+
# potentially fixable
113+
"PT011", # complains for `pytest.raises(ValueError)` but we use it a lot
114+
"N803", # clashes with the default naming of model protocols
115+
]
116+
per-file-ignores = { "__init__.py" = ["F401"] }
117+
exclude = [".git", "__pycache__", "build", "dist"]
11118

12-
[tool.nbqa.addopts]
13-
flake8 = [
14-
"--extend-ignore=E402"
119+
120+
[tool.ruff.isort]
121+
section-order = [
122+
"future",
123+
"standard-library",
124+
"third-party",
125+
"our-packages",
126+
"first-party",
127+
"local-folder",
15128
]
129+
known-first-party = ["s2cloudless"]
130+
sections = { our-packages = ["sentinelhub"] }
131+
132+
[tool.nbqa.addopts]
133+
ruff = ["--extend-ignore=E402,T201,B015,B018,NPY002,UP,FA"]
134+
# E402 -> imports on top
135+
# T201 -> print found
136+
# B015 & B018 -> useless expression (used to show values in ipynb)
137+
# NPY002 -> use RNG instead of old numpy.random
138+
# UP -> suggestions for new-style classes (future import might confuse readers)
139+
# FA -> necessary future annotations import
16140

17141
[tool.pylint.format]
18142
max-line-length = 120
@@ -25,27 +149,21 @@ disable = [
25149
"unsubscriptable-object",
26150
"invalid-unary-operand-type",
27151
"unspecified-encoding",
28-
"unnecessary-ellipsis"
152+
"unnecessary-ellipsis",
29153
]
30154

31155
[tool.pylint.design]
32156
max-args = 10
33157
max-attributes = 20
34158

35159
[tool.pytest.ini_options]
36-
markers = [
37-
"sh_integration: marks integration tests with Sentinel Hub service"
38-
]
160+
markers = ["sh_integration: marks integration tests with Sentinel Hub service"]
39161

40162
[tool.coverage.run]
41-
source = [
42-
"s2cloudless"
43-
]
163+
source = ["s2cloudless"]
44164

45165
[tool.coverage.report]
46-
omit = [
47-
"models/*"
48-
]
166+
omit = ["models/*"]
49167

50168
[tool.mypy]
51169
follow_imports = "normal"

requirements-dev.txt

-7
This file was deleted.

requirements.txt

-6
This file was deleted.

s2cloudless/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
from .cloud_detector import S2PixelCloudDetector
44
from .pixel_classifier import PixelClassifier
5-
from .utils import download_bands_and_valid_data_mask, get_s2_evalscript, get_timestamps
5+
from .utils import download_bands_and_valid_data_mask
66

7-
__version__ = "1.7.0"
7+
__version__ = "1.7.1"

s2cloudless/cloud_detector.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Module for pixel-based classification on Sentinel-2 L1C imagery."""
2+
from __future__ import annotations
3+
24
import os
3-
from typing import Any, Optional
5+
from typing import Any
46

7+
import cv2
58
import numpy as np
69
from lightgbm import Booster
7-
from scipy.ndimage import convolve
8-
from skimage.morphology import dilation, disk
910

1011
from .pixel_classifier import PixelClassifier
11-
from .utils import MODEL_BAND_IDS
12+
from .utils import MODEL_BAND_IDS, cv2_disk
1213

1314
MODEL_FILENAME = "pixel_s2_cloud_detector_lightGBM_v0.1.txt"
1415

@@ -42,9 +43,9 @@ def __init__(
4243
self,
4344
threshold: float = 0.4,
4445
all_bands: bool = False,
45-
average_over: Optional[int] = 1,
46-
dilation_size: Optional[int] = 1,
47-
model_filename: Optional[str] = None,
46+
average_over: int | None = 1,
47+
dilation_size: int | None = 1,
48+
model_filename: str | None = None,
4849
):
4950
self.threshold = threshold
5051
self.all_bands = all_bands
@@ -56,13 +57,14 @@ def __init__(
5657
model_filename = os.path.join(package_dir, "models", MODEL_FILENAME)
5758
self.model_filename = model_filename
5859

59-
self._classifier: Optional[PixelClassifier] = None
60+
self._classifier: PixelClassifier | None = None
6061

6162
if average_over is not None and average_over > 0:
62-
self.conv_filter = disk(average_over) / np.sum(disk(average_over))
63+
disk = cv2_disk(average_over)
64+
self.conv_filter = disk / np.sum(disk)
6365

6466
if dilation_size is not None and dilation_size > 0:
65-
self.dilation_filter = disk(dilation_size)
67+
self.dilation_filter = cv2_disk(dilation_size)
6668

6769
@property
6870
def classifier(self) -> PixelClassifier:
@@ -118,11 +120,10 @@ def get_cloud_masks(self, data: np.ndarray, **kwargs: Any) -> np.ndarray:
118120
"""
119121
self._check_data_dimension(data, 4)
120122
cloud_probs = self.get_cloud_probability_maps(data, **kwargs)
121-
cloud_masks = self.get_mask_from_prob(cloud_probs)
122123

123-
return cloud_masks
124+
return self.get_mask_from_prob(cloud_probs)
124125

125-
def get_mask_from_prob(self, cloud_probs: np.ndarray, threshold: Optional[float] = None) -> np.ndarray:
126+
def get_mask_from_prob(self, cloud_probs: np.ndarray, threshold: float | None = None) -> np.ndarray:
126127
"""
127128
Returns cloud mask by applying convolution and dilation to cloud probabilities.
128129
@@ -135,14 +136,18 @@ def get_mask_from_prob(self, cloud_probs: np.ndarray, threshold: Optional[float]
135136

136137
if self.average_over:
137138
cloud_masks = np.asarray(
138-
[convolve(cloud_prob, self.conv_filter) > threshold for cloud_prob in cloud_probs], dtype=np.uint8
139+
[
140+
cv2.filter2D(cloud_prob, -1, self.conv_filter, borderType=cv2.BORDER_REFLECT) > threshold
141+
for cloud_prob in cloud_probs
142+
],
143+
dtype=np.uint8,
139144
)
140145
else:
141146
cloud_masks = (cloud_probs > threshold).astype(np.int8)
142147

143148
if self.dilation_size:
144149
cloud_masks = np.asarray(
145-
[dilation(cloud_mask, self.dilation_filter) for cloud_mask in cloud_masks], dtype=np.uint8
150+
[cv2.dilate(cloud_mask, self.dilation_filter) for cloud_mask in cloud_masks], dtype=np.uint8
146151
)
147152

148153
return cloud_masks

0 commit comments

Comments
 (0)