Lors de mes présentations orales, j'ai diffusé une animation réalisée à l'aide du moteur Manim, dont l'excellente chaîne 3Blue1Brown se sert pour mettre en image des concepts mathématiques

La vidéo a été étoffée depuis la première présentation. C'est ici l'occasion d'expliquer comment générer des animations dans ce genre.


La méthode Hagerman et Olofsson offre la possibilité de séparer les sources de parole et de bruit en sortie d'aide auditive, afin d'en étudier les effets distincts. Il en est beaucoup question dans mes recherches. Elle nécessite deux enregistrements A et B (dont le bruit est en opposition de phase), comme l'expliquent les formules suivantes :

  • l'addition des signaux A et B génère le double du signal, sans le bruit,
  • tandis que la soustraction génère le double du bruit, sans le signal.
{ A = S i g n a l + N o i s e B = S i g n a l N o i s e { A + B = S + N + S N = 2 S A B = S + N S + N = 2 N
hostmath.com

Installation de Manim dans un Notebook Jupyter

Jupyter est une application web de programmation interactive, héritée de iPython et dédiée aux langages interprétés tels que Julia, Python et R - d'où le nom. Quant à Manim il peut être utilisé de deux manières.

  • soit en ligne de commande :
manim -p -ql example.py SquareToCircle
  • soit dans une cellule Jupyter :
%%manim [CLI options] MyAwesomeScene

class MyAweseomeScene(Scene):
    def construct(self):
        ...

Dans les deux cas, on obtiendra un fichier vidéo .mp4. Pour installer Manim sous Debian, il faudra commencer par ajouter les dépendances :

apt install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg

Puis utiliser le gestionnaire de paquets pip, ou conda (ou encore son alternative ultra rapide mamba) pour ajouter le programme en question :

conda install manim

+ de détails

Code source

Ci-dessous, le code saisi dans une cellule Jupyter pour générer la première partie de la vidéo. Dans cette "Scene", on utilise les fonctions suivantes :

  • Write pour faire apparaître le texte progressivement
  • FadeOut pour le faire disparaître en fondu
  • Axes pour générer un repère orthogonal
  • Scale pour mettre l'objet à l'échelle
  • Shift pour décaler l'objet
  • Create pour dessiner l'objet progressivement
  • Flip pour retourner l'objet
  • Stretch pour étirer l'objet

Enfin, la fonction sinus provient du paquet numpy. Idem pour la fonction random qui permet de générer du bruit aléatoire.

%%manim -ql HagermanOlofsson

class HagermanOlofsson(Scene):

    def construct(self):

        title1 = Text("La méthode", font="Ubuntu")
        title2 = Text("Hagerman & Olofsson", font="Ubuntu").next_to(title1, DOWN)
        self.play(Write(title1), Write(title2))
        self.wait(2)
        self.play(FadeOut(title1), FadeOut(title2))

        title3 = Text("L'addition des signaux de parole avec le bruit en opposition de phase", font="Ubuntu").scale(0.6)
        title4 = Text("donne le double du signal, sans le bruit...", font="Ubuntu").scale(0.6).next_to(title3, DOWN)
        self.play(Write(title3), Write(title4))
        self.wait(3)
        self.play(FadeOut(title3), FadeOut(title4))

        axes_up = Axes(
            x_range=[0, 12.56, 1],
            y_range= [-2, 2, 1],
            #axes_color=GRAY,
        )
        axes_up.scale(0.5).shift(1.8 * UP)

        axes_down = Axes(
            x_range=[0, 12.56, 1],
            y_range= [-2, 2, 1],
            #axes_color=GRAY,
        )
        axes_down.scale(0.5).shift(2 * DOWN)

        labels_up = axes_up.get_axis_labels(
            Tex(r"\tiny{temps [s]}"), Tex(r"\tiny{niveau [dB]}")
        )
        labels_down = axes_down.get_axis_labels(
            Tex(r"\tiny{temps [s]}"), Tex(r"\tiny{niveau [dB]}")
        )

        #Graph Up
        self.play(Create(axes_up), run_time = 2)
        self.play(Create(labels_up), run_time = 1)

        graph_up = axes_up.plot(lambda x : np.sin(x), color = GOLD_A)
        signal_A = MathTex(r"signal\ A", color = GOLD_B)
        signal_A.scale(0.9)
        signal_A_coord = axes_up.input_to_graph_point(8.5,graph_up)
        signal_A.next_to(signal_A_coord,RIGHT+UP)

        # Graph Down
        self.play(Create(axes_down), run_time = 2)
        self.play(Create(labels_down), run_time = 1)

        graph_down = axes_down.plot(lambda x : np.sin(x), color = BLUE_D)
        signal_B = MathTex(r"signal\ B", color = BLUE_D)
        signal_B.scale(0.9)
        signal_B_coord = axes_down.input_to_graph_point(8.5,graph_down)
        signal_B.next_to(signal_B_coord,RIGHT+UP)

        noise = axes_up.plot(lambda x : (2*np.random.rand()-1)/2,color = YELLOW_D,)
        noise_label = MathTex(r"bruit", color = YELLOW_D)
        noise_label_coord = axes_up.input_to_graph_point(11,graph_up)
        noise_label.next_to(noise_label_coord,RIGHT+DOWN)

        self.play(Create(graph_up), run_time = 2)
        self.play(Create(signal_A))
        self.play(Create(graph_down), run_time = 2)
        self.play(Create(signal_B))
        self.play(Create(noise))
        self.play(Create(noise_label))

        self.play(noise.animate.stretch_to_fit_height(0.5))
        self.play(noise.animate.stretch_to_fit_height(1.5))
        self.play(noise.animate.stretch_to_fit_height(1))
        self.wait(1)

        noise2=noise.copy()
        noise2.generate_target()
        noise2.target.shift(3.8*DOWN)
        self.play(MoveToTarget(noise2))
        self.wait(1)
        self.play(noise2.animate.flip(RIGHT).set_color(ORANGE))
        self.wait(2)

        plus = MathTex(r"+", color = PURPLE_C)
        plus.scale(4).shift(5*LEFT)
        self.play(Create(plus))

        noise2.generate_target()
        noise2.target.shift(3.8*UP)
        graph_down.generate_target()
        graph_down.target.shift(3.7*UP)

        self.play(FadeOut(axes_down,labels_down,signal_B), MoveToTarget(noise2), MoveToTarget(graph_down))
        self.wait(1)
        signal_AB = MathTex(r"signal\ A + B", color = GREEN_C)
        signal_AB.scale(0.9)
        signal_AB_coord = axes_up.input_to_graph_point(8.5,graph_up)
        signal_AB.next_to(signal_AB_coord,RIGHT+UP)
        self.play(FadeOut(noise, noise2, noise_label, graph_down, plus), graph_up.animate.stretch(2,1).set_color(GREEN_C), TransformMatchingTex(signal_A, signal_AB))
        self.wait(3)

Si vous avez besoin de convertir des fichiers depuis un format de balisage vers un autre, pandoc est votre couteau suisse. Dans notre cas, nous l'utilisons pour exporter les sources Markdown et LaTeX vers des fichiers PDF et HTML.

Par exemple, si nous souhaitons convertir tous les fichiers .md (Markdown) vers un fichier PDF, nous utilisons la commande suivante :

pandoc *.md -o fichier.pdf" --pdf-engine=xelatex --verbose

On utilise ici le moteur de rendu XeLaTeX et une sortie détaillée (verbeuse) de façon à afficher les éventuels erreurs.

Export PDF

L'intérêt d'utiliser pandoc dans un script est d'automatiser tous les paramètres d'export. Voici ce qu'exécute la commande make pdf (comprendre "fabrique-moi le PDF") :

PANDOC=pandoc

BASEDIR=$(CURDIR)
INPUTDIR=$(BASEDIR)/source
OUTPUTDIR=$(BASEDIR)/output
STYLEDIR=$(BASEDIR)/style

METAFILE=$(INPUTDIR)/metadata.yml
BIBFILE=$(INPUTDIR)/bibliographie.bib
CSLFILE=$(STYLEDIR)/apa.csl
FILENAME=memoire_audio_bdx_bl

pdf:
        $(PANDOC) \
        --filter=pandoc-shortcaption \
        --filter=pandoc-xnos \
        --template="$(STYLEDIR)/template.tex" \
        "$(INPUTDIR)"/*.md \
        "$(METAFILE)" \
        -o "$(OUTPUTDIR)/$(FILENAME).pdf" \
        -H "$(STYLEDIR)/preamble.tex" \
        --bibliography="$(BIBFILE)" 2>pandoc.log \
        --csl="$(CSLFILE)" \
        --number-sections \
        --metadata time="`date '+%d %b %Y, %H:%M'`" \
        --highlight-style=tango \
        --pdf-engine=xelatex \
        --verbose

Le contenu des variables en lettre capitale au début du fichier est réutilisé par la suite à l'aide de l'insertion "$(VARIABLE)".

C'est parti pour une explication détaillée des paramètres :

  • --filter=pandoc-shortcaption : affiche les légendes courtes sous les images.
  • --filter=pandoc-xnos : numérote automatiquement les figures, les équations, les tableaux et les sections.
  • --template=template.tex : applique le modèle de fichier LaTeX au fichier généré.
  • *.md : prend tous les fichiers Markdown en entrée.
  • metadata.yml : ce fichier contient le titre, le sous-titre, l'auteur et toutes les informations sur le document lui-même, ainsi que son format de base (A4, en français, de type rapport).
  • -o fichier.pdf : le nom du fichier final.
  • -H preamble.tex : "En-tête" (header) inséré avant toutes les commandes LaTeX.
  • --bibliography=bibliographie.bib : fichier formaté qui contient toutes les références bibliographiques.
  • --csl=apa.csl : modèle de fichier CSL (Citation Style Language) définissant le style des citations.
  • --number-sections : numéroter automatiquement les sections.
  • --metadata time="`date '+%d %b %Y, %H:%M'`" : insère la date et l'heure actuelle.
  • --highlight-style=tango : style de mise en évidence du code.

Nous obtenons alors le fichier PDF final que vous pouvez visualiser et télécharger ici :

Mémoire PDF

Export HTML

Voici maintenant ce qu'exécute la commande make html (les variables sont les mêmes que dans la partie PDF ci-dessus) :

html:
        $(PANDOC) "$(INPUTDIR)"/*.md \
        "$(METAFILE)" \
        -o "$(OUTPUTDIR)/$(FILENAME).html" \
        --filter=pandoc-shortcaption \
        --filter=pandoc-xnos \
        --lua-filter filter.lua \
        --standalone \
        --template="$(STYLEDIR)/template.html" \
        --bibliography="$(BIBFILE)" \
        --csl="$(CSLFILE)" \
        --include-in-header="$(STYLEDIR)/style.css" \
        --toc \
        --number-sections \
        --metadata time="`date '+%d %b %Y, %H:%M'`" \
        --highlight-style=tango \
        --mathjax=https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-AMS_CHTML-full
        rm -rf "$(OUTPUTDIR)/source"
        mkdir "$(OUTPUTDIR)/source"
        cp -r "$(INPUTDIR)/figures" "$(OUTPUTDIR)/source/figures"
        cp -r "$(INPUTDIR)/images" "$(OUTPUTDIR)/source/images"

C'est reparti pour une explication détaillée des paramètres (uniquement ceux qui diffèrent de la partie PDF ci-dessus) :

  • --lua-filter filter.lua : applique des transformations personnalisées (dans notre cas, conserver des souligement spéciaux).
  • --standalone : génère un fichier HTML autonome.
  • --include-in-header=style.css : insère le fichier de style CSS dans le corps.
  • --toc : insère la table des matières (Table Of Content).
  • --mathjax=... : donne l'URL du script Javascript externe qui va convertir les équations.

Nous obtenons alors le fichier HTML final que vous pouvez visualiser ici :

Mémoire HTML

Le modèle que j'ai utilisé a été créé par Tom Pollard et al. en 2016 et publié sous licence MIT.

Tom Pollard et al. (2016). Template for writing a PhD thesis in Markdown. Zenodo. dx.doi.org/10.5281/zenodo.58490

Une fois modifiée pour mes propres besoins, l'architecture des fichiers devient celle-ci :

├── filter.lua
├── install.sh
├── Makefile
├── output
│   ├── index.html
│   ├── memoire_audio_bdx_bl.docx
│   ├── memoire_audio_bdx_bl.html
│   ├── memoire_audio_bdx_bl.pdf
│   ├── memoire_audio_bdx_bl.tex
│   └── source
│       ├── figures
│       │   ├── [...].jpg
│       │   ├── [...].png
│       └── images
│           ├── department_logo.jpg
│           └── univ_logo.png
├── pandoc.log
├── README.md
├── source
│   ├── 01_remerciements.md
│   ├── 02_avant-propos.md
│   ├── 03_table_matieres.md
│   ├── 04_liste_figures.md
│   ├── 05_liste_tableaux.md
│   ├── 06_liste_sigles_abreviations.md
│   ├── 07_chapitre_1.md
│   ├── 08_chapitre_2.md
│   ├── 09_chapitre_3.md
│   ├── 10_chapitre_4.md
│   ├── 11_chapitre_5.md
│   ├── 12_conclusion.md
│   ├── 13_annexe_1.md
│   ├── 14_annexe_2.md
│   ├── 15_annexe_3.md
│   ├── 16_bibiographie.md
│   ├── bibliographie.bib
│   ├── figures
│   │   ├── [...].jpg
│   │   ├── [...].png
│   ├── images
│   │   ├── department_logo.jpg
│   │   └── univ_logo.png
│   └── metadata.yml
└── style
    ├── apa.csl
    ├── department_logo.jpg
    ├── preamble.tex
    ├── style.css
    ├── template.html
    ├── template.tex
    └── univ_logo.eps

Contenu en LaTeX

Remarquons que la page de garde et la quatrième de couverture n'ont pas de fichiers Markdown associés, car elles sont formatées en LaTeX dans le modèles style/template.tex.

Voici la partie de ce fichier qui débute le document, définit les marges puis affiche le titre et le sous-titre :

\begin{document}
\newgeometry{top=1cm,bottom=2cm,left=1.5cm,right=1.5cm}
\begin{titlepage}
    \begin{center}

    % logos université
    \includegraphics[width=0.45\textwidth]{style/univ_logo.eps}
    \hfill
    \includegraphics[width=0.45\textwidth]{style/department_logo.jpg}

        \vspace*{1.5cm}

        \huge
        \definecolor{bleu}{rgb}{0, 0.61, 0.88}
        \textcolor{bleu}{$title$}

        $if(subtitle)$
        \vspace{.5cm}

Contenu en Markdown

Dans le dossier source, les fichiers sont numérotés car ils seront insérés dans cet ordre dans le fichier final. Prenons l'exemple du fichier 07_chapitre_1.md, dont voici les premières lignes :

\newpage
\setcounter{page}{1} 
\pagenumbering{arabic}

# Introduction

> Afin d’introduire le contexte et le sujet de recherche développés dans ce mémoire, nous allons présenter le plan des chapitres qui répond aux questions du sous-titre. Les deux premières questions vont permettre de situer l'étude dans la littérature actuelle ; les deux questions suivantes aboutiront à la confrontation d'études cliniques.

## Contexte

\hypertarget{pourquoi}{%
\noindent\textbf{Pourquoi ?} Pour quelles raisons physiologiques cherche-t-on à améliorer le RSB ?}\label{pourquoi}
\addcontentsline{toc}{subsubsection}{Pourquoi ?}
 

On comprend que les sources Markdown peuvent contenir directement des commandes LaTeX à l'intérieur. Voyons en détail ce que signifient ces commandes qui débutent par un backslash :

  • \newpage : créer nouvelle page (!),
  • \setcounter{page}{1} : redémarrer le comptage des pages à 1,
  • \pagenumbering{arabic} : utiliser des chiffres arabes à partir d'ici (les pages précédentes étaient numérotées en chiffres romains),
  • \hypertarget(pourquoi) : générer une ancre et la nommer,
  • \noindent\textbf{Pourquoi ?} Pour quelles raisons [...]}\label{pourquoi} : écrire le titre en gras et y associer le label,
  • \addcontentsline{toc}{subsubsection}{Pourquoi ?} : ajouter la section dans la table des matières au niveau N-2.

Voilà à quoi ressemblent toutes les sources du mémoire en Markdown. Le fichiers .md peuvent être édités avec un éditeur de texte simple ou dédié, avec un aperçu direct du rendu. Une liste des logiciels libres compatibles peut être trouvée sur Framalibre.

Moi, j'ai tout écrit sous vim !