5. Extract ocean color data in optically-shallow waters - new version
Tom Oliver (NOAA/PIFSC), Melanie Abecassis (NOAA CoastWatch)
Remotely sensed ocean color algorithms are calibrated for optically-deep waters, where the signal received by the satellite sensor originates from the water column without any bottom contribution.
Optically shallow waters are those in which light reflected off the seafloor contributes significantly to the water-leaving signal, such as coral reefs, atolls, lagoons. This is known to affect geophysical variables derived by ocean-color algorithms, often leading to biased values in chlorophyll-a concentration for example.
In the tropical Pacific, optically-deep waters are typically deeper than 15 – 30 m. It is recommended to remove shallow-pixels (<30m depth) from the study area before computing ocean color metrics.
In this tutorial, we will extract chl-a concentration data at survey locations around Wake island in the Pacific Ocean. For survey points located in waters shallower than 30m, we will find chl-a pixels in deeper water and extract those values instead.
The survey locations can be downloaded here: https://oceanwatch.pifsc.noaa.gov/files/wake.csv
Load packages
library(raster)
library(sp)
library(rerddap)
library(lubridate)
Load survey data
Wake=read.csv('wake.csv')
Wake
Deploy_Longitude
Deploy_Latitude
Year
Island
Site
1
166.6073
19.29178
2014
WAK
WAK06
2
166.5983
19.31627
2014
WAK
WAK08
3
166.6516
19.27068
2014
WAK
WAK09
4
166.5983
19.31621
2017
WAK
WAK08
5
166.6272
19.31605
2017
WAK
WAK23
6
166.6278
19.28066
2017
WAK
WAK01
7
166.6511
19.30614
2017
WAK
WAK24
Let's transform our survey data into a SpatialPointsDataFrame
, which makes it easier to work with geospatial rasters:
coordinates(Wake)<- ~Deploy_Longitude+Deploy_Latitude
Wake
class : SpatialPointsDataFrame
features : 7
extent : 166.5983, 166.6516, 19.27068, 19.31627 (xmin, xmax, ymin, ymax)
crs : NA
variables : 3
names : Year, Island, Site
min values : 2014, WAK, WAK01
max values : 2017, WAK, WAK24
Download bathymetry and chl-a concentration data for the survey area
scale=.05
CW_u='https://coastwatch.pfeg.noaa.gov/erddap/'
ETOPO1_id='etopo180'
ETOPO1_info=info(datasetid = ETOPO1_id,url = CW_u)
WakeBathy=griddap(ETOPO1_info,url=CW_u, latitude=(range(Wake$Deploy_Latitude)+c(-1,1)*scale), longitude=(range(Wake$Deploy_Longitude)+c(-1,1)*scale), store=disk(),fmt = "nc")
For Chl-a, let's first extract a random time step:
OW_u='https://oceanwatch.pifsc.noaa.gov/erddap/'
VIIRS_id='noaa_snpp_chla_monthly'
VIIRS_info=info(datasetid = VIIRS_id,url = OW_u)
var=VIIRS_info$variable$variable_name
WakeVIIRS=griddap(url=OW_u, VIIRS_id, time = c('2016-04-01', '2016-04-01'), latitude = range(Wake$Deploy_Latitude)+c(-1,1)*scale, longitude = range(Wake$Deploy_Longitude)+c(-1,1)*scale, fields = var[1], store=disk(),fmt = "nc" )
Convert bathymetry and chl-a data to rasters
rWB=raster(WakeBathy$summary$filename)
rVI=raster(WakeVIIRS$summary$filename,varname="chlor_a")
Let's look at our rasters:
blue.col <- colorRampPalette(c("darkblue", "lightblue"))
chl.col=colorRampPalette(c("#00ffff","#00e600"))
plot(rWB,main="ETOPO1 Bathymetry Raster",col=blue.col(255))
contour(rWB,levels=c(-30,-1000,-2000),add=TRUE)
plot(Wake,add=TRUE,pch=16)
plot(log(rVI),main="VIIRS CHLA Raster (log scale)",col=chl.col(255))
plot(Wake,add=TRUE,pch=16)
Depth at survey sites
Wake$Depth=raster::extract(x=rWB,y=Wake)
Wake$Depth
[1] 3 53 2 53 2 2 5
The bathymetry data has ~2-km resolution, whereas the chl-a data has a ~4-km resolution.
We are going to build a function to look at the chl-a data in our survey area and determine whether to call each chl-a pixel "shallow" based on how many shallow (<30m) bathymetry pixels it overlaps.
1. Convert the depth raster to a SpatialPointsDataFrame : extent(rWB)=extent(rVI)
spWB=data.frame(rasterToPoints(rWB))
coordinates(spWB)=~x+y
plot(rWB,main="ETOPO1 Bathymetry Raster",col=blue.col(255))
contour(rWB,levels=c(-30),add=TRUE)
plot(spWB,add=TRUE,pch=20)
plot(log(rVI),main="VIIRS Chl-a Raster",col=chl.col(255))
contour(rWB,levels=c(-30),add=TRUE)
plot(spWB,add=TRUE,pch=20)
Then we plot those SpatialPoints on top of the chl-a raster:
Each chl-a pixel overlaps 4 bathymetry pixels
2. Define a function to consider a pixel necessary to mask:
count_shallow_pixels=function(depths,threshold=-30,na.rm=T){
return(length(which(depths>threshold)))
}
3. Build a raster of the chl-a grid, using the function to count how many (smaller) depth pixels in each (larger) Chla pixel are "too shallow" (out of 4):
rVI.N_SHALLOW=rasterize(x = spWB,y=rVI,field="Altitude",fun=count_shallow_pixels)
plot(rVI.N_SHALLOW,main="N Shallow Pixels",asp=1)
contour(rWB,levels=c(-30),add=TRUE)
plot(spWB,add=T,pch=20)
Masking
Decide what threshold means a pixel is 'bad', then generate the masked chl-a layer. For example, let's mask all chl-a pixels that overlap 2 or more shallow bathymetry pixels:
rVI.masked=rVI
rVI.masked[rVI.N_SHALLOW>=2]=NA
plot(log(rVI.masked),main="Masked VIIRS Chl-A",col=chl.col(255))
plot(Wake,add=T,pch=20,cex=2,col=2)
contour(rWB,levels=c(-30),add=TRUE)
text(Wake@coords[,1],Wake@coords[,2],row.names(Wake@coords),pos=4)
Values for survey locations in masked pixels
For the points located in shallow pixels, we now need to pick a new chl-a pixel to substitute for each masked one. To that end, for each survey location, we will select the closest unmasked chl-a pixel. We will then stored those new locations to download Chl-a data.
This will allow you to download Chl-a data for different time ranges for each location if needed.
Let's look at the example of survey location #5:
r=rVI.masked
xy=Wake@coords
i=5
d=replace(distanceFromPoints(r, xy[i,]), is.na(r), NA)
plot(d)
points(Wake@coords[i,1],Wake@coords[i,2],pch=20,col=2,cex=2)
text(Wake@coords[i,1],Wake@coords[i,2],row.names(Wake@coords)[i],pos=4)
d
is the distance of each chl-a pixel to survey location #5.
We need to select the pixel closest (minimum distance) to our location:
new_coords=xyFromCell(r,which.min(d))
plot(d)
points(Wake@coords[i,1],Wake@coords[i,2],pch=20,col=2,cex=2)
text(Wake@coords[i,1],Wake@coords[i,2],row.names(Wake@coords)[i],pos=4)
points(new_coords[1],new_coords[2],pch=20,col=4,cex=2)
Now we need to do this for all our locations, and store the new coordinates in the Wake data frame.
new_coords=array(NA,dim=c(dim(xy)[1],2))
for (i in 1:dim(xy)[1]) {
d=replace(distanceFromPoints(r, xy[i,]), is.na(r), NA)
new_coords[i,]=xyFromCell(r,which.min(d))
}
colnames(new_coords)=c("new_lon","new_lat")
Wake=cbind(data.frame(Wake),new_coords)
Wake
Deploy_Longitude
Deploy_Latitude
Year
Island
Site
Depth
optional
new_lon
new_lat
1
166.6073
19.29178
2014
WAK
WAK06
3
TRUE
166.5938
19.29375
2
166.5983
19.31627
2014
WAK
WAK08
53
TRUE
166.5938
19.29375
3
166.6516
19.27068
2014
WAK
WAK09
2
TRUE
166.6688
19.25625
4
166.5983
19.31621
2017
WAK
WAK08
53
TRUE
166.5938
19.29375
5
166.6272
19.31605
2017
WAK
WAK23
2
TRUE
166.5938
19.29375
6
166.6278
19.28066
2017
WAK
WAK01
2
TRUE
166.6313
19.25625
7
166.6511
19.30614
2017
WAK
WAK24
5
TRUE
166.6688
19.29375
plot(log(rVI.masked),main="Masked VIIRS Chl-A",col=chl.col(255))
contour(rWB,levels=c(-30),add=TRUE)
points(Wake$new_lon,Wake$new_lat,pch=20,col=2,cex=2)
text(Wake$new_lon,Wake$new_lat,row.names(Wake),pos=4)
Last updated