imshow中的 off by one 错误?
我正在绘制一张PGM格式的图片:

这是我使用的数据。
问题是,有些显示的像素不对。例如:
- 图片顶部的三个灰色框的值是11(所以它们应该是红色,而不是红色)
- 顶部一排的两个黄色像素的值是8,所以它们应该是黄绿色,而不是黄色
有没有人能解释一下这些不一致的地方,以及怎么解决它们?
这是我的代码:
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 files
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()
编辑
我现在明白发生了什么。基本上,imshow
似乎是这样做的:
- 确定动态范围(即
[ min(image), max(image) ]
) - 用颜色映射中指定的颜色数量来表示这个范围(13种颜色)
我希望它做的是:
- 使用我在创建颜色映射时指定的动态范围(13)
- 用颜色映射中的13种颜色来表示这个范围
我可以通过强制将图像的动态范围设置为13来验证这一点(见标记为HACK
的那一行)。有没有更好的方法来做到这一点?
这是更新后的图片:

2 个回答
其实并没有什么不一致,你只是手动把刻度标记成了和实际值不一样的数字。
注意到你的 LABELS
只是 range(13)
,而你的刻度位置(ticks
)并不是从0到12的。
所以,你把位置在10.6的刻度手动标记成了12!
试着去掉这一行 cb.ax.set_yticklabels(map(str, LABELS))
,你就会明白我说的意思了(另外,matplotlib会自动把它们转换成字符串,所以没必要调用 map(str, LABELS)
)。
也许你可以直接把实际的刻度位置转换成标签,而不是用一组固定的数字?比如可以用 [round(tick) for tick in ticks]
?
编辑:抱歉,我刚才的说法听起来有点刻薄……我并不是那个意思! :)
编辑2:针对更新的问题,是的,imshow
会根据输入的最小值和最大值自动确定范围。(我有点困惑……它还能做什么呢?)
如果你想要直接的颜色映射而不进行插值,那么就用一些离散的颜色映射,而不是 LinearSegmentedColormap
。不过,最简单的方法是手动设置matplotlib的某个 LinearSegmentedColormap
的限制(这就是 matplotlib.cm.spectral
)。
如果你想手动设置颜色映射的范围,只需在 imshow
返回的颜色轴对象上调用 set_clim([0,12])
。
例如:
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()
解决办法是设置 im.set_clim(vmin, vmax)
。简单来说,图像中的数值会被调整到覆盖整个颜色范围。比如说,如果你的数据中最大值是 3
,那么它就会被分配到颜色的最大值。
但是你需要告诉它,max_nodes
是最高值(在你的例子中是13),即使这个值在数据中并不存在,比如 im.set_clim(0, 13)
。
我稍微改了一下你的代码,以便能够处理其他数据文件中不同的 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')
这里有一些简单的测试数据来说明。注意,num_modes
的值是10,但没有数据点达到这个水平。这展示了数值是如何一一对应到颜色图上的:
P2
5 3
10
0 1 0 2 0
3 0 2 0 1
0 1 0 2 0
输出: