SciTools / cartopy

Cartopy - a cartographic python library with matplotlib support
https://scitools.org.uk/cartopy/docs/latest
BSD 3-Clause "New" or "Revised" License
1.41k stars 359 forks source link

Pickling/Unpickling Google Tiles Produces Different Image #1306

Open DominicAntonacci opened 5 years ago

DominicAntonacci commented 5 years ago

Description

Adding Google tiles to a matplotlib figure, pickling it, and then unpickling it produces a different image. It looks like image tiles are loaded from a different region, though the axes limits are the same between plots.

Based off the code in the "Code to reproduce" section, the longitude of points is correct, the the latitude has been shifted upwards.

My specific use case is speeding up plotting data against the same region over and over again, similar to #732 and #1093.

I think this is a cartopy bug and not a matplotlib bug because I can pickle/unpickle a plt.imshow with transforms without any problems.

Code to reproduce

import cartopy.crs as ccrs
from cartopy.io.img_tiles import GoogleTiles
import matplotlib.pyplot as plt
import pickle

fig = plt.figure()
ax = plt.axes(projection=ccrs.Mercator())
ax.set_extent((0, 5, 0, 5))
ax.add_image(GoogleTiles(style='street'), 7)
fig.show()
pickle_string = pickle.dumps(fig)

fig2 = pickle.loads(pickle_string)

fig2.show()  

Figure 1 shows (0, 0) lat/lon while figure 2 shows a section of Mali around (19, 0) lat/lon.

Full environment definition ### Operating system Windows 10 64 bit ### Cartopy version 0.17 ### conda list ``` # Name Version Build Channel alabaster 0.7.12 py36_0 asn1crypto 0.24.0 py36_0 astroid 2.1.0 py36_0 atomicwrites 1.2.1 pypi_0 pypi attrs 18.2.0 pypi_0 pypi babel 2.6.0 py36_0 backcall 0.1.0 py36_0 bitarray 0.8.3 py36hfa6e2cd_0 blas 1.0 mkl bleach 3.1.0 py36_0 ca-certificates 2019.1.23 0 cartopy 0.17.0 py36h5ae9855_1 certifi 2019.3.9 py36_0 cffi 1.12.1 py36h7a1dbc1_0 chardet 3.0.4 py36_1 cloudpickle 0.8.0 py36_0 colorama 0.4.1 py36_0 construct 2.9.45 pypi_0 pypi coverage 4.5.2 pypi_0 pypi cryptography 2.5 py36h7a1dbc1_0 cycler 0.10.0 py36h009560c_0 decorator 4.3.2 py36_0 docutils 0.14 py36h6012d8f_0 entrypoints 0.3 py36_0 freetype 2.9.1 ha9979f8_1 future 0.17.1 py36_0 geos 3.7.1 h33f27b4_0 gitdb 0.6.4 pypi_0 pypi gitpython 0.3.6 pypi_0 pypi icc_rt 2019.0.0 h0cc432a_1 icu 58.2 ha66f8fd_1 idna 2.8 py36_0 imagesize 1.1.0 py36_0 intel-openmp 2019.1 144 ipykernel 5.1.0 py36h39e3cac_0 ipython 7.3.0 py36h39e3cac_0 ipython_genutils 0.2.0 py36h3c5d0ee_0 isort 4.3.8 py36_0 jedi 0.13.3 py36_0 jinja2 2.10 py36_0 jpeg 9b hb83a4c4_2 jsonschema 2.6.0 py36h7636477_0 jupyter_client 5.2.4 py36_0 jupyter_core 4.4.0 py36_0 keyring 18.0.0 py36_0 kiwisolver 1.0.1 py36h6538335_0 lazy-object-proxy 1.3.1 py36hfa6e2cd_2 libiconv 1.15 h1df5818_7 libpng 1.6.36 h2a8f88b_0 libsodium 1.0.16 h9d3ae62_0 libtiff 4.0.10 hb898794_2 libxml2 2.9.9 h464c3ec_0 libxslt 1.1.33 h579f668_0 lxml 4.3.1 py36h1350720_0 markupsafe 1.1.1 py36he774522_0 matplotlib 3.0.2 py36hc8f65d3_0 mccabe 0.6.1 py36_1 mistune 0.8.4 py36he774522_0 mkl 2019.1 144 mkl_fft 1.0.10 py36h14836fe_0 mkl_random 1.0.2 py36h343c172_0 more-itertools 5.0.0 pypi_0 pypi nbconvert 5.3.1 py36_0 nbformat 4.4.0 py36h3a5bc1b_0 numpy 1.15.4 py36h19fb1c0_0 numpy-base 1.15.4 py36hc3f5095_0 numpydoc 0.8.0 py36_0 olefile 0.46 py36_0 openssl 1.1.1b he774522_1 owslib 0.17.0 py36_0 packaging 19.0 py36_0 pandas 0.23.4 pypi_0 pypi pandoc 2.2.3.2 0 pandocfilters 1.4.2 py36_1 parse 1.11.1 pypi_0 pypi parso 0.3.4 py36_0 patsy 0.5.1 py36_0 pickleshare 0.7.5 py36_0 pillow 5.4.1 py36hdc69c19_0 pip 19.0.3 py36_0 pluggy 0.8.1 pypi_0 pypi proj4 5.2.0 ha925a31_1 prompt_toolkit 2.0.9 py36_0 psutil 5.5.0 py36he774522_0 py 1.7.0 pypi_0 pypi pycodestyle 2.5.0 py36_0 pycparser 2.19 py36_0 pyepsg 0.4.0 py36_0 pyflakes 2.1.0 py36_0 pygments 2.3.1 py36_0 pykdtree 1.3.1 py36h8c2d366_2 pylint 2.2.2 py36_0 pyopenssl 19.0.0 py36_0 pyparsing 2.3.1 py36_0 pypi-publisher 0.0.4 pypi_0 pypi pyproj 1.9.6 py36h6782396_0 pyqt 5.9.2 py36h6538335_2 pyserial 3.4 py36_0 pyshp 2.0.1 py36_0 pysocks 1.6.8 py36_0 pytest 4.1.1 pypi_0 pypi pytest-cov 2.6.1 pypi_0 pypi pytest-runner 4.2 pypi_0 pypi python 3.6.8 h9f7ef89_7 python-dateutil 2.8.0 py36_0 python-pptx 0.6.17 pypi_0 pypi pytz 2018.9 py36_0 pywin32 223 py36hfa6e2cd_1 pyyaml 3.13 pypi_0 pypi pyzmq 18.0.0 py36ha925a31_0 qt 5.9.7 vc14h73c81de_0 qtawesome 0.5.6 py_0 qtconsole 4.4.3 py36_0 qtpy 1.6.0 py_0 requests 2.21.0 py36_0 rope 0.12.0 py36_0 scipy 1.2.1 py36h29ff71c_0 seaborn 0.9.0 py36_0 setuptools 40.8.0 py36_0 shapely 1.6.4 py36h222a598_0 sip 4.19.8 py36h6538335_0 six 1.12.0 py36_0 smmap 0.9.0 pypi_0 pypi snowballstemmer 1.2.1 py36h763602f_0 spectrum 0.7.5 pypi_0 pypi sphinx 1.8.4 py36_0 sphinx-rtd-theme 0.4.2 pypi_0 pypi sphinxcontrib 1.0 py36_1 sphinxcontrib-websupport 1.1.0 py36_1 spyder 3.3.3 py36_0 spyder-kernels 0.4.2 py36_0 sqlite 3.26.0 he774522_0 statsmodels 0.9.0 py36h452e1ab_0 testpath 0.4.2 py36_0 tk 8.6.8 hfa6e2cd_0 tornado 5.1.1 py36hfa6e2cd_0 tqdm 4.29.1 pypi_0 pypi traitlets 4.3.2 py36h096827d_0 typed-ast 1.3.1 py36he774522_0 urllib3 1.24.1 py36_0 vc 14.1 h0510ff6_4 vs2015_runtime 14.15.26706 h3a45250_0 wcwidth 0.1.7 py36h3d5aa90_0 webencodings 0.5.1 py36_1 wheel 0.33.1 py36_0 win_inet_pton 1.1.0 py36_0 wincertstore 0.2 py36h7fe50ca_0 wrapt 1.11.1 py36he774522_0 xlsxwriter 1.1.4 pypi_0 pypi xz 5.2.4 h2fa13f4_4 zeromq 4.3.1 h33f27b4_3 zlib 1.2.11 h62dcd97_3 zstd 1.3.7 h508b16e_0 ``` ### pip list ``` ```
wckoeppen commented 5 years ago

