Spatialize trees and forest stand metrics with BIOMASS

knitr::opts_chunk$set(
  collapse = TRUE, echo = TRUE,
  comment = "#>", fig.align = "center"
)
require(BIOMASS)
require(knitr)
require(ggplot2)

Overview

BIOMASS enables users to manage their plots by:

Required data

Two data frames are required to perform the analysis. One for the corner of the plot(s), and one for the trees, which contains at least their coordinates.

In this vignette, for educational purpose, we will not use only one but two datasets of corner coordinates, derived from permanent plots in the Nouragues forest (French Guiana):

  1. NouraguesPlot201 which contains simulated corner coordinates of one plot with repeated GPS measurements of each corner:
data("NouraguesPlot201")

kable(head(NouraguesPlot201), digits = 5, row.names = FALSE, caption = "Head of NouraguesPlot201")
  1. NouraguesCoords which contains corner coordinates of four plots with a single GPS measurement of each corner, also derived from Nouragues forest:
data("NouraguesCoords")

kable(head(NouraguesCoords), digits = 5, row.names = FALSE, caption = "Head of NouraguesCoords")
data("NouraguesTrees")

kable(head(NouraguesTrees), digits = 3, row.names = FALSE, caption = "Head of the table trees")

This dataset is also derived from the 2012 Nouragues forest dataset, but for educational purpose, some virtual trees with erroneous coordinates have been added in the data.

Checking plot's coordinates

Two situations may occur:

In both cases, the use of the check_plot_coord() function is recommended as a first step.

Checking the corners of the plot

The check_plot_coord() function handles both situations using the trust_GPS_corners argument (= TRUE or FALSE).

You can give either geographical coordinates with the 'longlat' argument or another projected coordinates with the 'proj_coord' argument for the corner coordinates.

