Canopy analysis in R using Forest Tools

Andrew Plowright

2017-01-18

Introduction

The Forest Tools R package offers functions to analyze remotely sensed forest data. Currently, tools to detect dominant treetops and outline tree crowns have been implemented, both of which are applied to a rasterized canopy height model (CHM), which is generally derived from LiDAR or photogrammetric point clouds. A function to summarize the height and count of treetops within user-defined geographical areas is also available.

The following vignette provides examples for using these functions.

Installation

Check that R is up-to-date. This can be done automatically using the installr package.

install.packages("installr")
library(installr)
updateR()

Download and install the Forest Tools package from CRAN (the Comprehensive R Archive Network) using the install.packages function.

install.packages("ForestTools")

Loading sample data

A sample CHM is included in the Forest Tools package. It represents a small 1.5 hectare swath of forest in the Kootenay Mountains, British Columbia. The following examples use this sample, but if you would rather use your own data, it can be loaded into R using the raster function. A brief section on reading and writing geospatial data in R is included in this document. Otherwise, begin by loading the necessary libraries and the sample CHM using the library and data functions respectively.

# Attach the Forest Tools and raster libraries
library(ForestTools)
library(raster)

# Load sample data
data("kootenayCHM")

View the CHM using the plot function. The cell values are equal to the canopy’s height above ground.

# Remove plot margins (optional)
par(mar = rep(0.5, 4))

# Plot CHM (extra optional arguments remove labels and tick marks from the plot)
plot(kootenayCHM, xlab = "", ylab = "", xaxt='n', yaxt = 'n')

Detecting treetops

Dominant treetops can be detected using TreeTopFinder. This function implements the variable window filter algorithm developped by Popescu and Wynne (2004). In short, a moving window scans the CHM, and if a given cell is found to be the highest within the window, it is tagged as a treetop. The size of the window itself changes depending on the height of the cell on which it is centered. This is to compensate for varying crown sizes, with tall trees having wide crowns and vice versa.

Therefore, the first step is to define the function that will define the dynamic window size. Essentially, this function should take a CHM cell value (i.e.: the height of the canopy above ground at that location) and return the radius of the search window. Here, we will define a simple linear equation, but any function with a single input and output will work.

lin <- function(x){x * 0.05 + 0.6}

We do not wish for the TreeTopFinder to tag low-lying underbrush or other spurious treetops, and so we also set a minimum height of 2 m using the minHeight argument. Any cell with a lower value will not be tagged as a treetop.

ttops <- TreeTopFinder(CHM = kootenayCHM, winFun = lin, minHeight = 2)

We can now plot these treetops on top of the CHM.

# Plot CHM
plot(kootenayCHM, xlab = "", ylab = "", xaxt='n', yaxt = 'n')

# Add dominant treetops to the plot
plot(ttops, col = "blue", pch = 20, cex = 0.5, add = TRUE)

The ttops object created by TreeTopFinder in this example contains the spatial coordinates of each detected treetop, as well as two default attributes: height and radius. These correspond to the tree’s height above ground and the radius of the moving window where the tree was located. Note that this radius is not necessarily equivalent to crown radius.

# Get the mean treetop height
mean(ttops$height)
## [1] 5.070345

Spatial statistics

Managed forests are often divided into discrete spatial units. In British Columbia, for instance, these can range from cut blocks measuring a few hectares to timber supply areas spanning several hundred square kilometers. The forest composition within these spatial units can be characterized through summarized statistics of tree attributes. For instance, a timber license holder may want a rough estimate of the number of dominant trees within a woodlot, while the standard deviation of tree height is of interest to anyone mapping heterogeneous old growth forest.

The TreeTopSummary function can be used to count trees within a set of spatial units, as well as compute statistics of the trees’ attributes. These spatial units can be in the form of spatial polygons, or can be generated in the form of a raster grid.

When no specific area is defined, TreeTopSummary will simply return the count of all inputted trees.

TreeTopSummary(ttops)
##           Value
## TreeCount  1211

By defining variables, TreeTopSummary can also generate summarized statistics.

TreeTopSummary(ttops, variables = "height")
##                    Value
## TreeCount    1211.000000
## heightMean      5.070345
## heightMedian    3.910026
## heightSD        2.957289
## heightMin       2.002042
## heightMax      13.491207

Stastics by polygon

