有没有一种使用matplotlib绘制指定宽度和高度的文本的方法?

4

我想使用matplotlib为我的工作创建一个序列标志。

序列标志类似于this,如https://en.wikipedia.org/wiki/Sequence_logo所示。每个字符具有特定的高度。我想使用matplotlib制作这个标志。

如何更改字体的纵横比? 我尝试使用来自matplotlib.transform的Affine2D,但它没有起作用。

ax.text(1,1,"A").set_transform(matplotlib.transforms.Affine2D().scale(0.5,1))

有没有简单的解决方法?


你可以这样做 ax.text(1,1,"A", fontsize = 20).....,但这是不改变宽高比的。你想让宽高比不是默认值吗? - Arty
顺便问一下,你用 Affine2D 做的那个有什么问题吗?它根本不起作用?还是写这样的代码行太长了?或者它可以工作但不是非常精确?另外,如果 .set_transform 不起作用,您也可以将变换对象作为参数传递 ax.text(1,1,"A", transform = .....Affine2D....) - Arty
抱歉我之前的描述不够清晰。因为我是第一次使用这个服务,所以无法上传图片。是的,我想要改变宽高比以满足制作序列标志的要求。在观看了这个页面之后,我认为通过应用Affine2D().scale(0.5,1)也可以对文本起作用,但结果是文本粘在了图像的左下角。 - Iwano Natsuki
我已经实现了自己的解决方案来完成你的任务请参见我的答案,如果有什么问题,请告诉我! - Arty
1个回答

2
我尝试了不同的方法来拉伸Matplotlib中文本的宽度,但没有成功。可能他们还没有正确实现拉伸,我甚至在文档中看到了这样的通知,比如对于一个字体拉伸函数,此功能尚未实现!
因此,我决定编写自己的辅助函数来完成此任务。它使用PIL模块的绘图函数,您必须安装下面的模块: python -m pip install pillow numpy matplotlib
请注意,我的辅助函数text_draw_mpl(...)接受以您的绘图单位表示的x、y偏移量和宽度、高度,例如,如果您的绘图范围从0到1,则必须在函数中使用值如0.1、0.2、0.3、0.4。
我的另一个辅助函数text_draw_np(...)是低级别函数,您可能永远不会直接使用它,它使用以像素表示的宽度和高度,并在输出上产生形状为(height,width,3)(3个RGB颜色)的RGB数组。
在我的函数中,您可以将背景颜色(bg参数)和前景颜色(color参数)都作为字符串颜色名称(如'magenta''blue')和RGB元组(例如(0, 255, 0)表示绿色)传递。如果未提供前景色,则默认为黑色,背景色为白色。

请注意,我的函数支持remove_gaps参数。如果它是True,则绘制的文本图片的所有边缘都将删除空格;如果它是False,则保留空格。空格是由字形在字体文件内部绘制的方式引入的,例如小写字母m在顶部有更多的空间,大写字母T在顶部的空间较少。字体具有这些空间,以便整个文本具有相同的高度,并且两行文本之间有一定的间隙而不会合并。

另外请注意,我提供了默认的Windows Arial字体路径c:/windows/fonts/arial.ttf,如果您使用Linux或者想要其他字体,只需从互联网上下载任何免费的Unicode TrueType (.ttf)字体(例如从这里),并将该字体放在脚本附近,并修改下面我的代码中的路径。此外,PIL模块支持其他格式,如其文档所述支持:TrueType和OpenType字体(以及FreeType库支持的其他字体格式)

在线试用!

