├── tests ├── __init__.py ├── test_threshold.py ├── test_maskpercent.py ├── test_bap.py └── test_outliers.py ├── setup.cfg ├── geebap ├── _version.py ├── __init__.py ├── regdec.py ├── utils.py ├── date.py ├── priority.py ├── masks.py ├── filters.py ├── expgen.py ├── functions.py ├── sites.py ├── ipytools.py ├── season.py ├── expressions.py └── bap.py ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── setup.py ├── README.rst └── notebooks ├── season └── Season.ipynb ├── scores ├── Threshold.ipynb ├── Medoid.ipynb ├── Outliers.ipynb ├── Brightness.ipynb ├── CloudDist.ipynb ├── Index.ipynb ├── Satellite.ipynb ├── MultiYear.ipynb ├── MaskPercent.ipynb ├── MaskPercentKernel.ipynb └── Doy.ipynb ├── priority └── SeasonPriority.ipynb └── bap └── Best_Available_Pixel_Composite.ipynb /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /geebap/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = "0.2.8" -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.md 3 | include test.py 4 | include *.txt -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto>=0.23.0 2 | certifi>=2017.7.27.1 3 | cffi>=1.11.2 4 | chardet>=3.0.4 5 | cryptography>=2.3 6 | earthengine-api>=0.1.127 7 | enum34>=1.1.6 8 | google-api-python-client>=1.6.4 9 | httplib2>=0.10.3 10 | idna>=2.6 11 | ipaddress>=1.0.18 12 | numpy>=1.13.3 13 | oauth2client>=4.1.2 14 | pkg-resources>=0.0.0 15 | pyasn1>=0.3.7 16 | pyasn1-modules>=0.1.5 17 | pycparser>=2.18 18 | pyOpenSSL>=17.5.0 19 | requests>=2.20.0 20 | rsa>=3.4.2 21 | simpleeval>=0.9.5 22 | six>=1.11.0 23 | uritemplate>=3.0.0 24 | urllib3>=1.22 25 | -------------------------------------------------------------------------------- /geebap/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Package to generate a "Best Available Pixel" image in Google Earth Engine 4 | """ 5 | 6 | from __future__ import absolute_import, division, print_function 7 | from ._version import __version__ 8 | 9 | __all__ = ( 10 | "__title__", "__summary__", "__uri__", "__version__", "__author__", 11 | "__email__", "__license__", "__copyright__", 12 | ) 13 | 14 | __title__ = "BestAvailablePixel" 15 | __summary__ = "Generate a 'Best Available Pixel' image in Google Earth Engine" 16 | __uri__ = "https://github.com/fitoprincipe/geebap" 17 | __author__ = "Rodrigo E. Principe" 18 | __email__ = "rprincipe@ciefap.org.ar" 19 | 20 | __license__ = "GNU GENERAL PUBLIC LICENSE, Version 3" 21 | __copyright__ = "Rodrigo E. Principe" 22 | 23 | try: 24 | from . import bap, date, expgen, expressions, filters, functions,\ 25 | ipytools, masks, regdec, scores, season, sites 26 | 27 | from .bap import Bap 28 | from .priority import SeasonPriority 29 | from .season import Season 30 | except ImportError: 31 | pass -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .coverage 37 | .coverage.* 38 | .cache 39 | nosetests.xml 40 | coverage.xml 41 | *.cover 42 | .hypothesis/ 43 | 44 | # Sphinx documentation 45 | docs/_build/ 46 | 47 | # PyBuilder 48 | target/ 49 | 50 | # Environments 51 | bap_env/ 52 | 53 | # mkdocs documentation 54 | /site 55 | 56 | # IDEA IDE 57 | .idea/ 58 | 59 | # DATA 60 | data/ 61 | 62 | # SCRIPTS 63 | crear_BAP_manual.py 64 | readme.py 65 | crear_compuesto_archivo.py 66 | crear_compuesto_gsheet.py 67 | 68 | # STRIPS 69 | *.png 70 | files/ 71 | 72 | # Jupyter staff 73 | **/.ipynb_checkpoints -------------------------------------------------------------------------------- /tests/test_threshold.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ee 4 | ee.Initialize() 5 | from geebap import scores 6 | from geetools import tools 7 | 8 | img = ee.Image('LANDSAT/LC08/C01/T1_SR/LC08_231090_20130414') 9 | 10 | p1 = ee.Geometry.Point([-71.51, -43.27]) 11 | p2 = ee.Geometry.Point([-71.53, -43.271]) 12 | p3 = ee.Geometry.Point([-71.547, -43.274]) 13 | 14 | region = ee.Geometry.Polygon( 15 | [[[-71.57, -43.25], 16 | [-71.57, -43.29], 17 | [-71.49, -43.29], 18 | [-71.49, -43.25]]]) 19 | 20 | original_value1 = 308 21 | original_value2 = 101 22 | original_value3 = 5701 23 | 24 | 25 | def test_minmax(): 26 | 27 | thres = scores.Threshold() 28 | newimg = thres.compute(img, thresholds={ 29 | 'B4': {'min':150, 'max':2000} 30 | }, name=thres.name) 31 | 32 | val1 = tools.image.getValue(newimg, point=p1, scale=30, side='client')[thres.name] 33 | val2 = tools.image.getValue(newimg, point=p2, scale=30, side='client')[thres.name] 34 | val3 = tools.image.getValue(newimg, point=p3, scale=30, side='client')[thres.name] 35 | 36 | assert val1 == 1 37 | assert val2 == 0 38 | assert val3 == 0 39 | -------------------------------------------------------------------------------- /tests/test_maskpercent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ee 4 | ee.Initialize() 5 | 6 | from geebap import scores 7 | from geetools import tools, collection 8 | 9 | maxDiff = None 10 | band = "B1" 11 | 12 | pol = ee.Geometry.Polygon( 13 | [[[-71.56928658485413, -43.356795174869426], 14 | [-71.56932950019836, -43.357879484308015], 15 | [-71.56781673431396, -43.35791848788363], 16 | [-71.56779527664185, -43.35682637828949]]]) 17 | 18 | p = ee.Geometry.Point([-71.56871795654297, 19 | -43.35720861888331]) 20 | 21 | col = collection.Landsat8TOA() 22 | colEE = col.collection 23 | colEE = colEE.filterBounds(p) 24 | 25 | image = ee.Image(colEE.first()).clip(pol).select([band]) 26 | 27 | # MASK OUT SOME PIXELS 28 | condition = image.gte(0.13) 29 | image = image.updateMask(condition) 30 | 31 | def test_default(): 32 | # SCORE 33 | score = scores.MaskPercent(band) 34 | newimg = score.compute(image, geometry=pol, scale=30) 35 | 36 | maskpercent_prop = newimg.get(score.name).getInfo() 37 | maskpercent_pix = tools.image.getValue(newimg, p, side='client')[score.name] 38 | 39 | assert maskpercent_prop == 0,5625 40 | assert maskpercent_pix == 0,5625 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/test_bap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ee 4 | ee.Initialize() 5 | from geebap import scores, bap, season, masks, filters 6 | 7 | 8 | # FILTERS 9 | filter = filters.CloudCover() 10 | 11 | # MASKS 12 | clouds = masks.Mask() 13 | 14 | # SEASON 15 | seas = season.Season('11-15', '02-15') 16 | 17 | # SCORES 18 | psat = scores.Satellite() 19 | pop = scores.AtmosOpacity() 20 | pmascpor = scores.MaskPercent() 21 | pindice = scores.Index() 22 | pout = scores.Outliers(("ndvi",)) 23 | pdoy = scores.Doy('01-15', seas) 24 | thres = scores.Threshold() 25 | 26 | # SITES 27 | site = ee.Geometry.Polygon( 28 | [[[-71.78, -42.79], 29 | [-71.78, -42.89], 30 | [-71.57, -42.89], 31 | [-71.57, -42.79]]]) 32 | centroid = site.centroid() 33 | 34 | def test_bap2016_0(): 35 | pmulti = scores.MultiYear(2016, seas) 36 | objbap = bap.Bap(season=seas, 37 | scores=(pindice, pmascpor, psat, 38 | pout, pop, pdoy, thres, 39 | pmulti), 40 | masks=(clouds,), 41 | filters=(filter,), 42 | ) 43 | 44 | composite = objbap.build_composite_best(2016, site, indices=("ndvi",)) 45 | 46 | assert isinstance(composite, ee.Image) == True 47 | -------------------------------------------------------------------------------- /geebap/regdec.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Registry Decorator """ 3 | 4 | import functools 5 | 6 | __all__ = ('register', 'register_all') 7 | 8 | 9 | def register(holder): 10 | """ Make a registry of the decorated Class in the given holder 11 | 12 | :Usage: 13 | 14 | .. code:: python 15 | 16 | from regdec import register 17 | from scores import Score 18 | 19 | registry = {} 20 | 21 | @register(registry) 22 | class NewScore(Score): 23 | def __init__(**kwargs): 24 | pass 25 | 26 | :param holder: dict that will hold the classes 27 | :type holder: dict 28 | """ 29 | # @functools.wraps(holder) 30 | def wrap(cls): 31 | name = cls.__name__ 32 | holder[name] = cls 33 | return cls 34 | return wrap 35 | 36 | 37 | def register_all(holder): 38 | """ Make a registry of the decorated Class in the given holder to use in 39 | module's __all__ variable 40 | 41 | :Usage: 42 | 43 | .. code:: python 44 | 45 | from regdec import register 46 | from scores import Score 47 | 48 | __all__ = [] 49 | 50 | @register_all(__all__) 51 | class NewScore(Score): 52 | def __init__(**kwargs): 53 | pass 54 | 55 | :param holder: list that will hold the classe's names 56 | :type holder: list 57 | """ 58 | # @functools.wraps(holder) 59 | def wrap(cls): 60 | name = cls.__name__ 61 | holder.append(name) 62 | return cls 63 | return wrap 64 | -------------------------------------------------------------------------------- /geebap/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Util functions """ 3 | 4 | 5 | def get_init_params(obj): 6 | init_params = obj.__init__.__code__.co_varnames 7 | obj_params = obj.__dict__.items() 8 | return {param: value for param, value in obj_params if param in init_params} 9 | 10 | 11 | def object_init(obj): 12 | init_params = get_init_params(obj) 13 | return '{}(**{})'.format(obj.__class__.__name__, init_params) 14 | 15 | 16 | def serialize(obj, name=None, result=None): 17 | """ Serialize an object to a dict """ 18 | if result is None: 19 | result = {} 20 | 21 | def make_name(obj, name=None): 22 | objname = obj.__class__.__name__ 23 | if name is None: 24 | # name = objname 25 | return '({})'.format(objname) 26 | return '{} ({})'.format(name, objname) 27 | 28 | name = make_name(obj, name) 29 | 30 | try: 31 | # If it is an object, it has a __dict__ 32 | obj_attr = obj.__dict__ 33 | except AttributeError: 34 | # If it's NOT an object 35 | if isinstance(obj, (tuple, list)): 36 | newlist = [] 37 | for element in obj: 38 | newlist.append(serialize(element)) 39 | result[name] = newlist 40 | elif isinstance(obj, (dict,)): 41 | newdict = {} 42 | for key, element in obj.items(): 43 | newdict[key] = serialize(element) 44 | result[name] = newdict 45 | else: 46 | result[name] = obj 47 | else: 48 | # If it IS an object 49 | attrs = {} 50 | result[name] = attrs 51 | for attr_name, attr_value in obj_attr.items(): 52 | serialize(attr_value, attr_name, attrs) 53 | 54 | return result 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | 7 | # Utility function to read the README file. 8 | # Used for the long_description. It's nice, because now 1) we have a top level 9 | # README file and 2) it's easier to type in the README file than to put a raw 10 | # string in below ... 11 | def read(fname): 12 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 13 | 14 | here = os.path.dirname(os.path.abspath(__file__)) 15 | version_ns = {} 16 | with open(os.path.join(here, 'geebap', '_version.py')) as f: 17 | exec(f.read(), {}, version_ns) 18 | 19 | # the setup 20 | setup( 21 | name='geebap', 22 | version=version_ns['__version__'], 23 | description='Generate a "Best Available Pixel (BAP)" composite in Google '\ 24 | 'Earth Engine (GEE)', 25 | # long_description=read('README'), 26 | url='', 27 | author='Rodrigo E. Principe', 28 | author_email='rprincipe@ciefap.org.ar', 29 | license='GNU', 30 | keywords='google earth engine raster image processing gis satelite', 31 | packages=find_packages(exclude=('docs', 'bap_env')), 32 | include_package_data=True, 33 | install_requires=['requests', 34 | 'simpleeval', 35 | 'numpy', 36 | 'geetools>=0.2.2'], 37 | extras_require={ 38 | 'dev': [], 39 | 'docs': [], 40 | 'testing': [], 41 | }, 42 | classifiers=['Programming Language :: Python :: 2', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3', 45 | 'Programming Language :: Python :: 3.3', 46 | 'Programming Language :: Python :: 3.4', 47 | 'Programming Language :: Python :: 3.5', 48 | 'License :: OSI Approved :: MIT License',], 49 | ) 50 | -------------------------------------------------------------------------------- /geebap/date.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Date module for Gee Bap """ 3 | import ee 4 | 5 | 6 | class Date(object): 7 | """ Class holding some custom methods too add a 'date' band in the 'Best 8 | Available Pixel' compostite. 9 | 10 | Agrega una band de name *fecha* de tipo *Uint16* que toma de la 11 | propiedad *system:time_start* y computa la cantidad de dias que 12 | transcurrieron a partir del 1970-01-01. 13 | Agrega a la imagen una propiedad con el id de la imagen llamada 'id_img' 14 | 15 | :metodos: 16 | :mapfecha: estatico para utilizar con ee.ImageCollection.map() 17 | """ 18 | oneday_local = 86400000 # milisegundos 19 | oneday = ee.Number(oneday_local) 20 | 21 | def __init__(self): 22 | ''' This Class doesn't initialize ''' 23 | pass 24 | 25 | @staticmethod 26 | def map(name="date"): 27 | """ 28 | :param name: name for the new band 29 | :type name: str 30 | """ 31 | def wrap(img): 32 | dateadq = img.date() # ee.Date 33 | days_since_70 = ee.Date(dateadq).millis().divide(Date.oneday) # days since 1970 34 | dateimg = ee.Image(days_since_70).select([0], [name]).toUint16() 35 | final = img.addBands(dateimg).set(name, days_since_70.toInt()) 36 | # return functions.pass_date(img, final) 37 | # return tools.passProperty(img, final, 'system:time_start') 38 | return final.copyProperties(img, ['system:time_start']) 39 | return wrap 40 | 41 | @staticmethod 42 | def local(date): 43 | """ Number of days since the beggining (1970-01-01) 44 | Dada una fecha obtiene la cantidad de dias desde el comienzo 45 | de las fechas (01-01-1970) 46 | 47 | :param date: date (yyyy-MM-dd) 48 | :type date: str 49 | :return: days since the beggining 50 | :rtype: float 51 | """ 52 | d = ee.Date(date) 53 | mili = d.millis().getInfo() 54 | return float(mili / Date.oneday_local) 55 | 56 | @staticmethod 57 | def get(date, unit="days"): 58 | """ get the date (ee.Date) of the given value in 'unit'. 59 | Currentrly ONLY process 'days', so: 60 | 61 | `date.Date.get(365) = '1971-01-01T00:00:00` 62 | 63 | :param date: the value to transform 64 | :type date: int 65 | :param unit: date's unit (currently ONLY 'days') 66 | :return: date corresponding to the given value 67 | :rtype: ee.Date 68 | """ 69 | if unit == "days": 70 | mili = ee.Number(date).multiply(Date.oneday) 71 | d = ee.Date(mili) 72 | # dstr = d.format() 73 | return d 74 | -------------------------------------------------------------------------------- /geebap/priority.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ee 3 | from datetime import date 4 | from geetools import collection 5 | from geetools.collection.group import CollectionGroup 6 | 7 | # IDS 8 | ID1 = 'LANDSAT/LM01/C01/T1' 9 | ID2 = 'LANDSAT/LM02/C01/T1' 10 | ID3 = 'LANDSAT/LM03/C01/T1' 11 | ID4TOA = 'LANDSAT/LT04/C01/T1_TOA' 12 | ID4SR = 'LANDSAT/LT04/C01/T1_SR' 13 | ID5TOA = 'LANDSAT/LT05/C01/T1_TOA' 14 | ID5SR = 'LANDSAT/LT05/C01/T1_SR' 15 | ID7TOA = 'LANDSAT/LE07/C01/T1_TOA' 16 | ID7SR = 'LANDSAT/LE07/C01/T1_SR' 17 | ID8TOA = 'LANDSAT/LC08/C01/T1_TOA' 18 | ID8SR = 'LANDSAT/LC08/C01/T1_SR' 19 | S2 = 'COPERNICUS/S2' 20 | S2SR = 'COPERNICUS/S2_SR' 21 | 22 | 23 | class SeasonPriority(object): 24 | """ Satellite priorities for seasons. 25 | 26 | :param breaks: list of years when there is a break 27 | :param periods: nested list of periods 28 | :param satlist: nested list of satellites in each period 29 | :param relation: dict of relations 30 | :param ee_relation: EE dict of relations 31 | """ 32 | breaks = [1972, 1974, 1976, 1978, 1982, 1983, 33 | 1994, 1999, 2003, 2012, 2013, date.today().year+1] 34 | 35 | periods = [] 36 | for i, b in enumerate(breaks): 37 | if i < len(breaks) - 1: 38 | periods.append(range(b, breaks[i + 1])) 39 | 40 | satlist = [[ID1], # 72 74 41 | [ID2, ID1], # 74 76 42 | [ID3, ID2, ID1], # 76 78 43 | [ID3, ID2], # 78 82 44 | [ID4SR, ID4TOA, ID3, ID2], # 82 83 45 | [ID5SR, ID5TOA, ID4SR, ID4TOA], # 83 94 46 | [ID5SR, ID5TOA], # 94 99 47 | [ID7SR, ID7TOA, ID5SR, ID5TOA], # 99 03 48 | [ID5SR, ID5TOA, ID7SR, ID7TOA], # 03 12 49 | [ID8SR, ID8TOA, ID7SR, ID7TOA, ID5SR, ID5TOA], # 12 13 50 | [ID8SR, ID8TOA, ID7SR, ID7TOA]] # 13 - 51 | 52 | relation = dict( 53 | [(p, sat) for per, sat in zip(periods, satlist) for p in per]) 54 | 55 | ee_relation = ee.Dictionary(relation) 56 | 57 | l7_slc_off = range(2003, date.today().year+1) 58 | 59 | def __init__(self, year): 60 | self.year = year 61 | 62 | @property 63 | def satellites(self): 64 | ''' 65 | :return: list of satellite's ids 66 | :rtype: list 67 | ''' 68 | return self.relation[self.year] 69 | 70 | @property 71 | def collections(self): 72 | ''' 73 | :return: list of satcol.Collection 74 | :rtype: list 75 | ''' 76 | sat = self.satellites 77 | return [collection.fromId(id) for id in sat] 78 | 79 | @property 80 | def colgroup(self): 81 | ''' 82 | :rtype: CollectionGroup 83 | ''' 84 | return CollectionGroup(*self.collections) 85 | -------------------------------------------------------------------------------- /tests/test_outliers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ee 4 | ee.Initialize() 5 | from geebap import scores 6 | from geetools import tools, collection 7 | 8 | 9 | maxDiff = None 10 | band = "B1" 11 | p = ee.Geometry.Point(-71.5, -42.5) 12 | 13 | col = collection.Landsat8TOA().collection 14 | col = col.filterBounds(p).filterDate("2016-11-15", "2017-03-15") 15 | 16 | # TEST: MASK VALUES LESS THAN 0.11 ## 17 | def mask(img): 18 | m = img.select(band).lte(0.11) 19 | return img.updateMask(m.Not()) 20 | 21 | col = col.map(mask).map(lambda img: img.unmask()) 22 | 23 | 24 | def test_median(): 25 | ''' 26 | # SECTION TO GENERATE THE RESULT TO COMPARE 27 | def listval(band): 28 | def wrap(img, it): 29 | val = img.reduceRegion(ee.Reducer.first(), p, 30).get(band) 30 | return ee.List(it).add(ee.Number(val)) 31 | return wrap 32 | 33 | list = col.iterate(listval(band), ee.List([])) 34 | 35 | # Sort list of values 36 | list = ee.List(list).sort() 37 | 38 | # assertEqual(list.getInfo(), val_list) 39 | 40 | # SCORE 41 | mean = ee.Number(list.reduce(ee.Reducer.mean())) 42 | std = ee.Number(list.reduce(ee.Reducer.stdDev())) 43 | 44 | min = mean.subtract(std) 45 | max = mean.add(std) 46 | 47 | def compute_score(el): 48 | el = ee.Number(el) 49 | cond = el.gte(min).And(el.lte(max)) 50 | 51 | return ee.Algorithms.If(cond, 52 | ee.List([ee.Number(el).multiply(10000).format('%.0f'), 1]), 53 | ee.List([ee.Number(el).multiply(10000).format('%.0f'), 0])) 54 | 55 | to_compare = ee.Dictionary(ee.List(list.map(compute_score)).flatten()).getInfo() 56 | ''' 57 | 58 | # OUTLIER SCORE 59 | score = scores.Outliers((band,)) 60 | newcol = score.map(col) 61 | 62 | to_compare = { 63 | "1210": 1, "7579": 0, "1159": 1, "1399": 1, "1171": 1, "1240": 1, 64 | "1200": 1, "1132": 0, "1328": 1, "1123": 0, "1168": 1, "1150": 1, 65 | "1178": 1, "0": 0, "1874": 0, "1846": 1, "3090": 0, "1100": 0, 66 | "1341": 1, "1249": 1, "1866": 1} 67 | 68 | # OUTLIERS SCORE 69 | val_dict = tools.imagecollection.getValues(newcol, p, 30, side='client') 70 | 71 | # OUTLIER SCORE 72 | compare = [(str(int(round(val[band]*10000))), val["score-outlier"]) for key, val in val_dict.items()] 73 | compare = dict(compare) 74 | 75 | assert to_compare == compare 76 | 77 | 78 | def test_mean(): 79 | score = scores.Outliers((band,), 'mean') 80 | newcol = score.map(col) 81 | 82 | to_compare = { 83 | "1210": 1, "7579": 0, "1159": 1, "1399": 1, "1171": 1, "1240": 1, 84 | "1200": 1, "1132": 1, "1328": 1, "1123": 1, "1168": 1, "1150": 1, 85 | "1178": 1, "0": 0, "1874": 1, "1846": 1, "3090": 0, "1100": 1, 86 | "1341": 1, "1249": 1, "1866": 1} 87 | 88 | # OUTLIERS SCORE 89 | val_dict = tools.imagecollection.getValues(newcol, p, 30, side='client') 90 | 91 | # OUTLIER SCORE 92 | compare = [(str(int(round(val[band]*10000))), val[score.name]) for key, val in val_dict.items()] 93 | compare = dict(compare) 94 | 95 | assert to_compare == compare -------------------------------------------------------------------------------- /geebap/masks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Common masks to use in BAP process """ 3 | from geetools import cloud_mask 4 | 5 | def _get_function(col, band, option, renamed=False): 6 | """ Get mask function for given band and option """ 7 | band_options = col.bitOptions(renamed) 8 | f = lambda img: img 9 | if band in band_options: 10 | bit_options = band_options[band] 11 | if option in bit_options: 12 | f = lambda img: col.applyMask(img, band, [option], 13 | renamed=renamed) 14 | return f 15 | 16 | 17 | class Mask(object): 18 | """ Compute common masks regarding the given collection. Looks for 19 | pixel_qa, BQA, sr_cloud_qa and QA60 bands in that order """ 20 | def __init__(self, options=None): 21 | self.options = options 22 | 23 | def map(self, collection, **kwargs): 24 | """ Map the mask function over a collection 25 | 26 | :param collection: the ImageCollection 27 | :type collection: ee.ImageCollection 28 | :param renamed: whether the collection is renamed or not 29 | :type renamed: bool 30 | :param col: the EE Collection 31 | :type col: geetools.collection.Collection 32 | :return: the ImageCollection with all images masked 33 | :rtype: ee.ImageCollection 34 | """ 35 | col = kwargs.get('col') 36 | renamed = kwargs.get('renamed', False) 37 | f = col.common_masks[0] 38 | if self.options: 39 | collection = collection.map(lambda i: f(i, self.options, renamed)) 40 | else: 41 | collection = collection.map(lambda i: f(i, renamed=renamed)) 42 | 43 | return collection 44 | 45 | 46 | class Hollstein(object): 47 | """ Compute Hollstein mask for Sentinel 2 """ 48 | def __init__(self, options=('cloud', 'shadow', 'snow')): 49 | self.options = options 50 | 51 | def map(self, collection, **kwargs): 52 | """ Map the mask function over a collection 53 | 54 | :param collection: the ImageCollection 55 | :type collection: ee.ImageCollection 56 | :param renamed: whether the collection is renamed or not 57 | :type renamed: bool 58 | :param col: the EE Collection 59 | :type col: geetools.collection.Collection 60 | :return: the ImageCollection with all images masked 61 | :rtype: ee.ImageCollection 62 | """ 63 | col = kwargs.get('col') 64 | renamed = kwargs.get('renamed', False) 65 | 66 | bands = [] 67 | for band in ['aerosol', 'blue', 'green', 'red_edge_1', 'red_edge_2', 68 | 'red_edge_3', 'red_edge_4', 'water_vapor', 'cirrus', 69 | 'swir']: 70 | if renamed: 71 | bands.append(band) 72 | else: 73 | bands.append(col.get_band(band, 'name').id) 74 | 75 | if 'hollstein' in col.algorithms: 76 | f = lambda img: cloud_mask.applyHollstein(img, self.options, 77 | *bands) 78 | return collection.map(f) 79 | else: 80 | return lambda img: img -------------------------------------------------------------------------------- /geebap/filters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Module holding custom filters for image collections """ 3 | from abc import ABCMeta, abstractmethod 4 | from .regdec import * 5 | 6 | __all__ = [] 7 | factory = {} 8 | 9 | 10 | class Filter(object): 11 | """ Abstract Base class for filters """ 12 | __metaclass__ = ABCMeta 13 | def __init__(self, **kwargs): 14 | pass 15 | 16 | @abstractmethod 17 | def apply(self, colEE, **kwargs): 18 | pass 19 | 20 | @register(factory) 21 | @register_all(__all__) 22 | class CloudCover(Filter): 23 | """ Cloud cover percentage filter 24 | 25 | :param percent: all values over this will be filtered. Goes from 0 26 | to 100 27 | :type percent: int 28 | :param kwargs: 29 | """ 30 | def __init__(self, percent=70, **kwargs): 31 | super(CloudCover, self).__init__(**kwargs) 32 | self.percent = percent 33 | self.name = 'CloudCover' 34 | 35 | def apply(self, collection, **kwargs): 36 | """ Apply the filter 37 | 38 | :param colEE: the image collection to apply the filter 39 | :type colEE: ee.ImageCollection 40 | :param col: if the collection is given, it will take the name of the 41 | attribute holding the 'cloud cover' information 42 | :type col: satcol.Collection 43 | :param prop: if the collection is not given, the name of the attribute 44 | holding the 'cloud cover' information can be given 45 | :type prop: str 46 | :param kwargs: 47 | :return: 48 | """ 49 | col = kwargs.get("col") 50 | if col.cloud_cover: 51 | return collection.filterMetadata(col.cloud_cover, "less_than", 52 | self.percent) 53 | elif 'prop' in kwargs.keys(): 54 | prop = kwargs.get('prop') 55 | return collection.filterMetadata(prop, 'less_than', self.percent) 56 | else: 57 | return collection 58 | 59 | 60 | @register(factory) 61 | @register_all(__all__) 62 | class MaskCover(Filter): 63 | """ This mask can only be used AFTER computing mask percentage score 64 | (`scores.MaskPercent`). This score writes an attribute to the image 65 | which contains 'mask percentage' of the given area (see score's docs). 66 | This filter uses this value to apply a filter. I's similar to common 67 | 'cloud cover' filter, but takes account only the given area and not 68 | the whole scene. 69 | 70 | :param percent: all values over this will be filtered. Goes from 0 to 1 71 | :type percent: float 72 | :param prop: name of the property containing the 'mask percentage' 73 | :param prop: str 74 | :param kwargs: 75 | """ 76 | def __init__(self, percent=0.7, prop="score-maskper", **kwargs): 77 | super(MaskCover, self).__init__(**kwargs) 78 | self.percent = percent 79 | self.prop = prop 80 | self.name = 'MaskCover' 81 | 82 | def apply(self, collection, **kwargs): 83 | """ Apply the filter 84 | 85 | :param collection: the image collection to apply the filter 86 | :type collection: ee.ImageCollection 87 | :return: 88 | """ 89 | return collection.filterMetadata(self.prop, "less_than", self.percent) 90 | -------------------------------------------------------------------------------- /geebap/expgen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Generate expression compatible with Google Earth Engine """ 3 | import simpleeval as sval 4 | import ast 5 | import math 6 | 7 | 8 | class ExpGen(object): 9 | def __init__(self): 10 | pass 11 | 12 | @staticmethod 13 | def max(a, b): 14 | """ Generates the expression max(a, b) to insert on GEE expression 15 | 16 | :param a: one value to compare 17 | :param b: the other value to compare 18 | :return: expression 19 | :rtype: str 20 | """ 21 | 22 | exp = "({a}>{b}?{a}:{b})".format(a=a, b=b) 23 | return exp 24 | 25 | @staticmethod 26 | def min(a, b): 27 | """ Generates the expression min(a, b) to insert on GEE expression 28 | 29 | :param a: one value to compare 30 | :param b: the other value to compare 31 | :return: expression 32 | :rtype: str 33 | """ 34 | 35 | exp = "({a}<{b}?{a}:{b})".format(a=a, b=b) 36 | return exp 37 | 38 | @staticmethod 39 | def parse(expr): 40 | s = SvalEE() 41 | return s.eval(expr) 42 | 43 | 44 | def cat(op, group=True): 45 | def wrap(a, b): 46 | if group: 47 | return "({}{}{})".format(a, op, b) 48 | else: 49 | return "{}{}{}".format(a, op, b) 50 | return wrap 51 | 52 | 53 | def cat_fun(nom_fun): 54 | def wrap(arg): 55 | return "{}({})".format(nom_fun, arg) 56 | return wrap 57 | 58 | 59 | def cat_band(band): 60 | return "b('{}')".format(band) 61 | 62 | 63 | # CLEAN sval 64 | DEFAULT_NAMES = {"pi": math.pi, 65 | "e": math.e} 66 | DEFAULT_FUNCTIONS = {"max": ExpGen.max, 67 | "min": ExpGen.min, 68 | "b": cat_band, 69 | "exp": cat_fun("exp"), 70 | "sqrt": cat_fun("sqrt")} 71 | DEFAULT_OPERATORS = {ast.Add: cat("+"), 72 | ast.UAdd: lambda a: '+{}'.format(a), 73 | ast.Sub: cat("-"), 74 | ast.USub: lambda a: '-{}'.format(a), 75 | ast.Mult: cat("*"), 76 | ast.Div: cat("/"), 77 | ast.FloorDiv: cat("//"), 78 | ast.Pow: cat("**"), 79 | ast.Mod: cat("%"), 80 | ast.Eq: cat("=="), 81 | ast.NotEq: cat("!="), 82 | ast.Gt: cat(">"), 83 | ast.Lt: cat("<"), 84 | ast.GtE: cat(">="), 85 | ast.LtE: cat("<=")} 86 | # ast.Not: op.not_, 87 | # ast.USub: op.neg, 88 | # ast.UAdd: op.pos, 89 | # ast.In: lambda x, y: op.contains(y, x), 90 | # ast.NotIn: lambda x, y: not op.contains(y, x), 91 | # ast.Is: lambda x, y: x is y, 92 | # ast.IsNot: lambda x, y: x is not y,} 93 | 94 | 95 | class SvalEE(sval.SimpleEval): 96 | def __init__(self, **kwargs): 97 | super(SvalEE, self).__init__(**kwargs) 98 | 99 | self.operators = DEFAULT_OPERATORS 100 | self.functions = DEFAULT_FUNCTIONS 101 | self.names = DEFAULT_NAMES -------------------------------------------------------------------------------- /geebap/functions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ee 3 | from geetools import collection 4 | 5 | 6 | def get_id_col(id): 7 | """ get the corresponding collection of the given id """ 8 | try: 9 | col = collection.IDS[id] 10 | except IndexError: 11 | raise IndexError 12 | else: 13 | return col 14 | 15 | 16 | def get_col_id(col): 17 | return collection.IDS.index(col.id) 18 | 19 | 20 | def get_col_id_image(col, name='col_id'): 21 | return ee.Image.constant(get_col_id(col)).rename(name).toUint8() 22 | 23 | 24 | def drange(ini, end, step=1, places=0): 25 | """ Create a range of floats 26 | 27 | :param ini: initial value (as in range) 28 | :param end: final value (as in range) 29 | :param step: Similar to range, except that if decimal places are specified 30 | the step is done between decimal places. 31 | :param places: decimal places 32 | :return: range 33 | :rtype: list 34 | """ 35 | factor = 10**places if places>0 else 1 36 | ini *= factor 37 | end = end * factor - factor + 1 38 | result = [float(val) / factor for val in range(int(ini), int(end), step)] 39 | return result 40 | 41 | 42 | def replace_duplicate(list, separator="_"): 43 | """ replace duplicated values from a list adding a suffix with a number 44 | 45 | :param list: list to be processed 46 | :type list: list 47 | :param separator: string to separate the name and the suffix 48 | :type separator: str 49 | :return: new list with renamed values 50 | :rtype: list 51 | """ 52 | def wrap(a): 53 | newlist = [a[0]] # list with first element 54 | for i, v in enumerate(a): 55 | if i == 0: continue # skip first element 56 | # t = a[i+1:] 57 | if v in newlist: 58 | if separator in v: 59 | two = v.split(separator) 60 | orig = two[0] 61 | n = int(two[1]) 62 | newval = "{}{}{}".format(orig, separator, n+1) 63 | else: 64 | newval = v+separator+"1" 65 | newlist.append(newval) 66 | else: 67 | newlist.append(v) 68 | return newlist 69 | 70 | new = wrap(list) 71 | while(new != list): 72 | list = new[:] 73 | new = wrap(new) 74 | return(new) 75 | 76 | 77 | def nirXred(nir="NIR", red="RED", output="nirXred"): 78 | """ Creates a NIR times RED band 79 | 80 | :param nir: name of the NIR band 81 | :type nir: str 82 | :param red: name of the RED band 83 | :type red: str 84 | :param output: name of the output band 85 | :type output: str 86 | :return: 87 | :rtype: function 88 | """ 89 | def wrap(img): 90 | n = img.select([nir]) 91 | r = img.select([red]) 92 | nirXred = n.multiply(r).select([0], [output]) 93 | return img.addBands(nirXred) 94 | return wrap 95 | 96 | 97 | def unmask_slc_off(image): 98 | """ Unmask pixels that are masked in ALL bands """ 99 | mask = image.mask() 100 | reduced = mask.reduce('sum') 101 | slc_off = reduced.eq(0) 102 | unmasked = image.unmask() 103 | newmask = mask.where(slc_off, 1) 104 | return unmasked.updateMask(newmask) -------------------------------------------------------------------------------- /geebap/sites.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ A simple module to store sites from fusion tables """ 3 | import ee 4 | import csv 5 | import requests 6 | 7 | 8 | class Site(object): 9 | """ Site Class to store sites related to fusion tables """ 10 | def __init__(self, name=None, id_ft=None, id_fld=None, name_fld=None, 11 | n_feat=0): 12 | """ 13 | :param name: name of the site 14 | :type name: str 15 | :param id_ft: Fusion Table ID 16 | :type id_ft: str 17 | :param id_fld: Name of the Fusion Table Field that contains the id for 18 | each individual site 19 | :type id_fld: str 20 | :param name_fld: Name of the Fusion Table Field that contains the name 21 | for each individual site 22 | :type name_fld: str 23 | :param n_feat: Number of features that contain the Fusion Table 24 | :type n_feat: int 25 | """ 26 | self.name = name 27 | self.id_ft = id_ft 28 | self.id_fld = id_fld 29 | self.name_fld = name_fld 30 | self.n_feat = n_feat 31 | 32 | @property 33 | def ft(self): 34 | if self.id_ft: 35 | return ee.FeatureCollection("ft:"+self.id_ft) 36 | else: 37 | return None 38 | 39 | def filter_id(self, id): 40 | """ Filters the fusion table by the given id 41 | 42 | :param id: id to filter 43 | :type id: int 44 | :return: ee.Feature, region (as a list of lists) 45 | :rtype: tuple 46 | """ 47 | try: 48 | place = self.ft.filterMetadata(self.id_fld, "equals", id) 49 | place = ee.Feature(place.first()) 50 | place = place.set("origin", self.name, "id", id) 51 | 52 | try: 53 | region = place.geometry().bounds().getInfo()['coordinates'][0] 54 | except AttributeError as ae: 55 | print(ae) 56 | region = place.getInfo()['coordinates'][0] 57 | except Exception as e: 58 | print(e) 59 | return None, None 60 | 61 | return place, region 62 | except Exception as e: 63 | # print "Hubo un error al filtrar el ID" 64 | print(e) 65 | return None, None 66 | 67 | 68 | def from_csv(file, name="name", id_ft="id_ft", id_fld="id_fld", 69 | name_fld=None): 70 | """ Generates a dictionary of Sites from a csv file. 71 | 72 | :param name: 73 | :param id_ft: 74 | :param id_fld: 75 | :param name_fld: 76 | """ 77 | sites = [] 78 | with open(file) as csvfile: 79 | reader = csv.DictReader(csvfile) 80 | for row in reader: 81 | params = [row[name], row[id_ft], row[id_fld]] 82 | params = params.append(row[name_fld]) if name_fld else params 83 | site = (row[name], Site(*params)) 84 | sites.append(site) 85 | 86 | return dict(sites) 87 | 88 | 89 | def from_gsheet(url, sheet, name=None, id_ft=None, id_fld=None, name_fld=None): 90 | """ Generates a dictionary of Sites from a Google SpreadSheet. It must be 91 | Public and shared to anyone. Doesn't use any API 92 | 93 | :param url: 94 | :param sheet: 95 | :param name: name of field that holds the name of the site 96 | :param id_ft: name of field that holds the id of the fusion table 97 | :param id_fld: name of field that holds the ID of the site 98 | :param name_fld: 99 | """ 100 | content = requests.get(url) 101 | json = content.json() 102 | sheet = json[sheet] 103 | sites = [] 104 | 105 | for n, row in enumerate(sheet): 106 | if n == 0: continue 107 | if row[name] == "": continue 108 | # params = [row[name], row[id_ft], row[id_fld]] 109 | # print params 110 | # params = params.append(row[name_fld]) if name_fld else params 111 | site = (row[name], Site(name=row[name], id_ft=row[id_ft], 112 | id_fld=row[id_fld], name_fld=row[name_fld])) 113 | sites.append(site) 114 | 115 | return dict(sites) 116 | -------------------------------------------------------------------------------- /geebap/ipytools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Tools to use in IPython """ 3 | 4 | import ee 5 | from geetools import tools 6 | from . import date, functions 7 | 8 | 9 | def information(): 10 | pass 11 | 12 | 13 | def info2map(map): 14 | """ Add an information Tab to a map displayed with `geetools.ipymap` 15 | module 16 | 17 | :param map: the Map where the tab will be added 18 | :type map: geetools.ipymap.Map 19 | :return: 20 | """ 21 | try: 22 | from ipywidgets import Accordion 23 | except: 24 | print('Cannot use ipytools without ipywidgets installed\n' 25 | 'ipywidgets.readthedocs.io') 26 | 27 | map.addTab('BAP Inspector', info_handler, Accordion()) 28 | 29 | 30 | def info_handler(**kwargs): 31 | """ Handler for the Bap Inspector Tab of the Map 32 | 33 | :param change: 34 | :return: 35 | """ 36 | try: 37 | from ipywidgets import HTML 38 | except: 39 | print('Cannot use ipytools without ipywidgets installed\n' 40 | 'ipywidgets.readthedocs.io') 41 | 42 | themap = kwargs['map'] 43 | widget = kwargs['widget'] 44 | # Get click coordinates 45 | coords = kwargs['coordinates'] 46 | 47 | event = kwargs['type'] # event type 48 | if event == 'click': # If the user clicked 49 | # create a point where the user clicked 50 | point = ee.Geometry.Point(coords) 51 | 52 | # First Accordion row text (name) 53 | first = 'Point {} at {} zoom'.format(coords, themap.zoom) 54 | 55 | # Reset list of widgets and names 56 | namelist = [first] 57 | wids4acc = [HTML('')] # first row has no content 58 | 59 | length = len(themap.EELayers.keys()) 60 | i = 1 61 | 62 | for name, obj in themap.EELayers.items(): # for every added layer 63 | # Clear children // Loading 64 | widget.children = [HTML('wait a second please..')] 65 | widget.set_title(0, 'Click on {}. Loading {} of {}'.format(coords, i, length)) 66 | i += 1 67 | 68 | # IMAGES 69 | if obj['type'] == 'Image': 70 | # Get the image's values 71 | image = obj['object'] 72 | properties = image.propertyNames().getInfo() 73 | 74 | if 'BAP_version' in properties: # Check if it's a BAP composite 75 | try: 76 | values = tools.image.getValue(image, point, 10, 'client') 77 | values = tools.dictionary.sort(values) 78 | col_id = int(values['col_id']) 79 | thedate = int(values['date']) 80 | collection = functions.get_id_col(col_id) 81 | realdate = date.Date.get(thedate).format().getInfo() 82 | 83 | # Get properties of the composite 84 | inidate = int(image.get('ini_date').getInfo()) 85 | inidate = date.Date.get(inidate).format().getInfo() 86 | enddate = int(image.get('end_date').getInfo()) 87 | enddate = date.Date.get(enddate).format().getInfo() 88 | 89 | # Create the content 90 | img_html = ''' 91 |

