7  Multilayer Rasters: Layer–wise Operations

7.1 Introduction

7.2 Raster Concatenation

Concatenation (placing the objects one after the other) of rasters follow the same syntax as arrays i.e. by using c(obj1, obj2). Concatenation of rasters can be used for, but not limited to, creating a raster time series to evaluate time-dependent variability in the spatial data. Here we will use the global NDVI rasters from MODIS (resampled to a coarse resolution for computational ease) to create a raster time series in the form of a multilayer rast object, and will demonstrate several spatial and arithmetic operations on the multilayer rasters.

7.2.1 Create Raster Time Series

The folder ./SampleData-master/raster_files/NDVI/ contains 23 rasters of global NDVI for the year 2016, with a retrieval frequency of 16 days. Let us import four sample rasters and concatenate them to generate a multilayer raster.

#~~~ Create and plot NDVI SpatRaster
library(terra)
library(cetcolor)

# Location of the NDVI raster files
ndvi_path="./SampleData-master/raster_files/NDVI/" 

# List of all NDVI rasters
ras_path=list.files(ndvi_path,    # The folder which contains the files of interest
                pattern='*.tif',  # Select files with this extention
                full.names=TRUE)  # Print full file path? Yes (TRUE) or no (FALSE)?

print(ras_path)     # The folder contains 23 rasters, in increasing order of time 
 [1] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-01-01.tif"
 [2] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-01-17.tif"
 [3] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-02-02.tif"
 [4] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-02-18.tif"
 [5] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-03-05.tif"
 [6] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-03-21.tif"
 [7] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-04-06.tif"
 [8] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-04-22.tif"
 [9] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-05-08.tif"
[10] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-05-24.tif"
[11] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-06-09.tif"
[12] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-06-25.tif"
[13] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-07-11.tif"
[14] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-07-27.tif"
[15] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-08-12.tif"
[16] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-08-28.tif"
[17] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-09-13.tif"
[18] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-09-29.tif"
[19] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-10-15.tif"
[20] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-10-31.tif"
[21] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-11-16.tif"
[22] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-12-02.tif"
[23] "./SampleData-master/raster_files/NDVI/NDVI_resamp_2016-12-18.tif"
# User the file path to import raster
r1=rast(ras_path[1])
r2=rast(ras_path[2])
r3=rast(ras_path[3])
r4=rast(ras_path[4])

# Concatenate the rasters i.e. arrange the rasters one after the other
rast_ts=c(r1,r2, r3, r4)    
nlyr(rast_ts)            # Number of layers in the multilayer raster
[1] 4
names(rast_ts)           # Names of the raster layers
[1] "NDVI_resamp_2016-01-01" "NDVI_resamp_2016-01-17" "NDVI_resamp_2016-02-02"
[4] "NDVI_resamp_2016-02-18"
# Plot concatenated rasters
colpal = cetcolor::cet_pal(20, name = "r2")  
plot(rast_ts, col=rev(colpal))

7.2.2 Programmatic Concatenation of rast Objects

Concatenation method shown above can be good for a few rasters. However, in the case of a long time series, this method could be cumbersome. In such cases, we can write a small script to concatenate the rasters in to a multilayer rast object. We will first explore the use of loops- which will concatenate each rast layer sequentially. This method may take longer, but helps to avoid memory bottleneck when a very large number of rasters are simultaneously processed (the threshold may depend on the system memory and processor). In contrast, lapply and map vectorize the operation of creating the rast object for concatenation, and maybe faster for a moderate number of files (again, depends on the system configuration).

#~~~~~~~~~~~~~~~~~~
# Method 1: Using for loop to create raster layers from the raster location
#~~ Suitable for large number of rasters and avoiding memory bottleneck
ras_stack=rast()
for (nRun in 1:length(ras_path)){
  ras_stack=c(ras_stack,rast(ras_path[[nRun]]))
}

# Check dimension of data cube
dim(ras_stack) #[x: y: z]- 23 raster layers with 456 x 964 cells
[1] 456 964  23
#~~~~~~~~~~~~~~~~~~
# Method 2: Use lapply to create raster layer list from the raster file paths
ras_list = lapply(paste(ras_path, sep = ""), rast)
# This a list of 23 raster objects stored as individual elements.

