feat(deskew): correction automatique du skew au chargement des PDFs

Nouveau module pipeline/deskew.py basé sur cv2.HoughLinesP :
- détecte les lignes quasi-horizontales (±15° de l'horizontale)
- prend la médiane de leurs angles (robuste aux outliers)
- seuils : |angle|>0.3° pour corriger, |angle|>10° = suspect (on ne corrige pas)
- PIL.rotate() avec BICUBIC + fillcolor blanc, sans expand

Intégré dans pipeline/ingest.py (paramètre `deskew=True` par défaut).
L'angle appliqué est tracé dans un fichier `page_XX.skew` à côté de
l'image, pour audit.

Mesuré sur les 18 dossiers de l'échantillon 2018 CARC : seule OGC 1 a
un skew au-dessus du seuil (+0.91°), les 17 autres sont déjà droits.
Le deskew corrige OGC 1 en 0.00° résiduel (vérif visuelle en-tête OK).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-24 23:07:29 +02:00
parent b47f5c47e0
commit 6c8184cc03
2 changed files with 148 additions and 3 deletions

View File

@@ -1,10 +1,16 @@
"""PDF → images PNG 300 dpi avec cache par hash SHA256."""
"""PDF → images PNG 300 dpi avec cache par hash SHA256.
Applique optionnellement un deskew automatique (redressement) sur chaque page
pour corriger le biais d'inclinaison des scans. Voir pipeline/deskew.py.
"""
import hashlib
import os
from pathlib import Path
from pdf2image import convert_from_path
from PIL import Image
from .deskew import deskew_image, MIN_ANGLE_DEG
DEFAULT_DPI = 300
CACHE_ROOT = Path(".cache/images")
@@ -18,23 +24,37 @@ def pdf_hash(pdf_path: str) -> str:
return h.hexdigest()[:16]
def pdf_to_images(pdf_path: str, dpi: int = DEFAULT_DPI, cache_root: Path = CACHE_ROOT) -> list[Path]:
def pdf_to_images(pdf_path: str, dpi: int = DEFAULT_DPI,
cache_root: Path = CACHE_ROOT,
deskew: bool = True) -> list[Path]:
"""Convertit un PDF en PNG 300 dpi. Retourne la liste des chemins (1 par page).
Le cache est indexé par hash du PDF : un PDF inchangé n'est jamais reconverti.
Avec `deskew=True` (défaut), chaque page est redressée si son angle de skew
dépasse le seuil défini dans `pipeline.deskew.MIN_ANGLE_DEG` (0.3°). L'angle
appliqué est persisté dans un fichier `<page>.skew` à côté (pour audit).
"""
cache_root = Path(cache_root)
h = pdf_hash(pdf_path)
out_dir = cache_root / h
out_dir.mkdir(parents=True, exist_ok=True)
existing = sorted(out_dir.glob("page_*.png"))
# Le glob est strict pour ne pas attraper les crops intermédiaires
# (page_XX_recodage.png, etc.)
existing = sorted(p for p in out_dir.glob("page_*.png")
if p.stem.replace("page_", "").isdigit())
if existing:
return existing
pages = convert_from_path(pdf_path, dpi)
paths = []
for i, img in enumerate(pages, 1):
if deskew:
img, applied = deskew_image(img)
if applied != 0.0:
# Trace d'audit : on note l'angle corrigé
(out_dir / f"page_{i:02d}.skew").write_text(f"{applied:.3f}\n")
p = out_dir / f"page_{i:02d}.png"
img.save(p, "PNG", optimize=True)
paths.append(p)