Skip to main content
NSF NEON, Operated by Battelle

Main navigation

  • About
    • NEON Overview
      • Vision and Management
      • Spatial and Temporal Design
      • History
    • About the NEON Biorepository
      • ASU Biorepository Staff
      • Contact the NEON Biorepository
    • Observatory Blog
    • Newsletters
    • Staff
    • FAQ
    • Contact Us

    About

  • Data
    • Data Portal
      • Data Availability Charts
      • API & GraphQL
      • Prototype Data
      • Externally Hosted Data
    • Data Collection Methods
      • Airborne Observation Platform (AOP)
      • Instrument System (IS)
        • Instrumented Collection Types
        • Aquatic Instrument System (AIS)
        • Terrestrial Instrument System (TIS)
      • Observational System (OS)
        • Observation Types
        • Observational Sampling Design
        • Sampling Schedules
        • Taxonomic Lists Used by Field Staff
        • Optimizing the Observational Sampling Designs
      • Protocols & Standardized Methods
    • Getting Started with NEON Data
      • neonUtilities for R and Python
      • Learning Hub
      • Code Hub
    • Using Data
      • Data Formats and Conventions
      • Released, Provisional, and Revised Data
      • Data Product Bundles
      • Usage Policies
      • Acknowledging and Citing NEON
      • Publishing Research Outputs
    • Data Notifications
    • NEON Data Management
      • Data Availability
      • Data Processing
      • Data Quality

    Data

  • Samples & Specimens
    • Biorepository Sample Portal at ASU
    • About Samples
      • Sample Types
      • Sample Repositories
      • Megapit and Distributed Initial Characterization Soil Archives
    • Finding and Accessing Sample Data
      • Species Checklists
      • Sample Explorer - Relationships and Data
      • Biorepository API
    • Requesting and Using Samples
      • Loans & Archival Requests
      • Usage Policies

    Samples & Specimens

  • Field Sites
    • Field Site Map and Info
    • Spatial Layers & Printable Maps

    Field Sites

  • Resources
    • Getting Started with NEON Data
    • Research Support Services
      • Field Site Coordination
      • Letters of Support
      • Mobile Deployment Platforms
      • Permits and Permissions
      • AOP Flight Campaigns
      • Research Support FAQs
      • Research Support Projects
    • Code Hub
      • neonUtilities for R and Python
      • Code Resources Guidelines
      • Code Resources Submission
      • NEON's GitHub Organization Homepage
    • Learning Hub
      • Tutorials
      • Workshops & Courses
      • Science Videos
      • Teaching Modules
    • Science Seminars and Data Skills Webinars
    • Document Library
    • Funding Opportunities

    Resources

  • Impact
    • Research Highlights
    • Papers & Publications
    • NEON in the News

    Impact

  • Get Involved
    • Upcoming Events
    • Research and Collaborations
      • Environmental Data Science Innovation and Inclusion Lab
      • Collaboration with DOE BER User Facilities and Programs
      • EFI-NEON Ecological Forecasting Challenge
      • NEON Great Lakes User Group
      • NCAR-NEON-Community Collaborations
    • Advisory Groups
      • Science, Technology & Education Advisory Committee
      • Technical Working Groups
    • NEON Ambassador Program
      • Exploring NEON-Derived Data Products Workshop Series
    • Partnerships
    • Community Engagement
    • Work Opportunities

    Get Involved

  • My Account
  • Search

Search

Learning Hub

  • Tutorials
  • Workshops & Courses
  • Science Videos
  • Teaching Modules

Breadcrumb

  1. Resources
  2. Learning Hub
  3. Tutorials
  4. Relating Reflectance Indices to Flux Data

Tutorial

Relating Reflectance Indices to Flux Data

Authors: [Claire K. Lunch, Chris Florian]

Last Updated: Apr 10, 2025

THIS TUTORIAL USES PRE-PREPARED DATA PROVIDED IN PERSON. CHECK BACK FOR AN UPDATED TUTORIAL INCLUDING INSTRUCTIONS FOR DATA WRANGLING.

