R/algorithm-itd.R

Defines functions manual lmf

Documented in lmf manual

# ===== LMF ======

#' Individual Tree Detection Algorithm
#'
#' This function is made to be used in \link{locate_trees}. It implements an algorithm for tree
#' detection based on a local maximum filter. The windows size can be fixed or variable and its
#' shape can be square or circular. The internal algorithm works either with a raster or a point cloud.
#' It is deeply inspired by Popescu & Wynne (2004) (see references).
#'
#' @param ws numeric or function. Length or diameter of the moving window used to detect the local
#' maxima in the units of the input data (usually meters). If it is numeric a fixed window size is used.
#' If it is a function, the function determines the size of the window at any given location on the canopy.
#' By default function takes the height of a given pixel or point as its only argument and return the
#' desired size of the search window when centered on that pixel/point. This can be controled with
#' the `ws_args` parameter
#' @param hmin numeric. Minimum height of a tree. Threshold below which a pixel or a point
#' cannot be a local maxima. Default is 2.
#' @param shape character. Shape of the moving window used to find the local maxima. Can be "square"
#' or "circular".
#' @param ws_args list. Named list of argument for the function `ws` if `ws` is a function.
#'
#' @references
#' Popescu, Sorin & Wynne, Randolph. (2004). Seeing the Trees in the Forest: Using Lidar and
#' Multispectral Data Fusion with Local Filtering and Variable Window Size for Estimating Tree Height.
#' Photogrammetric Engineering and Remote Sensing. 70. 589-604. 10.14358/PERS.70.5.589.
#'
#' @export
#'
#' @family individual tree detection algorithms
#'
#' @examples
#' LASfile <- system.file("extdata", "MixedConifer.laz", package="lidR")
#' las <- readLAS(LASfile, select = "xyzi", filter = "-inside 481250 3812980 481300 3813050")
#'
#' # =================
#' # point-cloud-based
#' # =================
#'
#' # 5x5 m fixed window size
#' ttops <- locate_trees(las, lmf(5))
#'
#' #plot(las) |> add_treetops3d(ttops)
#'
#' # variable windows size
#' f <- function(x) { x * 0.07 + 3}
#' ttops <- locate_trees(las, lmf(f))
#'
#' #plot(las) |> add_treetops3d(ttops)
#'
#' # Very custom variable windows size
#' f <- function(x, y, z) { x * 0.07 + y * 0.01 + z}
#' ws_args <- list(x = "Z", y = "Intensity", z = 3)
#' ttops <- locate_trees(las, lmf(f, ws_args = ws_args))
#'
#' # ============
#' # raster-based
#' # ============
#'
#' chm <- rasterize_canopy(las, res = 1, p2r(0.15), pkg = "terra")
#' ttops <- locate_trees(chm, lmf(5))
#'
#' plot(chm, col = height.colors(30))
#' plot(sf::st_geometry(ttops), add = TRUE, col = "black", cex = 0.5, pch = 3)
#'
#' # variable window size
#' f <- function(x) { x * 0.07 + 3 }
#' ttops <- locate_trees(chm, lmf(f))
#'
#' plot(chm, col = height.colors(30))
#' plot(sf::st_geometry(ttops), add = TRUE, col = "black", cex = 0.5, pch = 3)
#' @name itd_lmf
lmf = function(ws, hmin = 2, shape = c("circular", "square"), ws_args = "Z")
{
  shape <- match.arg(shape)
  circ  <- shape == "circular"
  ws    <- lazyeval::uq(ws)
  hmin  <- lazyeval::uq(hmin)
  ws_args  <- lazyeval::uq(ws_args)

  if (!is.numeric(ws) & !is.function(ws))
    stop("'ws' must be a number or a function", call. = FALSE)

  f = function(las)
  {
    assert_is_valid_context(LIDRCONTEXTITD, "lmf")

    if (is.function(ws))
    {
      args <- lapply(ws_args, function(x) if (x %in% names(las)) las@data[[x]] else x)
      ws <- do.call(ws, args)
      b <- las$Z < hmin
      ws[b] <- min(ws)

      n <- npoints(las)
      if (!is.numeric(ws)) stop("The function 'ws' did not return a correct output. ", call. = FALSE)
      if (any(ws <= 0))    stop("The function 'ws' returned negative or null values.", call. = FALSE)
      if (anyNA(ws))       stop("The function 'ws' returned NA values.",               call. = FALSE)
      if (length(ws) != n) stop("The function 'ws' did not return a correct output.",  call. = FALSE)
    }

    force_autoindex(las) <- LIDRGRIDPARTITION
    return(C_lmf(las, ws, hmin, circ, getThread()))
  }

  f <- plugin_itd(f, omp = TRUE, raster_based = FALSE)
  return(f)
}

# ===== MANUAL ======

