10. Procesamiento básico de imágenes con OpenCV

10.1 Introducción

Una vez que sabemos cargar imágenes con OpenCV, el siguiente paso natural es empezar a transformarlas. Eso es justamente el procesamiento básico de imágenes: aplicar operaciones simples pero muy útiles para modificar, preparar o resaltar información visual.

Estas operaciones son fundamentales porque aparecen en casi cualquier pipeline real. Antes de detectar objetos, entrenar una red o segmentar una escena, muchas veces conviene ajustar el formato, cambiar el brillo, convertir a grises, binarizar o suavizar la imagen.

En este tema veremos las operaciones básicas más comunes con OpenCV y entenderemos para qué sirven dentro de un flujo de visión por computadora.

10.2 ¿Qué significa procesar una imagen?

Procesar una imagen significa aplicar transformaciones sobre su representación numérica. Como ya vimos, una imagen digital es una matriz o conjunto de matrices. Entonces, procesarla implica modificar esos valores con algún objetivo.

Ese objetivo puede ser muy variado:

  • Mejorar la apariencia visual.
  • Reducir ruido.
  • Resaltar estructuras importantes.
  • Preparar la imagen para otro algoritmo.
  • Extraer regiones o simplificar información.
Procesar una imagen no es “hacerle efectos”. Es modificar sus datos numéricos para facilitar una tarea posterior o resaltar información relevante.

10.3 Punto de partida típico

Casi todos los ejemplos de procesamiento básico empiezan con la lectura de una imagen:

import cv2

imagen = cv2.imread("foto.jpg")

if imagen is None:
    raise ValueError("No se pudo cargar la imagen")

A partir de ahí podemos aplicar operaciones sucesivas según la necesidad del problema.

10.4 Conversión a escala de grises

Una de las primeras transformaciones más comunes es convertir la imagen a escala de grises. Esto simplifica el procesamiento y reduce la cantidad de canales a uno solo.

gris = cv2.cvtColor(imagen, cv2.COLOR_BGR2GRAY)

Esta operación se usa mucho como paso previo para umbralización, detección de bordes, análisis de intensidad y filtrado clásico.

10.5 Cambio de tamaño

Redimensionar una imagen es otra operación básica. Puede hacerse para adaptar el tamaño a una visualización, reducir costo computacional o normalizar entradas antes de aplicar un algoritmo.

redimensionada = cv2.resize(imagen, (400, 300))

Recordar que en OpenCV el nuevo tamaño se especifica como (ancho, alto).

10.6 Recorte de regiones

Otra operación muy frecuente es extraer solo una parte de la imagen, es decir, una región de interés:

recorte = imagen[100:300, 150:400]

Los recortes permiten focalizar el análisis en una zona concreta y también se usan mucho para preparar entradas a modelos o inspeccionar resultados.

10.7 Guardar resultados procesados

Después de transformar una imagen, es habitual guardar el resultado para compararlo o usarlo en otra etapa:

cv2.imwrite("resultado.jpg", recorte)

Este paso resulta útil tanto en experimentos como en pipelines automáticos.

10.8 Ajuste de brillo

Modificar el brillo significa aumentar o disminuir la intensidad general de los píxeles. Una forma simple de hacerlo es sumar un valor constante:

import numpy as np

brillo = np.clip(imagen + 40, 0, 255).astype(np.uint8)

El uso de np.clip evita que los valores salgan del rango permitido. Este tipo de ajuste puede ser útil para compensar imágenes oscuras o generar variantes de entrenamiento.

10.9 Ajuste de contraste

El contraste controla la diferencia entre regiones claras y oscuras. Una forma simple de modificarlo es multiplicar por un factor:

contraste = np.clip(imagen * 1.2, 0, 255).astype(np.uint8)

Cuando aumentamos el contraste, las diferencias visuales se intensifican. Esto puede ayudar a resaltar detalles, aunque un exceso también puede saturar información.

