├── docs ├── src │ ├── tutorial.md │ ├── guide.md │ ├── bibliography.md │ ├── assets │ │ └── favicon.ico │ ├── todo.md │ ├── reference.md │ ├── experimental.md │ ├── installation.md │ ├── .vitepress │ │ ├── theme │ │ │ ├── index.ts │ │ │ └── style.css │ │ └── config.mts │ ├── components │ │ ├── stargazers.data.ts │ │ ├── VersionPicker.vue │ │ └── StarUs.vue │ ├── index.md │ ├── validation.md │ ├── concepts.md │ ├── usage.md │ └── refs.bib ├── package.json ├── Project.toml └── make.jl ├── .github ├── dependabot.yml └── workflows │ ├── TagBot.yml │ ├── CI.yml │ ├── Docs.yml │ └── CompatHelper.yml ├── .gitignore ├── .JuliaFormatter.toml ├── LICENSE ├── ext ├── GeomorphometryEikonalExt.jl ├── GeomorphometryRastersExt.jl └── GeomorphometryGeoArraysExt.jl ├── src ├── Geomorphometry.jl ├── smf.jl ├── skew.jl ├── plot.jl ├── utils.jl ├── pmf.jl ├── relative.jl ├── spread.jl ├── hydrology.jl └── terrain.jl ├── README.md ├── Project.toml ├── test └── runtests.jl └── CHANGELOG.md /docs/src/tutorial.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/guide.md: -------------------------------------------------------------------------------- 1 | # Guides 2 | -------------------------------------------------------------------------------- /docs/src/bibliography.md: -------------------------------------------------------------------------------- 1 | ```@bibliography 2 | ``` 3 | -------------------------------------------------------------------------------- /docs/src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Deltares/Geomorphometry.jl/main/docs/src/assets/favicon.ico -------------------------------------------------------------------------------- /docs/src/todo.md: -------------------------------------------------------------------------------- 1 | # Planned features 2 | 3 | The following features are planned for future releases: 4 | 5 | - Classification metrics such as geomorphons 6 | - More support for multiresolution 7 | - Integration with DEM datasets/tiles 8 | 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" # Location of package manifests 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.jl.*.cov 2 | *.jl.cov 3 | *.jl.mem 4 | .DS_Store 5 | Manifest.toml 6 | docs/build/ 7 | *.tif 8 | *.gif 9 | test/benchmark.jl 10 | .vscode 11 | docs/node_modules 12 | docs/final_site 13 | _extensions 14 | 15 | 16 | scripts 17 | .documenter 18 | docs/src/CHANGELOG.md 19 | *.png 20 | -------------------------------------------------------------------------------- /docs/src/reference.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | DocTestSetup= quote 3 | using Geomorphometry 4 | end 5 | ``` 6 | ## Index 7 | ```@index 8 | Modules = [Geomorphometry] 9 | ``` 10 | 11 | ## Reference - Exported functions 12 | ```@autodocs 13 | Modules = [Geomorphometry] 14 | Private = false 15 | ``` 16 | 17 | ## Reference - Internal functions 18 | ```@autodocs 19 | Modules = [Geomorphometry] 20 | Public = false 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/src/experimental.md: -------------------------------------------------------------------------------- 1 | # Experimental 2 | 3 | > [!WARNING] 4 | > Methods here are experimental, not yet stable and may change or even be removed in future releases. 5 | 6 | ```@meta 7 | DocTestSetup= quote 8 | using Geomorphometry 9 | end 10 | ``` 11 | 12 | ```@docs 13 | Geomorphometry.prominence 14 | ``` 15 | 16 | ```@docs 17 | Geomorphometry.skbr 18 | ``` 19 | 20 | ```docs 21 | Geomorphometry.pmf2 22 | ``` 23 | -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | issue_comment: 4 | types: 5 | - created 6 | workflow_dispatch: 7 | jobs: 8 | TagBot: 9 | if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: JuliaRegistries/TagBot@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | ssh: ${{ secrets.DOCUMENTER_KEY }} 16 | -------------------------------------------------------------------------------- /docs/src/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Since `Geomorphometry.jl` is registered in the Julia General registry, you can simply run the following 4 | command in the Julia REPL: 5 | 6 | ```julia 7 | julia> using Pkg 8 | julia> Pkg.add("Geomorphometry") 9 | # or 10 | julia> ] # ']' should be pressed 11 | pkg> add Geomorphometry 12 | ``` 13 | 14 | If you want to use the latest unreleased version, you can run the following command: 15 | 16 | ```julia 17 | pkg> add Geomorphometry#main 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docs:dev": "vitepress dev build/.documenter", 4 | "docs:build": "vitepress build build/.documenter", 5 | "docs:preview": "vitepress preview build/.documenter" 6 | }, 7 | "dependencies": { 8 | "@types/d3-format": "^3.0.4", 9 | "@types/node": "^22.10.5", 10 | "d3-format": "^3.1.0", 11 | "@shikijs/transformers": "^1.1.7", 12 | "markdown-it": "^14.1.0", 13 | "markdown-it-footnote": "^4.0.0", 14 | "markdown-it-mathjax3": "^4.3.2", 15 | "vitepress": "^1.5.0", 16 | "vitepress-plugin-tabs": "^0.5.0" 17 | } 18 | } -------------------------------------------------------------------------------- /.JuliaFormatter.toml: -------------------------------------------------------------------------------- 1 | # Options for the JuliaFormatter auto syntax formatting tool. 2 | # https://domluna.github.io/JuliaFormatter.jl/stable/ 3 | # https://docs.sciml.ai/SciMLStyle/stable/ 4 | 5 | # We don't use SciML style since aligning with opening brackets doesn't look good if 6 | # the opening bracket is near the end of the line. This squeezes the code to the right, 7 | # and this alignment confuses auto indent. 8 | # Based on the default style we do pick these non-default options from SciML style: 9 | whitespace_ops_in_indices = true 10 | remove_extra_newlines = true 11 | always_for_in = true 12 | whitespace_typedefs = true 13 | 14 | # And add other options we like: 15 | separate_kwargs_with_semicolon = true 16 | -------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" 3 | Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" 4 | ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" 5 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 6 | DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" 7 | DocumenterVitepress = "4710194d-e776-4893-9690-8d956a29c365" 8 | Eikonal = "a6aab1ba-8f88-4217-b671-4d0788596809" 9 | GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab" 10 | Geomorphometry = "714e3e49-7933-471a-9334-4a6a65a92f36" 11 | LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" 12 | Rasters = "a3a2b9e3-a471-40c9-b274-f788e487c689" 13 | Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" 14 | 15 | [compat] 16 | Makie = "0.22" 17 | 18 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | // .vitepress/theme/index.ts 2 | import { h } from 'vue' 3 | import type { Theme } from 'vitepress' 4 | import DefaultTheme from 'vitepress/theme' 5 | import VersionPicker from "../../components/VersionPicker.vue" 6 | import StarUs from '../../components/StarUs.vue' 7 | 8 | import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' 9 | import './style.css' 10 | 11 | export default { 12 | extends: DefaultTheme, 13 | Layout() { 14 | return h(DefaultTheme.Layout, null, { 15 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 16 | 'nav-bar-content-after': () => h(StarUs), 17 | }) 18 | }, 19 | enhanceApp({ app, router, siteData }) { 20 | enhanceAppWithTabs(app); 21 | app.component('VersionPicker', VersionPicker); 22 | } 23 | } satisfies Theme 24 | -------------------------------------------------------------------------------- /docs/src/components/stargazers.data.ts: -------------------------------------------------------------------------------- 1 | const REPO = "Deltares/Geomorphometry.jl"; 2 | 3 | export default { 4 | async load() { 5 | let stargazers_count; 6 | try { 7 | ({ stargazers_count } = await github(`/repos/${REPO}`)); 8 | } catch (error) { 9 | if (process.env.CI) throw error; 10 | stargazers_count = NaN; 11 | } 12 | return stargazers_count; 13 | } 14 | }; 15 | 16 | async function github( 17 | path, 18 | { 19 | authorization = process.env.GITHUB_TOKEN && `token ${process.env.GITHUB_TOKEN}`, 20 | accept = "application/vnd.github.v3+json" 21 | } = {} 22 | ) { 23 | const url = new URL(path, "https://api.github.com"); 24 | const headers = { ...(authorization && { authorization }), accept }; 25 | const response = await fetch(url, { headers }); 26 | if (!response.ok) throw new Error(`fetch error: ${response.status} ${url}`); 27 | return await response.json(); 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Deltares 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /ext/GeomorphometryEikonalExt.jl: -------------------------------------------------------------------------------- 1 | module GeomorphometryEikonalExt 2 | 3 | using Geomorphometry 4 | using Eikonal 5 | 6 | """ 7 | spread(::FastSweeping, points::AbstractMatrix{<:Real}, initial::AbstractMatrix{<:Real}, friction::AbstractMatrix{<:Real}; ) 8 | 9 | Total friction distance spread from `points` as described by [Zhao, H (2005)](@cite zhaoFastSweepingMethod2005). 10 | 11 | # Output 12 | - `Array{Float64,2}` Total friction distance 13 | 14 | # Arguments 15 | - `points::Vector{CartesianIndex}` Input Array 16 | - `initial::AbstractVector{<:Real}` Initial values of the result 17 | - `friction::Matrix{<:Real}` Friction map 18 | """ 19 | function Geomorphometry.spread( 20 | fs::Geomorphometry.FastSweeping, 21 | points, 22 | initial, 23 | friction; 24 | kwargs..., 25 | ) 26 | result = similar(friction) 27 | 28 | solver = Eikonal.FastSweeping(parent(friction)) 29 | for I in points 30 | solver.t[I] = initial[I] 31 | end 32 | Eikonal.sweep!(solver; nsweeps = fs.iterations, verbose = fs.debug, epsilon = fs.eps) 33 | result .= solver.t[2:end, 2:end] 34 | result 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /src/Geomorphometry.jl: -------------------------------------------------------------------------------- 1 | module Geomorphometry 2 | using StatsBase: skewness 3 | using Distances: Euclidean, euclidean, evaluate 4 | using OffsetArrays: centered 5 | using PaddedViews: PaddedView 6 | using FillArrays: Fill 7 | using StaticArrays: @SMatrix, @MMatrix, @MVector 8 | import DataStructures 9 | using LocalFilters: LocalFilters, dilate, localfilter! 10 | using Stencils: Stencils, Annulus, Moore, NamedStencil, Stencil, Window, center, mapstencil 11 | using Statistics: mean, std 12 | using LocalFilters 13 | using QuickHeaps: FastPriorityQueue, PriorityQueue, enqueue!, dequeue! 14 | 15 | include("utils.jl") 16 | include("relative.jl") 17 | include("pmf.jl") 18 | include("smf.jl") 19 | include("plot.jl") 20 | include("spread.jl") 21 | include("terrain.jl") 22 | include("skew.jl") 23 | include("hydrology.jl") 24 | 25 | export ZevenbergenThorne, Horn, MDG 26 | export D8, DInf, FD8 27 | export pmf, smf, psf 28 | export pssm, hillshade, multihillshade 29 | export pitremoval 30 | export spread, Eastman, FastSweeping, Tomlin 31 | export roughness, TRI, TPI, BPI, RIE, rugosity, entropy 32 | export slope, 33 | aspect, curvature, laplacian, plan_curvature, profile_curvature, tangential_curvature 34 | export skb, skbr 35 | export filldepressions, flowaccumulation, TWI, SPI 36 | 37 | end # module 38 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | tags: ["*"] 6 | pull_request: 7 | # needed to allow julia-actions/cache to delete old caches that it has created 8 | permissions: 9 | actions: write 10 | contents: read 11 | jobs: 12 | test: 13 | name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | version: 19 | - "lts" 20 | - "1" 21 | - "nightly" 22 | os: 23 | - ubuntu-latest 24 | - macOS-latest 25 | - windows-latest 26 | arch: 27 | - x64 28 | steps: 29 | - uses: actions/checkout@v5 30 | - uses: julia-actions/setup-julia@v2 31 | with: 32 | version: ${{ matrix.version }} 33 | arch: ${{ matrix.arch }} 34 | - uses: julia-actions/cache@v2 35 | - uses: julia-actions/julia-buildpkg@v1 36 | - uses: julia-actions/julia-runtest@v1 37 | - uses: julia-actions/julia-processcoverage@v1 38 | - uses: codecov/codecov-action@v5 39 | with: 40 | files: lcov.info 41 | env: 42 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 43 | -------------------------------------------------------------------------------- /src/smf.jl: -------------------------------------------------------------------------------- 1 | """ 2 | ``` 3 | B = smf(A; ω, slope, dhₘ, dh₀, cellsize) 4 | ``` 5 | Applies the simple morphological filter by [Pingel et al. (2013)](@cite pingelImprovedSimpleMorphological2013a) to `A`. 6 | 7 | # Output 8 | - `B::Array{Float64,2}` A filtered version of A 9 | 10 | # Arguments 11 | - `A::Array{T,2}` Input Array 12 | - `ω::Float64=18.` Maximum window size [m] 13 | - `slope::Float64=0.01` Terrain slope [m/m] 14 | - `cellsize::Float64=1.` Cellsize in [m] 15 | """ 16 | function smf( 17 | A::AbstractMatrix{<:Real}; 18 | ω::Real = 17.0, 19 | slope::Real = 0.01, 20 | cellsize = abs(first(cellsize(A))), 21 | ) 22 | out = similar(A, Union{Missing, nonmissingtype(eltype(A))}) 23 | out .= A 24 | lastsurface = -copy(A) 25 | is_low = falses(size(A)) 26 | radii = 1:(round(Int, ω / cellsize) >> 1) 27 | 28 | threshold = slope * cellsize 29 | thissurface = opening_circ(lastsurface, 1) 30 | is_low .|= ((lastsurface - thissurface) .> threshold) 31 | lastsurface .= thissurface 32 | 33 | lastsurface = copy(A) 34 | is_obj = falses(size(A)) 35 | for radius in radii 36 | threshold = slope * radius * cellsize 37 | thissurface = opening_circ(lastsurface, radius) 38 | is_obj .|= ((lastsurface - thissurface) .> threshold) 39 | lastsurface .= thissurface 40 | end 41 | out[is_low .| is_obj] .= missing 42 | return out 43 | end 44 | -------------------------------------------------------------------------------- /ext/GeomorphometryRastersExt.jl: -------------------------------------------------------------------------------- 1 | module GeomorphometryRastersExt 2 | 3 | using Geomorphometry 4 | using Rasters, ArchGDAL 5 | using FillArrays 6 | 7 | degwidth::Float64 = 111_000.0 8 | 9 | function Geomorphometry.cellsize(dem::Raster) 10 | T = _crstrait(dem) 11 | _cellsize(T, dem) 12 | end 13 | 14 | function _cellsize(::Rasters.GI.AbstractProjectedTrait, dem::Raster) 15 | dim = Rasters.dims(dem, (Rasters.XDim, Rasters.YDim)) 16 | Rasters.isintervals(dim) || throw( 17 | ArgumentError("Cannot calculate cell size for a `Raster` with `Points` sampling."), 18 | ) 19 | (step(dim[1]), step(dim[2])) 20 | end 21 | function _cellsize(::Rasters.GI.AbstractGeographicTrait, dem::Raster) 22 | dim = Rasters.dims(dem, (Rasters.XDim, Rasters.YDim)) 23 | Rasters.isintervals(dim) || throw( 24 | ArgumentError("Cannot calculate cell size for a `Raster` with `Points` sampling."), 25 | ) 26 | centercoords = DimPoints(dem)[round.(Int, size(dem)[1:2] ./ 2)...] 27 | (step(dim[1]) * degwidth * cosd(centercoords[2]), step(dim[2]) * degwidth) 28 | end 29 | 30 | function _crstrait(dem::Raster) 31 | crs = Rasters.crs(dem) 32 | acrs = ArchGDAL.importCRS(crs) 33 | Bool(ArchGDAL.GDAL.osrisgeographic(acrs.ptr)) && return Rasters.GI.GeographicTrait() 34 | Bool(ArchGDAL.GDAL.osrisprojected(acrs.ptr)) && return Rasters.GI.ProjectedTrait() 35 | return Rasters.GI.UnknownTrait() 36 | end 37 | 38 | end # module 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/Deltares/GeoRasterFiltering.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/Deltares/GeoRasterFiltering.jl/actions/workflows/CI.yml) 2 | [![Codecov](https://codecov.io/gh/Deltares/Geomorphometry.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/Deltares/Geomorphometry.jl) 3 | [![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://deltares.github.io/Geomorphometry.jl/stable/) 4 | [![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://deltares.github.io/Geomorphometry.jl/dev/) 5 | 6 | # Geomorphometry 7 | Geospatial operations, cost and filtering algorithms as used for elevation rasters. 8 | 9 | ## Functionality 10 | - Terrain filters, such as Progressive Morphological Filters (PMF, SMF) and Skewness balancing 11 | - Geospatial cost (friction) operations that mimic PCRaster. These functions should however be more Julian, extensible and scale better. 12 | - Visualization, such as Perceptually Shaded Slope Map (PSSM) 13 | - Terrain analysis functions, such as slope, aspect, roughness, Topographic Position Index (TPI), Terrain Ruggedness Index (TRI), curvature and hillslope. 14 | 15 | ## Installation 16 | The package can be installed with the Julia package manager. 17 | From the Julia REPL, type `]` to enter the Pkg REPL mode and run: 18 | 19 | ``` 20 | pkg> add Geomorphometry 21 | ``` 22 | 23 | ## Alternative Packages 24 | If you are working in Python the [xDEM](https://xdem.readthedocs.io/en/stable/) package provides a comprehensive suite of tools for DEM analysis 25 | -------------------------------------------------------------------------------- /.github/workflows/Docs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages 2 | # 3 | name: Documenter 4 | 5 | on: 6 | # Runs on pushes targeting the `master` branch. Change this to `main` if you're 7 | # using the `main` branch as the default branch. 8 | push: 9 | branches: 10 | - main 11 | tags: ['*'] 12 | pull_request: 13 | 14 | # Allows you to run this workflow manually from the Actions tab 15 | workflow_dispatch: 16 | 17 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 18 | permissions: 19 | contents: write 20 | pages: write 21 | id-token: write 22 | statuses: write 23 | 24 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 25 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 26 | concurrency: 27 | group: pages 28 | cancel-in-progress: false 29 | 30 | jobs: 31 | # Build job 32 | build: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v5 37 | - name: Setup Julia 38 | uses: julia-actions/setup-julia@v2 39 | - name: Pull Julia cache 40 | uses: julia-actions/cache@v2 41 | - name: Install documentation dependencies 42 | run: julia --project=docs -e 'using Pkg; pkg"dev ."; Pkg.instantiate(); Pkg.precompile(); Pkg.status()' 43 | #- name: Creating new mds from src 44 | - name: Build and deploy docs 45 | uses: julia-actions/julia-docdeploy@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token 48 | DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key 49 | JULIA_DEBUG: "Documenter" 50 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "Geomorphometry" 2 | uuid = "714e3e49-7933-471a-9334-4a6a65a92f36" 3 | authors = ["Maarten Pronk ", "Deltares"] 4 | version = "0.7.1" 5 | 6 | [deps] 7 | DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" 8 | Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" 9 | FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" 10 | LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" 11 | LocalFilters = "085fde7c-5f94-55e4-8448-8bbb5db6dde9" 12 | OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" 13 | PaddedViews = "5432bcbf-9aad-5242-b902-cca2824c8663" 14 | QuickHeaps = "30b38841-0f52-47f8-a5f8-18d5d4064379" 15 | StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" 16 | Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" 17 | StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" 18 | Stencils = "264155e8-78a8-466a-aa59-c9b28c34d21a" 19 | 20 | [weakdeps] 21 | ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" 22 | Eikonal = "a6aab1ba-8f88-4217-b671-4d0788596809" 23 | Rasters = "a3a2b9e3-a471-40c9-b274-f788e487c689" 24 | GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab" 25 | 26 | [extensions] 27 | GeomorphometryEikonalExt = "Eikonal" 28 | GeomorphometryGeoArraysExt = "GeoArrays" 29 | GeomorphometryRastersExt = ["Rasters", "ArchGDAL"] 30 | 31 | [compat] 32 | ArchGDAL = "0.10" 33 | DataStructures = "0.18" 34 | Distances = "0.10" 35 | Eikonal = "0.1.1" 36 | FillArrays = "0.12, 0.13, 1" 37 | GeoArrays = "0.9" 38 | LocalFilters = "1.2" 39 | OffsetArrays = "1.10" 40 | PaddedViews = "0.5" 41 | QuickHeaps = "0.2" 42 | Rasters = "0.13, 0.14" 43 | StaticArrays = "1" 44 | Statistics = "1" 45 | StatsBase = "0.33, 0.34" 46 | Stencils = "0.3.4" 47 | julia = "1.10" 48 | 49 | [extras] 50 | ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" 51 | Eikonal = "a6aab1ba-8f88-4217-b671-4d0788596809" 52 | GeoArrays = "2fb1d81b-e6a0-5fc5-82e6-8e06903437ab" 53 | Rasters = "a3a2b9e3-a471-40c9-b274-f788e487c689" 54 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 55 | 56 | [targets] 57 | test = ["Test", "GeoArrays", "Rasters", "ArchGDAL", "Eikonal"] 58 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Geomorphometry 2 | using Test 3 | 4 | @testset "Geomorphometry" begin 5 | @testset "pmf" begin 6 | # Write your own tests here. 7 | A = rand(25, 25) 8 | A[2, 2] = NaN 9 | B, flags = pmf(A) 10 | @test (A .<= B) == (flags .== 0.0) 11 | 12 | Bc, flagsc = pmf(A; circular = true) 13 | @test (A .<= Bc) == (flagsc .== 0.0) 14 | A = reshape(A, (size(A)..., 1)) 15 | 16 | B3, flags3 = pmf(A) 17 | @test length(size(B3)) == 2 18 | @test isnan.(B3) == isnan.(B) 19 | @test B3[end] == B[end] 20 | @test isnan.(flags3) == isnan.(flags) 21 | end 22 | @testset "smf" begin 23 | # Write your own tests here. 24 | A = rand(25, 25) 25 | A[2, 2] = NaN 26 | B = smf(A) 27 | end 28 | @testset "pssm" begin 29 | B = pssm(rand(25, 25)) 30 | end 31 | @testset "skb" begin 32 | B = skb(rand(25, 25)) 33 | B = skb(rand(25, 25); mean = 0.25) 34 | B = skbr(rand(25, 25); iterations = 5) 35 | end 36 | @testset "pitremoval" begin 37 | B = pitremoval(rand(25, 25)) 38 | B = pitremoval(rand(25, 25); limit = 0.1) 39 | end 40 | @testset "spread" begin 41 | points = [0.0 0 0 0 2; 0 0 0 0 0; 0 0 0 0 0; 0 1 0 0 0; 0 0 0 0 0] 42 | initial = [8.0 8 8 8 4; 8 8 8 8 8; 8 8 8 8 8; 0 0 8 8 8; 0 0 8 8 8] 43 | friction = [1.0 200 1 1 1; 200 1 1 4 4; 1 1 4 4 4; 1 1 3 200 200; 1 Inf 3 200 4] 44 | @test spread(points, initial, friction; method = Tomlin()) == 45 | spread(points, initial, friction; method = Eastman()) 46 | end 47 | @testset "terrain" begin 48 | A = rand(25, 25) 49 | TRI(A) 50 | TPI(A) 51 | roughness(A) 52 | slope(A; method = Horn()) 53 | slope(A; cellsize = (5, 5), method = ZevenbergenThorne()) 54 | slope(A; cellsize = (10, 10), method = MDG()) 55 | aspect(A) 56 | aspect(A; method = MDG()) 57 | curvature(A) 58 | hillshade(A) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | ```@raw html 2 | --- 3 | # https://vitepress.dev/reference/default-theme-home-page 4 | layout: home 5 | 6 | hero: 7 | name: "Geomorphometry.jl" 8 | tagline: "Analyzing and visualizing the shape of the Earth" 9 | image: 10 | src: logo.svg 11 | alt: Geomorphometry 12 | actions: 13 | - theme: brand 14 | text: Get Started 15 | link: /tutorials/installation.md 16 | - theme: alt 17 | text: View on Github 18 | link: https://github.com/Deltares/Geomorphometry.jl 19 | - theme: alt 20 | text: API Reference 21 | link: /reference 22 | 23 | features: 24 | - title: Common operations 25 | details: Defines common methods for analyzing, filtering and visualizing (global) elevation models. All methods are implemented in Julia and are fast and scalable. 26 | link: /usage 27 | - title: Multiple algorithms 28 | details: Choose from multiple algorithms, ranging from different derivations of slope to multiresolution stencils. This enables the user to select the most appropriate method for their use case. 29 | link: /concepts 30 | - title: Seamless integration 31 | details: Geomorphometry.jl is fully compatible with the AbstractArray and GeoInterface.jl ecosystems. This enables plotting, operations and analysis using the full power of the Julia ecosystem. 32 | 33 | 34 | --- 35 | ``` 36 | 37 | ```@meta 38 | CurrentModule = Geomorphometry 39 | ``` 40 | 41 | ## Functionality 42 | - Terrain filters, such as Progressive Morphological Filters (PMF, SMF) and Skewness balancing 43 | - Geospatial cost (friction) operations that mimic PCRaster. These functions should however be more Julian, extensible and scale better. 44 | - Visualization, such as Perceptually Shaded Slope Map (PSSM) 45 | - Terrain analysis functions, such as slope, aspect, roughness, Topographic Position Index (TPI), Terrain Ruggedness Index (TRI), curvature and hillslope. 46 | 47 | ## Installation 48 | The package can be installed with the Julia package manager. 49 | From the Julia REPL, type `]` to enter the Pkg REPL mode and run: 50 | 51 | ``` 52 | pkg> add Geomorphometry 53 | ``` 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.7.0] - 2025-03-21 6 | 7 | ### Added 8 | - Improved documentation by using VitePress and showcasing all methods on a DEM. 9 | - Added hydrology operators: `priorityflood`, `streamflow` 10 | - Added *multiscale* options to some filters using a `window` kwarg for a Stencil from Stencils.jl package. Other methods now take a `radius` kwarg. 11 | - Added `BPI` 12 | - Overhauled curvature methods by introducing `plan_curvature`, `profile_curvature` and `contour_curvature`, while deprecating `curvature` for `laplacian` 13 | - Added direction kwarg to slope and curvature methods. 14 | - Added this `CHANGELOG.md` 15 | - Added package extensions on GeoArrays, Rasters to support automatic cellsizes. 16 | - Added package extension on Eikonal for a faster `spread` method. 17 | 18 | ### Changed 19 | - Added Stencils as dependency. 20 | - Refactored spread to choose from multiple algorithms 21 | 22 | ### Fixed 23 | - Relaxed `Array` input to `AbstractArray` for `opening` 24 | ## [0.6.0] - 2023-08-25 25 | ### Changed 26 | - Renamed package to Geomorphometry 27 | 28 | ### Added 29 | - Added `erosion` parameter in `PMF` 30 | 31 | ## [0.5.2] - 2023-08-14 32 | ### Changed 33 | - Light maintenance to CI scripts 34 | - Compat updates 35 | 36 | ## [0.5.1] - 2023-01-12 37 | ### Added 38 | - Relaxed input for `PMF` 39 | 40 | ## [0.5.0] - 2022-11-02 41 | 42 | ### Added 43 | - Added `multihillshade` 44 | 45 | ### Changed 46 | - Uses LocalFilters for terrain filters, improving performance, but changing the edge behaviour of some filters. 47 | 48 | ## [0.4.0] - 2022-10-17 49 | ### Added 50 | - Added `hillshade`, `curvature` 51 | 52 | ### Changed 53 | - Used LocalFilters for `PMF`. 54 | 55 | ## [0.3.2] - 2022-09-07 56 | ### Added 57 | - Added terrain kernels for different algorithms. 58 | 59 | ### Fixed 60 | - Fixed bug in TPI 61 | 62 | ## [0.3.1] - 2022-06-15 63 | ### Added 64 | - Added `skewness` filter 65 | 66 | ## [0.3.0] - 2022-02-01 67 | ### Added 68 | - Added PSF (Progressive Slope Filter) function. 69 | 70 | ### Changed 71 | - Improved performance of mapwindow function used in most filters. 72 | 73 | ## [0.2.0] - 2021-11-22 74 | ### Added 75 | - Initial release of the GeoArrayOps package. 76 | - Basic terrain analysis functions. 77 | 78 | -------------------------------------------------------------------------------- /.github/workflows/CompatHelper.yml: -------------------------------------------------------------------------------- 1 | name: CompatHelper 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * 5 | workflow_dispatch: 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | jobs: 10 | CompatHelper: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check if Julia is already available in the PATH 14 | id: julia_in_path 15 | run: which julia 16 | continue-on-error: true 17 | - name: Install Julia, but only if it is not already available in the PATH 18 | uses: julia-actions/setup-julia@v2 19 | with: 20 | version: '1' 21 | arch: ${{ runner.arch }} 22 | if: steps.julia_in_path.outcome != 'success' 23 | - name: "Add the General registry via Git" 24 | run: | 25 | import Pkg 26 | ENV["JULIA_PKG_SERVER"] = "" 27 | Pkg.Registry.add("General") 28 | shell: julia --color=yes {0} 29 | - name: "Install CompatHelper" 30 | run: | 31 | import Pkg 32 | name = "CompatHelper" 33 | uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" 34 | version = "3" 35 | Pkg.add(; name, uuid, version) 36 | shell: julia --color=yes {0} 37 | - name: "Run CompatHelper" 38 | run: | 39 | import CompatHelper 40 | CompatHelper.main() 41 | shell: julia --color=yes {0} 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | # This repo uses Documenter, so we can reuse our [Documenter SSH key](https://documenter.juliadocs.org/stable/man/hosting/walkthrough/). 45 | # If we didn't have one of those setup, we could configure a dedicated ssh deploy key `COMPATHELPER_PRIV` following https://juliaregistries.github.io/CompatHelper.jl/dev/#Creating-SSH-Key. 46 | # Either way, we need an SSH key if we want the PRs that CompatHelper creates to be able to trigger CI workflows themselves. 47 | # That is because GITHUB_TOKEN's can't trigger other workflows (see https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow). 48 | # Check if you have a deploy key setup using these docs: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/reviewing-your-deploy-keys. 49 | COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} 50 | # COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} 51 | -------------------------------------------------------------------------------- /docs/src/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress' 2 | import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' 3 | import mathjax3 from "markdown-it-mathjax3"; 4 | import footnote from "markdown-it-footnote"; 5 | 6 | function getBaseRepository(base: string): string { 7 | if (!base || base === '/') return '/'; 8 | const parts = base.split('/').filter(Boolean); 9 | return parts.length > 0 ? `/${parts[0]}/` : '/'; 10 | } 11 | 12 | const baseTemp = { 13 | base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs! 14 | } 15 | 16 | const navTemp = { 17 | nav: 'REPLACE_ME_DOCUMENTER_VITEPRESS', 18 | } 19 | 20 | const nav = [ 21 | ...navTemp.nav, 22 | { 23 | component: 'VersionPicker' 24 | } 25 | ] 26 | 27 | // https://vitepress.dev/reference/site-config 28 | export default defineConfig({ 29 | base: 'REPLACE_ME_DOCUMENTER_VITEPRESS',// TODO: replace this in makedocs! 30 | title: 'REPLACE_ME_DOCUMENTER_VITEPRESS', 31 | description: 'REPLACE_ME_DOCUMENTER_VITEPRESS', 32 | lastUpdated: true, 33 | cleanUrls: true, 34 | outDir: 'REPLACE_ME_DOCUMENTER_VITEPRESS', // This is required for MarkdownVitepress to work correctly... 35 | head: [ 36 | ['link', { rel: 'icon', href: 'favicon.ico' }], 37 | ['script', { src: `${getBaseRepository(baseTemp.base)}versions.js` }], 38 | // ['script', {src: '/versions.js'], for custom domains, I guess if deploy_url is available. 39 | ['script', { src: `${baseTemp.base}siteinfo.js` }] 40 | ], 41 | ignoreDeadLinks: true, 42 | 43 | markdown: { 44 | math: true, 45 | config(md) { 46 | md.use(tabsMarkdownPlugin), 47 | md.use(mathjax3), 48 | md.use(footnote) 49 | }, 50 | theme: { 51 | light: "github-light", 52 | dark: "github-dark" 53 | } 54 | }, 55 | themeConfig: { 56 | outline: 'deep', 57 | logo: '/logo.svg', 58 | search: { 59 | provider: 'local', 60 | options: { 61 | detailedView: true 62 | } 63 | }, 64 | nav, 65 | sidebar: 'REPLACE_ME_DOCUMENTER_VITEPRESS', 66 | editLink: 'REPLACE_ME_DOCUMENTER_VITEPRESS', 67 | socialLinks: [ 68 | // { icon: 'github', link: 'https://github.com/Deltares/Geomorphometry.jl' }, 69 | { icon: 'linkedin', link: 'https://www.linkedin.com/in/mjpronk/' }, 70 | { icon: 'mastodon', link: 'https://fosstodon.org/@evetion' }, 71 | ], 72 | footer: { 73 | message: 'Made with DocumenterVitepress.jl
', 74 | copyright: `© Copyright ${new Date().getUTCFullYear()}.` 75 | } 76 | } 77 | }) 78 | -------------------------------------------------------------------------------- /docs/make.jl: -------------------------------------------------------------------------------- 1 | using Revise 2 | using Geomorphometry 3 | using Documenter 4 | using DocumenterVitepress 5 | using CairoMakie 6 | using DocumenterCitations 7 | using Downloads 8 | 9 | Revise.revise() 10 | dir = @__DIR__ 11 | 12 | bib = CitationBibliography(joinpath(@__DIR__, "src", "refs.bib"); style = :authoryear) 13 | cp(joinpath(dir, "../CHANGELOG.md"), joinpath(dir, "src/CHANGELOG.md"); force = true) 14 | CairoMakie.activate!(; type = "png") 15 | 16 | fn = joinpath(dir, "src", "saba.tif") 17 | isfile(fn) || Downloads.download( 18 | "https://github.com/Deltares/Geomorphometry.jl/releases/download/v0.6.0/saba.tif", 19 | fn, 20 | ) 21 | fn = joinpath(dir, "src", "saba_dsm.tif") 22 | isfile(fn) || Downloads.download( 23 | "https://github.com/Deltares/Geomorphometry.jl/releases/download/v0.6.0/saba_dsm.tif", 24 | fn, 25 | ) 26 | fn = joinpath(dir, "src", "Copernicus_DSM_10_N52_00_E004_00_DEM.tif") 27 | isfile(fn) || Downloads.download( 28 | "https://github.com/Deltares/Geomorphometry.jl/releases/download/v0.6.0/Copernicus_DSM_10_N52_00_E004_00_DEM.tif", 29 | fn, 30 | ) 31 | 32 | DocMeta.setdocmeta!( 33 | Geomorphometry, 34 | :DocTestSetup, 35 | :(using Geomorphometry); 36 | recursive = true, 37 | ) 38 | 39 | makedocs(; 40 | modules = [Geomorphometry], 41 | authors = "Maarten Pronk and contributors", 42 | repo = "https://github.com/Deltares/Geomorphometry.jl/blob/{commit}{path}#L{line}", 43 | sitename = "Geomorphometry.jl", 44 | format = MarkdownVitepress(; 45 | repo = "github.com/Deltares/Geomorphometry.jl", 46 | # md_output_path = ".", 47 | # build_vitepress = false, 48 | devbranch = "main", 49 | ), 50 | doctest = true, 51 | checkdocs = :all, 52 | pages = [ 53 | "Home" => "index.md", 54 | "Getting started" => Any[ 55 | "Installation" => "installation.md", 56 | "Usage" => "usage.md", 57 | "Experimental" => "experimental.md", 58 | ], 59 | "Background" => Any["Concepts" => "concepts.md", "Future plans" => "todo.md"], 60 | "Reference" => Any[ 61 | "Validation" => "validation.md", 62 | "API" => "reference.md", 63 | "Changelog" => "CHANGELOG.md", 64 | "Bibliography" => "bibliography.md", 65 | ], 66 | ], 67 | # clean = false, 68 | plugins = [bib], 69 | warnonly = [:missing_docs, :cross_references], 70 | ) 71 | 72 | DocumenterVitepress.deploydocs(; 73 | repo = "github.com/Deltares/Geomorphometry.jl.git", 74 | target = joinpath(@__DIR__, "build"), 75 | devbranch = "main", 76 | branch = "gh-pages", 77 | push_preview = true, 78 | ) 79 | -------------------------------------------------------------------------------- /src/skew.jl: -------------------------------------------------------------------------------- 1 | """ 2 | mask = skb(A; mean=mean(A)) 3 | 4 | Applies skewness balancing by [Bartels e.a (2006)](@cite bartelsDTMGenerationLIDAR2006) to `A`. 5 | Improved the performance by applying a binary search to find the threshold value. 6 | 7 | # Output 8 | - `mask::BitMatrix` Mask of allowed values 9 | """ 10 | function skb(iA::AbstractArray; mean = _mean(iA)) 11 | 12 | # Replace infinite values with maxintfloat 13 | mask = .!isfinite.(iA) 14 | if sum(mask) > 0 15 | A = copy(iA) 16 | A[mask] .= maxintfloat(eltype(A)) 17 | else 18 | A = iA 19 | end 20 | 21 | # Sort A and get the indices 22 | I = sortperm(vec(A)) 23 | AA = A[I] 24 | II = invperm(I) 25 | 26 | skew = 1 27 | splitby = 2 28 | len = step = i = length(AA) - sum(mask) 29 | 30 | # Search for the threshold value using a binary search 31 | while step >= 1 32 | skew = skewness(view(AA, firstindex(AA):i)) 33 | step = len ÷ splitby 34 | splitby <<= 1 35 | if skew > 0 36 | i -= step 37 | else 38 | i += step 39 | end 40 | 1 <= i <= len || break 41 | end 42 | if skew <= 0 || i < 1 43 | i += 1 44 | end 45 | 46 | fill!(mask, true) 47 | mask[I[i:end]] .= false 48 | return mask 49 | end 50 | 51 | function skb2(iA::AbstractArray; mean = mean(iA)) 52 | m = .!isfinite.(iA) 53 | if sum(m) > 0 54 | A = copy(iA) 55 | A[m] .= maxintfloat(eltype(A)) 56 | else 57 | A = iA 58 | end 59 | I = sortperm(vec(A)) 60 | AA = A[I] 61 | AAA = copy(AA) 62 | s = 1 63 | d = 2 64 | step = length(AA) 65 | 66 | S = sum(AA) 67 | N = length(AA) 68 | 69 | i = length(AA) - sum(m) 70 | while i > 0 71 | s = skewness(AA[begin:i]) 72 | if s <= 0 73 | break 74 | end 75 | i -= 1 76 | end 77 | fill!(m, true) 78 | m[I[i:end]] .= false 79 | return m 80 | end 81 | 82 | _mean(A::AbstractArray) = mean(filter(isfinite, vec(A))) 83 | 84 | """ 85 | mask = skbr(A; iterations=10) 86 | 87 | Applies recursive skewness balancing by [Bartels e.a (2010)](@cite bartelsThresholdfreeObjectGround2010) to `A`. 88 | Applies `skb` `iterations` times to the object (non-terrain) mask, as to include 89 | more (sloped) terrain. 90 | 91 | # Output 92 | - `mask::BitMatrix` Mask of allowed values 93 | """ 94 | function skbr(A::AbstractMatrix{<:Real}; iterations = 1, mean = _mean(A)) 95 | @info mean 96 | terrain_mask = skb(A; mean) 97 | object_mask = .!terrain_mask 98 | while iterations > 1 && sum(object_mask) > 0 99 | @info "Iteration $iterations" 100 | terrain_mask[object_mask] .|= skb(A[object_mask]) 101 | object_mask .= .!terrain_mask 102 | iterations -= 1 103 | end 104 | terrain_mask 105 | end 106 | -------------------------------------------------------------------------------- /docs/src/validation.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | 3 | ```@setup plots 4 | using Geomorphometry, CairoMakie, GeoArrays, Rasters 5 | set_theme!(theme_minimal(); transparency = true) 6 | CairoMakie.activate!(type = "png") 7 | fn = "saba.tif" 8 | A = GeoArrays.read(fn) 9 | demr = Raster(fn; checkmem=false) 10 | dem = coalesce(A, NaN) 11 | ndem = GeoArrays.flipud!(deepcopy(dem)) 12 | # ndem = reverse(dem, dims=2) 13 | ``` 14 | 15 | This chapter compares our output with that of [gdaldem](https://gdal.org/en/stable/programs/gdaldem.html). 16 | 17 | We test with the same elevation model as that in the [Usage](usage.md) page, and also include an upside down version of it, to ensure that the algorithms are robust to different axes conventions. 18 | 19 | ```@example plots 20 | @info Geomorphometry.cellsize(dem) # default negative y spacing 21 | @info Geomorphometry.cellsize(ndem) # reversed y spacing 22 | ``` 23 | 24 | ## Hillshade 25 | 26 | :::tabs 27 | 28 | == Ours 29 | ```@example plots 30 | heatmap(hillshade(dem)) 31 | ``` 32 | == Ours (reverse y-axis) 33 | ```@example plots 34 | heatmap(hillshade(ndem)) 35 | ``` 36 | == GDAL 37 | ```@example plots 38 | heatmap(hillshade(Geomorphometry.GDAL(), dem)) 39 | ``` 40 | == GDAL (reverse y-axis) 41 | ```@example plots 42 | heatmap(hillshade(Geomorphometry.GDAL(), ndem)) 43 | ``` 44 | 45 | ::: 46 | 47 | ## Multihillshade 48 | 49 | :::tabs 50 | 51 | == Ours 52 | ```@example plots 53 | heatmap(multihillshade(dem)) 54 | ``` 55 | == Ours (reverse y-axis) 56 | ```@example plots 57 | heatmap(multihillshade(ndem)) 58 | ``` 59 | == GDAL 60 | ```@example plots 61 | heatmap(multihillshade(Geomorphometry.GDAL(), dem)) 62 | ``` 63 | == GDAL (reverse y-axis) 64 | ```@example plots 65 | heatmap(multihillshade(Geomorphometry.GDAL(), ndem)) 66 | ``` 67 | ::: 68 | 69 | ## Slope 70 | 71 | ### Horn 72 | :::tabs 73 | 74 | == Ours 75 | ```@example plots 76 | heatmap(slope(dem, method=Horn())) 77 | ``` 78 | == Ours (reverse y-axis) 79 | ```@example plots 80 | heatmap(slope(ndem, method=Horn())) 81 | ``` 82 | == GDAL 83 | ```@example plots 84 | heatmap(slope(Geomorphometry.GDAL(), dem, method=Horn())) 85 | ``` 86 | == GDAL (reverse y-axis) 87 | ```@example plots 88 | heatmap(slope(Geomorphometry.GDAL(), ndem, method=Horn())) 89 | ``` 90 | 91 | ::: 92 | 93 | ### ZevenbergenThorne 94 | 95 | :::tabs 96 | 97 | == Ours 98 | ```@example plots 99 | heatmap(slope(dem, method=ZevenbergenThorne())) 100 | ``` 101 | == Ours (reverse y-axis) 102 | ```@example plots 103 | heatmap(slope(ndem, method=ZevenbergenThorne())) 104 | ``` 105 | == GDAL 106 | ```@example plots 107 | heatmap(slope(Geomorphometry.GDAL(), dem, method=ZevenbergenThorne())) 108 | ``` 109 | == GDAL (reverse y-axis) 110 | ```@example plots 111 | heatmap(slope(Geomorphometry.GDAL(), ndem, method=ZevenbergenThorne())) 112 | ``` 113 | 114 | ::: 115 | 116 | 117 | ## Aspect 118 | 119 | ### Horn 120 | 121 | :::tabs 122 | 123 | == Ours 124 | ```@example plots 125 | heatmap(aspect(dem, method=Horn())) 126 | ``` 127 | == Ours (reverse y-axis) 128 | ```@example plots 129 | heatmap(aspect(ndem, method=Horn())) 130 | ``` 131 | == GDAL 132 | ```@example plots 133 | heatmap(aspect(Geomorphometry.GDAL(), dem, method=Horn())) 134 | ``` 135 | == GDAL (reverse y-axis) 136 | ```@example plots 137 | heatmap(aspect(Geomorphometry.GDAL(), ndem, method=Horn())) 138 | ``` 139 | 140 | ::: 141 | 142 | 143 | ### ZevenbergenThorne 144 | 145 | :::tabs 146 | 147 | == Ours 148 | ```@example plots 149 | heatmap(aspect(dem, method=ZevenbergenThorne())) 150 | ``` 151 | == Ours (reverse y-axis) 152 | ```@example plots 153 | heatmap(aspect(ndem, method=ZevenbergenThorne())) 154 | ``` 155 | == GDAL 156 | ```@example plots 157 | heatmap(aspect(Geomorphometry.GDAL(), dem, method=ZevenbergenThorne())) 158 | ``` 159 | == GDAL (reverse y-axis) 160 | ```@example plots 161 | heatmap(aspect(Geomorphometry.GDAL(), ndem, method=ZevenbergenThorne())) 162 | ``` 163 | 164 | ::: 165 | -------------------------------------------------------------------------------- /src/plot.jl: -------------------------------------------------------------------------------- 1 | # using RecipesBase 2 | """ 3 | image = pssm(dem; exaggeration=2.3, resolution=1.0) 4 | 5 | Perceptually Shaded Slope Map by [Pingel, Clarke., (2014)](@cite pingelPerceptuallyShadedSlope2014a). 6 | 7 | # Output 8 | - `image::Gray{T,2}` Grayscale image 9 | 10 | # Arguments 11 | - `A::Array{Real,2}` Input Array 12 | - `exaggeration::Real=2.3` Factor to exaggerate elevation 13 | - `cellsize::Real=1.0` Size of cell to account for horizontal resolution if different from vertical resolution 14 | """ 15 | function pssm( 16 | dem::AbstractMatrix{<:Real}; 17 | exaggeration = 2.3, 18 | cellsize = cellsize(dem), 19 | method = Horn(), 20 | ) 21 | slope(dem; cellsize, method, exaggeration) 22 | end 23 | 24 | """ 25 | hillshade(dem::Matrix{<:Real}; azimuth=315.0, zenith=45.0, cellsize=cellsize(dem)) 26 | 27 | hillshade is the simulated illumination of a surface based on its [`slope`](@ref) and 28 | [`aspect`](@ref) given a light source with azimuth and zenith angles in °, as defined in 29 | [Burrough, P. A., and McDonell, R. A., (1998)](@cite burroughPrinciplesGeographicalInformation2015). 30 | """ 31 | function hillshade( 32 | dem::AbstractMatrix{<:Real}; 33 | azimuth = 315.0, 34 | zenith = 45.0, 35 | cellsize = cellsize(dem), 36 | ) 37 | dst = similar(dem, Union{Missing, UInt8}) 38 | zenithr = deg2rad(zenith) 39 | azimuthr = deg2rad(azimuth) 40 | 41 | initial(A) = 42 | (zero(eltype(A)), zero(eltype(A)), zero(eltype(A)), zero(eltype(A)), cellsize) 43 | function store!(d, i, v) 44 | δzδx, δzδy = (v[3] - v[4]) / (8 * v[5][1]), (v[1] - v[2]) / (8 * v[5][2]) 45 | if δzδx != 0 46 | a = atan(δzδx, δzδy) 47 | if a < 0 48 | a += 2π 49 | end 50 | else 51 | a = π / 2 52 | if δzδy < 0 53 | a += 2π 54 | end 55 | end 56 | slope = atan(√(δzδx^2 + δzδy^2)) 57 | something = max( 58 | 0, 59 | 255 * ( 60 | (cos(zenithr) * cos(slope)) + 61 | (sin(zenithr) * sin(slope) * cos(azimuthr - a)) 62 | ), 63 | ) 64 | d[i] = isfinite(something) ? round(UInt8, max(0, something)) : missing 65 | end 66 | return localfilter!(dst, dem, nbkernel, initial, horn, store!) 67 | end 68 | 69 | """ 70 | multihillshade(dem::AbstractMatrix{<:Real}; cellsize=cellsize(dem)) 71 | 72 | multihillshade is the simulated illumination of a surface based on its [`slope`](@ref) and 73 | [`aspect`](@ref). Like [`hillshade`](@ref), but now using multiple sources as defined in 74 | [Mark, R.K. (1992)](@cite mark1992multidirectional), similar to GDALs -multidirectional. 75 | """ 76 | function multihillshade( 77 | dem::AbstractMatrix{<:Real}; 78 | azimuth = [225, 270, 315, 360], 79 | zenith = 45.0, 80 | cellsize = cellsize(dem), 81 | ) 82 | dst = similar(dem, Union{Missing, UInt8}) 83 | zenithr = deg2rad(zenith) 84 | 85 | initial(A) = 86 | (zero(eltype(A)), zero(eltype(A)), zero(eltype(A)), zero(eltype(A)), cellsize) 87 | function store!(d, i, v) 88 | δzδx, δzδy = (v[3] - v[4]) / (8 * v[5][1]), (v[1] - v[2]) / (8 * v[5][2]) 89 | if δzδx != 0 90 | a = atan(δzδx, δzδy) 91 | if a < 0 92 | a += 2π 93 | end 94 | else 95 | a = π / 2 96 | if δzδy < 0 97 | a += 2π 98 | end 99 | end 100 | slope = atan(√(δzδx^2 + δzδy^2)) 101 | 102 | w225 = sin(a - deg2rad(225 - 90))^2 103 | w270 = sin(a - deg2rad(270 - 90))^2 104 | w315 = sin(a - deg2rad(315 - 90))^2 105 | w360 = sin(a - deg2rad(360 - 90))^2 106 | 107 | α = cos(zenithr) * cos(slope) 108 | β = sin(zenithr) * sin(slope) 109 | weights = 0 110 | something = 0 111 | for az in azimuth 112 | weight = sin(a - deg2rad(az - 90))^2 113 | weights += weight 114 | something += weight * (α + β * cos(deg2rad(az) - a)) 115 | end 116 | something /= weights 117 | 118 | d[i] = isfinite(something) ? round(UInt8, max(0, 255 * something)) : missing 119 | end 120 | return localfilter!(dst, dem, nbkernel, initial, horn, store!) 121 | end 122 | -------------------------------------------------------------------------------- /ext/GeomorphometryGeoArraysExt.jl: -------------------------------------------------------------------------------- 1 | module GeomorphometryGeoArraysExt 2 | 3 | using Geomorphometry 4 | using GeoArrays 5 | using FillArrays 6 | 7 | degwidth::Float64 = 111_000.0 8 | 9 | function Geomorphometry.cellsize(dem::GeoArray) 10 | T = GeoArrays.GI.crstrait(dem) 11 | _cellsize(T, dem) 12 | end 13 | 14 | _cellsize(::GeoArrays.GI.AbstractProjectedTrait, dem::GeoArray) = 15 | (dem.f.linear[1], dem.f.linear[4]) 16 | function _cellsize(::GeoArrays.GI.AbstractGeographicTrait, dem::GeoArray) 17 | centercoords = GeoArrays.coords(dem, round.(Int, size(dem)[1:2] ./ 2)) 18 | (dem.f.linear[1] * degwidth * cosd(centercoords[2]), dem.f.linear[4] * degwidth) 19 | end 20 | 21 | function Geomorphometry.slope( 22 | ::Geomorphometry.GDAL, 23 | dem::GeoArray, 24 | cellsize = Geomorphometry.cellsize(dem), 25 | method = Geomorphometry.Horn(); 26 | kwargs..., 27 | ) 28 | T = GeoArrays.GI.crstrait(dem) 29 | options = GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsnew( 30 | [ 31 | "-alg", 32 | string(typeof(method)), 33 | "-s", 34 | T isa GeoArrays.GI.AbstractGeographicTrait ? "111120" : "1", 35 | "-of", 36 | "GTiff", 37 | ], 38 | C_NULL, 39 | ) 40 | fn_out = tempname() * ".tif" 41 | GeoArrays.ArchGDAL.Dataset(dem) do ds 42 | ds_dempr = GeoArrays.ArchGDAL.GDAL.gdaldemprocessing( 43 | fn_out, 44 | ds.ptr, 45 | "slope", 46 | C_NULL, 47 | options, 48 | C_NULL, 49 | ) 50 | GeoArrays.ArchGDAL.GDAL.gdalclose(ds_dempr) 51 | end 52 | GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsfree(options) 53 | GeoArrays.read(fn_out) 54 | end 55 | 56 | function Geomorphometry.aspect( 57 | ::Geomorphometry.GDAL, 58 | dem::GeoArray, 59 | cellsize = Geomorphometry.cellsize(dem), 60 | method = Geomorphometry.Horn(); 61 | kwargs..., 62 | ) 63 | options = GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsnew( 64 | ["-alg", string(typeof(method)), "-of", "GTiff"], 65 | C_NULL, 66 | ) 67 | fn_out = tempname() * ".tif" 68 | GeoArrays.ArchGDAL.Dataset(dem) do ds 69 | ds_dempr = GeoArrays.ArchGDAL.GDAL.gdaldemprocessing( 70 | fn_out, 71 | ds.ptr, 72 | "aspect", 73 | C_NULL, 74 | options, 75 | C_NULL, 76 | ) 77 | GeoArrays.ArchGDAL.GDAL.gdalclose(ds_dempr) 78 | end 79 | GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsfree(options) 80 | GeoArrays.read(fn_out) 81 | end 82 | 83 | function Geomorphometry.hillshade( 84 | ::Geomorphometry.GDAL, 85 | dem::GeoArray, 86 | cellsize = Geomorphometry.cellsize(dem), 87 | method = Geomorphometry.Horn(); 88 | kwargs..., 89 | ) 90 | T = GeoArrays.GI.crstrait(dem) 91 | options = GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsnew( 92 | [ 93 | "-alg", 94 | string(typeof(method)), 95 | "-s", 96 | T isa GeoArrays.GI.AbstractGeographicTrait ? "111120" : "1", 97 | "-of", 98 | "GTiff", 99 | ], 100 | C_NULL, 101 | ) 102 | fn_out = tempname() * ".tif" 103 | GeoArrays.ArchGDAL.Dataset(dem) do ds 104 | ds_dempr = GeoArrays.ArchGDAL.GDAL.gdaldemprocessing( 105 | fn_out, 106 | ds.ptr, 107 | "hillshade", 108 | C_NULL, 109 | options, 110 | C_NULL, 111 | ) 112 | GeoArrays.ArchGDAL.GDAL.gdalclose(ds_dempr) 113 | end 114 | GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsfree(options) 115 | GeoArrays.read(fn_out) 116 | end 117 | 118 | function Geomorphometry.multihillshade( 119 | ::Geomorphometry.GDAL, 120 | dem::GeoArray; 121 | method = Geomorphometry.Horn(), 122 | kwargs..., 123 | ) 124 | options = GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsnew( 125 | ["-multidirectional", "-alg", string(typeof(method)), "-of", "GTiff"], 126 | C_NULL, 127 | ) 128 | fn_out = tempname() * ".tif" 129 | GeoArrays.ArchGDAL.Dataset(dem) do ds 130 | ds_dempr = GeoArrays.ArchGDAL.GDAL.gdaldemprocessing( 131 | fn_out, 132 | ds.ptr, 133 | "hillshade", 134 | C_NULL, 135 | options, 136 | C_NULL, 137 | ) 138 | GeoArrays.ArchGDAL.GDAL.gdalclose(ds_dempr) 139 | end 140 | GeoArrays.ArchGDAL.GDAL.gdaldemprocessingoptionsfree(options) 141 | GeoArrays.read(fn_out) 142 | end 143 | 144 | end # module 145 | -------------------------------------------------------------------------------- /docs/src/concepts.md: -------------------------------------------------------------------------------- 1 | # Concepts 2 | 3 | In Geomorphometry.jl we provide a set of tools to analyse and visualize the shape of the Earth. 4 | The package is designed to be fast, flexible, and easy to use. It is built with the following concepts in mind: 5 | 6 | ## Geospatially aware 7 | With package extensions on [GeoArrays.jl](https://github.com/evetion/GeoArrays.jl) and [Rasters.jl](https://github.com/rafaqz/Rasters.jl) geospatial data is automatically handled to set the correct cellsize, even for geographical DEMs. 8 | 9 | ```@setup plots 10 | using Geomorphometry, CairoMakie, GeoArrays, Rasters 11 | set_theme!(theme_minimal(); transparency = true) 12 | CairoMakie.activate!(type = "png") 13 | r = GeoArrays.read("saba.tif") 14 | mask = ismissing.(r) 15 | dtm = coalesce(r, NaN) 16 | ``` 17 | 18 | :::tabs 19 | 20 | == Rasters (projected) 21 | ```@example plots 22 | r = Raster("saba.tif") 23 | r = coalesce.(r, NaN) # hide 24 | Geomorphometry.cellsize(r) 25 | ``` 26 | ```@example plots 27 | heatmap(multihillshade(r)) 28 | ``` 29 | 30 | == GeoArrays (projected) 31 | ```@example plots 32 | r = GeoArrays.read("saba.tif") 33 | r = coalesce(r, NaN) # hide 34 | Geomorphometry.cellsize(r) 35 | ``` 36 | ```@example plots 37 | heatmap(multihillshade(r)) 38 | ``` 39 | 40 | == Rasters (geographic) 41 | ```@example plots 42 | r = Raster("Copernicus_DSM_10_N52_00_E004_00_DEM.tif") 43 | r = coalesce.(r, NaN) # hide 44 | Geomorphometry.cellsize(r) 45 | ``` 46 | ```@example plots 47 | heatmap(multihillshade(r)) 48 | ``` 49 | 50 | == GeoArrays (geographic) 51 | ```@example plots 52 | r = GeoArrays.read("Copernicus_DSM_10_N52_00_E004_00_DEM.tif") 53 | r = coalesce(r, NaN) # hide 54 | Geomorphometry.cellsize(r) 55 | ``` 56 | ```@example plots 57 | heatmap(multihillshade(r)) 58 | ``` 59 | 60 | ::: 61 | 62 | ## Multiple algorithms 63 | We have implemented several algorithms for a common operation so that you can choose the one that best fits your needs. For example, the `slope` function has three methods: `Horn`, `ZevenbergenThorne`, and `MaximumDownwardGradient`, as shown in the [Usage](usage.md) section. 64 | 65 | Sometimes, as is the case for the `FD8` algorithm, these methods take different parameters that influence the results. `FD8` takes a `p` parameter that is used to weigh the flow direction, with higher powers resulting in less divergent flows (and thus more like D8). 66 | 67 | :::tabs 68 | 69 | == FD8 with power of 1 70 | ```@example plots 71 | acc, ldd = flowaccumulation(dtm; method=FD8(1)) 72 | acc[mask] .= NaN # hide 73 | heatmap(log10.(acc); colormap=:rain) 74 | ``` 75 | == FD8 with power of 2 76 | ```@example plots 77 | acc, ldd = flowaccumulation(dtm; method=FD8(2)) 78 | acc[mask] .= NaN # hide 79 | heatmap(log10.(acc); colormap=:rain) 80 | ``` 81 | == FD8 with power of 5 82 | ```@example plots 83 | acc, ldd = flowaccumulation(dtm; method=FD8(5)) 84 | acc[mask] .= NaN # hide 85 | heatmap(log10.(acc); colormap=:rain) 86 | ``` 87 | == D8 (single flow direction) 88 | ```@example plots 89 | acc, ldd = flowaccumulation(dtm; method=D8()) 90 | acc[mask] .= NaN # hide 91 | heatmap(log10.(acc); colormap=:rain) 92 | ``` 93 | 94 | ::: 95 | 96 | 97 | ## Multiple scales 98 | Inspired by the excellent [MultiScaleDTM](https://github.com/ailich/MultiscaleDTM) package in R by [ilichMultiscaleDTMOpensourcePackage2023](@citet), we have added multiscale options to some filters. 99 | 100 | Relative terrain filters have a `window` keyword argument for a Stencil from [Stencils.jl](https://github.com/rafaqz/Stencils.jl) package. 101 | 102 | :::tabs 103 | 104 | == Square window of 1 105 | ```@example plots 106 | Geomorphometry.Moore(1) 107 | ``` 108 | ```@example plots 109 | heatmap(TPI(dtm, Geomorphometry.Moore(1)); colorrange=(0,25), colormap=:speed) 110 | ``` 111 | 112 | == Square window of 3 113 | ```@example plots 114 | Geomorphometry.Moore(3) 115 | ``` 116 | ```@example plots 117 | heatmap(TPI(dtm, Geomorphometry.Moore(3)); colorrange=(0,25), colormap=:speed) 118 | ``` 119 | 120 | == Square window of 5 121 | ```@example plots 122 | Geomorphometry.Moore(5) 123 | ``` 124 | ```@example plots 125 | heatmap(TPI(dtm, Geomorphometry.Moore(5)); colorrange=(0,25), colormap=:speed) 126 | ``` 127 | 128 | ::: 129 | 130 | 131 | Other methods that require a specific type of window now take a `radius` kwarg, scaling said window. 132 | 133 | :::tabs 134 | 135 | == Radius of 1 136 | ```@example plots 137 | Geomorphometry.scaled8nb(1) 138 | ``` 139 | ```@example plots 140 | heatmap(profile_curvature(dtm, radius=1); colorrange=(-1,1), colormap=:tarn) 141 | ``` 142 | == Radius of 3 143 | ```@example plots 144 | Geomorphometry.scaled8nb(3) 145 | ``` 146 | ```@example plots 147 | heatmap(profile_curvature(dtm, radius=3); colorrange=(-1,1), colormap=:tarn) 148 | ``` 149 | == Radius of 5 150 | ```@example plots 151 | Geomorphometry.scaled8nb(5) 152 | ``` 153 | ```@example plots 154 | heatmap(profile_curvature(dtm, radius=5); colorrange=(-1,1), colormap=:tarn) 155 | ``` 156 | 157 | ::: 158 | -------------------------------------------------------------------------------- /docs/src/components/VersionPicker.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 118 | 119 | 134 | 135 | 143 | -------------------------------------------------------------------------------- /docs/src/components/StarUs.vue: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 52 | 53 | -------------------------------------------------------------------------------- /src/utils.jl: -------------------------------------------------------------------------------- 1 | # """Apply the opening operation to `A` with window size `ω`.""" 2 | # function opening(A::AbstractArray{T, 2}, ω::Integer) where {T <: Real} 3 | # A = mapwindow(minimum, A, ω) # erosion 4 | # A = mapwindow(maximum, A, ω) # dilation 5 | # A 6 | # end 7 | 8 | """Apply the opening operation to `A` with window size `ω`.""" 9 | function opening!( 10 | A::AbstractArray{T, 2}, 11 | ω::Integer, 12 | out::AbstractArray{T, 2}, 13 | ) where {T <: Real} 14 | mapwindow_sep!(minimum, A, ω, out, Inf) # erosion 15 | mapwindow_sep!(maximum, out, ω, A, -Inf) # dilation 16 | A 17 | end 18 | 19 | """Apply the opening operation to `A` with window size `ω`.""" 20 | function opening_circ!( 21 | A::AbstractArray{T, 2}, 22 | ω::Integer, 23 | out::AbstractArray{T, 2}, 24 | ) where {T <: Real} 25 | mapwindowcirc_approx!(minimum_mask, A, ω, out, Inf) # erosion 26 | mapwindowcirc_approx!(maximum_mask, out, ω, A, -Inf) # dilation 27 | A 28 | end 29 | 30 | function circmask(n::Integer) 31 | # TODO This could be precomputed for first N integers 32 | kern = falses((-n):n, (-n):n) 33 | for I in CartesianIndices(kern) 34 | i, j = Tuple(I) 35 | kern[I] = i^2 + j^2 <= n^2 36 | end 37 | return kern.parent 38 | end 39 | 40 | function opening_circ(A::AbstractMatrix{<:Real}, ω::Integer) 41 | window = Stencils.Circle(ω) 42 | A = mapstencil(minimum, window, A) 43 | A = mapstencil(maximum, window, A) 44 | A 45 | end 46 | 47 | # First discussed here https://github.com/JuliaImages/ImageFiltering.jl/issues/179 48 | function mapwindow(f, img, window) 49 | out = copy(img) 50 | mapwindow!(f, img, window, out) 51 | end 52 | 53 | function mapwindow!(f, img, window, out) 54 | R = CartesianIndices(img) 55 | I_first, I_last = first(R), last(R) 56 | Δ = CartesianIndex(ntuple(x -> window ÷ 2, ndims(img))) 57 | @inbounds for I in R 58 | patch = max(I_first, I - Δ):min(I_last, I + Δ) 59 | out[I] = f(view(img, patch)) 60 | end 61 | out 62 | end 63 | 64 | function mapwindow_stack!(f, img, window, out) 65 | R = CartesianIndices(img) 66 | I_first, I_last = first(R), last(R) 67 | Δ = CartesianIndex(1, 1) 68 | out2 = copy(img) 69 | iterations = window:-2:3 70 | @inbounds for _ in iterations # repeat 3x3 window 71 | for I in R 72 | patch = max(I_first, I - Δ):min(I_last, I + Δ) 73 | out[I] = f(view(out2, patch)) 74 | end 75 | out2 .= out 76 | end 77 | out 78 | end 79 | 80 | function mapwindow_sep!(f, img, window, out, fill = Inf) 81 | Δ = window ÷ 2 82 | 83 | w, h = size(img) 84 | A = PaddedView(fill, img, ((-Δ + 1):(w + Δ), (-Δ + 1):(h + Δ))) 85 | out2 = copy(out) 86 | 87 | # Maximum/minimum is seperable into 1d 88 | @inbounds for i in 1:h, j in 1:w 89 | out2[j, i] = f(@view A[(j - Δ):(j + Δ), i]) 90 | end 91 | A = PaddedView(fill, out2, ((-Δ + 1):(w + Δ), (-Δ + 1):(h + Δ))) 92 | @inbounds for j in 1:w, i in 1:h 93 | out[j, i] = f(@view A[j, (i - Δ):(i + Δ)]) 94 | end 95 | out 96 | end 97 | 98 | function mapwindowcirc!(f, img, window, out, fill = Inf) 99 | R = CartesianIndices(img) 100 | Δ = CartesianIndex(ntuple(x -> window ÷ 2, ndims(img))) 101 | 102 | w, h = size(img) 103 | A = PaddedView(fill, img, ((-Δ[1] + 1):(w + Δ[1]), (-Δ[2] + 1):(h + Δ[2]))) 104 | 105 | m = euclidean.(Tuple.((-Δ):Δ), Ref((0, 0))) .<= Δ[1] 106 | @inbounds for I in R 107 | patch = (I - Δ):(I + Δ) 108 | out[I] = f(view(A, patch), m) 109 | end 110 | out 111 | end 112 | 113 | function mapwindowcirc_approx!(f, img, window, out = copy(img), fill = Inf) 114 | R = CartesianIndices(img) 115 | Δ = CartesianIndex(1, 1) 116 | 117 | w, h = size(img) 118 | 119 | iterations = window:-2:3 120 | A = PaddedView(fill, img, ((-Δ[1] + 1):(w + Δ[1]), (-Δ[2] + 1):(h + Δ[2]))) 121 | 122 | m = euclidean.(Tuple.((-Δ):Δ), Ref((0, 0))) .<= Δ[1] 123 | @inbounds for _ in iterations # repeat 3x3 window 124 | for I in R 125 | patch = (I - Δ):(I + Δ) 126 | out[I] = f(view(A, patch), m) 127 | end 128 | img .= out 129 | end 130 | out 131 | end 132 | edge(x) = x == 1 || x == 3 || x == 7 || x == 9 133 | const xx = circmask(1) 134 | function mapwindowcirc_approx2!(f, A, window, out, fill = Inf) 135 | iterations = window:-2:3 136 | for _ in iterations 137 | localfilter!( 138 | out, 139 | A, 140 | 3, 141 | (a) -> (fill, 1), 142 | (v, a, _) -> ((edge(v[2]) ? f(v[1], a) : v[1]), v[2] + 1), 143 | (d, i, v) -> d[i] = v[1], 144 | ) 145 | A .= out 146 | end 147 | out 148 | end 149 | 150 | # Functions for future changes, based on LocalFiltering 151 | function opening_circ_approx2!( 152 | A::Array{T, 2}, 153 | ω::Integer, 154 | out::Array{T, 2}, 155 | ) where {T <: Real} 156 | iterations = ω:-2:3 157 | 158 | B = circmask(1) 159 | for _ in iterations 160 | localfilter!( 161 | out, 162 | A, 163 | B, 164 | (a) -> typemax(a), 165 | (v, a, b) -> b ? min(v, a) : v, 166 | (d, i, v) -> @inbounds(d[i] = v), 167 | ) 168 | A, out = out, A 169 | end 170 | for _ in iterations 171 | localfilter!( 172 | out, 173 | A, 174 | B, 175 | (a) -> typemin(a), 176 | (v, a, b) -> b ? max(v, a) : v, 177 | (d, i, v) -> @inbounds(d[i] = v), 178 | ) 179 | A, out = out, A 180 | end 181 | A 182 | end 183 | 184 | @inline function maximum_mask(x, m) 185 | o = -Inf 186 | @inbounds for I in eachindex(x) 187 | o = ifelse(m[I], max(o, x[I]), o) 188 | end 189 | o 190 | end 191 | 192 | @inline function minimum_mask(x, m) 193 | o = Inf 194 | @inbounds for I in eachindex(x) 195 | o = ifelse(m[I], min(o, x[I]), o) 196 | end 197 | o 198 | end 199 | 200 | """ 201 | cellsize(dem) 202 | 203 | Return an Tuple with the x and y length of each cell of the dem. 204 | Set them negatively to flip the image. 205 | """ 206 | function cellsize(dem) 207 | return (1.0, 1.0) 208 | end 209 | -------------------------------------------------------------------------------- /docs/src/.vitepress/theme/style.css: -------------------------------------------------------------------------------- 1 | /* Customize default theme styling by overriding CSS variables: 2 | https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css 3 | */ 4 | 5 | /* Layouts */ 6 | 7 | /* 8 | :root { 9 | --vp-layout-max-width: 1440px; 10 | } */ 11 | 12 | .VPHero .clip { 13 | white-space: pre; 14 | max-width: 500px; 15 | } 16 | 17 | /* Fonts */ 18 | 19 | @font-face { 20 | font-family: JuliaMono-Regular; 21 | src: url("https://cdn.jsdelivr.net/gh/cormullion/juliamono/webfonts/JuliaMono-Regular.woff2"); 22 | } 23 | 24 | :root { 25 | /* Typography */ 26 | --vp-font-family-base: "Barlow", "Inter var experimental", "Inter var", 27 | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, 28 | Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 29 | 30 | /* Code Snippet font */ 31 | --vp-font-family-mono: JuliaMono-Regular, monospace; 32 | 33 | } 34 | 35 | /* 36 | Disable contextual alternates (kind of like ligatures but different) in monospace, 37 | which turns `/>` to an up arrow and `|>` (the Julia pipe symbol) to an up arrow as well. 38 | This is pretty bad for Julia folks reading even though copy+paste retains the same text. 39 | */ 40 | /* Target elements with class 'mono' */ 41 | .mono-no-substitutions { 42 | font-family: "JuliaMono-Light", monospace; 43 | font-feature-settings: "calt" off; 44 | } 45 | 46 | /* Alternatively, you can use the following if you prefer: */ 47 | .mono-no-substitutions-alt { 48 | font-family: "JuliaMono-Light", monospace; 49 | font-variant-ligatures: none; 50 | } 51 | 52 | /* If you want to apply this globally to all monospace text: */ 53 | pre, 54 | code { 55 | font-family: "JuliaMono-Light", monospace; 56 | font-feature-settings: "calt" off; 57 | } 58 | 59 | /* Colors */ 60 | 61 | :root { 62 | --julia-blue: #4063D8; 63 | --julia-purple: #9558B2; 64 | --julia-red: #CB3C33; 65 | --julia-green: #389826; 66 | 67 | --vp-c-brand: #266398; 68 | --vp-c-brand-light: #281A2C; 69 | --vp-c-brand-lighter: #407398; 70 | --vp-c-brand-lightest: #89D5A3; 71 | --vp-c-brand-dark: #214C1E; 72 | --vp-c-brand-darker: #A78C3F; 73 | --vp-c-brand-dimm: #F1F1D2; 74 | } 75 | 76 | /* Component: Button */ 77 | 78 | :root { 79 | --vp-button-brand-border: var(--vp-c-brand-light); 80 | --vp-button-brand-text: var(--vp-c-white); 81 | --vp-button-brand-bg: var(--vp-c-brand); 82 | --vp-button-brand-hover-border: var(--vp-c-brand-light); 83 | --vp-button-brand-hover-text: var(--vp-c-white); 84 | --vp-button-brand-hover-bg: var(--vp-c-brand-light); 85 | --vp-button-brand-active-border: var(--vp-c-brand-light); 86 | --vp-button-brand-active-text: var(--vp-c-white); 87 | --vp-button-brand-active-bg: var(--vp-button-brand-bg); 88 | } 89 | 90 | /* Component: Home */ 91 | 92 | :root { 93 | --vp-home-hero-name-color: transparent; 94 | --vp-home-hero-name-background: -webkit-linear-gradient(-120deg, 95 | #F1F1D2, 96 | #A78C3F, 97 | #214C1E); 98 | 99 | --vp-home-hero-image-background-image: linear-gradient(-45deg, 100 | #9558B2 30%, 101 | #389826 30%, 102 | #CB3C33); 103 | --vp-home-hero-image-filter: blur(40px); 104 | } 105 | 106 | @media (min-width: 640px) { 107 | :root { 108 | --vp-home-hero-image-filter: blur(56px); 109 | } 110 | } 111 | 112 | @media (min-width: 960px) { 113 | :root { 114 | --vp-home-hero-image-filter: blur(72px); 115 | } 116 | } 117 | 118 | /* Component: Custom Block */ 119 | 120 | :root.dark { 121 | --vp-custom-block-tip-border: var(--vp-c-brand); 122 | --vp-custom-block-tip-text: var(--vp-c-brand-lightest); 123 | --vp-custom-block-tip-bg: var(--vp-c-brand-dimm); 124 | 125 | /* // Tweak the color palette for blacks and dark grays */ 126 | --vp-c-black: hsl(220 20% 9%); 127 | --vp-c-black-pure: hsl(220, 24%, 4%); 128 | --vp-c-black-soft: hsl(220 16% 13%); 129 | --vp-c-black-mute: hsl(220 14% 17%); 130 | --vp-c-gray: hsl(220 8% 56%); 131 | --vp-c-gray-dark-1: hsl(220 10% 39%); 132 | --vp-c-gray-dark-2: hsl(220 12% 28%); 133 | --vp-c-gray-dark-3: hsl(220 12% 23%); 134 | --vp-c-gray-dark-4: hsl(220 14% 17%); 135 | --vp-c-gray-dark-5: hsl(220 16% 13%); 136 | 137 | /* // Backgrounds */ 138 | /* --vp-c-bg: hsl(240, 2%, 11%); */ 139 | --vp-custom-block-info-bg: hsl(220 14% 17%); 140 | /* --vp-c-gutter: hsl(220 20% 9%); 141 | 142 | --vp-c-bg-alt: hsl(220 20% 9%); 143 | --vp-c-bg-soft: hsl(220 14% 17%); 144 | --vp-c-bg-mute: hsl(220 12% 23%); 145 | */ 146 | } 147 | 148 | /* Component: Algolia */ 149 | 150 | .DocSearch { 151 | --docsearch-primary-color: var(--vp-c-brand) !important; 152 | } 153 | 154 | /* Component: MathJax */ 155 | 156 | mjx-container>svg { 157 | display: block; 158 | margin: auto; 159 | } 160 | 161 | mjx-container { 162 | padding: 0.5rem 0; 163 | } 164 | 165 | mjx-container { 166 | display: inline; 167 | margin: auto 2px -2px; 168 | } 169 | 170 | mjx-container>svg { 171 | margin: auto; 172 | display: inline-block; 173 | } 174 | 175 | /** 176 | * Colors links 177 | * -------------------------------------------------------------------------- */ 178 | 179 | :root { 180 | --vp-c-brand-1: #CB3C33; 181 | --vp-c-brand-2: #CB3C33; 182 | --vp-c-brand-3: #CB3C33; 183 | --vp-c-sponsor: #ca2971; 184 | --vitest-c-sponsor-hover: #c13071; 185 | } 186 | 187 | .dark { 188 | --vp-c-brand-1: #91dd33; 189 | --vp-c-brand-2: #91dd33; 190 | --vp-c-brand-3: #91dd33; 191 | --vp-c-sponsor: #91dd33; 192 | --vitest-c-sponsor-hover: #e51370; 193 | } 194 | 195 | /** 196 | * Change images from light to dark theme 197 | * -------------------------------------------------------------------------- */ 198 | 199 | :root:not(.dark) .dark-only { 200 | display: none; 201 | } 202 | 203 | :root:is(.dark) .light-only { 204 | display: none; 205 | } 206 | 207 | /* https://bddxg.top/article/note/vitepress优化/一些细节上的优化.html#文档页面调整-加宽 */ 208 | 209 | .VPDoc.has-aside .content-container { 210 | max-width: 100% !important; 211 | } 212 | 213 | .aside { 214 | max-width: 200px !important; 215 | padding-left: 0 !important; 216 | } 217 | 218 | .VPDoc { 219 | padding-top: 15px !important; 220 | padding-left: 5px !important; 221 | 222 | } 223 | 224 | /* This one does the right menu */ 225 | 226 | .VPDocOutlineItem li { 227 | text-overflow: ellipsis; 228 | overflow: hidden; 229 | white-space: nowrap; 230 | max-width: 200px; 231 | } 232 | 233 | .VPNavBar .title { 234 | text-overflow: ellipsis; 235 | overflow: hidden; 236 | white-space: nowrap; 237 | } 238 | 239 | @media (max-width: 960px) { 240 | .VPDoc { 241 | padding-left: 25px !important; 242 | } 243 | } 244 | 245 | /* This one does the left menu */ 246 | 247 | /* .VPSidebarItem .VPLink p { 248 | text-overflow: ellipsis; 249 | overflow: hidden; 250 | white-space: nowrap; 251 | max-width: 200px; 252 | } */ 253 | 254 | 255 | /* Component: Docstring Custom Block */ 256 | 257 | .jldocstring.custom-block { 258 | border: 1px solid var(--vp-c-gray-2); 259 | color: var(--vp-c-text-1) 260 | } 261 | 262 | .jldocstring.custom-block summary { 263 | font-weight: 700; 264 | cursor: pointer; 265 | user-select: none; 266 | margin: 0 0 8px; 267 | } -------------------------------------------------------------------------------- /src/pmf.jl: -------------------------------------------------------------------------------- 1 | """ 2 | B, flags = pmf(A; ωₘ, slope, dhₘ, dh₀, cellsize, adjust, erode) 3 | 4 | Applies the progressive morphological filter by [Zhang (2003)](@cite keqizhangProgressiveMorphologicalFilter2003) to `A`. 5 | 6 | # Output 7 | - `B::Array{T,2}` Maximum allowable values 8 | - `flags::Array{Float64,2}` A sized array with window sizes if filtered, zero if not filtered. 9 | 10 | Afterwards, one can retrieve the resulting mask for `A` by `A .<= B` or `flags .== 0.`. 11 | 12 | # Arguments 13 | - `A::Array{T,2}` Input Array 14 | - `ωₘ::Real=20.` Maximum window size [m] 15 | - `slope::Real=0.01` Terrain slope [m/m] 16 | - `dhₘ::Real=2.5` Maximum elevation threshold [m] 17 | - `dh₀::Real=0.2` Initial elevation threshold [m] 18 | - `cellsize::Real=1.` Cellsize in [m] 19 | """ 20 | function pmf( 21 | A::AbstractMatrix{<:Real}; 22 | ωₘ = 20.0, 23 | slope = 0.01, 24 | dhₘ = 2.5, 25 | dh₀ = 0.2, 26 | cellsize = abs(first(cellsize(A))), 27 | circular = false, 28 | adjust = false, 29 | erode = false, 30 | ) 31 | _pmf(A, ωₘ, slope, dhₘ, dh₀, cellsize, circular, adjust, erode) 32 | end 33 | 34 | function _pmf( 35 | A::AbstractMatrix{<:Real}, 36 | ωₘ::Real, 37 | slope::Real, 38 | dhₘ::Real, 39 | dh₀::Real, 40 | cellsize::Real, 41 | circular::Bool, 42 | adjust::Bool, 43 | erode::Bool, 44 | ) 45 | _pmf( 46 | A, 47 | ωₘ, 48 | Fill(slope, size(A)), 49 | dhₘ, 50 | Fill(dh₀, size(A)), 51 | cellsize, 52 | circular, 53 | adjust, 54 | erode, 55 | ) 56 | end 57 | 58 | function _pmf( 59 | A::AbstractMatrix{<:Real}, 60 | ωₘ::Real, 61 | slope::AbstractMatrix{<:Real}, 62 | dhₘ::Real, 63 | dh₀::AbstractMatrix{<:Real}, 64 | cellsize::Real, 65 | circular::Bool, 66 | adjust::Bool, 67 | erode::Bool, 68 | ) 69 | 70 | # Compute windowsizes and thresholds 71 | ωₘ = round(Int, ωₘ / cellsize) 72 | κ_max = floor(Int, log2(ωₘ - 1)) # determine iterations based on exp growth 73 | windowsizes = Int.(exp2.(1:κ_max)) .+ 1 74 | 75 | # Compute tresholds 76 | dwindows = vcat(windowsizes[1], windowsizes) # prepend first element so we get 0 as diff 77 | window_diffs = [dwindows[i] - dwindows[i - 1] for i in 2:length(dwindows)] 78 | # height_tresholds = [min(dhₘ, slope * window_diff * cellsize + dh₀) for window_diff in window_diffs] 79 | 80 | # Set up arrays 81 | Af = copy(A) # array to be morphed 82 | nan_mask = isnan.(Af) 83 | Af[nan_mask] .= Inf # Replace NaN with Inf, as to always filter these 84 | 85 | B = copy(Af) # max_elevation raster 86 | out = copy(Af) # max_elevation raster 87 | 88 | flags = similar(A, Float64) # 0 = ground, other values indicate window size 89 | fill!(flags, 0.0) 90 | flags[nan_mask] .= NaN 91 | 92 | mask = falses(size(A)) 93 | 94 | # Iterate over window sizes and height tresholds 95 | for (i, ωₖ) in enumerate(windowsizes) 96 | s = (i > 1) && adjust ? dilate(slope, window_diffs[i]) : slope 97 | nωₖ = (i > 1) ? window_diffs[i] : ωₖ 98 | # @info "Window $nωₖ, $(ωₖ), $(window_diffs[i]) slope sum: $(sum(s))" 99 | dhₜ = min.(dhₘ, s * window_diffs[i] * cellsize .+ dh₀) 100 | if erode 101 | if circular 102 | # Modifies A and out in place 103 | mapwindowcirc_approx2!(min, Af, ωₖ - window_diffs[i], out, Inf) 104 | Af .= out 105 | else 106 | # modifies Af in place 107 | LocalFilters.erode!(Af, out, ωₖ) 108 | end 109 | else 110 | if circular 111 | # modifies Af in place 112 | opening_circ!(Af, ωₖ, out) 113 | out .= Af 114 | else 115 | # modifies Af in place 116 | LocalFilters.opening!(Af, out, Af, ωₖ) 117 | end 118 | end 119 | mask .= (A .- Af) .> dhₜ 120 | for I in eachindex(flags) 121 | if mask[I] && (flags[I] <= 0) 122 | flags[I] = ωₖ 123 | end 124 | end 125 | B .= min.(B, Af .+ dhₜ) 126 | end 127 | 128 | B, flags 129 | end 130 | 131 | function pmf(A::AbstractArray{<:Real, 3}; kwargs...) 132 | size(A, 3) == 1 || throw(ArgumentError("Only singleton 3rd dimension allowed")) 133 | pmf(view(A, :, :, 1); kwargs...) 134 | end 135 | 136 | function pmf2( 137 | A::AbstractMatrix{<:Real}; 138 | ωₘ = 20.0, 139 | slope = 0.01, 140 | dhₘ = 2.5, 141 | dh₀ = 0.2, 142 | cellsize = 1.0, 143 | circular = false, 144 | adjust = false, 145 | erode = false, 146 | ) 147 | _pmf2(A, ωₘ, slope, dhₘ, dh₀, cellsize, circular, adjust, erode) 148 | end 149 | 150 | function _pmf2( 151 | A::AbstractMatrix{<:Real}, 152 | ωₘ::AbstractMatrix{<:Real}, 153 | slope::Real, 154 | dhₘ::Real, 155 | dh₀::Real, 156 | cellsize::Real, 157 | circular::Bool, 158 | adjust::Bool, 159 | erode::Bool, 160 | ) 161 | _pmf( 162 | A, 163 | ωₘ, 164 | Fill(slope, size(A)), 165 | dhₘ, 166 | Fill(dh₀, size(A)), 167 | cellsize, 168 | circular, 169 | adjust, 170 | erode, 171 | ) 172 | end 173 | 174 | """ 175 | round_odd(x) 176 | 177 | Rounds `x` to the nearest odd number. 178 | """ 179 | round_odd(x) = round(Int, x / 2, RoundDown) * 2 + 1 180 | 181 | function halve_range(x, stop = 3) 182 | out = [x] 183 | while x > stop 184 | x = round_odd(x / 2) 185 | insert!(out, 1, x) 186 | end 187 | return out 188 | end 189 | 190 | function _pmf2( 191 | A::AbstractMatrix{<:Real}, 192 | windows::AbstractMatrix{<:Real}, 193 | slope::AbstractMatrix{<:Real}, 194 | dhₘ::Real, 195 | dh₀::AbstractMatrix{<:Real}, 196 | cellsize::Real, 197 | circular::Bool, 198 | adjust::Bool, 199 | erode::Bool, 200 | ) 201 | 202 | # Compute windowsizes and thresholds 203 | iwindows = round.(Int, windows ./ cellsize) .+ 3 204 | ωₘ = maximum(iwindows) 205 | # κ_max = floor(Int, log2(ωₘ - 1)) # determine # iterations based on exp growth 206 | # windowsizes = Int.(exp2.(1:κ_max)) .+ 1 207 | windowsizes = halve_range(ωₘ) 208 | 209 | # Compute tresholds 210 | dwindows = vcat(windowsizes[1], windowsizes) # prepend first element so we get 0 as diff 211 | window_diffs = [dwindows[i] - dwindows[i - 1] for i in 2:length(dwindows)] 212 | # height_tresholds = [min(dhₘ, slope * window_diff * cellsize + dh₀) for window_diff in window_diffs] 213 | 214 | # Set up arrays 215 | Af = copy(A) # array to be morphed 216 | nan_mask = isnan.(Af) 217 | Af[nan_mask] .= Inf # Replace NaN with Inf, as to always filter these 218 | 219 | B = copy(A) # max_elevation raster 220 | out = copy(A) # max_elevation raster 221 | 222 | flags = similar(A, Float64) # 0 = ground, other values indicate window size 223 | fill!(flags, 0.0) 224 | flags[nan_mask] .= NaN 225 | 226 | mask = falses(size(A)) 227 | 228 | # Iterate over window sizes and height tresholds 229 | for (i, ωₖ) in enumerate(windowsizes) 230 | s = (i > 1) && adjust ? dilate(slope, window_diffs[i]) : slope 231 | @debug "Window $(ωₖ), $(window_diffs[i]) slope sum: $(sum(s))" 232 | dhₜ = min.(dhₘ, s * window_diffs[i] * cellsize .+ dh₀) 233 | if erode 234 | if circular 235 | mapwindowcirc_approx!(minimum_mask, B, ωₖ, Af, Inf) 236 | else 237 | # mapwindow_stack!(minimum, A, ωₖ, Af) 238 | Af = LocalFilters.erode(out, ωₖ) 239 | end 240 | else 241 | if circular 242 | opening_circ!(Af, ωₖ, B) 243 | else 244 | Af = LocalFilters.opening(out, ωₖ) 245 | end 246 | end 247 | mask .= (out .- Af) .> dhₜ 248 | mask .&= (ωₖ .<= iwindows) 249 | for I in eachindex(flags) 250 | if mask[I] 251 | flags[I] = ωₖ 252 | end 253 | end 254 | out = Af 255 | # B[mask] .= min.(B[mask], Af[mask] .+ dhₜ[mask]) 256 | end 257 | Af, flags 258 | end 259 | -------------------------------------------------------------------------------- /src/relative.jl: -------------------------------------------------------------------------------- 1 | """ 2 | roughness(dem::AbstractMatrix{<:Real}) 3 | 4 | Roughness is the largest inter-cell difference of a central pixel and its surrounding cell, as defined in Wilson et al (2007, Marine Geodesy 30:3-35). 5 | """ 6 | function roughness(dem::AbstractMatrix{<:Real}, window::Stencil = Moore(1)) 7 | mapstencil(_roughness, window, dem) 8 | end 9 | function _roughness(x) 10 | o = zero(eltype(x)) 11 | for cell in x 12 | o = max(o, abs(cell - center(x))) 13 | end 14 | o 15 | end 16 | 17 | """ 18 | TPI(dem::AbstractMatrix{<:Real}) 19 | 20 | TPI stands for Topographic Position Index, which is defined as the difference between a central pixel and the mean of its surrounding cells (see Wilson et al 2007, Marine Geodesy 30:3-35). 21 | """ 22 | function TPI(dem::AbstractMatrix{<:Real}, window::Stencil = Moore(1)) 23 | mapstencil(x -> center(x) - mean(x), window, dem) 24 | end 25 | 26 | """ 27 | BPI(dem::AbstractMatrix{<:Real}) 28 | 29 | BPI stands for Bathymetric Position Index (Lundblad et al., 2006), which is defined as the difference between a central pixel and the mean of the cells in an annulus around it. 30 | """ 31 | BPI(dem::AbstractMatrix{<:Real}, window::Annulus = Annulus(3, 2)) = TPI(dem, window) 32 | 33 | """ 34 | RIE(dem::AbstractMatrix{<:Real}) 35 | 36 | RIE stands for Roughness Index Elevation, which quantifies the standard deviation of residual topography (Cavalli et al., 2008) 37 | """ 38 | function RIE(dem::AbstractMatrix{<:Real}, window::Stencil = Window(1)) 39 | meandem = TPI(dem, window) 40 | mapstencil(std, window, meandem) 41 | end 42 | 43 | """ 44 | TRI(dem::AbstractMatrix{<:Real}) 45 | 46 | TRI stands for Terrain Ruggedness Index, which measures the difference between a central pixel and its surrounding cells. 47 | This algorithm uses the square root of the sum of the square of the absolute difference between a central pixel and its surrounding cells. 48 | This is recommended for terrestrial use cases. 49 | """ 50 | function TRI(dem::AbstractMatrix{<:Real}; normalize = false, squared = true) 51 | dst = copy(dem) 52 | 53 | @inline initial(a) = (zero(a), a) 54 | @inline update(v, a, _) = 55 | if squared 56 | (v[1] + (a - v[2])^2, v[2]) 57 | else 58 | (v[1] + abs(a - v[2]), v[2]) 59 | end 60 | @inline store!(d, i, v) = 61 | if normalize && squared 62 | @inbounds d[i] = sqrt(v[1] / 8) 63 | elseif normalize 64 | @inbounds d[i] = v[1] / 8 65 | elseif squared 66 | @inbounds d[i] = sqrt(v[1]) 67 | else 68 | @warn "TRI: normalize=false and squared=false is not recommended." 69 | @inbounds d[i] = v[1] 70 | end 71 | 72 | return localfilter!(dst, dem, 3, initial, update, store!) 73 | end 74 | 75 | """ 76 | prominence(dem::AbstractMatrix{<:Real}) 77 | 78 | Prominence calculates the number of cells that are lower or equal than the central cell. 79 | Thus, 8 is a local maximum (peak), while 0 is a local minimum (pit). 80 | """ 81 | function prominence(dem::AbstractMatrix{<:Real}) 82 | dst = similar(dem, Int8) 83 | 84 | initial(a) = (; count = 0, center = a) 85 | update(v, a, _) = (; count = v.count + (a <= v.center), center = v.center) 86 | store!(d, i, v) = @inbounds d[i] = v.count - 1 87 | 88 | return localfilter!(dst, dem, 3, initial, update, store!) 89 | end 90 | 91 | round_step(x, step) = round(x / step) * step 92 | 93 | """ 94 | entropy(dem::AbstractMatrix{<:Real}; step=0.5) 95 | 96 | Entropy calculates the Shannon entropy of the surrounding cells of a central cell. 97 | `step` is the bin size for the histogram. 98 | """ 99 | function entropy(dem::AbstractMatrix{<:Real}; step = 0.5) 100 | if !isnothing(step) 101 | dem = round_step.(dem, step) 102 | end 103 | dst = copy(dem) 104 | entropy!(dst, dem) 105 | end 106 | 107 | """ 108 | entropy!(dem::AbstractMatrix{<:Real}) 109 | 110 | Entropy calculates the Shannon entropy of the surrounding cells of a central cell. 111 | """ 112 | function entropy!(dst::AbstractMatrix{<:Real}, dem::AbstractMatrix{T}) where {T <: Real} 113 | 114 | # TODO Exclude center cell? 115 | 116 | # Manually setup buffers to avoid allocations. 117 | values = @MVector zeros(T, 9) 118 | counts = @MVector zeros(Float32, 9) # Float32 so it can be normalized in place. 119 | 120 | # As previously set values are not removed, start at 0 121 | # to always have a new value at the first iteration. 122 | initial(_) = (; counts, values, i = 0) 123 | function update(v, a, _) 124 | # We either match an existing value 125 | for i in 1:(v.i) 126 | if a == v.values[i] 127 | v.counts[i] += 1 128 | return v 129 | end 130 | end 131 | # Or add a new value 132 | v.values[v.i + 1] = a 133 | v.counts[v.i + 1] = 1 134 | return (; counts = v.counts, values = v.values, i = v.i + 1) 135 | end 136 | function store!(d, i, v) 137 | @inbounds d[i] = @views _entropy(v.counts[begin:(v.i)], v.values[begin:(v.i)]) 138 | v.counts .= 0 # reset counts for next iteration 139 | end 140 | 141 | return localfilter!(dst, dem, 5, initial, update, store!) 142 | end 143 | 144 | function _entropy(counts, values) 145 | # Counts and values are re-used for the probability and log(probability) calculations. 146 | prob = counts ./= sum(counts) 147 | values .= log.(prob) 148 | prob .*= values 149 | return -sum(prob) 150 | end 151 | 152 | function cross2(a, b) 153 | if !(length(a) == length(b) == 3) 154 | throw(DimensionMismatch("cross product is only defined for vectors of length 3")) 155 | end 156 | a1, a2, a3 = a 157 | b1, b2, b3 = b 158 | return (a2 * b3 - a3 * b2, a3 * b1 - a1 * b3, a1 * b2 - a2 * b1) 159 | end 160 | 161 | """ 162 | rugosity(dem::AbstractMatrix{<:Real}) 163 | 164 | Compute the rugosity of a DEM, which is the ratio between the 165 | surface area divided by the planimetric area. 166 | 167 | Jenness 2019 https://onlinelibrary.wiley.com/doi/abs/10.2193/0091-7648%282004%29032%5B0829%3ACLSAFD%5D2.0.CO%3B2 168 | """ 169 | function rugosity(dem::AbstractMatrix{<:Real}; cellsize = cellsize(dem)) 170 | dst = similar(dem, eltype(dem)) 171 | fill!(dst, 0) 172 | 173 | δx, δy = abs.(cellsize) 174 | 175 | # Manually setup buffers to avoid allocations. 176 | values = @MVector zeros(Float32, 9) 177 | mask = falses(9) 178 | 179 | initial(a) = (; values = values, mask = mask, initial = a) 180 | function update(v, a, b) 181 | v.values[b] = a - v.initial 182 | v.mask[b] = true 183 | return (; values = v.values, mask = mask, initial = v.initial) 184 | end 185 | function store!(d, i, v) 186 | d[i] += area((δx, δy, v.values[1]), (δx, 0.0, v.values[2])) 187 | d[i] += area((δx, δy, v.values[3]), (δx, 0.0, v.values[2])) 188 | d[i] += area((δx, δy, v.values[3]), (δy, 0.0, v.values[6])) 189 | d[i] += area((δx, δy, v.values[9]), (δy, 0.0, v.values[6])) 190 | d[i] += area((δx, δy, v.values[9]), (δx, 0.0, v.values[8])) 191 | d[i] += area((δx, δy, v.values[7]), (δx, 0.0, v.values[8])) 192 | d[i] += area((δx, δy, v.values[7]), (δy, 0.0, v.values[4])) 193 | d[i] += area((δx, δy, v.values[1]), (δy, 0.0, v.values[4])) 194 | fill!(v.values, 0) 195 | fill!(v.mask, false) 196 | d[i] /= (δx * δy) 197 | end 198 | 199 | return localfilter!(dst, dem, nbkernel, initial, update, store!) 200 | end 201 | 202 | function area(a, b) 203 | # Divide by 2 for the triangle area, divide by 4 to get the area within the centre cell 204 | sqrt(sum(cross2(a, b) .^ 2)) / 8 205 | end 206 | 207 | """ 208 | pitremoval(dem::AbstractMatrix{<:Real}) 209 | 210 | Remove pits from a DEM Array if the center cell of a 3x3 patch is `limit` lower or than the surrounding cells. 211 | """ 212 | function pitremoval(dem::AbstractMatrix{<:Real}; limit = 0.0) 213 | dst = copy(dem) 214 | 215 | initial(a) = (true, typemax(a), a, limit) # (is_pit, min, center, limit) 216 | @inbounds function update(v, a, _) 217 | if v[3] == a 218 | return v 219 | elseif (a - v[3]) > v[4] 220 | return (v[1] & true, min(a, v[2]), v[3], v[4]) 221 | else 222 | (false, v[2], v[3], v[4]) 223 | end 224 | end 225 | store!(d, i, v) = @inbounds d[i] = v[1] ? v[2] : v[3] 226 | 227 | return localfilter!(dst, dem, 3, initial, update, store!) 228 | end 229 | -------------------------------------------------------------------------------- /src/spread.jl: -------------------------------------------------------------------------------- 1 | const sqrt2 = sqrt(2.0) 2 | const distance_8 = @SMatrix[sqrt2 1 sqrt2; 1 Inf 1; sqrt2 1 sqrt2] 3 | const distance_4 = @SMatrix[Inf 1 Inf; 1 Inf 1; Inf 1 Inf] 4 | const Δ = CartesianIndex(1, 1) 5 | 6 | "Spread algorithms." 7 | abstract type SpreadMethod end 8 | 9 | Base.@kwdef struct Tomlin <: SpreadMethod end 10 | 11 | """ 12 | spread(::Tomlin, points::Matrix{<:Real}, initial::Matrix{<:Real}, friction::Matrix{<:Real}; res=1, limit=Inf, method=Tomlin()) 13 | 14 | Total friction distance spread from `points` as by [Tomlin (1983)](@cite tomlin1983digital). 15 | This is also the method implemented by [PCRaster](https://pcraster.geo.uu.nl/pcraster/4.0.2/doc/manual/op_spread.html). 16 | 17 | # Output 18 | - `Array{Float64,2}` Total friction distance 19 | 20 | # Arguments 21 | - `points::Matrix{<:Real}` Input Array 22 | - `initial::Matrix{<:Real}` Initial values of the result 23 | - `friction::Matrix{<:Real}` Resolution of cell size 24 | - `res=1` Resolution or cell size 25 | - `limit=Inf` Initial fill value 26 | """ 27 | function spread( 28 | ::Tomlin, 29 | locations::Vector{CartesianIndex{2}}, 30 | initial::AbstractMatrix{T}, 31 | friction::AbstractMatrix{<:Real}; 32 | cellsize = cellsize(friction), 33 | limit = typemax(T), 34 | ) where {T <: Real} 35 | result = similar(friction) 36 | fill!(result, limit) 37 | result[locations] .= initial[locations] 38 | zone = zeros(Int32, size(friction)) 39 | mask = falses(size(friction)) 40 | 41 | II = CartesianIndices(size(friction)) 42 | pq = FastPriorityQueue{eltype(friction)}(size(friction)...) 43 | for I in II[locations] 44 | enqueue!(pq, Tuple(I), result[I]) 45 | mask[I] = true 46 | end 47 | 48 | δx, δy = abs.(cellsize) 49 | δxy = sqrt(δx^2 + δy^2) 50 | distances = @SMatrix[ 51 | δxy δx δxy 52 | δy Inf δy 53 | δxy δx δxy 54 | ] 55 | 56 | while !isempty(pq) 57 | spread!(pq, mask, result, friction, zone, distances) 58 | end 59 | result 60 | end 61 | 62 | function spread!(pq, mask, result, friction, zone, distances) 63 | I = CartesianIndices(pq.index)[dequeue!(pq)] 64 | # I = dequeue!(pq) 65 | mask[I] = true 66 | patch = (I - Δ):(I + Δ) 67 | 68 | # New distance is cell_distance + average friction values 69 | for (li, i) in enumerate(patch) 70 | i in CartesianIndices(result) || continue 71 | mask[i] && continue 72 | nr = muladd(friction[i] + friction[I], distances[li] / 2, result[I]) 73 | if nr < result[i] # cells where new distance is lower 74 | result[i] = nr 75 | zone[i] = zone[I] 76 | haskey(pq, Tuple(i)) || enqueue!(pq, Tuple(i), result[i]) 77 | end 78 | end 79 | end 80 | 81 | Base.@kwdef struct Eastman <: SpreadMethod 82 | iterations::Int = 3 83 | end 84 | 85 | """ 86 | spread(::Eastman, points::Matrix{<:Real}, initial::Matrix{<:Real}, friction::Matrix{<:Real}; res=1, limit=Inf, iterations=3) 87 | 88 | Pushbroom method for friction costs as discussed by [Eastman (1989)](@cite eastman1989pushbroom). 89 | This method should scale better (linearly) than the [Tomlin (1983)](@cite tomlin1983digital) method, but can require more 90 | `iterations` than set by default (3) in the case of maze-like, uncrossable obstacles. 91 | 92 | # Output 93 | - `Array{Float64,2}` Total friction distance 94 | 95 | # Arguments 96 | - `points::Matrix{<:Real}` Input Array 97 | - `initial::Matrix{<:Real}` Factor to exaggerate elevation 98 | - `friction::Matrix{<:Real}` Resolution of cell size 99 | - `res=1` Resolution or cell size 100 | - `limit=Inf` Initial fill value 101 | """ 102 | function spread( 103 | e::Eastman, 104 | # points::AbstractMatrix{<:Real}, 105 | locations::Vector{CartesianIndex{2}}, 106 | initial::AbstractMatrix{<:Real}, 107 | friction::AbstractMatrix{<:Real}; 108 | cellsize = cellsize(friction), 109 | limit = Inf, 110 | ) 111 | 112 | # ofriction = OffsetMatrix(fill(Inf, size(friction) .+ 2), UnitRange.(0, size(points) .+ 1)) 113 | # ofriction[begin+1:end-1, begin+1:end-1] .= friction 114 | 115 | # result = OffsetMatrix(fill(limit, size(friction) .+ 2), UnitRange.(0, size(points) .+ 1)) 116 | result = similar(friction) 117 | fill!(result, limit) 118 | 119 | # r = @view result[1:end-1, 1:end-1] 120 | # locations = points .> 0 121 | result[locations] .= initial[locations] 122 | 123 | # mask = OffsetMatrix(trues(size(points) .+ 2), UnitRange.(0, size(points) .+ 1)) 124 | # mask[begin+1:end-1, begin+1:end-1] .= false 125 | # mask = falses(size(points)) 126 | 127 | # zone = OffsetMatrix(fill(0, size(friction) .+ 2), UnitRange.(0, size(points) .+ 1)) 128 | # ozone = @view zone[1:end-1, 1:end-1] 129 | # ozone .= points 130 | # zone = copy(points) 131 | zone = zeros(Int32, size(friction)) 132 | 133 | δx, δy = abs.(cellsize) 134 | δxy = sqrt(δx^2 + δy^2) 135 | distances = @SMatrix[ 136 | δxy δx δxy 137 | δy Inf δy 138 | δxy δx δxy 139 | ] 140 | 141 | # minval, minidx = [0.0], [CartesianIndex(1, 1)] 142 | # x = @MMatrix zeros(3, 3) 143 | indices = CartesianIndices(size(friction)) 144 | counts = 1 145 | for i in 1:(e.iterations) 146 | if iszero(counts) 147 | break 148 | end 149 | counts -= counts 150 | II = (i % 2 == 1) ? indices : reverse(indices) 151 | for I in II 152 | patch = (I - Δ):(I + Δ) 153 | for (li, i) in enumerate(patch) 154 | i in CartesianIndices(result) || continue 155 | nr = muladd(friction[i] + friction[I], distances[li] / 2, result[I]) 156 | if nr < result[i] # cells where new distance is lower 157 | counts += 1 158 | result[i] = nr 159 | zone[i] = zone[I] 160 | end 161 | end 162 | end 163 | end 164 | result 165 | end 166 | 167 | """ 168 | spread(points::Matrix{<:Real}, initial::Matrix{<:Real}, friction::Real; distance=Euclidean(), res=1.0) 169 | spread(points::Matrix{<:Real}, initial::Real, friction::Real; distance=Euclidean(), res=1.0) 170 | 171 | Optimized (and more accurate) function based on the same friction everywhere. 172 | 173 | When the friction is the same everywhere, there's no need for searching the shortest cost path, 174 | as one can just take a direct line to the input points. 175 | 176 | The calculated cost is more accurate, as there's no 'zigzag' from cell center to cell center. 177 | """ 178 | function spread( 179 | points::AbstractMatrix{<:Real}, 180 | initial::AbstractMatrix{<:Real}, 181 | friction::Real; 182 | distance = Euclidean(), 183 | cellsize = cellsize(friction), 184 | kwargs..., 185 | ) 186 | locations = findall(>(0), points) 187 | I = CartesianIndices(size(points)) 188 | 189 | result = fill(Inf, size(points)) 190 | for location in I[locations] 191 | for cell in I 192 | result[cell] = min( 193 | evaluate(distance, location.I, cell.I) * first(abs(cellsize)) * friction + initial[location], 194 | result[cell], 195 | ) 196 | end 197 | end 198 | m = .~isfinite.(points) 199 | result[m] .= points[m] 200 | return result 201 | end 202 | 203 | Base.@kwdef struct FastSweeping <: SpreadMethod 204 | eps::Float64 = 1e-6 205 | debug::Bool = false 206 | iterations::Int = typemax(Int) 207 | end 208 | 209 | """ 210 | spread(points::Matrix{<:Real}, initial::Matrix{<:Real}, friction::Matrix{<:Real}; cellsize=(1,1), limit=Inf, method=Tomlin()) 211 | 212 | Total friction distance spread from `points` from `initial` with `friction`. 213 | By default uses Tomlin, see SpreadMethod for other algorithms. 214 | """ 215 | function spread( 216 | points::Vector{CartesianIndex{2}}, 217 | initial::AbstractMatrix{<:Real}, 218 | friction::AbstractMatrix{<:Real}; 219 | cellsize = cellsize(friction), 220 | limit = Inf, 221 | method = Tomlin(), 222 | ) 223 | spread(method, points, initial, friction; cellsize, limit) 224 | end 225 | 226 | function spread( 227 | points::AbstractMatrix{<:Real}, 228 | initial::AbstractMatrix{<:Real}, 229 | friction::AbstractMatrix{<:Real}; 230 | kwargs..., 231 | ) 232 | I = findall(>(0), points) 233 | return spread(I, initial, friction; kwargs...) 234 | end 235 | 236 | function spread( 237 | points::AbstractMatrix{<:Real}, 238 | initial::Real, 239 | friction::AbstractMatrix{<:Real}; 240 | kwargs..., 241 | ) 242 | init = Fill(initial, size(points)) 243 | return spread(points, init, friction; kwargs...) 244 | end 245 | 246 | # function spread( 247 | # points::AbstractMatrix{<:Real}, 248 | # initial::AbstractMatrix{<:Real}, 249 | # friction::Real; 250 | # kwargs..., 251 | # ) 252 | # friction = Fill(friction, size(points)) 253 | # return spread(points, initial, friction; kwargs...) 254 | # end 255 | 256 | function spread(points::AbstractMatrix{<:Real}, initial::Real, friction::Real; kwargs...) 257 | init = Fill(initial, size(points)) 258 | fric = Fill(friction, size(points)) 259 | return spread(points, init, fric; kwargs...) 260 | end 261 | -------------------------------------------------------------------------------- /docs/src/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```@meta 4 | CurrentModule = Geomorphometry 5 | ``` 6 | 7 | ```@setup plots 8 | using Geomorphometry, CairoMakie, GeoArrays, Statistics # hide 9 | import Eikonal # hide 10 | set_theme!(theme_minimal(); transparency = true) # hide 11 | CairoMakie.activate!(type = "png") 12 | A = GeoArrays.read("saba.tif") # hide 13 | B = GeoArrays.read("saba_dsm.tif") # hide 14 | mask = ismissing.(A) # hide 15 | # dem = coalesce(A, NaN).A # hide 16 | dtm = coalesce(A, NaN) # hide 17 | dsm = coalesce(B, NaN) # hide 18 | # ndem = GeoArrays.flipud!(deepcopy(dtm)) 19 | 20 | fdtm = deepcopy(dtm) 21 | out = zeros(size(fdtm)) 22 | queued = falses(size(fdtm)) 23 | fig, ax, hm = heatmap(out, colormap=:turbo, colorrange=(0, 600)) 24 | 25 | openn = Geomorphometry.PriorityQueue{CartesianIndex{2}, eltype(fdtm)}() 26 | pit = Geomorphometry.DataStructures.Queue{CartesianIndex{2}}() 27 | 28 | R = CartesianIndices(fdtm) 29 | I_first, I_last = first(R), last(R) 30 | fps=60 31 | for cell in Geomorphometry.edges(R) 32 | Geomorphometry.enqueue!(openn, cell, fdtm[cell]) 33 | queued[cell] = true # queued 34 | end 35 | record(fig, expanduser("test.mp4")) do io 36 | i = 0 37 | while (!isempty(openn) || !isempty(pit)) 38 | cell = !isempty(pit) ? Geomorphometry.DataStructures.dequeue!(pit) : Geomorphometry.dequeue!(openn) 39 | out[cell] = fdtm[cell] 40 | # sleep(1/fps) 41 | # hm[1] = out # update data 42 | if iszero(i % 5_000) 43 | hm[1] = out # update data 44 | recordframe!(io) 45 | end 46 | for ncell in max(I_first, cell - Geomorphometry.Δ):min(I_last, cell + Geomorphometry.Δ) 47 | (queued[ncell] || ncell == cell) && continue 48 | queued[ncell] = true 49 | if fdtm[ncell] <= fdtm[cell] 50 | fdtm[ncell] = fdtm[cell] 51 | Geomorphometry.DataStructures.enqueue!(pit, ncell) 52 | else 53 | Geomorphometry.enqueue!(openn, ncell, fdtm[ncell]) 54 | end 55 | end 56 | i += 1 57 | end 58 | end 59 | ``` 60 | 61 | In Geomorphometry.jl we provide a set of tools to analyze and visualize the shape of the Earth. The package is designed to be fast, flexible, and easy to use. 62 | Moreover, we have implemented several algorithms for a common operation so that you can choose the one that best fits your needs. 63 | 64 | In these pages we will use the elevation model of [Saba](https://en.wikipedia.org/wiki/Saba_(island)) to showcase the different categories of operations that are available in Geomorphometry.jl. 65 | 66 | ## Visualization 67 | Visualization is done using the [`hillshade`](@ref), [`multihillshade`](@ref), and [`pssm`](@ref) functions. The first two shade the terrain by using a single or multiple light source(s) respectively, while `pssm` is a slope map exaggerated for human perception. 68 | 69 | 70 | :::tabs 71 | 72 | == Hillshade 73 | ```@example plots 74 | heatmap(hillshade(dtm)) 75 | ``` 76 | 77 | == Multihillshade 78 | ```@example plots 79 | heatmap(multihillshade(dtm, azimuth=0:60:270, zenith=60)) 80 | ``` 81 | 82 | == PSSM 83 | ```@example plots 84 | heatmap(pssm(dtm), colormap=Reverse(:viridis)) 85 | ``` 86 | ::: 87 | 88 | We can also overlay the `pssm` visualization on top of a colored `aspect` map. 89 | 90 | ```@example plots 91 | f = heatmap(aspect(dtm); colormap=:curl) 92 | heatmap!(pssm(dtm); colormap=Reverse(:greys), alpha=0.5) 93 | f 94 | ``` 95 | 96 | ## Derivatives 97 | Common derivatives are implemented in Geomorphometry.jl. These include [`slope`](@ref), [`aspect`](@ref), and `curvature`. The latter is ill-defined, here we provide [`plan_curvature`](@ref) (also called *projected contour curvature*), [`profile_curvature`](@ref) (also called *normal slope line curvature*), and [`tangential_curvature`](@ref) (also called *normal contour curvature*). Note that functions here allow for a custom radius (but fixed positions, see X), as demonstrated for `profile_curvature`. 98 | 99 | 100 | ### Slope 101 | 102 | :::tabs 103 | 104 | == Horn 105 | ```@example plots 106 | heatmap(slope(dtm; method=Horn()); colormap=:matter, colorrange=(0, 60)) 107 | ``` 108 | == ZevenbergenThorne 109 | ```@example plots 110 | heatmap(slope(dtm; method=ZevenbergenThorne()); colormap=:matter, colorrange=(0, 60)) 111 | ``` 112 | == MaximumDownwardGradient 113 | ```@example plots 114 | heatmap(slope(dtm; method=Geomorphometry.MaximumDownwardGradient()); colormap=:matter, colorrange=(0, 60)) 115 | ``` 116 | 117 | ::: 118 | 119 | We can also determine the slope of the terrain along a specific direction. 120 | 121 | :::tabs 122 | 123 | == Horn (0°) 124 | ```@example plots 125 | heatmap(slope(dtm; method=Horn(), direction=0); colormap=:matter, colorrange=(-45, 45)) 126 | ``` 127 | == ZevenbergenThorne (0°) 128 | ```@example plots 129 | heatmap(slope(dtm; method=ZevenbergenThorne(), direction=0); colormap=:matter, colorrange=(-45, 45)) 130 | ``` 131 | == Horn (90°) 132 | ```@example plots 133 | heatmap(slope(dtm; method=Horn(), direction=90); colormap=:matter, colorrange=(-45, 45)) 134 | ``` 135 | == ZevenbergenThorne (90°)) 136 | ```@example plots 137 | heatmap(slope(dtm; method=ZevenbergenThorne(), direction=90); colormap=:matter, colorrange=(-45, 45)) 138 | ``` 139 | 140 | ::: 141 | 142 | ### Aspect 143 | 144 | :::tabs 145 | 146 | == Horn 147 | ```@example plots 148 | heatmap(aspect(dtm; method=Horn()); colormap=:romaO) 149 | ``` 150 | == ZevenbergenThorne 151 | ```@example plots 152 | heatmap(aspect(dtm; method=ZevenbergenThorne()); colormap=:romaO) 153 | ``` 154 | == MaximumDownwardGradient 155 | ```@example plots 156 | heatmap(aspect(dtm; method=Geomorphometry.MaximumDownwardGradient()); colormap=:romaO) 157 | ``` 158 | 159 | ::: 160 | 161 | ### Curvature 162 | 163 | :::tabs 164 | 165 | == Profile curvature 166 | ```@example plots 167 | heatmap(profile_curvature(dtm); colorrange=(-1,1), colormap=:tarn) 168 | ``` 169 | 170 | == Plan curvature 171 | ```@example plots 172 | heatmap(plan_curvature(dtm); colorrange=(-1,1), colormap=:tarn) 173 | ``` 174 | 175 | == Tangential curvature 176 | ```@example plots 177 | heatmap(tangential_curvature(dtm); colorrange=(-1,1), colormap=:tarn) 178 | ``` 179 | 180 | == Laplacian 181 | ```@example plots 182 | heatmap(laplacian(dtm); colorrange=(-1,1), colormap=:tarn) 183 | ``` 184 | 185 | ::: 186 | 187 | We can also determine the curvature of the terrain along a specific direction. 188 | 189 | :::tabs 190 | 191 | == Laplacian in 90° direction 192 | ```@example plots 193 | heatmap(laplacian(dtm, radius=1, direction=90); colorrange=(-1,1), colormap=:tarn) 194 | ``` 195 | 196 | == Laplacian in 0° direction 197 | ```@example plots 198 | heatmap(laplacian(dtm, radius=1, direction=0); colorrange=(-1,1), colormap=:tarn) 199 | ``` 200 | 201 | ::: 202 | 203 | ## Relative position 204 | There are several terrain descriptors that can be used to analyze the relative position of a point with respect to its neighbors. These include [`TPI`](@ref), [`TRI`](@ref), [`RIE`](@ref), [`BPI`](@ref), [`rugosity`](@ref) and [`roughness`](@ref). Here we use `BPI`, but with a custom sized `Window` (from Stencils.jl). All these functions can be used with a custom window size. 205 | 206 | :::tabs 207 | 208 | == BPI 209 | ```@example plots 210 | heatmap(BPI(dtm, Geomorphometry.Annulus(5, 3)); colormap=:delta, colorrange=(-10,10)) 211 | ``` 212 | == TPI 213 | ```@example plots 214 | heatmap(TPI(dtm); colormap=:delta, colorrange=(-10,10)) 215 | ``` 216 | == TRI 217 | ```@example plots 218 | heatmap(TRI(dtm); colormap=:speed) 219 | ``` 220 | == RIE 221 | ```@example plots 222 | heatmap(RIE(dtm); colormap=:speed) 223 | ``` 224 | == rugosity 225 | ```@example plots 226 | heatmap(rugosity(dtm); colormap=:speed) 227 | ``` 228 | == roughness 229 | ```@example plots 230 | heatmap(roughness(dtm); colormap=:speed) 231 | ``` 232 | 233 | ::: 234 | 235 | ## Hydrology 236 | Hydrological operations are used to analyze the flow of water on the terrain. We provide [`filldepressions`](@ref) to fill depressions, and [`flowaccumulation`](@ref) to calculate the flow accumulation. Here we use `flowaccumulation` to calculate the flow accumulation. 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()`. 237 | 238 | :::tabs 239 | 240 | == Flow accumulation with FD8 241 | ```@example plots 242 | acc, ldd = flowaccumulation(dtm; method=FD8(2)) 243 | acc[mask] .= NaN # hide 244 | heatmap(log10.(acc); colormap=:rain) 245 | ``` 246 | == Flow accumulation with D∞ 247 | ```@example plots 248 | acc, ldd = flowaccumulation(dtm; method=DInf()) 249 | acc[mask] .= NaN # hide 250 | heatmap(log10.(acc); colormap=:rain) 251 | ``` 252 | == Flow accumulation with D8 253 | ```@example plots 254 | acc, ldd = flowaccumulation(dtm; method=D8()) 255 | acc[mask] .= NaN # hide 256 | heatmap(log10.(acc); colormap=:rain) 257 | ``` 258 | == Underlying method 259 | 260 | We use the Priority Flood method by [barnesPriorityFloodOptimalDepressionFilling2014](@citet). 261 | 262 | ```@raw html 263 |