├── .gitignore ├── gee_landtrendr ├── tests │ ├── __init__.py │ ├── __init__.pyc │ ├── landtrendr.pyc │ └── landtrendr.py ├── __init__.pyc ├── landtrendr.pyc ├── __init__.py ├── ipytools.py ├── classification.py └── landtrendr.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /gee_landtrendr/tests/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' Test modules ''' -------------------------------------------------------------------------------- /gee_landtrendr/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/pygeeltr/HEAD/gee_landtrendr/__init__.pyc -------------------------------------------------------------------------------- /gee_landtrendr/landtrendr.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/pygeeltr/HEAD/gee_landtrendr/landtrendr.pyc -------------------------------------------------------------------------------- /gee_landtrendr/tests/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/pygeeltr/HEAD/gee_landtrendr/tests/__init__.pyc -------------------------------------------------------------------------------- /gee_landtrendr/tests/landtrendr.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/pygeeltr/HEAD/gee_landtrendr/tests/landtrendr.pyc -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from gee_landtrendr.tests import landtrendr 4 | import unittest 5 | import sys 6 | 7 | if __name__ == "__main__": 8 | if len(sys.argv) > 3: 9 | landtrendr.LandTrendr.FOLDER = sys.argv.pop() 10 | landtrendr.LandTrendr.USER = sys.argv.pop() 11 | 12 | unittest.main(landtrendr) -------------------------------------------------------------------------------- /gee_landtrendr/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | ''' Package to apply LandTrendr in Google Earth Engine ''' 4 | 5 | from __future__ import absolute_import, division, print_function 6 | 7 | __all__ = ( 8 | "__title__", "__summary__", "__uri__", "__version__", "__author__", 9 | "__email__", "__license__", "__copyright__", 10 | ) 11 | 12 | __title__ = "gee-landtrendr" 13 | __summary__ = "Apply LandTrendr Algorithm in Google Earth Engine time series" 14 | __uri__ = "" 15 | 16 | __version__ = "0.0.2" 17 | 18 | __author__ = "Rodrigo E. Principe" 19 | __email__ = "rprincipe@ciefap.org.ar" 20 | 21 | __license__ = "GNU GENERAL PUBLIC LICENSE, Version 3" 22 | __copyright__ = "Rodrigo E. Principe" -------------------------------------------------------------------------------- /gee_landtrendr/ipytools.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ Module with some tools for IPython Jupyter Notebook and Lab """ 3 | 4 | from ipywidgets import HTML, Accordion, VBox 5 | from geetools import chart 6 | import ee 7 | ee.Initialize() 8 | 9 | def add2map(landtrendr, map): 10 | """ add a Tab to Map (geetools.ipymap) to plot a LandTrendr result 11 | 12 | :param landtrendr: a landtrendr object 13 | :type landtrendr: landtrendr.LandTrendr 14 | """ 15 | # TODO: make it async so it does not block other widgets 16 | bands = [landtrendr.fit_band+'_fit', landtrendr.fit_band] 17 | slope = landtrendr.slope#.select(bands) 18 | 19 | def handler(**kwargs): 20 | event = kwargs['type'] 21 | coords = kwargs['coordinates'] 22 | wid = kwargs['widget'] 23 | 24 | if event == 'click': 25 | wait = HTML('Loading Chart for point {}..'.format(coords)) 26 | wid.children = [wait] 27 | 28 | region = ee.Geometry.Point(coords) 29 | 30 | try: 31 | ch = chart.Image.series(slope, region, 32 | bands=bands) 33 | ch.title = 'LandTrendr fit\n for point {}'.format(coords) 34 | chart_wid = ch.render_widget() 35 | except Exception as e: 36 | chart_wid = HTML('{}'.format(e)) 37 | 38 | wid.children = [chart_wid] 39 | 40 | widget = VBox() 41 | 42 | map.addTab('LandTrendr', handler, widget) 43 | 44 | 45 | -------------------------------------------------------------------------------- /gee_landtrendr/tests/landtrendr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | from .. import landtrendr 5 | import ee 6 | import pprint 7 | ee.Initialize() 8 | 9 | pp = pprint.PrettyPrinter(indent=2) 10 | 11 | try: 12 | from geetools import tools 13 | exist_geetools = True 14 | except: 15 | exist_geetools = False 16 | 17 | 18 | class LandTrendr(unittest.TestCase): 19 | 20 | FOLDER = None 21 | USER = None 22 | 23 | def output(self, image, name, vis_params): 24 | 25 | folder = LandTrendr.FOLDER if LandTrendr.FOLDER else 'test_landtrendr' 26 | 27 | path = 'users/{}/{}/{}'.format(LandTrendr.USER, folder, name) 28 | 29 | if LandTrendr.USER: 30 | task = ee.batch.Export.image.toAsset(image, name, 31 | path, 32 | region=self.region, 33 | scale=30) 34 | task.start() 35 | else: 36 | visimage = image.visualize(**vis_params) 37 | url = visimage.getThumbUrl({'region':self.region}) 38 | 39 | print(name, url) 40 | 41 | def output_collection(self, collection, name): 42 | folder = LandTrendr.FOLDER if LandTrendr.FOLDER else 'test_landtrendr' 43 | 44 | path = 'users/{}/{}/{}'.format(LandTrendr.USER, folder, name) 45 | 46 | if LandTrendr.USER: 47 | tools.col2asset(collection, path, region=self.region) 48 | 49 | def setUp(self): 50 | 51 | folder = LandTrendr.FOLDER if LandTrendr.FOLDER else 'test_landtrendr' 52 | # serie = LandTrendr.TIMESERIE if LandTrendr.TIMESERIE else 'test_time_serie' 53 | serie = 'test_time_serie' 54 | 55 | self.region = [[[-71.71, -42.77], 56 | [-71.71, -42.87], 57 | [-71.57, -42.87], 58 | [-71.57, -42.77]]] 59 | 60 | self.area = ee.Geometry.Polygon(self.region) # ee.Geometry.Polygon 61 | self.center = self.area.centroid() # ee.Geometry.Point 62 | 63 | time_serie = ee.ImageCollection('users/rprincipe/{}/{}'.format(folder, serie)) 64 | self.principe = landtrendr.LandTrendr.Principe(time_serie, 'nbr', self.area) 65 | self.kennedy = landtrendr.LandTrendr.Kennedy(time_serie, 'nbr', self.area) 66 | self.liang = landtrendr.LandTrendr.Liang(time_serie, 'nbr', self.area) 67 | 68 | def test_slope(self): 69 | slope = self.principe.slope() 70 | self.assertEqual(type(slope), ee.ImageCollection) 71 | 72 | self.output_collection(slope, 'test_slope_col') 73 | 74 | def test_statistics(self): 75 | stats = self.principe.statistics() 76 | r2 = stats.r2 77 | ssres = stats.ssres 78 | self.assertEqual(type(r2), ee.Image) 79 | self.assertEqual(type(ssres), ee.Image) 80 | 81 | vis_r2 = {'bands':'r2', 'min':0.2, 'max':0.95} 82 | 83 | self.output(r2, 'test_r2', vis_r2) 84 | 85 | def test_breakdown(self): 86 | bdown = self.principe.breakdown() 87 | self.assertEqual(type(bdown), list) 88 | 89 | image = bdown[0] 90 | bands = image.bandNames().getInfo() 91 | self.assertEqual(bands, ['nbr', 'nbr_fit', 'bkp', 'year']) 92 | 93 | self.output(image, 'test_breakdown', {'bands':['nbr_fit'], 'min':0, 'max':0.8}) 94 | self.output_collection(ee.ImageCollection(ee.List(bdown)), 'test_breakdown_col') 95 | 96 | def test_breakpoints(self): 97 | breaks = self.principe.breakpoints() 98 | image = breaks.image 99 | total = breaks.total 100 | 101 | self.assertEqual(type(image), ee.Image) 102 | self.assertEqual(type(total), ee.Number) 103 | self.assertEqual(type(total.getInfo()), int) 104 | 105 | self.output(image, 'test_breakpoints', {'bands':['n_bkp'], 'min':0, 'max':5}) 106 | 107 | def test_break2band(self): 108 | b2b = self.principe.break2band() 109 | 110 | self.assertEqual(type(b2b), ee.Image) 111 | if exist_geetools: 112 | values = tools.get_value(b2b, self.center, scale=30, side='client') 113 | pp.pprint(values) 114 | 115 | self.output(b2b, 'test_break2band', {'bands':['change_0', 'change_1', 'change_2'], 'min':0, 'max':1}) 116 | 117 | def test_stretches(self): 118 | stretches = self.principe.stretches() 119 | img_list = stretches.img_list 120 | unified = stretches.image 121 | one_image = img_list[0] 122 | 123 | self.assertEqual(type(one_image), ee.Image) 124 | self.assertEqual(type(unified), ee.Image) 125 | 126 | self.output(unified, 'test_stretches', {'bands':['t1_cat'],'min':1,'max':5}) 127 | self.output_collection(ee.ImageCollection(ee.List(img_list)), 'test_stretches_col') 128 | 129 | def test_stack(self): 130 | stack = self.principe.stack() 131 | 132 | self.assertEqual(type(stack), ee.Image) 133 | 134 | self.output(stack, 'test_stack', {'min':0, 'max':1}) 135 | 136 | def test_stable_pixels(self): 137 | stables = self.principe.stable_pixels() 138 | self.assertEqual(type(stables), ee.Image) 139 | self.output(stables, 'test_stables', {'bands':['viz-red', 140 | 'viz-green', 'viz-blue'], 'min':0, 'max':255}) 141 | 142 | def test_total_bkp(self): 143 | total_bkp = self.principe.total_bkp() 144 | self.assertEqual(type(total_bkp), ee.Image) 145 | self.output(total_bkp, 'test_total_bkp', {'bands':['total_bkp'], 146 | 'min':0, 'max':5}) 147 | 148 | def test_max_diff_gain(self): 149 | max_dif = self.principe.max_diff('gain') 150 | 151 | self.assertEqual(type(max_dif), ee.Image) 152 | self.output(max_dif, 'test_max_gain', {'bands':['max_change'], 153 | 'min':0, 'max':0.5}) 154 | 155 | def test_max_diff_loss(self): 156 | max_dif = self.principe.max_diff('loss') 157 | 158 | self.assertEqual(type(max_dif), ee.Image) 159 | self.output(max_dif, 'test_max_loss', {'bands':['max_change'], 160 | 'min':0, 'max':0.5}) 161 | 162 | 163 | def test_classify(self): 164 | classify = self.principe.classify() 165 | 166 | self.assertEqual(type(classify), ee.Image) 167 | self.output(classify, 'test_classify', {}) -------------------------------------------------------------------------------- /gee_landtrendr/classification.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ Module holding methods to classify landtrendr results """ 3 | 4 | import ee 5 | ee.Initialize() 6 | 7 | from geetools import tools 8 | from collections import namedtuple 9 | 10 | 11 | def classify(ltr, *args, **kwargs): 12 | """ Clasificación de los tramos 13 | 14 | :param args: Mismos argumentos que la función stretches() 15 | :param kwargs: Mismos argumentos que la funcion stretches() 16 | :return: 17 | """ 18 | 19 | tramos = stretches(*args, **kwargs) 20 | lista = tramos.img_list 21 | 22 | umbral1 = kwargs.get("min_threshold", 0.05) 23 | umbral2 = kwargs.get("max_threshold", 0.2) 24 | 25 | final = ee.Image.constant(0).select([0],["cat"]) 26 | 27 | for n, img in enumerate(lista): 28 | n += 1 29 | 30 | # img = tools.mask2zero(img) 31 | img = img.unmask() 32 | 33 | if n == 1: 34 | t1_cat = img.select("t1_cat") 35 | t1_duracion = img.select("t1_duration") 36 | t1_slope = img.select("t1_slope") 37 | 38 | t1_caida = t1_duracion.multiply(t1_slope).toFloat() 39 | cat = t1_caida.lte(-umbral1).multiply(3).select([0],["cat"]) 40 | # cat = t1_cat.eq(2).Or(t1_cat.eq(3)).multiply(3).select([0],["cat"]) 41 | final = final.add(cat) 42 | # continue 43 | break 44 | 45 | elif n == 2: 46 | t1_cat = img.select("t1_cat") 47 | t2_cat = img.select("t2_cat") 48 | t1_est = t1_cat.eq(1) 49 | t2_caida = t2_cat.eq(3) 50 | t2_suave = t2_cat.eq(2) 51 | 52 | cambio = t1_est.And(t2_caida) 53 | posible = t1_est.And(t2_suave).multiply(2) 54 | cat = cambio.add(posible).select([0],["cat"]) 55 | final = final.add(cat) 56 | # continue 57 | break 58 | 59 | cat0 = ee.Image.constant(0).select([0],["cat"]) 60 | 61 | for tramos in range(3, n+1): 62 | n1 = str(tramos-2) 63 | n2 = str(tramos-1) 64 | n3 = str(tramos) 65 | 66 | t1_cat = img.select("t{0}_cat".format(n1)) 67 | t2_cat = img.select("t{0}_cat".format(n2)) 68 | t3_cat = img.select("t{0}_cat".format(n3)) 69 | 70 | t2_slope = img.select("t{0}_slope".format(n2)) 71 | t2_duracion = img.select("t{0}_duration".format(n2)) 72 | 73 | t1_est = t1_cat.eq(1) 74 | t1_suave = t1_cat.eq(2) 75 | t1_caida = t1_cat.eq(3) 76 | t2_caida = t2_cat.eq(3) 77 | t2_suave = t2_cat.eq(2) 78 | t3_est = t3_cat.eq(1) 79 | t3_crec = t3_cat.eq(4) 80 | 81 | t2_mag = t2_slope.multiply(t2_duracion) 82 | 83 | cambio1 = t1_est.And(t2_caida).And(t3_est) # stable, 84 | cambio2 = t1_suave.And(t2_caida).And(t3_est) 85 | cambio3 = t1_est.And(t2_caida).And(t3_crec) 86 | cambio4 = t1_suave.And(t2_caida).And(t3_crec) 87 | cambio5 = t1_caida.And(t2_suave).And(t3_est) 88 | cambio6 = t1_caida.And(t2_suave).And(t3_crec) 89 | cambio7 = t1_caida.And(t2_caida).And(t3_est) 90 | cambio8 = t1_caida.And(t2_caida).And(t3_crec) 91 | 92 | cambio = cambio1.Or(cambio2).Or(cambio3).Or(cambio4).Or(cambio5).Or(cambio6).Or(cambio7).Or(cambio8) 93 | 94 | posible1 = t1_est.And(t2_suave).And(t3_est).multiply(2) 95 | # posible2 = t1_est.And(t2_mag.lte(-umbral2)).And(t3_est.Or(t3_crec)).multiply(2) 96 | 97 | posible = posible1#.Or(posible2) 98 | 99 | cat = cambio.add(posible).select([0],["cat"]) 100 | cat = tools.mask2zero(cat) 101 | 102 | cat0 = cat0.add(cat) 103 | 104 | final = final.add(cat0) 105 | 106 | return final 107 | 108 | 109 | def class1(ltr, umb_b=0.01, umb_m=0.05): 110 | """ Método para aplicar una clasificación al resultado de LandTendr 111 | 112 | :Parametros: 113 | :param umb_b: umbral para determinar el atributo "bajo", el cual 114 | va entre 0 y este umbral 115 | :type umb_b: float 116 | :param umb_m: umbral para determinar los atributos "moderado" y 117 | "alto", los cuales van entre umb_bajo y umb_mod, y mayor a umb_mod 118 | respectivamente 119 | :type umb_m: float 120 | 121 | :return: Una coleccion de imagenes, donde cada imagen tiene una 122 | banda de nombre "cat", que contiene la categoría, a saber: 123 | 124 | * cat 1: sin grandes cambios (azul) 125 | 126 | * cat 2: perdida suave (naranja) 127 | 128 | * cat 3: perdida alta (rojo) 129 | 130 | * cat 4: ganancia (o recuperacion) suave (amarillo) 131 | 132 | * cat 5: ganancia alta (verde) 133 | 134 | * y tres bandas para la visualizacion 135 | :rtype: ee.ImageCollection 136 | """ 137 | 138 | col = ltr.slope 139 | 140 | def categoria(img): 141 | d = img.get("system:time_start") 142 | adelante = ee.String("slope_after") 143 | atras = ee.String("slope_before") 144 | 145 | umb_bajo = ee.Image.constant(umb_b) 146 | umb_mod = ee.Image.constant(umb_m) 147 | 148 | at = ee.Image(img).select(atras) 149 | atAbs = at.abs() 150 | 151 | ad = ee.Image(img).select(adelante) 152 | adAbs = ad.abs() 153 | 154 | # INTENSIDAD QUIEBRE 155 | dif = ad.subtract(at) 156 | media = atAbs.add(adAbs).divide(2) 157 | 158 | # DIRECCION 159 | at_dec = at.lte(0) # atras decreciente? 160 | at_crec = at.gt(0) # atras creciente? 161 | 162 | ad_dec = ad.lte(0) # adelante decreciente? 163 | ad_crec = ad.gt(0) # adelante creciente? 164 | 165 | # OTRA OPCION DE CATEGORIAS 166 | 167 | # INTENSIDAD 168 | 169 | difAbs = dif.abs() 170 | 171 | menor_bajo = difAbs.lt(umb_bajo) 172 | menor_mod = difAbs.lt(umb_mod) 173 | mayor_bajo = difAbs.gte(umb_bajo) 174 | mayor_mod = difAbs.gte(umb_mod) 175 | 176 | int_baja = menor_bajo 177 | int_mod = mayor_bajo.And(menor_mod) 178 | int_alta = mayor_mod 179 | 180 | # int_baja = dif.abs().lt(umb_bajo) 181 | # int_mod = dif.abs().lt(umb_mod).And(dif.gte(umb_bajo)) 182 | # int_alta = dif.abs().gte(umb_mod) 183 | 184 | cat1 = int_baja # sin grandes cambios 185 | cat2 = int_mod.And(dif.lte(0)) # perdida suave 186 | cat3 = int_alta.And(dif.lte(0)) # perdida alta 187 | cat4 = int_mod.And(dif.gt(0)) # ganancia suave 188 | cat5 = int_alta.And(dif.gt(0)) # ganancia alta 189 | 190 | # ESCALO 191 | cat2 = cat2.multiply(2) 192 | cat3 = cat3.multiply(3) 193 | cat4 = cat4.multiply(4) 194 | cat5 = cat5.multiply(5) 195 | 196 | final = cat1.add(cat2).add(cat3).add(cat4).add(cat5) 197 | 198 | img = img.addBands( 199 | ee.Image(final).select([0], ["cat"])).addBands( 200 | ee.Image(at_dec).select([0], ["at_dec"])).addBands( 201 | ee.Image(at_crec).select([0], ["at_crec"])).addBands( 202 | ee.Image(ad_dec).select([0], ["ad_dec"])).addBands( 203 | ee.Image(ad_crec).select([0], ["ad_crec"])).addBands( 204 | ee.Image(dif).select([0], ["dif"])) 205 | 206 | mask = img.neq(0) 207 | 208 | return img.updateMask(mask).set("system:time_start", d) 209 | 210 | col = col.map(categoria) 211 | 212 | # VISUALIZACION DE IMG 213 | r = ee.String("viz-red") 214 | g = ee.String("viz-green") 215 | b = ee.String("viz-blue") 216 | 217 | def viz(imagen): 218 | d = imagen.get("system:time_start") 219 | img = ee.Image(imagen) 220 | 221 | ''' 222 | R G B 223 | 1 50, 25, 255 azul 224 | 2 255, 100, 50 naranja 225 | 3 255, 25, 25 rojo 226 | 4 255, 255, 50 amarillo 227 | 5 50, 150, 50 verde 228 | ''' 229 | 230 | cat1 = img.eq(1) 231 | r1 = cat1.multiply(50) 232 | g1 = cat1.multiply(25) 233 | b1 = cat1.multiply(255) 234 | 235 | cat2 = img.eq(2) 236 | r2 = cat2.multiply(255) 237 | g2 = cat2.multiply(100) 238 | b2 = cat2.multiply(50) 239 | 240 | cat3 = img.eq(3) 241 | r3 = cat3.multiply(255) 242 | g3 = cat3.multiply(25) 243 | b3 = cat3.multiply(25) 244 | 245 | cat4 = img.eq(4) 246 | r4 = cat4.multiply(255) 247 | g4 = cat4.multiply(255) 248 | b4 = cat4.multiply(50) 249 | 250 | cat5 = img.eq(5) 251 | r5 = cat5.multiply(50) 252 | g5 = cat5.multiply(150) 253 | b5 = cat5.multiply(50) 254 | 255 | red = r1.add(r2).add(r3).add(r4).add(r5).select([0], [r]).toUint8() 256 | 257 | green = (g1.add(g2).add(g3).add(g4).add(g5) 258 | .select([0], [g]).toUint8()) 259 | 260 | blue = (b1.add(b2).add(b3).add(b4).add(b5) 261 | .select([0], [b]).toUint8()) 262 | 263 | return img.addBands(ee.Image([red, green, blue])).set( 264 | "system:time_start", d) 265 | 266 | col = col.select("cat").map(viz) 267 | 268 | return col 269 | 270 | 271 | def classIncendio(ltr, umbral=0.05, agrupar=False): 272 | """ Método para aplicar una clasificación al resultado de LandTendr 273 | 274 | :parameter: 275 | :param umbral: umbra para |pendiente_posterior - pendiente_anterior| 276 | si pasa este umbral se considera 'cambio' 277 | :type umbral: float 278 | 279 | :param agrupar: agrupar pixeles 280 | :type agrupar: bool 281 | 282 | :return: Una coleccion de imagenes, donde cada imagen tiene una banda de 283 | nombre 'cat', que contiene la categoría, a saber: 284 | 285 | **cat 0**: No incendio 286 | 287 | **cat 1**: posible incendio (perdida y ganancia) 288 | 289 | **cat 2**: posible incendio (solo ganancia) 290 | 291 | tres bandas para la visualizacion y una banda para la intensidad 292 | denominada "int" 293 | :rtype: ee.ImageCollection 294 | 295 | """ 296 | col = ltr.slope() 297 | 298 | def categoria(img): 299 | d = img.get("system:time_start") 300 | 301 | adelante = ee.String("slope_after") 302 | atras = ee.String("slope_before") 303 | 304 | indice = ee.Image(img).select(ltr.fit_band + "_fit") 305 | umb = ee.Image.constant(umbral) 306 | 307 | # CREO UNA BANDA PARA EL AÑO 308 | t = ee.Date(ee.Image(img).get("system:time_start")).get("year") 309 | anio = ee.Image.constant(t).select([0], ["anio"]) 310 | 311 | at = ee.Image(img).select(atras) 312 | # atAbs = at.abs() 313 | 314 | ad = ee.Image(img).select(adelante) 315 | # adAbs = ad.abs() 316 | 317 | indice_ad = indice.add(ad) 318 | # indice_at = index.add(at) 319 | 320 | # indice_dif = indice_ad.subtract(index) 321 | indice_dif = indice_ad.subtract( 322 | indice) # .abs().divide(ee.Image.constant(500)) 323 | 324 | # INTENSIDAD QUIEBRE 325 | dif = ad.subtract(at) 326 | # media = atAbs.add(adAbs).divide(2) 327 | 328 | # DIRECCION 329 | at_dec = at.lte(0) # atras decreciente? 330 | at_crec = at.gt(0) # atras creciente? 331 | 332 | ad_dec = ad.lte(0) # adelante decreciente? 333 | ad_crec = ad.gt(0) # adelante creciente? 334 | 335 | # OTRA OPCION DE CATEGORIAS 336 | 337 | # INTENSIDAD 338 | 339 | difAbs = dif.abs() 340 | 341 | mayor_mod = difAbs.gte(umb) 342 | 343 | int_alta = mayor_mod 344 | 345 | perdida_alta = int_alta.And(dif.lte(0)) # perdida alta (pa) 346 | ganancia_alta = int_alta.And(dif.gt(0)) # ganancia alta (ga) 347 | 348 | pa_img = ee.Image(perdida_alta).select([0], ["pa"]) 349 | ga_img = ee.Image(ganancia_alta).select([0], ["ga"]) 350 | ind_dif_img = ee.Image(indice_dif).select([0], ["indice_dif"]) 351 | 352 | img = img.addBands(pa_img).addBands(ga_img).addBands( 353 | ind_dif_img).addBands(anio) 354 | 355 | # mask = img.neq(0) 356 | 357 | return img.set("system:time_start", d) # .updateMask(mask) 358 | 359 | col = col.map(categoria) 360 | 361 | colL = col.toList(50) 362 | s = tools.execli(colL.size().getInfo)() 363 | newCol = ee.List([]) 364 | 365 | # ARMO LAS CATEGORIAS TENIENDO EN CUENTA LO QUE SUCEDE ANTES 366 | # Y DESPUES DEL AÑO EN ANALISIS 367 | for i in range(0, s): 368 | 369 | ''' ASI ANDA, PRUEBO OTRA COSA Y SINO LO VUELVO A ESTA VER 370 | img1 = ee.Image(colL.get(i)) 371 | a1 = img1.select("year") 372 | pa1 = img1.select("pa") 373 | ga1 = img1.select("ga") 374 | dif1 = img1.select("indice_dif") 375 | 376 | if (i < s-1): 377 | img2 = ee.Image(colL.get(i+1)) 378 | a2 = img2.select("year") 379 | pa2 = img2.select("pa") 380 | ga2 = img2.select("ga") 381 | dif2 = img2.select("indice_dif") 382 | elif (i == s-1): 383 | # cat 1 384 | inc = pa1 385 | cat1 = inc 386 | 387 | if (i < s-2): 388 | img3 = ee.Image(colL.get(i+2)) 389 | 390 | a3 = img3.select("year") 391 | pa3 = img3.select("pa") 392 | ga3 = img3.select("ga") 393 | dif3 = img3.select("indice_dif") 394 | 395 | # cat 1 396 | inc1 = pa1.And(ga2) 397 | inc2 = pa1.And(ga3) 398 | inc = inc1.Or(inc2) 399 | cat1 = inc 400 | 401 | # cat 2 402 | noinc1 = pa1.eq(0) # en la actual no hay perdida 403 | noinc0 = 404 | cat2 = noinc1.And(ga2) 405 | cat2 = cat2.multiply(ee.Image.constant(2)) 406 | 407 | elif (i == s-2): 408 | # cat 1 409 | inc = pa1.And(ga2) 410 | cat1 = inc 411 | 412 | # cat 2 413 | noinc1 = pa1.eq(0) 414 | cat2 = noinc1.And(ga2) 415 | cat2 = cat2.multiply(ee.Image.constant(2)) 416 | ''' 417 | img1 = ee.Image(colL.get(i)) 418 | d = img1.get("system:time_start") 419 | a1 = img1.select("year") 420 | pa1 = img1.select("pa") 421 | ga1 = img1.select("ga") 422 | dif1 = img1.select("indice_dif") 423 | 424 | # EN TODAS LAS IMG EXCEPTO LA PRIMERA, OBTENGO LOS DATOS DE 425 | # LA IMAGEN ANTERIOR (i-1) 426 | if i > 0: 427 | img0 = ee.Image(colL.get(i - 1)) 428 | a0 = img0.select("year") 429 | pa0 = img0.select("pa") 430 | ga0 = img0.select("ga") 431 | dif0 = img0.select("indice_dif") 432 | 433 | # EN TODAS LAS IMG EXCEPTO LA ULTIMA, OBTENGO LOS DATOS DE 434 | # LA IMAGEN SIGUIENTE (i+1) 435 | if i < s - 1: 436 | img2 = ee.Image(colL.get(i + 1)) 437 | a2 = img2.select("year") 438 | pa2 = img2.select("pa") 439 | ga2 = img2.select("ga") 440 | dif2 = img2.select("indice_dif") 441 | 442 | # EN TODAS LAS IMG EXCEPTO LAS ULTIMAS 2, OBTENGO LOS DATOS DE 443 | # LA IMAGEN SUB SIGUIENTE (i+2) 444 | if i < s - 2: 445 | img3 = ee.Image(colL.get(i + 2)) 446 | 447 | a3 = img3.select("year") 448 | pa3 = img3.select("pa") 449 | ga3 = img3.select("ga") 450 | dif3 = img3.select("indice_dif") 451 | 452 | # ARMO LAS CATEGORIAS 453 | 454 | # EN LA PRIMER IMAGEN 455 | if i == 0: 456 | # 1 GANANCIA EN SIGUIENTE 457 | cond1 = ga2 458 | 459 | # 2 NADA EN EL SIGUIENTE, GANANCIA EN SUBSIGUIENTE 460 | # sin_perd_sig = pa2.Not() 461 | # cond2 = ga3.And(sin_perd_sig) 462 | 463 | cat2 = cond1 # .Or(cond2) 464 | 465 | cat1 = pa1 466 | 467 | # DE LA SEGUNDA A LA ANTEPENULTIMA 468 | if (i > 0) and (i < s - 2): 469 | # NADA SIGUIENTE 470 | sin_gan_sig = ga2.Not() 471 | sin_perd_sig = pa2.Not() 472 | nada_sig = sin_gan_sig.And(sin_perd_sig) 473 | 474 | # NADA ANTERIOR 475 | sin_gan_ant = ga0.Not() 476 | sin_perd_ant = pa0.Not() 477 | nada_ant = sin_gan_ant.And(sin_perd_ant) 478 | 479 | # NADA AHORA 480 | sin_gan = ga1.Not() 481 | sin_perd = pa1.Not() 482 | nada = sin_gan.And(sin_perd) 483 | 484 | # CAT 1 485 | 486 | # 1 PERDIDA SEGUIDA DE GANANCIA 487 | cond1 = pa1.And(ga2) 488 | 489 | # 2 PERDIDA SEGUIDA DE NADA, SEGUIDA DE GANANCIA 490 | cond2 = pa1.And(nada_sig).And(ga3) 491 | 492 | cat1 = cond1.Or(cond2) 493 | 494 | # CAT 2 495 | # 1 NADA ANTES, NADA AHORA Y GANANCIA SIGUIENTE 496 | cat2 = nada_ant.And(nada).And(ga2) 497 | 498 | # EN LA ANTEULTIMA 499 | if i == s - 2: 500 | cond1 = pa1.And(ga2) 501 | cat1 = cond1 502 | 503 | cond3 = pa0.Or(pa1).Not() 504 | cond4 = ga2.And(cond3) 505 | cat2 = cond4 506 | 507 | cat2 = cat2.multiply(ee.Image.constant(2)) 508 | 509 | final = cat1.add(cat2) 510 | img1 = img1.addBands(ee.Image(final).select([0], ["cat"])) 511 | img1 = img1.set("system:time_start", d) 512 | newCol = newCol.add(img1) 513 | 514 | col = ee.ImageCollection(newCol) 515 | 516 | def viz(img0): 517 | d = img0.get("system:time_start") 518 | 519 | imagen = ee.Image(img0).select("cat") 520 | img = ee.Image(imagen) 521 | 522 | indice = ee.Image(img0).select(ltr.fit_band + "_fit") 523 | dif = ee.Image(img0).select("indice_dif") 524 | difA = ee.Image(dif).abs() 525 | fact = difA.divide(indice) 526 | 527 | # VISUALIZACION DE IMG 528 | r = ee.String("viz-red") 529 | g = ee.String("viz-green") 530 | b = ee.String("viz-blue") 531 | ''' 532 | R G B 533 | 1 255, 25, 25 rojo 534 | 2 255, 100, 50 naranja 535 | ''' 536 | 537 | cat1 = img.eq(1) 538 | r1 = cat1.multiply(255) 539 | g1 = cat1.multiply(25) 540 | b1 = cat1.multiply(25) 541 | 542 | cat2 = img.eq(2) 543 | r2 = cat2.multiply(255) 544 | g2 = cat2.multiply(100) 545 | b2 = cat2.multiply(50) 546 | 547 | red = r1.add(r2).select([0], [r]).toUint8() 548 | green = g1.add(g2).select([0], [g]).toUint8() 549 | blue = b1.add(b2).select([0], [b]).toUint8() 550 | 551 | return img.addBands(ee.Image([red, green, blue])).set( 552 | "system:time_start", d) 553 | 554 | col = col.map(viz) 555 | 556 | if agrupar is False: 557 | return col 558 | else: 559 | def agrupando(img): 560 | d = ee.Date(img.get("system:time_start")) 561 | cat = img.select("cat") 562 | 563 | connected = img.toInt().connectedComponents(ee.Kernel.plus(1), 564 | 8).reproject( 565 | ee.Projection('EPSG:4326'), None, 30) 566 | conn2 = connected.mask().select("labels") 567 | holes = conn2.gt(cat) 568 | islas = cat.gt(conn2) 569 | objetos = holes.add(islas) 570 | 571 | kernel = ee.Kernel.plus(1, "pixels", False) 572 | reducer = ee.Reducer.countDistinct() 573 | 574 | vecinos = (objetos.select(0) 575 | .reduceNeighborhood(reducer, kernel, "kernel", False) 576 | .reproject(ee.Projection('EPSG:4326'), None, 30)) 577 | 578 | objNot = objetos.eq(0) 579 | 580 | final = objNot.Not().reduceNeighborhood(ee.Reducer.max(), 581 | kernel).reproject( 582 | ee.Projection('EPSG:4326'), None, 30) 583 | 584 | final = final.set("system:time_start", d) 585 | 586 | return final 587 | 588 | col = col.map(agrupando) 589 | 590 | return col 591 | 592 | 593 | def classIncendioImage(classified_image, time_list): 594 | """ Create a unique image with encoded values for the fire year of 595 | occurrence """ 596 | pass 597 | 598 | 599 | def stable_pixels(ltr, threshold=0.02): 600 | """ Generate a stable pixels throughout the year image 601 | 602 | :param threshold: value that divides 'change' of 'no change'. 603 | Defualt: 0.02 604 | :return: A 8-bits Image with the following bands: 605 | 606 | :cat: pixel's category. Categories are: 607 | 608 | - 1: neutral trend (nor gain neither loss) 609 | - 2: positive trend (gain) 610 | - 3: negative trend (loss) 611 | 612 | :viz-red: 8-bits band for category 1 613 | :viz-green: 8-bits band for category 2 614 | :viz-blue: 8-bits band for category 3 615 | """ 616 | 617 | col = ltr.slope() 618 | 619 | # imagen suma de breakpoints 620 | suma = ltr.total_bkp(col) 621 | 622 | # sort the collection ascending 623 | col = col.sort('system:time_start') 624 | 625 | # BANDA indice_fit DE LA PRIMER IMG DE LA COL 626 | primera = (ee.Image(col.first()) 627 | .select(ltr.fit_band + "_fit")) 628 | 629 | ultima = (ee.Image(col.sort('system:time_start', False).first()) 630 | .select(ltr.fit_band + '_fit')) 631 | 632 | # OBTENGO LA PENDIENTE GENERAL RESTANDO EL INDICE AL FINAL MENOS 633 | # EL INDICE INICIAL 634 | slope_global = (ee.Image(ultima.subtract(primera)) 635 | .select([0], ["slope"])) 636 | 637 | # CREO LA IMG CON EL UMBRAL 638 | umb_est = ee.Image.constant(threshold) 639 | 640 | # CREO UNA IMAGEN DE ceros Y CAMBIO EL NOMBRE DE LA BANDA A "bkps" 641 | ini = ee.Image(0).select([0], ["bkps"]) 642 | 643 | # CREO UNA IMG DONDE LOS PIXELES EN LOS QUE 644 | # |slope_after - slope_before| >= umbral 645 | # SON unos SINO ceros 646 | def breakpoints(img, inicial): 647 | # casteo la imagen inicial 648 | inicial = ee.Image(inicial) 649 | 650 | # obtengo el valor absoluto del slope 651 | adelante = img.select("slope_after") # .abs() 652 | atras = img.select("slope_before") # .abs() 653 | dif = adelante.subtract(atras).abs() 654 | 655 | # creo una imagen binaria 656 | # si el slope > umbral --> 1 657 | # sino --> 0 658 | cond = ee.Image(dif.gte(umb_est)) 659 | 660 | return inicial.add(cond) 661 | 662 | bkpImg = ee.Image(col.iterate(breakpoints, ini)) 663 | 664 | # estableMask = bkpImg.updateMask(bkpImg.eq(0)) 665 | 666 | # DETERMINO LA MASCARA DE PIXELES ESTABLES 667 | mask = bkpImg.eq(0) 668 | 669 | # ENMASCARO LA IMAGEN DE slopes DEJANDO SOLO LOS ESTABLES 670 | slope = slope_global.updateMask(mask) 671 | 672 | # slope band to float 673 | slope = slope.toFloat() 674 | 675 | # DETERMINO UN FACTOR PARA CADA PIXEL 676 | # DIVIDO POR 0.6 DEBIDO A QUE LA MAXIMA DIFERENCIA 677 | # ESPERABLE (EN NDVI AL MENOS) ES DE 0.2 (SIN BOSQUE) A 678 | # 0.8 (CON BOSQUE), LO QUE RESTADO DA 0.6 679 | slope_fact = slope.abs().divide(0.6) # .multiply(4) 680 | 681 | # SIN CAMBIOS: PARA LOS QUE LA PENDIENTE DEL SLOPE SEA 682 | # MENOR AL UMBRAL. QUEDA UNA IMAGEN BINARIA 683 | 684 | estable = (slope.abs() 685 | .lte(threshold) 686 | .select([0], ["cat"]) 687 | .toFloat()) 688 | 689 | # CRECIMIENTO: PARA LOS QUE LA PENDIENTE ES POSITIVA 690 | # QUEDA UNA IMG BINARIA MULTIPLICADA X2 691 | crecimiento = (ee.Image(slope.gte(threshold).multiply(2)) 692 | .select([0], ["cat"]) 693 | .toFloat()) 694 | 695 | # DECREMENTO: PARA LOS QUE LA PENDIENTE ES NEGATIVA 696 | # QUEDA UNA IMG BINARIA MULTIPLICADA X3 697 | decremento = (ee.Image(slope.lt(-threshold).multiply(3)) 698 | .select([0], ["cat"]) 699 | .toFloat()) 700 | 701 | # IMG CON LAS CATEGORIAS: 702 | # 'SIN CAMBIO' = 1 703 | # 'CRECIMIENTO' = 2 704 | # 'DECREMENTO' = 3 705 | categorias = estable.add(crecimiento).add(decremento) 706 | 707 | # Category band to int 8 708 | categorias = categorias.toInt8() 709 | 710 | # COLORES 711 | red = (ee.Image(slope.lt(0)) # IMG CON unos DONDE EL slope < 0 712 | .multiply(255) # MULTIPLICA x255 713 | .multiply(slope_fact) # MULTIPLICA x slope_fact 714 | .select([0], ["viz-red"]) # RENOMBRA LA BANDA 715 | # .toFloat()) 716 | .toInt8()) 717 | 718 | green = (ee.Image(slope.gte(0)) 719 | .multiply(255) 720 | .multiply(slope_fact) 721 | .select([0], ["viz-green"]) 722 | # .toFloat()) 723 | .toInt8()) 724 | 725 | blue = (ee.Image(slope.eq(0)) 726 | .multiply(255) 727 | .select([0], ["viz-blue"]) 728 | # .toFloat()) 729 | .toInt8()) 730 | 731 | final = (red.addBands(green) 732 | .addBands(blue) 733 | .addBands(categorias) 734 | .addBands(slope)) 735 | 736 | return final 737 | 738 | 739 | def max_diff(ltr, category): 740 | """ Generate an image with the maximum difference found 741 | 742 | :param category: 2 or gain, 3 or loss 743 | 744 | :return: an Image with the following bands: 745 | 746 | :max_change: maximum change value (loss or gain) 747 | :year: year that occur the max change 748 | :rtype: ee.Image 749 | """ 750 | 751 | segmentos = ltr.slope() 752 | slope_before = segmentos.select("slope_before") 753 | slope_after = segmentos.select("slope_after") 754 | change = segmentos.select("change") 755 | 756 | # Determino los pixeles que no contienen un breakpoint para 757 | # enmascararlos 758 | # estables, slope = ltr.stable_pixels() 759 | # estables = slope.mask().Not() 760 | estables_mask = ltr.stable_pixels().mask().Not().select([0]) 761 | 762 | # Agrego una banda con el año de la img 763 | # y enmascaro todas las imgs con la mascara de no stable_pixels 764 | def agregaFecha(img): 765 | # mask = img.mask() 766 | anio = ee.Date(img.get("system:time_start")).get("year") 767 | imga = ee.Image(anio).select([0], ["year"]).toInt16() 768 | return img.addBands(imga).updateMask(estables_mask) 769 | 770 | # slope_before = slope_before.map(agregaFecha) 771 | change = change.map(agregaFecha) # .select("change") 772 | 773 | # colL = slope_before.toList(50) 774 | colL = change.toList(50) 775 | 776 | # Primer imagen de la coleccion para la iteracion 777 | primera = ee.Image(colL.get(0)) 778 | 779 | # Resto de la col para la iteracion 780 | resto = colL.slice(1) 781 | 782 | def maxima(img, first): 783 | 784 | # Casts 785 | img = ee.Image(img) 786 | # imagen inicial (primer img de la col) 787 | firstI = ee.Image(first) 788 | 789 | # test img > img0? 790 | 791 | if category == "loss" or category == 3: 792 | test = img.select("change").lte( 793 | firstI.select("change")) # binaria 794 | elif category == "gain" or category == 2: 795 | test = img.select("change").gte( 796 | firstI.select("change")) # binaria 797 | 798 | # reemplazo condicional 799 | 800 | maxI = firstI.where(test, img) 801 | 802 | # ENMASCARO LA IMAGEN CON LOS PIXELES QUE SE MANTIENEN 803 | # ESTABLES A LO LARGO DE LA SERIE 804 | # imgF = maxI.updateMask(stable_pixels)#.addBands(imgy) 805 | return maxI 806 | 807 | img = ee.Image(resto.iterate(maxima, primera)) 808 | # img = img.updateMask(stable_pixels) 809 | return img 810 | 811 | def stretches(ltr, min_threshold=0.05, max_threshold=0.2): 812 | """ This method characterize the segmentation stretches and returns as 813 | many images as the amount of stretches. Categories are: 814 | 815 | * 1: no change: -min_threshold <= value <= min_threshold 816 | * 2: soft loss: -max_threshold <= value < -min_threshold 817 | * 3: steep loss: value < -max_threshold 818 | * 4: soft gain: min_threshold < value <= max_threshold 819 | * 5: steep gain: max_threshold < value 820 | 821 | :param min_threshold: divides 'no change' and 'soft change' 822 | :type min_threshold: float 823 | :param max_threshold: divides 'soft change' and 'steep change' 824 | :type max_threshold: float 825 | 826 | Return a new class called 'Stretches' with the following properties: 827 | 828 | - img_list: a list of images containing the stretches. 829 | 830 | Each image has the following bands: 831 | 832 | - t{n}_slope: slope of stretch n 833 | - t{n}_duration: duration of stretch n (in years) 834 | - t{n}_cat: category for stretch n 835 | 836 | - image: an image with unified results. Will have as many bands as 837 | found stretches, times 3. In case stretches in one pixel are 838 | less than stretches in the whole image, the last will be empty. 839 | 840 | Example: 841 | 842 | The whole image has 4 stretches. The pixel that has 2 843 | stretches will have the following values: 844 | 845 | - t1_slope: value 846 | - t2_slope: value 847 | - t3_slope: 0 848 | - t4_slope: 0 849 | 850 | :rtype: namedtuple 851 | """ 852 | # Threshold images 853 | umb1pos = ee.Image.constant(min_threshold).toFloat() 854 | umb2pos = ee.Image.constant(max_threshold).toFloat() 855 | 856 | umb1neg = ee.Image.constant(-min_threshold).toFloat() 857 | umb2neg = ee.Image.constant(-max_threshold).toFloat() 858 | 859 | # Number of breakpoints in each pixel 860 | bkps = ltr.breakpoints 861 | img_bkp = bkps.image 862 | 863 | # Total number of breakpoints 864 | total_bkp = bkps.total.getInfo() 865 | max_tramos = total_bkp - 1 866 | 867 | bandas_a = ["year_"+str(i) for i in range(total_bkp)] 868 | bandas_fit = ["fit_"+str(i) for i in range(total_bkp)] 869 | bandas = bandas_a + bandas_fit 870 | 871 | b2b = ltr.break2band().select(bandas) 872 | 873 | imagen = b2b.addBands(img_bkp) 874 | 875 | # OBTENGO TANTAS IMAGENES COMO TRAMOS HAYA EN LA COLECCION 876 | listaimg = [] 877 | 878 | for t, bkp in enumerate(range(2, total_bkp+1)): 879 | tramos = t+1 880 | mask = imagen.select("n_bkp").eq(bkp) 881 | masked = imagen.updateMask(mask) 882 | img_tramos = ee.Image.constant(0) 883 | for id, tramo in enumerate(range(tramos, 0, -1)): 884 | # NOMBRE DE LA BANDA 885 | nombre_tramo = "t"+str(id+1) 886 | 887 | # INDICES INI Y FIN 888 | ini = max_tramos-tramo 889 | fin = ini + 1 890 | 891 | # SELECCIONO LAS BANDAS 892 | a_ini = ee.Image(masked.select("year_"+str(ini))).toFloat() 893 | a_fin = ee.Image(masked.select("year_"+str(fin))).toFloat() 894 | fit_ini = masked.select("fit_"+str(ini)) 895 | fit_fin = masked.select("fit_"+str(fin)) 896 | 897 | # COMPUTO 898 | lapso = a_fin.subtract(a_ini) 899 | dif_total = fit_fin.subtract(fit_ini) 900 | 901 | dif_anual = dif_total.divide(lapso) 902 | slope = dif_anual.select([0],[nombre_tramo+"_slope"]) 903 | 904 | duracion = ee.Image(lapso.select([0],[nombre_tramo+"_duration"])).toUint8() 905 | 906 | # CATEGORIZACION 907 | uno = slope.gte(umb1neg).And(slope.lte(umb1pos)) 908 | dos = slope.lt(umb1neg).And(slope.gte(umb2neg)) 909 | tres = slope.lt(umb2neg) 910 | cuatro = slope.gt(umb1pos).And(slope.lte(umb2pos)) 911 | cinco = slope.gt(umb2pos) 912 | 913 | cat = uno.add( 914 | dos.multiply(2)).add( 915 | tres.multiply(3)).add( 916 | cuatro.multiply(4)).add( 917 | cinco.multiply(5)) 918 | 919 | cat = ee.Image(cat).select([0],[nombre_tramo+"_cat"]).toUint8() 920 | 921 | img_tramos = img_tramos.addBands(slope).addBands(duracion).addBands(cat) 922 | 923 | # SACO LA PRIMER BANDA QUE SE CREA ANTES DE INICIAR EL LOOP 924 | bandas = img_tramos.bandNames().slice(1) 925 | img_tramos = img_tramos.select(bandas) 926 | 927 | listaimg.append(img_tramos) 928 | 929 | # CREO UNA IMAGEN VACIA CON TODAS LAS BANDAS 930 | bandas_cat = ["t{0}_cat".format(str(n+1)) for n in range(max_tramos)] 931 | bandas_slope = ["t{0}_slope".format(str(n+1)) for n in range(max_tramos)] 932 | bandas_duracion = ["t{0}_duration".format(str(n+1)) for n in range(max_tramos)] 933 | 934 | bandas_todas = zip(bandas_slope, bandas_duracion, bandas_cat) 935 | 936 | bandas_todas = list(chain.from_iterable(bandas_todas)) 937 | 938 | # bandas_todas = bandas_cat+bandas_slope+bandas_duracion 939 | 940 | lista_ini = ee.List(bandas_todas).slice(1) 941 | 942 | img_ini = ee.Image.constant(0).select([0],["t1_slope"]) 943 | 944 | def addB(item, ini): 945 | ini = ee.Image(ini) 946 | img = ee.Image.constant(0).select([0],[item]) 947 | return ini.addBands(img) 948 | 949 | img_final = ee.Image(lista_ini.iterate(addB, img_ini)) 950 | 951 | for tramos, img in enumerate(listaimg): 952 | tramos += 1 953 | faltan = max_tramos - tramos 954 | 955 | # print "faltan", faltan 956 | # nombre = "falta{}tramo".format(str(faltan)) 957 | 958 | if faltan == 0: 959 | img = tools.mask2zero(img) 960 | img_final = img_final.add(img) 961 | # funciones.asset(img, nombre, "users/rprincipe/Pruebas/"+nombre, ltr.region) 962 | continue 963 | 964 | for tramo in range(faltan): 965 | tramo += 1 966 | i = tramo + tramos 967 | bcat = ee.Image.constant(0).select([0],["t{0}_cat".format(str(i))]) 968 | bslope = ee.Image.constant(0).select([0],["t{0}_slope".format(str(i))]) 969 | bdur = ee.Image.constant(0).select([0],["t{0}_duration".format(str(i))]) 970 | total = bcat.addBands(bslope).addBands(bdur) 971 | img = img.addBands(total) 972 | 973 | img = tools.mask2zero(img) 974 | 975 | # funciones.asset(img, nombre, "users/rprincipe/Pruebas/"+nombre, ltr.region) 976 | 977 | # bandas = img.getInfo()["bands"] 978 | # print "tramos:", tramos, [i["id"] for i in bandas] 979 | 980 | img_final = img_final.add(img) 981 | 982 | # return 983 | 984 | resultado = namedtuple("Stretches", ["img_list", "image"]) 985 | 986 | return resultado(listaimg, img_final) -------------------------------------------------------------------------------- /gee_landtrendr/landtrendr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from geetools import tools, tools_image 5 | import math 6 | from collections import namedtuple 7 | from itertools import chain 8 | 9 | import ee 10 | 11 | # Initialize EE if not initialized 12 | if not ee.data._initialized: ee.Initialize() 13 | 14 | 15 | def statistics(collection, band=None, suffix='_fit', skip_outliers=True): 16 | """ Compute sum of squares and R2 for values of every pixel in a 17 | ImageCollection that has already be fitted 18 | 19 | :param collection: collection 20 | :type collection: ee.ImageCollection 21 | :param band: name of the band that has been fitted. If None, the first 22 | band will be used 23 | :type band: str 24 | :param suffix: suffix of band that holds the fitted values 25 | :type suffix: str 26 | :param skip_outliers: outliers (in difference) are not included for 27 | computing 28 | :type skip_outliers: bool 29 | :return: 30 | :ssres: residual sum of square in an ee.Image 31 | :r2: coefficient of determination (r2) 32 | :rtype: namedtuple 33 | """ 34 | 35 | if not band: 36 | first = ee.Image(collection.first()).bandNames().get(0) 37 | band = ee.String(first).getInfo() 38 | 39 | fitted_band = band+suffix 40 | 41 | # Band mean 42 | justbands = collection.select([band, fitted_band]) 43 | 44 | def diff(img): 45 | nbr = img.select([0]) 46 | fit = img.select([1]) 47 | dif = fit.subtract(nbr).abs().select([0], ['diff']) 48 | return img.addBands(dif) 49 | 50 | justbands = justbands.map(diff) 51 | 52 | # Stats 53 | mean = justbands.mean() 54 | median = justbands.median().select(['diff'], ['median']) 55 | stddev = justbands.reduce(ee.Reducer.stdDev()).select(['diff_stdDev'], 56 | ['stddev']) 57 | interval_min = median.subtract(stddev.multiply(2)).select([0], ['min']) 58 | interval_max = median.add(stddev.multiply(2)).select([0], ['max']) 59 | 60 | # SSTOT 61 | sstot_image = tools_image.constant(0, ['sstot']) 62 | 63 | def sstot(img, sstoti): 64 | sstoti = ee.Image(sstoti) # cast 65 | mean_i = mean.select([band]) 66 | val = img.select([band]) 67 | diff_mean = val.subtract(mean_i) 68 | # excude outlier 69 | 70 | diff = img.select('diff') 71 | outlier = diff.lte(interval_min).Or(diff.gte(interval_max)).select([0], ['out']) 72 | 73 | # compute 74 | compute = diff_mean.pow(2).select([0], ['sstot']) 75 | 76 | # skip outliers 77 | if skip_outliers: 78 | compute = compute.updateMask(outlier.Not()).unmask() 79 | 80 | return sstoti.add(compute) 81 | 82 | sstot_image = ee.Image(justbands.iterate(sstot, sstot_image)) 83 | 84 | # SSRES 85 | ssres_image = tools_image.constant(0, ['ssres']) 86 | 87 | def ssres(img, ssresi): 88 | ssresi = ee.Image(ssresi) # cast 89 | 90 | diff = img.select('diff') 91 | outlier = diff.lte(interval_min).Or(diff.gte(interval_max)).select([0], ['out']) 92 | 93 | compute = diff.pow(2).select([0], ['ssres']) 94 | 95 | # skip outliers 96 | if skip_outliers: 97 | compute = compute.updateMask(outlier.Not()).unmask() 98 | 99 | return ssresi.add(compute) 100 | 101 | ssres_image = ee.Image(justbands.iterate(ssres, ssres_image)) 102 | 103 | # DIVIDE 104 | division = ssres_image.divide(sstot_image).select([0], ['r2']) 105 | 106 | # 1-division 107 | r2 = tools_image.constant(1, ['r2']).subtract(division).toFloat() 108 | 109 | result = namedtuple("Statistics", ["ssres", "r2"]) 110 | 111 | return result(ssres_image, r2) 112 | 113 | 114 | class LandTrendr(object): 115 | """ 116 | LandTrendr (Landsat-based detection of Trends in Disturbance and 117 | Recovery) is an algorithm that temporally segments a time-series of 118 | images by extracting the spectral trajectories of change over time. 119 | 120 | **Parameters:** 121 | 122 | - timeseries (ImageCollection): 123 | 124 | Collection from which to extract trends (it's assumed thateach 125 | image in the collection represents one year). The first band is 126 | usedto find breakpoints, and all subsequent bands are fitted 127 | using those breakpoints. 128 | 129 | - fit_band (str): 130 | 131 | band to use for regression 132 | 133 | **Optionals:** 134 | 135 | - area (ee.Geometry): area to compute LandTrendr. If `None`, takes the 136 | area of the first image 137 | 138 | - date_measure (str): What date represent every image in the collection?. 139 | Defaults to `year`. Can be `month`, `day`, etc 140 | 141 | **Originals:** 142 | 143 | - maxSegments (Integer): 144 | 145 | Maximum number of segments to be fitted on the time series. 146 | 147 | - spikeThreshold (Float, default: 0.9): 148 | 149 | Threshold for dampening the spikes (1.0 means no dampening). 150 | 151 | - vertexCountOvershoot (Integer, default: 3): 152 | 153 | The inital model can overshoot the maxSegments + 1 vertices by this 154 | amount. Later, it will be prunned down to maxSegments + 1. 155 | 156 | - preventOneYearRecovery (Boolean, default: false): 157 | Prevent segments that represent one year recoveries. 158 | 159 | - recoveryThreshold (Float, default: 0.25): 160 | If a segment has a recovery rate faster than 1/recoveryThreshold 161 | (in years), then the segment is disallowed. 162 | 163 | - pvalThreshold (Float, default: 0.1): 164 | If the p-value of the fitted model exceeds this threshold, then the 165 | current model is discarded and another one is fitted using the 166 | Levenberg-Marquardt optimizer. 167 | 168 | - bestModelProportion (Float, default: 1.25): 169 | Takes the model with most vertices that has a p-value that is at 170 | most this proportion away from the model with lowest p-value. 171 | 172 | - minObservationsNeeded (Integer, default: 6): 173 | Min observations needed to perform output fitting. 174 | """ 175 | 176 | def __init__(self, timeseries, fit_band, area=None, 177 | date_measure='year', **kwargs): 178 | 179 | self.timeSeries = timeseries 180 | self.fit_band = fit_band 181 | self.date_measure = date_measure 182 | 183 | self.maxSegments = kwargs.get("maxSegments", 4) 184 | self.spikeThreshold = kwargs.get("spikeThreshold", 0.9) 185 | self.vertexCountOvershoot = kwargs.get("vertexCountOvershoot", 3) 186 | self.preventOneYearRecovery = kwargs.get("preventOneYearRecovery", 187 | False) 188 | self.recoveryThreshold = kwargs.get("recoveryThreshold", 0.25) 189 | self.pvalThreshold = kwargs.get("pvalThreshold", 0.1) 190 | self.bestModelProportion = kwargs.get("bestModelProportion", 1.25) 191 | self.minObservationsNeeded = kwargs.get("minObservationsNeeded", 6) 192 | 193 | # Axes 194 | self.year_axis = 1 195 | self.band_axis = 0 196 | 197 | if area: 198 | self.area = area 199 | else: 200 | self.area = ee.Image(self.timeSeries.first()).geometry() 201 | 202 | self.region = tools.getRegion(self.area) 203 | 204 | self._core = None 205 | self._breakdown = None 206 | self._slope = None 207 | self._statistics = None 208 | self._date_range = None 209 | self._date_range_bitreader = None 210 | 211 | @classmethod 212 | def Principe(cls, timeseries, index, area=None): 213 | """ Factory Method to create a LandTrendr class with params defined 214 | by Rodrigo E. Principe (fitoprincipe82@gmail.com) 215 | """ 216 | newobj = cls(timeseries, index, area, 217 | maxSegments=4, 218 | spikeThreshold=0.1, 219 | vertexCountOvershoot=0, 220 | preventOneYearRecovery=False, 221 | recoveryThreshold=0.9, 222 | pvalThreshold=0.9, 223 | bestModelProportion=0.1, 224 | minObservationsNeeded=6) 225 | 226 | return newobj 227 | 228 | @classmethod 229 | def Kennedy(cls, timeseries, index, area=None): 230 | """ Factory Method to create a LandTrendr class with params defined 231 | by Kennedy (original) 232 | """ 233 | newobj = cls(timeseries, index, area, 234 | maxSegments=4, 235 | spikeThreshold=0.9, 236 | vertexCountOvershoot=3, 237 | preventOneYearRecovery=False, 238 | recoveryThreshold=0.25, 239 | pvalThreshold=0.05, 240 | bestModelProportion=0.75, 241 | minObservationsNeeded=6) 242 | 243 | return newobj 244 | 245 | @classmethod 246 | def Liang(cls, timeseries, index, area=None): 247 | """ Factory Method to create a LandTrendr class with params defined 248 | by Liang 249 | """ 250 | newobj = cls(timeseries, index, area, 251 | maxSegments=4, 252 | spikeThreshold=0.9, 253 | vertexCountOvershoot=0, 254 | preventOneYearRecovery=False, 255 | recoveryThreshold=0.25, 256 | pvalThreshold=0.1, 257 | bestModelProportion=0.75, 258 | minObservationsNeeded=6) 259 | 260 | return newobj 261 | 262 | @property 263 | def date_range(self): 264 | """ Range of the date measure 265 | 266 | :rtype: ee.List 267 | """ 268 | if not self._date_range: 269 | ordered = self.timeSeries.sort('system:time_start', True) 270 | def get_relative_date(img, ini): 271 | ini = ee.List(ini) 272 | date = img.date() 273 | measure = date.get(self.date_measure) 274 | return ini.add(measure) 275 | 276 | result = ordered.iterate(get_relative_date, ee.List([])) 277 | self._date_range = ee.List(result) 278 | 279 | return self._date_range 280 | 281 | 282 | @property 283 | def date_range_bitreader(self): 284 | """ Make a BitReader to encode the breakpoints 285 | 286 | Example: 287 | 288 | BitReader({0: 1999, 1: 2000, ... 12:2010} 289 | """ 290 | if not self._date_range_bitreader: 291 | time_list = self.date_range.getInfo() 292 | reader_dict = {} 293 | for i, t in enumerate(time_list): 294 | key = '{}'.format(i) 295 | reader_dict[key] = {1: t} 296 | 297 | self._date_range_bitreader = tools.BitReader(reader_dict) 298 | 299 | return self._date_range_bitreader 300 | 301 | @property 302 | def core(self): 303 | """ Apply original LandTrendr Algorithm as given in GEE 304 | 305 | :return: Same as the original algorithm (An array image with one band 306 | called 'LandTrendr' and the rest of the bands named: 307 | bandname_fit (ej: B2_fit)) 308 | """ 309 | if not self._core: 310 | # Select the index band only (example: 'nbr') in the whole 311 | # collection 312 | timeserie_index = self.timeSeries.select(self.fit_band) 313 | 314 | img = ee.Algorithms.TemporalSegmentation.LandTrendr( 315 | timeSeries=timeserie_index, 316 | # self.timeSeries, 317 | maxSegments=self.maxSegments, 318 | spikeThreshold=self.spikeThreshold, 319 | vertexCountOvershoot=self.vertexCountOvershoot, 320 | preventOneYearRecovery=self.preventOneYearRecovery, 321 | recoveryThreshold=self.recoveryThreshold, 322 | pvalThreshold=self.pvalThreshold, 323 | bestModelProportion=self.bestModelProportion, 324 | minObservationsNeeded=self.minObservationsNeeded) 325 | 326 | self._core = img 327 | 328 | return self._core 329 | 330 | @property 331 | def breakdown(self): 332 | ''' This method breaks down the resulting array and returns a list of 333 | images 334 | 335 | returns an ImageCollection in which each image has the following bands: 336 | - year = image's year 337 | - {ind} = index (ndvi, evi, etc) 338 | - {ind}_fit = index ajustado 339 | - bkp = breakpoint (1: yes, 0: no). 340 | 341 | It assumes that each image in the collection represents only one year 342 | of the time series 343 | ''' 344 | if not self._breakdown: 345 | core = self.core 346 | ltr = core.select('LandTrendr') 347 | rmse = core.select('rmse') 348 | n = self.timeSeries.size() 349 | ordered_ts = self.timeSeries.sort('system:time_start') 350 | ordered_list = ordered_ts.toList(n) 351 | seq = ee.List.sequence(0, n.subtract(1)) 352 | def create(position, ini): 353 | ini = ee.List(ini) 354 | nextt = ee.Number(position).add(1) 355 | start = ee.Image.constant(ee.Number(position)) 356 | end = ee.Image.constant(nextt) 357 | sli = ltr.arraySlice(1, start.toInt(), end.toInt(), 1) 358 | 359 | # CONVERT ARRAY TO IMG 360 | imgIndx = sli.arrayProject([0]).arrayFlatten( 361 | [["year", self.fit_band, self.fit_band + "_fit", "bkp"]]) 362 | 363 | date = ee.Image(ordered_list.get(position)).date().millis() 364 | result = imgIndx.addBands(rmse).set('system:time_start', date) 365 | return ini.add(result) 366 | collist = ee.List(seq.iterate(create, ee.List([]))) 367 | col = ee.ImageCollection(collist) 368 | 369 | self._breakdown = col 370 | 371 | return self._breakdown 372 | 373 | @property 374 | def statistics(self): 375 | """ Compute statistics for this object """ 376 | if not self._statistics: 377 | collection = self.slope 378 | self._statistics = statistics(collection, self.fit_band) 379 | 380 | return self._statistics 381 | 382 | @property 383 | def slope(self): 384 | """ Calculate slope of each segment in the LandTrendR fit 385 | 386 | Use: 387 | LandTrendr(imgcol, maxSegment, index, **kwargs).slope() 388 | 389 | Each image in the collection contains the following bands: 390 | - [0] index: original index 391 | - [1] {index}_fit: fitted index (example, ndvi_fit) 392 | - [2] bkp: breakpoint (1 change, 0 no change) 393 | - [3] year: image's year 394 | - [4] slope_before: slope of the segment 'before'. If *a1* 395 | is the main year, *slope_before = a1-a0* 396 | - [5] slope_after: slope of the segment 'after'. If *a1* is the 397 | main year, *slope_after = a2-a1* 398 | - [6] change: change's magnitud 399 | (between -1 (greater loose) and 1 (greater gain)) 400 | - [7] angle: change's angle in radians 401 | - [8] rmse: root mean square error (original) 402 | 403 | :rtype: ee.ImageCollection 404 | 405 | """ 406 | if not self._slope: 407 | breakdown = self.breakdown 408 | 409 | def add_date(img): 410 | """ Pass system:time_start property """ 411 | date = img.get("system:time_start") 412 | return img.set("system:time_start", date) 413 | 414 | collection = breakdown.map(add_date) 415 | 416 | def compute(central, before=None, after=None): 417 | """ Compute slope with image before and after 418 | 419 | :param central: imagen 'central' (1) 420 | :type central: ee.Image 421 | 422 | :param before: imagen anterior (0) 423 | :type before: ee.Image 424 | 425 | :param after: imagen posterior (2) 426 | :type after: ee.Image 427 | 428 | :return: the central image with computed values: 429 | 430 | **{band}**: original band value 431 | 432 | **{band}_fit**: fitted band value 433 | 434 | **slope_before**: slope to the image before 435 | 436 | **slope_after**: slope to the image after 437 | 438 | **change**: change magnitude (between -1 (greatest loss) 439 | and 1 (greatest gain)) 440 | 441 | **angle**: change angle in radians 442 | :rtype: ee.Image 443 | """ 444 | name_before = 'slope_before' 445 | name_after = 'slope_after' 446 | 447 | ### CENTRAL ################################################ 448 | central_img = ee.Image(central) 449 | central_fit = central_img.select(self.fit_band + "_fit") 450 | central_year = central_img.select("year") 451 | central_date = central_img.get("system:time_start") 452 | 453 | #### BEFORE ################################################# 454 | if before: 455 | before_img = ee.Image(before) 456 | 457 | before_fit = before_img.select(self.fit_band + "_fit") 458 | before_year = before_img.select("year") 459 | 460 | # difference (year) between central_img and before_img 461 | before_diff_year = central_year.subtract(before_year) 462 | 463 | # difference (fit) between central_img and before_img 464 | before_diff_fit = central_fit.subtract(before_fit)\ 465 | .divide(before_diff_year)\ 466 | .select([0], [name_before]) 467 | 468 | #### AFTER ############################################## 469 | if after: 470 | after_img = ee.Image(after) 471 | 472 | after_fit = after_img.select(self.fit_band + "_fit") 473 | after_year = after_img.select("year") 474 | after_diff_year = after_year.subtract(central_year) 475 | after_diff_fit = after_fit.subtract(central_fit)\ 476 | .divide(after_diff_year)\ 477 | .select([0], [name_after]) 478 | 479 | #### FIRST IMAGE ################# 480 | if after and not before: 481 | before_diff_fit = after_diff_fit.select([0], [name_before]) 482 | 483 | elif before and not after: 484 | after_diff_fit = before_diff_fit.select([0], [name_after]) 485 | 486 | #### change ################################################ 487 | pi = ee.Image.constant(math.pi) 488 | 489 | # angleS 490 | diff_before_radians = before_diff_fit.atan() 491 | diff_after_radians = after_diff_fit.atan() 492 | 493 | # Difference between after and before 494 | diff_fit = after_diff_fit.subtract(before_diff_fit) 495 | 496 | # Conditional mask 497 | gain_mask = diff_fit.gte(0) 498 | 499 | # each case images 500 | loss_img = diff_before_radians.subtract(diff_after_radians)\ 501 | .subtract(pi) 502 | gain_img = pi.add(diff_before_radians)\ 503 | .subtract(diff_after_radians) 504 | 505 | # angle 506 | angle = ee.Image(loss_img.where(gain_mask, gain_img))\ 507 | .select([0], ["angle"])\ 508 | .toFloat() 509 | 510 | # Magnitude (between -1 y 1) 511 | gain_magnitude = pi.subtract(angle).divide(pi) 512 | loss_magnitude = pi.add(angle).divide(pi).multiply(-2) 513 | 514 | change = ee.Image(loss_magnitude.where(gain_mask, gain_magnitude))\ 515 | .select([0], ["change"])\ 516 | .toFloat() 517 | 518 | # gain 519 | pos_pi = ee.Number(3.14) 520 | gain = angle.lt(pos_pi).And(angle.gt(0)).toInt() 521 | 522 | # loss 523 | neg_pi = ee.Number(-3.14) 524 | loss = angle.gt(neg_pi).And(angle.lt(0)).toInt() 525 | 526 | img = central_img.addBands(before_diff_fit)\ 527 | .addBands(after_diff_fit)\ 528 | .addBands(change)\ 529 | .addBands(angle)\ 530 | .addBands(gain.select([0], ['gain']))\ 531 | .addBands(loss.select([0], ['loss'])) 532 | 533 | img = img.set("system:time_start", central_date) 534 | 535 | return img 536 | 537 | # Collection to List 538 | col_list = collection.toList(collection.size()) 539 | 540 | new_col_list = ee.List([]) 541 | 542 | # collection size 543 | tam = col_list.size().getInfo() # - 1 544 | 545 | # first image 546 | im0 = col_list.get(0) 547 | 548 | # second image 549 | im1 = col_list.get(1) 550 | 551 | # compute first and second images 552 | im0 = compute(central=im0, after=im1) 553 | 554 | # add to list 555 | new_col_list = new_col_list.add(im0) 556 | 557 | # middle 558 | for im in range(1, tam - 1): 559 | im0 = col_list.get(im - 1) 560 | im1 = col_list.get(im) 561 | im2 = col_list.get(im + 1) 562 | 563 | im1 = compute(central=im1, before=im0, after=im2) 564 | new_col_list = new_col_list.add(im1) 565 | 566 | # last image 567 | im0 = col_list.get(tam - 2) 568 | im1 = col_list.get(tam - 1) 569 | 570 | im1 = compute(central=im1, before=im0) 571 | new_col_list = new_col_list.add(im1) 572 | 573 | newCol = ee.ImageCollection(new_col_list) 574 | 575 | self._slope = newCol 576 | 577 | return self._slope 578 | 579 | def total_bkp(self, collection=None): 580 | """ Compute the total number of breakpoint in each pixel 581 | 582 | :param collection: the collection that hold the breakpoint data, if 583 | None, it'll be computed. 584 | :type collection: ee.ImageCollection 585 | :return: A single Image with the total number of breakpoints in a band 586 | called `total_bkp` 587 | :rtype: ee.Image 588 | """ 589 | col = collection if collection else self.slope 590 | sum_bkp = ee.Image(col.select("bkp").sum()).select([0], ["total_bkp"]) 591 | return sum_bkp 592 | 593 | def stack(self): 594 | """ Generate an image holding the fitted index value for each year 595 | 596 | :return: An image with the following bands 597 | 598 | :{index}_{year}: fitted index value for that year 599 | :rtype: ee.Image 600 | """ 601 | col = self.breakdown 602 | anio0 = ee.Date(col[0].get("system:time_start")).get("year").format() 603 | 604 | imgF = col[0].select([self.fit_band + "_fit"], 605 | [ee.String(self.fit_band).cat(ee.String('_')).cat(anio0)]) 606 | 607 | for i in range(1, len(col)): 608 | anio = ee.Date(col[i].get("system:time_start")).get( 609 | "year").format() 610 | img = col[i].select([self.fit_band + "_fit"], 611 | [ee.String(self.fit_band).cat(ee.String('_')).cat(anio)]) 612 | imgF = imgF.addBands(img) 613 | 614 | return imgF 615 | 616 | def breakpoints(self): 617 | """ Number of breakpoints 618 | 619 | Return an object with 2 properties: 620 | 621 | - image (ee.Image): ee.Image containing the following bands: 622 | 623 | - n_bkp: number of breakpoints in each pixel 624 | 625 | - total (ee.Number): maximum amount of breakpoints 626 | 627 | :rtype: namedtuple 628 | """ 629 | # APLICO LANDTRENDR 630 | serie = self.breakdown 631 | col = ee.ImageCollection(serie) 632 | 633 | imgacum = ee.Image.constant(0).select([0], ["n_bkp"]).toUint8() 634 | # resta = ee.Image.constant(2) 635 | 636 | def check_bkp(img, acum): 637 | bkp = img.select("bkp") 638 | acum = ee.Image(acum) 639 | acum = acum.add(bkp) 640 | return acum 641 | 642 | n_bkp = ee.Image(col.iterate(check_bkp, imgacum)) 643 | img_final = n_bkp#.subtract(resta) 644 | # geometry = ee.Image(self.timeSeries.first()).geometry().buffer(-5000) 645 | 646 | # workaround = ee.FeatureCollection(ee.List([ee.Feature(geometry)])) 647 | 648 | total = img_final.reduceRegion(ee.Reducer.max(), 649 | # geometry=geometry, 650 | geometry=self.area, 651 | scale=30, 652 | maxPixels=1e13).get("n_bkp") 653 | 654 | total = ee.Number(total).toInt8() 655 | 656 | bkps = namedtuple("Breakpoints", ["image", "total"]) 657 | 658 | return bkps(image=img_final, total=total) 659 | 660 | def loss(self, skip_middle=True, skip_slope=0.05): 661 | """ Compute loss magnitude from a loss breakpoint until next 662 | breakpoint 663 | 664 | :param skip_middle: If the next date is also a loss, skip it. 665 | :type skip_middle: bool 666 | :param skip_slope: skip middle if the slope of the stretch before is 667 | greater than this value 668 | :type skip_slope: float 669 | :rtype: ee.ImageCollection 670 | """ 671 | slope = self.slope.sort('system:time_start', True) 672 | last_date = ee.Number(self.date_range.get(-1)).toInt() 673 | fit_band = self.fit_band + '_fit' 674 | 675 | def over_slope(img, inidict): 676 | inidict = ee.Dictionary(inidict) 677 | ini = ee.List(inidict.get('images')) 678 | 679 | fitted = img.select([fit_band]) 680 | year = img.select(['year']) 681 | 682 | # get image date to pass to the computed image 683 | time_start = img.date().millis() 684 | 685 | # statistics 686 | rmse = img.select(['rmse']) 687 | 688 | # date list from next to last 689 | date = img.date().get(self.date_measure).toInt() 690 | rrange = ee.List.sequence(date.add(1), last_date) 691 | 692 | # loss? 693 | loss_mask = img.select(['loss']) 694 | 695 | # loss before? 696 | loss_before = ee.Image(inidict.get('loss_before')) 697 | 698 | # update loss_before 699 | new_loss_before = loss_mask.select([0], ['loss_before']) 700 | 701 | times = tools_image.constant(0, ['times']) 702 | rest = tools_image.constant(0, ['next_bkp', 'loss']) 703 | 704 | initial = times.addBands(rest) 705 | 706 | # function to get the amount of loss and the elapsed loss time 707 | def over_range(d, ini): 708 | # cast 709 | ini = ee.Image(ini) 710 | d = ee.Number(d) 711 | 712 | date_range = ee.Image(d.subtract(date)).select([0], ['next_bkp']) 713 | 714 | # get slope image for this d (date) 715 | img_s = ee.Image( 716 | slope.filter( 717 | ee.Filter.calendarRange(d, d, self.date_measure) 718 | ).first()) 719 | 720 | # get image date before 721 | img_before = ee.Image( 722 | slope.filter( 723 | ee.Filter.calendarRange(d.subtract(1), 724 | d.subtract(1), 725 | self.date_measure) 726 | ).first()) 727 | 728 | img_before_loss = img_before.select(['loss']) 729 | img_before_bkp = img_before.select(['bkp']) 730 | 731 | # retain only loss pixels 732 | img_s = img_s.updateMask(loss_mask) 733 | 734 | # retain pixels that are not preceeded by loss 735 | # img_slope_before = img_s.select('slope_before') 736 | mask = loss_before.Not()#.And(img_slope_before.gte(skip_slope)) 737 | img_s = img_s.updateMask(mask) 738 | 739 | # catch loss 740 | this_loss_mask = img_s.select(['loss']) 741 | 742 | # catch gain 743 | this_gain_mask = img_s.select(['gain']) 744 | 745 | # catch breakpoint 746 | bkp = img_s.select(['bkp']) 747 | 748 | # make a mask only if it is the first occurrence of gain 749 | first_time = ini.select(['times']).eq(0).toInt() 750 | 751 | # CASE 1 752 | case1 = this_gain_mask.And(img_before_loss) 753 | 754 | # CASE 2 755 | case2 = this_loss_mask.And(img_before_bkp.Not()) 756 | 757 | # CASE 3 758 | case3 = this_loss_mask.Not().And(this_gain_mask.Not()).And(bkp) 759 | 760 | # EXTRA CASE 761 | if not skip_middle: 762 | extra_case = this_loss_mask.And(img_before_loss) 763 | retain = case1.Or(case2).Or(case3).Or(extra_case).And(first_time) 764 | else: 765 | # retain 766 | retain = case1.Or(case2).Or(case3).And(first_time) 767 | 768 | # update times 769 | times = ini.select(['times']).add(retain) 770 | 771 | # get loss magnitude 772 | new_fitted = img_s.select([fit_band]) 773 | loss = fitted.subtract(new_fitted) \ 774 | .select([0], ['loss_magnitude']) \ 775 | .updateMask(retain).unmask() 776 | 777 | range_i = date_range.updateMask(retain).unmask().toInt() 778 | 779 | to_add = times.addBands(range_i).addBands(loss) 780 | 781 | final = ini.select(['times', 'next_bkp', 'loss']).add(to_add) 782 | 783 | return final 784 | 785 | next_loss = ee.Image(rrange.iterate(over_range, initial)) \ 786 | .addBands(year)\ 787 | .addBands(rmse)\ 788 | .addBands(loss_before)\ 789 | .set('system:time_start', time_start) 790 | 791 | # set images 792 | inidict = inidict.set('images', ini.add(next_loss)) 793 | 794 | return ee.Dictionary(inidict).set('loss_before', new_loss_before) 795 | 796 | inidict = ee.Dictionary({ 797 | 'images': ee.List([]), 798 | 'loss_before': tools_image.constant(0, ['loss_before']) 799 | }) 800 | 801 | newdict = ee.Dictionary(slope.iterate(over_slope, inidict)) 802 | images = ee.List(newdict.get('images')) 803 | return ee.ImageCollection.fromImages(images) 804 | 805 | # ENCODED IMAGES 806 | 807 | def breakpoints_image(self): 808 | ''' Create an image with breakpoints occurrence encoded by a BitReader 809 | ''' 810 | breakdown = self.breakdown 811 | encoder = self.date_range_bitreader 812 | time_list = self.date_range.getInfo() 813 | 814 | # trim last and first value of time_list because are bkps always 815 | time_list = time_list[1:-1] 816 | 817 | # Create a dict with encoded values for each time 818 | values = {} 819 | for t in time_list: 820 | encoded = encoder.encode(t) 821 | values[t] = encoded 822 | 823 | valuesEE = ee.Dictionary(values) 824 | 825 | ini_img = tools_image.constant(from_dict={'bkp':0}) 826 | 827 | def over_col(img, ini): 828 | ini = ee.Image(ini) 829 | date = img.date().get(self.date_measure).format() 830 | bkp = img.select(['bkp']) 831 | date_value = valuesEE.get(date) 832 | date_value_img = ee.Image.constant(date_value).select([0], ['bkp']) 833 | masked_date_value = date_value_img.updateMask(bkp).unmask() 834 | 835 | return ini.add(masked_date_value) 836 | 837 | # slice first and last of breakdown because bkps are always 1 838 | breakdown = ee.ImageCollection.fromImages( 839 | breakdown.toList(breakdown.size())\ 840 | .slice(1, -1)) 841 | 842 | result = ee.Image(breakdown.iterate(over_col, ini_img)) 843 | result = result.clip(ee.Geometry.Polygon(self.region)) 844 | 845 | return result 846 | 847 | def lossdate_image(self, threshold, bandname='lossyear', skip_middle=True): 848 | """ make an encoded loss image holding dates of disturbance """ 849 | # slope = self.slope 850 | loss = self.loss(skip_middle) 851 | 852 | encoder = self.date_range_bitreader 853 | time_list = self.date_range.getInfo() 854 | 855 | # Create a dict with encoded values for each time 856 | values = {} 857 | for t in time_list: 858 | encoded = encoder.encode(t) 859 | values[t] = encoded 860 | 861 | valuesEE = ee.Dictionary(values) 862 | 863 | def over_col(img, ini): 864 | # last lossyear img 865 | ini = ee.Image(ini) 866 | 867 | # get date (example: 2010) 868 | date = img.date().get(self.date_measure).format() 869 | date_value = valuesEE.get(date) 870 | date_value_img = ee.Image.constant(date_value) \ 871 | .select([0], [bandname]) 872 | 873 | condition = img.select(['loss']).gt(threshold) 874 | 875 | masked_date_value = date_value_img.updateMask(condition).unmask() 876 | 877 | return ini.add(masked_date_value) 878 | 879 | initial = tools.empty_image(0, [bandname]) 880 | result = ee.Image(loss.iterate(over_col, initial)) 881 | result = result.clip(ee.Geometry.Polygon(self.region)) 882 | 883 | return result --------------------------------------------------------------------------------