Best library for calculating slope in an elevation raster?

I am trying to calculate the slope from a large number of DEM rasters. It seems like Geomorphometry.jl has an implementation of the Horn (1981) method, but are there other library options?

I have some custom code using Stencils.jl, which will be much faster than Geomorphometry. But I never got around to testing it or putting it into Geomorphometry.jl, but we have talked about it, and about using Stencils.jl

using Rasters
using Stencils
using Stencils: Window

abstract type SlopeFilter end
abstract type SlopeConvolution <: SlopeFilter end
# Not all slope algorithms can provide aspect
abstract type SlopeAspectConvolution <: SlopeConvolution end

struct FD2 <: SlopeAspectConvolution end
struct FD3Reciprocal <: SlopeAspectConvolution end
struct FD3ReciprocalSquared <: SlopeAspectConvolution end
struct FD3Linear <: SlopeAspectConvolution end
struct FDFrame <: SlopeAspectConvolution end
struct SimpleDifference <: SlopeConvolution end

@inline function aspect_filter(method::SlopeConvolution, n::Window, d)
    fx, fy = _slope_conv(method, n, d)
    return _aspect(fx, fy)
end

@inline function slope_filter(method::SlopeConvolution, n::Window, d)
    fx, fy = _slope_conv(method, n, d)
    return _slope(fx, fy)
end

@inline function slopeaspect_filter(method::SlopeAspectConvolution, n::Window, d)
    fx, fy = _slope_conv(method, n, d)
    return _slope(fx, fy), _aspect(fx, fy)
end

_slope(fx, fy) = atan(√(fx^2 + fy^2))
function _aspect(fx, fy)
    (ismissing(fx) || ismissing(fy)) && return missing
    # Rotate - we want high Y (north) as the origin
    # TODO: pass through the Order for X/Y dims 
    # So the result always has zero at North
    -atan(fx, fy) 
end

@inline function _slope_conv(::FD2, n::Window, d)
    fx = (n[6] - n[4]) / 2d
    fy = (n[8] - n[2]) / 2d
    return fx, fy
end

@inline function _slope_conv(::FD3Reciprocal, n::Window, d)
    fx = (n[3] -n[1] + √(2(n[6] - n[4])) + n[9] - n[7]) / (4 + 2 * √(2)) * d
    fy = (n[7] -n[1] + √(2(n[8] - n[2])) + n[9] - n[3]) / (4 + 2 * √(2)) * d
    return fx, fy
end

@inline function _slope_conv(::FD3Linear, n::Window, d)
    fx = (n[3] - n[1] + n[6] - n[4] + n[9] - n[7]) / 6d
    fy = (n[7] - n[1] + n[8] - n[2] + n[9] - n[3]) / 6d
    return fx, fy
end

@inline function _slope_conv(::FD3ReciprocalSquared, n::Window, d)
    fx = (n[3] - n[1] + 2(n[6] - n[4]) + n[9] - n[7]) / 8d
    fy = (n[7] - n[1] + 2(n[8] - n[2]) + n[9] - n[3]) / 8d
    return fx, fy
end

@inline function _slope_conv(::FDFrame, n::Window, d)
    fx = (n[3] - n[1] + n[9] - n[7]) / 4d
    fy = (n[7] - n[1] + n[9] - n[3]) / 4d
    return fx, fy
end

@inline function _slope_conv(::SimpleDifference, n::Window, d)
    fy = (n[5] - n[2]) / d
    return fy, fy # No aspect
end

# These are the functions for users to call:
# slope, aspect and slopeaspect with the form `slope(raster, method; cellsize)
for (f, filt) in (:slope => :slope_filter, :aspect => :aspect_filter, :_slopeaspect => :slopeaspect_filter)
    @eval begin 
        function $(f)(elevation::Raster, method=FD2(); cellsize=1)
            hood = Window{1}(); 
            padval = missingval(elevation)
            newdata = Stencils.mapstencil(hood, parent(elevation); boundary=Stencils.Remove(padval)) do w
                $(filt)(method, w, cellsize)
            end
            rebuild(elevation; data=newdata, name=$(QuoteNode(f))) 
        end
        function $(f)(elevation::AbstractArray, method=FD2(); cellsize=1)
            hood = Window{1}()
            Stencils.mapstencil(hood, elevation, parent(elevation)) do w, e
                $(filt)(method, w, cellsize)
            end
        end
    end