def text_draw_np(text, width, height, *, font = 'c:/windows/fonts/arial.ttf', bg = (255, 255, 255), color = (0, 0, 0), remove_gaps = False, cache = {}):
    import math, numpy as np, PIL.Image, PIL.ImageDraw, PIL.ImageFont, PIL.ImageColor
    def get_font(fname, size):
        key = ('font', fname, size)
        if key not in cache:
            cache[key] = PIL.ImageFont.truetype(fname, size = size, encoding = 'unic')
        return cache[key]
    width, height = math.ceil(width), math.ceil(height)
    pil_font = get_font(font, 24)
    text_width, text_height = pil_font.getsize(text)
    pil_font = get_font(font, math.ceil(1.2 * 24 * max(width / text_width, height / text_height)))
    text_width, text_height = pil_font.getsize(text)
    canvas = PIL.Image.new('RGB', (text_width, text_height), bg)
    draw = PIL.ImageDraw.Draw(canvas)
    draw.text((0, 0), text, font = pil_font, fill = color)
    if remove_gaps:
        a = np.asarray(canvas)
        bg_rgb = PIL.ImageColor.getrgb(bg)
        b = np.zeros_like(a)
        b[:, :, 0] = bg_rgb[0]; b[:, :, 1] = bg_rgb[1]; b[:, :, 2] = bg_rgb[2]
        t0 = np.any((a != b).reshape(a.shape[0], -1), axis = -1)
        top, bot = np.flatnonzero(t0)[0], np.flatnonzero(t0)[-1]
        t0 = np.any((a != b).transpose(1, 0, 2).reshape(a.shape[1], -1), axis = -1)
        lef, rig = np.flatnonzero(t0)[0], np.flatnonzero(t0)[-1]
        a = a[top : bot, lef : rig]
        canvas = PIL.Image.fromarray(a)
    canvas = canvas.resize((width, height), PIL.Image.LANCZOS)
    return np.asarray(canvas)
    
def text_draw_mpl(fig, ax, text, offset_x, offset_y, width, height, **nargs):
    axbb = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted())
    pxw, pxh = axbb.width * fig.dpi * width / (ax.get_xlim()[1] - ax.get_xlim()[0]), axbb.height * fig.dpi * height / (ax.get_ylim()[1] - ax.get_ylim()[0])
    ax.imshow(text_draw_np(text, pxw * 1.2, pxh * 1.2, **nargs), extent = (offset_x, offset_x + width, offset_y, offset_y + height), aspect = 'auto', interpolation = 'lanczos')

import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.set_ylim(0, 1000)
ax.set_xlim(0, 1000)
text_draw_mpl(fig, ax, 'Hello!', 100, 500, 150, 500, color = 'green', bg = 'magenta', remove_gaps = True)
text_draw_mpl(fig, ax, 'World!', 100, 200, 800, 100, color = 'blue', bg = 'yellow', remove_gaps = True)
text_draw_mpl(fig, ax, ' Gaps ', 400, 500, 500, 200, color = 'red', bg = 'gray', remove_gaps = False)
plt.show()

输出:

enter image description here


感谢您提供的运行示例和对文档的调查!我没有注意到在matplotlib中没有拉伸文本的功能(是的,我知道如果有这个功能,实际上没有人会使用它)。这个示例对我很有用,而且还不错!但我担心文本顶部的边距。这个能调整吗?创建序列标志时,需要填充顶部、底部、左侧和右侧字符之间的边距,因此如果可以更改该部分,那将非常有帮助。 - Iwano Natsuki
@IwanoNatsuki 这个边距实际上来自于字形本身,也就是说,在字体文件中绘制的符号的边界框内包含了顶部的这个间隙。这意味着不同的字体文件可能包含或不包含此间隙。您可以尝试其他字体。我不知道一般情况下不同种类的字体需要去除多少间隙。此外,对于同一字体文件中的某些Unicode字母,可能存在具有此间隙和不具有此间隙的情况。例如,如果您有大写字母H,则其间隙将较小,而对于小写字母m,间隙将更大。 - Arty
@IwanoNatsuki 一种消除这个间隙的方法是使用numpy搜索背景相等的行并将它们删除。如果您愿意,我可以帮您完成此操作。但这意味着在这种情况下,所有大写字母的大小都与小写字母相同,如果您可以接受的话。 - Arty
我明白了。后者的解决方法,即搜索字符边界,对我来说看起来不错。序列标识,特别是共识标识像[这个](https://upload.wikimedia.org/wikipedia/commons/8/85/LexA_gram_positive_bacteria_sequence_logo.png),只有4个字符; A、T、G和C。与“ I ”不同,当宽度和高度固定为相同大小时,它们看起来很好。实现容易吗?看起来有点棘手的实现... - Iwano Natsuki
1
@IwanoNatsuki 我已更新我的代码,支持有和没有间隙的情况,并在我的回答中附上了图片。请查看一下,如果符合您的要求! - Arty
这太棒了!这正是我想做的事情!非常遗憾我不能给你超过一个赞,但这真的救了我的命。谢谢! - Iwano Natsuki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接