I want to save a 2D numpy array that contains monochrome image data to a png file with matplotlib imsave(), using a colormap as well (viridis). The array contains np.nan values that should be drawn as transparent, which imsave does not handle directly, hence I'm converting the array to RGBA before feeding it to imsave. This works perfectly as long as I leave the origin option on imsave() on the default "upper". If I change it to "lower", matplotlib throws a "ndarray is not C-contiguous" error. (It's coming from the underlying PIL library in fact).
What is going wrong here?
Self contained example:
import numpy as np
from matplotlib import cm
import matplotlib.pyplot as plt
#create test data
image_no_nan = np.eye(100)
image = np.copy(image_no_nan)
image[image == 0] = np.nan
# to map the image to RGBA, scale to [0-255]
colmap = plt.get_cmap("viridis", 256)
lut = (colmap.colors[..., 0:4] * 255).astype(np.uint8)
rescaled = (
(image_no_nan.astype(float) - image_no_nan.min())
* 255
/ (image_no_nan.max() - image_no_nan.min())
).astype(np.uint8)
result = np.zeros((*rescaled.shape, 4), dtype=np.uint8)
# Take entries from RGB LUT according to greyscale values in image
result = np.take(lut, rescaled, axis=0, out=result)
# apply mask
mask = np.zeros((rescaled.shape), dtype=np.uint8)
mask[~np.isnan(image)] = 255
result[:,:,3]= mask
# try fixing the upcoming ndarray is not C-contiguous error
result = result.copy(order="C") # doesn't affect error
result = np.ascontiguousarray(result) # doesn't affect error
print(result.flags) # the ndarray is actually C-contiguous
plt.imsave(fname="test_upper.png", arr=result, format="png", origin="upper")# no problem
plt.imsave(fname="test_lower.png", arr=result, format="png", origin="lower")# error
This looks like a bug in matplotlib to me.
Here are the conditions needed to trigger it:
Here's a minimal reproduction case:
import numpy as np
import matplotlib.pyplot as plt
result = np.zeros((100, 100, 4), dtype='uint8')
print(result.flags) # the ndarray is actually C-contiguous
plt.imsave(fname="test_upper.png", arr=result, format="png", origin="upper")# no problem
plt.imsave(fname="test_lower.png", arr=result, format="png", origin="lower")# error
Investigating matplotlib's code, I find this:
if origin == "lower":
arr = arr[::-1]
if (isinstance(arr, memoryview) and arr.format == "B"
and arr.ndim == 3 and arr.shape[-1] == 4):
# Such an ``arr`` would also be handled fine by sm.to_rgba below
# (after casting with asarray), but it is useful to special-case it
# because that's what backend_agg passes, and can be in fact used
# as is, saving a few operations.
rgba = arr
else:
sm = cm.ScalarMappable(cmap=cmap)
sm.set_clim(vmin, vmax)
rgba = sm.to_rgba(arr, bytes=True)
if pil_kwargs is None:
pil_kwargs = {}
else:
# we modify this below, so make a copy (don't modify caller's dict)
pil_kwargs = pil_kwargs.copy()
pil_shape = (rgba.shape[1], rgba.shape[0])
image = PIL.Image.frombuffer(
"RGBA", pil_shape, rgba, "raw", "RGBA", 0, 1)
If origin == "lower"
, then the array is reversed in a zero-copy fashion. If this happens, then arr
is no longer C contiguous. It then uses ScalarMappable to convert to rgba. However, if the input is already in rgba, it does not copy it. Because of this, using RGB masks the bug, because the copy would be C contiguous.
It then calls PIL.Image.frombuffer
, which appears to assume that its input is C contiguous. (Pillow doesn't appear to document this assumption, so this may actually be a Pillow bug.)
As a work-around, you could avoid the use of origin="lower"
, through the following code:
result = np.ascontiguousarray(result[::-1])
plt.imsave(fname="test_upper.png", arr=result, format="png")
I would suggest you open an issue in matplotlib so this can be fixed for future users.