I've been exploring this as well, for a similar use case.

I can confirm that pickling and unpickling of geoaxes objects changes the extents, in some cases. My intuition is that pickling isn't correctly maintaining all aspects of the CRS and extent when the out of the box projections are modified.

In the above case, the CRS is ccrs.Mercator.GOOGLE. In my case, I'm using cartopy.crs.Mercator(central_longitude=210).

Is it possible that on unpickling, the projection is assumed to be the associated stock CRS ("Mercator") rather than the modified one? I can't find a way to find which CRS was used from the geoaxes object. The transforms look the same.

wckoeppen commented 5 years ago

Here's as simple as I can get this:

%matplotlib notebook  # because inline can't deserialize

import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import pickle

fig1 = plt.figure()
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180))
ax.set_extent((0, 5, 0, 5))

serialized = pickle.dumps(fig1)
fig2 = pickle.loads(serialized)

print(fig1.axes[0].projection.proj4_params)
print(fig2.axes[0].projection.proj4_params)

Outputs:

{'ellps': 'WGS84', 'a': 57.29577951308232, 'proj': 'eqc', 'lon_0': 180}
{'ellps': 'WGS84', 'a': 57.29577951308232, 'proj': 'eqc', 'lon_0': 0.0}
wckoeppen commented 5 years ago

Last comment for today.

As a workaround, I can get my case to work by explicitly setting the projection to what I know it was after deserialization. E.g., fig2.axes[0].projection = ccrs.PlateCarree(central_longitude=180)

@DominicAntonacci you might try doing the same thing. For example: fig2.axes[0].projection = fig.axes[0].projection or fig2.axes[0].projection = ccrs.Mercator.GOOGLE

I tried to do it, but I think I ended up getting blocked by google by trying it too many times earlier today.