Biifruu commited on
Commit
887cbdc
·
verified ·
1 Parent(s): 5f5f941

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -0
app.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import base64
3
+ import numpy as np
4
+ import cv2
5
+ import fitz # PyMuPDF
6
+ import pytesseract
7
+ from PIL import Image
8
+ import gradio as gr
9
+
10
+ def text_area_ratio(image):
11
+ """
12
+ Calcula la proporción del área ocupada por texto basado en contornos de letras.
13
+ """
14
+ np_img = np.array(image.convert("L"))
15
+ _, thresh = cv2.threshold(np_img, 150, 255, cv2.THRESH_BINARY_INV)
16
+ contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
17
+ text_area = 0
18
+ for cnt in contours:
19
+ x, y, w, h = cv2.boundingRect(cnt)
20
+ if 8 < h < 40 and 5 < w < 100:
21
+ text_area += w * h
22
+ total_area = np_img.shape[0] * np_img.shape[1]
23
+ return text_area / total_area if total_area > 0 else 0
24
+
25
+ def has_significant_text(image):
26
+ """
27
+ Determina si una imagen presenta abundantes contornos compatibles con letras.
28
+ """
29
+ return text_area_ratio(image) > 0.25
30
+
31
+ def is_primarily_text(image, ocr_threshold=30):
32
+ """
33
+ Usa OCR para determinar si el recorte contiene principalmente texto.
34
+ Si el análisis de contornos indica presencia de texto y el OCR devuelve
35
+ más de 'ocr_threshold' caracteres, se considera principalmente textual.
36
+ """
37
+ if has_significant_text(image):
38
+ ocr_result = pytesseract.image_to_string(image, lang="eng+spa")
39
+ if len(ocr_result.strip()) > ocr_threshold:
40
+ return True
41
+ return False
42
+
43
+ def is_likely_photo(crop):
44
+ """
45
+ Evalúa si un recorte es probablemente una imagen (foto o diagrama)
46
+ basándose en la variación tonal y la cantidad de colores.
47
+ """
48
+ np_crop = np.array(crop)
49
+ gray = cv2.cvtColor(np_crop, cv2.COLOR_RGB2GRAY)
50
+ std_dev = np.std(gray)
51
+ unique_colors = len(np.unique(gray))
52
+ return std_dev > 25 and unique_colors > 50
53
+
54
+ def extract_visual_regions(image):
55
+ """
56
+ Extrae recortes de la imagen que se asemejan a imágenes embebidas.
57
+ Devuelve una lista de pares (bounding_box, crop) aceptados si:
58
+ - Son visuales (is_likely_photo),
59
+ - Tienen menos del 25% de área ocupada por texto,
60
+ - Y no se consideran principalmente texto según OCR.
61
+ """
62
+ np_img = np.array(image.convert("RGB"))
63
+ gray = cv2.cvtColor(np_img, cv2.COLOR_RGB2GRAY)
64
+ _, binary = cv2.threshold(gray, 220, 255, cv2.THRESH_BINARY_INV)
65
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
66
+ closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
67
+
68
+ num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(closed, connectivity=8)
69
+ results = []
70
+ for i in range(1, num_labels): # se omite el fondo
71
+ x, y, w, h, area = stats[i]
72
+ aspect_ratio = w / float(h)
73
+ if area > 2000 and 0.3 < aspect_ratio < 3.5:
74
+ bbox = (x, y, x + w, y + h)
75
+ crop = image.crop(bbox)
76
+ ratio = text_area_ratio(crop)
77
+ if is_likely_photo(crop) and ratio < 0.25 and not is_primarily_text(crop):
78
+ results.append((bbox, crop))
79
+ return results
80
+
81
+ def pdf_to_images_from_bytes(pdf_bytes):
82
+ """
83
+ Convierte un PDF (en bytes) en una lista de imágenes PIL.
84
+ """
85
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
86
+ images = []
87
+ for page in doc:
88
+ pix = page.get_pixmap(dpi=200)
89
+ img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
90
+ images.append(img)
91
+ doc.close()
92
+ return images
93
+
94
+ def extract_text_from_pdf_bytes(pdf_bytes):
95
+ """
96
+ Extrae y concatena el texto de todas las páginas de un PDF.
97
+ """
98
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
99
+ all_text = ""
100
+ for page in doc:
101
+ all_text += page.get_text() + "\n"
102
+ doc.close()
103
+ return all_text.strip()
104
+
105
+ def pil_to_base64(img):
106
+ """
107
+ Convierte una imagen PIL a una cadena base64 codificada en PNG.
108
+ """
109
+ buffered = io.BytesIO()
110
+ img.save(buffered, format="PNG")
111
+ return base64.b64encode(buffered.getvalue()).decode("utf-8")
112
+
113
+ def process_pdf(pdf_file):
114
+ """
115
+ Función principal que procesa el PDF.
116
+ Extrae el texto y los recortes de imagen.
117
+ """
118
+ # Si pdf_file tiene el método read(), lo usamos, de lo contrario asumimos que es una ruta de archivo.
119
+ try:
120
+ pdf_bytes = pdf_file.read() # si es objeto file
121
+ except AttributeError:
122
+ with open(pdf_file, "rb") as f:
123
+ pdf_bytes = f.read()
124
+
125
+ text = extract_text_from_pdf_bytes(pdf_bytes)
126
+ imgs = pdf_to_images_from_bytes(pdf_bytes)
127
+ crops = []
128
+ for img in imgs:
129
+ regions = extract_visual_regions(img)
130
+ for (_, crop) in regions:
131
+ crops.append(crop)
132
+ images_base64 = [pil_to_base64(img) for img in crops]
133
+ return {"text": text, "images": images_base64}
134
+
135
+ # Configuramos la interfaz de Gradio para devolver JSON.
136
+ iface = gr.Interface(
137
+ fn=process_pdf,
138
+ inputs=gr.File(label="Sube un PDF"),
139
+ outputs="json",
140
+ title="Procesador de PDFs",
141
+ description="Extrae el texto y los recortes de imagen de un PDF. La salida es un JSON con 'text' e 'images' (imagenes en base64)."
142
+ )
143
+
144
+ iface.launch()