【问题标题】:Specifying and saving a figure with exact size in pixels以像素为单位指定和保存精确大小的图形
【发布时间】:2012-11-22 18:15:23
【问题描述】:

假设我有一个大小为 3841 x 7195 像素的图像。我想将图形的内容保存到磁盘,从而生成我指定的精确大小(以像素为单位)的图像。

没有轴,没有标题。只是图像。我个人并不关心 DPI,因为我只想指定图像在屏幕中的大小(以磁盘为单位)以像素为单位

我读过otherthreads,他们似乎都在转换为英寸,然后以英寸为单位指定图形的尺寸并以某种方式调整dpi。我想避免处理像素到英寸转换可能导致的精度损失。

我试过了:

w = 7195
h = 3841
fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(im_np, aspect='normal')
fig.savefig(some_path, dpi=1)

没有运气(Python 抱怨宽度和高度都必须低于 32768 (?))

据我所见,matplotlib 要求在inchesdpi 中指定图形大小,但我只对图形在磁盘中的像素 感兴趣。我该怎么做?

澄清一下:我正在寻找一种方法来使用matplotlib,而不是使用其他图像保存库。

【问题讨论】:

  • 使用matplotlib,无法直接以英寸为单位设置图形大小。

标签: python matplotlib scipy


【解决方案1】:

Matplotlib 不直接使用像素,而是使用物理尺寸和 DPI。如果要显示具有一定像素大小的图形,则需要知道显示器的 DPI。例如this link 会为您检测到。

如果您有 3841x7195 像素的图像,您的显示器不太可能会那么大,因此您将无法显示该尺寸的图形(matplotlib 要求图形适合屏幕,如果您问的话对于太大的尺寸,它将缩小到屏幕尺寸)。假设您想要一个 800x800 像素的图像,仅作为示例。以下是在我的显示器中显示 800x800 像素图像的方法 (my_dpi=96):

plt.figure(figsize=(800/my_dpi, 800/my_dpi), dpi=my_dpi)

因此,您基本上只需将尺寸(以英寸为单位)除以 DPI。

如果要保存特定大小的图形,那就另当别论了。屏幕 DPI 不再那么重要(除非您要求的图形不适合屏幕)。使用相同的 800x800 像素图形示例,我们可以使用 savefigdpi 关键字将其保存为不同的分辨率。要将其保存为与屏幕相同的分辨率,只需使用相同的 dpi:

plt.savefig('my_fig.png', dpi=my_dpi)

要将其保存为 8000x8000 像素的图像,请使用大 10 倍的 dpi:

plt.savefig('my_fig.png', dpi=my_dpi * 10)

请注意,并非所有后端都支持 DPI 的设置。这里使用了 PNG 后端,但 pdf 和 ps 后端会以不同的方式实现大小。此外,更改 DPI 和大小也会影响字体大小等内容。较大的 DPI 将保持相同的字体和元素的相对大小,但如果您想为较大的图形使用较小的字体,则需要增加物理大小而不是 DPI。

回到您的示例,如果您想保存 3841 x 7195 像素的图像,您可以执行以下操作:

plt.figure(figsize=(3.841, 7.195), dpi=100)
( your code ...)
plt.savefig('myfig.png', dpi=1000)

请注意,我使用 100 的图形 dpi 来适应大多数屏幕,但使用 dpi=1000 保存以达到所需的分辨率。在我的系统中,这会生成一个 3840x7190 像素的 png - 保存的 DPI 似乎总是比所选值小 0.02 像素/英寸,这将对大图像尺寸产生(小)影响。对此here的更多讨论。

【讨论】:

  • 请记住显示器尺寸(以及标准浏览器和 ui 窗口尺寸)通常以 96 dpi 表示 - 96 的倍数。突然之间,像 1440 像素这样的数字有意义(15 英寸)这么想的。
  • 无法通过 figsizeplt.figure 使其工作。解决方案是按照其他答案的建议进行操作,然后调用 without figsize,然后调用 fig.set_size_inches(w,h)
  • figuresavefig 的文档。
  • 该链接未显示 Apple Thunderbolt Display 的正确值。
  • 我喜欢这个解决方案,但我有一个警告。 文本大小 与 dpi 成反比。 (我的系统是 MacBook Pro、OS X),因此对于交互式打印,将 dpi 设置为大(如 10*my_dpi)会将文本缩小到几乎不可见。
【解决方案2】:

