Analyse Mise en Page 2
TL;DR: Vous pouvez utiliser docling-parse car ses modèles sont les plus fiables et efficaces de ceux que j’ai testés. Mais pour comprendre comment ça fonctionne, continuez à lire!
Nous allons voir ici comment utiliser des modèles de vision computationelle pour faire de l’analyse de mise en page d’un PDF et en extraire les titres de sections, alinéas, listes, et tableaux. Pour ce faire, on fera appel à la logithèque libre Transformers qui facilite beaucoup le téléchargement et utilisation de ces modèles en Python. On prendra d’abord un document très simple d’une seule page, fait sur mesure pour ce genre d’analyse. Ceci nous permettra plus tard (dans un autre texte) de mettre en lumière des défaillances de certains modèles.
Dépendances
D’abord installons des dépendances nécessaires:
pip install pillow playa-pdf timm torch torchvision transformers \
--extra-index-url https://download.pytorch.org/whl/cpu
On utilisera pillow
pour manipuler et annoter des images des pages,
playa-pdf
pour extraire des metadonnées des PDFs, et le reste sont
des nécessaires pour rouler les modèles de vision. On les installe
ici en mode CPU pour éviter de télécharger des gigaoctets de n’importe
quoi que nous impose NVidia, mais si vous avez un GPU pris en charge
(même un petit GT1030 suffit) vous pouvez enlever le --extra-index-url
.
Il faut dire tout de suite que les programmeurs de NVidia non seulement génèrent des logiciels obèses mais aussi peu fiables, alors, sous Ubuntu, il faut aussi gosser quelques affaires pour éviter d’être pris avec un fâchant problème de C++ (en réalité, c’est C++ le vrai problème, comme d’habitude):
sudo apt install gcc-10 g++-10
export CC=gcc-10
export CXX=g++-10
Traîtement des PDF en image
Si on veut faire de la vision, bon, ça prend des images matricielles. Pour le moment, on utilisera le bon vieux outil Poppler qui est installé partout sous GNU/Linux (et beaucoup moins ailleurs, désolé) pour convertir des PDF en images de pages.
Ce qu’il faut comprendre avec des modèles d’apprentissage automatique (et que les auteurs de certains logithèques populaires semblent ignorer) c’est que, lorsque possible, ces modèles sont plus performants lorsque les données qu’on leur demande de traiter ressemblent à celles sur lesquelles ils sont entraînés. Puisque tous les modèles d’analyse de mise en page sont entraînés sur DocLayNet, qui est composé d’images de page en format carré avec anticrénelage, et puisque PDF est un format vectoriel qui nous permet de générer des images de n’importe quelle taille et format, il est préférable de créer d’abord des images dans le format attendu.
D’ailleurs il ne sert à rien de créer des images de plus haute résolution que celle prise en charge par le modèle! C’est tout simplement du gaspillage d’énergie, de temps et de stockage (ou, autrement dit, un crime contre le climat et l’économie) puisqu’il faudra rééchantilloner par la suite ces images. Heureusement les bons modèles comme ceux de Docling nous diront leur format préféré.
Alors, on va construire une simple fonction pour nous donner des images
du format souhaité, en utilisant les arguments -scale-to-x
et
-scale-to-y
de Poppler:
import subprocess
import tempfile
from pathlib import Path
from typing import Iterator
from PIL import Image
def popple(path: Path, width: int, height: int) -> Iterator[Image.Image]:
with tempfile.TemporaryDirectory() as tempdir:
temppath = Path(tempdir)
subprocess.run(
[
"pdftoppm",
"-scale-to-x",
str(width),
"-scale-to-y",
str(height),
str(path),
temppath / "ppm",
],
check=True,
)
for ppm in sorted(temppath.iterdir()):
yield Image.open(ppm)
Reconnaissance des éléments de mise en page
On utilisera un modèle
RT-DETR
pour identifier les éléments dans les images. Pour des raisons
inconnues, le groupe Docling n’a pas mis son modèle dans un endroit
standard, alors on ne peut malheureusement pas utiliser
AutoModel.from_pretrained
. Ce n’est pas grave, on va tout
simplement télécharger les fichiers manuellement:
from huggingface_hub import hf_hub_download
processor_config_path = hf_hub_download(
"ds4sd/docling-models",
"model_artifacts/layout/preprocessor_config.json"
)
config_path = hf_hub_download("ds4sd/docling-models",
"model_artifacts/layout/config.json")
weights_path = hf_hub_download("ds4sd/docling-models",
"model_artifacts/layout/model.safetensors")
Pour la suite on va créer deux objets, un RTDetrImageProcessorFast
et un RTDetrForObjectDetection
:
import os
processor = RTDetrImageProcessorFast.from_json_file(processor_config_path)
model = RTDetrForObjectDetection.from_pretrained(os.path.dirname(config_path))
Maintenant on peut savoir le format attendu par le modèle:
width = processor.size["width"]
height = processor.size["height"]
Et les noms des éléments qu’il peut extraire (mais lisez plus loin…):
id2label = model.config.id2label
On va télécharger le document:
import requests
r = requests.get("https://ecolingui.ca/pdf_structure.pdf")
r.raise_for_status()
with open("pdf_structure.pdf", "wb") as fh:
fh.write(r.data)
Et hop, utiliser le modèle est très simple:
import torch
with torch.inference_mode():
for image in popple("pdf_structure.pdf"):
inputs = processor(images=image, return_tensors="pt")
outputs = model(**inputs)
Ces outputs
ne sont pas dans un format super intéressant, alors on
va utiliser le RTDetrImageProcessorFast
pour avoir des coordonnées
qui correspondent à l’image:
img_results = processor.post_process_object_detection(
outputs,
target_sizes=[(image.height, image.width)],
)[0]
print(img_results)
Ceci nous donne quelque chose plus intéressant, qu’on peut interpréter
en utilisant le id2label
mentionné ci-haut (notez qu’il faut
extraire les valeurs des tensor
qui nous retourne le modèle):
Mais oups! Les classifications sont un peu suspects, car il n’existe aucune image (Picture) dans notre document! Bon, il semble que les développeurs de DocLing se sont trompés un peu et il faut ajouter 1 aux indices des classes:
for score, label, box in zip(
img_results["scores"], img_results["labels"], img_results["boxes"]
):
label = id2label[label.item() + 1]
box = [round(x) for x in box.tolist()]
score = score.item()
print(f"Élément: {label} à {box} avec confiance {score}")
Ce qui nous donne quelque chose d’intéressant:
Élément: Text à [59, 138, 575, 197] avec confiance 0.9866001009941101
Élément: List-item à [78, 309, 571, 369] avec confiance 0.9816325902938843
Élément: List-item à [97, 290, 234, 300] avec confiance 0.9407719969749451
Élément: List-item à [78, 271, 171, 279] avec confiance 0.9286511540412903
Élément: Table à [58, 400, 581, 438] avec confiance 0.9260659217834473
Élément: List-item à [78, 252, 170, 261] avec confiance 0.925058901309967
Élément: Section-header à [59, 216, 109, 226] avec confiance 0.9144930243492126
Élément: Section-header à [59, 384, 113, 393] avec confiance 0.9069509506225586
Élément: Section-header à [59, 98, 115, 109] avec confiance 0.8970564603805542
Élément: Section-header à [192, 59, 447, 76] avec confiance 0.8865164518356323
Élément: Text à [59, 119, 224, 128] avec confiance 0.8446756601333618
Élément: Text à [59, 235, 157, 242] avec confiance 0.8046810626983643
Élément: Section-header à [59, 235, 157, 242] avec confiance 0.5821080803871155
Élément: Section-header à [59, 119, 224, 128] avec confiance 0.5068144202232361
On peut vérifier que tout a été bien identifier en utilisant ImageDraw
:
from PIL import ImageDraw
draw = ImageDraw.Draw(image)
for label, box in zip(img_results["labels"], img_results["boxes"]):
label = id2label[label.item() + 1]
box = [round(x) for x in box.tolist()]
draw.rectangle(box, outline="red")
draw.text((box[0], max(0, box[1] - 12)), label, fill="red")
image.save("pdf_structure.png")
Et voilà:
Mais attend, cette image a un aspect un peu bizarre! Si on veut vraiment utiliser les coordonnées, il faut plutôt les transformer pour correspondre à la page originelle. On peut le faire facilement en cherchant les informations avec PLAYA-PDF:
import playa
with playa.open("pdf_structure.pdf") as pdf:
page = pdf.pages[0]
page_results = processor.post_process_object_detection(
outputs,
target_sizes=[(page.height, page.width)],
)[0]
print(page_results)
Conclusion
Nous avons vu comment utiliser un modèle de vision pour identifier les éléments de mise en page dans un PDF. Pour la suite des choses, on regardera comment trouver les textes qui correspondent à ces éléments.
Comme mentionné dans le dernier billet de ce blogue, il existe d’autres modèles de vision qui fonctionnent essentiellement de la même manière, mais qui sont en général beaucoup moins rapides et fiables. Un prochain texte va en faire la comparaison.