Yes, I read that in the docs. Or do you mean it happens at the call to cx.add_basemap? Then I’ve no idea :man_shrugging:, but I still think showing us a trace-back would help others here. Answer from jeff5 on discuss.python.org
🌐
Python.org
discuss.python.org › python help
"cannot identify image file <_io.BytesIO object" - Python Help - Discussions on Python.org
November 19, 2023 - Getting the error “cannot identify image file <_io.BytesIO object” , while adding contextily basemaps, The same code worked well till date · I thought I could guess the cause: the call to savefig expects a file name but you expect it to write into your BytesIO.
Top answer
1 of 3
13

First make sure that your GeoDataframe is in Web Mercator projection (epsg=3857). Once your Geodataframe is correctly georeferenced, you can achieve this by Geopandas reprojection:

df = df.to_crs(epsg=3857)

Once you have this done, you easily choose any of the supported map styles. A full list can be found in contextily.sources module, at the time of writing:

### Tile provider sources ###

ST_TONER = 'http://tile.stamen.com/toner/tileZ/tileX/tileY.png'
ST_TONER_HYBRID = 'http://tile.stamen.com/toner-hybrid/tileZ/tileX/tileY.png'
ST_TONER_LABELS = 'http://tile.stamen.com/toner-labels/tileZ/tileX/tileY.png'
ST_TONER_LINES = 'http://tile.stamen.com/toner-lines/tileZ/tileX/tileY.png'
ST_TONER_BACKGROUND = 'http://tile.stamen.com/toner-background/tileZ/tileX/tileY.png'
ST_TONER_LITE = 'http://tile.stamen.com/toner-lite/tileZ/tileX/tileY.png'

ST_TERRAIN = 'http://tile.stamen.com/terrain/tileZ/tileX/tileY.png'
ST_TERRAIN_LABELS = 'http://tile.stamen.com/terrain-labels/tileZ/tileX/tileY.png'
ST_TERRAIN_LINES = 'http://tile.stamen.com/terrain-lines/tileZ/tileX/tileY.png'
ST_TERRAIN_BACKGROUND = 'http://tile.stamen.com/terrain-background/tileZ/tileX/tileY.png'

ST_WATERCOLOR = 'http://tile.stamen.com/watercolor/tileZ/tileX/tileY.png'

# OpenStreetMap as an alternative
OSM_A = 'http://a.tile.openstreetmap.org/tileZ/tileX/tileY.png'
OSM_B = 'http://b.tile.openstreetmap.org/tileZ/tileX/tileY.png'
OSM_C = 'http://c.tile.openstreetmap.org/tileZ/tileX/tileY.png'

Keep in mind that you should not be adding actual x,y,z tile numbers in your tile URL (like you did in your "EDIT" example). ctx will take care of all this.

You can find a working copy-pastable example and further info at GeoPandas docs.

import contextily as ctx

# Dataframe you want to plot
gdf = GeoDataFrame(df, crs= {"init": "epsg:4326"}) # Create a georeferenced dataframe  
gdf = gdf.to_crs(epsg=3857) # reproject it in Web mercator
ax = gdf.plot()

# choose any of the supported maps from ctx.sources
ctx.add_basemap(ax, url=ctx.sources.ST_TERRAIN)
ax.set_axis_off()
plt.show()
2 of 3
6

Contextily's default crs is epsg:3857. However, your data-frame is in different CRS. Use the following,refer the manual here:

ctx.add_basemap(ax, crs='epsg:4326', source=ctx.providers.Stamen.TonerLite)

Please, refer to this link for using different sources such as Stamen.Toner, Stamen.Terrain etc. (Stamen.Terrain is used as default).

Also, you can cast your data frame to EPSG:3857 by using df.to_crs(). In this case, you should skip crs argument inside ctx.add_basemap() function.

