Skip to content
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ In the GIS world, rasters are used for representing continuous phenomena (e.g. e
| [Dilate](xrspatial/morphology.py) | Morphological dilation (local maximum over structuring element) | Standard (morphology) | ✅️ | ✅️ | ✅️ | ✅️ |
| [Opening](xrspatial/morphology.py) | Erosion then dilation (removes small bright features) | Standard (morphology) | ✅️ | ✅️ | ✅️ | ✅️ |
| [Closing](xrspatial/morphology.py) | Dilation then erosion (fills small dark gaps) | Standard (morphology) | ✅️ | ✅️ | ✅️ | ✅️ |
| [Gradient](xrspatial/morphology.py) | Dilation minus erosion (edge detection) | Standard (morphology) | ✅️ | ✅️ | ✅️ | ✅️ |
| [White Top-hat](xrspatial/morphology.py) | Original minus opening (isolate bright features) | Standard (morphology) | ✅️ | ✅️ | ✅️ | ✅️ |
| [Black Top-hat](xrspatial/morphology.py) | Closing minus original (isolate dark features) | Standard (morphology) | ✅️ | ✅️ | ✅️ | ✅️ |

-------

Expand Down
21 changes: 21 additions & 0 deletions docs/source/reference/morphology.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ Closing

xrspatial.morphology.morph_closing

Gradient
========
.. autosummary::
:toctree: _autosummary

xrspatial.morphology.morph_gradient

White top-hat
=============
.. autosummary::
:toctree: _autosummary

xrspatial.morphology.morph_white_tophat

Black top-hat
=============
.. autosummary::
:toctree: _autosummary

xrspatial.morphology.morph_black_tophat

Kernel Construction
===================
.. autosummary::
Expand Down
59 changes: 21 additions & 38 deletions examples/user_guide/17_Morphological_Operators.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,12 @@
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Xarray-Spatial Morphology: Erosion, dilation, opening, and closing\n",
"\n",
"Morphological operators filter rasters by sliding a structuring element (kernel) across the surface and picking the local minimum or maximum at each cell. They show up everywhere from cleaning noisy classification masks to smoothing elevation surfaces before further analysis. This notebook walks through the four operations in `xrspatial.morphology` on both continuous terrain and binary masks."
]
"source": "# Xarray-Spatial Morphology\n\nMorphological operators filter rasters by sliding a structuring element (kernel) across the surface and picking the local minimum or maximum at each cell. They show up everywhere from cleaning noisy classification masks to smoothing elevation surfaces before further analysis. This notebook walks through the seven operations in `xrspatial.morphology` on both continuous terrain and binary masks."
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### What you'll build\n",
"\n",
"1. Generate synthetic terrain and a hillshade base layer\n",
"2. Apply erosion and dilation to see how local min/max reshape the surface\n",
"3. Use opening and closing to selectively remove noise\n",
"4. Clean up a noisy binary classification mask\n",
"5. Compare square and circular structuring elements\n",
"\n",
"![Morphological operators preview](images/morphological_operators_preview.png)\n",
"\n",
"[Erosion and dilation](#Erosion-and-dilation) · [Opening and closing](#Opening-and-closing) · [Binary mask cleanup](#Binary-mask-cleanup) · [Circular structuring element](#Circular-structuring-element)"
]
"source": "### What you'll build\n\n1. Generate synthetic terrain and a hillshade base layer\n2. Apply erosion and dilation to see how local min/max reshape the surface\n3. Use opening and closing to selectively remove noise\n4. Clean up a noisy binary classification mask\n5. Compare square and circular structuring elements\n6. Use gradient, white top-hat, and black top-hat to extract features\n\n![Morphological operators preview](images/morphological_operators_preview.png)\n\n[Erosion and dilation](#Erosion-and-dilation) · [Opening and closing](#Opening-and-closing) · [Binary mask cleanup](#Binary-mask-cleanup) · [Circular structuring element](#Circular-structuring-element) · [Gradient, white top-hat, black top-hat](#Gradient,-white-top-hat,-and-black-top-hat)"
},
{
"cell_type": "markdown",
Expand All @@ -45,25 +29,7 @@
}
},
"outputs": [],
"source": [
"%matplotlib inline\n",
"import numpy as np\n",
"import pandas as pd\n",
"import xarray as xr\n",
"\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.colors import ListedColormap\n",
"from matplotlib.patches import Patch\n",
"\n",
"import xrspatial\n",
"from xrspatial.morphology import (\n",
" _circle_kernel,\n",
" morph_closing,\n",
" morph_dilate,\n",
" morph_erode,\n",
" morph_opening,\n",
")"
]
"source": "%matplotlib inline\nimport numpy as np\nimport pandas as pd\nimport xarray as xr\n\nimport matplotlib.pyplot as plt\nfrom matplotlib.colors import ListedColormap\nfrom matplotlib.patches import Patch\n\nimport xrspatial\nfrom xrspatial.morphology import (\n _circle_kernel,\n morph_black_tophat,\n morph_closing,\n morph_dilate,\n morph_erode,\n morph_gradient,\n morph_opening,\n morph_white_tophat,\n)"
},
{
"cell_type": "markdown",
Expand Down Expand Up @@ -488,6 +454,23 @@
"The square kernel reaches farther into the diagonal corners than the circular one, so it produces a slightly deeper erosion along ridges that run diagonally. The difference map and cross-section show that the disagreement concentrates on steep slopes where the extra corner pixels pull the minimum lower."
]
},
{
"cell_type": "markdown",
"source": "The gradient lights up wherever the terrain has steep transitions, making it a useful edge detector. The white top-hat pulls out peaks and ridges that are narrower than the 15x15 kernel, while the black top-hat does the same for small valleys and depressions. The bottom-right panel (white minus black) is positive on peaks and negative in valleys, giving a signed feature-size map.\n\nAll three results are non-negative by construction. The gradient equals the sum of the two top-hats (dilate - erode = (orig - opening) + (closing - orig) when opening <= orig <= closing), which you can verify in the cross-section.",
"metadata": {}
},
{
"cell_type": "code",
"source": "### References\n\n- [Erosion (morphology)](https://en.wikipedia.org/wiki/Erosion_(morphology)), Wikipedia\n- [Dilation (morphology)](https://en.wikipedia.org/wiki/Dilation_(morphology)), Wikipedia\n- [Opening (morphology)](https://en.wikipedia.org/wiki/Opening_(morphology)), Wikipedia\n- [Closing (morphology)](https://en.wikipedia.org/wiki/Closing_(morphology)), Wikipedia\n- [Morphological gradient](https://en.wikipedia.org/wiki/Morphological_gradient), Wikipedia\n- [Top-hat transform](https://en.wikipedia.org/wiki/Top-hat_transform), Wikipedia\n- [Mathematical morphology](https://en.wikipedia.org/wiki/Mathematical_morphology), Wikipedia\n- [Structuring element](https://en.wikipedia.org/wiki/Structuring_element), Wikipedia",
"metadata": {},
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"source": "## Gradient, white top-hat, and black top-hat\n\nThese three operations are derived from the primitives above:\n\n| Operation | Formula | What it extracts |\n|-----------|---------|-----------------|\n| **Gradient** | dilate - erode | Edges and transitions |\n| **White top-hat** | original - opening | Bright features smaller than the kernel |\n| **Black top-hat** | closing - original | Dark features smaller than the kernel |\n\nEach is a single subtraction of two morphological results, so all four backends are supported automatically.",
"metadata": {}
},
{
"cell_type": "code",
"execution_count": null,
Expand Down Expand Up @@ -567,4 +550,4 @@
},
"nbformat": 4,
"nbformat_minor": 4
}
}
3 changes: 3 additions & 0 deletions xrspatial/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@
from xrspatial.hydro import flow_path_d8, flow_path_dinf, flow_path_mfd # noqa
from xrspatial.focal import mean # noqa
from xrspatial.glcm import glcm_texture # noqa
from xrspatial.morphology import morph_black_tophat # noqa
from xrspatial.morphology import morph_closing # noqa
from xrspatial.morphology import morph_dilate # noqa
from xrspatial.morphology import morph_erode # noqa
from xrspatial.morphology import morph_gradient # noqa
from xrspatial.morphology import morph_opening # noqa
from xrspatial.morphology import morph_white_tophat # noqa
from xrspatial.hydro import hand # noqa: unified wrapper
from xrspatial.hydro import hand_d8, hand_dinf, hand_mfd # noqa
from xrspatial.hillshade import hillshade # noqa
Expand Down
115 changes: 113 additions & 2 deletions xrspatial/morphology.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Morphological raster operators: erode, dilate, opening, closing.
"""Morphological raster operators.

Applies grayscale morphological operations using a structuring element
(kernel) over a 2D raster. Erosion computes the local minimum and
dilation the local maximum within the kernel footprint. Opening
(erosion then dilation) removes small bright features; closing
(dilation then erosion) fills small dark gaps.
(dilation then erosion) fills small dark gaps. Gradient, white
top-hat, and black top-hat are derived by subtracting pairs of the
above.

Supports all four backends: numpy, cupy, dask+numpy, dask+cupy.
"""
Expand Down Expand Up @@ -560,3 +562,112 @@ def morph_closing(agg, kernel=None, boundary='nan', name='closing'):
agg, kernel, boundary, name,
_closing_numpy, _closing_cupy, _closing_dask_numpy, _closing_dask_cupy,
)


