Este capítulo presenta dos temas fundamentales del Procesamiento Digital de Imágenes (PDI): la morfología matemática y la segmentación de imágenes. La morfología matemática proporciona un marco teórico basado en la teoría de conjuntos para analizar, refinar y cuantificar la forma de objetos en imágenes binarias y en tonos de gris, mediante operadores fundamentales como la erosión y la dilatación. La segmentación, por su parte, tiene como objetivo particionar la imagen en regiones de interés, separando objetos del fondo y produciendo representaciones adecuadas para el análisis y la interpretación.
El capítulo comienza con la umbralización, una de las técnicas más importantes de segmentación, introduciendo el método automático de Otsu y revisitando el análisis de histogramas mediante la varianza interclases, presentada en el Capítulo 1. A continuación, se estudian los principales operadores de la morfología matemática, incluyendo erosión, dilatación, apertura, cierre y reconstrucción morfológica, que permiten refinar máscaras binarias y preservar estructuras relevantes de los objetos. Finalmente, se presentan técnicas de segmentación basada en regiones, como el etiquetado de componentes conexos, la transformada de distancia y el algoritmo watershed basado en marcadores, culminando en la extracción de descriptores geométricos y en la generación de bounding boxes compatibles con sistemas modernos de detección de objetos.
4.1 Objetivos
Al final de este capítulo, usted será capaz de:
Aplicar umbralización: Comprender el criterio automático de Otsu por maximización de la varianza interclases (\(\sigma_B^2\)) y seleccionar estrategias adecuadas de preprocesamiento para facilitar la segmentación;
Dominar la morfología binaria: Comprender y aplicar erosión (\(A\ominus B\)) y dilatación (\(A\oplus B\)) como operadores fundamentales, derivando apertura (\(A\circ B\)), cierre (\(A\bullet B\)) y operaciones basadas en reconstrucción morfológica, como mm.clohole y mm.edgeoff;
Aplicar morfología en tonos de gris: Utilizar gradiente morfológico y filtros top-hat para realce y análisis de estructuras locales;
Etiquetar componentes conexos: Identificar y separar regiones conectadas en imágenes binarias mediante algoritmos de etiquetado;
Aplicar transformada de distancia: Interpretar y calcular distancias al fondo utilizando enfoques morfológicos y métricas geométricas;
Segmentar por regiones: Construir pipelines de segmentación basados en marcadores utilizando Transformada de Distancia y el algoritmo watershed;
Extraer descriptores geométricos: Calcular propiedades como área, perímetro, centroide, circularidad y bounding boxes mediante mm.label0 y extracción de contornos;
Relacionar PDI y visión computacional: Comprender cómo los descriptores extraídos por segmentación pueden convertirse a formatos utilizados por detectores modernos, como YOLO.
La umbralización (thresholding) es una de las formas más simples y eficientes de segmentación de imágenes. Su objetivo es clasificar cada píxel en dos clases de intensidad, normalmente asociadas a objeto y fondo:
\[
g(x,y) =
\begin{cases}
255, & \text{si } f(x,y) > T \\
0, & \text{en caso contrario}
\end{cases}
\tag{4.1}\]
donde \(f(x,y)\) representa la intensidad del píxel en la imagen original y \(g(x,y)\) la imagen binaria resultante.
La elección del umbral \(T\) es importante para la calidad de la segmentación. El método de Otsu determina automáticamente el umbral óptimo al maximizar la varianza entre clases\(\sigma_B^2\) definida por:
\(w_0(T)\) y \(w_1(T)\) son las probabilidades acumuladas de las clases fondo y objeto;
\(\mu_0(T)\) y \(\mu_1(T)\) son las medias de intensidad de esas clases;
\(\sigma_B^2(T)\) representa la varianza entre clases para un umbral dado \(T\).
El método funciona mejor cuando el histograma presenta dos grupos de intensidades relativamente separados. Para ello, el algoritmo evalúa todos los umbrales posibles de la imagen — típicamente en el intervalo \([0,255]\) para imágenes de 8 bits — y selecciona el valor que maximiza la varianza entre clases, denotada por \(\sigma_B^2\):
El método de Otsu produce mejores resultados cuando el histograma presenta dos picos bien definidos (bimodalidad), correspondientes al fondo y al objeto. Cuanto mayor sea la separación entre estos picos y más pronunciado sea el máximo de \(\sigma_B^2\), más confiable tiende a ser el umbral obtenido.
En imágenes con iluminación no uniforme o múltiples regiones de intensidad, las técnicas de umbralización adaptativa — en las cuales el umbral se calcula localmente — suelen producir segmentaciones más robustas.
El subíndice \(B\) en \(\sigma_B^2\) significa between classes (entre clases). Así, \(\sigma_B^2\) representa la varianza entre las clases (between-class variance).
4.2.1 Imagen de Monedas
La imagen utilizada para practicar la segmentación es una fotografía de una colección de monedas de diferentes países y épocas (Figura 4.1). Crédito: GAZI.MD.AHAD (CC BY-SA 4.0). Presenta objetos circulares con bordes bien definidos, siendo ideal para demostrar umbralización, operadores morfológicos, transformada de distancia, watershed y descriptores de forma.
Figura 4.1: Imagen con monedas de varios tipos. Crédito: GAZI.MD.AHAD (CC BY-SA 4.0).
4.2.2 Preprocesamiento para el Otsu
La calidad del método de Otsu depende directamente de cuán bimodal es el histograma de la imagen de entrada. La imagen original de las monedas presenta iluminación no uniforme y monedas oscuras (cobre oxidado) con intensidades cercanas a las del fondo oscuro, lo que hace que el histograma sea poco bimodal.
Para minimizar estas limitaciones, se evaluarán dos técnicas clásicas de preprocesamiento, presentadas en el capítulo anterior, aplicadas antes de la etapa de umbralización. La Tabla 4.1 resume las características de cada enfoque. Estas técnicas buscan aumentar la separación entre objeto y fondo, haciendo que el histograma se aproxime más a una distribución bimodal.
Tabla 4.1: Técnicas de preprocesamiento evaluadas para mejorar la separación entre monedas y fondo antes de la aplicación del método de Otsu.
Técnica
Qué hace
Cuándo usar
CLAHE
Ecualización de histograma adaptativa local
Bajo contraste global o regional
Gaussiano
Suavizado por convolución con gaussiana
Ruido de alta frecuencia (textura del fondo)
La función cv2.createCLAHE(clipLimit, tileGridSize) divide la imagen en bloques y aplica ecualización de histograma en cada uno de ellos, limitando la amplificación del ruido mediante el parámetro clipLimit. Por su parte, el filtro Gaussiano (cv2.GaussianBlur) suaviza texturas finas que podrían crear picos falsos en el histograma.
Para comparar objetivamente qué preprocesamiento produce la mejor entrada para el método de Otsu, se presentarán, para cada versión de la imagen, la propia imagen, el histograma con el umbral óptimo \(T^*\) destacado, la curva \(\sigma_B^2(T)\) y el resultado de la binarización.
NotaCriterio de comparación
La versión que presente el mayor valor de \(\sigma_B^2\) en su pico proporciona la mejor separación entre las clases de fondo y objeto y, en consecuencia, la mejor entrada para el método de Otsu (Figura 4.2).
import cv2, ioimport matplotlib.pyplot as pltdef otsu_criterio(img): h=mm.hist(img); p=h/h.sum();# histograma y probabilidades sigma2=np.zeros(len(p)) for T inrange(1,len(p)): # recorre umbrales w0,w1=p[:T].sum(),p[T:].sum() # probabilidades de las clasesif w0*w1==0: continue# evita división por cero mu0=(np.arange(T)*p[:T]).sum()/w0 # media fondo mu1=(np.arange(T,len(p))*p[T:]).sum()/w1 # media objeto sigma2[T]=w0*w1*(mu0-mu1)**2# σ²B(T)return sigma2,np.argmax(sigma2) # curva y T óptimodef fig2img(fig): b=io.BytesIO(); fig.savefig(b,format='png',dpi=100) # figura → buffer plt.close(fig); b.seek(0)return np.array(plt.imread(b)) # buffer → arraydef plot_curve(y,T,title,ylabel,color): fig,ax=plt.subplots(figsize=(4,3)) ax.plot(y,color=color) if ylabel=="σ²B"else ax.bar(range(len(y)),y,color=color,width=1) ax.axvline(T,color='red',lw=2,label=f"T*={T}") # umbral óptimo ax.set(xlabel="T"if ylabel=="σ²B"else"Intensidade",ylabel=ylabel) ax.legend(fontsize=8); plt.tight_layout()return fig2img(fig)# ── Preprocesamientos ───────────────────────────────────────────────────────img_clahe = mm.clahe(img_coins_gray, clipLimit=2.0, tiles=8)img_gauss = cv2.GaussianBlur(img_clahe,(5,5),0)imgs0=[("Original",img_coins_gray), ("CLAHE",img_clahe), ("CLAHE+Gauss",img_gauss)]# ── Tabla comparativa ───────────────────────────────────────────────────────print(f"{'Versão':<18}{'T*':>6}{'σ²B pico':>14}")print("-"*40)imgs,titles=[],[]for nome,img in imgs0: sigma2,T=otsu_criterio(img) # calcula σ²B(T)print(f"{nome:<18}{T:>6}{sigma2[T]:>14.4e}") imgs += [ img, # imagen plot_curve(mm.hist(img),T,f"Hist T*={T}","Freq.","steelblue"), plot_curve(sigma2,T,"σ²B(T)","σ²B","darkorange"), mm.threshold(img) # Otsu final ] titles += [nome,f"Hist T*={T}","σ²B(T)",f"Otsu T*={T}"]# ── Exhibición final ───────────────────────────────────────────────────────────mm.show(imgs,titles=titles,cols=4,figsize=(12,12),dpi=200)
Versão T* σ²B pico
----------------------------------------
Original 105 1.9437e+03
CLAHE 122 2.4695e+03
CLAHE+Gauss 123 2.4283e+03
Figura 4.2: Comparación de los preprocesamientos: imagen | histograma+T* | σ²B(T) | Otsu. La mejor separación bimodal indica el umbral más confiable.
4.2.3 Resultado: CLAHE como Mejor Preprocesamiento
El análisis de la Figura 4.2 indica que el CLAHE obtuvo el mayor valor de la varianza entre clases (\(\sigma_B^2 \approx 2{,}47 \times 10^3\)), con umbral óptimo \(T^* = 122\). Aunque la combinación CLAHE+Gaussiano haya producido un resultado muy similar (\(\sigma_B^2 \approx 2{,}43 \times 10^3\), \(T^* = 123\)), el criterio cuantitativo del método de Otsu favorece ligeramente el uso del CLAHE de forma aislada.
En términos visuales, las imágenes binarizadas obtenidas con CLAHE y CLAHE+Gaussiano son prácticamente equivalentes. La diferencia entre ambos enfoques se hace más evidente en el análisis de los histogramas y de los valores de \(\sigma_B^2(T)\) que en la inspección directa de las segmentaciones resultantes. Así, la elección del CLAHE se basa principalmente en la maximización de la separación estadística entre las clases de fondo y objeto.
TipInterpretación de los resultados
Observe que los preprocesamientos con CLAHE y CLAHE+Gaussiano producen histogramas y umbrales óptimos muy próximos (\(T^*=122\) y \(T^*=123\)). En consecuencia, las imágenes binarizadas resultantes también son bastante similares. En este caso, la decisión no se basa en diferencias visuales marcadas, sino en el criterio objetivo del método de Otsu: el mayor valor de \(\sigma_B^2\) indica la mejor separación entre las clases.
4.3 Morfología Matemática
La morfología matemática es una teoría basada en conjuntos utilizada para analizar la forma y la estructura de objetos en imágenes. A diferencia de los filtros lineales presentados en el Capítulo 3, los operadores morfológicos son no lineales, pues se basan en operaciones de mínimo, máximo e inclusión espacial, en lugar de combinaciones lineales de intensidades. Estos operadores actúan sobre la vecindad de cada píxel mediante un elemento estructurante\(\mathbb{B}\), responsable de definir la forma y el tamaño de la región analizada.
En imágenes binarias y en tonos de gris con elementos planos, el elemento estructurante trasladado a la posición \(x\) se define espacialmente como:
\[
\mathbb{B}_x = \{ x + b \mid b \in \mathbb{B} \}
\]
En las regiones de borde de la imagen, parte del conjunto \(\mathbb{B}_x\) puede extrapolar el dominio físico de la escena (\(\mathbb{E}\)). Para garantizar la consistencia matemática de los operadores primitivos en esas fronteras, se asume teóricamente que el espacio exterior al dominio de la imagen se rellena con el elemento neutro de la operación correspondiente (infinito positivo para la erosión e infinito negativo para la dilatación), impidiendo que el entorno externo corrompa las estructuras internas del objeto.
Cuando el elemento estructurante asocia pesos a sus elementos — es decir, \(b: \mathbb{B} \to \mathbb{Z}\) — se denomina función estructurante o elemento estructurante no plano.
Desarrollada por Matheron y Serra en la década de 1960 para imágenes binarias y posteriormente extendida a tonos de gris, la morfología matemática fundamenta operadores como el gradiente morfológico, el top-hat, el watershed y la transformada de distancia, todos derivados de dos primitivos: la erosión y la dilatación [Matheron (1975); Serra (1982)].
4.3.1 Erosión y Dilatación
Los dos operadores primitivos se definen de manera unificada para imágenes en tonos de gris (\(f: \mathbb{E} \to \mathbb{Z}\)) y, por restricción al dominio \(\{0,1\}\), también para imágenes binarias.
4.3.1.1 Erosión
La Erosión de una imagen \(f\) por una función estructurante \(b: \mathbb{B} \to \mathbb{Z}\) se define formalmente por:
En la práctica, la erosión sustituye la intensidad del píxel \(x\) por el valor mínimo resultante de la diferencia entre la imagen y el elemento estructurante en la vecindad definida por el dominio \(\mathbb{B}\). Los valores positivos en los pesos de \(b(z)\) fuerzan el resultado local hacia abajo, “excavando” el relieve de la imagen más profundamente e intensificando la erosión.
En el caso plano (donde los pesos son nulos dentro del dominio, es decir, \(b \equiv 0\)), la expresión se simplifica al mínimo local puro:
En imágenes binarias, esta operación equivale a exigir que el conjunto \(\mathbb{B}\), trasladado a la coordenada \(x\), esté completamente contenido en el objeto \(A\):
\[
A \ominus \mathbb{B} = \{\, z \in \mathbb{E} \mid \mathbb{B}_z \subseteq A \,\}
\]
Efecto Visual:Encoge objetos y estructuras claras, eliminando protuberancias, picos brillantes o ruidos que sean geométricamente menores que el dominio \(\mathbb{B}\).
4.3.1.2 Implementación de la erosión
La versión didáctica mm.ero0 implementa el caso particular de erosión con elemento estructurante plano. Para cada píxel \((y,x)\), la función recorre los vecinos espaciales permitidos por \(B\) y almacena el menor valor encontrado en la imagen de entrada \(f\), reproduciendo directamente la operación de mínimo local descrita en la Ecuación 4.3 para \(b \equiv 0\).
Observe que los valores de los vecinos siempre se leen de forma estática de la imagen original \(f\); la matriz de salida \(g\) se utiliza exclusivamente para registrar el mínimo acumulado de la vecindad actual. De esta forma, el resultado final es invariante respecto al orden de barrido de los píxeles (ya sea por filas o columnas).
La función auxiliar _viz calcula las coordenadas de los vecinos válidos dentro de los límites físicos de la imagen. En los bordes, la inicialización del acumulador en 255 emula con exactitud el relleno por elemento neutro exigido por la teoría. Por su parte, la función de interfaz mm.ero recurre a la implementación nativa y optimizada de OpenCV (mm.ero) cuando el elemento estructurante es plano, cambiando a la rutina general mm.ero1 en caso de que el elemento posea pesos topográficos.
El ejemplo computacional siguiente ilustra la aplicación de un elemento estructurante en cruz (mm.secross()) destacado en la Figura 4.3, comparando la ejecución de la variante didáctica en bucle (mm.ero0) con el motor computacional de OpenCV (mm.ero).
La función _viz utiliza yield para generar cada vecino bajo demanda, sin almacenar todos los resultados en memoria en una lista. En el experimento siguiente, una ventana de 3000×3000 produce 9 millones de vecinos. La implementación basada en lista consumió más de 1 GB de RAM y tomó aproximadamente 46 s, mientras que la versión con yield consumió memoria despreciable y se ejecutó en 38 s. En procesamiento de imágenes, los generadores son especialmente útiles para recorrer grandes vecindarios de manera eficiente.
import tracemallocdef lista(n): return [(i, i) for i inrange(n)]def gera(n):for i inrange(n): yield i, ifor nome, f in [("LISTA", lista), ("YIELD", gera)]: tracemalloc.start()sum(x+y for x,y in f(9_000_000)) _, pico = tracemalloc.get_traced_memory() tracemalloc.stop()print(f"{nome}: {pico/1024/1024:.1f} MB")
LISTA: 830.8 MB
YIELD: 0.0 MB
4.3.1.3 Dilatación
La Dilatación de una imagen \(f\) mediante una función estructurante \(b: \mathbb{B} \to \mathbb{Z}\) se define formalmente como:
En la práctica, la dilatación reemplaza la intensidad del píxel \(x\) por el mayor valor resultante de la suma entre la imagen y el elemento estructurante en la vecindad definida. El argumento de inversión espacial (\(x - z\)) indica que la dilatación evalúa implícitamente el elemento transpuesto (reflejado) \(\hat{b}\), propiedad fundamental para asegurar la dualidad matemática respecto a la erosión.
En el caso plano (donde los pesos son nulos dentro del dominio, es decir, \(b \equiv 0\)), la expresión se reduce al máximo local puro:
En imágenes binarias, esta operación equivale a exigir que el conjunto reflejado \(\hat{\mathbb{B}}\), trasladado a la coordenada \(x\), tenga una intersección no vacía con el objeto \(A\):
\[
A \oplus \mathbb{B} = \{\, z \in \mathbb{E} \mid \hat{\mathbb{B}}_z \cap A \neq \varnothing \,\}
\]
Efecto Visual:Expande las estructuras claras de la imagen, aumentando el relleno de objetos, conectando componentes cercanos y eliminando canales, fosas oscuras o valles que sean geométricamente menores que el dominio \(\mathbb{B}\).
4.3.1.4 Implementación de la dilatación
La versión didáctica mm.dil0 implementa el caso particular de dilatación con elemento estructurante plano. Para cada píxel \((y,x)\), la función recorre los vecinos espaciales permitidos por \(B\) y almacena el mayor valor encontrado en la imagen de entrada \(f\), reproduciendo directamente la operación de máximo local para \(b \equiv 0\).
Antes de iniciar el barrido espacial, el elemento estructurante sufre una reflexión geométrica mediante la instrucción np.flip(Bc) para construir explícitamente la matriz transpuesta \(\hat{B}\) exigida por la teoría. En máscaras perfectamente simétricas (como cruces, cuadrados y discos centrados en el origen), esta reflexión no altera la disposición de los píxeles; no obstante, para elementos asimétricos, tal etapa es estrictamente necesaria para garantizar la equivalencia con las definiciones formales y salvaguardar las leyes de dualidad.
Así como se verificó en el operador de erosión, los valores de los vecinos siempre se leen de forma estática a partir de la matriz original \(f\), mientras que la matriz de salida \(g\) actúa puramente como el registrador del máximo acumulado de la vecindad. En las fronteras de la imagen, la inicialización del acumulador en 0 emula con exactitud el relleno externo por elemento neutro (\(-\infty\), o cero en representaciones de 8 bits), garantizando que los bordes físicos de la escena sean dilatados en perfecta conformidad con el estándar adoptado por OpenCV.
El ejemplo computacional siguiente ilustra la aplicación práctica de un elemento en cruz (mm.secross()), validando la consistencia entre la lógica en bucles (mm.dil0) y el método nativo industrial (mm.dil).
def dil(f, Bc=np.zeros((3,3),dtype='uint8')):"""Dilatación (OpenCV o con pesos)."""try: return cv2.dilate(f, Bc)except: return mm.dil1(f, Bc)def dil0(f, Bc=np.zeros((3,3),dtype='uint8')):"""Dilatación plana siguiendo rigurosamente la teoría.""" g = np.empty_like(f) Bc = np.flip(Bc) # reflexión explícita: B̂for y inrange(f.shape[0]):for x inrange(f.shape[1]): g[y,x] =0# Inicializa con el valor mínimo para buscar el máximofor vy,vx,bv in mm._viz(f,Bc,y,x):if bv and g[y,x] < f[vy,vx]: g[y,x] = f[vy,vx]return g# Script de Prueba y ValidaciónB = mm.secross()print("Elemento estructurante B:")print(mm.drawImage(B))f = mm.randomImage(5,5)print("Imagen original f:")print(mm.drawImage(f))print("Dilatación Plana Didáctica (dil0):")print(mm.drawImage(dil0(f, B)))print("Dilatación Optimizada OpenCV (dil):")print(mm.drawImage(dil(f, B)))
NotaNota: El Enfrentamiento de los Signos (\(f(x+z)\) vs \(f(x-z)\))
Compare las definiciones formales de la erosión (Ecuación 4.3) y de la dilatación (Ecuación 4.4). Considere un elemento estructurante asimétrico a la derecha \(\mathbb{B}=\{0,1\}\) (origen y un píxel a la derecha) aplicado en la posición \(x=10\).
Debido al signo negativo (\(-z\)), avanzar en el elemento estructurante corresponde a retroceder en la imagen, haciendo que la dilatación consulte el píxel a la izquierda (\(9\)).
La función _viz, utilizada en morph.py, genera vecinos mediante desplazamientos aditivos de la forma \(x+z\). Por ese motivo, la implementación de mm.dil0 refleja previamente el elemento estructurante mediante np.flip(B). Tras la reflexión, el barrido basado en \(x+z\) pasa a acceder exactamente a los mismos puntos definidos por la expresión teórica \(f(x-z)\) de la dilatación en Ecuación 4.4.
Para elementos estructurantes simétricos (como discos, cuadrados y cruces centradas), la reflexión no altera la máscara. En cambio, para elementos asimétricos, esta etapa es indispensable para que la implementación reproduzca correctamente la definición matemática de la dilatación y preserve la dualidad erosión–dilatación.
NotaDualidad erosión–dilatación
La erosión y la dilatación son duales por complemento. Esto significa que un operador puede obtenerse completamente a partir del otro, siempre que se actúe sobre el complemento de la imagen utilizando el elemento estructurante reflejado \(\hat{B}\):
\[
(A \ominus B)^c = A^c \oplus \hat{B} \quad \Longleftrightarrow \quad A \ominus B = (A^c \oplus \hat{B})^c
\]
De manera análoga, la dilatación también puede obtenerse a partir de la erosión:
\[
(A \oplus B)^c = A^c \ominus \hat{B} \quad \Longleftrightarrow \quad A \oplus B = (A^c \ominus \hat{B})^c
\]
En términos prácticos, la erosión de un objeto puede obtenerse mediante la dilatación de su complemento, seguida de la complementación del resultado (y viceversa). En la implementación del paquete morph.py, las versiones didácticas mm.ero0 y mm.dil0 hacen explícita esta estructura mediante bucles (loops), mientras que mm.ero y mm.dil delegan las operaciones a OpenCV buscando una mayor eficiencia computacional.
NotaCondiciones de contorno e imágenes finitas
En la morfología matemática clásica, definida sobre un dominio infinito (típicamente \(\mathbb{Z}^2\)), esta dualidad es exacta. En imágenes digitales, sin embargo, se trabaja con matrices finitas, y el resultado pasa a depender de la forma en que se tratan los píxeles ubicados fuera de la imagen.
Para que las identidades de dualidad permanezcan válidas, el complemento debe definirse con respecto al mismo universo y las condiciones de contorno adoptadas para la erosión y para la dilatación deben ser complementarias entre sí. Por ejemplo, si la erosión asume que los píxeles externos pertenecen al objeto (\(255\)), entonces la dilatación aplicada al complemento debe asumir que esos mismos píxeles externos pertenecen al fondo (\(0\)).
Cuando se utilizan diferentes estrategias de relleno (replicación, reflexión, valor constante, etc.), la dualidad teórica puede dejar de satisfacerse exactamente en las regiones cercanas a los bordes de la imagen.
Para ilustrar numéricamente los operadores morfológicos y la dualidad erosión–dilatación, la Figura 4.4 presenta una imagen binaria de 10×10 procesada con un elemento estructurante en forma de “L”. En la implementación de morph.py, el origen de \(B\) se fija en el centro geométrico de la máscara —posición \((1,1)\) para un kernel de 3×3— y debe corresponder a un elemento activo para que la erosión se comporte correctamente (según lo discutido anteriormente). El elemento estructurante \(B_L\) definido a continuación satisface esa condición. La Figura 4.5 complementa el análisis con un simulador interactivo de la erosión, permitiendo visualizar el desplazamiento del elemento estructurante sobre la imagen e identificar las posiciones en las que este permanece completamente contenido en el objeto.
Figura 4.4: Erosión y dilatación en imagen binaria 10×10 con elemento estructurante ‘L’ asimétrico 3×3. Validación de la dualidad erosión–dilatación.
🪨 Simulador: Erosión MorfológicaA ⊖ B_L · offsets via _viz
Haz clic en una celda del lienzo para mover el kernel B_L o usa los controles deslizantes para probar la inclusión de contenidos.
Posición X (col)
4
Posición Y (fila)
4
Píxel Erosión
255
🖱️ Haz clic en una celda para mover el kernel B_L
🟢
Éxito: ¡contenido!
El píxel recibe 1 (255) en la imagen erosionada.
Controles de Coordenada
4
4
Kernel B_L (3×3)
1
0
0
1
★
0
1
1
0
Figura 4.5: Simulador: Erosión Morfológica (A ⊖ B_L)
4.3.2 Apertura y Cierre
Combinando erosión y dilatación se obtienen dos operadores de gran utilidad práctica: la apertura y el cierre, definidos por las Ecuaciones Ecuación 4.5 y Ecuación 4.6. Sus principales efectos se resumen en la Tabla 4.2.
Apertura (opening) — erosión seguida de dilatación por el mismo \(B\):
\[
A \circ B = (A \ominus B) \oplus B
\tag{4.5}\]
Cierre (closing) — dilatación seguida de erosión por el mismo \(B\):
\[
A \bullet B = (A \oplus B) \ominus B
\tag{4.6}\]
En la práctica, mm.open y mm.close aplican el mismo elemento estructurante en las dos etapas. Para elementos estructurantes simétricos (los más comunes), esta implementación coincide con la definición matemática presentada anteriormente.
Tabla 4.2: Propiedades de apertura y cierre.
Operador
Secuencia
Efecto principal
Apertura \(A \circ B\)
erosión → dilatación
Elimina estructuras incapaces de contener el elemento estructurante; suaviza contornos externos
Cierre \(A \bullet B\)
dilatación → erosión
Rellena agujeros más pequeños que \(B\); suaviza contornos internos
Propiedad importante: ambos son idempotentes. Por ejemplo,
\[
(A \circ B) \circ B = A \circ B,
\]
es decir, después de la primera aplicación, nuevas aplicaciones del mismo operador no alteran más el resultado.
4.3.2.1 Implementación de la apertura y el cierre
A diferencia de la erosión y la dilatación, la apertura y el cierre no introducen nuevos mecanismos computacionales. Ambos se obtienen mediante la composición secuencial de los operadores primitivos ya presentados:
La función mm.open delega la operación a mm.open(f, B), mientras que mm.close utiliza mm.close(f, B), produciendo el mismo resultado de forma más eficiente.
La apertura hereda de la erosión la capacidad de eliminar estructuras más pequeñas que el elemento estructurante y de la dilatación la restauración parcial de las regiones preservadas. El cierre realiza el proceso inverso: primero expande los objetos y luego restaura sus dimensiones originales, rellenando huecos y agujeros más pequeños que el elemento estructurante.
4.3.2.2 Filtro Secuencial Alternado
En la práctica, la apertura y el cierre se aplican frecuentemente en secuencia para eliminar simultáneamente el ruido externo y rellenar los huecos internos. La función mm.asf (Filtro Secuencial Alternado) generaliza esta estrategia al aplicar aperturas y cierres alternadamente con elementos estructurantes progresivamente más grandes. Las secuencias disponibles se presentan en la Tabla 4.3.
Tabla 4.3: Secuencias del filtro secuencial alternado mm.asf.
Secuencia
Orden
Uso típico
'OC'
apertura → cierre
elimina el ruido externo antes de rellenar pequeños huecos
'CO'
cierre → apertura
rellena pequeños huecos antes de eliminar el ruido externo
'OCO'
apertura → cierre → apertura
enfatiza la eliminación del ruido externo
'COC'
cierre → apertura → cierre
enfatiza el relleno de huecos y lagunas
El parámetro n controla el número de escalas utilizadas por el filtro. En cada iteración \(i\), el elemento estructurante se amplía mediante la suma de Minkowski (mm.sesum(b, i)), produciendo una secuencia de filtros morfológicos cada vez más abarcativos. A diferencia de una única apertura o cierre con un elemento estructurante grande, el ASF realiza una suavización progresiva en múltiples escalas, preservando mejor la geometría de los objetos relevantes mientras elimina estructuras menores. La Figura 4.6 presenta un ejemplo de aplicación de la apertura, el cierre y el ASF en la imagen de las monedas.
Figura 4.6: Apertura, cierre, composición y filtro secuencial alternado aplicados a la binarización Otsu de las monedas (preprocesadas por realce de contraste). Elemento estructurante: disco 13×13.
4.3.3 Operadores Geodésicos
Los operadores geodésicos introducen una restricción adicional a los operadores morfológicos clásicos mediante una imagen de control denominada máscara\(g\). En lugar de permitir que la erosión o la dilatación se propaguen libremente por la imagen, el resultado de cada iteración se limita punto a punto por los valores de la máscara, restringiendo la evolución de la operación a las regiones permitidas.
4.3.3.1 Dilatación Geodésica
La dilatación geodésica de una imagen marcador \(f\) bajo una imagen máscara \(g\), utilizando un elemento estructurante plano \(b\), se define por:
\[
f \oplus_g b = (f \oplus b) \wedge g,
\tag{4.7}\]
donde \(\wedge\) representa el mínimo punto a punto.
En otras palabras, se realiza inicialmente una dilatación convencional sobre el marcador y, a continuación, el resultado se restringe mediante la máscara \(g\). De esta manera, la propagación nunca puede superar las regiones permitidas por la máscara.
La formulación clásica de la dilatación geodésica presupone que el marcador esté contenido en la máscara, es decir, \(f \le g\), garantizando que la evolución de la operación permanezca siempre limitada por la máscara.
4.3.3.2 Implementación de la dilatación geodésica
La función mm.cdil implementa directamente este operador y permite ejecutar múltiples iteraciones consecutivas:
def cdil(f, g, b=np.zeros((3,3),dtype='uint8'), n=1):"""Dilatação geodésica do marcador f sob a máscara g.""" y = f.copy()for _ inrange(n): y = np.minimum(mm.dil(y, b), g)return y
La instrucción np.minimum(mm.dil(y, b), g) implementa exactamente la definición matemática de la dilatación geodésica, es decir, \((y \oplus b)\wedge g\).
Cuando \(n=1\), la función ejecuta una única dilatación geodésica. Para \(n>1\), el resultado de cada etapa se convierte en el marcador de la etapa siguiente, produciendo una propagación progresiva controlada por la máscara.
El simulador interactivo Figura 4.7 permite seguir, paso a paso, la propagación del marcador \(f\) a lo largo de los corredores del laberinto. En cada iteración de mm.cdil, el frente de dilatación avanza hacia las celdas vecinas libres —aquellas en las que \(g = 1\)—, mientras que las paredes (\(g = 0\)) permanecen intransitables. El número de pasos necesarios para que el marcador alcance la salida corresponde exactamente a la longitud geodésica del camino más corto dentro de la máscara, evidenciando la conexión directa entre la dilatación geodésica iterada y la noción de distancia en grafos.
🗺️ Simulador: Dilatación Geodésica en el Laberintoδ_g^(n)(f)
Pared
Camino libre (g)
Marcador f
Propagación
Salida
Paso 0 — marcador inicial f (entrada)
Figura 4.7: Simulador interactivo de dilatación geodésica: el marcador f (verde) se propaga paso a paso por los caminos libres de la máscara g, sin atravesar paredes.
4.3.3.3 Erosión Geodésica
De forma dual, la erosión geodésica de una imagen marcador \(f\) bajo una imagen máscara \(g\) se define por:
\[
f \ominus_g b = (f \ominus b) \vee g,
\tag{4.8}\]
donde \(\vee\) representa el operador de máximo punto a punto.
En este caso, la erosión convencional del marcador es seguida por una restricción inferior impuesta por la máscara. Así, ningún píxel del resultado puede asumir un valor inferior al correspondiente píxel de la máscara.
La formulación clásica de la erosión geodésica presupone la condición dual
\[
f \ge g,
\]
de modo que la máscara actúe como límite inferior durante todo el proceso.
4.3.3.4 Implementación de la erosión geodésica
La función estática mm.cero materializa este operador:
staticmethoddef cero(f, g, b=np.zeros((3,3),dtype='uint8'), n=1):"""Erosión geodésica del marcador f bajo la máscara g.""" y = f.copy()for _ inrange(n): y = np.maximum(mm.ero(y, b), g)return y
La instrucción np.maximum(mm.ero(y, b), g) implementa directamente la expresión \((y \ominus b)\vee g\).
Al igual que en la dilatación geodésica, el parámetro \(n\) define cuántas erosiones geodésicas sucesivas se calcularán antes de devolver la imagen final.
NotaRelación con la reconstrucción morfológica
La reconstrucción morfológica presentada en la siguiente sección se obtiene mediante la aplicación iterativa de la dilatación geodésica (mm.cdil) hasta alcanzar un punto fijo, es decir, hasta que ninguna celda cambie de valor entre dos iteraciones consecutivas. En otras palabras, la reconstrucción consiste en una secuencia de dilataciones geodésicas sucesivas que se propagan dentro de la máscara hasta que no haya más alteraciones.
De forma dual, también es posible definir reconstrucciones basadas en erosión geodésica mediante aplicaciones sucesivas de mm.cero.
4.3.3.5 Ejemplo: propagación en un laberinto mediante dualidad
La Figura 4.8 ilustra la resolución del problema de conectividad de un laberinto utilizando la dualidad morfológica por medio de las funciones mm.cero y mm.suprec.
En lugar de propagar un marcador por los corredores libres mediante dilataciones geodésicas, el problema se formula en el dominio complementario. Inicialmente, la máscara original se invierte,
\[
g = 1 - g_{orig},
\]
de modo que las paredes pasan a tomar el valor 1 y los corredores el valor 0. De manera análoga, el marcador se construye en ese mismo dominio complementario, conteniendo un único valor 0 en la posición de entrada del laberinto y valor 1 en los demás píxeles.
Utilizando el elemento estructurante en cruz (mm.secross()), la erosión geodésica actúa sobre el marcador complementado. En cada iteración de mm.cero, la región conectada al marcador inicial sufre erosiones sucesivas, mientras que la máscara impone un límite inferior que impide la propagación a través de las paredes del laberinto.
Las imágenes intermediarias muestran estados de la evolución después de diferentes números de iteraciones (n=5, n=12 y n=22). El resultado final se obtiene mediante la reconstrucción geodésica por erosión (mm.suprec), que aplica erosiones geodésicas sucesivas hasta alcanzar un punto fijo, es decir, una situación en la que no se produce ninguna alteración adicional entre dos iteraciones consecutivas.
En el dominio complementario, la región reconstruida corresponde exactamente al conjunto de corredores conectados a la entrada del laberinto. Así, la conectividad entre la entrada y la salida puede determinarse directamente a partir de la imagen reconstruida.
import numpy as npimport matplotlib.pyplot as pltimport matplotlib.colors as mcolors# 1. Máscara original g (0 = Pared, 1 = Pasillo)g_orig = np.array([ [0, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 0, 1, 1, 1], [0, 0, 0, 0, 0, 1, 0, 1, 0, 1], [0, 1, 1, 1, 0, 1, 1, 1, 0, 1], [0, 1, 0, 1, 0, 0, 0, 0, 0, 1], [0, 1, 0, 1, 1, 1, 1, 1, 1, 1], [0, 1, 0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 1, 1, 1, 1, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, 0]], dtype=np.uint8)# Inversión global al inicio (Dualidad Morfológica)g =1- g_orig # Ahora: 1 = Pared, 0 = Pasillof = np.ones((10, 10), dtype=np.uint8)f[0, 1] =0# Semilla inyectada como 0 en el pasillo# Elemento estructurante en cruzB_cruz = mm.secross()# Procesamiento directo usando mm.cero y mm.suprec en el dominio invertidopasso_5 = mm.cero(f, g, B_cruz, n=5)passo_12 = mm.cero(f, g, B_cruz, n=12)passo_22 = mm.cero(f, g, B_cruz, n=22)ponto_fixo = mm.suprec(f, g, B_cruz)# --- CONFIGURACIÓN DE LA EXHIBICIÓN GRÁFICA ---titles = ["Máscara (~g)", "Marcador (~f)", "Avanço (n=5)", "Avanço (n=12)", "Avanço (n=22)", "~mm.suprec"]images = [g, f, passo_5, passo_12, passo_22, ponto_fixo]fig, axes = plt.subplots(1, 6, figsize=(16, 4), facecolor='#fcfcfc')# Mapa de colores adaptado para el dominio complementario:# En el dominio invertido: 1 = Pared (Azul Oscuro)# Donde g == 0 e imagen == 1 = Pasillo Libre (Gris Claro)# Donde g == 0 e imagen == 0 = Onda Geodésica Activa (Dorado)cmap_pipeline = mcolors.ListedColormap(['#ffc13b', '#e0e0e0', '#1e3d59'])for i, ax inenumerate(axes):if i ==0or i ==1:# Para las condiciones iniciales (~g y ~f) cmap_init = mcolors.ListedColormap(['#e0e0e0', '#1e3d59']) ax.imshow(images[i], cmap=cmap_init, vmin=0, vmax=1)else:# Renderización basada en los estados complementarios render_step = np.zeros_like(g, dtype=np.uint8) render_step[g ==1] =2# Pared (1 original del mapa de colores) render_step[g ==0] =1# Pasillo estándar render_step[images[i] ==0] =0# Onda activa (0 original del mapa de colores) ax.imshow(render_step, cmap=cmap_pipeline, vmin=0, vmax=2) ax.set_title(titles[i], fontsize=10, fontweight='bold', color='#1e3d59', pad=12) ax.set_xticks(np.arange(-0.5, 10, 1), minor=True) ax.set_yticks(np.arange(-0.5, 10, 1), minor=True) ax.grid(which='minor', color='#ffffff', linestyle='-', linewidth=1) ax.tick_params(which='both', bottom=False, left=False, labelbottom=False, labelleft=False)# Indicadores gráficos de Entrada y Salida ax.plot(1, 0, marker='v', color='#2ecc71', markersize=7, markeredgewidth=1.5) ax.plot(8, 9, marker='o', color='#e74c3c', markersize=6, fillstyle='none', markeredgewidth=2)plt.tight_layout()plt.show()
Figura 4.8: Resolución de laberinto mediante Operaciones Complementarias: Invirtiendo la máscara y el marcador al inicio del pipeline, el flujo se resuelve directamente en el dominio complementario a través de mm.cero y mm.suprec, eliminando reinversiones redundantes.
4.3.4 Reconstrucción Morfológica
La reconstrucción morfológica propaga una imagen marcadora\(f\) dentro de una imagen máscara\(g\), garantizando que el resultado nunca supere los valores de intensidad impuestos por la máscara. El operador fundamental que posibilita esta propagación contenida es la dilatación geodésica, definida por la Ecuación 4.7.
La reconstrucción se obtiene mediante la aplicación iterativa de esta dilatación condicionada. Inicialmente, el marcador está limitado por la máscara para establecer el estado inicial:
\[
X^{(0)} = f \wedge g,
\]
y las iteraciones subsiguientes se definen de forma recursiva mediante:
\[
X^{(k)} = (X^{(k-1)} \oplus b) \wedge g.
\]
La secuencia crece de manera monótona hasta alcanzar un punto fijo, produciendo la reconstrucción morfológica por dilatación (también conocida en la literatura como inf-reconstrucción):
El ascenso iterativo se detiene tan pronto como se logra la estabilidad, es decir, cuando dos iteraciones consecutivas producen matrices con valores absolutamente idénticos.
4.3.4.1 Implementação da reconstrucción morfológica
La rutina didáctica mm.infrec implementa directamente el algoritmo iterativo de punto fijo. Inicialmente, el marcador efectivo inicial \(X^{(0)}\) se determina mediante la operación np.minimum(f, g). Para garantizar que el bucle de verificación ejecute la primera pasada sin disparar falsas convergencias prematuras, la variable de control de la iteración anterior (y1) se inicializa rellenada con un valor centinela fuera del dominio de los datos (o simplemente con una matriz que fuerce la primera ejecución).
def infrec(f, g, b=np.zeros((3,3), dtype='uint8')):"""Inf-reconstrucción: dilata el marcador (f ∧ g) hasta converger bajo la máscara g.""" y = np.minimum(f, g)# Inicializa y1 con valores imposibles para forzar la entrada en el bucle y1 = np.full_like(f, 256, dtype=np.int16) whilenot np.array_equal(y, y1): y1 = y.copy()# Aplica la dilatación geodésica: (y ⊕ b) ∧ g y = np.minimum(mm.dil(y, b), g)return y.astype('uint8')
En el interior del bucle while, la variable y almacena la estimación corriente de la reconstrucción \(X^{(k)}\), mientras que y1 preserva la imagen del estadio inmediatamente anterior \(X^{(k-1)}\). La instrucción de control condicional np.minimum(mm.dil(y, b), g) traduce fielmente la dilatación geodésica teórica, donde la expansión morfológica convencional comandada por OpenCV se “poda” inmediatamente y queda limitada por las barreras de intensidad de la máscara \(g\). El bucle cesa cuando no se registra ninguna modificación de píxel entre los pasos.
4.3.4.2 Ventajas de la reconstrucción morfológica
La reconstrucción morfológica es significativamente más robusta que la apertura convencional porque es capaz de eliminar estructuras no deseadas sin distorsionar ni alterar la morfología de los objetos que deben conservarse.
Mientras que la apertura clásica suaviza esquinas, elimina puntas y deforma contornos debido a la imposición geométrica rígida del elemento estructurante, la reconstrucción geodésica utiliza la máscara para recuperar con exactitud los límites y formatos originales de los objetos que poseen conectividad con el marcador original.
En términos intuitivos, el marcador actúa como una semilla de contagio que se expande progresivamente, pero solo transitando por las regiones permitidas por la máscara. Los componentes que no poseen ninguna intersección con el marcador jamás serán reconstruidos (siendo eliminados), mientras que los componentes tocados por la semilla se expanden hasta restaurar integralmente su geometría original.
Este comportamiento discriminatorio y conservativo se ilustra en la Figura 4.9, utilizando el elemento estructurante en cruz presentado en la Figura 4.3..
Figura 4.9: Pipeline de Reconstrucción Morfológica por Dilatación Condicionada: la máscara contiene dos objetos, el marcador aísla solo el núcleo del objeto principal, y las iteraciones reconstruyen su forma exacta hasta la convergencia.
4.3.5 Relleno de Agujeros y Eliminación de Bordes
Dos operadores basados en reconstrucción morfológica completan el pipeline de limpieza binaria. Sus características principales se resumen en la Tabla 4.4.
Relleno de agujeros (mm.clohole) elimina cavidades completamente rodeadas por el objeto, independientemente del tamaño, sin alterar los contornos externos. El procedimiento actúa sobre el complemento de la imagen, utilizando como marcador una restricción del marco (frame) al fondo:
En términos operativos, primero se reconstruye el fondo externo y, a continuación, se aplica la complementación para recuperar los objetos con los agujeros rellenos.
Eliminación de objetos de borde (mm.edgeoff) elimina todos los objetos que tocan el borde de la imagen, preservando únicamente los componentes totalmente internos. El marcador se obtiene mediante la intersección entre el marco (frame) y los objetos de la imagen:
\[
\text{edgeoff}(f) = f \setminus R_f^\delta(\text{frame}(f) \wedge f)
\tag{4.11}\]
Tabla 4.4: Comparación entre los operadores clohole y edgeoff.
Operador
Marcador
Máscara
Efecto
mm.clohole
frame restringido al fondo (\(f^c\))
\(f^c\)
Rellena agujeros internos
mm.edgeoff
frame restringido al objeto (\(f\))
\(f\)
Elimina objetos conectados al borde
La evolución paso a paso de estas transformaciones geodésicas puede seguirse en las figuras siguientes. La Figura 4.10 ilustra el mecanismo de inundación controlada del operador mm.clohole, en el cual la reconstrucción se produce a partir del fondo externo e impide la propagación hacia regiones internas no conectadas al exterior, dando como resultado el relleno consistente de las cavidades internas. En cambio, la Figura 4.11 detalla la dinámica del operador mm.edgeoff, en la que solo los componentes conectados al borde se reconstruyen y posteriormente se eliminan, preservando exclusivamente los objetos totalmente contenidos en el interior de la imagen.
La conectividad de la propagación geodésica se controla mediante el elemento estructurante: mm.sebox() (vecindad de 8) incluye conexiones diagonales, mientras que mm.secross() (vecindad de 4) las excluye. En consecuencia, la elección del elemento estructurante afecta a qué componentes se alcanzan mediante la reconstrucción y, por tanto, cuáles se preservarán o eliminarán.
4.3.5.1 Conformidad con la implementación
Las definiciones anteriores están directamente alineadas con la implementación en morph.py, reproducida a continuación:
staticmethoddef clohole(f, b=np.ones((3,3),dtype='uint8')):# marcador restrito ao fundo da imagem marcador = mm.frame(f, border=1) & mm.neg(f)return mm.neg(mm.infrec(marcador, mm.neg(f), b))staticmethoddef edgeoff(f, b=np.ones((3,3),dtype='uint8')):# marcador restrito aos objetos da imagem marcador = mm.frame(f, border=1) & freturn mm.subm(f, mm.infrec(marcador, f, b))
Estas implementaciones dejan explícito que ambos operadores son instancias directas de reconstrucción morfológica por dilatación geodésica con mm.infrec, diferenciándose únicamente en la elección del marcador y de la máscara: clohole actúa sobre el complemento de la imagen, mientras que edgeoff actúa directamente en el dominio de los objetos.
Figura 4.10: Pipeline de relleno de agujeros (clohole): el marcador proviene del borde de la imagen, restringido al complemento \(f^c\). La dilatación geodésica reconstruye el fondo externo; tras la complementación, los agujeros internos quedan rellenos.
Figura 4.11: Pipeline de eliminación de estructuras de borde (edgeoff): el marcador captura las raíces conectadas a los extremos, la reconstrucción delimita esos elementos y la resta preserva solo los objetos totalmente internos.
4.3.6Pipeline de Limpieza Binaria con CLAHE
Con base en el análisis anterior —en el cual el CLAHE produjo el mayor valor de la varianza interclases (\(\sigma_B^2 \approx 2{,}47 \times 10^3\)), ver Figura 4.2, y el operador mm.clohole mostró ser eficaz en el relleno de las cavidades internas—, el pipeline final de segmentación, ilustrado en la Figura 4.12, se estructura mediante el siguiente flujo computacional:
Tras la etapa de mm.clohole, se aplica una segunda apertura morfológica con un elemento estructurante mayor (mm.sedisk(33), disco de diámetro 33). Esta operación elimina pequeñas regiones residuales y artefactos que podrían haber permanecido después de la segmentación. En particular, el relleno geodésico puede transformar pequeñas cavidades aisladas en componentes conectados al objeto, haciendo conveniente una etapa adicional de filtrado basada en tamaño. El diámetro se eligió de modo que las monedas sigan siendo capaces de contener el elemento estructurante, mientras que los componentes significativamente más pequeños sean eliminados.
En esta imagen, ninguna moneda está conectada al borde de la matriz. En consecuencia, la aplicación de mm.edgeoff no altera el resultado obtenido tras la segunda apertura. Aun así, esta etapa se mantiene en el pipeline por robustez, pues en otras imágenes pueden existir objetos parcialmente visibles o conectados a los bordes, que deben eliminarse antes de la etapa de análisis.
Tip¿Por qué la apertura después del clohole?
El operador mm.clohole rellena todas las cavidades cerradas presentes en los objetos segmentados. En algunas situaciones, pequeñas regiones no deseadas pueden permanecer después de esta etapa o volverse conectadas a los objetos principales. La apertura morfológica subsiguiente elimina componentes más pequeños que el elemento estructurante, preservando las monedas debido a su tamaño significativamente mayor.
Figura 4.12: Pipeline completo de segmentación con CLAHE: Otsu → apertura (r=9) → clohole → apertura (r=33) → edgeoff.
4.3.7 Morfología en Tonos de Gris
Los operadores morfológicos se extienden naturalmente a imágenes en tonos de gris. En esta formulación, la erosión y la dilatación pasan a actuar directamente sobre los niveles de intensidad de la imagen. Para elementos estructurantes planos (\(b \equiv 0\)), la erosión corresponde al mínimo local y la dilatación al máximo local dentro de la vecindad definida por el elemento estructurante.
La interpretación intuitiva es sencilla: la erosión oscurece regiones al reemplazar cada píxel por el menor valor presente en su vecindad, mientras que la dilatación aclara regiones al utilizar el mayor valor disponible. La combinación de estos operadores permite construir transformaciones capaces de resaltar bordes, eliminar tendencias de iluminación y destacar estructuras locales.
Tres operadores derivados son especialmente útiles:
Gradiente morfológico — resalta bordes como la diferencia entre dilatación y erosión:
\[
\text{grad}_B(f) = (f \oplus B) - (f \ominus B)
\tag{4.12}\]
Top-hat — resalta estructuras brillantes más pequeñas que el elemento estructurante (diferencia entre la imagen original y su apertura):
\[
\text{top-hat}_B(f) = f - (f \circ B)
\tag{4.13}\]
Black-hat — resalta estructuras oscuras más pequeñas que el elemento estructurante (diferencia entre el cierre y la imagen original):
\[
\text{black-hat}_B(f) = (f \bullet B) - f
\tag{4.14}\]
El Top-hat extrae detalles brillantes que no sobreviven a la apertura, mientras que el Black-hat evidencia detalles oscuros eliminados por el cierre. Por su parte, el gradiente morfológico resalta transiciones abruptas de intensidad, produciendo una representación similar a la de un detector de bordes.
Para comprender el mecanismo de estos operadores a nivel local, la Figura 4.13 presenta un simulador interactivo de morfología en tonos de gris. El simulador permite editar libremente el elemento estructurante, visualizar su desplazamiento sobre la imagen y acompañar simultáneamente el perfil unidimensional de las intensidades. De esta forma, se hace posible observar directamente cómo la erosión selecciona mínimos locales, cómo la dilatación selecciona máximos locales y cómo el gradiente morfológico emerge de la diferencia entre estos dos operadores.
El botón ubicado en la esquina superior derecha permite alternar entre la visualización original en tonos de gris y una representación pseudocoloreada (colormap) solo en los tres tipos de gradientes. La versión coloreada facilita la percepción visual de las variaciones de intensidad, haciendo más evidente la acción de los operadores morfológicos sobre máximos, mínimos y transiciones locales de la imagen.
Los operadores presentados están disponibles en morph.py mediante las funciones mm.gradm, mm.tophat y mm.blackhat.
🎛️ Simulador Avanzado de Morfología Matemática
🖱️ Mueva el mouse para actualizar el perfil 1D de la línea correspondiente
X (col)
—
Y (fil)
—
f(x,y)
—
valor
—
Elemento B (Haga clic para Editar)
Apertura (f∘B): dil(ero(f)) Cierre (f•B): ero(dil(f)) Gradiente: dil(f) − ero(f) Top-hat: f − (f∘B) Black-hat: (f•B) − f
Perfil 1D de la línea: Ninguno (pase el mouse sobre la imagen)
Figura 4.13: Simulador interactivo avanzado de morfología con elemento estructurante editable y perfil 1D.
La Figura 4.14 ilustra los efectos de estos operadores sobre la imagen de las monedas y sus respectivos histogramas. Observe que la erosión desplaza la distribución hacia intensidades más bajas, mientras que la dilatación la desplaza hacia intensidades más altas. El gradiente concentra valores en las regiones de contorno, y los operadores Top-hat y Black-hat producen histogramas fuertemente concentrados en bajos niveles de intensidad, ya que solo se resaltan pequeñas estructuras locales.
import ioimport matplotlib.pyplot as pltimport numpy as npdef fig2img(fig): b = io.BytesIO(); fig.savefig(b, format='png', dpi=100); plt.close(fig); b.seek(0)return (plt.imread(b)[:, :, :3] *255).astype(np.uint8)def plot_hist(img, title): fig, ax = plt.subplots(figsize=(4, 3)) h = mm.hist(img)# CORRECCIÓN: range usa len(h) dinámicamente para coincidir con el retorno de la biblioteca ax.bar(range(len(h)), h, color='steelblue', width=1, edgecolor='steelblue') ax.set(xlim=(0, 255)); plt.tight_layout()return fig2img(fig)# 1. Procesamiento morfológico baseB = mm.sedisk(19)operadores = [ ("Original", img_coins_gray), ("Erosão", mm.ero(img_coins_gray, B)), ("Dilatação", mm.dil(img_coins_gray, B)), ("Gradiente Morf.", mm.gradm(img_coins_gray, B)), ("Top-hat", mm.tophat(img_coins_gray, B)), ("Black-hat", mm.blackhat(img_coins_gray, B))]# 2. Montaje dinámico del par: [Imagen, Histograma]imgs, titles = [], []for nome, img in operadores: imgs += [img, plot_hist(img, f"Hist. {nome}")] titles += [nome, f"Hist. {nome}"]# 3. Exhibición final en cuadrícula de dos columnas (Imagen | Histograma)mm.show(imgs, titles=titles, cols=4, figsize=(10, 8))
Figura 4.14: Morfología en tonos de gris y sus respectivos histogramas. La erosión reduce intensidades locales, la dilatación las amplía, el gradiente resalta bordes, y los operadores Top-hat y Black-hat evidencian detalles locales brillantes y oscuros. Elemento estructurante: disco de diámetro 19.
Los operadores morfológicos presentados anteriormente se utilizarán ahora como herramientas de refinamiento y generación de marcadores para métodos de segmentación más avanzados, que se presentan a continuación.
4.4 Segmentación de Imágenes: Fundamentación y Taxonomía
La segmentación de imágenes consiste en dividir la imagen en regiones asociadas a objetos o estructuras de interés. En PDI, representa la transición entre el procesamiento de bajo nivel —como filtrado y realce— y etapas de análisis más avanzadas, como extracción de características, reconocimiento e interpretación de la escena.
Formalmente, el objetivo de la segmentación consiste en descomponer el dominio espacial completo de una imagen, denotado por \(\Omega\), en una partición de subconjuntos \(\{R_1, R_2, \ldots, R_n\}\) que satisfaga simultáneamente los criterios de completitud y disyunción:
Además de las propiedades de completitud y disyunción expresadas en la Ecuación 4.15, cada subregión \(R_i\) debe constituir un dominio homogéneo según un predicado de similitud definido sobre propiedades locales —intensidad, color o textura— y, simultáneamente, ser distinta de las regiones adyacentes.
Las técnicas de segmentación pueden organizarse en diferentes familias. En este capítulo se enfatizarán los enfoques resumidos en la Tabla 4.5, fundamentados principalmente en criterios de intensidad, conectividad y proximidad espacial.
Tabla 4.5: Taxonomía simplificada de los principales enfoques de segmentación y refinamiento estudiados en este capítulo.
Enfoque
Criterio de Segmentación
Operadores de Referencia
Umbralización
Particionamiento del espacio de intensidades
Criterio de Otsu, umbralización global y local
Morfología Matemática
Relaciones espaciales definidas por elementos/funciones estructurantes
Erosión, dilatación, apertura, cierre y reconstrucción
Basada en Regiones
Homogeneidad local y conectividad espacial
Etiquetado de componentes conexos, Transformada de Distancia y Watershed
Hasta este punto, el desarrollo práctico se ha centrado en la umbralización, mediante la combinación entre ecualización adaptativa CLAHE y el método global de Otsu. Esta etapa se complementó con operadores de reconstrucción morfológica basados en dilataciones geodésicas, implementados por las funciones mm.infrec, mm.clohole y mm.edgeoff, produciendo una máscara binaria limpia y adecuada para el análisis.
Sin embargo, en escenarios donde objetos distintos aparecen conectados en la máscara binaria —ya sea por contacto físico, superposición parcial o por puentes estrechos de píxeles producidos por la segmentación—, la umbralización deja de ser suficiente para individualizar cada objeto. En estos casos, múltiples objetos pasan a componer un único componente conexo, dificultando etapas posteriores de medición e interpretación.
Para superar esta limitación, las próximas secciones introducen tres herramientas complementarias: el Etiquetado de Componentes Conexos, la Transformada de Distancia y el algoritmo de segmentación por Watershed basado en marcadores. En conjunto, estas técnicas permiten separar objetos adyacentes, identificar regiones individualmente y extraer descriptores geométricos consistentes para el análisis cuantitativo.
4.4.1 Etiquetado
El etiquetado de componentes conexas (connected component labeling) es el operador que asigna un identificador entero único a cada conjunto de píxeles pertenecientes a la misma componente conexa en una imagen binaria.
NotaDefinición formal
Dada una imagen binaria \(f\) y una relación de conectividad definida por un elemento estructurante \(B\) (típicamente conectividad-4 o conectividad-8), el algoritmo de etiquetado de la Figura 4.15 produce una imagen \(g\) en la cual todos los píxeles pertenecientes a la misma componente conexa reciben la misma etiqueta entera positiva, mientras que los píxeles pertenecientes a componentes distintas reciben etiquetas diferentes.
La conectividad define qué píxeles se consideran vecinos directos de un píxel \((x,y)\). Las definiciones más utilizadas son:
Conectividad-4: considera solo los cuatro vecinos ortogonales (norte, sur, este y oeste).
Conectividad-8: considera los cuatro vecinos ortogonales y los cuatro diagonales, totalizando ocho vecinos.
La elección de la conectividad influye directamente en la formación de las componentes conexas y, consecuentemente, en el resultado del etiquetado, como se ilustra en la Figura 4.17. Un ejemplo adicional puede explorarse de forma interactiva en el simulador presentado en la Figura 4.16.
La implementación en morph.py proporciona dos versiones de este operador. La función mm.label0 reproduce explícitamente el algoritmo de flood-fill utilizando una pila y permite controlar la conectividad mediante el elemento estructurante adoptado. En cambio, mm.label delega la operación a la implementación optimizada de OpenCV (mm.label0). En ambos casos, el resultado es una imagen etiquetada en la cual cada componente conexa recibe un identificador entero distinto.
Algoritmo de rotulação por flood-fill com pilha — painel interativo com HTML e SVG
Passo a passo
Cards
Linha do tempo
Código Python
Fluxograma
Flood-fill com pilha
Rotulagem de componentes conexas
1
Criar imagem de saída g, inicializada com zeros, mesma dimensão de f.
2
Inicializar contador de rótulos (cor) cor ← 1.
3
Percorrer f em ordem raster (coordenadas x e y) até encontrar uma semente: pixel ativo (f[x,y] ≠ 0) ainda não rotulado (g[x,y] = 0).
4
Inserir a semente encontrada na pilha pilha ← [[x,y]].
5
Enquanto a pilha contiver coordenadas (while pilha):
Desempilhar pixel atual: i, j ← pilha.pop() e atribuir o rótulo: g[i,j] ← cor.
Buscar vizinhos usando o iterador mm._viz(f,b,i,j). Se o vizinho for ativo no elemento estruturante (bv ≠ 0), ativo na imagem (f[vy,vx] ≠ 0) e não rotulado (g[vy,vx] = 0), empilhá-lo.
6
Pilha vazia ⟹ Toda a componente conexa atual foi explorada e rotulada com sucesso.
7
Incrementar o rótulo para a próxima componente: cor ← cor + 1 e continuar a varredura raster.
A conectividade (4 ou 8 vizinhos) é definida unicamente pela matriz morfológica b passada como parâmetro, alterando os pixels retornados em mm._viz.
01
Inicialização
Criar matriz de rótulos g preenchida com zeros (fundo). Definir rótulo inicial cor ← 1.
02
Varredura Raster
Percorrer a matriz bidimensional linha por linha, localizando pixels pertencentes ao objeto que ainda não possuem rótulo.
03
Semente inicial
Ao achar um pixel válido, inicializar a estrutura LIFO de busca: pilha = [[x, y]].
04
Expansão por Flood-Fill
Enquanto houver elementos na pilha:
Extrair (i, j) via pop() e marcar g[i, j] = cor.
Inspecionar vizinhança geométrica e adicionar novos candidatos à pilha.
05
Próxima Componente
Pilha esvaziada ⟹ Incrementar indexador cor ← cor + 1 para diferenciar o próximo objeto isolado.
01
Alocação Espacial
g ← zeros_like(f) e definição do primeiro identificador: cor ← 1.
02
Varredura Bidimensional
Laços encadeados varrendo as dimensões h e w da imagem.
03
Descoberta de Objeto
Filtro condicional localiza pixel ativo não indexado e cria a pilha semente.
04–05
Preenchimento por Região (Flood-fill)
while pilha
Remover último da pilha (i,j) e aplicar rótulo atual.
Empilhar vizinhos conectados que atendam aos critérios morfológicos de b.
06
Fechamento do Objeto
Pilha vazia determina o fim do isolamento daquela componente.
07
Atualização do Rótulo
Incremento linear: cor ← cor + 1. A varredura raster continua do ponto onde parou.
label0.pyflood-fill com pilha
deflabel0(f, b=np.ones((3,3),dtype='uint8')):
"""Rotulagem por flood-fill com pilha."""
h, w = f.shape
g = np.zeros(f.shape, dtype=int)
cor = 1for x inrange(h):
for y inrange(w):
if f[x,y] and not g[x,y]:
pilha = [[x,y]]
while pilha:
i,j = pilha.pop(); g[i,j] = cor
for vy,vx,bv in mm._viz(f,b,i,j):
if bv and f[vy,vx] and not g[vy,vx]:
pilha.append([vy,vx])
cor += 1return g
1
g = np.zeros(f.shape, dtype=int) — Inicializa a matriz de saída com zeros. Zeros representam o fundo invariável.
2
mm._viz(f, b, i, j) — O iterador morfológico avalia a conectividade. Passando B_cruz a busca expande em 4-vizinhança; passando quadrado (ones) expande em 8-vizinhança.
3
pilha.pop() — Remove o último par de coordenadas inserido, caracterizando um comportamento LIFO de busca em profundidade (DFS) para varrer o objeto de forma contígua.
4
cor += 1 — O incremento ocorre estritamente fora do laço while, garantindo que o mesmo número marque toda a extensão da componente concluída antes de passar para a próxima semente raster.
Figura 4.15: Algoritmo de etiquetado por flood-fill con pila.
El auxiliar _viz itera sobre la ventana estructurante b centrada en \((i,j)\), generando únicamente los vecinos válidos dentro de los límites de la imagen — la conectividad deseada está enteramente determinada por la forma de b pasada al algoritmo.
Ejemplo didáctico — efecto de la conectividad:
🪙 Simulador: Etiquetado de Componentes Conexasflood-fill con pila
Píxeles Activos
0
Componentes
0
Conectividad
4
Paso Raster
–
🖱️ Haga clic para activar/desactivar píxeles · Arrastre para pintar
Conectividad
4 vecinos ortogonales: N, S, E, O
Visualización
Paso a Paso
–
–
Ejemplos Predefinidos
Figura 4.16: Simulador interactivo de etiquetado de componentes conexas (connected component labeling): visualización de la expansión flood-fill, conectividad-4 y conectividad-8.
Figura 4.17: Efecto de la conectividad en la etiquetación: la imagen binaria 10×10 contiene píxeles diagonalmente adyacentes. Con conectividad-4, estos píxeles forman componentes distintas; con conectividad-8, se fusionan en un único componente.
4.4.2 Transformada de Distancia
La Transformada de Distancia (TD) es un operador que, aplicado a una imagen binaria \(f\), produce una imagen en niveles de gris \(D\) en la cual cada píxel perteneciente al objeto (\(f(x,y)\neq 0\)) recibe como valor la distancia geométrica hasta el píxel de fondo (\(f(x',y')=0\)) más cercano:
donde \(d(\cdot,\cdot)\) es una métrica de distancia — típicamente la distancia Euclidiana (\(L_2\)). El resultado es una representación topográfica de los objetos: los píxeles ubicados en el interior asumen valores elevados, mientras que los píxeles próximos a los bordes presentan bajos valores de distancia. Los máximos locales de \(D\) corresponden a los puntos más alejados del borde del objeto, frecuentemente próximos a sus centros geométricos o centros de máxima inscripción — propiedad particularmente útil para la generación automática de marcadores en el algoritmo watershed.
NotaDefinición formal mediante erosiones
La TD admite además una interpretación morfológica iterativa, conforme al algoritmo de la Figura 4.18. Considérese una función estructurante \(b\) cuyo valor central es nulo y cuyos vecinos poseen costos negativos asociados al desplazamiento. Al aplicar erosiones sucesivas con esta función estructurante particular, los valores de los píxeles de los objetos (que deben asumir la distancia máxima posible de la imagen) se reducen progresivamente según los costos definidos por \(b\). El valor acumulado de esta propagación pasa entonces a representar la distancia al fondo según la métrica inducida por la función estructurante.
Esta interpretación se implementa en mm.dist1(), que acumula erosiones sucesivas utilizando la operación mm.ero1(). Por su parte, mm.dist() delega el cálculo de la distancia Euclidiana al operador optimizado de OpenCV mm.dist(f), donde f es la imagen binaria de entrada, la distancia L2 especifica la métrica Euclidiana (\(L_2\)) y 5 indica el uso de una máscara 5×5 para aproximar la distancia con elevada precisión.
La función dist1 produce una transformada de distancia discreta cuya métrica está determinada por la geometría y los pesos de la función estructurante utilizada. Por ejemplo, utilizando una función estructurante en cruz con costo unitario para los cuatro vecinos ortogonales, se obtiene la distancia de Manhattan (\(L_1\)). Otras elecciones de vecindad y pesos inducen métricas diferentes. En cambio, mm.dist() calcula una aproximación eficiente de la distancia Euclidiana (\(L_2\)).
Por exigir sucesivas erosiones sobre toda la imagen, el enfoque dist1 posee un costo computacional significativamente mayor que mm.dist(), siendo empleado en este libro principalmente con fines didácticos y para evidenciar la relación entre morfología matemática y transformadas de distancia.
Transformada de distância por erosões numéricas sucessivas — painel interativo
Passo a passo
Cards
Linha do tempo
Código Python
Fluxograma
Transformada de distância por erosão numérica
Propagação matemática de distâncias via elemento estruturante com pesos
1
Inicializar a imagem de trabalho fazendo uma cópia da original: g ← f.copy(). Os pixels de fundo (0) servem como fontes de distância nula.
2
Entrar em um laço infinito de erosões com pesos (ponto fixo):
Salvar estado anterior: f ← g.copy().
Erodir: g ← ero1(g, b), aplicando a subtração local de pesos e computando o valor mínimo para cada vizinhança.
3
Verificar convergência: se f for idêntica a g (array_equal), a frente de onda de distâncias se estabilizou. Romper o laço (break).
4
Retornar a matriz modificada g contendo o mapa exato de distâncias.
Nesta abordagem morfológica numérica, não há incremento artificial ou contador. A distância propaga-se de fora para dentro porque a erosão contínua puxa o valor 0 do fundo e o decrementa matematicamente (subtraindo os pesos negativos como -1), fazendo com que os valores escalem radialmente.
Cruz — L₁ (Manhattan)
B_cruz [y,x]
Pesos: Centro=0, Lados=-1, Cantos=-inf
Erosão de Cinzas
f[vy,vx] - bv
Subtrai o peso e busca o valor mínimo local
Convergência
f == g
Para quando nenhum pixel muda de valor
01
Inicialização
Clonar imagem de entrada: g ← f.copy(). O objeto possui intensidade alta (255) e o fundo possui intensidade 0.
02
Mapeamento Local (ero1)
Para cada coordenada (y, x), buscar o mínimo valor da operação f[vy, vx] - bv aplicada à sua vizinhança estruturante.
03
Loop Iterativo
Atualizar sequencialmente: f = g.copy() seguido de g = ero1(g, b). Os valores nulos propagam-se para o interior do objeto.
04
Critério de Parada
Se np.array_equal(f, g), significa que o mapa de distâncias atingiu o equilíbrio estável e a propagação terminou.
01
Cópia de Trabalho
Prepara a matriz inicial `g`.
02
Loop de Erosão de Escala
while True
Guarda estado: f ← g.copy()
Aplica erosão com pesos: g ← ero1(g, b)
03
Estabilização Espacial
Condição de parada acionada assim que np.array_equal(f, g) se torna verdadeiro.
04
Retorno Numérico
Retorna g contendo as distâncias calculadas pela subtração cumulativa dos pesos.
morph_dist.pyErosão numérica iterativa
@staticmethoddefero1(f, b):
g = np.empty_like(f)
for y inrange(f.shape[0]):
for x inrange(f.shape[1]):
g[y,x] = 255for vy,vx,bv in mm._viz(f,b,y,x):
if np.isinf(bv): continue
val = int(f[vy,vx]) - int(bv)
if g[y,x] > val:
g[y,x] = max(0, val)
return g
@staticmethoddefdist1(f, b):
g = f.copy()
whileTrue:
f = g.copy()
g = mm.ero1(g, b)
if np.array_equal(f, g):
breakreturn g
1
g[y,x] = 255 — Inicializa o elemento com o valor máximo antes de computar o operador de mínimo da erosão.
2
f[vy,vx] - bv — Subtrai o peso associado da vizinhança. Como os pesos da cruz externa são negativos (ex: -1), a operação torna-se uma adição matemática (f[vy,vx] - (-1) = f[vy,vx] + 1) propagando a distância a partir das bordas zeradas.
3
np.array_equal(f, g) — Critério de convergência exato por estabilização de ponto fixo.
Figura 4.18: Algoritmo de la Transformada de Distancia.
La Figura 4.19 presenta un simulador interactivo de la TD: es posible posicionar el cursor sobre diferentes píxeles del objeto y observar, en tiempo real, el valor de la distancia asociado a esa posición, es decir, la distancia hasta el píxel de fondo más cercano. La Figura 4.20 presenta un ejemplo práctico de esta ejecución en entorno Python.
🗺️ Simulador: Transformada de Distancia (TD)Fronteras en +∞ (144)
Píxeles Activos
0
Dist. Máx.
0
Métrica Actual
L∞ (Chebyshev)
Iteración (k)
–
🖱️ Clic para activar/desactivar píxeles · Arrastra para pintar
Elemento Estructurante (b)
Clic para cambiar los pesos:
Visualización
Propagación Paso a Paso
Ejemplos Predefinidos
Figura 4.19: Simulador interactivo de la Transformada de Distancia (TD) iterativa mediante erosión en tonos de gris. Los píxeles fuera de la imagen asumen el valor máximo (144), propagando los costos a partir del fondo interno.
Figura 4.20: Transformada de Distancia en imagen binaria 10×10. Izquierda: original (foreground = 255). Centro: mm.dist1 iterativa (erosiones con cruz). Derecha: mm.dist (L2).
La anotación de los valores numéricos directamente sobre los píxeles permite verificar cómo dist1 propaga las distancias según la métrica inducida por la función estructural utilizada. En el caso del elemento cruz con costo unitario, los valores obtenidos corresponden a la distancia de Manhattan (\(L_1\)). Aunque dist1 y mm.dist producen valores numéricos distintos por adoptar métricas diferentes, ambas transformadas preservan la estructura topográfica de los objetos, haciendo que sus máximos ocurran en regiones centrales similares. Esta propiedad justifica el uso de mm.dist en aplicaciones prácticas, debido a su elevada eficiencia computacional.
4.4.3 Transformada de Distancia Euclidiana en cuatro pasos
El simulador de la Figura 4.21 implementa el algoritmo de la TDE de Lotufo (2001) en dos etapas. En la primera etapa, la función edt1 realiza una transformación unidimensional vertical de forma secuencial (in-place), recorriendo cada columna en raster (↓) y anti-raster (↑) para calcular las distancias en la dirección vertical (dos pasos: Sur y Norte). En la segunda etapa, la función edt2 utiliza ese resultado como entrada y realiza una propagación horizontal por colas: para cada fila de la matriz, dos colas de prioridad, Eq y Wq, se inicializan recorriendo los índices de columna en sentidos opuestos (Eq de W-1 hasta 1, Wq de 2 hasta W), de modo que cada píxel actualizado encola inmediatamente a sus vecinos para reprocesamiento dentro de la misma ronda (dos pasos más: Este y Oeste). Este mecanismo de cola permite aplicar erosiones sucesivas con pesos impares crecientes (b = 1, 3, 5, ..., incrementado en cada iteración del bucle externo) sin que la propagación quede atrapada en valores desactualizados, ya que cada ronda resuelve por completo la cadena de dependencias horizontales antes del siguiente incremento de b. La convergencia de esta propagación produce la Transformada de Distancia Euclidiana en toda la matriz, combinando la información vertical obtenida en edt1 con la propagación horizontal en cola realizada en edt2.
Usa la misma estructura raster/anti-raster descrita en el artículo para propagación de distancias exactas (ejemplo del artículo, pág. 103).
Paso Actual
–
Fase
–
b Actual
–
Vel.:
Figura 4.21: Simulador interactivo 2D (4x4) con sincronización estricta de colas de propagación horizontal (b) para obtener la convergencia exacta descrita en el artículo.
4.4.3.1 Transformada de Distancia Geodésica
La transformada de distancia geodésica asocia a cada píxel la menor distancia hasta un marcador, bajo la restricción impuesta por una máscara. De esta forma, la propagación ocurre exclusivamente por los píxeles permitidos, preservando la conectividad del dominio.
La Figura 4.22 ilustra este proceso en un laberinto: (a) la máscara g; (b) la distancia geodésica D1 calculada a partir de la entrada; (c) la distancia D2 calculada a partir de la salida; y (d) el camino mínimo obtenido a partir de estas dos transformadas.
El camino óptimo se determina mediante la suma de las distancias (D1 + D2). Los píxeles pertenecientes a la trayectoria mínima son aquellos para los cuales esta suma asume su menor valor, definiendo una conexión entre entrada y salida con longitud geodésica mínima.
Este principio permite resolver laberintos sin la necesidad de explorar explícitamente todas las posibilidades de recorrido. La solución emerge directamente de la propagación de distancias en un dominio restringido. Este enfoque es particularmente relevante en laberintos de elevada complejidad, como los construidos a partir de estructuras cuasicristalinas y ciclos hamiltonianos descritos por Singh (2024). En Zampirolli (2025), este mismo formalismo se emplea para resolver un laberinto complejo; a continuación, el método se ilustra en una versión simplificada del problema.
import numpy as np# 1 = pasillo, 0 = paredg = np.array([ [0,1,0,0,0,0,0,0,0,0], [0,1,1,1,1,1,0,1,1,1], [0,0,0,0,0,1,0,1,0,1], [0,1,1,1,0,1,1,1,0,1], [0,1,0,1,0,0,0,0,0,1], [0,1,0,1,1,1,1,1,1,1], [0,1,0,0,0,0,0,0,1,0], [0,1,1,1,1,1,1,0,1,0], [0,0,0,0,0,0,1,1,1,0], [0,0,0,0,0,0,0,0,1,0]], dtype=np.uint8)# marcador de la entradaentrada = np.zeros_like(g, dtype=np.uint8)entrada[0,1] =1# marcador de la salidasaida = np.zeros_like(g, dtype=np.uint8)saida[9,8] =1# distancias geodésicasD1 = mm.gdist(g, entrada)D2 = mm.gdist(g, saida)# suma de las distanciasS = D1 + D2# menor valor válido de la sumadmin = np.min(S[S >0])# píxeles pertenecientes a un camino óptimocaminho = (S == dmin)print("Distancia geodésica mínima:", dmin)mm.show( [g, D1, D2, caminho], titles=["Laberinto","Distancia de la Entrada","Distancia de la Salida",f"Menor Camino\n(d={dmin})" ], cols=4, figsize=(14,4), axis=True)
Distancia geodésica mínima: 15
Figura 4.22: Camino geodésico más corto en un laberinto. Las distancias geodésicas se calculan desde la entrada y la salida usando mm.gdist. Los píxeles cuya suma de las dos distancias es igual a la distancia mínima entre los marcadores pertenecen a un camino óptimo.
4.4.4 Segmentación por Watershed
El algoritmo Watershed interpreta una imagen en niveles de gris como una superficie topográfica, en la cual valores elevados corresponden a montañas y valores bajos corresponden a valles o cuencas de drenaje (catchment basins). En el contexto de la segmentación basada en marcadores, los máximos de la Transformada de Distancia se utilizan frecuentemente para identificar regiones internas de los objetos, proporcionando semillas confiables para el proceso de inundación.
La segmentación se realiza entonces mediante una simulación conceptual de inundación progresiva a partir de estos marcadores. A medida que las cuencas asociadas a diferentes semillas se expanden, las regiones vecinas eventualmente entran en contacto. En ese instante se construyen barreras virtuales, denominadas watershed lines, que pasan a delimitar los objetos de la escena. Este mecanismo permite separar objetos adyacentes o parcialmente superpuestos, incluso cuando forman una única componente conexa tras la umbralización.
La implementación didáctica presentada en este capítulo explora inicialmente el concepto de crecimiento de regiones (region growing) confinado por una máscara binaria, según se detalla en el algoritmo interactivo de la Figura 4.23.
NotaVersión didáctica versus implementación clásica
La función mm.watershed0 no implementa el algoritmo watershed clásico. Su objetivo es ilustrar, de forma simplificada, la propagación de marcadores por crecimiento de regiones (region growing), permitiendo visualizar cómo diferentes semillas compiten por la ocupación del espacio disponible. El crecimiento está delimitado por una máscara binaria de soporte y monitoreado por un control de estancamiento, generando un resultado similar a una partición de Voronoi restringida a la geometría de los objetos de entrada.
En cambio, la función mm.watershed utiliza la implementación optimizada de OpenCV (mm.watershed), que realiza la inundación sobre una superficie topográfica definida por la imagen de entrada. En este caso, la propagación de los marcadores está influenciada por los valores de los píxeles, haciendo que las líneas de separación se formen naturalmente sobre las crestas del relieve.
Algoritmo Didático do Watershed Limitado por Máscara — painel interativo
Passo a passo
Cards
Linha do tempo
Código Python
Fluxograma
Crescimento de Regiões Confinado por Máscara
Inundação concorrente com restrição geométrica de suporte e sincronização síncrona por malha
1
Rotular os marcadores sementes em f via mm.label0(f, b), instanciar a malha dinâmica g ← f.copy() e binarizar a mask.
2
Enquanto houver pixels não rotulados (while True), reiniciar o controle de atividade mudou ← False e varrer a imagem:
Identificar se a coordenada atual é um vazio contido no escopo: g[x,y] == 0 and mask[x,y].
Avaliar a vizinhança estrutural em mm._viz(f, b, x, y) baseada no estado síncrono estável f.
Se um vizinho possuir rótulo dominante (g[x,y] < f[vy,vx]), a célula em g absorve esse identificador e marca-se mudou ← True.
3
Verificar ponto fixo: caso uma varredura completa não expanda nenhuma fronteira (not mudou), interrompe-se o laço (break).
4
Atualizar o estado de referência de forma síncrona para a próxima iteração: f ← g.copy().
5
Se op == 'region', retornar o mapa de bacias g; caso contrário, extrair as cristas divisórias via mm.gradm(g).
A sincronização f = g.copy() ao final de cada ciclo impede o crescimento assimétrico ou dependente da ordem da varredura raster (propagação em estilo Jacobi).
Escopo Geométrico
mask[x,y] > 0
Restrição binária rígida impedindo o avanço periférico de rótulos.
Estabilização
if not mudou: break
Evita loops infinitos interrompendo ao saturar o domínio da máscara.
Mapeamento Jacobi
f = g.copy()
Sincronização em bloco após inspeção de todas as coordenadas.
01
Inicialização
Geração dos identificadores iniciais pelo mapeamento de componentes conexas e binarização da máscara de suporte.
02
Expansão Concorrente
Varredura 2D inspecionando vazios internos autorizados. A malha de trabalho g absorve os rótulos lidos da referência estável f.
03
Ponto Fixo Local
A flag mudou monitora mudanças estruturais. Se nenhuma frente avançar, o laço de inundação é finalizado via break.
04
Sincronização e Saída
Atualização em bloco do estado referencial. A saída pode ser moldada como partições regionais ou linhas de cristas (linhas de watershed).
01
Condicionamento Prévio
Rotulagem preliminar de marcadores e isolamento booleano do domínio.
02
Laço Síncrono Iterativo
while True
Redefinição de flag: mudou = False
Crescimento condicional: se g[x,y] == 0 e estiver na máscara, expande lendo f
Controle de estabilidade: if not mudou: break
Atualização síncrona: f = g.copy()
03
Extração Topológica
Retorno condicional das bacias preenchidas ou cálculo morfológico do gradiente de transição.
mm_watershed.pyAlgoritmo com Restrição de Máscara
defwatershed0(f, mask=None, b=np.zeros((3,3),dtype='uint8'), op='region'):
f = mm.label0(f, b)
g = f.copy()
mask = np.ones_like(f) if mask isNoneelse (mask > 0)
whileTrue:
mudou = Falsefor x inrange(f.shape[0]):
for y inrange(f.shape[1]):
if g[x,y] == 0and mask[x,y]:
for vy,vx,bv in mm._viz(f, b, x, y):
if bv and g[x,y] < f[vy,vx]:
g[x,y] = f[vy,vx]
mudou = Trueifnot mudou:
break
f = g.copy()
return g if op == 'region'else mm.gradm(g, mm.secross())
1
mask = (mask > 0) — Converte a imagem de suporte informada para um mapa Booleano indexável.
2
g[x,y] == 0 and mask[x,y] — Filtro ativo: pixels fora da máscara (fundo zero) são ignorados de imediato, confinando as frentes de expansão.
3
if not mudou: break — Mecanismo de escape. Quando todos os espaços internos permitidos forem preenchidos ou estabilizados contra a barreira, o laço aborta de forma limpa.
Figura 4.23: Algoritmo Didáctico de Watershed por Crecimiento de Regiones Limitado por Máscara.
A Figura 4.24 presenta un simulador iterativo que ilustra la propagación de los marcadores por la región de interés. Cada marcador actúa como una fuente de inundación que expande su área de influencia hasta encontrar regiones provenientes de otras semillas. En el algoritmo de OpenCV, los píxeles pertenecientes a las líneas divisorias se identifican por el valor -1, representando las fronteras entre cuencas adyacentes.
💧 Simulador: Segmentación por WatershedPropagación con Elemento Estructurante (b)
Área (Máscara)
0
Marcadores
0
Relleno
0%
Iteración (k)
–
🖱️ Arrastre para dibujar/borrar la máscara o las semillas
Herramientas
Elemento Estructurante (b)
Visualización
Inundación Paso a Paso
Ejemplos Iniciales
Figura 4.24: Simulador interactivo del Algoritmo Watershed por propagación morfológica. Dibuja la máscara, coloca los marcadores y ajusta el Elemento Estructurante para observar la inundación. Cuando las cuencas se encuentran simultáneamente, el empate se resuelve asumiendo una de las regiones de manera aleatoria.
Pipeline Morfológico del Watershed
El watershed basado en marcadores normalmente integra un flujo más amplio de segmentación. En imágenes reales, etapas de preprocesamiento son frecuentemente necesarias para mejorar el contraste, reducir ruidos y generar marcadores confiables. Este flujo completo está resumido en la Tabla 4.6.
Tabla 4.6: Pipeline completo del watershed basado en marcadores para imágenes reales.
Etapa
Operación
Finalidad
1
CLAHE + Suavizado
Realce de contraste y reducción de ruido
2
Umbralización
Separación inicial entre objeto y fondo
3
Apertura/Cierre
Eliminación de ruidos y pequeñas imperfecciones
4
Dilatación de la Máscara
Identificación del Fondo Seguro (Sure Background)
5
Transformada de Distancia + Umbral
Identificación del Objeto Seguro (Sure Foreground)
6
Región Incierta
Diferencia entre Fondo Seguro y Objeto Seguro
7
mm.watershed
Propagación de los marcadores por la región incierta
Para enfatizar exclusivamente los conceptos de Transformada de Distancia, marcadores e inundación topográfica, el ejemplo de la Figura 4.25 utiliza una imagen binaria sintética y adopta un flujo simplificado, resumido en la Tabla 4.7.
Tabla 4.7: Pipeline simplificado utilizado en el ejemplo didáctico de la Figura 4.25.
Etapa
Operación
Finalidad
1
Transformada de Distancia
Construcción de la superficie topográfica
2
Umbral de la TD
Extracción de los marcadores (Sure Foreground)
3
Dilatación
Determinación del Fondo Seguro (Sure Background)
4
Región Incierta
Diferencia entre fondo y marcadores
5
mm.watershed
Propagación de los marcadores y generación de las fronteras
Figura 4.25: Pipelinewatershed delimitado por máscara en imagen binaria 20×20.
Aplicación en monedas superpuestas:
img_base = img_coins_gray# 1. Simular monedas superpuestas/conectadas (usando la máscara binaria base)img_sobrepostas = mm.dil(img_final, mm.sebox(40))# 2. Apertura morfológica para limpiar ruidosopening = mm.open(img_sobrepostas, mm.sebox(2))# 3. Transformada de Distanciadist = mm.dist(opening)dist_vis=(255*(dist/dist.max())).astype(np.uint8) if dist.max() >0else dist.astype(np.uint8)# 4. Picos seguros (Marcadores de las monedas)picos = (dist >0.5* dist.max()).astype(np.uint8) *255# 5. Ejecución del Watershed (Ajustado para usar la nueva firma)# Pasamos 'opening' directo en mask, pues delimita el alcance de expansión de las monedasws_region = mm.watershed(picos, mask=opening, op='region')ws_line = mm.watershed(picos, mask=opening, op='line')# 6. Conteo de objetos (Ignora fondo 0)labels = np.unique(ws_region)labels = labels[labels >0]print(f"Objetos detectados: {len(labels)}")# 7. Anotación Finalimg_annotated = cv2.cvtColor(img_base, cv2.COLOR_GRAY2BGR)for idx, label_id inenumerate(labels): mask_reg = (ws_region == label_id).astype(np.uint8)if mask_reg.sum() <500: continue cy, cx = np.mean(np.where(mask_reg), axis=1).astype(int) cv2.putText(img_annotated, str(idx +1), (cx -25, cy +20), cv2.FONT_HERSHEY_SIMPLEX, 3.2, (0, 255, 0), 8, cv2.LINE_AA)# Dibuja las líneas de separación en rojomask_ann = mm.dil(ws_line, np.ones((11, 11), np.uint8))img_annotated[mask_ann >0 ] = [255, 0, 0]# Visualizaciónmm.show( [img_base, img_sobrepostas, dist_vis, picos, ws_region, img_annotated], titles=["Original", "Superpuestas", "Distancia", "Marcadores", "Watershed", "Anotado"], cols=3, rows=2, figsize=(12, 8))
Objetos detectados: 12
Figura 4.26: PipelineWatershed para separación de monedas superpuestas: desde la máscara binaria dilatada hasta los contornos finales anotados sobre la imagen original.
4.5 Extracción de Componentes y Descriptores de Forma
Tras la segmentación y el refinamiento morfológico, el siguiente paso consiste en identificar individualmente cada objeto presente en la imagen y extraer sus propiedades geométricas. Esta etapa es fundamental para tareas de medición, clasificación y reconocimiento de patrones.
Un componente conexo es un conjunto maximal de píxeles pertenecientes al objeto que permanecen mutuamente conectados según una relación de conectividad previamente definida (conectividad 4 u 8). Tras la rotulación, cada componente recibe un identificador único, permitiendo que sus características sean analizadas individualmente.
OpenCV ofrece dos enfoques complementarios para este análisis, resumidos en la Tabla 4.8.
Tabla 4.8: Comparación entre los enfoques basados en componentes conexos y en contornos.
connectedComponentsWithStats
findContours
Devuelve
etiqueta por píxel y estadísticas por componente
secuencia de puntos que describe el borde
Descriptores directos
área, bounding box y centroide
perímetro, forma y jerarquía
Objetos en contacto
tiende a fusionar regiones conectadas
tiende a producir un único contorno externo
Uso típico
conteo, filtrado y rotulación
análisis geométrico y descriptores de forma
4.5.1 Rotulado y Estadísticas de Componentes
La función mm.label0 realiza simultáneamente el rotulado de los componentes conexos y la extracción de descriptores básicos de cada región. El operador devuelve:
una imagen de etiquetas (labels);
estadísticas geométricas (stats);
coordenadas de los centroides (centroids).
Las estadísticas incluyen área, ancho, alto y posición de la bounding box mínima alineada con los ejes de la imagen.
La Figura 4.27 ilustra la aplicación de este operador después del pipeline de segmentación de las monedas. Cada componente conexo recibe un color distinto y su área se anota directamente sobre la imagen.
# 1. Etiquetado y estadísticasn, labels, stats, centroids = cv2.connectedComponentsWithStats( img_final, connectivity=8)# 2. Coloración de los componentes — paleta FIJA (generada una vez con# np.random.seed(4)) para que la pista C++ reproduzca color a color sin# depender del generador de numpy. Si n excede len(PALETA), cicla.PALETA = np.array([ [0, 0, 0], [224, 82, 193], [233, 155, 73], [190, 247, 244], [103, 94, 179], [51, 154, 59], [137, 96, 232], [250, 243, 205], [100, 141, 208], [228, 187, 163], [202, 108, 214], [131, 105, 104], [86, 153, 81]], dtype=np.uint8)colors = PALETA[np.arange(n) %len(PALETA)].copy()colors[0] = [0, 0, 0] # fondo negroimg_colored = colors[labels]img_annotated = img_colored.copy()# 3. Tabla de descriptoresprint(f"Componentes detectados (excluyendo fondo): {n -1}")print(f"\n{'ID':>4}{'Área':>8}{'cx':>6}{'cy':>6}{'w':>6}{'h':>6}")print("-"*42)for i inrange(1, n): area = stats[i, cv2.CC_STAT_AREA] cx, cy =int(centroids[i, 0]), int(centroids[i, 1]) w, h = stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT]print(f"{i:>4}{area:>8}{cx:>6}{cy:>6}{w:>6}{h:>6}")for cor, esp in [((0,0,0), 10), ((255,0,0), 5)]: cv2.putText(img_annotated, f"{i}: {area}", (cx -150, cy +18), cv2.FONT_HERSHEY_SIMPLEX, 2.0, cor, esp, cv2.LINE_AA)mm.show( [img_coins_gray, img_final, img_colored, img_annotated], titles=["Original", "Segmentación Final", "Componentes Conexos", "Áreas Anotadas"], cols=4, figsize=(18, 6))
Figura 4.27: Componentes conexos extraídos tras el pipeline CLAHE → Otsu → limpieza morfológica. Cada objeto se colorea con color distinto y se anota con su área en píxeles.
4.5.2 Descriptores de Forma
Mientras que connectedComponentsWithStats opera sobre regiones, la extracción de contornos actúa directamente sobre sus fronteras. El operador devuelve, para cada objeto, una secuencia ordenada de puntos que describe su contorno.
A partir de esta representación es posible calcular descriptores geométricos que no se proporcionan directamente mediante la etiquetación de componentes:
Área (área del contorno)
Perímetro (perímetro);
Circularidad (\(C = \frac{4\pi A}{P^2}\), donde \(A\) es el área y \(P\) el perímetro);
Aproximación poligonal (aproximación poligonal);
Casco convexo (convex hull);
Jerarquía de contornos, que permite representar relaciones padre-hijo entre regiones y sus agujeros internos.
La circularidad alcanza un valor máximo igual a 1 para un círculo perfecto y disminuye a medida que el objeto se vuelve más alargado o presenta irregularidades en su borde.
La Figura 4.28 presenta los contornos extraídos de las monedas segmentadas, junto con los valores de circularidad calculados para cada objeto.
Figura 4.28: Contornos extraídos con findContours. Cada moneda se anota con su circularidad — valores cercanos a 1 confirman forma circular.
4.5.3 Conexión con la Detección de Objetos Moderna
Los descriptores extraídos en las secciones anteriores — especialmente bounding boxes, centroides, áreas y medidas de forma — establecen un puente natural entre la segmentación morfológica clásica y los sistemas modernos de detección de objetos. Aunque las técnicas estudiadas en este capítulo utilizan operaciones sobre píxeles y regiones segmentadas, muchas de las representaciones producidas son directamente compatibles con los formatos empleados en modelos contemporáneos de visión por computadora.
Los detectores basados en aprendizaje profundo, como la familia YOLO (You Only Look Once) (REDMON, 2016), operan directamente sobre imágenes en color y producen, para cada objeto detectado, una bounding box descrita por el centro \((cx,cy)\) y las dimensiones \((w,h)\), además de una clase y una puntuación de confianza. Esta representación comparte la misma estructura geométrica básica obtenida por connectedComponentsWithStats, aunque es producida por un modelo aprendido y no mediante segmentación explícita.
La Figura 4.29 ilustra cómo las bounding boxes obtenidas por morfología pueden exportarse en el formato YOLO para componer conjuntos de datos utilizados en el entrenamiento o la evaluación de detectores.
# Recalcula etiquetas/estadísticas a partir de la segmentación finaln, labels, stats, centroids = cv2.connectedComponentsWithStats( img_final, connectivity=8)H_img, W_img = img_coins_gray.shapeimg_bbox = cv2.cvtColor(img_coins_gray, cv2.COLOR_GRAY2BGR)CLASSE =0# 0 = moneda (única categoría en este ejemplo)print(f"{'cls':>4}{'cx_n':>8}{'cy_n':>8}{'w_n':>8}{'h_n':>8} ← formato YOLO")print("-"*54)yolo_linhas = []for i inrange(1, n): x0 = stats[i, cv2.CC_STAT_LEFT] y0 = stats[i, cv2.CC_STAT_TOP] w = stats[i, cv2.CC_STAT_WIDTH] h = stats[i, cv2.CC_STAT_HEIGHT] cx_n = (x0 + w /2) / W_img cy_n = (y0 + h /2) / H_img w_n = w / W_img h_n = h / H_img yolo_linhas.append(f"{CLASSE}{cx_n:.4f}{cy_n:.4f}{w_n:.4f}{h_n:.4f}")print(f"{CLASSE:>4}{cx_n:>8.4f}{cy_n:>8.4f}{w_n:>8.4f}{h_n:>8.4f}") cv2.rectangle(img_bbox, (x0, y0), (x0 + w, y0 + h), (0, 255, 0), 4) cv2.putText(img_bbox, f"moeda", (x0 +8, y0 +60), cv2.FONT_HERSHEY_SIMPLEX, 3.8, (255, 0, 0), 5, cv2.LINE_AA)# Exportar archivo de anotación en el formato YOLOwithopen("moedas.txt", "w") as f: f.write("\n".join(yolo_linhas))print("\nAnotación guardada en monedas.txt")mm.show( [img_coins_gray, img_final, img_bbox], titles=["Original", "Segmentación Final", "Bounding Boxes (formato YOLO)"], cols=3, figsize=(18, 6))
Figura 4.29: Bounding boxes derivadas de los componentes conectados superpuestas a la imagen original. Las anotaciones se exportan en el formato YOLO (clase cx cy w h), con coordenadas normalizadas al intervalo [0,1].
El formato YOLO almacena cada objeto en una línea que contiene cinco campos:
donde \((cx,cy)\) representa el centro de la bounding box y \((w,h)\) sus dimensiones. Todos los valores geométricos se normalizan al intervalo \([0,1]\) con respecto al ancho y al alto de la imagen. La clase es un identificador entero asociado a una categoría definida por el conjunto de datos (por ejemplo, 0 → moneda). Cuando hay múltiples categorías —como moneda de oro (0), moneda de plata (1) y disco plástico (2)— basta con asignar el identificador correspondiente a cada objeto antes de la exportación, manteniendo exactamente el mismo formato de anotación. En el ejemplo anterior, esta información se almacenó en el archivo monedas.txt.
El flujo presentado en este capítulo —segmentación → etiquetado → extracción de bounding boxes— corresponde conceptualmente a la etapa de anotación (labeling) empleada en la construcción de conjuntos de entrenamiento para detectores modernos. Herramientas especializadas, como Label Studio y Roboflow, automatizan este proceso en imágenes complejas, pero la lógica fundamental sigue siendo la misma: asociar a cada objeto una región de interés y una clase. En escenarios controlados, con fondo uniforme y objetos bien separados, las técnicas morfológicas pueden incluso generar anotaciones automáticamente o servir como punto de partida para el etiquetado manual, reduciendo significativamente el esfuerzo de construcción del conjunto de datos. En aplicaciones reales más complejas, sin embargo, la validación humana sigue siendo necesaria para garantizar la calidad de las anotaciones.
NotaEvaluación: IoU (Intersection over Union)
Una forma sencilla de evaluar la calidad de una segmentación consiste en compararla con una máscara de referencia (ground truth). La métrica más utilizada para este propósito es la IoU (Intersection over Union):
donde \(A\) representa la segmentación producida por el algoritmo y \(B\) la segmentación de referencia.
El valor de la IoU varía entre 0 y 1. Cuanto mayor sea el valor, mayor será la superposición entre las máscaras. Una IoU igual a 1 indica una correspondencia perfecta entre la segmentación obtenida y la referencia.
La misma métrica también se utiliza ampliamente en la detección de objetos, aplicándose a las bounding boxes previstas y anotadas. En esta área, valores de IoU superiores a 0,5 se adoptan frecuentemente como criterio mínimo para considerar una detección correcta.
TipMás Allá de la Morfología
Las técnicas estudiadas en este capítulo segmentan objetos explorando la conectividad espacial, las operaciones morfológicas y el relieve topográfico. Existen, sin embargo, enfoques alternativos basados en la agrupación de características, como el algoritmo k-means, los modelos de mezcla Gaussiana (GMM) y métodos más recientes basados en aprendizaje profundo. Estas técnicas se retomarán en la Parte II del libro, dedicada a la Visión Computacional.
4.6 Resumen
En este capítulo se presentaron las principales técnicas de segmentación y morfología matemática, concluyendo el estudio del PDI en el dominio espacial.
Preprocesamiento y umbralización: La combinación entre ecualización adaptativa CLAHE y el método de Otsu mostró ser eficaz para inducir separación bimodal en el histograma y simplificar la binarización de imágenes con iluminación no uniforme.
Erosión y dilatación: Operadores morfológicos fundamentales basados en la búsqueda de mínimos y máximos locales en una vecindad definida por el elemento estructurante \(B\). Son operadores duales por el complemento y fueron implementados mediante las funciones mm.ero y mm.dil.
Apertura y cierre: Composiciones de erosión y dilatación que permiten eliminar ruidos, suavizar contornos y rellenar pequeñas lagunas, preservando la estructura global de los objetos.
Reconstrucción morfológica: Proceso geodésico iterativo que propaga un marcador dentro de los límites impuestos por una máscara, constituyendo la base de operadores como mm.clohole y mm.edgeoff.
Pipeline de limpieza binaria: Flujo consolidado compuesto por CLAHE → Otsu → apertura → mm.clohole → apertura restringida → mm.edgeoff, produciendo máscaras adecuadas para análisis cuantitativo.
Morfología en tonos de gris: Extensión algebraica basada en mínimos y máximos ponderados, viabilizando operadores como gradiente morfológico y filtros top-hat para realce de estructuras locales.
Transformada de Distancia y Watershed: La Transformada de Distancia permitió generar marcadores automáticos para el algoritmo watershed, posibilitando la separación de objetos adyacentes o parcialmente superpuestos.
Componentes conexos y descriptores: La rotulación de regiones (mm.label0) y la extracción de contornos permitieron calcular descriptores geométricos como área, centroide, perímetro, circularidad y bounding boxes.
Conexión con Visión Computacional Moderna: Las bounding boxes extraídas por morfología fueron exportadas en el formato YOLO, evidenciando el vínculo entre técnicas clásicas de segmentación y sistemas modernos de detección de objetos.
El Capítulo 5 introducirá técnicas de procesamiento en el dominio de la frecuencia, abordando la Transformada de Fourier, filtrado espectral y los fundamentos de la compresión de imágenes, incluyendo DCT, JPEG y wavelets.
4.7 🤖 Uso de Gemini Notebook como Tutor Complementario
En esta edición, se incentiva el uso de Gemini Notebook como herramienta complementaria de aprendizaje. Basado en inteligencia artificial, el sistema utiliza exclusivamente los documentos proporcionados por el autor como fuente de conocimiento, produciendo respuestas alineadas con el contenido y el enfoque adoptado a lo largo de este capítulo.
El proyecto de este capítulo en Gemini Notebook fue construido únicamente con el texto en portugués y los ejemplos de código en Python. Si estás estudiando con la edición en inglés o francés, o siguiendo la ruta en C++, las respuestas del tutor pueden no corresponder exactamente a la versión que estás leyendo.
⚠️ Aviso sobre Contenido Generado por IA
Aunque es una herramienta valiosa de apoyo al estudio, Gemini Notebook puede eventualmente producir respuestas incompletas, imprecisas o incorrectas. Se recomienda validar la información consultando el material del capítulo, libros, artículos científicos y otras fuentes académicas confiables. Siempre que sea posible, ejecuta y experimenta con los ejemplos prácticos presentados a lo largo del texto para consolidar la comprensión de los conceptos.
4.8 Lista de Ejercicios
(10%) Implemente manualmente el criterio de Otsu sin utilizar mm.threshold. Calcule la varianza entre clases \(\sigma_B^2(T)\) para todos los umbrales \(T \in [0,255]\) usando mm.hist, identifique el umbral óptimo \(T^*\) y compare el resultado con el valor obtenido por OpenCV. Grafique \(\sigma_B^2\) en función de \(T\) y resalte el punto de máximo.
(15%) Aplique umbralización adaptativa con bloques de tamaño 11, 31 y 51 a una imagen que contenga iluminación no uniforme. Compare los resultados con la umbralización global de Otsu y discuta las ventajas y limitaciones de cada enfoque.
(15%) Ejecute el pipeline de watershed de la imagen de monedas variando el umbral aplicado a la Transformada de Distancia (\(0.3\), \(0.5\) y \(0.7\) veces el valor máximo). Explique cómo este parámetro influye en la generación de los marcadores, la separación de objetos adyacentes y la ocurrencia de sobre-segmentación.
(15%) Utilizando mm.drawImg, construya una demostración visual paso a paso de la erosión de una imagen binaria 7×7 con elemento estructurante cuadrado 3×3. Para cada posición analizada, indique si el elemento estructurante está completamente contenido en el objeto y justifique el valor asignado al píxel de salida.
(15%) Demuestre experimentalmente la dualidad entre erosión y dilatación verificando la identidad \((A \ominus B)^c = A^c \oplus \hat{B}\) utilizando mm.ero, mm.dil y mm.bnot. Calcule la diferencia píxel a píxel entre los dos lados de la ecuación y presente el resultado utilizando mm.histImg o una visualización equivalente.
(15%) Implemente manualmente el gradiente morfológico utilizando únicamente mm.ero y mm.dil, comparando el resultado con mm.gradm(img, B). Evalúe el efecto de diferentes elementos estructurantes (cuadrado 3×3, disco 5×5 y línea 1×9) sobre la detección de bordes.
(15%) Construya un pipeline completo para el conteo y clasificación de monedas por tamaño (pequeña, mediana y grande) utilizando área y circularidad como descriptores. Genere una máscara de referencia (ground truth) manualmente y calcule la métrica IoU (Intersection over Union) para evaluar la calidad de la segmentación. Presente los resultados en una tabla y mediante visualizaciones producidas con mm.show.
Referencias del Capítulo
La fundamentación teórica de este capítulo se basa en las siguientes obras:
Gonzalez (2018) para los conceptos de segmentación, umbralización de Otsu, Transformada de Distancia, watershed, morfología matemática y descriptores de forma.
Matheron (1975) y Serra (1982) para la fundamentación teórica original, formulación algebraica y desarrollo de la Morfología Matemática.
Szeliski (2022) para segmentación basada en regiones, etiquetado de componentes conexos, watershed basado en marcadores y evaluación de segmentación mediante la métrica IoU.
Bradski (2008) para la utilización práctica de la biblioteca OpenCV, incluyendo funciones como mm.dist, mm.watershed, mm.label0 y extracción de contornos.
Redmon (2016) para la introducción a los detectores modernos de la familia YOLO y su relación con descriptores geométricos como bounding boxes extraídas por segmentación.
Singh (2024) para la construcción de laberintos complejos basados en ciclos hamiltonianos sobre mosaicos cuasicristalinos, utilizados como ejemplo de aplicación de la Transformada de Distancia Geodésica y de algoritmos de búsqueda de caminos.
Zampirolli (2025) para la implementación de los operadores morfológicos, transformadas geodésicas y resolución de laberintos por propagación de distancias en dominios restringidos.
4.9 EP4: Adición de Ruido y Filtros
Objetivo: Este ejercicio práctico (EP) tiene como objetivo ejercitar la aplicación de diferentes tipos de ruido a imágenes y el uso de filtros espaciales (media, mediana y gaussiano) para la restauración de imágenes degradadas. El estudiante deberá explorar los efectos de estos filtros sobre imágenes con ruido, comparando cualitativamente los resultados y entendiendo las características de cada técnica.
4.10 Cómo usar este notebook
Este notebook contiene celdas de código y celdas de texto. Las celdas de código deben ejecutarse en orden, de arriba hacia abajo, para que las variables y funciones definidas estén disponibles para las celdas posteriores. Para ejecutar una celda de código, selecciónela y presione Shift+Enter, o haga clic en el botón de ejecución en la barra de herramientas de Colab.
Las secciones marcadas como EJERCICIOS requieren que el estudiante complete el código (escriba una respuesta basada en los estudios previos). No se olvide de guardar una copia en su Google Drive (Archivo → Guardar una copia en Drive) antes de comenzar a editar. Para obtener una nota, será necesario completar todos los ejercicios propuestos y seguir las instrucciones dadas por el profesor.
4.11 Bibliotecas necesarias
A continuación se importan las bibliotecas necesarias para ejecutar este notebook. No modifique esta celda.
# Importando las bibliotecas necesariasimport numpy as npimport matplotlib.pyplot as pltfrom scipy import ndimage, signal, miscfrom google.colab import filesimport imageio as iiofrom skimage import img_as_floatimport sys, os, time, math
4.12 Función para mostrar imágenes
Esta función auxiliar se utiliza para visualizar las imágenes generadas en los ejercicios.
def mostrar_imagem(imagem, titulo=''):""" Función para mostrar una imagen en escala de grises. """ plt.figure(figsize=(6,6)) plt.imshow(imagem, cmap='gray', vmin=0, vmax=1) plt.title(titulo) plt.axis('off') plt.show()
4.13 Funciones para agregar ruido
Complete las funciones a continuación para agregar diferentes tipos de ruido a una imagen. Siga las indicaciones de cada sección.
4.13.1 Ruido sal y pimienta
Implemente la función ruido_salpimienta(imagem, prob), que agrega ruido sal y pimienta a una imagen en escala de grises (valores entre 0 y 1). La probabilidad prob indica la probabilidad total de que un píxel sea modificado (siendo la mitad para sal y la mitad para pimienta, en promedio).
# EJERCICIO 1: Implemente la función para agregar ruido sal y pimientadef ruido_salpimienta(imagem, prob):""" Agrega ruido sal y pimienta a una imagen. Parámetros: imagem: array 2D con valores entre 0 y 1 prob: probabilidad total de ruido (entre 0 y 1) Retorna: imagen con ruido """ saida = imagem.copy()# Complete el código aquí# 1. Genere una matriz de números aleatorios uniformes en [0, 1)# con el mismo tamaño que la imagen.# 2. Los píxeles con valor < prob/2 deben convertirse en 0 (pimienta).# 3. Los píxeles con valor entre prob/2 y prob deben convertirse en 1 (sal).# 4. Los demás píxeles permanecen igual.# <<< Inserte aquí su código >>>return saida
4.13.2 Ruido gaussiano
Implemente la función ruido_gaussiano(imagem, media=0, sigma=0.05), que agrega ruido gaussiano a la imagen. El ruido debe generarse con la función np.random.normal y sumarse a la imagen. Asegúrese de que los valores resultantes estén en el rango [0, 1].
# EJERCICIO 2: Implemente la función para agregar ruido gaussianodef ruido_gaussiano(imagem, media=0, sigma=0.05):""" Agrega ruido gaussiano a una imagen. Parámetros: imagem: array 2D con valores entre 0 y 1 media: media del ruido sigma: desviación estándar del ruido Retorna: imagen con ruido """ saida = imagem.copy()# Complete el código aquí# 1. Genere el ruido con np.random.normal(media, sigma, imagem.shape)# 2. Sume el ruido a la imagen# 3. Recorte los valores para que estén en el rango [0, 1]# <<< Inserte aquí su código >>>return saida
4.13.3 Ruido uniforme
Implemente la función ruido_uniforme(imagem, intensidade=0.1), que agrega ruido con distribución uniforme en el intervalo [-intensidade, intensidade] a la imagen. Asegúrese de que los valores resultantes estén en el rango [0, 1].
# EJERCICIO 3: Implemente la función para agregar ruido uniformedef ruido_uniforme(imagem, intensidade=0.1):""" Agrega ruido uniforme a una imagen. Parámetros: imagem: array 2D con valores entre 0 y 1 intensidade: límite del ruido (el ruido se distribuye en [-intensidade, intensidade]) Retorna: imagen con ruido """ saida = imagem.copy()# Complete el código aquí# 1. Genere el ruido con np.random.uniform(-intensidade, intensidade, imagem.shape)# 2. Sume el ruido a la imagen# 3. Recorte los valores para que estén en el rango [0, 1]# <<< Inserte aquí su código >>>return saida
4.14 Funciones para filtros
4.14.1 Filtro de media
Implemente la función filtro_media(imagem, tamanho=3), que aplica un filtro de media (promedio) con una máscara de tamaño tamanho x tamanho (tamaño impar). Utilice la función signal.convolve2d o ndimage.uniform_filter de SciPy.
# EJERCICIO 4: Implemente el filtro de mediadef filtro_media(imagem, tamanho=3):""" Aplica un filtro de media (promedio) a la imagen. Parámetros: imagem: array 2D tamanho: tamaño de la máscara (impar) Retorna: imagen filtrada """# Complete el código aquí# 1. Defina la máscara (kernel) como una matriz de unos de tamaño (tamanho, tamanho) dividida por tamanho**2# 2. Aplique la convolución 2D con signal.convolve2d(imagem, mascara, mode='same')# o use ndimage.uniform_filter(imagem, size=tamanho)# <<< Inserte aquí su código >>>return imagem
4.14.2 Filtro de mediana
Implemente la función filtro_mediana(imagem, tamanho=3), que aplica un filtro de mediana con una ventana de tamaño tamanho x tamanho (tamaño impar). Utilice ndimage.median_filter.
# EJERCICIO 5: Implemente el filtro de medianadef filtro_mediana(imagem, tamanho=3):""" Aplica un filtro de mediana a la imagen. Parámetros: imagem: array 2D tamanho: tamaño de la ventana (impar) Retorna: imagen filtrada """# Complete el código aquí# 1. Aplique ndimage.median_filter(imagem, size=tamanho)# <<< Inserte aquí su código >>>return imagem
4.14.3 Filtro gaussiano
Implemente la función filtro_gaussiano(imagem, sigma=1.0), que aplica un filtro gaussiano con desviación estándar sigma. Utilice ndimage.gaussian_filter.
# EJERCICIO 6: Implemente el filtro gaussianodef filtro_gaussiano(imagem, sigma=1.0):""" Aplica un filtro gaussiano a la imagen. Parámetros: imagem: array 2D sigma: desviación estándar del núcleo gaussiano Retorna: imagen filtrada """# Complete el código aquí# 1. Aplique ndimage.gaussian_filter(imagem, sigma=sigma)# <<< Inserte aquí su código >>>return imagem
4.15 Carga de la imagen
Ahora cargaremos la imagen que se utilizará en los ejercicios. La imagen debe estar en escala de grises con valores en el rango [0, 1].
# Cargando la imagen (elija una de las opciones)# Opción 1: usar una imagen de la biblioteca scikit-imagefrom skimage import dataimagem_original = img_as_float(data.camera())# Opción 2: subir una imagen desde su computadora (descomente el código)# from google.colab import files# uploaded = files.upload()# for filename in uploaded.keys():# imagem_original = img_as_float(iio.imread(filename, mode='L'))# Opción 3: usar imagen generada (descomente el código)# x, y = np.meshgrid(np.linspace(0, 1, 256), np.linspace(0, 1, 256))# imagem_original = 0.5 + 0.5 * np.sin(2 * np.pi * (x + y))print('Tamaño de la imagen:', imagem_original.shape)print('Rango de valores:', imagem_original.min(), '-', imagem_original.max())print('Tipo de datos:', imagem_original.dtype)mostrar_imagem(imagem_original, 'Imagen Original')
4.16 Aplicación de ruido
En esta sección, aplicaremos los tres tipos de ruido implementados anteriormente a la imagen original.
# Aplicando ruido sal y pimientaprobabilidade =0.05# 5% de ruidoimagem_salpimenta = ruido_salpimienta(imagem_original, probabilidade)mostrar_imagem(imagem_salpimenta, f'Ruido Sal y Pimienta - Prob: {probabilidade}')
4.17 Aplicación de filtros sobre imágenes ruidosas
Ahora aplicaremos los filtros implementados a las imágenes ruidosas. Compare los resultados.
4.17.1 Filtro de media sobre diferentes ruidos
# Filtro de media sobre ruido sal y pimientatamanho_filtro =3img_filtrada = filtro_media(imagem_salpimenta, tamanho=tamanho_filtro)mostrar_imagem(img_filtrada, f'Media (t={tamanho_filtro}) sobre Sal y Pimienta')
# Filtro de media sobre ruido gaussianoimg_filtrada = filtro_media(imagem_gaussiana, tamanho=tamanho_filtro)mostrar_imagem(img_filtrada, f'Media (t={tamanho_filtro}) sobre Gaussiano')
# Filtro de media sobre ruido uniformeimg_filtrada = filtro_media(imagem_uniforme, tamanho=tamanho_filtro)mostrar_imagem(img_filtrada, f'Media (t={tamanho_filtro}) sobre Uniforme')
4.17.2 Filtro de mediana sobre diferentes ruidos
# Filtro de mediana sobre ruido sal y pimientatamanho_filtro =3img_filtrada = filtro_mediana(imagem_salpimenta, tamanho=tamanho_filtro)mostrar_imagem(img_filtrada, f'Mediana (t={tamanho_filtro}) sobre Sal y Pimienta')
# Filtro de mediana sobre ruido gaussianoimg_filtrada = filtro_mediana(imagem_gaussiana, tamanho=tamanho_filtro)mostrar_imagem(img_filtrada, f'Mediana (t={tamanho_filtro}) sobre Gaussiano')
# Filtro de mediana sobre ruido uniformeimg_filtrada = filtro_mediana(imagem_uniforme, tamanho=tamanho_filtro)mostrar_imagem(img_filtrada, f'Mediana (t={tamanho_filtro}) sobre Uniforme')
4.17.3 Filtro gaussiano sobre diferentes ruidos
# Filtro gaussiano sobre ruido sal y pimientasigma_filtro =1.0img_filtrada = filtro_gaussiano(imagem_salpimenta, sigma=sigma_filtro)mostrar_imagem(img_filtrada, f'Gaussiano (sigma={sigma_filtro}) sobre Sal y Pimienta')
# Filtro gaussiano sobre ruido gaussianoimg_filtrada = filtro_gaussiano(imagem_gaussiana, sigma=sigma_filtro)mostrar_imagem(img_filtrada, f'Gaussiano (sigma={sigma_filtro}) sobre Gaussiano')
# Filtro gaussiano sobre ruido uniformeimg_filtrada = filtro_gaussiano(imagem_uniforme, sigma=sigma_filtro)mostrar_imagem(img_filtrada, f'Gaussiano (sigma={sigma_filtro}) sobre Uniforme')
4.18 Comparación de resultados
La celda a continuación genera una figura comparativa con varias combinaciones de ruido/filtro. Ejecútela para visualizar todos los resultados en una sola imagen.
EJERCICIO 7 (Análisis cualitativo): Después de ejecutar las celdas anteriores, responda en la celda Markdown a continuación (haga doble clic para editar):
¿Cuál filtro funciona mejor para eliminar el ruido sal y pimienta? ¿Por qué?
¿Cuál filtro es más eficaz para reducir el ruido gaussiano? Explique.
**Al aumentar el tamaño de la máscara en el filtro de media, ¿qué efecto observa
4.20 💻 Parte Práctica con Ejercicios de Programación
Esta lista transforma los conceptos del Capítulo 4 en una ruta práctica de segmentación y morfología matemática. Los EPs comienzan con umbralización y avanzan hasta etiquetado y descriptores de componentes, siempre con matrices pequeñas para que cada píxel pueda verificarse a mano.
ImportanteRegla común de los EPs morfológicos
En las operaciones con vecindad, no haga padding. Para cada píxel, evalúe únicamente las posiciones del elemento estructurante que caen dentro del dominio de la imagen. Esta es la misma idea de las implementaciones didácticas en morph.py, como mm.dil0, mm.ero0, mm.dil1 y mm.label0: la vecindad se recorta por el dominio válido de la imagen.
🎯 Objetivo de este cuaderno
El cuaderno permite desarrollar, validar, organizar y probar soluciones de Ejercicios de Programación (EPs) en entornos interactivos, como Colab, con los mismos casos de prueba de Moodle, copiándolos allí solo al momento de registrar la nota oficial.
Download
Descargue morph.py y testsuite.py ejecutando la celda a continuación:
Para evaluar las pruebas, ejecute TestSuite("EP04_01.extensión").run() en una nueva celda, reemplazando la extensión por la del lenguaje utilizado (.py, .java, .c, .cpp, .js o .r). El sistema descarga los casos de prueba de GitHub, ejecuta el programa y calcula la nota automáticamente.
Para probar código Python directamente, sin guardar archivo, use run_code(codigo) pasando el código como string en una variable codigo:
codigo ="""from morph import mm# ... su código aquí ..."""TestSuite("EP04_01").run_code(codigo)
4.20.1 EP04_01 🎚️ Umbralización Global por Umbral Fijo
En los escáneres de documentos y en los sistemas de lectura de códigos de barras, la primera etapa del procesamiento siempre consiste en separar lo que es “objeto” (tinta, texto, barras) de lo que es “fondo” (papel, embalaje). La umbralización global hace exactamente esto: compara cada píxel con un único umbral \(T\) y decide, en tiempo real, si pertenece a la clase clara o a la clase oscura. Es el operador de segmentación más simple — y aun así, está detrás de buena parte de los pipelines industriales de inspección visual. Ver en Figura 4.30 una simulación de este EP.
4.20.1.1 📋 Directrices de Implementación
Dimensiones: Leer los enteros \(L\) (filas) y \(C\) (columnas).
Umbral: Leer el entero \(T\) (umbral de decisión).
Datos: Leer los valores enteros de la matriz original fila a fila.
Mapeo: Para cada píxel \(p\), calcular el nuevo valor mediante la ecuación:
\[
p' =
\begin{cases}
255, & \text{si } p > T \\
0, & \text{si } p \le T
\end{cases}
\] 5. Salida: Mostrar la matriz binarizada con dimensiones \(L \times C\).
4.20.1.2 📌 Restricciones Computacionales
Binarización: La salida contiene solo los valores \(0\) o \(255\).
Comparación estricta: El criterio usa \(> T\) (los píxeles iguales a \(T\) se convierten en fondo).
Tipo: El resultado final debe ser entero.
Observación: Este EP sigue la convención de OpenCV (cv2.THRESL_BINARY): solo los píxeles con valor mayor que\(T\) se convierten en blancos (255); los píxeles con valor igual a\(T\) permanecen negros (0).
4.20.1.3 🧠 Fundamentación Teórica
Parámetro
Tipo
Impacto Visual
\(T\) pequeño
Entero
La mayoría de los píxeles se vuelven blancos
\(T\) grande
Entero
La mayoría de los píxeles se vuelven negros
\(T\) bien elegido
Entero
Separa nítidamente objeto y fondo
4.20.1.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Línea 3: Entero \(T\).
Líneas siguientes: Elementos enteros de la matriz original.
Salida:
Matriz binarizada en \(L\) filas y \(C\) columnas, valores \(0\) o \(255\) separados por espacio.
4.20.1.5 📌 Ejemplos
Entrada
Salida
Observación
2
4
100
0 99 100 180
255 30 120 80
0 0 0 255
255 0 255 0
\(T=100\): solo los píxeles con valor mayor que 100 se vuelven blancos;
por eso, 99 y 100 se vuelven negros.
1
3
0
0 50 255
0 255 255
\(T=0\): solo los píxeles con valor estrictamente mayor que 0 se vuelven blancos.
👆 Haga clic en una celda de Entrada Original para oscurecer el píxel (−30) y haga clic con el botón derecho para aclarar (+30). Ajuste el umbral T para la binarización.
128
Entrada Original (Clicable)
Resultado Binarizado (p')
Fórmula aplicada: (p > 128) ? 255 : 0
Figura 4.30: Simulador EP04_01: Umbralización Global por Umbral Fijo (p’ = (p > T) ? 255 : 0)
%%writefile EP04_01.py# Código Python
Overwriting EP04_01.py
TestSuite("EP04_01.py").run()
✔️ EP04_01.cases ya existe en casos/
📋 7 caso(s) cargado(s) de casos/EP04_01.cases
🔍 Probando Python: EP04_01.py
⚠️ EP04_01.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.2 EP04_02 📊 Umbralización Automática de Otsu
Elegir manualmente el umbral \(T\) funciona cuando la iluminación es estable, pero en microscopía digital y en inspección de láminas de sangre, cada muestra tiene un contraste diferente — un umbral fijo fallaría de imagen en imagen. El método de Otsu resuelve esto encontrando, por sí solo, el umbral que maximiza la separación estadística entre las dos clases de píxeles, haciendo la segmentación automática y adaptativa. Ver en Figura 4.31 una simulación de este EP.
4.20.2.1 📋 Directrices de Implementación
Dimensiones: Leer los enteros \(L\) (filas) y \(C\) (columnas).
Datos: Leer los valores enteros de la matriz original fila por fila.
Histograma: Construir el histograma \(h[i]\), \(i=0,\dots,255\), contando cuántos píxeles tienen valor \(i\).
Búsqueda del umbral: Para cada candidato \(T\) de \(1\) a \(255\), calcular la varianza entre clases: \[
\sigma_B^2(T) = \frac{n_0 \cdot n_1}{N^2}\,(m_0 - m_1)^2
\] donde \(n_0,n_1\) son las cantidades de píxeles con valor \(<T\) y \(\geq T\), \(m_0,m_1\) son sus medias, y \(N=L\times C\).
Elección: El umbral óptimo \(T^*\) es el que maximiza \(\sigma_B^2(T)\) (en caso de empate, mantener el primero encontrado).
Aplicación: Binarizar la imagen usando \(T^*\), aplicando: \[
p' =
\begin{cases}
255, & \text{si } p > T^* \\
0, & \text{si } p \le T^*
\end{cases}
\]
4.20.2.2 📌 Restricciones Computacionales
Candidatos válidos: Ignorar \(T\) que deje \(n_0=0\) o \(n_1=0\) (clase vacía).
Empate: Mantener siempre el primer\(T\) que alcanzó el valor máximo de \(\sigma_B^2\).
Tipo:\(T^*\) y la matriz de salida deben ser enteros.
Convención OpenCV: La binarización sigue cv2.THRESL_BINARY; los píxeles con valor exactamente igual a \(T^*\) se vuelven negros.
4.20.2.3 🧠 Fundamentación Teórica
Concepto
Significado
Impacto
\(\sigma_B^2(T)\) alta
Clases bien separadas en \(T\)
\(T\) es un buen candidato a umbral
Histograma bimodal
Dos “picos” distintos
Otsu encuentra el valle entre ellos
Histograma unimodal
Un único “pico”
Otsu aún elige algún\(T\), pero la segmentación es poco fiable
4.20.2.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Líneas siguientes: Elementos enteros de la matriz original.
Salida:
Matriz binarizada en \(L\) filas y \(C\) columnas, valores \(0\) o \(255\).
👆 Clic izquierdo oscurece (−25) y clic con el botón derecho aclara (+25) los píxeles de la entrada. Observe el umbral óptimo T* ajustarse dinámicamente al histograma.
T* = −
Entrada Original (Clicable)
Resultado Otsu (p')
Figura 4.31: Simulador EP04_02: Limiarización Automática de Otsu (T* = argmax σ²_B(T))
%%writefile EP04_02.py# Código Python
Overwriting EP04_02.py
TestSuite("EP04_02.py").run()
✔️ EP04_02.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP04_02.cases
🔍 Probando Python: EP04_02.py
⚠️ EP04_02.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.3 EP04_03 🌱 Dilatación Binaria Plana (mm.dil0)
En microscopía de partículas y en OCR de placas de automóvil desgastadas, los trazos finos o discontinuos deben “engrosarse” para que el reconocimiento funcione. La dilatación morfológica hace exactamente eso: expande regiones claras usando un elemento estructurante \(B\) — la misma operación implementada en morph.py como mm.dil0(f, B), usada cuando \(B\) es plano (sin pesos, solo \(0\)/\(1\)). Ver en Figura 4.32 una simulación de este EP.
4.20.3.1 📋 Directrices de Implementación
Dimensiones de la imagen: Leer los enteros \(L\) (filas) y \(C\) (columnas) de \(f\).
Dimensiones de \(B\): Leer los enteros \(L_B\) (filas) y \(C_B\) (columnas) del elemento estructurante.
Elemento estructurante: Leer la matriz \(B\) con valores \(0\) o \(1\), fila a fila.
Datos: Leer la matriz \(f\) (la imagen original), fila a fila.
Reflexión: Construir \(B_{ref}\), la versión de \(B\) reflejada en \(180°\) (filas y columnas invertidas) — exactamente como hace mm.dil0 internamente.
Vecindad sin padding: Para cada píxel \((y,x)\), recorrer las posiciones \((by,bx)\) de \(B_{ref}\) centradas en \((y,x)\), usando el desplazamiento \[
v_y = y + by + o_y,\quad v_x = x + bx + o_x,\quad o_y=-\tfrac{L_B}{2}+0{,}5,\quad o_x=-\tfrac{C_B}{2}+0{,}5
\]Descartar todo \((v_y,v_x)\) fuera de \([0,L)\times[0,C)\) — no rellenar con ceros.
Mapeo: Calcular cada píxel de salida como el máximo entre \(f(y,x)\) y todos los \(f(v_y,v_x)\) válidos cuya posición correspondiente en \(B_{ref}\) vale \(1\): \[
g(y,x) = \max\Big(f(y,x),\ \max_{\substack{(v_y,v_x)\ \text{válido}\\ B_{ref}(by,bx)=1}} f(v_y,v_x)\Big)
\]
Salida: Mostrar la matriz \(g\) con dimensiones \(L \times C\).
4.20.3.2 📌 Restricciones Computacionales
Sin padding: Nunca inventar vecinos fuera de la imagen; usar solo los que existen realmente.
Reflexión obligatoria:\(B\) debe reflejarse antes de aplicarse (es lo que diferencia mm.dil0 de una simple búsqueda de máximo).
Robustez de borde: Si ninguna posición válida de \(B_{ref}=1\) cae dentro del dominio para un píxel dado, este mantiene su valor original.
4.20.3.3 🧠 Fundamentación Teórica
Concepto
Significado
Impacto Visual
Dilatación
\(g \geq f\) siempre (extensiva)
Las regiones claras crecen, los huecos oscuros se encogen
\(B\) mayor
Vecindad más amplia
Crecimiento más agresivo
Reflexión de \(B\)
\(B_{ref}(y,x) = B(-y,-x)\)
Garantiza la definición formal de Minkowski de la dilatación
4.20.3.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Línea 3: Entero \(L_B\).
Línea 4: Entero \(C_B\).
Siguientes \(L_B\) líneas: elementos enteros (\(0\) o \(1\)) de la matriz \(B\).
Siguientes \(L\) líneas: elementos enteros de la matriz \(f\).
Salida:
Matriz \(g\) en \(L\) filas y \(C\) columnas, valores enteros separados por espacio.
4.20.3.5 📌 Ejemplos
Entrada
Salida
Observación
3
3
3
3
0 1 0
1 1 1
0 1 0
0 0 0
0 9 0
0 0 0
0 9 0
9 9 9
0 9 0
\(B\) en cruz simétrico: punto aislado se expande en cruz
1
4
1
3
1 1 1
10 200 5 80
200 200 200 80
\(B\) horizontal: cada píxel “atrae” el máximo de los vecinos de la fila
🌱 Simulador EP04_03: Dilatación Plana (mm.dil0)g = f ⊕ B
Cambie el elemento estructurante B (o seleccione los preajustes) y haga clic en las celdas de la imagen original f para encender o apagar píxeles.
Elemento Estructurante B (Clic para Alternar 0/1)
Imagen Original f (5×5)
Dilatada g (f ⊕ B)
g(y,x) = máximo sobre vecinos válidos de B reflejado
Figura 4.32: Simulador EP04_03: Dilatación Binaria Plana (g = f ⊕ B)
%%writefile EP04_03.py# Código Python
Overwriting EP04_03.py
TestSuite("EP04_03.py").run()
✔️ EP04_03.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP04_03.cases
🔍 Probando Python: EP04_03.py
⚠️ EP04_03.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.4 EP04_04 🪨 Erosión Binaria Plana (mm.ero0)
Si la dilatación engrosa, la erosión afina. En sistemas de conteo de células, se utiliza para separar células que se tocan: al “comer” los bordes de cada región, las conexiones finas entre objetos desaparecen incluso antes de que se realice cualquier conteo. En morph.py, esta es la operación mm.ero0(f, B) — la dual exacta de la dilatación, y la única de las dos que no refleja el elemento estructurante. Ver en Figura 4.33 una simulación de este EP.
4.20.4.1 📋 Directrices de Implementación
Dimensiones de la imagen: Leer los enteros \(L\) (filas) y \(C\) (columnas) de \(f\).
Dimensiones de \(B\): Leer los enteros \(L_B\) (filas) y \(C_B\) (columnas) del elemento estructurante.
Elemento estructurante: Leer la matriz \(B\) con valores \(0\) o \(1\), fila a fila.
Datos: Leer la matriz \(f\) (la imagen original), fila a fila.
Vecindad sin padding (¡sin reflexión!): Para cada píxel \((y,x)\), recorrer las posiciones \((by,bx)\) de \(B\)en el orden original (sin reflejar), usando el mismo desplazamiento del EP04_03: \[
v_y = y + by + o_y,\quad v_x = x + bx + o_x,\quad o_y=-\tfrac{L_B}{2}+0{,}5,\quad o_x=-\tfrac{C_B}{2}+0{,}5
\]
Descartar todo \((v_y,v_x)\) fuera de \([0,L)\times[0,C)\). 6. Mapeo: Calcular cada píxel de salida como el mínimo entre \(f(y,x)\) y todos los \(f(v_y,v_x)\) válidos cuya posición correspondiente en \(B\) vale \(1\): \[
g(y,x) = \min\Big(f(y,x),\ \min_{\substack{(v_y,v_x)\ \text{válido}\\ B(by,bx)=1}} f(v_y,v_x)\Big)
\] 7. Salida: Mostrar la matriz \(g\) con dimensiones \(L \times C\).
4.20.4.2 📌 Restricciones Computacionales
Sin reflexión: A diferencia de la dilatación, \(B\) se usa exactamente como se lee — reflejarlo aquí sería un error conceptual grave.
Sin padding: Los vecinos fuera de la imagen simplemente se ignoran, nunca se tratan como \(0\).
Robustez de borde: Si ninguna posición válida de \(B=1\) cae dentro del dominio, el píxel mantiene su valor original.
4.20.4.3 🧠 Fundamentación Teórica
Concepto
Significado
Impacto Visual
Erosión
\(g \leq f\) siempre (anti-extensiva)
Las regiones claras se encogen, el ruido puntual desaparece
Dualidad
\(\text{ero}(f,B) = -\text{dil}(-f, B_{ref})\)
La erosión y la dilatación son “espejos” matemáticos
\(B\) más grande
Erosión más agresiva
Los objetos finos desaparecen por completo
4.20.4.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Línea 3: Entero \(L_B\).
Línea 4: Entero \(C_B\).
Siguientes \(L_B\) líneas: elementos enteros (\(0\) o \(1\)) de la matriz \(B\).
Siguientes \(L\) líneas: elementos enteros de la matriz \(f\).
Salida:
Matriz \(g\) en \(L\) filas y \(C\) columnas, valores enteros separados por espacios.
4.20.4.5 📌 Ejemplos
Entrada
Salida
Observación
3
3
3
3
0 1 0
1 1 1
0 1 0
9 9 9
9 0 9
9 9 9
9 0 9
0 0 0
9 0 9
El “agujero” central (0) se propaga en cruz
1
4
1
3
1 1 1
10 200 5 80
10 5 5 80
\(B\) horizontal: cada píxel “extrae” el mínimo de los vecinos de la fila
🪨 Simulador EP04_04: Erosión Plana (mm.ero0)g = f ⊖ B
Alterne el elemento estructurante B (o seleccione los presets) y haga clic en las celdas de la imagen original f para encender o apagar píxeles.
Elemento Estructurante B (Clic para Alternar 0/1)
Imagen Original f (5×5)
Erosionada g (f ⊖ B)
g(y,x) = mínimo sobre vecinos válidos de B (sin reflejar)
Figura 4.33: Simulador EP04_04: Erosión Binaria Plana (g = f ⊖ B)
%%writefile EP04_04.py# Código Python
Overwriting EP04_04.py
TestSuite("EP04_04.py").run()
✔️ EP04_04.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP04_04.cases
🔍 Probando Python: EP04_04.py
⚠️ EP04_04.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.5 EP04_05 🧹 Apertura Morfológica (Eliminación de Ruido)
Las imágenes capturadas por sensores de bajo costo, como los de drones agrícolas, suelen venir salpicadas de pequeños puntos de ruido — píxeles aislados que no representan nada real. Aplicar erosión seguida de dilatación con el mismo elemento estructurante produce la apertura: esta “limpia” puntos y protuberancias finas, pero devuelve al objeto principal prácticamente su tamaño original. Es la combinación clásica utilizada en preprocesamiento de imágenes de satélite antes de cualquier conteo de área plantada. Ver en Figura 4.34 una simulación de este EP.
4.20.5.1 📋 Directrices de Implementación
Dimensiones de la imagen: Leer los enteros \(L\) (filas) y \(C\) (columnas) de \(f\).
Dimensiones de \(B\): Leer los enteros \(L_B\) (filas) y \(C_B\) (columnas) del elemento estructurante.
Elemento estructurante: Leer la matriz \(B\) con valores \(0\) o \(1\), fila a fila.
Datos: Leer la matriz binaria \(f\) (valores \(0\) o \(1\)), fila a fila.
Erosión: Calcular \(e = f \ominus B\), usando exactamente el algoritmo del EP04_04 (sin reflejar \(B\), sin padding).
Dilatación: Calcular \(g = e \oplus B\), usando exactamente el algoritmo del EP04_03 (reflejando \(B\), sin padding) — pero ahora aplicado sobre \(e\), no sobre \(f\).
Salida: Mostrar la matriz resultante \(g\) (la apertura de \(f\) por \(B\)) con dimensiones \(L \times C\).
4.20.5.2 📌 Restricciones Computacionales
Orden fijo: Es siempre erosión primero, luego dilatación — el orden inverso define otro operador (cierre, del próximo EP).
Mismo \(B\): El elemento estructurante utilizado en la erosión y en la dilatación debe ser idéntico.
Sin padding en ninguna de las dos etapas.
4.20.5.3 🧠 Fundamentación Teórica
Concepto
Significado
Impacto Visual
Antiextensividad
\(g \subseteq f\) siempre
La apertura nunca crea un píxel nuevo, solo elimina
Los puntos aislados y la protuberancia fina desaparecen; el cuadrado central sobrevive
🧹 Simulador EP04_05: Apertura Morfológicag = (f ⊖ B) ⊕ B
Haz clic en las celdas de f original para encender o apagar píxeles (¡crea tu propio ruido de fondo!) y ajusta el tamaño del elemento estructurante B.
3×3
f Original (Clicable)
e = f ⊖ B (Erosión)
g = e ⊕ B (Apertura)
Figura 4.34: Simulador EP04_05: Apertura Morfológica (g = (f ⊖ B) ⊕ B)
%%writefile EP04_05.py# Código Python
Overwriting EP04_05.py
TestSuite("EP04_05.py").run()
✔️ EP04_05.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP04_05.cases
🔍 Probando Python: EP04_05.py
⚠️ EP04_05.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.6 EP04_06 🧩 Cierre Morfológico (Relleno de Huecos)
En la digitalización de huellas dactilares, los surcos de la piel a veces se ven interrumpidos por suciedad o sequedad, creando pequeñas fallas en la curva continua que debería existir. El cierre —dilatación seguida de erosión con el mismo elemento estructurante— es el operador dual de la apertura: rellena huecos pequeños y entrantes estrechos, sin alterar significativamente el contorno externo del objeto. Es el paso estándar antes de extraer el esqueleto de una huella dactilar. Ver en Figura 4.35 una simulación de este EP.
4.20.6.1 📋 Directrices de Implementación
Dimensiones de la imagen: Leer los enteros \(L\) (líneas) y \(C\) (columnas) de \(f\).
Dimensiones de \(B\): Leer los enteros \(L_B\) (líneas) y \(C_B\) (columnas) del elemento estructurante.
Elemento estructurante: Leer la matriz \(B\) con valores \(0\) o \(1\), línea por línea.
Datos: Leer la matriz binaria \(f\) (valores \(0\) o \(1\)), línea por línea.
Dilatación: Calcular \(d = f \oplus B\), usando exactamente el algoritmo del EP04_03 (reflejando \(B\), sin padding).
Erosión: Calcular \(g = d \ominus B\), usando exactamente el algoritmo del EP04_04 (sin reflejar \(B\), sin padding) — ahora aplicado sobre \(d\), no sobre \(f\).
Salida: Mostrar la matriz resultante \(g\) (el cierre de \(f\) por \(B\)) con dimensiones \(L \times C\).
4.20.6.2 📌 Restricciones Computacionales
Orden fijo: Es siempre dilatación primero, luego erosión — el orden inverso es la apertura del EP04_05.
Mismo \(B\): El elemento estructurante usado en la dilatación y en la erosión debe ser idéntico.
Los dos huecos internos no adyacentes se rellenan totalmente
🧩 Simulador EP04_06: Cierre Morfológicog = (f ⊕ B) ⊖ B
Haz clic en las celdas de f original para encender o apagar píxeles (¡rellena huecos internos!) y ajusta el tamaño del elemento estructurante B.
3×3
f Original (Clickeable)
d = f ⊕ B (Dilatación)
g = d ⊖ B (Cierre)
Figura 4.35: Simulador EP04_06: Cierre Morfológico (g = (f ⊕ B) ⊖ B)
%%writefile EP04_06.py# Código Python
Overwriting EP04_06.py
TestSuite("EP04_06.py").run()
✔️ EP04_06.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP04_06.cases
🔍 Probando Python: EP04_06.py
⚠️ EP04_06.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.7 EP04_07 ⛰️ Dilatación y Erosión con Pesos (mm.dil1 / mm.ero1)
Hasta ahora, el elemento estructurante solo decía “este vecino cuenta” o “no cuenta” — pero en modelos digitales de elevación (usados en SIG y en planificación de drenaje urbano), cada vecino debería tener un peso diferente dependiendo de la distancia o de la dirección del relieve. Las versiones ponderadas de la dilatación y de la erosión, implementadas en morph.py como mm.dil1(f, b) y mm.ero1(f, b), suman (o restan) el peso de cada vecino antes de tomar el máximo (o mínimo) — generalizando todo lo realizado en los EPs anteriores. Ver en Figura 4.36 una simulación de este EP.
4.20.7.1 📋 Directrices de Implementación
Dimensiones de la imagen: Leer los enteros \(L\) (filas) y \(C\) (columnas) de \(f\).
Dimensiones de \(b\): Leer los enteros \(L_B\) (filas) y \(C_B\) (columnas) del elemento estructurante ponderado.
Pesos: Leer la matriz \(b\) de pesos enteros (pueden ser negativos, cero o positivos), fila a fila.
Datos: Leer la matriz \(f\) (la imagen original), fila a fila.
Vecindario sin padding: Para cada píxel \((y,x)\), recorrer todas las posiciones \((by,bx)\) de \(b\) (no solo donde valdría \(1\) — aquí todo peso participa), usando el mismo desplazamiento de los EPs anteriores: \[
v_y = y + by + o_y,\quad v_x = x + bx + o_x,\quad o_y=-\tfrac{L_B}{2}+0{,}5,\quad o_x=-\tfrac{C_B}{2}+0{,}5
\]Descartar todo \((v_y,v_x)\) fuera de \([0,L)\times[0,C)\).
Erosión ponderada: Calcular, usando el mismo \(b\) y sin reflejar: \[
g_{ero}(y,x) = \min\Big(f(y,x),\ \min_{(v_y,v_x)\ \text{válido}} \big(f(v_y,v_x) - b(by,bx)\big)\Big)
\]
Salida: Mostrar primero la matriz \(g_{dil}\) completa, y después la matriz \(g_{ero}\) completa.
4.20.7.2 📌 Restricciones Computacionales
Ninguna de las dos refleja \(b\) — la versión ponderada no usa reflexión, incluso en la dilatación (diferente de mm.dil0).
Todos los pesos participan: No existe aquí el filtro “\(B=1\)”; incluso el peso \(0\) entra en la cuenta.
Sin padding: los vecinos fuera de la imagen se ignoran, nunca se rellenan virtualmente.
Tipo: La salida puede contener valores negativos o mayores que \(255\) — no hay clipping en este EP.
Consejo: Para eliminar mensajes de desbordamiento al superar los límites del tipo uint8, incluir al inicio del código:
import warningswarnings.filterwarnings("ignore")
4.20.7.3 🧠 Fundamentación Teórica
Concepto
Significado
Impacto Visual
Peso positivo
“Empuja” el valor del vecino hacia arriba en la dilatación
Simula relieve que asciende en esa dirección
Peso negativo
Reduce la contribución del vecino
Simula distancia o atenuación direccional
Dualidad ponderada
\(\text{ero1}(f,b) = -\text{dil1}(-f,b)\)
La simetría entre las dos operaciones se mantiene incluso con pesos
4.20.7.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Línea 3: Entero \(L_B\).
Línea 4: Entero \(C_B\).
Siguientes \(L_B\) líneas: elementos enteros (pueden ser negativos) de la matriz \(b\).
Siguientes \(L\) líneas: elementos enteros de la matriz \(f\).
Salida:
Primero la matriz \(g_{dil}\) en \(L\) líneas y \(C\) columnas.
A continuación la matriz \(g_{ero}\) en \(L\) líneas y \(C\) columnas.
Peso central \(2\) acelera el crecimiento en la dilatación y la contracción en la erosión
⛰️ Simulador EP04_07: Pesos en el Elemento Estructurantedil1 / ero1
Ajuste los pesos del elemento estructurante b con los controles deslizantes y observe el efecto de la dilatación y erosión con pesos sobre la matriz f.
Pesos b (Ajuste los Sliders por Celda)
f Original
dil1(f, b) (Dilatación)
ero1(f, b) (Erosión)
Figura 4.36: Simulador EP04_07: Dilatación y Erosión con Pesos (mm.dil1 / mm.ero1)
%%writefile EP04_07.py# Código Python
Overwriting EP04_07.py
TestSuite("EP04_07.py").run()
✔️ EP04_07.cases ya existe en casos/
📋 3 caso(s) cargado(s) de casos/EP04_07.cases
🔍 Probando Python: EP04_07.py
⚠️ EP04_07.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.8 EP04_08 🌋 Gradiente morfológico, Top-hat y Black-hat
En la inspección automática de placas de circuito, tres preguntas aparecen todo el tiempo: ¿dónde están los bordes de los componentes? ¿Qué detalles claros y pequeños (como puntos de soldadura) se destacan del fondo? ¿Qué reentrancias oscuras (como fisuras) esconde el fondo? Un único par erosión/dilatación responde a las tres: el gradiente morfológico evidencia contornos, el top-hat revela picos estrechos, y el black-hat revela valles estrechos — tres herramientas, una sola vecindad. Ver en Figura 4.37 una simulación de este EP.
4.20.8.1 📋 Directrices de Implementación
Dimensiones de la imagen: Leer los enteros \(L\) (filas) y \(C\) (columnas) de \(f\).
Dimensiones de \(B\): Leer los enteros \(L_B\) (filas) y \(C_B\) (columnas) del elemento estructurante.
Elemento estructurante: Leer la matriz \(B\) con valores \(0\) o \(1\), línea a línea.
Datos: Leer la matriz \(f\) (la imagen original, en tonos de gris), línea a línea.
Operadores de base: Calcular, exactamente como en los EPs 04_03 a 04_06:
(gradiente: halo \(3\times3=70\) en torno a \((2,2)\) y halo \(3\times3=8\) en torno a \((6,6)\), resto \(0\))
(top-hat: único \(70\) en \((2,2)\), resto \(0\))
(black-hat: único \(8\) en \((6,6)\), resto \(0\))
Pico aislado se convierte en top-hat; valle aislado se convierte en black-hat; ambos aparecen en el gradiente
Agregue picos o valles en la matriz f y observe el comportamiento simultáneo de los operadores de gradiente, top-hat y black-hat.
f (Entrada)
Gradiente
Top-hat
Black-hat
Figura 4.37: Simulador EP04_08: Gradiente Morfológico, Top-hat y Black-hat
%%writefile EP04_08.py# Código Python
Overwriting EP04_08.py
TestSuite("EP04_08.py").run()
✔️ EP04_08.cases ya existe en casos/
📋 4 caso(s) cargado(s) de casos/EP04_08.cases
🔍 Probando Python: EP04_08.py
⚠️ EP04_08.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.9 EP04_09 🗺️ Transformada de Distancia y el “Centro” del Objeto
En robótica móvil, al planificar una ruta dentro de un pasillo, el robot quiere saber no solo dónde hay espacio libre, sino también qué tan lejos está cada punto libre de la pared más cercana. Las rutas más seguras tienden a pasar por el “centro” del pasillo, lejos de los obstáculos.
La transformada de distancia morfológica asigna a cada píxel un valor que representa su distancia hasta el borde más cercano, según la métrica definida por el elemento estructurante. Los píxeles cercanos al borde reciben valores bajos, mientras que los píxeles más internos reciben valores mayores. El píxel de valor máximo corresponde a la región más protegida del objeto, frecuentemente asociada a su centro morfológico.
Dimensiones de la imagen: leer los enteros \(L\) (filas) y \(C\) (columnas) de la imagen \(f\).
Dimensiones de \(B\): leer los enteros \(L_B\) (filas) y \(C_B\) (columnas) del elemento estructurante.
Elemento estructurante: leer la matriz \(b\), que contiene el valor \(0\) en el centro y valores negativos en las demás posiciones.
Imagen: leer la matriz binaria \(f\) (valores \(0\) o \(1\)), fila a fila.
Preparación: multiplicar la imagen por \(L\times C\), asegurando que los píxeles internos tengan un valor inicial suficientemente alto para la propagación de las distancias.
Transformada de distancia: calcular la matriz de distancias utilizando el método mm.dist1(f,b).
Salida: mostrar la matriz resultante de la transformada de distancia.
4.20.9.2 📌 Restricciones Computacionales
Utilizar la implementación de erosión ponderada proporcionada por la biblioteca.
El elemento estructurante puede contener valores negativos arbitrarios.
La transformada debe obtenerse mediante la aplicación iterativa de erosiones ponderadas hasta alcanzar un punto fijo.
⚠️ Nota Crucial sobre Lectura de Matrices: Como el elemento estructurante puede contener valores enteros negativos (por ejemplo, -1 y -99), no utilice la función mm.readImg para leer la matriz \(b\). Esa función convierte los datos al tipo uint8, provocando underflow y corrompiendo los valores negativos. Lea las \(L_B\) filas de \(b\) manualmente utilizando el tipo estándar int. La imagen \(f\) puede seguir leyéndose normalmente con mm.readImg.
4.20.9.3 🧠 Fundamentación Teórica
Concepto
Significado
Impacto Visual
\(\text{dist}(y,x)\)
Distancia morfológica hasta el borde más cercano según la métrica definida por \(b\)
Los píxeles más internos reciben valores mayores
Valor máximo
Píxel más distante del borde
Aproxima el centro morfológico del objeto
Elemento estructurante ponderado
Define los costos de desplazamiento entre píxeles vecinos
Determina la métrica de distancia utilizada
Objetos finos
Regiones estrechas del objeto
Producen valores bajos de distancia
4.20.9.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: entero \(L\).
Línea 2: entero \(C\).
Línea 3: entero \(L_B\).
Línea 4: entero \(C_B\).
Siguientes \(L_B\) líneas: elementos enteros de la matriz \(b\).
Siguientes \(L\) líneas: elementos binarios (\(0\) o \(1\)) de la matriz \(f\).
⚠️ Nota de implementación: Los elementos de la matriz \(f\) (0 o 1) deben multiplicarse por 255 para generar una imagen binaria adecuada (\(0\) y \(255\)) antes de aplicar la Transformada de Distancia (TD).
Salida:
Matriz de la transformada de distancia en \(L\) filas y \(C\) columnas.
Nota: el valor -99 actúa como una aproximación práctica de \(-\infty\), impidiendo la propagación por las diagonales. De esta forma, solo los vecinos horizontal y vertical contribuyen a la distancia, produciendo la distancia de Manhattan.
🗺️ Simulador EP04_09: Transformada de DistanciaCapas de Erosión
Haz clic en las celdas para dibujar tu propio objeto o selecciona una forma predefinida para calcular el mapa de distancias en cascada.
Mapa de Distancias Calculado
Figura 4.38: Simulador EP04_09: Transformada de Distância (Camadas de Erosión)
%%writefile EP04_09.py# Código Python
Overwriting EP04_09.py
TestSuite("EP04_09.py").run()
✔️ EP04_09.cases ya existe en casos/
📋 4 caso(s) cargado(s) de casos/EP04_09.cases
🔍 Probando Python: EP04_09.py
⚠️ EP04_09.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
4.20.10 EP04_10 🪙 Separación de Blobs, Etiquetado y Descriptores
En una línea de producción de monedas, es común que las piezas se toquen entre sí en la cinta transportadora, formando una única mancha conectada en la imagen — un conteo ingenuo erraría el total. La solución clásica combina operaciones morfológicas y análisis de conectividad: primero una erosión reduce o rompe conexiones frágiles entre objetos, y luego el etiquetado de componentes conectados separa cada objeto en una región distinta. Finalmente, descriptores geométricos (área y caja delimitadora) resumen cada componente encontrado.
Dimensiones de la imagen: leer los enteros \(L\) (filas) y \(C\) (columnas) de \(f\).
Dimensiones de \(B\): leer los enteros \(L_B\) (filas) y \(C_B\) (columnas) del elemento estructurante.
Elemento estructurante: leer la matriz \(B\), que contiene valores \(0\) o \(1\), fila a fila.
Datos: leer la matriz binaria \(f\) (valores \(0\) o \(1\)), fila a fila.
Separación: calcular \[
f_{ero} = f \ominus B
\] usando erosión binaria plana (como en el EP04_04), eliminando conexiones frágiles entre objetos.
Etiquetado: sobre \(f_{ero}\), identificar componentes conectados usando conectividad definida por la vecindad \(B\). El etiquetado debe seguir un barrido raster: al encontrar un píxel \(1\) aún no etiquetado, asignar una nueva etiqueta entera creciente a partir de 1 y propagar esa etiqueta a toda la región conectada.
Descriptores: para cada etiqueta \(k\), calcular:
Área: número de píxeles pertenecientes a la etiqueta;
Salida: mostrar el número total de etiquetas y, a continuación, una línea por etiqueta en el formato: \[
k,\ \text{área},\ y_{min},\ x_{min},\ y_{max},\ x_{max}
\]
4.20.10.2 📌 Restricciones Computacionales
La erosión debe aplicarse antes del etiquetado.
La conectividad es fija y está definida por la vecindad anterior.
El elemento estructurante \(B\) no interfiere en la conectividad del etiquetado.
Sin padding en ninguna etapa.
El orden de las etiquetas sigue la primera detección en el barrido raster.
4.20.10.3 🧠 Fundamentación Teórica
Concepto
Significado
Impacto
Puente fino
Conexión estrecha entre objetos
Puede ser eliminado por la erosión morfológica
Conectividad
Definida por el conjunto \[\mathcal{N}(y,x)\]
Determina qué píxeles pertenecen al mismo componente
Área
Número de píxeles por componente
Estimación directa del tamaño del objeto
Caja delimitadora
Extensión espacial de la etiqueta
Resumen geométrico del componente
4.20.10.4 📦 Especificación de Entrada y Salida (VPL)
Ajusta el grosor del puente entre las monedas y observa cómo la erosión morfológica separa los objetos para el conteo y la extracción de descriptores (área y cuadro delimitador).
1 píxel
f Original (Pegadas)
Tras Erosión + Etiquetas
Figura 4.39: Simulador EP04_10: Separación de Blobs, Etiquetado y Descriptores
%%writefile EP04_10.py# Código Python
Overwriting EP04_10.py
TestSuite("EP04_10.py").run()
✔️ EP04_10.cases ya existe en casos/
📋 4 caso(s) cargado(s) de casos/EP04_10.cases
🔍 Probando Python: EP04_10.py
⚠️ EP04_10.py: archivo vacío (menos de 3 líneas). Pruebas omitidas.
MATHERON, Georges. Random Sets and Integral Geometry. New York: John Wiley & Sons, 1975.