Nothing
# ==== STANDARD ====
#' Predefined standard metrics functions
#'
#' Predefined metrics functions intended to me used in `*_metrics` function such as
#' \link{pixel_metrics}, \link{cloud_metrics}, \link{crown_metrics}, \link{voxel_metrics} and
#' so on. Each function comes with a convenient shortcuts for lazy coding. The `lidR` package aims
#' to provide an easy way to compute user-defined metrics rather than to provide them. However, for
#' efficiency and to save time, sets of standard metrics have been predefined (see details). Every
#' function can be computed by every `*_metrics` functions however `stdmetrics*` are
#' more pixel-based metrics, `stdtreemetrics` are more tree-based metrics and `stdshapemetrics` are
#' more point-based metrics. For example the metric `zmean` computed by `stdmetrics_z` makes sense
#' when computed at the pixel level but brings no information at the voxel level.
#'
#' The function names, their parameters and the output names of the metrics rely on a nomenclature
#' chosen for brevity:
#' \itemize{
#' \item{`z`: refers to the elevation}
#' \item{`i`: refers to the intensity}
#' \item{`rn`: refers to the return number}
#' \item{`q`: refers to quantile}
#' \item{`a`: refers to the ScanAngleRank or ScanAngle}
#' \item{`n`: refers to a number (a count)}
#' \item{`p`: refers to a percentage}
#' }
#' For example the metric named `zq60` refers to the elevation, quantile, 60 i.e. the 60th percentile
#' of elevations. The metric `pground` refers to a percentage. It is the percentage of points
#' classified as ground. The function `stdmetric_i` refers to metrics of intensity. A description of
#' each existing metric can be found on the \href{https://github.com/r-lidar/lidR/wiki/stdmetrics}{lidR wiki page}.\cr\cr
#' Some functions have optional parameters. If these parameters are not provided the function
#' computes only a subset of existing metrics. For example, `stdmetrics_i` requires the intensity
#' values, but if the elevation values are also provided it can compute additional metrics such as
#' cumulative intensity at a given percentile of height.\cr\cr
#' Each function has a convenient associated variable. It is the name of the function, with a
#' dot before the name. This enables the function to be used without writing parameters. The cost
#' of such a feature is inflexibility. It corresponds to a predefined behaviour (see examples)\cr
#' \describe{
#' \item{`stdmetrics`}{is a combination of `stdmetrics_ctrl` + `stdmetrics_z` +
#' `stdmetrics_i` + `stdmetrics_rn`}
#' \item{`stdtreemetrics`}{is a special function that works with \link{crown_metrics}. Actually,
#' it won't fail with other functions but the output makes more sense if computed at the
#' individual tree level.}
#' \item{`stdshapemetrics`}{is a set of eigenvalue based feature described in Lucas et al, 2019
#' (see references).}
#' }
#'
#' @param x,y,z,i Coordinates of the points, Intensity
#' @param rn,class ReturnNumber, Classification
#' @param pulseID The number referencing each pulse
#' @param dz numeric. Layer thickness metric \link[=entropy]{entropy}
#' @param th numeric. Threshold for metrics pzabovex. Can be a vector to compute with several thresholds.
#' @param zmin numeric. Lower bound of the integral for zpcumx metrics.
#' See \href{https://github.com/r-lidar/lidR/wiki/stdmetrics}{wiki page} and Wood et al.
#' (2008) reference.
#' @examples
#' LASfile <- system.file("extdata", "Megaplot.laz", package="lidR")
#' las <- readLAS(LASfile, select = "*", filter = "-keep_random_fraction 0.5")
#'
#' # All the predefined metrics
#' m1 <- pixel_metrics(las, ~stdmetrics(X,Y,Z,Intensity,ReturnNumber,Classification,dz=1), res = 40)
#'
#' # Convenient shortcut
#' m2 <- pixel_metrics(las, .stdmetrics, res = 40)
#'
#' # Basic metrics from intensities
#' m3 <- pixel_metrics(las, ~stdmetrics_i(Intensity), res = 40)
#'
#' # All the metrics from intensities
#' m4 <- pixel_metrics(las, ~stdmetrics_i(Intensity, Z, Classification, ReturnNumber), res = 40)
#'
#' # Convenient shortcut for the previous example
#' m5 <- pixel_metrics(las, .stdmetrics_i, res = 40)
#'
#' # Combine some predefined function with your own new metrics
#' # Here convenient shortcuts are no longer usable.
#' myMetrics = function(z, i, rn)
#' {
#' first <- rn == 1L
#' zfirst <- z[first]
#' nfirst <- length(zfirst)
#' above2 <- sum(z > 2)
#'
#' x <- above2/nfirst*100
#'
#' # User's metrics
#' metrics <- list(
#' above2aboven1st = x, # Num of returns above 2 divided by num of 1st returns
#' zimean = mean(z*i), # Mean products of z by intensity
#' zsqmean = sqrt(mean(z^2)) # Quadratic mean of z
#' )
#'
#' # Combined with standard metrics
#' return( c(metrics, stdmetrics_z(z)) )
#' }
#'
#' m10 <- pixel_metrics(las, ~myMetrics(Z, Intensity, ReturnNumber), res = 40)
#'
#' # Users can write their own convenient shorcuts like this:
#' .myMetrics = ~myMetrics(Z, Intensity, ReturnNumber)
#' m11 <- pixel_metrics(las, .myMetrics, res = 40)
#'
#' @references
#' M. Woods, K. Lim, and P. Treitz. Predicting forest stand variables from LiDAR data in
#' the Great Lakes – St. Lawrence forest of Ontario. The Forestry Chronicle. 84(6): 827-839.
#' https://doi.org/10.5558/tfc84827-6
#' @rdname stdmetrics
#' @export
#' @md
stdmetrics = function(x, y, z, i, rn, class, dz = 1, th = 2, zmin = 0)
{
C <- stdmetrics_ctrl(x, y, z)
Z <- stdmetrics_z(z, dz, th, zmin)
I <- stdmetrics_i(i, z, class, rn)
RN <- stdmetrics_rn(rn, class)
#PU = stdmetrics_pulse(pulseID, rn)
metrics <- c(Z, I, RN, C)
return(metrics)
}
#' @rdname stdmetrics
#' @export
stdmetrics_z = function(z, dz = 1, th = 2, zmin = 0)
{
n <- length(z)
zmax <- max(z)
zmean <- mean(z)
probs <- seq(0.05, 0.95, 0.05)
zq <- as.list(stats::quantile(z, probs))
names(zq) <- paste0("zq", probs*100)
pzabovex <- lapply(th, function(x) { fast_countover(z, x) / n * 100 })
names(pzabovex) <- paste0("pzabove", th)
pzabovemean <- fast_countover(z, zmean) / n * 100
if (zmax <= zmin)
{
d <- rep(0, 9)
}
else
{
breaks <- seq(zmin, zmax, (zmax-zmin)/10)
d <- findInterval(z[z>zmin], breaks)
d <- fast_table(d, 10)
d <- d / sum(d)*100
d <- cumsum(d)[1:9]
d <- as.list(d)
}
names(d) <- paste0("zpcum", 1:9)
metrics <- list(
zmax = zmax,
zmean = zmean,
zsd = stats::sd(z),
zskew = (sum((z - zmean)^3)/n)/(sum((z - zmean)^2)/n)^(3/2),
zkurt = n * sum((z - zmean)^4)/(sum((z - zmean)^2)^2),
zentropy = entropy(z, dz),
pzabovezmean = pzabovemean
)
metrics <- c(metrics, pzabovex, zq, d)
return(metrics)
}
#' @rdname stdmetrics
#' @export
stdmetrics_i = function(i, z = NULL, class = NULL, rn = NULL)
{
itot <- imax <- imean <- isd <- icv <- iskew <- ikurt <- NULL
icumzq10 <- icumzq30 <- icumzq50 <- icumzq70 <- icumzq90 <- NULL
n <- length(i)
itot <- as.double(sum(i))
imean <- mean(i)
probs <- seq(0.05, 0.95, 0.05)
iq <- as.list(stats::quantile(i, probs))
names(iq) <- paste("iq", probs*100, sep = "")
metrics <- list(
itot = itot,
imax = max(i),
imean = imean,
isd = stats::sd(i),
iskew = (sum((i - imean)^3)/n)/(sum((i - imean)^2)/n)^(3/2),
ikurt = n * sum((i - imean)^4)/(sum((i - imean)^2)^2)
)
if (!is.null(class))
{
metrics <- c(metrics, list(ipground = sum(i[class == 2])/itot*100))
}
if (!is.null(z))
{
zq <- stats::quantile(z, probs = seq(0.1, 0.9, 0.2))
ipcum <- list(
ipcumzq10 = sum(i[z <= zq[1]])/itot*100,
ipcumzq30 = sum(i[z <= zq[2]])/itot*100,
ipcumzq50 = sum(i[z <= zq[3]])/itot*100,
ipcumzq70 = sum(i[z <= zq[4]])/itot*100,
ipcumzq90 = sum(i[z <= zq[5]])/itot*100
)
metrics <- c(metrics, ipcum)
}
metrics
}
#' @rdname stdmetrics
#' @export
stdmetrics_rn = function(rn, class = NULL)
{
nground <- NULL
n <- length(rn)
prn <- table(factor(rn, levels = c(1:5)))/n*100
prn <- as.list(prn)
names(prn) <- paste0("p", names(prn), "th")
metrics <- prn
if (!is.null(class))
metrics = c(metrics, list(pground = sum(class == 2)/n*100))
return(metrics)
}
#' @rdname stdmetrics
#' @export
stdmetrics_pulse = function(pulseID, rn)
{
. <- nr <- NULL
n <- length(rn)
dt <- data.table::data.table(pulseID, rn)
np_with_x_return <- dt[, .(nr = max(rn)), by = pulseID][, .N, by = nr]
data.table::setorder(np_with_x_return, nr)
np <- numeric(9)
names(np) <- paste0("ppulse", 1:9, "return")
np[np_with_x_return$nr] <- np_with_x_return$N/n*100
np <- as.list(np)
return(np)
}
#' @rdname stdmetrics
#' @export
stdmetrics_ctrl = function(x, y, z)
{
xmax <- max(x)
ymax <- max(y)
xmin <- min(x)
ymin <- min(y)
n <- length(z)
area <- (xmax - xmin)*(ymax - ymin)
metrics <- list(n = n, area = area)
return(metrics)
}
#' @rdname stdmetrics
#' @export
stdtreemetrics = function(x, y, z)
{
metrics <- list(
Z = max(z),
npoints = length(x),
convhull_area = round(area_convex_hull(x,y),3)
)
return(metrics)
}
#' @rdname stdmetrics
#' @export
#' @references
#' Lucas, C., Bouten, W., Koma, Z., Kissling, W. D., & Seijmonsbergen, A. C. (2019). Identification
#' of Linear Vegetation Elements in a Rural Landscape Using LiDAR Point Clouds. Remote Sensing, 11(3), 292.
stdshapemetrics = function(x,y,z)
{
xyz <- cbind(x,y,z)
eigen <- fast_eigen_values(xyz)
eigen_m <- eigen[[1]]
eigen_m <- as.numeric(eigen_m)
shapemetrics <- list(
eigen_largest = eigen_m[1],
eigen_medium = eigen_m[2],
eigen_smallest = eigen_m[3],
curvature = eigen_m[3]/(eigen_m[1] + eigen_m[2] + eigen_m[3]),
linearity = (eigen_m[1] - eigen_m[2])/eigen_m[1],
planarity = (eigen_m[2] - eigen_m[3])/eigen_m[1],
sphericity = eigen_m[3]/eigen_m[1],
anisotropy = (eigen_m[1] - eigen_m[3])/eigen_m[1],
horizontality = abs(eigen[[2]][3,3])
)
return(shapemetrics)
}
stdtreeapex <- function(x,y,z, ...)
{
if (length(x) < 4)
return(NULL)
j <- which.max(z)
point <- sf::st_point(c(x[j], y[j], z[j]))
return(list(geom = list(point)))
}
stdtreehullconvex = function(x,y,z, ...)
{
if (length(x) < 4)
return(NULL)
i <- grDevices::chull(x,y)
i <- c(i, i[1])
P <- cbind(x[i], y[i])
poly <- sf::st_polygon(list(P))
return(list(geom = list(poly)))
}
stdtreehullconcave = function(x,y,z, concavity, length_threshold)
{
if (length(x) < 4)
return(NULL)
P <- concaveman(x, y, concavity, length_threshold)
P <- as.matrix(P)
poly <- sf::st_polygon(list(P))
return(list(geom = list(poly)))
}
stdtreehullbbox = function(x,y,z, ...)
{
if (length(x) < 4)
return(NULL)
xmin <- min(x)
ymin <- min(y)
xmax <- max(x)
ymax <- max(y)
x <- c(xmin, xmax, xmax, xmin, xmin)
y <- c(ymin, ymin, ymax, ymax, ymin)
P <- cbind(x, y)
poly <- sf::st_polygon(list(P))
return(list(geom = list(poly)))
}
#' @rdname stdmetrics
#' @export
.stdmetrics = ~stdmetrics(X,Y,Z,Intensity, ReturnNumber, Classification, dz = 1, th = 2)
#' @rdname stdmetrics
#' @export
.stdmetrics_z = ~stdmetrics_z(Z, dz =1)
#' @rdname stdmetrics
#' @export
.stdmetrics_i = ~stdmetrics_i(Intensity, Z, Classification, ReturnNumber)
#' @rdname stdmetrics
#' @export
.stdmetrics_rn = ~stdmetrics_rn(ReturnNumber, Classification)
#' @rdname stdmetrics
#' @export
.stdmetrics_pulse = ~stdmetrics_pulse(pulseID, ReturnNumber)
#' @rdname stdmetrics
#' @export
.stdmetrics_ctrl = ~stdmetrics_ctrl(X, Y, Z)
#' @rdname stdmetrics
#' @export
.stdtreemetrics = ~stdtreemetrics(X, Y, Z)
#' @rdname stdmetrics
#' @export
.stdshapemetrics = ~stdshapemetrics(X, Y, Z)
# ===== OTHER =====
#' Predefined non standard metrics
#'
#' Functions and metrics from the literature. See details and references
#'
#' \describe{
#' \item{rumple_index}{Computes the roughness of a surface as the ratio between its area and its
#' projected area on the ground. If the input is a gridded object (raster) the function computes the
#' surfaces using Jenness's algorithm (see references). If the input is a point cloud the function
#' uses a Delaunay triangulation of the points and computes the area of each triangle.}
#' \item{gap_fraction_profile}{Computes the gap fraction profile using the method of Bouvier et al.
#' (see reference). The function assesses the number of laser points that actually reached the layer
#' z+dz and those that passed through the layer [z, z+dz]. By definition the layer 0
#' will always return 0 because no returns pass through the ground. Therefore, the layer 0 is removed
#' from the returned results.}
#' \item{LAD}{Computes a leaf area density profile based on the method of Bouvier et al. (see reference)
#' The function assesses the number of laser points that actually reached the layer z+dz and those that
#' passed through the layer [z, z+dz] (see \link[=gap_fraction_profile]{gap_fraction_profile}).
#' Then it computes the log of this quantity and divides it by the extinction coefficient k as
#' described in Bouvier et al. By definition the layer 0 will always return infinity because no returns
#' pass through the ground. Therefore, the layer 0 is removed from the returned results.}
#' \item{entropy}{A normalized Shannon vertical complexity index. The Shannon diversity index is a
#' measure for quantifying diversity and is based on the number and frequency of species present. This index,
#' developed by Shannon and Weaver for use in information theory, was successfully transferred
#' to the description of species diversity in biological systems (Shannon 1948). Here it is applied
#' to quantify the diversity and the evenness of an elevational distribution of las points. It
#' makes bins between 0 and the maximum elevation. If there are negative values the function
#' returns NA.}
#' \item{VCI}{Vertical Complexity Index. A fixed normalization of the entropy function from van Ewijk
#' et al. (2011) (see references)}
#' }
#'
#' @name nstdmetrics
#' @rdname nstdmetrics
#' @param by,dz numeric. The thickness of the layers used (height bin)
#' @param zmax numeric. Maximum elevation for an entropy normalized to zmax.
NULL
#' @param x A `RasterLayer`, a `stars` a `SpatRaster` or a vector of x coordinates.
#' @param y numeric. If `x` is a vector of coordinates: the associated y coordinates.
#' @param z numeric. If `x` is a vector of coordinates: the associated z coordinates.
#' @param ... unused
#' @return numeric. The computed Rumple index.
#'
#' @export
#' @examples
#' x <- runif(20, 0, 100)
#' y <- runif(20, 0, 100)
#'
#' if (require(geometry, quietly = TRUE))
#' {
#' # Perfectly flat surface, rumple_index = 1
#' z <- rep(10, 20)
#' rumple_index(x, y, z)
#'
#' # Rough surface, rumple_index > 1
#' z <- runif(20, 0, 10)
#' rumple_index(x, y, z)
#'
#' # Rougher surface, rumple_index increases
#' z <- runif(20, 0, 50)
#' rumple_index(x, y, z)
#'
#' # Measure of roughness is scale-dependent
#' rumple_index(x, y, z)
#' rumple_index(x/10, y/10, z)
#' }
#' @md
#' @references
#' Jenness, J. S. (2004). Calculating landscape surface area from digital elevation
#' models. Wildlife Society Bulletin, 32(3), 829–839.
#' @rdname nstdmetrics
rumple_index = function(x, y = NULL, z = NULL, ...)
{
UseMethod("rumple_index", x)
}
#' @export
rumple_index.RasterLayer <- function(x, y = NULL, z = NULL, ...)
{
res <- raster_res(x)
x <- raster_as_matrix(x)$z
return(rumple_index.matrix(x, res[1], res[2]))
}
#' @export
rumple_index.stars <- function(x, y = NULL, z = NULL, ...)
{
res <- raster_res(x)
x <- raster_as_matrix(x)$z
return(rumple_index.matrix(x, res[1], res[2]))
}
#' @export
rumple_index.SpatRaster <- function(x, y = NULL, z = NULL, ...)
{
res <- raster_res(x)
x <- raster_as_matrix(x)$z
return(rumple_index.matrix(x, res[1], res[2]))
}
#' @export
rumple_index.matrix <- function(x, y = NULL, z = NULL, ...)
{
area <- sp::surfaceArea(x, y, z)
parea <- sum(!is.na(x))*y*z
return(area/parea)
}
#' @export
rumple_index.numeric <- function(x, y = NULL, z = NULL, ...)
{
assert_is_numeric(x)
assert_is_numeric(y)
assert_is_numeric(z)
assert_are_same_length(x,y)
assert_are_same_length(x,z)
assert_package_is_installed("geometry")
if (length(x) <= 3)
return(NA_real_)
rumple <- tryCatch(
{
X <- cbind(x,y,z)
dn <- suppressMessages(geometry::delaunayn(X[,1:2], options = "QbB"))
N <- C_tinfo(dn, X)
area <- sum(N[,5])
parea <- sum(N[,6])
return(area/parea)
},
error = function(e)
{
message(paste0(e, "\n'rumple_index' returned NA."))
if (getOption("lidR.debug")) dput(X[,1:2])
return(NA_real_)
})
return(rumple)
}
#' @param z vector of positive z coordinates
#' @param z0 numeric. The bottom limit of the profile
#' @return A data.frame containing the bin elevations (z) and the gap fraction for each bin (gf)
#' @examples
#' z <- c(rnorm(1e4, 25, 6), rgamma(1e3, 1, 8)*6, rgamma(5e2, 5,5)*10)
#' z <- z[z<45 & z>0]
#'
#' hist(z, n=50)
#'
#' gapFraction = gap_fraction_profile(z)
#'
#' plot(gapFraction, type="l", xlab="Elevation", ylab="Gap fraction")
#' @references Bouvier, M., Durrieu, S., Fournier, R. a, & Renaud, J. (2015). Generalizing predictive
#' models of forest inventory attributes using an area-based approach with airborne las data. Remote
#' Sensing of Environment, 156, 322-334. http://doi.org/10.1016/j.rse.2014.10.004
#' @export
#' @rdname nstdmetrics
gap_fraction_profile = function(z, dz = 1, z0 = 2)
{
zrange = range(z)
if (z0 < zrange[1])
z0 = floor((zrange[1]- z0)/dz)*dz + z0
if (z0 >= zrange[2])
return(data.frame(z = numeric(0), gf = numeric(0)))
bk <- seq(z0, ceiling((zrange[2] - z0)/dz)*dz + z0, dz)
histogram <- graphics::hist(z, breaks = c(-Inf, bk), plot = F)
h <- histogram$mids
p <- histogram$counts
cs <- cumsum(p)
i <- cs[1:(length(cs)-1)]/cs[2:length(cs)]
i[is.na(i)] = 0
z = h[-1]
return(data.frame(z = z, gf = i))
}
#' @param k numeric. is the extinction coefficient
#' @examples
#' z <- c(rnorm(1e4, 25, 6), rgamma(1e3, 1, 8)*6, rgamma(5e2, 5,5)*10)
#' z <- z[z<45 & z>0]
#'
#' lad <- LAD(z)
#'
#' plot(lad, type="l", xlab="Elevation", ylab="Leaf area density")
#' @export
#' @rdname nstdmetrics
LAD = function(z, dz = 1, k = 0.5, z0 = 2) # (Bouvier et al. 2015)
{
ld <- gap_fraction_profile(z, dz, z0)
if (nrow(ld) == 0)
return(data.frame(z = numeric(0), lad = numeric(0)))
lad <- ld$gf
lad <- -log(lad)/(k*dz)
lad[is.infinite(lad)] = NA
lad <- lad
return(data.frame(z = ld$z, lad = lad))
}
#' @examples
#' z <- runif(10000, 0, 10)
#'
#' # expected to be close to 1. The highest diversity is given for a uniform distribution
#' entropy(z, by = 1)
#'
#' z <- runif(10000, 9, 10)
#'
#' # Must be 0. The lowest diversity is given for a unique possibility
#' entropy(z, by = 1)
#'
#' z <- abs(rnorm(10000, 10, 1))
#'
#' # expected to be between 0 and 1.
#' entropy(z, by = 1)
#' @references
#' Pretzsch, H. (2008). Description and Analysis of Stand Structures. Springer Berlin Heidelberg. http://doi.org/10.1007/978-3-540-88307-4 (pages 279-280)
#' Shannon, Claude E. (1948), "A mathematical theory of communication," Bell System Tech. Journal 27, 379-423, 623-656.
#' @return A number between 0 and 1
#' @export
#' @rdname nstdmetrics
entropy = function(z, by = 1, zmax = NULL)
{
# Fixed entropy (van Ewijk et al. (2011)) or flexible entropy
if (is.null(zmax))
zmax = max(z)
# If zmax < 3 it is meaningless to compute entropy
if (zmax < 2 * by)
return(NA_real_)
if (min(z) < 0)
return(NA_real_)
# Define the number of x meters bins from 0 to zmax (rounded to the next integer)
bk <- seq(0, ceiling(zmax/by)*by, by)
# Compute the p for each bin
hist <- findInterval(z, bk)
hist <- fast_table(hist, length(bk) - 1)
hist <- hist/sum(hist)
# Remove bins where there are no points because of log(0)
p <- hist[hist > 0]
pref <- rep(1/length(hist), length(hist))
# normalized entropy
S <- -sum(p*log(p)) / -sum(pref*log(pref))
return(S)
}
#' @return A number between 0 and 1
#' @examples
#' z <- runif(10000, 0, 10)
#'
#' VCI(z, by = 1, zmax = 20)
#'
#' z <- abs(rnorm(10000, 10, 1))
#'
#' # expected to be closer to 0.
#' VCI(z, by = 1, zmax = 20)
#' @references van Ewijk, K. Y., Treitz, P. M., & Scott, N. A. (2011). Characterizing Forest Succession in Central Ontario using LAS-derived Indices. Photogrammetric Engineering and Remote Sensing, 77(3), 261-269. Retrieved from <Go to ISI>://WOS:000288052100009
#' @export
#' @rdname nstdmetrics
VCI = function(z, zmax, by = 1)
{
z <- z[z < zmax]
return(entropy(z, by, zmax))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.