8  Remote sensing

Author
Affiliation

Ivan Alberto Lizarazo

Universidad Nacional de Colombia

Last updated

October 9, 2026

8.1 Introduction

Remote sensing techniques, in the sense of gathering & processing of data by a device separated from the object under study, are increasingly providing an important component of the set of technologies available for the study of vegetation systems and their functioning. This is despite the fact that many applications only provide indirect estimations of the biophysical variables of interest (Jones and Vaughan 2010).

Particular advantages of remote sensing for vegetation studies are that: (i) it is non-contact and non-destructive; and (ii) observations are easily extrapolated to larger scales. Even at the plant scale, remotely sensed imagery is advantageous as it allows rapid sampling of large number of plants (Jones and Vaughan 2010).

This chapter aims at providing a conceptual & practical approach to apply remote sensing data and techniques to infer information useful for monitoring crop diseases. The structure of this chapter is divided into four sections. The first one introduces basic remote sensing concepts and provides a summary of applications of remote sensing of crop diseases. The second one illustrates a case study focused on identification of banana Fusarium wilt from multispectral UAV imagery. The third one illustrates a case study dealing with estimation of cercospora leaf spot disease on table beet. Finally, it concludes with several reflections about potential and limitations of this technology.

8.2 Remote sensing background

8.2.1 Optical remote sensing

Optical remote sensing makes use of the radiation reflected by a surface in the visible (~400-700 nm), the near infrared (700-1300 nm) and shortwave infrared (1300-~3000 nm) parts of the electromagnetic spectrum. Spaceborne & airborne-based remote sensing and field spectroscopy utilize the solar radiation as an illumination source. Lab spectroscopy utilizes a lamp as an artificial illumination source (Figure 8.1).

