From 8473a3041e7ac28f944073ad47281f7d80b55ce3 Mon Sep 17 00:00:00 2001 From: adrgonlil Date: Fri, 5 Dec 2025 16:25:09 +0100 Subject: [PATCH] Limpieza de estructura --- comparacion/test_espaciado.png | Bin 27062 -> 0 bytes src/__init__.py | 3 - src/text_rectification/__init__.py | 3 +- src/text_rectification/rectificar.py | 252 --------------- src/text_rectification/rectificar2.py | 269 --------------- src/text_rectification/rectificar_simple.py | 342 -------------------- tests/__init__.py | 3 - 7 files changed, 1 insertion(+), 871 deletions(-) delete mode 100644 comparacion/test_espaciado.png delete mode 100644 src/__init__.py delete mode 100644 src/text_rectification/rectificar.py delete mode 100644 src/text_rectification/rectificar2.py delete mode 100644 src/text_rectification/rectificar_simple.py delete mode 100644 tests/__init__.py diff --git a/comparacion/test_espaciado.png b/comparacion/test_espaciado.png deleted file mode 100644 index 1c6ed771e4bf30630dab3736c2414fbfa47d2154..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27062 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 C${*nX diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index 2ffba16..0000000 --- a/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Paquete principal del proyecto de rectificación de texto. -""" diff --git a/src/text_rectification/__init__.py b/src/text_rectification/__init__.py index d267ad7..e903150 100644 --- a/src/text_rectification/__init__.py +++ b/src/text_rectification/__init__.py @@ -3,6 +3,5 @@ """ from .rectificar3 import rectificar_texto -from .rectificar_simple import rectificar_texto_simple -__all__ = ['rectificar_texto', 'rectificar_texto_simple'] +__all__ = ['rectificar_texto'] diff --git a/src/text_rectification/rectificar.py b/src/text_rectification/rectificar.py deleted file mode 100644 index 41d1123..0000000 --- a/src/text_rectification/rectificar.py +++ /dev/null @@ -1,252 +0,0 @@ -""" -Rectificación de texto recto o curvo con protección anti-recorte (padding) -========================================================================== - -Mejoras añadidas: - - Se añade padding alrededor de cada letra ANTES de rotarla - - Tras la rotación, se recalcula un bounding box “tight” - - Nunca se cortan letras al rotar - - Función principal lista para importar: rectificar_texto() -""" - -import cv2 as cv -import numpy as np -from scipy import stats - -# ============================================================================ -# CONFIGURACIÓN -# ============================================================================ -THRESHOLD_LINEALIDAD = 5 -GRADO_POLINOMIO = 3 - - -# ============================================================================ -# FUNCIONES DE PROCESAMIENTO -# ============================================================================ - -def binarizar(img): - gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) - _, th = cv.threshold(gray, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU) - if np.mean(th) > 127: - th = cv.bitwise_not(th) - return th - - -def detectar_contornos(th): - contours, _ = cv.findContours(th, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) - return [cnt for cnt in contours if cv.contourArea(cnt) > 10] - - -def calcular_centros(contours): - centros = [] - for cnt in contours: - M = cv.moments(cnt) - if M['m00'] != 0: - cx = M['m10'] / M['m00'] - cy = M['m01'] / M['m00'] - centros.append((cx, cy)) - - centros = np.array(centros) - - idx = np.argsort(centros[:, 0]) - return centros[idx], [contours[i] for i in idx] - - -def analizar_linealidad(centros): - if len(centros) <= 2: - return True, 0, centros[0, 1] if len(centros) else 0, 0 - - x = centros[:, 0] - y = centros[:, 1] - - slope, intercept, r, _, _ = stats.linregress(x, y) - - y_pred = slope * x + intercept - error_rms = np.sqrt(np.mean((y - y_pred) ** 2)) - - print(f"Análisis linealidad:") - print(f" slope = {slope:.4f}") - print(f" R² = {r*r:.4f}") - print(f" RMS = {error_rms:.2f}px") - - return (error_rms < THRESHOLD_LINEALIDAD), slope, intercept, error_rms - - -# ============================================================================ -# RECTIFICACIÓN DE TEXTO RECTO -# ============================================================================ - -def rectifica_texto_recto(img, slope): - print("→ Rectificando texto recto...") - - H, W = img.shape[:2] - ang_deg = np.degrees(np.arctan(slope)) - centro = (W // 2, H // 2) - - M = cv.getRotationMatrix2D(centro, ang_deg, 1.0) - - cos = abs(M[0, 0]) - sin = abs(M[0, 1]) - new_w = int(H * sin + W * cos) - new_h = int(H * cos + W * sin) - - M[0, 2] += new_w / 2 - centro[0] - M[1, 2] += new_h / 2 - centro[1] - - dest = cv.warpAffine(img, M, (new_w, new_h), - flags=cv.INTER_LINEAR, - borderMode=cv.BORDER_CONSTANT, - borderValue=(255, 255, 255)) - return dest - - -# ============================================================================ -# RECTIFICACIÓN DE TEXTO CURVO (con PADDING) -# ============================================================================ - -def rectifica_texto_curvo(img, th, centros, contours): - """ - Rectifica texto curvo usando interpolación polinómica y cálculo de tangentes. - Usa padding y una máscara por letra para evitar cortar o mezclar letras. - """ - print("Aplicando rectificación con interpolación de curva...") - - H, W = img.shape[:2] - - # Interpolar curva polinómica sobre los centros de las letras - x_coords = centros[:, 0] - y_coords = centros[:, 1] - - grado = min(GRADO_POLINOMIO, len(centros) - 1) - coeficientes = np.polyfit(x_coords, y_coords, grado) - polinomio = np.poly1d(coeficientes) - derivada = np.polyder(polinomio) - - print(f" Curva interpolada de grado {grado}") - print(f" Coeficientes: {coeficientes}") - - # Canvas destino con fondo blanco - dest = np.full_like(img, 255) - - # Línea base horizontal (para alinear) - baseline_y = int(np.mean(y_coords)) - - # Procesar cada letra - for i, cnt in enumerate(contours): - # Bounding box de la letra - x, y, w, h = cv.boundingRect(cnt) - - # ------------------------- - # 1) PADDING alrededor - # ------------------------- - pad = int(0.3 * max(w, h)) # 30% de margen - x1 = max(0, x - pad) - y1 = max(0, y - pad) - x2 = min(W, x + w + pad) - y2 = min(H, y + h + pad) - - crop_orig = img[y1:y2, x1:x2] - - # Máscara SOLO de esta letra (no de las vecinas) - mask_letra = np.zeros((y2 - y1, x2 - x1), dtype=np.uint8) - cnt_local = cnt.copy() - cnt_local[:, 0, 0] -= x1 # restar offset X - cnt_local[:, 0, 1] -= y1 # restar offset Y - cv.drawContours(mask_letra, [cnt_local], -1, 255, thickness=-1) - - # ------------------------- - # 2) Ángulo local (tangente) - # ------------------------- - cx = centros[i, 0] - pendiente = derivada(cx) - angulo_tangente_rad = np.arctan(pendiente) - angulo_tangente_deg = np.degrees(angulo_tangente_rad) - - print(f" Letra {i+1}/{len(contours)}:") - print(f" Centro x: {cx:.1f}") - print(f" Pendiente: {pendiente:.4f}") - print(f" Ángulo tangente: {angulo_tangente_deg:.2f}°") - - # ------------------------- - # 3) Rotar parche con padding - # ------------------------- - h_pad, w_pad = crop_orig.shape[:2] - cx_crop = w_pad / 2 - cy_crop = h_pad / 2 - - M = cv.getRotationMatrix2D((cx_crop, cy_crop), angulo_tangente_deg, 1.0) - - crop_rot = cv.warpAffine( - crop_orig, M, (w_pad, h_pad), - flags=cv.INTER_LINEAR, - borderMode=cv.BORDER_CONSTANT, - borderValue=(255, 255, 255) - ) - mask_rot = cv.warpAffine( - mask_letra, M, (w_pad, h_pad), - flags=cv.INTER_NEAREST, - borderMode=cv.BORDER_CONSTANT, - borderValue=0 - ) - - # ------------------------- - # 4) Recorte tight tras rotar - # ------------------------- - ys, xs = np.where(mask_rot > 0) - if len(xs) == 0: - continue - xa, xb = xs.min(), xs.max() + 1 - ya, yb = ys.min(), ys.max() + 1 - - crop_rot = crop_rot[ya:yb, xa:xb] - mask_rot = mask_rot[ya:yb, xa:xb] - - h_t, w_t = crop_rot.shape[:2] - - # ------------------------- - # 5) Posición en canvas - # ------------------------- - top = baseline_y - h_t // 2 - left = x # mantener X aproximada de la letra original - - # Ajustar para no salir del canvas - top = max(0, min(H - h_t, top)) - left = max(0, min(W - w_t, left)) - - y0, y1_dst = top, top + h_t - x0, x1_dst = left, left + w_t - - region = dest[y0:y1_dst, x0:x1_dst] - region[mask_rot > 0] = crop_rot[mask_rot > 0] - - return dest - - - -# ============================================================================ -# FUNCIÓN PRINCIPAL PARA IMPORTAR -# ============================================================================ - -def rectificar_texto(ruta_entrada, ruta_salida): - - img = cv.imread(ruta_entrada) - if img is None: - raise ValueError(f"No se pudo cargar {ruta_entrada}") - - th = binarizar(img) - contours = detectar_contornos(th) - if len(contours) == 0: - raise ValueError("No se encontraron letras.") - - centros, contours = calcular_centros(contours) - es_recto, slope, intercept, error = analizar_linealidad(centros) - - if es_recto: - dest = rectifica_texto_recto(img, slope) - else: - dest = rectifica_texto_curvo(img, th, centros, contours) - - cv.imwrite(ruta_salida, dest) - print(f"✓ Guardado en {ruta_salida}") - - return dest diff --git a/src/text_rectification/rectificar2.py b/src/text_rectification/rectificar2.py deleted file mode 100644 index d650ac7..0000000 --- a/src/text_rectification/rectificar2.py +++ /dev/null @@ -1,269 +0,0 @@ -import cv2 -import numpy as np -import math - -def rotar_imagen(imagen, angulo): - """ - Función auxiliar para rotar una imagen (una letra) sin cortarla. - Calcula el nuevo tamaño del bounding box para que la letra quepa al rotar. - """ - (h, w) = imagen.shape[:2] - (cX, cY) = (w // 2, h // 2) - - # Matriz de rotación básica - M = cv2.getRotationMatrix2D((cX, cY), angulo, 1.0) - - # Calcular seno y coseno - cos = np.abs(M[0, 0]) - sin = np.abs(M[0, 1]) - - # Calcular nuevas dimensiones del bounding box - nW = int((h * sin) + (w * cos)) - nH = int((h * cos) + (w * sin)) - - # Ajustar la matriz de rotación para tener en cuenta la traslación - M[0, 2] += (nW / 2) - cX - M[1, 2] += (nH / 2) - cY - - # Realizar la transformación - return cv2.warpAffine(imagen, M, (nW, nH), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=(255, 255, 255)) - -def rectificar_texto(ruta_imagen, ruta_salida=None): - # 1. CARGA Y PREPROCESAMIENTO - # --------------------------------------------------------- - img = cv2.imread(ruta_imagen) - if img is None: - print("Error: No se pudo cargar la imagen.") - return None, None - - gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) - - # Binarizar (Asumimos texto negro sobre blanco, invertimos para detectar contornos) - # Si tu imagen es texto blanco sobre negro, quita el cv2.THRESH_BINARY_INV y usa cv2.THRESH_BINARY - _, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) - - # 2. DETECCIÓN DE CARACTERES - # --------------------------------------------------------- - # Encontrar contornos - contornos, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - datos_letras = [] - - # Filtramos ruido (contornos muy pequeños) y extraemos datos - for c in contornos: - x, y, w, h = cv2.boundingRect(c) - if w > 5 and h > 5: # Filtro simple de ruido - # Calcular centro de masa del contorno (más preciso que el centro de la bounding box) - M = cv2.moments(c) - if M["m00"] != 0: - cx = int(M["m10"] / M["m00"]) - cy = int(M["m01"] / M["m00"]) - else: - # Fallback a centro de bounding box si no se puede calcular el centro de masa - cx = x + w // 2 - cy = y + h // 2 - - # Extraer la región de interés (la letra en sí) - roi = img[y:y+h, x:x+w] - datos_letras.append({ - 'x': x, 'y': y, 'w': w, 'h': h, - 'cx': cx, 'cy': cy, - 'roi': roi - }) - - # IMPORTANTE: Ordenar letras de izquierda a derecha según su coordenada X - datos_letras.sort(key=lambda k: k['x']) - - if not datos_letras: - print("No se detectaron letras.") - return None, None - - # Extraer arrays de coordenadas para cálculos matemáticos - puntos_x = np.array([d['cx'] for d in datos_letras]) - puntos_y = np.array([d['cy'] for d in datos_letras]) - puntos = np.array([[d['cx'], d['cy']] for d in datos_letras], dtype=np.int32) - - # 3. DETECCIÓN: ¿RECTO O CURVO? - # --------------------------------------------------------- - # Usamos np.polyfit grado 1 para ver cuánto se desvían los puntos de una recta - # Calculamos el error cuadrático medio (MSE) - z = np.polyfit(puntos_x, puntos_y, 1) - p = np.poly1d(z) - y_pred = p(puntos_x) - error = np.mean((puntos_y - y_pred) ** 2) - - # Umbral empírico: Si el error es bajo, es una recta. Si es alto, es curva. - UMBRAL_CURVATURA = 50 - es_curvo = error > UMBRAL_CURVATURA - - print(f"Error de ajuste lineal: {error:.2f}. Detección: {'CURVO' if es_curvo else 'RECTO'}") - - letras_procesadas = [] - - # 4. LÓGICA DE RECTIFICACIÓN - # --------------------------------------------------------- - - if not es_curvo: - # --- CASO TEXTO RECTO --- - # Usamos cv2.fitLine como se solicitó - [vx, vy, x0, y0] = cv2.fitLine(puntos, cv2.DIST_L2, 0, 0.01, 0.01) - - # Calcular ángulo de la recta global - # Math.atan2 devuelve radianes, convertimos a grados - angulo_radianes = math.atan2(vy, vx) - angulo_grados = math.degrees(angulo_radianes) - - print(f"Pendiente detectada (grados): {angulo_grados:.2f}") - - # Rotar todas las letras con el MISMO ángulo - for item in datos_letras: - # Rotamos la letra para que quede horizontal (restamos el ángulo de inclinación) - roi_rotada = rotar_imagen(item['roi'], angulo_grados) - letras_procesadas.append(roi_rotada) - - else: - # --- CASO TEXTO CURVO --- - # Ajustamos un polinomio de grado 2 (parábola) - coeficientes = np.polyfit(puntos_x, puntos_y, 2) # a*x^2 + b*x + c - a, b, c = coeficientes - - print("Ajustando curva polinómica...") - - for item in datos_letras: - cx = item['cx'] - - # Calcular la derivada en el punto x (pendiente de la tangente) - # Derivada de ax^2 + bx + c es: 2ax + b - pendiente_tangente = (2 * a * cx) + b - - # Convertir pendiente a ángulo - angulo_tangente = math.degrees(math.atan(pendiente_tangente)) - - # Rotar la letra individualmente según su tangente local - roi_rotada = rotar_imagen(item['roi'], angulo_tangente) - letras_procesadas.append(roi_rotada) - - # 5. RECONSTRUCCIÓN (SALIDA) - Respetando distancias relativas y alineación - # --------------------------------------------------------- - # Calcular distancias originales entre caracteres (centros X) - distancias_originales = [] - for i in range(len(datos_letras) - 1): - dist = datos_letras[i+1]['x'] - (datos_letras[i]['x'] + datos_letras[i]['w']) - distancias_originales.append(max(dist, 5)) # Mínimo 5px para evitar solapamientos - - # Calcular centroides de las letras procesadas para alinearlos - centroides_y = [] - for letra in letras_procesadas: - # Binarizar letra para encontrar su centro de masa - gray_letra = cv2.cvtColor(letra, cv2.COLOR_BGR2GRAY) if len(letra.shape) == 3 else letra - _, thresh_letra = cv2.threshold(gray_letra, 127, 255, cv2.THRESH_BINARY_INV) - - # Encontrar contornos para calcular centro de masa preciso - contornos_letra, _ = cv2.findContours(thresh_letra, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - if contornos_letra: - # Usar el contorno más grande - cnt_principal = max(contornos_letra, key=cv2.contourArea) - M_letra = cv2.moments(cnt_principal) - if M_letra["m00"] != 0: - cy_letra = M_letra["m01"] / M_letra["m00"] - else: - cy_letra = letra.shape[0] // 2 - else: - cy_letra = letra.shape[0] // 2 - - centroides_y.append(cy_letra) - - # Calculamos altura máxima y ancho total necesario - alturas = [img.shape[0] for img in letras_procesadas] - anchos = [img.shape[1] for img in letras_procesadas] - - alto_max = max(alturas) - # Ancho total: suma de anchos de letras + distancias originales entre ellas - ancho_total = sum(anchos) + sum(distancias_originales) + 20 # +20 para márgenes - - # Definir línea base común (baseline) en el canvas - baseline_y = alto_max // 2 + 20 # Posición Y de la baseline común - - # Lienzo blanco con altura suficiente - salida = np.ones((alto_max + 40, ancho_total), dtype=np.uint8) * 255 - # Convertir a BGR para guardar igual que la entrada - salida_bgr = cv2.cvtColor(salida, cv2.COLOR_GRAY2BGR) - - x_offset = 10 - - for idx, letra in enumerate(letras_procesadas): - h_letra, w_letra = letra.shape[:2] - - # Calcular posición Y para alinear el centroide con la baseline común - # El centroide de esta letra debe quedar en baseline_y - cy_letra_local = centroides_y[idx] - y_pos = int(baseline_y - cy_letra_local) - - # Asegurarse de que la letra no se sale del canvas - y_pos = max(0, min(y_pos, salida_bgr.shape[0] - h_letra)) - - # Pegar la letra en el lienzo - salida_bgr[y_pos:y_pos+h_letra, x_offset:x_offset+w_letra] = letra - - # Avanzar cursor X: ancho de letra + distancia original a la siguiente - x_offset += w_letra - if idx < len(distancias_originales): - x_offset += distancias_originales[idx] - - # Guardar imagen - if ruta_salida: - cv2.imwrite(ruta_salida, salida_bgr) - print(f"Imagen guardada como '{ruta_salida}'") - else: - cv2.imwrite('resultado_rectificado.png', salida_bgr) - print("Imagen guardada como 'resultado_rectificado.png'") - - # 6. CALCULAR MÉTRICAS DE CALIDAD - # --------------------------------------------------------- - # Calcular desviación estándar de las posiciones Y de las letras procesadas - # (una baseline recta debería tener baja desviación) - gray_salida = cv2.cvtColor(salida_bgr, cv2.COLOR_BGR2GRAY) - _, thresh_salida = cv2.threshold(gray_salida, 127, 255, cv2.THRESH_BINARY_INV) - contornos_salida, _ = cv2.findContours(thresh_salida, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - centros_y_rectificados = [] - for c in contornos_salida: - x, y, w, h = cv2.boundingRect(c) - if w > 5 and h > 5: - # Usar centro de masa para mayor precisión en las métricas - M = cv2.moments(c) - if M["m00"] != 0: - cy = int(M["m01"] / M["m00"]) - else: - cy = y + h // 2 - centros_y_rectificados.append(cy) - - baseline_std = np.std(centros_y_rectificados) if centros_y_rectificados else 0 - - # Score de calidad: inversamente proporcional al error y a la desviación de baseline - # Cuanto menor sea el baseline_std, mejor es la rectificación - quality_score = 100.0 / (1.0 + baseline_std) - - metricas = { - 'error_ajuste': error, - 'es_curvo': es_curvo, - 'baseline_std': baseline_std, - 'quality_score': quality_score, - 'num_letras': len(datos_letras) - } - - return salida_bgr, metricas - - -# --- EJECUCIÓN --- -# Cambia 'texto_curvo.png' por el nombre de tu imagen -if __name__ == "__main__": - # Genera una imagen de prueba o usa una ruta existente - resultado, metricas = rectificar_texto('images/texto_onda_rapida.png') - if metricas: - print(f"\nMétricas de calidad:") - print(f" - Número de letras: {metricas['num_letras']}") - print(f" - Error de ajuste: {metricas['error_ajuste']:.2f}") - print(f" - Desviación baseline: {metricas['baseline_std']:.2f}") - print(f" - Score de calidad: {metricas['quality_score']:.2f}") \ No newline at end of file diff --git a/src/text_rectification/rectificar_simple.py b/src/text_rectification/rectificar_simple.py deleted file mode 100644 index 7140d36..0000000 --- a/src/text_rectification/rectificar_simple.py +++ /dev/null @@ -1,342 +0,0 @@ -""" -Script simple y autocontenido para rectificar texto deformado. -Usa exclusivamente OpenCV y NumPy. - -Flujo: -1. Leer imagen en escala de grises -2. Binarizar -3. Detectar contornos (cada contorno = una letra) -4. Ordenar letras por posición x -5. Calcular centros de cada letra -6. Determinar si el texto es recto o curvo -7. Calcular orientación de cada letra -8. Rectificar cada letra individualmente -9. Reconstruir texto en una línea horizontal -""" - -import cv2 as cv -import numpy as np -import sys -import os - - -def rectificar_texto_simple(ruta_entrada, ruta_salida): - """ - Función principal que rectifica texto deformado. - - Args: - ruta_entrada: Ruta de la imagen con texto deformado - ruta_salida: Ruta donde guardar el texto rectificado - """ - - # ================================================================== - # PASO 1: LEER IMAGEN EN ESCALA DE GRISES - # ================================================================== - print("1. Leyendo imagen...") - imagen = cv.imread(ruta_entrada, cv.IMREAD_GRAYSCALE) - - if imagen is None: - raise ValueError(f"No se pudo leer la imagen: {ruta_entrada}") - - print(f" Dimensiones: {imagen.shape}") - - # ================================================================== - # PASO 2: BINARIZACIÓN SIMPLE - # ================================================================== - print("\n2. Binarizando imagen...") - # Usar threshold de Otsu para binarización automática - _, imagen_binaria = cv.threshold(imagen, 0, 255, cv.THRESH_BINARY_INV + cv.THRESH_OTSU) - print(" Binarización completada") - - # ================================================================== - # PASO 3: DETECCIÓN DE CONTORNOS (CADA CONTORNO = UNA LETRA) - # ================================================================== - print("\n3. Detectando contornos (letras)...") - contornos, _ = cv.findContours(imagen_binaria, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) - print(f" Contornos encontrados: {len(contornos)}") - - # Filtrar contornos muy pequeños (ruido) - area_minima = 50 # píxeles cuadrados - contornos_filtrados = [c for c in contornos if cv.contourArea(c) > area_minima] - print(f" Contornos después de filtrar ruido: {len(contornos_filtrados)}") - - if len(contornos_filtrados) == 0: - raise ValueError("No se detectaron letras en la imagen") - - # ================================================================== - # PASO 4: CALCULAR BOUNDING BOXES Y ORDENAR POR POSICIÓN X - # ================================================================== - print("\n4. Calculando bounding boxes y ordenando letras...") - letras_info = [] - - for contorno in contornos_filtrados: - x, y, w, h = cv.boundingRect(contorno) - - # Calcular el centro de la letra - centro_x = x + w // 2 - centro_y = y + h // 2 - - letras_info.append({ - 'contorno': contorno, - 'x': x, - 'y': y, - 'w': w, - 'h': h, - 'centro_x': centro_x, - 'centro_y': centro_y - }) - - # Ordenar letras de izquierda a derecha - letras_info.sort(key=lambda letra: letra['centro_x']) - print(f" Letras ordenadas: {len(letras_info)}") - - # ================================================================== - # PASO 5: EXTRAER CENTROS PARA AJUSTE DE CURVA - # ================================================================== - print("\n5. Extrayendo centros de letras...") - centros_x = np.array([letra['centro_x'] for letra in letras_info], dtype=np.float32) - centros_y = np.array([letra['centro_y'] for letra in letras_info], dtype=np.float32) - print(f" Centros extraídos: {len(centros_x)} puntos") - - # ================================================================== - # PASO 6: DETERMINAR SI EL TEXTO ES RECTO O CURVO - # ================================================================== - print("\n6. Determinando tipo de deformación...") - - # Intentar ajustar una línea recta - puntos_2d = np.column_stack([centros_x, centros_y]) - [vx, vy, x0, y0] = cv.fitLine(puntos_2d, cv.DIST_L2, 0, 0.01, 0.01) - - # Calcular error de ajuste lineal - # y_predicho = y0 + (vy/vx) * (x - x0) - pendiente_recta = vy / vx if vx != 0 else 0 - y_pred_lineal = y0 + pendiente_recta * (centros_x - x0) - error_lineal = np.sqrt(np.mean((centros_y - y_pred_lineal) ** 2)) - - print(f" Error de ajuste lineal (RMSE): {error_lineal:.2f} píxeles") - - # Umbral para decidir si es curvo o recto - umbral_curvatura = 10.0 # píxeles - - if error_lineal < umbral_curvatura: - # TEXTO RECTO - print(" Tipo: TEXTO RECTO") - es_curvo = False - angulo_global = np.arctan2(vy[0], vx[0]) - print(f" Ángulo global: {np.degrees(angulo_global):.2f}°") - else: - # TEXTO CURVO - print(" Tipo: TEXTO CURVO") - es_curvo = True - - # Ajustar polinomio de grado 2 - coeficientes = np.polyfit(centros_x, centros_y, 2) - print(f" Coeficientes polinomio: a={coeficientes[0]:.6f}, b={coeficientes[1]:.6f}, c={coeficientes[2]:.2f}") - - # Verificar ajuste del polinomio - y_pred_poly = np.polyval(coeficientes, centros_x) - error_poly = np.sqrt(np.mean((centros_y - y_pred_poly) ** 2)) - print(f" Error de ajuste polinómico (RMSE): {error_poly:.2f} píxeles") - - # ================================================================== - # PASO 7: CALCULAR ORIENTACIÓN DE CADA LETRA - # ================================================================== - print("\n7. Calculando orientación de cada letra...") - - for letra in letras_info: - if es_curvo: - # Para texto curvo: calcular tangente en la posición x de cada letra - # Derivada de y = ax² + bx + c es y' = 2ax + b - x_centro = letra['centro_x'] - tangente = 2 * coeficientes[0] * x_centro + coeficientes[1] - angulo = np.arctan(tangente) - else: - # Para texto recto: usar el ángulo global - angulo = angulo_global - - letra['angulo'] = angulo - - print(f" Ángulos calculados para {len(letras_info)} letras") - - # ================================================================== - # PASO 8: RECTIFICAR CADA LETRA INDIVIDUALMENTE - # ================================================================== - print("\n8. Rectificando letras individualmente...") - - letras_rectificadas = [] - altura_maxima = max([letra['h'] for letra in letras_info]) - - for i, letra in enumerate(letras_info): - # Extraer la región de la letra de la imagen binaria - x, y, w, h = letra['x'], letra['y'], letra['w'], letra['h'] - roi = imagen_binaria[y:y+h, x:x+w].copy() - - # Obtener el centro de la letra en coordenadas locales - centro_local = (w // 2, h // 2) - - # Calcular ángulo de rotación (convertir a grados y negar para rectificar) - angulo_grados = -np.degrees(letra['angulo']) - - # Crear matriz de rotación - matriz_rotacion = cv.getRotationMatrix2D(centro_local, angulo_grados, 1.0) - - # Calcular nuevas dimensiones después de la rotación - cos = np.abs(matriz_rotacion[0, 0]) - sin = np.abs(matriz_rotacion[0, 1]) - nuevo_w = int(h * sin + w * cos) - nuevo_h = int(h * cos + w * sin) - - # Ajustar la matriz de rotación para la nueva dimensión - matriz_rotacion[0, 2] += (nuevo_w / 2) - centro_local[0] - matriz_rotacion[1, 2] += (nuevo_h / 2) - centro_local[1] - - # Aplicar rotación - letra_rotada = cv.warpAffine(roi, matriz_rotacion, (nuevo_w, nuevo_h), - flags=cv.INTER_CUBIC, - borderMode=cv.BORDER_CONSTANT, - borderValue=0) - - # Recortar el borde negro extra - # Encontrar la región no-cero - coords = cv.findNonZero(letra_rotada) - if coords is not None: - x_min, y_min, w_crop, h_crop = cv.boundingRect(coords) - letra_rotada = letra_rotada[y_min:y_min+h_crop, x_min:x_min+w_crop] - - letras_rectificadas.append({ - 'imagen': letra_rotada, - 'altura': letra_rotada.shape[0], - 'ancho': letra_rotada.shape[1], - 'orden': i - }) - - print(f" Letras rectificadas: {len(letras_rectificadas)}") - - # ================================================================== - # PASO 9: RECONSTRUIR TEXTO EN UNA LÍNEA HORIZONTAL - # ================================================================== - print("\n9. Reconstruyendo texto en línea horizontal...") - - # Normalizar alturas (escalar todas a la altura máxima) - altura_objetivo = altura_maxima - letras_normalizadas = [] - - for letra in letras_rectificadas: - img = letra['imagen'] - h_actual = img.shape[0] - - # Si la letra es más pequeña, redimensionar - if h_actual < altura_objetivo: - factor_escala = altura_objetivo / h_actual - nuevo_ancho = int(img.shape[1] * factor_escala) - img_escalada = cv.resize(img, (nuevo_ancho, altura_objetivo), - interpolation=cv.INTER_CUBIC) - elif h_actual > altura_objetivo: - # Si es más grande, redimensionar hacia abajo - factor_escala = altura_objetivo / h_actual - nuevo_ancho = int(img.shape[1] * factor_escala) - img_escalada = cv.resize(img, (nuevo_ancho, altura_objetivo), - interpolation=cv.INTER_AREA) - else: - img_escalada = img - - letras_normalizadas.append(img_escalada) - - # Definir espaciado entre letras - espaciado = 10 # píxeles - - # Calcular ancho total de la imagen final - ancho_total = sum([letra.shape[1] for letra in letras_normalizadas]) - ancho_total += espaciado * (len(letras_normalizadas) - 1) - - # Agregar márgenes - margen = 20 - ancho_total += 2 * margen - altura_total = altura_objetivo + 2 * margen - - # Crear imagen de salida (fondo blanco) - imagen_salida = np.zeros((altura_total, ancho_total), dtype=np.uint8) - - # Colocar cada letra en la imagen final - x_actual = margen - y_base = margen - - for letra_img in letras_normalizadas: - h, w = letra_img.shape - - # Colocar la letra - imagen_salida[y_base:y_base+h, x_actual:x_actual+w] = letra_img - - # Avanzar posición x - x_actual += w + espaciado - - # Invertir para que el texto sea negro sobre blanco - imagen_salida = cv.bitwise_not(imagen_salida) - - print(f" Imagen final: {imagen_salida.shape}") - print(f" Dimensiones: {ancho_total}x{altura_total} píxeles") - - # ================================================================== - # PASO 10: GUARDAR RESULTADO - # ================================================================== - print(f"\n10. Guardando resultado en: {ruta_salida}") - - # Crear directorio si no existe - directorio_salida = os.path.dirname(ruta_salida) - if directorio_salida and not os.path.exists(directorio_salida): - os.makedirs(directorio_salida) - - cv.imwrite(ruta_salida, imagen_salida) - print(" ✓ Imagen guardada exitosamente") - - return imagen_salida - - -def main(): - """ - Función principal para ejecutar desde línea de comandos. - """ - if len(sys.argv) < 2: - print("\nUso: python rectificar_simple.py [imagen_salida]") - print("\nEjemplo:") - print(" python rectificar_simple.py images/synth_texto_arco_arriba.png output/rectificado.png") - print(" python rectificar_simple.py images/synth_texto_onda_suave.png") - print("\nSi no se especifica imagen de salida, se usará: output/rectificado.png") - sys.exit(1) - - ruta_entrada = sys.argv[1] - - if len(sys.argv) >= 3: - ruta_salida = sys.argv[2] - else: - ruta_salida = "output/rectificado.png" - - print("\n" + "="*70) - print("RECTIFICACIÓN DE TEXTO DEFORMADO") - print("="*70) - print(f"\nImagen entrada: {ruta_entrada}") - print(f"Imagen salida: {ruta_salida}") - print() - - try: - resultado = rectificar_texto_simple(ruta_entrada, ruta_salida) - - print("\n" + "="*70) - print("✓ PROCESO COMPLETADO EXITOSAMENTE") - print("="*70) - print(f"\nImagen rectificada guardada en: {ruta_salida}") - print() - - except Exception as e: - print("\n" + "="*70) - print("✗ ERROR EN EL PROCESO") - print("="*70) - print(f"\nError: {str(e)}") - print() - import traceback - traceback.print_exc() - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index d250d0a..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Tests para el proyecto de rectificación de texto. -"""