Skip to content

Commit d3edd6f

Browse files
committed
Merge pull request #180 from andrewgiessel/image_overlay_tweaks
Image overlay tweaks
2 parents c3cc01c + fd855a2 commit d3edd6f

File tree

3 files changed

+83
-18
lines changed

3 files changed

+83
-18
lines changed

folium/folium.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -952,16 +952,23 @@ def json_style(style_cnt, line_color, line_weight, line_opacity,
952952

953953
@iter_obj('image_overlay')
954954
def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
955-
min_lon=-180.0, max_lon=180.0, image_name=None, filename=None):
956-
"""Simple image overlay of raster data from a numpy array. This is a lightweight
957-
way to overlay geospatial data on top of a map. If your data is high res, consider
958-
implementing a WMS server and adding a WMS layer.
959-
960-
This function works by generating a PNG file from a numpy array. If you do not
961-
specifiy a filename, it will embed the image inline. Otherwise, it saves the file in the
962-
current directory, and then adds it as an image overlay layer in leaflet.js.
963-
By default, the image is placed and stretched using bounds that cover the
964-
entire globe.
955+
min_lon=-180.0, max_lon=180.0, image_name=None, filename=None,
956+
data_projection='mercator'):
957+
"""Simple image overlay of raster data from a numpy array. This is a
958+
lightweight way to overlay geospatial data on top of a map.
959+
If your data is high res, consider implementing a WMS server
960+
and adding a WMS layer.
961+
962+
This function works by generating a PNG file from a numpy
963+
array. If you do not specifiy a filename, it will embed the
964+
image inline. Otherwise, it saves the file in the current
965+
directory, and then adds it as an image overlay layer in
966+
leaflet.js. By default, the image is placed and stretched
967+
using bounds that cover the entire globe. By default, we
968+
assume that your data is in geodetic projection and thus
969+
project it to web mercator for display purposes. If you are
970+
overlaying a non-georeferenced image, set data_projection to
971+
None.
965972
966973
Parameters
967974
----------
@@ -976,10 +983,12 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
976983
max_lon: float, default 180.0
977984
image_name: string, default None
978985
The name of the layer object in leaflet.js
979-
filename: string, default None
986+
filename: string or None, default None
980987
Optional file name of output.png for image overlay. If None, we use a
981988
inline PNG.
982-
989+
data_projection: string or None, default 'mercator'
990+
Used to specify projection of image. If None, do no projection
991+
983992
Output
984993
------
985994
Image overlay data layer in obj.template_vars
@@ -988,7 +997,7 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
988997
-------
989998
# assumes a map object `m` has been created
990999
>>> import numpy as np
991-
>>> data = np.random.random((100,100))
1000+
>>> data = np.random.random((180,360))
9921001
9931002
# to make a rgba from a specific matplotlib colormap:
9941003
>>> import matplotlib.cm as cm
@@ -1000,12 +1009,16 @@ def image_overlay(self, data, opacity=0.25, min_lat=-90.0, max_lat=90.0,
10001009
10011010
# put it only over a single city (Paris)
10021011
>>> m.image_overlay(data, min_lat=48.80418, max_lat=48.90970, min_lon=2.25214, max_lon=2.44731)
1003-
1004-
"""
10051012
1013+
"""
10061014
if isinstance(data, str):
10071015
filename = data
10081016
else:
1017+
assert data_projection in [None, 'mercator']
1018+
# this assumes a lat x long array
1019+
# with 2x as many points in long as lat dims.
1020+
if data_projection is 'mercator':
1021+
data = utilities.geodetic_to_mercator(data)
10091022
try:
10101023
png_str = utilities.write_png(data)
10111024
except Exception as e:

folium/utilities.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,50 @@ def png_pack(png_tag, data):
344344
png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
345345
png_pack(b'IDAT', zlib.compress(raw_data, 9)),
346346
png_pack(b'IEND', b'')])
347+
348+
def _row2lat(row):
349+
return 180.0/np.pi*(2.0*np.arctan(np.exp(row*np.pi/180.0))-np.pi/2.0)
350+
351+
def geodetic_to_mercator(geodetic):
352+
"""This function takes an 2D array in geodetic coordinates (ie: lat x
353+
lon unprojected) and converts it to web mercator. This is needed
354+
to correctly overlay an image on a leaflet map, which uses web
355+
mercator by default.
356+
357+
This code works with arrays that match the relative proportions of
358+
latitude and longitude of the earth, meaning that they have twice
359+
as many points in longitude as latitude (i.e.: (180, 360) for 1
360+
degree resolution).
361+
362+
The code for this is from:
363+
http://stackoverflow.com/questions/25058880/convert-to-web-mercator-with-numpy
364+
365+
Parameters
366+
----------
367+
geodetic: numpy image array
368+
Latitude x Longitude array, in mono (NxM), rgb (NxMx3) or RGBA (NxMx4)
369+
370+
Returns
371+
-------
372+
meractor: projected numpy image array
373+
374+
"""
375+
376+
geo = np.repeat(np.atleast_3d(geodetic), 2, axis=0)
377+
merc = np.zeros_like(geo)
378+
side = geo.shape[0]
379+
380+
for row in range(side):
381+
lat = _row2lat(180 - ((row * 1.0)/side) * 360)
382+
g_row = (abs(90 - lat)/180)*side
383+
fraction = g_row-np.floor(g_row)
384+
385+
# Here I optimized the code by using the numpy vector operations
386+
# instead of the for loop:
387+
388+
high_row = geo[np.floor(g_row), :, :] * (fraction)
389+
low_row = geo[np.ceil(g_row), :, :] * (1-fraction)
390+
merc[row, :, :] = high_row + low_row
391+
392+
return np.squeeze(merc)
347393

tests/folium_tests.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -484,21 +484,26 @@ def test_fit_bounds(self):
484484
def test_image_overlay(self):
485485
"""Test image overlay"""
486486
from numpy.random import random
487-
from folium.utilities import write_png
487+
from folium.utilities import write_png, geodetic_to_mercator
488488
import base64
489489

490490
data = random((100,100))
491491
png_str = write_png(data)
492492
with open('data.png', 'wb') as f:
493493
f.write(png_str)
494-
inline_image_url = "data:image/png;base64,"+base64.b64encode(png_str).decode('utf-8')
494+
495+
image_url = 'data.png'
496+
inline_image_url = ("data:image/png;base64," +
497+
base64.b64encode(write_png(geodetic_to_mercator(data))).decode('utf-8'))
495498

496499
image_tpl = self.env.get_template('image_layer.js')
497500
image_name = 'Image_Overlay'
498501
image_opacity = 0.25
499-
image_url = 'data.png'
502+
500503
min_lon, max_lon, min_lat, max_lat = -90.0, 90.0, -180.0, 180.0
501504
image_bounds = [[min_lon, min_lat], [max_lon, max_lat]]
505+
506+
# Test the external png.
502507

503508
image_rendered = image_tpl.render({'image_name': image_name,
504509
'image_url': image_url,
@@ -509,6 +514,7 @@ def test_image_overlay(self):
509514
self.map.image_overlay(data, filename=image_url)
510515
assert image_rendered in self.map.template_vars['image_layers']
511516

517+
# Test the inline png.
512518

513519
image_rendered = image_tpl.render({'image_name': image_name,
514520
'image_url': inline_image_url,

0 commit comments

Comments
 (0)