Flux footprints represent an estimate of the region of land surface contributing to the fluxes estimated by eddy covariance. The exact footprint varies from moment to moment, due to changes in wind speed and direction, so footprints are typically presented as proportional estimates of the contribution from each point over the relevant time period. See Amiro 1998 for details and considerations about footprint calculations.

Because they represent regions of land around the flux tower, flux footprints are ideally suited to be used in combination with remotely sensed data. In this tutorial, we attempt to use flux footprints together with NDVI to estimate net ecosystem exchange (NEE), but the general principles used here can be applied to many data types in the remote sensing and eddy covariance suites (for example, canopy water content and latent heat fluxes, canopy height increment and cumulative carbon uptake, etc).

Background: NDVI in the flux footprint

Let's say we want to use reflectance data to scale up NEE, by extrapolating flux data across a larger landscape.

NEON remote sensing flights follow a detailed schedule, as described on the flight schedule page. Although flights are generally carried out at a similar time of year at each site over time, weather conditions can shift the exact date, and local weather and phenology may result in flights capturing slightly different conditions on the ground in different years. And of course, local conditions each year may result in different vegetation and soil conditions even at the same time of year and same phenological stage.

The flux footprints enable us to greatly improve the accuracy of the relationship between reflectance and fluxes - instead of guessing at the appropriate region around the tower to use for the comparison, we can use the region identified by the footprint, and weighted by the relative contribution of each pixel to the footprint.

To begin to bring together these data sources, in this tutorial, we will examine the relationship between NDVI within the flux footprint and NEE on the day(s) the remote sensing flight took place. In order to give ourselves the best odds of detecting differences, we'll focus on grasslands, where NDVI is a more reliable indicator of photosynthetic activity than, say, evergreen forests. The sites used here span a
large latitude range across the Great Plains.

Objectives

After completing this activity, you will be able to:

  • Examine rasters of NEON flux footprint and NDVI data
  • Calculate the weighted average NDVI within the footprint
  • Explore the relationship between NDVI and NEE

Things You’ll Need To Complete This Tutorial

  • The dataset provided on flash drives, containing flux data, footprint rasters, and NDVI rasters
  • The tutorial is written in R, but if you are more comfortable in Python, you can follow the narrative sections and translate the code sections into Python. The functions carried out by the raster package in R can be done with rasterio in Python.

Install Packages

  • R raster: install.packages("raster")
  • Python rasterio: pip install rasterio

Additional Resources

  • NEON Data Portal
  • Download and Explore tutorial
  • Introduction to NEON flux data
  • Python rasterio package documentation

Set Up Environment and Load Data

First install and load the necessary packages.

## 
## # install and load raster package
## # if raster is already installed, only the library() command
## # needs to be run
## install.packages("raster")
library(raster)
## 

Data

For this tutorial, we will need three data sets from two NEON data products: NDVI is published in Vegetation indices (DP3.30026.001), and NEE and footprints are published in Eddy covariance (DP4.00200.001). These datasets have been pre-prepared and provided on flash drives. The flux data for all sites are in one file, flux_allSites.csv. The NDVI and footprint data are in paired .grd and .gri files, one pair for each site and flight date. Finally, the flight_dates.csv file contains the date of roughly the middle of the flight campaign, to help you line up the comparable flux data with the NDVI data.

Four sites are included, three of them with data from two years. The sites, from south to north, are Konza Prairie Biological Station (KONZ), Central Plains Experimental Range (CPER), Northern Great Plains Research Laboratory (NOGP), and Dakota Coteau Field Station (DCFS).

If you're working in R, you may want to create an R Project and add the data folder to it, to keep the file paths simple. The code below assumes this structure; otherwise, you'll simply have to modify the file paths.

Start by loading the flux data and flight dates:

flux.all <- read.csv("~/data/flux_allSites.csv")
flight.dates <- read.csv("~/data/flight_dates.csv")

Let's start by viewing the data files for a single site and year, to get familiar with the data. We'll use KONZ July 2020.

Load the footprint and NDVI data. The .grd and .gri files both contain information, but loading the data only requires pointing to the .grd files.