The Forest Tools package includes the boundaries of three cutting blocks that can be overlayed on kootenayCHM. Tree counts and height statistics can be summarized within these boundaries using the areas argument.

data("kootenayBlocks")

# Compute tree count and height statistics for cut blocks
blockStats <- TreeTopSummary(ttops, areas = kootenayBlocks, variables = "height")

# Plot CHM
plot(kootenayCHM, xlab = "", ylab = "", xaxt='n', yaxt = 'n')

# Add block outlines to the plot
plot(kootenayBlocks, add = TRUE, border =  "darkmagenta", lwd = 2)

# Add tree counts to the plot
library(rgeos)
text(gCentroid(kootenayBlocks, byid = TRUE), blockStats[["TreeCount"]], col = "darkmagenta", font = 2)

# View height statistics
blockStats@data
##   BlockID Shape_Leng Shape_Area TreeCount heightMean heightMedian
## 0     101   304.3290   3706.389       313   7.329356     7.523089
## 1    3308   508.6240   6712.607       627   2.988681     2.668048
## 2     113   239.5202   2767.266       265   7.333409     7.997055
##    heightSD heightMin heightMax
## 0 2.7266677  2.003668 13.491207
## 1 0.9605248  2.002042  7.125149
## 2 2.7344077  2.033117 12.583441

Stastics by grid

Instead of defining polygonal areas, the TreeTopStatistics function can also generate counts and stastics in raster format. In this case, the grid argument should be used instead of areas. If you have an existing raster with the extent, cell size and alignment that you would like to use, it can be input as the grid argument. Otherwise, simply entering a numeric value will generate a raster with that cell size.

# Compute tree count within a 10 m x 10 m cell grid
gridCount <- TreeTopSummary(treetops = ttops, grid = 10)

# Plot grid
plot(gridCount, col = heat.colors(255), xlab = "", ylab = "", xaxt='n', yaxt = 'n')

If, in addition to tree count, tree attribute statistics are computed, the object returned by TreeTopSummary will be a RasterBrick, i.e.: a multi-layered raster.

# Compute tree height statistics within a 10 m x 10 m cell grid
gridStats <- TreeTopSummary(treetops = ttops, grid = 10, variables = "height")

# View layer names
names(gridStats)
## [1] "TreeCount"    "heightMean"   "heightMedian" "heightSD"    
## [5] "heightMin"    "heightMax"

Use the [[]] subsetting operator to extract a single layer.

# Plot mean tree height within 10 m x 10 m cell grid
plot(gridStats[["heightMean"]], col = heat.colors(255), xlab = "", ylab = "", xaxt='n', yaxt = 'n')

Outlining tree crowns

Canopy height models often represent continuous, dense forests, where tree crowns abut against eachother. Outlining discrete crown shapes from this type of forest is often refered to as canopy segmentation, where each crown outline is represented by a segment. Once a set of treetops have been detected from a canopy height model, the SegmentCrowns function can be used for this purpose.

The SegmentCrowns function implements the watershed algorithm from the imager library. Watershed algorithms are frequently used in topograhical analysis to outline drainage basins. Given the morphological similarity between an inverted canopy and a terrain model, this same process can be used to outline tree crowns. However, a potential problem is the issue of oversegmentation, whereby branches, bumps and other spurious treetops are given their own segments. This source of error can be mitigated by using a variant of the algorithm known as marker-controlled segmentation (Beucher & Meyer, 1993), whereby the watershed algorithm is constrained by a set of markers–in this case, treetops.

The SegmentCrowns function also takes a minHeight argument, although this value should be lower than that which was assigned to TreeTopFinder. For the latter, minHeight defines the lowest expected treetop, whereas for the former it should correspond to the height above ground of the fringes of the lowest trees.

# Create crown map
crowns <- SegmentCrowns(treetops = ttops, CHM = kootenayCHM, minHeight = 1.5)

# Plot crowns
plot(crowns, col = sample(rainbow(50), length(crowns), replace = TRUE), legend = FALSE, xlab = "", ylab = "", xaxt='n', yaxt = 'n')

SegmentCrowns returns a raster, where each crown is given a unique cell value. Depending on the intended purpose of the crown map, it may be preferable to store these outlines as polygons. The rasterToPolygons function from the raster package can be used for this purpose. Note that this conversion can be very slow for large datasets.

