r-lidar / lidR

Airborne LiDAR data manipulation and visualisation for forestry application
https://CRAN.R-project.org/package=lidR
GNU General Public License v3.0
582 stars 130 forks source link

height of trees in `locate_trees()` makes more sense with normalized data #699

Closed wiesehahn closed 1 year ago

wiesehahn commented 1 year ago

"The height of the trees (Z) are also repeated in the table ..." is stated in locate_trees() documentation. But this is just the case if the input is a normalized las file, otherwise it will be the highest point of the tree a.s.l. While this is quite logical if you think about it, it might be nice to add some info to the documentation.

I just stumbled upon this while I was wondering why locate_trees(las, lmf(ws = 7, hmin = 10)) did not work as expected. lmf() calculates hmin also based on absolute Z value and thus only works as expected with a normalized point cloud. A hint in the documentation or something might be good here too.

Whats the way to go if we want to calculate tree top position in absolute XYZ position (height a.s.l.) but the tree height (Z value in attribute table) as tree height?

E.g. in this example hmin does not work and tree heights are above 100m.

LASfile <- system.file("extdata", "MixedConifer.laz", package="lidR")
testlas <- readLAS(LASfile, select = "xyz", filter = "-inside 481250 3812980 481300 3813030")

testlas$Z <- testlas$Z +100

ttops <- locate_trees(testlas, lmf(ws = 5, hmin = 10))

Using the normalized point cloud on the other side will give us correct tree height, but the absolute tree top position is incorrect. I am looking for some option to calculate tree position and height base on the normalized height (Z) but the point geometry height based on absolute height (Zref).

Jean-Romain commented 1 year ago

This how I'm doing it:

library(lidR)
LASfile <- system.file("extdata", "MixedConifer.laz", package="lidR")
testlas <- readLAS(LASfile, select = "xyzc", filter = "-inside 481250 3812980 481300 3813030")
testlas$Z <- testlas$Z +100
ttops <- locate_trees(testlas, lmf(ws = 5, hmin = 10))
dtm <- rasterize_terrain(testlas)
asl <- terra::extract(dtm, terra::vect(ttops))$Z
ttops$Z <- ttops$Z - asl
wiesehahn commented 1 year ago

But you have a problem here. Because you added 100 previously, hmin is basically fulfilled in all cases. In your example there is no difference but if you try with e.g. ttops <- locate_trees(testlas, lmf(ws = 5, hmin = 20)) there are trees smaller than 20m.

Would it maybe make sense to provide an option in locate_trees() to get the Z geometry value from th unnormalized point cloud if run on a normalized pointcloud (or something similar in this direction)?

Jean-Romain commented 1 year ago

hmin does not make sense if your point cloud is not normalized. locate_tree() is designed for normalized point cloud. There is no way to know if the point cloud is normalized or not. An algorithm is a very dumb thing and will always process the input no matter if it makes sense in a given context. I cannot detect if the point cloud is normalized upstream to trigger a warning.

I'm keen to make modifications to help users but please suggest something that would help.

wiesehahn commented 1 year ago

Ok I see. But then our example above makes no sense, hmin is obsolete.

So if I would like to have height in the geometry z as a.s.l. and tree height normalized in attribute Z, while also be able to filter by relative tree height as hmin I have to use both point clouds concurrently. This should work then:

LASfile <- system.file("extdata", "MixedConifer.laz", package="lidR")
testlas <- readLAS(LASfile, select = "xyzc", filter = "-inside 481250 3812980 481300 3813030")
testlas$Z <- testlas$Z +100

nlas <- normalize_height(testlas, tin())
ttops <- locate_trees(nlas, lmf(ws = 5, hmin = 20))

chm <- rasterize_canopy(testlas)
top <- terra::extract(chm, terra::vect(ttops))$Z
ttops <- ttops |> 
  mutate(z = top)

# alternative
dtm <- rasterize_terrain(testlas)
asl <- terra::extract(dtm, terra::vect(ttops))$Z
ttops <- ttops |> 
  mutate(z = sf::st_coordinates(ttops)[,"Z"] + asl)

Back to my initial thought it might be nice to add some info to the documentation that hmin in lmf() is the minimum tree height if the input is normalized and otherwise refers to the absolute height.

Jean-Romain commented 1 year ago

Ok I see. But then our example above makes no sense, hmin is obsolete.

Yes. I do not see all issues systematically and did not catch that one at first glance. I'm just a human :wink:

So if I would like to have height in the geometry z as a.s.l. and tree height normalized in attribute Z, while also be able to filter by relative tree height as hmin I have to use both point clouds concurrently. This should work then:

Sounds like an option. Your first option is simpler and faster btw.

Alternatively I could add and options to retain the point ID. Something like.

ttops <- locate_trees(nlas, lmf(ws = 5, hmin = 20), pointID = TRUE)
ttops$z = testlas$Z[ttops$pointID]
wiesehahn commented 1 year ago

I once told you already that I believe you are a bot because of your response time, thanks :) This would be an option, but only you know if its worth the effort.

Can you just quickly (no detailed explanation nessecary) give me a hint why the first solution is faster?

Jean-Romain commented 1 year ago

Because you do not triangulate anything. Triangulation of ground points is likely to be slower than finding the highest point per pixel.