File size: 8,694 Bytes
597d9c3
fe821af
a3dc003
 
 
f35ce5e
2c6746f
a3dc003
 
597d9c3
a3dc003
 
 
 
 
fe821af
 
 
 
a3dc003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c6746f
 
a3dc003
 
2c6746f
 
a3dc003
 
2c6746f
a3dc003
 
 
 
 
 
 
 
 
 
 
 
 
 
0c2f23e
a3dc003
 
 
 
 
2c6746f
a3dc003
 
 
 
 
 
597d9c3
2c6746f
a3dc003
 
 
 
 
 
 
597d9c3
a3dc003
597d9c3
a3dc003
 
 
597d9c3
 
a3dc003
 
 
 
 
 
 
 
597d9c3
a3dc003
 
2c6746f
a3dc003
 
2c6746f
a3dc003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597d9c3
 
a3dc003
597d9c3
 
2c6746f
a3dc003
 
597d9c3
 
 
 
 
 
 
 
 
a3dc003
597d9c3
 
 
 
 
 
 
 
2c6746f
fe821af
 
a3dc003
 
fe821af
 
 
 
 
a3dc003
fe821af
 
 
 
 
 
 
 
 
a3dc003
 
f35ce5e
fe821af
f35ce5e
a3dc003
fe821af
597d9c3
 
a3dc003
2c6746f
a3dc003
 
dca2819
2c6746f
 
a3dc003
f35ce5e
 
 
 
 
 
 
 
a3dc003
fe821af
 
597d9c3
 
2c6746f
 
a3dc003
597d9c3
 
 
 
 
 
 
 
 
f35ce5e
2c6746f
fe821af
 
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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
import os
import uuid
from PIL import Image, ImageDraw
import numpy as np
import cv2
from gemini_ner_client import get_gemini_ner_tags

# Módulos de Doctr
from doctr.models import ocr_predictor

# Módulos locales
# Asegúrate de tener este archivo 'error_handler.py' en tu entorno
# from error_handler import ErrorHandler

# --- Configuración de Directorios ---
DATASET_BASE_DIR = "dataset"
IMAGES_DIR = os.path.join(DATASET_BASE_DIR, "imagenes")


# --- Inicialización del Modelo Doctr ---
# Se carga el modelo una sola vez al inicio del módulo
try:
    # Usaremos el modelo por defecto, que es robusto.
    OCR_MODEL = ocr_predictor(pretrained=True) 
    print("✅ Modelo Doctr cargado con éxito.")
except Exception as e:
    print(f"FATAL: Error al cargar el modelo Doctr. Asegúrate de instalar doctr[full]. Error: {e}")
    OCR_MODEL = None

# --- Función Dummy para ErrorHandler ---
# Se asume que ErrorHandler.show_error existe. Si no, usa print o logging.
class ErrorHandler:
    @staticmethod
    def show_error(title, message=""):
        print(f"[{title}] ERROR: {message}")


# --- Preprocesamiento de Imagen (Activado) ---

def preprocess_image_for_ocr(image: Image.Image) -> Image.Image:
    """
    Aplica preprocesamiento con OpenCV (conversión a escala de grises y umbral adaptativo)
    para mejorar la calidad del OCR.
    """
    """
    try:
        # Convertir PIL Image a array NumPy RGB
        img_np = np.array(image.convert('RGB'))
        
        # Convertir a escala de grises
        gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
        
        # Aplicar umbral adaptativo (útil para imágenes con iluminación variable)
        thresh = cv2.adaptiveThreshold(
            gray, 
            255, 
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C, 
            cv2.THRESH_BINARY, 
            11,  # Tamaño del bloque (debe ser impar y > 1)
            2    # Constante a restar de la media
        )
        
        # Devolver el array umbralizado como una imagen PIL
        return Image.fromarray(thresh).convert('RGB')
    
    except Exception as e:
        ErrorHandler.show_error("Error de Preprocesamiento", f"Fallo al aplicar OpenCV: {e}")
        # En caso de fallo, devolvemos la imagen original
        return image
    """
    return image


# --- Función Principal de OCR con Doctr ---

