library("sf")
library("terra")
library("lidR")
library("raster")
library("dbscan")
library("tidyverse")
library("leaflet")
library("RColorBrewer")
library("rgl")
Letchworth Learning Forest Structure Modeling Utilizing LiDAR Point Cloud Data
Understanding forest structure and characteristics in Letchworth Learning Forest
Introduction
[~ 200 words]
Observing forest structure is beneficial in understanding various forest characteristics. These characteristics include biodiversity, ecosystem disturbances and possibly any threats that may affect the forest structure and ecosystem. On-site forest modeling and monitoring is difficult to preserve long scale due to resource limitations. Utilizing remote sensing tools can allow researchers and foresters to overcome time, labor, and funding restrictions. In this project, I utilized remote sensing data to look at the Letchworth Learning Forest structure and try to interpret forest characteristics remotely.
Materials and methods
[~ 200 words]
In this project, I utilized 2019 remote sensing LiDAR point-cloud data to measure vertical forest structure within the Letchworth Learning Forest near the Ellicot Complex. By looking at vertical structure, I was able to find forest characteristics such as spatial clustering of the tallest (90%) woody individuals, approximately how many trees are residing in the study area, and tree height distributions.
The spatial data was mapped with Leaflet and distributions were visualized using ggplot
The main r packages that were utilized were lidR, terra, and raster.
Here’s my current on-going code.
Load Nessassary Packages
Load USGS Lidar Point Cloud Data of Letchworth Learning Forest
<- readLAS("data/Letchworth_Teaching_Forest_Lidar.laz")
las
## Look at the parameter coordinates of original data
<- st_bbox(las)
original_bbox original_bbox
xmin ymin xmax ymax
1387000 2349000 1388000 2350000
Create New Bounding Box Filter Out Non-Forest
<- matrix(c(
polygon_coords 1388000, 2349355, #right bottom corner
1388000, 2349700, #right top corner
1387450, 2349700, #mid top corner
1387450, 2349450, #mid mid corner
1387300, 2349200, #left bottom corner
1387350, 2349200, #mid bottom corner
1388000, 2349355 #right bottom corner
ncol = 2, byrow = TRUE)
),
## Bounding Box has been given CRS associated with las-CRS
<- st_sfc(st_polygon(list(polygon_coords)), crs = st_crs(las)) polygon_sf
<- lidR::clip_roi(las, polygon_sf)
las_clipped
## Check if the crs is still the same
st_crs(polygon_sf) == st_crs(las)
## Filter out any OUTliers z-values from las
<- filter_poi(las_clipped, Z >= 180, Z <= 220)
filterOUT_las
# rglwidget(x = scene3d(filterOUT_las), webgl= TRUE)
Check If New Data Will Correctly Transform to Leaflet Projection (The original data uses NAD 83 (2011) and Leaflet uses WGS 84)
## Reproject the las to 4326 for Leaflet
options(digits = 15)
<- st_as_sf(las_clipped, coords = c("X", "Y"), crs = 26918) # crs was NAD83(2011)
las84 <- st_transform(las_clipped, crs = 4326) las84
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Error 1: PROJ: vgridshift: could not find required grid(s).
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Error 1: PROJ: pipeline: Pipeline: Bad step definition: proj=vgridshift
(File not found or invalid)
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Error 1: PROJ: vgridshift: could not find required grid(s).
Warning in CPL_transform(x, crs, aoi, pipeline, reverse, desired_accuracy, :
GDAL Error 1: PROJ: pipeline: Pipeline: Bad step definition: proj=vgridshift
(File not found or invalid)
## Create a dataframe of points lat and long to check if st_transform worked
<- data.frame(lat = st_coordinates(las84)[, 2],
las_df long = st_coordinates(las84)[, 1])
unique(las_df$lat)
[1] 43.005 43.006 43.007 43.009 43.008
unique(las_df$long)
[1] -78.797 -78.796 -78.795 -78.794 -78.793 -78.792 -78.791 -78.790 -78.789
[10] -78.788 -78.787
Rasterize LiDAR Data to Identify Tree Tops, Identify the Tree Population, Create a Data Set of the 90th Percentile of Trees Based On Height (Tallest Trees) (Height is in Meters)
## LidR functions to find individual trees
<- rasterize_canopy(filterOUT_las, 0.25, pitfree(subcircle = 1))
chm
<- locate_trees(chm, lmf (ws=5))
tree_tops <- tree_tops %>%
filtered_tree_tops filter(Z >= 185)
#plot(chm, col = height.colors(50))
#plot(sf::st_geometry(filtered_tree_tops), pch = 3)
#plot(sf::st_geometry(filtered_tree_tops), add = TRUE, pch = 3)
<- quantile(filtered_tree_tops$Z, 0.90)
nintypercent <- filtered_tree_tops[filtered_tree_tops$Z > nintypercent,]
tall_trees
<- st_coordinates(tall_trees) coords
The Tree Height Distribution within the Forest
<- as.data.frame(filtered_tree_tops$Z)
tree_Z colnames(tree_Z) <- c("height")
<- tree_Z$height - 180
test <- as.data.frame(test)
test max(test)
[1] 33.901
<- as.data.frame(tall_trees$Z - 180)
tall_trees_height colnames(tall_trees_height) <- c("height")
## in meters
ggplot()+
geom_histogram(data = test, aes(x = test), binwidth = 0.5,, color = "black", fill="green", alpha = 0.25)+
geom_histogram(data = tall_trees_height, aes(x = height), binwidth = 0.5,, color = "black", fill="red", alpha = .5)+
geom_vline(aes(xintercept = mean(test$test)), color="blue", linetype="dashed", linewidth=1)+
theme_classic()+
labs(title = "Distribution of Tree Heights in Meters", x = "Height (m)", y = "Frequency",
subtitle = "Mean of all heights (Blue) \n90% of tallest trees (Red)",
caption = "Letchworth Teaching Forest (2019)")
Locate the Tallest Trees, and Tree Tops within the Forest
::opts_chunk$set(global.device = TRUE)
knitr
plot(chm, main = "Tree Tops")
plot(filtered_tree_tops[2],pch = 16, cex = .5, main = "tree_tops", add = TRUE)
plot(chm, main = "Tall Trees")
plot(tall_trees, pch = 16, cex = .5, main = "tall_trees", add = TRUE)
Locate Spatial Clustering of Tall Trees
::opts_chunk$set(global.device = TRUE)
knitr
<- dbscan(coords, eps = 12, minPts = 5)
dbscan_result
$dbscan_cluster <- as.factor(dbscan_result$cluster)
tall_trees= length (unique (tall_trees$dbscan_cluster))
colorsize
## Filter out first cluster
<- tall_trees %>%
cluster filter(dbscan_cluster != 0)
plot(chm)
plot(cluster[3], pch = 16, cex = .5, col = factor(cluster$dbscan_cluster), main = "Tree Clusters", add = TRUE)
Add Locations of Spatail Clustering to Leaflet Map
<- colorFactor(brewer.pal(4, "Set1"), domain = tall_trees$dbscan_cluster)
pal
<- st_transform(tall_trees, crs = 4326)
st_tall_trees <- st_tall_trees %>%
st_tall_trees_filtered filter(dbscan_cluster != 0)
$Z <- st_tall_trees_filtered$Z - 180
st_tall_trees_filtered
leaflet (st_tall_trees_filtered) %>%
setView(lng = -78.793, lat = 43.007, zoom = 13) %>%
addTiles() %>%
addCircleMarkers(
radius = 2,
color = ~pal(dbscan_cluster),
popup = ~paste("Tree ID:", treeID, "<br> Height (m):", Z))
Load any required packages in a code chunk (you may need to install some packages):
install.packages("sf")
install.packages("terra")
install.packages("lidR")
install.packages("raster")
install.packages("dbscan")
install.packages("tidyverse")
install.packages("leaflet")
install.packages("RColorBrewer")
install.packages("knitr")
install.packages("rgl")
Conclusions
[~200 words] apa format Clear summary adequately describing the results and putting them in context. Discussion of further questions and ways to continue investigation.
References
All sources are cited in a consistent manner