Skip to content

Commit

Permalink
Version 0.2
Browse files Browse the repository at this point in the history
- BPI Added
- WoodEvans now calculates the representative measures of plan, profile, and twisting curvature (Minar et al., 2020)
- WoodEvans crosc and longc removed
- - WoodEvans replaced directional derivative versions of minc, maxc, and meanc with kmin, kmean, kmax (Minar et al., 2020)
- WoodEvans features algorithm modified to use kmin, kmax, and planc
  • Loading branch information
ailich committed Dec 8, 2021
1 parent 37be3fd commit 9ef2d93
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 67 deletions.
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: MultiscaleDEM
Title: Calculates multi-scale geomorphometric terrain attributes from regularly gridded DEM/bathymetry rasters
Version: 0.1
Version: 0.2
Authors@R:
c(
person(given = "Alexander",
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# Generated by roxygen2: do not edit by hand

export(AdjSD)
export(BPI)
export(RDMV)
export(SAPA)
export(SlpAsp)
export(SurfaceArea)
export(TPI)
export(VRM)
export(WoodEvans)
export(annulus_window)
export(circle_window)
import(terra)
importFrom(Rcpp,sourceCpp)
importFrom(dplyr,case_when)
Expand Down
130 changes: 130 additions & 0 deletions R/BPI.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#' Creates circular focal window
#'
#' Creates circular focal window around central pixel.
#' @param radius radius of circular window
#' @param resolution resolution of intended raster layer (one number or a vector of length 2). Only necessary if unit= "map"
#' @param unit unit for radius. Either "cell" (number of cells, the default) or "map" for map units (e.g. meters).
#' @param return_dismat logical, if TRUE return a matrix of distances from focal cell instead of a matrix to pass to terra::focal
#' @export
circle_window<- function(radius, unit= "cell", resolution, return_dismat = FALSE){
if (unit != "map" & unit != "cell"){
stop("Error: unit must equal 'map' or 'cell'")
}
if(length(radius)!=1){
stop("Error: radius must be a single integer")
}
if(unit=="cell"){
resolution<- c(1,1)
}
if (length(resolution)==1){
resolution<- rep(resolution,2)
}
nrows<- floor((radius/resolution[2])*2+1)
ncols<- floor((radius/resolution[1])*2+1)
if(nrows %% 2 ==0){
nrows<- nrows+1
}
if(ncols %% 2 ==0){
ncols<- ncols+1
} #nrow and ncol must be odd to have central pixel
x<- matrix(seq(1:ncols), nrow = nrows, ncol =ncols, byrow = TRUE) - ((ncols+1)/2)
y<- matrix(seq(1:nrows), nrow=nrows, ncol=ncols, byrow = FALSE) - ((nrows+1)/2)
x<- x * resolution[1]
y<- y * resolution[2]
dis_mat<- sqrt((y^2)+(x^2)) #Distance from center of window
if(return_dismat){
return(dis_mat)
}
w<- matrix(NA, nrow = nrow(dis_mat), ncol= ncol(dis_mat))
w[dis_mat <= radius]<- 1
return(w)
}

#' Creates annulus focal window
#'
#' Creates annulus focal window around central pixel.
#' @param radius radius of inner annulus c(inner,outer)
#' @param unit unit for radius. Either "cell" (number of cells, the default) or "map" for map units (e.g. meters).
#' @param resolution resolution of intended raster layer (one number or a vector of length 2). Only necessary if unit= "map"
#' @param return_dismat logical, if TRUE return a matrix of distances from focal cell instead of a matrix to pass to terra::focal (default FALSE)
#' @export
annulus_window<- function(radius, unit= "cell", resolution, return_dismat=FALSE){
if(length(radius)==1){radius<- rep(radius, 2)}
if(length(radius) > 2){
stop("Specified radius exceeds 2 dimensions")
}
if(radius[1] > radius[2]){
stop("Error: inner radius must be less than or equal to outer radius")
}
if((radius[1] < 1) & (unit == "cell")){
stop("Error: inner radius must be at least 1")
}
if(unit=="cell"){
resolution<- c(1,1)
}
if((radius[1] < max(resolution)) & (unit == "map")){
stop("Error: inner radius must be >= resolution")
}
if(length(resolution) > 2){
stop("Specified inner radius exceeds 2 dimensions")
}

dis_mat<- circle_window(radius = radius[2], unit = unit, resolution = resolution, return_dismat = TRUE)
if(return_dismat){
return(dis_mat)
}
w<- matrix(NA, nrow=nrow(dis_mat), ncol=ncol(dis_mat))
w[(dis_mat >= radius[1]) & (dis_mat <= radius[2])]<- 1
return(w)
}

#' Calculates Bathymetric Position Index
#'
#' Calculates Bathymetric Position Index (BPI). This is the value of the focal pixel minus the mean of the surrounding pixels contained within an annulus shaped window.
#' @param r DEM as a SpatRaster or RasterLayer
#' @param radius a vector of length 2 specifying the inner and outer radii of the annulus c(inner,outer). This is ignored if w is provided.
#' @param unit unit for radius. Either "cell" (number of cells, the default) or "map" for map units (e.g. meters). This is ignored if w is provided.
#' @param w A focal weights matrix representing the annulus focal window created using MultiscaleDEM::annulus_window.
#' @param na.rm A logical vector indicating whether or not to remove NA values before calculations
#' @param include_scale logical indicating whether to append window size to the layer names (default = FALSE). Only valid if radius is used.
#' @return a SpatRaster or RasterLayer
#' @import terra
#' @importFrom raster raster
#' @references
#' Lundblad, E.R., Wright, D.J., Miller, J., Larkin, E.M., Rinehart, R., Naar, D.F., Donahue, B.T., Anderson, S.M., Battista, T., 2006. A benthic terrain classification scheme for American Samoa. Marine Geodesy 29, 89–111.
#' @export

BPI<- function(r, radius=NULL, unit= "cell", w=NULL, na.rm=FALSE, include_scale=FALSE){
og_class<- class(r)[1]
if(og_class=="RasterLayer"){
r<- terra::rast(r) #Convert to SpatRaster
}
#Input checks
if(!(og_class %in% c("RasterLayer", "SpatRaster"))){
stop("Error: Input must be a 'SpatRaster' or 'RasterLayer'")
}
if(terra::nlyr(r)!=1){
stop("Error: Input raster must be one layer.")
}
if(!is.null(w)){
if(class(w)[1] != "matrix"){
stop("Error: w must be a matrix")
}
if(any(0 == (dim(w) %% 2))){
stop("Error: dimensions of w must be odd")
}
}
if(length(radius)==1){radius<- rep(radius, 2)}

resolution<- terra::res(r)

if(is.null(w)){
w<- annulus_window(radius = radius, unit = unit, resolution = resolution)
}

bpi<- r - terra::focal(x = r, w = w, fun = mean, na.rm = na.rm)
names(bpi)<- "BPI"
if(include_scale & (!is.null(radius))){names(bpi)<- paste0(names(bpi), "_", radius[1],"x", radius[2])} #Add scale to layer names
if(og_class=="RasterLayer"){bpi<- raster::raster(bpi)}
return(bpi)
}
98 changes: 45 additions & 53 deletions R/WoodEvans.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@
#' Wood, J., 1996. The geomorphological characterisation of digital elevation models (Ph.D.). University of Leicester.

classify_features_ff<- function(slope_tolerance, curvature_tolerance){
#0=PLANAR
#1=PIT
#2=CHANNEL
#3=PASS
#4=RIDGE
#5=PEAK
out_fun<- function(slope, crosc, maxic, minic){
#1=PLANAR
#2=PIT
#3=CHANNEL
#4=PASS
#5=RIDGE
#6=PEAK
out_fun<- function(slope, planc, maxc, minc){
dplyr::case_when(is.na(slope) ~ NA_real_,
(slope > slope_tolerance) & (crosc > curvature_tolerance) ~ 4,
(slope > slope_tolerance) & (crosc < -curvature_tolerance) ~ 2,
slope > slope_tolerance ~ 0,
(maxic > curvature_tolerance) & (minic > curvature_tolerance) ~ 5,
(maxic > curvature_tolerance) & (minic < -curvature_tolerance) ~ 3,
maxic > curvature_tolerance ~ 4,
(minic < -curvature_tolerance) & (maxic < -curvature_tolerance) ~ 1,
minic < -curvature_tolerance ~ 2,
TRUE ~ 0)}
(slope > slope_tolerance) & (planc > curvature_tolerance) ~ 5,
(slope > slope_tolerance) & (planc < -curvature_tolerance) ~ 3,
slope > slope_tolerance ~ 1,
(maxc > curvature_tolerance) & (minc > curvature_tolerance) ~ 6,
(maxc > curvature_tolerance) & (minc < -curvature_tolerance) ~ 4,
maxc > curvature_tolerance ~ 5,
(minc < -curvature_tolerance) & (maxc < -curvature_tolerance) ~ 2,
minc < -curvature_tolerance ~ 3,
TRUE ~ 1)}
return(out_fun)
}

Expand Down Expand Up @@ -57,7 +57,7 @@ convert_aspect2<- function(aspect){
#' @param mask_aspect Logical. If TRUE (default), aspect will be set to NA and northness and eastness will be set to 0 when slope = 0. If FALSE, aspect is set to 270 degrees or 3*pi/2 radians ((-pi/2)- atan2(0,0)+2*pi) and northness and eastness will be calculated from this.
#' @param return_params Logical indicating whether to return Wood/Evans regression parameters (default = FALSE).
#' @return a SpatRaster (terra) or RasterStack/RasterLayer (raster)
#' @details This function calculates slope, aspect, eastness, northness, profile curvature, planform curvature, mean curvature, maximum curvature, minimum curvature, longitudinal curvature, cross-sectional curvature, and morphometric features using a quadratic surface fit from Z = aX^2+bY^2+cXY+dX+eY+f, where Z is the elevation or depth values, X and Y are the xy coordinates relative to the central cell in the focal window, and a-f are parameters to be estimated (Evans, 1980; Wood, 1996). This is an R/C++ function similar to r.param.scale GRASS GIS function. Note, for aspect, 0 degrees represents north (or if rotated, the direction that increases as you go up rows in your data) and increases clockwise which differs from the way r.param.scale reports aspect. For calculations of northness (cos(asp)) and eastness (sin(asp)), up in the y direction is assumed to be north, and if this is not true for your data (e.g. you are using a rotated coordinate system), you must adjust accordingly. Additionally, mean curvature is included, which is not available in r.param.scale. All formulas with the exception of mean curvature are from Wood 1996. Mean curvature is calculated according to Wilson et al 2007. Formulas for curvatures were adjusted to remove multiplicative constants so that they are all reported in units of 1/length and were adjusted to provide consistent sign convention (Minár et al, 2020). For curvatures, we have adopted a geographic sign convention where convex is positive and concave is negative (i.e., hills are considered convex with positive curvature values; Minár et al, 2020). Naming convention for curvatures is not consistent across the literature, however Minár et al (2020) has suggested a framework in which the reported measures of curvature translate to profile curvature = (kn)s), plan curvature = (kp)c), mean curvature = z''mean, maximum curvature = z''min, minimum curvature = z''max, longitudinal curvature = zss, and cross-sectional curvature = zcc.
#' @details This function calculates slope, aspect, eastness, northness, profile curvature, plan curvature, mean curvature, twisting curvature, maximum curvature, minimum curvature, and morphometric features using a quadratic surface fit from Z = aX^2+bY^2+cXY+dX+eY+f, where Z is the elevation or depth values, X and Y are the xy coordinates relative to the central cell in the focal window, and a-f are parameters to be estimated (Evans, 1980; Minár et al. 2020; Wood, 1996). For aspect, 0 degrees represents north (or if rotated, the direction that increases as you go up rows in your data) and increases clockwise. For calculations of northness (cos(asp)) and eastness (sin(asp)), up in the y direction is assumed to be north, and if this is not true for your data (e.g. you are using a rotated coordinate system), you must adjust accordingly. All curvature formulas are adapted from Minár et al 2020. Therefore all curvatures are reported in units of 1/length and have are reported according to a geographic sign convention where convex is positive and concave is negative (i.e., hills are considered convex with positive curvature values). Naming convention for curvatures is not consistent across the literature, however Minár et al (2020) has suggested a framework in which the reported measures of curvature translate to profile curvature = (kn)s, plan curvature = (kn)c, twisting curvature (τg)c, mean curvature = kmean, maximum curvature = kmax, minimum curvature = kmin. For morphometric features cross-sectional curvature (zcc) was replaced by planc (kn)c, z''min was replaced by kmax, and z''max was replaced by kmin as these are more robust ways to measures the same types of curvature (Minár et al., 2020).
#' @import terra
#' @importFrom raster raster
#' @importFrom raster stack
Expand All @@ -73,7 +73,7 @@ convert_aspect2<- function(aspect){

WoodEvans<- function(r, w=c(3,3), unit= "degrees", metrics= c("qslope", "qaspect", "qeastness", "qnorthness", "profc", "planc", "meanc", "maxc", "minc", "longc", "crosc", "features"), slope_tolerance=1, curvature_tolerance=0.0001, na.rm=FALSE, include_scale=FALSE, mask_aspect=TRUE, return_params= FALSE){

all_metrics<- c("qslope", "qaspect", "qeastness", "qnorthness", "profc", "planc", "meanc", "maxc", "minc", "longc", "crosc", "features")
all_metrics<- c("qslope", "qaspect", "qeastness", "qnorthness", "profc", "planc", "twistc", "meanc", "maxc", "minc", "longc", "crosc", "features")
og_class<- class(r)[1]
if(og_class=="RasterLayer"){
r<- terra::rast(r) #Convert to SpatRaster
Expand Down Expand Up @@ -108,7 +108,7 @@ WoodEvans<- function(r, w=c(3,3), unit= "degrees", metrics= c("qslope", "qaspect
stop("unit must be 'degrees' or 'radians'")
}
if (any(!(metrics %in% all_metrics))){
stop("Error: Invlaid metric. Valid metrics include 'qslope', 'qaspect', 'qeastness', 'qnorthness', 'profc', 'planc', 'meanc', 'maxc', 'minc', 'longc', 'crosc', and `features`.")
stop("Error: Invlaid metric. Valid metrics include 'qslope', 'qaspect', 'qeastness', 'qnorthness', 'profc', 'planc', 'twistc', 'meanc', 'maxc', 'minc', and `features`.")
}

needed_metrics<- metrics
Expand All @@ -119,7 +119,7 @@ WoodEvans<- function(r, w=c(3,3), unit= "degrees", metrics= c("qslope", "qaspect

if("features" %in% needed_metrics){
if(!("qslope" %in% needed_metrics)){needed_metrics<- c(needed_metrics, "qslope")}
if(!("crosc" %in% needed_metrics)){needed_metrics<- c(needed_metrics, "crosc")}
if(!("planc" %in% needed_metrics)){needed_metrics<- c(needed_metrics, "planc")}
if(!("maxc" %in% needed_metrics)){needed_metrics<- c(needed_metrics, "maxc")}
if(!("minc" %in% needed_metrics)){needed_metrics<- c(needed_metrics, "minc")}
}
Expand Down Expand Up @@ -163,9 +163,6 @@ WoodEvans<- function(r, w=c(3,3), unit= "degrees", metrics= c("qslope", "qaspect

if("qaspect" %in% needed_metrics){
asp<- terra::app(atan2(params$e,params$d), fun = convert_aspect2) #Shift aspect so north is zero
#asp<- (-pi/2) - atan2(params$e,params$d) #Shift aspect so north is zero
#asp[asp < 0]<- asp[asp < 0] + 2*pi # Constrain aspect from 0 to 2pi

if (mask_aspect){
slp0_idx<- (params$d== 0 & params$e== 0) #mask indicating when d ane e are 0 (slope is 0)
asp[slp0_idx]<- NA_real_
Expand Down Expand Up @@ -196,62 +193,57 @@ WoodEvans<- function(r, w=c(3,3), unit= "degrees", metrics= c("qslope", "qaspect
}

#Curvature
#Note curvature has been multiplied by 100 to express curvature as percent gradient per unit length (Albani et al 2004)
# Formulas adapted from Minar et al 2020.
#zxx= 2*a
#zyy = 2*b
#zxy=c
#zx=d
#zy=e

# Wood 1996 page 86
if("profc" %in% needed_metrics){
profc<- terra::lapp(params, fun = function(a,b,c,d,e,f)(-2 * (a*d^2 + b*e^2 + c*d*e)) / ((e^2 + d^2)*(1 + e^2 + d^2)^1.5))
profc<- terra::lapp(params, fun = kns)
profc[mask_raster]<- 0
names(profc)<- "profc"
out<- c(out, profc, warn=FALSE)
}

if("planc" %in% needed_metrics){
planc<- terra::lapp(params, fun = function(a,b,c,d,e,f)(-2 * (b*d^2 +a*e^2 - c*d*e)) / ((e^2+d^2)^1.5))
planc<- terra::lapp(params, fun = knc)
planc[mask_raster]<- 0
names(planc)<- "planc"
out<- c(out, planc, warn=FALSE)
}

#Wood 1996 page 88
if("longc" %in% needed_metrics){
longc<- terra::lapp(params, fun = function(a,b,c,d,e,f) -2 * ((a*d^2 + b*e^2 + c*d*e) / (d^2 + e^2)))
longc[mask_raster]<- 0
names(longc)<- "longc"
out<- c(out, longc, warn=FALSE)
}

if("crosc" %in% needed_metrics){
crosc<- terra::lapp(params, fun = function(a,b,c,d,e,f) -2 * ((b*d^2 + a*e^2 - c*d*e) / (d^2 + e^2)))
crosc[mask_raster]<- 0
names(crosc)<- "crosc"
out<- c(out, crosc, warn=FALSE)
if("twistc" %in% needed_metrics){
twistc<- terra::lapp(params, fun = tgc)
twistc[mask_raster]<- 0
names(twistc)<- "twistc"
out<- c(out, twistc, warn=FALSE)
}

#Wood 1996 page 115
if("maxc" %in% needed_metrics){
max_curv<- terra::lapp(params, fun = function(a,b,c,d,e,f) (-a - b + sqrt((a-b)^2+c^2)))
names(max_curv)<- "maxc"
out<- c(out, max_curv, warn=FALSE)
}
maxc<- terra::lapp(params, fun = kmax)
names(maxc)<- "maxc"
out<- c(out, maxc, warn=FALSE)
}

if("minc" %in% needed_metrics){
min_curv<- terra::lapp(params, fun = function(a,b,c,d,e,f) (-a - b - sqrt((a-b)^2+c^2)))
names(min_curv)<- "minc"
out<- c(out, min_curv, warn=FALSE)
minc<- terra::lapp(params, fun = kmin)
names(minc)<- "minc"
out<- c(out, minc, warn=FALSE)
}

#Wilson 2007 page 10
if("meanc" %in% needed_metrics){
mean_curv<- terra::lapp(params, fun = function(a,b,c,d,e,f) (-a - b))
mean_curv<- terra::lapp(params, fun = kmean)
names(mean_curv)<- "meanc"
out<- c(out, mean_curv, warn=FALSE)
}

#Morphometric Features (Wood 1996, Page 120)
#Modified version of Morphometric Features (Wood 1996, Page 120)
if("features" %in% needed_metrics){
classify_features<- classify_features_ff(slope_tolerance, curvature_tolerance) #Define classification function based on slope and curvature tolerance
features<- terra::lapp(c(slp, crosc, max_curv, min_curv), fun = classify_features)
levels(features)<- data.frame(ID=0:5, features = c("Planar", "Pit", "Channel", "Pass", "Ridge", "Peak"))
features<- terra::lapp(c(slp, planc, maxc, minc), fun = classify_features)
levels(features)<- data.frame(ID=1:6, features = c("Planar", "Pit", "Channel", "Pass", "Ridge", "Peak"))
names(features)<- "features"
out<- c(out, features, warn=FALSE)
}
Expand Down
41 changes: 41 additions & 0 deletions R/curvatures.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
knc<- function(a,b,c,d,e,f){
#Planc: Normal Contour Curvature
out<- -(2*a*(e^2) - 2*c*d*e + 2*b*d^2)/((d^2+e^2) * sqrt(1+d^2+e^2))
return(out)
}

kns<- function(a,b,c,d,e,f){
#Profc: Normal Slope Line Curvature
out<- (-2 * (a*d^2 + b*e^2 + c*d*e)) / ((e^2 + d^2)*(1 + e^2 + d^2)^1.5)
return(out)
}

tgc<- function(a,b,c,d,e,f){
#TwistC: Contour geodesic torsion
out<- (d*e*(2*a-2*b) - c*(d^2-e^2))/((d^2+e^2)*(1+d^2+e^2))
return(out)
}

kmean<- function(a,b,c,d,e,f){
#Mean Curvature
out<- -((1+e^2)*2*a - 2*c*d*e + (1+d^2)*2*b) / (2*sqrt((1+d^2+e^2)^3))
return(out)
}

ku<- function(a,b,c,d,e,f){
#unsphericity curvature
out<- sqrt((((1+e^2)*2*a - 2*c*d*e +(1+d^2)*2*b) / (2*sqrt((1+d^2+e^2)^3)))^2 - ((2*a*2*b-c^2)/(1+d^2+e^2)^2))
return(out)
}

kmin<- function(a,b,c,d,e,f){
#Min Curvature
out<- kmean(a,b,c,d,e)-ku(a,b,c,d,e)
return(out)
}

kmax<- function(a,b,c,d,e,f){
#Max Curvature
out<- kmean(a,b,c,d,e)+ku(a,b,c,d,e)
return(out)
}
Loading

0 comments on commit 9ef2d93

Please sign in to comment.