Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional gamma correction parameter #350

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"cachetools>=3.1.0",
"click",
"click-spinner",
"color-operations",
"flask",
"flask_cors",
"marshmallow>=3.0.0",
Expand Down
18 changes: 16 additions & 2 deletions terracotta/handlers/rgb.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def rgb(
tile_xyz: Optional[Tuple[int, int, int]] = None,
*,
stretch_ranges: Optional[ListOfRanges] = None,
color_transform: Optional[str] = None,
tile_size: Optional[Tuple[int, int]] = None
) -> BinaryIO:
"""Return RGB image as PNG
Expand Down Expand Up @@ -88,7 +89,8 @@ def get_band_future(band_key: str) -> Future:
keys = (*some_keys, band_key)
metadata = driver.get_metadata(keys)

band_stretch_range = list(metadata["range"])
band_range = list(metadata["range"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable

band_stretch_range = band_range.copy()
scale_min, scale_max = band_stretch_override

percentiles = metadata.get("percentiles", [])
Expand All @@ -103,8 +105,20 @@ def get_band_future(band_key: str) -> Future:
"Upper stretch bound must be higher than lower bound"
)

# normalize to [0, 1] range
band_data = band_data_future.result()
out_arrays.append(image.to_uint8(band_data, *band_stretch_range))
band_data = image.contrast_stretch(band_data, band_stretch_range, (0, 1))
out_arrays.append(band_data)

# out_ranges = np.ma.stack(out_ranges, axis=0)
band_data = np.ma.stack(out_arrays, axis=0)

if color_transform:
band_data = image.apply_color_transform(band_data, color_transform)

out_arrays = []
for k in range(band_data.shape[0]):
out_arrays.append(image.to_uint8(band_data[k], lower_bound=0, upper_bound=1))

out = np.ma.stack(out_arrays, axis=-1)
return image.array_to_png(out)
15 changes: 13 additions & 2 deletions terracotta/handlers/singleband.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

import collections

import numpy as np

from terracotta import get_settings, get_driver, image, xyz
from terracotta.profile import trace

Expand All @@ -26,6 +28,7 @@ def singleband(
*,
colormap: Union[str, Mapping[Number, RGBA], None] = None,
stretch_range: Optional[Tuple[NumberOrString, NumberOrString]] = None,
color_transform: Optional[str] = None,
tile_size: Optional[Tuple[int, int]] = None
) -> BinaryIO:
"""Return singleband image as PNG"""
Expand Down Expand Up @@ -61,7 +64,8 @@ def singleband(
out = image.label(tile_data, labels)
else:
# determine stretch range from metadata and arguments
stretch_range_ = list(metadata["range"])
band_range = list(metadata["range"])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

stretch_range_ = band_range.copy()

percentiles = metadata.get("percentiles", [])
if stretch_min is not None:
Expand All @@ -71,6 +75,13 @@ def singleband(
stretch_range_[1] = image.get_stretch_scale(stretch_max, percentiles)

cmap_or_palette = cast(Optional[str], colormap)
out = image.to_uint8(tile_data, *stretch_range_)

tile_data = np.expand_dims(tile_data, axis=0)
tile_data = image.contrast_stretch(tile_data, stretch_range_, (0, 1))

if color_transform:
tile_data = image.apply_color_transform(tile_data, color_transform)

out = image.to_uint8(tile_data, lower_bound=0, upper_bound=1)[0]

return image.array_to_png(out, colormap=cmap_or_palette)
12 changes: 12 additions & 0 deletions terracotta/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import numpy as np
from PIL import Image
from color_operations import parse_operations

from terracotta.profile import trace
from terracotta import exceptions, get_settings
Expand Down Expand Up @@ -162,6 +163,17 @@ def to_uint8(data: Array, lower_bound: Number, upper_bound: Number) -> Array:
return rescaled.astype(np.uint8)


def apply_color_transform(
masked_data: Array,
color_transform: str,
) -> Array:
"""Apply color transform to input array. Input array should be normalized to [0,1]."""
for func in parse_operations(color_transform):
arr = func(masked_data)

return arr


def label(data: Array, labels: Sequence[Number]) -> Array:
"""Create a labelled uint8 version of data, with output values starting at 1.

Expand Down
15 changes: 15 additions & 0 deletions terracotta/server/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from typing import Any, Union

from color_operations import parse_operations


class StringOrNumber(fields.Field):
"""
Expand Down Expand Up @@ -45,3 +47,16 @@ def validate_stretch_range(data: Any) -> None:
if isinstance(data, str):
if not re.match("^p\\d+$", data):
raise ValidationError("Percentile format is `p<digits>`")


def validate_color_transform(data: Any) -> None:
"""
Validate that the color transform is a string and can be parsed by `color_operations`.
"""
if not isinstance(data, str):
raise ValidationError("Color transform needs to be a string")

try:
parse_operations(data)
except (ValueError, KeyError):
raise ValidationError("Invalid color transform")
13 changes: 11 additions & 2 deletions terracotta/server/rgb.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
from marshmallow import Schema, fields, validate, pre_load, ValidationError, EXCLUDE
from flask import request, send_file, Response

from terracotta.server.fields import StringOrNumber, validate_stretch_range
from terracotta.server.fields import (
StringOrNumber,
validate_stretch_range,
validate_color_transform,
)
from terracotta.server.flask_api import TILE_API


Expand Down Expand Up @@ -48,7 +52,7 @@ class Meta:
example="[0,1]",
missing=None,
description=(
"Stretch range [min, max] to use for the gren band as JSON array. "
"Stretch range [min, max] to use for the green band as JSON array. "
"Min and max may be numbers to use as absolute range, or strings "
"of the format `p<digits>` with an integer between 0 and 100 "
"to use percentiles of the image instead. "
Expand All @@ -68,6 +72,11 @@ class Meta:
"Null values indicate global minimum / maximum."
),
)
color_transform = fields.String(
validate=validate_color_transform,
missing=None,
description="Color transform DSL string from color-operations.",
)
tile_size = fields.List(
fields.Integer(),
validate=validate.Length(equal=2),
Expand Down
14 changes: 13 additions & 1 deletion terracotta/server/singleband.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
)
from flask import request, send_file, Response

from terracotta.server.fields import StringOrNumber, validate_stretch_range
from terracotta.server.fields import (
StringOrNumber,
validate_stretch_range,
validate_color_transform,
)
from terracotta.server.flask_api import TILE_API
from terracotta.cmaps import AVAILABLE_CMAPS

Expand Down Expand Up @@ -65,6 +69,14 @@ class Meta:
"hex strings.",
)

color_transform = fields.String(
validate=validate_color_transform,
missing=None,
example="gamma 1 1.5, sigmoidal 1 15 0.5",
description="Color transform DSL string from color-operations."
"All color operations for singleband should specify band 1.",
)

tile_size = fields.List(
fields.Integer(),
validate=validate.Length(equal=2),
Expand Down