diff --git a/comparacion/test_espaciado.png b/comparacion/test_espaciado.png deleted file mode 100644 index 1c6ed77..0000000 Binary files a/comparacion/test_espaciado.png and /dev/null differ 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. -"""