Skip to content

Commit 4c5bfb1

Browse files
authored
allow branca ColorMap in write_png (#126)
* allow branca ColorMap in write_png and make it have parity with the version in Folium, so we can remove the code there. * add tests * Update utilities.py * fix typing * add numpy to requirements-dev
1 parent 9f7aaa1 commit 4c5bfb1

File tree

3 files changed

+63
-14
lines changed

3 files changed

+63
-14
lines changed

branca/utilities.py

+28-14
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import os
1313
import re
1414
import struct
15+
import typing
1516
import zlib
17+
from typing import Any, Callable, Union
1618

1719
from jinja2 import Environment, PackageLoader
1820

@@ -26,6 +28,9 @@
2628
except ImportError:
2729
np = None
2830

31+
if typing.TYPE_CHECKING:
32+
from branca.colormap import ColorMap
33+
2934

3035
rootpath = os.path.abspath(os.path.dirname(__file__))
3136

@@ -276,7 +281,11 @@ def image_to_url(image, colormap=None, origin="upper"):
276281
return url.replace("\n", " ")
277282

278283

279-
def write_png(data, origin="upper", colormap=None):
284+
def write_png(
285+
data: Any,
286+
origin: str = "upper",
287+
colormap: Union["ColorMap", Callable, None] = None,
288+
) -> bytes:
280289
"""
281290
Transform an array of data into a PNG string.
282291
This can be written to disk using binary I/O, or encoded using base64
@@ -292,28 +301,31 @@ def write_png(data, origin="upper", colormap=None):
292301
----------
293302
data: numpy array or equivalent list-like object.
294303
Must be NxM (mono), NxMx3 (RGB) or NxMx4 (RGBA)
295-
296304
origin : ['upper' | 'lower'], optional, default 'upper'
297305
Place the [0,0] index of the array in the upper left or lower left
298306
corner of the axes.
299-
300-
colormap : callable, used only for `mono` image.
301-
Function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)]
302-
for transforming a mono image into RGB.
303-
It must output iterables of length 3 or 4, with values between
304-
0. and 1. Hint: you can use colormaps from `matplotlib.cm`.
307+
colormap : ColorMap subclass or callable, optional
308+
Only needed to transform mono images into RGB. You have three options:
309+
- use a subclass of `ColorMap` like `LinearColorMap`
310+
- use a colormap from `matplotlib.cm`
311+
- use a custom function of the form [x -> (r,g,b)] or [x -> (r,g,b,a)].
312+
It must output iterables of length 3 or 4 with values between 0 and 1.
305313
306314
Returns
307315
-------
308316
PNG formatted byte string
309317
"""
318+
from branca.colormap import ColorMap
319+
310320
if np is None:
311321
raise ImportError("The NumPy package is required" " for this functionality")
312322

313-
if colormap is None:
314-
315-
def colormap(x):
316-
return (x, x, x, 1)
323+
if isinstance(colormap, ColorMap):
324+
colormap_callable = colormap.rgba_floats_tuple
325+
elif callable(colormap):
326+
colormap_callable = colormap
327+
else:
328+
colormap_callable = lambda x: (x, x, x, 1) # noqa E731
317329

318330
array = np.atleast_3d(data)
319331
height, width, nblayers = array.shape
@@ -323,7 +335,7 @@ def colormap(x):
323335
assert array.shape == (height, width, nblayers)
324336

325337
if nblayers == 1:
326-
array = np.array(list(map(colormap, array.ravel())))
338+
array = np.array(list(map(colormap_callable, array.ravel())))
327339
nblayers = array.shape[1]
328340
if nblayers not in [3, 4]:
329341
raise ValueError(
@@ -340,7 +352,9 @@ def colormap(x):
340352

341353
# Normalize to uint8 if it isn't already.
342354
if array.dtype != "uint8":
343-
array = array * 255.0 / array.max(axis=(0, 1)).reshape((1, 1, 4))
355+
with np.errstate(divide="ignore", invalid="ignore"):
356+
array = array * 255.0 / array.max(axis=(0, 1)).reshape((1, 1, 4))
357+
array[~np.isfinite(array)] = 0
344358
array = array.astype("uint8")
345359

346360
# Eventually flip the image.

requirements-dev.txt

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ isort
99
jupyter
1010
nbsphinx
1111
nbval
12+
numpy
1213
pylint
1314
pytest
1415
pytest-cov

tests/test_utilities.py

+34
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
import branca.utilities as ut
8+
from branca.colormap import LinearColormap
89

910
rootpath = Path(os.path.dirname(os.path.abspath(__file__))) / ".." / "branca"
1011
color_brewer_minimum_n = 3
@@ -132,3 +133,36 @@ def test_parse_size(value, result):
132133
def test_parse_size_exceptions(value):
133134
with pytest.raises((ValueError, TypeError)):
134135
ut._parse_size(value)
136+
137+
138+
def test_write_png_mono():
139+
mono_image = [
140+
[0.24309289, 0.75997446, 0.02971671, 0.52830537],
141+
[0.62339252, 0.65369358, 0.41545387, 0.03307279],
142+
]
143+
144+
mono_png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x02\x08\x06\x00\x00\x00\x7f\xa8}c\x00\x00\x00)IDATx\xdac\x08\x0c\x0c\xfc\x0f\x02\x9c\x9c\x9c\xff7n\xdc\xf8\x9f\xe1\xe2\xc5\x8b\xffo\xdf\xbe\xfd\xbf\xbb\xbb\xfb?77\xf7\x7f\x00f\x87\x14\xdd\x0c\r;\xc0\x00\x00\x00\x00IEND\xaeB`\x82" # noqa E501
145+
assert ut.write_png(mono_image) == mono_png
146+
147+
colormap = LinearColormap(colors=["red", "yellow", "green"])
148+
color_png = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x02\x08\x06\x00\x00\x00\x7f\xa8}c\x00\x00\x00)IDATx\xdac\xf8_\xcf\xf0\xbf\xea\x10\xc3\xff\xff\xfc\x0c\xff?\xfcg\xf8\xcfp\xe0\x19\xc3\xff\r\xf7\x80\x02\xb7\x80X\x90\xe1?\x00N\xca\x13\xcd\xfb\xad\r\xb8\x00\x00\x00\x00IEND\xaeB`\x82" # noqa E501
149+
assert ut.write_png(mono_image, colormap=colormap) == color_png
150+
151+
152+
def test_write_png_rgb():
153+
image_rgb = [
154+
[
155+
[0.8952778565195247, 0.6196806506704735, 0.2696137085302287],
156+
[0.3940794236804127, 0.9432178293916365, 0.16500617914697335],
157+
[0.5566755388192485, 0.10469673377265687, 0.27346260130585975],
158+
[0.2029951628162342, 0.5357152681832641, 0.13692921080346832],
159+
],
160+
[
161+
[0.5186482474007286, 0.8625240370164696, 0.6965561989987038],
162+
[0.04425586727957387, 0.45448042432657076, 0.8552600511205423],
163+
[0.696453974598333, 0.7508742900711168, 0.9646572952994652],
164+
[0.7471809029502141, 0.3218907599994758, 0.789193070740859],
165+
],
166+
]
167+
png = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x04\x00\x00\x00\x02\x08\x06\x00\x00\x00\x7f\xa8}c\x00\x00\x00-IDATx\xda\x01"\x00\xdd\xff\x00\xff\xa7G\xffp\xff+\xff\x9e\x1cH\xff9\x90$\xff\x00\x93\xe9\xb8\xff\x0cz\xe2\xff\xc6\xca\xff\xff\xd4W\xd0\xffYw\x15\x95\xcf\xb9@D\x00\x00\x00\x00IEND\xaeB`\x82' # noqa E501
168+
assert ut.write_png(image_rgb) == png

0 commit comments

Comments
 (0)