Reputation: 723
When a regular RGB image in range (0,255) is cast as float, then displayed by matplotlib, the image is displayed as negative. If it is cast as uint8, it displays correctly (of course). It caused me some trouble to figure out what was going on, because I accidentally cast one of images as float.
I am well aware that when cast as float, the image is expected to be in range (0,1), and sure enough, when divided by 255 the image displayed is correct. But, why would an image in range (0,255) that is cast as float displayed as negative? I would have expected either saturation (all white) or automatically inferred the range from the input (and thus correctly displayed)? If either of those expected things happened, I would have been able to debug my code quicker. I have included the required code to reproduce the behaviour. Does anyone have insight on why this happens?
import numpy as np
import matplotlib.pyplot as plt
a = np.random.randint(0,127,(200,400,3))
b = np.random.randint(128,255,(200,400,3))
img=np.concatenate((a,b)) # Top should be dark ; Bottom should be light
plt.imshow(img) # Inverted
plt.figure()
plt.imshow(np.float64(img)) # Still Bad. Added to address sascha's comment
plt.figure()
plt.imshow(255-img) # Displayed Correctly
plt.figure()
plt.imshow(np.uint8(img)) # Displayed Correctly
plt.figure()
plt.imshow(img/255.0) # Displays correctly
Upvotes: 11
Views: 11917
Reputation: 792
In the sources, in image.py
, in the AxesImage
class (what imshow
returns) a method _get_unsampled_image
is called at some point in the drawing process. The relevant code starts on line 226 for me (matplotlib-1.5.3):
if A.dtype == np.uint8 and A.ndim == 3:
im = _image.frombyte(A[yslice, xslice, :], 0)
im.is_grayscale = False
else:
if self._rgbacache is None:
x = self.to_rgba(A, bytes=False)
# Avoid side effects: to_rgba can return its argument
# unchanged.
if np.may_share_memory(x, A):
x = x.copy()
# premultiply the colors
x[..., 0:3] *= x[..., 3:4]
x = (x * 255).astype(np.uint8)
self._rgbacache = x
So the type and size of the input A
get checked:
if A.dtype == np.uint8 and A.ndim == 3:
in which case there is no preprocessing. Otherwise, without checking the range of the input, you ultimately have a multiplication by 255 and a cast to uint8
:
x = (x * 255).astype(np.uint8)
And we know what to expect if x
is from 0 to 255 instead of 0 to 1:
In [1]: np.uint8(np.array([1,2,128,254,255])*255)
Out[1]: array([255, 254, 128, 2, 1], dtype=uint8)
So light becomes dark. That this inverts the image is probably not a planned behavior as I think you assume.
You can compare the values of _rgbacache
in the object returned from imshow
for each of your input cases to observe the result, e.g. im._rbacache
where im = plt.imshow(np.float64(img))
.
Upvotes: 8
Reputation: 33532
I think you are on a wrong path here as you are claiming, that np.random.randint()
should return an float-based array. It does not! (docs)
This means:
Your first plot is calling imshow with an numpy-array of dtype=int64. This is not allowed as seen here!
The only allowed dtypes for your dimensions are described as: (see docs):
MxNx3 – RGB (float or uint8 array) # there are others
# -> this one applies to your dims!
The only valid calls to imshow in your example are number 3 and 4 (while 1 and 2 are invalid)!
Listing:
plt.imshow(img)
not ok as dtype=int64plt.imshow(255-img)
not ok as dtype=int64plt.imshow(np.uint8(img))
ok as dtype=uint8 (and compatible dims)plt.imshow(img/255.0)
ok as dtype=float (and compatible dims)Remark:
If this makes you nervous, you could checkout scikit-image which is by design a bit more cautious about the internal representation of images (during custom-modifications and the resulting types). The internal data-structures are still numpy-arrays!
Upvotes: 1