## 
foot.KONZ <- raster("~/data/footKONZ2020-07.grd")
ndvi.KONZ <- raster("~/data/ndviKONZ2020-07.grd")
## 
## # if using Python and rasterio:
## footKONZ = rasterio.open("/data/footKONZ2020-07.grd")
## ndviKONZ = rasterio.open("/data/ndviKONZ2020-07.grd")
## 

Explore Data from One Site

Let's start by viewing the NDVI data. The plot() function will recognize the data type and create a nice image:

plot(ndvi.KONZ)

plot() also works for the flux footprints, but for these data I prefer the look of a contour plot, which shows the boundaries of different levels of influence on the flux:

filledContour(foot.KONZ, col=topo.colors(20))

To get a better look at the region contributing the most to the flux, we can trim the raster down to pixels with a value greater than 0.0005, using the calc() and trim() functions from the raster package. First we set all pixels with a low value to zero, then trim the raster to exclude those pixels:

footAdj <- calc(foot.KONZ, 
                 fun=function(x){ x[x < 0.0005] <- 0; 
                 return(x)})
footMost <- trim(footAdj, values=0)
filledContour(footMost, col=topo.colors(20))

We can also overlay the NDVI and footprint data, to see where the footprint falls on the landscape. In this case we'll use the plot() function for clearer visuals:

plot(ndvi.KONZ)
plot(foot.KONZ, add=T,
     col=terrain.colors(5, alpha=0.5))

Next step: calculate mean NDVI from the sampled area, and then NDVI weighted by the contribution of the pixel to the fluxes. Starting with mean NDVI, we'll use the cellStats() function from the raster package.

cellStats(ndvi.KONZ, stat="mean", na.rm=T)

## [1] 0.7722162

To get NDVI weighted by the footprint, we need to make the two rasters comparable. Currently their boundaries don't match, and neither does their resolution. Again, the raster package has methods for this: crop() to crop the NDVI raster down to cover the same area as the footprint, and resample() to adjust the footprint raster to the resolution of the NDVI raster.

ndvi.KONZ <- crop(ndvi.KONZ, extent(foot.KONZ))
foot.KONZ <- resample(foot.KONZ, ndvi.KONZ)

In the footprint raster, each pixel's value is its proportional contribution to the flux. But now that we've resampled it to a different resolution, the sum of the pixels is no longer 1. We need to re-level the pixel values to bring them back to proportional values:

foot.KONZ <- foot.KONZ/cellStats(foot.KONZ, 
                                 stat="sum", 
                                 na.rm=T)

Now we can calculate the weighted NDVI, multiplying each NDVI pixel by its footprint weight and then using the cellStats() function as we did above:

prop.weight <- foot.KONZ*ndvi.KONZ
cellStats(prop.weight, stat="sum", 
          na.rm=T)

## [1] 0.7992741

Here we can see NDVI within the flux footprint is a little higher than NDVI across the entire region.

Let's take a look at the fluxes from the same time. We'll subset the flux.all table based on the site, and the dates from the flight.dates table. Let's include the date in the table, plus one on either side, since it's rare that the flights are completed in a single day (see Assumptions section below for discussion about the precise data collection dates).

flux.all$timeBgn <- as.POSIXct(flux.all$timeBgn, 
                               format="%Y-%m-%d %H:%M:%S",
                               tz="GMT")
flux.KONZ <- flux.all[which(flux.all$siteID=="KONZ" & 
                      flux.all$timeBgn>=as.POSIXct("2020-07-11", 
                                                   tz="GMT") &
                      flux.all$timeBgn<as.POSIXct("2020-07-14", 
                                                   tz="GMT")),]
plot(flux.KONZ$data.fluxCo2.nsae.flux~flux.KONZ$timeBgn, 
     pch=20, xlab="Date", ylab="Net ecosystem exchange")

Now we have fluxes and weighted NDVI from one site, but this doesn't tell us much by itself! We need context from other sites and years.

Calculate Weighted NDVI and Fluxes Across Sites

In the next two code chunks, we'll write a function to make the weighted NDVI calculations above, and then loop over the sites and dates in the dataset. I encourage you to try out writing the function and the loop yourself, especially if you're working on developing your coding skills. But if you're not familiar with how to do this, you can copy the code as written below.

