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

Range colormap #439

Merged
merged 4 commits into from
Oct 19, 2021
Merged
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
18 changes: 18 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,24 @@ with COGReader(
* compare dataset bounds and tile bounds in TMS crs in `rio_tiler.io.base.SpatialMixin.tile_exists` method to allow dataset and TMS not compatible with WGS84 crs (https://github.com/cogeotiff/rio-tiler/pull/429)
* use `httpx` package instead of requests (author @rodrigoalmeida94, https://github.com/cogeotiff/rio-tiler/pull/431)
* allow **half pixel** `tile_buffer` around the tile (e.g 0.5 -> 257x257, 1.5 -> 259x259) (author @bstadlbauer, https://github.com/cogeotiff/rio-tiler/pull/405)
* add support for **intervals** colormap (https://github.com/cogeotiff/rio-tiler/pull/439))

```python
from rio_tiler.colormap import apply_cmap, apply_intervals_cmap

data = numpy.random.randint(0, 255, size=(1, 256, 256))
cmap = [
# ([min, max], [r, g, b, a])
([0, 1], [0, 0, 0, 0]),
([1, 10], [255, 255, 255, 255]),
([10, 100], [255, 0, 0, 255]),
([100, 256], [255, 255, 0, 255]),
]

data, mask = apply_intervals_cmap(data, cmap)
# or
data, mask = apply_cmap(data, cmap)
```

**breaking changes**

Expand Down
28 changes: 28 additions & 0 deletions docs/colormap.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,34 @@ ndvi_dict = {idx: value.tolist() for idx, value in enumerate(cmap_uint8)}
cmap = cmap.register({"ndvi": ndvi_dict})
```

### Internvals colormaps

Starting with `rio-tiler` 3.0, *intervals* colormap support has been added. This is usefull when you want to define color breaks for a given data.

!!! warnings
For `intervals`, colormap has to be in form of `Sequence[Tuple[Sequence, Sequence]]`:
```
[
([min, max], [r, g, b, a]),
([min, max], [r, g, b, a]),
...
]
```

```python
from rio_tiler.colormap import apply_cmap

data = numpy.random.randint(0, 255, size=(1, 256, 256))
cmap = [
([0, 1], [0, 0, 0, 0]),
([1, 10], [255, 255, 255, 255]),
([10, 100], [255, 0, 0, 255]),
([100, 256], [255, 255, 0, 255]),
]

data, mask = apply_cmap(data, cmap)
```

### Default rio-tiler's colormaps

![](img/custom.png)
Expand Down
48 changes: 46 additions & 2 deletions rio_tiler/colormap.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import attr
import numpy

from .constants import NumType
from .errors import (
ColorMapAlreadyRegistered,
InvalidColorFormat,
Expand Down Expand Up @@ -79,7 +80,7 @@ def make_lut(colormap: Dict) -> numpy.ndarray:


def apply_cmap(
data: numpy.ndarray, colormap: Dict
data: numpy.ndarray, colormap: Union[Dict, Sequence]
) -> Tuple[numpy.ndarray, numpy.ndarray]:
"""Apply colormap on data.

Expand All @@ -97,6 +98,9 @@ def apply_cmap(
if data.shape[0] > 1:
raise InvalidFormat("Source data must be 1 band")

if isinstance(colormap, Sequence):
return apply_intervals_cmap(data, colormap)

# if colormap has more than 256 values OR its `max` key >= 256 we can't use
# rio_tiler.colormap.make_lut, because we don't want to create a `lookup table`
# with more than 256 entries (256 x 4) array. In this case we use `apply_discrete_cmap`
Expand Down Expand Up @@ -132,7 +136,7 @@ def apply_discrete_cmap(
Examples:
>>> data = numpy.random.randint(0, 3, size=(1, 256, 256))
cmap = {
0, [0, 0, 0, 0],
0: [0, 0, 0, 0],
1: [255, 255, 255, 255],
2: [255, 0, 0, 255],
3: [255, 255, 0, 255],
Expand All @@ -156,6 +160,46 @@ def apply_discrete_cmap(
return data[:-1], data[-1]


def apply_intervals_cmap(
data: numpy.ndarray, colormap: Sequence[Sequence[Sequence[NumType]]]
) -> Tuple[numpy.ndarray, numpy.ndarray]:
"""Apply intervals colormap.

Args:
data (numpy ndarray): 1D image array to translate to RGB.
color_map (Sequence): Sequence of intervals and color in form of [([min, max], [r, g, b, a]), ...].

Returns:
tuple: Data (numpy.ndarray) and Alpha band (numpy.ndarray).

Examples:
>>> data = numpy.random.randint(0, 3, size=(1, 256, 256))
cmap = [
([0, 1], [0, 0, 0, 0]),
([1, 2], [255, 255, 255, 255]),
([2, 3], [255, 0, 0, 255]),
([3, 4], [255, 255, 0, 255]),
]

data, mask = apply_intervals_cmap(data, cmap)
assert data.shape == (3, 256, 256)

"""
res = numpy.zeros((data.shape[1], data.shape[2], 4), dtype=numpy.uint8)

for (k, v) in colormap:
res[(data[0] >= k[0]) & (data[0] < k[1])] = v

data = numpy.transpose(res, [2, 0, 1])

# If the colormap has values between 0-255
# we cast the output array to Uint8
if data.min() >= 0 and data.max() <= 255:
data = data.astype("uint8")

return data[:-1], data[-1]


def parse_color(rgba: Union[Sequence[int], str]) -> Tuple[int, int, int, int]:
"""Parse RGB/RGBA color and return valid rio-tiler compatible RGBA colormap entry.

Expand Down
4 changes: 2 additions & 2 deletions rio_tiler/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,15 +312,15 @@ def render(
self,
add_mask: bool = True,
img_format: str = "PNG",
colormap: Optional[Dict] = None,
colormap: Optional[Union[Dict, Sequence]] = None,
**kwargs,
) -> bytes:
"""Render data to image blob.

Args:
add_mask (bool, optional): add mask to output image. Defaults to `True`.
img_format (str, optional): output image format. Defaults to `PNG`.
colormap (dict, optional): GDAL RGBA Color Table dictionary.
colormap (dict or sequence, optional): RGBA Color Table dictionary or sequence.
kwargs (optional): keyword arguments to forward to `rio_tiler.utils.render`.

Returns:
Expand Down
4 changes: 2 additions & 2 deletions rio_tiler/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ def render(
data: numpy.ndarray,
mask: Optional[numpy.ndarray] = None,
img_format: str = "PNG",
colormap: Optional[Dict] = None,
colormap: Optional[Union[Dict, Sequence]] = None,
**creation_options: Any,
) -> bytes:
"""Translate numpy.ndarray to image bytes.
Expand All @@ -438,7 +438,7 @@ def render(
data (numpy.ndarray): Image array to encode.
mask (numpy.ndarray, optional): Mask array.
img_format (str, optional): Image format. See: for the list of supported format by GDAL: https://www.gdal.org/formats_list.html. Defaults to `PNG`.
colormap (dict, optional): GDAL RGBA Color Table dictionary.
colormap (dict or sequence, optional): RGBA Color Table dictionary or sequence.
creation_options (optional): Image driver creation options to forward to GDAL.

Returns
Expand Down
39 changes: 38 additions & 1 deletion tests/test_cmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_apply_cmap():
def test_apply_discrete_cmap():
"""Should return valid data and mask."""
cm = {1: [0, 0, 0, 255], 2: [255, 255, 255, 255]}
data = numpy.zeros(shape=(1, 10, 10), dtype=numpy.uint8)
data = numpy.zeros(shape=(1, 10, 10), dtype=numpy.uint16)
data[0, 0:2, 0:2] = 1000
data[0, 2:5, 2:5] = 1
data[0, 5:, 5:] = 2
Expand Down Expand Up @@ -172,6 +172,43 @@ def test_apply_discrete_cmap():
assert colormap.apply_cmap(data, cm)


def test_apply_intervals_cmap():
"""Should return valid data and mask."""
cm = [
# ([min, max], [r, g, b, a])
([1, 2], [0, 0, 0, 255]),
([2, 3], [255, 255, 255, 255]),
]
data = numpy.zeros(shape=(1, 10, 10), dtype=numpy.uint16)
data[0, 0:2, 0:2] = 1000
data[0, 2:5, 2:5] = 1
data[0, 5:, 5:] = 2
d, m = colormap.apply_intervals_cmap(data, cm)
assert d.shape == (3, 10, 10)
assert m.shape == (10, 10)

mask = numpy.zeros(shape=(10, 10), dtype=numpy.uint8)
mask[2:5, 2:5] = 255
mask[5:, 5:] = 255
numpy.testing.assert_array_equal(m, mask)

data = data.astype("uint16")
d, m = colormap.apply_intervals_cmap(data, cm)
assert d.dtype == numpy.uint8
assert m.dtype == numpy.uint8

cm = [
# ([min, max], [r, g, b, a])
([1, 2], [0, 0, 0, 255]),
([2, 3], [255, 255, 255, 255]),
([2, 1000], [255, 0, 0, 255]),
]
d, m = colormap.apply_intervals_cmap(data, cm)
assert d.shape == (3, 10, 10)
assert m.shape == (10, 10)
d[:, 0, 0] == [255, 0, 0, 255]


@pytest.mark.parametrize(
"value,result",
[
Expand Down