15. Histogramas y análisis de intensidades

15.1 Introducción

Hasta ahora trabajamos con imágenes desde un punto de vista local: vecindades, filtros, gradientes, bordes y transformaciones geométricas. En este tema vamos a cambiar de escala y mirar la imagen desde una perspectiva más global: la distribución de sus intensidades.

Para eso usamos una herramienta fundamental en visión por computadora y procesamiento de imágenes: el histograma. Un histograma resume cuántos píxeles tienen cada nivel de intensidad o color, y por lo tanto nos permite entender si una imagen es oscura, clara, contrastada, plana o desbalanceada.

Esta herramienta resulta muy útil para análisis exploratorio, mejora de contraste, segmentación y normalización visual.

15.2 ¿Qué es un histograma de imagen?

Un histograma de imagen es una representación de la frecuencia con la que aparecen ciertos valores de intensidad. En una imagen en escala de grises de 8 bits, esos valores suelen ir de 0 a 255.

Eso significa que el histograma nos indica:

  • Cuántos píxeles tienen valor 0.
  • Cuántos tienen valor 1.
  • Cuántos tienen valor 2.
  • Y así sucesivamente hasta 255.

En vez de mirar cada píxel individualmente, el histograma resume la distribución global de la imagen.

El histograma no nos dice dónde están los píxeles, sino cuántos hay de cada intensidad. Es un resumen estadístico global de la imagen.

15.3 Interpretación intuitiva

Podemos pensar el histograma como una gráfica donde:

  • El eje horizontal representa intensidades.
  • El eje vertical representa cantidad de píxeles.

Si la mayor parte del histograma está concentrada en valores bajos, la imagen tenderá a ser oscura. Si se concentra en valores altos, tenderá a ser clara. Si ocupa un rango amplio de intensidades, habrá más contraste.

15.4 Histograma de una imagen en grises

En imágenes en escala de grises, el análisis es directo porque cada píxel tiene una sola intensidad. Un histograma típico puede construirse fácilmente con OpenCV o con Matplotlib.

Con OpenCV:

hist = cv2.calcHist([gris], [0], None, [256], [0, 256])

Aquí:

  • [gris] es la imagen.
  • [0] indica el canal.
  • None indica que no usamos máscara.
  • [256] indica la cantidad de bins.
  • [0, 256] define el rango de intensidades.

15.5 Visualizar histogramas con Matplotlib

Una forma muy habitual de visualizar histogramas es con Matplotlib:

import cv2
import matplotlib.pyplot as plt

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

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

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

plt.hist(gris.ravel(), bins=256, range=[0, 256])
plt.title("Histograma")
plt.xlabel("Intensidad")
plt.ylabel("Cantidad de pixeles")
plt.show()

La función ravel() convierte la matriz en una secuencia unidimensional de valores, lo que facilita construir el histograma.

15.6 ¿Qué nos dice el histograma sobre el contraste?

Uno de los usos más importantes del histograma es analizar el contraste de la imagen:

  • Si el histograma está muy concentrado en una franja estrecha, el contraste suele ser bajo.
  • Si está distribuido a lo largo de gran parte del rango, el contraste suele ser mayor.

Esto se debe a que el contraste depende de cuán separadas estén las intensidades entre regiones oscuras y claras.

15.7 Imágenes oscuras, claras y equilibradas

Un histograma también da una idea rápida del nivel general de luminosidad:

  • Imagen oscura: predominan intensidades bajas.
  • Imagen clara: predominan intensidades altas.
  • Imagen equilibrada: las intensidades se reparten de forma más amplia.

Esta observación es útil para decidir si conviene aplicar correcciones de brillo o contraste.

15.8 Histogramas en imágenes color

En una imagen color, no hay un único histograma posible. Podemos calcular uno por canal. Por ejemplo, en una imagen BGR o RGB podemos obtener histogramas separados para cada componente.

Con OpenCV:

import cv2
import matplotlib.pyplot as plt

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

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

hist_b = cv2.calcHist([imagen], [0], None, [256], [0, 256])
hist_g = cv2.calcHist([imagen], [1], None, [256], [0, 256])
hist_r = cv2.calcHist([imagen], [2], None, [256], [0, 256])

