Skip to content

Commit

Permalink
Merge pull request #171 from wri/dev
Browse files Browse the repository at this point in the history
Dev -> staging:  DIST alerts forest filter
  • Loading branch information
solomon-negusse authored Nov 7, 2024
2 parents 4e9ba8d + efab786 commit bd534de
Show file tree
Hide file tree
Showing 16 changed files with 423 additions and 94 deletions.
8 changes: 4 additions & 4 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
from .routes import preview

from .routes.titiler import routes as titiler_routes
from .routes.titiler.gfw_integrated_alerts import router as integrated_alerts_router
from .routes.titiler.umd_glad_dist_alerts import router as dist_alerts_router

gunicorn_logger = logging.getLogger("gunicorn.error")
logger.handlers = gunicorn_logger.handlers
Expand All @@ -55,6 +57,8 @@
burned_areas_tiles.router,
dynamic_vector_tiles.router,
vector_tiles.router,
integrated_alerts_router,
dist_alerts_router,
umd_tree_cover_loss_raster_tiles.router,
umd_glad_landsat_alerts_raster_tiles.router,
umd_glad_sentinel2_alerts_raster_tiles.router,
Expand All @@ -79,10 +83,6 @@
app.include_router(
titiler_routes.mosaic.router, prefix="/cog/mosaic", tags=["Mosaic Tiles"]
)
app.include_router(
titiler_routes.custom.router, prefix="/cog/custom", tags=["Custom Tiles"]
)


#####################
## Middleware
Expand Down
5 changes: 5 additions & 0 deletions app/models/enumerators/titiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
class AlertConfidence(str, Enum):
low = "low"
high = "high"


class IntegratedAlertConfidence(str, Enum):
low = "low"
high = "high"
highest = "highest"


Expand Down
70 changes: 29 additions & 41 deletions app/routes/titiler/algorithms/alerts.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from collections import OrderedDict, namedtuple
from datetime import date
from typing import Optional

import numpy as np
from dateutil.relativedelta import relativedelta
from fastapi.logger import logger
from pydantic import Field
from rio_tiler.models import ImageData
from titiler.core.algorithm import BaseAlgorithm

from app.models.enumerators.titiler import AlertConfidence, RenderType
from app.models.enumerators.titiler import IntegratedAlertConfidence, RenderType

Colors: namedtuple = namedtuple("Colors", ["red", "green", "blue"])
AlertConfig: namedtuple = namedtuple("AlertConfig", ["confidence", "colors"])
Expand All @@ -22,42 +20,24 @@ class Alerts(BaseAlgorithm):

