2013-10-14 13 views
8

In un progetto che sto facendo, devo prendere in input un utente da un file strutturato (xml). Il file contiene i dati stradali di un'area, che devo tracciare sul canvas matplotlib. Il problema è che, insieme alla strada, devo anche rendere il nome della strada, e la maggior parte delle strade sono curve. So come rendere il testo in un angolo. Ma mi stavo chiedendo se è possibile modificare l'angolo del testo a metà della stringa?Rendering di testo curvo in matplotlib

Qualcosa di simile a questo: Draw rotated text on curved path

Ma usando matplotlib.

+0

Hai mai risolvere questo problema? Ne ho bisogno io stesso. Grazie. –

+0

@ tommy.carstensen - Non è stato possibile risolvere questo problema :( – deepak

+0

@tommy.carstensen - sembra che qualcuno abbia trovato la risposta :) (non ne ho più bisogno però) – deepak

risposta

8

Ecco il mio assumere il problema: Al fine di rendere il testo robusto per capire le regolazioni dopo il disegno, mi deriva una classe figlia, CurvedText, da matplotlib.text. L'oggetto CurvedText accetta una stringa e una curva sotto forma di serie x - e y -valore. Il testo da visualizzare viene tagliato in caratteri separati, ciascuno dei quali viene aggiunto al grafico nella posizione appropriata. Come matplotlib.text non disegna nulla se la stringa è vuota, sostituisco tutti gli spazi con 'invisibili'. Con la regolazione figure, il sovraccarico draw() chiama la funzione update_positions(), che fa in modo che le posizioni e gli orientamenti dei caratteri rimangano corretti. Per assicurare l'ordine di chiamata (verrà chiamata anche la funzione draw() di ciascun carattere), l'oggetto CurvedText si preoccupa anche del fatto che lo zorder di ciascun carattere sia superiore al proprio zorder. Seguendo il mio esempio here, il testo può avere qualsiasi allineamento. Se il testo non può essere adattato alla curva alla risoluzione corrente, il resto verrà nascosto, ma verrà visualizzato al ridimensionamento. Di seguito è riportato il codice con un esempio di applicazione.

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() 

Il risultato è simile:

curved text in matplotlib

Ci sono ancora alcuni problemi, quando il testo segue la parte concava di una curva bruscamente piegatura. Questo perché i personaggi sono "cuciti insieme" lungo la curva senza tenere conto della sovrapposizione. Se avrò tempo, cercherò di migliorare su questo. Qualsiasi commento è molto benvenuto.

provata su python 3.5 e 2.7

+0

Ehi, mentre non ho più bisogno della risposta, apprezzo molto la tua risposta! Era proprio quello che stavo cercando - 4 anni fa! Spero che qualcun altro lo trovi utile :) – deepak

+0

@Thomas Kühn: buon uso di una classe derivata, risposta molto accurata, +1! Ho suggerito alcune modifiche per avere piena compatibilità con python 2.7. Dovrebbero essere visibili nella coda di modifica. – Daan

+0

@Daan Grazie per la modifica. –

4

Ho trovato il vostro problema molto interessante, quindi ho fatto una cosa che è molto vicino con lo strumento di testo 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

L'implementazione è ben lungi dall'essere perfetto, ma è un buon inizio punto secondo me.

Inoltre, sembra che lo sviluppo di matplotlib abbia uno scatterplot con rotazione dei marker, che sarebbe ideale per questo caso. Tuttavia, le mie capacità di programmazione non sono così difficili come devono essere per affrontare questo problema, quindi non posso aiutare qui.

matplotlib on github: pull request

matplotlib on github: issue

Problemi correlati