如何获得以像素为单位的固定高度、固定数据 x/y 纵横比并自动删除 Matplotlib 中的删除水平空白边距?

Posted

技术标签:

【中文标题】如何获得以像素为单位的固定高度、固定数据 x/y 纵横比并自动删除 Matplotlib 中的删除水平空白边距?【英文标题】:How to obtain a fixed height in pixels, fixed data x/y aspect ratio and automatically remove remove horizontal whitespace margin in Matplotlib? 【发布时间】:2021-02-14 22:49:28 【问题描述】:

让我们以此为起点:Specifying and saving a figure with exact size in pixels

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

h = int(sys.argv[1])
fig, ax = plt.subplots(nrows=2, ncols=1)
t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.savefig(
    'main.png',
    format='png',
    dpi=h/fig.get_size_inches()[1],
    facecolor='y',
)

这让我可以这样做:

./main.py 400 && identify main.png

生成正确的所需高度为 400 像素的图像:

main.png PNG 533x400 533x400+0+0 8-bit sRGB 6058B 0.000u 0:00.000

但是,地块的左右两侧有很多空白区域。这个空间是由于我也想要的1 的固定纵横比(x 和 y 数据具有相同的大小)。我们可以通过删除set_aspect 调用来确认这一点,这给出了一个具有合理大小边距的数字:

但我也想要 1/1 的纵横比。

我尝试使用以下几种方法删除此空间:Removing white space around a saved image in matplotlib,但没有一个给出我想要的。

例如,如果我添加:

plt.savefig(bbox_inches='tight',

我得到了想要的图像:

但高度不再是我想要的 400:

main.png PNG 189x345 189x345+0+0 8-bit sRGB 4792B 0.000u 0:00.000

或者如果我尝试添加:

plt.tight_layout(pad=0)

高度是正确的,但它没有删除水平空间:

我可以做的一件事是明确设置宽度,如下所示:

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(nrows=2, ncols=1)
wi, hi = fig.get_size_inches()
fig.set_size_inches(hi*(w/h), hi)
t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.tight_layout(pad=1)
plt.savefig(
    'main.png',
    format='png',
    dpi=h/hi,
    facecolor='y',
)

然后运行它:

./main.py 400 250 && identify main.png

其中 250 是通过反复试验选择的,它确实提供了准确的像素尺寸和好看的输出:

但我宁愿不必通过反复试验来找到值 250,我希望它由 matplotlib 自动确定。

这可能是matplotlib: Set width or height of figure without changing aspect ratio 所要求的,但如果没有具体的例子就很难确定。

在 matplotlib==3.2.2 上测试。

【问题讨论】:

【参考方案1】:

我不知道我是否理解了您的问题,但如果您想使用 1x2 子图布局限制图形中的空白,您只需创建一个宽度为高度一半的图形:

h = 400
nrows = 2
w = h/nrows
dpi = 100

fig, ax = plt.subplots(nrows=nrows, ncols=1, figsize=(w/dpi, h/dpi), dpi=dpi)

t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.tight_layout(pad=1)
plt.savefig(
    'main.png',
    format='png',
    dpi=dpi,
    facecolor='y',
)

>> identify main.png

main.png PNG 200x400 200x400+0+0 8-bit sRGB 6048B 0.000u 0:00.000

【讨论】:

好的,我应该对此进行更多测试。该解决方案是合理的。但是不得不自己进行 nrow/ncol 操作有点烦人。如果添加了其他垂直的非绘图区域元素,空间也会返回,例如a.set_title('A', fontdict=dict(fontsize='75'))。虽然在实践中这可能并不常见(字体大小被夸大了)。 我现在还注意到,在更一般的情况下也需要考虑纵横比(此答案适用于 a.set_aspect(1) 的特定情况)。【参考方案2】:

SVG 输出 + plt.savefig(bbox_inches='tight' + Inkscape 转换

这很糟糕,但它可以满足我的要求,无需大量额外的样板,所以我们开始吧:

#!/usr/bin/env python3

import sys

import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl

h = int(sys.argv[1])
fig, ax = plt.subplots(nrows=2, ncols=1)
t = np.arange(-10., 10., 1.)
a = ax[0]
a.set_aspect(1)
a.plot(t, t, '.')
a = ax[1]
a.plot(t, -t, '.')
a.set_aspect(1)
plt.savefig(
    'main.svg',
    format='svg',
    dpi=h/fig.get_size_inches()[1],
    facecolor='y',
    bbox_inches='tight',
)

然后:

inkscape -b FFF -e main.png -h 400 main.svg

输出:

bbox_inches='tight' 提供了一个没有太多边框的不错的图像,但让我失去了确切的尺寸,所以我们使用 SVG 输出来解决这个问题。

这应该很好用,因为 SVG 是一种矢量格式,因此应该可以无缝缩放到任何大小。

我使用 Inkscape 进行转换,因为 Imagemagick 需要您手动计算分辨率:

https://superuser.com/questions/598849/imagemagick-convert-how-to-produce-sharp-resized-png-files-from-svg-files/1602059#1602059 How to convert a SVG to a PNG with ImageMagick?

在 Inkscape 0.92.5 上测试。

【讨论】:

以上是关于如何获得以像素为单位的固定高度、固定数据 x/y 纵横比并自动删除 Matplotlib 中的删除水平空白边距?的主要内容,如果未能解决你的问题,请参考以下文章

如何将图像从固定尺寸动画到全屏?

PHP裁剪图像以固定宽度和高度而不丢失尺寸比

OpenGL如何使用固定管线下的着色器渲染一个正方形,并用特殊键控制移动

如何在页面加载时将 Kendo 网格高度设置为固定值

给定一个图像,一个像素点和一个以像素为单位的半径。如何找到它创建的圆形边框的像素坐标

如何在固定高度的 div 之后制作无尽的 div? [复制]