Filters#

GeoUtils implements filtering capabilities for both rasters and point clouds .

All support scalable execution on large datasets and geospatial awareness to define filter distances and skip invalid values.

Filters are not limited to smoothing data or removing outliers (e.g. Gaussian or median filters). They can perform any computation on a window (for rasters) or neighborhood (for points), such as deriving a local derivative or a local autocorrelation index.

See also

To derive terrain attributes on elevation data, which are also a type of filter, see xDEM’s implementations based on peer-reviewed studies.

Raster filters#

ds.rst.filter() or Raster.filter()

A raster filter computes each output pixel value from nearby input pixels, returning a new raster with the same shape and metadata. Neighborhoods are defined with a window size and shape, and optionally support fractional cell coverage.

For example, apply a median filter in a 15 × 15 pixel window:

import geoutils as gu
filename_rast = gu.examples.get_path("exploradores_aster_dem")
ds = gu.open_raster(filename_rast)
# Filter the raster with a median filter of size 15
ds_filtered = ds.rst.filter(method="median", backend="scipy", window=15)

Hide the code for plotting the figure

f, ax = plt.subplots(1, 2)
ax[0].set_title("Original raster")
ds.rst.plot(ax=ax[0], cmap="terrain", cbar_title="Elevation (m)")
ax[1].set_title("Filtered raster")
ds_filtered.rst.plot(ax=ax[1], cmap="terrain", cbar_title="Elevation (m)")
_ = ax[1].set_yticklabels([])
plt.tight_layout()
_images/119cc98fe2a54e3c0b8d29bc165ef7197a37c4842f0d898c065115890ad889ea.png

Use window to set the window size in pixels (odd, three by default), sigma sets the Gaussian standard deviation in pixels. Filters use SciPy by default, use backend="numba" to use Numba.

Point filters#

points.pc.filter() or PointCloud.filter()

A point filter computes each output point value from nearby input values, returning a new point cloud with the same coordinates and metadata. Neighborhoods are defined by a radius in coordinate units and maximum closest number of points.

For example, to compute the mean in a 1000-m radius, using at most the closest 20 points:

# Sample the projected DEM, then average up to 20 measurements within 1000 m
points = ds.rst.subsample(1500, random_state=42)
smoothed_points = points.pc.filter(
    method="mean", backend="scipy", radius=1000, max_points=20
)

Hide the code for plotting the figure

# Compare the same points with a shared extent and color scale
vmin, vmax = points.pc.data.min(), points.pc.data.max()
f, ax = plt.subplots(1, 2)
ax[0].set_title("Original points")
points.pc.plot(
    ref=ds, ax=ax[0], cmap="terrain", cbar_title="Elevation (m)",
    markersize=5, vmin=vmin, vmax=vmax
)
ax[1].set_title("Filtered points")
smoothed_points.pc.plot(
    ref=ds, ax=ax[1], cmap="terrain", cbar_title="Elevation (m)",
    markersize=5, vmin=vmin, vmax=vmax
)
_ = ax[1].set_yticklabels([])
plt.tight_layout()
_images/50f96edb5dd655c72269a912a57aaa35ff7f32634a7a5299559e4a66f2e1ba2c.png

Use include_self=False to exclude the point original value at this coordinate, or min_points to require enough valid neighbors. Lower batch_size to reduce temporary memory for dense neighborhoods.

Available methods#

GeoUtils supports the following raster and point filters. See Reducers for illustrations of supported reducers.

See also

Refer to Operators and neighborhoods for more details on reducers and neighborhoods.

Method

Description

Raster

Point

count

Number of valid neighbors

✓

✓

sum

Sum of neighboring values

✓

✓

mean, average

Mean of neighboring values

✓

✓

min, minimum

Minimum value

✓

✓

max, maximum

Maximum value

✓

✓

range

Difference between maximum and minimum

✓

✓

rms, rmse

Root mean square of neighboring values

✓

✓

stdev, std, standard_deviation

Population standard deviation

✓

✓

median, med

Median of neighboring values

✓

✓

mode

Most frequent value

✓

✓

q1, q3

First or third quartile

✓

✓

quantile

Quantile at the supplied fraction

✓

✓

average_distance

Mean distance from neighbors to the target

✓

✓

average_distance_pts

Mean distance between pairs of neighbors

✓

✓

gaussian

Smooth with a Gaussian kernel

✓

—

distance

Mask outliers relative to a Gaussian-smoothed average

✓

—

Reducer

Custom calculation on each neighborhood

✓

✓

Callable

Custom function taking and returning a 2D NumPy array

✓

—

Customizing filters#

Nodata propagation#

Filters can customize how they handle missing values. By default, we ignore them, but they can also be propagated.

The following example covers both behaviors by applying a mean filter to a DEM with gaps:

