Off by one error in imshow?
I'm plotting a PGM image:
Here's the data I'm using.The problem is some of the shown pixels are wrong. For example:
- the three grey boxes near the top of the image are of value 11 (so they should be red, not red)
- the two yellow pixels in the top row -- they are of value 8, so they should be yellow-green, not yellow
Can anybody explain the discrepancies and how to fix them?
Here's my source:
from pylab import *
import numpy
LABELS = range(13)
NUM_MODES = len(LABELS)
def read_ascii_pgm(fname):
"""
Very fragile PGM reader. It's OK since this is only for reading file开发者_Go百科s
output by my own app.
"""
lines = open(fname).read().strip().split('\n')
assert lines[0] == 'P2'
width, height = map(int, lines[1].split(' '))
assert lines[2] == '13'
pgm = numpy.zeros((height, width), dtype=numpy.uint8)
for i in range(height):
cols = lines[3+i].split(' ')
for j in range(width):
pgm[i,j] = int(cols[j])
return pgm
def main():
import sys
assert len(sys.argv) > 1
fname = sys.argv[1]
pgm = read_ascii_pgm(fname)
# EDIT: HACK!
pgm[0,0] = 12
cmap = cm.get_cmap('spectral', NUM_MODES)
imshow(pgm, cmap=cmap, interpolation='nearest')
edit = True
if edit:
cb = colorbar()
else:
ticks = [ (i*11./NUM_MODES + 6./NUM_MODES) for i in range(NUM_MODES) ]
cb = colorbar(ticks=ticks)
cb.ax.set_yticklabels(map(str, LABELS))
savefig('imshow.png')
if __name__ == '__main__':
main()
EDIT
I see what's happening here now. Basically, imshow
seems to be doing this:
- determining the dynamic range (as
[ min(image), max(image) ]
- represent this using the number of colors specified in the color map (13 colors)
What I want it to do is:
- use the dynamic range that I specified when creating the color map (13)
- represent this using the 13 colors in the color map
I can verify this by forcing the dynamic range of the image to be 13 (see the line labelled HACK
). Is there a better way to do this?
Here's an updated image:
The solution is to set im.set_clim(vmin, vmax)
. Basically the values in the image were being translated to cover the entire color range. For example if 3
was the largest value in your data, it would be assigned the maximum color value.
Instead you need to tell it that max_nodes
is the highest value (13 in your case), even though it doesn't appear in the data, e.g. im.set_clim(0, 13)
.
I changed your code slightly to work with other data files with different values for num_modes
:
import numpy
from pylab import *
def read_ascii_pgm(fname):
lines = open(fname).read().strip().split('\n')
assert lines[0] == 'P2'
width, height = map(int, lines[1].split(' '))
num_modes = int(lines[2])
pgm = numpy.zeros((height, width), dtype=numpy.uint8)
for i in range(height):
cols = lines[3+i].split(' ')
for j in range(width):
pgm[i,j] = int(cols[j])
return pgm, num_modes + 1
if __name__ == '__main__':
import sys
assert len(sys.argv) > 1
fname = sys.argv[1]
pgm, num_modes = read_ascii_pgm(fname)
labels = range(num_modes)
cmap = cm.get_cmap('spectral', num_modes)
im = imshow(pgm, cmap=cmap, interpolation='nearest')
im.set_clim(0, num_modes)
ticks = [(i + 0.5) for i in range(num_modes)]
cb = colorbar(ticks=ticks)
cb.ax.set_yticklabels(map(str, labels))
savefig('imshow_new.png')
Some simpler test data to illustrate. Notice that the num_modes
value is 10, but no data point reaches that level. This shows how the values index into the colormap 1:1:
P2
5 3
10
0 1 0 2 0
3 0 2 0 1
0 1 0 2 0
Output:
There's no discrepancy, you're just manually setting the ticks to be labeled with values that aren't what they actually are.
Notice that your LABELS
is just range(13)
, while your actual tick locations (ticks
) don't range from 0 to 12.
So, you're manually labeling the top tick, which has a position of 10.6, as 12!
Try taking out the line cb.ax.set_yticklabels(map(str, LABELS))
, and you'll see what I mean (Also, matplotlib will automatically cast them to strings. There's no reason to call map(str, LABELS)
).
Perhaps instead of using a static set of numbers as labels, you should just convert your actual tick locations to labels? Something like [round(tick) for tick in ticks]
?
Edit: Sorry, that sounded snarkier than I intended it to... I didn't mean it to sound that way! :)
Edit2:
In response to the updated question, yes, imshow
determines the range automatically from the min and max of the input. (I'm confused... What else would it do?)
If you want a direct color mapping with no interpolation, then use one of the discrete colormaps, not a LinearSegmentedColormap
. However, it's easiest to just manually set the limits on a one of matplotlib's LinearSegmentedColormap
s (which is what matplotlib.cm.spectral
is).
If you want to manually set the range of the color mapping used, just call set_clim([0,12])
on the coloraxis object that imshow
returns.
E.g.
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
with open('temp.pgm') as infile:
header, nrows, ncols = [infile.readline().strip() for _ in range(3)]
data = np.loadtxt(infile).astype(np.uint8)
cmap = mpl.cm.get_cmap('spectral', 13)
cax = plt.imshow(data, cmap, interpolation='nearest')
cax.set_clim([0,13])
cbar = plt.colorbar(cax, ticks=np.arange(0.5, 13, 1.0))
cbar.ax.set_yticklabels(range(13))
plt.show()
精彩评论