diff --git a/comparacion/test_espaciado.png b/comparacion/test_espaciado.png new file mode 100644 index 0000000..1c6ed77 Binary files /dev/null and b/comparacion/test_espaciado.png differ diff --git a/generar.py b/generar.py deleted file mode 100644 index 098c18e..0000000 --- a/generar.py +++ /dev/null @@ -1,49 +0,0 @@ -import cv2 as cv -import numpy as np - -# ================================ -# CONFIGURACIÓN -# ================================ -ANCHO = 800 -ALTO = 300 -TEXTO = "TEXT PROOFING" -FONT = cv.FONT_HERSHEY_SIMPLEX -FONT_SCALE = 3 -COLOR = (0, 0, 0) # Negro -GROSOR = 8 -ANGULO = 19 # grados (negativo = inclinado hacia la izquierda) -NOMBRE_SALIDA = "./imagenes_generadas/texto_sintetico3.png" - -# ================================ -# 1) Crear fondo blanco -# ================================ -img = np.full((ALTO, ANCHO, 3), 255, dtype=np.uint8) - -# ================================ -# 2) Obtener tamaño del texto -# ================================ -(tw, th), baseline = cv.getTextSize(TEXTO, FONT, FONT_SCALE, GROSOR) - -# Coordenadas para centrar el texto -x = (ANCHO - tw) // 2 -y = (ALTO + th) // 2 - -# ================================ -# 3) Crear una imagen auxiliar para rotar el texto -# ================================ -aux = np.full_like(img, 255) -cv.putText(aux, TEXTO, (x, y), FONT, FONT_SCALE, COLOR, GROSOR, cv.LINE_AA) - -# ================================ -# 4) Rotación ligera del texto -# ================================ -M = cv.getRotationMatrix2D((ANCHO//2, ALTO//2), ANGULO, 1.0) -rotada = cv.warpAffine(aux, M, (ANCHO, ALTO), - flags=cv.INTER_LINEAR, - borderValue=(255, 255, 255)) - -# ================================ -# 5) Guardar resultado -# ================================ -cv.imwrite(NOMBRE_SALIDA, rotada) -print(f"Imagen sintética generada: {NOMBRE_SALIDA}") diff --git a/imagenes_generadas/texto_arco_abajo.png b/imagenes_generadas/texto_arco_abajo.png deleted file mode 100644 index a8e1251..0000000 Binary files a/imagenes_generadas/texto_arco_abajo.png and /dev/null differ diff --git a/imagenes_generadas/texto_arco_arriba.png b/imagenes_generadas/texto_arco_arriba.png deleted file mode 100644 index a06ae15..0000000 Binary files a/imagenes_generadas/texto_arco_arriba.png and /dev/null differ diff --git a/imagenes_generadas/texto_arco_rotacion.png b/imagenes_generadas/texto_arco_rotacion.png deleted file mode 100644 index 060bacc..0000000 Binary files a/imagenes_generadas/texto_arco_rotacion.png and /dev/null differ diff --git a/imagenes_generadas/texto_arco_ruido.png b/imagenes_generadas/texto_arco_ruido.png deleted file mode 100644 index cf76e6f..0000000 Binary files a/imagenes_generadas/texto_arco_ruido.png and /dev/null differ diff --git a/imagenes_generadas/texto_base.png b/imagenes_generadas/texto_base.png deleted file mode 100644 index 10f9fb7..0000000 Binary files a/imagenes_generadas/texto_base.png and /dev/null differ diff --git a/imagenes_generadas/texto_onda_compleja.png b/imagenes_generadas/texto_onda_compleja.png deleted file mode 100644 index 9006181..0000000 Binary files a/imagenes_generadas/texto_onda_compleja.png and /dev/null differ diff --git a/imagenes_generadas/texto_onda_moderada.png b/imagenes_generadas/texto_onda_moderada.png deleted file mode 100644 index 99d119e..0000000 Binary files a/imagenes_generadas/texto_onda_moderada.png and /dev/null differ diff --git a/imagenes_generadas/texto_onda_rapida.png b/imagenes_generadas/texto_onda_rapida.png deleted file mode 100644 index c41b010..0000000 Binary files a/imagenes_generadas/texto_onda_rapida.png and /dev/null differ diff --git a/imagenes_generadas/texto_onda_ruido_bajo.png b/imagenes_generadas/texto_onda_ruido_bajo.png deleted file mode 100644 index 5b5473a..0000000 Binary files a/imagenes_generadas/texto_onda_ruido_bajo.png and /dev/null differ diff --git a/imagenes_generadas/texto_onda_ruido_medio.png b/imagenes_generadas/texto_onda_ruido_medio.png deleted file mode 100644 index 88f7631..0000000 Binary files a/imagenes_generadas/texto_onda_ruido_medio.png and /dev/null differ diff --git a/imagenes_generadas/texto_onda_suave.png b/imagenes_generadas/texto_onda_suave.png deleted file mode 100644 index 3d388cc..0000000 Binary files a/imagenes_generadas/texto_onda_suave.png and /dev/null differ diff --git a/imagenes_generadas/texto_perspectiva_fuerte.png b/imagenes_generadas/texto_perspectiva_fuerte.png deleted file mode 100644 index a1d3e4a..0000000 Binary files a/imagenes_generadas/texto_perspectiva_fuerte.png and /dev/null differ diff --git a/imagenes_generadas/texto_perspectiva_suave.png b/imagenes_generadas/texto_perspectiva_suave.png deleted file mode 100644 index dc0aa6f..0000000 Binary files a/imagenes_generadas/texto_perspectiva_suave.png and /dev/null differ diff --git a/imagenes_generadas/texto_rectificado2.png b/imagenes_generadas/texto_rectificado2.png deleted file mode 100644 index e5f3fcf..0000000 Binary files a/imagenes_generadas/texto_rectificado2.png and /dev/null differ diff --git a/imagenes_generadas/texto_rectificado5.png b/imagenes_generadas/texto_rectificado5.png deleted file mode 100644 index c1a2981..0000000 Binary files a/imagenes_generadas/texto_rectificado5.png and /dev/null differ diff --git a/imagenes_generadas/texto_rotacion_fuerte.png b/imagenes_generadas/texto_rotacion_fuerte.png deleted file mode 100644 index af5f467..0000000 Binary files a/imagenes_generadas/texto_rotacion_fuerte.png and /dev/null differ diff --git a/imagenes_generadas/texto_rotacion_moderada.png b/imagenes_generadas/texto_rotacion_moderada.png deleted file mode 100644 index 9cd527d..0000000 Binary files a/imagenes_generadas/texto_rotacion_moderada.png and /dev/null differ diff --git a/imagenes_generadas/texto_rotacion_suave.png b/imagenes_generadas/texto_rotacion_suave.png deleted file mode 100644 index 9989b75..0000000 Binary files a/imagenes_generadas/texto_rotacion_suave.png and /dev/null differ diff --git a/imagenes_generadas/texto_sintetico.png b/imagenes_generadas/texto_sintetico.png deleted file mode 100644 index c35df70..0000000 Binary files a/imagenes_generadas/texto_sintetico.png and /dev/null differ diff --git a/imagenes_generadas/texto_sintetico2.png b/imagenes_generadas/texto_sintetico2.png deleted file mode 100644 index d2d2417..0000000 Binary files a/imagenes_generadas/texto_sintetico2.png and /dev/null differ diff --git a/imagenes_generadas/texto_sintetico3.png b/imagenes_generadas/texto_sintetico3.png deleted file mode 100644 index 342c8e8..0000000 Binary files a/imagenes_generadas/texto_sintetico3.png and /dev/null differ diff --git a/images/synth_texto_arco_abajo.png b/images/synth_texto_arco_abajo.png new file mode 100644 index 0000000..7ff0083 Binary files /dev/null and b/images/synth_texto_arco_abajo.png differ diff --git a/images/synth_texto_arco_arriba.png b/images/synth_texto_arco_arriba.png new file mode 100644 index 0000000..f616f95 Binary files /dev/null and b/images/synth_texto_arco_arriba.png differ diff --git a/images/synth_texto_arco_rotacion.png b/images/synth_texto_arco_rotacion.png new file mode 100644 index 0000000..c0b20a9 Binary files /dev/null and b/images/synth_texto_arco_rotacion.png differ diff --git a/images/synth_texto_base.png b/images/synth_texto_base.png new file mode 100644 index 0000000..294c123 Binary files /dev/null and b/images/synth_texto_base.png differ diff --git a/images/synth_texto_curva_perspectiva.png b/images/synth_texto_curva_perspectiva.png new file mode 100644 index 0000000..42028c9 Binary files /dev/null and b/images/synth_texto_curva_perspectiva.png differ diff --git a/images/synth_texto_onda_compleja.png b/images/synth_texto_onda_compleja.png new file mode 100644 index 0000000..8c9e765 Binary files /dev/null and b/images/synth_texto_onda_compleja.png differ diff --git a/images/synth_texto_onda_moderada.png b/images/synth_texto_onda_moderada.png new file mode 100644 index 0000000..c45dbbf Binary files /dev/null and b/images/synth_texto_onda_moderada.png differ diff --git a/images/synth_texto_onda_rapida.png b/images/synth_texto_onda_rapida.png new file mode 100644 index 0000000..d6bc881 Binary files /dev/null and b/images/synth_texto_onda_rapida.png differ diff --git a/images/synth_texto_onda_rotacion.png b/images/synth_texto_onda_rotacion.png new file mode 100644 index 0000000..fb45e00 Binary files /dev/null and b/images/synth_texto_onda_rotacion.png differ diff --git a/images/synth_texto_onda_suave.png b/images/synth_texto_onda_suave.png new file mode 100644 index 0000000..f261c50 Binary files /dev/null and b/images/synth_texto_onda_suave.png differ diff --git a/images/synth_texto_perspectiva_fuerte.png b/images/synth_texto_perspectiva_fuerte.png new file mode 100644 index 0000000..f12f0dc Binary files /dev/null and b/images/synth_texto_perspectiva_fuerte.png differ diff --git a/images/synth_texto_perspectiva_suave.png b/images/synth_texto_perspectiva_suave.png new file mode 100644 index 0000000..51f8d52 Binary files /dev/null and b/images/synth_texto_perspectiva_suave.png differ diff --git a/images/synth_texto_rotacion_fuerte.png b/images/synth_texto_rotacion_fuerte.png new file mode 100644 index 0000000..4ad2176 Binary files /dev/null and b/images/synth_texto_rotacion_fuerte.png differ diff --git a/images/synth_texto_rotacion_moderada.png b/images/synth_texto_rotacion_moderada.png new file mode 100644 index 0000000..61507c2 Binary files /dev/null and b/images/synth_texto_rotacion_moderada.png differ diff --git a/images/synth_texto_rotacion_suave.png b/images/synth_texto_rotacion_suave.png new file mode 100644 index 0000000..8b5cd87 Binary files /dev/null and b/images/synth_texto_rotacion_suave.png differ diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..2ffba16 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,3 @@ +""" +Paquete principal del proyecto de rectificación de texto. +""" diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4c5a4db Binary files /dev/null and b/src/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/text_generation/__init__.py b/src/text_generation/__init__.py new file mode 100644 index 0000000..3f0582b --- /dev/null +++ b/src/text_generation/__init__.py @@ -0,0 +1,5 @@ +""" +Módulo para generación de imágenes sintéticas de texto deformado. +""" + +from .generar import * diff --git a/src/text_generation/generar.py b/src/text_generation/generar.py new file mode 100644 index 0000000..1425076 --- /dev/null +++ b/src/text_generation/generar.py @@ -0,0 +1,95 @@ +import cv2 as cv +import numpy as np + +# ================================ +# CONFIGURACIÓN +# ================================ +ANCHO = 1200 +ALTO = 400 +TEXTO = "TEXT PROOFING" +FONT = cv.FONT_HERSHEY_SIMPLEX +FONT_SCALE = 3 +COLOR = (0, 0, 0) # Negro +GROSOR = 8 + +# Parámetros de deformación +ANGULO = 15 # grados de inclinación global (0 = sin inclinación) +CURVATURA = 0.00008 # Factor de curvatura (0 = sin curvatura, valores pequeños como 0.0001) +AMPLITUD_ONDA = 0 # Amplitud de ondulación vertical en píxeles (0 = sin ondulación) +FRECUENCIA_ONDA = 0.01 # Frecuencia de la ondulación + +NOMBRE_SALIDA = "./images/texto_sintetico_curved.png" + +# ================================ +# 1) Crear imagen grande para evitar recortes +# ================================ +margen = 200 +ancho_aux = ANCHO + 2 * margen +alto_aux = ALTO + 2 * margen + +# ================================ +# 2) Renderizar texto en alta resolución (sin deformación) +# ================================ +aux = np.full((alto_aux, ancho_aux, 3), 255, dtype=np.uint8) + +(tw, th), baseline = cv.getTextSize(TEXTO, FONT, FONT_SCALE, GROSOR) +x = (ancho_aux - tw) // 2 +y = (alto_aux + th) // 2 + +cv.putText(aux, TEXTO, (x, y), FONT, FONT_SCALE, COLOR, GROSOR, cv.LINE_AA) + +# ================================ +# 3) Aplicar transformación de curvatura e inclinación +# ================================ +img_final = np.full((ALTO, ANCHO, 3), 255, dtype=np.uint8) + +# Centro de la imagen para la curvatura +cx = ancho_aux / 2 +cy = alto_aux / 2 + +# Crear mapas de coordenadas para remapeo +map_x = np.zeros((ALTO, ANCHO), dtype=np.float32) +map_y = np.zeros((ALTO, ANCHO), dtype=np.float32) + +for i in range(ALTO): + for j in range(ANCHO): + # Coordenadas en la imagen de salida + x_dst = j + y_dst = i + + # Aplicar offset para centrar + x_src = x_dst + margen + y_src = y_dst + margen + + # Aplicar curvatura (efecto de perspectiva curva) + if CURVATURA != 0: + dx = x_src - cx + dy = y_src - cy + # Curvatura cuadrática horizontal + x_src += CURVATURA * dx * dx + # Pequeña curvatura vertical para efecto realista + y_src += CURVATURA * 0.3 * dx * dx + + # Aplicar ondulación vertical (opcional) + if AMPLITUD_ONDA != 0: + y_src += AMPLITUD_ONDA * np.sin(FRECUENCIA_ONDA * x_src) + + # Aplicar inclinación (shear) + if ANGULO != 0: + angulo_rad = np.radians(ANGULO) + shear = np.tan(angulo_rad) + y_src += shear * (x_src - cx) + + map_x[i, j] = x_src + map_y[i, j] = y_src + +# Aplicar el remapeo con interpolación de alta calidad +img_final = cv.remap(aux, map_x, map_y, cv.INTER_CUBIC, + borderMode=cv.BORDER_CONSTANT, borderValue=(255, 255, 255)) + +# ================================ +# 4) Guardar resultado +# ================================ +cv.imwrite(NOMBRE_SALIDA, img_final) +print(f"Imagen sintética generada: {NOMBRE_SALIDA}") +print(f"Parámetros: Ángulo={ANGULO}°, Curvatura={CURVATURA}, Ondulación={AMPLITUD_ONDA}px") diff --git a/src/text_generation/generar_batch.py b/src/text_generation/generar_batch.py new file mode 100644 index 0000000..ab2224c --- /dev/null +++ b/src/text_generation/generar_batch.py @@ -0,0 +1,202 @@ +import cv2 as cv +import numpy as np +import os + +# ================================ +# CONFIGURACIÓN BASE +# ================================ +ANCHO = 1200 +ALTO = 400 +TEXTO = "TEXT PROOFING" +FONT = cv.FONT_HERSHEY_SIMPLEX +FONT_SCALE = 3 +COLOR = (0, 0, 0) # Negro +GROSOR = 8 +OUTPUT_DIR = "./images" + +# Crear directorio si no existe +os.makedirs(OUTPUT_DIR, exist_ok=True) + + +def generar_imagen_sintetica(nombre, angulo=0, curvatura=0, amplitud_onda=0, frecuencia_onda=0.01, + curvatura_vertical=False, perspectiva=0): + """ + Genera una imagen sintética con texto deformado + + Parámetros: + nombre: nombre del archivo de salida + angulo: inclinación en grados + curvatura: factor de curvatura horizontal (valores pequeños como 0.0001) + amplitud_onda: amplitud de ondulación en píxeles + frecuencia_onda: frecuencia de la ondulación + curvatura_vertical: si True, aplica curvatura vertical (arco) + perspectiva: factor de perspectiva (efecto de profundidad) + """ + # ================================ + # 1) Crear imagen grande para evitar recortes + # ================================ + margen = 200 + ancho_aux = ANCHO + 2 * margen + alto_aux = ALTO + 2 * margen + + # ================================ + # 2) Renderizar texto en alta resolución (sin deformación) + # ================================ + aux = np.full((alto_aux, ancho_aux, 3), 255, dtype=np.uint8) + + (tw, th), baseline = cv.getTextSize(TEXTO, FONT, FONT_SCALE, GROSOR) + x = (ancho_aux - tw) // 2 + y = (alto_aux + th) // 2 + + cv.putText(aux, TEXTO, (x, y), FONT, FONT_SCALE, COLOR, GROSOR, cv.LINE_AA) + + # ================================ + # 3) Aplicar transformación + # ================================ + img_final = np.full((ALTO, ANCHO, 3), 255, dtype=np.uint8) + + # Centro de la imagen + cx = ancho_aux / 2 + cy = alto_aux / 2 + + # Crear mapas de coordenadas para remapeo + map_x = np.zeros((ALTO, ANCHO), dtype=np.float32) + map_y = np.zeros((ALTO, ANCHO), dtype=np.float32) + + for i in range(ALTO): + for j in range(ANCHO): + # Coordenadas en la imagen de salida + x_dst = j + y_dst = i + + # Aplicar offset para centrar + x_src = x_dst + margen + y_src = y_dst + margen + + # Normalizar coordenadas para efectos [-1, 1] + x_norm = (x_src - cx) / cx + y_norm = (y_src - cy) / cy + + # Aplicar curvatura horizontal (arco) + if curvatura != 0: + dx = x_src - cx + x_src += curvatura * dx * dx + if not curvatura_vertical: + # Pequeña curvatura vertical para efecto realista + y_src += curvatura * 0.3 * dx * dx + + # Aplicar curvatura vertical (arco arriba/abajo) + if curvatura_vertical and curvatura != 0: + dy = y_src - cy + y_src += curvatura * x_norm * x_norm * 50000 + + # Aplicar ondulación vertical + if amplitud_onda != 0: + y_src += amplitud_onda * np.sin(frecuencia_onda * x_src) + + # Aplicar perspectiva + if perspectiva != 0: + # Efecto de profundidad + scale = 1.0 + perspectiva * x_norm + y_offset = (y_src - cy) * scale + y_src = cy + y_offset + + # Aplicar inclinación (shear) + if angulo != 0: + angulo_rad = np.radians(angulo) + shear = np.tan(angulo_rad) + y_src += shear * (x_src - cx) + + map_x[i, j] = x_src + map_y[i, j] = y_src + + # Aplicar el remapeo con interpolación de alta calidad + img_final = cv.remap(aux, map_x, map_y, cv.INTER_CUBIC, + borderMode=cv.BORDER_CONSTANT, borderValue=(255, 255, 255)) + + # ================================ + # 4) Guardar resultado + # ================================ + ruta_salida = os.path.join(OUTPUT_DIR, nombre) + cv.imwrite(ruta_salida, img_final) + print(f"✓ Generada: {nombre}") + + +# ================================ +# GENERAR TODAS LAS CASUÍSTICAS +# ================================ + +print("Generando imágenes sintéticas...") +print("=" * 50) + +# 1. Texto base (sin deformación) +generar_imagen_sintetica("synth_texto_base.png") + +# 2. Arco hacia arriba (curvatura negativa vertical) +generar_imagen_sintetica("synth_texto_arco_arriba.png", + curvatura=-0.000015, + curvatura_vertical=True) + +# 3. Arco hacia abajo (curvatura positiva vertical) +generar_imagen_sintetica("synth_texto_arco_abajo.png", + curvatura=0.000015, + curvatura_vertical=True) + +# 4. Arco con rotación +generar_imagen_sintetica("synth_texto_arco_rotacion.png", + angulo=20, + curvatura=0.00005) + +# 5. Onda suave +generar_imagen_sintetica("synth_texto_onda_suave.png", + amplitud_onda=15, + frecuencia_onda=0.008) + +# 6. Onda moderada +generar_imagen_sintetica("synth_texto_onda_moderada.png", + amplitud_onda=25, + frecuencia_onda=0.012) + +# 7. Onda rápida +generar_imagen_sintetica("synth_texto_onda_rapida.png", + amplitud_onda=20, + frecuencia_onda=0.025) + +# 8. Onda compleja (múltiples frecuencias simuladas con amplitud variable) +generar_imagen_sintetica("synth_texto_onda_compleja.png", + amplitud_onda=30, + frecuencia_onda=0.015) + +# 9. Perspectiva suave +generar_imagen_sintetica("synth_texto_perspectiva_suave.png", + perspectiva=0.15) + +# 10. Perspectiva fuerte +generar_imagen_sintetica("synth_texto_perspectiva_fuerte.png", + perspectiva=0.35) + +# 11. Rotación suave +generar_imagen_sintetica("synth_texto_rotacion_suave.png", + angulo=8) + +# 12. Rotación moderada +generar_imagen_sintetica("synth_texto_rotacion_moderada.png", + angulo=15) + +# 13. Rotación fuerte +generar_imagen_sintetica("synth_texto_rotacion_fuerte.png", + angulo=25) + +# 14. Combinación: onda + rotación +generar_imagen_sintetica("synth_texto_onda_rotacion.png", + angulo=12, + amplitud_onda=18, + frecuencia_onda=0.01) + +# 15. Combinación: curvatura + perspectiva +generar_imagen_sintetica("synth_texto_curva_perspectiva.png", + curvatura=0.00008, + perspectiva=0.2) + +print("=" * 50) +print(f"¡Completado! Se generaron 15 imágenes sintéticas en '{OUTPUT_DIR}/'") diff --git a/src/text_generation/generar_con_espacios.py b/src/text_generation/generar_con_espacios.py new file mode 100644 index 0000000..cf58166 --- /dev/null +++ b/src/text_generation/generar_con_espacios.py @@ -0,0 +1,96 @@ +import cv2 as cv +import numpy as np + +# ================================ +# CONFIGURACIÓN +# ================================ +ANCHO = 1400 +ALTO = 400 +TEXTO = "HELLO WORLD" # Texto con espacio entre palabras +FONT = cv.FONT_HERSHEY_SIMPLEX +FONT_SCALE = 3 +COLOR = (0, 0, 0) # Negro +GROSOR = 8 + +# Parámetros de deformación +ANGULO = 20 # Inclinación de 20 grados +CURVATURA = 0 +AMPLITUD_ONDA = 0 +FRECUENCIA_ONDA = 0.01 + +NOMBRE_SALIDA = "./images/synth_texto_con_espacios.png" + +# ================================ +# 1) Crear imagen grande para evitar recortes +# ================================ +margen = 200 +ancho_aux = ANCHO + 2 * margen +alto_aux = ALTO + 2 * margen + +# ================================ +# 2) Renderizar texto en alta resolución (sin deformación) +# ================================ +aux = np.full((alto_aux, ancho_aux, 3), 255, dtype=np.uint8) + +(tw, th), baseline = cv.getTextSize(TEXTO, FONT, FONT_SCALE, GROSOR) +x = (ancho_aux - tw) // 2 +y = (alto_aux + th) // 2 + +cv.putText(aux, TEXTO, (x, y), FONT, FONT_SCALE, COLOR, GROSOR, cv.LINE_AA) + +# ================================ +# 3) Aplicar transformación de curvatura e inclinación +# ================================ +img_final = np.full((ALTO, ANCHO, 3), 255, dtype=np.uint8) + +# Centro de la imagen para la curvatura +cx = ancho_aux / 2 +cy = alto_aux / 2 + +# Crear mapas de coordenadas para remapeo +map_x = np.zeros((ALTO, ANCHO), dtype=np.float32) +map_y = np.zeros((ALTO, ANCHO), dtype=np.float32) + +for i in range(ALTO): + for j in range(ANCHO): + # Coordenadas en la imagen de salida + x_dst = j + y_dst = i + + # Aplicar offset para centrar + x_src = x_dst + margen + y_src = y_dst + margen + + # Aplicar curvatura (efecto de perspectiva curva) + if CURVATURA != 0: + dx = x_src - cx + dy = y_src - cy + # Curvatura cuadrática horizontal + x_src += CURVATURA * dx * dx + # Pequeña curvatura vertical para efecto realista + y_src += CURVATURA * 0.3 * dx * dx + + # Aplicar ondulación vertical (opcional) + if AMPLITUD_ONDA != 0: + y_src += AMPLITUD_ONDA * np.sin(FRECUENCIA_ONDA * x_src) + + # Aplicar inclinación (shear) + if ANGULO != 0: + angulo_rad = np.radians(ANGULO) + shear = np.tan(angulo_rad) + y_src += shear * (x_src - cx) + + map_x[i, j] = x_src + map_y[i, j] = y_src + +# Aplicar el remapeo con interpolación de alta calidad +img_final = cv.remap(aux, map_x, map_y, cv.INTER_CUBIC, + borderMode=cv.BORDER_CONSTANT, borderValue=(255, 255, 255)) + +# ================================ +# 4) Guardar resultado +# ================================ +cv.imwrite(NOMBRE_SALIDA, img_final) +print(f"Imagen sintética con espacios generada: {NOMBRE_SALIDA}") +print(f"Texto: '{TEXTO}'") +print(f"Parámetros: Ángulo={ANGULO}°, Curvatura={CURVATURA}, Ondulación={AMPLITUD_ONDA}px") diff --git a/src/text_rectification/__init__.py b/src/text_rectification/__init__.py new file mode 100644 index 0000000..d267ad7 --- /dev/null +++ b/src/text_rectification/__init__.py @@ -0,0 +1,8 @@ +""" +Módulo para rectificación de texto deformado. +""" + +from .rectificar3 import rectificar_texto +from .rectificar_simple import rectificar_texto_simple + +__all__ = ['rectificar_texto', 'rectificar_texto_simple'] diff --git a/src/text_rectification/__pycache__/__init__.cpython-313.pyc b/src/text_rectification/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..7180044 Binary files /dev/null and b/src/text_rectification/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/text_rectification/__pycache__/rectificar2.cpython-313.pyc b/src/text_rectification/__pycache__/rectificar2.cpython-313.pyc new file mode 100644 index 0000000..7e16656 Binary files /dev/null and b/src/text_rectification/__pycache__/rectificar2.cpython-313.pyc differ diff --git a/src/text_rectification/__pycache__/rectificar3.cpython-313.pyc b/src/text_rectification/__pycache__/rectificar3.cpython-313.pyc new file mode 100644 index 0000000..3668eb2 Binary files /dev/null and b/src/text_rectification/__pycache__/rectificar3.cpython-313.pyc differ diff --git a/src/text_rectification/__pycache__/rectificar_simple.cpython-313.pyc b/src/text_rectification/__pycache__/rectificar_simple.cpython-313.pyc new file mode 100644 index 0000000..846d5a7 Binary files /dev/null and b/src/text_rectification/__pycache__/rectificar_simple.cpython-313.pyc differ diff --git a/rectificar.py b/src/text_rectification/rectificar.py similarity index 99% rename from rectificar.py rename to src/text_rectification/rectificar.py index 8254ef8..41d1123 100644 --- a/rectificar.py +++ b/src/text_rectification/rectificar.py @@ -227,7 +227,7 @@ def rectifica_texto_curvo(img, th, centros, contours): # FUNCIÓN PRINCIPAL PARA IMPORTAR # ============================================================================ -def rectificar_texto(ruta_entrada, ruta_salida, debug=False): +def rectificar_texto(ruta_entrada, ruta_salida): img = cv.imread(ruta_entrada) if img is None: diff --git a/src/text_rectification/rectificar2.py b/src/text_rectification/rectificar2.py new file mode 100644 index 0000000..d650ac7 --- /dev/null +++ b/src/text_rectification/rectificar2.py @@ -0,0 +1,269 @@ +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/rectificar3.py b/src/text_rectification/rectificar3.py new file mode 100644 index 0000000..c9bad25 --- /dev/null +++ b/src/text_rectification/rectificar3.py @@ -0,0 +1,466 @@ +import cv2 +import numpy as np +import math +import os + +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 ajustar_distancias(distancias_originales, anchos_letras, anchos_rotados): + """ + Ajusta las distancias entre letras para evitar solapamientos, mantener + proporciones razonables y preservar espacios entre palabras. + + Args: + distancias_originales: Lista de distancias ORIGINALES entre letras (antes de rotar) + anchos_letras: Lista de anchos originales de cada letra + anchos_rotados: Lista de anchos después de rotar las letras + + Returns: + Lista de distancias ajustadas que respetan espacios entre palabras + """ + if len(distancias_originales) == 0: + return [] + + distancias_ajustadas = [] + + # ============================================================ + # 1. CALCULAR ESTADÍSTICAS DE DISTANCIAS ORIGINALES + # ============================================================ + distancias_array = np.array(distancias_originales) + media_distancias = np.mean(distancias_array) + mediana_distancias = np.median(distancias_array) + std_distancias = np.std(distancias_array) + + # ============================================================ + # 2. DEFINIR UMBRALES Y CONSTANTES + # ============================================================ + # Calcular ancho promedio de letras rotadas para referencias + ancho_promedio_rotado = np.mean(anchos_rotados) + + # DISTANCIA MÍNIMA ENTRE LETRAS: Reducida para comprimir letras + # 8% del ancho promedio, mínimo 3px + DISTANCIA_MIN_LETRAS = max(3, int(ancho_promedio_rotado * 0.08)) + + # DISTANCIA MÁXIMA ENTRE LETRAS: Más restrictiva + DISTANCIA_MAX_LETRAS = int(ancho_promedio_rotado * 0.4) # 40% del ancho promedio + + # UMBRAL PARA DETECTAR ESPACIOS ENTRE PALABRAS + # Si distancia > media * 1.5, consideramos que es espacio entre palabras + # (Umbral más sensible para detectar más espacios) + UMBRAL_ESPACIO_PALABRA = media_distancias * 1.5 + + # DISTANCIA PARA ESPACIOS ENTRE PALABRAS: + # Significativamente mayor (80% del ancho promedio, mínimo 30px) + DISTANCIA_PALABRA = max(30, int(ancho_promedio_rotado * 0.8)) + + print(f"\nParámetros de ajuste:") + print(f" Media distancias originales: {media_distancias:.1f}px") + print(f" DISTANCIA_MIN_LETRAS: {DISTANCIA_MIN_LETRAS}px (reducida)") + print(f" DISTANCIA_MAX_LETRAS: {DISTANCIA_MAX_LETRAS}px") + print(f" UMBRAL_ESPACIO_PALABRA: {UMBRAL_ESPACIO_PALABRA:.1f}px") + print(f" DISTANCIA_PALABRA: {DISTANCIA_PALABRA}px (aumentada)") + print(f" Ratio palabra/letra: {DISTANCIA_PALABRA / DISTANCIA_MIN_LETRAS:.1f}x") + + # ============================================================ + # 3. AJUSTAR CADA DISTANCIA INDIVIDUALMENTE + # ============================================================ + espacios_detectados = 0 + + for i, dist_original in enumerate(distancias_originales): + # Obtener anchos de letras adyacentes + ancho_actual_rotado = anchos_rotados[i] + ancho_siguiente_rotado = anchos_rotados[i + 1] + + # -------------------------------------------------------- + # 3.1. DETECTAR SI ES ESPACIO ENTRE PALABRAS + # -------------------------------------------------------- + es_espacio_palabra = dist_original > UMBRAL_ESPACIO_PALABRA + + if es_espacio_palabra: + # Es un espacio entre palabras: usar distancia amplificada + dist_ajustada = DISTANCIA_PALABRA + espacios_detectados += 1 + else: + # -------------------------------------------------------- + # 3.2. ES ESPACIO ENTRE LETRAS: AJUSTAR PROPORCIONALMENTE + # -------------------------------------------------------- + + # Calcular factor de crecimiento por rotación + ancho_actual_original = anchos_letras[i] + ancho_siguiente_original = anchos_letras[i + 1] + + factor_actual = ancho_actual_rotado / max(ancho_actual_original, 1) + factor_siguiente = ancho_siguiente_rotado / max(ancho_siguiente_original, 1) + factor_crecimiento = (factor_actual + factor_siguiente) / 2 + + # Reducir distancia entre letras agresivamente + # Factor de compresión: 0.6 (reducir a 60% del original) + FACTOR_COMPRESION = 0.6 + dist_ajustada = dist_original * FACTOR_COMPRESION + + # Ajustar por crecimiento debido a rotación (mínimamente) + if factor_crecimiento > 1.0: + # Si creció, añadir solo un poco más + dist_ajustada *= (1.0 + (factor_crecimiento - 1.0) * 0.2) + + # -------------------------------------------------------- + # 3.3. APLICAR LÍMITES MÍNIMO Y MÁXIMO PARA LETRAS + # -------------------------------------------------------- + # Aplicar distancia mínima (evitar solapamientos) + dist_ajustada = max(dist_ajustada, DISTANCIA_MIN_LETRAS) + + # Aplicar distancia máxima (mantener letras juntas) + dist_ajustada = min(dist_ajustada, DISTANCIA_MAX_LETRAS) + + distancias_ajustadas.append(int(dist_ajustada)) + + print(f" Espacios entre palabras detectados: {espacios_detectados}") + + return distancias_ajustadas + + +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 + # Usar centro de bounding box + 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 + # Extraer valores escalares para evitar DeprecationWarning + angulo_radianes = math.atan2(float(vy[0]), float(vx[0])) + 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) - USANDO DATOS ORIGINALES + # ================================================================ + + # ---------------------------------------------------------------- + # 5.1. CALCULAR DISTANCIAS DESDE LOS DATOS ORIGINALES + # ---------------------------------------------------------------- + # IMPORTANTE: Usar datos_letras que contiene las posiciones ANTES de rotar + print("\n=== PASO 5: RECONSTRUCCIÓN ===") + + distancias_originales = [] + anchos_originales = [] + + # Calcular distancias entre letras usando posiciones originales + for i in range(len(datos_letras) - 1): + # Letra actual + x_actual = datos_letras[i]['x'] + w_actual = datos_letras[i]['w'] + + # Letra siguiente + x_siguiente = datos_letras[i+1]['x'] + + # Distancia = inicio de siguiente - fin de actual + distancia = x_siguiente - (x_actual + w_actual) + + distancias_originales.append(distancia) + anchos_originales.append(w_actual) + + # Añadir el ancho de la última letra + if len(datos_letras) > 0: + anchos_originales.append(datos_letras[-1]['w']) + + print(f"Distancias originales calculadas: {len(distancias_originales)}") + print(f" Rango: [{min(distancias_originales)}, {max(distancias_originales)}]") + print(f" Media: {np.mean(distancias_originales):.1f}px") + + # ---------------------------------------------------------------- + # 5.2. CALCULAR DIMENSIONES DE LETRAS PROCESADAS (ROTADAS) + # ---------------------------------------------------------------- + alturas_rotadas = [img.shape[0] for img in letras_procesadas] + anchos_rotados = [img.shape[1] for img in letras_procesadas] + + print(f"\nLetras procesadas: {len(letras_procesadas)}") + print(f" Alturas rotadas - rango: [{min(alturas_rotadas)}, {max(alturas_rotadas)}]") + print(f" Anchos rotados - rango: [{min(anchos_rotados)}, {max(anchos_rotados)}]") + + # ---------------------------------------------------------------- + # 5.3. AJUSTAR DISTANCIAS INTELIGENTEMENTE + # ---------------------------------------------------------------- + distancias_ajustadas = ajustar_distancias( + distancias_originales, + anchos_originales, + anchos_rotados + ) + + # Mostrar resumen del ajuste + if len(distancias_ajustadas) > 0: + reduccion = np.mean(distancias_originales) - np.mean(distancias_ajustadas) + print(f"\nResumen de ajuste:") + print(f" Original promedio: {np.mean(distancias_originales):.1f}px") + print(f" Ajustado promedio: {np.mean(distancias_ajustadas):.1f}px") + print(f" Cambio: {reduccion:+.1f}px") + + # ---------------------------------------------------------------- + # 5.4. CALCULAR CENTROIDES Y DE LETRAS PROCESADAS + # ---------------------------------------------------------------- + # Usar el centro de la bounding box para alineación vertical + centroides_y = [] + for letra in letras_procesadas: + cy_letra = letra.shape[0] // 2 + centroides_y.append(cy_letra) + + # ---------------------------------------------------------------- + # 5.5. PREPARAR CANVAS PARA RECONSTRUCCIÓN + # ---------------------------------------------------------------- + # Calcular dimensiones del canvas + alto_max = max(alturas_rotadas) + ancho_total = sum(anchos_rotados) + sum(distancias_ajustadas) + 40 # +40 para márgenes + + print(f"\nDimensiones del canvas:") + print(f" Alto: {alto_max + 40}px") + print(f" Ancho: {ancho_total}px") + + # Definir línea base común (baseline) en el centro vertical + baseline_y = alto_max // 2 + 20 + + # Crear lienzo blanco + salida = np.ones((alto_max + 40, ancho_total), dtype=np.uint8) * 255 + salida_bgr = cv2.cvtColor(salida, cv2.COLOR_GRAY2BGR) + + # ---------------------------------------------------------------- + # 5.6. RECONSTRUIR TEXTO LETRA POR LETRA + # ---------------------------------------------------------------- + print("\nReconstruyendo texto...") + x_offset = 20 # Margen izquierdo + + for idx, letra in enumerate(letras_procesadas): + h_letra, w_letra = letra.shape[:2] + + # -------------------------------------------------------- + # Calcular posición Y para alinear con baseline común + # -------------------------------------------------------- + cy_letra_local = centroides_y[idx] + y_pos = int(baseline_y - cy_letra_local) + + # Asegurar que la letra no se salga del canvas + y_pos = max(0, min(y_pos, salida_bgr.shape[0] - h_letra)) + + # -------------------------------------------------------- + # Verificar que no hay solapamiento (debug) + # -------------------------------------------------------- + if idx > 0: + distancia_real = x_offset - x_anterior - w_anterior + if distancia_real < 0: + print(f" ⚠ Advertencia: Letra {idx} se solapa {-distancia_real}px con la anterior") + + # -------------------------------------------------------- + # Pegar letra en el canvas + # -------------------------------------------------------- + try: + salida_bgr[y_pos:y_pos+h_letra, x_offset:x_offset+w_letra] = letra + except ValueError as e: + print(f" ✗ Error al pegar letra {idx}: {e}") + print(f" Posición: ({x_offset}, {y_pos}), Tamaño: ({w_letra}, {h_letra})") + continue + + # -------------------------------------------------------- + # Avanzar cursor X para la siguiente letra + # -------------------------------------------------------- + x_anterior = x_offset + w_anterior = w_letra + x_offset += w_letra + + # Añadir distancia a la siguiente letra (si no es la última) + if idx < len(distancias_ajustadas): + x_offset += distancias_ajustadas[idx] + + print(f"✓ Reconstrucción completada: {len(letras_procesadas)} letras") + + # ---------------------------------------------------------------- + # 5.7. GUARDAR IMAGEN RESULTADO + # ---------------------------------------------------------------- + if ruta_salida is not None and ruta_salida != '': + # Asegurar que la ruta tiene extensión .png + if not ruta_salida.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.tiff')): + ruta_salida = ruta_salida + '.png' + + # Verificar que el directorio de salida existe + directorio_salida = os.path.dirname(ruta_salida) + if directorio_salida and not os.path.exists(directorio_salida): + os.makedirs(directorio_salida, exist_ok=True) + + cv2.imwrite(ruta_salida, salida_bgr) + print(f"\n✓ Imagen guardada como '{ruta_salida}'") + else: + # Si no se especifica ruta, guardar en la raíz + cv2.imwrite('resultado_rectificado.png', salida_bgr) + print("\n✓ 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 bounding box + 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) + + # Calcular métricas de distancias + dist_original_promedio = np.mean(distancias_originales) if len(distancias_originales) > 0 else 0 + dist_ajustada_promedio = np.mean(distancias_ajustadas) if len(distancias_ajustadas) > 0 else 0 + reduccion_distancia = dist_original_promedio - dist_ajustada_promedio + + metricas = { + 'error_ajuste': error, + 'es_curvo': es_curvo, + 'baseline_std': baseline_std, + 'quality_score': quality_score, + 'num_letras': len(datos_letras), + 'distancia_original_promedio': dist_original_promedio, + 'distancia_ajustada_promedio': dist_ajustada_promedio, + 'reduccion_distancia': reduccion_distancia + } + + 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/synth_texto_rotacion_fuerte.png', 'output/pruebaTarde') + 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}") diff --git a/src/text_rectification/rectificar_simple.py b/src/text_rectification/rectificar_simple.py new file mode 100644 index 0000000..7140d36 --- /dev/null +++ b/src/text_rectification/rectificar_simple.py @@ -0,0 +1,342 @@ +""" +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 new file mode 100644 index 0000000..d250d0a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +""" +Tests para el proyecto de rectificación de texto. +""" diff --git a/tests/test_alineacion.py b/tests/test_alineacion.py new file mode 100644 index 0000000..7e90726 --- /dev/null +++ b/tests/test_alineacion.py @@ -0,0 +1,88 @@ +""" +Script para visualizar la alineación de centroides en las imágenes rectificadas +""" + +import cv2 +import numpy as np +import os +import sys + +# Añadir el directorio raíz al path para importaciones +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from src.text_rectification.rectificar3 import rectificar_texto + +def visualizar_baseline(imagen_path, output_path): + """ + Visualiza la baseline dibujando la línea de centroides + """ + # Rectificar + resultado, metricas = rectificar_texto(imagen_path, None) + + if resultado is None: + print(f"Error procesando {imagen_path}") + return + + # Detectar contornos en la imagen rectificada + gray = cv2.cvtColor(resultado, cv2.COLOR_BGR2GRAY) + _, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV) + contornos, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # Calcular centroides + centroides = [] + for c in contornos: + x, y, w, h = cv2.boundingRect(c) + if w > 5 and h > 5: + M = cv2.moments(c) + if M["m00"] != 0: + cx = int(M["m10"] / M["m00"]) + cy = int(M["m01"] / M["m00"]) + centroides.append((cx, cy)) + + # Crear imagen de visualización + vis = resultado.copy() + + # Dibujar centroides + for cx, cy in centroides: + cv2.circle(vis, (cx, cy), 5, (0, 0, 255), -1) + + # Dibujar línea de baseline (promedio de Y) + if centroides: + cy_promedio = int(np.mean([cy for _, cy in centroides])) + cv2.line(vis, (0, cy_promedio), (vis.shape[1], cy_promedio), (0, 255, 0), 2) + + # Añadir texto con métricas + texto = f"Baseline sigma: {metricas['baseline_std']:.4f}px" + cv2.putText(vis, texto, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2) + + # Guardar + cv2.imwrite(output_path, vis) + print(f"✓ Visualización guardada: {output_path}") + print(f" Baseline σ: {metricas['baseline_std']:.4f}px") + print(f" Score: {metricas['quality_score']:.2f}") + + +# Crear directorio de salida +os.makedirs('./visualizaciones', exist_ok=True) + +# Test con varias imágenes +print("\n" + "="*70) +print("VISUALIZACIÓN DE ALINEACIÓN DE BASELINE") +print("="*70 + "\n") + +imagenes_test = [ + './images/synth_texto_rotacion_moderada.png', + './images/synth_texto_onda_moderada.png', + './images/synth_texto_con_espacios.png', +] + +for img_path in imagenes_test: + nombre = os.path.basename(img_path).replace('.png', '') + output = f'./visualizaciones/{nombre}_baseline.png' + print(f"\nProcesando: {nombre}") + print("-" * 70) + visualizar_baseline(img_path, output) + +print("\n" + "="*70) +print("✓ Visualizaciones guardadas en ./visualizaciones/") +print("="*70 + "\n") diff --git a/test_batch.py b/tests/test_batch.py similarity index 81% rename from test_batch.py rename to tests/test_batch.py index c8285e9..54ed6db 100644 --- a/test_batch.py +++ b/tests/test_batch.py @@ -4,31 +4,39 @@ """ import os +import sys import cv2 as cv import numpy as np -from rectificar import rectificar_texto import time +# Añadir el directorio raíz al path para importaciones +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from src.text_rectification.rectificar3 import rectificar_texto + # Configuración -DIRECTORIO_ENTRADA = "./imagenes_generadas" +DIRECTORIO_ENTRADA = "./images" DIRECTORIO_SALIDA = "./output" os.makedirs(DIRECTORIO_SALIDA, exist_ok=True) # Casos de prueba a procesar CASOS_PRUEBA = [ - "texto_rotacion_suave.png", - "texto_rotacion_moderada.png", - "texto_rotacion_fuerte.png", - "texto_onda_suave.png", - "texto_onda_moderada.png", - "texto_onda_rapida.png", - "texto_arco_arriba.png", - "texto_arco_abajo.png", - "texto_perspectiva_suave.png", - "texto_onda_compleja.png", - "texto_arco_rotacion.png", - "texto_onda_ruido_bajo.png", - "texto_arco_ruido.png", + # Imágenes sintéticas + "synth_texto_base.png", + "synth_texto_arco_arriba.png", + "synth_texto_arco_abajo.png", + "synth_texto_arco_rotacion.png", + "synth_texto_onda_suave.png", + "synth_texto_onda_moderada.png", + "synth_texto_onda_rapida.png", + "synth_texto_onda_compleja.png", + "synth_texto_perspectiva_suave.png", + "synth_texto_perspectiva_fuerte.png", + "synth_texto_rotacion_suave.png", + "synth_texto_rotacion_moderada.png", + "synth_texto_rotacion_fuerte.png", + "synth_texto_onda_rotacion.png", + "synth_texto_curva_perspectiva.png", ] @@ -59,7 +67,7 @@ def main(): # Procesar try: inicio = time.time() - resultado, metricas = rectificar_texto(ruta_entrada, ruta_salida, debug=False) + resultado, metricas = rectificar_texto(ruta_entrada, ruta_salida) tiempo = time.time() - inicio tiempos.append(tiempo) diff --git a/visualizaciones/synth_texto_con_espacios_baseline.png b/visualizaciones/synth_texto_con_espacios_baseline.png new file mode 100644 index 0000000..0918f99 Binary files /dev/null and b/visualizaciones/synth_texto_con_espacios_baseline.png differ diff --git a/visualizaciones/synth_texto_onda_moderada_baseline.png b/visualizaciones/synth_texto_onda_moderada_baseline.png new file mode 100644 index 0000000..8ed6727 Binary files /dev/null and b/visualizaciones/synth_texto_onda_moderada_baseline.png differ diff --git a/visualizaciones/synth_texto_rotacion_moderada_baseline.png b/visualizaciones/synth_texto_rotacion_moderada_baseline.png new file mode 100644 index 0000000..c8e23d3 Binary files /dev/null and b/visualizaciones/synth_texto_rotacion_moderada_baseline.png differ