🌐
Codebardian
codebardian.github.io › vbb-gtfs › shapes.html
Shapes — VBB GTFS Notebooks
--------------------------------------------------------------------------- UnidentifiedImageError Traceback (most recent call last) <timed exec> in <module> /opt/hostedtoolcache/Python/3.8.10/x64/lib/python3.8/site-packages/contextily/plotting.py in add_basemap(ax, zoom, source, interpolation, attribution, attribution_size, reset_extent, crs, resampling, url, **extra_imshow_args) 141 ) 142 # Download image --> 143 image, extent = bounds2img( 144 left, bottom, right, top, zoom=zoom, source=source, ll=False 145 ) /opt/hostedtoolcache/Python/3.8.10/x64/lib/python3.8/site-packages/contextily/tile
🌐
GeoPandas
geopandas.org › en › v0.14.3 › gallery › plotting_basemap_background.html
Adding a background map to plots — GeoPandas 0.14.3+0.g5558c35.dirty documentation
1870 with self._lock: File ... UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7fdc4287c1d0> This reprojects map tiles to a target CRS which may in some cases cause a loss of sharpness. See contextily’s guide on warping tiles for more ...
🌐
Stack Overflow
stackoverflow.com › questions › tagged › contextily
Newest 'contextily' Questions - Stack Overflow
I've been trying several ways to ... image and none seemed to work. First I tried with contextily. I managed to make it work, but not with satellite data. It seems ... ... I am focusing on visualizing some datasets of Switzerland through 'contextily' package in python. (Here is just the sample. In fact, the whole dataset has 16000 records in Switzerland.) Unnamed: 0 ... ... I've been stuck for a few days trying to plot a TIF file over a basemap ...
🌐
Readthedocs
contextily.readthedocs.io › en › latest › working_with_local_files.html
Working with local files — contextily 1.7.1.dev4+gb1187979f.d20260312 documentation
Note how the crs functionality works just as expected in this context as well. contextily checks the CRS of the local file and, if it is different from that specified in the crs parameter, it warps the image so they align automatically.
🌐
Agile
agilescientific.com › blog › tag › tips
tips — Blog - Agile Scientific
Contextily also plays well with geopandas, allowing for an easy locality map of a given GeoDataFrame. Check out the accompanying Notebook for an example. ... We'd often like to load images into Python. Once loaded, we might want to treat them as images, for example cropping them, saving in ...
🌐
GitHub
github.com › geopandas › contextily › issues › 118
Error in generating Basemap · Issue #118 · geopandas/contextily
February 5, 2020 - Hello, I am having some trouble in generating a basic background map for some points on a map. The points were extracted from Flickr and are by default crs: 4326 and are of Preikestolen, Norway. I am running python3.7 with anaconda my co...
Author   geopandas
Find elsewhere
🌐
GitHub
github.com › python-pillow › Pillow › issues › 4834
Getting `UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x63a807fb0>` · Issue #4834 · python-pillow/Pillow
August 2, 2020 - Getting UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x63a807fb0> while executing this code
Author   python-pillow
🌐
GitHub
github.com › python-pillow › Pillow › issues › 3431
cannot identify image file <_io.BytesIO object · Issue #3431 · python-pillow/Pillow
October 23, 2018 - import requests from PIL import Image try: aux_im = Image.open(requests.get('https://www.bimbaylola.com/media/catalog/product/1/8/182BAC104_T2200_P_T_XX_1.jpg', stream=True).raw) except Exception as e: print(str(e))
Author   python-pillow
🌐
Readthedocs
contextily.readthedocs.io › en › stable › reference.html
Reference Guide — contextily 1.7.1.dev0+g76aa2c91a.d20251124 documentation
contextily.bounds2img(w, s, e, n, zoom='auto', source=None, headers: dict[str, str] | None = None, ll=False, wait=0, max_retries=2, n_connections=1, use_cache=True, zoom_adjust=None)# Take bounding box and zoom and return an image with all the tiles that compose the map and its Spherical Mercator extent. ... [Optional. Default: OpenStreetMap Humanitarian web tiles] The tile source: web tile provider or path to local file.
🌐
Darribas
darribas.org › gds19 › content › labs › lab_03.html
Mapping in Python with geopandas
Note that we need to be explicit when adding the basemap to state the coordinate reference system (crs) our data is expressed in, contextily will not be able to pick it up otherwise.
🌐
Readthedocs
contextily.readthedocs.io › en › latest › reference.html
Reference Guide — contextily 1.7.1.dev9+ge0f97f6b5.d20260622 documentation
contextily.bounds2img(w, s, e, n, zoom='auto', source=None, headers: dict[str, str] | None = None, ll=False, wait=0, max_retries=2, n_connections=1, use_cache=True, zoom_adjust=None, timeout=None)# Take bounding box and zoom and return an image with all the tiles that compose the map and its Spherical Mercator extent. ... [Optional. Default: OpenStreetMap Humanitarian web tiles] The tile source: web tile provider or path to local file.
🌐
Stack Exchange
gis.stackexchange.com › questions › 378067 › unable-to-get-the-correct-tiles-using-bounds2img-via-contextily-and-geopandas
coordinate system - Unable to get the correct tiles using `bounds2img` via contextily and geopandas for USA - Geographic Information Systems Stack Exchange
October 31, 2020 - Getting the tiles using inbuilt methods To get the basemap image I looked at the documentation of contextily and found that bound2img method may help me get what I want so I tried it this way -
🌐
GeoPandas
geopandas.org › en › stable › gallery › plotting_basemap_background.html
Adding a background map to plots — GeoPandas 1.1.4+0.g91ec4af.dirty documentation
--------------------------------------------------------------------------- HTTPError Traceback (most recent call last) File ~/checkouts/readthedocs.org/user_builds/geopandas/conda/stable/lib/python3.13/site-packages/contextily/tile.py:470, in _retryer(tile_url, wait, max_retries, headers) 469 request = requests.get(tile_url, headers={"user-agent": USER_AGENT, **headers}) --> 470 request.raise_for_status() 471 with io.BytesIO(request.content) as image_stream: File ~/checkouts/readthedocs.org/user_builds/geopandas/conda/stable/lib/python3.13/site-packages/requests/models.py:1167, in Response.ra
🌐
Bytemeta
bytemeta.vip › repo › geopandas › contextily › issues › 58
OSError: cannot identify image file - bytemeta
I checked add basemap after reinstalling contextily as you suggested, the built in function works!
Top answer
1 of 2
1

