【问题标题】:Curved text rendering in matplotlibmatplotlib 中的曲线文本渲染
【发布时间】:2013-10-14 04:33:23
【问题描述】:

在我正在做的一个项目中,我必须从结构化文件 (xml) 中获取用户输入。该文件包含一个区域的道路数据,我必须将其绘制到 matplotlib 画布上。问题是随着道路,我还要渲染道路名称,而且大部分道路都是弯曲的。我知道如何以某个角度呈现文本。但我想知道是否可以在字符串中途更改文本角度?

类似这样的东西:Draw rotated text on curved path

但是使用 matplotlib。

【问题讨论】:

  • 你解决过这个问题吗?我自己需要。谢谢。
  • @tommy.carstensen - 我无法解决这个问题:(
  • @tommy.carstensen - 好像有人找到了答案 :) (不过我不再需要它了)

标签: python matplotlib


【解决方案1】:

这是我对这个问题的看法: 为了使文本在绘制后对图形调整具有鲁棒性,我从matplotlib.text 派生了一个子类CurvedTextCurvedText 对象采用 x- 和 y-value 数组形式的字符串和曲线。要显示的文本本身被切割成单独的字符,每个字符都被添加到绘图的适当位置。如果字符串为空,matplotlib.text 不会绘制任何内容,因此我将所有空格替换为不可见的 'a'。在figure 调整时,重载的draw() 调用update_positions() 函数,该函数负责确保字符位置和方向保持正确。为了确保调用顺序(每个字符的draw() 函数也将被调用)CurvedText 对象还注意每个字符的zorder 高于它自己的zorder。按照我的示例here,文本可以有任何对齐方式。如果文本在当前分辨率下无法适应曲线,其余部分将被隐藏,但会在调整大小时出现。下面是带有应用程序示例的代码。

from matplotlib import pyplot as plt
from matplotlib import patches
from matplotlib import text as mtext
import numpy as np
import math