def get_ocr_data_doctr(image: Image.Image):
    """
    Ejecuta Doctr en la imagen, opcionalmente preprocesada, y devuelve 
    tokens y bboxes normalizados para LayoutXLM.
    """
    if image is None:
        raise ValueError("No se proporcionó ninguna imagen.")

    if OCR_MODEL is None:
        ErrorHandler.show_error("Error de Modelo", 
                                 "El motor Doctr no está disponible. Revise el log de inicio.")
        return None, [], None

    W, H = image.size
    tokens_data = []
    
    # 💡 PASO CLAVE: Aplicar Preprocesamiento
    # Almacenamos la imagen original ANTES del preprocesamiento
    image_orig_unprocessed = image.copy()
    processed_image = preprocess_image_for_ocr(image)
    
    try:
        # Doctr espera la imagen como un array numpy RGB
        img_np = np.array(processed_image.convert('RGB')) 
        
        # 1. Ejecutar la predicción
        result = OCR_MODEL([img_np]) # El modelo espera una lista de imágenes
        
        # 2. Parsear el resultado a nivel de 'Word'
        for page in result.pages:
            for block in page.blocks:
                for line in block.lines:
                    for word in line.words:
                        token_text = word.value.strip()
                        
                        if not token_text:
                            continue

                        # Las coordenadas de Doctr son normalizadas a 0-1 (fracciones)
                        # word.geometry: [[x_min, y_min], [x_max, y_max]]
                        bbox_frac = word.geometry 

                        # BBox original (en píxeles de la imagen original)
                        x_min, y_min, x_max, y_max = [
                            int(bbox_frac[0][0] * W), 
                            int(bbox_frac[0][1] * H), 
                            int(bbox_frac[1][0] * W), 
                            int(bbox_frac[1][1] * H)
                        ]
                        bbox_original = [x_min, y_min, x_max, y_max]

                        # BBox normalizado a 0-1000 (para LayoutXLM)
                        # Nota: Se corrigen los denominadores. LayoutXLM normaliza X e Y
                        # con respecto al tamaño completo (W o H).
                        bbox_normalized = [
                            int(x_min * 1000 / W),
                            int(y_min * 1000 / H),
                            int(x_max * 1000 / W),
                            int(y_max * 1000 / H)
                        ]
                        
                        tokens_data.append({
                            'token': token_text,
                            'bbox_norm': bbox_normalized,
                            'bbox_orig': bbox_original,
                            'ner_tag': 'O' 
                        })
                        
        # 💡 Importante: Devolvemos la imagen original sin procesar, ya que 
        # esta es la que se guarda y se usa para el dibujo en la interfaz.
        return image_orig_unprocessed, tokens_data, None

    except Exception as e:
        ErrorHandler.show_error("Error de Procesamiento OCR", e)
        return None, [], None


# --- Funciones de Visualización y Guardado ---

def draw_boxes(image: Image.Image, tokens_data: list, highlight_index: int = -1):
    """Dibuja un resaltado en la imagen para el bounding box seleccionado."""
    if image is None or not tokens_data:
        return None

    img_copy = image.copy()
    draw = ImageDraw.Draw(img_copy)
    
    if highlight_index >= 0 and highlight_index < len(tokens_data):
        # Usamos bbox_orig para dibujar en la imagen a tamaño completo
        bbox = tokens_data[highlight_index]['bbox_orig']
        x1, y1, x2, y2 = bbox
        
        outline_color = (255, 0, 0)
        draw.rectangle([x1, y1, x2, y2], outline=outline_color, width=4)
        
    return img_copy


def save_image_to_dataset(image: Image.Image) -> str:
    """
    Genera un nombre único y guarda la imagen en el directorio de dataset.
    Retorna el nombre único del archivo.
    """
    # 1. Crear el nombre único (UUID + extensión)
    unique_filename = f"{uuid.uuid4()}.jpeg"
    save_path = os.path.join(IMAGES_DIR, unique_filename)

    # 2. Asegurar que el directorio exista (es importante en un entorno efímero)
    os.makedirs(IMAGES_DIR, exist_ok=True)
    
    # 3. Guardar la imagen
    image.save(save_path, format="JPEG")
    
    print(f"Imagen guardada: {save_path}")
    return unique_filename


# --- Función de Flujo Principal ---

def process_and_setup(image_file, api_key: str): # 💡 ACEPTA LA LLAVE API
    """
    Función inicial: OCR con Doctr, **NER asistido por Gemini**, 
    configuración del estado y guardar la imagen.
    """
    if image_file is None:
        empty_df = {'token': [], 'ner_tag': []}
        return None, [], None, empty_df, None, None 
        
    # 💡 Llama a la función de OCR basada en Doctr
    image_orig, tokens_data, _ = get_ocr_data_doctr(image_file) 
    
    if image_orig is None:
        empty_df = {'token': [], 'ner_tag': []}
        return None, [], None, empty_df, "Error fatal al procesar el OCR con Doctr. Revise el log.", None
    
    # 💡 PASO NUEVO: NER Asistido por Gemini (CONDICIONAL)
    msg_ner_assist = ""
    if api_key:
        # Llama a la función que SÓLO se ejecuta si api_key no es None/vacío
        tokens_data = get_gemini_ner_tags(api_key, tokens_data)
        msg_ner_assist = " (NER asistido)"
    
    # --- Guardar la Imagen Original ---
    image_filename = save_image_to_dataset(image_orig)
    
    if not tokens_data:
        empty_df = {'token': [], 'ner_tag': []}
        msg = "OCR completado. No se detectaron tokens válidos."
        ErrorHandler.show_error(msg)
        return image_orig, [], None, empty_df, msg, image_filename
        
    # Crear el DataFrame inicial para la edición en Gradio
    df_data = {
        'token': [item['token'] for item in tokens_data],
        'ner_tag': [item['ner_tag'] for item in tokens_data]
    }
    
    # La imagen inicial no tiene resaltado
    highlighted_image = image_orig.copy() 
    msg = f"OCR de Doctr completado. Tokens detectados: {len(tokens_data)}.{msg_ner_assist}"
    print(msg)
    
    return image_orig, tokens_data, highlighted_image, df_data, msg, image_filename