In GEO-TREES
we are using the BIOMASS R package
to process tree inventory data to produce 50x50 m estimates of above-ground woody biomass density (AGBD). BIOMASS has a function called check_plot_coord() that takes GPS measurements of plot corners and fits a polygon to them, to estimate the area covered by the plot. GPS data are noisy and imprecise, meaning we often have some uncertainty in the location of the plot corners. check_plot_coord() allows you to use multiple repeat measurements of the plot corners to find the average the location of each corner.
However, check_plot_coord() doesn’t let you incorporate auxiliary GPS measurements taken at other known points within the plot, such as at the intersections of subplots, the plot centre, or around the perimeter of the plot. I have developed a more simple function that allows you to incorporate these auxiliary GPS measurements to estimate the true location of the plot.
As with check_plot_coord(), my function (polyFit()) lets you choose between either a procrustes analysis that trusts the plot dimensions first (i.e. maintains the plot dimensions) or an interpolation method that trusts the GPS coordinates first (i.e allows the plot shape to warp and sheer). In polyFit(), the interpolation method comes from an affine transformation of the GPS coordinates, while check_plot_coord() uses bilinear interpolation, which is fundamentally limited to use only four plot corners. This means that check_plot_coord() also cannot account for more irregular plot shapes. All plots must be rectangles in their method, while polyFit() can accommodate arbitrarily complex plots. Finally, polyFit() also allows a simple “exact” method which simply averages the locations of each plot corner individually.
Here is my function:
# @param x dataframe containing point locations
# @param method either 'exact', 'affine' or 'procrustes'
# @param proj_cols vector of length two specifying the X and Y coordinates of each point in `x`, in local UTM
# @param rel_cols vector of length two specifying the relative X and Y coordinates of each point in `x`, in metres
# @param type_col name of column in `x` specifying each point as either a plot corner or an auxiliary point.
#
polyFit <- function(x, method, proj_cols, rel_cols, type_col) {
# Rename columns
names(x)[names(x) %in% proj_cols] <- c("x_proj", "y_proj")
names(x)[names(x) %in% rel_cols] <- c("x_rel", "y_rel")
names(x)[names(x) == type_col] <- "type"
# Extract corners points only
corners <- x[x$type == "corner", ]
# Transformation
if (method == "exact") {
# Take mean of all values per corner
corners <- aggregate(cbind(x_proj, y_proj) ~ x_rel + y_rel,
data = corners, FUN = mean)
} else if (method == "affine") {
# Fits independent scale and shear
fit_x <- lm(x_proj ~ x_rel + y_rel, data = x)
fit_y <- lm(y_proj ~ x_rel + y_rel, data = x)
corners$x_proj <- predict(fit_x, newdata = corners)
corners$y_proj <- predict(fit_y, newdata = corners)
} else if (method == "procrustes") {
procrust_res <- BIOMASS::procrust(x[, c("x_proj", "y_proj")], x[, c("x_rel", "y_rel")])
procrust_coord <- as.matrix(corners[, c("x_rel", "y_rel")]) %*% procrust_res$rotation
procrust_coord <- sweep(procrust_coord, 2, procrust_res$translation, FUN = "+")
corners$x_proj <- procrust_coord[, 1]
corners$y_proj <- procrust_coord[, 2]
} else {
stop("method must be either 'exact', 'affine' or 'procrustes'")
}
# Collapse to unique corners
corners <- corners[!duplicated(corners),]
# Sort corners counter-clockwise
centroid <- colMeans(corners[,c("x_rel","y_rel")])
angles <- base::atan2(corners[["y_rel"]] - centroid[2], corners[["x_rel"]] - centroid[1])
corners <- corners[order(angles), ]
# Return
return(corners)
}
Below is a comparison of the performance of the different methods using various scenarios. The larger grey circles represent individual GPS measurements of a point. Red circles and lines represent results from BIOMASS::check_plot_coord(). Blue circles and lines are from my polyFit() function. Columns are different methods from both sets of functions. Rows are different data scenarios.

You can see that under most scenarios the methods return identical results. The big difference is in the “bad_centre” scenario where check_plot_coord() can’t handle non-corner points. ANother difference is in the “bad_corner” scenario under the affine / bilinear interpolation method, where check_plot_coord() just follows the corner measurements directly.