这对我有用,根据您的代码,生成一个 93Mb 的 png 图像,带有颜色噪声和所需的尺寸:

import matplotlib.pyplot as plt
import numpy

w = 7195
h = 3841

im_np = numpy.random.rand(h, w)

fig = plt.figure(frameon=False)
fig.set_size_inches(w,h)
ax = plt.Axes(fig, [0., 0., 1., 1.])
ax.set_axis_off()
fig.add_axes(ax)
ax.imshow(im_np, aspect='normal')
fig.savefig('figure.png', dpi=1)

我在 Linux Mint 13 中使用 Python 2.7 库的最新 PIP 版本。

希望有帮助!

【讨论】:

  • 设置非常低的 dpi 将意味着字体几乎不可见,除非明确使用非常大的字体。
  • 最好设置更高的 dpi 并将您的英寸大小(无论如何都是任意的)除以该 dpi。除此之外,您的设置确实会产生用于像素再现的精确像素,谢谢!
  • 我正在尝试在保存带有绘图元素(圆圈、线条等)的图片之前使用它。这会扰乱线宽,因此元素几乎不可见。
  • 如果aspect='normal' 更改为aspect='equal'aspect=1 对我有效(请参阅docs)。
【解决方案3】:

OP 想要保留 1:1 像素数据。作为一名处理科学图像的天文学家,我不能允许对图像数据进行任何插值,因为它会引入未知和不可预测的噪声或错误。例如,这是通过 pyplot.savefig() 保存的 480x480 图像的 sn-p: Detail of pixels which matplotlib resampled to be roughly 2x2, but notice the column of 1x2 pixels

您可以看到大多数像素只是简单地翻了一番(因此 1x1 像素变为 2x2),但一些列和行变为每像素 1x2 或 2x1,这意味着原始科学数据已被更改。

正如 Alka 所暗示的, plt.imsave() 将实现 OP 的要求。假设您将图像数据存储在图像数组 im 中,那么可以执行类似的操作

plt.imsave(fname='my_image.png', arr=im, cmap='gray_r', format='png')

在此示例中文件名具有“png”扩展名的位置(但据我所知,您仍然必须使用 format='png' 指定格式),图像数组是 arr,我们选择了倒置灰度“gray_r”作为颜色图。我通常添加 vmin 和 vmax 来指定动态范围,但这些是可选的。

最终结果是一个与 im 数组像素尺寸完全相同的 png 文件。

注意:OP 没有指定轴等,这正是该解决方案所做的。如果要添加轴、刻度等。我首选的方法是在单独的图上执行此操作,使用 transparent=True (PNG 或 PDF)保存,然后将后者覆盖在图像上。这可以保证您保持原始像素完好无损。

【讨论】:

  • 谢谢。我问这个 Q 已经有几年了,但从我记得和在你的回答中看到的,当你试图将一组数据保存为图像时,这会很好用,但如果你想保存的是实际的figure 本身(无论其内容如何)并且仍然准确地控制生成的图像文件的像素尺寸?
  • 我非常喜欢这种方法,但是对于某些特定的图像尺寸,似乎存在一个错误,即保存后缺少一个像素行。因为我也从事研究工作,所以当我丢失像素时,一切都乱了套!为了避免这种情况,我在matplotlib.image.imsave(output_path, img, dpi=1) 中使用了dpi=1。显然,这个错误已经知道了一段时间(见这里:github.com/matplotlib/matplotlib/issues/4280)。
【解决方案4】:

根据 tiago 接受的响应,这是一个小型通用函数,可将 numpy 数组导出到与数组具有相同分辨率的图像:

import matplotlib.pyplot as plt
import numpy as np

def export_figure_matplotlib(arr, f_name, dpi=200, resize_fact=1, plt_show=False):
    """
    Export array as figure in original resolution
    :param arr: array of image to save in original resolution
    :param f_name: name of file where to save figure
    :param resize_fact: resize facter wrt shape of arr, in (0, np.infty)
    :param dpi: dpi of your screen
    :param plt_show: show plot or not
    """
    fig = plt.figure(frameon=False)
    fig.set_size_inches(arr.shape[1]/dpi, arr.shape[0]/dpi)
    ax = plt.Axes(fig, [0., 0., 1., 1.])
    ax.set_axis_off()
    fig.add_axes(ax)
    ax.imshow(arr)
    plt.savefig(f_name, dpi=(dpi * resize_fact))
    if plt_show:
        plt.show()
    else:
        plt.close()