end

function slopeaspect(elevation, method=FD2(); cellsize=1)
    sa = _slopeaspect(elevation, method; cellsize)
    slope = first.(sa)
    aspect = last.(sa)
    nt = (; slope, aspect)
    return elevation isa Raster ? RasterStack(nt) : nt
end

I could also rewrite this using a NamedStencil to get rid of all the magic numbers

1 Like

There’s also a ZevenbergenThorne implementation in Geomorphometry (I’m one of the authors), but I’m not sure what you’re looking for? Is it performance or more/better algorithms, or anything else?

In terms of speed, the current implementations should be fast enough for most purposes (running in 0.05s for a raster with 1M cells), and there are indeed plans to use Stencils to make it even faster.

There’s a gdaldem executable in GDAL_jll (but not with different algorithms, and certainly not faster). I’m not aware of other native Julia libraries.

One can access gdaldem via GMT.jl or use the GMT’s grdgradient module to compute the slope.

Wow. First, I am blown away by how helpful the Julia community is. (I’m new). Thank you all for such rapid and helpful replies.

While speed is important, I was thinking more about sink/peak/line pre-processing, interpolation of missing data, and memory management for large datasets. It looks like I will need to process 1000 tiles (~2TB) of data, so I am curious about memory management methods for addressing the need to mosaic tiles in order to avoid edge effects.

Additional slope calculation methods are a bonus, but are not a critical concern for me at the moment.

1 Like

To my knowledge there is currently no well-tested tooling for formulating and end-to-end custom overlapping moving window pipeline on an out-of-core mosaic in the Julia Geo ecosystem.

Just out of curiosity and maybe to make it easier to help, is there any library from another programming language that provides the functionality you are looking for?

I was wondering if this is something DiskArrayEngine could automate in future… with the halo padding required for slope filters?

Would be amazing if we could automate using Stencils.jl with DiskArrayEngine.jl, I guess a bit like ParallelStencil.jl

Yes, this is exactly what I had in mind as well. Seeing your code I think it would be quite easy to wrap into a moving window DiskArrayEngine function so that DiskArrayEngine can take care of memory management and IO and Stencils.jl does the fast moving window loop.

However, to me DiskArrayEngine is currently too experimental yet which kept me from suggesting it as a solution here. But as I said I would be very interested in solutions from other languages to get inspired by there APIs, compare performance etc…

I am not aware of a library in R that does all of these things. I am aware that the R raster/terra library has memory management settings that enable memory-based chunking depending on the specific raster size, I suspect that this makes the boundary problem worse, but I haven’t tried it.

I guess this would be falling into Dask territory: Introduction to Geospatial Raster and Vector Data with Python: Parallel raster computations using Dask

I would recommend pyflwdir pyflwdir this was written by DELTARES no doubt the leaders in hydrology.

I would use pyflwdir.dem.slope

I would need to see how to wrap the code in Julia.

Notice the URL here for the julia package :wink:

As an update Geomorphometry.jl is based on Stencils.jl now and should be incredibly fast, likely beats anything.

1 Like

I would thank @Raf, the pyflwdir slope algorithme is based on PCraster, for which Geomorphometry.jl is also based in PCraster. In fact DELTARES is shifting models written in Python to Julia language e.g. Wflow

Thanks for the compliments on Deltares. We indeed have shifted to development of numerical models for hydrology in Julia, although we’re only a small part of Deltares. That said, processing libraries such as pyflwdir or Geomorphometry.jl are not part of that shift, and should be seen as two completely separate libraries. Both are the products of PhD research done at Deltares. Note that pyflwdir has been around longer and is more battle-tested.

PS. I took inspiration from PCRaster for the spread algorithm (and test their examples), but not for slope or other algorithms. I do think that Julia lends itself for map algebra computing more easily than other languages, given the functional and broadcasting abilities.

2 Likes