Si vous cherchez à mieux comprendre le règlement d’urbanisme de votre
ville, il est très utile d’avoir une carte interactive qui permet de
faire la correspondance entre adresses ou quartiers et zones. Des
grandes villes, qui ont de l’expertise interne en géomatique et des
abonnements coûteux à ArcGIS,
publient souvent ce genre de carte sur leur site web, par exemple
Laval,
Montréal ou
Sherbrooke. Des
petites villes… pas tant que ça!
C’est pour ça que, quand j’ai été élu au conseil à Sainte-Adèle, j’ai
créé ZONALDA, une carte de
zonage interactive simple, gratuite et libre. Cet article est la
première d’une série qui détaille la conception et l’implémentation
d’une telle carte, en commençant par la base - c’est à dire, la base
de données géomatique, qui contient les formes et emplacements de
toutes les zones ainsi que leurs identificateurs et attributs.
Obtenir un plan de zonage
Idéalement, à défaut d’une carte interactive, votre ville ou MRC
publie la géomatique du plan de zonage sur Données
Québec (voici celle de
Repentigny
par exemple) mais généralement ce n’est pas le cas. Par contre, les
villes publient presque toujours une carte de zonage sur leur sites
web, la plupart du temps en format PDF, plus souvent dans la section
des règlements d’urbanisme. On la trouve sous le rubrique
Règlements -
Urbanisme à
Sainte-Adèle, sur la page Règlementation et
Permis à
Morin-Heights, et sur la page générale de réglementation
municipale
à Saint-Calixte.
Afin d’en obtenir la géomatique sous-jacente (habituellement sous
forme d’un Shapefile) vous pouvez
aussi essayer de faire une demande d’accès à l’information. Pour les
petites villes, c’est généralement à la MRC qui il faut s’adresser -
c’est la raison qu’on voit «Source: MRC des Pays d’en Haut» sur les
plans de zonage de Sainte-Adèle et Morin-Heights.
Finalement, on peut aussi utiliser la carte publique pour reconstruire
manuellement la géomatique des zones. Pour ce faire, on utilisera le
logiciel libre QGIS, accompagné par
GIMP et InkScape et
assaisonné avec un peu de jus de bras.
Cas facile: Morin-Heights
Il faut d’abord savoir qu’un PDF n’est pas une seul et unique sorte de
fichier, et il est nécessaire de savoir d’abord à quelle sorte de PDF
vous avez affaire. Le plan de zonage de
Morin-Heights
est un très bon PDF parce qu’il est vectoriel et géoréférencé.
Comment le savoir? D’abord, on ouvre QGIS et on ajoute une carte de
base OpenStreetMap. Maintenant, il faut simplement essayer d’ajouter
le PDF comme couche vectorielle dans QGIS:
On voit que cette opération nous réussit à ajouter plusieurs couches
vecteur et, même si les zones ne sont pas ajoutées comme polygones,
les limites des zones sont présentes comme lignes et placées
correctement sur la carte:
Dans ce cas-ci, il ne nous reste qu’à tracer les limites de chaque
zone avec les options «accrochage» et «traçage» des outils de
numérisation. On va aussi importer la carte comme couche
matricielle (raster) pour être capable de voir les identificateurs
de zone. Donc, il faut importer le PDF une deuxième fois, cette fois
comme «raster»:
On est prêt à créer la géomatique des zones! Pour ce faire:
Mettre la carte raster en-dessous des vecteurs dans la liste de
couches.
Créer une nouvelle couche temporaire en mémoire, avec une géométrie
de polygone et un SCN de EPSG:4326 - WGS-84. Ajouter un champ
“zone” de type texte aussi. Maintenant, cliquer avec le bouton
droit sur la nouvelle couche et sélectionnez «Convertir en couche
permanente», puis sauvegardez-la en GeoJSON.
Activer la barre d’outils «accrochage» et sélectionner le bouton
«trace» dans la barre. Très important: la fonction «trace»
n’est pas toujours capable de prendre en charge plusieurs couches à la fois.
Il faut donc aussi selectionner «Configuration avancée» dans le
menu à droite de l’aimant, puis cocher seulement
Layers_ZONAGE_2022_PG dans le menu sous l’œil qui apparaît à
côté:
Maintenant vous pouvez utiliser l’outil «Ajouter une entité
polygonale» et cliquer avec le bouton gauche en suivant le contour
d’une zone. L’accrochage fera en sorte que les points seront
placés sur les limites de la zone, et la trace suivra les courbes
et autres détails. Si vous faites une erreur, utilisez la touche
«backspace» pour enlever le dernier point. Une fois rendu au point
de départ, fermer le polygone avec le bouton à droite. Dans le
dialogue, entrez l’identificateur de la zone tel qu’indiqué dans la
carte raster:
Pendant que vous tracez, vous pouvez effacer le dernier point avec
la touche «Backspace», annuler la figure avec «Esc», ou bouger la
carte en tenant la barre d’espace en bougeant la souris. Pour
désactiver ou réactiver la trace pésez sur «T», et pour désactiver
ou réactiver l’accrochage, la touche «S».
N’oubliez pas de sauvegarder votre travail de temps en temps!
Après une demi-heure environ de traçage, vous aurez une couche
vectorielle avec des polygones pour toutes les zones de Morin-Heights,
et vous aurez aussi un fichier GeoJSON que vous
pouvez visualiser avec uMap:
Cas plus difficile: Sainte-Adèle
Ce n’est pas toutes les villes qui produisent (ou demandent à leur MRC
de produire) des fichiers PDF aussi futés. Ça se peut que vous aurez
un PDF qui est tout simplement une image matricielle, qui ne contient
que des pixels. Donc, QGIS va refuser de l’importer comme couche
vecteur:
Pire encore, cette image peut aussi ne pas être géoréférencée. Donc,
si vous l’importez comme raster, elle finira quelque part dans
l’océan, ou dans les montagnes de l’Équateur, tout dépendant de votre
SCN:
Sainte-Adèle figure malheureusement parmi ces villes moins douées en
géomatique. Qui plus est, elle publie aussi deux cartes séparées de
zonage, une pour la ville au
complet et une autre
plus détaillée pour le périmètre
urbain. Mais tout n’est
pas perdu! QGIS est très capable de faire quelque chose avec ce genre
de PDF, avec un peu plus de travail.
D’abord, on n’ajoutera pas directement ces fichiers dans QGIS comme
couche raster, car ils ne sont pas géoréférencés, mais aussi parce que
QGIS par défaut va réduire la résolution de
l’image qui les rend pas très
lisibles. C’est très courant pour des cartes en PDF d’être composées
d’une seule image matricielle, donc, on va extraire ces images avec
PLAYA-PDF:
En effet, si on regarde les fichiers tout.json et
zoom.json on constate qu’il y a bel et bien une seule et
très grande image dans chaque fichier (tout
et zoom). Étant donné qu’on aura besoin de
les géoréférencer de toute façon, on va d’abord ouvrir ces images avec
GIMP pour les découper un peu:
Ouvrir les images .jpg avec GIMP
Découper l’image juste au-delà des limites de la ville, ou aux
limites de la carte pour le plan plus détaillé
Ajouter un canal alpha (Calque → Transparence → Ajouter un canal alpha)
Exporter l’image en PNG (ou TIFF, peu importe…)
Ça nous donne tout.png et zoom.png. Par la
suite on va les incorporer dans QGIS avec la fênêtre
«Géoréférencer…» (dans le menu «Couche»). Mais, pour faire ceci, ça
nous prend des points de référence fiable, comme les limites de la
ville. Très important: N’utilisez jamais les limites
administratives dans OpenStreetMap, elles sont incorrectes! On va
alors télécharger les données ouvertes des découpages administratifs
du
Québec.
Pour rendre ça plus rapide, on n’a qu’à sélectionner l’objet munic_s:
Il est sûrement possible de demander simplement la limite d’une seule
ville avec le serveur REST, mais je n’ai pas réussi à en trouver la
documentation…
On peut maintenant géoréférencer la carte complète de la ville par
rapport aux limites administratives:
Ouvrir le géoréférenceur et ouvrir tout.png comme
raster (Ctrl+O ou Fichier → Ouvrir un raster…)
Selectionner l’outil «Ajouter un point de contrôle» et placer un
point à un point sur le contour de la ville, puis selectionner
«Depuis le canevas de la carte…»
Identifier le point correspondant sur les limites administratives.
En activant l’accrochage (icône en forme d’aimant) sur cette couche
spécifique on peut avoir un peu plus de précision.
Le coin Sud-Est de la ville ne correspond pas exactement, alors il
vaut mieux ne pas l’utiliser. Ces 5 points suffisent:
Modifier les paramètres de transformation pour utiliser
«Polynomiale 1» et «Cubic» et définir le nom du fichier en sortie,
puis débuter le géoréférencement.
En superposant de nouveau la couche des limites administratives, on
confirme qu’on a un très bon alignement:
Pour la carte d’agrandissement, on fera un processus semblable, sauf
que les limites de l’agrandissement ne sont pas exactes, donc il vaut
mieux définir des points de contrôle sur les points saillants des
zones, au lieu des limites de la carte:
Après le géoréférencement, on peut réduire l’opacité de la nouvelle
couche pour confirmer que les zones sont bien alignées, bien qu’il y
ait un petit écart sur le haut de l’agrandissement:
Ça nous fais une très belle carte, mais… il faut quand même tracer
les zones. Pour celles qui sont plutôt rectiligne, et collées sur les
limites de la ville, ce n’est pas trop difficile, mais plusieurs ont
des formes très complexes. Il serait donc très utile de pouvoir
extraire au moins quelque chose de vectoriel sur lequel on pourait
accrocher pour faciliter le traçage.
Auto-traçage et géoréférencement
Pour obtenir une couche vectorielle qui nous aidera à guider le
traçage, on peut ouvrir nos fichiers PNG avec
InkScape, puis sélectionner «Chemin →
Vectoriser un objet matriciel…». Pour l’agrandissement, des
paramètres comme ceux-ci fonctionnent:
On peut masquer (ou carrément supprimer) l’image matricielle de base
pour voir que ça nous donne déjà quelque chose un peu plus potable:
Après avoir detecté les lignes, il faut sauvegarder le projet en
format DXF (pourquoi ce format? seul QGIS le sait…) et par la suite
refaire le géoréférencement, mais en ajoutant les objets dans ce
fichier comme vecteur:
La principale difficulté à tracer en utilisant ces vecteurs, c’est que
parce que les lignes dans l’image originale sont épaisses, on se
retrouve avec deux traces pour chaque ligne. Il faut aussi faire
attention aux flèches et textes qui se font aussi tracer (la touche
«T» est très utile).
En fin de compte, ça nous permet d’avoir des pas pires resultats, mais
ça prend du temps:
Par la suite, nous allons regarder comment géolocaliser des adresses
pour les situer dans une zone, et comment réparer la base géomatique
au cas où des géométries invalides empêchent ceci de fonctionner.
QGIS est muni des «outils de numérisation avancés» qui permettent de
nettoyer les données qu’on aura créées en traçant.
Il est aussi possible d’améliorer l’extraction de figures en détectant
automatiquement les étiquettes textuelles des zones et les enlever de
la figure… on regardera ça dans un deuxième temps!
Si vous voulez fouiller dans les entrailles d’un document PDF pour en
extraire des metadonnées, des images, et même du texte, j’ai des
superbes logiciels libres pour vous:
PLAYA-PDF et
PAVÉS. Si vous voulez savoir
comment je suis arrivé là, continuez à lire. Et si vous avez besoin
d’un consultant pour vos besoins d’intelligence documentaire je suis
bien sûr disponible pour des contrats de toute sorte!
«Vous n’êtes qu’un paquet d’objets indirects!»
Comme vous savez peut-être (ou pas), je suis chercheur en
linguistique
informatique
de formation et de
métier.
En 2021, fraîchement élu conseiller municipal dans une ville du grand
St-Jérôme que je ne nommerai pas, j’ai quitté mon poste de
scientifique principal chez une compagnie (que je ne nommerai pas non
plus), maintenant division de Microsoft, car il était impossible pour
moi de servir deux maîtres travailler à temps plein à Montréal
tout étant un élu efficace et à l’écoute. La chose municipale me
semblait aussi, à l’époque, bien plus intéressante que le raffinement
des modèles d’apprentissage automatique pour la compréhension du
langage naturel.
Entre-temps, il s’est passé des choses…
Un effet secondaire de ce changement de carrière plus ou moins bien
avisé est que je suis devenu (en tout humilité) expert en analyse et
manipulation des fichiers PDF, et ce, de la manière habituelle des
informaticiens de mon genre: j’ai parti un projet de logiciel libre.
Pourquoi?
Lorsqu’on regarde les défis de gestion des documents dans
une ville ou autre organisme, on se rend très vite compte que malgré
les grands efforts des promoteurs de ODP, OOXML, HTML et autres
formats universels, en fin de compte, le PDF, c’est la lingua franca
de tous les échanges documentaires. C’est la triste conséquence de la
domination du bureaucratique par nul autre que Microsoft, dont les
logiciels font exprès de multiplier les incompatibilités non seulement
avec d’autres produits (libres ou
pas), mais ne sont souvent
même pas compatibles entre eux-mêmes.
Comment?
J’avais des critères pour l’outil que je voulais utiliser, qui ne
correspondaient pas à l’état actuel des logiciels disponibles:
Licence libre et permissive (du genre BSD, MIT).
Écrit en Python et portable entre différentes plateformes.
Interface conviviale pour le programmeur.
Accès direct aux structures interne du PDF, avec la capacité
d’extraire non seulement du texte mais les éléments de mise en page
et les metadonnées.
Rapide et efficiente, autant que possible (c’est un peu en conflit
avec #2 mais bon).
Le logiciel qui se rapproche le plus de ces critères à l’époque était
pdfplumber, un très bon
logithèque qui satisfait néanmoins aux critères 1, 2 et 3! J’y ai
même contribué un module pour l’extraction des arborescences de
structure logique. Par contre, l’efficacité n’est pas trop au
rendez-vous, surtout parce que pdfplumber, comme d’autres projets
populaires dont sa logithèque sous-jacente
pdfminer.six, doit
analyser chaque page au complet et construire toutes les structures de
données avant de retourner des informations demandées.
De paresse et de parallélisme
C’est surtout ça l’innovation de PLAYA-PDF: il est
«paresseux»,
ne traitant que les informations nécessaires pour extraire
l’information que vous désirez. Si vous, par contre, vous êtes
paresseux·se, il possède aussi un interface qui peut convertir les
metadonnées d’un PDF en
JSON, et ce, très
rapidement:
with playa.open(path) as pdf:
json.dumps(playa.asobj(pdf))
L’autre élément clé, PLAYA-PDF prend en charge plusieurs cœurs de
processeur en
parallèle,
et ce, de manière très conviviale:
with playa.open(path, max_workers=4) as pdf:
texts =list(pdf.pages.map(playa.Page.extract_text))
Par-dessus la PLAYA, les PAVÉS!
Parce que les objectifs de PLAYA-PDF sont surtout l’efficacité et
l’absence de dépendances sur d’autres logiciels, il ne prend pas en
charge des tâches de plus haut niveau, nécessitant de l’imagerie, des
heuristiques ou des modèles d’apprentissage automatique.
Pour cette raison je suis aussi en train de construire
PAVÉS qui prendra de plus en plus
en charge:
L’analyse structurelle et textuelle des PDF, dont le traitement des
tableaux et l’extraction d’unités logiques de texte.
La
visualisation
des objets dans un PDF ainsi que la conversion des pages en images.
Ce deuxième logithèque est encore en chantier mais sert déjà à faire
l’analyse nécessaire pour alimenter mes projets tels que
ZONALDA et
SÈRAFIM.
Conclusion
Si vous faites partie du petit nombre de gens auxquels ça intéresse je
vous invite à l’essayer! J’ai publié entre autres de la
documentation et quelques carnets
Jupyter qui
démontrent la fonctionnalité.
Vous pouvez bien sûr aussi contribuer au développement sur
GitHub (notez qu’il se peut que je
le déplace bientôt vers Codeberg ou autre
hébergement indépendant et au-dehors des États-Unis, mais il restera
toujours disponible sur GitHub).
TL;DR: Vous pouvez utiliser
docling directement 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.
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):
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:
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:
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:
importrequestsr = requests.get("https://ecolingui.ca/pdf_structure.pdf")
r.raise_for_status()
withopen("pdf_structure.pdf", "wb") as fh:
fh.write(r.data)
Et hop, utiliser le modèle est très simple:
importtorchwith 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:
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 inzip(
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 identifié en utilisant ImageDraw:
fromPILimport ImageDraw
draw = ImageDraw.Draw(image)
for label, box inzip(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:
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.