├── Demo_Script └── Predict_L8.py ├── GEE_ImageFusion ├── __init__.py ├── core_functions.py ├── get_paired_collections.py └── prep_functions.py └── README.md /Demo_Script/Predict_L8.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Author: Ty Nietupski (ty.nietupski@oregonstate.edu) 5 | 6 | This script shows an example of how the functions in the prep_functions, 7 | get_paired_collections, and core_functions scripts can be applied to predict 8 | all images from a specified timeframe at an individual scene location. To 9 | use this code as a module, I've just placed the GEE_ImageFusion directory in 10 | the site-packages directory of the relevant virtual environment so that 11 | importing these functions into new script is easy. 12 | 13 | General outline: 14 | 1. Define global vars 15 | 2. Organize dataset into nested lists 16 | 3. Predict images in groups of 10 to avoid exceeding the memory limit. 17 | 1. Register Landsat and MODIS 18 | 2. Mask Landsat and MODIS and modify format for fusion (select similar 19 | pixels and convert images to 'neighborhood' images) 20 | 3. Calculate the spatial and spectral distance to neighboring pixels 21 | and use to determine individual pixel weight. 22 | 4. Perform regression over selected pixels to determine conversion 23 | coefficient. 24 | 5. Use weights and conversion coefficient to predict images between 25 | image pairs. 26 | 27 | 28 | The MIT License 29 | 30 | Copyright © 2021 Ty Nietupski 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining a copy of 33 | this software and associated documentation files (the "Software"), to deal in 34 | the Software without restriction, including without limitation the rights to 35 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 36 | of the Software, and to permit persons to whom the Software is furnished to do 37 | so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in all 40 | copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 47 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 48 | SOFTWARE. 49 | 50 | """ 51 | 52 | ############################################################################## 53 | # %% import 54 | ############################################################################## 55 | 56 | import ee 57 | from GEE_ImageFusion import * 58 | 59 | ee.Initialize() 60 | 61 | ############################################################################## 62 | # %% GLOBALS 63 | ############################################################################## 64 | 65 | # region of interest 66 | # location for prediction (outside scene overlap areas) 67 | region = ee.Geometry.Point([-118.12627784505537, 44.66875751833964]) 68 | 69 | # define training data temporal bounds broadly 70 | startDate = '2017-03-01' 71 | endDate = '2018-01-01' 72 | 73 | # common bands between sensors that will be used for fusion 74 | # would need to add functions for other indices (evi etc.). 75 | # ndvi is exported as 16 bit int so would have to make sure not to rescale 76 | # reflectance bands if that is what you are planning to predict (line 206) 77 | # some resturcturing of this code would be necessary if the goal was to 78 | # predict a multiband image but the core functions should work for any number 79 | # of bands 80 | commonBandNames = ee.List(['ndvi']) 81 | 82 | # image collections to use in fusion 83 | # NOTE: if using older Landsat and not using NDVI one would have to modify the 84 | # get_paired_collections script because this script harmonizes NDVI from 85 | # L5 & L7 to L8 based on Roy et al. 2016 (see etmToOli and getPaired functions) 86 | landsatCollection = 'LANDSAT/LC08/C01/T1_SR' 87 | modisCollection = 'MODIS/006/MCD43A4' 88 | 89 | # landsat band names including qc band for masking 90 | bandNamesLandsat = ee.List(['blue', 'green', 'red', 91 | 'nir', 'swir1', 'swir2', 'pixel_qa']) 92 | landsatBands = ee.List([1, 2, 3, 4, 5, 6, 10]) 93 | 94 | # modis band names 95 | bandNamesModis = ee.List(['blue', 'green', 'red', 'nir', 'swir1', 'swir2']) 96 | modisBands = ee.List([2, 3, 0, 1, 5, 6]) 97 | 98 | # radius of moving window 99 | # Note: Generally, larger windows are better but as the window size increases, 100 | # so does the memory requirement and we quickly will surpass the memory 101 | # capacity of a single node (in testing 13 was max size for single band, and 102 | # 10 was max size for up to 6 bands) 103 | kernelRadius = ee.Number(10) 104 | kernel = ee.Kernel.square(kernelRadius) 105 | numPixels = kernelRadius.add(kernelRadius.add(1)).pow(2) 106 | 107 | # number of land cover classes in scene 108 | coverClasses = 7 109 | 110 | # to export the images to an asset we need the path to the assets folder 111 | path = 'users/nietupst/' 112 | scene_name = 'NDVI_P43R29_' 113 | 114 | ############################################################################## 115 | # %% get filtered collections 116 | ############################################################################## 117 | 118 | # sorted, filtered, paired image retrieval 119 | paired = getPaired(startDate, endDate, 120 | landsatCollection, landsatBands, bandNamesLandsat, 121 | modisCollection, modisBands, bandNamesModis, 122 | commonBandNames, region) 123 | 124 | subs = makeSubcollections(paired) 125 | # subs_meta = subs.getInfo() 126 | 127 | ############################################################################## 128 | # %% Predict and Export Images 129 | ############################################################################## 130 | 131 | # loop through each list of paired images 132 | num_lists = subs.length().getInfo() 133 | for i in range(0, num_lists): 134 | # determine the number of modis images between pairs 135 | num_imgs = ee.List(ee.List(subs.get(i)).get(2)).length() 136 | 137 | # determine the remainder of images, if not groups of 10 138 | remaining = num_imgs.mod(10) 139 | 140 | # create sequence of starting indices for modis images 141 | index_seq = ee.List.sequence(0, num_imgs.subtract(remaining), 10) 142 | 143 | # images to be grouped and predicted 144 | subList = ee.List(ee.List(subs.get(i)).get(2)) 145 | 146 | # loop through indices predicting in batches of 10 147 | for x in range(0, index_seq.length().getInfo()): 148 | # starting index 149 | start = ee.Number(index_seq.get(x)) 150 | 151 | # ending index 152 | end = ee.Algorithms.If(start.add(10).gt(num_imgs), 153 | num_imgs, 154 | start.add(10)) 155 | 156 | # group of images to predict 157 | pred_group = subList.slice(start, end) 158 | landsat_t01 = ee.List(ee.List(subs.get(i)).get(0)) 159 | modis_t01 = ee.List(ee.List(subs.get(i)).get(1)) 160 | modis_tp = pred_group 161 | 162 | # get the start and end day values and year for the group to use 163 | # to label the file when exported to asset 164 | startDay = ee.Number.parse(ee.ImageCollection(pred_group) 165 | .first() 166 | .get('DOY')) 167 | endDay = ee.Number.parse(ee.ImageCollection(pred_group) 168 | .sort('system:time_start', False) 169 | .first() 170 | .get('DOY')) 171 | year = ee.Date(ee.ImageCollection(pred_group) 172 | .sort('system:time_start', False) 173 | .first() 174 | .get('system:time_start')).format('Y') 175 | 176 | # start and end day of year 177 | doys = landsat_t01 \ 178 | .map(lambda img: ee.String(ee.Image(img).get('DOY')).cat('_')) 179 | 180 | # register images 181 | landsat_t01, modis_t01, modis_tp = registerImages(landsat_t01, 182 | modis_t01, 183 | modis_tp) 184 | 185 | # prep landsat imagery (mask and format) 186 | maskedLandsat, pixPositions, pixBN = prepLandsat(landsat_t01, 187 | kernel, 188 | numPixels, 189 | commonBandNames, 190 | doys, 191 | coverClasses) 192 | 193 | # prep modis imagery (mask and format) 194 | modSorted_t01, modSorted_tp = prepMODIS(modis_t01, modis_tp, kernel, 195 | numPixels, commonBandNames, 196 | pixBN) 197 | 198 | # calculate spectral distance 199 | specDist = calcSpecDist(maskedLandsat, modSorted_t01, 200 | numPixels, pixPositions) 201 | 202 | # calculate spatial distance 203 | spatDist = calcSpatDist(pixPositions) 204 | 205 | # calculate weights from the spatial and spectral distances 206 | weights = calcWeight(spatDist, specDist) 207 | 208 | # calculate the conversion coefficients 209 | coeffs = calcConversionCoeff(maskedLandsat, modSorted_t01, 210 | doys, numPixels, commonBandNames) 211 | 212 | # predict all modis images in modis tp collection 213 | prediction = modSorted_tp \ 214 | .map(lambda image: 215 | predictLandsat(landsat_t01, modSorted_t01, 216 | doys, ee.List(image), 217 | weights, coeffs, 218 | commonBandNames, numPixels)) 219 | 220 | # create a list of new band names to apply to the multiband ndvi image 221 | # NOTE: cant export with names starting with 0 222 | preds = ee.ImageCollection(prediction).toBands() 223 | dates = modis_tp.map(lambda img: 224 | ee.Image(img).get('system:time_start')) 225 | predNames = ee.List.sequence(0, prediction.length().subtract(1)) \ 226 | .map(lambda i: 227 | commonBandNames\ 228 | .map(lambda name: 229 | ee.String(name) 230 | .cat(ee.String(ee.Number(dates.get(i)).format()))))\ 231 | .flatten() 232 | 233 | # export all predictions as a single multiband image 234 | # each band name corresponds to the timestamp for the image 235 | task = ee.batch.Export.image.toAsset( 236 | image=preds.rename(predNames).multiply(10000).toInt16(), 237 | description=ee.String(scene_name) 238 | .cat(year) 239 | .cat('_') 240 | .cat(startDay.format()) 241 | .cat('_').cat(endDay.format()).getInfo(), 242 | assetId=ee.String(path) 243 | .cat(ee.String(scene_name)) 244 | .cat(year) 245 | .cat('_') 246 | .cat(startDay.format()) 247 | .cat('_') 248 | .cat(endDay.format()).getInfo(), 249 | region=ee.Image(prediction.get(0)).geometry(), 250 | scale=30) 251 | 252 | task.start() 253 | -------------------------------------------------------------------------------- /GEE_ImageFusion/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # -*- coding: utf-8 -*- 4 | """ 5 | Author: Ty Nietupski (ty.nietupski@oregonstate.edu) 6 | """ 7 | 8 | from .core_functions import * 9 | from .prep_functions import * 10 | from .get_paired_collections import * -------------------------------------------------------------------------------- /GEE_ImageFusion/core_functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Author: Ty Nietupski (ty.nietupski@oregonstate.edu) 4 | 5 | Core functions of the image fusion process: 6 | - spatial distance 7 | - spectral distance 8 | - weights 9 | - conversion coefficients 10 | - prediction 11 | 12 | These function require that the preprocessing steps from 13 | get_paired_collections.py and prep_funtions.py have been used to prepare 14 | the imagery for fusion. An example of the use of these functions can 15 | be found in Predict_l8.py. 16 | 17 | 18 | 19 | 20 | The MIT License 21 | 22 | Copyright © 2021 Ty Nietupski 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy of 25 | this software and associated documentation files (the "Software"), to deal in 26 | the Software without restriction, including without limitation the rights to 27 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 28 | of the Software, and to permit persons to whom the Software is furnished to do 29 | so, subject to the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be included in all 32 | copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 39 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 40 | SOFTWARE. 41 | 42 | """ 43 | 44 | import ee 45 | 46 | 47 | def calcSpecDist(maskedLandsat, modSorted_t01, numPixels, pixPositions): 48 | """ 49 | Calculate the mean absolute spectral distance between the Landsat and\ 50 | MODIS pixels. 51 | 52 | Parameters 53 | ---------- 54 | maskedLandsat : ee_list.List 55 | Landsat neighborhood images masked and prepared with the prepLandsat 56 | function. 57 | modSorted_t01 : ee_list.List 58 | MODIS neighborhood images prepared with the prepMODIS function. 59 | numPixels : ee_number.Number 60 | Total number of pixels in the kernel. 61 | pixPositions : ee_list.List 62 | Names for each index in the window (e.g., "_0_0", "_0_1" ...). 63 | 64 | Returns 65 | ------- 66 | image.Image 67 | Image with the spectral distance between each pixel in neighborhood. 68 | 69 | """ 70 | sDist = ee.List.sequence(0, numPixels.subtract(1)) \ 71 | .map(lambda index: 72 | ee.Image(maskedLandsat.get(index)) \ 73 | .subtract(ee.Image(modSorted_t01.get(index))) \ 74 | .abs() \ 75 | .reduce(ee.Reducer.mean())) 76 | 77 | return ee.ImageCollection(sDist) \ 78 | .toBands() \ 79 | .rename(pixPositions.map(lambda name: 80 | ee.String('sDist').cat(name))) 81 | 82 | 83 | def calcSpatDist(positions): 84 | """ 85 | Calculate the spatial distance between each pixel in the window and the\ 86 | central pixel. 87 | 88 | Parameters 89 | ---------- 90 | positions : ee_list.List 91 | Names for each index in the window (e.g., "_0_0", "_0_1" ...). 92 | 93 | Returns 94 | ------- 95 | image.Image 96 | Image with the spatial distance between each pixel in neighborhood. 97 | 98 | """ 99 | # window width 100 | w2 = positions.length().sqrt().subtract(1).divide(2) 101 | 102 | # distance to each pixel in window 103 | dist = positions.map(lambda position: 104 | ee.Image.constant(ee.Number(1) 105 | .add(ee.Number.parse( 106 | ee.String(position) \ 107 | .match('(-?[0-9]+)', 'g')\ 108 | .get(0)) \ 109 | .pow(2) \ 110 | .add(ee.Number.parse( 111 | ee.String(position) \ 112 | .match('(-?[0-9]+)', 'g')\ 113 | .get(1))\ 114 | .pow(2)) \ 115 | .sqrt() \ 116 | .divide(w2)))) 117 | 118 | return ee.ImageCollection(dist) \ 119 | .toBands() \ 120 | .rename(positions.map(lambda bn: ee.String('corr').cat(bn))) 121 | 122 | def calcWeight(spatDist, specDist): 123 | """ 124 | Create diagonal weight matrix from a combination of the spatial and\ 125 | spectral distance images. 126 | 127 | Parameters 128 | ---------- 129 | spatDist : image.Image 130 | Spatial distance to each pixel in the kernel (window). 131 | specDist : image.Image 132 | Spectral distance to each pixel in the kernel (window). 133 | 134 | Returns 135 | ------- 136 | image.Image (array image) 137 | Weight for all similar pixels in the window. 138 | 139 | """ 140 | disIndex = specDist.multiply(spatDist) 141 | num = ee.Image.constant(1).divide(disIndex) 142 | sumNum = ee.ImageCollection(num.bandNames() 143 | .map(lambda bn: 144 | num.select([bn]).rename('num'))) \ 145 | .sum() 146 | W = num.divide(sumNum) 147 | 148 | return W.unmask().toArray().toArray(1).matrixToDiag() 149 | 150 | 151 | def calcConversionCoeff(maskedLandsat, modSorted_t01, 152 | doys, numPixels, commonBandNames): 153 | """ 154 | Perform linear regression between all Landsat and MODIS pixels in the\ 155 | window. 156 | 157 | Parameters 158 | ---------- 159 | maskedLandsat : ee_list.List 160 | Landsat neighborhood images masked and prepared with the prepLandsat 161 | function. 162 | modSorted_t01 : ee_list.List 163 | MODIS neighborhood images prepared with the prepMODIS function. 164 | doys : ee_list.List 165 | List of day of year associated with t0 and t1. 166 | numPixels : ee_number.Number 167 | Total number of pixels in the kernel. 168 | commonBandNames : ee_list.List 169 | Names of bands to use in fusion. 170 | 171 | Returns 172 | ------- 173 | coeffs : image.Image (array image) 174 | Scaling coefficients for the window. 175 | 176 | """ 177 | # reformat the landsat & modis data 178 | lanMod = doys \ 179 | .map(lambda doy: 180 | ee.List.sequence(0, numPixels.subtract(1)) \ 181 | .map(lambda index: 182 | ee.Image.constant(1).rename(['intercept']) \ 183 | .addBands(ee.Image(modSorted_t01.get(index))\ 184 | .select(ee.String(doy).cat('.+')) \ 185 | .rename(commonBandNames\ 186 | .map(lambda bn: 187 | ee.String(bn).cat('_modis')))) \ 188 | .addBands(ee.Image(maskedLandsat.get(index))\ 189 | .select(ee.String(doy).cat('.+')) \ 190 | .rename(commonBandNames\ 191 | .map(lambda bn: 192 | ee.String(bn).cat('_landsat')))))) 193 | 194 | # when we convert this collection to an array we get a 2-D array image 195 | # where the 0 axis is the similar pixel images and the 1 axis is the bands 196 | # of landsat and modis 197 | lanMod = ee.ImageCollection(lanMod.flatten()) 198 | 199 | # solve for conversion coefficients using linear regression reducer 200 | coeffs = lanMod \ 201 | .reduce(ee.Reducer.linearRegression(commonBandNames.length().add(1), 202 | commonBandNames.length()))\ 203 | .select([0], ['coefficients']) \ 204 | .arraySlice(0, 1, commonBandNames.length().add(1)) 205 | 206 | return coeffs 207 | 208 | 209 | def predictLandsat(landsat_t01, modSorted_t01, 210 | doys, modSorted_tp, weights, 211 | coeffs, commonBandNames, numPixels): 212 | """ 213 | Use weights and coefficients to predict landsat from MODIS. We predict\ 214 | from t0 and t1 and then merge predictions, weighting by the temporal\ 215 | proximity of predicted image to t0 and t1, to get the final prediction. 216 | 217 | Parameters 218 | ---------- 219 | landsat_t01 : ee_list.List 220 | Landsat images at time 0 (t0) and time 1 (t1). 221 | modSorted_t01 : ee_list.List 222 | MODIS neighborhood images prepared with the prepMODIS function. 223 | doys : ee_list.List 224 | List of day of year associated with t0 and t1. 225 | modSorted_tp : ee_list.List 226 | MODIS neighborhood image for the prediction date prepared with 227 | prepMODIS function. 228 | weights : image.Image (array image) 229 | Weight for all similar pixels in the window. 230 | coeffs : image.Image (array image) 231 | Scaling coefficients for the window. 232 | commonBandNames : ee_list.List 233 | Names of bands to use in fusion. 234 | numPixels : ee_number.Number 235 | Total number of pixels in the kernel. 236 | 237 | Returns 238 | ------- 239 | image.Image 240 | Predicted Landsat image for date corresponding to modSorted_tp. 241 | 242 | """ 243 | # split apart the modis images based on the doy 244 | modSplit_t01 = doys \ 245 | .map(lambda doy: 246 | modSorted_t01 \ 247 | .map(lambda image: 248 | ee.Image(image) \ 249 | .select(ee.String(doy).cat('.+')) \ 250 | .rename(commonBandNames))) 251 | 252 | # find difference between modis at time 0 and 1 and modis at time p 253 | # convert this to a (numPixels x numBands) array 254 | diffMod = modSplit_t01 \ 255 | .map(lambda imagelist: 256 | ee.ImageCollection(ee.List.sequence(0, numPixels.subtract(1)) \ 257 | .map(lambda index: 258 | ee.Image(modSorted_tp.get(index)) \ 259 | .subtract(ee.Image(ee.List(imagelist).get(index))))) \ 260 | .toArray()) 261 | 262 | # calculate y_hat for each pixel based on coeffs and the difference in 263 | # reflectance (index) between paired and unpaired modis then weight this 264 | # by the weights calculated above 265 | # will return the scaled, weighted sum of the similar pixel differences 266 | sumPixel = diffMod \ 267 | .map(lambda arrayImage: 268 | weights \ 269 | .matrixMultiply(ee.Image(arrayImage).matrixMultiply(coeffs)) \ 270 | .arrayReduce(ee.Reducer.sum(), [0]) \ 271 | .arrayProject([1]) \ 272 | .arrayFlatten([commonBandNames])) 273 | 274 | # create weights for each band based on the difference in modis reflectance 275 | # between the base and prediction time 276 | sumDiff_t0 = ee.Image.constant(1) \ 277 | .divide(ee.Image(diffMod.get(0))\ 278 | .arrayReduce(ee.Reducer.sum(), [0])\ 279 | .abs()) 280 | sumDiff_t1 = ee.Image.constant(1) \ 281 | .divide(ee.Image(diffMod.get(1))\ 282 | .arrayReduce(ee.Reducer.sum(), [0])\ 283 | .abs()) 284 | weight_t0 = sumDiff_t0 \ 285 | .divide(sumDiff_t0.add(sumDiff_t1)) \ 286 | .arrayProject([1]) \ 287 | .arrayFlatten([commonBandNames]) 288 | weight_t1 = sumDiff_t1 \ 289 | .divide(sumDiff_t0.add(sumDiff_t1)) \ 290 | .arrayProject([1]) \ 291 | .arrayFlatten([commonBandNames]) 292 | temporalWeight = ee.List([weight_t0, weight_t1]) 293 | 294 | # predict the new image based on sum of pixel differences and original 295 | # image. weight each prediction based on the calculated temporal weight. 296 | predictions = ee.List([0, 1]) \ 297 | .map(lambda time: 298 | ee.Image(landsat_t01.get(time)) \ 299 | .add(ee.Image(sumPixel.get(time))) \ 300 | .multiply(ee.Image(temporalWeight.get(time)))) 301 | 302 | mergedPrediction = ee.Image(predictions.get(0)) \ 303 | .add(ee.Image(predictions.get(1))) 304 | 305 | return mergedPrediction \ 306 | .setMulti({ 307 | 'DOY': ee.Image(modSorted_tp.get(0)).get('DOY'), 308 | 'system:time_start': ee.Image(modSorted_tp.get(0))\ 309 | .get('system:time_start') 310 | }) 311 | -------------------------------------------------------------------------------- /GEE_ImageFusion/get_paired_collections.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Author: Ty Nietupski (ty.nietupski@oregonstate.edu) 4 | 5 | Functions for image preprocessing and data organization: 6 | - mask landsat 7 | - mask modis 8 | - addNDVI 9 | - etmToOli 10 | - get paired image collections (getPaired) 11 | - reorganize paired collection to units for prediction (makeSubCollections) 12 | 13 | This script contains the functions used to acquire, preprocess, and organize 14 | all of the Landsat and MODIS images over a given period of time. These 15 | functions should be run first, after defining some global variables. Functions 16 | in prep_functions.py and core_functions.py follow these. An example of 17 | the use of these functions can be found in Predict_L8.py. 18 | 19 | 20 | The MIT License 21 | 22 | Copyright © 2021 Ty Nietupski 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy of 25 | this software and associated documentation files (the "Software"), to deal in 26 | the Software without restriction, including without limitation the rights to 27 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 28 | of the Software, and to permit persons to whom the Software is furnished to do 29 | so, subject to the following conditions: 30 | 31 | The above copyright notice and this permission notice shall be included in all 32 | copies or substantial portions of the Software. 33 | 34 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 35 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 37 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 38 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 39 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 40 | SOFTWARE. 41 | 42 | """ 43 | 44 | import ee 45 | 46 | ############################################################################## 47 | # MASKING, INDEX CALCULATION, L5 & L7 TO L8 HARMONIZATION 48 | ############################################################################## 49 | 50 | 51 | def maskLandsat(image): 52 | """ 53 | Mask cloud, shadow, and snow with fmask and append the percent of pixels \ 54 | masked as new image property. 55 | 56 | Parameters 57 | ---------- 58 | image : image.Image 59 | Landsat image with qa band. 60 | 61 | Returns 62 | ------- 63 | image.image 64 | Masked landsat image with CloudSnowMaskedPercent property. 65 | 66 | """ 67 | # Bits 3 and 5 are cloud shadow and cloud, respectively. 4 is snow 68 | cloudShadowBitMask = 1 << 3 69 | cloudsBitMask = 1 << 5 70 | snowBitMask = 1 << 4 71 | 72 | # Get the pixel QA band. 73 | qa = image.select('pixel_qa') 74 | 75 | # make mask 76 | mask = qa.bitwiseAnd(cloudShadowBitMask).eq(0) \ 77 | .And(qa.bitwiseAnd(cloudsBitMask).eq(0)) \ 78 | .And(qa.bitwiseAnd(snowBitMask).eq(0)) 79 | 80 | # mask the mask with the mask... 81 | maskedMask = mask.updateMask(mask) 82 | 83 | # count the number of nonMasked pixels 84 | maskedCount = maskedMask.select(['pixel_qa']) \ 85 | .reduceRegion(reducer=ee.Reducer.count(), 86 | geometry=image.geometry(), 87 | scale=ee.Number(30), 88 | maxPixels=ee.Number(4e10)) 89 | 90 | # count the total number of pixels 91 | origCount = image.select(['blue']) \ 92 | .reduceRegion(reducer=ee.Reducer.count(), 93 | geometry=image.geometry(), 94 | scale=ee.Number(30), 95 | maxPixels=ee.Number(4e10)) 96 | 97 | # calculate the percent of masked pixels 98 | percent = ee.Number(origCount.get('blue')) \ 99 | .subtract(maskedCount.get('pixel_qa')) \ 100 | .divide(origCount.get('blue')) \ 101 | .multiply(100) \ 102 | .round() 103 | 104 | # Return the masked image with new property and time stamp 105 | return image.updateMask(mask) \ 106 | .set('CloudSnowMaskedPercent', percent) \ 107 | .copyProperties(image, ["system:time_start"]) 108 | 109 | 110 | def maskMODIS(image): 111 | """ 112 | Mask snow covered and extremely high albedo areas from the modis images. 113 | 114 | Parameters 115 | ---------- 116 | image : image.Image 117 | MODIS image. 118 | 119 | Returns 120 | ------- 121 | image.image 122 | Masked MODIS image. 123 | 124 | """ 125 | # calculate snow water index for the image 126 | swi = image.expression( 127 | '(green * (nir - swir1)) / ((green + nir) * (nir + swir1))', 128 | {'green': image.select(['green']), 129 | 'nir': image.select(['nir']), 130 | 'swir1': image.select(['swir1']) 131 | }).rename('swi') 132 | 133 | # mask out values of swi above 0.1 134 | mask = swi.lt(0.1) 135 | 136 | return image \ 137 | .updateMask(mask) \ 138 | .copyProperties(image, ['system:time_start', 'system:id']) 139 | 140 | 141 | def addNDVI(image): 142 | """ 143 | Mask snow covered and extremely high albedo areas from the modis images. 144 | 145 | Parameters 146 | ---------- 147 | image : image.Image 148 | Landsat or MODIS image with bands named 'nir' and 'red'. 149 | 150 | Returns 151 | ------- 152 | image.image 153 | Image with additional NDVI band. 154 | """ 155 | # calculate NDVI 156 | ndvi = image.normalizedDifference(['nir', 'red']).select(['nd'], ['ndvi']) 157 | 158 | return image.addBands(ndvi) 159 | 160 | 161 | def etmToOli(img): 162 | """ 163 | Calibrate the NDVI values so that they are more similar to OLI NDVI. 164 | 165 | Parameters 166 | ---------- 167 | img : image.Image 168 | Landsat 5 or 7 image. 169 | 170 | Returns 171 | ------- 172 | image.image 173 | Adjusted Landsat image. 174 | 175 | """ 176 | # coefficients from Roy et al. 2016 177 | coefficients = {'beta_0': ee.Image.constant([0.0235]), 178 | 'beta_1': ee.Image.constant([0.9723])} 179 | 180 | return img \ 181 | .multiply(coefficients['beta_1']) \ 182 | .add(coefficients['beta_0']) \ 183 | .toFloat() \ 184 | .copyProperties(img, ["system:time_start", 'system:id', 'DOY']) 185 | 186 | 187 | ############################################################################## 188 | # FILTER AND PAIR IMAGES 189 | ############################################################################## 190 | 191 | 192 | def getPaired(startDate, endDate, 193 | landsatCollection, landsatBands, bandNamesLandsat, 194 | modisCollection, modisBands, bandNamesModis, 195 | commonBandNames, 196 | region): 197 | """ 198 | Create a list of image collections. Landsat and MODIS with low cloud cover\ 199 | from the same date and the MODIS images between these pairs. 200 | 201 | Parameters 202 | ---------- 203 | startDate: str 204 | Start date of fusion timeframe. 205 | endDate: str 206 | End date of the fusion timeframe. 207 | landsatCollection: str 208 | Landsat collection https://developers.google.com/earth-engine/datasets 209 | landsatBands: ee_list.List 210 | List of integers corresponding to Landsat bands. 211 | bandNamesLandsat: ee_list.List 212 | List of strings used to rename bands. 213 | modisCollection: str 214 | MODIS collection https://developers.google.com/earth-engine/datasets 215 | modisBands: ee_list.List 216 | List of integers corresponding to MODIS bands in same order as Landsat. 217 | bandNamesModis: ee_list.List 218 | List of strings used to rename bands. 219 | commonBandNames: ee_list.List 220 | List of bands to use in fusion. Common to both Landsat and MODIS. 221 | region: geometry.Geometry 222 | Location to use in filtering collections. Must not be in scene overlap. 223 | 224 | Returns 225 | ------- 226 | python list obejct 227 | Each element in this list is an ee.ImageCollection. The first and \ 228 | second elements are the Landsat occuring on the same date and \ 229 | the last element is the MODIS images between each of the pair \ 230 | dates. 231 | 232 | """ 233 | if landsatCollection == 'LANDSAT/LC08/C01/T1_SR': 234 | # get landsat images 235 | landsat = ee.ImageCollection(landsatCollection) \ 236 | .filterDate(startDate, endDate) \ 237 | .filterBounds(region) \ 238 | .filterMetadata('CLOUD_COVER', 'less_than', 5) \ 239 | .select(landsatBands, bandNamesLandsat) \ 240 | .map(addNDVI) \ 241 | .map(maskLandsat) \ 242 | .filterMetadata('CloudSnowMaskedPercent', 'less_than', 50)\ 243 | .map(lambda image: image \ 244 | .setMulti({ 245 | 'system:time_start': 246 | ee.Date(image.date().format('y-M-d')) \ 247 | .millis(), 248 | 'DOY': image.date().format('D') 249 | })) \ 250 | .select(commonBandNames) 251 | else: 252 | # get landsat images 253 | landsat = ee.ImageCollection(landsatCollection) \ 254 | .filterDate(startDate, endDate) \ 255 | .filterBounds(region) \ 256 | .filterMetadata('CLOUD_COVER', 'less_than', 5) \ 257 | .select(landsatBands, bandNamesLandsat) \ 258 | .map(addNDVI) \ 259 | .map(maskLandsat) \ 260 | .filterMetadata('CloudSnowMaskedPercent', 'less_than', 50)\ 261 | .map(lambda image: image \ 262 | .setMulti({ 263 | 'system:time_start': 264 | ee.Date(image.date().format('y-M-d')) \ 265 | .millis(), 266 | 'DOY': image.date().format('D') 267 | })) \ 268 | .select(commonBandNames) \ 269 | .map(etmToOli) 270 | 271 | # get modis images 272 | modis = ee.ImageCollection(modisCollection) \ 273 | .filterDate(startDate, endDate) \ 274 | .select(modisBands, bandNamesModis) \ 275 | .map(addNDVI) \ 276 | .map(maskMODIS) \ 277 | .map(lambda image: image.set('DOY', image.date().format('D'))) \ 278 | .select(commonBandNames) 279 | 280 | # filter the two collections by the date property 281 | dayfilter = ee.Filter.equals(leftField='system:time_start', 282 | rightField='system:time_start') 283 | 284 | # define simple join 285 | pairedJoin = ee.Join.simple() 286 | # define inverted join to find modis images without landsat pair 287 | invertedJoin = ee.Join.inverted() 288 | 289 | # create collections of paired landsat and modis images 290 | landsatPaired = pairedJoin.apply(landsat, modis, dayfilter) 291 | modisPaired = pairedJoin.apply(modis, landsat, dayfilter) 292 | modisUnpaired = invertedJoin.apply(modis, landsat, dayfilter) 293 | 294 | return [landsatPaired, modisPaired, modisUnpaired] 295 | 296 | 297 | ############################################################################## 298 | # CREATE SUBCOLLECTIONS FOR EACH SET OF LANDSAT/MODIS PAIRS 299 | ############################################################################## 300 | 301 | 302 | def getDates(image, empty_list): 303 | """ 304 | Get date from image and append to list. 305 | 306 | Parameters 307 | ---------- 308 | image : image.Image 309 | Any earth engine image. 310 | empty_list : ee_list.List 311 | Earth engine list object to append date to. 312 | 313 | Returns 314 | ------- 315 | updatelist : ee_list.List 316 | List with date appended to the end. 317 | 318 | """ 319 | # get date and update format 320 | date = ee.Image(image).date().format('yyyy-MM-dd') 321 | 322 | # add date to 'empty list' 323 | updatelist = ee.List(empty_list).add(date) 324 | 325 | return updatelist 326 | 327 | 328 | def makeSubcollections(paired): 329 | """ 330 | Reorganize the list of collections into a list of lists of lists. Each\ 331 | list within the list will contain 3 lists. The first of these three will\ 332 | have the earliest and latest Landsat images. The second list will have the\ 333 | earliest and latest MODIS images. The third list will have all the MODIS\ 334 | images between the earliest and latest pairs.\ 335 | (e.g. L8 on 05/22/2017 & 06/23/2017, MOD 05/23/2017 & 06/23/2017,\ 336 | MOD 05/23/2017 through 06/22/2017). 337 | 338 | Parameters 339 | ---------- 340 | paired : python List 341 | List of image collections. 1. Landsat pairs, 2. MODIS pairs, and\ 342 | 3. MODIS between each of the pairs. 343 | 344 | Returns 345 | ------- 346 | ee_list.List 347 | List of lists of lists. 348 | 349 | """ 350 | def getSub(ind): 351 | """ 352 | Local function to create individual subcollection. 353 | 354 | Parameters 355 | ---------- 356 | ind : int 357 | Element of the list to grab. 358 | 359 | Returns 360 | ------- 361 | ee_list.List 362 | List of pairs lists for prediction 2 pairs and images between. 363 | 364 | """ 365 | # get landsat images 366 | lan_01 = paired[0] \ 367 | .filterDate(ee.List(dateList).get(ind), 368 | ee.Date(ee.List(dateList).get(ee.Number(ind).add(1)))\ 369 | .advance(1, 'day')) \ 370 | .toList(2) 371 | # get modis paired images 372 | mod_01 = paired[1] \ 373 | .filterDate(ee.List(dateList).get(ind), 374 | ee.Date(ee.List(dateList).get(ee.Number(ind).add(1)))\ 375 | .advance(1, 'day')) \ 376 | .toList(2) 377 | # get modis images between these two dates 378 | mod_p = paired[2] \ 379 | .filterDate(ee.List(dateList).get(ind), 380 | ee.Date(ee.List(dateList).get(ee.Number(ind).add(1)))\ 381 | .advance(1, 'day')) 382 | 383 | mod_p = mod_p.toList(mod_p.size()) 384 | 385 | # combine collections to one object 386 | subcollection = ee.List([lan_01, mod_01, mod_p]) 387 | 388 | return subcollection 389 | 390 | # empty list to store dates 391 | empty_list = ee.List([]) 392 | 393 | # fill empty list with dates 394 | dateList = paired[0].iterate(getDates, empty_list) 395 | 396 | # filter out sub collections from paired and unpaired collections 397 | subcols = ee.List.sequence(0, ee.List(dateList).length().subtract(2))\ 398 | .map(getSub) 399 | 400 | return subcols 401 | -------------------------------------------------------------------------------- /GEE_ImageFusion/prep_functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Author: Ty Nietupski (ty.nietupski@oregonstate.edu) 4 | 5 | Functions to perform some additional preprocessing and image prep: 6 | - co-registration (registerImages) 7 | - threshold calculation and threshold based masking (Thresh, ThreshMask) 8 | - landsat and MODIS format preparation (prepLandsat, prepMODIS) 9 | - some functions used within these functions 10 | 11 | These functions should be run after those found in get_paired_collections.py 12 | and before core_functions.py. An example of the use of these functions can 13 | be found in Predict_l8.py. 14 | 15 | 16 | The MIT License 17 | 18 | Copyright © 2021 Ty Nietupski 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy of 21 | this software and associated documentation files (the "Software"), to deal in 22 | the Software without restriction, including without limitation the rights to 23 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 24 | of the Software, and to permit persons to whom the Software is furnished to do 25 | so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in all 28 | copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. 37 | 38 | """ 39 | 40 | import ee 41 | 42 | 43 | def registerImages(landsat_t01, modis_t01, modis_tp): 44 | """ 45 | Register each image to the earliest (t0) landsat image in the set of pairs. 46 | 47 | Parameters 48 | ---------- 49 | landsat_t01 : ee_list.List 50 | Landsat images, earliest (t0) and latest (t1). 51 | modis_t01 : ee_list.List 52 | MODIS images, earliest (t0) and latest (t1). 53 | modis_tp : ee_list.List 54 | MODIS images, evey image between t0 and t1. 55 | 56 | Returns 57 | ------- 58 | landsat_t01 : ee_list.List 59 | Resampled version of input. 60 | modis_t01 : ee_list.List 61 | Resampled, registered version of input (registered to Landsat t0). 62 | modis_tp : ee_list.List 63 | Resampled, registered version of input (registered to Landsat t0). 64 | 65 | """ 66 | # resample 67 | landsat_t01 = landsat_t01.map(lambda image: 68 | ee.Image(image).resample('bicubic')) 69 | 70 | modis_t01 = modis_t01.map(lambda image: 71 | ee.Image(image).resample('bicubic')) 72 | 73 | modis_tp = modis_tp.map(lambda image: 74 | ee.Image(image).resample('bicubic')) 75 | 76 | # register MODIS to landsat t0 77 | modis_t01 = modis_t01.map(lambda image: 78 | ee.Image(image)\ 79 | .register(referenceImage= 80 | ee.Image(landsat_t01.get(0)), 81 | maxOffset=ee.Number(150.0), 82 | stiffness=ee.Number(7.0))) 83 | 84 | modis_tp = modis_tp.map(lambda image: 85 | ee.Image(image)\ 86 | .register(referenceImage= 87 | ee.Image(landsat_t01.get(0)), 88 | maxOffset=ee.Number(150.0), 89 | stiffness=ee.Number(7.0))) 90 | 91 | return landsat_t01, modis_t01, modis_tp 92 | 93 | 94 | def threshold(landsat, coverClasses): 95 | """ 96 | Determine similarity threshold for each landsat image based on the number\ 97 | of cover classes. 98 | 99 | Parameters 100 | ---------- 101 | landsat : ee_list.List 102 | List of landsat images to determine threshold. 103 | coverClasses : int 104 | Number of cover classes in region. 105 | 106 | Returns 107 | ------- 108 | ee_list.List 109 | ee_list.List 110 | 111 | 112 | """ 113 | def getThresh(image): 114 | """ 115 | Local function to determine thresholds for each band of the landsat\ 116 | image. 117 | 118 | Parameters 119 | ---------- 120 | image : image.Image 121 | Landsat image. 122 | 123 | Returns 124 | ------- 125 | thresh : image.Image 126 | Image with constant bands for the threshold associated with each\ 127 | band. 128 | 129 | """ 130 | # calculate the standard deviation for each band within image 131 | stddev = ee.Image(image).reduceRegion(reducer=ee.Reducer.stdDev(), 132 | bestEffort=True, 133 | maxPixels=ee.Number(1e6)) 134 | # convert stddev dictionary to multiband image 135 | stddev = stddev.toImage() 136 | 137 | # get our band names from the image and rename for threshold 138 | names = stddev.bandNames() \ 139 | .map(lambda bn: ee.String(bn).cat('_thresh')) 140 | 141 | # calculate the threshold from stddev and number of landcover classes 142 | thresh = stddev.multiply(ee.Image.constant(ee.Number(2)) \ 143 | .divide(coverClasses)) 144 | thresh = thresh.rename(names) 145 | 146 | return thresh 147 | 148 | threshs = ee.List(landsat).map(getThresh) 149 | 150 | return threshs 151 | 152 | 153 | def threshMask(neighLandsat_t01, thresh, commonBandNames): 154 | """ 155 | Use thresholds calculated with the threshold function to mask pixels in\ 156 | the neighborhood images that are dissimilar. 157 | 158 | Parameters 159 | ---------- 160 | neighLandsat_t01 : ee_list.List 161 | List format of the neighborhood landsat images at t0 and t1. 162 | thresh : ee_list.List 163 | List of the threshold images for t0 and t1. 164 | commonBandNames : ee_list.List 165 | List of the band names to use in selecting bands. 166 | 167 | Returns 168 | ------- 169 | masks : ee_list.List 170 | Landsat neighborhood images with dissimilar pixels masked. 171 | 172 | """ 173 | masks = ee.List([0, 1]) \ 174 | .map(lambda i: 175 | commonBandNames \ 176 | .map(lambda name: 177 | # get t0 or t1 neighborhood image and calc distance from 178 | # central pixel, if over threshold, mask the pixel 179 | ee.Image(neighLandsat_t01.get(i)) \ 180 | .select([ee.String(name).cat('_(.+)')]) \ 181 | .select([ee.String(name).cat('_0_0')]) \ 182 | .subtract(ee.Image(neighLandsat_t01.get(i)) \ 183 | .select([ee.String(name).cat('_(.+)')])) \ 184 | .abs() \ 185 | .lte(ee.Image(thresh.get(i)) \ 186 | .select([ee.String(name).cat('_(.+)')])))) 187 | 188 | return masks 189 | 190 | 191 | def prepMODIS(modis_t01, modis_tp, kernel, 192 | numPixels, commonBandNames, pixelBandNames): 193 | """ 194 | Convert MODIS images to neighborhood images and reorganize so that they\ 195 | are in a format that will work with the 'core functions' 196 | 197 | Parameters 198 | ---------- 199 | modis_t01 : ee_list.List 200 | MODIS images at time 0 (t0) and time 1 (t1). 201 | modis_tp : ee_list.List 202 | MODIS images between earliest and latest pairs. 203 | kernel : Kernel 204 | Kernel object used to create neighborhood image (window). 205 | numPixels : ee_number.Number 206 | Total number of pixels in the kernel. 207 | commonBandNames : ee_list.List 208 | Names of bands to use in fusion. 209 | pixelBandNames : ee_list.List 210 | Names of bands that will be generated when the images are converted\ 211 | to neighborhood images. 212 | 213 | Returns 214 | ------- 215 | modSorted_t01 : ee_list.List 216 | The converted, reorganized neighborhood images for t0 and t1. 217 | modSorted_tp : ee_list.List 218 | The converted, reorganized neighborhood images between t0 and t1. 219 | 220 | """ 221 | # convert images to neighborhood images 222 | neighMod_t01 = modis_t01 \ 223 | .map(lambda image: ee.Image(image).neighborhoodToBands(kernel)) 224 | neighMod_tp = modis_tp \ 225 | .map(lambda image: ee.Image(image).neighborhoodToBands(kernel)) 226 | 227 | # convert into an array image 228 | modArr = ee.Image(neighMod_t01.get(0)).toArray() \ 229 | .arrayCat(ee.Image(neighMod_t01.get(1)).toArray(), 0) 230 | 231 | # create list of arrays sliced by pixel position 232 | modPixArrays_t01 = ee.List.sequence(0, numPixels.subtract(1)) \ 233 | .map(lambda i: 234 | modArr.arraySlice(0, 235 | ee.Number(i).int(), 236 | numPixels.multiply(commonBandNames.length()\ 237 | .multiply(2)).int(), 238 | numPixels)) 239 | 240 | modPixArrays_tp = neighMod_tp \ 241 | .map(lambda image: 242 | ee.List.sequence(0, numPixels.subtract(1)) \ 243 | .map(lambda i: 244 | ee.Image(image) \ 245 | .toArray() \ 246 | .arraySlice(0, 247 | ee.Number(i).int(), 248 | numPixels.multiply(commonBandNames\ 249 | .length()).int(), 250 | numPixels))) 251 | 252 | # flatten arrays and name based on doy, band, and pixel position 253 | modSorted_t01 = ee.List.sequence(0, numPixels.subtract(1)) \ 254 | .map(lambda i: 255 | ee.Image(modPixArrays_t01.get(i)) \ 256 | .arrayFlatten([pixelBandNames.get(i)])) 257 | 258 | modSorted_tp = ee.List.sequence(0, modPixArrays_tp.length().subtract(1)) \ 259 | .map(lambda i: 260 | ee.List.sequence(0, numPixels.subtract(1)) \ 261 | .map(lambda x: 262 | ee.Image(ee.List(modPixArrays_tp.get(i)).get(x)) \ 263 | .arrayFlatten([commonBandNames]) \ 264 | .set('DOY', ee.Image(modis_tp.get(i)).get('DOY')))) 265 | 266 | return modSorted_t01, modSorted_tp 267 | 268 | 269 | def prepLandsat(landsat_t01, kernel, 270 | numPixels, commonBandNames, 271 | doys, coverClasses): 272 | """ 273 | Convert Landsat images to neighborhood images, mask dissimilar pixels,\ 274 | and reorganize so that they are in a format that will work with the\ 275 | 'core functions'. 276 | 277 | Parameters 278 | ---------- 279 | landsat_t01 : ee_list.List 280 | Landsat images at time 0 (t0) and time 1 (t1). 281 | kernel : Kernel 282 | Kernel object used to create neighborhood image (window).. 283 | numPixels : ee_number.Number 284 | Total number of pixels in the kernel. 285 | commonBandNames : ee_list.List 286 | Names of bands to use in fusion. 287 | doys : ee_list.List 288 | List of day of year associated with t0 and t1. 289 | coverClasses : int 290 | Number of cover classes in region. 291 | 292 | Returns 293 | ------- 294 | maskedLandsat : ee_list.List 295 | The converted, masked, reorganized neighborhood images for t0 and t1. 296 | pixPositions : ee_list.List 297 | Names for each index in the window (e.g., "_0_0", "_0_1" ...). 298 | pixelBandNames : ee_list.List 299 | Names of bands that will be generated when the images are converted\ 300 | to neighborhood images. 301 | 302 | """ 303 | # convert images to neighborhood images 304 | neighLandsat_t01 = landsat_t01 \ 305 | .map(lambda image: ee.Image(image).neighborhoodToBands(kernel)) 306 | 307 | # create list of pixel postions 308 | pixPositions = ee.Image(neighLandsat_t01.get(0)).bandNames() \ 309 | .map(lambda bn: ee.String(bn).replace('[a-z]+_', '_')) \ 310 | .slice(0, numPixels) 311 | 312 | # create list of band names to rename output arrays 313 | pixelBandNames = pixPositions \ 314 | .map(lambda position: 315 | doys.map(lambda doy: 316 | commonBandNames.map(lambda bn: 317 | ee.String(doy).cat('_') \ 318 | .cat(ee.String(bn)) \ 319 | .cat(ee.String(position))))) \ 320 | .map(lambda l: ee.List(l).flatten()) 321 | 322 | # convert to array and sort 323 | # essentially we would have the array values stacked with 324 | # time 0 on top of time 1 325 | # ie 1 column of values for pix positions at both times 326 | lanArr_t01 = ee.Image(neighLandsat_t01.get(0)).toArray() \ 327 | .arrayCat(ee.Image(neighLandsat_t01.get(1)).toArray(), 0) 328 | 329 | pixArrays = ee.List([]) 330 | pixArrays = ee.List.sequence(0, numPixels.subtract(1)) \ 331 | .map(lambda i: lanArr_t01 \ 332 | .arraySlice(0, 333 | ee.Number(i).int(), 334 | numPixels.multiply(commonBandNames \ 335 | .length() \ 336 | .multiply(2)).int(), 337 | numPixels)) 338 | 339 | # flatten arrays and name based on doy, band, and pixel position 340 | lanSorted = ee.List.sequence(0, numPixels.subtract(1))\ 341 | .map(lambda i: 342 | ee.Image(pixArrays.get(i)).arrayFlatten([pixelBandNames.get(i)])) 343 | 344 | # determine threshold for images 345 | thresh = threshold(landsat_t01, coverClasses) 346 | 347 | # mask window images with thresholds 348 | mask_t01 = threshMask(neighLandsat_t01, thresh, commonBandNames) 349 | 350 | # convert list of masks of lists of masks to image and then to array 351 | maskArr_t01 = ee.ImageCollection(mask_t01.flatten()) \ 352 | .toBands() \ 353 | .toArray() 354 | 355 | maskArrays = ee.List.sequence(0, numPixels.subtract(1))\ 356 | .map(lambda i: 357 | maskArr_t01 \ 358 | .arraySlice(0, ee.Number(i).int(), 359 | numPixels.multiply(commonBandNames.length()\ 360 | .multiply(2)).int(), 361 | numPixels)) 362 | 363 | # flatten mask arrays and name based on doy, band, and pixel position 364 | masksSorted = ee.List.sequence(0, numPixels.subtract(1))\ 365 | .map(lambda i: 366 | ee.Image(maskArrays.get(i)).arrayFlatten([pixelBandNames.get(i)])) 367 | 368 | # mask landsat images 369 | maskedLandsat = ee.List.sequence(0, numPixels.subtract(1))\ 370 | .map(lambda index: 371 | ee.Image(lanSorted.get(index)) \ 372 | .updateMask(ee.Image(masksSorted.get(index)))) 373 | 374 | return maskedLandsat, pixPositions, pixelBandNames 375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | There are a total of four scripts here that can be used to automate large image fusion tasks in GEE. The 3 scripts in the GEE_ImageFusion folder execute different parts of the GEE image fusion process. If using anaconda as your package manager, you can create a virtual environment for gee (https://developers.google.com/earth-engine/guides/python_install) and drop the GEE_ImageFusion folder into the site-packages directory for that environment. From there, the module can be imported into a script as any other module in that virtual environment would be. Thefour scripts within the module handle different aspects of the image fusion process. It is reccommended that you examine the Predict_L8 script first to get an idea of a possible workflow on how to use each of the functions in the module. If only interested in using functions from one of the submodules, these can be individually imported (e.g., from GEE_ImageFusion import core_functions). Each script is described below. 3 | 4 | ## File descriptions 5 | 1. **Predict_L8.py**- this script shows an example workflow of using the functions in the GEE_ImageFusion module to automate a large image fusion task. 6 | 7 | 2. **get_paired_collections.py**- this script contains functions to retrieve, filter, mask, sort, and organize the Landsat and MODIS data. 8 | 9 | 3. **prep_functions.py**- this script contains functions to preprocess the Landsat and MODIS imagery. It has functions to perform a co-registration step, determine and mask similar pixels, and convert images to 'neighborhood' images with bands that are sorted in the necessary order for the core functions. 10 | 11 | 4. **core_functions.py**- this script contains the main functions needed to perform image fusion. If all images have been preprocessed and formatted correctly then these functions can be run to predict new images at times when only MODIS is available. Functions include spectral distance to similar pixels, spatial distance, weight calculation, conversion coefficient calculation, and prediction. 12 | 13 | ## Example time series MODIS vs. GEE image fusion 14 | [![MODIS vs. GEE image fusion](https://img.youtube.com/vi/v9F71tuqozY/maxresdefault.jpg)](https://www.youtube.com/watch?v=v9F71tuqozY) 15 | 16 | ## License 17 | MIT 18 | --------------------------------------------------------------------------------