See also

See Nodata propagation for more common nodata propagation rules across the package.

import numpy as np
from geoutils.operators.reducer import Mean

# Add a gap in a copy of the DEM detail
gap_dem = ds.rst.crop((630000, 4839000, 633000, 4842000))
gap_values = gap_dem.values.copy()
gap_values[45:55, 45:55] = np.nan
gap_dem = gap_dem.rst.copy(new_array=gap_values)

# Compare nodata rules, allowing originally missing centers to receive a value
ignored = gap_dem.rst.filter(
    Mean(), window=15, nodata_handling="ignore", preserve_nodata=False
)
propagated = gap_dem.rst.filter(
    Mean(), window=15, nodata_handling="propagate", preserve_nodata=False
)

Hide the code for plotting the figure

# Use the same color scale and show missing cells in gray
elevation_min, elevation_max = np.nanmin(gap_values), np.nanmax(gap_values)
cmap = plt.colormaps["terrain"].with_extremes(bad="lightgray")
f, ax = plt.subplots(1, 3, figsize=(9, 3.5))
for axis, raster, title in zip(
    ax, [gap_dem, ignored, propagated],
    ["Input with gaps", 'nodata_handling =\n"ignore"', 'nodata_handling =\n"propagate"']
):
    raster.rst.plot(
        ax=axis, title=title, cmap=cmap, vmin=elevation_min, vmax=elevation_max
    )
plt.tight_layout()
_images/86f114808f4521c76a1f71c08b775101754c6ca6c6d914964878e91aa130b7fe.png

Custom point reducers#

You can define a custom reducer to customize the calculation on neighboring values, while GeoUtils handles their selection and missing data.

Below, we show how define a custom trimmed mean, which discards the lowest and highest values before averaging. We apply it to a point cloud with a few added outliers to illustrate how it reduces their influence:

from geoutils.operators import LocalData, Reducer


class TrimmedMean(Reducer):
    """Custom trimmed mean reducer."""

    # We define the minimum values required, below that a NaN is returned
    minimum_inputs = 3

    # The custom reducer simply needs to implement "reduce", which receives a local data object
    def reduce(self, data: LocalData) -> float:
        # Remove the two extreme values
        values = data.values
        trimmed_sum = np.sum(values) - np.min(values) - np.max(values)
        return float(trimmed_sum / (values.size - 2))


# Add some outliers to the input raster
noisy_values = np.array(points.pc.data, dtype=float, copy=True)
noisy_values[::100] += 2000
noisy_points = points.pc.copy(new_array=noisy_values)
trimmed_points = noisy_points.pc.filter(
    TrimmedMean(), radius=1000, max_points=20, batch_size=256
)

Hide the code for plotting the figure

# Compare the noisy input and filtered values with the same color scale
vmin, vmax = noisy_values.min(), noisy_values.max()
f, ax = plt.subplots(1, 2)
ax[0].set_title("Points with outliers")
noisy_points.pc.plot(
    ref=ds, ax=ax[0], cmap="terrain", cbar_title="Elevation (m)",
    markersize=5, vmin=vmin, vmax=vmax
)
ax[1].set_title("Trimmed mean")
trimmed_points.pc.plot(
    ref=ds, ax=ax[1], cmap="terrain", cbar_title="Elevation (m)",
    markersize=5, vmin=vmin, vmax=vmax
)
_ = ax[1].set_yticklabels([])
plt.tight_layout()
_images/53ade693f044ea0ec2abe5c5b2f5adebf36d876ab8a3ea271681edd850427bd7.png

Custom raster functions#

Raster filters also accept custom functions operating on a NumPy array. Below, we derive terrain slope by combining the horizontal and vertical elevation gradients:

import numpy as np

# Convert the elevation gradient in each map direction to slope in degrees
pixel_width, pixel_height = ds.rst.res


def slope_filter(arr: np.ndarray) -> np.ndarray:
    """Calculate terrain slope in degrees from the elevation gradients."""
    gradient_y, gradient_x = np.gradient(arr, pixel_height, pixel_width)
    return np.degrees(np.arctan(np.hypot(gradient_x, gradient_y)))


ds_slope = ds.rst.filter(slope_filter, window=3)

Hide the code for plotting the figure

f, ax = plt.subplots(1, 2)
ax[0].set_title("Original raster")
ds.rst.plot(ax=ax[0], cmap="terrain", cbar_title="Elevation (m)")
ax[1].set_title("Terrain slope")
ds_slope.rst.plot(ax=ax[1], cmap="magma", cbar_title="Slope (°)", vmin=0, vmax=60)
_ = ax[1].set_yticklabels([])
plt.tight_layout()
_images/c0f51a74c9d39d1f90ecbb22d244b87308cc0f5621403f12badeb887282114b3.png