Stem Detection

Author

Seamus Murphy

Published

February 24, 2022

2.0 Overview

Individual tree detection (ITD) spatially locates trees and extracts height information from LiDAR point clouds or canopy height models (CHM). Individual tree segmentation (ITS) delineates crown boundaries for detected trees. This chapter demonstrates both approaches using the local maximum filter (lmf) method and compares point cloud-based versus raster-based workflows on the 1 ha Carr Fire clip normalised in Chapter 1.

It is important to include the uniqueness parameter in locate_trees() operations when processing tiled data. This is essential to prevent duplicate tree detection at tile boundaries. Trees detected in overlapping buffer zones can be counted multiple times without this parameter. Always use uniqueness="bitmerge" for tiled workflows.

Note: Tree detection across large catalogs is the most time-intensive stage. Dense canopies and high point densities substantially increase processing time. Expect multi-hour runtimes for operational-scale projects.


Environment Setup

pacman::p_load(
  "curl",
  "dplyr",
  "fontawesome", "ForestTools",
  "htmltools", "httr2",
  "janitor",
  "kableExtra", "knitr",
  "lidR", "lutz",
  "openxlsx",
  "PROJ",
  "raster", "rasterVis", "reproj", "RCSF", "RColorBrewer",
  "sf",
  "tinytex", "tmap", "tmaptools", "terra",
  "useful", "usethis"
  )

2.1 Point Cloud Method

The local maxima filter (lmf()) algorithm identifies tree tops directly from normalized point clouds1 without rasterization. For each point, the algorithm analyzes neighborhood points within a specified radius to determine if the point represents a local maximum.

Fixed Windows

Constant window size detection is computationally efficient but may over-detect in dense canopies or miss suppressed trees in heterogeneous stands.

  • Small windows (ws=3): High detection rate, increased false positives
  • Large windows (ws=11): Detects only dominant trees, omits understory
# Load the normalised Carr 1 ha AOI written in Chapter 1
las_ha <- lidR::readALSLAS("../data/las_1ha.laz")

# Detect tree tops with a fixed 5 m window
ttops <- locate_trees(las_ha, lmf(ws = 5))

# Visualise on a half-metre CHM
chm <- rasterize_canopy(las_ha, res = 0.5, pitfree(subcircle = 0.2))
plot(chm, col = height.colors(50))
plot(st_geometry(ttops), add = TRUE, pch = 3)

# Save outputs
terra::writeRaster(chm, "../data/chm_1m.tif", overwrite = TRUE)
sf::st_write(ttops, "../data/ttops_1ha.gpkg", delete_dsn = TRUE)

Variable Windows

Dynamic window functions adapt search radius to point height, accounting for allometric relationships between tree height and crown diameter. This approach improves detection accuracy in mixed-structure stands.

  • Minimum window ≥ 0.5 m to avoid spurious detections
  • Threshold heights <2 m handled by the lower bound in pmax
  • Functions must return positive values for all input heights
# Window function tuned for the Carr Fire stand (mixed conifer regrowth,
# scattered residual overstory)
wf <- function(x) {
  y <- 0.07 * x + 0.6
  pmax(y, 0.5)
}

# Visualize window behaviour
heights <- seq(0, 40, 0.5)
window  <- wf(heights)
plot(heights, window, type = "l",
     xlab = "point elevation (m)",
     ylab = "window diameter (m)")

# Detect tree stems
ttops    <- locate_trees(las_ha, lmf(wf), uniqueness = "bitmerge")
ttops_sf <- sf::st_as_sf(ttops)

mypalette <- RColorBrewer::brewer.pal(8, "Greens")
plot(chm, col = mypalette, alpha = 0.6)
plot(st_geometry(ttops_sf["treeID"]), add = TRUE, cex = 0.5,
     pch = 20, col = "red")

Variable window functionVariable-window tree tops

2.2 Rasterized Method

CHM-based detection is faster than point cloud methods due to reduced data volume, but results depend on CHM resolution, interpolation algorithm, and post-processing steps.

# Median filter raster smoothing
kernel     <- matrix(1, 3, 3)
chm_smooth <- terra::focal(chm, w = kernel, fun = median, na.rm = TRUE)

# Detect trees on smoothed CHM
ttops_chm <- locate_trees(chm_smooth, lmf(5))

plot(chm_smooth, col = height.colors(50))
plot(st_geometry(ttops_chm), add = TRUE, pch = 8)


2.3 Crown Segmentation

