{"id":6145,"date":"2025-12-15T17:21:20","date_gmt":"2025-12-15T16:21:20","guid":{"rendered":"https:\/\/gpmfactory.com\/?p=6145"},"modified":"2025-12-16T00:09:33","modified_gmt":"2025-12-15T23:09:33","slug":"lhistoire-dun-projet-ocr","status":"publish","type":"post","link":"https:\/\/gpmfactory.com\/index.php\/2025\/12\/15\/lhistoire-dun-projet-ocr\/","title":{"rendered":"L&rsquo;Histoire d&rsquo;un Projet OCR"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Le D\u00e9fi Initial<\/h2>\n\n\n\n<p>Le projet commence avec un objectif  : num\u00e9riser et extraire le contenu d&rsquo;une centaine<strong> documents d&rsquo;archives militaires d&rsquo;Indochine<\/strong>, datant des ann\u00e9es 1950. Ces documents dactylographi\u00e9s pr\u00e9sentent une structure particuli\u00e8re en <strong>deux colonnes<\/strong> : dates \u00e0 gauche, \u00e9v\u00e9nements \u00e0 droite.<\/p>\n\n\n\n<p>Le d\u00e9fi ? Ces documents vieillissants ont plus de 70 ans. Le papier a jauni, l&rsquo;encre s&rsquo;est estomp\u00e9e, et la qualit\u00e9 de la frappe varie. Impossible de simplement \u00ab\u00a0scanner et extraire\u00a0\u00bb &#8211; il faut un v\u00e9ritable pipeline de traitement.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 1 : Les Premiers Pas avec Google Cloud Vision<\/h2>\n\n\n\n<p>Le choix se porte sur <strong>Google Cloud Vision API<\/strong>, connu pour sa pr\u00e9cision sur les documents complexes. Le premier script <code>vision.py<\/code> na\u00eet avec deux optimisations cruciales :<\/p>\n\n\n\n<p>python<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>response = client.document_text_detection(\n    image=image,\n    image_context={\"language_hints\": &#91;\"fr\"]}\n)<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>document_text_detection<\/code> : optimis\u00e9 pour les documents structur\u00e9s<\/li>\n\n\n\n<li><code>language_hints: [\"fr\"]<\/code> : essentiel pour les accents fran\u00e7ais (\u00e9, \u00e8, \u00e7, etc.)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 2 : Le Casse-T\u00eate du Colonage<\/h2>\n\n\n\n<p>Le premier obstacle s\u00e9rieux appara\u00eet : comment <strong>apparier intelligemment<\/strong> les dates et leur contenu correspondant ?<\/p>\n\n\n\n<p>Les documents ont une structure claire visuellement, mais l&rsquo;OCR retourne un flux d\u00e9sordonn\u00e9 de paragraphes. La solution ? Un algorithme d&rsquo;<strong>appariement par proximit\u00e9 verticale<\/strong> :<\/p>\n\n\n\n<p>python<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def smart_pair_dates_content(dates, content_items):\n    <em># Pour chaque date, trouver le contenu le plus proche en Y<\/em>\n    <em># Pr\u00e9server l'ordre topologique du document<\/em>\n    <em># Fusionner les paragraphes orphelins<\/em><\/code><\/pre>\n\n\n\n<p>Ce syst\u00e8me :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Trie tous les \u00e9l\u00e9ments par position verticale (Y)<\/li>\n\n\n\n<li>Apparie chaque date avec le contenu le plus proche<\/li>\n\n\n\n<li>Fusionne les paragraphes sans date avec le paragraphe pr\u00e9c\u00e9dent<\/li>\n\n\n\n<li>Pr\u00e9serve l&rsquo;ordre chronologique du document original<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 3 : La Reconnaissance des Dates<\/h2>\n\n\n\n<p>Un nouveau probl\u00e8me surgit : les dates prennent <strong>plusieurs formats<\/strong> selon les documents :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>1\u00b0 Novembre<\/code> (format original attendu)<\/li>\n\n\n\n<li><code>14.11.1955<\/code> (format num\u00e9rique)<\/li>\n\n\n\n<li><code>Novembre 1955<\/code><\/li>\n\n\n\n<li><code>1955<\/code> (ann\u00e9e seule)<\/li>\n<\/ul>\n\n\n\n<p>La fonction <code>is_date()<\/code> \u00e9volue pour d\u00e9tecter tous ces formats via regex :<\/p>\n\n\n\n<p>python<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>patterns = &#91;\n    r'\\d{1,2}\\s*\u00b0?\\s*(?:Janvier|F\u00e9vrier|...)',  <em># Texte<\/em>\n    r'\\d{1,2}&#91;\\.\/-]\\d{1,2}&#91;\\.\/-]\\d{4}',         <em># 14.11.1955<\/em>\n    r'^\\d{4}$',                                   <em># 1955<\/em>\n]<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 4 : Le Pr\u00e9traitement ImageMagick<\/h2>\n\n\n\n<p>Pour les documents particuli\u00e8rement d\u00e9grad\u00e9s, l&rsquo;OCR seul ne suffit pas. Entre en jeu <strong>ImageMagick<\/strong> avec 6 pr\u00e9traitements diff\u00e9rents :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Baseline<\/strong> : contraste + nettet\u00e9 standard<\/li>\n\n\n\n<li><strong>Aggressive<\/strong> : contraste fort pour papier jauni<\/li>\n\n\n\n<li><strong>Denoise<\/strong> : r\u00e9duction du bruit pour vieilles frappes<\/li>\n\n\n\n<li><strong>Enhance<\/strong> : nettet\u00e9 maximale<\/li>\n\n\n\n<li><strong>Threshold<\/strong> : binarisation pour documents tr\u00e8s clairs\/fonc\u00e9s<\/li>\n\n\n\n<li><strong>Optimal<\/strong> : combinaison \u00e9quilibr\u00e9e<\/li>\n<\/ol>\n\n\n\n<p>Le script <code>batch_imagemagick.py<\/code> permet de tester visuellement quelle version donne les meilleurs r\u00e9sultats OCR. On utilise majoritairement l&rsquo;option <em>Denoise<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 5 : Le Probl\u00e8me de l&rsquo;Orientation<\/h2>\n\n\n\n<p>Surprise : certains documents ont \u00e9t\u00e9 scann\u00e9s dans le mauvais sens ! Ils ont \u00e9t\u00e9 redress\u00e9s manuellement. les m\u00e9tadonn\u00e9es EXIF indiquent la rotation, mais Cloud Vision les ignore.<\/p>\n\n\n\n<p>Solution : <code>check_orientation.py<\/code> qui :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Lit les m\u00e9tadonn\u00e9es EXIF<\/li>\n\n\n\n<li>Effectue une rotation physique de l&rsquo;image<\/li>\n\n\n\n<li>Peut g\u00e9n\u00e9rer 4 versions (0\u00b0, 90\u00b0, 180\u00b0, 270\u00b0) pour tester<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 6 : L&rsquo;Enfer de l&rsquo;Encodage Windows<\/h2>\n\n\n\n<p>Le vrai cauchemar technique : les <strong>probl\u00e8mes d&rsquo;encodage<\/strong>. Sur Windows, les scripts Python utilisent par d\u00e9faut cp1252, mais :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Les emojis dans les messages (\u2705 \u274c \ud83d\udd0d) sont en UTF-8<\/li>\n\n\n\n<li>Les accents fran\u00e7ais n\u00e9cessitent UTF-8<\/li>\n\n\n\n<li>Les appels <code>subprocess<\/code> m\u00e9langent les encodages<\/li>\n<\/ul>\n\n\n\n<p>La bataille fait rage ligne par ligne :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Suppression de tous les emojis : \u2705 \u2192 <code>[OK]<\/code><\/li>\n\n\n\n<li>Caract\u00e8res sp\u00e9ciaux : \u00b0 \u2192 <code>degres<\/code>, \u0394 \u2192 <code>d<\/code><\/li>\n\n\n\n<li>Force UTF-8 dans subprocess : <code>encoding='utf-8', errors='replace'<\/code><\/li>\n\n\n\n<li>Force UTF-8 dans les scripts : <code>sys.stdout = io.TextIOWrapper(...)<\/code><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 7 : La Grande Simplification &#8211; Adieu Node.js<\/h2>\n\n\n\n<p>Un tournant majeur : la g\u00e9n\u00e9ration des fichiers Word utilisait <strong>Node.js + docx<\/strong> via subprocess. Une d\u00e9pendance de trop.<\/p>\n\n\n\n<p>Refactorisation compl\u00e8te. <strong>115 lignes de code JavaScript \u00e9limin\u00e9es<\/strong>, remplac\u00e9es par du Python pur :<\/p>\n\n\n\n<p>python<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from docx import Document\ndoc = Document()\ntable = doc.add_table(rows=len(data), cols=2)\n<em># Supprimer les bordures, formater, sauvegarder<\/em>\ndoc.save(output_file)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Phase 8 : L&rsquo;Automatisation par Lot<\/h2>\n\n\n\n<p>Avec 118 documents, le traitement manuel est impensable. Naissance du <strong>pipeline batch<\/strong> :<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Scripts d\u00e9velopp\u00e9s :<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>batch_orientation.py<\/strong> : Corrige l&rsquo;orientation de toutes les images<\/li>\n\n\n\n<li><strong>batch_imagemagick.py<\/strong> : Applique le pr\u00e9traitement optimal \u00e0 tous les documents<\/li>\n\n\n\n<li><strong>batch_ocr.bat<\/strong> : Lance l&rsquo;OCR sur tout un dossier<\/li>\n\n\n\n<li><strong>remove_oriented.py<\/strong> : Nettoie les noms de fichiers<\/li>\n\n\n\n<li><strong>merge_word_simple.py<\/strong> : Fusionne tous les Word en un seul document<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Le workflow final :<\/h3>\n\n\n\n<p>bash<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code><em># 1. Pr\u00e9traitement (si n\u00e9cessaire)<\/em>\npython batch_imagemagick.py Archives\/ --tests 6 --output Archives_clean\/\n\n<em># 2. Correction orientation<\/em>\npython batch_orientation.py Archives_clean\/ --fix\n\n<em># 3. Nettoyage noms<\/em>\npython remove_oriented.py Archives_clean\/\n\n<em># 4. OCR massif<\/em>\nbatch_ocr.bat Archives_clean\/ Resultats\/\n\n<em># 5. Fusion finale<\/em>\npython merge_word_simple.py Resultats\/ Legion_Indochine_1950s.docx<\/code><\/pre>\n\n\n\n<p>Un seul document Word final : <strong>118 documents d&rsquo;archives structur\u00e9s, searchable, exploitables<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Les D\u00e9fis Techniques Surmont\u00e9s<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. <strong>D\u00e9tection de Structure<\/strong><\/h3>\n\n\n\n<p>Transformer un flux OCR d\u00e9sordonn\u00e9 en paires date-contenu coh\u00e9rentes via analyse g\u00e9om\u00e9trique.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. <strong>Polyvalence des Formats<\/strong><\/h3>\n\n\n\n<p>G\u00e9rer 7+ formats de dates diff\u00e9rents dans les m\u00eames documents.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. <strong>Qualit\u00e9 Variable<\/strong><\/h3>\n\n\n\n<p>Des documents de 70 ans, jaunis, estomp\u00e9s \u2192 pr\u00e9traitement ImageMagick adaptatif.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. <strong>Encodage Cross-Platform<\/strong><\/h3>\n\n\n\n<p>Le grand boss final : faire cohabiter UTF-8, cp1252, emojis, accents fran\u00e7ais, subprocess Python et batch Windows.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. <strong>Simplicit\u00e9 d&rsquo;Usage<\/strong><\/h3>\n\n\n\n<p>118 documents \u00d7 5 \u00e9tapes = <strong>trop complexe<\/strong>. Solution : scripts batch qui encha\u00eenent tout automatiquement.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Les Le\u00e7ons Apprises<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Technique<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Less is More<\/strong> : \u00c9liminer Node.js a simplifi\u00e9 tout le projet<\/li>\n\n\n\n<li><strong>UTF-8 Everywhere<\/strong> : Sur Windows, forcer UTF-8 partout \u00e9vite 90% des probl\u00e8mes<\/li>\n\n\n\n<li><strong>Tester d&rsquo;Abord<\/strong> : Les 6 variantes ImageMagick permettent de choisir visuellement avant de tout traiter<\/li>\n\n\n\n<li><strong>Chemins Absolus<\/strong> : Dans les scripts batch, toujours utiliser <code>__file__<\/code> pour les chemins<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">M\u00e9thodologie<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Mise en place de la team:\n<ul class=\"wp-block-list\">\n<li>Patrick pour les sp\u00e9cifications et les tests<\/li>\n\n\n\n<li>Claude <em>Anthropic <\/em>pour la r\u00e9alisation<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>It\u00e9ration<\/strong> : Le projet a \u00e9volu\u00e9 sur plusieurs jours, chaque probl\u00e8me amenant sa solution<\/li>\n\n\n\n<li><strong>Feedback Utilisateur<\/strong> : \u00ab\u00a0\u00c7a ne marche pas\u00a0\u00bb \u2192 Debug \u2192 Correction \u2192 \u00ab\u00a0Maintenant \u00e7a marche !\u00a0\u00bb<\/li>\n\n\n\n<li><strong>Pragmatisme<\/strong> : Abandonner les solutions complexes (NODE_PATH, npx) pour des solutions simples (python-docx direct)<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Le R\u00e9sultat Final<\/h2>\n\n\n\n<p><strong>De :<\/strong> 118 images JPEG de documents jaunis, mal orient\u00e9s, difficilement lisibles<\/p>\n\n\n\n<p><strong>\u00c0 :<\/strong> Un document Word de 300+ pages, structur\u00e9, searchable, avec :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Toutes les dates correctement identifi\u00e9es<\/li>\n\n\n\n<li>Le contenu associ\u00e9 \u00e0 chaque date<\/li>\n\n\n\n<li>Les paragraphes fusionn\u00e9s intelligemment<\/li>\n\n\n\n<li>Un format exploitable pour la recherche historique<\/li>\n<\/ul>\n\n\n\n<p><strong>Technologies utilis\u00e9es :<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Python 3.12<\/li>\n\n\n\n<li>Google Cloud Vision API<\/li>\n\n\n\n<li>python-docx<\/li>\n\n\n\n<li>ImageMagick<\/li>\n\n\n\n<li>PIL (Pillow)<\/li>\n\n\n\n<li>Regex pour la d\u00e9tection de dates<\/li>\n<\/ul>\n\n\n\n<p><strong>Lignes de code :<\/strong> ~2000 lignes Python + batch scripts<\/p>\n\n\n\n<p><strong>Temps de traitement :<\/strong> ~15 minutes pour 118 documents (avec pr\u00e9traitement)<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pour Aller Plus Loin<\/h2>\n\n\n\n<p>Ce projet d\u00e9montre que <strong>la num\u00e9risation d&rsquo;archives historiques<\/strong> est accessible avec des outils modernes, m\u00eame pour des documents complexes. Les d\u00e9fis techniques (encodage, structure, qualit\u00e9) sont surmontables avec patience et it\u00e9ration.<\/p>\n\n\n\n<p>Le code complet est modulaire et r\u00e9utilisable pour d&rsquo;autres projets d&rsquo;archives : registres d&rsquo;\u00e9tat civil, journaux de bord militaires, correspondances historiques, etc.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Le D\u00e9fi Initial Le projet commence avec un objectif : num\u00e9riser et extraire le contenu d&rsquo;une centaine documents d&rsquo;archives militaires d&rsquo;Indochine, datant des ann\u00e9es&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"ppma_author":[150],"class_list":["post-6145","post","type-post","status-publish","format-standard","hentry","category-non-classe"],"authors":[{"term_id":150,"user_id":1,"is_guest":0,"slug":"admin8700","display_name":"Patrick","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/209d5ed69b74d288390621ab4c1d3773?s=96&d=mm&r=g","0":null,"1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":""}],"_links":{"self":[{"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/posts\/6145","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/comments?post=6145"}],"version-history":[{"count":3,"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/posts\/6145\/revisions"}],"predecessor-version":[{"id":6149,"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/posts\/6145\/revisions\/6149"}],"wp:attachment":[{"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/media?parent=6145"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/categories?post=6145"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/tags?post=6145"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/gpmfactory.com\/index.php\/wp-json\/wp\/v2\/ppma_author?post=6145"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}