Search code examples
numpymatplotlibpython-imaging-library

Matplotlib imsave error: ndarray is not C-contiguous (but it is?)


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

Solution

  • This looks like a bug in matplotlib to me.

    Here are the conditions needed to trigger it:

    • The input must be an input of size MxNx4. RGBA fails but RGB works.
    • The dtype must be uint8.
    • origin must be set to "lower"
    • Image file type can be set to png, jpg, gif, or tiff, and all trigger the same issue. It's likely not a codec-specific problem.
    • The data in the image does not matter.

    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)
    

    Link to code.

    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.