class CurvedText(mtext.Text):
    """
    A text object that follows an arbitrary curve.
    """
    def __init__(self, x, y, text, axes, **kwargs):
        super(CurvedText, self).__init__(x[0],y[0],' ', **kwargs)

        axes.add_artist(self)

        ##saving the curve:
        self.__x = x
        self.__y = y
        self.__zorder = self.get_zorder()

        ##creating the text objects
        self.__Characters = []
        for c in text:
            if c == ' ':
                ##make this an invisible 'a':
                t = mtext.Text(0,0,'a')
                t.set_alpha(0.0)
            else:
                t = mtext.Text(0,0,c, **kwargs)

            #resetting unnecessary arguments
            t.set_ha('center')
            t.set_rotation(0)
            t.set_zorder(self.__zorder +1)

            self.__Characters.append((c,t))
            axes.add_artist(t)


    ##overloading some member functions, to assure correct functionality
    ##on update
    def set_zorder(self, zorder):
        super(CurvedText, self).set_zorder(zorder)
        self.__zorder = self.get_zorder()
        for c,t in self.__Characters:
            t.set_zorder(self.__zorder+1)

    def draw(self, renderer, *args, **kwargs):
        """
        Overload of the Text.draw() function. Do not do
        do any drawing, but update the positions and rotation
        angles of self.__Characters.
        """
        self.update_positions(renderer)

    def update_positions(self,renderer):
        """
        Update positions and rotations of the individual text elements.
        """

        #preparations

        ##determining the aspect ratio:
        ##from https://stackoverflow.com/a/42014041/2454357

        ##data limits
        xlim = self.axes.get_xlim()
        ylim = self.axes.get_ylim()
        ## Axis size on figure
        figW, figH = self.axes.get_figure().get_size_inches()
        ## Ratio of display units
        _, _, w, h = self.axes.get_position().bounds
        ##final aspect ratio
        aspect = ((figW * w)/(figH * h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0])

        #points of the curve in figure coordinates:
        x_fig,y_fig = (
            np.array(l) for l in zip(*self.axes.transData.transform([
            (i,j) for i,j in zip(self.__x,self.__y)
            ]))
        )

        #point distances in figure coordinates
        x_fig_dist = (x_fig[1:]-x_fig[:-1])
        y_fig_dist = (y_fig[1:]-y_fig[:-1])
        r_fig_dist = np.sqrt(x_fig_dist**2+y_fig_dist**2)

        #arc length in figure coordinates
        l_fig = np.insert(np.cumsum(r_fig_dist),0,0)

        #angles in figure coordinates
        rads = np.arctan2((y_fig[1:] - y_fig[:-1]),(x_fig[1:] - x_fig[:-1]))
        degs = np.rad2deg(rads)


        rel_pos = 10
        for c,t in self.__Characters:
            #finding the width of c:
            t.set_rotation(0)
            t.set_va('center')
            bbox1  = t.get_window_extent(renderer=renderer)
            w = bbox1.width
            h = bbox1.height

            #ignore all letters that don't fit:
            if rel_pos+w/2 > l_fig[-1]:
                t.set_alpha(0.0)
                rel_pos += w
                continue

            elif c != ' ':
                t.set_alpha(1.0)

            #finding the two data points between which the horizontal
            #center point of the character will be situated
            #left and right indices:
            il = np.where(rel_pos+w/2 >= l_fig)[0][-1]
            ir = np.where(rel_pos+w/2 <= l_fig)[0][0]

            #if we exactly hit a data point:
            if ir == il:
                ir += 1

            #how much of the letter width was needed to find il:
            used = l_fig[il]-rel_pos
            rel_pos = l_fig[il]

            #relative distance between il and ir where the center
            #of the character will be
            fraction = (w/2-used)/r_fig_dist[il]

            ##setting the character position in data coordinates:
            ##interpolate between the two points:
            x = self.__x[il]+fraction*(self.__x[ir]-self.__x[il])
            y = self.__y[il]+fraction*(self.__y[ir]-self.__y[il])

            #getting the offset when setting correct vertical alignment
            #in data coordinates
            t.set_va(self.get_va())
            bbox2  = t.get_window_extent(renderer=renderer)

            bbox1d = self.axes.transData.inverted().transform(bbox1)
            bbox2d = self.axes.transData.inverted().transform(bbox2)
            dr = np.array(bbox2d[0]-bbox1d[0])

            #the rotation/stretch matrix
            rad = rads[il]
            rot_mat = np.array([
                [math.cos(rad), math.sin(rad)*aspect],
                [-math.sin(rad)/aspect, math.cos(rad)]
            ])

            ##computing the offset vector of the rotated character
            drp = np.dot(dr,rot_mat)

            #setting final position and rotation:
            t.set_position(np.array([x,y])+drp)
            t.set_rotation(degs[il])

            t.set_va('center')
            t.set_ha('center')

            #updating rel_pos to right edge of character
            rel_pos += w-used




if __name__ == '__main__':
    Figure, Axes = plt.subplots(2,2, figsize=(7,7), dpi=100)


    N = 100

    curves = [
        [
            np.linspace(0,1,N),
            np.linspace(0,1,N),
        ],
        [
            np.linspace(0,2*np.pi,N),
            np.sin(np.linspace(0,2*np.pi,N)),
        ],
        [
            -np.cos(np.linspace(0,2*np.pi,N)),
            np.sin(np.linspace(0,2*np.pi,N)),
        ],
        [
            np.cos(np.linspace(0,2*np.pi,N)),
            np.sin(np.linspace(0,2*np.pi,N)),
        ],
    ]

    texts = [
        'straight lines work the same as rotated text',
        'wavy curves work well on the convex side',
        'you even can annotate parametric curves',
        'changing the plotting direction also changes text orientation',
    ]

    for ax, curve, text in zip(Axes.reshape(-1), curves, texts):
        #plotting the curve
        ax.plot(*curve, color='b')

        #adjusting plot limits
        stretch = 0.2
        xlim = ax.get_xlim()
        w = xlim[1] - xlim[0]
        ax.set_xlim([xlim[0]-stretch*w, xlim[1]+stretch*w])
        ylim = ax.get_ylim()
        h = ylim[1] - ylim[0]
        ax.set_ylim([ylim[0]-stretch*h, ylim[1]+stretch*h])

        #adding the text
        text = CurvedText(
            x = curve[0],
            y = curve[1],
            text=text,#'this this is a very, very long text',
            va = 'bottom',
            axes = ax, ##calls ax.add_artist in __init__
        )

    plt.show()

结果如下:

