├── 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='