Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move geometry tasks #32

Merged
merged 2 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
2 changes: 2 additions & 0 deletions extra-tasks/superpixel/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
eo-learn[GEOMETRY]==1.5.0 # last tested version
scikit-image>=0.15
157 changes: 157 additions & 0 deletions extra-tasks/superpixel/superpixel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""
Module for super-pixel segmentation

Copyright (c) 2017- Sinergise and contributors
For the full list of contributors, see https://github.com/sentinel-hub/eo-learn/blob/master/CREDITS.md.

This source code is licensed under the MIT license, see the LICENSE file in the root directory of this source tree.
"""
from __future__ import annotations

import logging
import warnings
from typing import Any, Callable

import numpy as np
import skimage.segmentation

from eolearn.core import EOPatch, EOTask, FeatureType
from eolearn.core.exceptions import EODeprecationWarning, EORuntimeWarning
from eolearn.core.types import Feature
from sentinelhub.exceptions import deprecated_class

LOGGER = logging.getLogger(__name__)


class SuperpixelSegmentationTask(EOTask):
"""Super-pixel segmentation task

Given a raster feature it will segment data into super-pixels. Representation of super-pixels will be returned as
a mask timeless feature where all pixels with the same value belong to one super-pixel

Examples of `segmentation_object` values:
- `skimage.segmentation.felzenszwalb` (the defalt)
- `skimage.segmentation.slic`
"""

def __init__(
self,
feature: Feature,
superpixel_feature: Feature,
*,
segmentation_object: Callable = skimage.segmentation.felzenszwalb,
**segmentation_params: Any,
):
"""
:param feature: Raster feature which will be used in segmentation
:param superpixel_feature: A new mask timeless feature to hold super-pixel mask
:param segmentation_object: A function (object) which performs superpixel segmentation, by default that is
`skimage.segmentation.felzenszwalb`
:param segmentation_params: Additional parameters which will be passed to segmentation_object function
"""
self.feature = self.parse_feature(feature, allowed_feature_types=lambda fty: fty.is_spatial())
self.superpixel_feature = self.parse_feature(
superpixel_feature, allowed_feature_types={FeatureType.MASK_TIMELESS}
)
self.segmentation_object = segmentation_object
self.segmentation_params = segmentation_params

def _create_superpixel_mask(self, data: np.ndarray) -> np.ndarray:
"""Method which performs the segmentation"""
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=RuntimeWarning)
return self.segmentation_object(data, **self.segmentation_params)

def execute(self, eopatch: EOPatch) -> EOPatch:
"""Main execute method"""
data = eopatch[self.feature]

if np.isnan(data).any():
warnings.warn( # noqa: B028
"There are NaN values in given data, super-pixel segmentation might produce bad results",
EORuntimeWarning,
)

if self.feature[0].is_temporal():
data = np.moveaxis(data, 0, 2)
data = data.reshape((data.shape[0], data.shape[1], data.shape[2] * data.shape[3]))

superpixel_mask = np.atleast_3d(self._create_superpixel_mask(data))

eopatch[self.superpixel_feature] = superpixel_mask

return eopatch


@deprecated_class(
EODeprecationWarning,
"Use `SuperpixelSegmentationTask` with `segmentation_object=skimage.segmentation.felzenszwalb`.",
)
class FelzenszwalbSegmentationTask(SuperpixelSegmentationTask):
"""Super-pixel segmentation which uses Felzenszwalb's method of segmentation

Uses segmentation function documented at:
https://scikit-image.org/docs/dev/api/skimage.segmentation.html#skimage.segmentation.felzenszwalb
"""

def __init__(self, feature: Feature, superpixel_feature: Feature, **kwargs: Any):
"""Arguments are passed to `SuperpixelSegmentationTask` task"""
super().__init__(feature, superpixel_feature, segmentation_object=skimage.segmentation.felzenszwalb, **kwargs)


@deprecated_class(
EODeprecationWarning,
"Use `SuperpixelSegmentationTask` with `segmentation_object=skimage.segmentation.slic` and `start_label=0`.",
)
class SlicSegmentationTask(SuperpixelSegmentationTask):
"""Super-pixel segmentation which uses SLIC method of segmentation

Uses segmentation function documented at:
https://scikit-image.org/docs/dev/api/skimage.segmentation.html#skimage.segmentation.slic
"""

def __init__(self, feature: Feature, superpixel_feature: Feature, **kwargs: Any):
"""Arguments are passed to `SuperpixelSegmentationTask` task"""
super().__init__(
feature, superpixel_feature, segmentation_object=skimage.segmentation.slic, start_label=0, **kwargs
)

def _create_superpixel_mask(self, data: np.ndarray) -> np.ndarray:
"""Method which performs the segmentation"""
if np.issubdtype(data.dtype, np.floating) and data.dtype != np.float64:
data = data.astype(np.float64)
return super()._create_superpixel_mask(data)


class MarkSegmentationBoundariesTask(EOTask):
"""Takes super-pixel segmentation mask and creates a new mask where boundaries of super-pixels are marked

