Skip to content

Commit

Permalink
Merge pull request #50 from pflickin/feature/glc_layers
Browse files Browse the repository at this point in the history
Add method for creating Copernicus Land Cover Layers.
  • Loading branch information
lossyrob authored Mar 25, 2021
2 parents 0005b59 + 375ea0c commit 26e0200
Show file tree
Hide file tree
Showing 19 changed files with 426 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ python:
path: stactools_aster/
- method: pip
path: stactools_corine/
- method: pip
path: stactools_cgls_lc100/
- method: pip
path: stactools_landsat/
- method: pip
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ See the [documentation page](https://stactools.readthedocs.io/en/latest/) for th
| `stactools_cli` | Contains the command line interface (cli) for running the `stactools` command |
| `stactools_aster` | Methods and commands for working with ASTER data |
| `stactools_corine` | Methods and commands for working with CORINE Land Cover data |
| `stactools_cgls_lc100` | Methods and commands for working with Copernicus Global Land Cover Layers |
| `stactools_planet` | Methods and commands for working with planet data |
| `stactools_landsat` | Methods and commands for working with landsat data |
| `stactools_sentinel2` | Methods and commands for working with Sentinel 2 data |
Expand Down
1 change: 1 addition & 0 deletions cgls_lc100
4 changes: 4 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ RUN pip install --no-build-isolation -r /tmp/aster/requirements.txt
COPY stactools_corine/requirements.txt /tmp/corine/requirements.txt
RUN pip install -r /tmp/corine/requirements.txt

# Copernicus Global Land Cover Layers
COPY stactools_cgls_lc100/requirements.txt /tmp/stactools_cgls_lc100/requirements.txt
RUN pip install -r /tmp/stactools_cgls_lc100/requirements.txt

# Landsat
COPY stactools_landsat/requirements.txt /tmp/landsat/requirements.txt
RUN pip install -r /tmp/landsat/requirements.txt
Expand Down
2 changes: 2 additions & 0 deletions scripts/env
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ STACTOOLS_SUBPACKAGE_DIRS=(
"stactools_cli"
"stactools_aster"
"stactools_corine"
"stactools_cgls_lc100"
"stactools_landsat"
"stactools_planet"
"stactools_naip"
Expand All @@ -17,6 +18,7 @@ STACTOOLS_COVERAGE_DIRS=(
"stactools_cli/stactools/cli"
"stactools_aster/stactools/aster"
"stactools_corine/stactools/corine"
"stactools_cgls_lc100/stactools/cgls_lc100"
"stactools_landsat/stactools/landsat"
"stactools_planet/stactools/planet"
"stactools_naip/stactools/naip"
Expand Down
1 change: 1 addition & 0 deletions stactools/cgls_lc100
1 change: 1 addition & 0 deletions stactools/glc_layers
3 changes: 3 additions & 0 deletions stactools_cgls_lc100/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# stactools_cgls_lc100

A subpackage of stactools for working with Copernicus Global Land Cover Layers data.
2 changes: 2 additions & 0 deletions stactools_cgls_lc100/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rasterio==1.1.8 --no-binary=rasterio
pyproj==3.0.0.post1
48 changes: 48 additions & 0 deletions stactools_cgls_lc100/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import os
from os.path import (basename, splitext)
from imp import load_source
from setuptools import setup, find_namespace_packages
from glob import glob
import io

name = 'stactools_cgls_lc100'
description = (
"Subpackage for working with Copernicus Global Land Cover Layers data in stactools, "
"a command line tool and Python library for working with STAC.")

__version__ = load_source(
'stactools.cgls_lc100.version',
os.path.join(os.path.dirname(__file__),
'stactools/cgls_lc100/version.py')).__version__

here = os.path.abspath(os.path.dirname(__file__))

# get the dependencies and installs
with io.open(os.path.join(here, 'requirements.txt'), encoding='utf-8') as f:
install_requires = [line.split(' ')[0] for line in f.read().split('\n')]

# Add stactools subpackage dependencies
install_requires.extend(['stactools_core=={}'.format(__version__)])

with open(os.path.join(here, 'README.md')) as readme_file:
readme = readme_file.read()

setup(name=name,
description=description,
version=__version__,
long_description=readme,
long_description_content_type="text/markdown",
author="stac-utils",
author_email='stac@radiant.earth',
url='https://github.com/stac-utils/stactools.git',
packages=find_namespace_packages(),
py_modules=[
splitext(basename(path))[0] for path in glob('stactools/*.py')
],
include_package_data=False,
install_requires=install_requires,
license="Apache Software License 2.0",
keywords=[
'stactools', 'psytac', 'CGLS_LC100', 'imagery', 'landcover',
'land cover', 'catalog', 'STAC', 'Copernicus'
])
16 changes: 16 additions & 0 deletions stactools_cgls_lc100/stactools/cgls_lc100/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# flake8: noqa

from stactools.cgls_lc100.stac import create_item
from stactools.cgls_lc100.cog import create_cogs

import stactools.core

stactools.core.use_fsspec()


def register_plugin(registry):
# Register subcommands

from stactools.cgls_lc100 import commands

registry.register_subcommand(commands.create_cgls_lc100_command)
62 changes: 62 additions & 0 deletions stactools_cgls_lc100/stactools/cgls_lc100/cog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import logging
import os
from subprocess import Popen, PIPE, STDOUT

import pystac

from stactools.cgls_lc100.constants import (ITEM_COG_IMAGE_NAME,
ITEM_TIF_IMAGE_NAME)

logger = logging.getLogger(__name__)


def call(command):
def log_subprocess_output(pipe):
for line in iter(pipe.readline, b''): # b'\n'-separated lines
logger.info(line.decode("utf-8").strip('\n'))

process = Popen(command, stdout=PIPE, stderr=STDOUT)
with process.stdout:
log_subprocess_output(process.stdout)
return process.wait() # 0 means success


def cogify(input_path, output_path):
call(['gdal_translate', '-of', 'COG', '-co', input_path, output_path])


def _create_cog(item, cog_directory):
tif_asset = item.assets.get(ITEM_TIF_IMAGE_NAME)
cogify(tif_asset.href, cog_directory)

return cog_directory


def create_cogs(item, cog_directory=None):
"""Create COGs from the tif asset contained in the passed in STAC item.
Args:
item (pystac.Item): Land Cover Layers Item that contains an asset
with key equal to stactools.cgls_lc100.constants.ITEM_TIF_IMAGE_NAME,
which will be converted to COGs.
cog_directory (str): A URI of a directory to store COGs. This will be used
in conjunction with the file names based on the COG asset to store
the COG data. If not supplied, the directory of the Item's self HREF
will be used.
Returns:
pystac.Item: The same item, mutated to include assets for the
new COGs.
"""
if cog_directory is None:
cog_directory = os.path.dirname(item.get_self_href())

cog_href = os.path.join(cog_directory, '{}-cog.tif'.format(item.id))
_create_cog(item, cog_href)

asset = pystac.Asset(href=cog_href,
media_type=pystac.MediaType.COG,
roles=['data'],
title='Raster Dataset')

item.assets[ITEM_COG_IMAGE_NAME] = asset
50 changes: 50 additions & 0 deletions stactools_cgls_lc100/stactools/cgls_lc100/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import logging
import os

import click

from stactools.cgls_lc100 import stac, cog

logger = logging.getLogger(__name__)


def create_cgls_lc100_command(cli):
"""Creates a command group for commands working with
Copernicus Global Land Cover Layers data.
"""
@cli.group('cgls_lc100',
short_help=("Commands for working with "
"Copernicus Global Land Cover Layers data."))
def cgls_lc100():
pass

@cgls_lc100.command('create-item',
short_help=("Create a STAC Item from a Copernicus "
"Global Land Cover Layers tif file."))
@click.argument('tif_href')
@click.argument('dst')
@click.option('-c',
'--cogify',
is_flag=True,
help='Convert the tif into COG.')
def create_item_command(tif_href, dst, cogify):
"""Creates a STAC Item based on metadata from a tif
Copernicus Global Land Cover Layers file.
TIF_REF is the Copernicus Global Land Cover Layers tif file.
DST is directory that a STAC Item JSON file will be created
in. This will have a filename that matches the ID, which will
is the name of the tif_REF, without it's file extension.
"""

item = stac.create_item(tif_href)

item_path = os.path.join(dst, '{}.json'.format(item.id))
item.set_self_href(item_path)

if cogify:
cog.create_cogs(item)

item.save_object()

return cgls_lc100
92 changes: 92 additions & 0 deletions stactools_cgls_lc100/stactools/cgls_lc100/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
PROVIDER_NAME = 'European Environment Agency (EEA) under the framework of the Copernicus programme'

ITEM_COG_IMAGE_NAME = 'cog_image'
ITEM_TIF_IMAGE_NAME = 'tif_image'

DISCRETE_CLASSIFICATION_CLASS_NAMES = {
0: "Unknown.",
20:
"""Shrubs. Woody perennial plants with persistent and woody stems and without
any defined main stem being less than 5 m tall. The shrub foliage can be either
evergreen or deciduous.""",
30:
"""Herbaceous vegetation. Plants without persistent stem or shoots above ground and
lacking definite firm structure. Tree and shrub cover is less than 10 %.""",
40:
"""Cultivated and managed vegetation / agriculture. Lands covered with temporary crops
followed by harvest and a bare soil period (e.g., single and multiple cropping systems).
Note that perennial woody crops will be classified as the appropriate forest or shrub
land cover type.""",
50:
"Urban / built up. Land covered by buildings and other man-made structures.",
60:
"""Bare / sparse vegetation. Lands with exposed soil, sand, or rocks and never has more
than 10 % vegetated cover during any time of the year.""",
70: "Snow and ice. Lands under snow or ice cover throughout the year.",
80:
"""Permanent water bodies. Lakes, reservoirs, and rivers. Can be either fresh or
salt-water bodies.""",
90:
"""Herbaceous wetland. Lands with a permanent mixture of water and herbaceous or woody
vegetation. The vegetation can be present in either salt, brackish, or fresh water.""",
100: "Moss and lichen.",
111:
"""Closed forest, evergreen needle leaf. Tree canopy >70 %, almost all needle leaf trees
remain green all year. Canopy is never without green foliage.""",
112:
"""Closed forest, evergreen broad leaf. Tree canopy >70 %, almost all broadleaf trees remain
green year round. Canopy is never without green foliage.""",
113:
"""Closed forest, deciduous needle leaf. Tree canopy >70 %, consists of seasonal needle
leaf tree communities with an annual cycle of leaf-on and leaf-off periods.""",
114:
"""Closed forest, deciduous broad leaf. Tree canopy >70 %, consists of seasonal broadleaf
tree communities with an annual cycle of leaf-on and leaf-off periods.""",
115: "Closed forest, mixed.",
116: "Closed forest, not matching any of the other definitions.",
121:
"""Open forest, evergreen needle leaf. Top layer- trees 15-70 and second layer- mixed of
shrubs and grassland, almost all needle leaf trees remain green all year. Canopy is never
without green foliage.""",
122:
"""Open forest, evergreen broad leaf. Top layer- trees 15-70 and second layer- mixed of
shrubs and grassland, almost all broadleaf trees remain green year round. Canopy is never
without green foliage.""",
123:
"""Open forest, deciduous needle leaf. Top layer- trees 15-70 and second layer- mixed of
shrubs and grassland, consists of seasonal needle leaf tree communities with an
annual cycle of leaf-on and leaf-off periods.""",
124:
"""Open forest, deciduous broad leaf. Top layer- trees 15-70 and second layer- mixed of
shrubs and grassland, consists of seasonal broadleaf tree communities with an
annual cycle of leaf-on and leaf-off periods.""",
125: "Open forest, mixed.",
126: "Open forest, not matching any of the other definitions.",
200: "Oceans, seas. Can be either fresh or salt-water bodies."
}

DISCRETE_CLASSIFICATION_CLASS_PALETTE = {
0: '282828',
1: 'FFBB22',
2: 'FFFF4C',
3: 'F096FF',
4: 'FA0000',
5: 'B4B4B4',
6: 'F0F0F0',
7: '0032C8',
8: '0096A0',
9: 'FAE6A0',
10: '58481F',
11: '009900',
12: '70663E',
13: '00CC00',
14: '4E751F',
15: '007800',
16: '666000',
17: '8DB400',
18: '8D7400',
19: 'A0DC00',
20: '929900',
21: '648C00',
22: '000080'
}
Loading

0 comments on commit 26e0200

Please sign in to comment.