2013-10-14 130 views
8

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

事情是这样的:Draw rotated text on curved path

但使用matplotlib。

+0

你有没有解决过这个问题?我自己需要它。谢谢。 –

+0

@ tommy.carstensen - 我无法解决这个问题:( – deepak

+0

@tommy。carstensen - 看起来像有人发现答案:) (我不需要它了,虽然) – deepak

回答

8

这是我对这个问题: 为了使文字强劲绘图后的图的调整,我得到一个子类,CurvedText,从matplotlib.textCurvedText对象以x - 和y-值数组的形式获取字符串和曲线。要显示的文本本身被分割成单独的字符,每个字符都被添加到适当位置的图中。由于matplotlib.text如果字符串是空的,我什么也没画,我用不可见的'a's替换所有的空格。在调整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],' ', axes, **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() 

结果看起来是这样的:

curved text in matplotlib

还存在一些问题,当文本遵循急剧地转向曲线的凹侧。这是因为角色沿着曲线“缝合在一起”而没有考虑重叠。如果我有时间,我会尽力改进。任何意见都非常受欢迎。

测试python 3.5和2.7

+0

嘿,虽然我不需要答案了,我真的很感激你的答案!这正是我想要的 - 4年前! 希望别人认为它有用:) – deepak

+0

@ThomasKühn:很好地使用派生类,非常简洁的答案,+1!我建议一些编辑与python 2.7完全兼容。它们应该在编辑队列中可见。 – Daan

+0

@达安感谢您的编辑。 –

4

,我发现你的问题很有趣,所以我做了一些东西,出来的结果很接近使用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)) 

example output

实施远非完美,但它是一个很好的起点指出我的看法。

此外,看起来matplotlib中有一些关于标记旋转的散点图,这对于这种情况是理想的。然而,我的编程技巧几乎不像他们需要解决这个问题那样顽固,所以我无法在这里帮忙。

matplotlib on github: pull request

matplotlib on github: issue