#' Individual Tree Detection Algorithm
#'
#' This function is made to be used in \link{locate_trees}. It implements an algorithm for manual
#' tree detection. Users can pinpoint the tree top positions manually and interactively using the mouse.
#' This is only suitable for small-sized plots. First the point cloud is displayed, then the user is
#' invited to select a rectangular region of interest in the scene using the mouse button.
#' Within the selected region the highest point will be flagged as 'tree top' in the scene. Once all the trees
#' are labelled the user can exit the tool by selecting an empty region. Points can also be unflagged.
#' The goal of this tool is mainly for minor correction of automatically-detected tree outputs. \cr
#' **This algorithm does not preserve tree IDs from `detected` and renumber all trees. It also looses
#' all attributes**
#'
#' @param detected `SpatialPoints* or `sf/sfc_POINT` with  2 or 3D points of already found tree tops
#' that need manual correction. Can be NULL
#' @param radius numeric. Radius of the spheres displayed on the point cloud (aesthetic purposes only).
#' @param color character. Colour of the spheres displayed on the point cloud (aesthetic purposes only).
#' @param button Which button to use for selection. One of "left", "middle", "right". lidR using left
#' for rotation and right for dragging using one of left or right will disable either rotation or dragging
#' @param ... supplementary parameters to be passed to \link{plot}.
#'
#' @family individual tree detection algorithms
#'
#' @export
#' @md
#' @examples
#' \dontrun{
#' LASfile <- system.file("extdata", "MixedConifer.laz", package="lidR")
#' las = readLAS(LASfile)
#'
#' # Full manual tree detection
#' ttops = locate_trees(las, manual())
#'
#' # Automatic detection with manual correction
#' ttops = locate_trees(las, lmf(5))
#' ttops = locate_trees(las, manual(ttops))
#' }
#' @name itd_manual
manual = function(detected = NULL, radius = 0.5, color = "red", button = "middle", ...) # nocov start
{
  f = function(las)
  {
    assert_is_valid_context(LIDRCONTEXTITD, "manual")

    . <- z <- X <- Y <- Z <- treeID <- NULL

    stopifnotlas(las)
    crs <- sf::st_crs(las)

    if (!interactive())
      stop("R is not being used interactively", call. = FALSE)

    if (is.null(detected))
    {
      apice <- data.table::data.table(X = numeric(0), Y = numeric(0), Z = numeric(0))
    }
    else
    {
      if (inherits(detected, "SpatialPoints"))
        detected <- sf::st_as_sf(detected)

      if (!is(detected, "sf") & !is(detected, "sfc"))
        stop("'apice' is not a SpatialPointsDataFrame or sf")

      coords   <- sf::st_coordinates(detected)
      u   <- coords[,1]
      v   <- coords[,2]

      if (ncol(coords) == 3)
        w <- coords[,3]
      else
        w <- detected[["Z"]]

      apice <- data.table::data.table(X = u, Y = v, Z = w)
    }

    minx <- min(las$X)
    miny <- min(las$Y)

    las@data <- las@data[, .(X, Y, Z)]
    las@data[, X := X - minx]
    las@data[, Y := Y - miny]
    apice$X = apice$X - minx
    apice$Y = apice$Y - miny

    plot.LAS(las, ..., clear_artifacts = FALSE)

    id = numeric(nrow(apice))

    # It's very slow to redraw after every sphere, so
    # turn off updates for a bit
    saveSkip = rgl::par3d(skipRedraw = TRUE)
    on.exit(rgl::par3d(saveSkip))  # Just in case of error

    for (i in 1:nrow(apice))
      id[i] = rgl::spheres3d(apice$X[i], apice$Y[i], apice$Z[i], radius = radius, color = color)

    # Now restore drawing mode
    rgl::par3d(saveSkip)
    on.exit() # exit restore no longer needed

    apice$id <- id

    repeat
    {
      # Select a region
      f <- rgl::select3d(button = button)

      # Get the apices in the selected region
      i <- if (nrow(apice) > 0) f(apice) else FALSE

      # There are some apices in the selected region: remove them
      if (sum(i) > 0)
      {
        ii <- which(i == TRUE)
        rgl::pop3d(id = apice[ii]$id)
        apice <- apice[-ii]
      }
      # There is no apex in the selected region: find an apex
      else
      {
        # Get the points in the selected region
        i <- f(las@data)

        # There is 0 points is the region: exit the function
        if (sum(i) == 0)
          break;

        # There are some points: find the highest one and add it to the list of apices
        pts     <- las@data[i, .(X,Y,Z)]
        apex    <- unique(pts[pts$Z == max(pts$Z)])[1]
        apex$id <- as.numeric(rgl::spheres3d(apex$X, apex$Y, apex$Z, radius = radius, color = color))
        apice   <- rbind(apice, apex)
      }
    }

    rgl::close3d()

    apice[, id := NULL]
    apice[, treeID := 1:.N]
    apice[, X := X + minx]
    apice[, Y := Y + miny]
    apice[, z := Z]
    output <- sf::st_as_sf(apice, coords = c("X", "Y", "z"), crs = crs)
    output = output[, c(2,1,3)]
    return(output)
  }

  f <- plugin_itd(f, omp = FALSE, raster_based = FALSE)
  return(f)
} # nocov end

Try the lidR package in your browser

Any scripts or data that you put into this service are public.

lidR documentation built on Sept. 8, 2023, 5:10 p.m.