Crown delineation assigns tree IDs to individual points or pixels using detected tree tops as seeds. Boundaries are grown with Dalponte’s algorithm (Dalponte & Coomes, 2016): each tree top seeds a region that expands outward until local minima in the CHM close the crown. The result is convex polygons representing individual crown extents.

Before handing the tree tops to dalponte2016(), renumber them as a plain integer sequence. Uniqueness strategies like "bitmerge" produce 64-bit IDs that the segmentation algorithm cannot accept in a single-tile context.

ttops$treeID <- seq_len(nrow(ttops))

algo    <- dalponte2016(chm, ttops)
las_seg <- segment_trees(las_ha, algo)

crowns <- crown_metrics(las_seg, func = .stdtreemetrics,
                        geom = "convex")

plot(las_seg, bg = "white", size = 2.5, color = "treeID")
plot(sf::st_geometry(crowns))

Segmented tree IDsCrown delineation

For a denser, fully-canopied reference, the lidR Megaplot.laz sample yields the familiar “lasso” of coloured crowns below — useful for sanity-checking that your segmentation recovers recognisable tree outlines when the input is tree-rich.

Megaplot 3DMegaplot lasso

2.4 Validation

Detection accuracy varies by forest structure and parameterisation. The following is a quick sanity check against a nominal field count for the Carr 1 ha AOI.

detected_count <- nrow(ttops)
field_count    <- 180   # nominal comparison value

detection_rate <- detected_count / field_count
omission_rate  <- 1 - detection_rate

cat("Detection rate:", round(detection_rate * 100, 1), "%\n")
cat("Omission rate:",  round(omission_rate  * 100, 1), "%\n")

Runtime Log

devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────
##  setting  value
##  version  R version 4.4.1 (2024-06-14)
##  os       macOS 15.7.5
##  system   aarch64, darwin20
##  ui       X11
##  language (EN)
##  collate  en_CA.UTF-8
##  ctype    en_CA.UTF-8
##  tz       America/Vancouver
##  date     2026-04-23
##  pandoc   3.9.0.2 @ /opt/local/bin/ (via rmarkdown)
##  quarto   1.7.33 @ /usr/local/bin/quarto
## 
## ─ Packages ───────────────────────────────────────────────────────
##  ! package      * version   date (UTC) lib source
##    abind          1.4-8     2024-09-12 [1] CRAN (R 4.4.1)
##    base64enc      0.1-6     2026-02-02 [1] CRAN (R 4.4.3)
##    cachem         1.1.0     2024-05-16 [1] CRAN (R 4.4.1)
##  P class          7.3-22    2023-05-03 [?] CRAN (R 4.4.1)
##    classInt       0.4-11    2025-01-08 [1] CRAN (R 4.4.1)
##    cli            3.6.5     2025-04-23 [1] CRAN (R 4.4.1)
##  P codetools      0.2-20    2024-03-31 [?] CRAN (R 4.4.1)
##    colorspace     2.1-2     2025-09-22 [1] CRAN (R 4.4.1)
##    cols4all       0.10      2025-10-27 [1] CRAN (R 4.4.1)
##    crosstalk      1.2.2     2025-08-26 [1] CRAN (R 4.4.1)
##    curl         * 7.0.0     2025-08-19 [1] CRAN (R 4.4.1)
##    data.table     1.18.2.1  2026-01-27 [1] CRAN (R 4.4.3)
##    DBI            1.3.0     2026-02-25 [1] CRAN (R 4.4.3)
##    deldir         2.0-4     2024-02-28 [1] CRAN (R 4.4.1)
##    devtools       2.5.0     2026-03-14 [1] CRAN (R 4.4.3)
##    digest         0.6.39    2025-11-19 [1] CRAN (R 4.4.3)
##    dplyr        * 1.2.0     2026-02-03 [1] CRAN (R 4.4.3)
##    e1071          1.7-17    2025-12-18 [1] CRAN (R 4.4.3)
##    ellipsis       0.3.2     2021-04-29 [1] CRAN (R 4.4.1)
##    evaluate       1.0.5     2025-08-27 [1] CRAN (R 4.4.1)
##    farver         2.1.2     2024-05-13 [1] CRAN (R 4.4.1)
##    fastmap        1.2.0     2024-05-15 [1] CRAN (R 4.4.1)
##    fontawesome  * 0.5.3     2024-11-16 [1] CRAN (R 4.4.1)
##    ForestTools  * 1.0.3     2025-02-04 [1] CRAN (R 4.4.1)
##    fs             1.6.7     2026-03-06 [1] CRAN (R 4.4.3)
##    generics       0.1.4     2025-05-09 [1] CRAN (R 4.4.1)
##    ggplot2      * 4.0.2     2026-02-03 [1] CRAN (R 4.4.3)
##    glue           1.8.0     2024-09-30 [1] CRAN (R 4.4.1)
##    gtable         0.3.6     2024-10-25 [1] CRAN (R 4.4.1)
##    hexbin         1.28.5    2024-11-13 [1] CRAN (R 4.4.1)
##    htmltools    * 0.5.9     2025-12-04 [1] CRAN (R 4.4.3)
##    htmlwidgets    1.6.4     2023-12-06 [1] CRAN (R 4.4.0)
##    httr2        * 1.2.2     2025-12-08 [1] CRAN (R 4.4.3)
##    interp         1.1-6     2024-01-26 [1] CRAN (R 4.4.1)
##    janitor      * 2.2.1     2024-12-22 [1] CRAN (R 4.4.1)
##    jpeg           0.1-11    2025-03-21 [1] CRAN (R 4.4.1)
##    jsonlite       2.0.0     2025-03-27 [1] CRAN (R 4.4.1)
##    kableExtra   * 1.4.0     2024-01-24 [1] CRAN (R 4.4.0)
##  P KernSmooth     2.23-24   2024-05-17 [?] CRAN (R 4.4.1)
##    knitr        * 1.51      2025-12-20 [1] CRAN (R 4.4.3)
##  P lattice      * 0.22-6    2024-03-20 [?] CRAN (R 4.4.1)
##    latticeExtra   0.6-31    2025-09-10 [1] CRAN (R 4.4.1)
##    leafem         0.2.5     2025-08-28 [1] CRAN (R 4.4.1)
##    leaflegend     1.2.1     2024-05-09 [1] CRAN (R 4.4.0)
##    leaflet        2.2.3     2025-09-04 [1] CRAN (R 4.4.1)
##    leafsync       0.1.0     2019-03-05 [1] CRAN (R 4.4.0)
##    lidR         * 4.2.3     2026-01-08 [1] CRAN (R 4.4.3)
##    lifecycle      1.0.5     2026-01-08 [1] CRAN (R 4.4.3)
##    logger         0.4.1     2025-09-11 [1] CRAN (R 4.4.1)
##    lubridate      1.9.5     2026-02-04 [1] CRAN (R 4.4.3)
##    lutz         * 0.3.2     2023-10-17 [1] CRAN (R 4.4.0)
##    lwgeom         0.2-15    2026-01-12 [1] CRAN (R 4.4.3)
##    magrittr       2.0.4     2025-09-12 [1] CRAN (R 4.4.1)
##    maptiles       0.11.0    2025-12-12 [1] CRAN (R 4.4.3)
##    memoise        2.0.1     2021-11-26 [1] CRAN (R 4.4.0)
##    openxlsx     * 4.2.8.1   2025-10-31 [1] CRAN (R 4.4.1)
##    otel           0.2.0     2025-08-29 [1] CRAN (R 4.4.1)
##  P pacman         0.5.1     2019-03-11 [?] CRAN (R 4.4.0)
##    pillar         1.11.1    2025-09-17 [1] CRAN (R 4.4.1)
##    pkgbuild       1.4.8     2025-05-26 [1] CRAN (R 4.4.1)
##    pkgconfig      2.0.3     2019-09-22 [1] CRAN (R 4.4.1)
##    pkgload        1.5.0     2026-02-03 [1] CRAN (R 4.4.3)
##    plyr           1.8.9     2023-10-02 [1] CRAN (R 4.4.1)
##    png            0.1-9     2026-03-15 [1] CRAN (R 4.4.3)
##    PROJ         * 0.6.0     2025-04-03 [1] CRAN (R 4.4.1)
##    proj4          1.0-15    2025-03-21 [1] CRAN (R 4.4.1)
##    proxy          0.4-29    2025-12-29 [1] CRAN (R 4.4.3)
##    purrr          1.2.1     2026-01-09 [1] CRAN (R 4.4.3)
##    R6             2.6.1     2025-02-15 [1] CRAN (R 4.4.1)
##    rappdirs       0.3.4     2026-01-17 [1] CRAN (R 4.4.3)
##    raster       * 3.6-32    2025-03-28 [1] CRAN (R 4.4.1)
##    rasterVis    * 0.51.7    2025-09-01 [1] CRAN (R 4.4.1)
##    RColorBrewer * 1.1-3     2022-04-03 [1] CRAN (R 4.4.1)
##    Rcpp           1.1.1     2026-01-10 [1] CRAN (R 4.4.3)
##    RCSF         * 1.0.2     2020-02-04 [1] CRAN (R 4.4.0)
##    renv           1.1.5     2025-07-24 [1] CRAN (R 4.4.1)
##    reproj       * 0.7.0     2024-06-11 [1] CRAN (R 4.4.0)
##    rlang          1.1.7     2026-01-09 [1] CRAN (R 4.4.3)
##    rmarkdown      2.30      2025-09-28 [1] CRAN (R 4.4.1)
##    rstudioapi     0.18.0    2026-01-16 [1] CRAN (R 4.4.3)
##    s2             1.1.9     2025-05-23 [1] CRAN (R 4.4.1)
##    S7             0.2.1     2025-11-14 [1] CRAN (R 4.4.3)
##    scales         1.4.0     2025-04-24 [1] CRAN (R 4.4.1)
##    sessioninfo    1.2.3     2025-02-05 [1] CRAN (R 4.4.1)
##    sf           * 1.1-0     2026-02-24 [1] CRAN (R 4.4.3)
##    snakecase      0.11.1    2023-08-27 [1] CRAN (R 4.4.0)
##    sp           * 2.2-1     2026-02-13 [1] CRAN (R 4.4.3)
##    spacesXYZ      1.6-0     2025-06-06 [1] CRAN (R 4.4.1)
##    stars          0.7-1     2026-02-13 [1] CRAN (R 4.4.3)
##    stringi        1.8.7     2025-03-27 [1] CRAN (R 4.4.1)
##    stringr        1.6.0     2025-11-04 [1] CRAN (R 4.4.1)
##    svglite        2.2.2     2025-10-21 [1] CRAN (R 4.4.1)
##    systemfonts    1.3.2     2026-03-05 [1] CRAN (R 4.4.3)
##    terra        * 1.9-1     2026-03-08 [1] CRAN (R 4.4.3)
##    textshaping    1.0.5     2026-03-06 [1] CRAN (R 4.4.3)
##    tibble         3.3.1     2026-01-11 [1] CRAN (R 4.4.3)
##    tidyselect     1.2.1     2024-03-11 [1] CRAN (R 4.4.0)
##    timechange     0.4.0     2026-01-29 [1] CRAN (R 4.4.3)
##    tinytex      * 0.58      2025-11-19 [1] CRAN (R 4.4.3)
##    tmap         * 4.2       2025-09-10 [1] CRAN (R 4.4.1)
##    tmaptools    * 3.3       2025-07-24 [1] CRAN (R 4.4.1)
##    units          1.0-1     2026-03-11 [1] CRAN (R 4.4.3)
##    useful       * 1.2.7     2026-02-26 [1] CRAN (R 4.4.3)
##    usethis      * 3.2.1     2025-09-06 [1] CRAN (R 4.4.1)
##    vctrs          0.7.2     2026-03-21 [1] CRAN (R 4.4.3)
##    viridisLite    0.4.3     2026-02-04 [1] CRAN (R 4.4.3)
##    withr          3.0.2     2024-10-28 [1] CRAN (R 4.4.1)
##    wk             0.9.5     2025-12-18 [1] CRAN (R 4.4.3)
##    xfun           0.57      2026-03-20 [1] CRAN (R 4.4.3)
##    XML            3.99-0.23 2026-03-20 [1] CRAN (R 4.4.3)
##    xml2           1.5.2     2026-01-17 [1] CRAN (R 4.4.3)
##    yaml           2.3.12    2025-12-10 [1] CRAN (R 4.4.3)
##    zip            2.3.3     2025-05-13 [1] CRAN (R 4.4.1)
##    zoo            1.8-15    2025-12-15 [1] CRAN (R 4.4.3)
## 
##  [1] /Users/seamus/repos/lidar-forestry/renv/library/macos/R-4.4/aarch64-apple-darwin20
##  [2] /Users/seamus/Library/Caches/org.R-project.R/R/renv/sandbox/macos/R-4.4/aarch64-apple-darwin20/93405863
## 
##  * ── Packages attached to the search path.
##  P ── Loaded and on-disk path mismatch.
## 
## ──────────────────────────────────────────────────────────────────

  1. Data used in this chapter are a public-domain USGS 3DEP release:

    ↩︎