The Topographic Position Index (TPI) compares the elevation of each cell of a digital terrain model (DTM) with the mean elevation of the cells around it (Guisan et al. 1999; Weiss 2001). Positive values mean that the cell is higher than its surroundings (a ridge or a hilltop), negative values that it is lower (a valley bottom), and values near zero that it is either on flat ground or half-way up a slope; slope is used to tell these two apart.
landform implements the two classifications proposed by Weiss (2001) and made popular by Jenness’s TPI extension for ArcView (Jenness 2006):
The package has three functions: landform(), which performs the classifications; tpi(), which returns the TPI surface itself; and malta_dtm(), which loads the sample DTM used below.
The DTM covers the island of Malta at 40 m resolution, with the sea coded as NA.
library(landform)
library(terra)
#> terra 1.7.65
dem <- malta_dtm()
dem
#> class : SpatRaster
#> dimensions : 535, 580, 1 (nrow, ncol, nlyr)
#> resolution : 40, 40 (x, y)
#> extent : 438651.4, 461851.4, 3962750, 3984150 (xmin, xmax, ymin, ymax)
#> coord. ref. : ED50 / UTM zone 33N (EPSG:23033)
#> source : malta_dtm_40.tif
#> name : elevation
plot(dem, col = hcl.colors(50, "Terrain 2"), main = "Malta DTM (m)")The TPI depends entirely on the size of the neighbourhood: the same point can be a ridge with respect to its immediate surroundings and part of a valley floor at a broader scale. The neighbourhood is set by scale, interpreted either as the width of a square window in cells (units = "cells", the default) or as a radius in map units (units = "map"; metres for longitude/latitude rasters). The window can be square, circular or annular (shape).
t_small <- tpi(dem, scale = 120, units = "map", shape = "circle", std = "none")
t_large <- tpi(dem, scale = 1000, units = "map", shape = "circle", std = "none")
rng <- c(-30, 30)
plot(c(t_small, t_large), range = rng,
col = hcl.colors(100, "Blue-Red 3"),
main = c("TPI (m), 120 m radius", "TPI (m), 1000 m radius"))Weiss’s thresholds are expressed in standard deviations, so the TPI has to be standardised first. Two approaches are available, and they can lead to noticeably different maps.
std = "global", the default). The TPI is turned into z-scores using the mean and standard deviation of the TPI over the whole raster. This is the method of Weiss (2001) and Jenness (2006): a value of 1 means “one standard deviation above the typical TPI of this landscape”.std = "local"). The TPI of each cell is divided by the standard deviation of elevation in its own neighbourhood, which is the “deviation from mean elevation” (DEV) of Wilson and Gallant (2000). De Reu et al. (2013) found it better suited to landscapes in which relief varies greatly from place to place. Its drawback is that in nearly flat areas the local standard deviation is tiny, so that small irregularities of the DTM (or plain noise) become large standardised values. This was the only method available in landform up to version 0.2.The difference is easiest to see on the plateaus and plains of eastern Malta:
g <- landform(dem, scale = 9, plot = FALSE)
l <- landform(dem, scale = 9, std = "local", plot = FALSE)
par(mfrow = c(1, 2))
plot(g$classes, main = "Global standardisation", legend = FALSE,
mar = c(1, 1, 2, 1), axes = FALSE)
plot(l$classes, main = "Local standardisation", legend = FALSE,
mar = c(1, 1, 2, 1), axes = FALSE)par(mfrow = c(1, 1))data.frame(class = g$stats$class,
global = round(g$stats$percent, 1),
local = round(l$stats$percent, 1))
#> class global local
#> 1 Valley 7.7 3.2
#> 2 Lower Slope 8.3 11.7
#> 3 Flat Slope 48.7 43.0
#> 4 Middle Slope 13.3 24.4
#> 5 Upper Slope 12.2 14.3
#> 6 Ridge 9.7 3.4With local standardisation the flat areas are broken up into a mosaic of valleys and ridges a few cells across. Which method is preferable depends on the landscape and on the question; when in doubt, it is worth running both.
sp <- landform(dem, scale = 9)sp
#> <landform> Slope Position Classification (Weiss 2001)
#> neighbourhood : scale = 9 in cells (window width) - square (focal cell excluded)
#> TPI standard. : global
#> classified : 151209 cells, 242.108 km2
#>
#> code class area_km2 percent
#> 1 Valley 18.525 7.7
#> 2 Lower Slope 20.158 8.3
#> 3 Flat Slope 117.906 48.7
#> 4 Middle Slope 32.303 13.3
#> 5 Upper Slope 29.647 12.2
#> 6 Ridge 23.569 9.7The thresholds (Weiss 2001), with z the standardised TPI and s the slope in degrees, are:
| Class | Rule |
|---|---|
| 1 Valley | z ≤ -1 |
| 2 Lower Slope | -1 < z ≤ -0.5 |
| 3 Flat Slope | -0.5 < z < 0.5 and s ≤ 5 |
| 4 Middle Slope | -0.5 < z < 0.5 and s > 5 |
| 5 Upper Slope | 0.5 ≤ z ≤ 1 |
| 6 Ridge | z > 1 |
All three thresholds can be changed with tpi.thr and slope.thr.
The classes can be drawn semi-transparently over a relief shading, which helps relate them to the terrain. A slopeshade (shade = "slope") shows the slope in grey levels, from white for flat ground to black for the steepest slopes; unlike a hillshade (shade = "hill"), it does not darken features just because they face away from the light. The opacity of the classes is set by alpha:
plot(sp, shade = "slope")Since the classification depends on the scale, a broader neighbourhood picks out broader features:
sp_broad <- landform(dem, scale = 600, units = "map", shape = "circle")The Landform Classification combines a small-neighbourhood TPI (sn) and a large-neighbourhood TPI (ln), each split into low (≤ -1), middle and high (≥ 1) values; plains and open slopes are separated by slope.
lf <- landform(dem, class.type = "landform.classification",
sn = 200, ln = 1000, units = "map", shape = "circle",
shade = "slope")| Class | SN | LN |
|---|---|---|
| 1 Canyons, Deeply Incised Streams | ≤ -1 | ≤ -1 |
| 2 Midslope Drainages, Shallow Valleys | ≤ -1 | middle |
| 3 Upland Drainages, Headwaters | ≤ -1 | ≥ 1 |
| 4 U-shaped Valleys | middle | ≤ -1 |
| 5 Plains (slope ≤ 5) | middle | middle |
| 6 Open Slopes (slope > 5) | middle | middle |
| 7 Upper Slopes, Mesas | middle | ≥ 1 |
| 8 Local Ridges, Hills in Valleys | ≥ 1 | ≤ -1 |
| 9 Midslope Ridges, Small Hills in Plains | ≥ 1 | middle |
| 10 Mountain Tops, High Ridges | ≥ 1 | ≥ 1 |
The two standardised TPI surfaces on which the classification rests can be plotted directly, here over a slopeshade (the colour scale is capped at ±3 standard deviations):
plot(lf, what = "tpi", shade = "slope")landform() returnslandform() returns an object of class "landform", a list holding:
classes: a categorical SpatRaster of the classes, with class names and colours attached;tpi: the raw and standardised TPI layers;slope: the slope (degrees);stats: the descriptive statistics of each class;dem and params: the input DTM and the settings used.summary(lf)
#> code class cells area_km2 percent
#> 1 1 Canyons, Deeply Incised Streams 6419 10.2776930 4.24508506
#> 2 2 Midslope Drainages, Shallow Valleys 4925 7.8856255 3.25706859
#> 3 3 Upland Drainages, Headwaters 126 0.2017418 0.08332717
#> 4 4 U-shaped Valleys 8849 14.1684167 5.85210456
#> 5 5 Plains 80119 128.2830591 52.98586948
#> 6 6 Open Slopes 25170 40.3008156 16.64579694
#> 7 7 Upper Slopes, Mesas 10931 17.5020090 7.22900727
#> 8 8 Local Ridges, Hills in Valleys 395 0.6324459 0.26122465
#> 9 9 Midslope Ridges, Small Hills in Plains 6763 10.8285440 4.47260786
#> 10 10 Mountain Tops, High Ridges 7512 12.0277066 4.96790844
#> mean_elev mean_slope
#> 1 57.11573 9.803690
#> 2 85.13888 8.740135
#> 3 130.32629 10.469815
#> 4 54.93890 6.371109
#> 5 72.88148 2.397099
#> 6 81.50629 8.873848
#> 7 137.34029 5.463080
#> 8 94.19899 14.683089
#> 9 84.16005 8.136700
#> 10 134.89600 7.425783
plot(lf, what = "area")Areas are computed from the actual size of each cell (terra::cellSize()), so they are correct also for rasters in longitude/latitude. Cells for which slope cannot be computed (the outer rim of the raster and the cells along the coast) are left unclassified.
Individual classes can be extracted as separate presence/absence layers with class_layers(), for instance to compute distances from ridges or to use the classes as covariates:
layers <- class_layers(lf)
names(layers)
#> [1] "Canyons, Deeply Incised Streams"
#> [2] "Midslope Drainages, Shallow Valleys"
#> [3] "Upland Drainages, Headwaters"
#> [4] "U-shaped Valleys"
#> [5] "Plains"
#> [6] "Open Slopes"
#> [7] "Upper Slopes, Mesas"
#> [8] "Local Ridges, Hills in Valleys"
#> [9] "Midslope Ridges, Small Hills in Plains"
#> [10] "Mountain Tops, High Ridges"
plot(layers[["Mountain Tops, High Ridges"]], legend = FALSE,
col = c("grey90", "#6A3D9A"), main = "Mountain Tops, High Ridges")Since classes is an ordinary SpatRaster, it can be saved with terra::writeRaster() and opened in any GIS.
Version 0.3 changed the default standardisation from local to global and, by default, excludes the focal cell from the neighbourhood mean. To reproduce results obtained with earlier versions, use:
landform(dem, scale = 9, std = "local", centre = TRUE)The only remaining differences are that cells on the outer rim of the raster are now always left unclassified, and that cells whose standardised TPI was exactly 0.5 (previously left without a class) are now Upper Slope.
De Reu, J., Bourgeois, J., Bats, M., Zwertvaegher, A., Gelorini, V., De Smedt, P., Chu, W., Antrop, M., De Maeyer, P., Finke, P., Van Meirvenne, M., Verniers, J. and Crombé, P. (2013). Application of the topographic position index to heterogeneous landscapes. Geomorphology, 186, 39-49.
Guisan, A., Weiss, S. B. and Weiss, A. D. (1999). GLM versus CCA spatial modeling of plant species distribution. Plant Ecology, 143, 107-122.
Jenness, J. (2006). Topographic Position Index (tpi_jen.avx) extension for ArcView 3.x, v. 1.2. Jenness Enterprises.
Weiss, A. (2001). Topographic Position and Landforms Analysis. Poster presentation, ESRI User Conference, San Diego, CA.
Wilson, J. P. and Gallant, J. C. (eds) (2000). Terrain Analysis: Principles and Applications. Wiley, New York.