General Properties

92 | Season starts at: {ini}
93 | Season ends at: {end}
94 |

Information at point

95 | Collection: {colid} ({col})
96 | Date: {thedate} ({date})'''.format(ini=inidate, 97 | end=enddate, col=collection, date=realdate, p=coords, 98 | thedate=thedate, colid=col_id) 99 | 100 | wid = HTML(img_html) 101 | # append widget to list of widgets 102 | wids4acc.append(wid) 103 | namelist.append(name) 104 | except Exception as e: 105 | widget = HTML(str(e).replace('<','{').replace('>','}')) 106 | text = 'ERROR in layer {}'.format(name) 107 | wids4acc.append(widget) 108 | namelist.append(text) 109 | else: 110 | continue 111 | # GEOMETRIES 112 | elif obj['type'] == 'Geometry': 113 | continue 114 | 115 | # BAP Widget 116 | # bapwid = m.childrenDict['BAP Inspector'] 117 | widget.children = wids4acc 118 | 119 | for i, n in enumerate(namelist): 120 | widget.set_title(i, n) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Best Available Pixel (Bap) Composite using the Python API of Google Earth Engine (Gee) 2 | -------------------------------------------------------------------------------------- 3 | 4 | This code is based on *Pixel-Based Image Compositing for Large-Area Dense Time 5 | Series Applications and Science. (White et al., 2014)* 6 | http://www.tandfonline.com/doi/full/10.1080/07038992.2014.945827 7 | 8 | It uses a series of pixel based scores to generate a composite with the 9 | *Best Available Pixel*, assuming it is the one that has better score. 10 | 11 | License and Copyright 12 | --------------------- 13 | 14 | 2017 Rodrigo E. Principe - geebap - https://github.com/fitoprincipe/geebap 15 | 16 | Contact 17 | ------- 18 | 19 | Rodrigo E. Principe: fitoprincipe82@gmail.com 20 | 21 | Installation 22 | ------------ 23 | 24 | To use this package you must have installed and running Google Earth Engine 25 | Python API: https://developers.google.com/earth-engine/python_install 26 | 27 | Once you have that, proceed 28 | 29 | :: 30 | 31 | pip install geebap 32 | 33 | this will install also `geetools` that you could use besides `geebap` 34 | 35 | Installation in DataLab 36 | ----------------------- 37 | 38 | After following Option 1 or 2 in https://developers.google.com/earth-engine/python_install, 39 | open a new notebook and write: 40 | 41 | .. code:: python 42 | 43 | import sys 44 | !{sys.executable} -m pip install geebap 45 | 46 | Available Collections 47 | --------------------- 48 | 49 | Collections come from `geetools.collection`. For examples see: 50 | https://github.com/gee-community/gee_tools/tree/master/notebooks/collection 51 | 52 | Available Scores 53 | ---------------- 54 | 55 | - Satellite 56 | - Distance to clouds and shadows masks 57 | - Atmospheric Opacity 58 | - Day of the year (best_doy) 59 | - Masked pixels percentage 60 | - Outliers 61 | - Absolute value of a vegetation index 62 | 63 | Available Indices 64 | ----------------- 65 | 66 | - ndvi 67 | - evi 68 | - nbr 69 | 70 | Some considerations 71 | ------------------- 72 | 73 | - Sites size should not be too big. Works with 300 km2 tiles 74 | 75 | Basic Usage 76 | ----------- 77 | 78 | If you are using Jupyter, you can download a notebook from 79 | https://github.com/fitoprincipe/geebap/blob/master/Best_Available_Pixel_Composite.ipynb 80 | 81 | else, if you are using another approach, like Spyder, create an empty script and 82 | paste the following code: 83 | 84 | .. code:: python 85 | 86 | import ee 87 | ee.Initialize() 88 | 89 | import geebap 90 | from geetools import tools 91 | 92 | import pprint 93 | pp = pprint.PrettyPrinter(indent=2) 94 | 95 | # SEASON 96 | a_season = geebap.Season('11-15', '03-15') 97 | 98 | # MASKS 99 | cld_mask = geebap.masks.Mask() 100 | 101 | # Combine masks in a tuple 102 | masks = (cld_mask,) 103 | 104 | # FILTERS 105 | filt_cld = geebap.filters.CloudCover() 106 | # filt_mask = geebap.filters.MaskCover() # Doesn't work 107 | 108 | # Combine filters in a tuple 109 | filters = (filt_cld,)#, filt_mask) 110 | 111 | # SCORES 112 | best_doy = geebap.scores.Doy('01-15', a_season) 113 | sat = geebap.scores.Satellite() 114 | out = geebap.scores.Outliers(("ndvi",)) 115 | ind = geebap.scores.Index("ndvi") 116 | maskpercent = geebap.scores.MaskPercentKernel() 117 | dist = geebap.scores.CloudDist() 118 | 119 | # Combine scores in a tuple 120 | scores = ( 121 | best_doy, 122 | sat, 123 | out, 124 | ind, 125 | maskpercent, 126 | dist 127 | ) 128 | 129 | # BAP OBJECT 130 | BAP = geebap.Bap(range=(0, 0), 131 | season=a_season, 132 | masks=masks, 133 | scores=scores, 134 | filters=filters) 135 | 136 | # SITE 137 | site = ee.Geometry.Polygon([[-71.5,-42.5], 138 | [-71.5,-43], 139 | [-72,-43], 140 | [-72,-42.5]]) 141 | 142 | # COMPOSITE 143 | composite = BAP.build_composite_best(2019, site=site, indices=("ndvi",)) 144 | 145 | # `composite` is a ee.Image object, so you can do anything 146 | # from here.. 147 | one_value = tools.image.getValue(composite, 148 | site.centroid(), 149 | 30, 'client') 150 | pp.pprint(one_value) 151 | 152 | *Prints:* 153 | 154 | :: 155 | 156 | { 'blue': 733, 157 | 'col_id': 29, 158 | 'date': 20190201, 159 | 'green': 552, 160 | 'ndvi': 0.7752976417541504, 161 | 'nir': 2524, 162 | 'red': 313, 163 | 'score': 5.351020336151123, 164 | 'swir': 661, 165 | 'swir2': 244, 166 | 'thermal': 2883} 167 | -------------------------------------------------------------------------------- /notebooks/season/Season.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Season object\n", 8 | "This is designed for the purpose of assigning one year to a multiyear range. For example: \n", 9 | "\n", 10 | "- the season goes from November 1 of 2010 to February 1 of 2011\n", 11 | "- I need one image for the whole season that will represent 2011\n", 12 | "\n", 13 | "Seasons do not have a year property" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import ee\n", 23 | "ee.Initialize()" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "from geebap import season as bap_season\n", 33 | "from geetools import ui" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "south = bap_season.Season('11-01', '02-01')" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "## Start, end and DOY (day of year) of season" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 19, 55 | "metadata": {}, 56 | "outputs": [ 57 | { 58 | "data": { 59 | "text/plain": [ 60 | "'11-01'" 61 | ] 62 | }, 63 | "execution_count": 19, 64 | "metadata": {}, 65 | "output_type": "execute_result" 66 | } 67 | ], 68 | "source": [ 69 | "south.start.date" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 20, 75 | "metadata": {}, 76 | "outputs": [ 77 | { 78 | "data": { 79 | "text/plain": [ 80 | "'02-01'" 81 | ] 82 | }, 83 | "execution_count": 20, 84 | "metadata": {}, 85 | "output_type": "execute_result" 86 | } 87 | ], 88 | "source": [ 89 | "south.end.date" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "Over end indicates whether the season is contained in two years (True) or in one (False)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 13, 102 | "metadata": {}, 103 | "outputs": [ 104 | { 105 | "data": { 106 | "text/plain": [ 107 | "True" 108 | ] 109 | }, 110 | "execution_count": 13, 111 | "metadata": {}, 112 | "output_type": "execute_result" 113 | } 114 | ], 115 | "source": [ 116 | "south.over_end" 117 | ] 118 | }, 119 | { 120 | "cell_type": "markdown", 121 | "metadata": {}, 122 | "source": [ 123 | "Add a year to the season" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 14, 129 | "metadata": {}, 130 | "outputs": [ 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "" 135 | ] 136 | }, 137 | "execution_count": 14, 138 | "metadata": {}, 139 | "output_type": "execute_result" 140 | } 141 | ], 142 | "source": [ 143 | "south.add_year(2018)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": 15, 149 | "metadata": {}, 150 | "outputs": [ 151 | { 152 | "data": { 153 | "text/plain": [ 154 | "" 155 | ] 156 | }, 157 | "execution_count": 15, 158 | "metadata": {}, 159 | "output_type": "execute_result" 160 | } 161 | ], 162 | "source": [ 163 | "dr = south.add_year(ee.Number(2018))\n", 164 | "dr" 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 16, 170 | "metadata": {}, 171 | "outputs": [ 172 | { 173 | "name": "stdout", 174 | "output_type": "stream", 175 | "text": [ 176 | "'2017-11-01T00:00:00'\n", 177 | "\n" 178 | ] 179 | } 180 | ], 181 | "source": [ 182 | "ui.eprint(dr.start())" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 17, 188 | "metadata": {}, 189 | "outputs": [ 190 | { 191 | "name": "stdout", 192 | "output_type": "stream", 193 | "text": [ 194 | "'2018-02-01T00:00:00'\n", 195 | "\n" 196 | ] 197 | } 198 | ], 199 | "source": [ 200 | "ui.eprint(dr.end())" 201 | ] 202 | } 203 | ], 204 | "metadata": { 205 | "kernelspec": { 206 | "display_name": "Python 3", 207 | "language": "python", 208 | "name": "python3" 209 | }, 210 | "language_info": { 211 | "codemirror_mode": { 212 | "name": "ipython", 213 | "version": 3 214 | }, 215 | "file_extension": ".py", 216 | "mimetype": "text/x-python", 217 | "name": "python", 218 | "nbconvert_exporter": "python", 219 | "pygments_lexer": "ipython3", 220 | "version": "3.5.2" 221 | } 222 | }, 223 | "nbformat": 4, 224 | "nbformat_minor": 2 225 | } 226 | -------------------------------------------------------------------------------- /notebooks/scores/Threshold.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Threshold Score" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## This score is based in the fact that every band has a \"normal\" range of values defined by a max and min threshold" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import ee\n", 24 | "ee.Initialize()" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 4, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "from geebap import season" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 5, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "from geebap import scores" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 17, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "from geetools import tools, ui, collection" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 7, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "i = ee.Image('COPERNICUS/S2/20181122T142749_20181122T143353_T18GYT')" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 8, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "Map = ui.Map()" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": 9, 75 | "metadata": {}, 76 | "outputs": [ 77 | { 78 | "data": { 79 | "application/vnd.jupyter.widget-view+json": { 80 | "model_id": "7c6a32ca6e954c0db8111faf54c97b0d", 81 | "version_major": 2, 82 | "version_minor": 0 83 | }, 84 | "text/plain": [ 85 | "Map(basemap={'url': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'max_zoom': 19, 'attribution': 'Map …" 86 | ] 87 | }, 88 | "metadata": {}, 89 | "output_type": "display_data" 90 | }, 91 | { 92 | "data": { 93 | "application/vnd.jupyter.widget-view+json": { 94 | "model_id": "e1992af39d3a4a378921163589251cd5", 95 | "version_major": 2, 96 | "version_minor": 0 97 | }, 98 | "text/plain": [ 99 | "Tab(children=(CustomInspector(children=(SelectMultiple(options=OrderedDict(), value=()), Accordion(selected_in…" 100 | ] 101 | }, 102 | "metadata": {}, 103 | "output_type": "display_data" 104 | } 105 | ], 106 | "source": [ 107 | "Map.show()" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": 10, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "vis = dict(bands=['B4', 'B3', 'B2'], min=0, max=3000)" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 11, 122 | "metadata": {}, 123 | "outputs": [], 124 | "source": [ 125 | "Map.addLayer(i, vis, 'S2 Image')" 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": 12, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "Map.centerObject(i)" 135 | ] 136 | }, 137 | { 138 | "cell_type": "markdown", 139 | "metadata": {}, 140 | "source": [ 141 | "Clouds are bright, so values in the visible bands are high. Let's say RGB higher than 2500 are clouds, and below 500 can be shadow" 142 | ] 143 | }, 144 | { 145 | "cell_type": "code", 146 | "execution_count": 13, 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "thresholds = {\n", 151 | " 'B2': {'min':500, 'max':2500},\n", 152 | " 'B3': {'min':500, 'max':2500},\n", 153 | " 'B4': {'min':500, 'max':2500},\n", 154 | "}" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": 14, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "score = scores.Threshold()" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": 15, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "score_image = score.compute(i, thresholds=thresholds)" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": 16, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "Map.addLayer(score_image, None, 'thresholds score')" 182 | ] 183 | } 184 | ], 185 | "metadata": { 186 | "kernelspec": { 187 | "display_name": "Python 3", 188 | "language": "python", 189 | "name": "python3" 190 | }, 191 | "language_info": { 192 | "codemirror_mode": { 193 | "name": "ipython", 194 | "version": 3 195 | }, 196 | "file_extension": ".py", 197 | "mimetype": "text/x-python", 198 | "name": "python", 199 | "nbconvert_exporter": "python", 200 | "pygments_lexer": "ipython3", 201 | "version": "3.5.2" 202 | } 203 | }, 204 | "nbformat": 4, 205 | "nbformat_minor": 2 206 | } 207 | -------------------------------------------------------------------------------- /geebap/season.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ee 3 | from collections import OrderedDict 4 | 5 | 6 | def is_leap(year): 7 | if isinstance(year, (int, float)): 8 | mod = year%4 9 | return True if mod == 0 else False 10 | elif isinstance(year, (ee.Number,)): 11 | mod = year.mod(4) 12 | return mod.Not() 13 | 14 | 15 | def _rel_month_day(leap_year): 16 | months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 17 | days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 18 | pairs = zip(months, days) 19 | 20 | month_day = OrderedDict() 21 | for m, d in pairs: 22 | if leap_year and m == 2: 23 | d = 29 24 | month_day[m] = d 25 | return month_day 26 | 27 | 28 | class SeasonDate(object): 29 | """ A simple class to hold dates as MM-DD """ 30 | def __init__(self, date): 31 | self.date = date 32 | 33 | # Check if format is valid 34 | self.check_valid() 35 | 36 | @property 37 | def month(self): 38 | return int(self.date.split('-')[0]) 39 | 40 | @property 41 | def day(self): 42 | return int(self.date.split('-')[1]) 43 | 44 | @property 45 | def day_of_year(self): 46 | """ Day of the year """ 47 | month = self.month 48 | day = self.day 49 | 50 | if month == 1: 51 | return day 52 | else: 53 | ini = 31 54 | 55 | for rel_month, days in _rel_month_day(True).items(): 56 | if rel_month == 1: continue 57 | 58 | if rel_month != month: 59 | ini += days 60 | else: 61 | break 62 | 63 | return ini + day 64 | 65 | def add_year(self, year): 66 | """ Just add the year """ 67 | if not is_leap(year) and self.date == '02-29': 68 | msg = "Year {} is leap, hence it does't contain day 29 in february" 69 | raise ValueError(msg.format(year)) 70 | return '{}-{}'.format(year, self.date) 71 | 72 | def check_valid(self): 73 | """ Verify if the season date has right format """ 74 | if not isinstance(self.date, str): 75 | mje = "Dates in Season must be strings with format: MM-dd, found " \ 76 | "{}" 77 | raise ValueError(mje.format(self.date)) 78 | 79 | split = self.date.split("-") 80 | assert len(split) == 2, \ 81 | "Error in Season {}: month and day must be divided by '-' " \ 82 | "and with the following format --> MM-dd".format(self.date) 83 | m = int(split[0]) 84 | d = int(split[1]) 85 | 86 | if m < 1 or m > 12: 87 | raise ValueError( 88 | "Error in Season {}: Month must be greater than 1 and less " 89 | "than 12".format(self.date)) 90 | 91 | maxday = _rel_month_day(True)[m] 92 | if d < 1 or d > maxday: 93 | raise ValueError( 94 | "Error in Season {}: In month {} the day must be less or " 95 | "equal than {}".format(self.date, m, maxday)) 96 | 97 | return True 98 | 99 | 100 | class Season(object): 101 | """ Growing season """ 102 | def __init__(self, start, end): 103 | self.start = start 104 | self.end = end 105 | 106 | # START 107 | @property 108 | def start(self): 109 | return self._start 110 | 111 | @start.setter 112 | def start(self, value): 113 | if value is not None: 114 | if not isinstance(value, SeasonDate): 115 | try: 116 | value = SeasonDate(value) 117 | except: 118 | raise ValueError('start date must be an instance of SeasonDate') 119 | self._start = value 120 | else: 121 | raise ValueError('initial date is required') 122 | 123 | # END 124 | @property 125 | def end(self): 126 | return self._end 127 | 128 | @end.setter 129 | def end(self, value): 130 | if value is not None: 131 | if not isinstance(value, SeasonDate): 132 | try: 133 | value = SeasonDate(value) 134 | except: 135 | raise ValueError('end date must be an instance of SeasonDate') 136 | 137 | self._end = value 138 | else: 139 | raise ValueError('end date is required') 140 | 141 | @property 142 | def over_end(self): 143 | """ True if the season goes over the end of the year """ 144 | if self.start.day_of_year >= self.end.day_of_year: 145 | return True 146 | else: 147 | return False 148 | 149 | @property 150 | def range_in_days(self): 151 | return abs(self.start.difference(self.end, self.over_end)) 152 | 153 | def add_year(self, year): 154 | year = ee.Number(year) 155 | if self.over_end: 156 | start_year = year.subtract(1) 157 | else: 158 | start_year = ee.Number(year) 159 | end_year = ee.Number(year) 160 | 161 | sday = self.start.day 162 | eday = self.end.day 163 | 164 | # look for feb 29h in non leap 165 | if not is_leap(year): 166 | if self.start.month == 2 and sday == 29: 167 | sday = 28 168 | if self.end.month == 2 and eday == 29: 169 | eday = 28 170 | 171 | start = ee.Date.fromYMD(start_year, self.start.month, sday) 172 | end = ee.Date.fromYMD(end_year, self.end.month, eday) 173 | daterange = ee.DateRange(ee.Date(start), ee.Date(end)) 174 | return daterange -------------------------------------------------------------------------------- /notebooks/scores/Medoid.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Medoid Score\n", 8 | "Instead of making a complete medoid composite, this score reflects \"how far\" is from the medoid value. So, the medoid pixel will have a value of 1, the closest a little lower, and so on. The core method is in `geetools` package (https://github.com/gee-community/gee_tools) under the module `composite`" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 1, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "import ee\n", 18 | "ee.Initialize()" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 2, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "from geebap import scores" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 3, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "from geetools import tools, ui, collection" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": 4, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "site = ee.Geometry.Point([-69.5, -38.5])" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": 5, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "s2 = collection.Sentinel2TOA()" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 6, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "colEE = s2.collection.filterBounds(site).limit(10)" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": 7, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "renamed = colEE.map(lambda img: s2.rename(img))" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": 8, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "score = scores.Medoid(bands=['red', 'green', 'blue', 'nir', 'swir'])" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": 9, 87 | "metadata": {}, 88 | "outputs": [], 89 | "source": [ 90 | "computed_col = score.map(renamed)" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": 10, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "Map = ui.Map()" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 11, 105 | "metadata": {}, 106 | "outputs": [ 107 | { 108 | "data": { 109 | "application/vnd.jupyter.widget-view+json": { 110 | "model_id": "f94b66bde95d47d58397618b5bf9c244", 111 | "version_major": 2, 112 | "version_minor": 0 113 | }, 114 | "text/plain": [ 115 | "Map(basemap={'max_zoom': 19, 'attribution': 'Map data (c) OpenStreetMapOpenStreetMap contributors',…" 163 | ] 164 | }, 165 | "metadata": {}, 166 | "output_type": "display_data" 167 | }, 168 | { 169 | "data": { 170 | "application/vnd.jupyter.widget-view+json": { 171 | "model_id": "04e356c6a1314678adef27e6342218cd", 172 | "version_major": 2, 173 | "version_minor": 0 174 | }, 175 | "text/plain": [ 176 | "Tab(children=(CustomInspector(children=(SelectMultiple(options=OrderedDict(), value=()), Accordion(selected_in…" 177 | ] 178 | }, 179 | "metadata": {}, 180 | "output_type": "display_data" 181 | } 182 | ], 183 | "source": [ 184 | "Map = ui.Map()\n", 185 | "Map.show()" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 16, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "Map.addLayer(first, {'bands':['ndvi'], 'min':0, 'max':1}, 'First NDVI')" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 17, 200 | "metadata": {}, 201 | "outputs": [], 202 | "source": [ 203 | "Map.addLayer(points, None, 'test points')" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 18, 209 | "metadata": {}, 210 | "outputs": [], 211 | "source": [ 212 | "Map.centerObject(points)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 19, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "score = scores.Index('ndvi', target=0.5, function='gauss')" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 20, 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [ 230 | "computed = score.map(colEE)" 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 21, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "first_computed = ee.Image(computed.first())" 240 | ] 241 | }, 242 | { 243 | "cell_type": "code", 244 | "execution_count": 22, 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "Map.addLayer(first_computed, {'bands':[score.name], 'min':0, 'max':1}, 'Computed Score')" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 23, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "chart = ui.chart.Image.bandsByRegion(first_computed, points, xProperty='ndvi', bands=[score.name])" 258 | ] 259 | }, 260 | { 261 | "cell_type": "code", 262 | "execution_count": 24, 263 | "metadata": {}, 264 | "outputs": [ 265 | { 266 | "data": { 267 | "application/vnd.jupyter.widget-view+json": { 268 | "model_id": "5e6de8f7733e458b9558ad8f16748f2e", 269 | "version_major": 2, 270 | "version_minor": 0 271 | }, 272 | "text/plain": [ 273 | "HTML(value='OpenStreetMapOpenStreetMap\n", 285 | "\n", 298 | "\n", 299 | " \n", 300 | " \n", 301 | " \n", 302 | " \n", 303 | " \n", 304 | " \n", 305 | " \n", 306 | " \n", 307 | " \n", 308 | " \n", 309 | " \n", 310 | " \n", 311 | " \n", 312 | " \n", 313 | " \n", 314 | " \n", 315 | " \n", 316 | " \n", 317 | " \n", 318 | " \n", 319 | " \n", 320 | " \n", 321 | " \n", 322 | " \n", 323 | " \n", 324 | " \n", 325 | " \n", 326 | " \n", 327 | " \n", 328 | " \n", 329 | " \n", 330 | " \n", 331 | " \n", 332 | " \n", 333 | " \n", 334 | " \n", 335 | " \n", 336 | " \n", 337 | " \n", 338 | " \n", 339 | " \n", 340 | " \n", 341 | " \n", 342 | " \n", 343 | " \n", 344 | " \n", 345 | " \n", 346 | " \n", 347 | " \n", 348 | " \n", 349 | " \n", 350 | " \n", 351 | " \n", 352 | " \n", 353 | " \n", 354 | " \n", 355 | " \n", 356 | " \n", 357 | " \n", 358 | " \n", 359 | " \n", 360 | " \n", 361 | " \n", 362 | " \n", 363 | " \n", 364 | " \n", 365 | " \n", 366 | " \n", 367 | " \n", 368 | " \n", 369 | " \n", 370 | " \n", 371 | " \n", 372 | " \n", 373 | " \n", 374 | " \n", 375 | " \n", 376 | " \n", 377 | " \n", 378 | " \n", 379 | " \n", 380 | " \n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | " \n", 387 | " \n", 388 | " \n", 389 | " \n", 390 | " \n", 391 | " \n", 392 | " \n", 393 | " \n", 394 | " \n", 395 | " \n", 396 | " \n", 397 | " \n", 398 | " \n", 399 | " \n", 400 | " \n", 401 | " \n", 402 | " \n", 403 | " \n", 404 | " \n", 405 | "
gauss stretch 1gauss stretch 2linear
2017-11-19 14:24:19.9404.965734e-012.554828e-010.587694
2017-11-26 14:30:03.6606.937751e-014.888855e-010.701388
2017-11-26 14:30:27.6206.937827e-014.888960e-010.701392
2017-12-05 14:24:15.4609.092028e-018.295881e-010.847416
2017-12-12 14:30:03.0209.938406e-019.879371e-010.961110
2017-12-12 14:30:26.9809.938421e-019.879398e-010.961114
2017-12-28 14:30:03.7608.190668e-016.761451e-010.779168
2018-01-06 14:24:13.7005.751663e-013.395132e-010.633140
2018-01-13 14:29:57.3803.849014e-011.565757e-010.519446
2018-01-13 14:30:21.3403.848944e-011.565702e-010.519442
2018-01-22 14:24:05.6201.924456e-014.256673e-020.373419
2018-01-29 14:29:47.9809.462637e-021.200343e-020.259726
2018-01-29 14:30:11.9409.462335e-021.200277e-020.259721
2018-02-07 14:23:57.4902.516278e-021.506232e-030.113698
2018-02-14 14:29:42.1106.663433e-072.371716e-080.000005
2018-02-14 14:30:06.0700.000000e+000.000000e+000.000000
\n", 406 | "" 407 | ], 408 | "text/plain": [ 409 | " gauss stretch 1 gauss stretch 2 linear\n", 410 | "2017-11-19 14:24:19.940 4.965734e-01 2.554828e-01 0.587694\n", 411 | "2017-11-26 14:30:03.660 6.937751e-01 4.888855e-01 0.701388\n", 412 | "2017-11-26 14:30:27.620 6.937827e-01 4.888960e-01 0.701392\n", 413 | "2017-12-05 14:24:15.460 9.092028e-01 8.295881e-01 0.847416\n", 414 | "2017-12-12 14:30:03.020 9.938406e-01 9.879371e-01 0.961110\n", 415 | "2017-12-12 14:30:26.980 9.938421e-01 9.879398e-01 0.961114\n", 416 | "2017-12-28 14:30:03.760 8.190668e-01 6.761451e-01 0.779168\n", 417 | "2018-01-06 14:24:13.700 5.751663e-01 3.395132e-01 0.633140\n", 418 | "2018-01-13 14:29:57.380 3.849014e-01 1.565757e-01 0.519446\n", 419 | "2018-01-13 14:30:21.340 3.848944e-01 1.565702e-01 0.519442\n", 420 | "2018-01-22 14:24:05.620 1.924456e-01 4.256673e-02 0.373419\n", 421 | "2018-01-29 14:29:47.980 9.462637e-02 1.200343e-02 0.259726\n", 422 | "2018-01-29 14:30:11.940 9.462335e-02 1.200277e-02 0.259721\n", 423 | "2018-02-07 14:23:57.490 2.516278e-02 1.506232e-03 0.113698\n", 424 | "2018-02-14 14:29:42.110 6.663433e-07 2.371716e-08 0.000005\n", 425 | "2018-02-14 14:30:06.070 0.000000e+00 0.000000e+00 0.000000" 426 | ] 427 | }, 428 | "execution_count": 24, 429 | "metadata": {}, 430 | "output_type": "execute_result" 431 | } 432 | ], 433 | "source": [ 434 | "both.dataframe" 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": null, 440 | "metadata": {}, 441 | "outputs": [], 442 | "source": [] 443 | } 444 | ], 445 | "metadata": { 446 | "kernelspec": { 447 | "display_name": "Python 3", 448 | "language": "python", 449 | "name": "python3" 450 | }, 451 | "language_info": { 452 | "codemirror_mode": { 453 | "name": "ipython", 454 | "version": 3 455 | }, 456 | "file_extension": ".py", 457 | "mimetype": "text/x-python", 458 | "name": "python", 459 | "nbconvert_exporter": "python", 460 | "pygments_lexer": "ipython3", 461 | "version": "3.7.3" 462 | } 463 | }, 464 | "nbformat": 4, 465 | "nbformat_minor": 2 466 | } 467 | -------------------------------------------------------------------------------- /geebap/expressions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import ee 3 | 4 | from . import expgen 5 | from .functions import drange 6 | from geetools import tools 7 | import math 8 | import simpleeval as sval 9 | import numpy as np 10 | import copy 11 | 12 | # FUNCIONES PARA simpleeval 13 | CUSTOM_FUNCTIONS = {"sqrt": math.sqrt, 14 | "exp": math.exp, 15 | "max": max, 16 | "min": min} 17 | 18 | CUSTOM_NAMES = {"pi": math.pi} 19 | 20 | sval.DEFAULT_FUNCTIONS.update(CUSTOM_FUNCTIONS) 21 | sval.DEFAULT_NAMES.update(CUSTOM_NAMES) 22 | 23 | 24 | class Expression(object): 25 | # TODO: Limitante: si hay mas de una variable 26 | """ El método principal de Expresiones() es map(**kwargs), el cual define 27 | la funcion que se usará en ImageCollection.map() 28 | 29 | :param expresion: Expression en formato texto. Se pueden usar algunas 30 | variables calculadas de la siguiente forma: 31 | 32 | - {max}: max_result valor del range 33 | - {min}: minimo valor del range 34 | - {mean}: mean del range 35 | - {std}: desvío estandar del range 36 | - {max_result}: max_result valor de los resultados 37 | 38 | La variable puede ser solo una, que al momento de aplicar el método 39 | map() se definirá de donde proviene, y debe incluirse en la expresión 40 | de la siguiente forma: {var}. Ejemplo: 41 | 42 | | 43 | `expr = "{var}*{mean}/{std}+40"` 44 | 45 | `e = Expression(expression=expr, range=(0, 100))` 46 | 47 | `print e.format_local()` 48 | 49 | >> {var}*50.0/28.8963665536+40 50 | 51 | :type expresion: str 52 | :param rango: Rango de valores entre los que varía la variable 53 | :type rango: tuple 54 | :param normalizar: Hacer que los valores (resultado) oscilen entre 55 | 0 y 1 dividiendo los valores por el max_result. Si se quiere 56 | normalize se debe proveer un range. 57 | :type normalizar: bool 58 | :param kargs: argumentos 'keywords', ej: a=1, b=2 59 | 60 | :Propiedades fijas: 61 | :param media: Si se provee un range, es la mean aritmetica, sino None 62 | :type media: float 63 | :param std: Si se provee un range es el desvío estandar, sino None 64 | :type std: float 65 | :param maximo: Determinar el max_result resultado posible. Aplicando la 66 | expression localmente con la funcion eval() 67 | :type maximo: float 68 | 69 | :Uso: 70 | 71 | .. code:: python 72 | 73 | # Defino las expresiones con sus rangos 74 | exp = Expression.Exponential(range=(0, 100)) 75 | exp2 = Expression.Exponential(range=(0, 5000), normalize=True) 76 | 77 | # Defino las funciones, indicando el name de la band resultante 78 | # y la variable que se usará para el cálculo 79 | fmap = exp.map("otronombre", prop="CLOUD_COVER") 80 | fmap2 = exp2.map("name", band="NIR") 81 | 82 | # Mapeo los resultados 83 | newcol = imgcol.map(fmap) 84 | newcol2 = imgcol.map(fmap2) 85 | 86 | :Operadores: 87 | :\+ \- * / % **: Add, Subtract, Multiply, Divide, Modulus, Exponent 88 | :== != < > <= >=: Equal, Not Equal, Less Than, Greater than, etc. 89 | 90 | :Constantes: 91 | :pi: pi (3.1415) 92 | :e: euler (2.71) 93 | 94 | :Funciones admitidas: 95 | :max: max_result valor entre dos argumentos 96 | :min: minimo valor entre dos argumentos 97 | :exp: exponencial de e: e^argumento 98 | :sqrt: raiz cuadrada del argumento 99 | """ 100 | def __init__(self, expression="{var}", normalize=False, range=None, 101 | name=None, **kwargs): 102 | self.expression = expression 103 | self.range = range 104 | self._normalize = normalize 105 | self.params = kwargs 106 | self._max = kwargs.get("max") 107 | self._min = kwargs.get("min") 108 | self._std = kwargs.get("std") 109 | self._mean = kwargs.get("mean") 110 | self.name = name 111 | 112 | def format_local(self): 113 | """ Reemplaza las variables de la expression por los valores asignados 114 | al objeto, excepto la variable 'var' que la deja como está porque es 115 | la que variará en la expression de EE""" 116 | # print self.expression 117 | # reemplaza las variables estadisticas 118 | params = copy.deepcopy(self.params) 119 | params["max"] = self.max 120 | params["min"] = self.min 121 | params["mean"] = self.mean 122 | params["std"] = self.std 123 | 124 | return self.expression.format(var="{var}", **params) 125 | 126 | def format_ee(self): 127 | """ Reemplaza las variables de la expression por los valores asignados 128 | al objeto y genera la expression lista para usar en Earth Engine """ 129 | # reemplaza las variables estadisticas 130 | params = copy.deepcopy(self.params) 131 | params["max"] = self.max 132 | params["min"] = self.min 133 | params["mean"] = self.mean 134 | params["std"] = self.std 135 | 136 | expr = self.expression.format(var="'var'", **params) 137 | 138 | return expgen.ExpGen.parse(expr) 139 | 140 | 141 | @staticmethod 142 | def adjust(name, valor): 143 | """ Ajusta el valor de la band resultante multipliandolo por 'valor' 144 | 145 | :param valor: Valor de adjust 146 | :type valor: float 147 | :param name: name de la band que contiene el valor a ajustar 148 | :type name: str 149 | :return: La funcion para map() 150 | :rtype: function 151 | """ 152 | def wrap(img): 153 | band = img.select(name).multiply(valor) 154 | return tools.image.replace(img, name, band) 155 | 156 | return wrap 157 | 158 | @property 159 | def normalize(self): 160 | """ 161 | :rtype: bool 162 | """ 163 | return self._normalize 164 | 165 | @normalize.setter 166 | def normalize(self, value): 167 | """ Metodo para setear el valor de la propiedad 'normalize' """ 168 | if type(value) is bool and type(self.range) is tuple: 169 | self._normalize = value 170 | else: 171 | self._normalize = False 172 | print("If you want to normalize the function, the range must be " 173 | " a tuple") 174 | 175 | # ESTADISTICAS DEL RANGO 176 | @property 177 | def mean(self): 178 | if type(self.range) is tuple: 179 | r = drange(self.range[0], self.range[1] + 1, places=1) 180 | return np.mean(r) 181 | elif self._mean: 182 | return self._mean 183 | else: 184 | raise ValueError("To determine the mean the 'range' param must be " 185 | "a tuple") 186 | 187 | @property 188 | def std(self): 189 | if type(self.range) is tuple: 190 | r = drange(self.range[0], self.range[1] + 1, places=1) 191 | return np.std(r) 192 | elif self._std: 193 | return self._std 194 | else: 195 | raise ValueError("To determine the std the 'range' param must be " 196 | "a tuple") 197 | 198 | @property 199 | def max_result(self): 200 | """ Determinar el max_result resultado posible. Aplicando la expression 201 | localmente con la funcion eval() 202 | 203 | :return: 204 | """ 205 | if type(self.range) is tuple: 206 | rango = self.range 207 | elif self._max and self._min: 208 | rango = (self._min, self._max) 209 | else: 210 | raise ValueError("To determine the max result the 'range' param " 211 | "must be a tuple") 212 | 213 | r = drange(rango[0], rango[1]+1, places=1) 214 | lista_result = [self.eval(var) for var in r] 215 | maximo = max(lista_result) 216 | return maximo 217 | 218 | @property 219 | def max(self): 220 | """ Maximo valor del range """ 221 | val = self.range[1] if self.range else self.params.get("max", None) 222 | return val 223 | 224 | @property 225 | def min(self): 226 | """ Minimo valor del range """ 227 | val = self.range[0] if self.range else self.params.get("min", None) 228 | return val 229 | 230 | def eval(self, var): 231 | """ Metodo para aplicar la funcion localmente con un valor dado 232 | 233 | :param var: Valor que se usara como variable 234 | :return: el resultado de evaluar la expression con un valor dado 235 | :rtype: float 236 | """ 237 | expr = self.format_local() 238 | expr = expr.format(var=var) 239 | result = sval.simple_eval(expr) 240 | return result 241 | 242 | def eval_normalized(self, var): 243 | """ Metodo para aplicar la funcion normalizada (resultado entre 0 y 1) 244 | localmente con un valor dado. No influye el parametro 'normalize' 245 | 246 | :param var: Valor que se usara como variable 247 | :return: el resultado de evaluar la expression con un valor dado 248 | :rtype: float 249 | """ 250 | e = self.format_local() 251 | expr = "({e})/{maximo}".format(e=e, maximo=self.max_result) 252 | expr = expr.format(var=var) 253 | result = sval.simple_eval(expr) 254 | return result 255 | 256 | def map(self, name="expression", band=None, prop=None, eval=None, 257 | map=None, **kwargs): 258 | """ Funcion para mapear el resultado de la expression 259 | 260 | :param name: name que se le dara a la band de la imagen una vez 261 | calculada la expression 262 | :type name: str 263 | :param band: name de la band que se usara como valor variable 264 | :type band: str 265 | :param prop: name de la propiedad que se usara como valor variable 266 | :type prop: str 267 | :param eval: funcion para aplicar a la variable. Si la variable es el 268 | valor de una band, entonces el argumento de la funcion será 269 | esa band, y si es una propiedad, el argumento será la propiedad. 270 | :type eval: function 271 | :param map: funcion para aplicarle al valor final. Puede usarse para 272 | hacer un adjust o ponderacion. El argumento de la funcion debe ser 273 | la imagen con la band agregada 274 | :type map: function 275 | :return: la funcion para map() 276 | :rtype: function 277 | """ 278 | # Define la funcion para aplicarle a la variable 279 | if eval is None: 280 | func = lambda x: x 281 | elif callable(eval): 282 | func = eval 283 | else: 284 | raise ValueError("el parametro 'eval' debe ser una funcion") 285 | 286 | # Define la funcion para aplicar 287 | # print "map:", map 288 | 289 | if map is None: 290 | finalf = lambda x: x 291 | elif callable(map): 292 | finalf = map 293 | else: 294 | raise ValueError("el parametro 'map' debe ser una funcion") 295 | 296 | # reemplazo las variables de la expression 297 | expr = self.format_ee() 298 | 299 | # Normalizar 300 | if self.normalize: 301 | expr = "({e})/{maximo}".format(e=expr, maximo=self.max_result) 302 | else: 303 | expr = expr 304 | 305 | # print "name", name 306 | # print "propiedad", prop 307 | # print "expression", expr 308 | # Define la funcion de retorno según si se eligio una propiedad o una band 309 | if prop is None and band is not None: # BANDA 310 | def wrap(img): 311 | # Selecciono los pixeles con valor distinto de cero 312 | ceros = img.select([0]).eq(0).Not() 313 | 314 | # aplico la funcion 'eval' a la band 315 | variable = func(img.select(band)) 316 | # aplico la expression 317 | ''' 318 | calculo = img.expression(expr, 319 | dict(var=variable, **self.params)) 320 | ''' 321 | 322 | calculo = img.expression(expr, dict(var=variable)) 323 | 324 | # renombro 325 | calculo = calculo.select([0], [name]) 326 | # aplico la funcion final sobre la imagen completa 327 | imgfinal = finalf(img.addBands(calculo)) 328 | # retorno la imagen con la band agregada 329 | return imgfinal.updateMask(ceros) 330 | elif band is None and prop is not None: # PROPIEDAD 331 | def wrap(img): 332 | # Selecciono los pixeles con valor distinto de cero 333 | ceros = img.select([0]).eq(0).Not() 334 | # aplico la funcion 'eval' a la propiedad 335 | propval = func(ee.Number(img.get(prop))) 336 | # aplico la expression 337 | ''' 338 | calculo = img.expression(expr, 339 | dict(var=propval, **self.params)) 340 | ''' 341 | calculo = img.expression(expr, dict(var=propval)) 342 | 343 | # renombro 344 | calculo = calculo.select([0], [name]) 345 | # aplico la funcion final sobre la imagen completa 346 | imgfinal = finalf(img.addBands(calculo)) 347 | # imgfinal = img.addBands(calculo) 348 | # retorno la imagen con la band agregada 349 | return imgfinal.updateMask(ceros) 350 | else: 351 | raise ValueError("la funcion map debe ser llamada con \ 352 | 'band' o 'prop'") 353 | 354 | return wrap 355 | 356 | @classmethod 357 | def Exponential(cls, a=-10, range=(0, 100), **kwargs): 358 | """ Funcion Exponential 359 | 360 | :USO: 361 | 362 | :param var: valor variable 363 | :param media: valor de la mean aritmetica de la variable 364 | :param a: constante a. El signo determina si el max_result está al final 365 | de la serie (positivo) o al principio (negativo) 366 | :param b: constante b. Determina el punto de quiebre de de la curva. 367 | Cuando es cero, el punto esta en la mean de la serie. Cuando es 368 | positivo se acerca al principio de la serie, y cuando es negativo 369 | al final de la serie. 370 | """ 371 | # DETERMINO LOS PARAMETROS SEGUN EL RANGO DADO SI EXISTIERA 372 | exp = "1.0-(1.0/(exp(((min({var}, {max})-{mean})*(1/{max}*{a})))+1.0))" 373 | return cls(expression=exp, a=a, range=range, name="Exponential", 374 | **kwargs) 375 | 376 | @classmethod 377 | def Normal(cls, range=(0, 100), ratio=-0.5, **kwargs): 378 | """ Campana de Normal 379 | 380 | :param rango: Rango entre los que oscilan los valores de entrada 381 | :type rango: tuple 382 | :param ratio: factor de 'agusamiento' de la curva. Debe ser menor a 383 | cero. Cuanto menor sea, mas 'fina' será la curva 384 | :type ratio: float 385 | :param kwargs: 386 | :return: 387 | """ 388 | if ratio > 0: 389 | print("el ratio de la curva gaussiana debe ser menor a cero, " 390 | "convirtiendo..") 391 | ratio *= -1 392 | if not isinstance(range, tuple): 393 | raise ValueError("el range debe ser una tupla") 394 | 395 | exp = "exp(((({var}-{mean})/{std})**2)*{ratio})/(sqrt(2*pi)*{std})" 396 | return cls(expression=exp, range=range, ratio=ratio, 397 | name="Normal", **kwargs) -------------------------------------------------------------------------------- /notebooks/bap/Best_Available_Pixel_Composite.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# BEST AVAILABLE PIXEL COMPOSITE (BAP) *in Google Earth Engine Python API*\n", 8 | "based on *Pixel-Based Image Compositing for Large-Area Dense\n", 9 | "Time Series Applications and Science (White, 2014)*\n", 10 | "https://goo.gl/Fi8fCY" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "metadata": {}, 16 | "source": [ 17 | "## The process consist in 2 steps:\n", 18 | "1. Compute all scores\n", 19 | "2. Make composite" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## 1. Compute all scores\n", 27 | "The first step is to build a `Bap` object using this package:" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "### Make imports" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 5, 40 | "metadata": { 41 | "codeCollapsed": false, 42 | "hiddenCell": false 43 | }, 44 | "outputs": [ 45 | { 46 | "name": "stdout", 47 | "output_type": "stream", 48 | "text": [ 49 | "geebap version 0.2.3\n", 50 | "geetools version 0.4.0\n" 51 | ] 52 | } 53 | ], 54 | "source": [ 55 | "# Import Earth Engine and initialize\n", 56 | "import ee\n", 57 | "ee.Initialize()\n", 58 | "\n", 59 | "# Import packages\n", 60 | "import geebap\n", 61 | "from geetools import tools,collection, __version__\n", 62 | "import ipygee as ui\n", 63 | "import pprint\n", 64 | "\n", 65 | "print('geebap version', geebap.__version__)\n", 66 | "print('geetools version', __version__)" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "### The `Bap` object will be created using some parameters:\n", 74 | "\n", 75 | "``` python\n", 76 | "bap = geebap.Bap(season, range, colgroup, scores, masks, filters, target_collection, brdf, harmonize)\n", 77 | "```\n", 78 | "As follows:" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "## Season\n", 86 | "This object holds information of the growing season (start and end). This object does not hold any year, just day and month. For example, start on November 15 (11-15) and ends on March 15 (03-15)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 6, 92 | "metadata": {}, 93 | "outputs": [], 94 | "source": [ 95 | "a_season = geebap.Season('11-15', '03-15')" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "You can add the year to it" 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": 7, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "daterange = a_season.add_year(2019)" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": 8, 117 | "metadata": {}, 118 | "outputs": [ 119 | { 120 | "data": { 121 | "application/vnd.jupyter.widget-view+json": { 122 | "model_id": "c66b2f9ca3bf41cf87d98ba5a0fb5697", 123 | "version_major": 2, 124 | "version_minor": 0 125 | }, 126 | "text/plain": [ 127 | "VBox(children=(Accordion(children=(Button(description='Cancel', style=ButtonStyle()),), _titles={'0': 'Loading…" 128 | ] 129 | }, 130 | "metadata": {}, 131 | "output_type": "display_data" 132 | } 133 | ], 134 | "source": [ 135 | "ui.eprint(daterange)" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "Note that when the season covers two years, start date is in the previous year." 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "## Range\n", 150 | "\n", 151 | "This is a 2 items `tuple` indicating the amount of years that will be used. The first item indicates how many years to go \"backwards\" and the second \"forward\"" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 32, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "a_range = (0, 0) # One year backwards" 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "## Colgroup\n", 168 | "This comes from `geetools.collection.CollectioGroup` and basically aggregates the collections available in that module." 169 | ] 170 | }, 171 | { 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "You could also make your own `colgroup`, but you have to keep in mind that it is composed by `geetools.Collection` objects." 176 | ] 177 | }, 178 | { 179 | "cell_type": "markdown", 180 | "metadata": {}, 181 | "source": [ 182 | "If this parameter is `None` the process will use the `priority` module, that holds information in which Landsat satellite was available according to the year parsed." 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 10, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "# My own colgroup\n", 192 | "l8toa = collection.Landsat8TOA()\n", 193 | "l7toa = collection.Landsat7TOA()\n", 194 | "mycolgroup = collection.CollectionGroup(l8toa, l7toa)" 195 | ] 196 | }, 197 | { 198 | "cell_type": "markdown", 199 | "metadata": {}, 200 | "source": [ 201 | "## Masks\n", 202 | "The mask that will be used to get rid of `clouds`, `shadow`, etc" 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 11, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "cld_mask = geebap.masks.Mask()" 212 | ] 213 | }, 214 | { 215 | "cell_type": "markdown", 216 | "metadata": {}, 217 | "source": [ 218 | "## Filters\n", 219 | "There are two filters you can use in the process:\n", 220 | "\n", 221 | "**cloud percentage**: `filters.CloudsCover`\n", 222 | "\n", 223 | "**masked pixel percentage**: `filters.MaskCover`. This filter can be used **only** if maskpercent score is included in the process." 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 12, 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "filt_cld = geebap.filters.CloudCover() # defaults on 70 %\n", 233 | "filt_mask = geebap.filters.MaskCover() # defaults on 70 %" 234 | ] 235 | }, 236 | { 237 | "cell_type": "markdown", 238 | "metadata": {}, 239 | "source": [ 240 | "## Scores\n", 241 | "This is what makes the difference. Every score has its own parameters, and share the following:\n", 242 | "\n", 243 | "- **range_out**: the range of values the score will be, by default it is (0, 1)\n", 244 | "- **name**: the name of the resulting band\n", 245 | "\n", 246 | "Also, all scores have a static method that can be used without the need of a `Bap` object. I'd be the core method of every score. In the scores that need a whole `ee.ImageCollection` to be computed, this method is called `apply` and the ones that can be computed using a single `ee.Image` the method is called `compute`" 247 | ] 248 | }, 249 | { 250 | "cell_type": "markdown", 251 | "metadata": {}, 252 | "source": [ 253 | "## White's scores" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": {}, 259 | "source": [ 260 | "### DOY (best day of the year)\n", 261 | "Basically, pixels from images closer to that date will have higher score\n", 262 | "It takes 2 params:\n", 263 | "\n", 264 | "- **best day of the year**: You have to specify which day you are goint to prioritize\n", 265 | "- **season**: You can use the same as the one for the process, or not\n", 266 | "\n", 267 | "Optional:\n", 268 | "\n", 269 | "- **function**: There are two options: `linear` or `gauss`. Defaults to `linear`" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 13, 275 | "metadata": {}, 276 | "outputs": [], 277 | "source": [ 278 | "doy = geebap.scores.Doy('01-15', a_season)" 279 | ] 280 | }, 281 | { 282 | "cell_type": "markdown", 283 | "metadata": {}, 284 | "source": [ 285 | "### Satellite\n", 286 | "It uses a list of available satellite for each year that you can check:" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": 14, 292 | "metadata": {}, 293 | "outputs": [ 294 | { 295 | "data": { 296 | "text/plain": [ 297 | "['LANDSAT/LE07/C01/T1_SR',\n", 298 | " 'LANDSAT/LE07/C01/T1_TOA',\n", 299 | " 'LANDSAT/LT05/C01/T1_SR',\n", 300 | " 'LANDSAT/LT05/C01/T1_TOA']" 301 | ] 302 | }, 303 | "execution_count": 14, 304 | "metadata": {}, 305 | "output_type": "execute_result" 306 | } 307 | ], 308 | "source": [ 309 | "# List of satellites for 2000\n", 310 | "geebap.priority.SeasonPriority.relation[2000]" 311 | ] 312 | }, 313 | { 314 | "cell_type": "markdown", 315 | "metadata": {}, 316 | "source": [ 317 | "the score has one main param:\n", 318 | "\n", 319 | "**ratio**: how much score will decrease for each satellite. For example, for 2000, if rate is 0.05 (default value):\n", 320 | "\n", 321 | "* 'LANDSAT/LE07/C01/T1_SR' --> 1\n", 322 | "* 'LANDSAT/LE07/C01/T1_TOA' --> 0.95\n", 323 | "* 'LANDSAT/LT05/C01/T1_SR' --> 0.90\n", 324 | "* 'LANDSAT/LT05/C01/T1_TOA' --> 0.85" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 15, 330 | "metadata": {}, 331 | "outputs": [], 332 | "source": [ 333 | "sat = geebap.scores.Satellite()" 334 | ] 335 | }, 336 | { 337 | "cell_type": "markdown", 338 | "metadata": {}, 339 | "source": [ 340 | "### Atmospheric Opacity\n", 341 | "It uses the atmospheric opacity band computed by Surface Reflectance, so only SR collections will have this score. If the process uses a non-SR collection, like TOA, the this score will be zero." 342 | ] 343 | }, 344 | { 345 | "cell_type": "code", 346 | "execution_count": 16, 347 | "metadata": {}, 348 | "outputs": [], 349 | "source": [ 350 | "atm_op = geebap.scores.AtmosOpacity()" 351 | ] 352 | }, 353 | { 354 | "cell_type": "markdown", 355 | "metadata": {}, 356 | "source": [ 357 | "### Distance to mask\n", 358 | "This assigns a score regarding the distance of the pixel to the closest masked pixel. As the only mask is for clouds, it could be considered 'distance to cloud'. It has 3 main params:\n", 359 | "\n", 360 | "**unit**: unit to measure distance. Defaults to 'meters'\n", 361 | "\n", 362 | "**dmax**: max distance. Pixel further than this distance will have score 1. Defaults to 600\n", 363 | "\n", 364 | "**dmin**: min distance. Defaults to 0 (next pixel from the maks will have score 0)." 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": 17, 370 | "metadata": {}, 371 | "outputs": [], 372 | "source": [ 373 | "dist = geebap.scores.CloudDist()" 374 | ] 375 | }, 376 | { 377 | "cell_type": "markdown", 378 | "metadata": {}, 379 | "source": [ 380 | "## Custom Scores *(not White's)*" 381 | ] 382 | }, 383 | { 384 | "cell_type": "markdown", 385 | "metadata": {}, 386 | "source": [ 387 | "### Outliers\n", 388 | "It computes an statistics over the whole collection (in the season) and assigns score regarding the *distance* of each pixel value to that statistic. It has 3 main parameters:\n", 389 | "\n", 390 | "**bands**: a list of bands to include in the process. The process splits the score in the number of given bands. For example, if 4 bands are given, the max score for each band will be 0.25\n", 391 | "\n", 392 | "**process**: one of 'mean' or 'median'. Defaults to 'median'\n", 393 | "\n", 394 | "**dist**: distance from 'mean' or 'median'. Defaults to 0.7\n", 395 | "\n", 396 | "*NOTE: bands must be in the image, so if you use a vegetation index, be sure to include it in the process*" 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": 18, 402 | "metadata": {}, 403 | "outputs": [], 404 | "source": [ 405 | "out = geebap.scores.Outliers((\"ndvi\",))" 406 | ] 407 | }, 408 | { 409 | "cell_type": "markdown", 410 | "metadata": {}, 411 | "source": [ 412 | "### Mask percentage\n", 413 | "It computes the precentage of masked pixels in the image (not the whole scene). It has one main parameter:\n", 414 | "\n", 415 | "**band**: the band that holds the mask." 416 | ] 417 | }, 418 | { 419 | "cell_type": "code", 420 | "execution_count": 19, 421 | "metadata": {}, 422 | "outputs": [], 423 | "source": [ 424 | "maskper = geebap.scores.MaskPercent()" 425 | ] 426 | }, 427 | { 428 | "cell_type": "markdown", 429 | "metadata": {}, 430 | "source": [ 431 | "### Mask percentage (Kernel)\n", 432 | "Similar to `MaskPercent` but uses a kernel for computation. It may help to avoid exceed memory capacity and also may help to build bigger composites" 433 | ] 434 | }, 435 | { 436 | "cell_type": "code", 437 | "execution_count": 20, 438 | "metadata": {}, 439 | "outputs": [], 440 | "source": [ 441 | "maskper_kernel = geebap.scores.MaskPercentKernel()" 442 | ] 443 | }, 444 | { 445 | "cell_type": "markdown", 446 | "metadata": {}, 447 | "source": [ 448 | "### Vegetation Index\n", 449 | "This scores is based on the absolute value of the given index, parametrized to `range_out`. The only parameter is **index**: the name of it (*ndvi*, *evi* or *nbr*). Defaults to *ndvi*." 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 21, 455 | "metadata": {}, 456 | "outputs": [], 457 | "source": [ 458 | "ind = geebap.scores.Index(\"ndvi\")" 459 | ] 460 | }, 461 | { 462 | "cell_type": "markdown", 463 | "metadata": {}, 464 | "source": [ 465 | "### Multiple years (seasons)\n", 466 | "If you want to use images from a range of seasons, and not just only one, this scores prioritizes the main season. Take in count that a season may hold 2 years, but the main is the least (see `Season`). It has 3 main params:\n", 467 | "\n", 468 | "**main_year**: this is the central year. Images from this year (season) will have score 1\n", 469 | "\n", 470 | "**season**: a `Season` object.\n", 471 | "\n", 472 | "**ratio**: amount of score that will decrease as it goes further to the main year. Defaults to 0.05. It is similar to *rate* parameter in `Satellite`.\n", 473 | "\n" 474 | ] 475 | }, 476 | { 477 | "cell_type": "code", 478 | "execution_count": 22, 479 | "metadata": {}, 480 | "outputs": [], 481 | "source": [ 482 | "# Will not use it in the test\n", 483 | "multi = geebap.scores.MultiYear(2019, a_season, range_out=(0.8, 1))" 484 | ] 485 | }, 486 | { 487 | "cell_type": "markdown", 488 | "metadata": {}, 489 | "source": [ 490 | "## Target collection\n", 491 | "The `target_collection` parameter of the `Bap` object defines the output's band type. It must be an instance of `geetools.Collection` and defaults to `collection.Landsat8SR()`. So, by default, the values for the bands of the output will be parametrized between 0 and 10000, except for the new bands" 492 | ] 493 | }, 494 | { 495 | "cell_type": "markdown", 496 | "metadata": {}, 497 | "source": [ 498 | "## Making the composite (BAP)\n", 499 | "Next step is to create a `Bap` object. It has the following parameters:" 500 | ] 501 | }, 502 | { 503 | "cell_type": "code", 504 | "execution_count": 33, 505 | "metadata": {}, 506 | "outputs": [], 507 | "source": [ 508 | "bap_obj = geebap.Bap(\n", 509 | " range=a_range,\n", 510 | " season=a_season,\n", 511 | " masks=(cld_mask,),\n", 512 | " scores=(\n", 513 | " doy, \n", 514 | " sat, \n", 515 | " # atm_op, \n", 516 | " dist,\n", 517 | " out, \n", 518 | " # maskper,\n", 519 | " maskper_kernel, # use one at a time\n", 520 | " ind,\n", 521 | " multi,\n", 522 | " ),\n", 523 | " filters=(\n", 524 | " filt_cld, \n", 525 | " # filt_mask\n", 526 | " ),\n", 527 | " brdf=True,\n", 528 | " harmonize=True\n", 529 | ")" 530 | ] 531 | }, 532 | { 533 | "cell_type": "markdown", 534 | "metadata": {}, 535 | "source": [ 536 | "## Define a site" 537 | ] 538 | }, 539 | { 540 | "cell_type": "code", 541 | "execution_count": 34, 542 | "metadata": {}, 543 | "outputs": [], 544 | "source": [ 545 | "site = ee.Geometry.Polygon([[-71.5,-42.5],\n", 546 | " [-71.5,-43],\n", 547 | " [-72,-43],\n", 548 | " [-72,-42.5]])" 549 | ] 550 | }, 551 | { 552 | "cell_type": "markdown", 553 | "metadata": {}, 554 | "source": [ 555 | "## Finally, compute the composite\n", 556 | "`Bap` object has a method named `build_composite_best` that creates one image out of the pixels with higher score in the collection (which includes all collections given). It has the following parameters:\n", 557 | "\n", 558 | "- **year**: The year to process\n", 559 | "- **site**: The site where the composite will be computed\n", 560 | "- **indices**: a list of vegetation indices. Defaults to `None`\n", 561 | "- **name**: name for the band that holds the final score. Defaults to *score*\n", 562 | "- **buffer**: distance of the buffer around the site. Defaults to 0.\n", 563 | "- **add_individual_scores**: if this param is `True` the resulting composite will have all individual scores" 564 | ] 565 | }, 566 | { 567 | "cell_type": "code", 568 | "execution_count": 35, 569 | "metadata": {}, 570 | "outputs": [], 571 | "source": [ 572 | "composite = bap_obj.build_composite_best(2019, site, indices=['ndvi'], add_individual_scores=True)" 573 | ] 574 | }, 575 | { 576 | "cell_type": "markdown", 577 | "metadata": {}, 578 | "source": [ 579 | "## You can also get the complete collection in which all images will have the computed scores" 580 | ] 581 | }, 582 | { 583 | "cell_type": "code", 584 | "execution_count": 26, 585 | "metadata": {}, 586 | "outputs": [], 587 | "source": [ 588 | "bapcol = bap_obj.compute_scores(2019, site, indices=['ndvi'], add_individual_scores=True)" 589 | ] 590 | }, 591 | { 592 | "cell_type": "markdown", 593 | "metadata": {}, 594 | "source": [ 595 | "## Inspect the results in a Map" 596 | ] 597 | }, 598 | { 599 | "cell_type": "code", 600 | "execution_count": 27, 601 | "metadata": {}, 602 | "outputs": [], 603 | "source": [ 604 | "Map = ui.Map()" 605 | ] 606 | }, 607 | { 608 | "cell_type": "code", 609 | "execution_count": 28, 610 | "metadata": {}, 611 | "outputs": [ 612 | { 613 | "data": { 614 | "application/vnd.jupyter.widget-view+json": { 615 | "model_id": "4a6076d12b6245338d8a7361ceff1874", 616 | "version_major": 2, 617 | "version_minor": 0 618 | }, 619 | "text/plain": [ 620 | "Map(basemap={'url': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'max_zoom': 19, 'attribution': 'Map …" 621 | ] 622 | }, 623 | "metadata": {}, 624 | "output_type": "display_data" 625 | }, 626 | { 627 | "data": { 628 | "application/vnd.jupyter.widget-view+json": { 629 | "model_id": "1e952719a78e43c391efbefefb305246", 630 | "version_major": 2, 631 | "version_minor": 0 632 | }, 633 | "text/plain": [ 634 | "Tab(children=(CustomInspector(children=(SelectMultiple(options=OrderedDict(), value=()), Accordion(selected_in…" 635 | ] 636 | }, 637 | "metadata": {}, 638 | "output_type": "display_data" 639 | } 640 | ], 641 | "source": [ 642 | "Map.show()" 643 | ] 644 | }, 645 | { 646 | "cell_type": "code", 647 | "execution_count": 29, 648 | "metadata": {}, 649 | "outputs": [], 650 | "source": [ 651 | "vis = bap_obj.target_collection.visualization('NSR', True)" 652 | ] 653 | }, 654 | { 655 | "cell_type": "code", 656 | "execution_count": 31, 657 | "metadata": {}, 658 | "outputs": [], 659 | "source": [ 660 | "Map.addLayer(site)" 661 | ] 662 | }, 663 | { 664 | "cell_type": "code", 665 | "execution_count": 36, 666 | "metadata": {}, 667 | "outputs": [], 668 | "source": [ 669 | "Map.addLayer(composite, vis, 'BAP composite (0,0)')" 670 | ] 671 | }, 672 | { 673 | "cell_type": "code", 674 | "execution_count": 23, 675 | "metadata": {}, 676 | "outputs": [], 677 | "source": [ 678 | "Map.addLayer(bapcol.select(['nir', 'swir', 'red', '^score.+', 'date', 'col_id']), vis, 'BAP collection')" 679 | ] 680 | }, 681 | { 682 | "cell_type": "code", 683 | "execution_count": 24, 684 | "metadata": {}, 685 | "outputs": [], 686 | "source": [ 687 | "Map.centerObject(composite)" 688 | ] 689 | }, 690 | { 691 | "cell_type": "code", 692 | "execution_count": 25, 693 | "metadata": {}, 694 | "outputs": [], 695 | "source": [ 696 | "Map.addLayer(composite.select('date').randomVisualizer(), dict(min=0, max=255), 'Composite dates')" 697 | ] 698 | }, 699 | { 700 | "cell_type": "code", 701 | "execution_count": null, 702 | "metadata": {}, 703 | "outputs": [], 704 | "source": [] 705 | } 706 | ], 707 | "metadata": { 708 | "kernelspec": { 709 | "display_name": "Python 3", 710 | "language": "python", 711 | "name": "python3" 712 | }, 713 | "language_info": { 714 | "codemirror_mode": { 715 | "name": "ipython", 716 | "version": 3 717 | }, 718 | "file_extension": ".py", 719 | "mimetype": "text/x-python", 720 | "name": "python", 721 | "nbconvert_exporter": "python", 722 | "pygments_lexer": "ipython3", 723 | "version": "3.7.3" 724 | } 725 | }, 726 | "nbformat": 4, 727 | "nbformat_minor": 2 728 | } 729 | -------------------------------------------------------------------------------- /geebap/bap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ Main module holding the Bap Class and its methods """ 3 | 4 | from geetools import collection, tools 5 | from . import scores, priority, functions, utils, __version__ 6 | import ee 7 | import json 8 | 9 | 10 | class Bap(object): 11 | def __init__(self, season, range=(0, 0), colgroup=None, scores=None, 12 | masks=None, filters=None, target_collection=None, brdf=False, 13 | harmonize=True, projection=None, **kwargs): 14 | self.range = range 15 | self.scores = scores 16 | self.masks = masks or () 17 | self.filters = filters or () 18 | self.season = season 19 | self.colgroup = colgroup 20 | self.brdf = brdf 21 | self.harmonize = harmonize 22 | self.projection = projection or ee.Projection('EPSG:3857') 23 | 24 | if target_collection is None: 25 | target_collection = collection.Landsat8SR() 26 | self.target_collection = target_collection 27 | 28 | self.score_name = kwargs.get('score_name', 'score') 29 | 30 | # Band names in case user needs different names 31 | self.bandname_col_id = kwargs.get('bandname_col_id', 'col_id') 32 | self.bandname_date = kwargs.get('bandname_date', 'date') 33 | 34 | @property 35 | def score_names(self): 36 | if self.scores: 37 | punt = [p.name for p in self.scores] 38 | return functions.replace_duplicate(punt) 39 | else: 40 | return [] 41 | 42 | @property 43 | def max_score(self): 44 | """ gets the maximum score it can get """ 45 | maxpunt = 0 46 | for score in self.scores: 47 | maxpunt += score.max 48 | return maxpunt 49 | 50 | def year_range(self, year): 51 | try: 52 | i = year - abs(self.range[0]) 53 | f = year + abs(self.range[1]) + 1 54 | 55 | return range(i, f) 56 | except: 57 | return None 58 | 59 | def time_start(self, year): 60 | """ Get time start property """ 61 | return ee.Date('{}-{}-{}'.format(year, 1, 1)) 62 | 63 | def make_proxy(self, image, collection, year): 64 | """ Make a proxy collection """ 65 | 66 | size = collection.size() 67 | 68 | # unmask all bands 69 | unmasked = image.unmask() 70 | 71 | proxy_date = ee.Date('{}-01-01'.format(year)) 72 | 73 | bands = image.bandNames() 74 | empty = tools.image.empty(0, bands) 75 | proxy = unmasked.where(unmasked, empty) 76 | 77 | proxy = proxy.set('system:time_start', proxy_date.millis()) 78 | 79 | proxy_col = ee.ImageCollection.fromImages([proxy]) 80 | 81 | return ee.ImageCollection(ee.Algorithms.If(size.gt(0), 82 | collection, 83 | proxy_col)) 84 | 85 | 86 | def compute_scores(self, year, site, indices=None, **kwargs): 87 | """ Add scores and merge collections 88 | 89 | :param add_individual_scores: adds the individual scores to the images 90 | :type add_individual_scores: bool 91 | :param buffer: make a buffer before cutting to the given site 92 | :type buffer: float 93 | """ 94 | add_individual_scores = kwargs.get('add_individual_scores', False) 95 | buffer = kwargs.get('buffer', None) 96 | 97 | # list to 'collect' collections 98 | all_collections = ee.List([]) 99 | 100 | # TODO: get common bands for col of all years 101 | if self.colgroup is None: 102 | colgroup = priority.SeasonPriority(year).colgroup 103 | all_col = [] 104 | for year in self.year_range(year): 105 | _colgroup = priority.SeasonPriority(year).colgroup 106 | for col in _colgroup.collections: 107 | all_col.append(col) 108 | else: 109 | all_col = self.colgroup.collections 110 | colgroup = self.colgroup 111 | 112 | common_bands = collection.getCommonBands(*all_col, match='name') 113 | 114 | # add col_id to common bands 115 | common_bands.append(self.bandname_col_id) 116 | 117 | # add date band to common bands 118 | common_bands.append(self.bandname_date) 119 | 120 | # add score names if 'add_individual_scores' 121 | if add_individual_scores: 122 | for score_name in self.score_names: 123 | common_bands.append(score_name) 124 | 125 | # add indices to common bands 126 | if indices: 127 | for i in indices: 128 | common_bands.append(i) 129 | 130 | # add score band to common bands 131 | common_bands.append(self.score_name) 132 | 133 | # create an empty score band in case no score is parsed 134 | empty_score = ee.Image.constant(0).rename(self.score_name).toUint8() 135 | 136 | # List to store all used images 137 | used_images = dict() 138 | 139 | for col in colgroup.collections: 140 | col_ee_bounds = col.collection 141 | 142 | # Filter bounds 143 | if isinstance(site, ee.Feature): site = site.geometry() 144 | col_ee_bounds = col_ee_bounds.filterBounds(site) 145 | 146 | # Collection ID 147 | col_id = functions.get_col_id(col) 148 | col_id_img = functions.get_col_id_image(col) 149 | 150 | for year in self.year_range(year): 151 | daterange = self.season.add_year(year) 152 | 153 | # filter date 154 | col_ee = col_ee_bounds.filterDate(daterange.start(), 155 | daterange.end()) 156 | 157 | # some filters 158 | if self.filters: 159 | for filt in self.filters: 160 | if filt.name in ['CloudCover']: 161 | col_ee = filt.apply(col_ee, col=col) 162 | 163 | # BRDF 164 | if self.brdf: 165 | if 'brdf' in col.algorithms.keys(): 166 | col_ee = col_ee.map(lambda img: col.brdf(img)) 167 | 168 | # Proxy in case size == 0 169 | col_ee = self.make_proxy(col.collection.first(), col_ee, year) 170 | 171 | # clip with site 172 | if buffer is not None: 173 | site = site.buffer(buffer) 174 | col_ee = col_ee.map(lambda img: img.clip(site)) 175 | 176 | # Add year as a property (YEAR_BAP) 177 | col_ee = col_ee.map(lambda img: img.set('YEAR_BAP', year)) 178 | 179 | # Catch SLC off 180 | slcoff = False 181 | if col.spacecraft == 'LANDSAT' and col.number == 7: 182 | if year in priority.SeasonPriority.l7_slc_off: 183 | # Convert masked values to zero 184 | col_ee = col_ee.map(lambda img: functions.unmask_slc_off(img)) 185 | slcoff = True 186 | 187 | # Apply masks 188 | if self.masks: 189 | for mask in self.masks: 190 | col_ee = mask.map(col_ee, col=col) 191 | 192 | # Rename 193 | col_ee = col_ee.map(lambda img: col.rename(img)) 194 | 195 | # Rescale 196 | col_ee = col_ee.map( 197 | lambda img: collection.rescale( 198 | img, col, self.target_collection, renamed=True)) 199 | 200 | # Indices 201 | if indices: 202 | for i in indices: 203 | f = getattr(col, i) 204 | def addindex(img): 205 | ind = f(img, renamed=True) 206 | return img.addBands(ind) 207 | col_ee = col_ee.map(addindex) 208 | 209 | # Apply scores 210 | if self.scores: 211 | for score in self.scores: 212 | zero = False if slcoff and isinstance(score, (scores.MaskPercent, scores.MaskPercentKernel)) else True 213 | col_ee = score._map( 214 | col_ee, 215 | col=col, 216 | year=year, 217 | colEE=col_ee, 218 | geom=site, 219 | include_zero=zero) 220 | 221 | # Mask all bands with mask 222 | col_ee = col_ee.map(lambda img: img.updateMask(img.select([0]).mask())) 223 | 224 | # Get an image before the filter to catch all bands for proxy image 225 | col_ee_image = col_ee.first() 226 | 227 | # Filter Mask Cover 228 | if self.filters: 229 | for filt in self.filters: 230 | if filt.name in ['MaskCover']: 231 | col_ee = filt.apply(col_ee) 232 | 233 | # col_ee = self.make_proxy(col, col_ee, year, True) 234 | col_ee = self.make_proxy(col_ee_image, col_ee, year) 235 | 236 | # Add col_id band 237 | # Add col_id to the image as a property 238 | def addBandID(img): 239 | return img.addBands(col_id_img).set( 240 | self.bandname_col_id.upper(), col_id) 241 | col_ee = col_ee.map(addBandID) 242 | 243 | # Add date band 244 | def addDateBand(img): 245 | date = img.date() 246 | year = date.get('year').format() 247 | 248 | # Month 249 | month = date.get('month') 250 | month_str = month.format() 251 | month = ee.String(ee.Algorithms.If( 252 | month.gte(10), 253 | month_str, 254 | ee.String('0').cat(month_str))) 255 | 256 | # Day 257 | day = date.get('day') 258 | day_str = day.format() 259 | day = ee.String(ee.Algorithms.If( 260 | day.gte(10), 261 | day_str, 262 | ee.String('0').cat(day_str))) 263 | 264 | date_str = year.cat(month).cat(day) 265 | newdate = ee.Number.parse(date_str) 266 | newdate_img = ee.Image.constant(newdate) \ 267 | .rename(self.bandname_date).toUint32() 268 | 269 | return img.addBands(newdate_img) 270 | col_ee = col_ee.map(addDateBand) 271 | 272 | # Harmonize 273 | if self.harmonize: 274 | # get max value for the needed bands 275 | 276 | if 'harmonize' in col.algorithms.keys(): 277 | col_ee = col_ee.map( 278 | lambda img: col.harmonize(img, renamed=True)) 279 | 280 | # store used images 281 | # Property name for storing images as properties 282 | prop_name = 'BAP_IMAGES_COLID_{}_YEAR_{}'.format(col_id, year) 283 | # store used images 284 | imlist = ee.List(col_ee.toList(col_ee.size()).map( 285 | lambda img: 286 | ee.String(col.id).cat('/').cat(ee.Image(img).id()))) 287 | used_images[prop_name] = imlist 288 | 289 | col_ee_list = col_ee.toList(col_ee.size()) 290 | all_collections = all_collections.add(col_ee_list).flatten() 291 | 292 | all_collection = ee.ImageCollection.fromImages(all_collections) 293 | 294 | # Compute final score 295 | if self.scores: 296 | def compute_score(img): 297 | score = img.select(self.score_names).reduce('sum') \ 298 | .rename('score').toFloat() 299 | return img.addBands(score) 300 | else: 301 | def compute_score(img): 302 | return img.addBands(empty_score) 303 | 304 | all_collection = all_collection.map(compute_score) 305 | 306 | # Select common bands 307 | # all_collection = functions.select_match(all_collection) 308 | final_collection = all_collection.map( 309 | lambda img: img.select(common_bands)) 310 | 311 | self._used_images = used_images 312 | 313 | return final_collection 314 | 315 | def build_composite_best(self, year, site, indices=None, **kwargs): 316 | """ Build the a composite with best score 317 | 318 | :param add_individual_scores: adds the individual scores to the images 319 | :type add_individual_scores: bool 320 | :param buffer: make a buffer before cutting to the given site 321 | :type buffer: float 322 | """ 323 | # TODO: pass properties 324 | col = self.compute_scores(year, site, indices, **kwargs) 325 | mosaic = col.qualityMosaic(self.score_name) 326 | 327 | return self._set_properties(mosaic, year, col) 328 | 329 | def build_composite_reduced(self, year, site, indices=None, **kwargs): 330 | """ Build the composite where 331 | 332 | :param add_individual_scores: adds the individual scores to the images 333 | :type add_individual_scores: bool 334 | :param buffer: make a buffer before cutting to the given site 335 | :type buffer: float 336 | """ 337 | # TODO: pass properties 338 | nimages = kwargs.get('set', 5) 339 | reducer = kwargs.get('reducer', 'interval_mean') 340 | col = self.compute_scores(year, site, indices, **kwargs) 341 | mosaic = reduce_collection(col, nimages, reducer, self.score_name) 342 | 343 | return self._set_properties(mosaic, year, col) 344 | 345 | def _set_properties(self, mosaic, year, col): 346 | """ Set some BAP common properties to the given mosaic """ 347 | # # USED IMAGES 348 | # used_images = self._used_images 349 | # for prop, value in used_images.items(): 350 | # mosaic = mosaic.set(prop, str(value)) 351 | 352 | # SCORES 353 | bap_scores = [] 354 | for score in self.scores: 355 | pattern = utils.object_init(score) 356 | bap_scores.append(pattern) 357 | mosaic = mosaic.set('BAP_SCORES', str(bap_scores)) 358 | 359 | # MASKS 360 | bap_masks = [] 361 | for mask in self.masks: 362 | pattern = utils.object_init(mask) 363 | bap_masks.append(pattern) 364 | mosaic = mosaic.set('BAP_MASKS', str(bap_masks)) 365 | 366 | # FILTERS 367 | bap_filters = [] 368 | for filter in self.filters: 369 | pattern = utils.object_init(filter) 370 | bap_filters.append(pattern) 371 | mosaic = mosaic.set('BAP_FILTERS', str(bap_filters)) 372 | 373 | # DATE 374 | date = self.time_start(year).millis() 375 | mosaic = mosaic.set('system:time_start', date) 376 | 377 | # BAP Version 378 | mosaic = mosaic.set('BAP_VERSION', __version__) 379 | 380 | # FOOTPRINT 381 | geom = tools.imagecollection.mergeGeometries(col) 382 | mosaic = mosaic.set('system:footprint', geom) 383 | 384 | # Seasons 385 | for year in self.year_range(year): 386 | # yearstr = ee.Number(year).format() 387 | daterange = self.season.add_year(year) 388 | start = daterange.start().format('yyyy-MM-dd') 389 | end = daterange.end().format('yyyy-MM-dd') 390 | string = start.cat(' to ').cat(end) 391 | # propname = ee.String('BAP_SEASON_').cat(yearstr) 392 | propname = ee.String('BAP_SEASON') 393 | mosaic = mosaic.set(propname, string) 394 | 395 | return mosaic 396 | 397 | def to_file(self, filename, path=None): 398 | """ Make a configuration file (JSON) to be able to reconstruct the 399 | object """ 400 | import os 401 | def make_name(filename): 402 | split = filename.split('.') 403 | if len(split) == 1: 404 | filename = '{}.json'.format(filename) 405 | else: 406 | ext = split[-1] 407 | if ext != 'json': 408 | name = '.'.join(split[:-1]) 409 | filename = '{}.json'.format(name) 410 | return filename 411 | filename = make_name(filename) 412 | path = '' if path == None else path 413 | path = os.path.join(os.getcwd(), path, filename) 414 | serial = utils.serialize(self, 'config')['config (Bap)'] 415 | with open(path, 'w') as thefile: 416 | json.dump(serial, thefile, indent=2) 417 | 418 | 419 | def load(filename, path=None): 420 | """ Create a Bap object using a config file """ 421 | from . import season, masks, filters, scores 422 | from geetools import collection 423 | import os 424 | if path is None: 425 | path = os.getcwd() 426 | split = filename.split('.') 427 | if split[-1] != 'json': 428 | filename = '{}.json'.format(filename) 429 | obj = json.load(open(os.path.join(path, filename))) 430 | 431 | # HELPER 432 | def get_number(param_dict, name): 433 | if name != '': 434 | params = ['{} (int)', '{} (float)'] 435 | else: 436 | params = ['{}(int)', '{}(float)'] 437 | 438 | for param in params: 439 | result = param_dict.get(param.format(name)) 440 | if result is not None: 441 | return result 442 | return None 443 | 444 | # SEASON 445 | seas = obj['season (Season)'] 446 | start = seas['_start (SeasonDate)']['date (str)'] 447 | end = seas['_end (SeasonDate)']['date (str)'] 448 | season_param = season.Season(start, end) 449 | 450 | # RANGE 451 | ran = obj.get('range (tuple)') or obj.get('range (list)') 452 | range_param = (ran[0]['(int)'], ran[1]['(int)']) 453 | 454 | # MASKS 455 | mask_list = [] 456 | for mask in obj.get('masks (tuple)') or obj.get('masks (list)') or []: 457 | mask_class = list(mask.keys())[0] 458 | params = mask[mask_class] 459 | if mask_class == '(Mask)': 460 | options = [opt['(str)'] for opt in params.get('options (list)') or params.get('options (tuple)')] 461 | mask_param = masks.Mask(options) 462 | elif mask_class == '(Hollstein)': 463 | options = [opt['(str)'] for opt in params.get('options (list)') or params.get('options (tuple)')] 464 | mask_param = masks.Hollstein(options) 465 | else: 466 | continue 467 | mask_list.append(mask_param) 468 | 469 | # FILTERS 470 | filter_list = [] 471 | for filter in obj.get('filters (tuple)') or obj.get('filters (list)') or []: 472 | filter_class = list(filter.keys())[0] 473 | params = filter[filter_class] 474 | if filter_class == '(CloudCover)': 475 | percent = get_number(params, 'percent') 476 | filter_param = filters.CloudCover(percent) 477 | elif filter_class == 'MaskCover': 478 | percent = get_number(params, 'percent') 479 | filter_param = filters.MaskCover(percent) 480 | else: 481 | continue 482 | filter_list.append(filter_param) 483 | 484 | # COLGROUP 485 | colgroup = obj.get('colgroup (CollectionGroup') or obj.get('colgroup (NoneType)') 486 | if colgroup: 487 | collections = [] 488 | collections_param = colgroup.get('collections (tuple)') or colgroup.get('collections (list)') 489 | for col in collections_param: 490 | sat = list(col.keys())[0] 491 | params = col[sat] 492 | satid = params['_id (str)'] 493 | instance = collection.fromId(satid) 494 | collections.append(instance) 495 | colgroup_param = collection.CollectionGroup(collections) 496 | else: 497 | colgroup_param = None 498 | 499 | 500 | # TARGET COLLECTION 501 | target_collection = obj.get('target_collection (Landsat)') or \ 502 | obj.get('target_collection (Sentinel)') 503 | 504 | target_id = target_collection.get('id (str)') 505 | target_param = collection.fromId(target_id) 506 | 507 | # SCORES 508 | score_list = [] 509 | for score in obj.get('scores (list)') or obj.get('scores (tuple)') or []: 510 | score_class = list(score.keys())[0] 511 | params = score[score_class] 512 | # RANGE OUT 513 | range_out_param = params.get('range_out (tuple)') or params.get('range_out (list)') 514 | if range_out_param: 515 | range_out_0 = range_out_param[0] 516 | range_out_1 = range_out_param[1] 517 | range_out = (get_number(range_out_0, ''), get_number(range_out_1, '')) 518 | else: 519 | range_out = None 520 | 521 | # RANGE IN 522 | range_in_param = params.get('range_in (tuple)') or params.get('range_in (list)') 523 | if range_in_param: 524 | range_in_0 = range_in_param[0] 525 | range_in_1 = range_in_param[1] 526 | range_in = (get_number(range_in_0, ''), get_number(range_in_1, '')) 527 | else: 528 | range_in = None 529 | 530 | # NAME 531 | name = params.get('name (str)') 532 | sleep = get_number(params, 'sleep') 533 | if score_class == '(CloudScene)': 534 | continue 535 | if score_class == '(CloudDist)': 536 | dmax = get_number(params, 'dmax') 537 | dmin = get_number(params, 'dmin') 538 | kernel = params.get('kernel (str)') 539 | units = params.get('units (str)') 540 | score_param = scores.CloudDist(dmin, dmax, name, kernel=kernel, units=units) 541 | elif score_class == '(Doy)': 542 | best_doy = params.get('best_doy (str)') 543 | doy_season_param = params.get('season (Season)') 544 | start = doy_season_param['_start (SeasonDate)']['date (str)'] 545 | end = doy_season_param['_end (SeasonDate)']['date (str)'] 546 | Season = season.Season(start, end) 547 | function = params.get('function (str)') 548 | stretch = get_number(params, 'stretch') 549 | score_param = scores.Doy(best_doy, Season, name, function, stretch) 550 | elif score_class == '(AtmosOpacity)': 551 | continue 552 | elif score_class == '(MaskPercent)': 553 | band = params.get('band (str)') 554 | maxPixels = params.get('maxPixels (int)') 555 | count_zeros = params.get('count_zeros (bool)') 556 | score_param = scores.MaskPercent(band, name, maxPixels, count_zeros) 557 | elif score_class == '(MaskPercentKernel)': 558 | kernel = params.get('kernel (str)') 559 | distance = get_number(params, 'distance') 560 | units = params.get('units (str)') 561 | score_param = scores.MaskPercentKernel(kernel, distance, units, name) 562 | elif score_class == '(Satellite)': 563 | ratio = get_number(params, 'ratio') 564 | score_param = scores.Satellite(ratio, name) 565 | elif score_class == '(Outliers)': 566 | bands = params.get('bands (tuple)') or params.get('bands (list)') 567 | bandlist = [band['(str)'] for band in bands] 568 | process = params.get('process (str)') 569 | dist = get_number(params, 'dist') 570 | score_param = scores.Outliers(bandlist, process, dist, name) 571 | elif score_class == '(Index)': 572 | index = params.get('index (str)') 573 | target = get_number(params, 'target') 574 | function = params.get('function (str)') 575 | stretch = get_number(params, 'stretch') 576 | score_param = scores.Index(index, target, name, function, stretch) 577 | elif score_class == '(MultiYear)': 578 | main_year = params.get('main_year (int)') 579 | my_season_param = params.get('season (Season)') 580 | start = my_season_param['_start (SeasonDate)']['date (str)'] 581 | end = my_season_param['_end (SeasonDate)']['date (str)'] 582 | Season = season.Season(start, end) 583 | ratio = get_number(params, 'ratio') 584 | function = params.get('function (str)') 585 | stretch = get_number(params, 'stretch') 586 | score_param = scores.MultiYear(main_year, Season, ratio, function, stretch, name) 587 | elif score_class == '(Threshold)': 588 | continue 589 | elif score_class == '(Medoid)': 590 | bands = params.get('bands (list)') or params.get('bands (tuple)') 591 | discard_zeros = params.get('discard_zeros (bool)') 592 | score_param = scores.Medoid(bands, discard_zeros, name) 593 | elif score_class == '(Brightness)': 594 | target = get_number(params, 'target') 595 | bands = params.get('bands (list)') or params.get('bands (tuple)') 596 | function = params.get('function (str)') 597 | score_param = scores.Brightness(target, bands, name, function) 598 | else: 599 | continue 600 | 601 | score_param.sleep = sleep 602 | score_param.range_out = range_out 603 | score_param.range_in = range_in 604 | score_list.append(score_param) 605 | 606 | # MISC 607 | score_name_param = obj.get('score_name (str)') 608 | bandname_date_param = obj.get('bandname_date (str)') 609 | brdf_param = obj.get('brdf (bool)') 610 | harmonize_param = obj.get('harmonize (bool)') 611 | bandname_col_id_param = obj.get('bandname_col_id (str)') 612 | 613 | return Bap(season_param, range_param, colgroup_param, score_list, 614 | mask_list, filter_list, target_param, brdf_param, 615 | harmonize_param, score_name=score_name_param, 616 | bandname_date=bandname_date_param, 617 | bandname_col_id=bandname_col_id_param) 618 | 619 | 620 | def reduce_collection(collection, set=5, reducer='mean', 621 | scoreband='score'): 622 | """ Reduce the collection and get a statistic from a set of pixels 623 | 624 | Transform the collection to a 2D array 625 | 626 | * axis 1 (horiz) -> bands 627 | * axis 0 (vert) -> images 628 | 629 | ====== ====== ===== ===== ======== ========= 630 | \ B2 B3 B4 index score 631 | ------ ------ ----- ----- -------- --------- 632 | img1 633 | img2 634 | img3 635 | ====== ====== ===== ===== ======== ========= 636 | 637 | :param reducer: Reducer to use for the set of images. Options are: 638 | 'mean', 'median', 'mode', 'intervalMean'(default) 639 | :type reducer: str || ee.Reducer 640 | :param collection: collection that holds the score 641 | :type collection: ee.ImageCollection 642 | :return: An image in which every pixel is the reduction of the set of 643 | images with best score 644 | :rtype: ee.Image 645 | """ 646 | reducers = {'mean': ee.Reducer.mean(), 647 | 'median': ee.Reducer.median(), 648 | 'mode': ee.Reducer.mode(), 649 | 'interval_mean': ee.Reducer.intervalMean(50, 90), 650 | 'first': ee.Reducer.first(), 651 | } 652 | 653 | if reducer in reducers.keys(): 654 | selected_reducer = reducers[reducer] 655 | elif isinstance(reducer, ee.Reducer): 656 | selected_reducer = reducer 657 | else: 658 | raise ValueError('Reducer {} not recognized'.format(reducer)) 659 | 660 | # Convert masked pixels to 0 value 661 | # collection = collection.map(tools.mask2zero) 662 | collection = collection.map(lambda img: img.unmask()) 663 | 664 | array = collection.toArray() 665 | 666 | # Axis 667 | bands_axis = 1 668 | images_axis = 0 669 | 670 | # band names 671 | bands = ee.Image(collection.first()).bandNames() 672 | 673 | # index of score 674 | score_index = bands.indexOf(scoreband) 675 | 676 | # get only scores band 677 | score = array.arraySlice(axis= bands_axis, 678 | start= score_index, 679 | end= score_index.add(1)) 680 | 681 | # Sort the array (ascending) by the score band 682 | sorted_array = array.arraySort(score) 683 | 684 | # total longitud of the array image (number of images) 685 | longitud = sorted_array.arrayLength(0) 686 | 687 | # Cut the Array 688 | # lastvalues = arrayOrdenado.arraySlice(self.ejeImg, 689 | # longitud.subtract(self.set), longitud) 690 | lastvalues = sorted_array.arraySlice(axis=images_axis, 691 | start=longitud.subtract(set), 692 | end=longitud) 693 | 694 | # Cut score axis 695 | solopjes = lastvalues.arraySlice(axis=bands_axis, 696 | start=score_index, 697 | end= score_index.add(1)) 698 | 699 | #### Process #### 700 | processed = lastvalues.arrayReduce(selected_reducer, 701 | ee.List([images_axis])) 702 | 703 | # Transform the array to an Image 704 | result_image = processed.arrayProject([bands_axis]) \ 705 | .arrayFlatten([bands]) 706 | 707 | return result_image --------------------------------------------------------------------------------