├── .idea ├── misc.xml ├── modules.xml ├── vcs.xml ├── water-quality-gee.iml └── workspace.xml ├── LICENSE ├── README.md ├── javascript ├── Ls8_FAI.js ├── Ls8_WQ_TimeSeries.js ├── Ls8_single_image.js ├── MODIS_WQ_TimeSeries.js ├── s2_FAI.js ├── s2_WQ_TimeSeries.js └── s2_single_image.js └── python ├── README.md ├── ls8_fai.py ├── ls8_single_image.py ├── ls8_wq_timeseries.py ├── modis_wq_timeseries.py ├── s2_fai.py ├── s2_single_image.py └── s2_wq_timeseries.py /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/water-quality-gee.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # water-quality-gee 2 | This repository contains scripts for assessing water quality from remotely sensed data using Google Earth Engine 3 | 4 | If using any of these materials or scripts, please cite: Page, B.P. and D. Mishra (2018), A Modified Atmospheric Correction when Coupling Sentinel-2A and Landsat-8 for Inland Water Quality Monitoring, ISPRS J PHOTOGRAMM, *In Review* 5 | 6 | 7 | ## REQUIREMETS: 8 | - The scripts require a [Google Earth Engine](https://www.earthengine.google.com "Google Earth Engine Homepage") account. 9 | 10 | To use in the [GEE Code Editor](https://code.earthengine.google.com), download the javascript files and click run. 11 | 12 | To use in a Python environment, install the GEE Python API and run programs in the Python IDE of your choice. 13 | 14 | 15 | ## CONTENTS: 16 | - javascript/ contains javascript implementation of water quality processing code 17 | - python/ contains Python implementation of water quality processing code 18 | 19 | 20 | ## CONTRIBUTING: 21 | If you have any ideas, just [open an issue](https://github.com/SERVIR/water-quality-gee/issues) and tell us what you think. 22 | 23 | If you'd like to contribute, please fork the repository and make changes as 24 | you'd like. Pull requests are welcome. 25 | 26 | 27 | ## DISCLAIMERS: 28 | This is a work-in-progress. The scripts are offered as-is, although we will make every effort to test 29 | and fix any issues, we can't guarantee that these will be fit for your purpose. 30 | -------------------------------------------------------------------------------- /javascript/Ls8_FAI.js: -------------------------------------------------------------------------------- 1 | // This script processes a single Landsat 8 Tier 1 Raw image to Rayleigh corrected reflectances. 2 | // The floating algal index (FAI) is calculated from Rrc over water bodies. 3 | // How to use: 4 | // (1) If a "geometry" variable exists in the imports window, delete it. 5 | // (2) Within the map, select the point button near the top left side. 6 | // (3) Create a new point by clicking on a location of interest and click run. 7 | // (4) The "available imagery" ImageCollection within the console displays all available imagery 8 | // over that location with the necessary filters described by the user below. 9 | // (5) Expand the features within the "available imagery" image collection and select an image 10 | // by highlighting the FILE_ID and pasting it here: 11 | var l8 = ee.Image('LANDSAT/LC08/C01/T1/LC08_170060_20171226'); 12 | // (6) Click "Run" 13 | // (7) Export the image to your Google Drive by clicking on the "tasks" tab and clicking "RUN", be sure to specify 14 | // the proper folder. 15 | 16 | // Author: Benjamin Page // 17 | // Citations: 18 | // Page, B.P., Kumar, A. and Mishra, D.R., 2018. A novel cross-satellite based assessment of the spatio-temporal development of a cyanobacterial harmful algal bloom. International Journal of Applied Earth Observation and Geoinformation, 66, pp.69-81. 19 | 20 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 21 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 22 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 23 | // User Input // 24 | 25 | // begin date 26 | var iniDate = '2015-05-01'; 27 | 28 | // end date 29 | var endDate = '2018-03-31'; 30 | 31 | var oliCloudPerc = 5 32 | 33 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 34 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 35 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 36 | 37 | // Import Collections // 38 | 39 | // landsat 8 raw dn 40 | var OLI_DN = ee.ImageCollection('LANDSAT/LC08/C01/T1'); 41 | 42 | // landsat-8 surface reflactance product (for masking purposes) 43 | var SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR'); 44 | 45 | // toms / omi 46 | var ozone = ee.ImageCollection('TOMS/MERGED'); 47 | 48 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 49 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 50 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 51 | // Filtering Collection and Masking // 52 | 53 | var pi = ee.Image(3.141592); 54 | 55 | // water mask 56 | var startMonth = 5; 57 | var endMonth = 9; 58 | var startYear = 2013; 59 | var endYear = 2017; 60 | 61 | var forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')); 62 | var mask = ee.Image(forMask.select('B6').median().lt(600)) 63 | mask = mask.updateMask(mask) 64 | Map.addLayer(mask, {}, 'mask', false) 65 | 66 | // filter landsat 8 collection 67 | var FC_OLI = OLI_DN.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUD_COVER', "less_than", oliCloudPerc); 68 | print(FC_OLI, 'Available Imagery') 69 | 70 | print(l8, 'l8 image info') 71 | 72 | // oli image date 73 | var oliDate = l8.date(); 74 | print(oliDate, 'l8 Date') 75 | 76 | var footprint = l8.geometry() 77 | Map.centerObject(footprint) 78 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 79 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 80 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 81 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 82 | // single Ls8 image Rayleigh Correction // 83 | 84 | // dem 85 | var DEM_OLI = ee.Image('USGS/SRTMGL1_003').clip(footprint); 86 | 87 | // ozone 88 | var DU_OLI = ee.Image(ozone.filterDate(iniDate,endDate).filterBounds(footprint).mean()); 89 | 90 | //Julian Day 91 | var imgDate_OLI = ee.Date(l8.get('system:time_start')); 92 | var FOY_OLI = ee.Date.fromYMD(imgDate_OLI.get('year'),1,1); 93 | var JD_OLI = imgDate_OLI.difference(FOY_OLI,'day').int().add(1); 94 | 95 | // Earth-Sun distance 96 | var d_OLI = ee.Image.constant(l8.get('EARTH_SUN_DISTANCE')); 97 | 98 | //Sun elevation 99 | var SunEl_OLI = ee.Image.constant(l8.get('SUN_ELEVATION')); 100 | 101 | //Sun azimuth 102 | var SunAz_OLI = ee.Image.constant(l8.get('SUN_AZIMUTH')); 103 | 104 | //Satellite zenith 105 | var SatZe_OLI = ee.Image(0.0) 106 | var cosdSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).cos(); 107 | var sindSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).sin(); 108 | 109 | //Satellite azimuth 110 | var SatAz_OLI = ee.Image(0.0) 111 | 112 | //Sun zenith 113 | var SunZe_OLI = ee.Image(90).subtract(SunEl_OLI) 114 | var cosdSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image.constant(180))).cos(); // in degrees 115 | var sindSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image(180))).sin(); // in degrees 116 | 117 | //Relative azimuth 118 | var RelAz_OLI = ee.Image(SunAz_OLI); 119 | var cosdRelAz_OLI = RelAz_OLI.multiply(pi.divide(ee.Image(180))).cos(); 120 | 121 | //Pressure calculation 122 | var P_OLI = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM_OLI)).pow(5.25588)).multiply(0.01); 123 | var Po_OLI = ee.Image(1013.25); 124 | 125 | // Radiometric Calibration // 126 | //define bands to be converted to radiance 127 | var bands_OLI = ['B1','B2','B3','B4','B5','B6','B7']; 128 | 129 | // radiance_mult_bands 130 | var rad_mult_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_MULT_BAND_1')), 131 | ee.Image(l8.get('RADIANCE_MULT_BAND_2')), 132 | ee.Image(l8.get('RADIANCE_MULT_BAND_3')), 133 | ee.Image(l8.get('RADIANCE_MULT_BAND_4')), 134 | ee.Image(l8.get('RADIANCE_MULT_BAND_5')), 135 | ee.Image(l8.get('RADIANCE_MULT_BAND_6')), 136 | ee.Image(l8.get('RADIANCE_MULT_BAND_7'))] 137 | )).toArray(1); 138 | 139 | // radiance add band 140 | var rad_add_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_ADD_BAND_1')), 141 | ee.Image(l8.get('RADIANCE_ADD_BAND_2')), 142 | ee.Image(l8.get('RADIANCE_ADD_BAND_3')), 143 | ee.Image(l8.get('RADIANCE_ADD_BAND_4')), 144 | ee.Image(l8.get('RADIANCE_ADD_BAND_5')), 145 | ee.Image(l8.get('RADIANCE_ADD_BAND_6')), 146 | ee.Image(l8.get('RADIANCE_ADD_BAND_7'))] 147 | )).toArray(1); 148 | 149 | //create an empty image to save new radiance bands to 150 | var imgArr_OLI = l8.select(bands_OLI).toArray().toArray(1); 151 | var Ltoa_OLI = imgArr_OLI.multiply(rad_mult_OLI).add(rad_add_OLI) 152 | 153 | // esun 154 | var ESUN_OLI = ee.Image.constant(197.24790954589844) 155 | .addBands(ee.Image.constant(201.98426818847656)) 156 | .addBands(ee.Image.constant(186.12677001953125)) 157 | .addBands(ee.Image.constant(156.95257568359375)) 158 | .addBands(ee.Image.constant(96.04714965820312)) 159 | .addBands(ee.Image.constant(23.8833221450863)) 160 | .addBands(ee.Image.constant(8.04995873449635)).toArray().toArray(1); 161 | ESUN_OLI = ESUN_OLI.multiply(ee.Image(1)) 162 | 163 | var ESUNImg_OLI = ESUN_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 164 | 165 | // Ozone Correction // 166 | // Ozone coefficients 167 | var koz_OLI = ee.Image.constant(0.0039).addBands(ee.Image.constant(0.0218)) 168 | .addBands(ee.Image.constant(0.1078)) 169 | .addBands(ee.Image.constant(0.0608)) 170 | .addBands(ee.Image.constant(0.0019)) 171 | .addBands(ee.Image.constant(0)) 172 | .addBands(ee.Image.constant(0)) 173 | .toArray().toArray(1); 174 | 175 | // Calculate ozone optical thickness 176 | var Toz_OLI = koz_OLI.multiply(DU_OLI).divide(ee.Image.constant(1000)); 177 | 178 | // Calculate TOA radiance in the absense of ozone 179 | var Lt_OLI = Ltoa_OLI.multiply(((Toz_OLI)).multiply((ee.Image.constant(1).divide(cosdSunZe_OLI)).add(ee.Image.constant(1).divide(cosdSatZe_OLI))).exp()); 180 | 181 | // Rayleigh optical thickness 182 | var bandCenter_OLI = ee.Image(443).divide(1000).addBands(ee.Image(483).divide(1000)) 183 | .addBands(ee.Image(561).divide(1000)) 184 | .addBands(ee.Image(655).divide(1000)) 185 | .addBands(ee.Image(865).divide(1000)) 186 | .addBands(ee.Image(1609).divide(1000)) 187 | .addBands(ee.Number(2201).divide(1000)) 188 | .toArray().toArray(1); 189 | 190 | // create an empty image to save new Tr values to 191 | var Tr_OLI = (P_OLI.divide(Po_OLI)).multiply(ee.Image(0.008569).multiply(bandCenter_OLI.pow(-4))).multiply((ee.Image(1).add(ee.Image(0.0113).multiply(bandCenter_OLI.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter_OLI.pow(-4))))); 192 | 193 | // Fresnel Reflection // 194 | // Specular reflection (s- and p- polarization states) 195 | var theta_V_OLI = ee.Image(0.0000000001); 196 | var sin_theta_j_OLI = sindSunZe_OLI.divide(ee.Image(1.333)); 197 | 198 | var theta_j_OLI = sin_theta_j_OLI.asin().multiply(ee.Image(180).divide(pi)); 199 | 200 | var theta_SZ_OLI = SunZe_OLI; 201 | 202 | var R_theta_SZ_s_OLI = (((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).subtract(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).add(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2))); 203 | 204 | var R_theta_V_s_OLI = ee.Image(0.0000000001); 205 | 206 | var R_theta_SZ_p_OLI = (((theta_SZ_OLI.multiply(pi.divide(180))).subtract(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(180))).add(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2))); 207 | 208 | var R_theta_V_p_OLI = ee.Image(0.0000000001); 209 | 210 | var R_theta_SZ_OLI = ee.Image(0.5).multiply(R_theta_SZ_s_OLI.add(R_theta_SZ_p_OLI)); 211 | 212 | var R_theta_V_OLI = ee.Image(0.5).multiply(R_theta_V_s_OLI.add(R_theta_V_p_OLI)); 213 | 214 | // Rayleigh scattering phase function // 215 | // Sun-sensor geometry 216 | var theta_neg_OLI = ((cosdSunZe_OLI.multiply(ee.Image(-1))).multiply(cosdSatZe_OLI)).subtract((sindSunZe_OLI).multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)); 217 | 218 | var theta_neg_inv_OLI = theta_neg_OLI.acos().multiply(ee.Image(180).divide(pi)); 219 | 220 | var theta_pos_OLI = (cosdSunZe_OLI.multiply(cosdSatZe_OLI)).subtract(sindSunZe_OLI.multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)); 221 | 222 | var theta_pos_inv_OLI = theta_pos_OLI.acos().multiply(ee.Image(180).divide(pi)); 223 | 224 | var cosd_tni_OLI = theta_neg_inv_OLI.multiply(pi.divide(180)).cos(); // in degrees 225 | 226 | var cosd_tpi_OLI = theta_pos_inv_OLI.multiply(pi.divide(180)).cos(); // in degrees 227 | 228 | var Pr_neg_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni_OLI.pow(2)))); 229 | 230 | var Pr_pos_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi_OLI.pow(2)))); 231 | 232 | // Rayleigh scattering phase function 233 | var Pr_OLI = Pr_neg_OLI.add((R_theta_SZ_OLI.add(R_theta_V_OLI)).multiply(Pr_pos_OLI)); 234 | 235 | // Calulate Lr, 236 | var denom_OLI = ee.Image(4).multiply(pi).multiply(cosdSatZe_OLI); 237 | var Lr_OLI = (ESUN_OLI.multiply(Tr_OLI)).multiply(Pr_OLI.divide(denom_OLI)); 238 | 239 | // Rayleigh corrected radiance 240 | var Lrc_OLI = (Lt_OLI.divide(ee.Image(10))).subtract(Lr_OLI); 241 | var LrcImg_OLI = Lrc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 242 | 243 | // Rayleigh corrected reflectance 244 | var prc_OLI = Lrc_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI)); 245 | var pc = prc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 246 | 247 | // Calculate FAI 248 | var NIRprime = (pc.select('B4')).add((pc.select('B6').subtract(pc.select('B4'))).multiply((ee.Image(865).subtract(ee.Image(655))).divide((ee.Image(1609).subtract(ee.Image(655)))))); 249 | var fai = ((pc.select('B5').subtract(NIRprime))).multiply(mask); 250 | 251 | // Map Layers // 252 | // Map Layers 253 | Map.addLayer(footprint, {}, 'footprint', false); 254 | Map.addLayer(pc.multiply(mask), {bands: ['B4', 'B3', 'B2'], min: 0, max: 0.1}, 'pc rgb', false) 255 | Map.addLayer(fai, {min: -0.05, max: 0.2, palette: ['000080','0080FF','7BFF7B','FF9700','800000']}, 'FAI', true); 256 | 257 | // Export Image to Drive 258 | Export.image.toDrive({ 259 | image: fai, 260 | description: 'l8_FAI', 261 | scale: 30, 262 | region: footprint 263 | }); -------------------------------------------------------------------------------- /javascript/Ls8_WQ_TimeSeries.js: -------------------------------------------------------------------------------- 1 | // This script processes and charts WQ parameter valuse from a collection of Lansdat 8 archive for a particular pixel throughout time. 2 | // WQ parameters such secchi depth, tropshic state index, and lake temperature 3 | // How to use: 4 | // (1) If a "geometry" variable exists in the imports window, delete it. 5 | // (2) Within the map, select the point button near the top left side. 6 | // (3) Create a new point by clicking on a location of interest. 7 | // (4) Adjust the time frame you wish to browse by adding here: 8 | // begin date 9 | var iniDate = '2016-04-01'; 10 | // end date 11 | var endDate = '2016-10-31'; 12 | // (5) Adjust a cloud % threshold here: 13 | var oliCloudPerc = 10 14 | // (5) Click Run 15 | // (5) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 16 | // image from this collection, highlight the FILE_ID and copy and paste it into the proper location within the "Ls8 Single Image" script. 17 | // (6) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 18 | // (6) Click "Run" 19 | // (7) Export each chart as a csv by clicking on the export icon near the top-right of the chart. 20 | 21 | // Author: Benjamin Page // 22 | // Citations: 23 | // Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring 24 | 25 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 26 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 28 | Map.centerObject(geometry, 7) 29 | 30 | // Import Collections // 31 | 32 | // landsat 8 raw dn 33 | var OLI_DN = ee.ImageCollection('LANDSAT/LC08/C01/T1'); 34 | 35 | // landsat-8 surface reflactance product (for masking purposes) 36 | var SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR'); 37 | 38 | // toms / omi 39 | var ozone = ee.ImageCollection('TOMS/MERGED'); 40 | 41 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 43 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 44 | // Filtering Collection and Masking // 45 | 46 | var pi = ee.Image(3.141592); 47 | 48 | // water mask 49 | var startMonth = 5; 50 | var endMonth = 9; 51 | var startYear = 2013; 52 | var endYear = 2017; 53 | 54 | var forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')); 55 | var mask = ee.Image(forMask.select('B6').median().lt(600)) 56 | mask = mask.updateMask(mask) 57 | 58 | // filter landsat 8 collection 59 | var FC_OLI = OLI_DN.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUD_COVER', "less_than", oliCloudPerc); 60 | print(FC_OLI, 'Available Imagery') 61 | 62 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 63 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 64 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | // Processing Collection // 66 | var Rrs_coll = FC_OLI.map(l8Correction); 67 | 68 | var sd_coll = Rrs_coll.map(secchi); 69 | 70 | var tsi_coll = sd_coll.map(trophicState) 71 | 72 | var tsi_collR = tsi_coll.map(reclassify); 73 | 74 | var lst_coll = FC_OLI.map(LST) 75 | 76 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 77 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 78 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 79 | // Mapping Functions // 80 | 81 | function l8Correction(img){ 82 | 83 | // tile geometry 84 | 85 | var l8Footprint = img.geometry() 86 | 87 | // dem 88 | var DEM_OLI = ee.Image('USGS/SRTMGL1_003').clip(l8Footprint); 89 | 90 | // ozone 91 | var DU_OLI = ee.Image(ozone.filterDate(iniDate,endDate).filterBounds(l8Footprint).mean()); 92 | 93 | //Julian Day 94 | var imgDate_OLI = ee.Date(img.get('system:time_start')); 95 | var FOY_OLI = ee.Date.fromYMD(imgDate_OLI.get('year'),1,1); 96 | var JD_OLI = imgDate_OLI.difference(FOY_OLI,'day').int().add(1); 97 | 98 | // Earth-Sun distance 99 | var d_OLI = ee.Image.constant(img.get('EARTH_SUN_DISTANCE')); 100 | 101 | //Sun elevation 102 | var SunEl_OLI = ee.Image.constant(img.get('SUN_ELEVATION')); 103 | 104 | //Sun azimuth 105 | var SunAz_OLI = ee.Image.constant(img.get('SUN_AZIMUTH')); 106 | 107 | //Satellite zenith 108 | var SatZe_OLI = ee.Image(0.0) 109 | var cosdSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).cos(); 110 | var sindSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).sin(); 111 | 112 | //Satellite azimuth 113 | var SatAz_OLI = ee.Image(0.0).clip(l8Footprint); 114 | 115 | //Sun zenith 116 | var SunZe_OLI = ee.Image(90).subtract(SunEl_OLI) 117 | var cosdSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image.constant(180))).cos(); // in degrees 118 | var sindSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image(180))).sin(); // in degrees 119 | 120 | //Relative azimuth 121 | var RelAz_OLI = ee.Image(SunAz_OLI); 122 | var cosdRelAz_OLI = RelAz_OLI.multiply(pi.divide(ee.Image(180))).cos(); 123 | 124 | //Pressure calculation 125 | var P_OLI = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM_OLI)).pow(5.25588)).multiply(0.01); 126 | var Po_OLI = ee.Image(1013.25); 127 | 128 | // Radiometric Calibration // 129 | //define bands to be converted to radiance 130 | var bands_OLI = ['B1','B2','B3','B4','B5','B6','B7']; 131 | 132 | // radiance_mult_bands 133 | var rad_mult_OLI = ee.Image(ee.Array([ee.Image(img.get('RADIANCE_MULT_BAND_1')), 134 | ee.Image(img.get('RADIANCE_MULT_BAND_2')), 135 | ee.Image(img.get('RADIANCE_MULT_BAND_3')), 136 | ee.Image(img.get('RADIANCE_MULT_BAND_4')), 137 | ee.Image(img.get('RADIANCE_MULT_BAND_5')), 138 | ee.Image(img.get('RADIANCE_MULT_BAND_6')), 139 | ee.Image(img.get('RADIANCE_MULT_BAND_7'))] 140 | )).toArray(1); 141 | 142 | // radiance add band 143 | var rad_add_OLI = ee.Image(ee.Array([ee.Image(img.get('RADIANCE_ADD_BAND_1')), 144 | ee.Image(img.get('RADIANCE_ADD_BAND_2')), 145 | ee.Image(img.get('RADIANCE_ADD_BAND_3')), 146 | ee.Image(img.get('RADIANCE_ADD_BAND_4')), 147 | ee.Image(img.get('RADIANCE_ADD_BAND_5')), 148 | ee.Image(img.get('RADIANCE_ADD_BAND_6')), 149 | ee.Image(img.get('RADIANCE_ADD_BAND_7'))] 150 | )).toArray(1); 151 | 152 | //create an empty image to save new radiance bands to 153 | var imgArr_OLI = img.select(bands_OLI).toArray().toArray(1); 154 | var Ltoa_OLI = imgArr_OLI.multiply(rad_mult_OLI).add(rad_add_OLI) 155 | 156 | // esun 157 | var ESUN_OLI = ee.Image.constant(197.24790954589844) 158 | .addBands(ee.Image.constant(201.98426818847656)) 159 | .addBands(ee.Image.constant(186.12677001953125)) 160 | .addBands(ee.Image.constant(156.95257568359375)) 161 | .addBands(ee.Image.constant(96.04714965820312)) 162 | .addBands(ee.Image.constant(23.8833221450863)) 163 | .addBands(ee.Image.constant(8.04995873449635)).toArray().toArray(1); 164 | ESUN_OLI = ESUN_OLI.multiply(ee.Image(1)) 165 | 166 | var ESUNImg_OLI = ESUN_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 167 | 168 | // Ozone Correction // 169 | // Ozone coefficients 170 | var koz_OLI = ee.Image.constant(0.0039).addBands(ee.Image.constant(0.0218)) 171 | .addBands(ee.Image.constant(0.1078)) 172 | .addBands(ee.Image.constant(0.0608)) 173 | .addBands(ee.Image.constant(0.0019)) 174 | .addBands(ee.Image.constant(0)) 175 | .addBands(ee.Image.constant(0)) 176 | .toArray().toArray(1); 177 | 178 | // Calculate ozone optical thickness 179 | var Toz_OLI = koz_OLI.multiply(DU_OLI).divide(ee.Image.constant(1000)); 180 | 181 | // Calculate TOA radiance in the absense of ozone 182 | var Lt_OLI = Ltoa_OLI.multiply(((Toz_OLI)).multiply((ee.Image.constant(1).divide(cosdSunZe_OLI)).add(ee.Image.constant(1).divide(cosdSatZe_OLI))).exp()); 183 | 184 | // Rayleigh optical thickness 185 | var bandCenter_OLI = ee.Image(443).divide(1000).addBands(ee.Image(483).divide(1000)) 186 | .addBands(ee.Image(561).divide(1000)) 187 | .addBands(ee.Image(655).divide(1000)) 188 | .addBands(ee.Image(865).divide(1000)) 189 | .addBands(ee.Image(1609).divide(1000)) 190 | .addBands(ee.Number(2201).divide(1000)) 191 | .toArray().toArray(1); 192 | 193 | // create an empty image to save new Tr values to 194 | var Tr_OLI = (P_OLI.divide(Po_OLI)).multiply(ee.Image(0.008569).multiply(bandCenter_OLI.pow(-4))).multiply((ee.Image(1).add(ee.Image(0.0113).multiply(bandCenter_OLI.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter_OLI.pow(-4))))); 195 | 196 | // Fresnel Reflection // 197 | // Specular reflection (s- and p- polarization states) 198 | var theta_V_OLI = ee.Image(0.0000000001); 199 | var sin_theta_j_OLI = sindSunZe_OLI.divide(ee.Image(1.333)); 200 | 201 | var theta_j_OLI = sin_theta_j_OLI.asin().multiply(ee.Image(180).divide(pi)); 202 | 203 | var theta_SZ_OLI = SunZe_OLI; 204 | 205 | var R_theta_SZ_s_OLI = (((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).subtract(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).add(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2))); 206 | 207 | var R_theta_V_s_OLI = ee.Image(0.0000000001); 208 | 209 | var R_theta_SZ_p_OLI = (((theta_SZ_OLI.multiply(pi.divide(180))).subtract(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(180))).add(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2))); 210 | 211 | var R_theta_V_p_OLI = ee.Image(0.0000000001); 212 | 213 | var R_theta_SZ_OLI = ee.Image(0.5).multiply(R_theta_SZ_s_OLI.add(R_theta_SZ_p_OLI)); 214 | 215 | var R_theta_V_OLI = ee.Image(0.5).multiply(R_theta_V_s_OLI.add(R_theta_V_p_OLI)); 216 | 217 | // Rayleigh scattering phase function // 218 | // Sun-sensor geometry 219 | var theta_neg_OLI = ((cosdSunZe_OLI.multiply(ee.Image(-1))).multiply(cosdSatZe_OLI)).subtract((sindSunZe_OLI).multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)); 220 | 221 | var theta_neg_inv_OLI = theta_neg_OLI.acos().multiply(ee.Image(180).divide(pi)); 222 | 223 | var theta_pos_OLI = (cosdSunZe_OLI.multiply(cosdSatZe_OLI)).subtract(sindSunZe_OLI.multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)); 224 | 225 | var theta_pos_inv_OLI = theta_pos_OLI.acos().multiply(ee.Image(180).divide(pi)); 226 | 227 | var cosd_tni_OLI = theta_neg_inv_OLI.multiply(pi.divide(180)).cos(); // in degrees 228 | 229 | var cosd_tpi_OLI = theta_pos_inv_OLI.multiply(pi.divide(180)).cos(); // in degrees 230 | 231 | var Pr_neg_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni_OLI.pow(2)))); 232 | 233 | var Pr_pos_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi_OLI.pow(2)))); 234 | 235 | // Rayleigh scattering phase function 236 | var Pr_OLI = Pr_neg_OLI.add((R_theta_SZ_OLI.add(R_theta_V_OLI)).multiply(Pr_pos_OLI)); 237 | 238 | // Calulate Lr, 239 | var denom_OLI = ee.Image(4).multiply(pi).multiply(cosdSatZe_OLI); 240 | var Lr_OLI = (ESUN_OLI.multiply(Tr_OLI)).multiply(Pr_OLI.divide(denom_OLI)); 241 | 242 | // Rayleigh corrected radiance 243 | var Lrc_OLI = (Lt_OLI.divide(ee.Image(10))).subtract(Lr_OLI); 244 | var LrcImg_OLI = Lrc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 245 | 246 | // Rayleigh corrected reflectance 247 | var prc_OLI = Lrc_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI)); 248 | var prcImg_OLI = prc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 249 | 250 | // Aerosol Correction // 251 | // Bands in nm 252 | var bands_nm_OLI = ee.Image(443).addBands(ee.Image(483)) 253 | .addBands(ee.Image(561)) 254 | .addBands(ee.Image(655)) 255 | .addBands(ee.Image(865)) 256 | .addBands(ee.Image(0)) 257 | .addBands(ee.Image(0)) 258 | .toArray().toArray(1); 259 | 260 | // Lam in SWIR bands 261 | var Lam_6_OLI = LrcImg_OLI.select('B6'); 262 | var Lam_7_OLI = LrcImg_OLI.select('B7'); 263 | 264 | // Calculate aerosol type 265 | var eps_OLI = (((((Lam_7_OLI).divide(ESUNImg_OLI.select('B7'))).log()).subtract(((Lam_6_OLI).divide(ESUNImg_OLI.select('B6'))).log())).divide(ee.Image(2201).subtract(ee.Image(1609)))).multiply(mask); 266 | 267 | // Calculate multiple scattering of aerosols for each band 268 | var Lam_OLI = (Lam_7_OLI).multiply(((ESUN_OLI).divide(ESUNImg_OLI.select('B7')))).multiply((eps_OLI.multiply(ee.Image(-1))).multiply((bands_nm_OLI.divide(ee.Image(2201)))).exp()); 269 | 270 | // diffuse transmittance 271 | var trans_OLI = Tr_OLI.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe_OLI)).exp(); 272 | 273 | // Compute water-leaving radiance 274 | var Lw_OLI = Lrc_OLI.subtract(Lam_OLI).divide(trans_OLI); 275 | 276 | // water-leaving reflectance 277 | var pw_OLI = (Lw_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI))); 278 | var pwImg_OLI = pw_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 279 | 280 | // Rrs 281 | var Rrs_coll = (pw_OLI.divide(pi).arrayProject([0]).arrayFlatten([bands_OLI]).slice(0,5)); 282 | 283 | return(Rrs_coll.set('system:time_start',img.get('system:time_start'))); 284 | 285 | } 286 | function LST(img){ 287 | var TIRS_1 = img.select('B10'); 288 | 289 | var b10_add_band = ee.Number(img.get('RADIANCE_ADD_BAND_10')); 290 | var b10_mult_band = ee.Number(img.get('RADIANCE_MULT_BAND_10')); 291 | 292 | var Oi = ee.Number(0.29); 293 | 294 | var TIRS_cal = (TIRS_1.multiply(b10_mult_band).add(b10_add_band).subtract(Oi)); //.multiply(lakes); 295 | 296 | var K1_b10 = ee.Number(TIRS_1.get('K1_CONSTANT_BAND_10')); 297 | var K2_b10 = ee.Number(TIRS_1.get('K2_CONSTANT_BAND_10')); 298 | 299 | var lst_coll = ((ee.Image(K2_b10).divide(((ee.Image(K1_b10).divide(ee.Image(TIRS_cal))).add(ee.Image(1))).log())).subtract(ee.Image(273))).multiply(mask); // Celsius 300 | lst_coll = (ee.Image(0.7745).multiply(lst_coll)).add(ee.Image(9.6502)); 301 | 302 | // Define a square kernel with radius 1.5 303 | var sq_kernel = ee.Kernel.square(1.5,'pixels'); 304 | 305 | // focal convolution 306 | lst_coll = lst_coll.focal_median({kernel: sq_kernel, iterations: 1}); 307 | 308 | return(lst_coll.set('system:time_start',img.get('system:time_start'))) 309 | 310 | } 311 | function secchi(img){ 312 | var blueRed_coll = (img.select('B2').divide(img.select('B4'))).log() 313 | var lnMOSD_coll = (ee.Image(1.4856).multiply(blueRed_coll)).add(ee.Image(0.2734)); // R2 = 0.8748 with Anthony's in-situ data 314 | var MOSD_coll = ee.Image(10).pow(lnMOSD_coll); 315 | var sd_coll = (ee.Image(0.1777).multiply(MOSD_coll)).add(ee.Image(1.0813)); 316 | return(sd_coll.updateMask(sd_coll.lt(10)).set('system:time_start',img.get('system:time_start'))) 317 | } 318 | function trophicState(img){ 319 | var tsi_coll = ee.Image(60).subtract(ee.Image(14.41).multiply(img.log())); 320 | return(tsi_coll.updateMask(tsi_coll.lt(200)).set('system:time_start',img.get('system:time_start'))) 321 | 322 | } 323 | function reclassify(img){ 324 | 325 | // Create conditions 326 | var mask1 = img.lt(30); // (1) 327 | var mask2 = img.gte(30).and(img.lt(40));// (2) 328 | var mask3 = img.gte(40).and(img.lt(50)); // (3) 329 | var mask4 = img.gte(50).and(img.lt(60)); // (4) 330 | var mask5 = img.gte(60).and(img.lt(70)); // (5) 331 | var mask6 = img.gte(70).and(img.lt(80)); // (6) 332 | var mask7 = img.gte(70); // (7) 333 | 334 | // Reclassify conditions into new values 335 | var img1 = img.where(mask1.eq(1), 1).mask(mask1); 336 | var img2 = img.where(mask2.eq(1), 2).mask(mask2); 337 | var img3 = img.where(mask3.eq(1), 3).mask(mask3); 338 | var img4 = img.where(mask4.eq(1), 4).mask(mask4); 339 | var img5 = img.where(mask5.eq(1), 5).mask(mask5); 340 | var img6 = img.where(mask6.eq(1), 6).mask(mask6); 341 | var img7 = img.where(mask7.eq(1), 7).mask(mask7); 342 | 343 | // Ouput of reclassified image 344 | var tsi_collR = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7); 345 | return(tsi_collR.updateMask(tsi_collR.set('system:time_start',img.get('system:time_start')))) 346 | } 347 | 348 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 349 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 350 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 351 | // Map Layers // 352 | Map.addLayer(mask, {}, 'mask', false) 353 | 354 | // l8 355 | Map.addLayer(Rrs_coll.mean(), {bands: ['B4', 'B3', 'B1'], min: 0, max: 0.04}, 'l8 Rrs RGB', false); 356 | Map.addLayer(lst_coll.mean(), {min: 23, max: 27, palette: ['darkblue', 'blue', 'white', 'red', 'darkred']}, 'LST [c]', false) 357 | Map.addLayer(sd_coll.mean(), {min: 0, max: 3, palette: ['800000', 'FF9700', '7BFF7B', '0080FF', '000080']}, 'Zsd [m]', false); 358 | Map.addLayer(tsi_coll.mean(), {min: 30, max: 70, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'darkred']}, 'TSI [0-100]', false); 359 | Map.addLayer(tsi_collR.mode(), {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 'TSI Reclass', true); 360 | 361 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 362 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 363 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 364 | // Time Series // 365 | 366 | // LST time series 367 | var lstTimeSeries = ui.Chart.image.seriesByRegion( 368 | lst_coll, geometry, ee.Reducer.mean()) 369 | .setChartType('ScatterChart') 370 | .setOptions({ 371 | title: 'Mean LST', 372 | vAxis: {title: 'LST [c]'}, 373 | lineWidth: 1, 374 | pointSize: 4, 375 | }); 376 | 377 | // SD time series 378 | var sdTimeSeries = ui.Chart.image.seriesByRegion( 379 | sd_coll, geometry, ee.Reducer.mean()) 380 | .setChartType('ScatterChart') 381 | .setOptions({ 382 | title: 'Mean Secchi Depth', 383 | vAxis: {title: 'Zsd [m]'}, 384 | lineWidth: 1, 385 | pointSize: 4, 386 | }); 387 | 388 | 389 | // TSI time series 390 | var tsiTimeSeries = ui.Chart.image.seriesByRegion( 391 | tsi_coll, geometry, ee.Reducer.mean()) 392 | .setChartType('ScatterChart') 393 | .setOptions({ 394 | title: 'Mean Trophic State Index', 395 | vAxis: {title: 'TSI [1-100]'}, 396 | lineWidth: 1, 397 | pointSize: 4, 398 | }); 399 | 400 | // TSI Reclass time series 401 | var tsiRTimeSeries = ui.Chart.image.seriesByRegion( 402 | tsi_collR, geometry, ee.Reducer.mean()) 403 | .setChartType('ScatterChart') 404 | .setOptions({ 405 | title: 'Mode Trophic State Index Class', 406 | vAxis: {title: 'TSI Class'}, 407 | lineWidth: 1, 408 | pointSize: 4, 409 | }); 410 | 411 | print(lstTimeSeries) 412 | print(sdTimeSeries) 413 | print(tsiTimeSeries) 414 | print(tsiRTimeSeries) -------------------------------------------------------------------------------- /javascript/Ls8_single_image.js: -------------------------------------------------------------------------------- 1 | // This script processes a single Landsat 8 image of interest. 2 | // WQ parameters such chlor-a, secchi depth, trophic state index are calculated 3 | // How to use: 4 | // (1) If a "geometry" variable exists in the imports window, delete it. 5 | // (2) Within the map, select the point button near the top left side. 6 | // (3) Create a new point by clicking on a location of interest. 7 | // (4) Adjust the time frame you wish to browse by adding here: 8 | // begin date 9 | var iniDate = '2015-05-01'; 10 | // end date 11 | var endDate = '2018-03-31'; 12 | // (5) Adjust a cloud % threshold here: 13 | var oliCloudPerc = 5 14 | // (6) Click Run 15 | // (7) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 16 | // image from this collection, find the FILE_ID within the features and copy and paste it here: 17 | var l8 = ee.Image('LANDSAT/LC08/C01/T1/LC08_170060_20171226'); 18 | // (8) Click "Run" 19 | // (9) Export each image by clicking on the run button within the "tasks" tab. 20 | 21 | // Author: Benjamin Page // 22 | // Citations: 23 | // Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring, In Review 24 | 25 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 26 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 28 | 29 | // Import Collections // 30 | 31 | // landsat 8 raw dn 32 | var OLI_DN = ee.ImageCollection('LANDSAT/LC08/C01/T1'); 33 | 34 | // landsat-8 surface reflactance product (for masking purposes) 35 | var SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR'); 36 | 37 | // toms / omi 38 | var ozone = ee.ImageCollection('TOMS/MERGED'); 39 | 40 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 41 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 43 | // Filtering Collection and Masking // 44 | 45 | var pi = ee.Image(3.141592); 46 | 47 | // water mask 48 | var startMonth = 5; 49 | var endMonth = 9; 50 | var startYear = 2013; 51 | var endYear = 2017; 52 | 53 | var forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')); 54 | var mask = ee.Image(forMask.select('B6').median().lt(600)) 55 | mask = mask.updateMask(mask) 56 | 57 | // filter landsat 8 collection 58 | var FC_OLI = OLI_DN.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUD_COVER', "less_than", oliCloudPerc); 59 | print(FC_OLI, 'Available Imagery') 60 | 61 | print(l8, 'l8 image info') 62 | 63 | // oli image date 64 | var oliDate = l8.date(); 65 | print(oliDate, 'l8 Date') 66 | 67 | var footprint = l8.geometry() 68 | 69 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 70 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 71 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 72 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 73 | // single OLI image ATMOSPHERIC CORRECTION // 74 | 75 | // dem 76 | var DEM_OLI = ee.Image('USGS/SRTMGL1_003').clip(footprint); 77 | 78 | // ozone 79 | var DU_OLI = ee.Image(ozone.filterDate(iniDate,endDate).filterBounds(footprint).mean()); 80 | 81 | //Julian Day 82 | var imgDate_OLI = ee.Date(l8.get('system:time_start')); 83 | var FOY_OLI = ee.Date.fromYMD(imgDate_OLI.get('year'),1,1); 84 | var JD_OLI = imgDate_OLI.difference(FOY_OLI,'day').int().add(1); 85 | 86 | // Earth-Sun distance 87 | var d_OLI = ee.Image.constant(l8.get('EARTH_SUN_DISTANCE')); 88 | 89 | //Sun elevation 90 | var SunEl_OLI = ee.Image.constant(l8.get('SUN_ELEVATION')); 91 | 92 | //Sun azimuth 93 | var SunAz_OLI = ee.Image.constant(l8.get('SUN_AZIMUTH')); 94 | 95 | //Satellite zenith 96 | var SatZe_OLI = ee.Image(0.0) 97 | var cosdSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).cos(); 98 | var sindSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).sin(); 99 | 100 | //Satellite azimuth 101 | var SatAz_OLI = ee.Image(0.0) 102 | 103 | //Sun zenith 104 | var SunZe_OLI = ee.Image(90).subtract(SunEl_OLI) 105 | var cosdSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image.constant(180))).cos(); // in degrees 106 | var sindSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image(180))).sin(); // in degrees 107 | 108 | //Relative azimuth 109 | var RelAz_OLI = ee.Image(SunAz_OLI); 110 | var cosdRelAz_OLI = RelAz_OLI.multiply(pi.divide(ee.Image(180))).cos(); 111 | 112 | //Pressure calculation 113 | var P_OLI = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM_OLI)).pow(5.25588)).multiply(0.01); 114 | var Po_OLI = ee.Image(1013.25); 115 | 116 | // Radiometric Calibration // 117 | //define bands to be converted to radiance 118 | var bands_OLI = ['B1','B2','B3','B4','B5','B6','B7']; 119 | 120 | // radiance_mult_bands 121 | var rad_mult_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_MULT_BAND_1')), 122 | ee.Image(l8.get('RADIANCE_MULT_BAND_2')), 123 | ee.Image(l8.get('RADIANCE_MULT_BAND_3')), 124 | ee.Image(l8.get('RADIANCE_MULT_BAND_4')), 125 | ee.Image(l8.get('RADIANCE_MULT_BAND_5')), 126 | ee.Image(l8.get('RADIANCE_MULT_BAND_6')), 127 | ee.Image(l8.get('RADIANCE_MULT_BAND_7'))] 128 | )).toArray(1); 129 | 130 | // radiance add band 131 | var rad_add_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_ADD_BAND_1')), 132 | ee.Image(l8.get('RADIANCE_ADD_BAND_2')), 133 | ee.Image(l8.get('RADIANCE_ADD_BAND_3')), 134 | ee.Image(l8.get('RADIANCE_ADD_BAND_4')), 135 | ee.Image(l8.get('RADIANCE_ADD_BAND_5')), 136 | ee.Image(l8.get('RADIANCE_ADD_BAND_6')), 137 | ee.Image(l8.get('RADIANCE_ADD_BAND_7'))] 138 | )).toArray(1); 139 | 140 | //create an empty image to save new radiance bands to 141 | var imgArr_OLI = l8.select(bands_OLI).toArray().toArray(1); 142 | var Ltoa_OLI = imgArr_OLI.multiply(rad_mult_OLI).add(rad_add_OLI) 143 | 144 | // esun 145 | var ESUN_OLI = ee.Image.constant(197.24790954589844) 146 | .addBands(ee.Image.constant(201.98426818847656)) 147 | .addBands(ee.Image.constant(186.12677001953125)) 148 | .addBands(ee.Image.constant(156.95257568359375)) 149 | .addBands(ee.Image.constant(96.04714965820312)) 150 | .addBands(ee.Image.constant(23.8833221450863)) 151 | .addBands(ee.Image.constant(8.04995873449635)).toArray().toArray(1); 152 | ESUN_OLI = ESUN_OLI.multiply(ee.Image(1)) 153 | 154 | var ESUNImg_OLI = ESUN_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 155 | 156 | // Ozone Correction // 157 | // Ozone coefficients 158 | var koz_OLI = ee.Image.constant(0.0039).addBands(ee.Image.constant(0.0218)) 159 | .addBands(ee.Image.constant(0.1078)) 160 | .addBands(ee.Image.constant(0.0608)) 161 | .addBands(ee.Image.constant(0.0019)) 162 | .addBands(ee.Image.constant(0)) 163 | .addBands(ee.Image.constant(0)) 164 | .toArray().toArray(1); 165 | 166 | // Calculate ozone optical thickness 167 | var Toz_OLI = koz_OLI.multiply(DU_OLI).divide(ee.Image.constant(1000)); 168 | 169 | // Calculate TOA radiance in the absense of ozone 170 | var Lt_OLI = Ltoa_OLI.multiply(((Toz_OLI)).multiply((ee.Image.constant(1).divide(cosdSunZe_OLI)).add(ee.Image.constant(1).divide(cosdSatZe_OLI))).exp()); 171 | 172 | // Rayleigh optical thickness 173 | var bandCenter_OLI = ee.Image(443).divide(1000).addBands(ee.Image(483).divide(1000)) 174 | .addBands(ee.Image(561).divide(1000)) 175 | .addBands(ee.Image(655).divide(1000)) 176 | .addBands(ee.Image(865).divide(1000)) 177 | .addBands(ee.Image(1609).divide(1000)) 178 | .addBands(ee.Number(2201).divide(1000)) 179 | .toArray().toArray(1); 180 | 181 | // create an empty image to save new Tr values to 182 | var Tr_OLI = (P_OLI.divide(Po_OLI)).multiply(ee.Image(0.008569).multiply(bandCenter_OLI.pow(-4))).multiply((ee.Image(1).add(ee.Image(0.0113).multiply(bandCenter_OLI.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter_OLI.pow(-4))))); 183 | 184 | // Fresnel Reflection // 185 | // Specular reflection (s- and p- polarization states) 186 | var theta_V_OLI = ee.Image(0.0000000001); 187 | var sin_theta_j_OLI = sindSunZe_OLI.divide(ee.Image(1.333)); 188 | 189 | var theta_j_OLI = sin_theta_j_OLI.asin().multiply(ee.Image(180).divide(pi)); 190 | 191 | var theta_SZ_OLI = SunZe_OLI; 192 | 193 | var R_theta_SZ_s_OLI = (((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).subtract(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).add(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2))); 194 | 195 | var R_theta_V_s_OLI = ee.Image(0.0000000001); 196 | 197 | var R_theta_SZ_p_OLI = (((theta_SZ_OLI.multiply(pi.divide(180))).subtract(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(180))).add(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2))); 198 | 199 | var R_theta_V_p_OLI = ee.Image(0.0000000001); 200 | 201 | var R_theta_SZ_OLI = ee.Image(0.5).multiply(R_theta_SZ_s_OLI.add(R_theta_SZ_p_OLI)); 202 | 203 | var R_theta_V_OLI = ee.Image(0.5).multiply(R_theta_V_s_OLI.add(R_theta_V_p_OLI)); 204 | 205 | // Rayleigh scattering phase function // 206 | // Sun-sensor geometry 207 | var theta_neg_OLI = ((cosdSunZe_OLI.multiply(ee.Image(-1))).multiply(cosdSatZe_OLI)).subtract((sindSunZe_OLI).multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)); 208 | 209 | var theta_neg_inv_OLI = theta_neg_OLI.acos().multiply(ee.Image(180).divide(pi)); 210 | 211 | var theta_pos_OLI = (cosdSunZe_OLI.multiply(cosdSatZe_OLI)).subtract(sindSunZe_OLI.multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)); 212 | 213 | var theta_pos_inv_OLI = theta_pos_OLI.acos().multiply(ee.Image(180).divide(pi)); 214 | 215 | var cosd_tni_OLI = theta_neg_inv_OLI.multiply(pi.divide(180)).cos(); // in degrees 216 | 217 | var cosd_tpi_OLI = theta_pos_inv_OLI.multiply(pi.divide(180)).cos(); // in degrees 218 | 219 | var Pr_neg_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni_OLI.pow(2)))); 220 | 221 | var Pr_pos_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi_OLI.pow(2)))); 222 | 223 | // Rayleigh scattering phase function 224 | var Pr_OLI = Pr_neg_OLI.add((R_theta_SZ_OLI.add(R_theta_V_OLI)).multiply(Pr_pos_OLI)); 225 | 226 | // Calulate Lr, 227 | var denom_OLI = ee.Image(4).multiply(pi).multiply(cosdSatZe_OLI); 228 | var Lr_OLI = (ESUN_OLI.multiply(Tr_OLI)).multiply(Pr_OLI.divide(denom_OLI)); 229 | 230 | // Rayleigh corrected radiance 231 | var Lrc_OLI = (Lt_OLI.divide(ee.Image(10))).subtract(Lr_OLI); 232 | var LrcImg_OLI = Lrc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 233 | 234 | // Rayleigh corrected reflectance 235 | var prc_OLI = Lrc_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI)); 236 | var prcImg_OLI = prc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 237 | 238 | // Aerosol Correction // 239 | // Bands in nm 240 | var bands_nm_OLI = ee.Image(443).addBands(ee.Image(483)) 241 | .addBands(ee.Image(561)) 242 | .addBands(ee.Image(655)) 243 | .addBands(ee.Image(865)) 244 | .addBands(ee.Image(0)) 245 | .addBands(ee.Image(0)) 246 | .toArray().toArray(1); 247 | 248 | // Lam in SWIR bands 249 | var Lam_6_OLI = LrcImg_OLI.select('B6') 250 | var Lam_7_OLI = LrcImg_OLI.select('B7') 251 | 252 | // Calculate aerosol type 253 | var eps_OLI = (((((Lam_7_OLI).divide(ESUNImg_OLI.select('B7'))).log()).subtract(((Lam_6_OLI).divide(ESUNImg_OLI.select('B6'))).log())).divide(ee.Image(2201).subtract(ee.Image(1609)))).multiply(mask) 254 | 255 | // Calculate multiple scattering of aerosols for each band 256 | var Lam_OLI = (Lam_7_OLI).multiply(((ESUN_OLI).divide(ESUNImg_OLI.select('B7')))).multiply((eps_OLI.multiply(ee.Image(-1))).multiply((bands_nm_OLI.divide(ee.Image(2201)))).exp()); 257 | 258 | // diffuse transmittance 259 | var trans_OLI = Tr_OLI.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe_OLI)).exp(); 260 | 261 | // Compute water-leaving radiance 262 | var Lw_OLI = Lrc_OLI.subtract(Lam_OLI).divide(trans_OLI); 263 | 264 | // water-leaving reflectance 265 | var pw_OLI = (Lw_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI))); 266 | var pwImg_OLI = pw_OLI.arrayProject([0]).arrayFlatten([bands_OLI]); 267 | 268 | // Rrs 269 | var Rrs = (pw_OLI.divide(pi).arrayProject([0]).arrayFlatten([bands_OLI]).slice(0,5)); 270 | 271 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 272 | // Models // 273 | 274 | // Surface water temperature 275 | var TIRS_1 = l8.select('B10'); 276 | 277 | var b10_add_band = ee.Number(TIRS_1.get('RADIANCE_ADD_BAND_10')); 278 | var b10_mult_band = ee.Number(TIRS_1.get('RADIANCE_MULT_BAND_10')); 279 | 280 | var Oi = ee.Number(0.29); 281 | 282 | var TIRS_cal = (TIRS_1.multiply(b10_mult_band).add(b10_add_band).subtract(Oi)); //.multiply(lakes); 283 | 284 | var K1_b10 = ee.Number(TIRS_1.get('K1_CONSTANT_BAND_10')); 285 | var K2_b10 = ee.Number(TIRS_1.get('K2_CONSTANT_BAND_10')); 286 | 287 | var LST = ((ee.Image(K2_b10).divide(((ee.Image(K1_b10).divide(ee.Image(TIRS_cal))).add(ee.Image(1))).log())).subtract(ee.Image(273))).multiply(mask); // Celsius 288 | LST = (ee.Image(0.7745).multiply(LST)).add(ee.Image(9.6502)); // calibration R2 = 0.8599 289 | 290 | // chlor_a 291 | // Chlorophyll-a OC3 292 | var a0 = ee.Image(0.2412); 293 | var a1 = ee.Image(-2.0546); 294 | var a2 = ee.Image(1.1776); 295 | var a3 = ee.Image(-0.5538); 296 | var a4 = ee.Image(-0.4570); 297 | 298 | var log_BG = (Rrs.select('B1').divide(Rrs.select('B3'))).log10(); 299 | 300 | var a1a = a1.multiply(log_BG.pow(1)); 301 | var a2a = a2.multiply(log_BG.pow(2)); 302 | var a3a = a3.multiply(log_BG.pow(3)); 303 | var a4a = a4.multiply(log_BG.pow(4)); 304 | 305 | var sum = a1a.add(a2a).add(a3a).add(a4a); 306 | 307 | var log10_chlor_a = a0.add(sum); 308 | 309 | var chlor_a = ee.Image(10).pow(log10_chlor_a); 310 | var chlor_a_cal = ee.Image(4.0752).multiply(chlor_a).subtract(ee.Image(3.9617)); 311 | 312 | // SD 313 | var ln_BlueRed = (Rrs.select('B2').divide(Rrs.select('B4'))).log() 314 | var lnMOSD = (ee.Image(1.4856).multiply(ln_BlueRed)).add(ee.Image(0.2734)); // R2 = 0.8748 with in-situ 315 | var MOSD = ee.Image(10).pow(lnMOSD); // log space to (m) 316 | var SD = (ee.Image(0.1777).multiply(MOSD)).add(ee.Image(1.0813)); 317 | 318 | // tsi 319 | var TSI_c = ee.Image(30.6).add(ee.Image(9.81)).multiply(chlor_a_cal.log()) 320 | var TSI_s = ee.Image(60.0).subtract(ee.Image(14.41)).multiply(SD.log()) 321 | var TSI = (TSI_c.add(TSI_s)).divide(ee.Image(2)); 322 | 323 | // Reclassify TSI 324 | 325 | // Create conditions 326 | var mask1 = TSI.lt(30); // (1) 327 | var mask2 = TSI.gte(30).and(TSI.lt(40));// (2) 328 | var mask3 = TSI.gte(40).and(TSI.lt(50)); // (3) 329 | var mask4 = TSI.gte(50).and(TSI.lt(60)); // (4) 330 | var mask5 = TSI.gte(60).and(TSI.lt(70)); // (5) 331 | var mask6 = TSI.gte(70).and(TSI.lt(80)); // (6) 332 | var mask7 = TSI.gte(80); // (7) 333 | 334 | // Reclassify conditions into new values 335 | var img1 = TSI.where(mask1.eq(1), 1).mask(mask1); 336 | var img2 = TSI.where(mask2.eq(1), 2).mask(mask2); 337 | var img3 = TSI.where(mask3.eq(1), 3).mask(mask3); 338 | var img4 = TSI.where(mask4.eq(1), 4).mask(mask4); 339 | var img5 = TSI.where(mask5.eq(1), 5).mask(mask5); 340 | var img6 = TSI.where(mask6.eq(1), 6).mask(mask6); 341 | var img7 = TSI.where(mask7.eq(1), 7).mask(mask7); 342 | 343 | // Ouput of reclassified image 344 | var TSI_R = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7); 345 | 346 | // Map Layers 347 | Map.addLayer(footprint, {}, 'footprint') 348 | Map.addLayer(l8.multiply(mask), {bands: ['B4', 'B3', 'B2'], min: 0, max: 15000}, 'rgb', false) // rgb 349 | Map.addLayer(LST, {min: 20, max: 30, palette: ['darkblue', 'blue', 'white', 'red', 'darkred']}, 'LST', false) 350 | Map.addLayer(chlor_a_cal, {min: 0, max: 80, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange' , 'darkred']}, 'chlor_a', false) 351 | Map.addLayer(SD, {min: 0, max: 3, palette: ['darkred', 'orange', 'yellow', 'limegreen', 'cyan', 'blue']}, 'SD', false) 352 | Map.addLayer(TSI, {min: 30, max: 70, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange' , 'darkred']}, 'TSI', false) 353 | Map.addLayer(TSI_R, {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 'TSI_R', true) 354 | Map.centerObject(footprint, 7) 355 | 356 | // EXPORT IMAGES // 357 | 358 | // Export LST 359 | Export.image.toDrive({ 360 | image: LST, 361 | description: 'LST', 362 | scale: 30, 363 | region: footprint 364 | }); 365 | 366 | // Export chlor_a 367 | Export.image.toDrive({ 368 | image: chlor_a, 369 | description: 'chlor_a', 370 | scale: 30, 371 | region: footprint 372 | }); 373 | 374 | // Export SD 375 | Export.image.toDrive({ 376 | image: SD, 377 | description: 'SD', 378 | scale: 30, 379 | region: footprint 380 | }); 381 | 382 | // Export TSI 383 | Export.image.toDrive({ 384 | image: TSI, 385 | description: 'TSI', 386 | scale: 30, 387 | region: footprint 388 | }); 389 | 390 | // Export TSI_R 391 | Export.image.toDrive({ 392 | image: TSI_R, 393 | description: 'TSI_R', 394 | scale: 30, 395 | region: footprint 396 | }); -------------------------------------------------------------------------------- /javascript/MODIS_WQ_TimeSeries.js: -------------------------------------------------------------------------------- 1 | // This script processes and charts WQ parameter values from a collection of MODIS L3 OC archive for a particular pixel throughout time. 2 | // WQ parameters such as chlorophyll-a, secchi depth and the trophic state index 3 | // How to use: 4 | // (1) If a "geometry" variable exists in the imports window, delete it. 5 | // (2) Within the map, select the polygon button near the top left side. 6 | // (3) Create a new polygon by drawing around the area of interest 7 | // (4) Adjust the time frame you wish to browse by adding here: 8 | // begin date 9 | var startDate = '2016-04-01'; 10 | // end date 11 | var endDate = '2016-10-31'; 12 | // (5) Click Run 13 | // (5) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 14 | // image from this collection, highlight the FILE_ID and copy and paste it into the proper location within the "Ls8 Single Image" script. 15 | // (6) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 16 | // (6) Click "Run" 17 | // (7) Export each chart as a csv by clicking on the export icon near the top-right of the chart. 18 | // (8) If you are interested in a single image from the collection, copy and paste the FILE_ID here: 19 | var singleImage = ee.Image('NASA/OCEANDATA/MODIS-Aqua/L3SMI/A2017183').clip(geometry); 20 | 21 | // Author: Benjamin Page // 22 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 23 | // Import Colelction 24 | var MODIS = ee.ImageCollection('NASA/OCEANDATA/MODIS-Aqua/L3SMI'); 25 | 26 | // filter collection 27 | var FC = MODIS.filterDate(startDate, endDate).filterBounds(geometry); 28 | print(FC, 'available imagery') 29 | 30 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | 32 | ////// Models /////// 33 | 34 | // chlor_a 35 | var chlor_a = FC.select('chlor_a'); 36 | 37 | // SD 38 | var SD = ee.ImageCollection(FC.map(secchi).copyProperties(FC, ['system:time_start'])) 39 | 40 | // TSI 41 | var TSI = SD.map(trophicState); 42 | 43 | ////// Map Functions ////// 44 | 45 | function secchi(img){ 46 | 47 | var Rrs_488 = img.select('Rrs_488') 48 | var Rrs_667 = img.select('Rrs_667') 49 | var ln_blueRed = (Rrs_488.divide(Rrs_667)).log() 50 | var lnMOSD = (ee.Image(1.4856).multiply(ln_blueRed)).add(ee.Image(0.2734)); // R2 = 0.8748 with Anthony's in-situ data 51 | var MOSD = ee.Image(10).pow(lnMOSD); 52 | 53 | var SD = (ee.Image(0.1777).multiply(MOSD)).add(ee.Image(1.0813)); 54 | 55 | return(SD.set('system:time_start', img.get('system:time_start'))) 56 | 57 | } 58 | function trophicState(img){ 59 | var TSI = ee.Image(60).subtract((ee.Image(14.41)).multiply((img.log()))); 60 | return(TSI.set('system:time_start', img.get('system:time_start'))) 61 | 62 | } 63 | 64 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | // Time Series 66 | 67 | var chlorTimeSeries = ui.Chart.image.seriesByRegion( 68 | chlor_a, geometry, ee.Reducer.mean()) 69 | .setChartType('ScatterChart') 70 | .setOptions({ 71 | title: 'Mean chlor_a', 72 | vAxis: {title: 'chlor_a (ug / L)'}, 73 | lineWidth: 1, 74 | pointSize: 4, 75 | }); 76 | 77 | var sdTimeSeries = ui.Chart.image.seriesByRegion( 78 | SD, geometry, ee.Reducer.mean()) 79 | .setChartType('ScatterChart') 80 | .setOptions({ 81 | title: 'Mean Secchi Depth', 82 | vAxis: {title: 'Zsd [m]'}, 83 | lineWidth: 1, 84 | pointSize: 4, 85 | }); 86 | 87 | var tsiTimeSeries = ui.Chart.image.seriesByRegion( 88 | TSI, geometry, ee.Reducer.mean()) 89 | .setChartType('ScatterChart') 90 | .setOptions({ 91 | title: 'Mean TSI', 92 | vAxis: {title: 'TSI value)'}, 93 | lineWidth: 1, 94 | pointSize: 4, 95 | }); 96 | 97 | print(chlorTimeSeries) 98 | print(sdTimeSeries) 99 | print(tsiTimeSeries) 100 | 101 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 102 | 103 | // Map layers // 104 | 105 | Map.centerObject(geometry, 7) 106 | Map.addLayer(chlor_a.mean().clip(geometry), {min:0, max: 100, palette: ['darkblue','blue','limegreen','yellow','orange', 'orangered', 'darkred']}, 'chlor_a', false) 107 | Map.addLayer(SD.mean().clip(geometry), {min:0, max: 2, palette: ['red','orangered','orange','yellow','limegreen', 'blue', 'darkblue']}, 'SD', false) 108 | Map.addLayer(TSI.mean().clip(geometry), {min:30, max: 80, palette: ['darkblue','blue','limegreen','yellow','orange', 'orangered', 'red']}, 'TSI', true) 109 | Map.addLayer(singleImage.select('chlor_a'), {min: 0, max:100, palette: ['darkblue','blue','limegreen','yellow','orange', 'orangered', 'darkred']}, 'singleImage_chlor_a', false); 110 | 111 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 112 | // Exporting Images // 113 | 114 | // Export mean chlor_a 115 | Export.image.toDrive({ 116 | image: chlor_a.mean().clip(geometry), 117 | description: 'Mean Chlorophyll-a', 118 | scale: 250, 119 | region: geometry 120 | }); 121 | 122 | // Export mean SD 123 | Export.image.toDrive({ 124 | image: SD.mean().clip(geometry), 125 | description: 'Mean Secchi Depth', 126 | scale: 250, 127 | region: geometry 128 | }); 129 | 130 | // Export mean TSI 131 | Export.image.toDrive({ 132 | image: TSI.mean().clip(geometry), 133 | description: 'Mean Trophic State (from index)', 134 | scale: 250, 135 | region: geometry 136 | }); -------------------------------------------------------------------------------- /javascript/s2_FAI.js: -------------------------------------------------------------------------------- 1 | // This script processes a single Sentinel-2 TOA to Rayleigh corrected reflectances. 2 | // The floating algal index (FAI) is calculated from Rrc over water bodies. 3 | // How to use: 4 | // (1) If a "geometry" variable exists in the imports window, delete it. 5 | // (2) Within the map, select the point button near the top left side. 6 | // (3) Create a new point by clicking on a location of interest and click run. 7 | // (4) The "available imagery" ImageCollection within the console displays all available imagery 8 | // over that location with the necessary filters described by the user below. 9 | // (5) Expand the features within the "available imagery" image collection and select an image 10 | // by highlighting the FILE_ID and pasting it here: 11 | var s2 = ee.Image('COPERNICUS/S2/20180216T075011_20180216T080852_T36MXE') 12 | // (6) Click "Run" 13 | // (7) Export the image to your Google Drive by clicking on the "tasks" tab and clicking "RUN", be sure to specify 14 | // the proper folder. 15 | 16 | // Author: Benjamin Page // 17 | // Citations: 18 | // Page, B.P., Kumar, A. and Mishra, D.R., 2018. A novel cross-satellite based assessment of the spatio-temporal development of a cyanobacterial harmful algal bloom. International Journal of Applied Earth Observation and Geoinformation, 66, pp.69-81. 19 | 20 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 21 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 22 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 23 | // User Input // 24 | 25 | // begin date 26 | var iniDate = '2015-01-01'; 27 | 28 | // end date 29 | var endDate = '2018-02-28'; 30 | 31 | var cloudPerc = 5 32 | 33 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 34 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 35 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 36 | // Import Collections // 37 | 38 | // sentinel-2 39 | var MSI = ee.ImageCollection('COPERNICUS/S2'); 40 | 41 | // toms / omi 42 | var ozone = ee.ImageCollection('TOMS/MERGED'); 43 | 44 | var SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR'); 45 | 46 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 47 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 48 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 49 | // water mask 50 | var startMonth = 5; 51 | var endMonth = 9; 52 | var startYear = 2013; 53 | var endYear = 2017; 54 | 55 | var forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')); 56 | var mask = ee.Image(forMask.select('B6').median().lt(300)) 57 | mask = mask.updateMask(mask) 58 | 59 | // constants 60 | var pi = ee.Image(3.141592); 61 | var imDate = s2.date(); 62 | 63 | // filter sentinel 2 collection 64 | var FC = MSI.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUDY_PIXEL_PERCENTAGE', "less_than", cloudPerc); 65 | print(FC, 'Sentinel 2 Collection') 66 | print(imDate, 'image date') 67 | 68 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 69 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 70 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 71 | // MSI Atmospheric Correction // 72 | 73 | var bands = ['B1','B2','B3','B4','B5','B6','B7', 'B8', 'B8A', 'B11', 'B12']; 74 | 75 | // rescale 76 | var rescale = ee.Image(s2.divide(10000).copyProperties(s2)).select(bands); 77 | 78 | // tile footprint 79 | var footprint = s2.geometry() 80 | Map.centerObject(footprint) 81 | 82 | // dem 83 | var DEM = ee.Image('USGS/SRTMGL1_003').clip(footprint); 84 | 85 | // ozone 86 | var DU = ee.Image(ozone.filterDate(iniDate,endDate).filterBounds(geometry).mean()); 87 | 88 | //Julian Day 89 | var imgDate = ee.Date(s2.get('system:time_start')); 90 | var FOY = ee.Date.fromYMD(imgDate.get('year'),1,1); 91 | var JD = imgDate.difference(FOY,'day').int().add(1); 92 | 93 | // earth-sun distance 94 | var myCos = ((ee.Image(0.0172).multiply(ee.Image(JD).subtract(ee.Image(2)))).cos()).pow(2) 95 | var cosd = myCos.multiply(pi.divide(ee.Image(180))).cos(); 96 | var d = ee.Image(1).subtract(ee.Image(0.01673)).multiply(cosd).clip(footprint) 97 | 98 | // sun azimuth 99 | var SunAz = ee.Image.constant(s2.get('MEAN_SOLAR_AZIMUTH_ANGLE')).clip(footprint); 100 | 101 | // sun zenith 102 | var SunZe = ee.Image.constant(s2.get('MEAN_SOLAR_ZENITH_ANGLE')).clip(footprint); 103 | var cosdSunZe = SunZe.multiply(pi.divide(ee.Image(180))).cos(); // in degrees 104 | var sindSunZe = SunZe.multiply(pi.divide(ee.Image(180))).sin(); // in degrees 105 | 106 | // sat zenith 107 | var SatZe = ee.Image.constant(s2.get('MEAN_INCIDENCE_ZENITH_ANGLE_B5')).clip(footprint); 108 | var cosdSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).cos(); 109 | var sindSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).sin(); 110 | 111 | // sat azimuth 112 | var SatAz = ee.Image.constant(s2.get('MEAN_INCIDENCE_AZIMUTH_ANGLE_B5')).clip(footprint); 113 | 114 | // relative azimuth 115 | var RelAz = SatAz.subtract(SunAz); 116 | var cosdRelAz = RelAz.multiply(pi.divide(ee.Image(180))).cos(); 117 | 118 | // Pressure 119 | var P = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM)).pow(5.25588)).multiply(0.01) 120 | var Po = ee.Image(1013.25); 121 | 122 | // esun 123 | var ESUN = ee.Image(ee.Array([ee.Image(s2.get('SOLAR_IRRADIANCE_B1')), 124 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2')), 125 | ee.Image(s2.get('SOLAR_IRRADIANCE_B3')), 126 | ee.Image(s2.get('SOLAR_IRRADIANCE_B4')), 127 | ee.Image(s2.get('SOLAR_IRRADIANCE_B5')), 128 | ee.Image(s2.get('SOLAR_IRRADIANCE_B6')), 129 | ee.Image(s2.get('SOLAR_IRRADIANCE_B7')), 130 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8')), 131 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8A')), 132 | ee.Image(s2.get('SOLAR_IRRADIANCE_B11')), 133 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2'))] 134 | )).toArray().toArray(1); 135 | 136 | ESUN = ESUN.multiply(ee.Image(1)) 137 | 138 | var ESUNImg = ESUN.arrayProject([0]).arrayFlatten([bands]); 139 | 140 | // create empty array for the images 141 | var imgArr = rescale.select(bands).toArray().toArray(1); 142 | 143 | // pTOA to Ltoa 144 | var Ltoa = imgArr.multiply(ESUN).multiply(cosdSunZe).divide(pi.multiply(d.pow(2))); 145 | 146 | // band centers 147 | var bandCenter = ee.Image(443).divide(1000).addBands(ee.Image(490).divide(1000)) 148 | .addBands(ee.Image(560).divide(1000)) 149 | .addBands(ee.Image(665).divide(1000)) 150 | .addBands(ee.Image(705).divide(1000)) 151 | .addBands(ee.Image(740).divide(1000)) 152 | .addBands(ee.Number(783).divide(1000)) 153 | .addBands(ee.Number(842).divide(1000)) 154 | .addBands(ee.Number(865).divide(1000)) 155 | .addBands(ee.Number(1610).divide(1000)) 156 | .addBands(ee.Number(2190).divide(1000)) 157 | .toArray().toArray(1); 158 | 159 | // ozone coefficients 160 | var koz = ee.Image(0.0039).addBands(ee.Image(0.0213)) 161 | .addBands(ee.Image(0.1052)) 162 | .addBands(ee.Image(0.0505)) 163 | .addBands(ee.Image(0.0205)) 164 | .addBands(ee.Image(0.0112)) 165 | .addBands(ee.Image(0.0075)) 166 | .addBands(ee.Image(0.0021)) 167 | .addBands(ee.Image(0.0019)) 168 | .addBands(ee.Image(0)) 169 | .addBands(ee.Image(0)) 170 | .toArray().toArray(1); 171 | 172 | ////////////////////////////////////// 173 | 174 | // Calculate ozone optical thickness 175 | var Toz = koz.multiply(DU).divide(ee.Image(1000)); 176 | 177 | // Calculate TOA radiance in the absense of ozone 178 | var Lt = Ltoa.multiply(((Toz)).multiply((ee.Image(1).divide(cosdSunZe)).add(ee.Image(1).divide(cosdSatZe))).exp()); 179 | 180 | // Rayleigh optical thickness 181 | var Tr = (P.divide(Po)).multiply(ee.Image(0.008569).multiply(bandCenter.pow(-4))).multiply((ee.Image(1).add(ee.Image(0.0113).multiply(bandCenter.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter.pow(-4))))); 182 | 183 | // Specular reflection (s- and p- polarization states) 184 | var theta_V = ee.Image(0.0000000001); 185 | var sin_theta_j = sindSunZe.divide(ee.Image(1.333)); 186 | 187 | var theta_j = sin_theta_j.asin().multiply(ee.Image(180).divide(pi)); 188 | 189 | var theta_SZ = SunZe; 190 | 191 | var R_theta_SZ_s = (((theta_SZ.multiply(pi.divide(ee.Image(180)))).subtract(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ.multiply(pi.divide(ee.Image(180)))).add(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2))); 192 | 193 | var R_theta_V_s = ee.Image(0.0000000001); 194 | 195 | var R_theta_SZ_p = (((theta_SZ.multiply(pi.divide(180))).subtract(theta_j.multiply(pi.divide(180)))).tan().pow(2)).divide((((theta_SZ.multiply(pi.divide(180))).add(theta_j.multiply(pi.divide(180)))).tan().pow(2))); 196 | 197 | var R_theta_V_p = ee.Image(0.0000000001); 198 | 199 | var R_theta_SZ = ee.Image(0.5).multiply(R_theta_SZ_s.add(R_theta_SZ_p)); 200 | 201 | var R_theta_V = ee.Image(0.5).multiply(R_theta_V_s.add(R_theta_V_p)); 202 | 203 | // Sun-sensor geometry 204 | var theta_neg = ((cosdSunZe.multiply(ee.Image(-1))).multiply(cosdSatZe)).subtract((sindSunZe).multiply(sindSatZe).multiply(cosdRelAz)); 205 | 206 | var theta_neg_inv = theta_neg.acos().multiply(ee.Image(180).divide(pi)); 207 | 208 | var theta_pos = (cosdSunZe.multiply(cosdSatZe)).subtract(sindSunZe.multiply(sindSatZe).multiply(cosdRelAz)); 209 | 210 | var theta_pos_inv = theta_pos.acos().multiply(ee.Image(180).divide(pi)); 211 | 212 | var cosd_tni = theta_neg_inv.multiply(pi.divide(180)).cos(); // in degrees 213 | 214 | var cosd_tpi = theta_pos_inv.multiply(pi.divide(180)).cos(); // in degrees 215 | 216 | var Pr_neg = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni.pow(2)))); 217 | 218 | var Pr_pos = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi.pow(2)))); 219 | 220 | // Rayleigh scattering phase function 221 | var Pr = Pr_neg.add((R_theta_SZ.add(R_theta_V)).multiply(Pr_pos)); // for water Rayleigh correction 222 | //var Pr = ee.Image(1); // for terrestrial Rayleigh correction 223 | 224 | // rayleigh radiance contribution 225 | var denom = ee.Image(4).multiply(pi).multiply(cosdSatZe); 226 | var Lr = (ESUN.multiply(Tr)).multiply(Pr.divide(denom)); 227 | 228 | // rayleigh corrected radiance 229 | var Lrc = Lt.subtract(Lr); 230 | var LrcImg = Lrc.arrayProject([0]).arrayFlatten([bands]); 231 | 232 | // rayleigh corrected reflectance 233 | var prc = (Lrc.multiply(pi).multiply(d.pow(2)).divide(ESUN.multiply(cosdSunZe))); 234 | var prcImg = prc.arrayProject([0]).arrayFlatten([bands]); 235 | print(prcImg, 'prc') 236 | 237 | //////////////////////////////////////////////// 238 | // models // 239 | 240 | // Calculate FAI 241 | var NIRprime = (prcImg.select('B4')).add((prcImg.select('B11').subtract(prcImg.select('B4'))).multiply((ee.Image(865).subtract(ee.Image(665))).divide((ee.Image(1610).subtract(ee.Image(665)))))); 242 | var FAI = ((prcImg.select('B8A').subtract(NIRprime))).multiply(mask); 243 | 244 | // Map Layers 245 | Map.addLayer(footprint, {}, 'footprint', true); 246 | Map.addLayer(prcImg.multiply(mask), {bands: ['B4', 'B3', 'B2'], min: 0, max: 0.4}, 'prc rgb', false) 247 | Map.addLayer(FAI, {min: -0.05, max: 0.2, palette: ['000080','0080FF','7BFF7B','FF9700','800000']}, 'FAI', true); 248 | 249 | // export image // 250 | 251 | Export.image.toDrive({ 252 | image: FAI, 253 | description: 's2_FAI', 254 | scale: 30, 255 | region: footprint 256 | }); 257 | 258 | -------------------------------------------------------------------------------- /javascript/s2_WQ_TimeSeries.js: -------------------------------------------------------------------------------- 1 | // This script processes and charts WQ parameter valuse from a collection of Sentinel-2 archive for a particular pixel throughout time. 2 | // WQ parameters such chlor-a, secchi depth, trophic state index 3 | // How to use: 4 | // (1) If a "geometry" variable exists in the imports window, delete it. 5 | // (2) Within the map, select the point button near the top left side. 6 | // (3) Create a new point by clicking on a location of interest. 7 | // (4) Adjust the time frame you wish to browse by adding here: 8 | // begin date 9 | var iniDate = '2015-05-01'; 10 | // end date 11 | var endDate = '2018-03-31'; 12 | // (5) Adjust a cloud % threshold here: 13 | var cloudPerc = 5 14 | // (5) Click Run 15 | // (5) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 16 | // image from this collection, highlight the FILE_ID and copy and paste it into the proper location within the "s2 Single Image" script. 17 | // (6) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 18 | // (6) Click "Run" 19 | // (7) Export each chart as a csv by clicking on the export icon near the top-right of the chart. 20 | 21 | // Author: Benjamin Page // 22 | // Citations: 23 | // Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring 24 | 25 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 26 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 28 | Map.centerObject(geometry, 7) 29 | 30 | // Import Collections // 31 | 32 | // sentinel-2 33 | var MSI = ee.ImageCollection('COPERNICUS/S2'); 34 | 35 | // landsat-8 surface reflactance product (for masking purposes) 36 | var SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR'); 37 | 38 | // toms / omi 39 | var ozone = ee.ImageCollection('TOMS/MERGED'); 40 | 41 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 43 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 44 | 45 | var pi = ee.Image(3.141592); 46 | 47 | // water mask 48 | var startMonth = 5; 49 | var endMonth = 9; 50 | var startYear = 2013; 51 | var endYear = 2017; 52 | 53 | var forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')); 54 | var mask = ee.Image(forMask.select('B6').median().lt(300)) 55 | mask = mask.updateMask(mask) 56 | 57 | // filter sentinel 2 collection 58 | var FC = MSI.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUDY_PIXEL_PERCENTAGE', "less_than", cloudPerc); 59 | print(FC, 'Sentinel 2 Collection') 60 | 61 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 62 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 63 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 64 | // Collection Processing // 65 | 66 | // atmospheric correction 67 | var Rrs_coll = FC.map(s2Correction); 68 | 69 | 70 | // chlorophyll-a 71 | var chlor_a_coll = Rrs_coll.map(chlorophyll); 72 | 73 | // sd 74 | var sd_coll = Rrs_coll.map(secchi); 75 | 76 | // tsi 77 | var tsi_coll = chlor_a_coll.map(trophicState); 78 | 79 | // tsi reclass 80 | var tsi_collR = tsi_coll.map(reclassify); 81 | 82 | // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 83 | // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 84 | // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 85 | // Mapping functions // 86 | 87 | function s2Correction(img){ 88 | 89 | // msi bands 90 | var bands = ['B1','B2','B3','B4','B5','B6','B7', 'B8', 'B8A', 'B11', 'B12']; 91 | 92 | // rescale 93 | var rescale = img.select(bands).divide(10000).multiply(mask) 94 | 95 | // tile footprint 96 | var footprint = rescale.geometry() 97 | 98 | // dem 99 | var DEM = ee.Image('USGS/SRTMGL1_003').clip(footprint); 100 | 101 | // ozone 102 | var DU = ee.Image(ozone.filterDate(iniDate,endDate).filterBounds(footprint).mean()); 103 | 104 | //Julian Day 105 | var imgDate = ee.Date(img.get('system:time_start')); 106 | var FOY = ee.Date.fromYMD(imgDate.get('year'),1,1); 107 | var JD = imgDate.difference(FOY,'day').int().add(1); 108 | 109 | // earth-sun distance 110 | var myCos = ((ee.Image(0.0172).multiply(ee.Image(JD).subtract(ee.Image(2)))).cos()).pow(2) 111 | var cosd = myCos.multiply(pi.divide(ee.Image(180))).cos(); 112 | var d = ee.Image(1).subtract(ee.Image(0.01673)).multiply(cosd).clip(footprint) 113 | 114 | // sun azimuth 115 | var SunAz = ee.Image.constant(img.get('MEAN_SOLAR_AZIMUTH_ANGLE')).clip(footprint); 116 | 117 | // sun zenith 118 | var SunZe = ee.Image.constant(img.get('MEAN_SOLAR_ZENITH_ANGLE')).clip(footprint); 119 | var cosdSunZe = SunZe.multiply(pi.divide(ee.Image(180))).cos(); // in degrees 120 | var sindSunZe = SunZe.multiply(pi.divide(ee.Image(180))).sin(); // in degrees 121 | 122 | // sat zenith 123 | var SatZe = ee.Image.constant(img.get('MEAN_INCIDENCE_ZENITH_ANGLE_B5')).clip(footprint); 124 | var cosdSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).cos(); 125 | var sindSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).sin(); 126 | 127 | // sat azimuth 128 | var SatAz = ee.Image.constant(img.get('MEAN_INCIDENCE_AZIMUTH_ANGLE_B5')).clip(footprint); 129 | 130 | // relative azimuth 131 | var RelAz = SatAz.subtract(SunAz); 132 | var cosdRelAz = RelAz.multiply(pi.divide(ee.Image(180))).cos(); 133 | 134 | // Pressure 135 | var P = (ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM)).pow(5.25588)).multiply(0.01)).multiply(mask); 136 | var Po = ee.Image(1013.25); 137 | 138 | // esun 139 | var ESUN = ee.Image(ee.Array([ee.Image(img.get('SOLAR_IRRADIANCE_B1')), 140 | ee.Image(img.get('SOLAR_IRRADIANCE_B2')), 141 | ee.Image(img.get('SOLAR_IRRADIANCE_B3')), 142 | ee.Image(img.get('SOLAR_IRRADIANCE_B4')), 143 | ee.Image(img.get('SOLAR_IRRADIANCE_B5')), 144 | ee.Image(img.get('SOLAR_IRRADIANCE_B6')), 145 | ee.Image(img.get('SOLAR_IRRADIANCE_B7')), 146 | ee.Image(img.get('SOLAR_IRRADIANCE_B8')), 147 | ee.Image(img.get('SOLAR_IRRADIANCE_B8A')), 148 | ee.Image(img.get('SOLAR_IRRADIANCE_B11')), 149 | ee.Image(img.get('SOLAR_IRRADIANCE_B2'))] 150 | )).toArray().toArray(1); 151 | 152 | ESUN = ESUN.multiply(ee.Image(1)) 153 | 154 | var ESUNImg = ESUN.arrayProject([0]).arrayFlatten([bands]); 155 | 156 | // create empty array for the images 157 | var imgArr = rescale.select(bands).toArray().toArray(1); 158 | 159 | // pTOA to Ltoa 160 | var Ltoa = imgArr.multiply(ESUN).multiply(cosdSunZe).divide(pi.multiply(d.pow(2))); 161 | 162 | // band centers 163 | var bandCenter = ee.Image(443).divide(1000).addBands(ee.Image(490).divide(1000)) 164 | .addBands(ee.Image(560).divide(1000)) 165 | .addBands(ee.Image(665).divide(1000)) 166 | .addBands(ee.Image(705).divide(1000)) 167 | .addBands(ee.Image(740).divide(1000)) 168 | .addBands(ee.Image(783).divide(1000)) 169 | .addBands(ee.Image(842).divide(1000)) 170 | .addBands(ee.Image(865).divide(1000)) 171 | .addBands(ee.Image(1610).divide(1000)) 172 | .addBands(ee.Image(2190).divide(1000)) 173 | .toArray().toArray(1); 174 | 175 | // ozone coefficients 176 | var koz = ee.Image(0.0039).addBands(ee.Image(0.0213)) 177 | .addBands(ee.Image(0.1052)) 178 | .addBands(ee.Image(0.0505)) 179 | .addBands(ee.Image(0.0205)) 180 | .addBands(ee.Image(0.0112)) 181 | .addBands(ee.Image(0.0075)) 182 | .addBands(ee.Image(0.0021)) 183 | .addBands(ee.Image(0.0019)) 184 | .addBands(ee.Image(0)) 185 | .addBands(ee.Image(0)) 186 | .toArray().toArray(1); 187 | 188 | // Calculate ozone optical thickness 189 | var Toz = koz.multiply(DU).divide(ee.Image(1000)); 190 | 191 | // Calculate TOA radiance in the absense of ozone 192 | var Lt = Ltoa.multiply(((Toz)).multiply((ee.Image(1).divide(cosdSunZe)).add(ee.Image(1).divide(cosdSatZe))).exp()); 193 | 194 | // Rayleigh optical thickness 195 | var Tr = (P.divide(Po)).multiply(ee.Image(0.008569).multiply(bandCenter.pow(-4))).multiply((ee.Image(1).add(ee.Image(0.0113).multiply(bandCenter.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter.pow(-4))))); 196 | 197 | // Specular reflection (s- and p- polarization states) 198 | var theta_V = ee.Image(0.0000000001); 199 | var sin_theta_j = sindSunZe.divide(ee.Image(1.333)); 200 | 201 | var theta_j = sin_theta_j.asin().multiply(ee.Image(180).divide(pi)); 202 | 203 | var theta_SZ = SunZe; 204 | 205 | var R_theta_SZ_s = (((theta_SZ.multiply(pi.divide(ee.Image(180)))).subtract(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ.multiply(pi.divide(ee.Image(180)))).add(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2))); 206 | 207 | var R_theta_V_s = ee.Image(0.0000000001); 208 | 209 | var R_theta_SZ_p = (((theta_SZ.multiply(pi.divide(180))).subtract(theta_j.multiply(pi.divide(180)))).tan().pow(2)).divide((((theta_SZ.multiply(pi.divide(180))).add(theta_j.multiply(pi.divide(180)))).tan().pow(2))); 210 | 211 | var R_theta_V_p = ee.Image(0.0000000001); 212 | 213 | var R_theta_SZ = ee.Image(0.5).multiply(R_theta_SZ_s.add(R_theta_SZ_p)); 214 | 215 | var R_theta_V = ee.Image(0.5).multiply(R_theta_V_s.add(R_theta_V_p)); 216 | 217 | // Sun-sensor geometry 218 | var theta_neg = ((cosdSunZe.multiply(ee.Image(-1))).multiply(cosdSatZe)).subtract((sindSunZe).multiply(sindSatZe).multiply(cosdRelAz)); 219 | 220 | var theta_neg_inv = theta_neg.acos().multiply(ee.Image(180).divide(pi)); 221 | 222 | var theta_pos = (cosdSunZe.multiply(cosdSatZe)).subtract(sindSunZe.multiply(sindSatZe).multiply(cosdRelAz)); 223 | 224 | var theta_pos_inv = theta_pos.acos().multiply(ee.Image(180).divide(pi)); 225 | 226 | var cosd_tni = theta_neg_inv.multiply(pi.divide(180)).cos(); // in degrees 227 | 228 | var cosd_tpi = theta_pos_inv.multiply(pi.divide(180)).cos(); // in degrees 229 | 230 | var Pr_neg = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni.pow(2)))); 231 | 232 | var Pr_pos = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi.pow(2)))); 233 | 234 | // Rayleigh scattering phase function 235 | var Pr = Pr_neg.add((R_theta_SZ.add(R_theta_V)).multiply(Pr_pos)); 236 | 237 | // rayleigh radiance contribution 238 | var denom = ee.Image(4).multiply(pi).multiply(cosdSatZe); 239 | var Lr = (ESUN.multiply(Tr)).multiply(Pr.divide(denom)); 240 | 241 | // rayleigh corrected radiance 242 | var Lrc = Lt.subtract(Lr); 243 | var LrcImg = Lrc.arrayProject([0]).arrayFlatten([bands]); 244 | 245 | // Aerosol Correction // 246 | 247 | // Bands in nm 248 | var bands_nm = ee.Image(443).addBands(ee.Image(490)) 249 | .addBands(ee.Image(560)) 250 | .addBands(ee.Image(665)) 251 | .addBands(ee.Image(705)) 252 | .addBands(ee.Image(740)) 253 | .addBands(ee.Image(783)) 254 | .addBands(ee.Image(842)) 255 | .addBands(ee.Image(865)) 256 | .addBands(ee.Image(0)) 257 | .addBands(ee.Image(0)) 258 | .toArray().toArray(1); 259 | 260 | // Lam in SWIR bands 261 | var Lam_10 = LrcImg.select('B11'); 262 | var Lam_11 = LrcImg.select('B12'); 263 | 264 | // Calculate aerosol type 265 | var eps = ((((Lam_11).divide(ESUNImg.select('B12'))).log()).subtract(((Lam_10).divide(ESUNImg.select('B11'))).log())).divide(ee.Image(2190).subtract(ee.Image(1610))); 266 | 267 | // Calculate multiple scattering of aerosols for each band 268 | var Lam = (Lam_11).multiply(((ESUN).divide(ESUNImg.select('B12')))).multiply((eps.multiply(ee.Image(-1))).multiply((bands_nm.divide(ee.Image(2190)))).exp()); 269 | 270 | // diffuse transmittance 271 | var trans = Tr.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe)).exp(); 272 | 273 | // Compute water-leaving radiance 274 | var Lw = Lrc.subtract(Lam).divide(trans); 275 | 276 | // water-leaving reflectance 277 | var pw = (Lw.multiply(pi).multiply(d.pow(2)).divide(ESUN.multiply(cosdSunZe))); 278 | 279 | // remote sensing reflectance 280 | var Rrs_coll = (pw.divide(pi).arrayProject([0]).arrayFlatten([bands]).slice(0,9)); 281 | 282 | return(Rrs_coll.set('system:time_start',img.get('system:time_start'))); 283 | 284 | } 285 | function chlorophyll(img){ 286 | var NDCI_coll = (img.select('B5').subtract(img.select('B4'))).divide(img.select('B5').add(img.select('B4'))); 287 | var chlor_a_coll = ee.Image(14.039).add(ee.Image(86.115).multiply(NDCI_coll)).add(ee.Image(194.325).multiply(NDCI_coll.pow(ee.Image(2)))); 288 | return(chlor_a_coll.updateMask(chlor_a_coll.lt(100)).set('system:time_start',img.get('system:time_start'))) 289 | } 290 | function secchi(img){ 291 | var blueRed_coll = (img.select('B2').divide(img.select('B4'))).log() 292 | var lnMOSD_coll = (ee.Image(1.4856).multiply(blueRed_coll)).add(ee.Image(0.2734)); // R2 = 0.8748 with Anthony's in-situ data 293 | var MOSD_coll = ee.Image(10).pow(lnMOSD_coll); 294 | var sd_coll = (ee.Image(0.1777).multiply(MOSD_coll)).add(ee.Image(1.0813)); 295 | return(sd_coll.updateMask(sd_coll.lt(10)).set('system:time_start',img.get('system:time_start'))) 296 | } 297 | function trophicState(img){ 298 | var tsi_coll = ee.Image(30.6).add(ee.Image(9.81).multiply(img.log())); 299 | return(tsi_coll.updateMask(tsi_coll.lt(200)).set('system:time_start',img.get('system:time_start'))) 300 | } 301 | function reclassify(img){ 302 | 303 | // Create conditions 304 | var mask1 = img.lt(30); // (1) 305 | var mask2 = img.gte(30).and(img.lt(40));// (2) 306 | var mask3 = img.gte(40).and(img.lt(50)); // (3) 307 | var mask4 = img.gte(50).and(img.lt(60)); // (4) 308 | var mask5 = img.gte(60).and(img.lt(70)); // (5) 309 | var mask6 = img.gte(70).and(img.lt(80)); // (6) 310 | var mask7 = img.gte(80); // (7) 311 | 312 | // Reclassify conditions into new values 313 | var img1 = img.where(mask1.eq(1), 1).mask(mask1); 314 | var img2 = img.where(mask2.eq(1), 2).mask(mask2); 315 | var img3 = img.where(mask3.eq(1), 3).mask(mask3); 316 | var img4 = img.where(mask4.eq(1), 4).mask(mask4); 317 | var img5 = img.where(mask5.eq(1), 5).mask(mask5); 318 | var img6 = img.where(mask6.eq(1), 6).mask(mask6); 319 | var img7 = img.where(mask7.eq(1), 7).mask(mask7); 320 | 321 | // Ouput of reclassified image 322 | var tsi_collR = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7); 323 | return(tsi_collR.updateMask(tsi_collR.set('system:time_start',img.get('system:time_start')))) 324 | } 325 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 326 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 327 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 328 | // Time Series // 329 | 330 | // Chlorophyll-a time series 331 | var chlorTimeSeries = ui.Chart.image.seriesByRegion( 332 | chlor_a_coll, geometry, ee.Reducer.mean()) 333 | .setChartType('ScatterChart') 334 | .setOptions({ 335 | title: 'Mean Chlorphyll-a', 336 | vAxis: {title: 'Chlor-a [micrograms/L]'}, 337 | lineWidth: 1, 338 | pointSize: 4, 339 | }); 340 | 341 | // SD time series 342 | var sdTimeSeries = ui.Chart.image.seriesByRegion( 343 | sd_coll, geometry, ee.Reducer.mean()) 344 | .setChartType('ScatterChart') 345 | .setOptions({ 346 | title: 'Mean Secchi Depth', 347 | vAxis: {title: 'Zsd [m]'}, 348 | lineWidth: 1, 349 | pointSize: 4, 350 | }); 351 | 352 | // TSI time series 353 | var tsiTimeSeries = ui.Chart.image.seriesByRegion( 354 | tsi_coll, geometry, ee.Reducer.mean()) 355 | .setChartType('ScatterChart') 356 | .setOptions({ 357 | title: 'Mean Trophic State Index', 358 | vAxis: {title: 'TSI [1-100]'}, 359 | lineWidth: 1, 360 | pointSize: 4, 361 | }); 362 | 363 | // TSI Reclass time series 364 | var tsiRTimeSeries = ui.Chart.image.seriesByRegion( 365 | tsi_collR, geometry, ee.Reducer.mode()) 366 | .setChartType('ScatterChart') 367 | .setOptions({ 368 | title: 'Mode Trophic State Index Class', 369 | vAxis: {title: 'TSI Class'}, 370 | lineWidth: 1, 371 | pointSize: 4, 372 | }); 373 | 374 | 375 | print(chlorTimeSeries) 376 | print(sdTimeSeries) 377 | print(tsiTimeSeries) 378 | print(tsiRTimeSeries) 379 | 380 | // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 381 | // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 382 | // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 383 | // Map Layers // 384 | Map.addLayer(mask, {}, 'mask', false) 385 | Map.addLayer(Rrs_coll.mean(), {min: 0, max: 0.03, bands: ['B4', 'B3', 'B2']}, 'Mean RGB', false); 386 | Map.addLayer(chlor_a_coll.mean(), {min: 0, max: 40, palette: ['darkblue','blue','cyan','limegreen','yellow', 'orange', 'orangered', 'darkred']}, 'Mean chlor-a', false); 387 | Map.addLayer(sd_coll.mean(), {min: 0, max: 2, palette: ['800000', 'FF9700', '7BFF7B', '0080FF', '000080']}, 'Mean Zsd', false); 388 | Map.addLayer(tsi_coll.mean(), {min: 30, max: 80, palette: ['darkblue','blue','cyan','limegreen','yellow', 'orange', 'orangered', 'darkred']}, 'Mean TSI', false); 389 | Map.addLayer(tsi_collR.mode(), {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 'Mode TSI Class', true); 390 | -------------------------------------------------------------------------------- /javascript/s2_single_image.js: -------------------------------------------------------------------------------- 1 | // This script processes a single Sentinel-2 image of interest. 2 | // WQ parameters such chlor-a, secchi depth, trophic state index are calculated 3 | // How to use: 4 | // (1) If a "geometry" variable exists in the imports window, delete it. 5 | // (2) Within the map, select the point button near the top left side. 6 | // (3) Create a new point by clicking on a location of interest. 7 | // (4) Adjust the time frame you wish to browse by adding here: 8 | // begin date 9 | var iniDate = '2015-05-01'; 10 | // end date 11 | var endDate = '2018-03-31'; 12 | // (5) Adjust a cloud % threshold here: 13 | var cloudPerc = 5 14 | // (6) Click Run 15 | // (7) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 16 | // image from this collection, highlight the FILE_ID in the available imagery features and copy and paste it here: 17 | var s2 = ee.Image('COPERNICUS/S2/20171128T075251_20171128T080644_T36MXE') 18 | // (8) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 19 | // (9) Click "Run" 20 | // (10) Export each image by clicking on the run button within the "tasks" tab. 21 | 22 | // Author: Benjamin Page // 23 | // Citations: 24 | // Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring 25 | 26 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 28 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 29 | // Import Collections // 30 | 31 | // sentinel-2 32 | var MSI = ee.ImageCollection('COPERNICUS/S2'); 33 | 34 | // landsat-8 surface reflactance product (for masking purposes) 35 | var SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR'); 36 | 37 | // toms / omi 38 | var ozone = ee.ImageCollection('TOMS/MERGED'); 39 | 40 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 41 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 43 | 44 | var pi = ee.Image(3.141592); 45 | var imDate = s2.date(); 46 | 47 | var footprint = s2.geometry() 48 | 49 | // water mask 50 | var startMonth = 5; 51 | var endMonth = 9; 52 | var startYear = 2013; 53 | var endYear = 2017; 54 | 55 | var forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')); 56 | var mask = ee.Image(forMask.select('B6').median().lt(300)) 57 | mask = mask.updateMask(mask).clip(footprint) 58 | 59 | // filter sentinel 2 collection 60 | var FC = MSI.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUDY_PIXEL_PERCENTAGE', "less_than", cloudPerc); 61 | print(FC, 'Sentinel 2 Collection') 62 | print(imDate, 'image date') 63 | 64 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 66 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 67 | // MSI Atmospheric Correction // 68 | 69 | var bands = ['B1','B2','B3','B4','B5','B6','B7', 'B8', 'B8A', 'B11', 'B12']; 70 | 71 | // rescale* 72 | var rescale = ee.Image(s2.divide(10000).multiply(mask).copyProperties(s2)).select(bands); 73 | 74 | // dem* 75 | var DEM = ee.Image('USGS/SRTMGL1_003').clip(footprint); 76 | 77 | // ozone* 78 | var DU = ee.Image(ozone.filterDate(iniDate,endDate).filterBounds(footprint).mean()); 79 | 80 | //Julian Day 81 | var imgDate = ee.Date(s2.get('system:time_start')); 82 | var FOY = ee.Date.fromYMD(imgDate.get('year'),1,1); 83 | var JD = imgDate.difference(FOY,'day').int().add(1); 84 | 85 | // earth-sun distance 86 | var myCos = ((ee.Image(0.0172).multiply(ee.Image(JD).subtract(ee.Image(2)))).cos()).pow(2) 87 | var cosd = myCos.multiply(pi.divide(ee.Image(180))).cos(); 88 | var d = ee.Image(1).subtract(ee.Image(0.01673)).multiply(cosd).clip(footprint) 89 | 90 | // sun azimuth 91 | var SunAz = ee.Image.constant(s2.get('MEAN_SOLAR_AZIMUTH_ANGLE')).clip(footprint); 92 | 93 | // sun zenith 94 | var SunZe = ee.Image.constant(s2.get('MEAN_SOLAR_ZENITH_ANGLE')).clip(footprint); 95 | var cosdSunZe = SunZe.multiply(pi.divide(ee.Image(180))).cos(); // in degrees 96 | var sindSunZe = SunZe.multiply(pi.divide(ee.Image(180))).sin(); // in degrees 97 | 98 | // sat zenith 99 | var SatZe = ee.Image.constant(s2.get('MEAN_INCIDENCE_ZENITH_ANGLE_B5')).clip(footprint); 100 | var cosdSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).cos(); 101 | var sindSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).sin(); 102 | 103 | // sat azimuth 104 | var SatAz = ee.Image.constant(s2.get('MEAN_INCIDENCE_AZIMUTH_ANGLE_B5')).clip(footprint); 105 | 106 | // relative azimuth 107 | var RelAz = SatAz.subtract(SunAz); 108 | var cosdRelAz = RelAz.multiply(pi.divide(ee.Image(180))).cos(); 109 | 110 | // Pressure 111 | var P = (ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM)).pow(5.25588)).multiply(0.01)).multiply(mask); 112 | var Po = ee.Image(1013.25); 113 | 114 | // esun 115 | var ESUN = ee.Image(ee.Array([ee.Image(s2.get('SOLAR_IRRADIANCE_B1')), 116 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2')), 117 | ee.Image(s2.get('SOLAR_IRRADIANCE_B3')), 118 | ee.Image(s2.get('SOLAR_IRRADIANCE_B4')), 119 | ee.Image(s2.get('SOLAR_IRRADIANCE_B5')), 120 | ee.Image(s2.get('SOLAR_IRRADIANCE_B6')), 121 | ee.Image(s2.get('SOLAR_IRRADIANCE_B7')), 122 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8')), 123 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8A')), 124 | ee.Image(s2.get('SOLAR_IRRADIANCE_B11')), 125 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2'))] 126 | )).toArray().toArray(1); 127 | 128 | ESUN = ESUN.multiply(ee.Image(1)) 129 | 130 | var ESUNImg = ESUN.arrayProject([0]).arrayFlatten([bands]); 131 | 132 | // create empty array for the images 133 | var imgArr = rescale.select(bands).toArray().toArray(1); 134 | 135 | // pTOA to Ltoa 136 | var Ltoa = imgArr.multiply(ESUN).multiply(cosdSunZe).divide(pi.multiply(d.pow(2))); 137 | 138 | // band centers 139 | var bandCenter = ee.Image(443).divide(1000).addBands(ee.Image(490).divide(1000)) 140 | .addBands(ee.Image(560).divide(1000)) 141 | .addBands(ee.Image(665).divide(1000)) 142 | .addBands(ee.Image(705).divide(1000)) 143 | .addBands(ee.Image(740).divide(1000)) 144 | .addBands(ee.Number(783).divide(1000)) 145 | .addBands(ee.Number(842).divide(1000)) 146 | .addBands(ee.Number(865).divide(1000)) 147 | .addBands(ee.Number(1610).divide(1000)) 148 | .addBands(ee.Number(2190).divide(1000)) 149 | .toArray().toArray(1); 150 | 151 | // ozone coefficients 152 | var koz = ee.Image(0.0039).addBands(ee.Image(0.0213)) 153 | .addBands(ee.Image(0.1052)) 154 | .addBands(ee.Image(0.0505)) 155 | .addBands(ee.Image(0.0205)) 156 | .addBands(ee.Image(0.0112)) 157 | .addBands(ee.Image(0.0075)) 158 | .addBands(ee.Image(0.0021)) 159 | .addBands(ee.Image(0.0019)) 160 | .addBands(ee.Image(0)) 161 | .addBands(ee.Image(0)) 162 | .toArray().toArray(1); 163 | 164 | ////////////////////////////////////// 165 | 166 | // Calculate ozone optical thickness 167 | var Toz = koz.multiply(DU).divide(ee.Image(1000)); 168 | 169 | // Calculate TOA radiance in the absense of ozone 170 | var Lt = Ltoa.multiply(((Toz)).multiply((ee.Image(1).divide(cosdSunZe)).add(ee.Image(1).divide(cosdSatZe))).exp()); 171 | 172 | // Rayleigh optical thickness 173 | var Tr = (P.divide(Po)).multiply(ee.Image(0.008569).multiply(bandCenter.pow(-4))).multiply((ee.Image(1).add(ee.Image(0.0113).multiply(bandCenter.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter.pow(-4))))); 174 | 175 | // Specular reflection (s- and p- polarization states) 176 | var theta_V = ee.Image(0.0000000001); 177 | var sin_theta_j = sindSunZe.divide(ee.Image(1.333)); 178 | 179 | var theta_j = sin_theta_j.asin().multiply(ee.Image(180).divide(pi)); 180 | 181 | var theta_SZ = SunZe; 182 | 183 | var R_theta_SZ_s = (((theta_SZ.multiply(pi.divide(ee.Image(180)))).subtract(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ.multiply(pi.divide(ee.Image(180)))).add(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2))); 184 | 185 | var R_theta_V_s = ee.Image(0.0000000001); 186 | 187 | var R_theta_SZ_p = (((theta_SZ.multiply(pi.divide(180))).subtract(theta_j.multiply(pi.divide(180)))).tan().pow(2)).divide((((theta_SZ.multiply(pi.divide(180))).add(theta_j.multiply(pi.divide(180)))).tan().pow(2))); 188 | 189 | var R_theta_V_p = ee.Image(0.0000000001); 190 | 191 | var R_theta_SZ = ee.Image(0.5).multiply(R_theta_SZ_s.add(R_theta_SZ_p)); 192 | 193 | var R_theta_V = ee.Image(0.5).multiply(R_theta_V_s.add(R_theta_V_p)); 194 | 195 | // Sun-sensor geometry 196 | var theta_neg = ((cosdSunZe.multiply(ee.Image(-1))).multiply(cosdSatZe)).subtract((sindSunZe).multiply(sindSatZe).multiply(cosdRelAz)); 197 | 198 | var theta_neg_inv = theta_neg.acos().multiply(ee.Image(180).divide(pi)); 199 | 200 | var theta_pos = (cosdSunZe.multiply(cosdSatZe)).subtract(sindSunZe.multiply(sindSatZe).multiply(cosdRelAz)); 201 | 202 | var theta_pos_inv = theta_pos.acos().multiply(ee.Image(180).divide(pi)); 203 | 204 | var cosd_tni = theta_neg_inv.multiply(pi.divide(180)).cos(); // in degrees 205 | 206 | var cosd_tpi = theta_pos_inv.multiply(pi.divide(180)).cos(); // in degrees 207 | 208 | var Pr_neg = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni.pow(2)))); 209 | 210 | var Pr_pos = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi.pow(2)))); 211 | 212 | // Rayleigh scattering phase function 213 | var Pr = Pr_neg.add((R_theta_SZ.add(R_theta_V)).multiply(Pr_pos)); 214 | 215 | // rayleigh radiance contribution 216 | var denom = ee.Image(4).multiply(pi).multiply(cosdSatZe); 217 | var Lr = (ESUN.multiply(Tr)).multiply(Pr.divide(denom)); 218 | 219 | // rayleigh corrected radiance 220 | var Lrc = Lt.subtract(Lr); 221 | var LrcImg = Lrc.arrayProject([0]).arrayFlatten([bands]); 222 | 223 | // Aerosol Correction // 224 | 225 | // Bands in nm 226 | var bands_nm = ee.Image(443).addBands(ee.Image(490)) 227 | .addBands(ee.Image(560)) 228 | .addBands(ee.Image(665)) 229 | .addBands(ee.Image(705)) 230 | .addBands(ee.Image(740)) 231 | .addBands(ee.Image(783)) 232 | .addBands(ee.Image(842)) 233 | .addBands(ee.Image(865)) 234 | .addBands(ee.Image(0)) 235 | .addBands(ee.Image(0)) 236 | .toArray().toArray(1); 237 | 238 | // Lam in SWIR bands 239 | var Lam_10 = LrcImg.select('B11'); 240 | var Lam_11 = LrcImg.select('B12'); 241 | 242 | // Calculate aerosol type 243 | var eps = ((((Lam_11).divide(ESUNImg.select('B12'))).log()).subtract(((Lam_10).divide(ESUNImg.select('B11'))).log())).divide(ee.Image(2190).subtract(ee.Image(1610))); 244 | 245 | // Calculate multiple scattering of aerosols for each band 246 | var Lam = (Lam_11).multiply(((ESUN).divide(ESUNImg.select('B12')))).multiply((eps.multiply(ee.Image(-1))).multiply((bands_nm.divide(ee.Image(2190)))).exp()); 247 | 248 | // diffuse transmittance 249 | var trans = Tr.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe)).exp(); 250 | 251 | // Compute water-leaving radiance 252 | var Lw = Lrc.subtract(Lam).divide(trans); 253 | 254 | // water-leaving reflectance 255 | var pw = (Lw.multiply(pi).multiply(d.pow(2)).divide(ESUN.multiply(cosdSunZe))); 256 | var pwImg = pw.arrayProject([0]).arrayFlatten([bands]); 257 | 258 | // remote sensing reflectance 259 | var Rrs = (pw.divide(pi).arrayProject([0]).arrayFlatten([bands]).slice(0,9)); 260 | print(Rrs, 'Rrs') 261 | 262 | /// Bio optical Models /// 263 | // chlor_a 264 | var NDCI = (Rrs.select('B5').subtract(Rrs.select('B4'))).divide(Rrs.select('B5').add(Rrs.select('B4'))) // ndci 265 | var chlor_a = ee.Image(14.039).add(ee.Image(86.115).multiply(NDCI)).add(ee.Image(194.325).multiply(NDCI.pow(ee.Image(2)))); // chlor_a 266 | 267 | // SD 268 | var ln_BlueRed = (Rrs.select('B2').divide(Rrs.select('B4'))).log() 269 | var lnMOSD = (ee.Image(1.4856).multiply(ln_BlueRed)).add(ee.Image(0.2734)); // R2 = 0.8748 with in-situ 270 | var MOSD = ee.Image(10).pow(lnMOSD); // log space to (m) 271 | var SD = (ee.Image(0.1777).multiply(MOSD)).add(ee.Image(1.0813)); 272 | 273 | // tsi 274 | var TSI_c = ee.Image(30.6).add(ee.Image(9.81)).multiply(chlor_a.log()) 275 | var TSI_s = ee.Image(60.0).subtract(ee.Image(14.41)).multiply(SD.log()) 276 | var TSI = (TSI_c.add(TSI_s)).divide(ee.Image(2)); 277 | 278 | // tsi reclassified 279 | // Create conditions 280 | var mask1 = TSI.lt(30); // classical oligitrophy (1) 281 | var mask2 = TSI.gte(30).and(TSI.lt(40));// (2) 282 | var mask3 = TSI.gte(40).and(TSI.lt(50)); // (3) 283 | var mask4 = TSI.gte(50).and(TSI.lt(60)); // (4) 284 | var mask5 = TSI.gte(60).and(TSI.lt(70)); // (5) 285 | var mask6 = TSI.gte(70).and(TSI.lt(80)); // (6) 286 | var mask7 = TSI.gte(80); // (7) 287 | 288 | 289 | // Reclassify conditions into new values 290 | var img1 = TSI.where(mask1.eq(1), 1).mask(mask1); 291 | var img2 = TSI.where(mask2.eq(1), 2).mask(mask2); 292 | var img3 = TSI.where(mask3.eq(1), 3).mask(mask3); 293 | var img4 = TSI.where(mask4.eq(1), 4).mask(mask4); 294 | var img5 = TSI.where(mask5.eq(1), 5).mask(mask5); 295 | var img6 = TSI.where(mask6.eq(1), 6).mask(mask6); 296 | var img7 = TSI.where(mask7.eq(1), 7).mask(mask7); 297 | 298 | 299 | // Ouput of reclassified image 300 | var TSI_R = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7); 301 | 302 | // Map Layers // 303 | Map.centerObject(footprint, 7); 304 | Map.addLayer(footprint, {}, 'footprint', true) // footprint 305 | Map.addLayer(s2.multiply(mask), {bands: ['B4', 'B3', 'B2'], min: 0, max: 1000}, 'rgb', false) // rgb 306 | Map.addLayer(SD, {min: 0, max: 3, palette: ['darkred', 'orange', 'yellow', 'limegreen', 'cyan' , 'blue']}, 'SD', false); 307 | Map.addLayer(chlor_a, {min: 0, max: 40, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange' , 'darkred']}, 'chlor_a', false); 308 | Map.addLayer(TSI, {min: 30, max: 70, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange' , 'darkred']}, 'TSI', false); 309 | Map.addLayer(TSI_R, {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 'TSI_R', true); 310 | 311 | // EXPORT IMAGES // 312 | 313 | // Export SD 314 | Export.image.toDrive({ 315 | image: SD, 316 | description: 'SD', 317 | scale: 20, 318 | region: footprint 319 | }); 320 | 321 | // Export chlor_a 322 | Export.image.toDrive({ 323 | image: chlor_a, 324 | description: 'chlor_a', 325 | scale: 20, 326 | region: footprint 327 | }); 328 | 329 | // Export TSI 330 | Export.image.toDrive({ 331 | image: TSI, 332 | description: 'TSI', 333 | scale: 20, 334 | region: footprint 335 | }); 336 | 337 | // Export TSI_R 338 | Export.image.toDrive({ 339 | image: TSI_R, 340 | description: 'TSI_R', 341 | scale: 20, 342 | region: footprint 343 | }); -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | #Coming soon... 2 | -------------------------------------------------------------------------------- /python/ls8_fai.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from ee.ee_exception import EEException 3 | 4 | try: 5 | ee.Initialize() 6 | except EEException as e: 7 | from oauth2client.service_account import ServiceAccountCredentials 8 | credentials = ServiceAccountCredentials.from_p12_keyfile( 9 | service_account_email='', 10 | filename='', 11 | private_key_password='notasecret', 12 | scopes=ee.oauth.SCOPE + ' https://www.googleapis.com/auth/drive ') 13 | ee.Initialize(credentials) 14 | 15 | geometry = ee.Geometry.Polygon([[[30.76171875, 0.9049611504960419], 16 | [30.8935546875, -3.487377195492663], 17 | [35.5517578125, -3.2680324702882952], 18 | [35.5517578125, 1.9593043032313748]]]) 19 | l8 = ee.Image('LANDSAT/LC08/C01/T1/LC08_170060_20171226') 20 | 21 | #Start date 22 | iniDate = '2015-05-01' 23 | 24 | #End Date 25 | endDate = '2018-03-31' 26 | 27 | oliCloudPerc = 5 28 | 29 | #Landsat 8 raw dn 30 | OLI_DN = ee.ImageCollection('LANDSAT/LC08/C01/T1') 31 | 32 | #Landsat-8 surface reflactance product (for masking purposes) 33 | SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR') 34 | 35 | #toms / omi 36 | ozone = ee.ImageCollection('TOMS/MERGED') 37 | 38 | #Filtering Collection and Masking 39 | pi = ee.Image(3.141592) 40 | 41 | #Water Mask 42 | startMonth = 5 43 | endMonth = 9 44 | startYear = 2013 45 | endYear = 2017 46 | 47 | forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')) 48 | mask = ee.Image(forMask.select('B6').median().lt(600)) 49 | mask = mask.updateMask(mask) 50 | FC_OLI = OLI_DN.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUD_COVER', "less_than", oliCloudPerc) 51 | 52 | #OLI image date 53 | oliDate = l8.date() 54 | footprint = l8.geometry() 55 | 56 | #DEM 57 | DEM_OLI = ee.Image('USGS/SRTMGL1_003').clip(footprint) 58 | 59 | #Ozone 60 | DU_OLI = ee.Image(ozone.filterDate(iniDate,endDate).filterBounds(footprint).mean()) 61 | 62 | #Julian Day 63 | imgDate_OLI = ee.Date(l8.get('system:time_start')) 64 | FOY_OLI = ee.Date.fromYMD(imgDate_OLI.get('year'),1,1) 65 | JD_OLI = imgDate_OLI.difference(FOY_OLI,'day').int().add(1) 66 | 67 | #Earth-Sun distance 68 | d_OLI = ee.Image.constant(l8.get('EARTH_SUN_DISTANCE')) 69 | 70 | #Sun elevation 71 | SunEl_OLI = ee.Image.constant(l8.get('SUN_ELEVATION')) 72 | 73 | #Sun azimuth 74 | SunAz_OLI = ee.Image.constant(l8.get('SUN_AZIMUTH')) 75 | 76 | #Satellite Zenith 77 | SatZe_OLI = ee.Image(0.0) 78 | cosdSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).cos() 79 | sindSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).sin() 80 | 81 | #Satellite Azimuth 82 | SatAz_OLI = ee.Image(0.0) 83 | 84 | #Sun zenith 85 | SunZe_OLI = ee.Image(90).subtract(SunEl_OLI) 86 | cosdSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image.constant(180))).cos() 87 | sindSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image(180))).sin() 88 | 89 | #Relative azimuth 90 | RelAz_OLI = ee.Image(SunAz_OLI) 91 | cosdRelAz_OLI = RelAz_OLI.multiply(pi.divide(ee.Image(180))).cos() 92 | 93 | #Pressure calculation 94 | P_OLI = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM_OLI)).pow(5.25588)).multiply(0.01) 95 | Po_OLI = ee.Image(1013.25) 96 | 97 | #Radiometric Calibration 98 | #define bands to be converted to radiance 99 | bands_OLI = ['B1','B2','B3','B4','B5','B6','B7'] 100 | 101 | #Radiance Mult Bands 102 | rad_mult_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_MULT_BAND_1')), 103 | ee.Image(l8.get('RADIANCE_MULT_BAND_2')), 104 | ee.Image(l8.get('RADIANCE_MULT_BAND_3')), 105 | ee.Image(l8.get('RADIANCE_MULT_BAND_4')), 106 | ee.Image(l8.get('RADIANCE_MULT_BAND_5')), 107 | ee.Image(l8.get('RADIANCE_MULT_BAND_6')), 108 | ee.Image(l8.get('RADIANCE_MULT_BAND_7'))] 109 | )).toArray(1) 110 | 111 | #Radiance add band 112 | rad_add_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_ADD_BAND_1')), 113 | ee.Image(l8.get('RADIANCE_ADD_BAND_2')), 114 | ee.Image(l8.get('RADIANCE_ADD_BAND_3')), 115 | ee.Image(l8.get('RADIANCE_ADD_BAND_4')), 116 | ee.Image(l8.get('RADIANCE_ADD_BAND_5')), 117 | ee.Image(l8.get('RADIANCE_ADD_BAND_6')), 118 | ee.Image(l8.get('RADIANCE_ADD_BAND_7'))] 119 | )).toArray(1) 120 | 121 | # Create an empty image to save new radiance bands to 122 | imgArr_OLI = l8.select(bands_OLI).toArray().toArray(1) 123 | Ltoa_OLI = imgArr_OLI.multiply(rad_mult_OLI).add(rad_add_OLI) 124 | 125 | #esun 126 | ESUN_OLI = ee.Image.constant(197.24790954589844) \ 127 | .addBands(ee.Image.constant(201.98426818847656))\ 128 | .addBands(ee.Image.constant(186.12677001953125))\ 129 | .addBands(ee.Image.constant(156.95257568359375))\ 130 | .addBands(ee.Image.constant(96.04714965820312))\ 131 | .addBands(ee.Image.constant(23.8833221450863))\ 132 | .addBands(ee.Image.constant(8.04995873449635)).toArray().toArray(1) 133 | 134 | ESUN_OLI = ESUN_OLI.multiply(ee.Image(1)) 135 | 136 | ESUNImg_OLI = ESUN_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 137 | 138 | #Ozone correction 139 | #Ozone coefficients 140 | koz_OLI = ee.Image.constant(0.0039).addBands(ee.Image.constant(0.0218))\ 141 | .addBands(ee.Image.constant(0.1078))\ 142 | .addBands(ee.Image.constant(0.0608))\ 143 | .addBands(ee.Image.constant(0.0019))\ 144 | .addBands(ee.Image.constant(0))\ 145 | .addBands(ee.Image.constant(0))\ 146 | .toArray().toArray(1) 147 | 148 | #Calculate ozone optical thickness 149 | Toz_OLI = koz_OLI.multiply(DU_OLI).divide(ee.Image.constant(1000)) 150 | # Calculate TOA radiance in the absense of ozone 151 | Lt_OLI = Ltoa_OLI.multiply(((Toz_OLI)).multiply((ee.Image.constant(1).divide(cosdSunZe_OLI)).add(ee.Image.constant(1).divide(cosdSatZe_OLI))).exp()) 152 | 153 | # Rayleigh optical thickness 154 | bandCenter_OLI = ee.Image(443).divide(1000).addBands(ee.Image(483).divide(1000))\ 155 | .addBands(ee.Image(561).divide(1000))\ 156 | .addBands(ee.Image(655).divide(1000))\ 157 | .addBands(ee.Image(865).divide(1000))\ 158 | .addBands(ee.Image(1609).divide(1000))\ 159 | .addBands(ee.Number(2201).divide(1000))\ 160 | .toArray().toArray(1) 161 | 162 | # create an empty image to save new Tr values to 163 | Tr_OLI = (P_OLI.divide(Po_OLI)).multiply(ee.Image(0.008569).multiply(bandCenter_OLI.pow(-4))).multiply((ee.Image(1).add(ee.Image(0.0113).multiply(bandCenter_OLI.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter_OLI.pow(-4))))) 164 | 165 | # Fresnel Reflection # 166 | # Specular reflection (s- and p- polarization states) 167 | theta_V_OLI = ee.Image(0.0000000001) 168 | sin_theta_j_OLI = sindSunZe_OLI.divide(ee.Image(1.333)) 169 | 170 | theta_j_OLI = sin_theta_j_OLI.asin().multiply(ee.Image(180).divide(pi)) 171 | 172 | theta_SZ_OLI = SunZe_OLI 173 | 174 | R_theta_SZ_s_OLI = (((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).subtract(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).add(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2))) 175 | 176 | R_theta_V_s_OLI = ee.Image(0.0000000001) 177 | R_theta_SZ_p_OLI = (((theta_SZ_OLI.multiply(pi.divide(180))).subtract(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2)).divide((((theta_SZ_OLI.multiply(pi.divide(180))).add(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2))) 178 | R_theta_V_p_OLI = ee.Image(0.0000000001) 179 | R_theta_SZ_OLI = ee.Image(0.5).multiply(R_theta_SZ_s_OLI.add(R_theta_SZ_p_OLI)) 180 | R_theta_V_OLI = ee.Image(0.5).multiply(R_theta_V_s_OLI.add(R_theta_V_p_OLI)) 181 | 182 | # Rayleigh scattering phase function # 183 | # Sun-sensor geometry 184 | theta_neg_OLI = ((cosdSunZe_OLI.multiply(ee.Image(-1))).multiply(cosdSatZe_OLI)).subtract((sindSunZe_OLI).multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)) 185 | 186 | theta_neg_inv_OLI = theta_neg_OLI.acos().multiply(ee.Image(180).divide(pi)) 187 | 188 | theta_pos_OLI = (cosdSunZe_OLI.multiply(cosdSatZe_OLI)).subtract(sindSunZe_OLI.multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)) 189 | 190 | theta_pos_inv_OLI = theta_pos_OLI.acos().multiply(ee.Image(180).divide(pi)) 191 | 192 | cosd_tni_OLI = theta_neg_inv_OLI.multiply(pi.divide(180)).cos() # in degrees 193 | 194 | cosd_tpi_OLI = theta_pos_inv_OLI.multiply(pi.divide(180)).cos() # in degrees 195 | 196 | Pr_neg_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni_OLI.pow(2)))) 197 | 198 | Pr_pos_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi_OLI.pow(2)))) 199 | 200 | # Rayleigh scattering phase function 201 | Pr_OLI = Pr_neg_OLI.add((R_theta_SZ_OLI.add(R_theta_V_OLI)).multiply(Pr_pos_OLI)) 202 | 203 | # Calulate Lr, 204 | denom_OLI = ee.Image(4).multiply(pi).multiply(cosdSatZe_OLI) 205 | Lr_OLI = (ESUN_OLI.multiply(Tr_OLI)).multiply(Pr_OLI.divide(denom_OLI)) 206 | 207 | # Rayleigh corrected radiance 208 | Lrc_OLI = (Lt_OLI.divide(ee.Image(10))).subtract(Lr_OLI) 209 | LrcImg_OLI = Lrc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 210 | 211 | # Rayleigh corrected reflectance 212 | prc_OLI = Lrc_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI)) 213 | pc = prc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 214 | 215 | pcVisParams = {'bands': 'B4,B3,B2','min': 0,'max': 0.1} 216 | pc = pc.multiply(mask) 217 | pcImgID = pc.getMapId(pcVisParams) 218 | # Calculate FAI 219 | NIRprime = (pc.select('B4')).add((pc.select('B6').subtract(pc.select('B4'))).multiply((ee.Image(865).subtract(ee.Image(655))).divide((ee.Image(1609).subtract(ee.Image(655)))))) 220 | fai = (pc.select('B5').subtract(NIRprime)) 221 | fai = fai.multiply(mask) 222 | 223 | faiVisParams = {'min': -0.05, 'max': 0.2, 'palette': '#000080,#0080FF,#7BFF7B,#FF9700,#800000'} 224 | faiImgID = fai.getMapId(faiVisParams) 225 | 226 | print pcImgID 227 | print faiImgID 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /python/ls8_single_image.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from ee.ee_exception import EEException 3 | 4 | try: 5 | ee.Initialize() 6 | except EEException as e: 7 | from oauth2client.service_account import ServiceAccountCredentials 8 | credentials = ServiceAccountCredentials.from_p12_keyfile( 9 | service_account_email='', 10 | filename='', 11 | private_key_password='notasecret', 12 | scopes=ee.oauth.SCOPE + ' https://www.googleapis.com/auth/drive ') 13 | ee.Initialize(credentials) 14 | # This script processes a single Landsat 8 image of interest. 15 | # WQ parameters such chlor-a, secchi depth, trophic state index are calculated 16 | # How to use: 17 | # (1) If a "geometry" variable exists in the imports window, delete it. 18 | # (2) Within the map, select the point button near the top left side. 19 | # (3) Create a new point by clicking on a location of interest. 20 | # (4) Adjust the time frame you wish to browse by adding here: 21 | 22 | geometry = ee.Geometry.Polygon([[[30.76171875, 0.9049611504960419], 23 | [30.8935546875, -3.487377195492663], 24 | [35.5517578125, -3.2680324702882952], 25 | [35.5517578125, 1.9593043032313748]]]) 26 | # begin date 27 | iniDate = '2015-05-01' 28 | # end date 29 | endDate = '2018-03-31' 30 | # (5) Adjust a cloud % threshold here: 31 | oliCloudPerc = 5 32 | # (6) Click Run 33 | # (7) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 34 | # image from this collection, find the FILE_ID within the features and copy and paste it here: 35 | l8 = ee.Image('LANDSAT/LC08/C01/T1/LC08_170060_20171226') 36 | # (8) Click "Run" 37 | # (9) Export each image by clicking on the run button within the "tasks" tab. 38 | 39 | # Author: Benjamin Page # 40 | # Citations: 41 | # Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring, In Review 42 | 43 | #########################################################/ 44 | #########################################################/ 45 | #########################################################/ 46 | 47 | # Import Collections # 48 | 49 | # landsat 8 raw dn 50 | OLI_DN = ee.ImageCollection('LANDSAT/LC08/C01/T1') 51 | 52 | # landsat-8 surface reflactance product (for masking purposes) 53 | SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR') 54 | 55 | # toms / omi 56 | ozone = ee.ImageCollection('TOMS/MERGED') 57 | 58 | #########################################################/ 59 | #########################################################/ 60 | #########################################################/ 61 | # Filtering Collection and Masking # 62 | 63 | pi = ee.Image(3.141592) 64 | 65 | # water mask 66 | startMonth = 5 67 | endMonth = 9 68 | startYear = 2013 69 | endYear = 2017 70 | 71 | forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter \ 72 | (ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')) 73 | mask = ee.Image(forMask.select('B6').median().lt(600)) 74 | mask = mask.updateMask(mask) 75 | 76 | # filter landsat 8 collection 77 | FC_OLI = OLI_DN.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUD_COVER', "less_than", oliCloudPerc) 78 | 79 | # oli image date 80 | oliDate = l8.date() 81 | 82 | footprint = l8.geometry() 83 | 84 | #########################################################/ 85 | #########################################################/ 86 | #########################################################/ 87 | ################################################################################ 88 | # single OLI image ATMOSPHERIC CORRECTION # 89 | 90 | # dem 91 | DEM_OLI = ee.Image('USGS/SRTMGL1_003').clip(footprint) 92 | 93 | # ozone 94 | DU_OLI = ee.Image(ozone.filterDate(iniDate ,endDate).filterBounds(footprint).mean()) 95 | 96 | # Julian Day 97 | imgDate_OLI = ee.Date(l8.get('system:time_start')) 98 | FOY_OLI = ee.Date.fromYMD(imgDate_OLI.get('year') ,1 ,1) 99 | JD_OLI = imgDate_OLI.difference(FOY_OLI ,'day').int().add(1) 100 | 101 | # Earth-Sun distance 102 | d_OLI = ee.Image.constant(l8.get('EARTH_SUN_DISTANCE')) 103 | 104 | # Sun elevation 105 | SunEl_OLI = ee.Image.constant(l8.get('SUN_ELEVATION')) 106 | 107 | # Sun azimuth 108 | SunAz_OLI = ee.Image.constant(l8.get('SUN_AZIMUTH')) 109 | 110 | # Satellite zenith 111 | SatZe_OLI = ee.Image(0.0) 112 | cosdSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).cos() 113 | sindSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).sin() 114 | 115 | # Satellite azimuth 116 | SatAz_OLI = ee.Image(0.0) 117 | 118 | # Sun zenith 119 | SunZe_OLI = ee.Image(90).subtract(SunEl_OLI) 120 | cosdSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image.constant(180))).cos() # in degrees 121 | sindSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image(180))).sin() # in degrees 122 | 123 | # Relative azimuth 124 | RelAz_OLI = ee.Image(SunAz_OLI) 125 | cosdRelAz_OLI = RelAz_OLI.multiply(pi.divide(ee.Image(180))).cos() 126 | 127 | # Pressure calculation 128 | P_OLI = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM_OLI)).pow(5.25588)).multiply \ 129 | (0.01) 130 | Po_OLI = ee.Image(1013.25) 131 | 132 | # Radiometric Calibration # 133 | # define bands to be converted to radiance 134 | bands_OLI = ['B1' ,'B2' ,'B3' ,'B4' ,'B5' ,'B6' ,'B7'] 135 | 136 | # radiance_mult_bands 137 | rad_mult_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_MULT_BAND_1')), 138 | ee.Image(l8.get('RADIANCE_MULT_BAND_2')), 139 | ee.Image(l8.get('RADIANCE_MULT_BAND_3')), 140 | ee.Image(l8.get('RADIANCE_MULT_BAND_4')), 141 | ee.Image(l8.get('RADIANCE_MULT_BAND_5')), 142 | ee.Image(l8.get('RADIANCE_MULT_BAND_6')), 143 | ee.Image(l8.get('RADIANCE_MULT_BAND_7'))] 144 | )).toArray(1) 145 | 146 | # radiance add band 147 | rad_add_OLI = ee.Image(ee.Array([ee.Image(l8.get('RADIANCE_ADD_BAND_1')), 148 | ee.Image(l8.get('RADIANCE_ADD_BAND_2')), 149 | ee.Image(l8.get('RADIANCE_ADD_BAND_3')), 150 | ee.Image(l8.get('RADIANCE_ADD_BAND_4')), 151 | ee.Image(l8.get('RADIANCE_ADD_BAND_5')), 152 | ee.Image(l8.get('RADIANCE_ADD_BAND_6')), 153 | ee.Image(l8.get('RADIANCE_ADD_BAND_7'))] 154 | )).toArray(1) 155 | 156 | # create an empty image to save new radiance bands to 157 | imgArr_OLI = l8.select(bands_OLI).toArray().toArray(1) 158 | Ltoa_OLI = imgArr_OLI.multiply(rad_mult_OLI).add(rad_add_OLI) 159 | 160 | # esun 161 | ESUN_OLI = ee.Image.constant(197.24790954589844)\ 162 | .addBands(ee.Image.constant(201.98426818847656))\ 163 | .addBands(ee.Image.constant(186.12677001953125))\ 164 | .addBands(ee.Image.constant(156.95257568359375))\ 165 | .addBands(ee.Image.constant(96.04714965820312))\ 166 | .addBands(ee.Image.constant(23.8833221450863))\ 167 | .addBands(ee.Image.constant(8.04995873449635)).toArray().toArray(1) 168 | ESUN_OLI = ESUN_OLI.multiply(ee.Image(1)) 169 | 170 | ESUNImg_OLI = ESUN_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 171 | 172 | # Ozone Correction # 173 | # Ozone coefficients 174 | koz_OLI = ee.Image.constant(0.0039).addBands(ee.Image.constant(0.0218))\ 175 | .addBands(ee.Image.constant(0.1078))\ 176 | .addBands(ee.Image.constant(0.0608))\ 177 | .addBands(ee.Image.constant(0.0019))\ 178 | .addBands(ee.Image.constant(0))\ 179 | .addBands(ee.Image.constant(0))\ 180 | .toArray().toArray(1) 181 | 182 | # Calculate ozone optical thickness 183 | Toz_OLI = koz_OLI.multiply(DU_OLI).divide(ee.Image.constant(1000)) 184 | 185 | # Calculate TOA radiance in the absense of ozone 186 | Lt_OLI = Ltoa_OLI.multiply(((Toz_OLI)).multiply 187 | ((ee.Image.constant(1).divide(cosdSunZe_OLI)).add(ee.Image.constant(1).divide(cosdSatZe_OLI))).exp()) 188 | 189 | # Rayleigh optical thickness 190 | bandCenter_OLI = ee.Image(443).divide(1000).addBands(ee.Image(483).divide(1000))\ 191 | .addBands(ee.Image(561).divide(1000))\ 192 | .addBands(ee.Image(655).divide(1000))\ 193 | .addBands(ee.Image(865).divide(1000))\ 194 | .addBands(ee.Image(1609).divide(1000))\ 195 | .addBands(ee.Number(2201).divide(1000))\ 196 | .toArray().toArray(1) 197 | 198 | # create an empty image to save new Tr values to 199 | Tr_OLI = (P_OLI.divide(Po_OLI)).multiply(ee.Image(0.008569).multiply(bandCenter_OLI.pow(-4))).multiply((ee.Image(1).add\ 200 | (ee.Image(0.0113).multiply(bandCenter_OLI.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter_OLI.pow(-4))))) 201 | 202 | # Fresnel Reflection # 203 | # Specular reflection (s- and p- polarization states) 204 | theta_V_OLI = ee.Image(0.0000000001) 205 | sin_theta_j_OLI = sindSunZe_OLI.divide(ee.Image(1.333)) 206 | 207 | theta_j_OLI = sin_theta_j_OLI.asin().multiply(ee.Image(180).divide(pi)) 208 | 209 | theta_SZ_OLI = SunZe_OLI 210 | 211 | R_theta_SZ_s_OLI = (((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).subtract 212 | (theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ_OLI.multiply 213 | (pi.divide(ee.Image(180)))).add(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2))) 214 | 215 | R_theta_V_s_OLI = ee.Image(0.0000000001) 216 | 217 | R_theta_SZ_p_OLI = (((theta_SZ_OLI.multiply(pi.divide(180))).subtract(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2)).\ 218 | divide((((theta_SZ_OLI.multiply(pi.divide(180))).add(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2))) 219 | 220 | R_theta_V_p_OLI = ee.Image(0.0000000001) 221 | 222 | R_theta_SZ_OLI = ee.Image(0.5).multiply(R_theta_SZ_s_OLI.add(R_theta_SZ_p_OLI)) 223 | 224 | R_theta_V_OLI = ee.Image(0.5).multiply(R_theta_V_s_OLI.add(R_theta_V_p_OLI)) 225 | 226 | # Rayleigh scattering phase function # 227 | # Sun-sensor geometry 228 | theta_neg_OLI = ((cosdSunZe_OLI.multiply(ee.Image(-1))).multiply(cosdSatZe_OLI)).\ 229 | subtract((sindSunZe_OLI).multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)) 230 | 231 | theta_neg_inv_OLI = theta_neg_OLI.acos().multiply(ee.Image(180).divide(pi)) 232 | 233 | theta_pos_OLI = (cosdSunZe_OLI.multiply(cosdSatZe_OLI)).\ 234 | subtract(sindSunZe_OLI.multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)) 235 | 236 | theta_pos_inv_OLI = theta_pos_OLI.acos().multiply(ee.Image(180).divide(pi)) 237 | 238 | cosd_tni_OLI = theta_neg_inv_OLI.multiply(pi.divide(180)).cos() # in degrees 239 | 240 | cosd_tpi_OLI = theta_pos_inv_OLI.multiply(pi.divide(180)).cos() # in degrees 241 | 242 | Pr_neg_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni_OLI.pow(2)))) 243 | 244 | Pr_pos_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi_OLI.pow(2)))) 245 | 246 | # Rayleigh scattering phase function 247 | Pr_OLI = Pr_neg_OLI.add((R_theta_SZ_OLI.add(R_theta_V_OLI)).multiply(Pr_pos_OLI)) 248 | 249 | # Calulate Lr, 250 | denom_OLI = ee.Image(4).multiply(pi).multiply(cosdSatZe_OLI) 251 | Lr_OLI = (ESUN_OLI.multiply(Tr_OLI)).multiply(Pr_OLI.divide(denom_OLI)) 252 | 253 | # Rayleigh corrected radiance 254 | Lrc_OLI = (Lt_OLI.divide(ee.Image(10))).subtract(Lr_OLI) 255 | LrcImg_OLI = Lrc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 256 | 257 | # Rayleigh corrected reflectance 258 | prc_OLI = Lrc_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI)) 259 | prcImg_OLI = prc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 260 | 261 | # Aerosol Correction # 262 | # Bands in nm 263 | bands_nm_OLI = ee.Image(443).addBands(ee.Image(483))\ 264 | .addBands(ee.Image(561))\ 265 | .addBands(ee.Image(655))\ 266 | .addBands(ee.Image(865))\ 267 | .addBands(ee.Image(0))\ 268 | .addBands(ee.Image(0))\ 269 | .toArray().toArray(1) 270 | 271 | # Lam in SWIR bands 272 | Lam_6_OLI = LrcImg_OLI.select('B6') 273 | Lam_7_OLI = LrcImg_OLI.select('B7') 274 | 275 | # Calculate aerosol type 276 | eps_OLI = (((((Lam_7_OLI).divide(ESUNImg_OLI.select('B7'))).log()).\ 277 | subtract(((Lam_6_OLI).divide(ESUNImg_OLI.select('B6'))).log())).divide(ee.Image(2201).subtract(ee.Image(1609))))\ 278 | .multiply(mask) 279 | 280 | # Calculate multiple scattering of aerosols for each band 281 | Lam_OLI = (Lam_7_OLI).multiply(((ESUN_OLI).divide(ESUNImg_OLI.select('B7'))))\ 282 | .multiply((eps_OLI.multiply(ee.Image(-1))).multiply((bands_nm_OLI.divide(ee.Image(2201)))).exp()) 283 | 284 | # diffuse transmittance 285 | trans_OLI = Tr_OLI.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe_OLI)).exp() 286 | 287 | # Compute water-leaving radiance 288 | Lw_OLI = Lrc_OLI.subtract(Lam_OLI).divide(trans_OLI) 289 | 290 | # water-leaving reflectance 291 | pw_OLI = (Lw_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI))) 292 | pwImg_OLI = pw_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 293 | 294 | # Rrs 295 | Rrs = (pw_OLI.divide(pi).arrayProject([0]).arrayFlatten([bands_OLI]).slice(0, 5)) 296 | 297 | ###########################################################/ 298 | # Models # 299 | 300 | # Surface water temperature 301 | TIRS_1 = l8.select('B10') 302 | 303 | b10_add_band = ee.Number(TIRS_1.get('RADIANCE_ADD_BAND_10')) 304 | b10_mult_band = ee.Number(TIRS_1.get('RADIANCE_MULT_BAND_10')) 305 | 306 | Oi = ee.Number(0.29) 307 | 308 | TIRS_cal = (TIRS_1.multiply(b10_mult_band).add(b10_add_band).subtract(Oi)) # .multiply(lakes) 309 | 310 | K1_b10 = ee.Number(TIRS_1.get('K1_CONSTANT_BAND_10')) 311 | K2_b10 = ee.Number(TIRS_1.get('K2_CONSTANT_BAND_10')) 312 | 313 | LST = ((ee.Image(K2_b10).divide(((ee.Image(K1_b10).divide(ee.Image(TIRS_cal))).add(ee.Image(1))).log()))\ 314 | .subtract(ee.Image(273))).multiply(mask) # Celsius 315 | LST = (ee.Image(0.7745).multiply(LST)).add(ee.Image(9.6502)) # calibration R2 = 0.8599 316 | 317 | # chlor_a 318 | # Chlorophyll-a OC3 319 | a0 = ee.Image(0.2412) 320 | a1 = ee.Image(-2.0546) 321 | a2 = ee.Image(1.1776) 322 | a3 = ee.Image(-0.5538) 323 | a4 = ee.Image(-0.4570) 324 | 325 | log_BG = (Rrs.select('B1').divide(Rrs.select('B3'))).log10() 326 | 327 | a1a = a1.multiply(log_BG.pow(1)) 328 | a2a = a2.multiply(log_BG.pow(2)) 329 | a3a = a3.multiply(log_BG.pow(3)) 330 | a4a = a4.multiply(log_BG.pow(4)) 331 | 332 | sum = a1a.add(a2a).add(a3a).add(a4a) 333 | 334 | log10_chlor_a = a0.add(sum) 335 | 336 | chlor_a = ee.Image(10).pow(log10_chlor_a) 337 | chlor_a_cal = ee.Image(4.0752).multiply(chlor_a).subtract(ee.Image(3.9617)) 338 | 339 | # SD 340 | ln_BlueRed = (Rrs.select('B2').divide(Rrs.select('B4'))).log() 341 | lnMOSD = (ee.Image(1.4856).multiply(ln_BlueRed)).add(ee.Image(0.2734)) # R2 = 0.8748 with in-situ 342 | MOSD = ee.Image(10).pow(lnMOSD) # log space to (m) 343 | SD = (ee.Image(0.1777).multiply(MOSD)).add(ee.Image(1.0813)) 344 | 345 | # tsi 346 | TSI_c = ee.Image(30.6).add(ee.Image(9.81)).multiply(chlor_a_cal.log()) 347 | TSI_s = ee.Image(60.0).subtract(ee.Image(14.41)).multiply(SD.log()) 348 | TSI = (TSI_c.add(TSI_s)).divide(ee.Image(2)) 349 | 350 | # Reclassify TSI 351 | 352 | # Create conditions 353 | mask1 = TSI.lt(30) # (1) 354 | mask2 = TSI.gte(30).And(TSI.lt(40)) # (2) 355 | mask3 = TSI.gte(40).And(TSI.lt(50)) # (3) 356 | mask4 = TSI.gte(50).And(TSI.lt(60)) # (4) 357 | mask5 = TSI.gte(60).And(TSI.lt(70)) # (5) 358 | mask6 = TSI.gte(70).And(TSI.lt(80)) # (6) 359 | mask7 = TSI.gte(80) # (7) 360 | 361 | # Reclassify conditions into new values 362 | img1 = TSI.where(mask1.eq(1), 1).mask(mask1) 363 | img2 = TSI.where(mask2.eq(1), 2).mask(mask2) 364 | img3 = TSI.where(mask3.eq(1), 3).mask(mask3) 365 | img4 = TSI.where(mask4.eq(1), 4).mask(mask4) 366 | img5 = TSI.where(mask5.eq(1), 5).mask(mask5) 367 | img6 = TSI.where(mask6.eq(1), 6).mask(mask6) 368 | img7 = TSI.where(mask7.eq(1), 7).mask(mask7) 369 | 370 | # Ouput of reclassified image 371 | TSI_R = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7) 372 | 373 | l8Lyr = l8.multiply(mask) 374 | l8VisParams = {'bands': 'B4,B3,B2', 'min': 0, 'max': 15000} 375 | l8MapId = l8Lyr.getMapId(l8VisParams) 376 | 377 | LSTvisParams = {'min': 20, 'max': 30, 'palette': 'darkblue,blue,white,red,darkred'} 378 | LSTMapId = LST.getMapId(LSTvisParams) 379 | 380 | chlor_a_cal_visParams = {'min': 0, 'max': 80, 'palette':'blue,cyan,limegreen,yellow,orange,darkred'} 381 | chlor_a_cal_mapid = chlor_a_cal.getMapId(chlor_a_cal_visParams) 382 | 383 | SDvisParams = {'min': 0, 'max': 3, 'palette': 'darkred,orange,yellow,limegreen,cyan,blue'} 384 | SDMapId = SD.getMapId(SDvisParams) 385 | 386 | TSIvisParams = {'min': 30, 'max': 70, 'palette': 'blue,cyan,limegreen,yellow,orange,darkred'} 387 | TSIMapId = TSI.getMapId(TSIvisParams) 388 | 389 | TSI_R_visParams = {'min': 1, 'max': 7, 'palette': 'purple,blue,limegreen,yellow,orange,orangered,darkred'} 390 | TSI_R_MapId = TSI_R.getMapId(TSI_R_visParams) 391 | 392 | print 'L8',l8MapId 393 | print 'LST',LSTMapId 394 | print 'Chlor A',chlor_a_cal_mapid 395 | print 'SD',SDMapId 396 | print 'TSI',TSIMapId 397 | print 'TSI R',TSI_R_MapId 398 | # Map Layers 399 | 400 | # Map.addLayer(l8.multiply(mask), {bands: ['B4', 'B3', 'B2'], min: 0, max: 15000}, 'rgb', false) # rgb 401 | # Map.addLayer(LST, {min: 20, max: 30, palette: ['darkblue', 'blue', 'white', 'red', 'darkred']}, 'LST', false) 402 | # Map.addLayer(chlor_a_cal, {min: 0, max: 80, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange', 'darkred']}, 403 | # 'chlor_a', false) 404 | # Map.addLayer(SD, {min: 0, max: 3, palette: ['darkred', 'orange', 'yellow', 'limegreen', 'cyan', 'blue']}, 'SD', false) 405 | # Map.addLayer(TSI, {min: 30, max: 70, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange', 'darkred']}, 'TSI', 406 | # false) 407 | # Map.addLayer(TSI_R, 408 | # {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 409 | # 'TSI_R', true) 410 | # 411 | # 412 | -------------------------------------------------------------------------------- /python/ls8_wq_timeseries.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from ee.ee_exception import EEException 3 | import datetime 4 | 5 | try: 6 | ee.Initialize() 7 | except EEException as e: 8 | from oauth2client.service_account import ServiceAccountCredentials 9 | credentials = ServiceAccountCredentials.from_p12_keyfile( 10 | service_account_email='', 11 | filename='', 12 | private_key_password='notasecret', 13 | scopes=ee.oauth.SCOPE + ' https://www.googleapis.com/auth/drive ') 14 | ee.Initialize(credentials) 15 | 16 | geometry = ee.Geometry.Polygon([[[78.41079711914062, 17.465297700926097], 17 | [78.41629028320312, 17.334252606951086], 18 | [78.59893798828125, 17.33490806580805], 19 | [78.55087280273438, 17.494769882318828]]]) 20 | 21 | # This script processes and charts WQ parameter valuse from a collection of Lansdat 8 archive for a particular pixel throughout time. 22 | # WQ parameters such secchi depth, tropshic state index, and lake temperature 23 | # How to use: 24 | # (1) If a "geometry" iable exists in the imports window, delete it. 25 | # (2) Within the map, select the point button near the top left side. 26 | # (3) Create a new point by clicking on a location of interest. 27 | # (4) Adjust the time frame you wish to browse by adding here: 28 | # begin date 29 | iniDate = '2016-04-01' 30 | # end date 31 | endDate = '2016-10-31' 32 | # (5) Adjust a cloud % threshold here: 33 | oliCloudPerc = 10 34 | # (5) Click Run 35 | # (5) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 36 | # image from this collection, highlight the FILE_ID and copy and paste it into the proper location within the "Ls8 Single Image" script. 37 | # (6) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 38 | # (6) Click "Run" 39 | # (7) Export each chart as a csv by clicking on the export icon near the top-right of the chart. 40 | 41 | # Author: Benjamin Page # 42 | # Citations: 43 | # Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring 44 | 45 | #########################################################/ 46 | #########################################################/ 47 | #########################################################/ 48 | 49 | 50 | # Import Collections # 51 | 52 | # landsat 8 raw dn 53 | OLI_DN = ee.ImageCollection('LANDSAT/LC08/C01/T1') 54 | 55 | # landsat-8 surface reflactance product (for masking purposes) 56 | SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR') 57 | 58 | # toms / omi 59 | ozone = ee.ImageCollection('TOMS/MERGED') 60 | 61 | #########################################################/ 62 | #########################################################/ 63 | #########################################################/ 64 | # Filtering Collection and Masking # 65 | 66 | pi = ee.Image(3.141592) 67 | 68 | # water mask 69 | startMonth = 5 70 | endMonth = 9 71 | startYear = 2013 72 | endYear = 2017 73 | 74 | forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10)\ 75 | .filter(ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')) 76 | mask = ee.Image(forMask.select('B6').median().lt(600)) 77 | mask = mask.updateMask(mask) 78 | 79 | # filter landsat 8 collection 80 | FC_OLI = OLI_DN.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUD_COVER', "less_than",oliCloudPerc) 81 | 82 | # Mapping Functions # 83 | 84 | def l8Correction(img): 85 | 86 | # tile geometry 87 | 88 | l8Footprint = img.geometry() 89 | 90 | # dem 91 | DEM_OLI = ee.Image('USGS/SRTMGL1_003').clip(l8Footprint) 92 | 93 | # ozone 94 | DU_OLI = ee.Image(ozone.filterDate(iniDate, endDate).filterBounds(l8Footprint).mean()) 95 | 96 | # Julian Day 97 | imgDate_OLI = ee.Date(img.get('system:time_start')) 98 | FOY_OLI = ee.Date.fromYMD(imgDate_OLI.get('year'), 1, 1) 99 | JD_OLI = imgDate_OLI.difference(FOY_OLI, 'day').int().add(1) 100 | 101 | # Earth-Sun distance 102 | d_OLI = ee.Image.constant(img.get('EARTH_SUN_DISTANCE')) 103 | 104 | # Sun elevation 105 | SunEl_OLI = ee.Image.constant(img.get('SUN_ELEVATION')) 106 | 107 | # Sun azimuth 108 | SunAz_OLI = ee.Image.constant(img.get('SUN_AZIMUTH')) 109 | 110 | # Satellite zenith 111 | SatZe_OLI = ee.Image(0.0) 112 | cosdSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).cos() 113 | sindSatZe_OLI = (SatZe_OLI).multiply(pi.divide(ee.Image(180))).sin() 114 | 115 | # Satellite azimuth 116 | SatAz_OLI = ee.Image(0.0).clip(l8Footprint) 117 | 118 | # Sun zenith 119 | SunZe_OLI = ee.Image(90).subtract(SunEl_OLI) 120 | cosdSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image.constant(180))).cos() # in degrees 121 | sindSunZe_OLI = SunZe_OLI.multiply(pi.divide(ee.Image(180))).sin() # in degrees 122 | 123 | # Relative azimuth 124 | RelAz_OLI = ee.Image(SunAz_OLI) 125 | cosdRelAz_OLI = RelAz_OLI.multiply(pi.divide(ee.Image(180))).cos() 126 | 127 | # Pressure calculation 128 | P_OLI = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM_OLI)).pow(5.25588)).multiply( 129 | 0.01) 130 | Po_OLI = ee.Image(1013.25) 131 | 132 | # Radiometric Calibration # 133 | # define bands to be converted to radiance 134 | bands_OLI = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7'] 135 | 136 | # radiance_mult_bands 137 | rad_mult_OLI = ee.Image(ee.Array([ee.Image(img.get('RADIANCE_MULT_BAND_1')), 138 | ee.Image(img.get('RADIANCE_MULT_BAND_2')), 139 | ee.Image(img.get('RADIANCE_MULT_BAND_3')), 140 | ee.Image(img.get('RADIANCE_MULT_BAND_4')), 141 | ee.Image(img.get('RADIANCE_MULT_BAND_5')), 142 | ee.Image(img.get('RADIANCE_MULT_BAND_6')), 143 | ee.Image(img.get('RADIANCE_MULT_BAND_7'))] 144 | )).toArray(1) 145 | 146 | # radiance add band 147 | rad_add_OLI = ee.Image(ee.Array([ee.Image(img.get('RADIANCE_ADD_BAND_1')), 148 | ee.Image(img.get('RADIANCE_ADD_BAND_2')), 149 | ee.Image(img.get('RADIANCE_ADD_BAND_3')), 150 | ee.Image(img.get('RADIANCE_ADD_BAND_4')), 151 | ee.Image(img.get('RADIANCE_ADD_BAND_5')), 152 | ee.Image(img.get('RADIANCE_ADD_BAND_6')), 153 | ee.Image(img.get('RADIANCE_ADD_BAND_7'))] 154 | )).toArray(1) 155 | 156 | # create an empty image to save new radiance bands to 157 | imgArr_OLI = img.select(bands_OLI).toArray().toArray(1) 158 | Ltoa_OLI = imgArr_OLI.multiply(rad_mult_OLI).add(rad_add_OLI) 159 | 160 | # esun 161 | ESUN_OLI = ee.Image.constant(197.24790954589844)\ 162 | .addBands(ee.Image.constant(201.98426818847656))\ 163 | .addBands(ee.Image.constant(186.12677001953125))\ 164 | .addBands(ee.Image.constant(156.95257568359375))\ 165 | .addBands(ee.Image.constant(96.04714965820312))\ 166 | .addBands(ee.Image.constant(23.8833221450863))\ 167 | .addBands(ee.Image.constant(8.04995873449635)).toArray().toArray(1) 168 | ESUN_OLI = ESUN_OLI.multiply(ee.Image(1)) 169 | 170 | ESUNImg_OLI = ESUN_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 171 | 172 | # Ozone Correction # 173 | # Ozone coefficients 174 | koz_OLI = ee.Image.constant(0.0039).addBands(ee.Image.constant(0.0218))\ 175 | .addBands(ee.Image.constant(0.1078))\ 176 | .addBands(ee.Image.constant(0.0608))\ 177 | .addBands(ee.Image.constant(0.0019))\ 178 | .addBands(ee.Image.constant(0))\ 179 | .addBands(ee.Image.constant(0))\ 180 | .toArray().toArray(1) 181 | 182 | # Calculate ozone optical thickness 183 | Toz_OLI = koz_OLI.multiply(DU_OLI).divide(ee.Image.constant(1000)) 184 | 185 | # Calculate TOA radiance in the absense of ozone 186 | Lt_OLI = Ltoa_OLI.multiply(((Toz_OLI)).multiply( 187 | (ee.Image.constant(1).divide(cosdSunZe_OLI)).add(ee.Image.constant(1).divide(cosdSatZe_OLI))).exp()) 188 | 189 | # Rayleigh optical thickness 190 | bandCenter_OLI = ee.Image(443).divide(1000).addBands(ee.Image(483).divide(1000))\ 191 | .addBands(ee.Image(561).divide(1000))\ 192 | .addBands(ee.Image(655).divide(1000))\ 193 | .addBands(ee.Image(865).divide(1000))\ 194 | .addBands(ee.Image(1609).divide(1000))\ 195 | .addBands(ee.Number(2201).divide(1000))\ 196 | .toArray().toArray(1) 197 | 198 | # create an empty image to save new Tr values to 199 | Tr_OLI = (P_OLI.divide(Po_OLI)).multiply(ee.Image(0.008569).multiply(bandCenter_OLI.pow(-4))).multiply((ee.Image(1).add( 200 | ee.Image(0.0113).multiply(bandCenter_OLI.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter_OLI.pow(-4))))) 201 | 202 | # Fresnel Reflection # 203 | # Specular reflection (s- and p- polarization states) 204 | theta_V_OLI = ee.Image(0.0000000001) 205 | sin_theta_j_OLI = sindSunZe_OLI.divide(ee.Image(1.333)) 206 | 207 | theta_j_OLI = sin_theta_j_OLI.asin().multiply(ee.Image(180).divide(pi)) 208 | 209 | theta_SZ_OLI = SunZe_OLI 210 | 211 | R_theta_SZ_s_OLI = (((theta_SZ_OLI.multiply(pi.divide(ee.Image(180)))).subtract( 212 | theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2)).divide((((theta_SZ_OLI.multiply( 213 | pi.divide(ee.Image(180)))).add(theta_j_OLI.multiply(pi.divide(ee.Image(180))))).sin().pow(2))) 214 | 215 | R_theta_V_s_OLI = ee.Image(0.0000000001) 216 | 217 | R_theta_SZ_p_OLI = ( 218 | ((theta_SZ_OLI.multiply(pi.divide(180))).subtract(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2)).divide( 219 | (((theta_SZ_OLI.multiply(pi.divide(180))).add(theta_j_OLI.multiply(pi.divide(180)))).tan().pow(2))) 220 | 221 | R_theta_V_p_OLI = ee.Image(0.0000000001) 222 | 223 | R_theta_SZ_OLI = ee.Image(0.5).multiply(R_theta_SZ_s_OLI.add(R_theta_SZ_p_OLI)) 224 | 225 | R_theta_V_OLI = ee.Image(0.5).multiply(R_theta_V_s_OLI.add(R_theta_V_p_OLI)) 226 | 227 | # Rayleigh scattering phase function # 228 | # Sun-sensor geometry 229 | theta_neg_OLI = ((cosdSunZe_OLI.multiply(ee.Image(-1))).multiply(cosdSatZe_OLI)).subtract( 230 | (sindSunZe_OLI).multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)) 231 | 232 | theta_neg_inv_OLI = theta_neg_OLI.acos().multiply(ee.Image(180).divide(pi)) 233 | 234 | theta_pos_OLI = (cosdSunZe_OLI.multiply(cosdSatZe_OLI)).subtract( 235 | sindSunZe_OLI.multiply(sindSatZe_OLI).multiply(cosdRelAz_OLI)) 236 | 237 | theta_pos_inv_OLI = theta_pos_OLI.acos().multiply(ee.Image(180).divide(pi)) 238 | 239 | cosd_tni_OLI = theta_neg_inv_OLI.multiply(pi.divide(180)).cos() # in degrees 240 | 241 | cosd_tpi_OLI = theta_pos_inv_OLI.multiply(pi.divide(180)).cos() # in degrees 242 | 243 | Pr_neg_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni_OLI.pow(2)))) 244 | 245 | Pr_pos_OLI = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi_OLI.pow(2)))) 246 | 247 | # Rayleigh scattering phase function 248 | Pr_OLI = Pr_neg_OLI.add((R_theta_SZ_OLI.add(R_theta_V_OLI)).multiply(Pr_pos_OLI)) 249 | 250 | # Calulate Lr, 251 | denom_OLI = ee.Image(4).multiply(pi).multiply(cosdSatZe_OLI) 252 | Lr_OLI = (ESUN_OLI.multiply(Tr_OLI)).multiply(Pr_OLI.divide(denom_OLI)) 253 | 254 | # Rayleigh corrected radiance 255 | Lrc_OLI = (Lt_OLI.divide(ee.Image(10))).subtract(Lr_OLI) 256 | LrcImg_OLI = Lrc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 257 | 258 | # Rayleigh corrected reflectance 259 | prc_OLI = Lrc_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI)) 260 | prcImg_OLI = prc_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 261 | 262 | # Aerosol Correction # 263 | # Bands in nm 264 | bands_nm_OLI = ee.Image(443).addBands(ee.Image(483))\ 265 | .addBands(ee.Image(561))\ 266 | .addBands(ee.Image(655))\ 267 | .addBands(ee.Image(865))\ 268 | .addBands(ee.Image(0))\ 269 | .addBands(ee.Image(0))\ 270 | .toArray().toArray(1) 271 | 272 | # Lam in SWIR bands 273 | Lam_6_OLI = LrcImg_OLI.select('B6') 274 | Lam_7_OLI = LrcImg_OLI.select('B7') 275 | 276 | # Calculate aerosol type 277 | eps_OLI = (((((Lam_7_OLI).divide(ESUNImg_OLI.select('B7'))).log()).subtract( 278 | ((Lam_6_OLI).divide(ESUNImg_OLI.select('B6'))).log())).divide(ee.Image(2201).subtract(ee.Image(1609)))).multiply( 279 | mask) 280 | 281 | # Calculate multiple scattering of aerosols for each band 282 | Lam_OLI = (Lam_7_OLI).multiply(((ESUN_OLI).divide(ESUNImg_OLI.select('B7')))).multiply( 283 | (eps_OLI.multiply(ee.Image(-1))).multiply((bands_nm_OLI.divide(ee.Image(2201)))).exp()) 284 | 285 | # diffuse transmittance 286 | trans_OLI = Tr_OLI.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe_OLI)).exp() 287 | 288 | # Compute water-leaving radiance 289 | Lw_OLI = Lrc_OLI.subtract(Lam_OLI).divide(trans_OLI) 290 | 291 | # water-leaving reflectance 292 | pw_OLI = (Lw_OLI.multiply(pi).multiply(d_OLI.pow(2)).divide(ESUN_OLI.multiply(cosdSunZe_OLI))) 293 | pwImg_OLI = pw_OLI.arrayProject([0]).arrayFlatten([bands_OLI]) 294 | 295 | # Rrs 296 | Rrs_coll = (pw_OLI.divide(pi).arrayProject([0]).arrayFlatten([bands_OLI]).slice(0, 5)) 297 | 298 | return (Rrs_coll.set('system:time_start', img.get('system:time_start'))) 299 | 300 | 301 | def LST(img): 302 | 303 | TIRS_1 = img.select('B10') 304 | 305 | b10_add_band = ee.Number(img.get('RADIANCE_ADD_BAND_10')) 306 | b10_mult_band = ee.Number(img.get('RADIANCE_MULT_BAND_10')) 307 | 308 | Oi = ee.Number(0.29) 309 | 310 | TIRS_cal = (TIRS_1.multiply(b10_mult_band).add(b10_add_band).subtract(Oi)) # .multiply(lakes) 311 | 312 | K1_b10 = ee.Number(TIRS_1.get('K1_CONSTANT_BAND_10')) 313 | K2_b10 = ee.Number(TIRS_1.get('K2_CONSTANT_BAND_10')) 314 | 315 | lst_coll = ((ee.Image(K2_b10).divide(((ee.Image(K1_b10).divide(ee.Image(TIRS_cal))).add(ee.Image(1))).log())).subtract( 316 | ee.Image(273))).multiply(mask) # Celsius 317 | lst_coll = (ee.Image(0.7745).multiply(lst_coll)).add(ee.Image(9.6502)) 318 | 319 | # Define a square kernel with radius 1.5 320 | sq_kernel = ee.Kernel.square(1.5, 'pixels') 321 | 322 | # focal convolution 323 | lst_coll = lst_coll.focal_median(kernel=sq_kernel,iterations=ee.Number(1)) 324 | # lst_coll = lst_coll.focal_median({'kernel': sq_kernel, 'iterations': 1}) 325 | 326 | return (lst_coll.set('system:time_start', img.get('system:time_start'))) 327 | 328 | def secchi(img): 329 | 330 | blueRed_coll = (img.select('B2').divide(img.select('B4'))).log() 331 | lnMOSD_coll = (ee.Image(1.4856).multiply(blueRed_coll)).add(ee.Image(0.2734)) # R2 = 0.8748 with Anthony's in-situ data 332 | MOSD_coll = ee.Image(10).pow(lnMOSD_coll) 333 | sd_coll = (ee.Image(0.1777).multiply(MOSD_coll)).add(ee.Image(1.0813)) 334 | return (sd_coll.updateMask(sd_coll.lt(10)).set('system:time_start', img.get('system:time_start'))) 335 | 336 | def trophicState(img): 337 | 338 | tsi_coll = ee.Image(60).subtract(ee.Image(14.41).multiply(img.log())) 339 | return (tsi_coll.updateMask(tsi_coll.lt(200)).set('system:time_start', img.get('system:time_start'))) 340 | 341 | 342 | def reclassify(img): 343 | 344 | # Create conditions 345 | mask1 = img.lt(30) # (1) 346 | mask2 = img.gte(30).And(img.lt(40)) # (2) 347 | mask3 = img.gte(40).And(img.lt(50)) # (3) 348 | mask4 = img.gte(50).And(img.lt(60)) # (4) 349 | mask5 = img.gte(60).And(img.lt(70)) # (5) 350 | mask6 = img.gte(70).And(img.lt(80)) # (6) 351 | mask7 = img.gte(70) # (7) 352 | 353 | # Reclassify conditions into new values 354 | img1 = img.where(mask1.eq(1), 1).mask(mask1) 355 | img2 = img.where(mask2.eq(1), 2).mask(mask2) 356 | img3 = img.where(mask3.eq(1), 3).mask(mask3) 357 | img4 = img.where(mask4.eq(1), 4).mask(mask4) 358 | img5 = img.where(mask5.eq(1), 5).mask(mask5) 359 | img6 = img.where(mask6.eq(1), 6).mask(mask6) 360 | img7 = img.where(mask7.eq(1), 7).mask(mask7) 361 | 362 | # Ouput of reclassified image 363 | tsi_collR = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7) 364 | return (tsi_collR.updateMask(tsi_collR.set('system:time_start', img.get('system:time_start')))) 365 | 366 | #########################################################/ 367 | #########################################################/ 368 | #########################################################/ 369 | # Processing Collection # 370 | Rrs_coll = FC_OLI.map(l8Correction) 371 | 372 | sd_coll = Rrs_coll.map(secchi) 373 | 374 | tsi_coll = sd_coll.map(trophicState) 375 | 376 | tsi_collR = tsi_coll.map(reclassify) 377 | 378 | lst_coll = FC_OLI.map(LST) 379 | 380 | ########################################################################################## 381 | ########################################################################################## 382 | ########################################################################################## 383 | 384 | 385 | ########################################################################################## 386 | ########################################################################################## 387 | ########################################################################################## 388 | # Map Layers # 389 | 390 | # l8 391 | 392 | Rrs_coll_mean = Rrs_coll.mean() 393 | l8_rsr_vis = {'bands': 'B4,B3,B1', 'min': 0, 'max': 0.04} 394 | l8_mapid = Rrs_coll_mean.getMapId(l8_rsr_vis) 395 | 396 | lst_coll_mean = lst_coll.mean() 397 | lst_vis = {'min': 23, 'max': 27, 'palette': 'darkblue,blue,white,red,darkred'} 398 | lst_mapid = lst_coll_mean.getMapId(lst_vis) 399 | 400 | tsi_coll_mean = tsi_coll.mean() 401 | tsi_vis = {'min': 30, 'max': 70, 'palette': 'blue,cyan,limegreen,yellow,darkred'} 402 | tsi_mapid = tsi_coll_mean.getMapId(tsi_vis) 403 | 404 | tsi_collR_mean = tsi_collR.mean() 405 | tsir_vis = {'min': 1, 'max': 7, 'palette': 'purple,blue,limegreen,yellow,orange,orangered,darkred'} 406 | tsir_mapid = tsi_collR_mean.getMapId(tsir_vis) 407 | 408 | print 'L8',l8_mapid 409 | print 'LST',lst_mapid 410 | print 'TSI',tsi_mapid 411 | print 'TSI R',tsir_mapid 412 | 413 | 414 | # 415 | # Map.addLayer(Rrs_coll.mean(), {bands: ['B4', 'B3', 'B1'], min: 0, max: 0.04}, 'l8 Rrs RGB', false) 416 | # Map.addLayer(lst_coll.mean(), {min: 23, max: 27, palette: ['darkblue', 'blue', 'white', 'red', 'darkred']}, 'LST [c]', 417 | # false) 418 | # Map.addLayer(sd_coll.mean(), {min: 0, max: 3, palette: ['800000', 'FF9700', '7BFF7B', '0080FF', '000080']}, 'Zsd [m]', 419 | # false) 420 | # Map.addLayer(tsi_coll.mean(), {min: 30, max: 70, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'darkred']}, 421 | # 'TSI [0-100]', false) 422 | # Map.addLayer(tsi_collR.mode(), 423 | # {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 424 | # 'TSI Reclass', true) 425 | 426 | #########################################################/ 427 | #########################################################/ 428 | #########################################################/ 429 | # Time Series # 430 | # def makeTimeSeries(collection,geometry,key=None): 431 | # 432 | # def reducerMapping(img): 433 | # reduction = img.reduceRegion( 434 | # ee.Reducer.mean(), geometry, 30) 435 | # 436 | # time = img.get('system:time_start') 437 | # 438 | # return img.set('indexVal',[ee.Number(time),reduction.get(key)]) 439 | # 440 | # collection = collection.filterBounds(geometry) #.getInfo() 441 | # 442 | # indexCollection = collection.map(reducerMapping) 443 | # 444 | # indexSeries = indexCollection.aggregate_array('indexVal').getInfo() 445 | # 446 | # formattedSeries = [[x[0],round(float(x[1]),3)] for x in indexSeries] 447 | # 448 | # days_with_data = [[datetime.datetime.fromtimestamp((int(x[0]) / 1000)).strftime('%Y %B %d'),round(float(x[1]),3)] for x in indexSeries if x[1] > 0 ] 449 | # 450 | # return sorted(formattedSeries) 451 | # # LST time series 452 | # print lst_coll 453 | # lstTimeSeries = makeTimeSeries(lst_coll,geometry,key='radiance') 454 | # 455 | # print lstTimeSeries 456 | # lstTimeSeries = ui.Chart.image.seriesByRegion( 457 | # lst_coll, geometry, ee.Reducer.mean()) 458 | # .setChartType('ScatterChart') 459 | # .setOptions({ 460 | # title: 'Mean LST', 461 | # vAxis: {title: 'LST [c]'}, 462 | # lineWidth: 1, 463 | # pointSize: 4, 464 | # }) 465 | # 466 | # # SD time series 467 | # sdTimeSeries = ui.Chart.image.seriesByRegion( 468 | # sd_coll, geometry, ee.Reducer.mean()) 469 | # .setChartType('ScatterChart') 470 | # .setOptions({ 471 | # title: 'Mean Secchi Depth', 472 | # vAxis: {title: 'Zsd [m]'}, 473 | # lineWidth: 1, 474 | # pointSize: 4, 475 | # }) 476 | # 477 | # # TSI time series 478 | # tsiTimeSeries = ui.Chart.image.seriesByRegion( 479 | # tsi_coll, geometry, ee.Reducer.mean()) 480 | # .setChartType('ScatterChart') 481 | # .setOptions({ 482 | # title: 'Mean Trophic State Index', 483 | # vAxis: {title: 'TSI [1-100]'}, 484 | # lineWidth: 1, 485 | # pointSize: 4, 486 | # }) 487 | # 488 | # # TSI Reclass time series 489 | # tsiRTimeSeries = ui.Chart.image.seriesByRegion( 490 | # tsi_collR, geometry, ee.Reducer.mean()) 491 | # .setChartType('ScatterChart') 492 | # .setOptions({ 493 | # title: 'Mode Trophic State Index Class', 494 | # vAxis: {title: 'TSI Class'}, 495 | # lineWidth: 1, 496 | # pointSize: 4, 497 | # }) 498 | # 499 | # print(lstTimeSeries) 500 | # print(sdTimeSeries) 501 | # print(tsiTimeSeries) 502 | # print(tsiRTimeSeries) -------------------------------------------------------------------------------- /python/modis_wq_timeseries.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from ee.ee_exception import EEException 3 | import datetime 4 | 5 | ####Still under development######### 6 | try: 7 | ee.Initialize() 8 | except EEException as e: 9 | from oauth2client.service_account import ServiceAccountCredentials 10 | credentials = ServiceAccountCredentials.from_p12_keyfile( 11 | service_account_email='', 12 | filename='', 13 | private_key_password='notasecret', 14 | scopes=ee.oauth.SCOPE + ' https://www.googleapis.com/auth/drive ') 15 | ee.Initialize(credentials) 16 | 17 | geometry = ee.Geometry.Polygon([[[78.41079711914062, 17.465297700926097], 18 | [78.41629028320312, 17.334252606951086], 19 | [78.59893798828125, 17.33490806580805], 20 | [78.55087280273438, 17.494769882318828]]]) 21 | 22 | # This script processes and charts WQ parameter values from a collection of MODIS L3 OC archive for a particular pixel throughout time. 23 | # WQ parameters such as chlorophyll-a, secchi depth and the trophic state index 24 | # How to use: 25 | # (1) If a "geometry" iable exists in the imports window, delete it. 26 | # (2) Within the map, select the polygon button near the top left side. 27 | # (3) Create a new polygon by drawing around the area of interest 28 | # (4) Adjust the time frame you wish to browse by adding here: 29 | # begin date 30 | startDate = '2016-04-01' 31 | # end date 32 | endDate = '2016-10-31' 33 | # (5) Click Run 34 | # (5) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 35 | # image from this collection, highlight the FILE_ID and copy and paste it into the proper location within the "Ls8 Single Image" script. 36 | # (6) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 37 | # (6) Click "Run" 38 | # (7) Export each chart as a csv by clicking on the export icon near the top-right of the chart. 39 | # (8) If you are interested in a single image from the collection, copy and paste the FILE_ID here: 40 | singleImage = ee.Image('NASA/OCEANDATA/MODIS-Aqua/L3SMI/A2017183').clip(geometry) 41 | 42 | # Author: Benjamin Page # 43 | ###################################################################/ 44 | # Import Colelction 45 | MODIS = ee.ImageCollection('NASA/OCEANDATA/MODIS-Aqua/L3SMI') 46 | 47 | # filter collection 48 | FC = MODIS.filterDate(startDate, endDate).filterBounds(geometry) 49 | 50 | 51 | ###################################################################/ 52 | ### Map Functions ### 53 | 54 | def secchi(img): 55 | Rrs_488 = img.select('Rrs_488') 56 | Rrs_667 = img.select('Rrs_667') 57 | ln_blueRed = (Rrs_488.divide(Rrs_667)).log() 58 | lnMOSD = (ee.Image(1.4856).multiply(ln_blueRed)).add(ee.Image(0.2734)) # R2 = 0.8748 with Anthony's in-situ data 59 | MOSD = ee.Image(10).pow(lnMOSD) 60 | 61 | SD = (ee.Image(0.1777).multiply(MOSD)).add(ee.Image(1.0813)) 62 | 63 | return (SD.set('system:time_start', img.get('system:time_start'))) 64 | 65 | 66 | def trophicState(img): 67 | TSI = ee.Image(60).subtract((ee.Image(14.41)).multiply((img.log()))) 68 | return (TSI.set('system:time_start', img.get('system:time_start'))) 69 | 70 | ### Models ###/ 71 | 72 | # chlor_a 73 | chlor_a = FC.select('chlor_a') 74 | 75 | # SD 76 | SD = ee.ImageCollection(FC.map(secchi).copyProperties(FC, ['system:time_start'])) 77 | 78 | # TSI 79 | TSI = SD.map(trophicState) 80 | 81 | 82 | 83 | 84 | 85 | ###################################################################/ 86 | # Time Series 87 | 88 | # chlorTimeSeries = ui.Chart.image.seriesByRegion( 89 | # chlor_a, geometry, ee.Reducer.mean()) 90 | # .setChartType('ScatterChart') 91 | # .setOptions({ 92 | # title: 'Mean chlor_a', 93 | # vAxis: {title: 'chlor_a (ug / L)'}, 94 | # lineWidth: 1, 95 | # pointSize: 4, 96 | # }) 97 | # 98 | # sdTimeSeries = ui.Chart.image.seriesByRegion( 99 | # SD, geometry, ee.Reducer.mean()) 100 | # .setChartType('ScatterChart') 101 | # .setOptions({ 102 | # title: 'Mean Secchi Depth', 103 | # vAxis: {title: 'Zsd [m]'}, 104 | # lineWidth: 1, 105 | # pointSize: 4, 106 | # }) 107 | # 108 | # tsiTimeSeries = ui.Chart.image.seriesByRegion( 109 | # TSI, geometry, ee.Reducer.mean()) 110 | # .setChartType('ScatterChart') 111 | # .setOptions({ 112 | # title: 'Mean TSI', 113 | # vAxis: {title: 'TSI value)'}, 114 | # lineWidth: 1, 115 | # pointSize: 4, 116 | # }) 117 | # 118 | # print(chlorTimeSeries) 119 | # print(sdTimeSeries) 120 | # print(tsiTimeSeries) 121 | 122 | ###################################################################/ 123 | 124 | # Map layers # 125 | 126 | chlor_a_mean = chlor_a.mean() 127 | # chlor_a_mean = chlor_a_mean.clip(geometry) 128 | chlor_a_vis = {'min': 0, 'max': 100, 'palette': 'darkblue,blue,limegreen,yellow,orange,orangered,darkred'} 129 | chlor_a_mapid = chlor_a_mean.getMapId(chlor_a_vis) 130 | 131 | SD_mean = SD.mean() 132 | # SD_mean = SD_mean.clip(geometry) 133 | SD_vis = {'min': 0, 'max': 2, 'palette': 'red,orangered,orange,yellow,limegreen,blue,darkblue'} 134 | SD_mapid = SD_mean.getMapId(SD_vis) 135 | 136 | TSI_mean = TSI.mean() 137 | # TDI_mean = TSI_mean.clip(geometry) 138 | TSI_vis = {'min': 0, 'max': 2, 'palette': 'red,orangered,orange,yellow,limegreen,blue,darkblue'} 139 | TSI_mapid = TSI_mean.getMapId(TSI_vis) 140 | 141 | singleImageLyr = singleImage.select('chlor_a') 142 | singleImage_vis = {'min': 0, 'max': 100, 'palette': 'darkblue,blue,limegreen,yellow,orange,orangered,darkred'} 143 | singleImage_mapid = singleImageLyr.getMapId(singleImage_vis) 144 | 145 | print "CHLOR A",chlor_a_mapid 146 | print "SD",SD_mapid 147 | print "TSI",TSI_mapid 148 | print "Single", singleImage_mapid 149 | # Map.addLayer(chlor_a.mean().clip(geometry), 150 | # {min: 0, max: 100, palette: ['darkblue', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 151 | # 'chlor_a', false) 152 | # Map.addLayer(SD.mean().clip(geometry), 153 | # {min: 0, max: 2, palette: ['red', 'orangered', 'orange', 'yellow', 'limegreen', 'blue', 'darkblue']}, 'SD', 154 | # false) 155 | # Map.addLayer(TSI.mean().clip(geometry), 156 | # {min: 30, max: 80, palette: ['darkblue', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'red']}, 157 | # 'TSI', true) 158 | # Map.addLayer(singleImage.select('chlor_a'), 159 | # {min: 0, max: 100, palette: ['darkblue', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 160 | # 'singleImage_chlor_a', false) 161 | 162 | ################################################################/ 163 | # Exporting Images # 164 | 165 | # # Export mean chlor_a 166 | # Export.image.toDrive({ 167 | # image: chlor_a.mean().clip(geometry), 168 | # description: 'Mean Chlorophyll-a', 169 | # scale: 250, 170 | # region: geometry 171 | # }) 172 | # 173 | # # Export mean SD 174 | # Export.image.toDrive({ 175 | # image: SD.mean().clip(geometry), 176 | # description: 'Mean Secchi Depth', 177 | # scale: 250, 178 | # region: geometry 179 | # }) 180 | # 181 | # # Export mean TSI 182 | # Export.image.toDrive({ 183 | # image: TSI.mean().clip(geometry), 184 | # description: 'Mean Trophic State (from index)', 185 | # scale: 250, 186 | # region: geometry 187 | # }) -------------------------------------------------------------------------------- /python/s2_fai.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from ee.ee_exception import EEException 3 | import datetime 4 | 5 | try: 6 | ee.Initialize() 7 | except EEException as e: 8 | from oauth2client.service_account import ServiceAccountCredentials 9 | credentials = ServiceAccountCredentials.from_p12_keyfile( 10 | service_account_email='', 11 | filename='', 12 | private_key_password='notasecret', 13 | scopes=ee.oauth.SCOPE + ' https://www.googleapis.com/auth/drive ') 14 | ee.Initialize(credentials) 15 | 16 | # geometry = ee.Geometry.Polygon([[[78.41079711914062, 17.465297700926097], 17 | # [78.41629028320312, 17.334252606951086], 18 | # [78.59893798828125, 17.33490806580805], 19 | # [78.55087280273438, 17.494769882318828]]]) 20 | 21 | geometry = ee.Geometry.Polygon([[[30.76171875, 0.9049611504960419], 22 | [30.8935546875, -3.487377195492663], 23 | [35.5517578125, -3.2680324702882952], 24 | [35.5517578125, 1.9593043032313748]]]) 25 | 26 | # This script processes a single Sentinel-2 TOA to Rayleigh corrected reflectances. 27 | # The floating algal index (FAI) is calculated from Rrc over water bodies. 28 | # How to use: 29 | # (1) If a "geometry" iable exists in the imports window, delete it. 30 | # (2) Within the map, select the point button near the top left side. 31 | # (3) Create a new point by clicking on a location of interest and click run. 32 | # (4) The "available imagery" ImageCollection within the console displays all available imagery 33 | # over that location with the necessary filters described by the user below. 34 | # (5) Expand the features within the "available imagery" image collection and select an image 35 | # by highlighting the FILE_ID and pasting it here: 36 | s2 = ee.Image('COPERNICUS/S2/20180216T075011_20180216T080852_T36MXE') 37 | # (6) Click "Run" 38 | # (7) Export the image to your Google Drive by clicking on the "tasks" tab and clicking "RUN", be sure to specify 39 | # the proper folder. 40 | 41 | # Author: Benjamin Page # 42 | # Citations: 43 | # Page, B.P., Kumar, A. and Mishra, D.R., 2018. A novel cross-satellite based assessment of the spatio-temporal development of a cyanobacterial harmful algal bloom. International Journal of Applied Earth Observation and Geoinformation, 66, pp.69-81. 44 | 45 | #########################################################/ 46 | #########################################################/ 47 | #########################################################/ 48 | # User Input # 49 | 50 | # begin date 51 | iniDate = '2015-01-01' 52 | 53 | # end date 54 | endDate = '2018-02-28' 55 | 56 | cloudPerc = 5 57 | 58 | #########################################################/ 59 | #########################################################/ 60 | #########################################################/ 61 | # Import Collections # 62 | 63 | # sentinel-2 64 | MSI = ee.ImageCollection('COPERNICUS/S2') 65 | 66 | # toms / omi 67 | ozone = ee.ImageCollection('TOMS/MERGED') 68 | 69 | SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR') 70 | 71 | #########################################################/ 72 | #########################################################/ 73 | #########################################################/ 74 | # water mask 75 | startMonth = 5 76 | endMonth = 9 77 | startYear = 2013 78 | endYear = 2017 79 | 80 | forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter( 81 | ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')) 82 | mask = ee.Image(forMask.select('B6').median().lt(300)) 83 | mask = mask.updateMask(mask) 84 | 85 | # constants 86 | pi = ee.Image(3.141592) 87 | imDate = s2.date() 88 | 89 | # filter sentinel 2 collection 90 | FC = MSI.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUDY_PIXEL_PERCENTAGE', "less_than", 91 | cloudPerc) 92 | 93 | 94 | #########################################################/ 95 | #########################################################/ 96 | #########################################################/ 97 | # MSI Atmospheric Correction # 98 | 99 | bands = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B11', 'B12'] 100 | 101 | # rescale 102 | rescale = ee.Image(s2.divide(10000).copyProperties(s2)).select(bands) 103 | 104 | # tile footprint 105 | footprint = s2.geometry() 106 | 107 | # dem 108 | DEM = ee.Image('USGS/SRTMGL1_003').clip(footprint) 109 | 110 | # ozone 111 | DU = ee.Image(ozone.filterDate(iniDate, endDate).filterBounds(geometry).mean()) 112 | 113 | # Julian Day 114 | imgDate = ee.Date(s2.get('system:time_start')) 115 | FOY = ee.Date.fromYMD(imgDate.get('year'), 1, 1) 116 | JD = imgDate.difference(FOY, 'day').int().add(1) 117 | 118 | # earth-sun distance 119 | myCos = ((ee.Image(0.0172).multiply(ee.Image(JD).subtract(ee.Image(2)))).cos()).pow(2) 120 | cosd = myCos.multiply(pi.divide(ee.Image(180))).cos() 121 | d = ee.Image(1).subtract(ee.Image(0.01673)).multiply(cosd).clip(footprint) 122 | 123 | # sun azimuth 124 | SunAz = ee.Image.constant(s2.get('MEAN_SOLAR_AZIMUTH_ANGLE')).clip(footprint) 125 | 126 | # sun zenith 127 | SunZe = ee.Image.constant(s2.get('MEAN_SOLAR_ZENITH_ANGLE')).clip(footprint) 128 | cosdSunZe = SunZe.multiply(pi.divide(ee.Image(180))).cos() # in degrees 129 | sindSunZe = SunZe.multiply(pi.divide(ee.Image(180))).sin() # in degrees 130 | 131 | # sat zenith 132 | SatZe = ee.Image.constant(s2.get('MEAN_INCIDENCE_ZENITH_ANGLE_B5')).clip(footprint) 133 | cosdSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).cos() 134 | sindSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).sin() 135 | 136 | # sat azimuth 137 | SatAz = ee.Image.constant(s2.get('MEAN_INCIDENCE_AZIMUTH_ANGLE_B5')).clip(footprint) 138 | 139 | # relative azimuth 140 | RelAz = SatAz.subtract(SunAz) 141 | cosdRelAz = RelAz.multiply(pi.divide(ee.Image(180))).cos() 142 | 143 | # Pressure 144 | P = ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM)).pow(5.25588)).multiply(0.01) 145 | Po = ee.Image(1013.25) 146 | 147 | # esun 148 | ESUN = ee.Image(ee.Array([ee.Image(s2.get('SOLAR_IRRADIANCE_B1')), 149 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2')), 150 | ee.Image(s2.get('SOLAR_IRRADIANCE_B3')), 151 | ee.Image(s2.get('SOLAR_IRRADIANCE_B4')), 152 | ee.Image(s2.get('SOLAR_IRRADIANCE_B5')), 153 | ee.Image(s2.get('SOLAR_IRRADIANCE_B6')), 154 | ee.Image(s2.get('SOLAR_IRRADIANCE_B7')), 155 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8')), 156 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8A')), 157 | ee.Image(s2.get('SOLAR_IRRADIANCE_B11')), 158 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2'))] 159 | )).toArray().toArray(1) 160 | 161 | ESUN = ESUN.multiply(ee.Image(1)) 162 | 163 | ESUNImg = ESUN.arrayProject([0]).arrayFlatten([bands]) 164 | 165 | # create empty array for the images 166 | imgArr = rescale.select(bands).toArray().toArray(1) 167 | 168 | # pTOA to Ltoa 169 | Ltoa = imgArr.multiply(ESUN).multiply(cosdSunZe).divide(pi.multiply(d.pow(2))) 170 | 171 | # band centers 172 | bandCenter = ee.Image(443).divide(1000).addBands(ee.Image(490).divide(1000))\ 173 | .addBands(ee.Image(560).divide(1000))\ 174 | .addBands(ee.Image(665).divide(1000))\ 175 | .addBands(ee.Image(705).divide(1000))\ 176 | .addBands(ee.Image(740).divide(1000))\ 177 | .addBands(ee.Number(783).divide(1000))\ 178 | .addBands(ee.Number(842).divide(1000))\ 179 | .addBands(ee.Number(865).divide(1000))\ 180 | .addBands(ee.Number(1610).divide(1000))\ 181 | .addBands(ee.Number(2190).divide(1000))\ 182 | .toArray().toArray(1) 183 | 184 | # ozone coefficients 185 | koz = ee.Image(0.0039).addBands(ee.Image(0.0213))\ 186 | .addBands(ee.Image(0.1052))\ 187 | .addBands(ee.Image(0.0505))\ 188 | .addBands(ee.Image(0.0205))\ 189 | .addBands(ee.Image(0.0112))\ 190 | .addBands(ee.Image(0.0075))\ 191 | .addBands(ee.Image(0.0021))\ 192 | .addBands(ee.Image(0.0019))\ 193 | .addBands(ee.Image(0))\ 194 | .addBands(ee.Image(0))\ 195 | .toArray().toArray(1) 196 | 197 | ################### 198 | 199 | # Calculate ozone optical thickness 200 | Toz = koz.multiply(DU).divide(ee.Image(1000)) 201 | 202 | # Calculate TOA radiance in the absense of ozone 203 | Lt = Ltoa.multiply(((Toz)).multiply((ee.Image(1).divide(cosdSunZe)).add(ee.Image(1).divide(cosdSatZe))).exp()) 204 | 205 | # Rayleigh optical thickness 206 | Tr = (P.divide(Po)).multiply(ee.Image(0.008569).multiply(bandCenter.pow(-4))).multiply((ee.Image(1).add( 207 | ee.Image(0.0113).multiply(bandCenter.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter.pow(-4))))) 208 | 209 | # Specular reflection (s- and p- polarization states) 210 | theta_V = ee.Image(0.0000000001) 211 | sin_theta_j = sindSunZe.divide(ee.Image(1.333)) 212 | 213 | theta_j = sin_theta_j.asin().multiply(ee.Image(180).divide(pi)) 214 | 215 | theta_SZ = SunZe 216 | 217 | R_theta_SZ_s = ( 218 | ((theta_SZ.multiply(pi.divide(ee.Image(180)))).subtract(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow( 219 | 2)).divide( 220 | (((theta_SZ.multiply(pi.divide(ee.Image(180)))).add(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2))) 221 | 222 | R_theta_V_s = ee.Image(0.0000000001) 223 | 224 | R_theta_SZ_p = (((theta_SZ.multiply(pi.divide(180))).subtract(theta_j.multiply(pi.divide(180)))).tan().pow(2)).divide( 225 | (((theta_SZ.multiply(pi.divide(180))).add(theta_j.multiply(pi.divide(180)))).tan().pow(2))) 226 | 227 | R_theta_V_p = ee.Image(0.0000000001) 228 | 229 | R_theta_SZ = ee.Image(0.5).multiply(R_theta_SZ_s.add(R_theta_SZ_p)) 230 | 231 | R_theta_V = ee.Image(0.5).multiply(R_theta_V_s.add(R_theta_V_p)) 232 | 233 | # Sun-sensor geometry 234 | theta_neg = ((cosdSunZe.multiply(ee.Image(-1))).multiply(cosdSatZe)).subtract( 235 | (sindSunZe).multiply(sindSatZe).multiply(cosdRelAz)) 236 | 237 | theta_neg_inv = theta_neg.acos().multiply(ee.Image(180).divide(pi)) 238 | 239 | theta_pos = (cosdSunZe.multiply(cosdSatZe)).subtract(sindSunZe.multiply(sindSatZe).multiply(cosdRelAz)) 240 | 241 | theta_pos_inv = theta_pos.acos().multiply(ee.Image(180).divide(pi)) 242 | 243 | cosd_tni = theta_neg_inv.multiply(pi.divide(180)).cos() # in degrees 244 | 245 | cosd_tpi = theta_pos_inv.multiply(pi.divide(180)).cos() # in degrees 246 | 247 | Pr_neg = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni.pow(2)))) 248 | 249 | Pr_pos = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi.pow(2)))) 250 | 251 | # Rayleigh scattering phase function 252 | Pr = Pr_neg.add((R_theta_SZ.add(R_theta_V)).multiply(Pr_pos)) # for water Rayleigh correction 253 | # Pr = ee.Image(1) # for terrestrial Rayleigh correction 254 | 255 | # rayleigh radiance contribution 256 | denom = ee.Image(4).multiply(pi).multiply(cosdSatZe) 257 | Lr = (ESUN.multiply(Tr)).multiply(Pr.divide(denom)) 258 | 259 | # rayleigh corrected radiance 260 | Lrc = Lt.subtract(Lr) 261 | LrcImg = Lrc.arrayProject([0]).arrayFlatten([bands]) 262 | 263 | # rayleigh corrected reflectance 264 | prc = (Lrc.multiply(pi).multiply(d.pow(2)).divide(ESUN.multiply(cosdSunZe))) 265 | prcImg = prc.arrayProject([0]).arrayFlatten([bands]) 266 | 267 | 268 | ######################## 269 | # models # 270 | 271 | # Calculate FAI 272 | NIRprime = (prcImg.select('B4')).add((prcImg.select('B11').subtract(prcImg.select('B4'))).multiply( 273 | (ee.Image(865).subtract(ee.Image(665))).divide((ee.Image(1610).subtract(ee.Image(665)))))) 274 | FAI = ((prcImg.select('B8A').subtract(NIRprime))).multiply(mask) 275 | 276 | prcLyr = prcImg.multiply(mask) 277 | prc_vis = {'bands': 'B4,B3,B2', 'min': 0, 'max': 0.4} 278 | prc_mapid = prcLyr.getMapId(prc_vis) 279 | 280 | FAI_vis = {'min': -0.05, 'max': 0.2, 'palette': '#000080,#0080FF,#7BFF7B,#FF9700,#800000'} 281 | FAI_mapid = FAI.getMapId(FAI_vis) 282 | 283 | print 'PRC',prc_mapid 284 | print 'FAI',FAI_mapid 285 | # Map Layers 286 | # Map.addLayer(footprint, {}, 'footprint', true) 287 | # Map.addLayer(prcImg.multiply(mask), {bands: ['B4', 'B3', 'B2'], min: 0, max: 0.4}, 'prc rgb', false) 288 | # Map.addLayer(FAI, {min: -0.05, max: 0.2, palette: ['000080', '0080FF', '7BFF7B', 'FF9700', '800000']}, 'FAI', true) 289 | # 290 | # # export image # 291 | # 292 | # Export.image.toDrive({ 293 | # image: FAI, 294 | # description: 's2_FAI', 295 | # scale: 30, 296 | # region: footprint 297 | # }) 298 | 299 | -------------------------------------------------------------------------------- /python/s2_single_image.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from ee.ee_exception import EEException 3 | import datetime 4 | 5 | try: 6 | ee.Initialize() 7 | except EEException as e: 8 | from oauth2client.service_account import ServiceAccountCredentials 9 | credentials = ServiceAccountCredentials.from_p12_keyfile( 10 | service_account_email='', 11 | filename='', 12 | private_key_password='notasecret', 13 | scopes=ee.oauth.SCOPE + ' https://www.googleapis.com/auth/drive ') 14 | ee.Initialize(credentials) 15 | 16 | # geometry = ee.Geometry.Polygon([[[78.41079711914062, 17.465297700926097], 17 | # [78.41629028320312, 17.334252606951086], 18 | # [78.59893798828125, 17.33490806580805], 19 | # [78.55087280273438, 17.494769882318828]]]) 20 | 21 | geometry = ee.Geometry.Polygon([[[30.76171875, 0.9049611504960419], 22 | [30.8935546875, -3.487377195492663], 23 | [35.5517578125, -3.2680324702882952], 24 | [35.5517578125, 1.9593043032313748]]]) 25 | 26 | # This script processes a single Sentinel-2 image of interest. 27 | # WQ parameters such chlor-a, secchi depth, trophic state index are calculated 28 | # How to use: 29 | # (1) If a "geometry" iable exists in the imports window, delete it. 30 | # (2) Within the map, select the point button near the top left side. 31 | # (3) Create a new point by clicking on a location of interest. 32 | # (4) Adjust the time frame you wish to browse by adding here: 33 | # begin date 34 | iniDate = '2015-05-01' 35 | # end date 36 | endDate = '2018-03-31' 37 | # (5) Adjust a cloud % threshold here: 38 | cloudPerc = 5 39 | # (6) Click Run 40 | # (7) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 41 | # image from this collection, highlight the FILE_ID in the available imagery features and copy and paste it here: 42 | s2 = ee.Image('COPERNICUS/S2/20171128T075251_20171128T080644_T36MXE') 43 | # (8) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 44 | # (9) Click "Run" 45 | # (10) Export each image by clicking on the run button within the "tasks" tab. 46 | 47 | # Author: Benjamin Page # 48 | # Citations: 49 | # Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring 50 | 51 | #########################################################/ 52 | #########################################################/ 53 | #########################################################/ 54 | # Import Collections # 55 | 56 | # sentinel-2 57 | MSI = ee.ImageCollection('COPERNICUS/S2') 58 | 59 | # landsat-8 surface reflactance product (for masking purposes) 60 | SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR') 61 | 62 | # toms / omi 63 | ozone = ee.ImageCollection('TOMS/MERGED') 64 | 65 | #########################################################/ 66 | #########################################################/ 67 | #########################################################/ 68 | 69 | pi = ee.Image(3.141592) 70 | imDate = s2.date() 71 | 72 | footprint = s2.geometry() 73 | 74 | # water mask 75 | startMonth = 5 76 | endMonth = 9 77 | startYear = 2013 78 | endYear = 2017 79 | 80 | forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter( 81 | ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')) 82 | mask = ee.Image(forMask.select('B6').median().lt(300)) 83 | mask = mask.updateMask(mask).clip(footprint) 84 | 85 | # filter sentinel 2 collection 86 | FC = MSI.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUDY_PIXEL_PERCENTAGE', "less_than", 87 | cloudPerc) 88 | 89 | 90 | #########################################################/ 91 | #########################################################/ 92 | #########################################################/ 93 | # MSI Atmospheric Correction # 94 | 95 | bands = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B11', 'B12'] 96 | 97 | # rescale* 98 | rescale = ee.Image(s2.divide(10000).multiply(mask).copyProperties(s2)).select(bands) 99 | 100 | # dem* 101 | DEM = ee.Image('USGS/SRTMGL1_003').clip(footprint) 102 | 103 | # ozone* 104 | DU = ee.Image(ozone.filterDate(iniDate, endDate).filterBounds(footprint).mean()) 105 | 106 | # Julian Day 107 | imgDate = ee.Date(s2.get('system:time_start')) 108 | FOY = ee.Date.fromYMD(imgDate.get('year'), 1, 1) 109 | JD = imgDate.difference(FOY, 'day').int().add(1) 110 | 111 | # earth-sun distance 112 | myCos = ((ee.Image(0.0172).multiply(ee.Image(JD).subtract(ee.Image(2)))).cos()).pow(2) 113 | cosd = myCos.multiply(pi.divide(ee.Image(180))).cos() 114 | d = ee.Image(1).subtract(ee.Image(0.01673)).multiply(cosd).clip(footprint) 115 | 116 | # sun azimuth 117 | SunAz = ee.Image.constant(s2.get('MEAN_SOLAR_AZIMUTH_ANGLE')).clip(footprint) 118 | 119 | # sun zenith 120 | SunZe = ee.Image.constant(s2.get('MEAN_SOLAR_ZENITH_ANGLE')).clip(footprint) 121 | cosdSunZe = SunZe.multiply(pi.divide(ee.Image(180))).cos() # in degrees 122 | sindSunZe = SunZe.multiply(pi.divide(ee.Image(180))).sin() # in degrees 123 | 124 | # sat zenith 125 | SatZe = ee.Image.constant(s2.get('MEAN_INCIDENCE_ZENITH_ANGLE_B5')).clip(footprint) 126 | cosdSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).cos() 127 | sindSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).sin() 128 | 129 | # sat azimuth 130 | SatAz = ee.Image.constant(s2.get('MEAN_INCIDENCE_AZIMUTH_ANGLE_B5')).clip(footprint) 131 | 132 | # relative azimuth 133 | RelAz = SatAz.subtract(SunAz) 134 | cosdRelAz = RelAz.multiply(pi.divide(ee.Image(180))).cos() 135 | 136 | # Pressure 137 | P = (ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM)).pow(5.25588)).multiply( 138 | 0.01)).multiply(mask) 139 | Po = ee.Image(1013.25) 140 | 141 | # esun 142 | ESUN = ee.Image(ee.Array([ee.Image(s2.get('SOLAR_IRRADIANCE_B1')), 143 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2')), 144 | ee.Image(s2.get('SOLAR_IRRADIANCE_B3')), 145 | ee.Image(s2.get('SOLAR_IRRADIANCE_B4')), 146 | ee.Image(s2.get('SOLAR_IRRADIANCE_B5')), 147 | ee.Image(s2.get('SOLAR_IRRADIANCE_B6')), 148 | ee.Image(s2.get('SOLAR_IRRADIANCE_B7')), 149 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8')), 150 | ee.Image(s2.get('SOLAR_IRRADIANCE_B8A')), 151 | ee.Image(s2.get('SOLAR_IRRADIANCE_B11')), 152 | ee.Image(s2.get('SOLAR_IRRADIANCE_B2'))] 153 | )).toArray().toArray(1) 154 | 155 | ESUN = ESUN.multiply(ee.Image(1)) 156 | 157 | ESUNImg = ESUN.arrayProject([0]).arrayFlatten([bands]) 158 | 159 | # create empty array for the images 160 | imgArr = rescale.select(bands).toArray().toArray(1) 161 | 162 | # pTOA to Ltoa 163 | Ltoa = imgArr.multiply(ESUN).multiply(cosdSunZe).divide(pi.multiply(d.pow(2))) 164 | 165 | # band centers 166 | bandCenter = ee.Image(443).divide(1000).addBands(ee.Image(490).divide(1000))\ 167 | .addBands(ee.Image(560).divide(1000))\ 168 | .addBands(ee.Image(665).divide(1000))\ 169 | .addBands(ee.Image(705).divide(1000))\ 170 | .addBands(ee.Image(740).divide(1000))\ 171 | .addBands(ee.Number(783).divide(1000))\ 172 | .addBands(ee.Number(842).divide(1000))\ 173 | .addBands(ee.Number(865).divide(1000))\ 174 | .addBands(ee.Number(1610).divide(1000))\ 175 | .addBands(ee.Number(2190).divide(1000))\ 176 | .toArray().toArray(1) 177 | 178 | # ozone coefficients 179 | koz = ee.Image(0.0039).addBands(ee.Image(0.0213))\ 180 | .addBands(ee.Image(0.1052))\ 181 | .addBands(ee.Image(0.0505))\ 182 | .addBands(ee.Image(0.0205))\ 183 | .addBands(ee.Image(0.0112))\ 184 | .addBands(ee.Image(0.0075))\ 185 | .addBands(ee.Image(0.0021))\ 186 | .addBands(ee.Image(0.0019))\ 187 | .addBands(ee.Image(0))\ 188 | .addBands(ee.Image(0))\ 189 | .toArray().toArray(1) 190 | 191 | ################### 192 | 193 | # Calculate ozone optical thickness 194 | Toz = koz.multiply(DU).divide(ee.Image(1000)) 195 | 196 | # Calculate TOA radiance in the absense of ozone 197 | Lt = Ltoa.multiply(((Toz)).multiply((ee.Image(1).divide(cosdSunZe)).add(ee.Image(1).divide(cosdSatZe))).exp()) 198 | 199 | # Rayleigh optical thickness 200 | Tr = (P.divide(Po)).multiply(ee.Image(0.008569).multiply(bandCenter.pow(-4))).multiply((ee.Image(1).add( 201 | ee.Image(0.0113).multiply(bandCenter.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter.pow(-4))))) 202 | 203 | # Specular reflection (s- and p- polarization states) 204 | theta_V = ee.Image(0.0000000001) 205 | sin_theta_j = sindSunZe.divide(ee.Image(1.333)) 206 | 207 | theta_j = sin_theta_j.asin().multiply(ee.Image(180).divide(pi)) 208 | 209 | theta_SZ = SunZe 210 | 211 | R_theta_SZ_s = ( 212 | ((theta_SZ.multiply(pi.divide(ee.Image(180)))).subtract(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow( 213 | 2)).divide( 214 | (((theta_SZ.multiply(pi.divide(ee.Image(180)))).add(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2))) 215 | 216 | R_theta_V_s = ee.Image(0.0000000001) 217 | 218 | R_theta_SZ_p = (((theta_SZ.multiply(pi.divide(180))).subtract(theta_j.multiply(pi.divide(180)))).tan().pow(2)).divide( 219 | (((theta_SZ.multiply(pi.divide(180))).add(theta_j.multiply(pi.divide(180)))).tan().pow(2))) 220 | 221 | R_theta_V_p = ee.Image(0.0000000001) 222 | 223 | R_theta_SZ = ee.Image(0.5).multiply(R_theta_SZ_s.add(R_theta_SZ_p)) 224 | 225 | R_theta_V = ee.Image(0.5).multiply(R_theta_V_s.add(R_theta_V_p)) 226 | 227 | # Sun-sensor geometry 228 | theta_neg = ((cosdSunZe.multiply(ee.Image(-1))).multiply(cosdSatZe)).subtract( 229 | (sindSunZe).multiply(sindSatZe).multiply(cosdRelAz)) 230 | 231 | theta_neg_inv = theta_neg.acos().multiply(ee.Image(180).divide(pi)) 232 | 233 | theta_pos = (cosdSunZe.multiply(cosdSatZe)).subtract(sindSunZe.multiply(sindSatZe).multiply(cosdRelAz)) 234 | 235 | theta_pos_inv = theta_pos.acos().multiply(ee.Image(180).divide(pi)) 236 | 237 | cosd_tni = theta_neg_inv.multiply(pi.divide(180)).cos() # in degrees 238 | 239 | cosd_tpi = theta_pos_inv.multiply(pi.divide(180)).cos() # in degrees 240 | 241 | Pr_neg = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni.pow(2)))) 242 | 243 | Pr_pos = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi.pow(2)))) 244 | 245 | # Rayleigh scattering phase function 246 | Pr = Pr_neg.add((R_theta_SZ.add(R_theta_V)).multiply(Pr_pos)) 247 | 248 | # rayleigh radiance contribution 249 | denom = ee.Image(4).multiply(pi).multiply(cosdSatZe) 250 | Lr = (ESUN.multiply(Tr)).multiply(Pr.divide(denom)) 251 | 252 | # rayleigh corrected radiance 253 | Lrc = Lt.subtract(Lr) 254 | LrcImg = Lrc.arrayProject([0]).arrayFlatten([bands]) 255 | 256 | # Aerosol Correction # 257 | 258 | # Bands in nm 259 | bands_nm = ee.Image(443).addBands(ee.Image(490))\ 260 | .addBands(ee.Image(560))\ 261 | .addBands(ee.Image(665))\ 262 | .addBands(ee.Image(705))\ 263 | .addBands(ee.Image(740))\ 264 | .addBands(ee.Image(783))\ 265 | .addBands(ee.Image(842))\ 266 | .addBands(ee.Image(865))\ 267 | .addBands(ee.Image(0))\ 268 | .addBands(ee.Image(0))\ 269 | .toArray().toArray(1) 270 | 271 | # Lam in SWIR bands 272 | Lam_10 = LrcImg.select('B11') 273 | Lam_11 = LrcImg.select('B12') 274 | 275 | # Calculate aerosol type 276 | eps = ( 277 | (((Lam_11).divide(ESUNImg.select('B12'))).log()).subtract(((Lam_10).divide(ESUNImg.select('B11'))).log())).divide( 278 | ee.Image(2190).subtract(ee.Image(1610))) 279 | 280 | # Calculate multiple scattering of aerosols for each band 281 | Lam = (Lam_11).multiply(((ESUN).divide(ESUNImg.select('B12')))).multiply( 282 | (eps.multiply(ee.Image(-1))).multiply((bands_nm.divide(ee.Image(2190)))).exp()) 283 | 284 | # diffuse transmittance 285 | trans = Tr.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe)).exp() 286 | 287 | # Compute water-leaving radiance 288 | Lw = Lrc.subtract(Lam).divide(trans) 289 | 290 | # water-leaving reflectance 291 | pw = (Lw.multiply(pi).multiply(d.pow(2)).divide(ESUN.multiply(cosdSunZe))) 292 | pwImg = pw.arrayProject([0]).arrayFlatten([bands]) 293 | 294 | # remote sensing reflectance 295 | Rrs = (pw.divide(pi).arrayProject([0]).arrayFlatten([bands]).slice(0, 9)) 296 | 297 | 298 | # / Bio optical Models #/ 299 | # chlor_a 300 | NDCI = (Rrs.select('B5').subtract(Rrs.select('B4'))).divide(Rrs.select('B5').add(Rrs.select('B4'))) # ndci 301 | chlor_a = ee.Image(14.039).add(ee.Image(86.115).multiply(NDCI)).add( 302 | ee.Image(194.325).multiply(NDCI.pow(ee.Image(2)))) # chlor_a 303 | 304 | # SD 305 | ln_BlueRed = (Rrs.select('B2').divide(Rrs.select('B4'))).log() 306 | lnMOSD = (ee.Image(1.4856).multiply(ln_BlueRed)).add(ee.Image(0.2734)) # R2 = 0.8748 with in-situ 307 | MOSD = ee.Image(10).pow(lnMOSD) # log space to (m) 308 | SD = (ee.Image(0.1777).multiply(MOSD)).add(ee.Image(1.0813)) 309 | 310 | # tsi 311 | TSI_c = ee.Image(30.6).add(ee.Image(9.81)).multiply(chlor_a.log()) 312 | TSI_s = ee.Image(60.0).subtract(ee.Image(14.41)).multiply(SD.log()) 313 | TSI = (TSI_c.add(TSI_s)).divide(ee.Image(2)) 314 | 315 | # tsi reclassified 316 | # Create conditions 317 | mask1 = TSI.lt(30) # classical oligitrophy (1) 318 | mask2 = TSI.gte(30).And(TSI.lt(40)) # (2) 319 | mask3 = TSI.gte(40).And(TSI.lt(50)) # (3) 320 | mask4 = TSI.gte(50).And(TSI.lt(60)) # (4) 321 | mask5 = TSI.gte(60).And(TSI.lt(70)) # (5) 322 | mask6 = TSI.gte(70).And(TSI.lt(80)) # (6) 323 | mask7 = TSI.gte(80) # (7) 324 | 325 | # Reclassify conditions into new values 326 | img1 = TSI.where(mask1.eq(1), 1).mask(mask1) 327 | img2 = TSI.where(mask2.eq(1), 2).mask(mask2) 328 | img3 = TSI.where(mask3.eq(1), 3).mask(mask3) 329 | img4 = TSI.where(mask4.eq(1), 4).mask(mask4) 330 | img5 = TSI.where(mask5.eq(1), 5).mask(mask5) 331 | img6 = TSI.where(mask6.eq(1), 6).mask(mask6) 332 | img7 = TSI.where(mask7.eq(1), 7).mask(mask7) 333 | 334 | # Ouput of reclassified image 335 | TSI_R = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7) 336 | 337 | s2Lyr = s2.multiply(mask) 338 | s2_vis = {'bands': 'B4,B3,B2', 'min': 0, 'max': 1000} 339 | s2_mapid = s2Lyr.getMapId(s2_vis) 340 | 341 | chlor_a_vis = {'min': 0, 'max': 40, 'palette': 'blue,cyan,limegreen,yellow,orange,darkred'} 342 | chlor_a_mapid = chlor_a.getMapId(chlor_a_vis) 343 | 344 | TSI_vis = {'min': 30, 'max': 70, 'palette': 'blue,cyan,limegreen,yellow,orange,darkred'} 345 | TSI_mapid = TSI.getMapId(TSI_vis) 346 | 347 | TSI_R_vis = {'min': 1, 'max': 7, 'palette': 'purple,blue,limegreen,yellow,orange,orangered,darkred'} 348 | TSI_R_mapid = TSI_R.getMapId(TSI_R_vis) 349 | 350 | print 'S2',s2_mapid 351 | print 'Chlor A',chlor_a_mapid 352 | print 'TSI',TSI_mapid 353 | print 'TSI R',TSI_R_mapid 354 | # Map Layers # 355 | # Map.centerObject(footprint, 7) 356 | # Map.addLayer(footprint, {}, 'footprint', true) # footprint 357 | # Map.addLayer(s2.multiply(mask), {bands: ['B4', 'B3', 'B2'], min: 0, max: 1000}, 'rgb', false) # rgb 358 | # Map.addLayer(SD, {min: 0, max: 3, palette: ['darkred', 'orange', 'yellow', 'limegreen', 'cyan', 'blue']}, 'SD', false) 359 | # Map.addLayer(chlor_a, {min: 0, max: 40, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange', 'darkred']}, 360 | # 'chlor_a', false) 361 | # Map.addLayer(TSI, {min: 30, max: 70, palette: ['blue', 'cyan', 'limegreen', 'yellow', 'orange', 'darkred']}, 'TSI', 362 | # false) 363 | # Map.addLayer(TSI_R, 364 | # {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 365 | # 'TSI_R', true) 366 | 367 | # EXPORT IMAGES # 368 | 369 | # # Export SD 370 | # Export.image.toDrive({ 371 | # image: SD, 372 | # description: 'SD', 373 | # scale: 20, 374 | # region: footprint 375 | # }) 376 | # 377 | # # Export chlor_a 378 | # Export.image.toDrive({ 379 | # image: chlor_a, 380 | # description: 'chlor_a', 381 | # scale: 20, 382 | # region: footprint 383 | # }) 384 | # 385 | # # Export TSI 386 | # Export.image.toDrive({ 387 | # image: TSI, 388 | # description: 'TSI', 389 | # scale: 20, 390 | # region: footprint 391 | # }) 392 | # 393 | # # Export TSI_R 394 | # Export.image.toDrive({ 395 | # image: TSI_R, 396 | # description: 'TSI_R', 397 | # scale: 20, 398 | # region: footprint 399 | # }) -------------------------------------------------------------------------------- /python/s2_wq_timeseries.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from ee.ee_exception import EEException 3 | import datetime 4 | 5 | try: 6 | ee.Initialize() 7 | except EEException as e: 8 | from oauth2client.service_account import ServiceAccountCredentials 9 | credentials = ServiceAccountCredentials.from_p12_keyfile( 10 | service_account_email='', 11 | filename='', 12 | private_key_password='notasecret', 13 | scopes=ee.oauth.SCOPE + ' https://www.googleapis.com/auth/drive ') 14 | ee.Initialize(credentials) 15 | 16 | # geometry = ee.Geometry.Polygon([[[78.41079711914062, 17.465297700926097], 17 | # [78.41629028320312, 17.334252606951086], 18 | # [78.59893798828125, 17.33490806580805], 19 | # [78.55087280273438, 17.494769882318828]]]) 20 | 21 | geometry = ee.Geometry.Polygon([[[30.76171875, 0.9049611504960419], 22 | [30.8935546875, -3.487377195492663], 23 | [35.5517578125, -3.2680324702882952], 24 | [35.5517578125, 1.9593043032313748]]]) 25 | 26 | # This script processes and charts WQ parameter valuse from a collection of Sentinel-2 archive for a particular pixel throughout time. 27 | # WQ parameters such chlor-a, secchi depth, trophic state index 28 | # How to use: 29 | # (1) If a "geometry" iable exists in the imports window, delete it. 30 | # (2) Within the map, select the point button near the top left side. 31 | # (3) Create a new point by clicking on a location of interest. 32 | # (4) Adjust the time frame you wish to browse by adding here: 33 | # begin date 34 | iniDate = '2015-05-01' 35 | # end date 36 | endDate = '2018-03-31' 37 | # (5) Adjust a cloud % threshold here: 38 | cloudPerc = 5 39 | # (5) Click Run 40 | # (5) The "available imagery" ImageCollection within the console displays all available imagery. If you wish to investigate a single 41 | # image from this collection, highlight the FILE_ID and copy and paste it into the proper location within the "s2 Single Image" script. 42 | # (6) The charts will appear on the right hand side of the interface, and will display the mean map in the map user interface. 43 | # (6) Click "Run" 44 | # (7) Export each chart as a csv by clicking on the export icon near the top-right of the chart. 45 | 46 | # Author: Benjamin Page # 47 | # Citations: 48 | # Page, B.P. and Mishra, D.R., 2018. A modified atmospheric correction when coupling sentinel-2 and landsat-8 for inland water quality monitoring 49 | 50 | #########################################################/ 51 | #########################################################/ 52 | #########################################################/ 53 | # Map.centerObject(geometry, 7) 54 | 55 | # Import Collections # 56 | 57 | # sentinel-2 58 | MSI = ee.ImageCollection('COPERNICUS/S2') 59 | 60 | # landsat-8 surface reflactance product (for masking purposes) 61 | SRP = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR') 62 | 63 | # toms / omi 64 | ozone = ee.ImageCollection('TOMS/MERGED') 65 | 66 | #########################################################/ 67 | #########################################################/ 68 | #########################################################/ 69 | 70 | pi = ee.Image(3.141592) 71 | 72 | # water mask 73 | startMonth = 5 74 | endMonth = 9 75 | startYear = 2013 76 | endYear = 2017 77 | 78 | forMask = SRP.filterBounds(geometry).select('B6').filterMetadata('CLOUD_COVER', "less_than", 10).filter( 79 | ee.Filter.calendarRange(startMonth, endMonth, 'month')).filter(ee.Filter.calendarRange(startYear, endYear, 'year')) 80 | mask = ee.Image(forMask.select('B6').median().lt(300)) 81 | mask = mask.updateMask(mask) 82 | 83 | # filter sentinel 2 collection 84 | FC = MSI.filterDate(iniDate, endDate).filterBounds(geometry).filterMetadata('CLOUDY_PIXEL_PERCENTAGE', "less_than", 85 | cloudPerc) 86 | 87 | 88 | # #########################################################/ 89 | # #########################################################/ 90 | # #########################################################/ 91 | # Mapping functions # 92 | 93 | def s2Correction(img): 94 | # msi bands 95 | bands = ['B1', 'B2', 'B3', 'B4', 'B5', 'B6', 'B7', 'B8', 'B8A', 'B11', 'B12'] 96 | 97 | # rescale 98 | rescale = img.select(bands).divide(10000).multiply(mask) 99 | 100 | # tile footprint 101 | footprint = rescale.geometry() 102 | 103 | # dem 104 | DEM = ee.Image('USGS/SRTMGL1_003').clip(footprint) 105 | 106 | # ozone 107 | DU = ee.Image(ozone.filterDate(iniDate, endDate).filterBounds(footprint).mean()) 108 | 109 | # Julian Day 110 | imgDate = ee.Date(img.get('system:time_start')) 111 | FOY = ee.Date.fromYMD(imgDate.get('year'), 1, 1) 112 | JD = imgDate.difference(FOY, 'day').int().add(1) 113 | 114 | # earth-sun distance 115 | myCos = ((ee.Image(0.0172).multiply(ee.Image(JD).subtract(ee.Image(2)))).cos()).pow(2) 116 | cosd = myCos.multiply(pi.divide(ee.Image(180))).cos() 117 | d = ee.Image(1).subtract(ee.Image(0.01673)).multiply(cosd).clip(footprint) 118 | 119 | # sun azimuth 120 | SunAz = ee.Image.constant(img.get('MEAN_SOLAR_AZIMUTH_ANGLE')).clip(footprint) 121 | 122 | # sun zenith 123 | SunZe = ee.Image.constant(img.get('MEAN_SOLAR_ZENITH_ANGLE')).clip(footprint) 124 | cosdSunZe = SunZe.multiply(pi.divide(ee.Image(180))).cos() # in degrees 125 | sindSunZe = SunZe.multiply(pi.divide(ee.Image(180))).sin() # in degrees 126 | 127 | # sat zenith 128 | SatZe = ee.Image.constant(img.get('MEAN_INCIDENCE_ZENITH_ANGLE_B5')).clip(footprint) 129 | cosdSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).cos() 130 | sindSatZe = (SatZe).multiply(pi.divide(ee.Image(180))).sin() 131 | 132 | # sat azimuth 133 | SatAz = ee.Image.constant(img.get('MEAN_INCIDENCE_AZIMUTH_ANGLE_B5')).clip(footprint) 134 | 135 | # relative azimuth 136 | RelAz = SatAz.subtract(SunAz) 137 | cosdRelAz = RelAz.multiply(pi.divide(ee.Image(180))).cos() 138 | 139 | # Pressure 140 | P = (ee.Image(101325).multiply(ee.Image(1).subtract(ee.Image(0.0000225577).multiply(DEM)).pow(5.25588)).multiply( 141 | 0.01)).multiply(mask) 142 | Po = ee.Image(1013.25) 143 | 144 | # esun 145 | ESUN = ee.Image(ee.Array([ee.Image(img.get('SOLAR_IRRADIANCE_B1')), 146 | ee.Image(img.get('SOLAR_IRRADIANCE_B2')), 147 | ee.Image(img.get('SOLAR_IRRADIANCE_B3')), 148 | ee.Image(img.get('SOLAR_IRRADIANCE_B4')), 149 | ee.Image(img.get('SOLAR_IRRADIANCE_B5')), 150 | ee.Image(img.get('SOLAR_IRRADIANCE_B6')), 151 | ee.Image(img.get('SOLAR_IRRADIANCE_B7')), 152 | ee.Image(img.get('SOLAR_IRRADIANCE_B8')), 153 | ee.Image(img.get('SOLAR_IRRADIANCE_B8A')), 154 | ee.Image(img.get('SOLAR_IRRADIANCE_B11')), 155 | ee.Image(img.get('SOLAR_IRRADIANCE_B2'))] 156 | )).toArray().toArray(1) 157 | 158 | ESUN = ESUN.multiply(ee.Image(1)) 159 | 160 | ESUNImg = ESUN.arrayProject([0]).arrayFlatten([bands]) 161 | 162 | # create empty array for the images 163 | imgArr = rescale.select(bands).toArray().toArray(1) 164 | 165 | # pTOA to Ltoa 166 | Ltoa = imgArr.multiply(ESUN).multiply(cosdSunZe).divide(pi.multiply(d.pow(2))) 167 | 168 | # band centers 169 | bandCenter = ee.Image(443).divide(1000).addBands(ee.Image(490).divide(1000)) \ 170 | .addBands(ee.Image(560).divide(1000)) \ 171 | .addBands(ee.Image(665).divide(1000)) \ 172 | .addBands(ee.Image(705).divide(1000)) \ 173 | .addBands(ee.Image(740).divide(1000)) \ 174 | .addBands(ee.Image(783).divide(1000)) \ 175 | .addBands(ee.Image(842).divide(1000)) \ 176 | .addBands(ee.Image(865).divide(1000)) \ 177 | .addBands(ee.Image(1610).divide(1000)) \ 178 | .addBands(ee.Image(2190).divide(1000)) \ 179 | .toArray().toArray(1) 180 | 181 | # ozone coefficients 182 | koz = ee.Image(0.0039).addBands(ee.Image(0.0213)) \ 183 | .addBands(ee.Image(0.1052)) \ 184 | .addBands(ee.Image(0.0505)) \ 185 | .addBands(ee.Image(0.0205)) \ 186 | .addBands(ee.Image(0.0112)) \ 187 | .addBands(ee.Image(0.0075)) \ 188 | .addBands(ee.Image(0.0021)) \ 189 | .addBands(ee.Image(0.0019)) \ 190 | .addBands(ee.Image(0)) \ 191 | .addBands(ee.Image(0)) \ 192 | .toArray().toArray(1) 193 | 194 | # Calculate ozone optical thickness 195 | Toz = koz.multiply(DU).divide(ee.Image(1000)) 196 | 197 | # Calculate TOA radiance in the absense of ozone 198 | Lt = Ltoa.multiply(((Toz)).multiply((ee.Image(1).divide(cosdSunZe)).add(ee.Image(1).divide(cosdSatZe))).exp()) 199 | 200 | # Rayleigh optical thickness 201 | Tr = (P.divide(Po)).multiply(ee.Image(0.008569).multiply(bandCenter.pow(-4))).multiply((ee.Image(1).add( 202 | ee.Image(0.0113).multiply(bandCenter.pow(-2))).add(ee.Image(0.00013).multiply(bandCenter.pow(-4))))) 203 | 204 | # Specular reflection (s- and p- polarization states) 205 | theta_V = ee.Image(0.0000000001) 206 | sin_theta_j = sindSunZe.divide(ee.Image(1.333)) 207 | 208 | theta_j = sin_theta_j.asin().multiply(ee.Image(180).divide(pi)) 209 | 210 | theta_SZ = SunZe 211 | 212 | R_theta_SZ_s = ( 213 | ((theta_SZ.multiply(pi.divide(ee.Image(180)))).subtract(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow( 214 | 2)).divide( 215 | (((theta_SZ.multiply(pi.divide(ee.Image(180)))).add(theta_j.multiply(pi.divide(ee.Image(180))))).sin().pow(2))) 216 | 217 | R_theta_V_s = ee.Image(0.0000000001) 218 | 219 | R_theta_SZ_p = ( 220 | ((theta_SZ.multiply(pi.divide(180))).subtract(theta_j.multiply(pi.divide(180)))).tan().pow(2)).divide( 221 | (((theta_SZ.multiply(pi.divide(180))).add(theta_j.multiply(pi.divide(180)))).tan().pow(2))) 222 | 223 | R_theta_V_p = ee.Image(0.0000000001) 224 | 225 | R_theta_SZ = ee.Image(0.5).multiply(R_theta_SZ_s.add(R_theta_SZ_p)) 226 | 227 | R_theta_V = ee.Image(0.5).multiply(R_theta_V_s.add(R_theta_V_p)) 228 | 229 | # Sun-sensor geometry 230 | theta_neg = ((cosdSunZe.multiply(ee.Image(-1))).multiply(cosdSatZe)).subtract( 231 | (sindSunZe).multiply(sindSatZe).multiply(cosdRelAz)) 232 | 233 | theta_neg_inv = theta_neg.acos().multiply(ee.Image(180).divide(pi)) 234 | 235 | theta_pos = (cosdSunZe.multiply(cosdSatZe)).subtract(sindSunZe.multiply(sindSatZe).multiply(cosdRelAz)) 236 | 237 | theta_pos_inv = theta_pos.acos().multiply(ee.Image(180).divide(pi)) 238 | 239 | cosd_tni = theta_neg_inv.multiply(pi.divide(180)).cos() # in degrees 240 | 241 | cosd_tpi = theta_pos_inv.multiply(pi.divide(180)).cos() # in degrees 242 | 243 | Pr_neg = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tni.pow(2)))) 244 | 245 | Pr_pos = ee.Image(0.75).multiply((ee.Image(1).add(cosd_tpi.pow(2)))) 246 | 247 | # Rayleigh scattering phase function 248 | Pr = Pr_neg.add((R_theta_SZ.add(R_theta_V)).multiply(Pr_pos)) 249 | 250 | # rayleigh radiance contribution 251 | denom = ee.Image(4).multiply(pi).multiply(cosdSatZe) 252 | Lr = (ESUN.multiply(Tr)).multiply(Pr.divide(denom)) 253 | 254 | # rayleigh corrected radiance 255 | Lrc = Lt.subtract(Lr) 256 | LrcImg = Lrc.arrayProject([0]).arrayFlatten([bands]) 257 | 258 | # Aerosol Correction # 259 | 260 | # Bands in nm 261 | bands_nm = ee.Image(443).addBands(ee.Image(490)) \ 262 | .addBands(ee.Image(560)) \ 263 | .addBands(ee.Image(665)) \ 264 | .addBands(ee.Image(705)) \ 265 | .addBands(ee.Image(740)) \ 266 | .addBands(ee.Image(783)) \ 267 | .addBands(ee.Image(842)) \ 268 | .addBands(ee.Image(865)) \ 269 | .addBands(ee.Image(0)) \ 270 | .addBands(ee.Image(0)) \ 271 | .toArray().toArray(1) 272 | 273 | # Lam in SWIR bands 274 | Lam_10 = LrcImg.select('B11') 275 | Lam_11 = LrcImg.select('B12') 276 | 277 | # Calculate aerosol type 278 | eps = ( 279 | (((Lam_11).divide(ESUNImg.select('B12'))).log()).subtract( 280 | ((Lam_10).divide(ESUNImg.select('B11'))).log())).divide( 281 | ee.Image(2190).subtract(ee.Image(1610))) 282 | 283 | # Calculate multiple scattering of aerosols for each band 284 | Lam = (Lam_11).multiply(((ESUN).divide(ESUNImg.select('B12')))).multiply( 285 | (eps.multiply(ee.Image(-1))).multiply((bands_nm.divide(ee.Image(2190)))).exp()) 286 | 287 | # diffuse transmittance 288 | trans = Tr.multiply(ee.Image(-1)).divide(ee.Image(2)).multiply(ee.Image(1).divide(cosdSatZe)).exp() 289 | 290 | # Compute water-leaving radiance 291 | Lw = Lrc.subtract(Lam).divide(trans) 292 | 293 | # water-leaving reflectance 294 | pw = (Lw.multiply(pi).multiply(d.pow(2)).divide(ESUN.multiply(cosdSunZe))) 295 | 296 | # remote sensing reflectance 297 | Rrs_coll = (pw.divide(pi).arrayProject([0]).arrayFlatten([bands]).slice(0, 9)) 298 | 299 | return (Rrs_coll.set('system:time_start', img.get('system:time_start'))) 300 | 301 | 302 | def chlorophyll(img): 303 | NDCI_coll = (img.select('B5').subtract(img.select('B4'))).divide(img.select('B5').add(img.select('B4'))) 304 | chlor_a_coll = ee.Image(14.039).add(ee.Image(86.115).multiply(NDCI_coll)).add( 305 | ee.Image(194.325).multiply(NDCI_coll.pow(ee.Image(2)))) 306 | return (chlor_a_coll.updateMask(chlor_a_coll.lt(100)).set('system:time_start', img.get('system:time_start'))) 307 | 308 | 309 | def secchi(img): 310 | 311 | blueRed_coll = (img.select('B2').divide(img.select('B4'))).log() 312 | lnMOSD_coll = (ee.Image(1.4856).multiply(blueRed_coll)).add( 313 | ee.Image(0.2734)) # R2 = 0.8748 with Anthony's in-situ data 314 | MOSD_coll = ee.Image(10).pow(lnMOSD_coll) 315 | sd_coll = (ee.Image(0.1777).multiply(MOSD_coll)).add(ee.Image(1.0813)) 316 | return (sd_coll.updateMask(sd_coll.lt(10)).set('system:time_start', img.get('system:time_start'))) 317 | 318 | 319 | def trophicState(img): 320 | tsi_coll = ee.Image(30.6).add(ee.Image(9.81).multiply(img.log())) 321 | return (tsi_coll.updateMask(tsi_coll.lt(200)).set('system:time_start', img.get('system:time_start'))) 322 | 323 | 324 | def reclassify(img): 325 | # Create conditions 326 | mask1 = img.lt(30) # (1) 327 | mask2 = img.gte(30).And(img.lt(40)) # (2) 328 | mask3 = img.gte(40).And(img.lt(50)) # (3) 329 | mask4 = img.gte(50).And(img.lt(60)) # (4) 330 | mask5 = img.gte(60).And(img.lt(70)) # (5) 331 | mask6 = img.gte(70).And(img.lt(80)) # (6) 332 | mask7 = img.gte(80) # (7) 333 | 334 | # Reclassify conditions into new values 335 | img1 = img.where(mask1.eq(1), 1).mask(mask1) 336 | img2 = img.where(mask2.eq(1), 2).mask(mask2) 337 | img3 = img.where(mask3.eq(1), 3).mask(mask3) 338 | img4 = img.where(mask4.eq(1), 4).mask(mask4) 339 | img5 = img.where(mask5.eq(1), 5).mask(mask5) 340 | img6 = img.where(mask6.eq(1), 6).mask(mask6) 341 | img7 = img.where(mask7.eq(1), 7).mask(mask7) 342 | 343 | # Ouput of reclassified image 344 | tsi_collR = img1.unmask(img2).unmask(img3).unmask(img4).unmask(img5).unmask(img6).unmask(img7) 345 | return (tsi_collR.updateMask(tsi_collR.set('system:time_start', img.get('system:time_start')))) 346 | 347 | 348 | #########################################################/ 349 | #########################################################/ 350 | #########################################################/ 351 | # Collection Processing # 352 | 353 | # atmospheric correction 354 | Rrs_coll = FC.map(s2Correction) 355 | 356 | # chlorophyll-a 357 | chlor_a_coll = Rrs_coll.map(chlorophyll) 358 | 359 | # sd 360 | sd_coll = Rrs_coll.map(secchi) 361 | 362 | # tsi 363 | tsi_coll = chlor_a_coll.map(trophicState) 364 | 365 | # tsi reclass 366 | tsi_collR = tsi_coll.map(reclassify) 367 | 368 | Rsr = Rrs_coll.mean() 369 | Rsr_vis = {'min': 0, 'max': 0.03, 'bands': 'B4,B3,B2'} 370 | Rsr_mapid = Rsr.getMapId(Rsr_vis) 371 | 372 | SD = sd_coll.mean() 373 | SD_vis = {'min': 0, 'max': 2, 'palette': '#800000,#FF9700,#7BFF7B,#0080FF,#000080'} 374 | SD_mapid = SD.getMapId(SD_vis) 375 | 376 | TSI = tsi_coll.mean() 377 | TSI_vis = {'min': 30, 'max': 80,'palette':'darkblue,blue,cyan,limegreen,yellow,orange,orangered,darkred'} 378 | TSI_mapid = TSI.getMapId(TSI_vis) 379 | 380 | TSI_R = tsi_collR.mean() 381 | TSI_R_vis = {'min': 1, 'max': 7, 'palette': 'purple,blue,limegreen,yellow,orange,orangered,darkred'} 382 | TSI_R_mapid = TSI_R.getMapId(TSI_R_vis) 383 | 384 | print 'RSR',Rsr_mapid 385 | print 'SD',SD_mapid 386 | print 'TSI',TSI_mapid 387 | print 'TSI R',TSI_R_mapid 388 | 389 | # #########################################################/ 390 | # #########################################################/ 391 | # #########################################################/ 392 | # Map Layers # 393 | # Map.addLayer(mask, {}, 'mask', false) 394 | # Map.addLayer(Rrs_coll.mean(), {min: 0, max: 0.03, bands: ['B4', 'B3', 'B2']}, 'Mean RGB', false) 395 | # Map.addLayer(chlor_a_coll.mean(), {min: 0, max: 40, 396 | # palette: ['darkblue', 'blue', 'cyan', 'limegreen', 'yellow', 'orange', 'orangered', 397 | # 'darkred']}, 'Mean chlor-a', false) 398 | # Map.addLayer(sd_coll.mean(), {min: 0, max: 2, palette: ['800000', 'FF9700', '7BFF7B', '0080FF', '000080']}, 'Mean Zsd', 399 | # false) 400 | # Map.addLayer(tsi_coll.mean(), {min: 30, max: 80, 401 | # palette: ['darkblue', 'blue', 'cyan', 'limegreen', 'yellow', 'orange', 'orangered', 402 | # 'darkred']}, 'Mean TSI', false) 403 | # Map.addLayer(tsi_collR.mode(), 404 | # {min: 1, max: 7, palette: ['purple', 'blue', 'limegreen', 'yellow', 'orange', 'orangered', 'darkred']}, 405 | # 'Mode TSI Class', true) 406 | 407 | #########################################################/ 408 | #########################################################/ 409 | #########################################################/ 410 | # Time Series # 411 | 412 | # # Chlorophyll-a time series 413 | # chlorTimeSeries = ui.Chart.image.seriesByRegion( 414 | # chlor_a_coll, geometry, ee.Reducer.mean()) 415 | # .setChartType('ScatterChart') 416 | # .setOptions({ 417 | # title: 'Mean Chlorphyll-a', 418 | # vAxis: {title: 'Chlor-a [micrograms/L]'}, 419 | # lineWidth: 1, 420 | # pointSize: 4, 421 | # }) 422 | # 423 | # # SD time series 424 | # sdTimeSeries = ui.Chart.image.seriesByRegion( 425 | # sd_coll, geometry, ee.Reducer.mean()) 426 | # .setChartType('ScatterChart') 427 | # .setOptions({ 428 | # title: 'Mean Secchi Depth', 429 | # vAxis: {title: 'Zsd [m]'}, 430 | # lineWidth: 1, 431 | # pointSize: 4, 432 | # }) 433 | # 434 | # # TSI time series 435 | # tsiTimeSeries = ui.Chart.image.seriesByRegion( 436 | # tsi_coll, geometry, ee.Reducer.mean()) 437 | # .setChartType('ScatterChart') 438 | # .setOptions({ 439 | # title: 'Mean Trophic State Index', 440 | # vAxis: {title: 'TSI [1-100]'}, 441 | # lineWidth: 1, 442 | # pointSize: 4, 443 | # }) 444 | # 445 | # # TSI Reclass time series 446 | # tsiRTimeSeries = ui.Chart.image.seriesByRegion( 447 | # tsi_collR, geometry, ee.Reducer.mode()) 448 | # .setChartType('ScatterChart') 449 | # .setOptions({ 450 | # title: 'Mode Trophic State Index Class', 451 | # vAxis: {title: 'TSI Class'}, 452 | # lineWidth: 1, 453 | # pointSize: 4, 454 | # }) 455 | # 456 | # print(chlorTimeSeries) 457 | # print(sdTimeSeries) 458 | # print(tsiTimeSeries) 459 | # print(tsiRTimeSeries) 460 | 461 | 462 | --------------------------------------------------------------------------------