# Convert raster layer lists to Spatraster 
ras_stack = rast(ras_list)     # Stacking all rasters as a SpatRaster 
# This a multi-layer (23 layers in this case) SpatRaster Object.

#~~~~~~~~~~~~~~~~~~
# Method 3: maping the rast function on the raster file paths
library(purrr) 
ras_list = purrr::map(ras_path, ~ rast(.x))  # Import the rasters into a list
ras_stack = rast(ras_list)                   # Convert a list of rasters to rast object

dim(ras_stack) #[x: y: z]- 23 raster layers with 456 x 964 cells
[1] 456 964  23

Computing Counsel: Exporting concatenated rasters as a netCDF is a convenient time-saving measure. We can use terra::writeCDF to export SpatRaster object as netCDF. This allows the multilayer raster to be imported directly to the workspace and avoid repeating the steps in the preceeding section.

# Export concatenated rasters as a netCDF
terra::writeCDF(ras_stack,                 # SpatRaster
         file.path("NDVI.nc"),             # Output filename
         overwrite=TRUE,
         varname="NDVI",
         unit="[-]",
         longname="Global MODIS NDVI, 2016, 16_day, 36KM",
         zname='time')

7.3 Spatial Operations on Multilayer Raster

7.3.1 Subset Multilayer Raster

Subsetting multilayer rasters (i.e. creating of smaller subset of layers) follows the same syntax we would use to subset a list. Notice the double square bracket i.e. [[ ]] we use to subset raster layers. When generating a spatial plot of multilayer rasters using terra::plot, to add vector boundaries to all plot layers, we need to define a custom function to add the object to each layer.

# Subset raster stack/brick (notice the double [[]] bracket and similarity to lists)
sub_ras_stack=ras_stack[[c(1,3,5,10,12)]] #Select 1st, 3rd, 5th, 10th and 12th layers

#~~ Plot first 4 elements of NDVI SpatRaster
coastlines = vect("./SampleData-master/ne_10m_coastline/ne_10m_coastline.shp")

# Function to add shapefile to the plot
addCoastlines=function(){
  plot(coastlines, add=TRUE)   # Add Coastline vector to existing plot
  } 

# Notice how a subset of first 4 rasters is created 
terra::plot(sub_ras_stack[[1:4]],  # Select raster layers to plot
     col=colpal,                   # Specify colormap
     asp=NA,                       # Aspect ratio= NA
     fun=addCoastlines)            # Add coastline to each layer

7.3.2 Crop and Mask

Cropping and masking a multilater raster follows the same format as that of a single raster we learned in Sec 5.3.

# Names of the states to mask NDVI
states = c('Oklahoma','Texas','New Mexico', 'Louisiana', 'Arkansas')

# Subset the selected states from CONUS simple feature (sf) object
library(sf)
library(spData)
us_shp=spData::us_states

# Subset sf for selected states using the NAME feature
selectState = us_shp[(us_shp$NAME %in% states),]

# Convert selectState sf to vect object
stateVect= vect(selectState)

# Reproject the state vector to match the CRS of the NDVI raster
stateVect_proj=terra::project(stateVect,crs(ras_stack))

# Raster operation
ndvi_crp = crop(ras_stack, ext(stateVect_proj))       # Crop raster
ndvi_msk = terra::mask(ndvi_crp,stateVect_proj)       # Mask

dim(ndvi_msk) #Notice the number of layers in the masked rast
[1] 30 53 23
# Plot raster and shapefile
plot(ndvi_msk[[1:4]], 
     col=colpal, 
     fun=function(){plot(stateVect_proj, add=TRUE)} # Add state borders
     )

7.3.3 Spatial Extraction

Similar to single-band raster operation shown in Sec. 5.5.1.2, this operation can be executed using terra::extract. However, the output will contain values from all layers in the multiband raster.

# Extract cell values within each feature for all layers
ndvi_state_mean = terra::extract(ndvi_msk,       # Data cube
                         stateVect_proj,    # Shapefile for feature reference 
                         na.rm=T)

# Extract 'mean' statistic of cell values within each feature for all layers
ndvi_state_mean = terra::extract(ndvi_msk,  # Multilayer rast object
                         stateVect_proj,    # Vect object of states
                         fun=mean,          # Summary statistics (mean/sum/min/max)
                         na.rm=T )          # Remove NA values? 