Figure 8.1: Optical remote sensing via spaceborne sensors, field spectroscopy and laboratory spectroscopy (Adapted from https://pages.cms.hu-berlin.de/EOL/geo_rs/index.htm) .

The proportion of the radiation reflected by a surface depends on the surface’s spectral reflection, absorption and transmission properties and varies with wavelength (Figure 8.2). These spectral properties in turn depend on the surface’s physical and chemical constituents (Figure 8.2). Measuring the reflected radiation hence allows us to draw conclusions on a surface’s characteristic, which is the basic principle behind optical remote sensing.

Figure 8.2: Reflection, absorption and transmission by a surface (left). Spectral reflectance profile of a vegetation with major factors determining the reflection (right). Source: https://pages.cms.hu-berlin.de/EOL/geo_rs/

8.2.2 Vegetation spectral properties

Optical remote sensing enables the deduction of various vegetation-related characteristics, including biochemical properties (e.g., pigments, water content), structural properties (e.g., leaf area index (LAI), biomass) or process properties (e.g., light use efficiency (LUE)). The ability to deduce these characteristics depends on the ability of a sensor to resolve vegetation spectra. Hyperspectral sensors capture spectral information in hundreds of narrow and contiguous bands in the VIS, NIR and SWIR, and, thus, resolve subtle absorption features caused by specific vegetation constituents (e.g. anthocyanins, carotenoids, lignin, cellulose, proteins). In contrast, multispectral sensors capture spectral information in a few broad spectral bands and, thus, only resolve broader spectral features. Still, multispectral systems like Sentinel-2 have been demonstrated to be useful to derive valuable vegetation properties (e.g., LAI, chlorophyll).

Figure 8.3: Vegetation spectrum in hyperspectral (ASD FieldSpec4, EnMAP) and multispectral (Sentinel-2) resolution as well as characteristic spectral features caused by various constituents and processes (absorption lines shown as grey dashed lines). Source: (Hank et al. 2018)

8.2.3 What does a remote sensor measure?

Optical sensors/spectrometers measure the radiation reflected by a surface to a certain solid angle in the physical quantity radiance. The unit of radiance is watts per square meter per steradian (W • m-2 • sr-1) (Figure 8.4). In other words, radiance describes the amount of energy (W) that is reflected from a surface (m-2) and arrives at the sensor in a three-dimensional angle (sr-1).

Figure 8.4: Source: https://pages.cms.hu-berlin.de/EOL/geo_rs/

A general problem related to the use of radiance as unit of measurement is the variation of radiance values with illumination. For example, the absolute incoming solar radiation varies over the course of the day as a function of the relative position between sun and surface and so does the absolute amount of radiance measured. We can only compare measurements taken a few hours apart or on different dates when we are putting the measured radiance in relation to the incoming illumination.

The quotient between measured reflected radiance and measured incoming radiance (Radiancereflected / Radianceincoming) is called reflectance (usually denoted as \(\rho\)). Reflectance provides a stable unit of measurement which is independent from illumination and is the percentage of the total measurable radiation, which has not been absorbed or transmitted.

8.2.4 Hyperspectral vs. multispectral imagery

Hyperspectral imaging involves capturing and analyzing data from a large number of narrow, contiguous bands across the electromagnetic spectrum, resulting in a high-resolution spectrum for each pixel in the image. As a result, a hyperspectral camera provides smooth spectra. The spectra provided by multispectral cameras are more like stairs or saw teeth without the ability to depict acute spectral signatures (Figure 8.6).

8.2.5 Vegetation Indices

A vegetation index (VI) represents a spectral transformation of two or more bands of spectral imagery into a single-band image. A VI is designed to enhance the vegetation signal with regard to different vegetation properties, while minimizing confounding factors such as soil background reflectance, directional, or atmospheric effects. There are many different VIs, including multispectral broadband indices as well as hyperspectral narrowband indices.

Most of the multispectral broadband indices make use of the inverse relationship between the lower reflectance in the red (through chlorophyll absorption) and higher reflectance in the near-infrared (through leaf structure) to provide a measure of greenness that can be indirectly related to biochemical or structural vegetation properties (e.g., chlorophyll content, LAI). The Normalized Difference Vegetation Index (NDVI) is one of the most commonly used broadband VIs:

\[NDVI = \frac{\rho_{nir} - \rho_{red} }{\rho_{nir} + \rho_{red}}\]

The interpretation of the absolute value of the NDVI is highly informative, as it allows the immediate recognition of the areas of the farm or field that have problems. The NDVI is a simple index to interpret: its values vary between -1 and 1, and each value corresponds to a different agronomic situation, regardless of the crop (Figure 8.5)

Figure 8.5: Agronomic conditions depending on the values in a NDVI scale

8.3 Remote sensing of crop diseases

8.3.1 Detection of plant stress

One popular use of remote sensing is in diagnosis and monitoring of plant responses to biotic (i.e. disease and insect damage) and abiotic stress (e.g. water stress, heat, high light, pollutants) with hundreds of publications on the topic. It is worth noting that most available techniques monitor the plant response rather than the stress itself. For example, with some diseases, it is common to estimate changes in canopy cover (using vegetation indices) as measures of “disease” but this measure could also be associated to water deficit (Jones and Vaughan 2010). This highlights the importance of measuring crop conditions in the field & laboratory to collect reliable data and be able to disentangle complex plant responses. Anyway, remote sensing can be used as the first step in site-specific disease control and also to phenotype the reactions of plant genotypes to pathogen attack (Lowe et al. 2017).

8.3.2 Optical methods for measuring crop disease

There are a variety of optical sensors for the assessment of plant diseases. Sensors can be based only on the visible spectrum (400-700 nm) or on the visible and/or infrared spectrum (700 nm - 1mm). The latter may include near-infrared (NIR) (0.75-1.4 \(μm\)), short wavelength infrared (SWIR) (1.4–3 \(μm\)), medium wavelength infrared (MWIR) (3-8 \(μm\)), or thermal infrared (8-15 \(μm\)) (Figure 8.6). Sensors record either imaging or non imaging (i.e average) spectral radiance values which need to be converted to reflectance before conducting any crop disease monitoring task.

Figure 8.6: source: Del Ponte et al. (2024)

In a recent chapter of Agrios’ Plant Pathology, Del Ponte et al. (2024) highlights the importance of understanding the basic principles of the interaction of light with plant tissue or the plant canopy as a crucial prerequisite for the analysis and interpretation for disease assessment. When a plant is infected, there are changes to the physiology and biochemistry of the host, with the eventual development of disease symptoms and/or signs of the pathogen which may be accompanied by structural and biochemical changes that affect absorbance, transmittance, and reflectance of light (Figure 8.7).

Figure 8.7: source: Del Ponte et al. (2024)

8.3.3 Scopes of disease sensing

The quantification of typical disease symptoms (disease severity) and assessment of leaves infected by several pathogens are relatively simple for imaging systems but may become a challenge for nonimaging sensors and sensors with inadequate spatial resolution (Oerke 2020). Systematic monitoring of a crop by remote sensors can allow farmers to take preventive actions if infections are detected early.  Remote sensing sensors & processing techniques need to be carefully selected to be capable of (a) detecting a deviation in the crop’s health status brought about by pathogens, (b) identifying the disease, and (c) quantifying the severity of the disease.  Remote sensing can also be effectively used in (d) food quality control (Figure 8.8).

Figure 8.8: Source: (Oerke 2020)

8.3.4 Monitoring plant diseases

Sensing of plants for precision disease control is done in large fields or greenhouses where the aim is to detect the occurrence of diseases at the early stages of epidemics, i.e., at low symptom frequency. Lowe et al. (2017) reviewed hyperspectral imaging of plant diseases, focusing on early detection of diseases for crop monitoring. They report several analysis techniques successfully used for the detection of biotic and abiotic stresses with reported levels of accuracy higher than 80%.

Statistical techniques used to detect both biotic and abiotic stresses in crops. Source: Lowe et al. (2017)
Technique Plant (stress)
Quadratic discriminant analysis (QDA) Wheat (yellow rust)
Avocado (laurel wilt)
Decision tree (DT) Avocado (laurel wilt)
Sugarbeet (cercospora leaf spot)
Sugarbeet (powdery mildew)
Sugarbeet (leaf rust)
Multilayer perceptron (MLP) Wheat (yellow rust)
Partial least squares regression (PLSR) Celery (sclerotinia rot)
Raw
Savitzky-Golay 1st derivative
Savitzky-Golay 2nd derivative
Partial least squares regression (PLSR) Wheat (yellow rust)
Fisher’s linear discriminant analysis Wheat (aphid)
Wheat (powdery mildew)
Wheat (powdery mildew)
Erosion and dilation Cucumber (downy mildew)
Spectral angle mapper (SAM) Sugarbeet (cercospora leaf spot)
Sugarbeet (powdery mildew)
Sugarbeet (leaf rust)
Wheat (head blight)
Artificial neural network (ANN) Sugarbeet (cercospora leaf spot)
Sugarbeet (powdery mildew)
Sugarbeet (leaf rust)
Support vector machine (SVM) Sugarbeet (cercospora leaf spot)
Sugarbeet (powdery mildew)
Sugarbeet (leaf rust)
Barley (drought)
Spectral information divergence (SID) Grapefruit
(canker, greasy spot, insect
damage, scab, wind scar)

Lowe et al. (2017) state that remote sensing of diseases under production conditions is challenging because of variable environmental factors and crop-intrinsic characteristics, e.g., 3D architecture, various growth stages, variety of diseases that may occur simultaneously, and the high sensitivity required to reliably perceive low disease levels suitable for decision-making in disease control. The use of less sensitive systems may be restricted to the assessment of crop damage and yield losses due to diseases.

8.3.5 UAV applications for plant disease detection and monitoring

Kouadio et al. (2023) undertook a systematic quantitative literature review to summarize existing literature in UAV-based applications for plant disease detection and monitoring. Results reveal a global disparity in research on the topic, with Asian countries being the top contributing countries. World regions such as Oceania and Africa exhibit comparatively lesser representation. To date, research has largely focused on diseases affecting wheat, sugar beet, potato, maize, and grapevine (Figure 8.9). Multispectral, red-green-blue, and hyperspectral sensors were most often used to detect and identify disease symptoms, with current trends pointing to approaches integrating multiple sensors and the use of machine learning and deep learning techniques. The authors suggest that future research should prioritize (i) development of cost-effective and user-friendly UAVs, (ii) integration with emerging agricultural technologies, (iii) improved data acquisition and processing efficiency (iv) diverse testing scenarios, and (v) ethical considerations through proper regulations.

Figure 8.9: Source: Kouadio et al. (2023)

8.4 Disease detection

This section illustrates the use of unmanned aerial vehicle (UAV) remote sensing imagery for identifying banana wilt disease. Fusarium wilt of banana, also known as “banana cancer”, threatens banana production areas worldwide. Timely and accurate identification of Fusarium wilt disease is crucial for effective disease control and optimizing agricultural planting structure (Pegg et al. 2019).

A common initial symptom of this disease is the appearance of a faint pale yellow streak at the base of the petiole of the oldest leaf. This is followed by leaf chlorosis which progresses from lower to upper leaves, wilting of leaves and longitudinal splitting of their bases. Pseudostem splitting of leaf bases is more common in young, rapidly growing plants Pegg et al. (2019).

Figure 8.10: Cavendish plant affected by Race 1 Foc (D. Peasley). Source: (Pegg et al. 2019)

Ye et al. (2020) made publicly available experimental data (Huichun YE et al. 2022) on wilted banana plants collected in a banana plantation located in Long’an County, Guangxi (China). The data set includes UAV multispectral reflectance data and ground survey data on the incidence of banana wilt disease. The paper by Ye et al. (2020) reports that the banana Fusarium wilt disease can be easily identified using several vegetation indices (VIs) obtained from this data set. Tested VIs include green chlorophyll index (CIgreen), red-edge chlorophyll index (CIRE), normalized difference vegetation index (NDVI), and normalized difference red-edge index (NDRE). The dataset can be downloaded from here.

8.4.1 Software setup

Let’s start by cleaning up R memory:

rm(list=ls())

Then, we need to install several packages (if they are not installed yet):

list.of.packages <- c("terra", 
                      "tidyterra", 
                      "stars", 
                      "sf", 
                      "leaflet", 
                      "leafem", 
                      "dplyr", 
                      "ggplot2", 
                      "tidymodels")
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
if(length(new.packages)) install.packages(new.packages)

Now, let’s load all the required packages:

library(terra)
library(tidyterra)
library(stars)
library(sf)
library(leaflet)
library(leafem)
library(dplyr)
library(ggplot2)
library(tidymodels)

8.4.2 Reading the dataset

The following code assumes you have already downloaded the Huichun YE et al. (2022) dataset and unzipped its content under the data/banana_data directory.

8.4.3 File formats

Let’s list the files under each subfolder:

list.files("data/banana_data/1_UAV multispectral reflectance")
[1] "UAV multispectral reflectance.tfw"        
[2] "UAV multispectral reflectance.tif"        
[3] "UAV multispectral reflectance.tif.aux.xml"
[4] "UAV multispectral reflectance.tif.ovr"    

Note that the .tif file contains an orthophotomosaic of surface reflectance. It was created from UAV images taken with a Micasense Red Edge M camera which has five narrow spectral bands: Blue (465–485 nm), green (550–570 nm), red (653–673 nm), red edge (712–722 nm), and near-infrared (800–880 nm). We assume here that those images have been radiometrically and geometrically corrected.

list.files("data/banana_data/2_Ground survey data of banana Fusarium wilt")
[1] "Ground_survey_data_of_banana_Fusarium_wilt.dbf"    
[2] "Ground_survey_data_of_banana_Fusarium_wilt.prj"    
[3] "Ground_survey_data_of_banana_Fusarium_wilt.sbn"    
[4] "Ground_survey_data_of_banana_Fusarium_wilt.sbx"    
[5] "Ground_survey_data_of_banana_Fusarium_wilt.shp"    
[6] "Ground_survey_data_of_banana_Fusarium_wilt.shp.xml"
[7] "Ground_survey_data_of_banana_Fusarium_wilt.shx"    

This is a shapefile with 80 points where the plant health status was collected on the same date as the images.

list.files("data/banana_data/3_Boundary of banana planting region")
[1] "Boundary_of_banana_planting_region.dbf"    
[2] "Boundary_of_banana_planting_region.prj"    
[3] "Boundary_of_banana_planting_region.sbn"    
[4] "Boundary_of_banana_planting_region.sbx"    
[5] "Boundary_of_banana_planting_region.shp"    
[6] "Boundary_of_banana_planting_region.shp.xml"
[7] "Boundary_of_banana_planting_region.shx"    

This is a shapefile with one polygon representing the boundary of the study area.

8.4.4 Read the orthomosaic and the ground data

Now, let’s read the orthomosaic using the terra package:

# Open the tif 
tif <- "data/banana_data/1_UAV multispectral reflectance/UAV multispectral reflectance.tif"


rrr <- terra::rast(tif)

Let’s check what we get:

rrr
class       : SpatRaster 
size        : 7885, 14420, 5  (nrow, ncol, nlyr)
resolution  : 0.08, 0.08  (x, y)
extent      : 779257.9, 780411.5, 2560496, 2561127  (xmin, xmax, ymin, ymax)
coord. ref. : WGS 84 / UTM zone 48N (EPSG:32648) 
source      : UAV multispectral reflectance.tif 
names       : UAV mu~ance_1, UAV mu~ance_2, UAV mu~ance_3, UAV mu~ance_4, UAV mu~ance_5 
min values  :      0.000000,      0.000000,      0.000000,     0.0000000,      0.000000 
max values  :      1.272638,      1.119109,      1.075701,     0.9651694,      1.069767 

Note that this is a 5-band multispectral image with 8 cm pixel size.

Now, let’s read the ground data:

shp <- "data/banana_data/2_Ground survey data of banana Fusarium wilt/Ground_survey_data_of_banana_Fusarium_wilt.shp"
ggg <- sf::st_read(shp)
Reading layer `Ground_survey_data_of_banana_Fusarium_wilt' from data source 
  `C:\Users\edelp\OneDrive\Documentos\GitHub\epidemiology-R_review\data\banana_data\2_Ground survey data of banana Fusarium wilt\Ground_survey_data_of_banana_Fusarium_wilt.shp' 
  using driver `ESRI Shapefile'
Simple feature collection with 80 features and 4 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 779548.9 ymin: 2560702 xmax: 780097 ymax: 2561020
Projected CRS: WGS 84 / UTM zone 48N

What we got?

ggg
Simple feature collection with 80 features and 4 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 779548.9 ymin: 2560702 xmax: 780097 ymax: 2561020
Projected CRS: WGS 84 / UTM zone 48N
First 10 features:
   OBJECTID 样点类型   x_经度   y_纬度                 geometry
1         1 健康植株 107.7326 23.13240 POINT (779838.5 2560800)
2         2 健康植株 107.7332 23.13316 POINT (779901.2 2560885)
3         3 健康植株 107.7334 23.13394 POINT (779920.1 2560971)
4         4 健康植株 107.7326 23.13430 POINT (779837.5 2561010)
5         5 健康植株 107.7302 23.13225 POINT (779595.2 2560779)
6         6 健康植株 107.7301 23.13190 POINT (779584.6 2560739)
7         7 健康植株 107.7300 23.13297 POINT (779569.6 2560857)
8         8 健康植株 107.7315 23.13301 POINT (779729.4 2560865)
9         9 健康植株 107.7313 23.13245 POINT (779710.5 2560803)
10       10 健康植株 107.7349 23.13307 POINT (780078.9 2560879)

Note that the attributes are in Chinese language. It seems that we will need to do several changes.

8.4.5 Visualizing the data

As the orthomosaic is too heavy to visualize, we will need a coarser version of it. Let’s use the terra package for doing it.

rrr8 <- terra::aggregate(rrr, 8)

|---------|---------|---------|---------|
=========================================
                                          
#terra <- resample(elev, template, method='bilinear')

Let’s check the output:

rrr8
class       : SpatRaster 
size        : 986, 1803, 5  (nrow, ncol, nlyr)
resolution  : 0.64, 0.64  (x, y)
extent      : 779257.9, 780411.8, 2560496, 2561127  (xmin, xmax, ymin, ymax)
coord. ref. : WGS 84 / UTM zone 48N (EPSG:32648) 
source(s)   : memory
names       : UAV mu~ance_1, UAV mu~ance_2, UAV mu~ance_3, UAV mu~ance_4, UAV mu~ance_5 
min values  :      0.000000,      0.000000,      0.000000,      0.000000,      0.000000 
max values  :      1.272638,      1.119109,      1.075701,      0.949925,      1.069767 

Note that the pixel size of the aggregated raster is 64 cm. Now, in order to visualize the ground points, we will need a color palette:

pal <- colorFactor(
  palette = c('green',  'red'),
  domain = ggg$样点类型
)

Then, we will use the leaflet package to plot the new image and the ground points:

leaflet(data = ggg) |>
  addProviderTiles("Esri.WorldImagery") |>
  addRasterImage(rrr8) |>
  addCircleMarkers(~x_经度, ~y_纬度,
    radius = 5,
    label = ~样点类型,
    fillColor = ~pal(样点类型),  
    fillOpacity = 1,
    stroke = F)

8.4.6 Extracting image values at sampled points

Now we will extract raster values at point locations using the st_extract() function from the {stars} library. It is expected that a value per band is extracted at each point.

We need to convert the raster object into a stars object:

sss <- st_as_stars(rrr)

What we got?

sss
stars_proxy object with 1 attribute in 1 file(s):
$`UAV multispectral reflectance.tif`
[1] "[...]/UAV multispectral reflectance.tif"

dimension(s):
     from    to  offset delta                refsys point x/y
x       1 14420  779258  0.08 WGS 84 / UTM zone 48N FALSE [x]
y       1  7885 2561127 -0.08 WGS 84 / UTM zone 48N FALSE [y]
band    1     5      NA    NA                    NA    NA    

Before conducting the extraction task, it is advisable to collect band values not at a single pixel but at a small window (e.g. 3x3 pixels). Thus, we will start creating 20cm buffers at each site:

poly <- st_buffer(ggg, dist = 0.20)

Now, the extraction task:

# Extract the median value per polygon
buf_values <- aggregate(sss, poly, FUN = median) |>
  st_as_sf()

What we got:

buf_values
Simple feature collection with 80 features and 5 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 779548.7 ymin: 2560702 xmax: 780097.2 ymax: 2561021
Projected CRS: WGS 84 / UTM zone 48N
First 10 features:
   UAV multispectral reflectance.tif.V1 UAV multispectral reflectance.tif.V2
1                             0.2837147                            0.3570867
2                             0.3096394                            0.4598360
3                             0.2652082                            0.3556182
4                             0.2729177                            0.3626878
5                             0.3033864                            0.3089822
6                             0.2993084                            0.3655908
7                             0.2471483                            0.2859972
8                             0.2491679                            0.3223873
9                             0.3213297                            0.4069843
10                            0.2976966                            0.3430840
   UAV multispectral reflectance.tif.V3 UAV multispectral reflectance.tif.V4
1                             0.2049306                            0.6370987
2                             0.2058005                            0.6609572
3                             0.1769855                            0.5988070
4                             0.1828043                            0.7094992
5                             0.1894684                            0.6805743
6                             0.1988409                            0.7750074
7                             0.1880568                            0.5221797
8                             0.1855947                            0.5934904
9                             0.2013687                            0.5593004
10                            0.2144508                            0.8057290
   UAV multispectral reflectance.tif.V5                       geometry
1                             0.4779058 POLYGON ((779838.7 2560800,...
2                             0.5714727 POLYGON ((779901.4 2560885,...
3                             0.4352523 POLYGON ((779920.3 2560971,...
4                             0.5149931 POLYGON ((779837.7 2561010,...
5                             0.4008912 POLYGON ((779595.4 2560779,...
6                             0.4967270 POLYGON ((779584.8 2560739,...
7                             0.3308629 POLYGON ((779569.8 2560857,...
8                             0.4570443 POLYGON ((779729.6 2560865,...
9                             0.5094757 POLYGON ((779710.7 2560803,...
10                            0.5005793 POLYGON ((780079.1 2560879,...

Note that names of bands are weird:

names(buf_values)
[1] "UAV multispectral reflectance.tif.V1"
[2] "UAV multispectral reflectance.tif.V2"
[3] "UAV multispectral reflectance.tif.V3"
[4] "UAV multispectral reflectance.tif.V4"
[5] "UAV multispectral reflectance.tif.V5"
[6] "geometry"                            

Let’s rename band values:

buf_values |> rename(blue = "UAV multispectral reflectance.tif.V1",
                      green = "UAV multispectral reflectance.tif.V2",
                      red = "UAV multispectral reflectance.tif.V3",
                      redge = "UAV multispectral reflectance.tif.V4",
                      nir = "UAV multispectral reflectance.tif.V5") -> buf_values2

Now, we got shorter names per band:

buf_values2
Simple feature collection with 80 features and 5 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 779548.7 ymin: 2560702 xmax: 780097.2 ymax: 2561021
Projected CRS: WGS 84 / UTM zone 48N
First 10 features:
        blue     green       red     redge       nir
1  0.2837147 0.3570867 0.2049306 0.6370987 0.4779058
2  0.3096394 0.4598360 0.2058005 0.6609572 0.5714727
3  0.2652082 0.3556182 0.1769855 0.5988070 0.4352523
4  0.2729177 0.3626878 0.1828043 0.7094992 0.5149931
5  0.3033864 0.3089822 0.1894684 0.6805743 0.4008912
6  0.2993084 0.3655908 0.1988409 0.7750074 0.4967270
7  0.2471483 0.2859972 0.1880568 0.5221797 0.3308629
8  0.2491679 0.3223873 0.1855947 0.5934904 0.4570443
9  0.3213297 0.4069843 0.2013687 0.5593004 0.5094757
10 0.2976966 0.3430840 0.2144508 0.8057290 0.5005793
                         geometry
1  POLYGON ((779838.7 2560800,...
2  POLYGON ((779901.4 2560885,...
3  POLYGON ((779920.3 2560971,...
4  POLYGON ((779837.7 2561010,...
5  POLYGON ((779595.4 2560779,...
6  POLYGON ((779584.8 2560739,...
7  POLYGON ((779569.8 2560857,...
8  POLYGON ((779729.6 2560865,...
9  POLYGON ((779710.7 2560803,...
10 POLYGON ((780079.1 2560879,...

8.4.7 Computing vegetation indices

Ye et al. (2020) used the following indices (Figure 8.11):

Figure 8.11

Thus, we will compute several of those indices:

buf_indices <-  buf_values2 |> 
  mutate(ndvi = (nir - red) / (nir+red),
         ndre = (nir - redge) / (nir+redge),
          cire = (nir / redge) - 1,
          sipi = (nir - blue) / (nir - red)
  ) |> select(ndvi, ndre, cire, sipi)

What we got:

buf_indices
Simple feature collection with 80 features and 4 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: 779548.7 ymin: 2560702 xmax: 780097.2 ymax: 2561021
Projected CRS: WGS 84 / UTM zone 48N
First 10 features:
        ndvi        ndre        cire      sipi                       geometry
1  0.3997667 -0.14277326 -0.24987155 0.7113871 POLYGON ((779838.7 2560800,...
2  0.4704552 -0.07260820 -0.13538624 0.7160328 POLYGON ((779901.4 2560885,...
3  0.4218407 -0.15816766 -0.27313430 0.6584046 POLYGON ((779920.3 2560971,...
4  0.4760534 -0.15884638 -0.27414570 0.7287284 POLYGON ((779837.7 2561010,...
5  0.3581253 -0.25861496 -0.41095167 0.4611839 POLYGON ((779595.4 2560779,...
6  0.4282631 -0.21881961 -0.35906808 0.6627319 POLYGON ((779584.8 2560739,...
7  0.2751988 -0.22427574 -0.36638109 0.5862114 POLYGON ((779569.8 2560857,...
8  0.4223983 -0.12988254 -0.22990450 0.7658009 POLYGON ((779729.6 2560865,...
9  0.4334381 -0.04661848 -0.08908399 0.6106513 POLYGON ((779710.7 2560803,...
10 0.4001629 -0.23359699 -0.37872496 0.7090615 POLYGON ((780079.1 2560879,...

Note that the health status is missing in buf_indices. Therefore, we will need to use a spatial join to link such status:

samples <- st_join(
  ggg,
  buf_indices,
  join = st_intersects)

Let’s check the output:

samples
Simple feature collection with 80 features and 8 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 779548.9 ymin: 2560702 xmax: 780097 ymax: 2561020
Projected CRS: WGS 84 / UTM zone 48N
First 10 features:
   OBJECTID 样点类型   x_经度   y_纬度      ndvi        ndre        cire
1         1 健康植株 107.7326 23.13240 0.3997667 -0.14277326 -0.24987155
2         2 健康植株 107.7332 23.13316 0.4704552 -0.07260820 -0.13538624
3         3 健康植株 107.7334 23.13394 0.4218407 -0.15816766 -0.27313430
4         4 健康植株 107.7326 23.13430 0.4760534 -0.15884638 -0.27414570
5         5 健康植株 107.7302 23.13225 0.3581253 -0.25861496 -0.41095167
6         6 健康植株 107.7301 23.13190 0.4282631 -0.21881961 -0.35906808
7         7 健康植株 107.7300 23.13297 0.2751988 -0.22427574 -0.36638109
8         8 健康植株 107.7315 23.13301 0.4223983 -0.12988254 -0.22990450
9         9 健康植株 107.7313 23.13245 0.4334381 -0.04661848 -0.08908399
10       10 健康植株 107.7349 23.13307 0.4001629 -0.23359699 -0.37872496
        sipi                 geometry
1  0.7113871 POINT (779838.5 2560800)
2  0.7160328 POINT (779901.2 2560885)
3  0.6584046 POINT (779920.1 2560971)
4  0.7287284 POINT (779837.5 2561010)
5  0.4611839 POINT (779595.2 2560779)
6  0.6627319 POINT (779584.6 2560739)
7  0.5862114 POINT (779569.6 2560857)
8  0.7658009 POINT (779729.4 2560865)
9  0.6106513 POINT (779710.5 2560803)
10 0.7090615 POINT (780078.9 2560879)

It seems we succeeded.

unique(samples$样点类型)
[1] "健康植株"   "枯萎病植株"

Let’s check it:

samples
Simple feature collection with 80 features and 8 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 779548.9 ymin: 2560702 xmax: 780097 ymax: 2561020
Projected CRS: WGS 84 / UTM zone 48N
First 10 features:
   OBJECTID 样点类型   x_经度   y_纬度      ndvi        ndre        cire
1         1 健康植株 107.7326 23.13240 0.3997667 -0.14277326 -0.24987155
2         2 健康植株 107.7332 23.13316 0.4704552 -0.07260820 -0.13538624
3         3 健康植株 107.7334 23.13394 0.4218407 -0.15816766 -0.27313430
4         4 健康植株 107.7326 23.13430 0.4760534 -0.15884638 -0.27414570
5         5 健康植株 107.7302 23.13225 0.3581253 -0.25861496 -0.41095167
6         6 健康植株 107.7301 23.13190 0.4282631 -0.21881961 -0.35906808
7         7 健康植株 107.7300 23.13297 0.2751988 -0.22427574 -0.36638109
8         8 健康植株 107.7315 23.13301 0.4223983 -0.12988254 -0.22990450
9         9 健康植株 107.7313 23.13245 0.4334381 -0.04661848 -0.08908399
10       10 健康植株 107.7349 23.13307 0.4001629 -0.23359699 -0.37872496
        sipi                 geometry
1  0.7113871 POINT (779838.5 2560800)
2  0.7160328 POINT (779901.2 2560885)
3  0.6584046 POINT (779920.1 2560971)
4  0.7287284 POINT (779837.5 2561010)
5  0.4611839 POINT (779595.2 2560779)
6  0.6627319 POINT (779584.6 2560739)
7  0.5862114 POINT (779569.6 2560857)
8  0.7658009 POINT (779729.4 2560865)
9  0.6106513 POINT (779710.5 2560803)
10 0.7090615 POINT (780078.9 2560879)

Now, we will replace the Chinese words for English words:

samples |> 
  mutate(score = ifelse(样点类型 == '健康植株', 'healthy', 'wilted')) |>
  rename(east = x_经度,
         north = y_纬度 ) |>
  select(OBJECTID,score, ndvi, ndre, cire, sipi) -> nsamples

Let’s check the output:

nsamples
Simple feature collection with 80 features and 6 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: 779548.9 ymin: 2560702 xmax: 780097 ymax: 2561020
Projected CRS: WGS 84 / UTM zone 48N
First 10 features:
   OBJECTID   score      ndvi        ndre        cire      sipi
1         1 healthy 0.3997667 -0.14277326 -0.24987155 0.7113871
2         2 healthy 0.4704552 -0.07260820 -0.13538624 0.7160328
3         3 healthy 0.4218407 -0.15816766 -0.27313430 0.6584046
4         4 healthy 0.4760534 -0.15884638 -0.27414570 0.7287284
5         5 healthy 0.3581253 -0.25861496 -0.41095167 0.4611839
6         6 healthy 0.4282631 -0.21881961 -0.35906808 0.6627319
7         7 healthy 0.2751988 -0.22427574 -0.36638109 0.5862114
8         8 healthy 0.4223983 -0.12988254 -0.22990450 0.7658009
9         9 healthy 0.4334381 -0.04661848 -0.08908399 0.6106513
10       10 healthy 0.4001629 -0.23359699 -0.37872496 0.7090615
                   geometry
1  POINT (779838.5 2560800)
2  POINT (779901.2 2560885)
3  POINT (779920.1 2560971)
4  POINT (779837.5 2561010)
5  POINT (779595.2 2560779)
6  POINT (779584.6 2560739)
7  POINT (779569.6 2560857)
8  POINT (779729.4 2560865)
9  POINT (779710.5 2560803)
10 POINT (780078.9 2560879)

As we will not intend to use the geometry in our model, we can remove it:

st_geometry(nsamples) <- NULL

Let’s check the output:

nsamples
   OBJECTID   score       ndvi         ndre        cire       sipi
1         1 healthy 0.39976674 -0.142773261 -0.24987155  0.7113871
2         2 healthy 0.47045520 -0.072608196 -0.13538624  0.7160328
3         3 healthy 0.42184072 -0.158167658 -0.27313430  0.6584046
4         4 healthy 0.47605337 -0.158846378 -0.27414570  0.7287284
5         5 healthy 0.35812534 -0.258614958 -0.41095167  0.4611839
6         6 healthy 0.42826313 -0.218819608 -0.35906808  0.6627319
7         7 healthy 0.27519883 -0.224275739 -0.36638109  0.5862114
8         8 healthy 0.42239829 -0.129882540 -0.22990450  0.7658009
9         9 healthy 0.43343808 -0.046618476 -0.08908399  0.6106513
10       10 healthy 0.40016295 -0.233596986 -0.37872496  0.7090615
11       11 healthy 0.39041685 -0.228953927 -0.37259969  0.6571411
12       12 healthy 0.42558618 -0.140911613 -0.24701583  0.5150259
13       13 healthy 0.49340855 -0.196911688 -0.32903294  0.6761255
14       14  wilted 0.05874133 -0.068648057 -0.12847645  3.4707223
15       15  wilted 0.38400155  0.038426164  0.07992348  0.7087015
16       16  wilted 0.20475192  0.099664192  0.22139338  1.1451788
17       17  wilted 0.23249338 -0.012808011 -0.02529208  0.9541926
18       18  wilted 0.07308271  0.059516988  0.12656685  3.3069715
19       19  wilted 0.09034886  0.050902211  0.10726442 -1.1224448
20       20  wilted 0.16015626  0.166594498  0.39979217  1.9539515
21       21  wilted 0.23390146  0.150849653  0.35529551  0.7991395
22       22  wilted 0.37398272 -0.026842737 -0.05228208  0.7405385
23       23  wilted 0.49255873  0.128742778  0.29553334  0.7991471
24       24  wilted 0.31349498 -0.271995471 -0.42766736  0.6524136
25       25  wilted 0.30539446 -0.170267124 -0.29098848  0.6172290
26       26  wilted 0.37186502  0.007804645  0.01573207  0.7467369
27       27  wilted 0.11435660  0.048839732  0.10269506  1.4091384
28       28  wilted 0.46834754  0.009134113  0.01843663  0.8078043
29       29  wilted 0.16656825  0.018023671  0.03670897  1.4190911
30       30  wilted 0.42070587 -0.144575443 -0.25262720  0.5272193
31       31  wilted 0.36820886  0.058621948  0.12454497  0.8245829
32       32  wilted 0.20858497 -0.056220022 -0.10645513  1.1397723
33       33  wilted 0.21998168 -0.119623392 -0.21368505  1.2761610
34       34  wilted 0.28577459 -0.015965114 -0.03142847  0.8868655
35       35  wilted 0.51481347  0.007888690  0.01590283  0.7890558
36       36  wilted 0.26034165 -0.186300667 -0.31408676  0.6313475
37       37  wilted 0.35448491  0.049444260  0.10403232  0.7842711
38       38  wilted 0.35485134  0.049169099  0.10342343  0.3232732
39       39  wilted 0.41155355 -0.075256088 -0.13997798  0.3632746
40       40  wilted 0.27764292 -0.043296886 -0.08300012  0.6920519
41       41  wilted 0.47575852  0.012897741  0.02613253  0.7788887
42       42  wilted 0.28374316  0.100019670  0.22227079  1.0312235
43       43  wilted 0.43536054 -0.005372302 -0.01068719  0.8065662
44       44  wilted 0.36866873 -0.023050553 -0.04506239  0.5711057
45       45  wilted 0.33280476 -0.058164786 -0.10993521  0.6822160
46       46  wilted 0.51897525 -0.080920969 -0.14972597  0.7637551
47       47  wilted 0.51320069 -0.029567154 -0.05743609  0.9153090
48       48  wilted 0.36543237  0.053204886  0.11238944  1.0314492
49       49 healthy 0.36090044 -0.279952871 -0.43744247  0.5027884
50       50  wilted 0.35309882 -0.001355745 -0.00270782  0.7946941
51       51 healthy 0.39490497 -0.130486894 -0.23085079  0.6122870
52       52 healthy 0.42757915 -0.240080432 -0.38720139  0.5241772
53       53 healthy 0.37856775 -0.233516246 -0.37861884  0.7662385
54       54 healthy 0.45668180 -0.197173474 -0.32939833  0.6839776
55       55 healthy 0.40846426 -0.148771702 -0.25901004  0.5934538
56       56 healthy 0.44160665 -0.192621610 -0.32302217  0.5988123
57       57 healthy 0.52035481 -0.141993366 -0.24867634  0.5994354
58       58 healthy 0.36400542 -0.238072742 -0.38458603  0.6079211
59       59 healthy 0.33408493 -0.228534207 -0.37204370  0.5059822
60       60 healthy 0.34397502 -0.200302031 -0.33375272  0.5625230
61       61 healthy 0.37706210 -0.348438178 -0.51680260  0.6534776
62       62 healthy 0.36836135 -0.066868925 -0.12535546  0.4413452
63       63 healthy 0.38415111 -0.180227754 -0.30541182  0.6743726
64       64 healthy 0.28650043 -0.116778741 -0.20913496  0.7196425
65       65 healthy 0.37453715 -0.251235648 -0.40158007  0.5122386
66       66 healthy 0.43549960 -0.237002851 -0.38318885  0.6752221
67       67 healthy 0.41255324 -0.191220292 -0.32104942  0.6745082
68       68 healthy 0.36656292 -0.238273304 -0.38484768  0.6054673
69       69 healthy 0.47892785 -0.215340798 -0.35437105  0.7113262
70       70 healthy 0.40082514 -0.279709014 -0.43714471  0.7465864
71       71 healthy 0.33697212 -0.181384162 -0.30707058  0.5021931
72       72 healthy 0.40791291 -0.222465226 -0.36396164  0.6068031
73       73 healthy 0.55198422  0.009196845  0.01856442  0.8259194
74       74 healthy 0.27583200 -0.288149676 -0.44738540  0.5335927
75       75 healthy 0.46411580 -0.149982883 -0.26084368  0.7550614
76       76 healthy 0.41437170 -0.249396644 -0.39922733  0.6959438
77       77  wilted 0.42071386 -0.014630713 -0.02883948  0.7815805
78       78  wilted 0.44323734  0.035346511  0.07328333  0.8931209
79       79  wilted 0.42093366  0.097763989  0.21671489  0.8684168
80       80  wilted 0.26383824  0.045756804  0.09590177  1.0954009

As our task is a binary classification (i.e any site can be either healthy or wilted), the variable to estimate is a factor (not a character).

Let’s change the data type of such variable:

nsamples$score = as.factor(nsamples$score) 

Let’s check the result:

nsamples
   OBJECTID   score       ndvi         ndre        cire       sipi
1         1 healthy 0.39976674 -0.142773261 -0.24987155  0.7113871
2         2 healthy 0.47045520 -0.072608196 -0.13538624  0.7160328
3         3 healthy 0.42184072 -0.158167658 -0.27313430  0.6584046
4         4 healthy 0.47605337 -0.158846378 -0.27414570  0.7287284
5         5 healthy 0.35812534 -0.258614958 -0.41095167  0.4611839
6         6 healthy 0.42826313 -0.218819608 -0.35906808  0.6627319
7         7 healthy 0.27519883 -0.224275739 -0.36638109  0.5862114
8         8 healthy 0.42239829 -0.129882540 -0.22990450  0.7658009
9         9 healthy 0.43343808 -0.046618476 -0.08908399  0.6106513
10       10 healthy 0.40016295 -0.233596986 -0.37872496  0.7090615
11       11 healthy 0.39041685 -0.228953927 -0.37259969  0.6571411
12       12 healthy 0.42558618 -0.140911613 -0.24701583  0.5150259
13       13 healthy 0.49340855 -0.196911688 -0.32903294  0.6761255
14       14  wilted 0.05874133 -0.068648057 -0.12847645  3.4707223
15       15  wilted 0.38400155  0.038426164  0.07992348  0.7087015
16       16  wilted 0.20475192  0.099664192  0.22139338  1.1451788
17       17  wilted 0.23249338 -0.012808011 -0.02529208  0.9541926
18       18  wilted 0.07308271  0.059516988  0.12656685  3.3069715
19       19  wilted 0.09034886  0.050902211  0.10726442 -1.1224448
20       20  wilted 0.16015626  0.166594498  0.39979217  1.9539515
21       21  wilted 0.23390146  0.150849653  0.35529551  0.7991395
22       22  wilted 0.37398272 -0.026842737 -0.05228208  0.7405385
23       23  wilted 0.49255873  0.128742778  0.29553334  0.7991471
24       24  wilted 0.31349498 -0.271995471 -0.42766736  0.6524136
25       25  wilted 0.30539446 -0.170267124 -0.29098848  0.6172290
26       26  wilted 0.37186502  0.007804645  0.01573207  0.7467369
27       27  wilted 0.11435660  0.048839732  0.10269506  1.4091384
28       28  wilted 0.46834754  0.009134113  0.01843663  0.8078043
29       29  wilted 0.16656825  0.018023671  0.03670897  1.4190911
30       30  wilted 0.42070587 -0.144575443 -0.25262720  0.5272193
31       31  wilted 0.36820886  0.058621948  0.12454497  0.8245829
32       32  wilted 0.20858497 -0.056220022 -0.10645513  1.1397723
33       33  wilted 0.21998168 -0.119623392 -0.21368505  1.2761610
34       34  wilted 0.28577459 -0.015965114 -0.03142847  0.8868655
35       35  wilted 0.51481347  0.007888690  0.01590283  0.7890558
36       36  wilted 0.26034165 -0.186300667 -0.31408676  0.6313475
37       37  wilted 0.35448491  0.049444260  0.10403232  0.7842711
38       38  wilted 0.35485134  0.049169099  0.10342343  0.3232732
39       39  wilted 0.41155355 -0.075256088 -0.13997798  0.3632746
40       40  wilted 0.27764292 -0.043296886 -0.08300012  0.6920519
41       41  wilted 0.47575852  0.012897741  0.02613253  0.7788887
42       42  wilted 0.28374316  0.100019670  0.22227079  1.0312235
43       43  wilted 0.43536054 -0.005372302 -0.01068719  0.8065662
44       44  wilted 0.36866873 -0.023050553 -0.04506239  0.5711057
45       45  wilted 0.33280476 -0.058164786 -0.10993521  0.6822160
46       46  wilted 0.51897525 -0.080920969 -0.14972597  0.7637551
47       47  wilted 0.51320069 -0.029567154 -0.05743609  0.9153090
48       48  wilted 0.36543237  0.053204886  0.11238944  1.0314492
49       49 healthy 0.36090044 -0.279952871 -0.43744247  0.5027884
50       50  wilted 0.35309882 -0.001355745 -0.00270782  0.7946941
51       51 healthy 0.39490497 -0.130486894 -0.23085079  0.6122870
52       52 healthy 0.42757915 -0.240080432 -0.38720139  0.5241772
53       53 healthy 0.37856775 -0.233516246 -0.37861884  0.7662385
54       54 healthy 0.45668180 -0.197173474 -0.32939833  0.6839776
55       55 healthy 0.40846426 -0.148771702 -0.25901004  0.5934538
56       56 healthy 0.44160665 -0.192621610 -0.32302217  0.5988123
57       57 healthy 0.52035481 -0.141993366 -0.24867634  0.5994354
58       58 healthy 0.36400542 -0.238072742 -0.38458603  0.6079211
59       59 healthy 0.33408493 -0.228534207 -0.37204370  0.5059822
60       60 healthy 0.34397502 -0.200302031 -0.33375272  0.5625230
61       61 healthy 0.37706210 -0.348438178 -0.51680260  0.6534776
62       62 healthy 0.36836135 -0.066868925 -0.12535546  0.4413452
63       63 healthy 0.38415111 -0.180227754 -0.30541182  0.6743726
64       64 healthy 0.28650043 -0.116778741 -0.20913496  0.7196425
65       65 healthy 0.37453715 -0.251235648 -0.40158007  0.5122386
66       66 healthy 0.43549960 -0.237002851 -0.38318885  0.6752221
67       67 healthy 0.41255324 -0.191220292 -0.32104942  0.6745082
68       68 healthy 0.36656292 -0.238273304 -0.38484768  0.6054673
69       69 healthy 0.47892785 -0.215340798 -0.35437105  0.7113262
70       70 healthy 0.40082514 -0.279709014 -0.43714471  0.7465864
71       71 healthy 0.33697212 -0.181384162 -0.30707058  0.5021931
72       72 healthy 0.40791291 -0.222465226 -0.36396164  0.6068031
73       73 healthy 0.55198422  0.009196845  0.01856442  0.8259194
74       74 healthy 0.27583200 -0.288149676 -0.44738540  0.5335927
75       75 healthy 0.46411580 -0.149982883 -0.26084368  0.7550614
76       76 healthy 0.41437170 -0.249396644 -0.39922733  0.6959438
77       77  wilted 0.42071386 -0.014630713 -0.02883948  0.7815805
78       78  wilted 0.44323734  0.035346511  0.07328333  0.8931209
79       79  wilted 0.42093366  0.097763989  0.21671489  0.8684168
80       80  wilted 0.26383824  0.045756804  0.09590177  1.0954009

A simple summary of the extracted data can be useful:

nsamples |>
  group_by(score) |>
  summarize(n())
# A tibble: 2 × 2
  score   `n()`
  <fct>   <int>
1 healthy    40
2 wilted     40

This means the dataset is balanced which is very good.

8.4.8 Saving the extracted dataset

Now, let’s save the nsamples object. Just in case R crashes due to lack of memory.

#uncomment if needed
#st_write(nsamples, "./banana_data/nsamples.csv", overwrite=TRUE)

8.4.9 Classification of Fusarium wilt using machine learning (ML)

The overall process to classify the crop disease under study will be conducted using the tidymodels framework which is an extension of the tidyverse suite. It is especially focused towards providing a generalized way to define, run and optimize ML models in R.

8.4.9.1 Exploratory analysis

As a first step in modeling, it’s always a good idea to visualize the data. Let’s start with a boxplot to display the distribution of a vegetation index. It visualizes five summary statistics (the median, two hinges and two whiskers), and all “outlying” points individually.

p <- ggplot(nsamples, aes(score, ndre))+
  r4pde::theme_r4pde()
p + geom_boxplot()

Next, we will do a scatterplot to visualize the indices NDRE and CIRE:

ggplot(nsamples) +
  aes(x = ndre, y = cire, color = score) +
  geom_point(shape = 16, size = 4) +
  labs(x = "NDRE", y = "CIRE") +
  r4pde::theme_r4pde() +
  scale_color_manual(values = c("#71b075", "#ba0600"))

8.4.9.2 Splitting the data

Next step is to divide the data into a training and a test set. The set.seed() function can be used for reproducibility of the computations that are dependent on random numbers. By default, the training/testing split is 0.75 to 0.25.

set.seed(42)
data_split <- initial_split(data = nsamples)
data_train <- training(data_split)
data_test <- testing(data_split)

Let’s check the result:

data_train
   OBJECTID   score       ndvi         ndre        cire      sipi
1        49 healthy 0.36090044 -0.279952871 -0.43744247 0.5027884
2        65 healthy 0.37453715 -0.251235648 -0.40158007 0.5122386
3        25  wilted 0.30539446 -0.170267124 -0.29098848 0.6172290
4        74 healthy 0.27583200 -0.288149676 -0.44738540 0.5335927
5        18  wilted 0.07308271  0.059516988  0.12656685 3.3069715
6        80  wilted 0.26383824  0.045756804  0.09590177 1.0954009
7        47  wilted 0.51320069 -0.029567154 -0.05743609 0.9153090
8        24  wilted 0.31349498 -0.271995471 -0.42766736 0.6524136
9        71 healthy 0.33697212 -0.181384162 -0.30707058 0.5021931
10       37  wilted 0.35448491  0.049444260  0.10403232 0.7842711
11       20  wilted 0.16015626  0.166594498  0.39979217 1.9539515
12       26  wilted 0.37186502  0.007804645  0.01573207 0.7467369
13        3 healthy 0.42184072 -0.158167658 -0.27313430 0.6584046
14       41  wilted 0.47575852  0.012897741  0.02613253 0.7788887
15       27  wilted 0.11435660  0.048839732  0.10269506 1.4091384
16       36  wilted 0.26034165 -0.186300667 -0.31408676 0.6313475
17       72 healthy 0.40791291 -0.222465226 -0.36396164 0.6068031
18       31  wilted 0.36820886  0.058621948  0.12454497 0.8245829
19       45  wilted 0.33280476 -0.058164786 -0.10993521 0.6822160
20        5 healthy 0.35812534 -0.258614958 -0.41095167 0.4611839
21       70 healthy 0.40082514 -0.279709014 -0.43714471 0.7465864
22       34  wilted 0.28577459 -0.015965114 -0.03142847 0.8868655
23       28  wilted 0.46834754  0.009134113  0.01843663 0.8078043
24       40  wilted 0.27764292 -0.043296886 -0.08300012 0.6920519
25       68 healthy 0.36656292 -0.238273304 -0.38484768 0.6054673
26       33  wilted 0.21998168 -0.119623392 -0.21368505 1.2761610
27       42  wilted 0.28374316  0.100019670  0.22227079 1.0312235
28       73 healthy 0.55198422  0.009196845  0.01856442 0.8259194
29       30  wilted 0.42070587 -0.144575443 -0.25262720 0.5272193
30       43  wilted 0.43536054 -0.005372302 -0.01068719 0.8065662
31       15  wilted 0.38400155  0.038426164  0.07992348 0.7087015
32       22  wilted 0.37398272 -0.026842737 -0.05228208 0.7405385
33        8 healthy 0.42239829 -0.129882540 -0.22990450 0.7658009
34       79  wilted 0.42093366  0.097763989  0.21671489 0.8684168
35        4 healthy 0.47605337 -0.158846378 -0.27414570 0.7287284
36       75 healthy 0.46411580 -0.149982883 -0.26084368 0.7550614
37       76 healthy 0.41437170 -0.249396644 -0.39922733 0.6959438
38       58 healthy 0.36400542 -0.238072742 -0.38458603 0.6079211
39       61 healthy 0.37706210 -0.348438178 -0.51680260 0.6534776
40       46  wilted 0.51897525 -0.080920969 -0.14972597 0.7637551
41       59 healthy 0.33408493 -0.228534207 -0.37204370 0.5059822
42       35  wilted 0.51481347  0.007888690  0.01590283 0.7890558
43       53 healthy 0.37856775 -0.233516246 -0.37861884 0.7662385
44       23  wilted 0.49255873  0.128742778  0.29553334 0.7991471
45       69 healthy 0.47892785 -0.215340798 -0.35437105 0.7113262
46        6 healthy 0.42826313 -0.218819608 -0.35906808 0.6627319
47       39  wilted 0.41155355 -0.075256088 -0.13997798 0.3632746
48        2 healthy 0.47045520 -0.072608196 -0.13538624 0.7160328
49       60 healthy 0.34397502 -0.200302031 -0.33375272 0.5625230
50       56 healthy 0.44160665 -0.192621610 -0.32302217 0.5988123
51       62 healthy 0.36836135 -0.066868925 -0.12535546 0.4413452
52       21  wilted 0.23390146  0.150849653  0.35529551 0.7991395
53       55 healthy 0.40846426 -0.148771702 -0.25901004 0.5934538
54       64 healthy 0.28650043 -0.116778741 -0.20913496 0.7196425
55       57 healthy 0.52035481 -0.141993366 -0.24867634 0.5994354
56       10 healthy 0.40016295 -0.233596986 -0.37872496 0.7090615
57       48  wilted 0.36543237  0.053204886  0.11238944 1.0314492
58       54 healthy 0.45668180 -0.197173474 -0.32939833 0.6839776
59        1 healthy 0.39976674 -0.142773261 -0.24987155 0.7113871
60       17  wilted 0.23249338 -0.012808011 -0.02529208 0.9541926

8.4.9.3 Defining the model

We will use a logistic regression which is a simple model. It may be useful to have a look at this explanation of such a model.

spec_lr <-
logistic_reg() |> 
  set_engine("glm") |> 
  set_mode("classification")

8.4.9.4 Defining the recipe

The recipe() function to be used here has two arguments:

  • A formula. Any variable on the left-hand side of the tilde (~) is considered the model outcome (here, outcome). On the right-hand side of the tilde are the predictors. Variables may be listed by name, or you can use the dot (.) to indicate all other variables as predictors.

  • The data. A recipe is associated with the data set used to create the model. This will typically be the training set, so data = data_train here.

recipe_lr <-
  recipe(score ~ ., data_train) |>
  add_role(OBJECTID, new_role = "id") |>
  step_zv(all_predictors()) |>
  step_corr(all_predictors())

8.4.9.5 Evaluating model performance

Next, we need to specify what we would like to see for determining the performance of the model. Different modelling algorithms have different types of metrics. Because we have a binary classification problem (healthy vs. wilted classification), we will choose the AUC - ROC evaluation metric here.

8.4.9.6 Combining model and recipe into a workflow

We will want to use our recipe across several steps as we train and test our model. We will:

  • Process the recipe using the training set: This involves any estimation or calculations based on the training set. For our recipe, the training set will be used to determine which predictors should be converted to dummy variables and which predictors will have zero-variance in the training set, and should be slated for removal.

  • Apply the recipe to the training set: We create the final predictor set on the training set.

  • Apply the recipe to the test set: We create the final predictor set on the test set. Nothing is recomputed and no information from the test set is used here; the dummy variable and zero-variance results from the training set are applied to the test set.

To simplify this process, we can use a model workflow, which pairs a model and recipe together. This is a straightforward approach because different recipes are often needed for different models, so when a model and recipe are bundled, it becomes easier to train and test workflows. We’ll use the workflows package from tidymodels to bundle our model with our recipe.

Now we are ready to set up our complete modelling workflow. This workflow contains the model specification and the recipe.

wf_bana_wilt <-
  workflow(
    spec = spec_lr,
    recipe_lr
    )

wf_bana_wilt
══ Workflow ════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: logistic_reg()

── Preprocessor ────────────────────────────────────────────────────────────────
2 Recipe Steps

• step_zv()
• step_corr()

── Model ───────────────────────────────────────────────────────────────────────
Logistic Regression Model Specification (classification)

Computational engine: glm 

8.4.9.7 Fitting the logistic regression model

Now we use the workflow previously created to fit the model on our training data. We use the training partition of the data.

fit_lr <- wf_bana_wilt  |> 
  fit(data = data_train)

Let’s check the output:

fit_lr
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: logistic_reg()

── Preprocessor ────────────────────────────────────────────────────────────────
2 Recipe Steps

• step_zv()
• step_corr()

── Model ───────────────────────────────────────────────────────────────────────

Call:  stats::glm(formula = ..y ~ ., family = stats::binomial, data = data)

Coefficients:
(Intercept)     OBJECTID         ndvi         ndre         sipi  
    9.27889     -0.02585    -16.62083     24.61264      1.51054  

Degrees of Freedom: 59 Total (i.e. Null);  55 Residual
Null Deviance:      83.18 
Residual Deviance: 31.96    AIC: 41.96

Now, we will use the fitted model to estimate health status in the training data:

rf_training_pred <- 
  predict(fit_lr, data_train) |> 
  bind_cols(predict(fit_lr, data_train, type = "prob")) |> 
  # Add the true outcome data back in
  bind_cols(data_train |> 
              select(score))

What we got?

rf_training_pred
# A tibble: 60 × 4
   .pred_class .pred_healthy .pred_wilted score  
   <fct>               <dbl>        <dbl> <fct>  
 1 healthy       0.984             0.0160 healthy
 2 healthy       0.983             0.0174 healthy
 3 wilted        0.426             0.574  wilted 
 4 healthy       0.971             0.0292 healthy
 5 wilted        0.000000784       1.000  wilted 
 6 wilted        0.00366           0.996  wilted 
 7 wilted        0.453             0.547  wilted 
 8 healthy       0.906             0.0944 wilted 
 9 healthy       0.866             0.134  healthy
10 wilted        0.00791           0.992  wilted 
# ℹ 50 more rows

Let’s estimate the training accuracy:

rf_training_pred |> # training set predictions
  accuracy(truth = score, .pred_class) -> acc_train
acc_train
# A tibble: 1 × 3
  .metric  .estimator .estimate
  <chr>    <chr>          <dbl>
1 accuracy binary         0.867

The accuracy of the model on the training data is 0.87, which is above 0.5 (the accuracy expected by chance for two balanced classes). This basically means that the model was able to learn predictive patterns from the training data. To see if the model is able to generalize what it learned when exposed to new data, we evaluate the model on our hold-out (or so-called test data). We created a test dataset when splitting the data at the start of the modelling.

8.4.9.8 Evaluating the model on test data

Now, we will use the fitted model to estimate health status in the testing data:

lr_testing_pred <- 
  predict(fit_lr, data_test) |> 
  bind_cols(predict(fit_lr, data_test, type = "prob")) |> 
  bind_cols(data_test |> select(score))

What we got:

lr_testing_pred
# A tibble: 20 × 4
   .pred_class .pred_healthy .pred_wilted score  
   <fct>               <dbl>        <dbl> <fct>  
 1 healthy         0.528           0.472  healthy
 2 wilted          0.166           0.834  healthy
 3 healthy         0.894           0.106  healthy
 4 healthy         0.689           0.311  healthy
 5 healthy         0.956           0.0438 healthy
 6 wilted          0.0000102       1.000  wilted 
 7 wilted          0.0000647       1.000  wilted 
 8 wilted          0.00107         0.999  wilted 
 9 wilted          0.000237        1.000  wilted 
10 wilted          0.00486         0.995  wilted 
11 wilted          0.0163          0.984  wilted 
12 wilted          0.0904          0.910  wilted 
13 wilted          0.0361          0.964  wilted 
14 healthy         0.709           0.291  healthy
15 healthy         0.986           0.0135 healthy
16 healthy         0.896           0.104  healthy
17 healthy         0.989           0.0112 healthy
18 healthy         0.952           0.0475 healthy
19 wilted          0.247           0.753  wilted 
20 wilted          0.108           0.892  wilted 

Let’s compute the testing accuracy:

lr_testing_pred |>                   # test set predictions
  accuracy(score, .pred_class)
# A tibble: 1 × 3
  .metric  .estimator .estimate
  <chr>    <chr>          <dbl>
1 accuracy binary          0.95

The accuracy on the test data is similar to or higher than the accuracy on the training data, so there is no sign of overfitting. Note that the test set has only 20 samples, so this estimate is imprecise. The result is good for a first go and a relatively simple classification model.

## Let's plot the AUC-ROC 
lr_testing_pred |> 
  roc_curve(truth = score, .pred_wilted, event_level="second") |> 
  mutate(model = "Logistic Regression") |>
  autoplot()

8.4.10 Conclusions

In this section, we trained and tested a logistic regression model (LGM) using four spectral indices as predictor variables (i.e. NDVI, NDRE, CIRE and SIPI). Compare this section results, in terms of equation and accuracy, with the individual LGMs tested by Ye et al. (2020).

Figure 8.12

Note that we have not tested other ML algorithms. But there are a lot of them available from the tidymodels framework (e.g. random forests, support vector machines, gradient boosting machines).

To conclude, this section illustrated how to use VIs derived from UAV-based multispectral imagery and ground data to develop an identification model for detecting banana Fusarium wilt. The results showed that a simple logistic regression model is able to identify Fusarium wilt of banana from several VIs with a good accuracy. However, before going too optimistic, I would suggest to study the Ye et al. (2020) paper and critically evaluate their experiment design, methods and results.

8.5 Disease quantification

8.5.1 Introduction

This section illustrates the use of UAV multispectral imagery for estimating the severity of cercospora leaf spot (CLS) disease in table beet, the most destructive fungal disease of table beet (Skaracis et al. 2010; Tan et al. 2023). The CLS disease causes rapid defoliation and significant crop loss may occur through the inability to harvest with top-pulling machinery. It may also render the produce unsaleable for the fresh market (Skaracis et al. 2010).

Saif et al. (2024) recently published on Mendeley data UAV imagery & ground truth CLS data collected at several table beet plots at Cornell AgriTech, Geneva, New York, USA. Note that, in this study, UAV multispectral images were collected using a Micasense Red Edge camera, similar to the one used in the case study for the detection of Fusarium wilt on banana. I have not found any scientific paper estimating CLS severity from this dataset. However, in Saif et al. (2023), a similar dataset was used to forecast table beet root yield at Cornell Agritech. While it is just a guess, it is worth visualizing the plots in the latter study.

Figure 8.13: Color mosaic of third flight hyperspectral image. The visualization bands are the following: red (639 nm), green (550 nm), and blue (470 nm). The crops in the red boxes in the figure were the plots under study. Source: (Saif et al. 2023)

This section aims at estimating CLS leaf severity using vegetation indices (VIs) derived from the multispectral UAV imagery as spectral covariates. The section comprises two parts: (i) Part 1 creates a spectral covariate table to summarize the complete UAV imagery & ground truth data; and (ii) Part 2 trains and tests a machine learning model to estimate CLS severity.

8.5.2 CLS severity on leaves

It is very important to visualize how different levels of table beet CLS disease severity look in RGB color leaf images. The figure below is a standard area diagram set (SADs) that is used by raters during the assessment of visual severity to increase their accuracy and reliability of the estimates.

Figure 8.14: Standard area diagram set for the severity of table beet cercospora leaf spot. Source: (Del Ponte et al. 2019)

8.5.3 Software setup

Let’s start by cleaning up R memory:

# Re-enable execution for the Cercospora leaf spot section
knitr::opts_chunk$set(eval = TRUE)
rm(list=ls())

Then, we need to install several packages (if they are not installed yet):

list.of.packages <- c("readr","terra", "tidyterra", "stars", "sf", "leaflet", "leafem", "dplyr", "ggplot2", "tidymodels")
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
if(length(new.packages)) install.packages(new.packages)

Now, let’s load all the required packages:

library(readr)
library(terra)
library(tidyterra)
library(stars)
library(sf)
library(leaflet)
library(leafem)
library(dplyr)
library(ggplot2)
library(tidymodels)

8.5.4 Read the dataset

The following code assumes you have already downloaded the dataset and unzipped its content under the data/cercospora_data directory. What files are in that folder?

list.files("data/cercospora_data")
 [1] "CLS_DS.csv"          "D0_2021.csv"         "D1_2021.csv"        
 [4] "D2_2021.csv"         "D3_2021.csv"         "D4_2021.csv"        
 [7] "fcovar_2021.csv"     "multispec_2021_2022" "multispec_2023"     
[10] "ncovar_2021.csv"     "README.md"          

Note that there is one CLS_DS.csv file with the CLS ground data and two folders with the UAV multispectral images. We can infer that 2021, 2022, 2023 refer to the image acquisition years. At this point, it is very important to check the README file. I will summarize the following points:

8.5.4.1 File naming convention

Each image file is named according to the plot number and the date of capture, using the format plt_rYYYYMMDD, where:

  • ‘plt’ stands for the plot number.
  • ‘YYYYMMDD’ represents the date on which the image was captured (year, month, day).

For example, the file name 5_r20210715 corresponds to an image taken on July 15, 2021, from study plot 5.

8.5.4.2 CLS Severity Data

The file named CLS_DS contains visual assessments of CLS disease severity noted for each plot.

8.5.5 Inspect the format of each file

Let’s list the first images under an image folder:

list.files("data/cercospora_data/multispec_2021_2022/")[1:15]
 [1] "1_r20210707.tif"         "1_r20210707.tif.aux.xml"
 [3] "1_r20210715.tif"         "1_r20210720.tif"        
 [5] "1_r20210802.tif"         "1_r20210825.tif"        
 [7] "1_r20210825.tif.aux.xml" "1_r20220707.tif"        
 [9] "1_r20220715.tif"         "1_r20220726.tif"        
[11] "1_r20220810.tif"         "1_r20220818.tif"        
[13] "10_r20210707.tif"        "10_r20210715.tif"       
[15] "10_r20210720.tif"       

Note that, for 2021, there are six dates of image acquisition: 20210707, 20210715, 20210720,20210726, 20210802,20210825

8.5.6 Read the orthomosaics

Now, let’s read several plot images using the terra package:

# Open a tif collected on study plot 5 on  7th July 2021
tif1 <- "data/cercospora_data/multispec_2021_2022/1_r20210707.tif"
tif2 <- "data/cercospora_data/multispec_2021_2022/2_r20210707.tif"
tif3 <- "data/cercospora_data/multispec_2021_2022/3_r20210707.tif"
tif4 <- "data/cercospora_data/multispec_2021_2022/4_r20210707.tif"
tif5 <- "data/cercospora_data/multispec_2021_2022/5_r20210707.tif"
tif6 <- "data/cercospora_data/multispec_2021_2022/6_r20210707.tif"
tif7 <- "data/cercospora_data/multispec_2021_2022/7_r20210707.tif"
tif8 <- "data/cercospora_data/multispec_2021_2022/8_r20210707.tif"
tif9 <- "data/cercospora_data/multispec_2021_2022/9_r20210707.tif"
tif10 <- "data/cercospora_data/multispec_2021_2022/10_r20210707.tif"

It may be convenient to increase image pixel size (to make the raster lighter):

p01 <- terra::rast(tif1) |> aggregate(20)
p02 <- terra::rast(tif2) |> aggregate(20)
p03 <- terra::rast(tif3) |> aggregate(20)
p04 <- terra::rast(tif4) |> aggregate(20)
p05 <- terra::rast(tif5) |> aggregate(20)
p06 <- terra::rast(tif6) |> aggregate(20)
p07 <- terra::rast(tif7) |> aggregate(20)
p08 <- terra::rast(tif8) |> aggregate(20)
p09 <- terra::rast(tif9) |> aggregate(20)
p10 <- terra::rast(tif10) |> aggregate(20)

Let’s check what we get:

p01
class       : SpatRaster 
size        : 15, 9, 5  (nrow, ncol, nlyr)
resolution  : 0.212, 0.212  (x, y)
extent      : 334196.9, 334198.8, 4748990, 4748993  (xmin, xmax, ymin, ymax)
coord. ref. : WGS 84 / UTM zone 18N (EPSG:32618) 
source(s)   : memory
names       : 1_r20210707_1, 1_r20210707_2, 1_r20210707_3, 1_r20210707_4, 1_r20210707_5 
min values  :    0.01288329,    0.02743819,    0.01721323,    0.06647335,     0.1167282 
max values  :    0.03016389,    0.06923942,    0.07651012,    0.18791453,     0.4155874 

Note that each image has 5 bands with ~21 cm pixel size.

As the images have been split in plots, it may be useful to “mosaic” them.

# with many SpatRasters, make a SpatRasterCollection from a list
rlist <- list(p01, p02, p03, p04, p05, p06, p07, p08, p09, p10)
rsrc <- sprc(rlist)

m <- merge(rsrc)
Warning: [merge] 10 raster(s) that did not share the base geometry of the first
raster were resampled

What we got?

m
class       : SpatRaster 
size        : 209, 24, 5  (nrow, ncol, nlyr)
resolution  : 0.212, 0.212  (x, y)
extent      : 334193.8, 334198.9, 4748990, 4749035  (xmin, xmax, ymin, ymax)
coord. ref. : WGS 84 / UTM zone 18N (EPSG:32618) 
source(s)   : memory
names       : 1_r20210707_1, 1_r20210707_2, 1_r20210707_3, 1_r20210707_4, 1_r20210707_5 
min values  :    0.01390121,    0.02954650,    0.01960831,     0.0706145,     0.1283448 
max values  :    0.03741143,    0.08793678,    0.08386827,     0.2208381,     0.5030925 

Let’s check the band names:

names(m)
[1] "1_r20210707_1" "1_r20210707_2" "1_r20210707_3" "1_r20210707_4"
[5] "1_r20210707_5"

We will rename band names:

# Rename
m2 <- m |>
  rename(blue = "1_r20210707_1", green = "1_r20210707_2", 
         red = "1_r20210707_3", redge= "1_r20210707_4",
         nir = "1_r20210707_5")

Let’s get a summary of the mosaic:

summary(m2)
      blue            green             red             redge       
 Min.   :0.0139   Min.   :0.0295   Min.   :0.0196   Min.   :0.0706  
 1st Qu.:0.0189   1st Qu.:0.0516   1st Qu.:0.0291   1st Qu.:0.1133  
 Median :0.0222   Median :0.0576   Median :0.0410   Median :0.1378  
 Mean   :0.0228   Mean   :0.0578   Mean   :0.0431   Mean   :0.1391  
 3rd Qu.:0.0265   3rd Qu.:0.0639   3rd Qu.:0.0556   3rd Qu.:0.1645  
 Max.   :0.0374   Max.   :0.0879   Max.   :0.0839   Max.   :0.2208  
 NA's   :3896     NA's   :3896     NA's   :3896     NA's   :3896    
      nir        
 Min.   :0.1283  
 1st Qu.:0.2162  
 Median :0.2912  
 Mean   :0.2985  
 3rd Qu.:0.3767  
 Max.   :0.5031  
 NA's   :3896    
hist(m2)

Let’s visualize the mosaic:

ggplot() +
  geom_spatraster(data = m2) +
  facet_wrap(~lyr, ncol = 5) +
   r4pde::theme_r4pde(font_size = 10)+
  scale_fill_whitebox_c(
    palette = "muted",
    labels = scales::label_number(suffix = "%"),
    n.breaks = 10,
    guide = guide_legend(reverse = TRUE)
  ) +
  labs(
    fill = "",
    title = "UAV multispectral mosaic",
    subtitle = "07.07.2021"
  )

Let’s read all images collected at a later date:

# Open all images collected on  25th August 2021
tif1 <- "data/cercospora_data/multispec_2021_2022/1_r20210825.tif"
tif2 <- "data/cercospora_data/multispec_2021_2022/2_r20210825.tif"
tif3 <- "data/cercospora_data/multispec_2021_2022/3_r20210825.tif"
tif4 <- "data/cercospora_data/multispec_2021_2022/4_r20210825.tif"
tif5 <- "data/cercospora_data/multispec_2021_2022/5_r20210825.tif"
tif6 <- "data/cercospora_data/multispec_2021_2022/6_r20210825.tif"
tif7 <- "data/cercospora_data/multispec_2021_2022/7_r20210825.tif"
tif8 <- "data/cercospora_data/multispec_2021_2022/8_r20210825.tif"
tif9 <- "data/cercospora_data/multispec_2021_2022/9_r20210825.tif"
tif10 <- "data/cercospora_data/multispec_2021_2022/10_r20210825.tif"
p01 <- terra::rast(tif1) |> aggregate(20)
p02 <- terra::rast(tif2) |> aggregate(20)
p03 <- terra::rast(tif3) |> aggregate(20)
p04 <- terra::rast(tif4) |> aggregate(20)
p05 <- terra::rast(tif5) |> aggregate(20)
p06 <- terra::rast(tif6) |> aggregate(20)
p07 <- terra::rast(tif7) |> aggregate(20)
p08 <- terra::rast(tif8) |> aggregate(20)
p09 <- terra::rast(tif9) |> aggregate(20)
p10 <- terra::rast(tif10) |> aggregate(20)
# with many SpatRasters, make a SpatRasterCollection from a list
rlist <- list(p01, p02, p03, p04, p05, p06, p07, p08, p09, p10)
rsrc <- sprc(rlist)

mm <- merge(rsrc)
Warning: [merge] 9 raster(s) that did not share the base geometry of the first
raster were resampled
names(mm)
[1] "1_r20210825_1" "1_r20210825_2" "1_r20210825_3" "1_r20210825_4"
[5] "1_r20210825_5"
# Rename
mm2 <- mm |>
  rename(blue = "1_r20210825_1", green = "1_r20210825_2", 
         red = "1_r20210825_3", redge= "1_r20210825_4",
         nir = "1_r20210825_5")

Now, a visualization task:

ggplot() +
  geom_spatraster(data = mm2) +
  facet_wrap(~lyr, ncol = 5) +
  r4pde::theme_r4pde(font_size = 10)+
  scale_fill_whitebox_c(
    palette = "muted",
    labels = scales::label_number(suffix = "%"),
    n.breaks = 10,
    guide = guide_legend(reverse = TRUE)
  ) +
  labs(
    fill = "",
    title = "UAV multispectral mosaic",
    subtitle = "08.25.2021"
  )

8.5.7 Reading severity data

Now, let’s read the ground CLS data:

file <- "data/cercospora_data/CLS_DS.csv"
cls <- readr::read_csv(file, col_names = TRUE, show_col_types = FALSE)

What we got?

cls
# A tibble: 136 × 9
    Plot    D0    D1    D2    D3    D4    D5    D6  year
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1     1     0 0.25   0.9   4.58  8.75  21      NA  2021
 2     2     0 0.75   3.68 12.4  20.8   45.4    NA  2021
 3     3     0 2.92   7.38 18    34.6   65.2    NA  2021
 4     4     0 0.75   5.45 17.4  42.6   75.5    NA  2021
 5     5     0 3.38   3.45 15.4  17.2   55.2    NA  2021
 6     6     0 0.8    1.38  8.8  12.2   15.4    NA  2021
 7     7     0 0.375  3.05  9.1  13     31.0    NA  2021
 8     8     0 3.7    8.4  29.4  41.2   71.8    NA  2021
 9     9     0 2.38   4.28 13.2  15.6   38.1    NA  2021
10    10     0 2.22   2.28 30.2  17.3   68      NA  2021
# ℹ 126 more rows

Note that columns D0 to D6 refer to the disease severity recorded in percentage at different dates for each study plot. Note also that this object is structured in wide format (i.e. each row represents a single plot for each year).

unique(cls$Plot)
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
[51] 51 52 53 54 55 56

Let’s know the values stored in the year attribute:

unique(cls$year)
[1] 2021 2022 2023

8.5.8 Preparing a set of spectral covariables per plot & date

The 2021 data collection dates are 20210707, 20210715, 20210720, 20210802,and 20210825.

We will write a function that processes all the plot images of one date. For each image, the function reads the five bands, clamps the reflectance values to the range 0 to 1, computes several vegetation indices (VIs), and obtains the average value of the original bands and of the VIs. The results for all plots of the date are returned as a data frame with one row per plot.

process_date <- function(date_string) {
  # all plot images collected on the given date
  pattern <- paste0("*_r", format(as.Date(date_string), "%Y%m%d"), ".tif")
  files <- Sys.glob(file.path("data/cercospora_data/multispec_2021_2022", pattern))

  # plot number taken from the file name (files are sorted as text)
  plot_id <- as.integer(sub("_.*", "", basename(files)))

  means <- lapply(files, function(file) {
    r <- clamp(rast(file), 0, 1)
    ndvi <- (r[[5]] - r[[3]]) / (r[[5]] + r[[3]])
    ndre <- (r[[5]] - r[[4]]) / (r[[5]] + r[[4]])
    cire <- (r[[5]] / r[[4]]) - 1
    sipi <- (r[[5]] - r[[1]]) / (r[[5]] - r[[3]])
    ndvi_range <- minmax(ndvi)
    ndvi_mean <- global(ndvi, fun = "mean", na.rm = TRUE)[, 1]
    mndvi <- 2 + log((ndvi - ndvi_mean) / (ndvi_range[2] - ndvi_range[1]))
    all_layers <- c(r, ndvi, ndre, cire, sipi, mndvi)
    global(all_layers, fun = "mean", na.rm = TRUE)[, 1]
  })

  df <- as.data.frame(do.call(rbind, means))
  colnames(df) <- c("blue", "green", "red", "redge", "nir", "ndvi",
                    "ndre", "cire", "sipi", "mndvi")
  df$plot <- plot_id
  df$date <- as.Date(date_string)
  df
}

Now we apply the function to the five dates of 2021 and write one CSV file per date (D0_2021.csv to D4_2021.csv). This step reads all the images, so it can take several minutes.

dates_2021 <- c(D0 = "2021-07-07", D1 = "2021-07-15", D2 = "2021-07-20",
                D3 = "2021-08-02", D4 = "2021-08-25")

for (i in seq_along(dates_2021)) {
  covariates <- process_date(dates_2021[[i]])
  write_csv(covariates,
            paste0("data/cercospora_data/", names(dates_2021)[i], "_2021.csv"))
}

Let’s inspect the covariates of the first date:

read_csv("data/cercospora_data/D0_2021.csv", show_col_types = FALSE)
# A tibble: 40 × 12
     blue  green    red redge   nir  ndvi  ndre  cire  sipi mndvi  plot
    <dbl>  <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1 0.0234 0.0596 0.0436 0.144 0.313 0.699 0.352  1.14  1.17 0.336    10
 2 0.0228 0.0597 0.0443 0.143 0.300 0.691 0.340  1.07  1.18 0.383    11
 3 0.0223 0.0603 0.0430 0.144 0.304 0.699 0.342  1.08  1.17 0.300    12
 4 0.0221 0.0629 0.0427 0.149 0.318 0.712 0.348  1.11  1.16 0.364    13
 5 0.0231 0.0599 0.0462 0.139 0.285 0.676 0.331  1.02  1.19 0.444    14
 6 0.0224 0.0612 0.0415 0.146 0.312 0.720 0.350  1.12  1.15 0.249    15
 7 0.0209 0.0587 0.0400 0.141 0.302 0.724 0.352  1.13  1.15 0.301    16
 8 0.0213 0.0575 0.0399 0.137 0.296 0.716 0.355  1.14  1.15 0.341    17
 9 0.0219 0.0587 0.0409 0.141 0.302 0.712 0.350  1.12  1.16 0.326    18
10 0.0226 0.0598 0.0436 0.143 0.299 0.696 0.340  1.07  1.18 0.398    19
# ℹ 30 more rows
# ℹ 1 more variable: date <date>

8.5.9 Put all covariate data in a single object

Now, we will read all the recently created csv files and put all covariate data values into a single object.

Next code obtains a list with the path of every csv file matching the D*_2021.csv expression:

(csv_files <- Sys.glob("data/cercospora_data/D*_2021.csv"))
[1] "data/cercospora_data/D0_2021.csv" "data/cercospora_data/D1_2021.csv"
[3] "data/cercospora_data/D2_2021.csv" "data/cercospora_data/D3_2021.csv"
[5] "data/cercospora_data/D4_2021.csv"

Now, we will read all files in the list:

(df.2021 <- do.call(rbind,lapply(csv_files,read.csv)))
          blue      green        red     redge       nir      ndvi      ndre
1   0.02343657 0.05959132 0.04362133 0.1444027 0.3131252 0.6988392 0.3515343
2   0.02282100 0.05970168 0.04427105 0.1432050 0.3003496 0.6909051 0.3404127
3   0.02229088 0.06034372 0.04296553 0.1441267 0.3037592 0.6985894 0.3424855
4   0.02211413 0.06289556 0.04265936 0.1493579 0.3175088 0.7124582 0.3477512
5   0.02311490 0.05985614 0.04618947 0.1392655 0.2849417 0.6763379 0.3314924
6   0.02237584 0.06117992 0.04150024 0.1462904 0.3117826 0.7198424 0.3504247
7   0.02086133 0.05870957 0.03996187 0.1412853 0.3017970 0.7235433 0.3521370
8   0.02132975 0.05750314 0.03989218 0.1370614 0.2956135 0.7157179 0.3550798
9   0.02188219 0.05869401 0.04093412 0.1413798 0.3023368 0.7115535 0.3503842
10  0.02260319 0.05975366 0.04357307 0.1429067 0.2992025 0.6962978 0.3402171
11  0.02089238 0.04880216 0.04060995 0.1215226 0.2446871 0.6569093 0.3221139
12  0.02350888 0.05890453 0.04759297 0.1368886 0.2807072 0.6557864 0.3276572
13  0.02218707 0.05638867 0.04349103 0.1341888 0.2819767 0.6780262 0.3441717
14  0.02275558 0.05684062 0.04544145 0.1339135 0.2753768 0.6666547 0.3337416
15  0.02272742 0.05738843 0.04389310 0.1379283 0.2873050 0.6838893 0.3418451
16  0.02140498 0.05666796 0.04005207 0.1370657 0.2937609 0.7116084 0.3544429
17  0.02086186 0.05736772 0.03807191 0.1398500 0.2991122 0.7290118 0.3560801
18  0.02015755 0.05423116 0.03848824 0.1311203 0.2787107 0.7109047 0.3537186
19  0.02193068 0.05637152 0.03996171 0.1306193 0.2834647 0.7008749 0.3578544
20  0.02214865 0.05725453 0.04177672 0.1331511 0.2830351 0.6914292 0.3488766
21  0.02122350 0.05473366 0.04193676 0.1295753 0.2737450 0.6830712 0.3463346
22  0.02200368 0.05443210 0.04510647 0.1327359 0.2787727 0.6671694 0.3387558
23  0.02334062 0.05691194 0.04427723 0.1309551 0.2608062 0.6588717 0.3212313
24  0.02211155 0.06021804 0.03976967 0.1403651 0.3081556 0.7156574 0.3615820
25  0.02174683 0.05562259 0.03816646 0.1312835 0.2858236 0.7057869 0.3560048
26  0.02082576 0.05881460 0.03393639 0.1407333 0.3136037 0.7548543 0.3704691
27  0.02192849 0.06039847 0.03532052 0.1438516 0.3201219 0.7514753 0.3694975
28  0.02302246 0.06453867 0.03590782 0.1454413 0.3360538 0.7627826 0.3862154
29  0.02060561 0.05634195 0.03435760 0.1348848 0.3139023 0.7577029 0.3915793
30  0.02453648 0.05776692 0.04101824 0.1400822 0.3165895 0.7123657 0.3729309
31  0.02449699 0.06516875 0.04320010 0.1488943 0.3369249 0.7188490 0.3734588
32  0.02375349 0.06403329 0.04276755 0.1462964 0.3370367 0.7210620 0.3804087
33  0.02255618 0.05711345 0.04411843 0.1383896 0.2971490 0.6880898 0.3481446
34  0.02217878 0.05772793 0.04388026 0.1337953 0.2885867 0.6798194 0.3490214
35  0.02187461 0.05617408 0.04287472 0.1365350 0.2917336 0.6953013 0.3481033
36  0.02296074 0.05817811 0.04563811 0.1391187 0.2921268 0.6801861 0.3393280
37  0.02267559 0.05957335 0.04213828 0.1420391 0.3082079 0.7142905 0.3568940
38  0.02360669 0.05999425 0.04294853 0.1433218 0.3103642 0.7052194 0.3528355
39  0.02364231 0.06191251 0.04298429 0.1483199 0.3220375 0.7163665 0.3560244
40  0.02427710 0.06139374 0.04394927 0.1444968 0.3085851 0.6985304 0.3476681
41  0.02052956 0.05020432 0.02973388 0.1338569 0.3185785 0.7882094 0.3981185
42  0.01859800 0.05052206 0.03105854 0.1318898 0.3058317 0.7805916 0.3922774
43  0.01981702 0.05348824 0.03288278 0.1322381 0.3022270 0.7638127 0.3819167
44  0.02000617 0.05614472 0.03134368 0.1402745 0.3260600 0.7887921 0.3904000
45  0.02095564 0.05658479 0.03321928 0.1439913 0.3291102 0.7828250 0.3835718
46  0.02237261 0.05968790 0.03353345 0.1488491 0.3470870 0.7903656 0.3917955
47  0.02100918 0.05998822 0.03180204 0.1560300 0.3643333 0.8096700 0.3941080
48  0.02181143 0.05995075 0.03243477 0.1516184 0.3587694 0.8000431 0.3988671
49  0.02132649 0.05706844 0.03089749 0.1471923 0.3448123 0.8004532 0.3926974
50  0.02159894 0.05753348 0.03114798 0.1416646 0.3302551 0.7919107 0.3914513
51  0.02038298 0.04942617 0.03205623 0.1217379 0.2899206 0.7603175 0.3967210
52  0.02266377 0.05830423 0.03712530 0.1481954 0.3369567 0.7581285 0.3783657
53  0.02015731 0.04875384 0.03535302 0.1201503 0.2717107 0.7356596 0.3799353
54  0.02165452 0.05001698 0.03766276 0.1215014 0.2670488 0.7155948 0.3650533
55  0.02265802 0.05245606 0.03529764 0.1328123 0.3006981 0.7540973 0.3802748
56  0.02051525 0.05213257 0.03268753 0.1382642 0.3256237 0.7833498 0.3957619
57  0.02128752 0.05645641 0.03279428 0.1483382 0.3501462 0.7989040 0.4000442
58  0.02095490 0.05585342 0.03271893 0.1444677 0.3498758 0.7997719 0.4113192
59  0.02185206 0.05682143 0.03272919 0.1417155 0.3463017 0.7920995 0.4109469
60  0.02202972 0.05714222 0.03436180 0.1461714 0.3441786 0.7825692 0.3957799
61  0.02098234 0.05483850 0.03241813 0.1348340 0.3203631 0.7814717 0.3997993
62  0.01901755 0.04792084 0.02924172 0.1168986 0.2677576 0.7610976 0.3815744
63  0.02388539 0.05884962 0.03938045 0.1462760 0.3194442 0.7426530 0.3626077
64  0.02140199 0.05705007 0.03633223 0.1389278 0.3217712 0.7609113 0.3886500
65  0.02201808 0.05953653 0.03894399 0.1374054 0.3171746 0.7461247 0.3899800
66  0.02075849 0.05788808 0.03145918 0.1467951 0.3700974 0.8162132 0.4289832
67  0.02015069 0.05689458 0.03134979 0.1515357 0.3765375 0.8211904 0.4239588
68  0.02026395 0.05835547 0.03137467 0.1573338 0.3931594 0.8284231 0.4268455
69  0.01992303 0.05484262 0.03180804 0.1509191 0.3793238 0.8212715 0.4326179
70  0.02065776 0.05741422 0.03358031 0.1470773 0.3617977 0.7967555 0.4185559
71  0.02220296 0.06168994 0.03496599 0.1541648 0.3697565 0.7967047 0.4086072
72  0.02038571 0.05698300 0.03187045 0.1479112 0.3529448 0.8037041 0.4055701
73  0.01967439 0.04903127 0.02905272 0.1239711 0.2874766 0.7748027 0.3851905
74  0.02137720 0.05585394 0.03576933 0.1306864 0.3214592 0.7630346 0.4159502
75  0.01963094 0.04868798 0.03043499 0.1312665 0.3107001 0.7820182 0.3978929
76  0.02113100 0.05204241 0.03232803 0.1335988 0.3020883 0.7639990 0.3756051
77  0.02048588 0.05105247 0.03119704 0.1411513 0.3349993 0.7918959 0.3975757
78  0.02243174 0.05674670 0.03130302 0.1461549 0.3674505 0.8055775 0.4201542
79  0.02187014 0.05383427 0.03043378 0.1401986 0.3415687 0.7977970 0.4083110
80  0.02188459 0.05444195 0.02963870 0.1365380 0.3385607 0.8036543 0.4163733
81  0.02108185 0.05315597 0.03629927 0.1615935 0.4061288 0.8110457 0.4222097
82  0.02107638 0.05629456 0.03690022 0.1572443 0.3762103 0.7876093 0.4011143
83  0.02178106 0.05907055 0.03867815 0.1628041 0.3881885 0.7835115 0.3986314
84  0.02047992 0.06098736 0.03665144 0.1691727 0.4158930 0.8096072 0.4135806
85  0.02129717 0.05862935 0.03736545 0.1607825 0.3928958 0.7985510 0.4127268
86  0.02194515 0.06127932 0.03643291 0.1634409 0.4002293 0.8077606 0.4146739
87  0.02097622 0.06260348 0.03316698 0.1759699 0.4279086 0.8375735 0.4146203
88  0.02082045 0.05968328 0.03347267 0.1692592 0.4194361 0.8302575 0.4193664
89  0.02052110 0.05855188 0.03285183 0.1667946 0.4183311 0.8334915 0.4252922
90  0.02118406 0.05952772 0.03409333 0.1669941 0.4164679 0.8290411 0.4224324
91  0.02066534 0.05198353 0.03630887 0.1469969 0.3602886 0.7819583 0.4100563
92  0.02286327 0.06025029 0.03771215 0.1674188 0.4105733 0.8133905 0.4146804
93  0.02418177 0.06143854 0.04705647 0.1632921 0.3503713 0.7270033 0.3567380
94  0.02357276 0.05738955 0.04634505 0.1583690 0.3343389 0.7189932 0.3482989
95  0.02251601 0.05564380 0.04242904 0.1626545 0.3609011 0.7562440 0.3706428
96  0.02082985 0.05493789 0.03747858 0.1581995 0.3747444 0.7851167 0.3976251
97  0.02073972 0.05667054 0.03645775 0.1673818 0.3937831 0.8025926 0.3977367
98  0.02024999 0.05755859 0.03581590 0.1662561 0.4091635 0.8133004 0.4166660
99  0.02082345 0.05814276 0.03632322 0.1663019 0.4077442 0.8067838 0.4127645
100 0.02130631 0.05952263 0.03733125 0.1691712 0.4058952 0.7983298 0.4039078
101 0.02079688 0.05889001 0.03666624 0.1697102 0.3959562 0.8020224 0.3937777
102 0.02154315 0.05398097 0.03832096 0.1526989 0.3727947 0.7756811 0.4090068
103 0.02068967 0.05666765 0.04165745 0.1675508 0.3665856 0.7608506 0.3641168
104 0.02287252 0.06036144 0.04135329 0.1691424 0.3596196 0.7572024 0.3523070
105 0.02327678 0.05737544 0.04043948 0.1639254 0.3273054 0.7462585 0.3248970
106 0.02055421 0.05506976 0.03165367 0.1679061 0.3830571 0.8245731 0.3848886
107 0.02045120 0.05556580 0.03147341 0.1721244 0.3911810 0.8288812 0.3848388
108 0.01936702 0.05359267 0.03041155 0.1670385 0.4003922 0.8384306 0.4065918
109 0.01892969 0.05400171 0.03181207 0.1714764 0.3980453 0.8328814 0.3948891
110 0.01908086 0.05353836 0.03287591 0.1710066 0.3968207 0.8205218 0.3920708
111 0.02038547 0.05592239 0.03392618 0.1781330 0.4067277 0.8204882 0.3854746
112 0.02008451 0.05720333 0.03208596 0.1750760 0.4149822 0.8331989 0.4011998
113 0.02067345 0.05449945 0.03739998 0.1559458 0.3971303 0.7953698 0.4269298
114 0.01985213 0.05373437 0.03486695 0.1637985 0.3662829 0.7934669 0.3733396
115 0.02132429 0.05593877 0.03814748 0.1559295 0.3894839 0.7920895 0.4204294
116 0.02137609 0.05544646 0.03941156 0.1535504 0.3745666 0.7752996 0.4094564
117 0.02173253 0.05606452 0.03581073 0.1600026 0.3982399 0.8091809 0.4198104
118 0.01983960 0.05239147 0.03299102 0.1556517 0.3980271 0.8196374 0.4295256
119 0.01958886 0.05078259 0.03284344 0.1529905 0.3931337 0.8197849 0.4326471
120 0.02143953 0.05433169 0.03362620 0.1559428 0.4089219 0.8242763 0.4413151
121 0.02456960 0.05829370 0.03337980 0.2035489 0.4674467 0.8522272 0.3940106
122 0.02227492 0.05626306 0.03402620 0.1924762 0.4087379 0.8252356 0.3618694
123 0.02523303 0.06403575 0.03893872 0.1969021 0.4110210 0.8061321 0.3530707
124 0.02431507 0.06981933 0.03149349 0.2080660 0.4400168 0.8538962 0.3608609
125 0.02721698 0.06959043 0.04073738 0.2240817 0.4563009 0.8229697 0.3463280
126 0.02798405 0.07293051 0.04063069 0.2243524 0.4512955 0.8183378 0.3413659
127 0.02362352 0.06819463 0.03388499 0.2225483 0.4584950 0.8516509 0.3536612
128 0.02337971 0.06235514 0.03212167 0.2017982 0.4440984 0.8520046 0.3771044
129 0.02266283 0.06217227 0.03157511 0.2027005 0.4475631 0.8549204 0.3785745
130 0.02366451 0.06000658 0.03321898 0.1894632 0.4401444 0.8441077 0.4001559
131 0.02513064 0.06334645 0.03448836 0.1972631 0.4274775 0.8350754 0.3716506
132 0.02377771 0.05588092 0.03341388 0.1794289 0.4061702 0.8257592 0.3845869
133 0.02449490 0.05809191 0.04298490 0.1784245 0.3147830 0.7299304 0.2795985
134 0.02731799 0.05988043 0.04708922 0.1774982 0.3350545 0.7195767 0.3055429
135 0.02619083 0.05744756 0.03728376 0.1842603 0.3627074 0.7868434 0.3266340
136 0.02581965 0.06053502 0.03775902 0.1914101 0.3971750 0.8019641 0.3518380
137 0.02561499 0.06698112 0.04009003 0.2271990 0.4421288 0.8138778 0.3278621
138 0.02400703 0.06453540 0.03525491 0.2070071 0.4443489 0.8351464 0.3707701
139 0.02158047 0.05805170 0.02776623 0.1873562 0.4307834 0.8663875 0.3984368
140 0.02182744 0.05941715 0.02958924 0.1884719 0.4182472 0.8503351 0.3807011
141 0.02015692 0.05312046 0.02761516 0.1659301 0.3789922 0.8446382 0.3930457
142 0.02510567 0.06159803 0.03564669 0.1842460 0.3929318 0.8109585 0.3607708
143 0.01993199 0.04180365 0.02775356 0.1207182 0.2949298 0.8009226 0.4183965
144 0.02300851 0.05705218 0.03661990 0.1733665 0.3311235 0.7724715 0.3138201
145 0.02476484 0.05523785 0.03972681 0.1718589 0.3296071 0.7534944 0.3139303
146 0.02154290 0.05242739 0.03053955 0.1765510 0.3719669 0.8277281 0.3569453
147 0.02210420 0.05258662 0.03051192 0.1727336 0.3759095 0.8276559 0.3698586
148 0.02330147 0.05844441 0.03332284 0.1898374 0.4210377 0.8347262 0.3800209
149 0.02282903 0.05852711 0.03503826 0.2062768 0.4417818 0.8371609 0.3690559
150 0.02201556 0.05756078 0.03422850 0.1881062 0.4182142 0.8332135 0.3817618
151 0.02389892 0.05898878 0.03347903 0.1847862 0.4078247 0.8310272 0.3799784
152 0.02083712 0.05540730 0.02953161 0.1768605 0.3941414 0.8413166 0.3825045
153 0.02458310 0.06176598 0.03476932 0.1976596 0.4174477 0.8280028 0.3586799
154 0.01718078 0.04930132 0.02856733 0.1486271 0.3652903 0.8334064 0.4221488
155 0.02748465 0.06946837 0.04363258 0.2318181 0.4706404 0.8123663 0.3418064
156 0.02747831 0.07332335 0.04389518 0.2264001 0.4458781 0.7968273 0.3272567
157 0.02521912 0.06154772 0.03600436 0.2019781 0.4287934 0.8282891 0.3619903
158 0.02434867 0.06283113 0.03451642 0.2081286 0.4614013 0.8472470 0.3825518
159 0.02530022 0.05947328 0.03604093 0.2065795 0.4364578 0.8318662 0.3616867
160 0.02323038 0.06380880 0.03683221 0.2106691 0.4795214 0.8422059 0.3925382
161 0.03244730 0.06243503 0.07069395 0.1331881 0.2240197 0.5263451 0.2603764
162 0.03220764 0.06421227 0.07358433 0.1305611 0.2126103 0.4872965 0.2416332
163 0.03148392 0.06400177 0.06896637 0.1465971 0.2389044 0.5390938 0.2399182
164 0.03167203 0.06808206 0.06547172 0.1751177 0.3086365 0.6280323 0.2732817
165 0.03320107 0.06398255 0.07626774 0.1272121 0.1949513 0.4500080 0.2154965
166 0.03155779 0.06765107 0.06388105 0.1640356 0.2689753 0.6018466 0.2430515
167 0.03049306 0.06404963 0.06585743 0.1490913 0.2448651 0.5740527 0.2454515
168 0.03252230 0.06384611 0.06648974 0.1375281 0.2243945 0.5383073 0.2420900
169 0.02987872 0.05902407 0.06794822 0.1251337 0.2040113 0.5055839 0.2431990
170 0.03446312 0.06983649 0.07757790 0.1447033 0.2299757 0.5042939 0.2310344
171 0.03317173 0.06795603 0.06613100 0.1632761 0.3060204 0.6192177 0.2999994
172 0.03227812 0.06842014 0.07143898 0.1660777 0.2665647 0.5637743 0.2309203
173 0.03398287 0.07115492 0.07250453 0.1389441 0.2177167 0.5067603 0.2260623
174 0.02865669 0.06429306 0.05747477 0.1574716 0.2590684 0.6237297 0.2431701
175 0.03021290 0.06066011 0.06666227 0.1384870 0.2170173 0.5271643 0.2225675
176 0.02967587 0.06158339 0.06095244 0.1500869 0.2465646 0.5848673 0.2416536
177 0.02884645 0.05705891 0.06533360 0.1202913 0.1869982 0.4882258 0.2250775
178 0.03276704 0.06359781 0.07456907 0.1251366 0.1891989 0.4570189 0.2115906
179 0.02708242 0.05999907 0.05766794 0.1504990 0.2450337 0.5946147 0.2374369
180 0.02766003 0.05688758 0.06456498 0.1280580 0.2001966 0.5069556 0.2206031
181 0.02838966 0.05722613 0.06488701 0.1271373 0.1992304 0.5115626 0.2244217
182 0.03559652 0.06614677 0.07592767 0.1315287 0.2143333 0.4807597 0.2449726
183 0.03122141 0.06389911 0.07011235 0.1232556 0.1820245 0.4541211 0.1994603
184 0.02919906 0.06398205 0.06071808 0.1454490 0.2416222 0.5913800 0.2485037
185 0.02982668 0.06742336 0.06261795 0.1664833 0.2768481 0.6132203 0.2448847
186 0.02978697 0.05961399 0.06366181 0.1285860 0.2094327 0.5358823 0.2420457
187 0.02802117 0.05635194 0.05964353 0.1242803 0.2039935 0.5432331 0.2473246
188 0.02804431 0.05990398 0.05714082 0.1520841 0.2605731 0.6137755 0.2602729
189 0.03115430 0.06222985 0.06680599 0.1226242 0.1971786 0.5040743 0.2421152
190 0.03017063 0.05987192 0.06688301 0.1147182 0.1808972 0.4625647 0.2312153
191 0.02969164 0.06141537 0.06461569 0.1333502 0.2173871 0.5265676 0.2410969
192 0.03245863 0.06580556 0.07369124 0.1179781 0.1793431 0.4283260 0.2142952
193 0.03780640 0.06896099 0.08072512 0.1249145 0.2023160 0.4538598 0.2488864
194 0.03015803 0.06140500 0.06845817 0.1301699 0.2018116 0.4929597 0.2222330
195 0.03273967 0.06385214 0.07184862 0.1220430 0.2069285 0.4991207 0.2646952
196 0.03340377 0.06654732 0.07474162 0.1302466 0.2149186 0.4934244 0.2531492
197 0.03576925 0.07247605 0.07037803 0.1731286 0.3140637 0.6212680 0.2859256
198 0.02975048 0.05792985 0.06628184 0.1342174 0.2345209 0.5560936 0.2760421
199 0.03765677 0.07335121 0.08514252 0.1333163 0.2088120 0.4441819 0.2313678
200 0.03487981 0.06780985 0.07374642 0.1484053 0.2625147 0.5622498 0.2799192
         cire     sipi         mndvi plot       date
1   1.1415594 1.168504  0.3357828097   10 2021-07-07
2   1.0743886 1.178469  0.3831091052   11 2021-07-07
3   1.0801215 1.171007  0.3004619363   12 2021-07-07
4   1.1075915 1.160622  0.3637140071   13 2021-07-07
5   1.0232721 1.188881  0.4442368504   14 2021-07-07
6   1.1150448 1.146710  0.2486349703   15 2021-07-07
7   1.1250963 1.147409  0.3010096154   16 2021-07-07
8   1.1420617 1.153974  0.3411934009   17 2021-07-07
9   1.1186718 1.158002  0.3256349981   18 2021-07-07
10  1.0709430 1.177161  0.3981083214   19 2021-07-07
11  0.9989965 1.208993  0.4992914029    1 2021-07-07
12  1.0246774 1.222624  0.5812196925   20 2021-07-07
13  1.0917440 1.187306  0.5261465375   21 2021-07-07
14  1.0412629 1.199819  0.5088588230   22 2021-07-07
15  1.0772731 1.179034  0.3970453680   23 2021-07-07
16  1.1437437 1.152637  0.4036515029   24 2021-07-07
17  1.1446112 1.135257  0.1922804965   25 2021-07-07
18  1.1474183 1.154173  0.3741285225   26 2021-07-07
19  1.1670434 1.164372  0.2192195769   27 2021-07-07
20  1.1211586 1.180103  0.2670115421   28 2021-07-07
21  1.1068452 1.183483  0.3921222267   29 2021-07-07
22  1.0722585 1.202996  0.4383503430    2 2021-07-07
23  1.0088580 1.212415  0.3244785295   30 2021-07-07
24  1.1826306 1.145451  0.3773438604   31 2021-07-07
25  1.1565180 1.151022  0.3542278410   32 2021-07-07
26  1.2309874 1.106173  0.1469966333   33 2021-07-07
27  1.2197816 1.108914  0.0840190302   34 2021-07-07
28  1.3102119 1.094956  0.0132994128   35 2021-07-07
29  1.3468557 1.104615  0.0978333987   36 2021-07-07
30  1.2481819 1.141349  0.3183810870   37 2021-07-07
31  1.2416056 1.144968  0.3515716385   38 2021-07-07
32  1.2898444 1.144735  0.3309417213   39 2021-07-07
33  1.1165162 1.181832  0.4072417552    3 2021-07-07
34  1.1218080 1.190335  0.4463134659   40 2021-07-07
35  1.1116724 1.169715  0.3786825647    4 2021-07-07
36  1.0754732 1.183114  0.4450529648    5 2021-07-07
37  1.1581654 1.149740  0.3065144945    6 2021-07-07
38  1.1404401 1.158901  0.2235725778    7 2021-07-07
39  1.1538314 1.148876  0.2117349834    8 2021-07-07
40  1.1171347 1.163275  0.2623829017    9 2021-07-07
41  1.3782598 1.071387  0.0205864548   10 2021-07-15
42  1.3566259 1.090718  0.0250170201   11 2021-07-15
43  1.2891365 1.100060  0.0061431869   12 2021-07-15
44  1.3329724 1.079262 -0.0686704756   13 2021-07-15
45  1.2906723 1.080714 -0.0450803877   14 2021-07-15
46  1.3350544 1.073452 -0.1398443835   15 2021-07-15
47  1.3428199 1.063935 -0.3200177751   16 2021-07-15
48  1.3698651 1.068173 -0.1140547795   17 2021-07-15
49  1.3478594 1.066137 -0.1478313020   18 2021-07-15
50  1.3304157 1.069318 -0.1254230424   19 2021-07-15
51  1.3809846 1.095432  0.1445930197    1 2021-07-15
52  1.2723543 1.104388  0.1423226946   20 2021-07-15
53  1.2872960 1.120003  0.2427405562   21 2021-07-15
54  1.1995585 1.131178  0.2966248439   22 2021-07-15
55  1.2711511 1.095153 -0.0224570025   23 2021-07-15
56  1.3672315 1.081331  0.0539492552   24 2021-07-15
57  1.3914122 1.068620 -0.1999835409   25 2021-07-15
58  1.4482764 1.069124 -0.0812479746   26 2021-07-15
59  1.4574950 1.071044 -0.2049412495   27 2021-07-15
60  1.3665155 1.081635 -0.1793357879   28 2021-07-15
61  1.3814683 1.078394 -0.1324388877   29 2021-07-15
62  1.2858749 1.089837  0.0458092649    2 2021-07-15
63  1.2022264 1.116328  0.0183683589   30 2021-07-15
64  1.3347470 1.102757  0.1305738483   31 2021-07-15
65  1.3490326 1.109362  0.0378045532   32 2021-07-15
66  1.5758712 1.057704 -0.3042757279   33 2021-07-15
67  1.5293288 1.059005 -0.3881285964   34 2021-07-15
68  1.5529622 1.055250 -0.3992933887   35 2021-07-15
69  1.6024960 1.060230 -0.3792228663   36 2021-07-15
70  1.5128737 1.077600 -0.0678144716   37 2021-07-15
71  1.4460478 1.075913 -0.1745530817   38 2021-07-15
72  1.4243774 1.070826 -0.2386634167   39 2021-07-15
73  1.3121340 1.080626  0.0282156325    3 2021-07-15
74  1.5050103 1.102703  0.0438634149   40 2021-07-15
75  1.3785072 1.079370  0.0274106440    4 2021-07-15
76  1.2548895 1.087083  0.1502160095    5 2021-07-15
77  1.3748307 1.074167 -0.0794766912    6 2021-07-15
78  1.5042189 1.058518 -0.2078983833    7 2021-07-15
79  1.4313024 1.060803 -0.1346557992    8 2021-07-15
80  1.4784583 1.055124 -0.2510148625    9 2021-07-15
81  1.5091684 1.065504 -0.1696912521   10 2021-07-20
82  1.3854419 1.085917 -0.0483153431   11 2021-07-20
83  1.3605557 1.087327 -0.0953645846   12 2021-07-20
84  1.4496963 1.074684 -0.1508310510   13 2021-07-20
85  1.4437110 1.076338 -0.1012477650   14 2021-07-20
86  1.4500552 1.068223 -0.2495194123   15 2021-07-20
87  1.4430745 1.050196 -0.5378942151   16 2021-07-20
88  1.4756373 1.054488 -0.3193034281   17 2021-07-20
89  1.5209923 1.051677 -0.3029425461   18 2021-07-20
90  1.4934348 1.053301 -0.3581289293   19 2021-07-20
91  1.4544218 1.090791  0.0765274948    1 2021-07-20
92  1.4523725 1.056664 -0.1852615254   20 2021-07-20
93  1.1368177 1.130660  0.2412537373   21 2021-07-20
94  1.0923325 1.135407  0.2240251204   22 2021-07-20
95  1.2074760 1.108039 -0.0651335365   23 2021-07-20
96  1.3581943 1.088449  0.0090607709   24 2021-07-20
97  1.3492680 1.074566 -0.2128007022   25 2021-07-20
98  1.4664407 1.070300 -0.2144020219   26 2021-07-20
99  1.4447817 1.075076 -0.2931720787   27 2021-07-20
100 1.3892259 1.080070 -0.0064584782   28 2021-07-20
101 1.3287352 1.076244 -0.1350575729   29 2021-07-20
102 1.4399733 1.093076  0.1324019681    2 2021-07-20
103 1.1796431 1.110340 -0.0759637430   30 2021-07-20
104 1.1143660 1.106180 -0.1119407775   31 2021-07-20
105 0.9838179 1.098887 -0.0819634859   32 2021-07-20
106 1.2786321 1.050752 -0.3987374946   33 2021-07-20
107 1.2778733 1.050203 -0.4709627746   34 2021-07-20
108 1.3963778 1.047230 -0.5005099302   35 2021-07-20
109 1.3281949 1.052344 -0.4394932170   36 2021-07-20
110 1.3181372 1.062573 -0.1964393774   37 2021-07-20
111 1.2844313 1.061355 -0.3757807604   38 2021-07-20
112 1.3688477 1.052621 -0.4935354112   39 2021-07-20
113 1.5577239 1.084939  0.0619995742    3 2021-07-20
114 1.2300633 1.080936 -0.0512569619   40 2021-07-20
115 1.4995324 1.082575  0.0467589503    4 2021-07-20
116 1.4469041 1.095501  0.1295138163    5 2021-07-20
117 1.4935743 1.069802 -0.1673312381    6 2021-07-20
118 1.5547877 1.065488 -0.2913712994    7 2021-07-20
119 1.5787545 1.062266 -0.2423790191    8 2021-07-20
120 1.6324658 1.056176 -0.4109303033    9 2021-07-20
121 1.3324811 1.031138 -0.9329644398   10 2021-08-02
122 1.1790729 1.053371 -0.4702134438   11 2021-08-02
123 1.1274064 1.062791 -0.4919324211   12 2021-08-02
124 1.1642643 1.030428 -0.9219435727   13 2021-08-02
125 1.0878222 1.058747 -0.8734865582   14 2021-08-02
126 1.0703626 1.050969 -0.4580022808   15 2021-08-02
127 1.1245460 1.036121 -0.8471248176   16 2021-08-02
128 1.2452050 1.035747 -0.8700951548   17 2021-08-02
129 1.2534928 1.035822 -0.7559168262   18 2021-08-02
130 1.3741697 1.042222 -1.0814628753   19 2021-08-02
131 1.2310151 1.041681 -0.5201193156    1 2021-08-02
132 1.2930730 1.047657 -0.4141809408   20 2021-08-02
133 0.8095930 1.115223  0.0056911166   21 2021-08-02
134 0.9136160 1.124892 -0.0245878304   22 2021-08-02
135 1.0016757 1.064336 -0.4527557321   23 2021-08-02
136 1.1278397 1.061822 -0.7375545901   24 2021-08-02
137 1.0184010 1.056483 -0.5740803039   25 2021-08-02
138 1.2284968 1.044680 -0.7155234903   26 2021-08-02
139 1.3590637 1.024674 -1.0858622822   27 2021-08-02
140 1.2615503 1.034384 -0.7847178421   28 2021-08-02
141 1.3249284 1.035002 -0.7231518295   29 2021-08-02
142 1.1773176 1.054953 -0.4577592358    2 2021-08-02
143 1.5202302 1.051947 -0.4565179757   30 2021-08-02
144 0.9398590 1.076097 -0.2878019011   31 2021-08-02
145 0.9409901 1.084722 -0.2876957587   32 2021-08-02
146 1.1373376 1.042886 -0.6047624726   33 2021-08-02
147 1.2034997 1.049083 -0.9068635437   34 2021-08-02
148 1.2560443 1.043206 -0.8599549281   35 2021-08-02
149 1.2022205 1.045737 -0.9818120336   36 2021-08-02
150 1.2628642 1.056609 -0.9147740655   37 2021-08-02
151 1.2545735 1.044365 -0.8599796085   38 2021-08-02
152 1.2602425 1.039036 -0.5206771201   39 2021-08-02
153 1.1628946 1.045994 -0.5744413976    3 2021-08-02
154 1.5433084 1.053258 -0.6361958420   40 2021-08-02
155 1.0764829 1.063901 -0.5624987735    4 2021-08-02
156 1.0193597 1.074447 -0.3103782430    5 2021-08-02
157 1.1817977 1.046306 -0.6498980336    6 2021-08-02
158 1.2882047 1.039040 -0.8562301124    7 2021-08-02
159 1.1714568 1.043690 -0.7183310744    8 2021-08-02
160 1.3420360 1.049136 -0.7612609133    9 2021-08-02
161 0.7298258 1.324342  0.0698481711   10 2021-08-25
162 0.6602580 1.393986  0.1635182319   11 2021-08-25
163 0.6482139 1.304943  0.0540153634   12 2021-08-25
164 0.7777530 1.215610  0.0089946689   13 2021-08-25
165 0.5605548 1.425463 -0.2514779990   14 2021-08-25
166 0.6562813 1.223703 -0.0269560659   15 2021-08-25
167 0.6699439 1.264781  0.0280182172   16 2021-08-25
168 0.6564857 1.298317  0.0010159223   17 2021-08-25
169 0.6654889 1.360910  0.0434079012   18 2021-08-25
170 0.6200022 1.385264  0.2062654531   19 2021-08-25
171 0.9131850 1.225855  0.0860920107    1 2021-08-25
172 0.6216048 1.304122  0.1443419035   20 2021-08-25
173 0.5981404 1.337526 -0.1961771307   21 2021-08-25
174 0.6564541 1.196577 -0.1139408403   22 2021-08-25
175 0.5855172 1.316696 -0.1561919432   23 2021-08-25
176 0.6572801 1.270045  0.1313856963   24 2021-08-25
177 0.6019302 1.370359 -0.0051956405   25 2021-08-25
178 0.5535844 1.444511 -0.0308850317   26 2021-08-25
179 0.6428781 1.260062  0.0416937059   27 2021-08-25
180 0.5872713 1.376995  0.1208025802   28 2021-08-25
181 0.5958389 1.351988 -0.0581929352   29 2021-08-25
182 0.6723947 1.366653  0.0264775382    2 2021-08-25
183 0.5166089 1.415353 -0.0897205516   30 2021-08-25
184 0.6779860 1.231409  0.0007949469   31 2021-08-25
185 0.6634278 1.216019 -0.0764145067   32 2021-08-25
186 0.6564104 1.294261 -0.0502768648   33 2021-08-25
187 0.6858638 1.292920  0.0990268744   34 2021-08-25
188 0.7262786 1.226937  0.0094243580   35 2021-08-25
189 0.6753764 1.339246  0.0401994088   36 2021-08-25
190 0.6312105 1.419802  0.1535082537   37 2021-08-25
191 0.6580441 1.335205  0.0355454843   38 2021-08-25
192 0.5638364 1.474723  0.0133503191   39 2021-08-25
193 0.6897908 1.399921  0.0388698356    3 2021-08-25
194 0.6005139 1.374915 -0.0782489060   40 2021-08-25
195 0.7484466 1.346672  0.0512449521    4 2021-08-25
196 0.7062528 1.353491  0.0458086211    5 2021-08-25
197 0.8448268 1.237137  0.1118462609    6 2021-08-25
198 0.7923682 1.295075  0.0697294939    7 2021-08-25
199 0.6194196 1.437227  0.1443525005    8 2021-08-25
200 0.8111978 1.278937  0.1160957419    9 2021-08-25

Then, we will convert the dataframe into a tibble:

covar.2021 <- tibble(df.2021)

Let’s check what we got:

covar.2021
# A tibble: 200 × 12
     blue  green    red redge   nir  ndvi  ndre  cire  sipi mndvi  plot date    
    <dbl>  <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int> <chr>   
 1 0.0234 0.0596 0.0436 0.144 0.313 0.699 0.352  1.14  1.17 0.336    10 2021-07…
 2 0.0228 0.0597 0.0443 0.143 0.300 0.691 0.340  1.07  1.18 0.383    11 2021-07…
 3 0.0223 0.0603 0.0430 0.144 0.304 0.699 0.342  1.08  1.17 0.300    12 2021-07…
 4 0.0221 0.0629 0.0427 0.149 0.318 0.712 0.348  1.11  1.16 0.364    13 2021-07…
 5 0.0231 0.0599 0.0462 0.139 0.285 0.676 0.331  1.02  1.19 0.444    14 2021-07…
 6 0.0224 0.0612 0.0415 0.146 0.312 0.720 0.350  1.12  1.15 0.249    15 2021-07…
 7 0.0209 0.0587 0.0400 0.141 0.302 0.724 0.352  1.13  1.15 0.301    16 2021-07…
 8 0.0213 0.0575 0.0399 0.137 0.296 0.716 0.355  1.14  1.15 0.341    17 2021-07…
 9 0.0219 0.0587 0.0409 0.141 0.302 0.712 0.350  1.12  1.16 0.326    18 2021-07…
10 0.0226 0.0598 0.0436 0.143 0.299 0.696 0.340  1.07  1.18 0.398    19 2021-07…
# ℹ 190 more rows

Now, we will need to join the CLS severity data to the spectral covariates data.

Let’s recall how the CLS data are structured:

cls
# A tibble: 136 × 9
    Plot    D0    D1    D2    D3    D4    D5    D6  year
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1     1     0 0.25   0.9   4.58  8.75  21      NA  2021
 2     2     0 0.75   3.68 12.4  20.8   45.4    NA  2021
 3     3     0 2.92   7.38 18    34.6   65.2    NA  2021
 4     4     0 0.75   5.45 17.4  42.6   75.5    NA  2021
 5     5     0 3.38   3.45 15.4  17.2   55.2    NA  2021
 6     6     0 0.8    1.38  8.8  12.2   15.4    NA  2021
 7     7     0 0.375  3.05  9.1  13     31.0    NA  2021
 8     8     0 3.7    8.4  29.4  41.2   71.8    NA  2021
 9     9     0 2.38   4.28 13.2  15.6   38.1    NA  2021
10    10     0 2.22   2.28 30.2  17.3   68      NA  2021
# ℹ 126 more rows

Note that the covar.2021 object has a date attribute in a single column and that the cls object has different columns represent dates (i.e. D0 to D5). This means that we need to change the cls object structure to be able to join its attributes to the covar.2021 object. In technical terms, our task is to convert the cls table from wide format into long format.

We will use the pivot_longer function from the tidyr package:

cls |>
  pivot_longer(
    cols = starts_with("D"),
    names_to = "date",
    values_to = "severity",
    values_drop_na = TRUE
  ) -> long_cls

Now, we will do an additional step, we will replace qualitative date values (D0, D1, …) for actual date values.

change <- tibble(old = c("D0", "D1", "D2", "D3", "D4"),
                 new = c("2021-07-07", "2021-07-15", "2021-07-20",
                          "2021-08-02","2021-08-25"))

Let’s check what we got:

change
# A tibble: 5 × 2
  old   new       
  <chr> <chr>     
1 D0    2021-07-07
2 D1    2021-07-15
3 D2    2021-07-20
4 D3    2021-08-02
5 D4    2021-08-25

Now, let’s do a left join to produce the changes:

(ncls <- left_join(long_cls, change, by = c(date = "old")))
# A tibble: 872 × 5
    Plot  year date  severity new       
   <dbl> <dbl> <chr>    <dbl> <chr>     
 1     1  2021 D0        0    2021-07-07
 2     1  2021 D1        0.25 2021-07-15
 3     1  2021 D2        0.9  2021-07-20
 4     1  2021 D3        4.58 2021-08-02
 5     1  2021 D4        8.75 2021-08-25
 6     1  2021 D5       21    <NA>      
 7     2  2021 D0        0    2021-07-07
 8     2  2021 D1        0.75 2021-07-15
 9     2  2021 D2        3.68 2021-07-20
10     2  2021 D3       12.4  2021-08-02
# ℹ 862 more rows

Finally, we can apply a join:

ncovar.2021 <- left_join(ncls, covar.2021, by = c("Plot" = "plot", "new" = "date"))
ncovar.2021
# A tibble: 872 × 15
    Plot  year date  severity new      blue   green     red  redge    nir   ndvi
   <dbl> <dbl> <chr>    <dbl> <chr>   <dbl>   <dbl>   <dbl>  <dbl>  <dbl>  <dbl>
 1     1  2021 D0        0    2021…  0.0209  0.0488  0.0406  0.122  0.245  0.657
 2     1  2021 D1        0.25 2021…  0.0204  0.0494  0.0321  0.122  0.290  0.760
 3     1  2021 D2        0.9  2021…  0.0207  0.0520  0.0363  0.147  0.360  0.782
 4     1  2021 D3        4.58 2021…  0.0251  0.0633  0.0345  0.197  0.427  0.835
 5     1  2021 D4        8.75 2021…  0.0332  0.0680  0.0661  0.163  0.306  0.619
 6     1  2021 D5       21    <NA>  NA      NA      NA      NA     NA     NA    
 7     2  2021 D0        0    2021…  0.0220  0.0544  0.0451  0.133  0.279  0.667
 8     2  2021 D1        0.75 2021…  0.0190  0.0479  0.0292  0.117  0.268  0.761
 9     2  2021 D2        3.68 2021…  0.0215  0.0540  0.0383  0.153  0.373  0.776
10     2  2021 D3       12.4  2021…  0.0251  0.0616  0.0356  0.184  0.393  0.811
# ℹ 862 more rows
# ℹ 4 more variables: ndre <dbl>, cire <dbl>, sipi <dbl>, mndvi <dbl>

8.5.10 Change severity percentage to severity categories

Our final step is to convert percentage severity values into ordinal severity levels.

First, let’s check the severity histogram

#uncomment if you need to the read the file
#ncovar.2021 <- read_csv("./data/ncovar_2021.csv", show_col_types = FALSE)
hist(ncovar.2021$severity)

Next code produces seven levels of CLS severity:

Severity [%] Category
0 - 1 0
1 - 10 1
10 - 20 2
20 - 30 3
30 - 40 4
40 - 50 5
50 - 100 6
fcovar.2021 <- ncovar.2021 |> mutate(category=cut(severity, breaks=c(0,1,  10, 20, 30,40, 50, 100), labels=c(0,1,2,3,4,5,6), include.lowest = TRUE))

What we got?

summary(fcovar.2021$category)
  0   1   2   3   4   5   6 
284 212 138  77  43  40  78 

Note that the distribution of values at each disease level is not balanced.

8.5.11 Write 2021 csv files

write_csv(
  ncovar.2021,
  "data/cercospora_data/ncovar_2021.csv",
  na = "NA",
  col_names = TRUE,
  append = FALSE
)
write_csv(
  fcovar.2021,
  "data/cercospora_data/fcovar_2021.csv",
  na = "NA",
  col_names = TRUE,
  append = FALSE
)

In this first part we have conducted a very important task: to translate UAV multispectral imagery and CLS severity ground data into a single table storing spectral covariates and severity classes which we will use next for building a prediction model.

8.5.12 Machine learning model

Now we will train & test a machine learning model to estimate qualitative levels of CLS disease severity using the previously created dataset.

8.5.12.1 Software setup

Let’s start by cleaning up R memory:

rm(list=ls())

Then, we need to install several packages (if they are not installed yet):

list.of.packages <- c("readr","terra", "tidyterra", "stars", "sf", "leaflet", "leafem", "dplyr", "ggplot2", "tidymodels", "xgboost", "tune")
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
if(length(new.packages)) install.packages(new.packages)

Now, let’s load all the required packages:

library(readr)
library(terra)
library(tidyterra)
library(stars)
library(sf)
library(leaflet)
library(leafem)
library(dplyr)
library(ggplot2)
library(tidymodels)
library(xgboost)
library(tune)

What files are in the data folder under the current path:

list.files("data/cercospora_data")
 [1] "CLS_DS.csv"          "D0_2021.csv"         "D1_2021.csv"        
 [4] "D2_2021.csv"         "D3_2021.csv"         "D4_2021.csv"        
 [7] "fcovar_2021.csv"     "multispec_2021_2022" "multispec_2023"     
[10] "ncovar_2021.csv"     "README.md"          

Let’s remind that fcovar_2021.csv is the file containing the dataset we created previously. We will use the readr package to read the data as a tibble:

covar21 <- read_csv("data/cercospora_data/fcovar_2021.csv", 
                    col_types = cols(category = col_character())) |>
  drop_na()

What we got?

covar21
# A tibble: 600 × 16
    Plot  year date  severity new          blue  green    red redge   nir  ndvi
   <dbl> <dbl> <chr>    <dbl> <date>      <dbl>  <dbl>  <dbl> <dbl> <dbl> <dbl>
 1     1  2021 D0        0    2021-07-07 0.0209 0.0488 0.0406 0.122 0.245 0.657
 2     1  2021 D1        0.25 2021-07-15 0.0204 0.0494 0.0321 0.122 0.290 0.760
 3     1  2021 D2        0.9  2021-07-20 0.0207 0.0520 0.0363 0.147 0.360 0.782
 4     1  2021 D3        4.58 2021-08-02 0.0251 0.0633 0.0345 0.197 0.427 0.835
 5     1  2021 D4        8.75 2021-08-25 0.0332 0.0680 0.0661 0.163 0.306 0.619
 6     2  2021 D0        0    2021-07-07 0.0220 0.0544 0.0451 0.133 0.279 0.667
 7     2  2021 D1        0.75 2021-07-15 0.0190 0.0479 0.0292 0.117 0.268 0.761
 8     2  2021 D2        3.68 2021-07-20 0.0215 0.0540 0.0383 0.153 0.373 0.776
 9     2  2021 D3       12.4  2021-08-02 0.0251 0.0616 0.0356 0.184 0.393 0.811
10     2  2021 D4       20.8  2021-08-25 0.0356 0.0661 0.0759 0.132 0.214 0.481
# ℹ 590 more rows
# ℹ 5 more variables: ndre <dbl>, cire <dbl>, sipi <dbl>, mndvi <dbl>,
#   category <chr>

We will change data type for the category column from character to factor:

covar21$category <- as.factor(covar21$category)

Let’s see a summary of the dataset:

summary(covar21)
      Plot            year          date              severity     
 Min.   : 1.00   Min.   :2021   Length:600         Min.   : 0.000  
 1st Qu.:10.75   1st Qu.:2021   Class :character   1st Qu.: 0.200  
 Median :20.50   Median :2022   Mode  :character   Median : 2.275  
 Mean   :20.50   Mean   :2022                      Mean   : 7.865  
 3rd Qu.:30.25   3rd Qu.:2023                      3rd Qu.:11.875  
 Max.   :40.00   Max.   :2023                      Max.   :60.750  
                                                                   
      new                  blue             green              red         
 Min.   :2021-07-07   Min.   :0.01718   Min.   :0.04180   Min.   :0.02762  
 1st Qu.:2021-07-15   1st Qu.:0.02097   1st Qu.:0.05603   1st Qu.:0.03330  
 Median :2021-07-20   Median :0.02233   Median :0.05833   Median :0.03757  
 Mean   :2021-07-26   Mean   :0.02397   Mean   :0.05889   Mean   :0.04288  
 3rd Qu.:2021-08-02   3rd Qu.:0.02522   3rd Qu.:0.06156   3rd Qu.:0.04416  
 Max.   :2021-08-25   Max.   :0.03781   Max.   :0.07335   Max.   :0.08514  
                                                                           
     redge             nir              ndvi             ndre       
 Min.   :0.1147   Min.   :0.1793   Min.   :0.4283   Min.   :0.1995  
 1st Qu.:0.1368   1st Qu.:0.2846   1st Qu.:0.6837   1st Qu.:0.3278  
 Median :0.1483   Median :0.3299   Median :0.7639   Median :0.3671  
 Mean   :0.1550   Mean   :0.3319   Mean   :0.7278   Mean   :0.3512  
 3rd Qu.:0.1682   3rd Qu.:0.3939   3rd Qu.:0.8114   3rd Qu.:0.3969  
 Max.   :0.2318   Max.   :0.4795   Max.   :0.8664   Max.   :0.4413  
                                                                    
      cire             sipi           mndvi         category
 Min.   :0.5166   Min.   :1.025   Min.   :-1.0859   0:249   
 1st Qu.:1.0223   1st Qu.:1.062   1st Qu.:-0.3058   1:176   
 Median :1.2022   Median :1.092   Median :-0.0467   2:105   
 Mean   :1.1493   Mean   :1.139   Mean   :-0.1076   3: 32   
 3rd Qu.:1.3594   3rd Qu.:1.182   3rd Qu.: 0.1316   4: 17   
 Max.   :1.6325   Max.   :1.475   Max.   : 0.5812   5: 13   
                                                    6:  8   

Now, we will filter several columns of this tibble using the dplyr package. First, get the column names:

names(covar21)
 [1] "Plot"     "year"     "date"     "severity" "new"      "blue"    
 [7] "green"    "red"      "redge"    "nir"      "ndvi"     "ndre"    
[13] "cire"     "sipi"     "mndvi"    "category"

Now, the selection:

covar21 |> select(Plot,ndvi,ndre, cire, sipi, mndvi, category) -> covar

Let’s check what we get:

covar
# A tibble: 600 × 7
    Plot  ndvi  ndre  cire  sipi   mndvi category
   <dbl> <dbl> <dbl> <dbl> <dbl>   <dbl> <fct>   
 1     1 0.657 0.322 0.999  1.21  0.499  0       
 2     1 0.760 0.397 1.38   1.10  0.145  0       
 3     1 0.782 0.410 1.45   1.09  0.0765 0       
 4     1 0.835 0.372 1.23   1.04 -0.520  1       
 5     1 0.619 0.300 0.913  1.23  0.0861 1       
 6     2 0.667 0.339 1.07   1.20  0.438  0       
 7     2 0.761 0.382 1.29   1.09  0.0458 0       
 8     2 0.776 0.409 1.44   1.09  0.132  1       
 9     2 0.811 0.361 1.18   1.05 -0.458  2       
10     2 0.481 0.245 0.672  1.37  0.0265 3       
# ℹ 590 more rows

8.5.12.2 CLS classification using machine learning (ML)

The overall process to classify the crop disease under study will be conducted using the tidymodels framework which is an extension of the tidyverse suite. It is especially focused towards providing a generalized way to define, run and optimize ML models in R. See https://rstudio-connect.hu.nl/caait/

8.5.12.2.1 Exploratory analysis

As a first step in modeling, it’s always a good idea to plot the data.

Let’s try a boxplot:

plot <- ggplot(covar, aes(x=category, y=mndvi, color = category))+
     geom_boxplot()+
     r4pde::theme_r4pde()+
     theme(legend.position = "none" )

plot

Next, we will do a scatterplot to visualize the relationship between the ndvi and mndvi indices:

ggplot(covar) +
  aes(x = ndvi, y = mndvi, color = category) +
  geom_point(shape = 16, size = 3) +
  labs(x = "ndvi", y = "mndvi", color = "Severity category") +
  r4pde::theme_r4pde()+
    theme(legend.position = "top")

8.5.12.2.2 Splitting the data

Next step is to divide the data into a training and a test set. The set.seed() function can be used for reproducibility of the computations that are dependent on random numbers. By default, the training/testing split is 0.75 to 0.25.

set.seed(42)
data_split <- initial_split(data = covar)
data_train <- training(data_split)
data_test <- testing(data_split)

Let’s check the result:

data_train
# A tibble: 450 × 7
    Plot  ndvi  ndre  cire  sipi  mndvi category
   <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl> <fct>   
 1    33 0.755 0.370  1.23  1.11  0.147 0       
 2    25 0.729 0.356  1.14  1.14  0.192 0       
 3    31 0.757 0.352  1.11  1.11 -0.112 1       
 4    15 0.818 0.341  1.07  1.05 -0.458 1       
 5     6 0.809 0.420  1.49  1.07 -0.167 0       
 6    30 0.659 0.321  1.01  1.21  0.324 0       
 7    10 0.852 0.394  1.33  1.03 -0.933 4       
 8    26 0.813 0.417  1.47  1.07 -0.214 1       
 9    21 0.727 0.357  1.14  1.13  0.241 0       
10     5 0.797 0.327  1.02  1.07 -0.310 2       
# ℹ 440 more rows
8.5.12.2.3 Defining the model

We will use a gradient boosting machine algorithm which seems a robust algorithm for different applications.

spec_lr <-
boost_tree(
  mode = "classification",
  trees=1000,
  #tree_depth = tune::tune(),
  #learn_rate = tune::tune(),
  #loss_reduction = tune::tune()
  ) |>
  set_engine("xgboost", objective = "multi:softprob", eval_metric = "mlogloss",) 
8.5.12.2.4 Defining the recipe

The recipe() function to be used here has two arguments:

  • A formula. Any variable on the left-hand side of the tilde (~) is considered the model outcome (here, outcome). On the right-hand side of the tilde are the predictors. Variables may be listed by name, or you can use the dot (.) to indicate all other variables as predictors.

  • The data. A recipe is associated with the data set used to create the model. This will typically be the training set, so data = data_train here.

recipe_lr <-
  recipe(category ~ ., data_train) |>
  add_role(Plot, new_role = "id") |>
  step_zv(all_predictors()) |>
  step_corr(all_predictors())
8.5.12.2.5 Model performance metrics

Next, we need to specify what we would like to see for determining the performance of the model. Different modelling algorithms have different types of metrics. Because we have a multiclass classification problem (different categories of severity), we will choose the accuracy evaluation metric here.

8.5.12.2.6 Combine model specification and recipe into a workflow

We will want to use our recipe across several steps as we train and test our model. We will:

  • Process the recipe using the training set: This involves any estimation or calculations based on the training set. For our recipe, the training set will be used to determine which predictors should be converted to dummy variables and which predictors will have zero-variance in the training set, and should be slated for removal.

  • Apply the recipe to the training set: We create the final predictor set on the training set.

  • Apply the recipe to the test set: We create the final predictor set on the test set. Nothing is recomputed and no information from the test set is used here; the dummy variable and zero-variance results from the training set are applied to the test set.

To simplify this process, we can use a model workflow, which pairs a model and recipe together. This is a straightforward approach because different recipes are often needed for different models, so when a model and recipe are bundled, it becomes easier to train and test workflows. We’ll use the workflows package from tidymodels to bundle our model with our recipe.

Now we are ready to set up our complete modelling workflow. This workflow contains the model specification and the recipe.

wf_cls_wilt <-
  workflow(
    spec = spec_lr,
    recipe_lr
    )

wf_cls_wilt
══ Workflow ════════════════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: boost_tree()

── Preprocessor ────────────────────────────────────────────────────────────────
2 Recipe Steps

• step_zv()
• step_corr()

── Model ───────────────────────────────────────────────────────────────────────
Boosted Tree Model Specification (classification)

Main Arguments:
  trees = 1000

Engine-Specific Arguments:
  objective = multi:softprob
  eval_metric = mlogloss

Computational engine: xgboost 
8.5.12.2.7 Fitting the GBM regression model

Now we use the workflow previously created to fit the model on our training data. We use the training partition of the data.

fit_lr <- wf_cls_wilt |>
  fit(data = data_train)

Let’s check the output:

fit_lr
══ Workflow [trained] ══════════════════════════════════════════════════════════
Preprocessor: Recipe
Model: boost_tree()

── Preprocessor ────────────────────────────────────────────────────────────────
2 Recipe Steps

• step_zv()
• step_corr()

── Model ───────────────────────────────────────────────────────────────────────
##### xgb.Booster
call:
  xgboost::xgb.train(params = list(eta = 0.3, max_depth = 6, gamma = 0, 
    colsample_bytree = 1, colsample_bynode = 1, min_child_weight = 1, 
    subsample = 1, num_class = 7L, objective = "multi:softprob", 
    nthread = 1, eval_metric = "mlogloss"), data = x$data, nrounds = 1000, 
    evals = x$watchlist, verbose = 0)
# of features: 4 
# of rounds:  1000 
callbacks:
   evaluation_log 
evaluation_log:
  iter training_mlogloss
 <num>             <num>
     1         1.1744612
     2         1.0144948
   ---               ---
   999         0.3573057
  1000         0.3573040

Now, we will use the fitted model to estimate CLS severity in the training data:

rf_training_pred <- 
  predict(fit_lr, data_train) |>
  # Add the true outcome data back in
  bind_cols(data_train |>
              select(category))

What we got?

rf_training_pred
# A tibble: 450 × 2
   .pred_class category
   <fct>       <fct>   
 1 0           0       
 2 0           0       
 3 0           1       
 4 1           1       
 5 0           0       
 6 0           0       
 7 1           4       
 8 1           1       
 9 0           0       
10 1           2       
# ℹ 440 more rows

Let’s estimate the training accuracy:

rf_training_pred |> # training set predictions
  accuracy(truth = category, .pred_class) -> acc_train
acc_train
# A tibble: 1 × 3
  .metric  .estimator .estimate
  <chr>    <chr>          <dbl>
1 accuracy multiclass     0.784

The accuracy of the model on the training data should be judged against the accuracy of always predicting the most frequent class (the no-information rate), which is the relevant baseline for this seven-class problem. This basically means that the model was able to learn predictive patterns from the training data. To see if the model is able to generalise what it learned when exposed to new data, we evaluate the model on our hold-out (or so-called test data). We created a test dataset when splitting the data at the start of the modelling.

8.5.12.2.8 Evaluating the model on the test data

Now, we will use the fitted model to estimate health status in the testing data:

lr_testing_pred <- 
  predict(fit_lr, data_test) |>
  bind_cols(predict(fit_lr, data_test, type = "prob")) |>
  bind_cols(data_test |> select(category))

What we got:

lr_testing_pred
# A tibble: 150 × 9
   .pred_class    .pred_0  .pred_1   .pred_2    .pred_3  .pred_4 .pred_5 .pred_6
   <fct>            <dbl>    <dbl>     <dbl>      <dbl>    <dbl>   <dbl>   <dbl>
 1 1           0.00000979 0.997    0.00282   0.000150    1.40e-4 1.36e-5 5.32e-6
 2 1           0.0000107  0.999    0.00107   0.0000379   1.90e-5 8.19e-5 5.31e-6
 3 0           0.501      0.499    0.000140  0.0000691   6.78e-5 2.01e-5 3.20e-5
 4 0           0.997      0.00326  0.0000102 0.0000132   2.00e-6 3.90e-6 1.13e-6
 5 2           0.00000115 0.000244 1.000     0.000201    7.16e-7 6.45e-7 2.41e-7
 6 0           0.995      0.00449  0.0000203 0.0000318   2.09e-6 8.23e-6 2.49e-6
 7 1           0.00444    0.995    0.0000303 0.0000604   2.12e-5 8.48e-6 1.35e-5
 8 3           0.00000112 0.00407  0.00196   0.994       5.32e-6 4.55e-6 1.84e-6
 9 0           1.000      0.000154 0.0000109 0.00000796  2.92e-6 1.57e-6 1.94e-6
10 1           0.00000631 0.998    0.00167   0.000459    1.95e-4 3.61e-6 3.40e-6
# ℹ 140 more rows
# ℹ 1 more variable: category <fct>

Let’s compute the testing accuracy:

lr_testing_pred |> # test set predictions
  accuracy(category, .pred_class)
# A tibble: 1 × 3
  .metric  .estimator .estimate
  <chr>    <chr>          <dbl>
1 accuracy multiclass     0.527

The resulting accuracy is much lower than the accuracy on the training data. This means that we would need to figure out how to improve our classification model.

## Let's plot the AUC-ROC 
lr_testing_pred |>
  conf_mat(truth = category, .pred_class) -> cm 
cm
          Truth
Prediction  0  1  2  3  4  5  6
         0 48 14  0  1  0  0  0
         1 11 19  9  2  0  1  0
         2  0  6 11  5  1  0  0
         3  0  2  8  0  2  0  1
         4  0  0  1  1  1  0  1
         5  0  1  2  2  0  0  0
         6  0  0  0  0  0  0  0

Note what are the disease categories more difficult to discriminate.

In this section we trained and tested a gradient boosting model (GBM) using five spectral indices as predictor variables (i.e. NDVI, NDRE, CIRE, SIPI, and MNDVI) obtained from imagery and field data. Accuracy metrics show that both data & model need improvement.

Some ideas for further experiments:

  • Include data collected in 2022 and 2023
  • Refine CLS severity categories (see (Del Ponte et al. 2019))
  • Compute other vegetation indices as advised in relevant literature
  • Compute other covariates (e.g. image texture metrics)

This section illustrated how to use VIs derived from UAV-based multispectral imagery and ground data to develop a classification model for estimating CLS in table beet. The results showed that a gradient boosting machine model is unable to estimate severity from several VIs with a good accuracy. We can explore the use of additional covariates to improve accuracy of the initial model.

8.6 Summary

We have completed a journey that hopefully may serve as foundation to apply remote sensing data and techniques to infer information useful for monitoring crop diseases.

Our journey consisted of three stages. In the first section we reviewed basic remote sensing concepts and provided a summary of applications of remote sensing of crop diseases. In the second section we navigated a case study on identification of banana Fusarium wilt from multispectral UAV imagery. In the third section we moved to a case study dealing with estimation of cercospora leaf spot disease severity on table beet. For the two case studies we used datasets publicly available.

Now, it is time to conclude with several reflections about: (i) what we learned in this chapter; (ii) what are the potential and limitations of remote sensing for identification & monitoring crop diseases; and (iii) what are main challenges to overcome to advance the use of remote sensing techniques for identification and quantification of crop diseases.

8.6.1 Lessons learned in this journey

First of all, we started to become familiar with concepts and techniques that can guide us in future studies focused on the use of remote sensing for crop disease monitoring. Although they are only the tip of the remote sensing iceberg, which we must know in depth, they represent part of the foundation necessary to be successful in any research related to the topic of this chapter.

Along the way, we learned to use some R libraries that allow us to: (i) read, visualize and process multispectral images and geospatial data; (ii) merge raster and vector data and extract spectral covariates related to plant biophysical, biochemical, and functional traits; and (iii) apply machine learning algorithms to infer information useful for plant disease identification and quantification.

The results obtained in our hands-on activities suggest that the application of machine learning algorithms to learning from data is driven by an iterative process: starting from an initial workflow, users need to iteratively modify their workflow, based on previous results, to improve performance.

We have learned that, in order to use data properly, research data needs to be fully described and documented. Otherwise, users may become frustrated or lost in guessing, cleaning, and pre-processing tasks. We have also learned that if the code is well documented, it is feasible to reuse it in future studies.

8.6.2 Potential and limitations of remote sensing for crop disease monitoring

A variety of remote sensing (RS) devices can be used to obtain data on the spectral response of plants and crops to diseases and insect damage. Processing and analyzing such data allows to estimate information useful for monitoring crops and analysing different stages of plant health. As individual devices have some limitations, multimodal remote sensing data & techniques (i.e. reflectance, fluorescence, thermal, microwave, lidar, multiangular sensing) are increasingly being used in research.

In the multimodal setting, data from different platforms & instruments are used together to obtain a better outcome. Multimodal methods allow for the integration of spectral responses from different & complementary electromagnetic radiation ranges, allowing to quantify different plant responses (e.g. stomatal closure, increased respiration, decreased chlorophyll, altered pigments, modified biochemistry, photosynthetic inhibition, altered growth, modified leaf angle, water content), and analyzing them to obtain a comprehensive understanding of disease progression, including eventual recovery. By using multimodal methods, it is possible to obtain a more accurate and detailed analysis of crop conditions, leading to advanced knowledge, improved decision-making and ultimately, better crop health management.

8.6.3 Main challenges to overcome

Building more resilient agricultural systems requires concurrent and near-real-time and reliable crop information for effective decision making. Using remote sensing data & techniques for monitoring crop health can become a relevant method to derive crop information at leaf, canopy, field, and farm scales.

In spite of the increasing literature reporting the application of remote sensing for monitoring crop diseases, it seems that there is a lack of open, transparent and reproducible data and methods to ensure a robust evaluation of performance, reliability and generalization of machine learning and deep learning models, a necessary pre-requisite to safely advance the applicability of remote sensing for crop monitoring in the field.

To fully realize the potential of remote sensing for estimating crop health status, there is a need of reliable ground truth data to enable both data calibration (i.e. linking in-situ to remote values) and accuracy assessment of estimations. Since field measurements require a lot of work and time, there is always a risk that the sampling is not correct (e.g. not enough samples, incorrect sample location, or the sampled area does not match the sensor footprint).

Both collaborative research and crowdsourcing could become a solution to overcome the restrictions of ground-truth data accessibility as well as the lack of open, transparent, and reusable remote sensing data & workflows. It is anticipated that joint multidisciplinary research as well as user participation in the complete process of crop monitoring from remote sensing could improve the reliability of crop health information.