I'm not sure what's going on here. I have reproduced your code, which generates a .tif file I can open fine on QGIS, no black. I can also plot the file directly with rasterio:

import rasterio
from rasterio.plotting import show as rioshow
rioshow(rasterio.open("C:\\Temp\\topo.tif"))

And that plots the portion of the map (this is all on the latest 1.0.0 version and rasterio 1.1.5, which actually handles all the I/O).

More generally, contextily only currently works with tiles in Web Mercator (EPSG:3857). There is an issue here discussion potential extensions but these are at the idea level currently:

https://github.com/geopandas/contextily/issues/119

Note however that you can use the square trick you have above, together with ctx.add_basemap to retrieve the map, but this would be for display only, we currently have no functionality to query with a different CRS and write to disk in a GeoTif format. That would be a cool feature to have and should not be too hard to do (basically hook up the CRS option in add_basemap to the bounds2X methods), but it does not exist currently (PRs welcome!).

An alternative is to convert your 27700 coords into Web Mercator, use img2raster to write the Tiff in Web Mercator and then reproject either with QGIS, GDAL, or rio transform

2 of 2
1

A further update, although I can't be certain I have a hunch the problem related to the GDAL install. Specifically the Windows Environment Variables: GDAL_DATA, GDAL_DRIVER_PATH, GDAL_VERSION.

Since I had multiple python environments, python 3.7x32, 3.7x64, 3.8x32 I therefore have multiple GDAL Env variables. Perhaps when running a 3.7x64 script, the first GDAL variable it found in the registry was the 3.7x32 version pointing to the wrong GDAL resources- creating the error.

Trying to get contextily working involved installing GDAL windows msi installers on www.gisinternals.com. Before using those I had a lot of visual studio errors CL.exe compiling errors when installing gdal via pip install. But after installing loads of versions from there I created more problems for myself.

SOLUTION:

  1. I uninstalled all python environments
  2. deleted all GDAL registry entries
  3. deleted all python IDLE context menu registry entries
  4. Installed GDAL 3.1.1 from a wheel from the unofficial windows binaries
  5. Installed rasterio 1.15 from wheel
  6. Fiona from wheel
  7. pip install geopandas (as I need to combine with vector data too)
  8. pip install contextily

and then contextily worked on 3.7x64!

My GDAL Env Variables look like this:

My Path Env Variables look like this:

And PYTHONPATH:

🌐
Readthedocs
contextily.readthedocs.io › en › latest › intro_guide.html
Introduction guide to contextily — contextily 1.7.1.dev9+ge0f97f6b5.d20260622 documentation
%matplotlib inline import contextily as cx import geopandas import rasterio from rasterio.plot import show as rioshow import matplotlib.pyplot as plt from geodatasets import get_path