# Try: View(ndvi_state_mean)
#~~~ Each row corresponds to one state. Columns store layer-wise mean NDVI per state
head(ndvi_state_mean)
  ID NDVI_resamp_2016-01-01 NDVI_resamp_2016-01-17 NDVI_resamp_2016-02-02
1  1              0.5522976              0.5080151              0.4881696
2  2              0.3818293              0.3652746              0.3635756
3  3              0.3994706              0.3769143              0.3628214
4  4              0.4775158              0.4626955              0.4467108
5  5              0.1515851              0.1885286              0.1956329
  NDVI_resamp_2016-02-18 NDVI_resamp_2016-03-05 NDVI_resamp_2016-03-21
1              0.4899222              0.4895655              0.5495264
2              0.3819785              0.4111656              0.4301364
3              0.3888056              0.4115137              0.4409466
4              0.4653435              0.4628991              0.4804158
5              0.2090006              0.2141808              0.2182761
  NDVI_resamp_2016-04-06 NDVI_resamp_2016-04-22 NDVI_resamp_2016-05-08
1              0.6105156              0.6728889              0.6717543
2              0.4858011              0.5957775              0.6227723
3              0.4841774              0.4977419              0.4900750
4              0.5476653              0.6672000              0.6972526
5              0.2341186              0.2381249              0.2497053
  NDVI_resamp_2016-05-24 NDVI_resamp_2016-06-09 NDVI_resamp_2016-06-25
1              0.6957812              0.7140958              0.7334875
2              0.6317198              0.6319490              0.6212515
3              0.5081387              0.5217799              0.4975696
4              0.7327560              0.7381307              0.7769872
5              0.2611739              0.2605942              0.2693318
  NDVI_resamp_2016-07-11 NDVI_resamp_2016-07-27 NDVI_resamp_2016-08-12
1              0.7264149              0.7297604              0.7205446
2              0.6212688              0.5903892              0.5904805
3              0.4569178              0.4475357              0.4784838
4              0.7936874              0.7996058              0.7991813
5              0.2534896              0.2710826              0.2933462
  NDVI_resamp_2016-08-28 NDVI_resamp_2016-09-13 NDVI_resamp_2016-09-29
1              0.7327004              0.6952247              0.6563840
2              0.5939433              0.5947148              0.5561649
3              0.5335192              0.5370391              0.5085575
4              0.7747568              0.7191144              0.6574943
5              0.3281408              0.3158202              0.2957566
  NDVI_resamp_2016-10-15 NDVI_resamp_2016-10-31 NDVI_resamp_2016-11-16
1              0.6274066              0.6019123              0.5819281
2              0.5264949              0.5015890              0.4637258
3              0.4742536              0.4476652              0.4316670
4              0.6204193              0.5829816              0.5198666
5              0.2680033              0.2750498              0.2497373
  NDVI_resamp_2016-12-02 NDVI_resamp_2016-12-18
1              0.5348509              0.5075413
2              0.3907982              0.3692964
3              0.3791562              0.3770174
4              0.4779205              0.4680233
5              0.2450552              0.2306126

7.4 Layer–wise Statistical/Arithmetic Operations on Multilayer Raster

7.4.1 Layer–wise Statistics

Similar to what we learned in Sec. 4.2 Raster Statistics, we can use the global function to obtain summary statistics of the multilayer raster. Here the operation will yield the desired statistic for each constituent layer.

# Layer-wise cell-statistics 
# Layer-wise Mean
global(sub_ras_stack, mean, na.rm= T) # Try modal, median, min etc. 
                            mean
NDVI_resamp_2016-01-01 0.2840846
NDVI_resamp_2016-02-02 0.2948298
NDVI_resamp_2016-03-05 0.3057182
NDVI_resamp_2016-05-24 0.4618550
NDVI_resamp_2016-06-25 0.4997354
# Layer-wise quantiles
global(sub_ras_stack, quantile, probs=c(.25,.75), na.rm= T)
                             X25.      X75.
