Issue using branca colormap with folium.raster_layers.ImageOverlay
Describe the bug Exhausted my troubleshooting - I believe this is in fact a bug, hopefully I'm not wrong!
I think there is a bug attempting to use a branca colormap (of type branca.colormap.LinearColormap) with an ImageOverlay raster layer. Here is the error I receive when running the code below:
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_26316/1517857374.py in <module>
13
14 # Add the raster overlay to the map
---> 15 image = folium.raster_layers.ImageOverlay(
16 name="Random Image",
17 image = rand_img,
c:\users\ianmp\git_repos\ce-api-endpoint-tests\venv\lib\site-packages\folium\raster_layers.py in __init__(self, image, bounds, origin, colormap, mercator_project, pixelated, name, overlay, control, show, **kwargs)
258 )
259
--> 260 self.url = image_to_url(image, origin=origin, colormap=colormap)
261
262 def render(self, **kwargs):
c:\users\ianmp\git_repos\ce-api-endpoint-tests\venv\lib\site-packages\folium\utilities.py in image_to_url(image, colormap, origin)
137 url = 'data:image/{};base64,{}'.format(fileformat, b64encoded)
138 elif 'ndarray' in image.__class__.__name__:
--> 139 img = write_png(image, origin=origin, colormap=colormap)
140 b64encoded = base64.b64encode(img).decode('utf-8')
141 url = 'data:image/png;base64,{}'.format(b64encoded)
c:\users\ianmp\git_repos\ce-api-endpoint-tests\venv\lib\site-packages\folium\utilities.py in write_png(data, origin, colormap)
200 if nblayers == 1:
201 arr = np.array(list(map(colormap, arr.ravel())))
--> 202 nblayers = arr.shape[1]
203 if nblayers not in [3, 4]:
204 raise ValueError('colormap must provide colors of r'
IndexError: tuple index out of range
To Reproduce
Below is a self-contained code snippet which reproduces the issue. I have substituted in a random numpy array (of a particular size) to replace a raster image I was using so that you can easily reproduce this. The error is the same using either the raster or the random array.
import branca
import branca.colormap as cm
import folium
from folium import plugins
import numpy as np
# Create an array of random values to simulate a raster image array
rand_img = np.random.rand(333, 443)
# Set the image bounds (WGS84 lon/lats)
bbox = [-128.5831, 31.3986, -112.7140, 43.0319]
# Create a folium map object
m = folium.Map(location=[39.3, -118.4], zoom_start=5, height=500)
# Define a Branca colormap for the colorbar
vmin = 0.2
vmax = 0.8
palette = ['red', 'orange', 'yellow', 'cyan', 'blue', 'darkblue'][::-1]
cmap = cm.LinearColormap(colors=palette,
vmin=vmin,
vmax=vmax,
caption='Image Colormap')
# Add the raster overlay to the map
image = folium.raster_layers.ImageOverlay(
name="Random Image",
image = rand_img,
bounds=[[bbox[1], bbox[0]], [bbox[3], bbox[2]]],
interactive=True,
colormap=cmap,
overlay=True)
image.add_to(m)
# Add a layer control panel to the map
m.add_child(folium.LayerControl())
m.add_child(cmap)
# Add fullscreen button
plugins.Fullscreen().add_to(m)
# Display the map
display(m)
Expected behavior I expected the image to appear on the folium map, with a color palette as specified (dark blue values for 0.2 and below, red values for 0.8 and above, and other values linearly spaced in between).
Environment (please complete the following information):
- Jupyter Notebook
- Python version 3.9.5
- folium version 0.12.1.post1 (also experienced on 0.12.1)
- branca version 0.4.2
Additional context
If you remove the colormap from the folium.raster_layers.ImageOverlay() call, the image plots just fine on the map, and the colormap will appear as a colorbar, but the image will just be in greyscale.
Possible solutions No solutions yet, but I think the error lies in the line identified in the stack trace (l212 in utilities.py).
If nblayers is already known to be 1 (mono/singleband image, i.e. a NxM array), I am not sure why it grabs the array shape (which at this point is single-dimensional) and tries to reset the nblayers value.
Did you find a way around it? I am kind of using a workaround by using a matplotlib listed colormap, and using branca to write a separate colorbar using the same matplotlib colors. It is working so far but I can't pass vmin and vmax arguments to the matplotlib colormap (so now I need to find a way to normalize my raster).
@suryaya I'm afraid not 😞 I think I just bailed on my original approach and went a different direction.
OK, well it turns out you can colorize the raster before plotting it via folium avoiding the whole problem. For the sake of anyone reading this, I used this guide, it was very helpful (but I used nanmin/nanmax to normalize):
https://www.linkedin.com/pulse/visualize-dem-interactive-map-chonghua-yin/?trk=related_artice_Visualize%20DEM%20in%20An%20Interactive%20Map_article-card_title
Possibly related to (or a duplicate of) https://github.com/python-visualization/folium/issues/1280?
I resolved this issue in this way: cmap = branca.colormap.LinearColormap(cm.magma_r.colors, vmin=0.3, vmax=1)
cmap_func = lambda x: ImageColor.getcolor(cmap(x), 'RGBA') if ~np.isnan(x) else (0,0,0,0)
folium.raster_layers.ImageOverlay(numpy_array, opacity=0.6, bounds=map_bounds colormap=cmap_func).add_to(map)
At first glance, seems this line arr = np.array(list(map(colormap, arr.ravel()))) (l252 in utilities.py) is returning a 1d array though write_png() is looking for an array with 2 columns (arr.shape[1]). I am not sure if this is an issue with the branca colormap or the logic of write_png().
Happy to work on this but guidance welcome on where I should focus on.
Thanks for pointing that out @suryaya. I looked at what happens at that line to be able to provide some guidance, and while doing that I think I found the issue.
At that place colormap is used as a callable. That means for the case of the branca ColorMap subclasses, we call the __call__ method of ColorMap. That method returns a hex string. That has always been the case, since the first version. https://github.com/python-visualization/branca/blob/482221c1dc5a4b66443a94af0fda91eb529ddef9/branca/colormap.py#L332
Then in folium utilities.py we expect that call to return a tuple of length 3 or 4 with values between 0 and 1. That's clearly a mismatch. It seems this has been this way since that code got there: https://github.com/python-visualization/folium/pull/697/files#diff-a5304c77596c485e3bd5487400b4f052ad495d40da698ac038a6757996f1ca7eR199. So maybe this never worked and/or was never intended to work.
A fix is to use ColorMap.rgba_floats_tuple instead. I'll open a PR, maybe you can verify that that change indeed solves your issue @suryaya?
Update: branca and folium have duplicate functions here. Let's make this fix in branca and remove the functions from folium, use the branca versions only.
Fixes are up in https://github.com/python-visualization/branca/pull/126 and https://github.com/python-visualization/folium/pull/1708.
Okay, I'll give this a test on a few maps and will report back soon! I was wondering why we were getting hex strings when using the colormap but the lambda function where a colormap was not passed was providing a list of length 4 tuples as you said was expected.
Thanks!
about your question, I think that’s because this function was created with Matplotlib colormaps in mind. I suspect it was never intended to work with branca colormaps. Because these two packages use the same name, ‘color map’, this has lead to confusion.
But with that fix it will support branca colormaps also.
Thanks!
about your question, I think that’s because this function was created with Matplotlib colormaps in mind. I suspect it was never intended to work with branca colormaps. Because these two packages use the same name, ‘color map’, this has lead to confusion.
But with that fix it will support branca colormaps also.
Thanks, I've tried your fork and it seems to work fine. I tested it on a .tif of a flood depths raster that I read in as a NxM array using rasterio. I'd provide a screenshot but the raster I am using is from a work project. @ipritchard's code also plots well.
(Weirdly, ImageOverlay.RasterLayer() on the .tif directly (rather than an array) rendered nothing in the html map. I am guessing it's because the file was too big.)
Appreciate you were able to test the change! Good to hear it works now. I'll merge it soon.
After merging this fix is available for those using Branca and Folium from our main git branches. They will also be included in the next releases of both packages: folium > 0.14.0 and branca > 0.6.0, both of which currently have not been planned yet.