Repository: SciTools/cartopy

Issue:
imshow don't work with RGBA image when transform keyword is provided
### Description
Hi,

When using imshow with RGBA data, and with **no transformation** (*PlateCarre* data to *PlateCarre* axis for example), there is no problem.

However, **when using transformation** (for example, *PlateCarre* data to *Orthographic* axis), the A channel from RGBA data is skipped.


#### Code to reproduce and minimal example

```python
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
np.random.seed(0)

fig = plt.figure(figsize=(8,5), dpi=120)
ax1 = fig.add_subplot(2, 2, 1, projection=ccrs.Orthographic(-10, 45))
ax2 = fig.add_subplot(2, 2, 2, projection=ccrs.Orthographic(-10, 45))
ax3 = fig.add_subplot(2, 2, 3, projection=ccrs.PlateCarree())
ax4 = fig.add_subplot(2, 2, 4, projection=ccrs.PlateCarree())

dy, dx = (9,18)
extent = [-180, 180, -90, 90]

# create RGB Image with random data
img1 = np.random.randint(0, 255, size=(dy, dx, 3), dtype=np.uint8)
for ax in ax1, ax3:
    ax.imshow(img1, extent=extent, transform=ccrs.PlateCarree())
    ax.set_title("no alpha (RGB)")

# Create RGBA Image with random data and linspace alpha
img2 = np.zeros((dy, dx, 4), dtype=np.uint8)
img2[:,:,:3] = img1
img2[:,:,3] = np.linspace(0, 255, dx, dtype=np.uint8).reshape(1,dx)

for ax in ax2, ax4:
    ax.imshow(img2, extent=extent, transform=ccrs.PlateCarree())
    ax.set_title("with alpha (RGBA)")

fig.suptitle("Problem imshow with alpha")
fig
```
![image](https://user-images.githubusercontent.com/49512274/135983862-6355df6a-07ba-4d52-b949-93f7936e14d4.png)


With cartopy==0.18 there is no problems : 

![image](https://user-images.githubusercontent.com/49512274/135983993-ee4d2002-363d-42f2-b5ce-1a82bd73514d.png)


I noticed this problem using cartopy 0.19.x and 0.20.x . I suspect that #1582 introduced this problem.


after reading commit changes, i noticed i did not use alpha kwargs, so i tried it : 

```python
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
np.random.seed(0)

fig = plt.figure(figsize=(12,5), dpi=120)
ax1 = fig.add_subplot(2, 3, 1, projection=ccrs.Orthographic(120, 45))
ax2 = fig.add_subplot(2, 3, 2, projection=ccrs.Orthographic(120, 45))
ax3 = fig.add_subplot(2, 3, 3, projection=ccrs.Orthographic(120, 45))
ax4 = fig.add_subplot(2, 3, 4, projection=ccrs.PlateCarree())
ax5 = fig.add_subplot(2, 3, 5, projection=ccrs.PlateCarree())
ax6 = fig.add_subplot(2, 3, 6, projection=ccrs.PlateCarree())

dy, dx = (9,18)
extent = [-180, 180, -90, 90]

# create RGB Image with random data
img1 = np.random.randint(0, 255, size=(dy, dx, 3), dtype=np.uint8)
for ax in ax1, ax4:
    ax.imshow(img1, extent=extent, transform=ccrs.PlateCarree())
    ax.set_title("no alpha (RGB)")

# Create RGBA Image with random data and linspace alpha
img2 = np.zeros((dy, dx, 4), dtype=np.uint8)
img2[:,:,:3] = img1
img2[:,:,3] = np.linspace(0, 255, dx, dtype=np.uint8).reshape(1,dx)

for ax in ax2, ax5:
    ax.imshow(img2, extent=extent, transform=ccrs.PlateCarree())
    ax.set_title("with alpha (RGBA)")

    
for ax in ax3, ax6:
    # converting 0...255 values to 0...1 float, or exception is raised
    ax.imshow(img2, alpha=img2[:,:,3]/255, extent=extent, transform=ccrs.PlateCarree())
    ax.set_title("with alpha (RGBA) and alpha kwargs")

fig.suptitle("Problem imshow with alpha")
fig
```

This gives interesting results. Only the data where  alpha=1 is plotted, others are skipped

![image](https://user-images.githubusercontent.com/49512274/135984112-5d430e9b-7397-4122-bb92-43cd552e8586.png)


I'll continue investing next days

<details>
 <summary>Full environment definition</summary>

<!-- fill in the following information as appropriate -->



### Cartopy version

```python
import cartopy
import matplotlib

print("cartopy    : ", cartopy.__version__)
print("matplotlib : ", matplotlib.__version__)
```

```bash
cartopy    :  0.19.0
matplotlib :  3.4.3
```

</details>



Discussion:
Indeed, I think you're right that the original alpha channel is getting dropped along the way. Some extra bookkeeping probably needs to be added in that code path to handle 1) the original alpha channel in the image, 2) a potential masked array, 3) the alpha keyword argument. We may want to raise/warn if multiple of these combinations come in together too.

Resolving patch:
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
@@ -1332,24 +1332,25 @@ def imshow(self, img, *args, **kwargs):
             kwargs['alpha'] = alpha
 
             # As a workaround to a matplotlib limitation, turn any images
-            # which are RGB(A) with a mask into unmasked RGBA images with alpha
-            # put into the A channel.
+            # which are masked array RGB(A) into RGBA images
+
             if np.ma.is_masked(img) and len(img.shape) > 2:
-                # if we don't pop alpha, imshow will apply (erroneously?) a
-                # 1D alpha to the RGBA array
-                # kwargs['alpha'] is guaranteed to be either 1D, 2D, or None
-                alpha = kwargs.pop('alpha')
-                old_img = img[:, :, 0:3]
-                img = np.zeros(img.shape[:2] + (4, ), dtype=img.dtype)
-                img[:, :, 0:3] = old_img
-                # Put an alpha channel in if the image was masked.
-                if not np.any(alpha):
-                    alpha = 1
-                img[:, :, 3] = np.ma.filled(alpha, fill_value=0) * \
-                    (~np.any(old_img.mask, axis=2))
-                if img.dtype.kind == 'u':
+
+                # transform RGB(A) into RGBA
+                old_img = img
+                img = np.ones(old_img.shape[:2] + (4, ),
+                              dtype=old_img.dtype)
+                img[:, :, :3] = old_img[:, :, :3]
+
+                # if img is RGBA, save alpha channel
+                if old_img.shape[-1] == 4:
+                    img[:, :, 3] = old_img[:, :, 3]
+                elif img.dtype.kind == 'u':
                     img[:, :, 3] *= 255
 
+                # apply the mask to the A channel
+                img[np.any(old_img[:, :, :3].mask, axis=2), 3] = 0
+
             result = super().imshow(img, *args, extent=extent, **kwargs)
 
         return result


Test patch:
diff --git a/lib/cartopy/tests/mpl/test_images.py b/lib/cartopy/tests/mpl/test_images.py
--- a/lib/cartopy/tests/mpl/test_images.py
+++ b/lib/cartopy/tests/mpl/test_images.py
@@ -154,6 +154,22 @@ def test_imshow_rgba():
     assert sum(img.get_array().data[:, 0, 3]) == 0
 
 
+def test_imshow_rgba_alpha():
+    # test that alpha channel from RGBA is not skipped
+    dy, dx = (3, 4)
+
+    ax = plt.axes(projection=ccrs.Orthographic(-120, 45))
+
+    # Create RGBA Image with random data and linspace alpha
+    RGBA = np.linspace(0, 255*31, dx*dy*4, dtype=np.uint8).reshape((dy, dx, 4))
+
+    alpha = np.array([0, 85, 170, 255])
+    RGBA[:, :, 3] = alpha
+
+    img = ax.imshow(RGBA, transform=ccrs.PlateCarree())
+    assert np.all(np.unique(img.get_array().data[:, :, 3]) == alpha)
+
+
 def test_imshow_rgb():
     # tests that the alpha of a RGB array passed to imshow is set to 0
     # instead of masked