10.10 Brillo y contraste con convertScaleAbs

OpenCV ofrece una función conveniente para ajustar brillo y contraste al mismo tiempo:

ajustada = cv2.convertScaleAbs(imagen, alpha=1.2, beta=30)

Aquí:

  • alpha controla el contraste.
  • beta controla el brillo.

Esta función es práctica y suele ser preferible a implementar estas operaciones manualmente.

10.11 Invertir una imagen

Una operación sencilla pero ilustrativa consiste en invertir intensidades. En una imagen de 8 bits, esto puede hacerse así:

invertida = 255 - imagen

En una imagen en escala de grises, los píxeles oscuros pasan a claros y viceversa. Aunque no siempre tiene utilidad práctica directa, ayuda a entender cómo las operaciones numéricas alteran la imagen.

10.12 Conversión a binario: umbralización

Una de las operaciones más importantes del procesamiento básico es la umbralización. Consiste en convertir una imagen de grises en una imagen binaria, donde cada píxel pasa a ser negro o blanco según supere o no un umbral.

_, binaria = cv2.threshold(gris, 127, 255, cv2.THRESH_BINARY)

Esto significa:

  • Si el valor del píxel es mayor que 127, pasa a 255.
  • Si no, pasa a 0.

La umbralización es muy útil para separar foreground y background en problemas relativamente simples.

10.13 Tipos de umbralización

OpenCV soporta varios tipos de umbralización, no solo la binaria tradicional. Algunas variantes importantes son:

  • THRESH_BINARY
  • THRESH_BINARY_INV
  • THRESH_TRUNC
  • THRESH_TOZERO
  • THRESH_TOZERO_INV

Cada una modifica la imagen de manera diferente, y su utilidad depende del problema específico.

10.14 Umbralización adaptativa

Cuando la iluminación no es uniforme, usar un único umbral global puede fallar. En esos casos, OpenCV ofrece umbralización adaptativa, que calcula umbrales locales para distintas regiones.

adaptativa = cv2.adaptiveThreshold(
    gris,
    255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    11,
    2
)

Este tipo de técnica es muy útil en documentos, escenas con sombras o imágenes donde la iluminación varía significativamente.

10.15 Suavizado básico

El suavizado reduce variaciones bruscas entre píxeles vecinos. Esto puede ayudar a disminuir ruido y preparar mejor la imagen para otras etapas.

Una forma simple es aplicar desenfoque promedio:

suavizada = cv2.blur(imagen, (5, 5))

Aquí el kernel de tamaño (5, 5) promedia los valores cercanos.

10.16 Desenfoque gaussiano

Otra técnica muy usada es el desenfoque gaussiano, que suaviza de forma más natural que el promedio simple:

gauss = cv2.GaussianBlur(imagen, (5, 5), 0)

Este filtro será especialmente importante cuando lleguemos a detección de bordes y reducción de ruido.

10.17 Dibujo sobre imágenes

OpenCV también permite dibujar elementos gráficos directamente sobre una imagen. Esto es muy útil para anotaciones, visualización de resultados o debugging.

Por ejemplo, dibujar un rectángulo:

cv2.rectangle(imagen, (50, 50), (200, 200), (0, 255, 0), 2)

O escribir texto:

cv2.putText(imagen, "Objeto", (50, 40),
            cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)

Estas herramientas son muy valiosas cuando queremos mostrar predicciones o resaltar regiones.

10.18 Conversión de espacios de color

Otra operación básica muy frecuente es cambiar el espacio de color. Por ejemplo:

hsv = cv2.cvtColor(imagen, cv2.COLOR_BGR2HSV)

Este tipo de conversión es útil cuando ciertas tareas, como la segmentación por color, resultan más sencillas en otro espacio diferente de BGR.

10.19 Separación y unión de canales

OpenCV permite trabajar por separado sobre cada canal:

b, g, r = cv2.split(imagen)