正如tiago之前的回复所说,需要先找到屏幕DPI,例如:http://dpi.lv

我在函数中添加了一个附加参数resize_fact,例如,您可以将图像导出为原始分辨率的 50% (0.5)。

【讨论】:

    【解决方案5】:

    不同方法的比较

    这里是我尝试过的一些方法的快速比较,图片显示了所提供的内容。

    不尝试设置图像尺寸的基准示例

    只是为了有个比较点:

    base.py

    #!/usr/bin/env python3
    
    import sys
    
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    
    fig, ax = plt.subplots()
    print('fig.dpi = {}'.format(fig.dpi))
    print('fig.get_size_inches() = ' + str(fig.get_size_inches())
    t = np.arange(-10., 10., 1.)
    plt.plot(t, t, '.')
    plt.plot(t, t**2, '.')
    ax.text(0., 60., 'Hello', fontdict=dict(size=25))
    plt.savefig('base.png', format='png')
    

    运行:

    ./base.py
    identify base.png
    

    输出:

    fig.dpi = 100.0
    fig.get_size_inches() = [6.4 4.8]
    base.png PNG 640x480 640x480+0+0 8-bit sRGB 13064B 0.000u 0:00.000
    

    到目前为止我最好的方法:plt.savefig(dpi=h/fig.get_size_inches()[1] 仅高度控制

    我认为这是我大部分时间都会使用的,因为它简单且可扩展:

    get_size.py

    #!/usr/bin/env python3
    
    import sys
    
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    
    height = int(sys.argv[1])
    fig, ax = plt.subplots()
    t = np.arange(-10., 10., 1.)
    plt.plot(t, t, '.')
    plt.plot(t, t**2, '.')
    ax.text(0., 60., 'Hello', fontdict=dict(size=25))
    plt.savefig(
        'get_size.png',
        format='png',
        dpi=height/fig.get_size_inches()[1]
    )
    

    运行:

    ./get_size.py 431
    

    输出:

    get_size.png PNG 574x431 574x431+0+0 8-bit sRGB 10058B 0.000u 0:00.000
    

    ./get_size.py 1293
    

    输出:

    main.png PNG 1724x1293 1724x1293+0+0 8-bit sRGB 46709B 0.000u 0:00.000
    

    我倾向于只设置高度,因为我通常最关心图像将在文本中间占据多少垂直空间。

    plt.savefig(bbox_inches='tight' 改变图片大小

    我总觉得图片周围留白太多,倾向于加bbox_inches='tight' from: Removing white space around a saved image in matplotlib

    但是,这可以通过裁剪图像来实现,并且您不会获得所需的尺寸。

    相反,在同一个问题中提出的另一种方法似乎效果很好:

    plt.tight_layout(pad=1)
    plt.savefig(...
    

    它给出了高度等于 431 的确切期望高度:

    固定高度,set_aspect,自动调整宽度和小边距

    Ermmm,set_aspect 又搞砸了,阻止 plt.tight_layout 实际删除边距...

    提问于:How to obtain a fixed height in pixels, fixed data x/y aspect ratio and automatically remove remove horizontal whitespace margin in Matplotlib?

    plt.savefig(dpi=h/fig.get_size_inches()[1] + 宽度控制

    如果您真的需要除高度之外的特定宽度,这似乎可以正常工作:

    宽度.py

    #!/usr/bin/env python3
    
    import sys
    
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    
    h = int(sys.argv[1])
    w = int(sys.argv[2])
    fig, ax = plt.subplots()
    wi, hi = fig.get_size_inches()
    fig.set_size_inches(hi*(w/h), hi)
    t = np.arange(-10., 10., 1.)
    plt.plot(t, t, '.')
    plt.plot(t, t**2, '.')
    ax.text(0., 60., 'Hello', fontdict=dict(size=25))
    plt.savefig(
        'width.png',
        format='png',
        dpi=h/hi
    )
    

    运行:

    ./width.py 431 869
    

    输出:

    width.png PNG 869x431 869x431+0+0 8-bit sRGB 10965B 0.000u 0:00.000
    

    对于小宽度:

    ./width.py 431 869
    

    输出:

    width.png PNG 211x431 211x431+0+0 8-bit sRGB 6949B 0.000u 0:00.000
    

    所以看起来字体缩放是正确的,我们只是在标签被切断的非常小的宽度上遇到了一些麻烦,例如左上角的100

    我设法解决了 Removing white space around a saved image in matplotlib 的问题

    plt.tight_layout(pad=1)
    

    给出:

    width.png PNG 211x431 211x431+0+0 8-bit sRGB 7134B 0.000u 0:00.000
    

    由此我们也看到tight_layout去除了图片顶部的很多空白空间,所以我只是一般一直使用它。

    固定魔法基础高度,dpi on fig.set_size_inchesplt.savefig(dpi= 缩放

    我相信这和上面提到的方法是等价的:https://stackoverflow.com/a/13714720/895245

    magic.py

    #!/usr/bin/env python3
    
    import sys
    
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    
    magic_height = 300
    w = int(sys.argv[1])
    h = int(sys.argv[2])
    dpi = 80
    fig, ax = plt.subplots(dpi=dpi)
    fig.set_size_inches(magic_height*w/(h*dpi), magic_height/dpi)
    t = np.arange(-10., 10., 1.)
    plt.plot(t, t, '.')
    plt.plot(t, t**2, '.')
    ax.text(0., 60., 'Hello', fontdict=dict(size=25))
    plt.savefig(
        'magic.png',
        format='png',
        dpi=h/magic_height*dpi,
    )
    

    运行:

    ./magic.py 431 231
    

    输出:

    magic.png PNG 431x231 431x231+0+0 8-bit sRGB 7923B 0.000u 0:00.000
    

    然后看看它是否可以很好地扩展:

    ./magic.py 1291 693
    

    输出:

    magic.png PNG 1291x693 1291x693+0+0 8-bit sRGB 25013B 0.000u 0:00.000
    

    所以我们看到这种方法也很有效。我唯一遇到的问题是您必须设置 magic_height 参数或等效参数。

    固定 DPI + set_size_inches

    这种方法给出了一个稍微错误的像素大小,并且很难无缝缩放所有内容。

    set_size_inches.py

    #!/usr/bin/env python3
    
    import sys
    
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    
    w = int(sys.argv[1])
    h = int(sys.argv[2])
    fig, ax = plt.subplots()
    fig.set_size_inches(w/fig.dpi, h/fig.dpi)
    t = np.arange(-10., 10., 1.)
    plt.plot(t, t, '.')
    plt.plot(t, t**2, '.')
    ax.text(
        0,
        60.,
        'Hello',
        # Keep font size fixed independently of DPI.
        # https://stackoverflow.com/questions/39395616/matplotlib-change-figsize-but-keep-fontsize-constant
        fontdict=dict(size=10*h/fig.dpi),
    )
    plt.savefig(
        'set_size_inches.png',
        format='png',
    )
    

    运行:

    ./set_size_inches.py 431 231
    

    输出:

    set_size_inches.png PNG 430x231 430x231+0+0 8-bit sRGB 8078B 0.000u 0:00.000
    

    所以高度稍微偏离了,图像:

    如果我把它放大 3 倍,像素大小也是正确的:

    ./set_size_inches.py 1291 693
    

    输出:

    set_size_inches.png PNG 1291x693 1291x693+0+0 8-bit sRGB 19798B 0.000u 0:00.000
    

    但我们从中了解到,要使这种方法很好地缩放,您需要使每个 DPI 相关设置与以英寸为单位的大小成比例。

    在前面的示例中,我们只使“Hello”文本成比例,它的高度确实保持在我们预期的 60 到 80 之间。但是我们没有这样做的所有东西看起来都很小,包括:

    • 坐标区的线宽
    • 勾选标签
    • 点标记

    SVG

    我找不到如何为 SVG 图像设置它,我的方法仅适用于 PNG,例如:

    get_size_svg.py

    #!/usr/bin/env python3
    
    import sys
    
    import numpy as np
    import matplotlib.pyplot as plt
    import matplotlib as mpl
    
    height = int(sys.argv[1])
    fig, ax = plt.subplots()
    t = np.arange(-10., 10., 1.)
    plt.plot(t, t, '.')
    plt.plot(t, t**2, '.')
    ax.text(0., 60., 'Hello', fontdict=dict(size=25))
    plt.savefig(
        'get_size_svg.svg',
        format='svg',
        dpi=height/fig.get_size_inches()[1]
    )
    

    运行:

    ./get_size_svg.py 431
    

    并且生成的输出包含:

    <svg height="345.6pt" version="1.1" viewBox="0 0 460.8 345.6" width="460.8pt"
    

    并确定说:

    get_size_svg.svg SVG 614x461 614x461+0+0 8-bit sRGB 17094B 0.000u 0:00.000
    

    如果我在 Chromium 86 中打开它,浏览器调试工具鼠标图像悬停确认高度为 460.79。

    当然,由于 SVG 是一种矢量格式,理论上一切都应该按比例缩放,因此您可以在不损失分辨率的情况下转换为任何固定大小的格式,例如:

    inkscape -h 431 get_size_svg.svg -b FFF -e get_size_svg.png
    

    给出准确的高度:

    TODO 重新生成图像,不知何故搞砸了上传。

    我在这里使用 Inkscape 而不是 Imagemagick 的 convert,因为您还需要使用 -density 来使用 ImageMagick 获得锐利的 SVG 大小调整:

    在 HTML 上设置 &lt;img height="" 也应该只适用于浏览器。

    在 matplotlib==3.2.2 上测试。

    【讨论】:

      【解决方案6】:

      我有同样的问题。我使用 PIL Image 加载图像并转换为 numpy 数组,然后使用 matplotlib 修补一个矩形。这是一个 jpg 图像,所以我无法从 PIL img.info['dpi'] 获取 dpi,因此接受的解决方案对我不起作用。但经过一番修修补补后,我想出了将图形保存为与原始大小相同的方法。

      我在这里添加以下解决方案,认为它会帮助遇到与我相同问题的人。

      import matplotlib.pyplot as plt
      from PIL import Image
      import numpy as np
      
      img = Image.open('my_image.jpg') #loading the image
      image = np.array(img) #converting it to ndarray
      dpi = plt.rcParams['figure.dpi'] #get the default dpi value
      fig_size = (img.size[0]/dpi, img.size[1]/dpi) #saving the figure size
      fig, ax = plt.subplots(1, figsize=fig_size) #applying figure size
      #do whatver you want to do with the figure
      fig.tight_layout() #just to be sure
      fig.savefig('my_updated_image.jpg') #saving the image
      

      这会以与原始图像相同的分辨率保存图像。

      如果您不使用 jupyter 笔记本。您可以通过以下方式获取dpi。

      figure = plt.figure()
      dpi = figure.dpi
      

      【讨论】:

        【解决方案7】:

        此解决方案适用于 matplotlib 版本 3.0.1、3.0.3 和 3.2.1。

        def save_inp_as_output(_img, c_name, dpi=100):
            h, w, _ = _img.shape
            fig, axes = plt.subplots(figsize=(h/dpi, w/dpi))
            fig.subplots_adjust(top=1.0, bottom=0, right=1.0, left=0, hspace=0, wspace=0) 
            axes.imshow(_img)
            axes.axis('off')
            plt.savefig(c_name, dpi=dpi, format='jpeg') 
        

        因为 subplots_adjust 设置使轴填充图形,所以您不想指定 bbox_inches='tight',因为在这种情况下它实际上会创建空白填充。当您有超过 1 个子图时,此解决方案也有效。

        【讨论】:

        • 使用 matplotlib 3.4.1 版,这是此页面上唯一正确输出包含具有精确像素大小且没有多余空白的图像的图形的答案。
        【解决方案8】:

        为什么每个人都继续使用 matplotlib?
        如果你的图片是一个形状为(3841, 7195, 3)的numpy数组,它的数据类型是numpy.uint8,rgb的取值范围是0到255,你可以简单地把这个数组保存为图片而不用matplotlib:

        from PIL import Image
        im = Image.fromarray(A)
        im.save("your_file.jpeg")
        

        我从另一个post找到了这段代码

        【讨论】:

          【解决方案9】:

          plt.imsave 为我工作。 你可以在这里找到文档:https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.imsave.html

          #file_path = directory address where the image will be stored along with file name and extension
          #array = variable where the image is stored. I think for the original post this variable is im_np
          plt.imsave(file_path, array)
          

          【讨论】:

          • 请添加示例代码,准确显示您正在设置的参数以及原始帖子用例的推荐值。
          猜你喜欢
          • 2015-04-14
          • 1970-01-01
          • 1970-01-01
          • 2013-01-20
          • 1970-01-01
          • 1970-01-01
          • 2013-11-21
          • 1970-01-01
          • 2016-06-10
          相关资源
          最近更新 更多