I would like to filter the points that are used in a canopy height model. In particular I would like filter out any points whose height is over 200 feet. I've tried the code below but when I plot the difference between my normalized and filtered tiles, they are exactly the same and there are still z values above 200 feet. I must be using the opt_filter command incorrectly, or have chosen the wrong mechanism.


indata <- suppressWarnings(readLAScatalog ("c:/test/laz"))

# Normalize point cloud data
opt_output_files(indata) <- "c:/test/norm/norm_{XRIGHT}_{YBOTTOM}"
normalized_las <- normalize_height (indata, tin(), na.rm=TRUE)

# Keep points where height is between 0 and 200 feet
filtered_las <- suppressWarnings(readLAScatalog ("c:/test/norm/"))
opt_filter (filtered_las) <- "Z>=0 and Z<=200"
opt_output_files(filtered_las) <- "c:/test/filt/filt_{XRIGHT}_{YBOTTOM}"
filt_ctg <- catalog_retile(filtered_las)

# Now read in the same filtered and normalized tile
filt1 <- 'c:/test/filt/filt_2885000_2320000.las'
filtlas <- readLAS(filt1)
norm1 <- 'c:/test/norm/norm_2885000_2320000.las'
normlas <- readLAS(norm1)

# Visualize difference between filtered and unfiltered points
boxplot(normlas@data$Z, ylim = c(-10,1000), ylab = "Height (ft)", main = "Before Filtering")
boxplot(filtlas@data$Z, ylim = c(-10,1000), ylab = "Height (ft)", main = "After Filtering")
hist(normlas@data$Z, xlim=c(-2,1000), xlab = "Height (ft)", main = "Before Filtering")
hist(filtlas@data$Z, xlim=c(-2,1000), xlab = "Height (ft)", main = "After Filtering")

enter image description here

1 Answer 1


You are mistaking the argument filter in readLAS() and the function filter_poi().


When you have read a LAS file in R and the point cloud is already loaded, you can use a classical R syntax, that you are likely to be familiar with because dplyr popularize it a lot, to select some points of interest (POI)

las = readLAS(LASfile)
las = filter_poi(las, Z>=0, Z<=200)

filter argument in readLAS()

readLAS can take an optional parameter filter and allows selection of “rows” (or points) while reading the LAS file. It happens at C++ level and not at the R level. It is better for different reasons that are documented in readLAS() and in this vignette. But because it does not happen at R level, R syntax does not work. Instead you have to use predefined set of commands. List of commands can be printed using readLAS(filter = "-help"). You can do:

las = readLAS(LASfile, filter = "-drop_z_below 0 -drop_z_above 200")


When processing a LAScatalog you do not call readLAS() yourself. It is called internally.opt_filter() is used to propagate the information:

opt_filter(ctg) = "-drop_z_below 0 -drop_z_above 200"

Other filter

For the sake of comprehensiveness I must also speak about the other filter argument you can find in lidR such as in grid_metrics() or segment_shapes(). Those filter arguments are R-based filters and thus expect R syntax. They allow for processing a subset of the point-cloud without creating a copy of the point-cloud unlike with filter_poi(). They correspond to clever memory optimization but cannot exist for every function.

metrics = grid_metrics(las, ~mean(Z), 20, filter = ~Z>=0 & Z<=200)


  1. Use readLAS(... filter = "-command") whenever you can (i.e. when it exists a command that do what you want to do) to do not load in R the points you don't need.
  2. Use filter = R expression if you want to use filter_poi() but the function you are using is capable of being smarter than that.
  3. Use filter_poi() the other cases.
  4. Read the LAS formal class vignette and/or the first chapter of the lidR book.

Your Answer

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

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