Function to calculate footprint-weighted NDVI:

foot.weighted <- function(ndvi.raster, foot.raster) {

  ndvi.foot <- crop(ndvi.raster, extent(foot.raster))
  foot.ndvi <- raster::resample(foot.raster, ndvi.foot)
  foot.ndvi <- foot.ndvi/cellStats(foot.ndvi, stat="sum", na.rm=T)
  comb <- foot.ndvi*ndvi.foot
  w.ndvi <- cellStats(comb, stat="sum", na.rm=T)
  return(w.ndvi)
  
}

Loop over the rasters of all sites and dates, reading in both the NDVI and footprint rasters and applying the weighting function. If your data aren't in a project folder, or your folder structure differs, you'll need to adjust the file paths at the start of the for loop.

flight.dates$month <- substring(flight.dates$FlightDate, 1, 7)
ndvi.w <- character()
for(i in unique(flight.dates$Site)) {
  
  ffls <- list.files("~/data", "foot", full.names=T)
  afls <- list.files("~/data", "ndvi", full.names=T)
  ffls <- grep(".grd$", ffls, value=T)
  afls <- grep(".grd$", afls, value=T)

  flight.dates.i <- flight.dates[which(flight.dates$Site==i),]
  
  for(j in unique(flight.dates.i$month)) {
    
    footfl <- grep(i, ffls, value=T)
    footfl <- grep(j, footfl, value=T)
    if(length(footfl)==0) {next}
    nfl <- grep(i, afls, value=T)
    nfl <- grep(j, nfl, value=T)
    
    foot <- raster(footfl)
    ndvi <- raster(nfl)
    
    nw <- foot.weighted(ndvi, foot)
    ndvi.w <- rbind(ndvi.w, c(i, j, nw))
    
  }
  
}

ndvi.w <- data.frame(ndvi.w)
names(ndvi.w) <- c('site','month','ndvi')
ndvi.w$ndvi <- as.numeric(ndvi.w$ndvi)

ndvi.w

##   site   month      ndvi
## 1 KONZ 2019-05 0.7561873
## 2 KONZ 2020-07 0.7992741
## 3 NOGP 2019-07 0.7307495
## 4 NOGP 2021-06 0.4495142
## 5 CPER 2020-06 0.2578832
## 6 CPER 2021-06 0.3197897
## 7 DCFS 2020-06 0.7258271

Now we have a table of footprint-weighted NDVI for every site and month of data collection. Great! All we need now is flux data to compare it to.

We'll again loop over the sites and dates in the dataset, but this time we'll subset the flux dataset to the date of the flight, plus one day on either side, and calculate the daily net ecosystem exchange. Again, I encourage you to give this a try on your own, but the code I used to do it is in the next code chunk.

ndvi.w$flux <- NA
flight.dates$FlightDate <- as.POSIXct(flight.dates$FlightDate, 
                                      tz="GMT")
for(i in 1:nrow(flight.dates)) {
  
  flux.sub <- flux.all[which(flux.all$siteID==
                               flight.dates$Site[i] & 
                        flux.all$timeBgn>=
                          I(flight.dates$FlightDate[i]-86400) &
                        flux.all$timeBgn<
                          I(flight.dates$FlightDate[i]+86400)),]
  fl <- mean(flux.sub$data.fluxCo2.nsae.flux, na.rm=T)
  ndvi.w$flux[which(ndvi.w$site==flight.dates$Site[i] & 
                      ndvi.w$month==flight.dates$month[i])] <- fl
  
}

# convert to units of grams of carbon per meter squared per day
ndvi.w$flux <- ndvi.w$flux*1e-6*12.011*86400

Now plot NEE against weighted NDVI:

plot(ndvi.w$flux~ndvi.w$ndvi, 
     xlab="NDVI", ylab="NEE",
     pch=20)

To get a little more insight, let's plot the site code for each data point:

plot(ndvi.w$flux~ndvi.w$ndvi, 
     xlab="NDVI", ylab="NEE",
     type="n", pch=20)
