Clicky

11  Frequently Asked Questions

11.1 Working with Pre-Segmented Individual Trees

I want to make QSMs of individual trees that were already segmented manually out of Arbor. I get errors.

Arbor expects point cloud prepared by Arbor that were segmented (semantic) by Arbor, hybrid decimated by Arbor and, super important, that have an attribute hag for Height Above Ground. Thus, applying qsm() on a single tree point cloud the users may get an error that stipulate that wood/foliage classification is missing or hag is missing. It means that segment_ground(), wood_likelihood() and segment_semantic() must be applied first. Sadly without ground ground points it is impossible to perform these operations. The solution is to use add_single_tree_ground() to add fake ground points.

tree <- lidR::readTLS(x, select = "xyz", filter = "-keep_random_fraction 0.4")
tree <- hybrid_homogeneization(tree)
tree <- add_single_tree_ground(tree)
tree <- segment_ground(tree)
tree <- wood_likelihood(tree)
tree <- segment_semantic(tree)
qsm  <- qsm(tree)

If the individual tree already contains exclusively wood (foliage removed) it is even easier. We don’t need fake ground.

tree <- lidR::readTLS(x, select = "xyz", filter = "-keep_random_fraction 0.4")
tree <- hybrid_homogeneization(tree)
tree <- lidR::add_attribute(tree, 0L, "foliage")
tree <- lidR::add_attribute(tree, tree$Z - min(tree$Z), "hag")

11.2 Data Already 1-cm Voxel Decimated

Our dataset is already voxel-decimated at 1 cm, which is discouraged in this book. We do not have access to the raw point cloud. Can we still work with these data?

You are more likely to obtain acceptable results with TLS data than with MLS. Read the point cloud without applying additional read-time decimation (readTLS(x, select = "xyz")).

However, performance may be reduced compared with our recommended pipeline, particularly for instance segmentation, as some structural information has already been lost through prior voxel decimation. Any benchmarking or comparison should therefore clearly acknowledge this limitation and avoid directly comparing results with benchmarks or claims reported in this book that were obtained from our decimation pipeline.

11.3 Processing the Entire Point Cloud

Arbor is never processing the entire point cloud through its decimations pipeline. This is ok if the desired end product is the QSMs. But in our case the desired end product is a classified point cloud.

Arbor as a function transfer_attributes() designed for this very purpose. It transfers the attributes from a lower density point cloud to a higher density point cloud

11.4 Massive Datasets

Our dataset is massive (3 ha). The point cloud is very dense, and we do not have enough RAM to run Arbor.

Arbor is indeed memory-intensive and does not yet include built-in mechanisms for chunked processing. Below is the workflow we use to process such large datasets on a laptop.

Show the code
library(lidR)
library(arbor)

file = "file.las"

display = FALSE
random_fraction  = 1
cut_above_ground = 0.15
buffer_size = 10

par = arbor_parameters_default
par$global$cut_above_ground = cut_above_ground

o <- tools::file_path_sans_ext(file)
dir <- paste0(o, "_output")
o = basename(o)
tmp = paste0(dir, "/", "tmp")
dir.create(dir, showWarnings = FALSE)
dir.create(tmp, showWarnings = FALSE)

ctg = readTLScatalog(file, select = "xyz0", filter = paste("-keep_random_fraction", random_fraction))
opt_chunk_size(ctg) = 50
opt_chunk_buffer(ctg) = buffer_size
plot(ctg, chunk = T)
chunks = lidR::engine_chunks(ctg)

maxID = 0
for (i in seq_along(chunks))
{
  cat("==============\n")
  cat("Chunk", i, "of", length(chunks), "\n")
  cat("==============\n")

  # Regular pipeline
  chk <- chunks[[i]]
  las <- readLAS(chk)
  las <- hybrid_homogeneization(las)
  las <- segment_ground(las, par)
  las <- wood_likelihood(las, par)
  las <- segment_semantic(las, par)
  see <- find_seeds(las, par)
  las <- segment_instance(las, see, par)
  las <- colorize_trees(las, FALSE)

  # Custom tree remove
  bb <- st_bbox(chk)
  bb <- sf::st_as_sfc(bb)
  tm <- see@data[, .(x = mean(X), y = mean(Y)), by = treeID]
  tm <- sf::st_as_sf(tm, coords = c("x", "y"))
  seeds_roi <- sf::st_contains(bb, tm)
  seeds_roi <- tm[seeds_roi[[1]], ]
  seeds_roi_ids <- unique(seeds_roi$treeID)
  seeds_roi <- filter_poi(seeds, treeID %in% seeds_roi_ids)

  las <- filter_poi(las, treeID %in% seeds_roi_ids)
  las$treeID <- las$treeID + maxID
  maxID <- max(las@data$treeID)

  dtm <- rasterize_terrain(las, 0.1)
  dtm <- terra::crop(dtm, terra::vect(bb))

  # Export
  s <- paste0(tmp, "/", o, "_segmented_part", i, ".laz")
  r <- paste0(tmp, "/", o, "_dtm_part", i, ".tif")
  writeLAS(las, s)
  terra::writeRaster(dtm, r)

  # Clean up
  rm(las)
  rm(see)
  rm(dtm)
  gc()
}

gc()

# Build the full DTM
files = list.files(tmp, pattern = "dtm", full.names = T)
dtm = terra::vrt(files)
terra::writeRaster(dtm, paste0(dir, "/", o, "_dtm.tif"))

# Build the full point cloud without buffer
ctg = readLAScatalog(tmp, pattern = "segmented", select = "xyzic0")
las = readLAS(ctg)
las = flag_buffer(las, buffer = -buffer_size)
las = filter_poi(las, treeID > 0)
writeLAS(las, paste0(dir, "/", o, "_full.laz"))

q("no")

11.5 Custom Allometric Model

We would like to change the allometric model used in Section 7.6. How can we use our own equation?

Because Arbor is a standalone C++ library, allowing users to inject arbitrary R code is technically challenging. Therefore, this feature has not been implemented yet.

However, feel free to contact us and we will gladly add your equation directly to the library.