@supports_dataset
def morph_gradient(agg, kernel=None, boundary='nan', name='gradient'):
"""Morphological gradient: dilation minus erosion.

Highlights edges and transitions in a 2D raster. The result is
always non-negative for non-NaN cells.

Parameters
----------
agg : xarray.DataArray
2D raster of numeric values.
kernel : numpy.ndarray or None
2D structuring element with odd dimensions. Defaults to a
3x3 square.
boundary : str, default ``'nan'``
Edge handling: ``'nan'``, ``'nearest'``, ``'reflect'``, or
``'wrap'``.
name : str, default ``'gradient'``
Name for the output DataArray.

Returns
-------
xarray.DataArray
Gradient raster (dilate - erode).

Examples
--------
>>> from xrspatial.morphology import morph_gradient
>>> edges = morph_gradient(elevation)
"""
dilated = morph_dilate(agg, kernel=kernel, boundary=boundary)
eroded = morph_erode(agg, kernel=kernel, boundary=boundary)
result = dilated - eroded
result.name = name
return result


@supports_dataset
def morph_white_tophat(agg, kernel=None, boundary='nan', name='white_tophat'):
"""White top-hat: original minus opening.

Isolates bright features that are smaller than the structuring
element.

Parameters
----------
agg : xarray.DataArray
2D raster of numeric values.
kernel : numpy.ndarray or None
2D structuring element with odd dimensions. Defaults to a
3x3 square.
boundary : str, default ``'nan'``
Edge handling: ``'nan'``, ``'nearest'``, ``'reflect'``, or
``'wrap'``.
name : str, default ``'white_tophat'``
Name for the output DataArray.

Returns
-------
xarray.DataArray
White top-hat raster (original - opening).

Examples
--------
>>> from xrspatial.morphology import morph_white_tophat
>>> bright = morph_white_tophat(elevation, kernel=circle_kernel)
"""
opened = morph_opening(agg, kernel=kernel, boundary=boundary)
result = agg - opened
result.name = name
return result


@supports_dataset
def morph_black_tophat(agg, kernel=None, boundary='nan', name='black_tophat'):
"""Black top-hat: closing minus original.

Isolates dark features that are smaller than the structuring
element.

Parameters
----------
agg : xarray.DataArray
2D raster of numeric values.
kernel : numpy.ndarray or None
2D structuring element with odd dimensions. Defaults to a
3x3 square.
boundary : str, default ``'nan'``
Edge handling: ``'nan'``, ``'nearest'``, ``'reflect'``, or
``'wrap'``.
name : str, default ``'black_tophat'``
Name for the output DataArray.

Returns
-------
xarray.DataArray
Black top-hat raster (closing - original).

Examples
--------
>>> from xrspatial.morphology import morph_black_tophat
>>> dark = morph_black_tophat(elevation, kernel=circle_kernel)
"""
closed = morph_closing(agg, kernel=kernel, boundary=boundary)
result = closed - agg
result.name = name
return result
Loading
Loading