imshow中的 off by one 错误?

5 投票
2 回答
711 浏览
提问于 2025-04-16 18:19

我正在绘制一张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 个回答

3

其实并没有什么不一致,你只是手动把刻度标记成了和实际值不一样的数字。

注意到你的 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()

enter image description here

5

解决办法是设置 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

输出:

enter image description here

撰写回答