Repository: SciTools/cartopy

Issue:
SciPy dependency
### Description
<!-- Please provide a general introduction to the issue/proposal. -->

Currently we unconditionally import scipy in two places:

https://github.com/SciTools/cartopy/blob/d12c86ca228365c61d5fb8b7121b49201f19fea8/lib/cartopy/img_transform.py#L13-L18

(when no pykdtree) and here:

https://github.com/SciTools/cartopy/blob/d59d275045f4aac42d3665f3532e0ed9cc3cdbf4/lib/cartopy/vector_transform.py#L12-L14

That would contradict where we list scipy as an "optional" dependency. We should either update the docs in [`INSTALL`](https://github.com/SciTools/cartopy/blob/a3ce915cdcd245e99d971ff707d83912f8aa21f1/INSTALL) or handle those imports differently.


Discussion:
Ran into this issue today. Followed the installation guide and thought I installed all deps. Tried running the `animate_surface.ipynb` notebook, but failed, because I have no pykdtree nor scipy installed
We also list matplotlib as an optional dependency, which seems a bit odd :) So, that should probably be updated as well.

Resolving patch:
diff --git a/lib/cartopy/img_transform.py b/lib/cartopy/img_transform.py
--- a/lib/cartopy/img_transform.py
+++ b/lib/cartopy/img_transform.py
@@ -14,7 +14,11 @@
     import pykdtree.kdtree
     _is_pykdtree = True
 except ImportError:
-    import scipy.spatial
+    try:
+        import scipy.spatial
+    except ImportError as e:
+        raise ImportError("Using image transforms requires either "
+                          "pykdtree or scipy.") from e
     _is_pykdtree = False
 
 import cartopy.crs as ccrs
diff --git a/lib/cartopy/mpl/geoaxes.py b/lib/cartopy/mpl/geoaxes.py
--- a/lib/cartopy/mpl/geoaxes.py
+++ b/lib/cartopy/mpl/geoaxes.py
@@ -34,13 +34,11 @@
 from cartopy import config
 import cartopy.crs as ccrs
 import cartopy.feature
-import cartopy.img_transform
 import cartopy.mpl.contour
 import cartopy.mpl.geocollection
 import cartopy.mpl.feature_artist as feature_artist
 import cartopy.mpl.patch as cpatch
 from cartopy.mpl.slippy_image_artist import SlippyImageArtist
-from cartopy.vector_transform import vector_scalar_to_grid
 
 
 assert mpl.__version__ >= '3.1', \
@@ -1371,7 +1369,9 @@ def imshow(self, img, *args, **kwargs):
             regrid_shape = kwargs.pop('regrid_shape', 750)
             regrid_shape = self._regrid_shape_aspect(regrid_shape,
                                                      target_extent)
-            warp_array = cartopy.img_transform.warp_array
+            # Lazy import because scipy/pykdtree in img_transform are only
+            # optional dependencies
+            from cartopy.img_transform import warp_array
             original_extent = extent
             img, extent = warp_array(img,
                                      source_proj=transform,
@@ -2073,6 +2073,9 @@ def quiver(self, x, y, u, v, *args, **kwargs):
             # manually and plotting in native coordinates.
             regrid_shape = self._regrid_shape_aspect(regrid_shape,
                                                      target_extent)
+            # Lazy load vector_scalar_to_grid due to the optional
+            # scipy dependency
+            from cartopy.vector_transform import vector_scalar_to_grid
             if args:
                 # Interpolate color array as well as vector components.
                 x, y, u, v, c = vector_scalar_to_grid(
@@ -2145,6 +2148,9 @@ def barbs(self, x, y, u, v, *args, **kwargs):
             # manually and plotting in native coordinates.
             regrid_shape = self._regrid_shape_aspect(regrid_shape,
                                                      target_extent)
+            # Lazy load vector_scalar_to_grid due to the optional
+            # scipy dependency
+            from cartopy.vector_transform import vector_scalar_to_grid
             if args:
                 # Interpolate color array as well as vector components.
                 x, y, u, v, c = vector_scalar_to_grid(
@@ -2218,6 +2224,9 @@ def streamplot(self, x, y, u, v, **kwargs):
             scalars.append(lw)
         # Do the regridding including any scalar fields.
         target_extent = self.get_extent(self.projection)
+        # Lazy load vector_scalar_to_grid due to the optional
+        # scipy dependency
+        from cartopy.vector_transform import vector_scalar_to_grid
         gridded = vector_scalar_to_grid(t, self.projection, regrid_shape,
                                         x, y, u, v, *scalars,
                                         target_extent=target_extent)
diff --git a/lib/cartopy/vector_transform.py b/lib/cartopy/vector_transform.py
--- a/lib/cartopy/vector_transform.py
+++ b/lib/cartopy/vector_transform.py
@@ -10,7 +10,10 @@
 """
 
 import numpy as np
-from scipy.interpolate import griddata
+try:
+    from scipy.interpolate import griddata
+except ImportError as e:
+    raise ImportError("Regridding vectors requires scipy.") from e
 
 
 def _interpolate_to_grid(nx, ny, x, y, *scalars, **kwargs):


Test patch: