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>
66 lines
2.2 KiB
Python
66 lines
2.2 KiB
Python
"""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")
|
|
|
|
|
|
def pdf_hash(pdf_path: str) -> str:
|
|
"""Hash SHA256 court du contenu PDF."""
|
|
h = hashlib.sha256()
|
|
with open(pdf_path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(65536), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()[:16]
|
|
|
|
|
|
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)
|
|
|
|
# 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)
|
|
return paths
|
|
|
|
|
|
def load_image(path: Path) -> Image.Image:
|
|
return Image.open(path)
|