plt.figure(figsize=(10, 4))
plt.plot(hist_b, color="blue", label="Canal azul")
plt.plot(hist_g, color="green", label="Canal verde")
plt.plot(hist_r, color="red", label="Canal rojo")
plt.title("Histogramas por canal")
plt.xlabel("Intensidad")
plt.ylabel("Cantidad de pixeles")
plt.xlim([0, 256])
plt.legend()
plt.show()

Esto permite analizar cómo se distribuyen las intensidades en cada canal por separado.

15.9 ¿Para qué sirven histogramas por canal?

Los histogramas por canal son útiles cuando queremos:

  • Detectar dominancias de color.
  • Analizar si una imagen está desbalanceada cromáticamente.
  • Comparar imágenes entre sí.
  • Entender cómo afecta una transformación de color.

Por ejemplo, si el canal azul domina claramente sobre los demás, la imagen podría tener un sesgo frío o estar influida por ciertas condiciones de iluminación.

15.10 Uso de máscaras en histogramas

No siempre queremos analizar toda la imagen. A veces interesa estudiar solo una región. Para eso pueden utilizarse máscaras.

Ejemplo:

import cv2
import numpy as np
import matplotlib.pyplot as plt

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

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

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

mascara = np.zeros(gris.shape[:2], dtype="uint8")
mascara[100:300, 150:400] = 255

roi = cv2.bitwise_and(gris, gris, mask=mascara)
hist_roi = cv2.calcHist([gris], [0], mascara, [256], [0, 256])

plt.figure(figsize=(12, 4))

plt.subplot(1, 3, 1)
plt.imshow(gris, cmap="gray")
plt.title("Imagen en grises")
plt.axis("off")

plt.subplot(1, 3, 2)
plt.imshow(roi, cmap="gray")
plt.title("ROI con máscara")
plt.axis("off")

plt.subplot(1, 3, 3)
plt.plot(hist_roi, color="black")
plt.title("Histograma de la ROI")
plt.xlabel("Intensidad")
plt.ylabel("Cantidad de pixeles")
plt.xlim([0, 256])

plt.tight_layout()
plt.show()

Esto permite calcular el histograma únicamente dentro de la región marcada por la máscara.

15.11 Histograma acumulado

Además del histograma estándar, a veces se usa el histograma acumulado. En lugar de indicar cuántos píxeles tienen un valor exacto, indica cuántos píxeles tienen intensidad menor o igual a cierto nivel.

Esta idea es importante porque está directamente relacionada con transformaciones como la ecualización del histograma.

15.12 Ecualización del histograma

La ecualización es una técnica destinada a mejorar el contraste redistribuyendo las intensidades de forma más uniforme. La idea general es expandir el uso del rango dinámico disponible.

Con OpenCV, en imágenes en grises, puede hacerse así:

ecualizada = cv2.equalizeHist(gris)

El resultado suele hacer más visibles ciertas regiones cuando la imagen original tiene contraste pobre.

15.13 ¿Cuándo ayuda la ecualización?

La ecualización puede ser útil cuando:

  • La imagen tiene poco contraste global.
  • Las intensidades están concentradas en un rango estrecho.
  • Queremos resaltar detalles que estaban poco diferenciados.

Sin embargo, no siempre mejora la imagen de forma deseable. En algunos casos puede exagerar ruido o alterar demasiado la apariencia.

15.14 CLAHE: ecualización adaptativa limitada

Una variante muy útil es CLAHE (Contrast Limited Adaptive Histogram Equalization). En lugar de ecualizar toda la imagen globalmente, trabaja por regiones locales y además limita el realce excesivo.

En OpenCV:

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
resultado = clahe.apply(gris)

CLAHE es especialmente útil en imágenes con iluminación desigual o cuando queremos mejorar contraste local sin generar artefactos demasiado fuertes.

15.15 Histograma y umbralización

El histograma también ayuda a entender si una imagen podría segmentarse bien con un umbral global. Por ejemplo:

  • Si hay dos grupos bien separados de intensidades, la umbralización puede funcionar bien.
  • Si la distribución es muy mezclada o continua, segmentar con un único umbral será más difícil.

Esto conecta directamente con técnicas como Otsu, que buscan automáticamente un buen umbral a partir de la distribución de intensidades.

