Skip to content

Commit

Permalink
Merge all the dist_alerts changes to production
Browse files Browse the repository at this point in the history
  • Loading branch information
danscales committed Dec 12, 2024
2 parents f9ab770 + bc6a6c7 commit e346d10
Show file tree
Hide file tree
Showing 20 changed files with 804 additions and 149 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
7 changes: 0 additions & 7 deletions app/models/enumerators/alerts_confidence.py

This file was deleted.

17 changes: 17 additions & 0 deletions app/models/enumerators/titiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from enum import Enum


class AlertConfidence(str, Enum):
low = "low"
high = "high"


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


class RenderType(str, Enum):
true_color = "true_color"
encoded = "encoded"
101 changes: 0 additions & 101 deletions app/routes/titiler/algorithms.py

This file was deleted.

Empty file.
175 changes: 175 additions & 0 deletions app/routes/titiler/algorithms/alerts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from collections import OrderedDict, namedtuple
from typing import Optional

import numpy as np
from fastapi.logger import logger
from rio_tiler.models import ImageData
from titiler.core.algorithm import BaseAlgorithm

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

Colors: namedtuple = namedtuple("Colors", ["red", "green", "blue"])
AlertConfig: namedtuple = namedtuple("AlertConfig", ["confidence", "colors"])


class Alerts(BaseAlgorithm):
"""Decode Deforestation Alerts."""

title: str = "Deforestation Alerts"
description: str = "Decode and visualize alerts"

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

record_start_date: str = "2014-12-31"

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
output_nbands: int = 4
output_dtype: str = "uint8"

def __call__(self, img: ImageData) -> ImageData:
"""Process the input image and decode deforestation or land disturbance
alert raster data into RGBA format.
Args:
img (ImageData): Input image data with alert date/confidence and intensity
(zoom-level visibility) layers.
Returns:
ImageData: Processed image with RGBA channels either with true colors ready for
visualization or encoding date and confidence for front-end processing.
"""
date_conf_data = img.data[0]

self.intensity = img.data[1]
self.no_data = img.array.mask[0]
self.data_alert_confidence = date_conf_data // 10000
self.alert_date = date_conf_data % 10000

self.mask = self.create_mask()

if self.render_type == RenderType.true_color:
rgb = self.create_true_color_rgb()
alpha = self.create_true_color_alpha()
else: # encoded
rgb = self.create_encoded_rgb()
alpha = self.create_encoded_alpha()

data = np.vstack([rgb, alpha[np.newaxis, ...]]).astype(self.output_dtype)
data = np.ma.MaskedArray(data, mask=False)

return ImageData(data, assets=img.assets, crs=img.crs, bounds=img.bounds)

def create_mask(self):
"""Generate a mask for pixel visibility based on date and confidence
filters, and no data values.
Returns:
np.ndarray: A mask array pixels with no alert or alerts not meeting filter
condition are masked.
"""

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

def create_true_color_rgb(self):
"""Map alert confidence levels to RGB values for visualization.
Returns:
np.ndarray: A 3D array with RGB channels.
"""
r, g, b = self._rgb_zeros_array()

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

return np.stack([r, g, b], axis=0)

def create_encoded_rgb(self):
"""Encode the alert date and confidence into the RGB channels, allowing
interactive date filtering and color control on Flagship.
Returns:
np.ndarray: A 3D array with encoded RGB values.
"""
r, g, b = self._rgb_zeros_array()
r = self.alert_date // 255
g = self.alert_date % 255
b = (self.data_alert_confidence // 3 + 1) * 100 + self.intensity

return np.stack([r, g, b], axis=0)

def create_true_color_alpha(self):
"""Set the transparency (alpha) channel for alert pixels based on date,
confidence filters, and intensity input. The intensity multiplier is
used to control how isolated alerts fade out at low zoom levels,
matching the rendering behavior in Flagship.
Returns:
np.ndarray: Array representing the alpha (transparency) channel, where pixel
visibility is adjusted by intensity.
"""
alpha = np.where(self.mask, self.intensity * 150, 0)
return np.minimum(255, alpha)

def create_encoded_alpha(self):
"""Generate the alpha channel for encoded alerts. The default
implementation sets pixel visibility based on date/confidence filters
and intensity input. Can be overridden for specific alert types.
Returns:
np.ndarray: An array representing the alpha channel.
"""
logger.info(
"""Encoded alpha not provided, returning alpha
from input layer and date/confidence mask."""
)
return self.create_true_color_alpha()

def _rgb_zeros_array(self):
r = np.zeros_like(self.data_alert_confidence, dtype=np.uint8)
g = np.zeros_like(self.data_alert_confidence, dtype=np.uint8)
b = np.zeros_like(self.data_alert_confidence, dtype=np.uint8)

return r, g, b
66 changes: 66 additions & 0 deletions app/routes/titiler/algorithms/dist_alerts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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

from .alerts import AlertConfig, Alerts, Colors


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(
confidence=2, colors=Colors(237, 164, 194)
),
AlertConfidence.high: AlertConfig(
confidence=3, colors=Colors(220, 102, 153)
),
}
)

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
Loading

0 comments on commit e346d10

Please sign in to comment.