Individual Tree Detection & Segmentation
Relevant resources
Overview
This code demonstrates individual tree segmentation (ITS) using LiDAR data. It covers CHM-based and point cloud-based methods for tree detection and segmentation. The code also shows how to extract metrics at the tree level and visualize them.
Environment
# Clear environment
rm(list = ls(globalenv()))
# Load packages
library(lidR)
library(sf)
library(terra)
# Read in LiDAR file and set some color palettes
<- readLAS("data/MixedEucaNat_normalized.laz", filter = "-set_withheld_flag 0")
las <- height.colors(50)
col <- pastel.colors(900) col1
CHM based methods
We start by creating a Canopy Height Model (CHM) from the LiDAR data. The rasterize_canopy()
function generates the CHM using a specified resolution (res
) and a chosen algorithm, here p2r(0.15)
, to compute the percentiles.
# Generate CHM
<- rasterize_canopy(las = las, res = 0.5, algorithm = p2r(0.15))
chm plot(chm, col = col)
After building the CHM, we visualize it using a color palette (col
).
Optionally smooth the CHM
Optionally, we can smooth the CHM using a kernel to remove small-scale variations and enhance larger features like tree canopies.
# Generate kernel and smooth chm
<- matrix(1, 3, 3)
kernel <- terra::focal(x = chm, w = kernel, fun = median, na.rm = TRUE)
schm plot(schm, col = col)
Here, we smooth the CHM using a median filter with a 3x3 kernel. The smoothed CHM (schm
) is visualized using a color palette to represent height values.
Tree detection
Next, we detect tree tops using the smoothed CHM. The locate_trees()
function identifies tree tops based on local maxima.
# Detect trees
<- locate_trees(las = schm, algorithm = lmf(ws = 2.5))
ttops
ttops#> Simple feature collection with 935 features and 2 fields
#> Attribute-geometry relationships: constant (2)
#> Geometry type: POINT
#> Dimension: XYZ
#> Bounding box: xmin: 203830.2 ymin: 7358900 xmax: 203979.8 ymax: 7359050
#> Projected CRS: SIRGAS 2000 / UTM zone 23S
#> First 10 features:
#> treeID Z geometry
#> 1 1 5.230 POINT Z (203878.8 7359050 5...
#> 2 2 3.900 POINT Z (203889.8 7359050 3.9)
#> 3 3 7.360 POINT Z (203898.8 7359050 7...
#> 4 4 17.790 POINT Z (203905.8 7359050 1...
#> 5 5 11.940 POINT Z (203935.8 7359050 1...
#> 6 6 3.275 POINT Z (203939.2 7359050 3...
#> 7 7 21.550 POINT Z (203951.8 7359050 2...
#> 8 8 17.290 POINT Z (203963.8 7359050 1...
#> 9 9 21.085 POINT Z (203977.2 7359050 2...
#> 10 10 19.220 POINT Z (203914.8 7359049 1...
plot(chm, col = col)
plot(ttops, col = "black", add = TRUE, cex = 0.5)
The detected tree tops (ttops
) are plotted on top of the CHM (chm
) to visualize their positions.
Segmentation
Now, we perform tree segmentation using the detected tree tops. The segment_trees()
function segments the trees in the LiDAR point cloud based on the previously detected tree tops.
# Segment trees using dalponte
<- segment_trees(las = las, algorithm = dalponte2016(chm = schm, treetops = ttops)) las
# Count number of trees detected and segmented
length(unique(las$treeID) |> na.omit())
#> [1] 935
# Visualize all trees
plot(las, color = "treeID")
# Select trees by ID
<- filter_poi(las = las, treeID == 25)
tree25 <- filter_poi(las = las, treeID == 125) tree125
plot(tree25, size = 4)
plot(tree125, size = 3)
After segmentation, we count the number of trees detected and visualize all the trees in the point cloud. We then select two trees (tree25
and tree125
) and visualize them individually.
Working with rasters
The lidR
package is designed for point clouds, but some functions can be applied to raster data as well. Here, we show how to extract trees from the CHM without using the point cloud directly.
# Generate rasterized delineation
<- dalponte2016(chm = chm, treetops = ttops)() # Notice the parenthesis at the end
trees
trees#> class : SpatRaster
#> dimensions : 300, 300, 1 (nrow, ncol, nlyr)
#> resolution : 0.5, 0.5 (x, y)
#> extent : 203830, 203980, 7358900, 7359050 (xmin, xmax, ymin, ymax)
#> coord. ref. : SIRGAS 2000 / UTM zone 23S (EPSG:31983)
#> source(s) : memory
#> name : Z
#> min value : 1
#> max value : 935
plot(trees, col = col1)
plot(ttops, add = TRUE, cex = 0.5)
#> Warning in plot.sf(ttops, add = TRUE, cex = 0.5): ignoring all but the first
#> attribute
We create tree objects (trees
) using the dalponte2016
algorithm with the CHM and tree tops. The resulting objects are visualized alongside the detected tree tops.
Point-cloud based methods (no CHM)
In this section, we will explore tree detection and segmentation methods that do not require a CHM.
Tree detection
We begin with tree detection using the local maxima filtering (lmf) algorithm. This approach directly works with the LiDAR point cloud to detect tree tops.
# Detect trees
<- locate_trees(las = las, algorithm = lmf(ws = 3, hmin = 5)) ttops
# Visualize
<- plot(las)
x add_treetops3d(x = x, ttops = ttops, radius = 0.5)
We detect tree tops using the lmf
algorithm and visualize them in 3D by adding the tree tops to the LiDAR plot.
Tree segmentation
Next, we perform tree segmentation using the li2012
algorithm, which directly processes the LiDAR point cloud.
# Segment using li
<- segment_trees(las = las, algorithm = li2012()) las
plot(las, color = "treeID")
# This algorithm does not seem pertinent for this dataset.
The li2012
algorithm segments the trees in the LiDAR point cloud based on local neighborhood information. However, it may not be optimal for this specific dataset.
Extraction of metrics
We will extract various metrics from the tree segmentation results.
Using crown_metrics()
The crown_metrics()
function extracts metrics from the segmented trees using a user-defined function. We use the length of the Z coordinate to obtain the tree height as an example.
# Generate metrics for each delineated crown
<- crown_metrics(las = las, func = ~list(n = length(Z)))
metrics
metrics#> Simple feature collection with 752 features and 2 fields
#> Geometry type: POINT
#> Dimension: XYZ
#> Bounding box: xmin: 203830 ymin: 7358900 xmax: 203980 ymax: 7359050
#> z_range: zmin: 2.02 zmax: 34.46
#> Projected CRS: SIRGAS 2000 / UTM zone 23S
#> First 10 features:
#> treeID n geometry
#> 1 1 2774 POINT Z (203969.4 7359045 3...
#> 2 2 1918 POINT Z (203969.5 7358922 3...
#> 3 3 859 POINT Z (203967.8 7358926 3...
#> 4 4 1605 POINT Z (203943.1 7358936 3...
#> 5 5 454 POINT Z (203954.5 7358949 3...
#> 6 6 417 POINT Z (203957.8 7358949 3...
#> 7 7 946 POINT Z (203949.7 7358943 3...
#> 8 8 1671 POINT Z (203970 7358900 32.09)
#> 9 9 1106 POINT Z (203975.2 7358915 3...
#> 10 10 411 POINT Z (203947.7 7358949 3...
plot(metrics["n"], cex = 0.8)
We calculate the number of points (n
) in each tree crown using a user-defined function, and then visualize the results.
Applying user-defined functions
We can map any user-defined function at the tree level using the crown_metrics()
function, just like pixel_metrics()
. Here, we calculate the convex hull area of each tree using a custom function f()
and then visualize the results.
# User defined function for area calculation
<- function(x, y) {
f # Get xy for tree
<- cbind(x, y)
coords
# Convex hull
<- chull(coords)
ch
# Close coordinates
<- c(ch, ch[1])
ch <- coords[ch, ]
ch_coords
# Generate polygon
<- sf::st_polygon(list(ch_coords))
p
#calculate area
<- sf::st_area(p)
area
return(list(A = area))
}
# Apply user-defined function
<- crown_metrics(las = las, func = ~f(X, Y))
metrics
metrics#> Simple feature collection with 752 features and 2 fields
#> Geometry type: POINT
#> Dimension: XYZ
#> Bounding box: xmin: 203830 ymin: 7358900 xmax: 203980 ymax: 7359050
#> z_range: zmin: 2.02 zmax: 34.46
#> Projected CRS: SIRGAS 2000 / UTM zone 23S
#> First 10 features:
#> treeID A geometry
#> 1 1 172.51720 POINT Z (203969.4 7359045 3...
#> 2 2 103.84100 POINT Z (203969.5 7358922 3...
#> 3 3 72.18270 POINT Z (203967.8 7358926 3...
#> 4 4 79.85055 POINT Z (203943.1 7358936 3...
#> 5 5 17.34630 POINT Z (203954.5 7358949 3...
#> 6 6 17.45860 POINT Z (203957.8 7358949 3...
#> 7 7 41.13120 POINT Z (203949.7 7358943 3...
#> 8 8 71.97225 POINT Z (203970 7358900 32.09)
#> 9 9 50.51530 POINT Z (203975.2 7358915 3...
#> 10 10 24.61015 POINT Z (203947.7 7358949 3...
plot(metrics["A"], cex = 0.8)
Using pre-defined metrics
Some metrics are already recorded, and we can directly calculate them at the tree level using crown_metrics()
.
<- crown_metrics(las = las, func = .stdtreemetrics)
metrics
metrics#> Simple feature collection with 752 features and 4 fields
#> Geometry type: POINT
#> Dimension: XYZ
#> Bounding box: xmin: 203830 ymin: 7358900 xmax: 203980 ymax: 7359050
#> z_range: zmin: 2.02 zmax: 34.46
#> Projected CRS: SIRGAS 2000 / UTM zone 23S
#> First 10 features:
#> treeID Z npoints convhull_area geometry
#> 1 1 34.46 2774 172.517 POINT Z (203969.4 7359045 3...
#> 2 2 32.52 1918 103.841 POINT Z (203969.5 7358922 3...
#> 3 3 32.46 859 72.183 POINT Z (203967.8 7358926 3...
#> 4 4 32.35 1605 79.851 POINT Z (203943.1 7358936 3...
#> 5 5 32.33 454 17.346 POINT Z (203954.5 7358949 3...
#> 6 6 32.22 417 17.459 POINT Z (203957.8 7358949 3...
#> 7 7 32.14 946 41.131 POINT Z (203949.7 7358943 3...
#> 8 8 32.09 1671 71.972 POINT Z (203970 7358900 32.09)
#> 9 9 32.08 1106 50.515 POINT Z (203975.2 7358915 3...
#> 10 10 32.01 411 24.610 POINT Z (203947.7 7358949 3...
# Visualize individual metrics
plot(x = metrics["convhull_area"], cex = 0.8)
plot(x = metrics["Z"], cex = 0.8)
We calculate tree-level metrics using .stdtreemetrics
and visualize individual metrics like convex hull area and height.
Delineating crowns
The crown_metrics()
function segments trees and extracts metrics at the crown level.
<- crown_metrics(las = las, func = .stdtreemetrics, geom = 'convex')
cvx_hulls
cvx_hulls#> Simple feature collection with 752 features and 4 fields
#> Geometry type: POLYGON
#> Dimension: XY
#> Bounding box: xmin: 203830 ymin: 7358900 xmax: 203980 ymax: 7359050
#> Projected CRS: SIRGAS 2000 / UTM zone 23S
#> First 10 features:
#> treeID Z npoints convhull_area geometry
#> 1 1 34.46 2774 172.517 POLYGON ((203977.3 7359044,...
#> 2 2 32.52 1918 103.841 POLYGON ((203963.7 7358914,...
#> 3 3 32.46 859 72.183 POLYGON ((203969.1 7358926,...
#> 4 4 32.35 1605 79.851 POLYGON ((203948 7358933, 2...
#> 5 5 32.33 454 17.346 POLYGON ((203955.8 7358949,...
#> 6 6 32.22 417 17.459 POLYGON ((203959.3 7358948,...
#> 7 7 32.14 946 41.131 POLYGON ((203950.8 7358941,...
#> 8 8 32.09 1671 71.972 POLYGON ((203975.7 7358902,...
#> 9 9 32.08 1106 50.515 POLYGON ((203977 7358915, 2...
#> 10 10 32.01 411 24.610 POLYGON ((203950.3 7358949,...
plot(cvx_hulls)
plot(ttops, add = TRUE, cex = 0.5)
#> Warning in plot.sf(ttops, add = TRUE, cex = 0.5): ignoring all but the first
#> attribute
# Visualize individual metrics based on values
plot(x = cvx_hulls["convhull_area"])
plot(x = cvx_hulls["Z"])
We use crown_metrics()
with .stdtreemetrics
to segment trees and extract metrics based on crown delineation.
Exercises
Using:
<- readLAS(files = "data/MixedEucaNat_normalized.laz", filter = "-set_withheld_flag 0") las
E1.
Find and count the trees.
E2.
Compute and map the density of trees with a 10 m resolution.
E3.
Segment the trees.
E4.
Assuming that the biomass of a tree can be estimated using the crown area and the mean Z of the points with the formula 2.5 * area + 3 * mean Z
, estimate the biomass of each tree.
E5.
Map the total biomass at a resolution of 10 m. The output is a mix of ABA and ITS. Hint: use the terra
package to rasterize spatial object with the function rasterize()
.
Conclusion
This concludes the tutorial on various methods for tree detection, segmentation, and extraction of metrics using the lidR
package in R.