15.16 Umbralización de Otsu

El método de Otsu busca automáticamente un umbral que separe lo mejor posible dos grupos de intensidades. Se apoya justamente en el histograma de la imagen.

Con OpenCV:

_, otsu = cv2.threshold(
    gris,
    0,
    255,
    cv2.THRESH_BINARY + cv2.THRESH_OTSU
)

Esto evita fijar manualmente el umbral y resulta útil cuando la imagen tiene una separación relativamente clara entre foreground y background.

15.17 Distribución de intensidades y rango dinámico

El rango dinámico se refiere al intervalo de intensidades efectivamente usado por la imagen. Una imagen puede tener valores posibles de 0 a 255, pero si casi todos sus píxeles están entre 80 y 140, en la práctica está usando un rango reducido.

Esto suele traducirse en bajo contraste. Analizar el histograma nos permite detectar rápidamente esta situación.

15.18 Histogramas normalizados

A veces no interesa trabajar con frecuencias absolutas, sino con proporciones. En ese caso puede usarse un histograma normalizado, donde la suma de las frecuencias se ajusta a una escala común.

Esto es útil para:

  • Comparar imágenes de distinto tamaño.
  • Medir similitud entre distribuciones.
  • Construir descriptores más comparables.

15.19 Comparación de histogramas

Los histogramas también pueden utilizarse como descriptores globales de imagen. Una imagen y otra pueden compararse observando cuán similares son sus distribuciones de intensidad o color.

Esto se usa en tareas como:

  • Búsqueda de imágenes similares.
  • Detección de cambios globales.
  • Clasificación simple basada en apariencia global.

Sin embargo, un histograma pierde completamente la información espacial. Dos imágenes muy diferentes pueden compartir histogramas parecidos.

15.20 Limitaciones del histograma

Aunque el histograma es muy útil, también tiene límites importantes:

  • No dice dónde están los píxeles.
  • No representa forma ni geometría.
  • No conserva estructura espacial.
  • No distingue entre imágenes con la misma distribución pero distinta organización.

Por eso debe entenderse como una herramienta global y complementaria, no como una descripción completa de la imagen.

15.21 Ejemplo integrado de análisis

Un ejemplo más completo puede resolverse con una aplicación de escritorio que cargue foto1.jpg, muestre la imagen original y su versión en grises, y además incorpore gráficos embebidos para estudiar la distribución de intensidades.

En este caso, la interfaz permite analizar:

  1. La imagen original en color.
  2. La imagen convertida a escala de grises.
  3. El histograma de intensidades en grises.
  4. Los histogramas por canal de color.
  5. El histograma acumulado.
  6. Un pequeño resumen estadístico de la imagen.

Este enfoque integra visualización, análisis numérico y gráficos en una sola ventana, lo que resulta especialmente útil para comprender el carácter global de un histograma.

Aplicación gráfica para análisis de histogramas e intensidades

15.22 Aplicación completa en código

import tkinter as tk
from tkinter import ttk, messagebox
import cv2
import numpy as np
from PIL import Image, ImageTk

from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