Y luego volver a unirlos:

combinada = cv2.merge([b, g, r])

Esto puede ser útil para inspección, ajustes selectivos o análisis de componentes específicas.

10.20 Cambio de orientación

Entre las operaciones elementales también están los volteos y algunas rotaciones simples. Por ejemplo:

horizontal = cv2.flip(imagen, 1)
vertical = cv2.flip(imagen, 0)

Estas transformaciones son sencillas pero útiles tanto para inspección visual como para aumentar datos en entrenamiento.

10.21 Una secuencia típica de preprocesamiento

En muchos problemas reales, el procesamiento básico aparece como una secuencia de pasos concatenados. Por ejemplo:

  1. Cargar imagen.
  2. Redimensionar.
  3. Convertir a grises.
  4. Aplicar suavizado.
  5. Umbralizar o detectar bordes.

Esto muestra que las operaciones básicas rara vez aparecen aisladas. Suelen integrarse en pipelines más amplios.

10.22 Ejemplo integrado

Veamos ahora una aplicación completa que reúne, en una sola interfaz, gran parte de las funcionalidades básicas estudiadas en este tema:

Aplicación completa de procesamiento básico de imágenes con OpenCV
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 AplicacionOpenCV:
    def __init__(self, root):
        self.root = root
        self.root.title("Procesamiento básico de imágenes con OpenCV")
        self.root.geometry("1450x850")

        self.imagen_original = None
        self.imagen_actual = None
        self.nombre_archivo_actual = None

        self.crear_interfaz()
        self.cargar_lista_imagenes()

    def crear_interfaz(self):
        contenedor = ttk.Frame(self.root, padding=10)
        contenedor.pack(fill="both", expand=True)

        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="Brillo").pack(anchor="w")
        self.scale_brillo = tk.Scale(panel_izquierdo, from_=-100, to=100, orient="horizontal", length=250)
        self.scale_brillo.set(40)
        self.scale_brillo.pack()

        ttk.Label(panel_izquierdo, text="Contraste").pack(anchor="w")
        self.scale_contraste = tk.Scale(panel_izquierdo, from_=0.1, to=3.0, resolution=0.1,
                                        orient="horizontal", length=250)
        self.scale_contraste.set(1.2)
        self.scale_contraste.pack()

        ttk.Label(panel_izquierdo, text="Umbral").pack(anchor="w")
        self.scale_umbral = tk.Scale(panel_izquierdo, from_=0, to=255, orient="horizontal", length=250)
        self.scale_umbral.set(127)
        self.scale_umbral.pack()

        ttk.Label(panel_izquierdo, text="Alpha (contraste convertScaleAbs)").pack(anchor="w")
        self.scale_alpha = tk.Scale(panel_izquierdo, from_=0.1, to=3.0, resolution=0.1,
                                    orient="horizontal", length=250)
        self.scale_alpha.set(1.2)
        self.scale_alpha.pack()

        ttk.Label(panel_izquierdo, text="Beta (brillo convertScaleAbs)").pack(anchor="w")
        self.scale_beta = tk.Scale(panel_izquierdo, from_=-100, to=100, orient="horizontal", length=250)
        self.scale_beta.set(30)
        self.scale_beta.pack()

        ttk.Label(panel_izquierdo, text="Nuevo ancho").pack(anchor="w")
        self.entry_ancho = ttk.Entry(panel_izquierdo)
        self.entry_ancho.insert(0, "400")
        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, "300")
        self.entry_alto.pack(fill="x", pady=2)

        ttk.Label(panel_izquierdo, text="Texto para dibujar").pack(anchor="w")
        self.entry_texto = ttk.Entry(panel_izquierdo)
        self.entry_texto.insert(0, "Objeto")
        self.entry_texto.pack(fill="x", pady=2)

        ttk.Separator(panel_izquierdo, orient="horizontal").pack(fill="x", pady=8)

        panel_centro = ttk.Frame(contenedor)
        panel_centro.pack(side="left", fill="y", padx=(0, 10))

        ttk.Label(panel_centro, text="Herramientas OpenCV").pack(anchor="w")

        self.frame_botones = ttk.Frame(panel_centro)
        self.frame_botones.pack(fill="y", pady=5)

        botones = [
            ("Escala de grises", self.a_grises),
            ("Redimensionar", self.redimensionar),
            ("Recorte central", self.recorte_central),
            ("Ajustar brillo", self.ajustar_brillo),
            ("Ajustar contraste", self.ajustar_contraste),
            ("Brillo + contraste", self.ajustar_convert_scale_abs),
            ("Invertir", self.invertir),
            ("Threshold binario", self.threshold_binario),
            ("THRESH_BINARY_INV", self.threshold_binary_inv),
            ("THRESH_TRUNC", self.threshold_trunc),
            ("THRESH_TOZERO", self.threshold_tozero),
            ("THRESH_TOZERO_INV", self.threshold_tozero_inv),
            ("Threshold adaptativo", self.threshold_adaptativo),
            ("Blur promedio", self.blur_promedio),
            ("Gaussian Blur", self.gaussian_blur),
            ("Dibujar rectángulo", self.dibujar_rectangulo),
            ("Dibujar texto", self.dibujar_texto),
            ("Convertir a HSV", self.convertir_hsv),
            ("Canal azul", self.mostrar_canal_azul),
            ("Canal verde", self.mostrar_canal_verde),
            ("Canal rojo", self.mostrar_canal_rojo),
            ("Reconstruir BGR", self.reconstruir_bgr),
            ("Flip horizontal", self.flip_horizontal),
            ("Flip vertical", self.flip_vertical),
            ("Rotar 90", self.rotar_90),
            ("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=22)
            btn.grid(row=fila, column=col, padx=3, pady=3, sticky="ew")
            fila += 1
            if fila == 13:
                fila = 0
                col += 1

        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 procesada").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)

    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 = 500
        max_alto = 700

        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
        if len(forma) == 2:
            texto = f"Archivo: {self.nombre_archivo_actual} | Tamaño: {forma[1]} x {forma[0]} | 1 canal (grises)"
        else:
            texto = f"Archivo: {self.nombre_archivo_actual} | Tamaño: {forma[1]} x {forma[0]} | {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 obtener_gris(self):
        if len(self.imagen_actual.shape) == 2:
            return self.imagen_actual.copy()
        return cv2.cvtColor(self.imagen_actual, cv2.COLOR_BGR2GRAY)

    def obtener_bgr(self):
        if len(self.imagen_actual.shape) == 2:
            return cv2.cvtColor(self.imagen_actual, cv2.COLOR_GRAY2BGR)
        return self.imagen_actual.copy()

    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 procesada",
            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 a_grises(self):
        if not self.verificar_imagen():
            return
        self.imagen_actual = self.obtener_gris()
        self.actualizar_vista_procesada()

    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
            self.imagen_actual = cv2.resize(self.imagen_actual, (ancho, alto))
            self.actualizar_vista_procesada()
        except ValueError:
            messagebox.showerror("Error", "Ingrese ancho y alto válidos.")

    def recorte_central(self):
        if not self.verificar_imagen():
            return

        h, w = self.imagen_actual.shape[:2]
        x1 = w // 4
        x2 = 3 * w // 4
        y1 = h // 4
        y2 = 3 * h // 4

        self.imagen_actual = self.imagen_actual[y1:y2, x1:x2]
        self.actualizar_vista_procesada()

    def ajustar_brillo(self):
        if not self.verificar_imagen():
            return
        valor = self.scale_brillo.get()

        img = self.imagen_actual.astype(np.int16) + valor
        self.imagen_actual = np.clip(img, 0, 255).astype(np.uint8)
        self.actualizar_vista_procesada()

    def ajustar_contraste(self):
        if not self.verificar_imagen():
            return
        factor = float(self.scale_contraste.get())

        img = self.imagen_actual.astype(np.float32) * factor
        self.imagen_actual = np.clip(img, 0, 255).astype(np.uint8)
        self.actualizar_vista_procesada()

    def ajustar_convert_scale_abs(self):
        if not self.verificar_imagen():
            return
        alpha = float(self.scale_alpha.get())
        beta = int(self.scale_beta.get())
        self.imagen_actual = cv2.convertScaleAbs(self.imagen_actual, alpha=alpha, beta=beta)
        self.actualizar_vista_procesada()

    def invertir(self):
        if not self.verificar_imagen():
            return
        self.imagen_actual = 255 - self.imagen_actual
        self.actualizar_vista_procesada()

    def threshold_binario(self):
        if not self.verificar_imagen():
            return
        gris = self.obtener_gris()
        umbral = self.scale_umbral.get()
        _, self.imagen_actual = cv2.threshold(gris, umbral, 255, cv2.THRESH_BINARY)
        self.actualizar_vista_procesada()

    def threshold_binary_inv(self):
        if not self.verificar_imagen():
            return
        gris = self.obtener_gris()
        umbral = self.scale_umbral.get()
        _, self.imagen_actual = cv2.threshold(gris, umbral, 255, cv2.THRESH_BINARY_INV)
        self.actualizar_vista_procesada()

    def threshold_trunc(self):
        if not self.verificar_imagen():
            return
        gris = self.obtener_gris()
        umbral = self.scale_umbral.get()
        _, self.imagen_actual = cv2.threshold(gris, umbral, 255, cv2.THRESH_TRUNC)
        self.actualizar_vista_procesada()

    def threshold_tozero(self):
        if not self.verificar_imagen():
            return
        gris = self.obtener_gris()
        umbral = self.scale_umbral.get()
        _, self.imagen_actual = cv2.threshold(gris, umbral, 255, cv2.THRESH_TOZERO)
        self.actualizar_vista_procesada()

    def threshold_tozero_inv(self):
        if not self.verificar_imagen():
            return
        gris = self.obtener_gris()
        umbral = self.scale_umbral.get()
        _, self.imagen_actual = cv2.threshold(gris, umbral, 255, cv2.THRESH_TOZERO_INV)
        self.actualizar_vista_procesada()

    def threshold_adaptativo(self):
        if not self.verificar_imagen():
            return
        gris = self.obtener_gris()
        self.imagen_actual = cv2.adaptiveThreshold(
            gris,
            255,
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            cv2.THRESH_BINARY,
            11,
            2
        )
        self.actualizar_vista_procesada()

    def blur_promedio(self):
        if not self.verificar_imagen():
            return
        self.imagen_actual = cv2.blur(self.imagen_actual, (5, 5))
        self.actualizar_vista_procesada()

    def gaussian_blur(self):
        if not self.verificar_imagen():
            return
        self.imagen_actual = cv2.GaussianBlur(self.imagen_actual, (5, 5), 0)
        self.actualizar_vista_procesada()

    def dibujar_rectangulo(self):
        if not self.verificar_imagen():
            return
        img = self.obtener_bgr()
        h, w = img.shape[:2]
        cv2.rectangle(img, (w // 6, h // 6), (5 * w // 6, 5 * h // 6), (0, 255, 0), 2)
        self.imagen_actual = img
        self.actualizar_vista_procesada()

    def dibujar_texto(self):
        if not self.verificar_imagen():
            return
        img = self.obtener_bgr()
        texto = self.entry_texto.get().strip()
        if not texto:
            texto = "Objeto"
        cv2.putText(img, texto, (30, 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
        self.imagen_actual = img
        self.actualizar_vista_procesada()

    def convertir_hsv(self):
        if not self.verificar_imagen():
            return
        img = self.obtener_bgr()
        self.imagen_actual = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        self.actualizar_vista_procesada()

    def mostrar_canal_azul(self):
        if not self.verificar_imagen():
            return
        img = self.obtener_bgr()
        b, g, r = cv2.split(img)
        self.imagen_actual = b
        self.actualizar_vista_procesada()

    def mostrar_canal_verde(self):
        if not self.verificar_imagen():
            return
        img = self.obtener_bgr()
        b, g, r = cv2.split(img)
        self.imagen_actual = g
        self.actualizar_vista_procesada()

    def mostrar_canal_rojo(self):
        if not self.verificar_imagen():
            return
        img = self.obtener_bgr()
        b, g, r = cv2.split(img)
        self.imagen_actual = r
        self.actualizar_vista_procesada()

    def reconstruir_bgr(self):
        if not self.verificar_imagen():
            return
        img = self.obtener_bgr()
        b, g, r = cv2.split(img)
        self.imagen_actual = cv2.merge([b, g, r])
        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 rotar_90(self):
        if not self.verificar_imagen():
            return
        self.imagen_actual = cv2.rotate(self.imagen_actual, cv2.ROTATE_90_CLOCKWISE)
        self.actualizar_vista_procesada()

    def ejemplo_integrado(self):
        if not self.verificar_imagen():
            return

        img = self.obtener_bgr()
        img = cv2.resize(img, (400, 300))
        gris = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        gauss = cv2.GaussianBlur(gris, (5, 5), 0)
        _, binaria = cv2.threshold(gauss, 120, 255, cv2.THRESH_BINARY)

        self.imagen_actual = binaria
        self.actualizar_vista_procesada()


if __name__ == "__main__":
    root = tk.Tk()
    app = AplicacionOpenCV(root)
    root.mainloop()

Esta aplicación funciona como un pequeño laboratorio interactivo: permite cargar imágenes, aplicar transformaciones, ajustar parámetros y visualizar inmediatamente el resultado. Es un buen ejemplo de cómo muchas operaciones básicas de OpenCV pueden integrarse en una herramienta práctica y reutilizable.

10.23 Errores comunes

Al empezar con procesamiento básico de imágenes suelen aparecer errores frecuentes:

  • No controlar si la imagen se cargó correctamente.
  • Confundir BGR con RGB.
  • Usar umbralización global donde la iluminación cambia mucho.
  • Aplicar ajustes de brillo o contraste sin controlar overflow.
  • Redimensionar deformando la relación de aspecto.
  • Olvidar que muchas operaciones dependen del tipo de dato.

Estos errores son normales al principio, pero conviene detectarlos pronto porque afectan mucho la calidad del pipeline.

10.24 Qué debes recordar de este tema

  • Procesar una imagen significa transformar sus valores numéricos con un objetivo concreto.
  • Las operaciones básicas incluyen conversión a grises, redimensionado, recorte, ajuste de brillo y contraste, suavizado y umbralización.
  • OpenCV permite encadenar estas operaciones con mucha facilidad.
  • La umbralización es una técnica central para separar regiones claras y oscuras.
  • Los filtros básicos ayudan a reducir ruido y preparar imágenes para análisis posterior.
  • Estas transformaciones suelen funcionar como preprocesamiento para tareas más complejas.

10.25 Conclusión

El procesamiento básico de imágenes con OpenCV constituye la base práctica del trabajo clásico en visión por computadora. Aunque estas operaciones son relativamente simples, tienen un valor enorme porque preparan la imagen, simplifican la información y hacen posible etapas posteriores de análisis.

Dominar estas herramientas permite construir una intuición muy útil sobre cómo responden las imágenes a distintas transformaciones y qué tipo de preprocesamiento conviene aplicar en cada caso.

En el próximo tema profundizaremos en las operaciones sobre píxeles y matrices, donde veremos con más detalle la manipulación numérica directa de la imagen.