仍有一些问题,当文字沿着急剧弯曲的曲线的凹边时。这是因为字符沿曲线“缝合在一起”而没有考虑重叠。如果我有时间,我会努力改进。任何 cmets 都非常受欢迎。

python 3.5 和 2.7 上测试

【讨论】:

  • 嘿,虽然我不再需要答案了,但非常感谢您的回答!这正是我所寻找的——4 年前!希望其他人觉得它有用:)
  • @Thomas Kühn:很好地使用派生类,非常简洁的答案,+1!我建议进行一些编辑以与 python 2.7 完全兼容。它们应该在编辑队列中可见。
  • 成功了,我有一个相关的问题,但还没有解决。请你看一下,我不知道如何私信你。谢谢。stackoverflow.com/q/48225888/2525479
  • super(CurvedText, self).__init__(x[0],y[0],' ', axes, **kwargs) 应该是 super(CurvedText, self).__init__(x[0],y[0],' ', **kwargs),否则 axes 将作为颜色参数传递给 Text
  • +1,我发现这个解决方案非常有用!但是,如果我将图形保存为 .pdf,您能解释为什么文本的位置不同吗?一般来说,我发现保存时我需要使用低约 25% 的 rel_pos 值,以便将文本放在同一个位置。此外,如果我在保持rel_pos 的值的同时更改行的len,则文本的位置会发生变化,这是我所期望的,但只有在某些情况下。我不明白这个?保存语句中dpi 的值不会改变任何内容。
【解决方案2】:

我发现你的问题很有趣,所以我使用 matplotlib 文本工具做了一些非常接近的东西:

from __future__ import division
import itertools
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

# define figure and axes properties
fig, ax = plt.subplots(figsize=(8,6))
ax.set_xlim(left=0, right=10)
ax.set_ylim(bottom=-1.5, top=1.5)
(xmin, xmax), (ymin, ymax) = ax.get_xlim(), ax.get_ylim()

# calculate a shape factor, more explanation on usage further
# it is a representation of the distortion of the actual image compared to a 
# cartesian space:
fshape = abs(fig.get_figwidth()*(xmax - xmin)/(ymax - ymin)/fig.get_figheight())

# the text you want to plot along your line
thetext = 'the text is flowing      '

# generate a cycler, so that the string is cycled through
lettercycler = itertools.cycle(tuple(thetext))

# generate dummy river coordinates
xvals = np.linspace(1, 10, 300)
yvals = np.sin(xvals)**3

# every XX datapoints, a character is printed
markerevery = 10

# calculate the rotation angle for the labels (in degrees)
# the angle is calculated as the slope between two datapoints.
# it is then multiplied by a shape factor to get from the angles in a
# cartesian space to the angles in this figure
# first calculate the slope between two consecutive points, multiply with the
# shape factor, get the angle in radians with the arctangens functions, and
# convert to degrees
angles = np.rad2deg(np.arctan((yvals[1:]-yvals[:-1])/(xvals[1:]-xvals[:-1])*fshape))

# plot the 'river'
ax.plot(xvals, yvals, 'b', linewidth=3)

# loop over the data points, but only plot a character every XX steps
for counter in np.arange(0, len(xvals)-1, step=markerevery):
    # plot the character in between two datapoints
    xcoord = (xvals[counter] + xvals[counter+1])/2.
    ycoord = (yvals[counter] + yvals[counter+1])/2.

    # plot using the text method, set the rotation so it follows the line,
    # aling in the center for a nicer look, optionally, a box can be drawn
    # around the letter
    ax.text(xcoord, ycoord, lettercycler.next(),
            fontsize=25, rotation=angles[counter],
            horizontalalignment='center', verticalalignment='center',
            bbox=dict(facecolor='white', edgecolor='white', alpha=0.5))

实现远非完美,但在我看来这是一个很好的起点。

此外,matplotlib 中似乎有一些关于标记旋转散点图的发展,这对于这种情况来说是理想的。但是,我的编程技能几乎没有达到解决这个问题所需的核心水平,所以在这里我无能为力。

matplotlib on github: pull request

matplotlib on github: issue

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-29
    • 2011-02-17
    • 1970-01-01
    • 2016-07-26
    • 1970-01-01
    • 2012-01-30
    相关资源
    最近更新 更多