If we rely on the GPS coordinates of the corners: {#trustGSPcorner}

check_plot_trust_GPS <- check_plot_coord(
  corner_data = NouraguesPlot201,
  longlat = c("Long", "Lat"),  # or proj_coord = c("Xutm", "Yutm"), 
  rel_coord = c("Xfield", "Yfield"),
  trust_GPS_corners = T,
  draw_plot = TRUE,
  max_dist = 10, rm_outliers = TRUE)

The two blue arrows represent the origin of the plot's relative coordinate system.

If we rely on the shape of the plot measured on the field: {#trustPlotShape}

Let's degrade the data to mimic the case where we only have 8 unreliable GPS coordinates.

degraded_corner_coord <- NouraguesPlot201[c(1:2,11:12,21:22,31:32),]

check_plot_trust_field <- check_plot_coord(
  corner_data = degraded_corner_coord,
  longlat = c("Long", "Lat"),  # or proj_coord = c("Xutm", "Yutm"), 
  rel_coord = c("Xfield", "Yfield"),
  trust_GPS_corners = FALSE,
  draw_plot = TRUE, rm_outliers = FALSE)

We can see that the corners of the plot do not match the GPS measurements. In fact, they correspond to the best compromise between the shape and dimensions of the plot and the GPS measurements.

Recovering reference corner coordinates and the associated polygon(s)

Reference corner coordinates are returned by the function via the $corner_coord output, with standardised column names for future data processing.

kable(check_plot_trust_GPS$corner_coord, row.names = FALSE, caption = "Reference corner coordinates")

The associated polygon is returned via the $polygon output and can be saved into a shapefile as follows:

sf::st_write(check_plot_trust_GPS$polygon, "your_directory/plot201.shp")

For full details, the $outlier_corners output returns all the information about GPS outliers found by the function, the $UTM_code output returns the UTM code calculated by the function if geographic coordinates have been provided, and the $sd_coord output returns the average standard deviation of the GPS measurements for each corner on the X and Y axes.

Visualising and retrieving projected tree coordinates {#tree_coordinates}

Tree coordinates are usually measured in the plot's relative coordinate system. To project them in the projected/GPS system, you can supply their relative coordinates using the tree_data and tree_coords arguments.

plot201Trees <- NouraguesTrees[NouraguesTrees$Plot==201,]

check_plot_trust_GPS <- check_plot_coord(
  corner_data = NouraguesPlot201,
  longlat = c("Long", "Lat"), rel_coord = c("Xfield", "Yfield"),
  trust_GPS_corners = TRUE,
  tree_data = plot201Trees, tree_coords = c("Xfield","Yfield"))

The projected/GPS coordinates of the trees are added to the tree data-frame and returned by the output $tree_data (columns x_proj/long and y_proj/lat).

kable(head(check_plot_trust_GPS$tree_data[,-c(5,6,7)]), digits = 3, row.names = FALSE, caption = "Head of the $tree_data output")

The output of the function also standardises the names of the relative tree coordinates (to x_rel and y_rel) and adds the is_in_plot column, indicating if a tree is in the plot or not.

If you have a shapefile containing useful information about the plot, you can also display it using the shapefile argument.

Finally, you can access and modify the plot via the $plot_design output which is a ggplot object. For example, to change the plot title:

plot_to_change <- check_plot_trust_GPS$plot_design
plot_to_change <- plot_to_change + ggtitle("A custom title")
plot_to_change

Integrating LiDAR data

If you have LiDAR data in raster format (typically a CHM raster) that you want to compare with a tree metric, this can be done with the ref_raster, prop_tree and threshold_tree arguments.

# Load internal CHM raster
nouraguesRaster <- terra::rast(system.file("extdata", "NouraguesRaster.tif",package = "BIOMASS", mustWork = TRUE))

check_plot_trust_GPS <- check_plot_coord(
  corner_data = NouraguesPlot201,
  longlat = c("Long", "Lat"), rel_coord = c("Xfield", "Yfield"),
  trust_GPS_corners = TRUE,
  tree_data = plot201Trees, tree_coords = c("Xfield","Yfield"),
  prop_tree = "D", threshold_tree = 20, # Display tree diameters >= 20
  ref_raster = nouraguesRaster )

Checking multiple plots at once

When corner_data and tree_data contain several plots, you have to supply the column names containing the plots IDs of the corners and the trees via the plot_ID and tree_plot_ID arguments:

multiple_checks <- check_plot_coord(
  corner_data = NouraguesCoords, # NouraguesCoords contains 4 plots
  proj_coord = c("Xutm", "Yutm"), rel_coord = c("Xfield", "Yfield"),
  trust_GPS_corners = TRUE, 
  plot_ID = "Plot",
  tree_data = NouraguesTrees, tree_coords = c("Xfield","Yfield"), 
  prop_tree = "D", tree_plot_ID = "Plot",
  ref_raster = nouraguesRaster)

Be aware that by default, the function will ask you to type Enter between each plot (argument 'ask = TRUE').

Dividing plots

Dividing plots into several sub-plots is performed using the divide_plot() function. This function takes the relative coordinates of the 4 corners of the plot to divide it into a grid. Be aware that the plot must be rectangular in the plot's relative coordinates system, i.e. have 4 right angles:

subplots <- divide_plot(
  corner_data = check_plot_trust_GPS$corner_coord,
  rel_coord = c("x_rel","y_rel"),
  proj_coord = c("x_proj","y_proj"),
  grid_size = 25 # or c(25,25)
  )

kable(head(subplots$sub_corner_coord, 10), digits = 1, row.names = FALSE, caption = "Head of the divide_plot()$sub_corner_coord output.")

If you want to stay in the plot's relative coordinate system, just set proj_coord = NULL.

The function also handles imperfect cuts with the origin and grid_tol arguments. Here an example with a 40mx45m grid and origin coordinates set at (10 ; 5).

subplots <- divide_plot(
  corner_data = check_plot_trust_GPS$corner_coord, 
  rel_coord = c("x_rel","y_rel"), proj_coord = c("x_proj","y_proj"),
  grid_size = c(40,45), 
  grid_tol = 0.3, # by default =0.1, ie, if more than 10% of the plot is not covered by the grid, it will returned an error
  origin = c(10,5)
  )
non_centred <- divide_plot(
  corner_data = check_plot_trust_GPS$corner_coord, 
  rel_coord = c("x_rel","y_rel"), proj_coord = c("x_proj","y_proj"),
  grid_size = c(40,45), 
  grid_tol = 0.3)

ggplot(data = subplots$sub_corner_coord, mapping = aes(x=x_proj, y=y_proj)) + 
  geom_point(data = check_plot_trust_GPS$corner_coord, 
             mapping = aes(x=x_proj, y=y_proj),
             shape = 15, size = 2) + 
  geom_point(col="red") + 
  coord_equal() + 
  theme_bw() + 
  labs(title = "subplot divisions with origin at (10,5)")

ggplot(data = non_centred$sub_corner_coord, mapping = aes(x=x_proj, y=y_proj)) + 
  geom_point(data = check_plot_trust_GPS$corner_coord, 
             mapping = aes(x=x_proj, y=y_proj),
             shape = 15, size = 2.5) + 
  geom_point(col="red") + 
  coord_equal() + 
  theme_bw() + 
  labs(title = "subplot divisions with origin at (0,0) by default")

For the purpose of summarising and representing subplots (coming in the next section), the function returns the coordinates of subplot corners but can also assign to each tree its subplot with the tree_data and tree_coords arguments:

# Add AGB predictions (calculated in Vignette BIOMASS) to plot201Trees
AGB_data <- readRDS("saved_data/NouraguesTreesAGB.rds")
plot201Trees <- merge(plot201Trees , AGB_data[c("Xfield","Yfield","D","AGB")], sort=FALSE)

subplots <- divide_plot(
  corner_data = check_plot_trust_GPS$corner_coord, 
  rel_coord = c("x_rel","y_rel"),
  proj_coord = c("x_proj","y_proj"),
  grid_size = 25, # or c(25,25)
  tree_data = plot201Trees, tree_coords = c("Xfield","Yfield")
  )

The function now returns a list containing:

kable(head(subplots$tree_data[,-c(2,3,4)]), digits = 1, row.names = FALSE, caption = "Head of the divide_plot()$tree_data returns")

Of course, the function can handle as many plots as you want, using the corner_plot_ID and tree_plot_ID arguments:

multiple_subplots <- divide_plot(
  corner_data = NouraguesCoords,
  rel_coord = c("Xfield","Yfield"), proj_coord = c("Xutm","Yutm"), corner_plot_ID = "Plot",
  grid_size = 25, 
  tree_data = NouraguesTrees, tree_coords = c("Xfield","Yfield"), tree_plot_ID = "Plot"
)

Last but not least, the function can account for uncertainty in the corner coordinates using the sd_coord and n arguments. In this case, n simulations of corner positioning will be drawn and returned via the $simu_coord output list. The uncertainty (sd) on each corner position along the X- and Y- axis will be equal to the value of sd_coord:

sd_coord_subplots <- divide_plot(
  corner_data = check_plot_trust_GPS$corner_coord,
  rel_coord = c("x_rel","y_rel"),
  proj_coord = c("x_proj","y_proj"),
  grid_size = 25, # or c(25,25)
  tree_data = plot201Trees, tree_coords = c("Xfield","Yfield"),
  sd_coord = check_plot_trust_GPS$sd_coord, n = 50
  )

Summarising metrics at subplot level

Summarising tree metrics

Once you've applied the divide_plot() function with a non-null tree_data argument, you can summarise any tree metric at the subplot level with the subplot_summary() function.

subplot_metric <- subplot_summary(
  subplots = subplots,
  value = "AGB", # AGB was added before applying divide_plot()
  per_ha = TRUE) 

By default, the function sums the metric per subplot and divides the result by the area of each subplot (to obtain a summary per hectare), but you can request any valid function using fun argument and choose between a raw or a per hectare summary using per_ha argument.

subplot_metric <- subplot_summary(
  subplots = subplots,
  value = "AGB",
  fun = quantile, probs = 0.5, # yes, it is the median
  per_ha = FALSE)

The output of the function is a list containing:

The returned polygons can be saved into a shapefile like this:

# Set the CRS of the polygons
subplot_polygons <- sf::st_set_crs(
  subplot_metric$polygon ,
  value = "EPSG:2972") # EPSG:2972 (corresponding to UTM Zone 22N) is the UTM coordinate system of Nouragues

# Save the polygons in a shapefile
sf::st_write(subplot_polygons, "your_directory/subplots_201.shp")

And of course, the function can handle as many plots as provided in divide_plot():

multiple_subplot_metric <- subplot_summary(
  subplots = multiple_subplots, draw_plot = FALSE,
  value = "D", fun = mean, per_ha = FALSE)

Summarising the AGBD and its uncertainties

If you have obtained the AGB estimates and their uncertainty using the 'AGBmonteCarlo()' function (see the Estimating stand biomass vignette, you can provide the output matrix of this function using the AGB_simu argument.
The function also handles uncertainty in corner coordinates if the subplots argument provided has been created using the sd_coord argument (optional):

error_prop <- readRDS("saved_data/error_prop.rds")
subplot_AGBD <- subplot_summary(
  subplots = sd_coord_subplots,
  AGB_simu = error_prop$AGB_simu # error_prop has been created in the previous vignette
)

The simulated AGBs are therefore summarised in AGBD (i.e. AGB per hectare), and the function returns an additional output containing all the simulations performed: $long_AGB_simu.

Summarising LiDAR metrics

subplot_summary() can also summarise the metric contained in a raster (typically a CHM raster obtained from LiDAR data) by providing the ref_raster and the raster_fun arguments:

raster_summary <- subplot_summary(
  subplots = subplots,
  ref_raster = nouraguesRaster, raster_fun = median)

All at once

Let us consider the uncertainty in the coordinates of the plot corners and summarise the following information:

subplot_metric <- subplot_summary(
  subplots = sd_coord_subplots,
  value = "D", fun = sd, per_ha = TRUE,
  AGB_simu = error_prop$AGB_simu, 
  ref_raster = nouraguesRaster # by default, the associated function is the mean function
  )

Customizing the ggplot

Here are some examples to custom the ggplot of the subplot_summary() function:

subplot_metric <- subplot_summary(subplots = subplots,
                                  value = "AGB") 

custom_plot <- subplot_metric$plot_design
# Change the title and legend:
custom_plot + 
  labs(title = "Nouragues plot" , fill="Sum of AGB per hectare")
# Display trees with diameter as size and transparency (and a smaller legend on the right): 
custom_plot + 
  geom_point(data=check_plot_trust_GPS$tree_data, mapping = aes(x = x_proj, y = y_proj, size = D, alpha= D), shape=1,) +
  labs(fill = "Sum of AGB per hectare") +
  guides(alpha = guide_legend(title = "Diameter (cm)"),
         size = guide_legend(title = "Diameter (cm)")) + 
  theme(legend.position = "right", legend.key.size = unit(0.5, 'cm'))


Try the BIOMASS package in your browser

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

BIOMASS documentation built on March 11, 2026, 9:08 a.m.