class AplicacionHistogramas:
    def __init__(self, root):
        self.root = root
        self.root.title("Histogramas y análisis de intensidades")
        self.root.geometry("1450x900")
        self.root.configure(bg="#f4f6f8")

        self.ruta_imagen = "foto1.jpg"

        self.imagen_bgr = None
        self.imagen_rgb = None
        self.imagen_gris = None

        self.tk_original = None
        self.tk_gris = None

        self.crear_interfaz()
        self.cargar_imagen()

    # ---------------------------------------------------------
    # INTERFAZ
    # ---------------------------------------------------------
    def crear_interfaz(self):
        estilo = ttk.Style()
        estilo.theme_use("clam")

        estilo.configure("Titulo.TLabel", font=("Arial", 18, "bold"))
        estilo.configure("Subtitulo.TLabel", font=("Arial", 11, "bold"))
        estilo.configure("Info.TLabel", font=("Arial", 10))
        estilo.configure("Card.TFrame", background="white")
        estilo.configure("CardTitle.TLabel", background="white", font=("Arial", 11, "bold"))
        estilo.configure("CardText.TLabel", background="white", font=("Arial", 10))

        contenedor = ttk.Frame(self.root, padding=12)
        contenedor.pack(fill="both", expand=True)

        titulo = ttk.Label(
            contenedor,
            text="Histogramas y análisis global de intensidades",
            style="Titulo.TLabel"
        )
        titulo.pack(anchor="center", pady=(0, 10))

        subtitulo = ttk.Label(
            contenedor,
            text="Visualización de foto1.jpg, escala de grises e histogramas",
            style="Info.TLabel"
        )
        subtitulo.pack(anchor="center", pady=(0, 12))

        cuerpo = ttk.Frame(contenedor)
        cuerpo.pack(fill="both", expand=True)

        # Panel izquierdo: imágenes y datos
        self.panel_izquierdo = ttk.Frame(cuerpo)
        self.panel_izquierdo.pack(side="left", fill="y", padx=(0, 10))

        # Panel derecho: gráficos
        self.panel_derecho = ttk.Frame(cuerpo)
        self.panel_derecho.pack(side="left", fill="both", expand=True)

        self.crear_panel_imagenes()
        self.crear_panel_graficos()

    def crear_panel_imagenes(self):
        marco_original = ttk.Frame(self.panel_izquierdo, style="Card.TFrame", padding=10)
        marco_original.pack(fill="x", pady=(0, 10))

        ttk.Label(marco_original, text="Imagen original", style="CardTitle.TLabel").pack(anchor="center", pady=(0, 8))
        self.label_original = ttk.Label(marco_original, background="white")
        self.label_original.pack()

        marco_gris = ttk.Frame(self.panel_izquierdo, style="Card.TFrame", padding=10)
        marco_gris.pack(fill="x", pady=(0, 10))

        ttk.Label(marco_gris, text="Imagen en escala de grises", style="CardTitle.TLabel").pack(anchor="center", pady=(0, 8))
        self.label_gris = ttk.Label(marco_gris, background="white")
        self.label_gris.pack()

        marco_info = ttk.Frame(self.panel_izquierdo, style="Card.TFrame", padding=10)
        marco_info.pack(fill="x", pady=(0, 10))

        ttk.Label(marco_info, text="Resumen estadístico", style="CardTitle.TLabel").pack(anchor="w", pady=(0, 8))

        self.label_info = ttk.Label(
            marco_info,
            text="Cargando imagen...",
            style="CardText.TLabel",
            justify="left"
        )
        self.label_info.pack(anchor="w")

    def crear_panel_graficos(self):
        notebook = ttk.Notebook(self.panel_derecho)
        notebook.pack(fill="both", expand=True)

        self.tab_gris = ttk.Frame(notebook)
        self.tab_color = ttk.Frame(notebook)
        self.tab_acumulado = ttk.Frame(notebook)

        notebook.add(self.tab_gris, text="Histograma en grises")
        notebook.add(self.tab_color, text="Histogramas por canal")
        notebook.add(self.tab_acumulado, text="Histograma acumulado")

        self.crear_figura_gris()
        self.crear_figura_color()
        self.crear_figura_acumulada()

    # ---------------------------------------------------------
    # CARGA DE IMAGEN
    # ---------------------------------------------------------
    def cargar_imagen(self):
        self.imagen_bgr = cv2.imread(self.ruta_imagen)

        if self.imagen_bgr is None:
            messagebox.showerror(
                "Error",
                "No se pudo cargar foto1.jpg\n\nVerifica que esté en la misma carpeta del programa."
            )
            return

        self.imagen_rgb = cv2.cvtColor(self.imagen_bgr, cv2.COLOR_BGR2RGB)
        self.imagen_gris = cv2.cvtColor(self.imagen_bgr, cv2.COLOR_BGR2GRAY)

        self.mostrar_imagenes()
        self.actualizar_estadisticas()
        self.dibujar_histograma_gris()
        self.dibujar_histogramas_color()
        self.dibujar_histograma_acumulado()

    # ---------------------------------------------------------
    # MOSTRAR IMÁGENES
    # ---------------------------------------------------------
    def convertir_a_tk(self, imagen, ancho_max=420, alto_max=260, es_gris=False):
        if es_gris:
            imagen_pil = Image.fromarray(imagen)
            imagen_pil = imagen_pil.convert("L")
        else:
            imagen_pil = Image.fromarray(imagen)

        ancho, alto = imagen_pil.size
        escala = min(ancho_max / ancho, alto_max / alto)
        nuevo_ancho = max(1, int(ancho * escala))
        nuevo_alto = max(1, int(alto * escala))

        imagen_pil = imagen_pil.resize((nuevo_ancho, nuevo_alto), Image.Resampling.LANCZOS)
        return ImageTk.PhotoImage(imagen_pil)

    def mostrar_imagenes(self):
        self.tk_original = self.convertir_a_tk(self.imagen_rgb, es_gris=False)
        self.label_original.configure(image=self.tk_original)

        self.tk_gris = self.convertir_a_tk(self.imagen_gris, es_gris=True)
        self.label_gris.configure(image=self.tk_gris)

    # ---------------------------------------------------------
    # ESTADÍSTICAS
    # ---------------------------------------------------------
    def actualizar_estadisticas(self):
        gris = self.imagen_gris

        minimo = int(np.min(gris))
        maximo = int(np.max(gris))
        promedio = float(np.mean(gris))
        desvio = float(np.std(gris))

        hist = cv2.calcHist([gris], [0], None, [256], [0, 256]).flatten()
        intensidad_mas_frecuente = int(np.argmax(hist))

        texto = (
            f"Dimensiones: {gris.shape[1]} x {gris.shape[0]}\n"
            f"Intensidad mínima: {minimo}\n"
            f"Intensidad máxima: {maximo}\n"
            f"Promedio: {promedio:.2f}\n"
            f"Desvío estándar: {desvio:.2f}\n"
            f"Nivel más frecuente: {intensidad_mas_frecuente}"
        )

        self.label_info.configure(text=texto)

    # ---------------------------------------------------------
    # FIGURAS MATPLOTLIB
    # ---------------------------------------------------------
    def crear_figura_gris(self):
        self.fig_gris = Figure(figsize=(8, 5), dpi=100)
        self.ax_gris = self.fig_gris.add_subplot(111)
        self.fig_gris.tight_layout(pad=3.0)

        self.canvas_gris = FigureCanvasTkAgg(self.fig_gris, master=self.tab_gris)
        self.canvas_gris.get_tk_widget().pack(fill="both", expand=True)

    def crear_figura_color(self):
        self.fig_color = Figure(figsize=(8, 5), dpi=100)
        self.ax_color = self.fig_color.add_subplot(111)
        self.fig_color.tight_layout(pad=3.0)

        self.canvas_color = FigureCanvasTkAgg(self.fig_color, master=self.tab_color)
        self.canvas_color.get_tk_widget().pack(fill="both", expand=True)

    def crear_figura_acumulada(self):
        self.fig_acum = Figure(figsize=(8, 5), dpi=100)
        self.ax_acum = self.fig_acum.add_subplot(111)
        self.fig_acum.tight_layout(pad=3.0)

        self.canvas_acum = FigureCanvasTkAgg(self.fig_acum, master=self.tab_acumulado)
        self.canvas_acum.get_tk_widget().pack(fill="both", expand=True)

    # ---------------------------------------------------------
    # DIBUJO DE HISTOGRAMAS
    # ---------------------------------------------------------
    def dibujar_histograma_gris(self):
        self.ax_gris.clear()

        hist = cv2.calcHist([self.imagen_gris], [0], None, [256], [0, 256]).flatten()

        self.ax_gris.bar(range(256), hist, width=1.0)
        self.ax_gris.set_title("Histograma de intensidades en escala de grises", fontsize=13, fontweight="bold")
        self.ax_gris.set_xlabel("Intensidad")
        self.ax_gris.set_ylabel("Cantidad de píxeles")
        self.ax_gris.set_xlim(0, 255)
        self.ax_gris.grid(True, alpha=0.25)

        self.canvas_gris.draw()

    def dibujar_histogramas_color(self):
        self.ax_color.clear()

        hist_b = cv2.calcHist([self.imagen_bgr], [0], None, [256], [0, 256]).flatten()
        hist_g = cv2.calcHist([self.imagen_bgr], [1], None, [256], [0, 256]).flatten()
        hist_r = cv2.calcHist([self.imagen_bgr], [2], None, [256], [0, 256]).flatten()

        self.ax_color.plot(hist_b, color="blue", linewidth=2, label="Canal azul")
        self.ax_color.plot(hist_g, color="green", linewidth=2, label="Canal verde")
        self.ax_color.plot(hist_r, color="red", linewidth=2, label="Canal rojo")

        self.ax_color.set_title("Histogramas por canal de color", fontsize=13, fontweight="bold")
        self.ax_color.set_xlabel("Intensidad")
        self.ax_color.set_ylabel("Cantidad de píxeles")
        self.ax_color.set_xlim(0, 255)
        self.ax_color.grid(True, alpha=0.25)
        self.ax_color.legend()

        self.canvas_color.draw()

    def dibujar_histograma_acumulado(self):
        self.ax_acum.clear()

        hist = cv2.calcHist([self.imagen_gris], [0], None, [256], [0, 256]).flatten()
        hist_acum = np.cumsum(hist)

        self.ax_acum.plot(hist_acum, color="black", linewidth=2.5)
        self.ax_acum.set_title("Histograma acumulado", fontsize=13, fontweight="bold")
        self.ax_acum.set_xlabel("Intensidad")
        self.ax_acum.set_ylabel("Cantidad acumulada de píxeles")
        self.ax_acum.set_xlim(0, 255)
        self.ax_acum.grid(True, alpha=0.25)

        self.canvas_acum.draw()


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

