указание размера содержимого оси matplotlib - PullRequest
0 голосов
/ 15 апреля 2019

Допустим, я пытаюсь построить изображение черной дыры с x*y пикселями (или дюймами). В отличие от многих других, я не хочу указывать общий размер рисунка, но размер содержимого графика (поэтому все в кадре, за исключением самого кадра, отметок, меток, цветовых полос, полей, ...).

Какой предпочтительный способ сделать это?


Собственные попытки

У меня уже была эта проблема пару раз, но теперь я бы предпочел какой-то другой метод, чем предыдущая итерация размеров методом проб и ошибок ... вот мой "игровой код":

import numpy as np

%matplotlib inline
import matplotlib
from matplotlib import pyplot as plt
import notebook

print(matplotlib.__version__, notebook.__version__)
# 3.0.0, 5.7.0 in my case

# some data
x, y = 456, 123
a = np.random.randn(y, x)

# the following at least gets the actual figure size in browser right,
# but if possible i'd like to avoid large white space margins as well...
# %config InlineBackend.print_figure_kwargs = {'bbox_inches': None}

dpi = plt.rcParams['figure.dpi']
fig, ax = plt.subplots(figsize=(x/dpi, y/dpi), dpi=dpi)
im = ax.imshow(a, interpolation='none')
cb = fig.colorbar(im)

# print sizes
bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
print(f"content size: ({bbox.width}, {bbox.height}) inch, ({bbox.width*fig.dpi}, {bbox.height*fig.dpi}) px")
print(f"fig size:     {fig.get_size_inches()} in, {fig.dpi*fig.get_size_inches()} px")
print(f"dpi: {fig.dpi}")

выход:

content size: (4.023888888888889, 1.3870138888888888) inch, (289.72, 99.865) px
fig size:     [6.33333333 1.70833333] in, [456. 123.] px
dpi: 72.0

sample output

Как видно, размер напечатанного рисунка составляет 456x123 пикселей, но если вы действительно осмотрите загруженное изображение (скопированное из браузера), вы увидите, что оно составляет всего 376x119 пикселей. Хотя это можно исправить (как указано в коде), фактический размер «контента», не зависящий от этого, остается 282x75 пикселей: - /.

Есть идеи?

1 Ответ

1 голос
/ 15 апреля 2019

К сожалению, для этого нет предпочтительного пути. Некоторые более или менее сложные обходные пути приходят на ум.

A. Сделав фигуру размером с изображение, увеличьте ее, сохранив

Если цель в основном состоит в том, чтобы создать файл изображения фигуры, проще всего сделать оси графика изображения такими же большими, как фигура, а затем разрешить расширение конечного файла изображения с помощью параметра bbox_inches="tight" .

Для этого потребуется вручную разместить цветную полосу вне фигуры.

import numpy as np
import matplotlib.pyplot as plt

#create some image, with lines every second pixel
rows = 123
cols = 456
image = np.zeros((rows,cols))
image[:, np.arange(0,image.shape[1], 2)] = 1
image[np.arange(0,image.shape[0], 2), :] = 0.5

dpi = 100
fig, ax = plt.subplots(figsize=(image.shape[1]/dpi, image.shape[0]/dpi), dpi=dpi)
fig.subplots_adjust(0,0,1,1)

im = ax.imshow(image)

cax = fig.add_axes([1.05, 0, 0.03, 1])
fig.colorbar(im, cax=cax)

fig.savefig("test.png", bbox_inches="tight")

enter image description here

Основным недостатком этого является то, что может привести к неправильному изображению на один пиксель. Это связано с тем, что позиции всегда находятся в координатах фигуры, что приводит к ошибкам округления при штамповке размера осей в пикселях.

например. если в приведенном выше примере выбрать dpi=69, результат будет

enter image description here

Чередующиеся линии позволяют легко определить, что изображение на один пиксель слишком мало по высоте.

B. Сделайте фигуру больше, чем изображение, отрегулируйте поля

Одним из недостатков вышесказанного является то, что украшения осей и цветная полоса находятся за пределами фигуры. Чтобы они были внутри, можно определить все поля и вычислить, насколько большой должна быть конечная цифра. Это но громоздко.

import numpy as np
import matplotlib.pyplot as plt

#create some image
rows = 123
cols = 456
image = np.zeros((rows,cols))
image[:, np.arange(0,image.shape[1], 2)] = 1
image[np.arange(0,image.shape[0], 2), :] = 0.5

dpi = 100

left = right = 60
top = bottom = 40
cbarwidth = 24
wspace = 10

width = left + cols + wspace + cbarwidth + right
height = top + rows + bottom

w = width / dpi
h = height / dpi

fig, (ax, cax) = plt.subplots(ncols = 2, figsize=(w,h), dpi=dpi, 
                              gridspec_kw=dict(width_ratios=[cols, cbarwidth]))
fig.subplots_adjust(left = left/width, right = 1-right/width, 
                    bottom = bottom/height, top = 1-top/height, 
                    wspace = wspace / (cols + cbarwidth))

im = ax.imshow(image)
fig.colorbar(im, cax=cax)

fig.savefig("test2.png")

enter image description here

Он также будет страдать от того же недостатка, что и A. , например если используются нечетные числа, такие как

dpi = 72

left = right = 59
top = bottom = 37
cbarwidth = 19
wspace = 12

enter image description here

C. Используйте figimage и положите топоры сверху.

Единственный способ убедиться в отсутствии эффектов наложения - использовать figimage. Это помещает изображение в пиксельных координатах на фигуру. Однако тогда у одного не будет никаких осей по умолчанию. @Anntzer недавно предложил решение , которое заключается в том, чтобы просто поместить оси в положение на рисунке, где figimage равно.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.transforms import Bbox

#create some image, with lines every second pixel
rows = 123
cols = 456
image = np.zeros((rows,cols))
image[:, np.arange(0,image.shape[1], 2)] = 1
image[np.arange(0,image.shape[0], 2), :] = 0.5

dpi = 100

left = right = 60
top = 40
bottom = 65
cbarwidth = 24
wspace = 10

width = left + cols + wspace + cbarwidth + right
height = top + rows + bottom

w = width / dpi
h = height / dpi


fig = plt.figure(figsize=(w,h), dpi=dpi)

im = fig.figimage(image, xo=left, yo=bottom); 

# create axes on top
# bbox in pixels
bbox = Bbox([[left, bottom], [left + cols, bottom + rows]])
ax = fig.add_axes(fig.transFigure.inverted().transform_bbox(bbox))
ax.set_facecolor("None")
# recreate axis limits
ax.set(xlim=(-0.5, cols-0.5), ylim=(rows-0.5, -0.5))

# add colorbar
cbbox =  Bbox([[left + cols + wspace, bottom], 
               [left + cols + wspace + cbarwidth, bottom + rows]])
cax = fig.add_axes(fig.transFigure.inverted().transform_bbox(cbbox))
fig.colorbar(im, cax=cax)

fig.savefig("test3.png")

enter image description here

При этом можно быть уверенным, что само изображение не искажается. Но тики осей могут быть смещены на пиксель или около того, потому что они проходят преобразование фигуры. Кроме того, я не продумал полностью, нужно ли сдвигать координаты bbox на половину единицы или нет. (Комментарии приветствуются по последнему пункту!)

...