2

I am normalizing a set of LAS files. In the function after normalize_height, I filter out vegetation with heights below 0 using filter_poi, and returning this as input to catalog_apply. However, I would also like to save vegetation with heights below 0 as a check.

It doesn't appear possible to return 2 objects (veg_ge_0 and veg_lt_0). It is also not possible to use opt_filter outside of the function since this seems to work on the input to the function and not on the output.

Is there a more efficient way (like returning 2 objects from the function to use in a second catalog_apply) other than creating a new function combined with catalog_apply for filtering the negatives?

library(lidR)


setwd("D:/Projects2021/p2021_0101_lidR_vs_LasTools/")
dirLASfiles_Raw <- "LasFiles__original/"
dirLASfiles_Norm <- "LasFiles_normalizedWith_lidR_wTIN/"

interpolationMethod <- tin('', extrapolate = knnidw(k = 10, p = 2, rmax = 50))


ctg <- readLAScatalog(dirLASfiles_Raw,
                      progress = TRUE,
                      filter = "-drop_withheld -drop_overlap -drop_z_below -1")


groundWaterVeg_filterAndNormalization_Function = function(chunk) {
  lasChunk = readLAS(chunk)
  if (is.empty(lasChunk)) return(NULL)
  
  ground <- filter_poi(lasChunk, Classification == 2L | Classification == 9L)
  if (is.empty(ground))
    return(NULL)
  
  vegetation <- filter_poi(lasChunk, Classification == 3L | Classification == 4L | Classification == 5L)
  if (is.empty(vegetation))
    return(NULL)

  lasNorm <- normalize_height(lasChunk, algorithm = interpolationMethod, na.rm = TRUE, use_class = c(2L, 9L))

  #lasNormFltrNegVeg <- filter_poi(lasNorm, ((Classification == 3L | Classification == 4L | Classification ==5L) & (Z < 0)), buffer == 0)
  lasNormFltr <- filter_poi(lasNorm, ((Classification == 3L | Classification == 4L | Classification ==5L) & (Z >= 0 & Z <= 30)) | (Classification == 2L | Classification == 9L), buffer == 0)  #! LASFilter is depracated. use Filter_POi
  return(lasNormFltr) #,lasNormFltrNegVeg)
}

opt_chunk_buffer(ctg) <- 25
opt_output_files(ctg) <- paste(dirLASfiles_Norm,"{ORIGINALFILENAME}_normTIN_vegge0PlusGround", sep = "")
mylasCatalogueNorm <- catalog_apply(ctg, groundWaterVeg_filterAndNormalization_Function)

7
  • Everything is possible. I already did something similar. But please add a example of code so I have something to start with to build an example.
    – JRR
    Commented Feb 19, 2021 at 17:38
  • okay thanks, code added...
    – Ray J
    Commented Feb 19, 2021 at 17:47
  • It is unclear what you want to output. Please clarify. Do you want to output lasNormFltr + lasNorm + ground + vegetation in a single shot ?
    – JRR
    Commented Feb 19, 2021 at 18:12
  • My current output is from lasNormFltr, which is ground (now all 0'd) plus vegetation heights that are greater than 0. Since some of my vegetation heights come out negative where there are cliffs and sparse ground hits, I would also like a set of vegetation hits that are negative (lasNormFltrNegVeg).
    – Ray J
    Commented Feb 19, 2021 at 18:18
  • It is still unclear. I can't see any variable named lasNormFltrNegVeg
    – JRR
    Commented Feb 19, 2021 at 18:22

1 Answer 1

4

What you want to do is definitively complex. I had to read my own documentation to achieve it. The LAScatalog processing engine allows to do almost everything but is consequently hard to use. Everything is documented but it is definitively hard to find and put all pieces together. So we will go step by step and this answer will serve as reference.

There are several way to code it. I'm using my own style following this vignette but you can do it differently.

Reproducible example

First I'm generating data to make a reproducible example.

LASfile <- system.file("extdata", "Topography.laz", package="lidR")
las = readLAS(LASfile)
las$Classification[las$Classification == LASUNCLASSIFIED] <- LASLOWVEGETATION
f = tempfile(fileext = ".las")
writeLAS(las, f)
ctg = readLAScatalog(f)

groundWaterVeg_filterAndNormalization_Function = function(las)
{
  UseMethod("groundWaterVeg_filterAndNormalization_Function", las)
}

Modification of your function

I modified your function so it is more readable (imo) and more importantly it works for a LAS object and returns 2 outputs in a list

groundWaterVeg_filterAndNormalization_Function.LAS = function(las) 
{
  ground <- las$Classification %in% c(LASGROUND, LASWATER)
  if (!any(ground)) return(NULL)
  
  vegetation <- las$Classification %in% c(LASLOWVEGETATION, LASMEDIUMVEGETATION, LASHIGHVEGETATION)
  if (!any(vegetation)) return(NULL)
  
  interpolationMethod <- tin('', extrapolate = knnidw(k = 10, p = 2, rmax = 50))
  lasNorm <- normalize_height(las, algorithm = interpolationMethod, na.rm = TRUE, use_class = c(2L, 9L))
  
  lasNormFltrNegVeg <- filter_poi(lasNorm, (Classification %in% c(LASLOWVEGETATION, LASMEDIUMVEGETATION, LASHIGHVEGETATION) & (Z < 0)))
  lasNormFltr <- filter_poi(lasNorm, (Classification %in% c(LASLOWVEGETATION, LASMEDIUMVEGETATION, LASHIGHVEGETATION) & (Z >= 0 & Z <= 30)) |  Classification %in% c(LASGROUND, LASWATER))
  output <- list(lasNormFltr, lasNormFltrNegVeg)
  return(output)
}

We can try it to check if it works. It returns a list with two LAS object

out = groundWaterVeg_filterAndNormalization_Function(las)

Your function for LAScluster

We have a function that works for a LAS now we need a version compatible with catalog_apply(). In your example you only created the latest but I always split in two parts to make versatile functions that can be easily tested. Here the key is that you must clear the buffer of the two objects.

groundWaterVeg_filterAndNormalization_Function.LAScluster = function(las)
{
  las = readLAS(las)
  if (is.empty(las)) return(NULL)
  
  out = groundWaterVeg_filterAndNormalization_Function(las)
  if (is.null(out)) return(NULL)
  
  out[[1]] = filter_poi(out[[1]], buffer == 0L)
  out[[2]] = filter_poi(out[[2]], buffer == 0L)
  
  return(out)
}

The example file being tiny we can test it as is without redirecting the output into disk files. The output is a list with 4 elements. Each element is a list of two LAS objects

opt_chunk_size(ctg) <- 250
out = catalog_apply(ctg, groundWaterVeg_filterAndNormalization_Function)
out

Redirection in files

This is the trickiest part. The output of each file is a list of LAS and the catalog engine does not know how to write that in files. First we create a function capable of writing two LAS objects using a single path as input

write_two_las = function(two_las, file)
{
  file1 = file2 = tools::file_path_sans_ext(file)
  file1 = paste0(file1, "_NormFltr.las")
  file2 = paste0(file2, "_NormFltrNegVeg.las")
  writeLAS(two_las[[1]], file1)
  writeLAS(two_las[[2]], file2)
}

Now we must tell the catalog how to write a list because it does not know how to do it. We will create a new driver for this LAScatalog. This is documented in ?lidR-LAScatalog-drivers and in this vignette

ctg@output_options$drivers$list = list(
  write = write_two_las,
  object = "two_las",
  path = "file",
  extension = "",
  param = list())

We have now a driver that knows how to write list of 2 LAS so, we can use opt_output_file()

opt_filter(ctg) <- "-drop_withheld -drop_overlap -drop_z_below -1"
opt_output_files(ctg) <- paste0(tempdir(),"/file_{ID}")
out = catalog_sapply(ctg, groundWaterVeg_filterAndNormalization_Function)
out
#> [1] "/tmp/RtmpgBhEaH/file_1" "/tmp/RtmpgBhEaH/file_2" "/tmp/RtmpgBhEaH/file_3" "/tmp/RtmpgBhEaH/file_4"

Notice that it returns the name of the files as defined by the engine not the one you actually used by overwriting the path generated by the engine.

Final output

ctgNormFltr = readLAScatalog(paste0(out, "_NormFltr.las"))
ctgNormFltrNegVeg = readLAScatalog(paste0(out, "_NormFltrNegVeg.las"))
3
  • thank you this is very detailed. I am stuck on creating the final output in the last two lines. For me, 'out' is a Large LAScatalog and not the list, and I get an error. > ctgNormFltr = readLAScatalog(paste0(out, "_NormFltr.las")) Error in as.character.default(new("LAScatalog", chunk_options = list(size = 0, : no method for coercing this S4 class to a vector
    – Ray J
    Commented Feb 19, 2021 at 20:03
  • Maybe use extension = "". I've updated my anwser. Also you can use catalog_apply() instead of catalog_sapply()
    – JRR
    Commented Feb 19, 2021 at 20:05
  • yes! it all works now. I used catalog_apply().
    – Ray J
    Commented Feb 19, 2021 at 20:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.