Spaces:
Running on Zero
Running on Zero
File size: 6,256 Bytes
27f55b7 7862373 27f55b7 2a8b5e5 27f55b7 7862373 27f55b7 7862373 27f55b7 64f3785 27f55b7 64f3785 27f55b7 64f3785 27f55b7 64f3785 27f55b7 64f3785 27f55b7 64f3785 858c1df 27f55b7 7862373 27f55b7 2a8b5e5 27f55b7 7862373 27f55b7 7862373 27f55b7 7862373 27f55b7 7862373 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | """Lecture OCR et décompte de la 2e colonne d'un tableau photographié."""
import os
import re
import statistics
from difflib import get_close_matches
import cv2
import numpy as np
import pytesseract
from PIL import Image, ImageOps
COLS = ["PASF", "IPM", "PR", "MUT", "RESULTAT", "CONTROLE"]
ALIASES = {"ASS": "IPM", "RESULTATS": "RESULTAT"}
KNOWN = COLS + list(ALIASES)
CONFUSIONS = str.maketrans({"J": "I", "1": "I", "L": "I", "|": "I", "!": "I", "0": "O", "5": "S", "$": "S"})
def load_bgr(source):
"""source : chemin de fichier ou objet fichier."""
img = ImageOps.exif_transpose(Image.open(source)).convert("RGB")
return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
def preprocess(bgr, size=2200):
"""Redresse la photo, neutralise les ombres, efface les traits de la grille."""
h, w = bgr.shape[:2]
s = size / max(h, w)
bgr = cv2.resize(
bgr, None, fx=s, fy=s,
interpolation=cv2.INTER_CUBIC
)
gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY)
bw = cv2.adaptiveThreshold(
gray, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV,
41, 15
)
angle = 0.0
segs = cv2.HoughLinesP(
bw, 1, np.pi / 720,
threshold=200,
minLineLength=bw.shape[1] // 4,
maxLineGap=20
)
if segs is not None:
angs = []
for segment in segs.reshape(-1, 4):
x1, y1, x2, y2 = map(int, segment)
a = np.degrees(
np.arctan2(y2 - y1, x2 - x1)
)
if abs(a) < 20:
angs.append(float(a))
if angs:
angle = float(np.median(angs))
M = cv2.getRotationMatrix2D(
(bw.shape[1] / 2, bw.shape[0] / 2),
angle, 1
)
bw = cv2.warpAffine(
bw, M,
(bw.shape[1], bw.shape[0]),
flags=cv2.INTER_LINEAR
)
hk = cv2.getStructuringElement(
cv2.MORPH_RECT, (80, 1)
)
vk = cv2.getStructuringElement(
cv2.MORPH_RECT, (1, 80)
)
grid = cv2.add(
cv2.morphologyEx(bw, cv2.MORPH_OPEN, hk),
cv2.morphologyEx(bw, cv2.MORPH_OPEN, vk)
)
bw = cv2.subtract(
bw,
cv2.dilate(grid, np.ones((3, 3), np.uint8))
)
return 255 - bw, angle
def read_words(bw, lang="eng", psm=11):
d = pytesseract.image_to_data(bw, lang=lang, config=f"--psm {int(psm)}",
output_type=pytesseract.Output.DICT)
words = []
for i, t in enumerate(d["text"]):
if t.strip():
words.append({"text": t.strip(), "left": d["left"][i], "top": d["top"][i],
"w": d["width"][i], "h": d["height"][i]})
return words
def classify(token, fuzzy=False):
"""Renvoie le code canonique (PASF, IPM, PR, MUT, RESULTAT, CONTROLE) ou None."""
u = re.sub(r"[^A-Z0-9]", "", token.upper())
if len(u) < 2:
return None
cands = [u]
if len(u) <= 4: # 1PM, |PM, lPM -> IPM
cands.append(u.translate(CONFUSIONS))
for c in cands:
if c in KNOWN:
return ALIASES.get(c, c)
if fuzzy:
for c in cands:
m = get_close_matches(c, KNOWN, n=1, cutoff=0.75)
if m:
return ALIASES.get(m[0], m[0])
return None
def count_column2(words, width, fuzzy=True):
"""Ne compte que les mots alignés sur la colonne des codes (la 2e colonne)."""
anchors = [w for w in words if classify(w["text"])]
counts = {c: 0 for c in COLS}
notes = []
if not anchors:
return counts, ["Aucun code reconnu : image peu lisible ?"]
center = statistics.median(w["left"] for w in anchors)
cmid = statistics.median(w["left"] + w["w"] / 2 for w in anchors)
tol = 0.04 * width
def in_band(w): # bord gauche OU centre (cellules centrées dans certains PDF)
return abs(w["left"] - center) <= tol or abs(w["left"] + w["w"] / 2 - cmid) <= tol
band = [w for w in words if in_band(w)]
tops = sorted(w["top"] for w in anchors)
pitch = statistics.median([y2 - y1 for y1, y2 in zip(tops, tops[1:]) if y2 - y1 > 2] or [40])
first_y = min(w["top"] for w in anchors)
unknown = []
for w in band:
code = classify(w["text"], fuzzy=fuzzy)
if code:
counts[code] += 1
elif w["top"] >= first_y - 0.9 * pitch and re.fullmatch(r"[A-Za-z]{2,}", w["text"]):
unknown.append(w["text"])
outside = len(anchors) - sum(1 for w in anchors if in_band(w))
if outside:
notes.append(f"{outside} code(s) hors 2e colonne ignoré(s)")
if unknown:
notes.append("Non reconnu en 2e colonne : " + ", ".join(unknown[:6]))
return counts, notes
def analyse(source, lang="eng", psm=11, fuzzy=True):
bw, _ = preprocess(load_bgr(source))
words = read_words(bw, lang, psm)
return count_column2(words, bw.shape[1], fuzzy)
def _open_pdf(source):
import pymupdf
if isinstance(source, (str, os.PathLike)):
return pymupdf.open(source)
data = source.read() if hasattr(source, "read") else source
return pymupdf.open(stream=data, filetype="pdf")
def analyse_pdf(source, lang="eng", psm=11, fuzzy=True, dpi=200):
"""Une page = un résultat (n° de page, comptes, remarques).
Texte natif du PDF s'il existe, sinon OCR sur la page rendue en image."""
doc = _open_pdf(source)
results = []
for i, page in enumerate(doc, 1):
words = [{"text": w[4], "left": w[0], "top": w[1], "w": w[2] - w[0], "h": w[3] - w[1]}
for w in page.get_text("words")]
counts, notes = count_column2(words, page.rect.width, fuzzy)
method = "texte natif"
if sum(counts.values()) == 0:
pix = page.get_pixmap(dpi=dpi, colorspace=pymupdf_rgb(), alpha=False)
rgb = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.height, pix.width, 3)
bw, _ = preprocess(cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR))
counts, notes = count_column2(read_words(bw, lang, psm), bw.shape[1], fuzzy)
method = "OCR"
results.append((i, counts, [f"Méthode : {method}"] + notes))
return results
def pymupdf_rgb():
import pymupdf
return pymupdf.csRGB
|