folium icon indicating copy to clipboard operation
folium copied to clipboard

Issue using branca colormap with folium.raster_layers.ImageOverlay

Open ipritchard opened this issue 3 years ago • 4 comments

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.

ipritchard avatar Feb 17 '22 17:02 ipritchard

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).

hydroEng avatar Nov 07 '22 10:11 hydroEng

@suryaya I'm afraid not 😞 I think I just bailed on my original approach and went a different direction.

ipritchard avatar Nov 07 '22 14:11 ipritchard

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

hydroEng avatar Nov 08 '22 08:11 hydroEng

Possibly related to (or a duplicate of) https://github.com/python-visualization/folium/issues/1280?

Conengmo avatar Nov 30 '22 14:11 Conengmo

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)

mgovorcin avatar Dec 22 '22 20:12 mgovorcin

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.

hydroEng avatar Jan 22 '23 07:01 hydroEng

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.

Conengmo avatar Jan 22 '23 13:01 Conengmo

Fixes are up in https://github.com/python-visualization/branca/pull/126 and https://github.com/python-visualization/folium/pull/1708.

Conengmo avatar Jan 22 '23 14:01 Conengmo

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.

hydroEng avatar Jan 22 '23 19:01 hydroEng

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.

Conengmo avatar Jan 22 '23 20:01 Conengmo

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.)

hydroEng avatar Jan 22 '23 21:01 hydroEng

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.

Conengmo avatar Jan 23 '23 10:01 Conengmo