Esta aplicación permite analizar foto1.jpg desde una única ventana, combinando imágenes, estadísticas descriptivas e histogramas embebidos para escala de grises, canales de color y distribución acumulada.

15.23 Uso práctico en documentos

En documentos, el análisis de intensidades resulta útil para:

  • Mejorar contraste entre fondo y texto.
  • Preparar la imagen para OCR.
  • Corregir capturas con mala iluminación.
  • Elegir una estrategia de umbralización.

En este tipo de casos, CLAHE y Otsu son recursos particularmente valiosos.

15.24 Uso práctico en medicina e industria

En imágenes médicas o industriales, la distribución de intensidades puede dar información muy importante sobre calidad de captura y visibilidad de estructuras.

Por ejemplo:

  • Una imagen demasiado comprimida tonalmente puede ocultar detalles.
  • Una ecualización adecuada puede resaltar regiones sutiles.
  • El análisis del rango dinámico puede ayudar a validar datos de entrada.

15.25 Errores comunes

Al trabajar con histogramas y análisis de intensidades, algunos errores frecuentes son:

  • Interpretar el histograma como si describiera la geometría de la imagen.
  • Aplicar ecualización sin evaluar si realmente mejora el problema.
  • Comparar histogramas de imágenes de distinto tamaño sin normalizar.
  • Ignorar que en color conviene analizar canales por separado cuando sea necesario.
  • Asumir que más contraste siempre implica una mejor imagen para el algoritmo.

La mejora visual percibida por un humano no siempre coincide con la representación más útil para un sistema automático.

15.26 Qué debes recordar de este tema

  • El histograma resume la distribución de intensidades de una imagen.
  • Permite analizar brillo general, contraste y rango dinámico.
  • En imágenes color se pueden calcular histogramas por canal.
  • Las máscaras permiten analizar regiones específicas.
  • La ecualización y CLAHE ayudan a mejorar contraste en ciertos contextos.
  • Otsu usa el histograma para elegir automáticamente un umbral de segmentación.
  • El histograma es una herramienta global útil, pero no conserva información espacial.

15.27 Conclusión

Los histogramas y el análisis de intensidades ofrecen una forma muy poderosa de resumir la información visual de una imagen. Nos permiten entender su distribución tonal, evaluar contraste y decidir estrategias de mejora o segmentación.

Con este tema cerramos una primera etapa sólida de procesamiento clásico: ya vimos representación digital, espacios de color, OpenCV, operaciones matriciales, transformaciones, filtrado, bordes e histogramas.

En el próximo tema comenzaremos la transición hacia el aprendizaje profundo con una introducción a las redes convolucionales (CNN), el puente natural entre visión por computadora clásica y modelos modernos.