conf_colors: OrderedDict = OrderedDict(
{
AlertConfidence.low: AlertConfig(
IntegratedAlertConfidence.low: AlertConfig(
confidence=2, colors=Colors(237, 164, 194)
),
AlertConfidence.high: AlertConfig(
IntegratedAlertConfidence.high: AlertConfig(
confidence=3, colors=Colors(220, 102, 153)
),
AlertConfidence.highest: AlertConfig(
IntegratedAlertConfidence.highest: AlertConfig(
confidence=4, colors=Colors(201, 42, 109)
),
}
)

record_start_date: str = "2014-12-31"

today: date = date.today()

# Parameters
default_start_date: str = (today - relativedelta(days=180)).strftime("%Y-%m-%d")
start_date: str = Field(
default_start_date,
description="start date of alert in YYYY-MM-DD format.",
)

default_end_date: str = today.strftime("%Y-%m-%d")
end_date: str = Field(
default_end_date, description="end date of alert in YYYY-MM-DD format."
)

alert_confidence: AlertConfidence = Field(
AlertConfidence.low, description="Alert confidence"
)

render_type: RenderType = Field(
RenderType.true_color,
description="Render true color or encoded pixels",
)
start_date: Optional[str] = None
end_date: Optional[str] = None
alert_confidence: Optional[str] = None
render_type: RenderType = RenderType.true_color

# metadata
input_nbands: int = 2
Expand Down Expand Up @@ -105,18 +85,27 @@ def create_mask(self):
np.ndarray: A mask array pixels with no alert or alerts not meeting filter
condition are masked.
"""
start_mask = self.alert_date >= (
np.datetime64(self.start_date) - np.datetime64(self.record_start_date)
)
end_mask = self.alert_date <= (
np.datetime64(self.end_date) - np.datetime64(self.record_start_date)
)

confidence_mask = (
self.data_alert_confidence
>= self.conf_colors[self.alert_confidence].confidence
)
mask = ~self.no_data * start_mask * end_mask * confidence_mask
mask = ~self.no_data

if self.alert_confidence:
confidence_mask = (
self.data_alert_confidence
>= self.conf_colors[self.alert_confidence].confidence
)
mask *= confidence_mask

if self.start_date:
start_mask = self.alert_date >= (
np.datetime64(self.start_date) - np.datetime64(self.record_start_date)
)
mask *= start_mask

if self.end_date:
end_mask = self.alert_date <= (
np.datetime64(self.end_date) - np.datetime64(self.record_start_date)
)
mask *= end_mask

return mask

Expand All @@ -131,7 +120,6 @@ def create_true_color_rgb(self):
for properties in self.conf_colors.values():
confidence = properties.confidence
colors = properties.colors

r[self.data_alert_confidence >= confidence] = colors.red
g[self.data_alert_confidence >= confidence] = colors.green
b[self.data_alert_confidence >= confidence] = colors.blue
Expand Down
43 changes: 43 additions & 0 deletions app/routes/titiler/algorithms/dist_alerts.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from collections import OrderedDict
from typing import Optional

from pydantic import ConfigDict
from rio_tiler.models import ImageData

from app.models.enumerators.titiler import AlertConfidence

Expand All @@ -9,6 +13,7 @@ class DISTAlerts(Alerts):
title: str = "Land Disturbance (DIST) Alerts"
description: str = "Decode and visualize DIST alerts"

model_config = ConfigDict(arbitrary_types_allowed=True)
conf_colors: OrderedDict = OrderedDict(
{
AlertConfidence.low: AlertConfig(
Expand All @@ -21,3 +26,41 @@ class DISTAlerts(Alerts):
)

record_start_date: str = "2020-12-31"

tree_cover_density_mask: Optional[int] = None
tree_cover_density_data: Optional[ImageData] = None

tree_cover_height_mask: Optional[int] = None
tree_cover_height_data: Optional[ImageData] = None

# the highest loss year that is used to exclude alerts for
# the purpose of showing only alerts in forests
tree_cover_loss_mask: Optional[int] = None
tree_cover_loss_data: Optional[ImageData] = None

def create_mask(self):
mask = super().create_mask()

if self.tree_cover_density_mask:
mask *= (
self.tree_cover_density_data.array[0, :, :]
>= self.tree_cover_density_mask
)

if self.tree_cover_height_mask:
mask *= (
self.tree_cover_height_data.array[0, :, :]
>= self.tree_cover_height_mask
)

if self.tree_cover_loss_mask:
# Tree cover loss data before 2020 can't be used to filter out pixels as not forest.
# Instead, we use tree cover height taken that year as source of truth.
# For example, if a pixel had tree cover loss in 2018, but has tree cover
# height (2020) that meets the forest threshold, the pixel meets
# the forest criteria for alerts and is not masked out.
mask *= (
self.tree_cover_loss_data.array[0, :, :] > self.tree_cover_loss_mask
) | (self.tree_cover_loss_data.array[0, :, :] <= 2020)

return mask
8 changes: 4 additions & 4 deletions app/routes/titiler/algorithms/integrated_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

from app.models.enumerators.titiler import AlertConfidence
from app.models.enumerators.titiler import IntegratedAlertConfidence

from .alerts import AlertConfig, Alerts, Colors

Expand All @@ -13,13 +13,13 @@ class IntegratedAlerts(Alerts):

conf_colors: OrderedDict = OrderedDict(
{
AlertConfidence.low: AlertConfig(
IntegratedAlertConfidence.low: AlertConfig(
confidence=2, colors=Colors(237, 164, 194)
),
AlertConfidence.high: AlertConfig(
IntegratedAlertConfidence.high: AlertConfig(
confidence=3, colors=Colors(220, 102, 153)
),
AlertConfidence.highest: AlertConfig(
IntegratedAlertConfidence.highest: AlertConfig(
confidence=4, colors=Colors(201, 42, 109)
),
}
Expand Down
92 changes: 92 additions & 0 deletions app/routes/titiler/gfw_integrated_alerts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import os
from typing import Optional, Tuple

from aenum import Enum, extend_enum
from fastapi import APIRouter, Depends, Query, Response
from titiler.core.resources.enums import ImageType
from titiler.core.utils import render_image

from ...crud.sync_db.tile_cache_assets import get_versions
from ...models.enumerators.tile_caches import TileCacheType
from ...models.enumerators.titiler import IntegratedAlertConfidence, RenderType
from .. import DATE_REGEX, raster_xyz
from .algorithms.integrated_alerts import IntegratedAlerts
from .readers import AlertsReader

DATA_LAKE_BUCKET = os.environ.get("DATA_LAKE_BUCKET")

router = APIRouter()

dataset = "gfw_integrated_alerts"


class GfwIntegratdAlertsVersions(str, Enum):
"""GFW Integrated Alerts versions.
When using `latest` call will be redirected (307) to version tagged
as latest.
"""

latest = "latest"


_versions = get_versions(dataset, TileCacheType.cog)
for _version in _versions:
extend_enum(GfwIntegratdAlertsVersions, _version, _version)


# will turn this on when we're ready to replace tile cache service
# @router.get(
# f"/{dataset}/{{version}}/dynamic/{{z}}/{{x}}/{{y}}.png",
# response_class=Response,
# tags=["Raster Tiles"],
# response_description="PNG Raster Tile",
# )
@router.get(
f"/{dataset}/{{version}}/titiler/{{z}}/{{x}}/{{y}}.png",
response_class=Response,
tags=["Raster Tiles"],
response_description="PNG Raster Tile",
)
async def gfw_integrated_alerts_raster_tile(
*,
version: GfwIntegratdAlertsVersions,
xyz: Tuple[int, int, int] = Depends(raster_xyz),
start_date: Optional[str] = Query(
None,
regex=DATE_REGEX,
description="Only show alerts for given date and after",
),
end_date: Optional[str] = Query(
None, regex=DATE_REGEX, description="Only show alerts until given date."
),
render_type: RenderType = Query(
RenderType.encoded, description="Render true color or encoded tiles"
),
alert_confidence: Optional[IntegratedAlertConfidence] = Query(
IntegratedAlertConfidence.low,
description="Show alerts with at least this confidence level",
),
) -> Response:
"""GFW Integrated Alerts raster tiles."""

bands = ["default", "intensity"]
folder: str = f"s3://{DATA_LAKE_BUCKET}/{dataset}/{version}/raster/epsg-4326/cog"
with AlertsReader(input=folder) as reader:
tile_x, tile_y, zoom = xyz
image_data = reader.tile(tile_x, tile_y, zoom, bands=bands)

processed_image = IntegratedAlerts(
start_date=start_date,
end_date=end_date,
render_type=render_type,
alert_confidence=alert_confidence,
)(image_data)

content, media_type = render_image(
processed_image,
output_format=ImageType("png"),
add_mask=False,
)

return Response(content, media_type=media_type)
3 changes: 1 addition & 2 deletions app/routes/titiler/readers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@


@attr.s
class IntegratedAlertsReader(MultiBandReader):
class AlertsReader(MultiBandReader):

input: str = attr.ib()
# bands: Sequence[str] = attr.ib(init=False)
tms: morecantile.TileMatrixSet = attr.ib(
default=morecantile.tms.get("WebMercatorQuad")
)
Expand Down
24 changes: 1 addition & 23 deletions app/routes/titiler/routes.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,10 @@
"""Titiler Dynamic Raster tiles for Cloud Optimized Geotiffs (COG)"""

from typing import Callable

from titiler.core.algorithm import Algorithms
from titiler.core.algorithm import algorithms as default_algorithms
from titiler.core.factory import AlgorithmFactory, MultiBandTilerFactory, TilerFactory
from titiler.core.factory import AlgorithmFactory, TilerFactory
from titiler.extensions import cogValidateExtension, cogViewerExtension
from titiler.mosaic.factory import MosaicTilerFactory

from ...routes import cog_asset_dependency
from .algorithms.integrated_alerts import IntegratedAlerts
from .algorithms.dist_alerts import DISTAlerts
from .readers import IntegratedAlertsReader


algorithms: Algorithms = default_algorithms.register(
{"integrated_alerts": IntegratedAlerts, "dist_alerts": DISTAlerts}
)

# Create a PostProcessParams dependency
PostProcessParams: Callable = algorithms.dependency

custom = MultiBandTilerFactory(
router_prefix="/cog/custom",
process_dependency=PostProcessParams,
reader=IntegratedAlertsReader,
path_dependency=cog_asset_dependency,
)

cog = TilerFactory(
router_prefix="/cog/basic",
Expand Down
Loading

0 comments on commit bd534de

Please sign in to comment.