Enregistrement des figures interactives de Matplotlib

119

Existe-t-il un moyen de sauvegarder une figure Matplotlib de sorte qu'elle puisse être rouverte et avoir une interaction typique restaurée? (Comme le format .fig dans MATLAB?)

Je me retrouve à exécuter les mêmes scripts plusieurs fois pour générer ces figures interactives. Ou j'envoie à mes collègues plusieurs fichiers PNG statiques pour montrer différents aspects d'un tracé. Je préfère envoyer l'objet figure et les faire interagir avec lui-même.

Mat
la source

Réponses:

30

Ce serait une fonctionnalité intéressante, mais AFAIK n'est pas implémentée dans Matplotlib et serait probablement difficile à implémenter vous-même en raison de la façon dont les chiffres sont stockés.

Je suggérerais soit (a) de séparer le traitement des données de la génération de la figure (qui enregistre les données avec un nom unique) et d'écrire un script de génération de figure (charger un fichier spécifié des données enregistrées) et de le modifier comme bon vous semble ou (b ) enregistrez au format PDF / SVG / PostScript et modifiez-les dans un éditeur de figures sophistiqué comme Adobe Illustrator (ou Inkscape ).

EDIT post Automne 2012 : Comme d'autres l'ont souligné ci-dessous (bien que mentionnant ici car c'est la réponse acceptée), Matplotlib depuis la version 1.2 vous a permis de décaper les chiffres. Comme l' indiquent les notes de publication , il s'agit d'une fonctionnalité expérimentale et ne prend pas en charge l'enregistrement d'une figure dans une version de matplotlib et son ouverture dans une autre. Il est également généralement peu sûr de restaurer un cornichon à partir d'une source non fiable.

Pour le partage / l'édition ultérieure des parcelles (qui nécessitent d'abord un traitement de données important et peuvent devoir être peaufinées des mois plus tard, par exemple lors de l'examen par les pairs pour une publication scientifique), je recommande toujours le flux de travail de (1) avoir un script de traitement de données qui avant de générer un graphique enregistre les données traitées (qui vont dans votre tracé) dans un fichier, et (2) ont un script de génération de tracé séparé (que vous ajustez si nécessaire) pour recréer le tracé. De cette façon, pour chaque tracé, vous pouvez rapidement exécuter un script et le régénérer (et copier rapidement les paramètres de votre tracé avec de nouvelles données). Cela dit, le décapage d'une figure peut être pratique pour une analyse de données à court terme / interactive / exploratoire.

dr jimbob
la source
2
Un peu surpris que cela ne soit pas implémenté. Mais d'accord, je vais enregistrer les données traitées dans un fichier intermédiaire et envoyer cela et un script pour le traçage à des collègues. Merci.
Matt
2
Je soupçonne que la mise en œuvre est difficile, c'est pourquoi cela fonctionne si mal un MATLAB. À l'époque où je l'ai utilisé, les chiffres faisaient planter MATLAB, et même des versions légèrement différentes ne pouvaient pas lire les fichiers .fig les uns des autres.
Adrian Ratnapala
6
picklefonctionne maintenant sur les figures MPL, donc cela peut être fait et semble fonctionner raisonnablement bien - presque comme un fichier de figures Matlab ".fig". Voir ma réponse ci-dessous (pour le moment?) Pour un exemple de la façon de le faire.
Demis
@Demis: comme ptomato l'a souligné dans sa réponse ci-dessous, il existait déjà à cette époque.
strpeter
@strpeter - Les chiffres Matlab n'étaient pas décapables en 2010 comme indiqué dans ce commentaire . La fonctionnalité expérimentale a été ajoutée avec matplotlib 1.2 publié à l'automne 2012 . Comme indiqué ici, vous ne devriez pas vous attendre à ce que cela fonctionne entre les versions de matplotlib et vous ne devriez pas ouvrir les pickles provenant d'une source non fiable.
dr jimbob
63

Je viens de découvrir comment faire cela. Le "support de cornichon expérimental" mentionné par @pelson fonctionne assez bien.

Essaye ça:

# Plot something
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
ax.plot([1,2,3],[10,-10,30])

Après votre peaufinage interactif, enregistrez l'objet figure sous forme de fichier binaire:

import pickle
pickle.dump(fig, open('FigureObject.fig.pickle', 'wb')) # This is for Python 3 - py2 may need `file` instead of `open`

Plus tard, ouvrez la figure et les modifications doivent être enregistrées et l'interactivité de l'interface graphique doit être présente:

import pickle
figx = pickle.load(open('FigureObject.fig.pickle', 'rb'))

figx.show() # Show the figure, edit it, etc.!

Vous pouvez même extraire les données des tracés:

data = figx.axes[0].lines[0].get_data()

(Cela fonctionne pour les lignes, pcolor et imshow - pcolormesh fonctionne avec quelques astuces pour reconstruire les données aplaties .)

J'ai eu l'excellent conseil de Saving Matplotlib Figures Using Pickle .

Demis
la source
Je pense que ce n'est pas robuste aux changements de version, etc., et pas de compatibilité croisée entre py2.x et py3.x. De plus, je pensais que la pickledocumentation indiquait que nous devions configurer l'environnement de la même manière que lorsque l'objet était pickled (enregistré), mais j'ai trouvé que vous n'en avez pas besoin import matplotlib.pyplot as pltlors du décapage (chargement) - il enregistre les instructions d'importation dans le fichier pickled .
Demis
5
Vous devriez envisager de fermer les fichiers ouverts: par exemplewith open('FigureObject.fig.pickle', 'rb') as file: figx = pickle.load(file)
strpeter
1
Je viens d'obtenir: 'AttributeError: l'objet' Figure 'n'a pas d'attribut' _cachedRenderer ''
user2673238
Si vous ne voulez pas que le script continue et se termine probablement immédiatement après figx.show(), vous devez appeler à la plt.show()place, ce qui bloque.
maechler
38

Depuis Matplotlib 1.2, nous avons maintenant un support expérimental de pickle . Essayez-le et voyez si cela fonctionne bien pour votre cas. Si vous rencontrez des problèmes, veuillez nous en informer sur la liste de diffusion Matplotlib ou en ouvrant un numéro sur github.com/matplotlib/matplotlib .

Pelson
la source
2
N'importe quelle raison pour laquelle cette fonction utile pourrait être ajoutée au "Enregistrer sous" de la figure elle-même. Ajouter .pkl peut-être?
dashesy le
Bonne idée @dashesy. Je soutiendrais cela si vous vouliez essayer sa mise en œuvre?
pelson
1
Cela ne fonctionne-t-il que sur un sous-ensemble de backends? Quand j'essaye de décaper une figure simple dans OSX, j'obtiens PicklingError: Can't pickle <type '_macosx.GraphicsContext'>: it's not found as _macosx.GraphicsContext.
farenorth le
Ce qui précède PicklingErrorne se produit que si vous appelez plt.show()avant de faire le cornichon. Alors placez juste plt.show()après pickle.dump().
salomonvh
Sur mon py3.5 sur MacOS 10.11, l'ordre de fig.show()ne semble pas avoir d'importance - peut-être que ce bogue a été corrigé. Je peux décaper avant / après show()sans problème.
Demis
7

Pourquoi ne pas simplement envoyer le script Python? Les fichiers .fig de MATLAB nécessitent que le destinataire dispose de MATLAB pour les afficher, c'est donc à peu près équivalent à l'envoi d'un script Python qui nécessite l'affichage de Matplotlib.

Alternativement (avertissement: je n'ai pas encore essayé cela), vous pouvez essayer de décaper la figure:

import pickle
output = open('interactive figure.pickle', 'wb')
pickle.dump(gcf(), output)
output.close()
ptomate
la source
3
Malheureusement, les figures matplotlib ne peuvent pas être décapées, donc cette approche ne fonctionnera pas. Dans les coulisses, il y a trop d'extensions C qui ne prennent pas en charge le décapage. Je suis tout à fait d'accord pour envoyer simplement le script + les données, cependant ... Je suppose que je n'ai jamais vraiment vu l'intérêt des fichiers .fig enregistrés par matlab, donc je ne les ai jamais utilisés. Envoyer à quelqu'un du code et des données autonomes a été le plus simple à long terme, de toute façon d'après mon expérience. Pourtant, ce serait bien si les objets de la figure de matplotlib pouvaient être décapés.
Joe Kington
1
Même nos données prétraitées sont assez volumineuses et la procédure de traçage est complexe. Cela semble être le seul recours. Merci.
Matt
1
Les chiffres sont apparemment maintenant décapables - cela fonctionne plutôt bien! Exemple ci-dessous.
Demis
L'avantage de pickle est que vous n'avez pas à ajuster par programme tous les espacements / positions des figures / sous-tracés. Vous pouvez utiliser l'interface graphique du tracé MPL pour rendre la figure agréable, etc., puis enregistrer le MyPlot.fig.picklefichier - en conservant la possibilité ultérieure d'ajuster la présentation du tracé si nécessaire. C'est aussi ce qui est génial avec les .figfichiers Matlab . Particulièrement utile lorsque vous devez modifier le rapport taille / format d'une figue (pour l'insertion dans des présentations / papiers).
Demis
1

Bonne question. Voici le texte du doc ​​de pylab.save:

pylab ne fournit plus de fonction de sauvegarde, bien que l'ancienne fonction pylab soit toujours disponible sous le nom matplotlib.mlab.save (vous pouvez toujours vous y référer dans pylab sous le nom de "mlab.save"). Cependant, pour les fichiers texte brut, nous recommandons numpy.savetxt. Pour enregistrer des tableaux numpy, nous vous recommandons numpy.save et son analogique numpy.load, qui sont disponibles dans pylab sous les noms np.save et np.load.

Steve Tjoa
la source
Cela enregistre les données de l'objet a pylab, mais ne vous permet pas de régénérer la figure.
dr jimbob
Correct. Je devrais préciser que la réponse n'était pas une recommandation à utiliser pylab.save. En fait, d'après le texte du doc, il semble qu'il ne faut pas l' utiliser.
Steve Tjoa
Existe-t-il une méthode externe pour envoyer une figure 3D? Possible même une simple interface graphique pour exe ..
CromeX
0

J'ai trouvé un moyen relativement simple (mais un peu non conventionnel) de sauvegarder mes figurines matplotlib. Cela fonctionne comme ceci:

import libscript

import matplotlib.pyplot as plt
import numpy as np

t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2*np.pi*t)

#<plot>
plt.plot(t, s)
plt.xlabel('time (s)')
plt.ylabel('voltage (mV)')
plt.title('About as simple as it gets, folks')
plt.grid(True)
plt.show()
#</plot>

save_plot(fileName='plot_01.py',obj=sys.argv[0],sel='plot',ctx=libscript.get_ctx(ctx_global=globals(),ctx_local=locals()))

avec la fonction save_plotdéfinie comme ceci (version simple pour comprendre la logique):

def save_plot(fileName='',obj=None,sel='',ctx={}):
    """
    Save of matplolib plot to a stand alone python script containing all the data and configuration instructions to regenerate the interactive matplotlib figure.

    Parameters
    ----------
    fileName : [string] Path of the python script file to be created.
    obj : [object] Function or python object containing the lines of code to create and configure the plot to be saved.
    sel : [string] Name of the tag enclosing the lines of code to create and configure the plot to be saved.
    ctx : [dict] Dictionary containing the execution context. Values for variables not defined in the lines of code for the plot will be fetched from the context.

    Returns
    -------
    Return ``'done'`` once the plot has been saved to a python script file. This file contains all the input data and configuration to re-create the original interactive matplotlib figure.
    """
    import os
    import libscript

    N_indent=4

    src=libscript.get_src(obj=obj,sel=sel)
    src=libscript.prepend_ctx(src=src,ctx=ctx,debug=False)
    src='\n'.join([' '*N_indent+line for line in src.split('\n')])

    if(os.path.isfile(fileName)): os.remove(fileName)
    with open(fileName,'w') as f:
        f.write('import sys\n')
        f.write('sys.dont_write_bytecode=True\n')
        f.write('def main():\n')
        f.write(src+'\n')

        f.write('if(__name__=="__main__"):\n')
        f.write(' '*N_indent+'main()\n')

return 'done'

ou définir une fonction save_plotcomme celle-ci (meilleure version utilisant la compression zip pour produire des fichiers de figures plus légers):

def save_plot(fileName='',obj=None,sel='',ctx={}):

    import os
    import json
    import zlib
    import base64
    import libscript

    N_indent=4
    level=9#0 to 9, default: 6
    src=libscript.get_src(obj=obj,sel=sel)
    obj=libscript.load_obj(src=src,ctx=ctx,debug=False)
    bin=base64.b64encode(zlib.compress(json.dumps(obj),level))

    if(os.path.isfile(fileName)): os.remove(fileName)
    with open(fileName,'w') as f:
        f.write('import sys\n')
        f.write('sys.dont_write_bytecode=True\n')
        f.write('def main():\n')
        f.write(' '*N_indent+'import base64\n')
        f.write(' '*N_indent+'import zlib\n')
        f.write(' '*N_indent+'import json\n')
        f.write(' '*N_indent+'import libscript\n')
        f.write(' '*N_indent+'bin="'+str(bin)+'"\n')
        f.write(' '*N_indent+'obj=json.loads(zlib.decompress(base64.b64decode(bin)))\n')
        f.write(' '*N_indent+'libscript.exec_obj(obj=obj,tempfile=False)\n')

        f.write('if(__name__=="__main__"):\n')
        f.write(' '*N_indent+'main()\n')

return 'done'

Cela utilise libscriptmon propre module , qui repose principalement sur des modules inspectet ast. Je peux essayer de le partager sur Github si un intérêt est exprimé (cela nécessiterait d'abord un nettoyage et moi pour commencer avec Github).

L'idée derrière cette save_plotfonction et ce libscriptmodule est de récupérer les instructions python qui créent la figure (en utilisant le module inspect), de les analyser (en utilisant le module ast) pour extraire toutes les variables, fonctions et modules sur lesquels il s'appuie, les extraire du contexte d'exécution et les sérialiser comme instructions python (le code pour les variables sera comme t=[0.0,2.0,0.01]... et le code pour les modules sera comme import matplotlib.pyplot as plt...) ajouté aux instructions de la figure. Les instructions python résultantes sont enregistrées en tant que script python dont l'exécution reconstruira la figure originale de matplotlib.

Comme vous pouvez l'imaginer, cela fonctionne bien pour la plupart (sinon tous) des figures matplotlib.

Astrum42
la source