Skip to content
Open
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
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ LocalFilters = "1.2"
OffsetArrays = "1.10"
PaddedViews = "0.5"
QuickHeaps = "0.2"
Random = "1"
Rasters = "0.13, 0.14, 0.15"
StaticArrays = "1"
Statistics = "1"
Expand All @@ -61,8 +62,9 @@ ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
Eikonal = "a6aab1ba-8f88-4217-b671-4d0788596809"
ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7"
GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Rasters = "a3a2b9e3-a471-40c9-b274-f788e487c689"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "GeoArrays", "Rasters", "ArchGDAL", "Eikonal", "Aqua", "ExplicitImports"]
test = ["Test", "Random", "GeoArrays", "Rasters", "ArchGDAL", "Eikonal", "Aqua", "ExplicitImports"]
34 changes: 28 additions & 6 deletions docs/src/refs.bib
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ @article{barnesPriorityFloodOptimalDepressionFilling2014
keywords = {Computer Science - Data Structures and Algorithms}
}


@inproceedings{bartelsDTMGenerationLIDAR2006,
title = {{{DTM Generation}} from {{LIDAR Data}} Using {{Skewness Balancing}}},
booktitle = {18th {{International Conference}} on {{Pattern Recognition}} ({{ICPR}}'06)},
Expand All @@ -34,7 +33,6 @@ @inproceedings{bartelsDTMGenerationLIDAR2006
ZSCC: 0000080}
}


@article{bartelsThresholdfreeObjectGround2010,
title = {Threshold-Free Object and Ground Point Separation in {{LIDAR}} Data},
author = {Bartels, Marc and Wei, Hong},
Expand All @@ -53,7 +51,6 @@ @article{bartelsThresholdfreeObjectGround2010
keywords = {_tablet,DSM,DTM,GIS,LIDAR,Remote sensing,Skewness Balancing}
}


@book{burroughPrinciplesGeographicalInformation2015,
title = {Principles of {{Geographical Information Systems}}},
author = {Burrough, P. A. and McDonnell, Rachael A. and Lloyd, Christopher D.},
Expand All @@ -66,7 +63,6 @@ @book{burroughPrinciplesGeographicalInformation2015
keywords = {Business & Economics / Economic History,Science / Earth Sciences / Geography,Technology & Engineering / Cartography,Technology & Engineering / Remote Sensing & Geographic Information Systems,Technology & Engineering / Surveying,Travel / General}
}


@article{cavalliEffectivenessAirborneLidar2008,
title = {The Effectiveness of Airborne {{LiDAR}} Data in the Recognition of Channel-Bed Morphology},
author = {Cavalli, Marco and Tarolli, Paolo and Marchi, Lorenzo and {Dalla Fontana}, Giancarlo},
Expand Down Expand Up @@ -106,7 +102,6 @@ @inproceedings{eastman1989pushbroom
year = {1989}
}


@article{hornHillShadingReflectance1981,
title = {Hill Shading and the Reflectance Map},
author = {Horn, B.K.P.},
Expand All @@ -123,7 +118,6 @@ @article{hornHillShadingReflectance1981
keywords = {Automation,Graphics,History,Photography,Production,Reflectivity,Shadow mapping,Streaming media,Surface topography,Surface treatment}
}


@article{ilichMultiscaleDTMOpensourcePackage2023,
title = {{{MultiscaleDTM}}: {{An}} Open-Source {{R}} Package for Multiscale Geomorphometric Analysis},
shorttitle = {{{MultiscaleDTM}}},
Expand Down Expand Up @@ -475,3 +469,31 @@ @article{zhaoFastSweepingMethod2005
keywords = {characteristics,Courant-Friedrichs-Lewy (CFL) condition,Eikonal equation,Gauss-Seidel iteration,Godunov scheme,upwind difference}
}

@article{barnesEfficientAssignmentDrainage2014,
title = {An Efficient Assignment of Drainage Direction over Flat Surfaces in Raster Digital Elevation Models},
author = {Barnes, Richard and Lehman, Clarence and Mulla, David},
year = {2014},
month = jan,
journal = {Computers \& Geosciences},
volume = {62},
pages = {128--135},
issn = {00983004},
doi = {10.1016/j.cageo.2013.01.009},
abstract = {Depression-filling leaves flat regions with no local relief, over which drainage directions are undefined. This paper presents an algorithm which assigns drainage directions over flats by combining a gradient away from higher terrain with a gradient towards lower terrain, computed with two breadth-first traversals. The result is a realistic, convergent drainage pattern in O(n) time.},
keywords = {flats,flow direction,hydrology}
}

@article{zhouEfficientVariantPriorityFlood2016,
title = {An Efficient Variant of the {{Priority-Flood}} Algorithm for Filling Depressions in Raster Digital Elevation Models},
author = {Zhou, Guiyun and Sun, Zhongxuan and Fu, Suhua},
year = {2016},
month = may,
journal = {Computers \& Geosciences},
volume = {90},
pages = {87--96},
issn = {00983004},
doi = {10.1016/j.cageo.2016.02.021},
abstract = {A variant of Priority-Flood that keeps the majority of cells out of the priority queue. Slope cells are processed by region growing on two plain queues, and only potential spill cells, where terrain turns back downhill, are pushed onto the priority queue. This reduces the complexity to O(m log m) with m much smaller than the number of cells, and runs 44.6\% faster than Barnes et al. (2014) in testing.},
keywords = {depression filling,hydrology,priority flood}
}

21 changes: 21 additions & 0 deletions docs/src/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,27 @@ depression_volume(dtm; filled=fdtm)

:::

### Filling and flow directions

[`filldepressions`](@ref) takes a `method`: the default [`PriorityFlood`](@ref) of
[barnesPriorityFloodOptimalDepressionFilling2014](@citet), or [`Zhou`](@ref) after
[zhouEfficientVariantPriorityFlood2016](@citet), which reaches the same surface 1.1-1.4x
faster. Filling leaves flats, which have no flow direction, so by default it also gives
them a gradient with [`resolveflats`](@ref) after
[barnesEfficientAssignmentDrainage2014](@citet); `flats=false` gives the filled surface
alone. [`flowdirection`](@ref) then reads directions off that surface, with the share each
receiver takes beside them.

```@example plots
z = filldepressions(cdtm)
directions, weights = flowdirection(z; method=D8())
parent(directions[400:800, 300:700]) # arrows, as the drainage direction below
```

[`D8`](@ref) has one receiver that takes everything, so its weights are a `Fill` costing
nothing at any grid size. [`DInf`](@ref) and [`FD8`](@ref) store a share per receiver, two
and eight, which `weighttype=Float16` halves.

Comment on lines +381 to +401

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way too much text and the example doesn't plot anything.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Section is 22 lines now from 48, and the example shows the arrow encoding (parent(directions[400:800, 300:700])) rather than assigning to nothing. My first attempt plotted sum(weights; dims=3), which throws on a Raster with a Band lookup, so it is the arrows instead.

Filling the depressions in a DEM is not necessary to calculate the flow accumulation. Here we use [`flowaccumulation`](@ref) to do so, and it automatically carves out depressions. Note that the local drainage direction is also returned. By default the FD8 algorithm is used, but you can also use the D∞ or D8 algorithm by setting the `method` keyword argument to `DInf()` or `D8()`.

::: warning
Expand Down
6 changes: 6 additions & 0 deletions ext/GeomorphometryRastersExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,10 @@ end
Geomorphometry.outlets(r::Raster) = Geomorphometry.outlets(parent(r))
Geomorphometry.neighbors(r::Raster, cell::CartesianIndex{2}) = Geomorphometry.neighbors(parent(r), cell)

# A direction eltype cannot represent the source missingval, so allocate without one.
Geomorphometry._directiongrid(
dem::Raster, ::Type{C}
) where {C <: Geomorphometry.FlowDirectionConvention} =
similar(dem, FlowDirection{C, UInt8}; missingval = nothing)

end # module
4 changes: 4 additions & 0 deletions src/Geomorphometry.jl
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ include("spread.jl")
include("skew.jl")
include("flowdir.jl")
include("hydrology.jl")
include("flats.jl")
include("direction.jl")
include("horizon.jl")
include("geomorphon.jl")

Expand All @@ -60,6 +62,8 @@ export roughness,
export slope,
aspect, laplacian, plan_curvature, profile_curvature, tangential_curvature
export skewness_balancing
export PriorityFlood, Zhou
export resolveflats, resolveflats!, flowdirection
export filldepressions,
flowaccumulation,
topographic_wetness_index,
Expand Down
182 changes: 182 additions & 0 deletions src/direction.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Single-receiver methods use the same LDD encoding as `flowaccumulation`; methods that can
# send flow several ways need the bitwise D8D encoding.
convention(::D8) = LDD
convention(::DInf) = D8D
convention(::FD8) = D8D

# A method that can send flow several ways needs the share each receiver takes; a single
# receiver takes everything, so there is nothing to carry.
hasweights(::FlowDirectionMethod) = false
hasweights(::DInf) = true
hasweights(::FD8) = true

"""Most receivers a method can ever give one cell."""
maxreceivers(::D8) = 1
maxreceivers(::DInf) = 2
maxreceivers(::FD8) = 8

"""
Share of each cell's flow per receiver, one dimension larger than the DEM, the trailing
axis in [`D8D`](@ref) bit order. A single-receiver method stores nothing: its weights are a
[`Fill`](@extref FillArrays.Fill) and cost no memory.
"""
function allocweights(method::FlowDirectionMethod, dem, T)
n = maxreceivers(method)
# A single receiver takes everything, so there is nothing to store.
hasweights(method) || return Fill(one(T), size(dem)..., n)
return fill!(_alloc_directions(dem, T, n), zero(T))
end


"""Record the share of a cell's `slot`-th receiver. Nothing to record without weights."""
@inline setshare!(::Fill, cell, slot, fraction) = fraction
@inline setshare!(weights::AbstractArray{T}, cell, slot, fraction) where {T} =
@inbounds weights[cell, slot] = T(fraction)

"""
Whatever a method wants to set up once for the whole DEM rather than per cell: a grid of
aspects for [`DInf`](@ref) off a raster, and for [`FD8`](@ref) the scratch buffers it
reuses from cell to cell. Methods that need neither get `nothing`.
"""
directionstate(::FlowDirectionMethod, dem, cellsize) = nothing
directionstate(::DInf, dem, cellsize) = _dinf_aspects(dem, cellsize)
directionstate(::FD8, dem, cellsize) = _fd8scratch(dem)

"""
flowdirection(dem; method=D8(), cellsize=cellsize(dem), weighttype=Float32)

Derive flow directions from `dem` by comparing each cell with its neighbours, and return
them with the share each receiver takes. The encoding matches [`flowaccumulation`](@ref):
[`LDD`](@ref) for the single receiver of [`D8`](@ref), [`D8D`](@ref) for the methods that
can send flow several ways at once.

These depend only on the local neighbourhood, so a cell with no lower neighbour is a pit.
Pass a surface from [`filldepressions`](@ref) if every cell must drain.

# Arguments
- `dem`: An indexed digital elevation model.
- `method::FlowDirectionMethod=D8()`: [`D8`](@ref), [`DInf`](@ref) or [`FD8`](@ref).
- `cellsize`: Cell size, used to scale horizontal distances. Derived from `dem` by default.
- `weighttype=Float32`: Element type of the weights. `Float16` halves the memory a
divergent method needs, at the cost of a little of the flow, as a cell's shares then no
longer sum to exactly one. Integer types are not usable.
"""
function flowdirection(
dem;
method::FlowDirectionMethod = D8(),
cellsize = cellsize(dem),
weighttype::Type{<:AbstractFloat} = Float32,
)
C = convention(method)
directions = _directiongrid(dem, C)
weights = allocweights(method, dem, weighttype)
state = directionstate(method, dem, cellsize)
@inbounds for cell in eachcell(dem)
code = celldirection!(weights, method, state, dem, cell, cellsize)
directions[cell] = FlowDirection{C}(code)
end
return directions, weights
end


"""
Offset towards `cell`'s steepest downslope neighbour, or zero when it has none: the
[`D8`](@ref) rule of
[Jenson and Domingue (1988)](@cite jensonExtractingTopographicStructure1988), and the
fallback wherever a method leaves a cell undecided.
"""
function steepestdescent(dem, cell, cellsize)
offset = cell - cell
z = dem[cell]
isfinite(z) || return offset
steepest = zero(float(typeof(z)))
@inbounds for neighbor in neighbors(dem, cell)
nz = dem[neighbor]
(isfinite(nz) && nz < z) || continue
distance = celldistance(dem, cell, neighbor; cellsize)
iszero(distance) && continue
gradient = (z - nz) / distance
if gradient > steepest
steepest = gradient
offset = neighbor - cell
end
end
return offset
end

steepestcode(dem, cell, cellsize) =
_directioncode(dem, steepestdescent(dem, cell, cellsize), cellsize)

celldirection!(weights, ::D8, _, dem, cell, cellsize) =
_orient(steepestdescent(dem, cell, cellsize), cellsize)

# DInf: the two neighbours bounding the steepest facet, using the same `_dinfreceivers`
# partition as `flowaccumulation`.
function celldirection!(weights, ::DInf, state, dem, cell, cellsize)
z = dem[cell]
isfinite(z) || return zero(UInt8)

receivers = _dinfreceivers(dem, cell, state, cellsize)
isnothing(receivers) && return steepestshare!(weights, dem, cell, cellsize)
acell, bcell, afraction, bfraction = receivers

# The partition can name a cell that is not actually lower, most often beside a flat.
# Such a receiver takes nothing and the other is renormalised to the whole; if that
# leaves nobody, fall back rather than strand a cell that can drain.
lower(ncell) = isfinite(dem[ncell]) && dem[ncell] < z
a = (_directioncode(dem, acell - cell, cellsize), lower(acell) ? afraction : 0.0)
b = (_directioncode(dem, bcell - cell, cellsize), lower(bcell) ? bfraction : 0.0)
total = last(a) + last(b)
total > 0 || return steepestshare!(weights, dem, cell, cellsize)

# Shares are read back in the order `decompose` hands the directions out, which is by
# bit, so emit the pair in that order.
code = zero(UInt8)
slot = 0
@inbounds for (bit, fraction) in (first(a) <= first(b) ? (a, b) : (b, a))
iszero(fraction) && continue
slot += 1
setshare!(weights, cell, slot, fraction / total)
code |= bit
end
return code
end

# FD8: every downslope neighbour receives flow, weighted by slope and contour length, using
# the same `_fd8weights!` as `flowaccumulation`.
function celldirection!(weights, fd8::FD8, scratch, dem, cell, cellsize)
z = dem[cell]
isfinite(z) || return zero(UInt8)
total = _fd8weights!(scratch, fd8, dem, cell, Unvisited(), cellsize)
iszero(total) && return steepestshare!(weights, dem, cell, cellsize)

# Gather the shares against their direction bits, then emit them in bit order, which is
# the order the directions come back out in.
fill!(scratch.bitweights, 0.0)
code = zero(UInt8)
@inbounds for (index, neighbor) in enumerate(scratch.neighbors)
weight = scratch.weights[index]
iszero(weight) && continue
bit = _directioncode(dem, neighbor - cell, cellsize)
scratch.bitweights[trailing_zeros(Int(bit)) + 1] = weight / total
code |= bit
end
slot = 0
@inbounds for bit in decompose(D8D, code)
iszero(bit) && continue
slot += 1
setshare!(weights, cell, slot, scratch.bitweights[trailing_zeros(Int(bit)) + 1])
end
return code
end

"""The steepest descent fallback: one receiver, taking everything."""
function steepestshare!(weights, dem, cell, cellsize)
code = steepestcode(dem, cell, cellsize)
iszero(code) || setshare!(weights, cell, 1, one(Float32))
return code
end

"""A stencil has no traversal order, so nothing is excluded for being already visited."""
struct Unvisited end
Base.getindex(::Unvisited, _...) = false
Loading
Loading