Skip to content

Commit

Permalink
Add filter function to GridObject (TopoToolbox#107)
Browse files Browse the repository at this point in the history
* Add filter function

* Fix: add scipy to requirements

* Fix: add scipy to build dependencies

* Fix: correct std output
  • Loading branch information
Teschl authored Dec 10, 2024
1 parent 71bd51a commit df1c307
Showing 1 changed file with 80 additions and 5 deletions.
85 changes: 80 additions & 5 deletions src/topotoolbox/grid_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import convolve

from scipy.ndimage import convolve, median_filter, generic_filter
from scipy.signal import wiener
from rasterio import CRS
from rasterio.warp import reproject
from rasterio.enums import Resampling
Expand Down Expand Up @@ -301,8 +303,81 @@ def excesstopography(

return result

def gradient8(self, unit: str = 'tangent',
multiprocessing: bool = True) -> 'GridObject':
def filter(self, method: str = 'mean', kernelsize: int = 3) -> 'GridObject':
"""The function filter is a wrapper around various image filtering
algorithms. Only filters with rectangular kernels of uneven
dimensions are supported.
Parameters
----------
method : str, optional
Which method will be used to filter the DEM: ['mean', 'average',
'median', 'sobel', 'scharr', 'wiener', 'std'], by default 'mean'
kernelsize : int, optional
Size of the kernel that will be applied. Note that ['sobel',
'scharr'] require that the kernelsize is 3, by default 3
Returns
-------
GridObject
The filtered DEM as a GridObject.
Raises
------
ValueError
If the kernelsize does not match the requirements of this function
or the selected method is not implemented in the function.
"""

valid_methods = ['mean', 'average', 'median',
'sobel', 'scharr', 'wiener', 'std']

if method in ['mean', 'average']:
kernel = np.ones((kernelsize, kernelsize)) / kernelsize**2
filtered = convolve(self.z, kernel, mode='nearest')

elif method in ['median']:
filtered = median_filter(self.z, size=kernelsize, mode='reflect')

elif method in ['sobel', 'scharr']:
if kernelsize != 3:
arr = f"The method '{method}' only works with a 3x3 kernel'."
raise ValueError(arr) from None

if method == 'sobel':
ky = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]])
else: # 'scharr'
ky = np.array([[3, 10, 3], [0, 0, 0], [-3, - 10, - 3]])

kx = ky.T
filtered = np.hypot(
convolve(self.z, ky, mode='nearest'),
convolve(self.z, kx, mode='nearest'))

elif method in ['wiener']:
filtered = wiener(self.z, mysize=kernelsize)

elif method in ['std']:
# This solution is based on this thread:
# https://stackoverflow.com/questions/19518827/what-is-the-python
# -equivalent-for-matlabs-stdfilt-function
filtered = generic_filter(self.z, np.std, size=kernelsize)
factor = np.sqrt(kernelsize**2 / (kernelsize**2 - 1))
np.multiply(filtered, factor, out=filtered)

else:
err = (f"Argument 'method={method}' has to be"
f"one of {valid_methods}.")
raise ValueError(err) from None

# Keep NaNs like they are in self.z
filtered[np.isnan(self.z)] = np.nan

result = copy.copy(self)
result.z = filtered
return result

def gradient8(self, unit: str = 'tangent', multiprocessing: bool = True):
"""
Compute the gradient of a digital elevation model (DEM) using an
8-direction algorithm.
Expand Down Expand Up @@ -352,14 +427,14 @@ def gradient8(self, unit: str = 'tangent',
def curvature(self, ctype='profc', meanfilt=False) -> 'GridObject':
"""curvature returns the second numerical derivative (curvature) of a
digital elevation model. By default, curvature returns the profile
curvature (profc).
curvature (profc).
Parameters
----------
ctype : str, optional
What type of curvature will be computed, by default 'profc'
- 'profc' : profile curvature [m^(-1)],
- 'planc' : planform curvature [m^(-1))],
- 'planc' : planform curvature [m^(-1))],
- 'tangc' : tangential curvature [m^(-1)],
- 'meanc' : mean curvature [m^(-1)],
- 'total' : total curvature [m^(-2)]
Expand Down

0 comments on commit df1c307

Please sign in to comment.