The result is a binary mask with values 0 and 1 and dtype `numpy.uint8`

Uses `mark_boundaries` function documented at:
https://scikit-image.org/docs/dev/api/skimage.segmentation.html#skimage.segmentation.mark_boundaries
"""

def __init__(self, feature: Feature, new_feature: Feature, **params: Any):
"""
:param feature: Input feature - super-pixel mask
:param new_feature: Output feature - a new feature where new mask with boundaries will be put
:param params: Additional parameters which will be passed to `mark_boundaries`. Supported parameters are `mode`
and `background_label`
"""
self.feature = self.parse_feature(feature, allowed_feature_types={FeatureType.MASK_TIMELESS})
self.new_feature = self.parse_feature(new_feature, allowed_feature_types={FeatureType.MASK_TIMELESS})

self.params = params

def execute(self, eopatch: EOPatch) -> EOPatch:
"""Execute method"""
segmentation_mask = eopatch[self.feature][..., 0]

bounds_mask = skimage.segmentation.mark_boundaries(
np.zeros(segmentation_mask.shape[:2], dtype=np.uint8), segmentation_mask, **self.params
)

bounds_mask = bounds_mask[..., :1].astype(np.uint8)
eopatch[self.new_feature[0]][self.new_feature[1]] = bounds_mask
return eopatch
79 changes: 79 additions & 0 deletions extra-tasks/superpixel/test_superpixel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Copyright (c) 2017- Sinergise and contributors.
For the full list of contributors, see https://github.com/sentinel-hub/eo-learn/blob/master/CREDITS.md.

This source code is licensed under the MIT license, see the LICENSE file in the root directory of this source tree.
"""
from __future__ import annotations

import os

import numpy as np
import pytest
import skimage.segmentation
from superpixel import SuperpixelSegmentationTask

from eolearn.core import EOPatch, FeatureType
from sentinelhub.testing_utils import assert_statistics_match

SUPERPIXEL_FEATURE = FeatureType.MASK_TIMELESS, "SP_FEATURE"


@pytest.fixture(name="test_eopatch")
def example_eopatch_fixture():
path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "_test_eopatch")
return EOPatch.load(path, lazy_loading=True)


@pytest.mark.parametrize(
("task", "expected_statistics"),
[
(
SuperpixelSegmentationTask(
(FeatureType.DATA, "BANDS-S2-L1C"), SUPERPIXEL_FEATURE, scale=100, sigma=0.5, min_size=100
),
{"exp_dtype": np.int64, "exp_min": 0, "exp_max": 25, "exp_mean": 10.6809, "exp_median": 11},
),
(
SuperpixelSegmentationTask(
(FeatureType.DATA_TIMELESS, "MAX_NDVI"), SUPERPIXEL_FEATURE, scale=21, sigma=1.0, min_size=52
),
{"exp_dtype": np.int64, "exp_min": 0, "exp_max": 22, "exp_mean": 8.5302, "exp_median": 7},
),
(
SuperpixelSegmentationTask((FeatureType.MASK, "CLM"), SUPERPIXEL_FEATURE, scale=1, sigma=0, min_size=15),
{"exp_dtype": np.int64, "exp_min": 0, "exp_max": 171, "exp_mean": 86.46267, "exp_median": 90},
),
(
SuperpixelSegmentationTask(
(FeatureType.DATA, "CLP"),
SUPERPIXEL_FEATURE,
segmentation_object=skimage.segmentation.slic,
start_label=0,
n_segments=55,
compactness=25.0,
max_num_iter=20,
sigma=0.8,
),
{"exp_dtype": np.int64, "exp_min": 0, "exp_max": 48, "exp_mean": 24.6072, "exp_median": 25},
),
(
SuperpixelSegmentationTask(
(FeatureType.MASK_TIMELESS, "RANDOM_UINT8"),
SUPERPIXEL_FEATURE,
segmentation_object=skimage.segmentation.slic,
start_label=0,
n_segments=231,
compactness=15.0,
max_num_iter=7,
sigma=0.2,
),
{"exp_dtype": np.int64, "exp_min": 0, "exp_max": 195, "exp_mean": 100.1844, "exp_median": 101},
),
],
)
def test_superpixel(test_eopatch, task, expected_statistics):
task.execute(test_eopatch)
result = test_eopatch[SUPERPIXEL_FEATURE]

assert_statistics_match(result, **expected_statistics, abs_delta=1e-4)
Loading