开发者

Off by one error in imshow?

I'm plotting a PGM image:

Off by one error in imshow?

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:

Off by one error in imshow?


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:

Off by one error in imshow?


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 LinearSegmentedColormaps (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()

Off by one error in imshow?

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