NDVI_resamp_2016-01-01 0.07188153 0.5135894
NDVI_resamp_2016-02-02 0.08442930 0.5177374
NDVI_resamp_2016-03-05 0.09917563 0.5113073
NDVI_resamp_2016-05-24 0.23671686 0.6695234
NDVI_resamp_2016-06-25 0.26000642 0.7303487
# User-defined statistics by defining own function
quant_fun = function(x) {quantile(x, probs = c(0.25, 0.75), na.rm=TRUE)} 
global(sub_ras_stack, quant_fun) # 25th, and 75th percentile of each layer
                             X25.      X75.
NDVI_resamp_2016-01-01 0.07188153 0.5135894
NDVI_resamp_2016-02-02 0.08442930 0.5177374
NDVI_resamp_2016-03-05 0.09917563 0.5113073
NDVI_resamp_2016-05-24 0.23671686 0.6695234
NDVI_resamp_2016-06-25 0.26000642 0.7303487
# Custom function for mean, variance and skewness
my_fun = function(x){ 
  meanVal=mean(x, na.rm=TRUE)              # Mean 
  varVal=var(x, na.rm=TRUE)                # Variance
  skewVal=moments::skewness(x, na.rm=TRUE) # Skewness
  output=c(meanVal,varVal,skewVal)         # Combine all statistics
  names(output)=c("Mean", "Var","Skew")    # Rename output variables
  return(output)                           # Return output
} 

global(sub_ras_stack, my_fun) # Mean, Variance and skewness of each layer
                            Mean        Var       Skew
NDVI_resamp_2016-01-01 0.2840846 0.07224034  0.5409973
NDVI_resamp_2016-02-02 0.2948298 0.06677124  0.5075244
NDVI_resamp_2016-03-05 0.3057182 0.06171894  0.5018285
NDVI_resamp_2016-05-24 0.4618550 0.05944947 -0.2721941
NDVI_resamp_2016-06-25 0.4997354 0.06587261 -0.3392214

7.4.2 Layer–wise Arithmetic Operations

Again, this operation is similar to the operations on a single-band rasters, except the operation is carried out for each constituent layer simultaneously. Let’s explore some examples.

# Arithmetic operations on SpatRaster are same as lists
add = ndvi_msk+10                  # Add a number to raster layers
mult = ndvi_msk*5                  # Multiply a number to raster layers
subset_mult = ndvi_msk[[1:3]]*10   # Multiply a number to a subset of raster layers
log_ra = log(ndvi_msk)        # Layer-wise Log-transformation

# Data filtering based on cell-value
#~~~ Assign NA to any value less than 0.5
#~~~ Try ?terra::clamp for help and other options/uses
filter_rast = terra::clamp(ndvi_msk, lower=0.5, values=FALSE)

# Let's plot the filtered rasters
plot(filter_rast[[1:4]], 
     asp=NA,            # Aspect ratio: NA, fill to plot space
     col=colpal,        # Assign colormap to the plot
     nc=2,              # Number of columns to arrange plots, 
     range=c(0,0.9),    # Set custom z range for the plots
     fun=function(){plot(stateVect_proj, add=TRUE)} # Add state borders
)            

# Summary statistics of layers using boxplots
#~~~ Type ?boxplot in console for details on the selected options below
#~~~ Based on the boxplot, can we be sure that clamping worked? 

dateStr = substr(names(ndvi_msk),13,23) # Substring of dates for X-axis labels
boxplot(filter_rast, 
        horizontal=F, 
        col = "royalblue",frame=F, 
        cex.axis=1, notch=T, 
        col.axis = "black",
        whisklty=1,
        whiskcol="black", whisklwd=1,
        staplelwd = 1, 
        staplecol = "royalblue",
        medcol = "red",medlwd=1.2, 
        col.ticks = "black", 
        outlier=F, 
        names=dateStr) 
axis(1,  tck=0.05, col.axis = "transparent", lwd=1)

# Layerwise normalization to [0,1]
#~~~ Calculate minumum and maximum cell values for each layer
min_val = global(ndvi_msk, min, na.rm= T) # Layer-wise minima
max_val = global(ndvi_msk, max, na.rm= T) # Layer-wise maxima
#~~~ Normalize layers using respective minima and maxima
norm_stk=(ndvi_msk-min_val$min)/(max_val$max-min_val$min) 

# Plot the normalized rasters. Notice the raster values range between 0 and 1
plot(norm_stk[[1:4]],col=colpal)