2013-10-14 3 views
8

В проекте, который я делаю, я должен взять пользовательский ввод из структурированного файла (xml). Файл содержит дорожные данные области, которую я должен наложить на холст matplotlib. Проблема в том, что наряду с дорогой, я также должен сделать название дороги, и большая часть дорог изогнута. Я знаю, как сделать текст под углом. Но мне было интересно, можно ли изменить угол текста в середине строки?Изогнутый рендеринг текста в matplotlib

Что-то вроде этого: Draw rotated text on curved path

Но использование Matplotlib.

+0

ли вы когда-нибудь решить эту проблему? Мне это нужно. Благодарю. –

+0

@ tommy.carstensen - Мне не удалось это решить :( – deepak

+0

@tommy.carstensen - похоже, кто-то нашел ответ :) (мне это больше не нужно) – deepak

ответ

8

Вот мой взгляд на проблему: Для того, чтобы сделать текст прочным, чтобы понять корректировки после того, как рисунок, я вывести дочерний класс, CurvedText из matplotlib.text. Объект CurvedText берет строку и кривую в виде x - и y -значения массивов. Текст, который будет отображаться сам, вырезается на отдельные символы, каждый из которых добавляется к сюжету в соответствующей позиции. Поскольку 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],' ', 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

@Thomas Kühn: Хорошее использование производного класса, очень аккуратный ответ, +1! Я предложил некоторые изменения для полной совместимости с python 2.7. Они должны быть видны в очереди редактирования. – Daan

+0

@Daan Спасибо за редактирование. –

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

Смежные вопросы