librerías usadas
library(ForestTools)
library(BIOMASS)
library(ggplot2)
library(readxl)
library(scales)
library(raster)
library(dplyr)
library(tidyr)
library(terra)
library(dplyr)
library(tmap)
library(sf)
library(ForestTools)
library(BIOMASS)
library(ggplot2)
library(readxl)
library(scales)
library(raster)
library(dplyr)
library(tidyr)
library(terra)
library(dplyr)
library(tmap)
library(sf)
Presentamos una metodología de 3 componentes: fotogrametría, dendrometría en campo y modelos alométricos, para estimar biomasa en parcelas agrícolas, específicamente del tipo de Milpa Intercalada con Árboles Frutales (MIAF). La metodología consiste en los siguientes pasos:
En campo:
En gabinete:
DAP ~ altura
a los datos tomados en campo.BIOMASS::computeAGB
le suministra la altura de la cúspide del Modelo de Altura de Dosel, el diámetro estimado con el modelo lineal y la especie de árbol: el paquete BIOMASS tiene a la fecha 16,467 valores de densidad de maderas para distintas especies, incluyendo las monitoreadas en este proyecto (carambolo, mango, chico zapote, durazno y manzano).Para este proceso utilizamos la aplicación para línea de comandos OpenDroneMap, la cual corre en un contenedor Docker. En el siguiente bloque de código se muestra cómo instalar Docker para Ubuntu. (para derivativos de Ubuntu cambiar $VERSION_CODENAME por $UBUNTU_CODENAME)
# Para instalar docker
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
# instalar los paquetes de docker:
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# probamos la instalación
sudo docker run hello-world
Teniendo la instalación de Docker ahora podemos correr OpenDroneMap. Lo más sencillo para usarlo es crear una carpeta con el nombre de nuestro sitio y adentro de este una carpeta llamada images- que tenga las imágenes del vuelo. Una vez lista la carpeta con las imágenes nos posicionamos en la carpeta con el nombre de nuestro sitio podemos ejecutae el comando odm en su forma más elemental:
usuario@computadora:~/sitio/images$ docker run -ti --rm -v /my/project:$(pwd) opendronemap/odm --project-path /datasets
Para el caso de modelos de elevación, frecuentemente debemos de ajustar el comando con los siguientes parámetros:
--use-fixed-camera-params
Para evitar efectos de “bowling” en el terreno. Este parámetro no lleva más argumentos.--pc-rectify
Hacer rectificación del suelo en la nube de puntos. Esto busca que los puntos de suelo clasificados erróneamente serán re-clasificados, rellenándose así los vacíos. Este parámetro no lleva más argumentos.--smrf-scalar
Valor de escalamiento: incrementar este parámetro para sitios con mucha variación de alturas. Default: 1.25--smrf-slope
Parámetro de tolerancia: una medida de la “tolerancia a pendientes”. Incrementar este parámetro para terrenos con mucha variación de alturas. Entre 0.1 y 1.2. Default: 0.15--smrf-threshold
Umbral de elevación. Ajustar este parámetro a la altura mínima, en metros, que esperamos para los objetos para diferenciarlos del terreno. Default: 0.5--smrf-window
Parámetro de radio de ventana, en metros, que corresponde al tamaño del objeto más grange (edificio, árboles, etc.) que removeremos de ser considerado como suelo. Debe ser un valor mayor a 10. Default: 18.Para generar el MAD, el primer paso es restar los modelos de superficie y de terreno (Figura 1). En el código siguiente además lo recortamos a un área específica, trazada sobre el MIAF, eliminando el resto de la imagen que no nos interesa, y usamos la función terra::resample
para reducir la resolución y hacer más rápidos los cálculos subsecuentes.
= read_sf("ejemplo/polys_miaf.gpkg") |>
fincas st_zm() |> vect() |> project("epsg:32614")
= 1
i = rast("ejemplo/odm_orthophoto.tif")
orto = rast("ejemplo/dtm.tif")
dtm = rast("ejemplo/dsm.tif")
dsm
= terra::crop(dsm, fincas[i,]) |> terra::mask(fincas[i,])
dsm = terra::crop(dtm, fincas[i,]) |> terra::mask(fincas[i,])
dtm = dsm - dtm
c_height
= rast(ext(c_height), resolution = 0.14, crs = crs(c_height))
ea_5 = resample(c_height, ea_5)
ea_5 = resample(orto, ea_5)
orto = raster(ea_5) # foresttools no usa terra, sino raster
ea_5
plot(ea_5)
Para hacer un modelo de altura de dosel efectivo tenemos que ajustar los parámetros de la función de la ventana móvil (winFun
) y la altura mínima (minHeight
) de la función filtro de ventana variable vwf
. De acuerdo a la altura y tamaños de copa de nuestros frutales. Por lo general, a mayor altura usaremos ventanas más amplias y alturas mínimas (minHeight
) más grandes. Este es un ejemplo Tabla 1 de distintas parcelas.
Zona | Frutal | Función |
---|---|---|
Huejotzingo | Manzana y durazno | lin <- function(x){x * 0.05 + .4} |
ttops <- vwf(CHM = ea_5, winFun = lin, minHeight = 0.35) |
||
Xigüipilincan | Mango | lin <- function(x){x * 0.05 + 1} |
ttops <- vwf(CHM = ea_5, winFun = lin, minHeight = 0.5) |
||
Axochío | Carambolo | lin <- function(x){x * 0.05 + .8} |
ttops <- vwf(CHM = ea_5, winFun = lin, minHeight = 0.5) |
||
Axochío | Chico zapote | lin <- function(x){x * 0.05 + .9} |
ttops <- vwf(CHM = ea_5, winFun = lin, minHeight = 0.5) |
Ahora correremos las funciones de ForestTools::vwf
(filtro de ancho de ventana variable) para detectar las cúspides de los árboles y el ForestTools:mcws
para detectar el área de copas el cual es un algoritmo parecido a los de delimitación de cuencas, pero invertido. El resultado de las cúspides y demarcaciónes de copas está en Figura 2. Podemos ver que el modelo funcionó correctamente porque están en la división de las copas se puede ver el corte en tatura de la parcela MIAF.
<- function(x){x * 0.05 + .4}
lin <- vwf(CHM = ea_5, winFun = lin, minHeight = 0.35)
ttops <- mcws(treetops = ttops, CHM = ea_5, format = "polygons",
crownsPoly minHeight = 0.35) |> vect() #, verbose = FALSE)
$layer = paste("Cúspides, total:", nrow(ttops))
ttops$layer = paste("Copas, área total:", comma( sum( round( expanse( crownsPoly ) ,2))), " m²")
crownsPoly
=
mapa_rgb tm_layout(main.title = sprintf("Modelo de altura de dosel, Finca %s", fincas$sitio[i]),
main.title.size = 1.2,
title = paste0("No. cúspides: ", nrow(crownsPoly), "\n",
"Cobertura arbórea: ", round( sum(expanse(crownsPoly) / expanse(fincas[i,]), 2 )),
"%\n",
"Área predio: ", round(expanse(fincas[i,])/10000,1), " ha" )
+
) tm_shape(orto, unit = "m") +
tm_rgb() +
tm_shape(ttops) +
tm_symbols(col = "height", size = .1,
palette = viridis::viridis(8), title.col = "Altura, m",
border.col = "black", border.lwd = .8, legend.hist = F,
legend.col.reverse = T, legend.format = list(text.separator = "a")) +
tm_shape(ttops) +
tm_symbols(col = "height", size = .1,
palette = viridis::viridis(8), title.col = "Altura, m", legend.col.show = F,
border.col = "black", border.lwd = .8, legend.hist = T,
legend.hist.title = "Frec. alturas, m",
legend.col.reverse = F, legend.format = list(text.separator = "a")) +
tm_shape(st_as_sf(crownsPoly)) +
tm_polygons(col = "layer", alpha = 0, border.col = "black", legend.show = T, title = "") +
tm_layout(legend.outside = T, legend.hist.width = 4 ) +
tm_scale_bar(text.color = "black", text.size = .8, breaks = c(0,10,50), position = c(0.04, 0.01),
bg.color = "white") +
tm_graticules(projection = 32614, labels.cardinal = F, n.x = 4, n.y = 4,
labels.format = list(fun = function(x) x), labels.rot = c(0,90), col = "gray70",
lwd = .7)
mapa_rgb
Ahora cargamos un archivo con las lecturas de circunferencia (la cual transformaremos a diámetro) y altura, con esto obtenemos un modelo lineal, ya sea generalizado o estándar (funciones glm
y lm
de R
) para cada frutal (Figura 3), el cual ajustaremos al objeto de cúspides (ttops
) para estimar un diámetro para cada cúspide.
= c("Averrhoa", "Manilkara", "Prunus", "Mangifera", "Malus")
genero = c("carambola", "zapota", "persica", "indica", "domestica")
especie names(genero) = c("Carambola", "Chico zapote", "Durazno", "Mango", "Manzana")
names(especie) = names(genero)
= read_excel("ejemplo/mediciones_arbol_fruto.xlsx", sheet = "Mediciones") |>
df ::select(1:6) |> dplyr::filter(!if_all(.cols = everything(), .fns = is.na)) |>
dplyrfill(c(parcela, fruta, sitio), .direction = "down")
$diametro_cm = df$diametro_cm/pi
df
|> ggplot(aes(diametro_cm, altura_m)) +
df geom_point(pch = 17) +
geom_smooth(method = "lm", se = FALSE, color = 1, lty = "12", lwd = 0.3) +
facet_wrap(~fruta, scales = "free") + theme_minimal() + theme(panel.background = element_rect(fill = "cornsilk"))
= ttops |> st_transform(4326)
cuspides
= df[df$parcela == sprintf("Parcela %s", fincas$unidad[i]) & df$fruta == fincas$frutal[i],]
temp = glm(diametro_cm ~ altura_m, data = temp)
modelo
AIC(modelo) # the smaller the AIC or BIC, the better the fit {help(AIC)}
[1] 48.42084
# para comparar:
# lm1 <- lm(Fertility ~ . , data = swiss); AIC(lm1)
# nos da AIC: 326
# necesitamos un ajuste por la variación de lo que mide el personal en campo y lo que registra el dron
= ttops #st_filter(cuspides, parcelas[i,])
cuspides_parcela_fruta = if( max(temp$altura_m) - max(cuspides_parcela_fruta$height)< 0){
ajuste 0} else { max(temp$altura_m) - max(cuspides_parcela_fruta$height)}
= predict.glm(modelo, data.frame(altura_m = cuspides_parcela_fruta$height + ajuste), type = "response")
diametros
= BIOMASS::computeAGB(D = diametros, H = cuspides_parcela_fruta$height + ajuste,
biomasa WD = rep(getWoodDensity(genero[[fincas$frutal[i]]], especie[[fincas$frutal[i]]],
region = "NorthAmerica")$meanWD,
length(diametros) ))
# Toneladas
par(mar=c(5, 4, 4, 8), bg = "cornsilk", xpd = FALSE) # xpd es para que imprima fuera del margen, se activa en legend
with(temp,
plot(diametro_cm, altura_m,
{main = sprintf("Parcela %s, %s, %s", fincas$unidad[i], fincas$sitio[i], fincas$frutal[i]),
sub = paste("Ajuste del modelo lineal; AIC:",
round( AIC(modelo),1) )) }
)
lines(diametros, cuspides_parcela_fruta$height + ajuste)
legend( "bottomleft", cex = 0.8,y.intersp = 1.3, xpd = NA,
c("Mediciones\nen campo.", "Modelo lineal\nde dron."), pch = c(1, NA), lty = c(NA, 1))
mtext(paste("Biomasa:\n", round(sum(biomasa),1), "tons\n",
"Superficie:\n", round(expanse(fincas[i,]),1), "m²"), cex = 0.8, side = 4, line = 2, las = 1)
Nuesto modelo lineal tiene un AIC de 48.4, lo cual es un buen ajust. La estimación para esta parcela nos da 4.9 toneladas de biomasa de los árboles de manzana, la cual tiene una densidad de madera promedio de 0.6017 g/cm³, según el paquete BIOMASS
para la región de Norteamérica.
El método mixto de dendrometría manual, vuelo de dron y modelos alométricos resultó ser útil para la estimación de biomasa en 5 cultivos distintos en dos regiones de México (en este ejercicio sólo presentamos un frutal de una región). Hemos presentado un método plenamente replicable que hace uso exclusivamente de software libre, que permite la estimación para extensiones de varias hectáreas con una sola visita al sitio. Por otra parte, la metodología requiere un mínimo capacidades técnicas del personal: realizar vuelos programados, generar ortomosaicos, ajustar parámetros para el modelo de altura de dosel y leer y reemplazar valores en un archivo de R; estas capacidades son alcanzables y este manual ayuda en cada parte del proceso, a excepción de hacer vuelos programados.