For running the demo, please download the materials from this link. The project folder should contain itd_demo.las and reference.csv. In addition it should have a bucking folder which contains the MARV42Polkyttaja.exe, made by Ilkka Korpela. For matters related to the script / workflow, please contact henri.riihimaki@helsinki.fi. For matters related to bucking and field reference contact ilkka.korpela@helsinki.fi. The following site description was provided by Ilkka Korpela (edited):
The study site is a drained peatland (cottongrass-bog). Main tree species in the area is Scots pine (Pinus sylvestris). First ditches were done in the 1920s. Clear-cut in 1946, planted in about 1950. Current site type Puolukkaturvekangas in the Finnish classification system.
Reference data collection was done by the students of field course FOR110B in 12 teams of two students, during 2016. The study area size is 132 x 164 meters (.las data clipped approximately). Trees were first mapped in LiDAR+aerial photos - then the tree map was verified and filled with omission trees of sufficient tree dbh (stem diameter). The so called photo-trees have thus an estimate of top XYZ, height (thru the DEM), and species. The species classification in image interpretation was pine, spruce, birch and dead trees. Students visited all trees, observed species, crown status, stem defects and dbh. A portion of trees was sampled and measured for d6 (stem diameter 6 m above ground) and height and height of the lowest living branch.
Airborne laser scanning data was collected with Optech Titan multispectral lidar in 2016: see documentation from http://www.helsinki.fi/%7Ekorpela/HYDE_REF/English_documentation.pdf (p. 74, accessed 23.05.2018).
ITD workflow follows the Andrew Plowright’s ForestTools vignette: https://cran.r-project.org/web/packages/ForestTools/vignettes/treetopAnalysis.html
Disclaimer regarding the use of LAStools. User must have a proper LAStools license to use the program; read LAStools LICENSE terms before running script: http://www.cs.unc.edu/~isenburg/lastools/LICENSE.txt This demo is strictly for educational purposes!
First: clear workspace and check that you have everything you need in your workspace:
rm(list=ls(all=T))
list.files()
## [1] "itd_demo.las" "itd_demo.Rmd" "ITD_Demo.Rproj" "MARV42polkky"
## [5] "reference.csv"
For the first time install the needed packages (e.g. install.packages(“raster”) : and load the libraries
library(raster); library(ForestTools); library(sp); library(rgdal); library(tidyverse); library(sf); library(PerformanceAnalytics); library(gridExtra); library(Metrics);library(RColorBrewer)
Next, setup LAStools directory and a function that runs LAStools through R
See the info from the input .las-file
f("lasinfo -i itd_demo.las")
## [1] "C:/HYapp/LAStools/bin/lasinfo -i itd_demo.las"
We see that the itd_demo.las has already been normalized. Next we create a pit free canopy height model (CHM). Here, we follow this workflow: https://rapidlasso.com/2014/11/04/rasterizing-perfect-canopy-height-models-from-lidar/
f("lasthin -i itd_demo.las -subcircle 0.1 -step 0.25 -highest -o tmp.laz")
f("las2dem -i tmp.laz -elevation -step 0.5 -kill 1.0 -o chm_00.asc")
f("las2dem -i tmp.laz -elevation -drop_z_below 2 -step 0.5 -kill 1.0 -o chm_02.asc")
f("las2dem -i tmp.laz -elevation -drop_z_below 5 -step 0.5 -kill 1.0 -o chm_05.asc")
f("las2dem -i tmp.laz -elevation -drop_z_below 10 -step 0.5 -kill 1.0 -o chm_10.asc")
f("las2dem -i tmp.laz -elevation -drop_z_below 15 -step 0.5 -kill 1.0 -o chm_15.asc")
f("las2dem -i tmp.laz -elevation -drop_z_below 20 -step 0.5 -kill 1.0 -o chm_20.asc")
# f("las2dem -i tmp.laz -elevation -drop_z_below 25 -step 0.5 -kill 1.0 -o chm_25.asc") # use more if needed
f("lasgrid -i chm*.asc -merged -step 0.5 -elevation -highest -o chm_pf.asc")
Load the result to R. (Set small marginals)
par(mar = c(0.1, 0.1, 0.1, 0.1))
(chm <- raster("chm_pf.asc"))
## class : RasterLayer
## dimensions : 416, 399, 165984 (nrow, ncol, ncell)
## resolution : 0.5, 0.5 (x, y)
## extent : 2516759, 2516959, 6857749, 6857957 (xmin, xmax, ymin, ymax)
## coord. ref. : NA
## data source : F:\FOR254\ITD_Demo\chm_pf.asc
## names : chm_pf
# Check
plot(chm)
Define a function which controls local maxima search window size Here, I define that the search window size (in meters) is one tenth of the tree height
diam <- function(tree_height){ tree_height * 0.1 }
h <- seq(2,36,2)
par(mai = rep(1,4))
plot(diam(h), h, type="l", xlab="window radius, m", main = "Window size for local maxima (treetop) detection")
Next we use ForestTools to locate the treetops and segment the crowns The ForestTools::mcws function uses the local maxima (treetops) as markers which controls the crown segmentation:
ttops <- vwf(chm, winFun = diam, minHeight = 5) # drop trees under 5 m
crowns <- mcws(ttops, chm, minHeight = 5, verbose = FALSE, format = "polygons")
See summary for the treetops and the crowns and write the result files:
sp_summarise(ttops)
## Value
## TreeCount 1129
sp_summarise(crowns, variables = c("height", "crownArea"))
## Value
## TreeCount 1129.000000
## heightMean 15.696422
## heightMedian 15.960000
## heightSD 2.737337
## heightMin 5.040000
## heightMax 22.309999
## crownAreaMean 11.275465
## crownAreaMedian 10.500000
## crownAreaSD 5.613104
## crownAreaMin 0.250000
## crownAreaMax 42.750000
writeOGR(ttops, getwd(), "treetops", driver = "ESRI Shapefile", overwrite_layer = T)
writeOGR(crowns, getwd(), "crowns", driver = "ESRI Shapefile", overwrite_layer = T)
Reference data consist of 1226 trees measured on-site. The main species in the area is pine.
Read the reference data and remove fallen trees, unfound and stump observations
(d <- read_csv2("reference.csv"))
## # A tibble: 1,226 x 13
## ID plaji plk elava x y z_foto h_foto d13 d6 h
## <chr> <int> <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 1 11 1 2.52e6 6.86e6 170. 16.2 212 17.3 NA
## 2 2 1 11 1 2.52e6 6.86e6 171. 17.8 187 15.2 NA
## 3 3 1 11 1 2.52e6 6.86e6 173. 19.6 257 21.1 NA
## 4 4 4 11 1 2.52e6 6.86e6 168. 15.0 125 10 14.5
## 5 5 1 11 1 2.52e6 6.86e6 168. 14.1 126 10.0 NA
## 6 6 2 11 1 2.52e6 6.86e6 164. 10.0 120 9.54 NA
## 7 7 4 11 1 2.52e6 6.86e6 167. 13.9 144 11.6 NA
## 8 8 2 11 1 2.52e6 6.86e6 166. 12.2 129 10.3 NA
## 9 9 1 12a1 1 2.52e6 6.86e6 171. 17.2 214 17.5 NA
## 10 10 1 11 1 2.52e6 6.86e6 174. 20.4 252 20.7 NA
## # ... with 1,216 more rows, and 2 more variables: hc <dbl>, h_est <dbl>
d$plaji <- as.factor(d$plaji) # convert factor variables to factors
d$plk <- as.factor(d$plk)
d <- subset(d, !(plk %in% c("23", "31", "41")))
Plot scatterplots & see correlations
chart.Correlation(d[,7:13])
Next we convert the field reference to a simple feature (spatial) format by using the sf package.
Note: The coordinate system is Finland Zone 2 (EPSG: 2392). The current Finnish coordinate system is EUREF FIN TM35 (EPSG: 3067), the user could transform the data with sf::st_transform function (not used here)
sd <- st_as_sf(d, coords = c("x","y")) %>% st_set_crs(2392) # %>% st_transform(3067))
Convert the treetops and crowns to simple feature format, and add an ID column for the crowns
ttops <- st_as_sf(ttops) %>% st_set_crs(2392) # add transform if wanted
crowns <- st_as_sf(crowns) %>% st_set_crs(2392)
crowns$crownID <- row.names(crowns) # id column
Compare the data sets
summary(crowns$height) # ITD
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 5.04 14.54 15.96 15.70 17.42 22.31
summary(d$h_est) # Field
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 6.28 15.05 16.36 16.27 17.70 22.64
Finally, join the two data sets ( see https://en.wikipedia.org/wiki/DE-9IM for further information)
st_join(crowns, sd, join = st_intersects) -> cr_to_ref # crowns intersecting with reference
st_join(sd, crowns, join = st_covered_by) -> ref_to_cr # reference trees covered by a crown
Calculate omission and comission statistics Compare the data sets
n_comission <- sum(is.na(cr_to_ref$d13)) # extra crowns that don't have any attributes
n_omission <- sum(is.na(ref_to_cr$height)) # reference trees that are not covered by a crown
Find out missed observations because the tree is under another canopy or the canopies are merged i.e. a segmented crown is containing more than one reference observation:
subset(cr_to_ref, !(is.na(d13))) -> cr_to_ref2 # drop comission trees from this
n_omission <- n_omission + nrow(cr_to_ref2)-nrow(crowns)
Outputs: 1. unique crowns with reference data 2. all crowns
itd_res <- subset(ref_to_cr, !(is.na(ref_to_cr$height)))
See results:
n_omission/nrow(ref_to_cr)*100
## [1] 7.610475
n_comission/nrow(ref_to_cr)*100
## [1] 10.72013
nrow(itd_res)/nrow(ref_to_cr)*100 # crowns which have at least one reference tree
## [1] 99.509
First estimate the diameter at breast height with a model. Model input is the height of the treetop. The model used is from Kalliovirta & Tokola 2005, http://www.metla.fi/silvafennica/full/sf39/sf392227.pdf
Estimate d13 with model nr. 2.1.1. The study reports a 0.64 correlation for tree-level data. Compare the results to reference data (itd_res)
mod211 <- function(height) {
d13 <- (-0.785 + 1.177*sqrt(height))^2
return(d13)
}
mod211(itd_res$height*10) -> itd_res$d13_est_211
mod211(ttops$height*10) -> ttops$d13_est_211
plot( x = itd_res$d13, y = itd_res$d13_est_211, xlab="Actual", ylab="Predicted", main = "Model 2.1.1")
cor(itd_res$d13, itd_res$d13_est_211, use = "complete.obs")
## [1] 0.5966722
We can see that the correlation is close to the correlation of the original study.
Next, prepare ITD tree data (ttops) for bucking. Note that the system separator must be “.” The program needs PlotID, TreeID, tree species, d13 and height in an ascii file This version of the script doesn’t have tree species identification included, but the data consist mainly of pine, thus we assume that all the trees are pines (species code = 1). R will open the program as a pop-up window. Close the program after executing it.
#ITD
out_data_itd <- ttops
st_geometry(out_data_itd) <- NULL
out_data_itd$plotID <- 1
out_data_itd$species <- 1
out_data_itd$d13_est_211 <- out_data_itd$d13_est_211/10 # convert to cm
out_data_itd <- dplyr::select(out_data_itd, c(plotID,treeID, species,d13_est_211, height))
head(out_data_itd)
## plotID treeID species d13_est_211 height
## 1 1 1 1 23.99394 19.12
## 2 1 2 1 22.71583 18.15
## 3 1 3 1 25.78867 20.48
## 4 1 4 1 24.33681 19.38
## 5 1 5 1 24.12580 19.22
## 6 1 6 1 24.71938 19.67
# Reference
out_data_ref <- d
out_data_ref$plotID <- 1
out_data_ref$plaji <- as.numeric(out_data_ref$plaji)
out_data_ref$plaji[is.na(out_data_ref$plaji)] <- 1 # pine by default
out_data_ref$ID <- seq(1,nrow(out_data_ref),1) # orig_id has characters, program requires numeric ID
out_data_ref$d13 <- out_data_ref$d13/10 # convert to cm
out_data_ref <- dplyr::select(out_data_ref, c(plotID, ID, plaji, d13, h_est))
head(out_data_ref) # names are different but variables are the same
## # A tibble: 6 x 5
## plotID ID plaji d13 h_est
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 1 1 21.2 16.2
## 2 1 2 1 18.7 17.8
## 3 1 3 1 25.7 19.6
## 4 1 4 4 12.5 14.5
## 5 1 5 1 12.6 14.1
## 6 1 6 2 12 10.0
Create a directory, note that it must be in C:/data, Then write the output data and execute the bucking program for it, and read the results back to R. Repeat the bucking for both, ITD and reference files: [File -> open -> *executes program -> close program]
if(dir.exists("C:/data/") == F) { dir.create("C:/data/")}
# ITD
write.table(out_data_itd, "C:/data/in_trees.txt", dec=".", sep = ",", row.names = F, col.names = F)
# Run bucking
system("MARV42polkky/MARV42Polkyttaja.exe",wait=T)
results_itd <- read_csv("C:/data/out_trees.txt")
rename(results_itd, treeID = TreeID) %>%
left_join(ttops, ., by ="treeID") -> results_itd
# Reference
write.table(out_data_ref, "C:/data/in_trees.txt", dec=".", sep = ",", row.names = F, col.names = F)
# Run bucking
system("MARV42polkky/MARV42Polkyttaja.exe",wait=T)
results_ref <- read_csv("C:/data/out_trees.txt")
Join the itd-data to crown segments
st_join(crowns,results_itd, join = st_intersects) -> itd_res2
Finally, plot some of the results
ggplot() +
geom_sf(data = itd_res2, aes(fill = SawVol)) +
scale_fill_distiller(palette = "Greens", direction = 1, breaks = seq(0,700,100))+
ggtitle("Sawlog volume")+
labs(fill="litres")+
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks = element_blank(),
rect = element_blank())
ggplot() +
geom_sf(data = itd_res2, aes(fill = PulpVol)) +
scale_fill_distiller(palette = "Greens", direction = 1, limits = c(0,250), breaks = seq(0,250,50))+
ggtitle("Pulpwood volume")+
labs(fill="litres")+
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks = element_blank(),
rect = element_blank())
ggplot() +
geom_sf(data = itd_res2, aes(fill = ToTVol)) +
scale_fill_distiller(palette = "Greens", direction = 1, limits = c(0,700), breaks = seq(0,700,100))+
ggtitle("Total volume")+
labs(fill="litres")+
theme(plot.title = element_text(hjust = 0.5),
axis.text.x = element_blank(),
axis.text.y = element_blank(),
axis.ticks = element_blank(),
rect = element_blank())
Lastly, compare bucking results
res_mat <- as.data.frame(matrix(NA,3,4))
colnames(res_mat) <- c("Sawlog volume", "Pulp volume", "Residue volume", "Total volume")
rownames(res_mat) <- c("Reference", "ITD", "Difference")
res_mat[1,1]<- sum(results_ref$SawVol)/1000 #m3
res_mat[1,2]<- sum(results_ref$PulpVol)/1000 #m3
res_mat[1,3]<- sum(results_ref$TopVol)/1000 #m3
res_mat[2,1]<- sum(results_itd$SawVol)/1000 #m3
res_mat[2,2]<- sum(results_itd$PulpVol)/1000 #m3
res_mat[2,3]<- sum(results_itd$TopVol)/1000 #m3
res_mat[1,4] <- sum(res_mat[1,1:3])
res_mat[2,4] <- sum(res_mat[2,1:3])
res_mat[3,1] <- res_mat[1,1]-res_mat[2,1]
res_mat[3,2] <- res_mat[1,2]-res_mat[2,2]
res_mat[3,3] <- res_mat[1,3]-res_mat[2,3]
res_mat[3,4] <- res_mat[1,4]-res_mat[2,4]
area_size <- 2.165 # ha (field plot size 132 m x 164 m, dem cropped approximately to same size, might be small differences)
res_mat # absolute m3
## Sawlog volume Pulp volume Residue volume Total volume
## Reference 131.4683 159.8456 5.8000 297.1139
## ITD 140.5440 136.6579 5.0555 282.2574
## Difference -9.0757 23.1877 0.7445 14.8565
res_mat/area_size # m3 / ha
## Sawlog volume Pulp volume Residue volume Total volume
## Reference 60.724388 73.83169 2.6789838 137.235058
## ITD 64.916397 63.12143 2.3351039 130.372933
## Difference -4.192009 10.71025 0.3438799 6.862125
We can see that ITD gives a slight overestimate of sawlog volume, but underestimates the pulpwood and total volume. This can be explained by small omission trees, whereas larger trees are well detected.