Commit e07cfaad authored by Monnet Jean-Matthieu's avatar Monnet Jean-Matthieu
Browse files

catalog engine used in batch processing of tree detection

parent b40b8c98
No related merge requests found
Showing with 230 additions and 305 deletions
+230 -305
...@@ -170,7 +170,7 @@ gaps1r <- lidaRtRee::gap_detection(chm, ...@@ -170,7 +170,7 @@ gaps1r <- lidaRtRee::gap_detection(chm,
# display detected gaps # display detected gaps
par(mfrow = c(1, 3)) par(mfrow = c(1, 3))
# plot gaps # plot gaps
terra::plot(chm, main = "Canopy heightmodel") terra::plot(chm, main = "Canopy height model")
terra::plot(gaps1$gap_id > 0, main = "Gaps", legend = FALSE) terra::plot(gaps1$gap_id > 0, main = "Gaps", legend = FALSE)
terra::plot(gaps1r$gap_id > 0, main = "Gaps extended by reconstruction", legend = FALSE) terra::plot(gaps1r$gap_id > 0, main = "Gaps extended by reconstruction", legend = FALSE)
``` ```
......
...@@ -570,13 +570,6 @@ lidaRtRee::plot_tree_inventory(tree_metrics_h ...@@ -570,13 +570,6 @@ lidaRtRee::plot_tree_inventory(tree_metrics_h
# write.csv(tree_metrics_h, "./data/output/correct_detections.csv", row.names = FALSE) # write.csv(tree_metrics_h, "./data/output/correct_detections.csv", row.names = FALSE)
# write.csv(tree_inventory_chablais3, "./data/output/tree_inventory_chablais.csv", row.names = FALSE) # write.csv(tree_inventory_chablais3, "./data/output/tree_inventory_chablais.csv", row.names = FALSE)
``` ```
```{r test, include=FALSE, fig.dim=c(9, 9)}
# add attributes of apices to polygons by merging
# !!!! for some reason the segment_id is messed up during the process
# segments_v@data <- merge(segments_v@data, metrics, by.x = "segments_id", by.y = "seg_id")
# remove ground segment
# segments_v <- segments_v[segments_v$segments_id != 0,]
```
## Display point cloud ## Display point cloud
...@@ -699,56 +692,55 @@ for (i in 1:nrow(apices)) ...@@ -699,56 +692,55 @@ for (i in 1:nrow(apices))
## Batch processing ## Batch processing
The following code exemplifies how to process numerous LAS files and extract apices for the whole area with parallel processing. The processing runs faster if data is provided as chunks to the segmentation algorithm, and results are then aggregated, rather than running on the full coverage of the data. The following code exemplifies how use the catalog engine of package `lidR` to process numerous LAS files and extract apices for the whole area with parallel processing. The catalog engine provides the data as chunks to the segmentation algorithm, and results are aggregated afterwards.
In order to avoid border effects, chunks are provided to the algorithm as overlapping tiles. Results are cropped to prevent the same tree from appearing twice in the final results. Tile size and buffer size are important parameters : In order to avoid border effects, a buffer zone is added to chunks. Chunk results are cropped to prevent the same tree from appearing twice in the final results. Chunk size and buffer size are important parameters :
- tile size is a trade-off between the number of chunks to process and the amount of RAM required to process a single tile ; - `chunk_size` is a trade-off between the number of chunks to process and the amount of RAM required to process a single chunk ;
- buffer size is a trade-off between redundant processing of the overlap area, and assuring that a tree is which treetop is located at the border of a tile has its full crown within the buffer size. - `chunk_buffer` size is a trade-off between redundant processing of the overlap area, and assuring that a tree which treetop is located at the border of a chunk has its full crown within the buffer size.
Tiles can be processed with parallel computing, within limits of the cluster's RAM and number of cores. Chunks can be processed with parallel computing, within limits of the cluster's RAM and number of cores.
The steps for processing a batch of las/laz files are : The steps for processing a batch of las/laz files are :
- build catalog of files - build catalog of files and set chunk processing options
- provide tiling parameters
- provide segmentation parameters - provide segmentation parameters
- provide output parameters - provide output parameters
- set cluster options for parallel processing - set cluster options for parallel processing
- compute the X and Y coordinates of tiles - use the catalog engine to proceed with the following steps for each chunk:
- parallelize the processing with the package `future`, by sending to the parallel sessions the coordinates of tiles to process, and a function with the instructions to proceed :
- load tile corresponding to the coordinates
- compute CHM - compute CHM
- segment and extract apices - segment and extract apices
- remove apices inside buffer zone
- aggregate list of results - aggregate list of results
Be aware that in case tree segments are vectorized, some obtained polygons might overlap. The segmentation algorithm might not be deterministic and borders are sometimes not consistent when adjacent polygons are pasted from different tiles. Be aware that in case tree segments are vectorized, some obtained polygons might overlap. The segmentation algorithm might not be deterministic and borders are sometimes not consistent when adjacent polygons are pasted from different tiles.
```{r batch, include=TRUE, warning = FALSE} ```{r batch, include=TRUE, warning = FALSE}
rm(list = ls()) rm(list = ls())
# BUILD CATALOG OF FILES # BUILD CATALOG OF FILES AND SET CATALOG PROCESSING OPTIONS
# folder containing the files # folder containing the files
lazdir <- "../data/forest.structure.metrics" lazdir <- "../data/forest.structure.metrics"
# build catalog # build catalog and set options
cata <- lidR::readALSLAScatalog(lazdir) # - progress: disable display of catalog processing
# disable display of catalog processing # - select: read only xyzc attributes (coordinates, intensity,
lidR::opt_progress(cata) <- FALSE # echo order and classification) from height files
# set coordinate system # - chunk_size: tile size to split area into chunks to process,
lidR::projection(cata) <- 2154
# option to read only xyzc attributes (coordinates, intensity, echo order and classification) from height files
lidR::opt_select(cata) <- "xyz"
#
# BATCH PROCESSING PARAMETERS
# tile size to split area into chunks to process
# trade-off between RAM capacity VS total number of tiles to process # trade-off between RAM capacity VS total number of tiles to process
tile_size <- 70 # here 70 for example purpose with small area # here 70 for example purpose with small area
# buffer size: around each tile to avoid border effects on segmentation results # - buffer_size: around each tile to avoid border effects on segmentation results
# trade-off between making sure a whole tree crown is processed in case its top # trade-off between making sure a whole tree crown is processed in case its top
# is on the border VS duplicate processing # is on the border VS duplicate processing
buffer_size <- 10 # 5 m is minimum, 10 is probably better depending on tree size # 5 m is minimum, 10 is probably better depending on tree size
cata <- lidR::readALSLAScatalog(lazdir,
progress = FALSE,
select = "xyz",
chunk_size = 70,
chunk_buffer = 10)
# set coordinate system
lidR::projection(cata) <- 2154
# #
# TREE SEGMENTATION PARAMETERS # TREE SEGMENTATION PARAMETERS
# set raster resolution # set raster resolution
resolution <- 1 res <- 1
# #
# OUTPUT PARAMETERS # OUTPUT PARAMETERS
# option to vectorize crowns (set to FALSE if too slow) # option to vectorize crowns (set to FALSE if too slow)
...@@ -764,112 +756,79 @@ future::plan("multisession", workers = 2L) ...@@ -764,112 +756,79 @@ future::plan("multisession", workers = 2L)
# remove warning when using random numbers in parallel sessions # remove warning when using random numbers in parallel sessions
options(future.rng.onMisuse = "ignore") options(future.rng.onMisuse = "ignore")
# #
# COORDINATES OF TILES # FUNCTION TO APPLY TO CATALOG
bbox <- sf::st_bbox(cata) routine <- function(chunk, resolution = res)
# low left corner {
x <- seq( # empty list for output
from = floor(bbox$xmin / tile_size) * tile_size, output <- list()
by = tile_size, # bounding box of region, without buffer
to = ceiling(bbox$xmax / tile_size - 1) * tile_size bbox <- sf::st_bbox(chunk)
) # [1:2] # read chunk of data
length_x <- length(x) las <- lidR::readLAS(chunk)
y <- seq( # return NULL if empty
from = floor(bbox$ymin / tile_size) * tile_size, if (lidR::is.empty(las)) return(NULL)
by = tile_size, # normalization if required
to = ceiling(bbox$ymax / tile_size - 1) * tile_size # las <- lidR::normalize_height(las, lidR::tin())
) # [1:2] # in this example LAS tiles are already normalized
length_y <- length(y) # compute canopy height model
# repeat coordinates for tiling while area chm <- lidR::rasterize_canopy(las, resolution, algorithm = lidR::p2r(), pkg = "terra")
x <- as.list(rep(x, length_y)) #
y <- as.list(rep(y, each = length_x)) # check all chm is not NA
# if (all(is.na(terra::values(chm)))) return(NULL)
# PARALLEL PROCESSING #
# function takes coordinates of tile as arguments # tree detection (default parameters)
resultats <- future.apply::future_mapply( segms <- lidaRtRee::tree_segmentation(chm)
# i and j are the coordinated of the low left corner of the tile to process # tree extraction
FUN = function(i, j) { apices <- lidaRtRee::tree_extraction(
# initialize output segms$filled_dem,
output <- list() segms$local_maxima,
output$name <- paste0(i, "_", j) segms$segments_id
# extract tile plus buffer )
las_tile <- lidR::clip_rectangle( # remove apices in buffer area
cata, apices <- sf::st_crop(apices, sf::st_bbox(chunk))
i - buffer_size, # add tile id
j - buffer_size, apices$tile <- paste0(bbox$xmin, "_", bbox$ymin)
i + tile_size + buffer_size, # put apices into output slot
j + tile_size + buffer_size output$apices <- apices
#
# crop chm if requested output (global variable)
if (out_chm | out_save_chm)
{
chm <- terra::crop(chm, terra::ext(bbox$xmin, bbox$xmax, bbox$ymin, bbox$ymax))
}
# output chm if asked for
if (out_chm)
{
output$chm <- terra::wrap(chm)
}
# if save on disk
if (out_save_chm) {
terra::writeRaster(chm,
file = paste0("chm_", bbox$xmin, "_", bbox$ymin, ".tif"),
overwrite = TRUE
) )
# check if points are present }
if (nrow(las_tile) > 0) { # convert to vectors if option is TRUE
# normalization if required if (out_vectorize_apices) {
# las_tile <- lidR::normalize_height(las_tile, lidR::tin()) # vectorize
# in this example LAS tiles are already normalized apices_v <- terra::as.polygons(segms$segments_id)
# compute canopy height model # remove polygons which treetop is in buffer
chm <- lidR::rasterize_canopy(las_tile, resolution, algorithm = lidR::p2r(), pkg = "terra") apices_v <- apices_v[is.element(apices_v$segments_id, apices$id), ]
# check all chm is not NA # convert to sf
if (!all(is.na(terra::values(chm)))) { apices_v <- sf::st_as_sf(apices_v)
# output chm if asked for names(apices_v)[1] <- "id"
if (out_chm) { # add attributes
output$chm <- terra::wrap(terra::crop(chm, terra::ext( apices_v <- merge(apices_v, sf::st_drop_geometry(apices), all.x = TRUE)
i, i + tile_size, # save in list
j, j + tile_size output$apices_v <- apices_v
))) }
} output
# save on disk } # end of routine function
if (out_save_chm) {
terra::writeRaster(terra::crop(
chm,
terra::ext(
i, i + tile_size,
j, j + tile_size
)
),
file = paste0("chm_", i, "_", j, ".tif"),
overwrite = TRUE
)
}
#
# tree detection
segms <- lidaRtRee::tree_segmentation(chm)
# tree extraction
apices <- lidaRtRee::tree_extraction(
segms$filled_dem,
segms$local_maxima,
segms$segments_id
)
# remove apices in buffer area
# copy coordinates
apices <- cbind(apices, sf::st_coordinates(apices))
apices <- apices[apices$X >= i & apices$X < i + tile_size &
apices$Y >= j & apices$Y < j + tile_size, ]
# add tile id to apices to avoid duplicates in final file
apices$tile <- paste0(i, "_", j)
# convert to vectors if option is TRUE
if (out_vectorize_apices) {
# vectorize
apices_v <- terra::as.polygons(segms$segments_id)
# remove polygons which treetop is in buffer
apices_v <- apices_v[is.element(apices_v$segments_id, apices$id), ]
# convert to sf
apices_v <- sf::st_as_sf(apices_v)
names(apices_v)[1] <- "id"
# add attributes
apices_v <- merge(apices_v, sf::st_drop_geometry(apices), all.x = TRUE)
# save in list
output$apices_v <- apices_v
}
# save apices in list
output$apices <- apices
} # end of raster is not all NAs check
} # end of nrow LAS check
output
}, x, y, SIMPLIFY = FALSE
) # function applied to the lists of coordinates (x and y)
# #
# RESULTS AGGREGATION # APPLY FUNCTION TO CATALOG
# extract results from nested list into separate lists and then bind data resultats <- lidR::catalog_apply(cata, routine)
id <- unlist(lapply(resultats, function(x) x[["name"]]))
# #
# RESULTS AGGREGATION
# apices # apices
apices <- lapply(resultats, function(x) x[["apices"]]) apices <- lapply(resultats, function(x) x[["apices"]])
# remove NULL elements # remove NULL elements
...@@ -884,7 +843,6 @@ if (out_chm) { ...@@ -884,7 +843,6 @@ if (out_chm) {
# merge chm # merge chm
# no names in list otherwise do.call returns an error # no names in list otherwise do.call returns an error
chm_all <- do.call(terra::merge, chm) chm_all <- do.call(terra::merge, chm)
names(chm) <- id
} }
# apices_v # apices_v
if (out_vectorize_apices) { if (out_vectorize_apices) {
......
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment