kebson commited on
Commit
ecf1403
·
verified ·
1 Parent(s): 2728550

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -68
app.py CHANGED
@@ -3,117 +3,134 @@ import numpy as np
3
  import unicodedata
4
  from paddleocr import PaddleOCR
5
 
6
- # -------------------------------------------------
7
- # OCR
8
- # -------------------------------------------------
9
- ocr = PaddleOCR(lang="fr", use_textline_orientation=True)
10
-
11
- # -------------------------------------------------
12
- # Normalisation
13
- # -------------------------------------------------
14
- def normalize(text):
 
 
 
 
15
  text = text.lower()
16
  text = unicodedata.normalize("NFD", text)
17
  text = "".join(c for c in text if unicodedata.category(c) != "Mn")
18
  return " ".join(text.split())
19
 
20
- # -------------------------------------------------
21
- # Titres possibles colonne 2
22
- # -------------------------------------------------
23
- COL_TITLES = [
 
24
  "designation",
25
  "designations",
26
  "description",
27
  "description des services"
28
- ]
29
-
30
- # -------------------------------------------------
31
- # Lignes à ignorer
32
- # -------------------------------------------------
33
- IGNORE_KEYWORDS = [
34
- "prix", "total", "ht", "htva", "tva", "ttc",
35
- "general", "generale"
36
- ]
37
-
38
- # -------------------------------------------------
39
- # Détection début cellule (règle métier)
40
- # -------------------------------------------------
41
- def is_new_cell(text):
42
- return text and text[0].isupper()
43
-
44
- # -------------------------------------------------
45
  # Fonction principale
46
- # -------------------------------------------------
47
  def extract_second_column(image):
48
  if image is None:
49
  return "Aucune image fournie."
50
 
51
  img = np.array(image)
52
- result = ocr.predict(img)
53
 
54
- if not result or not result[0]:
 
 
 
 
 
 
 
 
 
 
 
55
  return "OCR : aucun texte détecté."
56
 
57
- texts = result[0]["rec_texts"]
58
- boxes = result[0]["dt_polys"]
 
59
 
60
  blocks = []
61
  for text, box in zip(texts, boxes):
62
- text = text.strip()
63
- if len(text) < 2:
64
  continue
65
 
66
  x = np.mean([p[0] for p in box])
67
  y = np.mean([p[1] for p in box])
68
- blocks.append((text, x, y))
69
 
70
- if len(blocks) < 5:
 
 
71
  return "Pas assez de texte exploitable."
72
 
73
  # -------------------------------------------------
74
- # 1. Détection X colonne 2 par le TITRE (robuste)
75
  # -------------------------------------------------
76
  col_x = None
77
  for text, x, y in blocks:
78
  nt = normalize(text)
79
- if any(nt.startswith(t) for t in COL_TITLES):
80
- col_x = x
 
 
 
81
  break
82
 
83
  if col_x is None:
84
- return "Titre de la colonne 2 non détecté."
85
 
86
  # -------------------------------------------------
87
- # 2. Sélection blocs proches de X
88
  # -------------------------------------------------
89
- X_THRESHOLD = 60
90
- col_blocks = [(t, x, y) for t, x, y in blocks if abs(x - col_x) < X_THRESHOLD]
 
 
 
91
 
92
- if not col_blocks:
93
  return "Colonne détectée mais vide."
94
 
95
  # -------------------------------------------------
96
- # 3. Tri vertical
97
  # -------------------------------------------------
98
- col_blocks.sort(key=lambda e: e[2])
99
 
100
  # -------------------------------------------------
101
- # 4. Reconstruction cellules (RÈGLE MAJUSCULE)
102
  # -------------------------------------------------
103
- cells = []
104
  current = ""
105
  last_y = None
106
  Y_THRESHOLD = 28
107
 
108
- for text, x, y in col_blocks:
109
  nt = normalize(text)
110
 
111
  if any(k in nt for k in IGNORE_KEYWORDS):
112
  continue
113
 
114
- if current == "" or is_new_cell(text) or (last_y and abs(y - last_y) > Y_THRESHOLD):
115
  if current:
116
- cells.append(current.strip())
117
  current = text
118
  else:
119
  current += " " + text
@@ -121,34 +138,54 @@ def extract_second_column(image):
121
  last_y = y
122
 
123
  if current:
124
- cells.append(current.strip())
125
 
126
  # -------------------------------------------------
127
- # 5. Nettoyage final
128
  # -------------------------------------------------
129
  final = []
130
- for c in cells:
131
- nt = normalize(c)
132
- if len(nt) < 4:
 
 
 
 
 
 
 
 
 
 
133
  continue
134
- if sum(ch.isdigit() for ch in c) > len(c) * 0.6:
 
 
135
  continue
136
- final.append(c)
 
137
 
138
  if not final:
139
- return "Aucune cellule valide trouvée."
140
 
141
- return "\n".join(f"{i+1}. {c}" for i, c in enumerate(final))
 
 
 
142
 
143
- # -------------------------------------------------
144
- # Gradio
145
- # -------------------------------------------------
146
  demo = gr.Interface(
147
  fn=extract_second_column,
148
  inputs=gr.Image(type="pil", label="Image du tableau"),
149
- outputs=gr.Textbox(label="Contenu colonne 2", lines=15),
150
  title="Extraction fiable de la colonne 2",
151
- description="Extraction robuste de la colonne 2 (Désignation / Description)"
 
 
 
 
152
  )
153
 
154
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
3
  import unicodedata
4
  from paddleocr import PaddleOCR
5
 
6
+ # =================================================
7
+ # OCR Paddle
8
+ # =================================================
9
+ ocr = PaddleOCR(
10
+ lang="fr",
11
+ use_textline_orientation=True,
12
+ show_log=False
13
+ )
14
+
15
+ # =================================================
16
+ # Normalisation texte (casse + accents)
17
+ # =================================================
18
+ def normalize(text: str) -> str:
19
  text = text.lower()
20
  text = unicodedata.normalize("NFD", text)
21
  text = "".join(c for c in text if unicodedata.category(c) != "Mn")
22
  return " ".join(text.split())
23
 
24
+ # =================================================
25
+ # Titres possibles de la colonne 2
26
+ # (casse ignorée automatiquement)
27
+ # =================================================
28
+ COL_TITLES = {
29
  "designation",
30
  "designations",
31
  "description",
32
  "description des services"
33
+ }
34
+
35
+ # =================================================
36
+ # Mots à ignorer absolument
37
+ # =================================================
38
+ IGNORE_KEYWORDS = {
39
+ "prix", "ht", "htva", "tva", "ttc",
40
+ "total", "generale", "general"
41
+ }
42
+
43
+ # =================================================
 
 
 
 
 
 
44
  # Fonction principale
45
+ # =================================================
46
  def extract_second_column(image):
47
  if image is None:
48
  return "Aucune image fournie."
49
 
50
  img = np.array(image)
 
51
 
52
+ # -------------------------------------------------
53
+ # 0. Rotation automatique si image couchée
54
+ # -------------------------------------------------
55
+ h, w = img.shape[:2]
56
+ if w > h:
57
+ img = np.rot90(img, 1)
58
+
59
+ # -------------------------------------------------
60
+ # 1. OCR
61
+ # -------------------------------------------------
62
+ result = ocr.predict(img)
63
+ if not result:
64
  return "OCR : aucun texte détecté."
65
 
66
+ data = result[0]
67
+ texts = data.get("rec_texts", [])
68
+ boxes = data.get("dt_polys", [])
69
 
70
  blocks = []
71
  for text, box in zip(texts, boxes):
72
+ t = text.strip()
73
+ if len(t) < 2:
74
  continue
