Hasta ahora trabajamos principalmente con operaciones que modifican los valores de los píxeles. En este tema cambiaremos de enfoque: estudiaremos transformaciones que modifican la posición espacial de esos píxeles dentro de la imagen.
Estas transformaciones se llaman geométricas porque alteran la geometría visual de la imagen: pueden desplazarla, rotarla, escalarla, reflejarla o deformarla según ciertas reglas matemáticas.
Son muy importantes en visión por computadora porque aparecen en preprocesamiento, corrección de perspectiva, alineación de documentos, data augmentation, seguimiento de objetos y muchos otros contextos.
En una transformación geométrica no nos interesa tanto cambiar el valor de cada píxel como decidir dónde debe quedar cada punto de la imagen en la nueva representación.
Esto significa que la pregunta principal pasa a ser:
¿cómo se reasignan las coordenadas de los píxeles?
Por ejemplo:
Estas transformaciones aparecen constantemente en problemas reales. Algunos ejemplos:
En otras palabras, muchas veces la imagen original no está en la forma ideal para ser analizada, y una transformación geométrica permite llevarla a una representación más conveniente.
De forma general, podemos pensar que algunas transformaciones preservan mejor la forma original y otras introducen deformaciones más complejas.
Esta distinción es útil porque no todos los problemas requieren el mismo nivel de complejidad geométrica.
Una de las transformaciones geométricas más comunes es el escalado, es decir, cambiar el tamaño de la imagen. Ya vimos la operación desde un punto de vista práctico, pero aquí la pensamos como transformación espacial.
Con OpenCV:
redimensionada = cv2.resize(imagen, (400, 300))
Escalar una imagen implica recalcular qué valor debe tener cada posición nueva en función de la imagen original. Esto introduce un concepto importante: la interpolación, que veremos más adelante.
Cuando escalamos una imagen, a menudo queremos conservar la relación de aspecto. Si no lo hacemos, la imagen puede deformarse visualmente.
alto, ancho = imagen.shape[:2]
nuevo_ancho = 500
nuevo_alto = int(alto * nuevo_ancho / ancho)
redimensionada = cv2.resize(imagen, (nuevo_ancho, nuevo_alto))
Este cuidado es importante tanto en visualización como en entrenamiento de modelos, donde una deformación artificial puede perjudicar el aprendizaje.
La traslación consiste en desplazar la imagen horizontal y verticalmente. No cambia la forma ni la orientación de los objetos, solo su posición.
Matemáticamente, esto equivale a sumar un desplazamiento a las coordenadas de cada punto.
En OpenCV se puede implementar con una matriz de transformación y warpAffine:
import cv2
import numpy as np
imagen = cv2.imread("foto1.jpg")
M = np.float32([[1, 0, 50],
[0, 1, 30]])
trasladada = cv2.warpAffine(imagen, M, (imagen.shape[1], imagen.shape[0]))
cv2.imshow("Imagen original", imagen)
cv2.imshow("Imagen trasladada", trasladada)
cv2.waitKey(0)
cv2.destroyAllWindows()
En este ejemplo, la imagen se desplaza 50 píxeles a la derecha y 30 hacia abajo.
La rotación gira la imagen alrededor de un punto. Generalmente se usa el centro de la imagen, aunque no es obligatorio.
OpenCV ofrece una forma práctica de construir esta transformación:
import cv2
imagen = cv2.imread("foto1.jpg")
alto, ancho = imagen.shape[:2]
centro = (ancho // 2, alto // 2)
M = cv2.getRotationMatrix2D(centro, 45, 1.0)
rotada = cv2.warpAffine(imagen, M, (ancho, alto))
cv2.imshow("Imagen original", imagen)
cv2.imshow("Imagen rotada", rotada)
cv2.waitKey(0)
cv2.destroyAllWindows()
Aquí la imagen se rota 45 grados con escala 1.0, es decir, sin agrandarla ni reducirla.
Cuando rotamos una imagen pueden aparecer varios problemas prácticos:
Por eso, en algunos casos se amplía el lienzo de salida o se calculan dimensiones nuevas para no perder contenido.
El flip es una reflexión de la imagen. Puede ser horizontal, vertical o ambas. Es una operación muy útil tanto para inspección como para data augmentation.
import cv2
imagen = cv2.imread("foto1.jpg")
horizontal = cv2.flip(imagen, 1)
vertical = cv2.flip(imagen, 0)
ambos = cv2.flip(imagen, -1)
cv2.imshow("Imagen original", imagen)
cv2.imshow("Flip horizontal", horizontal)
cv2.imshow("Flip vertical", vertical)
cv2.imshow("Flip horizontal y vertical", ambos)
cv2.waitKey(0)
cv2.destroyAllWindows()
El flip horizontal es especialmente común cuando se generan variantes de entrenamiento para modelos de clasificación o detección.
El recorte también puede considerarse una transformación geométrica, porque altera la región espacial visible de la imagen. Aunque técnicamente es más simple que una rotación o una proyección, su efecto geométrico es claro: conservamos solo una parte del plano original.
recorte = imagen[100:350, 150:450]
Es una operación fundamental en detección, seguimiento, OCR y preparación de regiones de interés.
Muchas transformaciones geométricas generan coordenadas nuevas que no coinciden exactamente con posiciones enteras de la imagen original. En esos casos hace falta estimar el valor del nuevo píxel. A eso se le llama interpolación.
OpenCV ofrece varios métodos, entre ellos:
INTER_NEARESTINTER_LINEARINTER_CUBICINTER_AREALa interpolación elegida influye en la calidad visual y en el costo computacional.
Como regla práctica:
En una redimensión simple de imágenes para entrenamiento, una interpolación lineal suele ser una opción razonable. Para imágenes con texto o máscaras, a veces la elección debe hacerse con más cuidado.
Una transformación afín es más general que una simple traslación o rotación. Permite combinar operaciones como:
Una característica importante es que preserva líneas rectas y paralelismo, aunque puede alterar ángulos y longitudes.
En OpenCV, las transformaciones afines se aplican también con cv2.warpAffine.
Para definir una transformación afín se suelen especificar tres puntos en la imagen original y sus posiciones correspondientes en la salida:
import cv2
import numpy as np
imagen = cv2.imread("foto1.jpg")
pts1 = np.float32([[50, 50], [200, 50], [50, 200]])
pts2 = np.float32([[10, 100], [200, 50], [100, 250]])
M = cv2.getAffineTransform(pts1, pts2)
afin = cv2.warpAffine(imagen, M, (imagen.shape[1], imagen.shape[0]))
cv2.imshow("Imagen original", imagen)
cv2.imshow("Transformacion afin", afin)
cv2.waitKey(0)
cv2.destroyAllWindows()
Esta transformación puede inclinar o deformar la imagen dentro del modelo afín.
La transformación de perspectiva es aún más general. Se utiliza cuando la imagen parece observada desde un ángulo oblicuo y queremos rectificarla como si la estuviéramos viendo de frente.
Es especialmente útil en:
En este caso se necesitan cuatro puntos y se usa una matriz proyectiva.
import cv2
import numpy as np
imagen = cv2.imread("foto1.jpg")
pts1 = np.float32([[56, 65], [368, 52], [28, 387], [389, 390]])
pts2 = np.float32([[0, 0], [300, 0], [0, 400], [300, 400]])
M = cv2.getPerspectiveTransform(pts1, pts2)
perspectiva = cv2.warpPerspective(imagen, M, (300, 400))
cv2.imshow("Imagen original", imagen)
cv2.imshow("Transformacion de perspectiva", perspectiva)
cv2.waitKey(0)
cv2.destroyAllWindows()
Este tipo de transformación puede “enderezar” visualmente una región inclinada y convertirla en una vista más regular.
Dos funciones de OpenCV aparecen constantemente al trabajar con transformaciones geométricas:
cv2.warpAffine para transformaciones afines.cv2.warpPerspective para transformaciones proyectivas.Ambas toman una imagen, una matriz de transformación y un tamaño de salida, y producen una nueva imagen reubicando los píxeles según esa geometría.
En una transformación geométrica, no solo importa la matriz de cambio. También importa el tamaño de salida elegido. Si la imagen transformada no entra en el nuevo lienzo, se perderá parte del contenido. Si entra con espacio sobrante, aparecerán regiones vacías.
En OpenCV, esas zonas vacías suelen rellenarse con un color por defecto, normalmente negro, aunque esto puede ajustarse según la función utilizada.
En Deep Learning, las transformaciones geométricas tienen otro papel muy importante: generar nuevas variantes de las imágenes de entrenamiento. Esto ayuda a mejorar la generalización del modelo.
Algunos ejemplos de aumentos geométricos:
La idea es enseñar al modelo a reconocer objetos aunque cambien de posición, orientación o tamaño.
Cuando una imagen tiene anotaciones asociadas, como cajas, máscaras o puntos clave, no alcanza con transformar solo la imagen. También hay que transformar esas anotaciones de forma consistente.
Por ejemplo:
Este detalle es esencial en datasets para detección, pose y segmentación.
El orden en que aplicamos transformaciones geométricas importa. Rotar y luego trasladar no siempre produce el mismo resultado que trasladar y luego rotar. Esto ocurre porque las coordenadas se van redefiniendo en cada paso.
En problemas complejos, esta composición de transformaciones se expresa mediante matrices que se combinan entre sí.
Veamos ahora un ejemplo integrado y más completo con una interfaz gráfica para aplicar distintas transformaciones geométricas sobre una imagen:
import os
import cv2
import numpy as np
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
EXTENSIONES_VALIDAS = (".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp")
class AplicacionTransformacionesGeometricas:
def __init__(self, root):
self.root = root
self.root.title("Transformaciones geométricas con OpenCV")
self.root.geometry("1550x900")
self.imagen_original = None
self.imagen_actual = None
self.nombre_archivo_actual = None
self.crear_interfaz()
self.cargar_lista_imagenes()
# ---------------------------------------------------------
# INTERFAZ
# ---------------------------------------------------------
def crear_interfaz(self):
contenedor = ttk.Frame(self.root, padding=10)
contenedor.pack(fill="both", expand=True)
# ---------------- Panel izquierdo ----------------
panel_izquierdo = ttk.Frame(contenedor)
panel_izquierdo.pack(side="left", fill="y", padx=(0, 10))
ttk.Label(panel_izquierdo, text="Imágenes del directorio actual").pack(anchor="w")
self.lista_imagenes = tk.Listbox(panel_izquierdo, width=35, height=18)
self.lista_imagenes.pack(fill="y", pady=5)
self.lista_imagenes.bind("<<ListboxSelect>>", self.seleccionar_imagen)
ttk.Button(panel_izquierdo, text="Actualizar lista", command=self.cargar_lista_imagenes).pack(fill="x", pady=2)
ttk.Button(panel_izquierdo, text="Abrir otra imagen...", command=self.abrir_otra_imagen).pack(fill="x", pady=2)
ttk.Button(panel_izquierdo, text="Restablecer imagen", command=self.restablecer_imagen).pack(fill="x", pady=2)
ttk.Button(panel_izquierdo, text="Guardar resultado", command=self.guardar_resultado).pack(fill="x", pady=2)
ttk.Separator(panel_izquierdo, orient="horizontal").pack(fill="x", pady=8)
ttk.Label(panel_izquierdo, text="Parámetros").pack(anchor="w")
ttk.Label(panel_izquierdo, text="Nuevo ancho").pack(anchor="w")
self.entry_ancho = ttk.Entry(panel_izquierdo)
self.entry_ancho.insert(0, "500")
self.entry_ancho.pack(fill="x", pady=2)
ttk.Label(panel_izquierdo, text="Nuevo alto").pack(anchor="w")
self.entry_alto = ttk.Entry(panel_izquierdo)
self.entry_alto.insert(0, "350")
self.entry_alto.pack(fill="x", pady=2)
ttk.Label(panel_izquierdo, text="Desplazamiento X").pack(anchor="w")
self.entry_tx = ttk.Entry(panel_izquierdo)
self.entry_tx.insert(0, "50")
self.entry_tx.pack(fill="x", pady=2)
ttk.Label(panel_izquierdo, text="Desplazamiento Y").pack(anchor="w")
self.entry_ty = ttk.Entry(panel_izquierdo)
self.entry_ty.insert(0, "30")
self.entry_ty.pack(fill="x", pady=2)
ttk.Label(panel_izquierdo, text="Ángulo de rotación").pack(anchor="w")
self.scale_angulo = tk.Scale(panel_izquierdo, from_=-180, to=180,
orient="horizontal", length=250)
self.scale_angulo.set(30)
self.scale_angulo.pack()
ttk.Label(panel_izquierdo, text="Escala de rotación").pack(anchor="w")
self.scale_escala_rot = tk.Scale(panel_izquierdo, from_=0.1, to=2.0,
resolution=0.1, orient="horizontal", length=250)
self.scale_escala_rot.set(1.0)
self.scale_escala_rot.pack()
ttk.Label(panel_izquierdo, text="Recorte (%) del ancho").pack(anchor="w")
self.scale_crop_x = tk.Scale(panel_izquierdo, from_=10, to=100,
orient="horizontal", length=250)
self.scale_crop_x.set(50)
self.scale_crop_x.pack()
ttk.Label(panel_izquierdo, text="Recorte (%) del alto").pack(anchor="w")
self.scale_crop_y = tk.Scale(panel_izquierdo, from_=10, to=100,
orient="horizontal", length=250)
self.scale_crop_y.set(50)
self.scale_crop_y.pack()
ttk.Label(panel_izquierdo, text="Interpolación").pack(anchor="w")
self.combo_interpolacion = ttk.Combobox(
panel_izquierdo,
state="readonly",
values=["INTER_NEAREST", "INTER_LINEAR", "INTER_CUBIC", "INTER_AREA"]
)
self.combo_interpolacion.current(1)
self.combo_interpolacion.pack(fill="x", pady=2)
ttk.Separator(panel_izquierdo, orient="horizontal").pack(fill="x", pady=8)
texto_ayuda = (
"Funciones incluidas:\n"
"- Escalado y proporción\n"
"- Traslación\n"
"- Rotación\n"
"- Flip\n"
"- Recorte\n"
"- Afín\n"
"- Perspectiva\n"
"- Ejemplo integrado"
)
ttk.Label(panel_izquierdo, text=texto_ayuda, foreground="blue", justify="left").pack(anchor="w", pady=5)
# ---------------- Panel central ----------------
panel_centro = ttk.Frame(contenedor)
panel_centro.pack(side="left", fill="y", padx=(0, 10))
ttk.Label(panel_centro, text="Herramientas geométricas").pack(anchor="w")
self.frame_botones = ttk.Frame(panel_centro)
self.frame_botones.pack(fill="y", pady=5)
botones = [
("Redimensionar", self.redimensionar),
("Mantener proporción", self.redimensionar_proporcion),
("Trasladar", self.trasladar),
("Rotar", self.rotar),
("Rotar sin recortar", self.rotar_sin_recorte),
("Flip horizontal", self.flip_horizontal),
("Flip vertical", self.flip_vertical),
("Flip ambos", self.flip_ambos),
("Recorte central", self.recorte_central),
("Transformación afín", self.transformacion_afin),
("Transformación perspectiva", self.transformacion_perspectiva),
("Ejemplo integrado", self.ejemplo_integrado),
]
fila = 0
col = 0
for texto, comando in botones:
btn = ttk.Button(self.frame_botones, text=texto, command=comando, width=24)
btn.grid(row=fila, column=col, padx=3, pady=3, sticky="ew")
fila += 1
if fila == 6:
fila = 0
col += 1
# ---------------- Panel derecho ----------------
panel_derecho = ttk.Frame(contenedor)
panel_derecho.pack(side="left", fill="both", expand=True)
ttk.Label(panel_derecho, text="Imagen original").pack(anchor="center", pady=(0, 5))
self.label_original = ttk.Label(panel_derecho, relief="solid", anchor="center")
self.label_original.pack(fill="both", expand=True, padx=5, pady=(0, 10))
ttk.Label(panel_derecho, text="Imagen transformada").pack(anchor="center", pady=(0, 5))
self.label_procesada = ttk.Label(panel_derecho, relief="solid", anchor="center")
self.label_procesada.pack(fill="both", expand=True, padx=5, pady=(0, 5))
self.label_info = ttk.Label(
panel_derecho,
text="Seleccione una imagen para comenzar.",
foreground="blue"
)
self.label_info.pack(anchor="w", pady=5)
# ---------------------------------------------------------
# UTILIDADES
# ---------------------------------------------------------
def cargar_lista_imagenes(self):
self.lista_imagenes.delete(0, tk.END)
archivos = sorted([
archivo for archivo in os.listdir(".")
if os.path.isfile(archivo) and archivo.lower().endswith(EXTENSIONES_VALIDAS)
])
for archivo in archivos:
self.lista_imagenes.insert(tk.END, archivo)
def seleccionar_imagen(self, event=None):
seleccion = self.lista_imagenes.curselection()
if not seleccion:
return
nombre = self.lista_imagenes.get(seleccion[0])
imagen = cv2.imread(nombre)
if imagen is None:
messagebox.showerror("Error", f"No se pudo cargar la imagen:\n{nombre}")
return
self.nombre_archivo_actual = nombre
self.imagen_original = imagen.copy()
self.imagen_actual = imagen.copy()
self.mostrar_imagen(self.imagen_original, self.label_original)
self.mostrar_imagen(self.imagen_actual, self.label_procesada)
self.actualizar_info()
def abrir_otra_imagen(self):
ruta = filedialog.askopenfilename(
title="Seleccionar imagen",
filetypes=[
("Imágenes", "*.jpg *.jpeg *.png *.bmp *.tif *.tiff *.webp"),
("Todos los archivos", "*.*")
]
)
if not ruta:
return
imagen = cv2.imread(ruta)
if imagen is None:
messagebox.showerror("Error", "No se pudo cargar la imagen seleccionada.")
return
self.nombre_archivo_actual = os.path.basename(ruta)
self.imagen_original = imagen.copy()
self.imagen_actual = imagen.copy()
self.mostrar_imagen(self.imagen_original, self.label_original)
self.mostrar_imagen(self.imagen_actual, self.label_procesada)
self.actualizar_info()
def mostrar_imagen(self, imagen_cv, label):
if imagen_cv is None:
return
imagen_mostrar = self.convertir_para_tk(imagen_cv)
max_ancho = 700
max_alto = 350
alto, ancho = imagen_mostrar.shape[:2]
escala = min(max_ancho / ancho, max_alto / alto, 1.0)
nuevo_ancho = int(ancho * escala)
nuevo_alto = int(alto * escala)
imagen_redim = cv2.resize(imagen_mostrar, (nuevo_ancho, nuevo_alto))
imagen_pil = Image.fromarray(imagen_redim)
imagen_tk = ImageTk.PhotoImage(imagen_pil)
label.configure(image=imagen_tk)
label.image = imagen_tk
def convertir_para_tk(self, imagen_cv):
if len(imagen_cv.shape) == 2:
return imagen_cv
return cv2.cvtColor(imagen_cv, cv2.COLOR_BGR2RGB)
def actualizar_vista_procesada(self):
if self.imagen_actual is not None:
self.mostrar_imagen(self.imagen_actual, self.label_procesada)
self.actualizar_info()
def actualizar_info(self):
if self.imagen_actual is None:
self.label_info.config(text="No hay imagen cargada.")
return
forma = self.imagen_actual.shape
texto = f"Archivo: {self.nombre_archivo_actual} | Tamaño: {forma[1]} x {forma[0]}"
if len(forma) == 2:
texto += " | 1 canal"
else:
texto += f" | {forma[2]} canales"
self.label_info.config(text=texto)
def verificar_imagen(self):
if self.imagen_actual is None:
messagebox.showwarning("Aviso", "Primero debe seleccionar una imagen.")
return False
return True
def restablecer_imagen(self):
if self.imagen_original is None:
return
self.imagen_actual = self.imagen_original.copy()
self.actualizar_vista_procesada()
def guardar_resultado(self):
if not self.verificar_imagen():
return
ruta = filedialog.asksaveasfilename(
title="Guardar imagen transformada",
defaultextension=".png",
filetypes=[
("PNG", "*.png"),
("JPG", "*.jpg"),
("BMP", "*.bmp"),
("Todos los archivos", "*.*")
]
)
if not ruta:
return
ok = cv2.imwrite(ruta, self.imagen_actual)
if ok:
messagebox.showinfo("Guardar", "Imagen guardada correctamente.")
else:
messagebox.showerror("Error", "No se pudo guardar la imagen.")
def obtener_interpolacion(self):
nombre = self.combo_interpolacion.get()
mapa = {
"INTER_NEAREST": cv2.INTER_NEAREST,
"INTER_LINEAR": cv2.INTER_LINEAR,
"INTER_CUBIC": cv2.INTER_CUBIC,
"INTER_AREA": cv2.INTER_AREA
}
return mapa.get(nombre, cv2.INTER_LINEAR)
# ---------------------------------------------------------
# TRANSFORMACIONES GEOMÉTRICAS
# ---------------------------------------------------------
def redimensionar(self):
if not self.verificar_imagen():
return
try:
ancho = int(self.entry_ancho.get())
alto = int(self.entry_alto.get())
if ancho <= 0 or alto <= 0:
raise ValueError
interpolacion = self.obtener_interpolacion()
self.imagen_actual = cv2.resize(self.imagen_actual, (ancho, alto), interpolation=interpolacion)
self.actualizar_vista_procesada()
except ValueError:
messagebox.showerror("Error", "Ingrese un ancho y alto válidos.")
def redimensionar_proporcion(self):
if not self.verificar_imagen():
return
try:
nuevo_ancho = int(self.entry_ancho.get())
if nuevo_ancho <= 0:
raise ValueError
alto, ancho = self.imagen_actual.shape[:2]
nuevo_alto = int(alto * nuevo_ancho / ancho)
interpolacion = self.obtener_interpolacion()
self.imagen_actual = cv2.resize(
self.imagen_actual,
(nuevo_ancho, nuevo_alto),
interpolation=interpolacion
)
self.actualizar_vista_procesada()
except ValueError:
messagebox.showerror("Error", "Ingrese un ancho válido.")
def trasladar(self):
if not self.verificar_imagen():
return
try:
tx = int(self.entry_tx.get())
ty = int(self.entry_ty.get())
alto, ancho = self.imagen_actual.shape[:2]
M = np.float32([[1, 0, tx],
[0, 1, ty]])
self.imagen_actual = cv2.warpAffine(
self.imagen_actual,
M,
(ancho, alto),
borderValue=(0, 0, 0)
)
self.actualizar_vista_procesada()
except ValueError:
messagebox.showerror("Error", "Ingrese desplazamientos válidos.")
def rotar(self):
if not self.verificar_imagen():
return
alto, ancho = self.imagen_actual.shape[:2]
centro = (ancho // 2, alto // 2)
angulo = float(self.scale_angulo.get())
escala = float(self.scale_escala_rot.get())
M = cv2.getRotationMatrix2D(centro, angulo, escala)
self.imagen_actual = cv2.warpAffine(
self.imagen_actual,
M,
(ancho, alto),
flags=self.obtener_interpolacion(),
borderValue=(0, 0, 0)
)
self.actualizar_vista_procesada()
def rotar_sin_recorte(self):
if not self.verificar_imagen():
return
imagen = self.imagen_actual
alto, ancho = imagen.shape[:2]
centro = (ancho / 2, alto / 2)
angulo = float(self.scale_angulo.get())
escala = float(self.scale_escala_rot.get())
M = cv2.getRotationMatrix2D(centro, angulo, escala)
coseno = abs(M[0, 0])
seno = abs(M[0, 1])
nuevo_ancho = int((alto * seno) + (ancho * coseno))
nuevo_alto = int((alto * coseno) + (ancho * seno))
M[0, 2] += (nuevo_ancho / 2) - centro[0]
M[1, 2] += (nuevo_alto / 2) - centro[1]
self.imagen_actual = cv2.warpAffine(
imagen,
M,
(nuevo_ancho, nuevo_alto),
flags=self.obtener_interpolacion(),
borderValue=(0, 0, 0)
)
self.actualizar_vista_procesada()
def flip_horizontal(self):
if not self.verificar_imagen():
return
self.imagen_actual = cv2.flip(self.imagen_actual, 1)
self.actualizar_vista_procesada()
def flip_vertical(self):
if not self.verificar_imagen():
return
self.imagen_actual = cv2.flip(self.imagen_actual, 0)
self.actualizar_vista_procesada()
def flip_ambos(self):
if not self.verificar_imagen():
return
self.imagen_actual = cv2.flip(self.imagen_actual, -1)
self.actualizar_vista_procesada()
def recorte_central(self):
if not self.verificar_imagen():
return
imagen = self.imagen_actual
alto, ancho = imagen.shape[:2]
porc_x = self.scale_crop_x.get() / 100.0
porc_y = self.scale_crop_y.get() / 100.0
nuevo_ancho = int(ancho * porc_x)
nuevo_alto = int(alto * porc_y)
if nuevo_ancho <= 0 or nuevo_alto <= 0:
messagebox.showerror("Error", "El tamaño del recorte no es válido.")
return
x1 = (ancho - nuevo_ancho) // 2
y1 = (alto - nuevo_alto) // 2
x2 = x1 + nuevo_ancho
y2 = y1 + nuevo_alto
self.imagen_actual = imagen[y1:y2, x1:x2]
self.actualizar_vista_procesada()
def transformacion_afin(self):
if not self.verificar_imagen():
return
imagen = self.imagen_actual
alto, ancho = imagen.shape[:2]
pts1 = np.float32([
[0.15 * ancho, 0.15 * alto],
[0.85 * ancho, 0.15 * alto],
[0.15 * ancho, 0.85 * alto]
])
pts2 = np.float32([
[0.05 * ancho, 0.25 * alto],
[0.90 * ancho, 0.10 * alto],
[0.25 * ancho, 0.90 * alto]
])
M = cv2.getAffineTransform(pts1, pts2)
self.imagen_actual = cv2.warpAffine(
imagen,
M,
(ancho, alto),
flags=self.obtener_interpolacion(),
borderValue=(0, 0, 0)
)
self.actualizar_vista_procesada()
def transformacion_perspectiva(self):
if not self.verificar_imagen():
return
imagen = self.imagen_actual
alto, ancho = imagen.shape[:2]
pts1 = np.float32([
[0.15 * ancho, 0.10 * alto],
[0.85 * ancho, 0.15 * alto],
[0.10 * ancho, 0.90 * alto],
[0.90 * ancho, 0.85 * alto]
])
pts2 = np.float32([
[0, 0],
[ancho - 1, 0],
[0, alto - 1],
[ancho - 1, alto - 1]
])
M = cv2.getPerspectiveTransform(pts1, pts2)
self.imagen_actual = cv2.warpPerspective(
imagen,
M,
(ancho, alto),
flags=self.obtener_interpolacion(),
borderValue=(0, 0, 0)
)
self.actualizar_vista_procesada()
def ejemplo_integrado(self):
if not self.verificar_imagen():
return
imagen = self.imagen_actual.copy()
alto, ancho = imagen.shape[:2]
# 1) Escalado moderado
imagen = cv2.resize(
imagen,
(int(ancho * 0.8), int(alto * 0.8)),
interpolation=self.obtener_interpolacion()
)
alto2, ancho2 = imagen.shape[:2]
centro = (ancho2 // 2, alto2 // 2)
# 2) Rotación
M_rot = cv2.getRotationMatrix2D(centro, 20, 1.0)
imagen = cv2.warpAffine(
imagen,
M_rot,
(ancho2, alto2),
flags=self.obtener_interpolacion(),
borderValue=(0, 0, 0)
)
# 3) Traslación
M_tras = np.float32([[1, 0, 40],
[0, 1, 20]])
imagen = cv2.warpAffine(
imagen,
M_tras,
(ancho2, alto2),
flags=self.obtener_interpolacion(),
borderValue=(0, 0, 0)
)
self.imagen_actual = imagen
self.actualizar_vista_procesada()
if __name__ == "__main__":
root = tk.Tk()
app = AplicacionTransformacionesGeometricas(root)
root.mainloop()
Este programa reúne en una sola interfaz varias transformaciones geométricas y permite probarlas interactivamente sobre imágenes del directorio actual o cargadas por el usuario.
Al trabajar con transformaciones geométricas suelen aparecer errores frecuentes:
Estos errores afectan tanto la calidad visual como la utilidad del resultado para algoritmos posteriores.
resize, warpAffine y warpPerspective.Las transformaciones geométricas permiten reorganizar el contenido visual de una imagen para hacerlo más útil, más comparable o más adecuado para un algoritmo posterior. Son herramientas básicas, pero extremadamente potentes en aplicaciones prácticas.
Entender cómo se desplazan, rotan o deforman las coordenadas es un paso importante para pensar en visión por computadora con mayor profundidad matemática y operativa.
En el próximo tema estudiaremos el filtrado y suavizado de imágenes, que nos llevará desde la geometría hacia técnicas que modifican la información local de cada región para reducir ruido o resaltar estructuras.