text(ndvi.w$ndvi, ndvi.w$flux,
     labels=ndvi.w$site, cex=0.5)

We can see that increasing NDVI is associated with increased carbon uptake. But we calculated this using overall NEE, which includes respiration. Depending on our analysis, it might be more appropriate to try to build a model to predict uptake, and to model respiration separately.

To do this properly, we'd need to use flux partitioning methods, but we can get a very crude idea of what the results might look like by re-calculating our NEE estimate using only the negative values from each day. We'll re-use the flux calculation for loop above:

ndvi.w$dayflux <- NA
for(i in 1:nrow(flight.dates)) {
  
  flux.sub <- flux.all[which(flux.all$siteID==
                               flight.dates$Site[i] & 
                        flux.all$timeBgn>=
                          I(flight.dates$FlightDate[i]-86400) &
                        flux.all$timeBgn<
                          I(flight.dates$FlightDate[i]+86400)),]
  fl <- mean(flux.sub$data.fluxCo2.nsae.flux
             [which(flux.sub$data.fluxCo2.nsae.flux<0)], na.rm=T)
  ndvi.w$dayflux[which(ndvi.w$site==flight.dates$Site[i] & 
                      ndvi.w$month==flight.dates$month[i])] <- fl
  
}

ndvi.w$dayflux <- ndvi.w$dayflux*1e-6*12.011*86400

And plot carbon uptake as above:

plot(ndvi.w$dayflux~ndvi.w$ndvi, 
     xlab="NDVI", ylab="Daytime NEE",
     type="n", pch=20)
text(ndvi.w$ndvi, ndvi.w$dayflux,
     labels=ndvi.w$site, cex=0.5)

The relationship is much tighter when we consider only net uptake values.

Assumptions, Simplifications, and Further Possibilities

As mentioned, it's best to model GPP and respiration separately, and we would expect NDVI to have a much stronger relationship with GPP than respiration. But that's not the only way our analysis here is limited relative to what's possible.

To simplify our calculations of which fluxes correspond to the NDVI values, I provided the flight.dates table, which contains a single date for each site and year. In reality, the flights can span several days, which aren't always consecutive. In addition to simplifying the flight info, the flux footprints in the provided dataset are averaged over all the days when the plane was overhead.

The data product Spectrometer orthorectified surface directional reflectance - mosaic (DP3.30006.001) includes data tiles containing the flight date when each pixel was collected. The expanded package of the flux data product, Bundled data products - eddy covariance (DP4.00200.001) includes flux footprints for every half-hourly flux calculation interval. It is possible to use these datasets to calculate the contribution to the flux of every NDVI pixel at the time it was observed. However, this would be a very elaborate calculation, and before going down that road, consider the scientific question you're trying to answer. Given what you know about uncertainties in fluxes, uncertainties in NDVI, and the relative rates of change of NDVI and NEE over time, is your question sensitive to this level of detail?

Considering sensitivity raises another question: How important were the flux footprints to our analysis? How different would the results be if we simply calculated NDVI for, say, a one kilometer radius around the tower? This is an easy question to answer, with only a small modification to the code above. Give it a try, and again, consider the types of analyses that might demand different levels of precision.

Finally, note that in our dataset, we had three pairs of repeat measurements at the same site. In general, we would expect much better model predictions within a site than between sites. And we restricted ourselves to Great Plains grasslands. How would we expect this exercise to differ if we expanded into other site types?

Get Lesson Code

flux-footprint-NDVI.R

Questions?

If you have questions or comments on this content, please contact us.

Contact Us
NSF NEON, Operated by Battelle

Follow Us:

Join Our Newsletter

Get updates on events, opportunities, and how NEON is being used today.

Subscribe Now

Footer

  • About Us
  • Contact Us
  • Terms & Conditions
  • Careers
  • Code of Conduct

Copyright © Battelle, 2026

The National Ecological Observatory Network is a major facility fully funded by the U.S. National Science Foundation.

Any opinions, findings and conclusions or recommendations expressed in this material do not necessarily reflect the views of the U.S. National Science Foundation.