# Convert raster crown map to polygons
crownsPoly <- rasterToPolygons(crowns, dissolve = TRUE)

# Plot CHM
plot(kootenayCHM, xlab = "", ylab = "", xaxt='n', yaxt = 'n')

# Add crown outlines to the plot
plot(crownsPoly, border = "blue", lwd = 0.5, add = TRUE)

Once converted to polygons, the two-dimensional area of each outline can be computed using the gArea function from the rgeos package (also is installed during the installation of Forest Tools).

library(rgeos)

# Compute crown area
crownsPoly[["area"]] <- gArea(crownsPoly, byid = TRUE)

Assuming that each crown has a roughly circular shape, we can use the crown’s area to compute it’s average circular diameter.

# Compute average crown diameter
crownsPoly[["diameter"]] <- sqrt(crownsPoly[["area"]]/ pi) * 2

# Mean crown diameter
mean(crownsPoly$diameter)
## [1] 2.710377

Handling large datasets

Canopy height models are frequently stored as very large raster datasets (> 1 GB). This can present a problem, since loading such a large dataset into memory can cause performance issues.

To handle large datasets, both the TreeTopFinder and SegmentCrowns functions have a built-in functionality whereby an inputted CHM can be automatically divided into tiles, which are then processed individually and then reassembled into a single output. This functionality is controlled with the maxCells argument: any input CHM with a higher number of cells will be divided into tiles for processing.

Since both the TreeTopFinder and SegmentCrowns processes can be subject to edge effects, these tiles are automatically buffered. While the buffers created by TreeTopFinder are set according to maximum window size, the buffers for SegmentCrowns are defined by the user using the tileBuffer argument, which should be equal to half the diameter of the widest expected tree crown.

Though the automatic tiling feature can improve performance and limit the amount of memory used during processing, both functions will still return a single output file, which in itself can be very large and still overload memory. Therefore, while automatic tiling is provided for user conveninence, it may still be a better practice to pre-tile large datasets and store outputs in tiled form.

Reading and writing geospatial data in R

The raster and sp libraries

The Forest Tools package is built on the raster and sp libraries, which are automatically installed when ForestTools is downloaded. These libraries define a variety of classes and functions for working with raster and vector datasets in R.

It is recommended that any user performing geospatial analyses in R be familiar with both of these libraries. Relatively easy and straightforward guides for raster and sp have been written by their respective authors.

Geospatial classes used by Forest Tools

Data product Data type Object class
Canopy height model Single-layer raster RasterLayer
Treetops Points SpatialPointsDataFrame
Crown outlines Polygons SpatialPolygonsDataFrame
Gridded statistics Multi-layer raster RasterBrick

Raster files

To load a raster file, such as a CHM, use the raster function from the raster library (both the function and the library have the same name). Simply provide a path to a valid raster file. Don’t forget to use either double backslashes \\ or forward slashes / in the file path.

library(raster)

# Load a canopy height model
inCHM <- raster("C:\\myFiles\\inputs\\testCHM.tif")

Once you have performed your analysis, use the writeRaster function to save any raster files you may have produced. Setting an appropriate dataType is optional, but can save disk space.

# Write a crown map raster file
writeRaster(crowns, "C:\\myFiles\\outputs\\crowns.tif", dataType = "INT2U")

Polygon and point files

There are many options for saving point and polygon files to disk. The rgdal library provides functions for reading and writing the most common vector formats. The following examples use ESRI Shapefiles.

Use the readOGR function to load a polygonal ESRI Shapefile. Instead of providing an entire file path, readOGR takes two separate arguments: the file’s directory, followed by the file name without an extension. The following would import a file named “C:\myFiles\blockBoundaries\block375.shp”.

library(rgdal)

# Load the 'block375.shp' file
blk375boundary <- readOGR("C:\\myFiles\\blockBoundaries", "block375")

Follow this same convention for saving a vector file to disk using writeOGR. A driver must also be specified.

# Save a set of dominant treetops
writeOGR(ttops, "C:\\myFiles\\outputs", "treetops", driver = "ESRI Shapefile")

References

Popescu, S. C., & Wynne, R. H. (2004). Seeing the trees in the forest. Photogrammetric Engineering & Remote Sensing, 70(5), 589-604.

Beucher, S., and Meyer, F. (1993). The morphological approach to segmentation: the watershed transformation. Mathematical morphology in image processing, 433-481.