75
 
76
  x = np.mean([p[0] for p in box])
77
  y = np.mean([p[1] for p in box])
 
78
 
79
+ blocks.append((t, x, y))
80
+
81
+ if len(blocks) < 8:
82
  return "Pas assez de texte exploitable."
83
 
84
  # -------------------------------------------------
85
+ # 2. Détection robuste du X de la colonne 2
86
  # -------------------------------------------------
87
  col_x = None
88
  for text, x, y in blocks:
89
  nt = normalize(text)
90
+ for title in COL_TITLES:
91
+ if title in nt:
92
+ col_x = x
93
+ break
94
+ if col_x is not None:
95
  break
96
 
97
  if col_x is None:
98
+ return "Titre de la colonne cible non détecté."
99
 
100
  # -------------------------------------------------
101
+ # 3. Sélection des blocs de la colonne 2
102
  # -------------------------------------------------
103
+ X_THRESHOLD = 55
104
+ column_blocks = [
105
+ (t, x, y) for t, x, y in blocks
106
+ if abs(x - col_x) < X_THRESHOLD
107
+ ]
108
 
109
+ if not column_blocks:
110
  return "Colonne détectée mais vide."
111
 
112
  # -------------------------------------------------
113
+ # 4. Tri vertical (haut → bas)
114
  # -------------------------------------------------
115
+ column_blocks.sort(key=lambda e: e[2])
116
 
117
  # -------------------------------------------------
118
+ # 5. Fusion intelligente des lignes OCR
119
  # -------------------------------------------------
120
+ merged = []
121
  current = ""
122
  last_y = None
123
  Y_THRESHOLD = 28
124
 
125
+ for text, x, y in column_blocks:
126
  nt = normalize(text)
127
 
128
  if any(k in nt for k in IGNORE_KEYWORDS):
129
  continue
130
 
131
+ if last_y is None or abs(y - last_y) > Y_THRESHOLD:
132
  if current:
133
+ merged.append(current.strip())
134
  current = text
135
  else:
136
  current += " " + text
 
138
  last_y = y
139
 
140
  if current:
141
+ merged.append(current.strip())
142
 
143
  # -------------------------------------------------
144
+ # 6. Nettoyage final des cellules texte
145
  # -------------------------------------------------
146
  final = []
147
+ for line in merged:
148
+ nt = normalize(line)
149
+
150
+ # ignorer le titre de colonne
151
+ if nt in COL_TITLES:
152
+ continue
153
+
154
+ # longueur minimale
155
+ if len(nt) < 5:
156
+ continue
157
+
158
+ # ignorer lignes trop numériques
159
+ if sum(c.isdigit() for c in line) > len(line) / 3:
160
  continue
161
+
162
+ # règle métier : commence par majuscule
163
+ if not line[0].isupper():
164
  continue
165
+
166
+ final.append(line)
167
 
168
  if not final:
169
+ return "Aucune cellule texte valide trouvée."
170
 
171
+ # -------------------------------------------------
172
+ # 7. Résultat numéroté
173
+ # -------------------------------------------------
174
+ return "\n".join(f"{i+1}. {line}" for i, line in enumerate(final))
175
 
176
+ # =================================================
177
+ # Interface Gradio (Hugging Face)
178
+ # =================================================
179
  demo = gr.Interface(
180
  fn=extract_second_column,
181
  inputs=gr.Image(type="pil", label="Image du tableau"),
182
+ outputs=gr.Textbox(label="Contenu de la colonne 2"),
183
  title="Extraction fiable de la colonne 2",
184
+ description=(
185
+ "Extraction automatique et adaptative de la deuxième colonne "
186
+ "(Désignation, DESIGNATIONS, Description, Description des services) "
187
+ "à partir de tableaux scannés."
188
+ )
189
  )
190
 
191
  demo.launch(server_name="0.0.0.0", server_port=7860)