3Operaciones Espaciales: Intensidad, Histograma y Filtrado
Este capítulo profundiza en el procesamiento de imágenes en el dominio espacial, partiendo de la manipulación directa de píxeles e histogramas para el realce de contraste, hasta la aplicación de filtros locales por convolución para suavizado, reducción de ruido y detección de bordes. El objetivo es desarrollar la intuición matemática y computacional que sustenta gran parte de los algoritmos modernos de Visión Computacional.
3.1 Objetivos
Al final de este capítulo, usted será capaz de:
Manipular intensidad y píxeles: Ejecutar operaciones aritméticas saturadas (mm::addm, mm::subm) y lógicas bit a bit (mm::band, mm::bor, mm::bnot) para combinación y selección de regiones de interés (ROI), y aplicar alpha blending (mm::blend) para fusión ponderada de imágenes;
Procesar histogramas: Interpretar el histograma como diagnóstico tonal y aplicar ecualización global mediante CDF (mm::equalize); en la ruta Python, también la ecualización adaptativa (CLAHE) y la especificación de histograma para transferencia de perfil tonal entre imágenes;
Comprender fundamentos espaciales: Entender vecindad, padding de borde (mm::pad) y la diferencia entre correlación cruzada (mm::conv) y convolución — incluyendo por qué kernels asimétricos como el de Sobel producen resultados distintos en las dos operaciones;
Aplicar filtrado de suavizado: Usar el filtro de media (mm::blur, o mm::conv con kernel uniforme) y el filtro Gaussiano (mm::gaussian) para reducción de ruido, comprendiendo la ventaja de la ponderación radial y de la separabilidad Gaussiana;
Aplicar filtrado de realce: Usar el Laplaciano \(w_4\) y \(w_8\) (mm::laplacian) para realce isotrópico de bordes, el operador de Sobel (mm::sobel) para la magnitud del gradiente — y, en la ruta Python, la descomposición direccional \(G_x\), \(G_y\) y ángulo —, y el Unsharp Masking (mm::usm) para amplificación de alta frecuencia controlada por el parámetro \(k\);
Utilizar filtros de orden: Aplicar el filtro de la mediana (mm::median) para eliminación de ruido sal y pimienta, comprendiendo por qué su naturaleza no lineal y la robustez a outliers lo hacen superior a los filtros lineales en ese escenario;
Resolver problemas prácticos: Encadenar técnicas en pipelines de preprocesamiento (ecualización → Gaussiano → Canny; con CLAHE en lugar de la ecualización en la ruta Python) y usar las funciones de morph (mm::conv, mm::histImg, mm::equalize, mm::drawImgKernel) para análisis y visualización didáctica de cada etapa.
3.2 Operaciones a Nivel de Intensidad
El nivel más elemental de procesamiento de imágenes actúa directamente sobre los valores de los píxeles, sin considerar la vecindad. Estas operaciones —llamadas transformaciones de punto (point operations)— son las más rápidas computacionalmente y forman la base para técnicas más complejas.
Formalmente, una transformación de punto puede describirse como:
\[
g(x,y) = T[f(x,y)]
\tag{3.1}\]
donde \(f(x,y)\) es la imagen de entrada, \(g(x,y)\) es la salida y \(T\) es una función aplicada a cada píxel individualmente.
3.2.1 Preparando el Entorno Práctico
El siguiente bloque carga la biblioteca morph del repositorio (el módulo morph.py y, en la ruta C++, también el morph.hpp utilizado en el #include de las celdas compiladas).
import os, urllib.requestos.makedirs("tmp/state", exist_ok=True) # artefatos de build de la pista C++ (.cpp, binario, PNGs)url ="https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph/config.py"ifnot os.path.exists("config.py"): urllib.request.urlretrieve(url, "config.py")# El kernel es Python incluso en la pista C++: `mm` (morph.py) es usado por los# simuladores, por la visualización de las figuras que el binario C++ genera y por el# estado mm::Image entre celdas. cpp=True también descarga la pista compilada# (morph.hpp + stb_image*.h), usada en el #include de las celdas %%writefile *.cpp.import configconfig.setup(cpp=True)from morph import mmimport numpy as np
✅ Entorno listo. Morph: 1.1.9 | OpenCV: 5.0.0
Como objeto de estudio a lo largo de este capítulo, utilizaremos las imágenes de vida silvestre presentadas en las Figura 3.1 y Figura 3.3.. A partir de ellas, exploraremos operaciones espaciales sobre intensidad, histogramas y filtrado, analizando sus efectos en el realce, la suavización, la reducción de ruido y la detección de bordes, con el fin de comprender los fundamentos matemáticos y computacionales del PDI.
%%writefile tmp/fig_03_mandrill.cpp#define MM_OUT "tmp/fig_03_mandrill.png"//| label: fig-03-mandrill//| fig-cap: "*Mandrill* (*Mandrillus sphinx*) fotografado em ambiente natural na África do Sul. Crédito: Carlos Guilherme Rodrigues (CC BY-SA 3.0)."//| echo: true#include "morph.hpp"#include <iostream>#include <filesystem>int main() { std::string base ="https://upload.wikimedia.org/wikipedia/commons"; std::string arquivo ="Carlos_Guilherme_Rodrigues_%2876515283%29.jpeg"; std::string url = base +"/9/9b/"+ arquivo; mm::Image img_color = mm::read(url); mm::Image img_gray = mm::gray(img_color); mm::show(img_color, MM_OUT);// [pdi:state-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp/state");mm::write(img_gray, "tmp/state/img_gray_8.png");// [pdi:state-io:end]return0;}
Overwriting tmp/fig_03_mandrill.cpp
!g++-I. -std=c++17 tmp/fig_03_mandrill.cpp -o tmp/fig_03_mandrill \&& ./tmp/fig_03_mandrill \&& test -f "tmp/fig_03_mandrill.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_mandrill.png"
try: mm.show(mm.read("tmp/fig_03_mandrill.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_mandrill.png (ver a versao Python)")
Figura 3.1: Mandrill (Mandrillus sphinx) fotografado em ambiente natural na África do Sul. Crédito: Carlos Guilherme Rodrigues (CC BY-SA 3.0).
3.2.2 Operaciones Aritméticas
Las operaciones aritméticas entre imágenes se utilizan ampliamente en PDI para combinar, comparar o realzar información. La resta de imágenes es especialmente poderosa para detectar diferencias entre dos cuadros — por ejemplo, en la eliminación del fondo estático en cámaras de vigilancia:
\[
g(x,y) = f_1(x,y) - f_2(x,y)
\tag{3.2}\]
La suma saturada limita el resultado al intervalo \([0, 255]\): los valores superiores a 255 se fijan en 255, evitando el overflow silencioso del tipo uint8 (p. ej., \(200 + 100 = 44\) en lugar de 300). La resta saturada aplica el mismo principio por el lado inferior: los valores negativos se fijan en 0.
AdvertenciaSaturación y overflow
Las operaciones aritméticas en uint8 sufren overflow silencioso: \(200 + 100 = 44\) (no 300). mm::addm y mm::subm realizan la saturación automática, fijando el resultado en \([0, 255]\). El blending utiliza pesos fraccionarios: mm::blend opera internamente en punto flotante y solo entonces redondea y satura a uint8.
La Figura 3.2 demuestra la suma de una constante (aclarado) y la resta de una constante (oscurecimiento con saturación en 0).
%%writefile tmp/fig_03_aritmetica.cpp#define MM_OUT "tmp/fig_03_aritmetica.png"//| label: fig-03-aritmetica//| fig-cap: "Operações aritméticas saturadas: adição de constante (clareamento) e subtração de constante (escurecimento com saturação em 0)."//| echo: true//| output: true#include "morph.hpp"#include <iostream>#include <vector>#include <string>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_8.png");// [pdi:state-io:end]int fundo =60; mm::Image img_add = mm::addm(img_gray, fundo); mm::Image img_sub = mm::subm(img_gray, fundo); mm::show( std::vector<mm::Image>{img_gray, img_add, img_sub}, MM_OUT, std::vector<std::string>{"Original", "addm (+60)", "subm (−60)"},3 );// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray, "tmp/fig_03_aritmetica_0.png");mm::write(img_add, "tmp/fig_03_aritmetica_1.png");mm::write(img_sub, "tmp/fig_03_aritmetica_2.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_aritmetica.cpp
!g++-I. -std=c++17 tmp/fig_03_aritmetica.cpp -o tmp/fig_03_aritmetica \&& ./tmp/fig_03_aritmetica \&& test -f "tmp/fig_03_aritmetica.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_aritmetica.png"
Cuando \(\alpha = 1\), se obtiene únicamente la imagen \(f_1\); cuando \(\alpha = 0\), solo \(f_2\). Los valores intermedios producen una transición suave entre ambas, siendo ampliamente utilizados en composición de imágenes, superposición de capas, marcas de agua y efectos de fusión visual.
Para que la combinación produzca un resultado coherente, es necesario alinear previamente las regiones de interés. En la Figura 3.4, se recorta el rostro del leopardo con mm::crop(img_leop_gray, 250, H-300, 100, W-200) y la región facial del mandril con mm::crop(img_gray, 100, 400, 380, 530), de modo que los ojos y la estructura facial queden aproximadamente alineados. El recorte del leopardo se redimensiona entonces (mm::resize) a las dimensiones del mandril antes de la mezcla.
mm::blend realiza la operación en punto flotante — evitando overflow en los cálculos con pesos fraccionarios — y solo entonces redondea y satura el resultado a uint8.
%%writefile tmp/fig_03_leopardo.cpp#define MM_OUT "tmp/fig_03_leopardo.png"// Compile: g++-std=c++17-o programa programa.cpp -I. -lcurl -lpng -ljpeg#include "morph.hpp"#include <iostream>#include <filesystem>int main() {//| label: fig-03-leopardo//| fig-cap: "Retrato de um leopardo (*Panthera pardus*) em ambiente natural. Crédito: C. Brück (CC BY-SA 4.0)."//| echo: true mm::Image img_leop = mm::read("https://upload.wikimedia.org/wikipedia/commons/9/92/Leopard_%28Panthera_pardus%29_portrait.jpg"); mm::Image img_leop_gray = mm::gray(img_leop); mm::show(img_leop, MM_OUT);// [pdi:state-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp/state");mm::write(img_leop, "tmp/state/img_leop_12.png");mm::write(img_leop_gray, "tmp/state/img_leop_gray_12.png");// [pdi:state-io:end]return0;}
Overwriting tmp/fig_03_leopardo.cpp
!g++-I. -std=c++17 tmp/fig_03_leopardo.cpp -o tmp/fig_03_leopardo \&& ./tmp/fig_03_leopardo \&& test -f "tmp/fig_03_leopardo.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_leopardo.png"
try: mm.show(mm.read("tmp/fig_03_leopardo.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_leopardo.png (ver a versao Python)")
Figura 3.3: Retrato de um leopardo (Panthera pardus) em ambiente natural. Crédito: C. Brück (CC BY-SA 4.0).
%%writefile tmp/fig_03_blend.cpp#define MM_OUT "tmp/fig_03_blend.png"//| label: fig-03-blend//| fig-cap: "*Alpha blending* entre recortes alinhados de mandrill e do leopardo (@fig-03-leopardo) para diferentes valores de α. Em α=1 vê-se apenas mandrill; em α=0, apenas o leopardo; valores intermediários fundem os olhares das duas imagens proporcionalmente."//| echo: true//| output: true#include "morph.hpp"#include <vector>#include <string>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_8.png");mm::Image img_leop_gray = mm::_read_state("tmp/state/img_leop_gray_12.png");// [pdi:state-io:end]// recortes alinhados: rosto do leopardo e região facial do mandril mm::Image leo = mm::crop(img_leop_gray, 250, img_leop_gray.h -300, 100, img_leop_gray.w -200); mm::Image mandrill = mm::crop(img_gray, 100, 400, 380, 530); mm::Image leo_r = mm::resize(leo, mandrill.w, mandrill.h, "bilinear"); mm::show( std::vector<mm::Image>{mm::blend(mandrill, leo_r, 1.0), mm::blend(mandrill, leo_r, 0.8), mm::blend(mandrill, leo_r, 0.6), mm::blend(mandrill, leo_r, 0.4), mm::blend(mandrill, leo_r, 0.2), mm::blend(mandrill, leo_r, 0.0)}, MM_OUT, std::vector<std::string>{"α=1.0", "α=0.8", "α=0.6", "α=0.4", "α=0.2", "α=0.0"},6 );return0;}
Overwriting tmp/fig_03_blend.cpp
!g++-I. -std=c++17 tmp/fig_03_blend.cpp -o tmp/fig_03_blend \&& ./tmp/fig_03_blend \&& test -f "tmp/fig_03_blend.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_blend.png"
try: mm.show(mm.read("tmp/fig_03_blend.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_blend.png (ver a versao Python)")
Figura 3.4: Alpha blending entre recortes alinhados de mandrill e do leopardo (Figura 3.3) para diferentes valores de α. Em α=1 vê-se apenas mandrill; em α=0, apenas o leopardo; valores intermediários fundem os olhares das duas imagens proporcionalmente.
3.2.4 Operaciones Lógicas y Máscaras Bit a Bit
Las operaciones lógicas bit a bit (AND, OR y NOT) actúan directamente sobre los bits de cada píxel y son la base para la creación y aplicación de máscaras (masks) —imágenes binarias con solo 0 (negro) y 255 (blanco) utilizadas para aislar Regiones de Interés (ROI).
El comportamiento de cada operación se deriva de la representación binaria del 255 (11111111) y del 0 (00000000):
AND con la máscara: donde \(m = 255\), los bits originales se preservan; donde \(m = 0\), el píxel se pone a cero. Resultado: recorte de la ROI. \[g(x,y) = f(x,y) \;\text{AND}\; m(x,y)
\tag{3.4}\]
OR con la máscara: donde \(m = 255\), el píxel se fuerza a blanco; donde \(m = 0\), se mantiene el valor original. Resultado: iluminación de la ROI.
NOT (sin máscara): invierte todos los bits (\(g = 255 - f\)), produciendo el negativo fotográfico de la imagen.
La Figura 3.5 ilustra las tres operaciones aplicadas a la imagen del mandril con una máscara circular.
%%writefile tmp/fig_03_logica.cpp#define MM_OUT "tmp/fig_03_logica.png"//| label: fig-03-logica//| fig-cap: "Operações lógicas bit a bit com máscara circular: AND (isolamento da ROI), OR (iluminação da ROI) e NOT (negativo)."//| echo: true//| output: true#include "morph.hpp"#include <iostream>#include <vector>#include <string>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_8.png");// [pdi:state-io:end]// img_gray ya está inicializado (proporcionado automáticamente)int h = img_gray.h;int w = img_gray.w;// Máscara circular preenchida, centrada na imagem (mm.circle desenha o// disco; a versão didática, teste de raio pixel a pixel, é mm.circle0). mm::Image mask_circ(h, w);int radius = std::min(h, w) /3-10; mask_circ = mm::circle(mask_circ, w /2, h /2, radius, 255, -1);// Operações via morph mm::Image img_not = mm::bnot(img_gray);// NOT: negativo fotográfico mm::Image img_and = mm::band(img_gray, mask_circ);// preserva apenas a ROI circular mm::Image img_or = mm::bor(img_gray, mask_circ);// ilumina a região da máscara mm::show( std::vector<mm::Image>{img_gray, img_and, img_or, img_not}, MM_OUT, std::vector<std::string>{"Original", "AND (ROI circular)", "OR (ilumina ROI)", "NOT (negativo)"},4 );// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray, "tmp/fig_03_logica_0.png");mm::write(img_and, "tmp/fig_03_logica_1.png");mm::write(img_or, "tmp/fig_03_logica_2.png");mm::write(img_not, "tmp/fig_03_logica_3.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_logica.cpp
!g++-I. -std=c++17 tmp/fig_03_logica.cpp -o tmp/fig_03_logica \&& ./tmp/fig_03_logica \&& test -f "tmp/fig_03_logica.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_logica.png"
[1] Original
[2] AND (ROI circular)
[3] OR (ilumina ROI)
[4] NOT (negativo)
donde \(r_k\) es el \(k\)-ésimo nivel de intensidad, \(n_k\) es el número de píxeles con esa intensidad y \(L\) es el total de niveles (típicamente 256 para 8 bits). El histograma normalizado estima la probabilidad de cada nivel:
\[
p(r_k) = \frac{n_k}{MN}
\tag{3.6}\]
donde \(MN\) es el total de píxeles. Por ser una estadística global, el histograma no contiene información posicional, pero revela características esenciales como brillo medio, contraste y distribución tonal. En la práctica, mm::hist(img) devuelve el vector de conteos \(h(r_k)\), que sirve tanto para visualización (mediante mm::histImg) como para cálculos como función de distribución acumulada (CDF) y ecualización.
NotaInterpretación del Histograma
Estrecho a la izquierda: imagen subexpuesta (oscura).
Estrecho a la derecha: imagen sobreexpuesta (clara).
Concentrado en el centro: bajo contraste.
Distribuido por todo el rango: alto contraste, buena utilización de los tonos disponibles.
La Figura 3.6 presenta el histograma de la imagen del mandril, así como versiones oscurecida (mm::subm) y aclarada (mm::addm). Se observa el desplazamiento de la distribución de intensidades hacia la izquierda y hacia la derecha, respectivamente. Nótese que el intervalo representado en el eje \(x\) no corresponde necesariamente a todo el rango de 0 a 255.
%%writefile tmp/fig_03_histograma.cpp#define MM_OUT "tmp/fig_03_histograma.png"//| label: fig-03-histograma//| fig-cap: "Histogramas da imagem original, de uma versão escurecida (−80) e de uma clareada (+80). A subtração/adição satura em 0 e 255."//| echo: true//| output: true#include "morph.hpp"#include <iostream>#include <vector>#include <string>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_8.png");// [pdi:state-io:end]// img_gray is provided already initialized mm::Image img_dark = mm::subm(img_gray, 80); mm::Image img_high = mm::addm(img_gray, 80); mm::show( std::vector<mm::Image>{img_gray, img_dark, img_high, mm::histImg(img_gray), mm::histImg(img_dark), mm::histImg(img_high)}, MM_OUT, std::vector<std::string>{"Original", "Escurecida (-80)", "Clareada (+80)","Histograma - original", "Histograma - escurecida", "Histograma - clareada"},3 );return0;}
Overwriting tmp/fig_03_histograma.cpp
!g++-I. -std=c++17 tmp/fig_03_histograma.cpp -o tmp/fig_03_histograma \&& ./tmp/fig_03_histograma \&& test -f "tmp/fig_03_histograma.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_histograma.png"
[1] Original
[2] Escurecida (-80)
[3] Clareada (+80)
[4] Histograma - original
[5] Histograma - escurecida
[6] Histograma - clareada
try: mm.show(mm.read("tmp/fig_03_histograma.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_histograma.png (ver a versao Python)")
Figura 3.6: Histogramas da imagem original, de uma versão escurecida (−80) e de uma clareada (+80). A subtração/adição satura em 0 e 255.
3.3.1 Ecualización de Histograma
La ecualización de histograma redistribuye las intensidades para que el histograma resultante sea lo más uniforme posible. El mapeo está dado por la función de distribución acumulada (CDF):
La transformación es monótona: los niveles frecuentes reciben intervalos mayores en el dominio de salida (mayor separación → más contraste), mientras que los niveles raros se comprimen.
El algoritmo completo, en cinco etapas, se presenta en la Tabla 3.1.
Tabla 3.1: Algoritmo de ecualización de histograma.
Etapa
Operación
Fórmula
1
Histograma
\(h[k] \leftarrow\) número de píxeles con intensidad \(k\), \(k=0\ldots L-1\)
\(g[i,j] \leftarrow \text{lut}[f[i,j]]\) (para todo píxel)
Observe en la Figura 3.7 que la ecualización redistribuye los tonos existentes a posiciones más espaciadas en el rango \([0, L-1]\), pero no crea tonos nuevos — la imagen ecualizada continúa con exactamente 3 tonos distintos, ahora en \(\{1, 5, 7\}\) en lugar de \(\{2, 3, 4\}\).
%%writefile tmp/fig_03_equalizacao_didatica.cpp#define MM_OUT "tmp/fig_03_equalizacao_didatica.png"//| label: fig-03-equalizacao-didatica//| fig-cap: "Equalização de histograma numa imagem 5×5 de 3 bits (L=8): tons concentrados em {2,3,4} são redistribuídos pela CDF. *mm::equalize(img, 3)* faz o mapeamento."//| echo: true//| output: true#include "morph.hpp"#include <iostream>#include <vector>int main() { mm::Image img5(5, 5);int vals[5][5] = {{3, 4, 2, 3, 4}, {4, 3, 3, 4, 3}, {2, 3, 4, 3, 2}, {3, 4, 3, 2, 3}, {4, 3, 2, 3, 4}};for (int y =0; y <5; y++) {for (int x =0; x <5; x++) { img5.at(y, x) = vals[y][x]; } } mm::Image img5_eq = mm::equalize(img5, 3);// L =2^3=8 std::cout <<"Imagem original 5x5 (3 bits):\n"; std::cout << mm::drawImg(img5); std::cout <<"Imagem equalizada 5x5:\n"; std::cout << mm::drawImg(img5_eq); mm::show(std::vector<mm::Image>{img5, img5_eq, mm::histImg(img5), mm::histImg(img5_eq)}, MM_OUT, std::vector<std::string>{"Original", "Equalizada", "Histograma - original", "Histograma - equalizada"},2);return0;}
Overwriting tmp/fig_03_equalizacao_didatica.cpp
!g++-I. -std=c++17 tmp/fig_03_equalizacao_didatica.cpp -o tmp/fig_03_equalizacao_didatica \&& ./tmp/fig_03_equalizacao_didatica \&& test -f "tmp/fig_03_equalizacao_didatica.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_equalizacao_didatica.png"
try: mm.show(mm.read("tmp/fig_03_equalizacao_didatica.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_equalizacao_didatica.png (ver a versao Python)")
Figura 3.7: Equalização de histograma numa imagem 5×5 de 3 bits (L=8): tons concentrados em {2,3,4} são redistribuídos pela CDF. mm::equalize(img, 3) faz o mapeamento.
Limitación: la ecualización global puede realzar en exceso los ruidos y producir contraste excesivo en regiones homogéneas. El CLAHE (Contrast Limited Adaptive Histogram Equalization) reduce este problema al aplicar la ecualización en bloques locales (tiles) y limitar la altura de los picos del histograma antes de la ecualización.
La Figura 3.8 compara la imagen original, la ecualización global mediante mm::equalize y el CLAHE de OpenCV, mostrando también los histogramas resultantes. A diferencia de la ecualización global, que utiliza una única transformación basada en la CDF de toda la imagen, el CLAHE adapta el contraste a cada región, siendo particularmente útil en imágenes con iluminación no uniforme.
En el ejemplo, se utilizó clipLimit=2.0 y tileGridSize=(32,32). El parámetro clipLimit define cuánto pueden crecer los picos del histograma local antes de ser recortados (clipped). En OpenCV, este valor es un factor relativo: el límite real se calcula aproximadamente como clipLimit × (número de píxeles del bloque / número de niveles de gris). Por ejemplo, en un bloque con 4096 píxeles y una imagen de 8 bits (256 niveles de gris), la frecuencia media por nivel es \(4096/256=16\). Así, clipLimit=2.0 permite picos de aproximadamente \(2\times16=32\) ocurrencias antes del recorte. Las ocurrencias excedentes no se descartan: se redistribuyen entre los demás niveles de gris del histograma, reduciendo la concentración excesiva en pocos niveles y evitando una amplificación exagerada del contraste local. Valores menores limitan más el contraste y reducen la amplificación del ruido, mientras que valores mayores permiten un realce más intenso, pero pueden introducir artefactos.
clipLimit=1.0: realce suave y conservador;
clipLimit=2.0: buen equilibrio entre contraste y naturalidad;
clipLimit=4.0: mayor énfasis en los detalles locales;
clipLimit=8.0: contraste agresivo, con posible amplificación del ruido.
Así, el CLAHE suele producir resultados más naturales que la ecualización global, especialmente en imágenes con sombras, reflejos o iluminación desigual.
%%writefile tmp/fig_03_equalizacao.cpp#define MM_OUT "tmp/fig_03_equalizacao.png"//| label: fig-03-equalizacao//| fig-cap: "Equalização de histograma global (mm::equalize, via CDF) e os histogramas antes/depois. CLAHE (adaptativa) fica só na trilha Python — não tem equivalente em morph.hpp."//| echo: true//| output: true#include "morph.hpp"#include <iostream>#include <vector>#include <string>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_8.png");// [pdi:state-io:end] mm::Image img_eq = mm::equalize(img_gray); mm::show( std::vector<mm::Image>{img_gray, img_eq, mm::histImg(img_gray), mm::histImg(img_eq)}, MM_OUT, std::vector<std::string>{"Original", "mm.equalize (CDF)", "Histograma - original", "Histograma - equalizado"},2 );return0;}
Overwriting tmp/fig_03_equalizacao.cpp
!g++-I. -std=c++17 tmp/fig_03_equalizacao.cpp -o tmp/fig_03_equalizacao \&& ./tmp/fig_03_equalizacao \&& test -f "tmp/fig_03_equalizacao.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_equalizacao.png"
[1] Original
[2] mm.equalize (CDF)
[3] Histograma - original
[4] Histograma - equalizado
try: mm.show(mm.read("tmp/fig_03_equalizacao.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_equalizacao.png (ver a versao Python)")
Figura 3.8: Equalização de histograma global (mm::equalize, via CDF) e os histogramas antes/depois. CLAHE (adaptativa) fica só na trilha Python — não tem equivalente em morph.hpp.
3.3.2 Especificación de Histograma
Mientras que la ecualización impone una distribución uniforme, la especificación de histograma (histogram matching) permite que el histograma de la imagen de salida siga una distribución arbitraria — por ejemplo, el histograma de otra imagen de referencia.
El procedimiento involucra tres etapas:
Calcular la CDF de la imagen de entrada: \(P_r(r_k)\).
Calcular la CDF de la imagen de referencia: \(P_z(z_k)\).
Para cada nivel \(r_k\), encontrar el nivel \(z\) que minimiza \(|P_z(z) - P_r(r_k)|\).
En la Figura 3.9, transferimos el perfil tonal del leopardo (Figura 3.3) a la imagen del mandril — una aplicación directa del concepto visto en el blending: en lugar de fusionar píxeles, aquí fusionamos distribuciones tonales.
%%writefile tmp/fig_03_especificacao.cpp#define MM_OUT "tmp/fig_03_especificacao.png"//| label: fig-03-especificacao//| fig-cap: "Especificação de histograma (mapear o mandril para o perfil tonal do leopardo) precisa da CDF inversa da referência — fica só na trilha Python. Aqui, a equalização global do mandril, como comparação."//| echo: true//| output: true#include "morph.hpp"#include <iostream>#include <vector>#include <string>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_8.png");mm::Image img_leop_gray = mm::_read_state("tmp/state/img_leop_gray_12.png");// [pdi:state-io:end] mm::Image img_eq = mm::equalize(img_gray); mm::show(std::vector<mm::Image>{img_gray, img_leop_gray, img_eq}, MM_OUT, std::vector<std::string>{"Mandril (original)", "Leopardo (referencia)", "Mandril equalizado"},3);// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray, "tmp/fig_03_especificacao_0.png");mm::write(img_leop_gray, "tmp/fig_03_especificacao_1.png");mm::write(img_eq, "tmp/fig_03_especificacao_2.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_especificacao.cpp
!g++-I. -std=c++17 tmp/fig_03_especificacao.cpp -o tmp/fig_03_especificacao \&& ./tmp/fig_03_especificacao \&& test -f "tmp/fig_03_especificacao.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_especificacao.png"
Figura 3.9: Especificação de histograma (mapear o mandril para o perfil tonal do leopardo) precisa da CDF inversa da referência — fica só na trilha Python. Aqui, a equalização global do mandril, como comparação.
3.4 Fundamentos Espaciales: Vecindad, Convolución y Kernels
Las operaciones de filtrado espacial no actúan sobre un píxel aislado, sino sobre una vecindad que lo rodea. Para ello, se utiliza una pequeña matriz de coeficientes denominada kernel (o máscara), que recorre toda la imagen mediante una ventana deslizante (sliding window).
Las ventanas más comunes son de 3×3, 5×5 y 7×7. En una ventana de 3×3, por ejemplo, el píxel central se procesa junto con sus ocho vecinos inmediatos. En cada posición de la ventana, los valores de los píxeles se combinan con los coeficientes del kernel, produciendo un nuevo valor para el píxel central.
3.4.1 Vecindad
Considere una ventana de 3×3 centrada en el píxel \((x,y)\):
De forma general, una ventana de tamaño \((2a+1)\times(2b+1)\) abarca todos los píxeles situados hasta \(a\) posiciones en horizontal y hasta \(b\) posiciones en vertical con respecto al píxel central. Así, una ventana de 3×3 corresponde a \(a=b=1\), una ventana de 5×5 a \(a=b=2\), y así sucesivamente.
Los píxeles cercanos a los bordes tienen parte de su vecindad fuera de la imagen. Para aplicar filtros en estas regiones, es necesario definir cómo se obtendrán los valores externos. Las tres estrategias más comunes (con la constante equivalente de OpenCV entre paréntesis) son:
Zero-padding (BORDER_CONSTANT): completa la región externa con ceros.
Replicación (BORDER_REPLICATE): repite el valor del píxel del borde.
Reflexión (BORDER_REFLECT_101): refleja los píxeles vecinos, sin repetir el del borde.
mm::conv usa la reflexión por defecto, ya que preserva mejor la continuidad de los niveles de gris y reduce artefactos en el tratamiento de los bordes.
El siguiente ejemplo compara las tres estrategias con mm::pad en una matriz 3×3. Observa cómo cada una rellena los píxeles externos necesarios para aplicar un filtro 3×3 también en las esquinas.
%%writefile tmp/mm_out_1.cpp// Compile with: g++-std=c++17-o program program.cpp -I. -lm#include "morph.hpp"#include <iostream>#include <vector>int main() {// Construir la imagen 3x3 mm::Image img(3, 3);int valores[3][3] = {{1,2,3},{4,5,6},{7,8,9}};for (int y =0; y <3; y++)for (int x =0; x <3; x++) img.at(y, x) = valores[y][x]; std::vector<std::string> bordas = {"constant", "replicate", "reflect101"}; std::cout <<"Imagem original:\n"; std::cout << mm::drawImg(img) <<"\n";for (int k : {3, 5}) {int b = k /2; std::cout <<"=== Kernel "<< k <<"x"<< k <<" (padding b="<< b <<") ===\n";for (const auto& nome : bordas) { std::cout << nome <<"\n"; mm::Border border;if (nome =="constant") border = mm::Border::CONSTANT;elseif (nome =="replicate") border = mm::Border::REPLICATE;else border = mm::Border::REFLECT101; std::cout << mm::drawImg(mm::pad(img, b, border)) <<"\n"; } }return0;}
Note que el resultado de un filtro puede variar significativamente según el tratamiento adoptado para los bordes de la imagen.
En morph.hpp, las funciones de filtrado (mm::conv, mm::blur, mm::gaussian, mm::laplacian, mm::usm) aplican padding por reflexión (mm::Border::REFLECT101) por defecto — el mismo comportamiento que cv2.filter2D. La variante didáctica mm::conv0 usa mm::Border::KEEP: los píxeles del borde mantienen el valor original, sin el filtro. En cambio, mm::sobel y mm::prewitt dejan el borde en cero (calculan solo el interior).
3.4.3 Correlación vs. Convolución
Existen dos mecanismos matemáticamente relacionados.
Correlación cruzada (cross-correlation) — el kernel se aplica directamente:
Para kernels simétricos (Gaussiano, Laplaciano, media) las dos operaciones producen resultados idénticos. Para kernels asimétricos (Sobel, Prewitt) la diferencia es significativa, como lo muestran los ejemplos a continuación.
3.4.3.1 Correlación (mm::conv)
%%writefile tmp/mm_out_2.cpp// Converte código Python para C++ (usando morph.hpp)// Compile com: g++-std=c++17 programa.cpp -o programa $(pkg-config --cflags --libs opencv4)#include "morph.hpp"#include <iostream>int main() {// imagem 4x4 (uint8) e kernel assimétrico 3x3 mm::Image img(4, 4);// Preenche com os valores da matriz {int valores[4][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}};for (int y =0; y <4; y++) {for (int x =0; x <4; x++) { img.at(y, x) = static_cast<unsigned char>(valores[y][x]); } } } mm::Kernel w{{0,1,2},{0,0,0},{0,0,0}}; mm::Image corr = mm::conv(img, w, mm::Border::CONSTANT);// zero fora da imagem std::cout <<"Imagem original:\n"; std::cout << mm::drawImg(img); std::cout <<"Kernel:\n"; std::cout << mm::drawImg(w); std::cout <<"Resultado da correlação:\n"; std::cout << mm::drawImg(corr);return0;}
La convolución utiliza el kernel rotado 180°. Para reproducir la definición matemática de convolución, se rota el kernel (aquí, [[0,1,2],[0,0,0],[0,0,0]] → [[0,0,0],[0,0,0],[2,1,0]]) antes de aplicar mm::conv.
3.4.4 El Papel del Kernel
Los coeficientes del kernel determinan completamente el efecto producido por el filtro, tal como se resume en la Tabla 3.2.
Tabla 3.2: Interpretación típica de los coeficientes del kernel.
Característica
Efecto típico
Coeficientes positivos con suma 1
Suavizado (pasa-baja)
Suma igual a 0, con valores positivos y negativos
Detección de bordes (pasa-alta)
Coeficiente central positivo dominante y vecinos negativos
La Figura 3.10 demuestra el mecanismo paso a paso: para cada posición de la ventana, se multiplica cada coeficiente del kernel por el píxel correspondiente de la vecindad y se suman los productos obtenidos. El resultado es exactamente el valor definido por la Ecuación 3.11 para esa posición de la imagen. Aunque las imágenes producidas por mm::conv0 y cv2.filter2D (o mm::conv) sean visualmente muy similares, la implementación basada en OpenCV es miles de veces más rápida, como se muestra a continuación.
AdvertenciaRendimiento: bucles de Python vs. operaciones vectorizadas
La función mm::conv0 implementa la correlación directamente en Python mediante bucles anidados. Aunque este enfoque es adecuado para fines didácticos, ejecuta un gran número de operaciones y se vuelve lento para imágenes más grandes.
En cambio, mm::conv utiliza cv2.filter2D, implementado en C++ y optimizado para operaciones matriciales. En el ejemplo presentado, la versión vectorizada fue más de 3000 veces más rápida que la implementación didáctica, produciendo un resultado visualmente equivalente.
Las diferencias numéricas observadas se concentran principalmente en los bordes de la imagen. En mm::conv0, los píxeles del borde permanecen inalterados, mientras que mm::conv utiliza una estrategia de reflexión de bordes (cv2.BORDER_REFLECT_101, estándar de cv2.filter2D).
Por ello, mm::conv0 debe utilizarse para comprender el algoritmo, mientras que mm::conv es la opción recomendada para aplicaciones prácticas.
%%writefile tmp/fig_03_convolucao_passo.cpp#define MM_OUT "tmp/fig_03_convolucao_passo.png"#include "morph.hpp"#include <iostream>#include <vector>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_leop_gray = mm::_read_state("tmp/state/img_leop_gray_12.png");// [pdi:state-io:end]//| label: fig-03-convolucao-passo//| fig-cap: "Correlação com *kernel* de média 3×3: versão didática mm::conv0 (bordas preservadas) vs. mm::conv (borda refletida). A diferença se concentra nas bordas."//| echo: true//| output: true mm::Kernel w_mean = mm::Kernel::mean(3); mm::Image img_gray = img_leop_gray; mm::Image img_conv0 = mm::conv0(img_gray, w_mean);// laços, bordas preservadas mm::Image img_conv = mm::conv(img_gray, w_mean);// borda refletida std::cout <<"Correlacao no pixel central [251,251]:"<<"\n"; std::cout <<" original = "<< (int)img_gray.at(251, 251) <<"\n"; std::cout <<" conv0 = "<< (int)img_conv0.at(251, 251) <<"\n"; std::cout <<" conv = "<< (int)img_conv.at(251, 251) <<"\n"; mm::show(std::vector<mm::Image>{img_gray, img_conv0, img_conv}, MM_OUT, std::vector<std::string>{"Original", "conv0 (laços)", "conv (vetorizado)"}, 3);// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray, "tmp/fig_03_convolucao_passo_0.png");mm::write(img_conv0, "tmp/fig_03_convolucao_passo_1.png");mm::write(img_conv, "tmp/fig_03_convolucao_passo_2.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_convolucao_passo.cpp
!g++-I. -std=c++17 tmp/fig_03_convolucao_passo.cpp -o tmp/fig_03_convolucao_passo \&& ./tmp/fig_03_convolucao_passo \&& test -f "tmp/fig_03_convolucao_passo.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_convolucao_passo.png"
Correlacao no pixel central [251,251]:
original = 104
conv0 = 102
conv = 102
[1] Original
[2] conv0 (laços)
[3] conv (vetorizado)
Figura 3.10: Correlação com kernel de média 3×3: versão didática mm::conv0 (bordas preservadas) vs. mm::conv (borda refletida). A diferença se concentra nas bordas.
%%writefile tmp/mm_out_4.cpp#include "morph.hpp"#include <iostream>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_leop = mm::_read_state("tmp/state/img_leop_12.png");// [pdi:state-io:end]//| echo: false// A partir daqui o "sujeito" dos exemplos de filtragem passa a ser o// leopardo (mais textura e bordas que o mandril). mm::Image img_gray = mm::gray(img_leop);// [pdi:state-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp/state");mm::write(img_gray, "tmp/state/img_gray_45.png");// [pdi:state-io:end]return0;}
Para hacer concreto el mecanismo de la Ecuación 3.11, considere el kernel de media 3×3 (\(a=b=1\), todos los coeficientes \(= 1/9 \approx 0{,}111\)) aplicado al patch 5×5 extraído de la imagen del leopardo. La Figura 3.11 muestra el patch con la cuadrícula y resalta en amarillo la ventana 3×3 centrada en el píxel \([1,1]\):
%%writefile tmp/fig_03_patch.cpp#define MM_OUT "tmp/fig_03_patch.png"#include "morph.hpp"#include <iostream>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_45.png");// [pdi:state-io:end]//| label: fig-03-patch//| fig-cap: "*Patch* 5×5 extraído da imagem do leopardo (posição [250:255, 250:255]). A janela amarela destaca a vizinhança 3×3 centrada no pixel [1,1] onde a correlação será calculada."//| echo: true//| output: true mm::Image patch = mm::crop(img_gray, 250, 255, 250, 255); mm::Kernel B = mm::Kernel::ones(3); std::cout <<"Patch 5×5 (intensidades):"<< std::endl; std::cout << mm::drawImg(patch); mm::drawImgKernel(patch, B, 1, 1, MM_OUT, 40);return0;}
Overwriting tmp/fig_03_patch.cpp
!g++-I. -std=c++17 tmp/fig_03_patch.cpp -o tmp/fig_03_patch \&& ./tmp/fig_03_patch \&& test -f "tmp/fig_03_patch.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_patch.png"
try: mm.show(mm.read("tmp/fig_03_patch.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_patch.png (ver a versao Python)")
Figura 3.11: Patch 5×5 extraído da imagem do leopardo (posição [250:255, 250:255]). A janela amarela destaca a vizinhança 3×3 centrada no pixel [1,1] onde a correlação será calculada.
Para ilustrar el cálculo de la correlación, considere el píxel en la posición \([1,1]\) del patch 5×5 mostrado en la Figura 3.11.. Esa posición fue elegida solo por conveniencia didáctica, ya que posee una vecindad 3×3 completa a su alrededor.
Los valores de esa vecindad corresponden a la submatriz superior izquierda del patch:
El resultado (103) es ligeramente menor que el valor original del píxel central (107), pues la media incorpora vecinos de menor intensidad, produciendo el efecto de suavizado. En la práctica, el algoritmo inicia el procesamiento en \([0,0]\) y repite ese mismo cálculo para cada posición de la imagen, desplazando la ventana hasta cubrir todo el dominio.
3.5 Filtrado Espacial de Suavizado
Los filtros de suavizado (smoothing filters) atenúan variaciones bruscas de intensidad, reduciendo ruido y detalles de alta frecuencia. Son filtros paso-bajo — preservan las componentes de baja frecuencia (estructuras grandes) y atenúan las de alta frecuencia (ruido, bordes).
3.5.1 Filtro de Media (Box Filter)
El filtro de media utiliza un kernel uniforme de tamaño \(n \times n\), donde todos los coeficientes valen \(1/n^2\):
Cada píxel de salida es la media aritmética de los \(n^2\) píxeles de su vecindario. Nótese que la suma de los coeficientes es siempre 1 — el brillo medio de la imagen se preserva. Los kernels más grandes producen un suavizado más agresivo, pero desenfocan progresivamente los bordes.
Figura 3.12 muestra el efecto del filtro de media con kernels\(3\times3\), \(7\times7\) y \(15\times15\) sobre un detalle de la imagen del leopardo. Los resultados se obtuvieron con mm::blur, que implementa el filtro de media mediante la función cv2.blur, equivalente a la convolución de la imagen con un kernel uniforme cuyos coeficientes son \(h(x,y)=1/N^2\); de forma equivalente, el mismo resultado puede obtenerse con mm::conv, calculando (\(g=f*h\)). A medida que el kernel aumenta, más píxeles contribuyen a cada valor de salida, intensificando el suavizado, reduciendo el ruido y haciendo que los detalles finos y los bordes se vuelvan progresivamente más borrosos.
donde \(\sigma\) es la desviación estándar y controla el radio de influencia. Los píxeles más cercanos al centro tienen un peso mayor; los píxeles distantes se ignoran progresivamente.
La Figura 3.13 presenta el kernel Gaussiano \(5\times5\) generado para \(\sigma=1\). El kernel fue construido a partir del producto externo de dos vectores Gaussianos unidimensionales y posteriormente normalizado para que la suma de sus coeficientes sea igual a \(1\). Se observa que los mayores pesos se concentran en el centro de la matriz, decreciendo radialmente hacia los bordes. Esta distribución hace que los píxeles centrales tengan mayor influencia en el resultado del filtrado, contribuyendo a una suavización más natural y con mejor preservación de bordes que el filtro de media.
%%writefile tmp/fig_03_gauss_kernel.cpp#define MM_OUT "tmp/fig_03_gauss_kernel.png"#include "morph.hpp"#include <iostream>#include <vector>#include <string>#include <numeric>#include <iomanip>int main() {//| label: fig-03-gauss-kernel//| fig-cap: "*Kernel* Gaussiano 5×5 (σ=1): resposta ao impulso do filtro — pesos maiores no centro, decrescendo radialmente."//| echo: true//| output: true mm::Kernel w = mm::Kernel::gaussian(5, 1.0);// mm::Kernel::gaussian(5, 1.0) na morph.hpp std::cout <<"Kernel Gaussiano 5x5 (s=1), normalizado:"<<"\n";for (int y =0; y <5; y++) { std::cout <<" ";for (int x =0; x <5; x++) { std::cout << std::fixed << std::setprecision(4) << w.at(y, x);if (x <4) std::cout <<" "; } std::cout <<"\n"; } std::cout <<"Peso central [2,2] = "<< std::fixed << std::setprecision(4) << w.at(2, 2) <<" | canto [0,0] = "<< w.at(0, 0) <<"\n";// Visualização: resposta do filtro Gaussiano a um impulso central mm::Image impulso(5, 5); impulso.at(2, 2) =255; mm::drawImgPlt(mm::gaussian(impulso, 5, 1.0), MM_OUT);return0;}
Overwriting tmp/fig_03_gauss_kernel.cpp
!g++-I. -std=c++17 tmp/fig_03_gauss_kernel.cpp -o tmp/fig_03_gauss_kernel \&& ./tmp/fig_03_gauss_kernel \&& test -f "tmp/fig_03_gauss_kernel.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_gauss_kernel.png"
try: mm.show(mm.read("tmp/fig_03_gauss_kernel.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_gauss_kernel.png (ver a versao Python)")
Figura 3.13: Kernel Gaussiano 5×5 (σ=1): resposta ao impulso do filtro — pesos maiores no centro, decrescendo radialmente.
NotaVentaja computacional de la separabilidad
Considere un kernel cuadrado de tamaño \(n \times n\). Si este filtro es separable (como el Gaussiano), la convolución 2D puede descomponerse en dos convoluciones 1D: una horizontal y otra vertical.
En ese caso, el costo por píxel pasa de aproximadamente \(O(n^2)\) operaciones (convolución 2D directa) a \(O(2n)\) operaciones (dos convoluciones 1D). Así, la complejidad se reduce de forma significativa, haciendo el procesamiento más eficiente.
En comparación con el filtro de media, el Gaussiano:
Preserva mejor los bordes — la ponderación radial suaviza sin crear transiciones abruptas;
No introduce anillos (ringing) en el dominio de la frecuencia, pues la Gaussiana es su propia transformada de Fourier (Capítulo 5);
Está controlado por \(\sigma\) — aumentar \(\sigma\) equivale a aumentar el radio de suavizado de forma continua y predecible.
La Figura 3.14 compara los filtros de media y gaussiano aplicados a la imagen del leopardo usando una ventana \(9\times9\). El filtro de media se implementó mediante convolución con un kernel uniforme, donde todos los \(81\) píxeles de la vecindad poseen el mismo peso (\(1/81\)), mientras que el filtro gaussiano se obtuvo con cv2.GaussianBlur, utilizando pesos definidos por una distribución gaussiana. Ambos reducen ruido y suavizan la imagen, pero el filtro gaussiano preserva mejor los bordes y los detalles locales, como puede observarse en la región ampliada del ojo.
La Figura 3.14 compara los filtros de media y gaussiano aplicados a un detalle de la imagen del leopardo con kernels\(9\times 9\). El filtro de media se obtuvo con mm::blur, equivalente a la convolución con un kernel uniforme cuyos coeficientes valen \(1/81\), mientras que el filtro gaussiano se obtuvo con mm::gaussian, equivalente a la convolución con un kernel generado a partir de una distribución gaussiana. Ambos promueven suavización y reducción de ruido, pero el filtro gaussiano asigna mayor peso a los píxeles centrales de la vecindad, preservando mejor los bordes y los detalles locales, como puede observarse en la región ampliada del ojo.
%%writefile tmp/fig_03_gauss.cpp#define MM_OUT "tmp/fig_03_gauss.png"// Compile: g++-std=c++17-O2 -o prog prog.cpp -lm && ./prog#include "morph.hpp"#include <iostream>#include <vector>#include <string>#include <filesystem>//| label: fig-03-gauss//| fig-cap: "Comparação entre filtro de média e Gaussiano (*kernel* 9×9, σ=0). O Gaussiano preserva melhor as bordas, visível no detalhe do rosto."//| echo: true//| output: trueint main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_45.png");// [pdi:state-io:end]// img_gray é fornecido automaticamente (não declarar) mm::Image img_media9 = mm::blur(img_gray, 9); mm::Image img_gauss9 = mm::gaussian(img_gray, 9, 0);// Detalhe da região do olho mm::Image img_gray_crop = mm::crop(img_gray, 580, 740, 680, 900); mm::Image img_media9_crop = mm::crop(img_media9, 580, 740, 680, 900); mm::Image img_gauss9_crop = mm::crop(img_gauss9, 580, 740, 680, 900); mm::show( std::vector<mm::Image>{img_gray_crop, img_media9_crop, img_gauss9_crop}, MM_OUT, std::vector<std::string>{"Detalhe: Original", "Média 9×9", "Gaussiano 9×9"},3 );// [pdi:state-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp/state");mm::write(img_gray_crop, "tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray_crop, "tmp/fig_03_gauss_0.png");mm::write(img_media9_crop, "tmp/fig_03_gauss_1.png");mm::write(img_gauss9_crop, "tmp/fig_03_gauss_2.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_gauss.cpp
!g++-I. -std=c++17 tmp/fig_03_gauss.cpp -o tmp/fig_03_gauss \&& ./tmp/fig_03_gauss \&& test -f "tmp/fig_03_gauss.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_gauss.png"
[1] Detalhe: Original
[2] Média 9×9
[3] Gaussiano 9×9
Figura 3.14: Comparação entre filtro de média e Gaussiano (kernel 9×9, σ=0). O Gaussiano preserva melhor as bordas, visível no detalhe do rosto.
3.6 Filtrado Espacial de Realce
Los filtros de realce (sharpening filters) enfatizan transiciones abruptas de intensidad, aumentando la nitidez y la visibilidad de bordes. Son filtros paso-alto — amplifican los componentes de alta frecuencia (bordes, textura) y suprimen los de baja frecuencia (regiones uniformes).
La intuición es simple: si restamos de una imagen su versión suavizada (que contiene solo las bajas frecuencias), lo que queda son las altas frecuencias — bordes y detalles. Sumando ese residuo de vuelta a la imagen original, el contraste local aumenta:
\[
g = f + k\,(f - f_{\text{suave}}), \quad k > 0
\tag{3.15}\]
La Figura 3.15 ilustra este proceso en una señal 1D sintética con tres estructuras distintas: un escalón ancho, un pico fino y una rampa suave. En el panel ①, la señal original \(f(x)\); en el ②, la versión suavizada \(f_{\text{suave}}(x)\) obtenida por media móvil — nótese cómo el pico fino se atenúa. El panel ③ muestra el residuo \(f - f_{\text{suave}}\), que retiene solo las transiciones abruptas. Finalmente, el panel ④ muestra \(g(x)\): el pico, antes atenuado, se restaura y amplifica en relación con el original. Ajuste \(k\) y el tamaño de la ventana para observar el trade-off entre nitidez y amplificación de ruido.
Los filtros de realce formalizan esta idea directamente en el kernel, sin necesidad de dos etapas separadas.
🎮 Simulador: Filtrado Espacial de Realce 1Dg = f + k·(f − f_suave)
k = 1.5
ventana = 9
σ = 0.04
① f(x) — Señal Original
↓ filtro pasa-bajas (media móvil)
② f_suave(x) — Pico Atenuado por el Filtro
↓ sustracción: f − f_suave
③ Residuo (f − f_suave) — Altas Frecuencias / Bordes
↓ suma: f + k · residuo
④ g(x) — Señal con Pico Realzado
Figura 3.15: Simulador: Filtrado Espacial de Realce 1D (Unsharp Masking y High-Boost)
3.6.1 Laplaciano
El Laplaciano es un operador de segunda derivada isotrópico, es decir, responde de igual manera a las variaciones en todas las direcciones, a diferencia de los operadores de primera derivada, como Sobel y Prewitt, que son direccionales:
Una propiedad importante de la segunda derivada es que su valor es cercano a cero en regiones uniformes y elevado en las transiciones de intensidad. Así, al restar el Laplaciano de la imagen original, se refuerzan los bordes y detalles, aumentando el contraste local:
\[
g(x,y) = f(x,y) - \nabla^2 f(x,y)
\tag{3.17}\]
En la forma discreta, la segunda derivada en \(x\) se aproxima por \(f(x+1,y) - 2f(x,y) + f(x-1,y)\), y de manera análoga en \(y\). Sumando ambas direcciones, se obtiene el kernel\(w_4\) (4-vecinos) o \(w_8\) (8-vecinos, incluyendo diagonales):
Ambos kernels tienen suma de coeficientes igual a cero: en regiones uniformes, la salida es 0 — el Laplaciano no altera el brillo medio, solo detecta variaciones. El centro negativo indica que el píxel se compara con sus vecinos: cuanto más se destaque (hacia arriba o hacia abajo), mayor será el valor absoluto del Laplaciano en ese punto.
En el siguiente ejemplo, el píxel central \([1,1]=107\) posee vecinos \(\{95, 106, 108, 103\}\). Como estos valores son cercanos entre sí, la región es casi uniforme y el Laplaciano devuelve un valor bajo, produciendo poco realce. En regiones de borde, donde hay diferencias mayores entre el píxel central y sus vecinos, el Laplaciano asume valores más elevados (positivos o negativos), y la operación de Ecuación 3.17 intensifica esas transiciones.
La Figura 3.16 ilustra el cálculo del Laplaciano con el kernel\(w_4\) en una vecindad \(3\times3\) destacada dentro de un patch\(5\times5\). El ejemplo muestra el valor obtenido por el operador y el correspondiente píxel realzado en la imagen de salida, que pasa de 107 a 123.
%%writefile tmp/fig_03_laplaciano_patch.cpp#define MM_OUT "tmp/fig_03_laplaciano_patch.png"// Compile with: g++-std=c++17-o program program.cpp -lmorph#include "morph.hpp"#include <iostream>//| label: fig-03-laplaciano-patch//| fig-cap: "*Kernel* Laplaciano w4 sobre el *patch* 5×5: la ventana amarilla destaca la vecindad 3×3 donde el operador de segunda derivada se calcula."//| echo: true//| output: trueint main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray = mm::_read_state("tmp/state/img_gray_45.png");// [pdi:state-io:end] mm::Image patch = mm::crop(img_gray, 250, 255, 250, 255); mm::Kernel B = mm::Kernel::ones(3); std::cout <<"Patch 5x5 (intensidades):\n"; std::cout << mm::drawImg(patch) <<"\n"; mm::drawImgKernel(patch, B, 1, 1, MM_OUT, 40);return0;}
Overwriting tmp/fig_03_laplaciano_patch.cpp
!g++-I. -std=c++17 tmp/fig_03_laplaciano_patch.cpp -o tmp/fig_03_laplaciano_patch \&& ./tmp/fig_03_laplaciano_patch \&& test -f "tmp/fig_03_laplaciano_patch.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_laplaciano_patch.png"
try: mm.show(mm.read("tmp/fig_03_laplaciano_patch.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_laplaciano_patch.png (ver a versao Python)")
Figura 3.16: Kernel Laplaciano w4 sobre o patch 5×5: a janela amarela destaca a vizinhança 3×3 onde o operador de segunda derivada é calculado.
A Figura 3.17 compara a aplicação de los kernels laplacianos \(w_4\) y \(w_8\) en un recorte más grande de la imagen del leopardo. Para cada caso, se muestran la respuesta bruta del operador, que evidencia los bordes y las transiciones de intensidad, y la imagen obtenida tras el realce por sustracción del laplaciano. Se observa que el kernel\(w_8\), al considerar también los vecinos diagonales, produce una respuesta más intensa y detecta variaciones en más direcciones, resultando en un realce ligeramente más acentuado.
%%writefile tmp/fig_03_laplaciano.cpp#define MM_OUT "tmp/fig_03_laplaciano.png"//| label: fig-03-laplaciano//| fig-cap: "Laplaciano aplicado à imagem do leopardo: resposta bruta (bordas) com w4 e w8, e imagens realçadas pela subtração do Laplaciano. w8 é mais sensível às diagonais."//| echo: true//| output: true#include "morph.hpp"#include <iostream>#include <vector>#include <string>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]// img_gray_crop is provided automatically mm::Kernel w4{{0,1,0},{1,-4,1},{0,1,0}}; mm::Kernel w8{{1,1,1},{1,-8,1},{1,1,1}}; mm::show( {img_gray_crop, mm::laplacian_viz(img_gray_crop, w4), mm::laplacian(img_gray_crop, w4), img_gray_crop, mm::laplacian_viz(img_gray_crop, w8), mm::laplacian(img_gray_crop, w8)}, MM_OUT, {"Original", "Laplaciano w4", "Realce w4","Original", "Laplaciano w8", "Realce w8"},3 );return0;}
Overwriting tmp/fig_03_laplaciano.cpp
!g++-I. -std=c++17 tmp/fig_03_laplaciano.cpp -o tmp/fig_03_laplaciano \&& ./tmp/fig_03_laplaciano \&& test -f "tmp/fig_03_laplaciano.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_laplaciano.png"
[1] Original
[2] Laplaciano w4
[3] Realce w4
[4] Original
[5] Laplaciano w8
[6] Realce w8
try: mm.show(mm.read("tmp/fig_03_laplaciano.png"), figsize=(14, 8))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_laplaciano.png (ver a versao Python)")
Figura 3.17: Laplaciano aplicado à imagem do leopardo: resposta bruta (bordas) com w4 e w8, e imagens realçadas pela subtração do Laplaciano. w8 é mais sensível às diagonais.
3.6.2 Operador de Sobel
El operador de Sobel estima las derivadas parciales de primer orden en las direcciones horizontal y vertical. A diferencia del Laplaciano (segunda derivada), el Sobel es direccional y más robusto al ruido, pues cada kernel combina una derivada con un suavizado Gaussiano perpendicular:
\(G_x\) detecta bordes verticales (variación en la dirección \(x\)); \(G_y\) detecta bordes horizontales (variación en la dirección \(y\)). Los pesos \(\{1,2,1\}\) en la dirección perpendicular corresponden al suavizado Gaussiano 1D, que reduce la sensibilidad al ruido.
NotaSobel es correlación, no convolución
Los kernels de Sobel son asimétricos — la rotación de 180° altera el resultado. cv2.Sobel implementa correlación cruzada (como cv2.filter2D). Para obtener la derivada direccional correcta, las señales ya están definidas para correlación: \(G_x\) devuelve valores positivos donde la intensidad crece de izquierda a derecha.
La magnitud del gradiente combina los dos componentes, representando la fuerza del borde independientemente de la dirección:
\[
|\nabla f| = \sqrt{G_x^2 + G_y^2}
\tag{3.20}\]
Y la dirección del gradiente (perpendicular al borde) es:
Para ilustrar numéricamente, se calculan \(G_x\) y \(G_y\) manualmente en el píxel central \([1,1]\) del patch de 5×5:
El valor reducido de \(|{\nabla f}|\) en ese patch confirma que la región es casi uniforme, pues el gradiente asume valores elevados solo donde hay cambios significativos de intensidad. La Figura 3.18 aplica el operador de Sobel a un recorte mayor de la imagen del leopardo. Se presentan las respuestas horizontal (\(G_x\)) y vertical (\(G_y\)), obtenidas por convolución con los respectivos kernels de Sobel, además de la magnitud \(|{\nabla f}|\), calculada a partir de la combinación de ambas. Mientras que \(G_x\) destaca bordes verticales y \(G_y\) bordes horizontales, la magnitud evidencia bordes en cualquier dirección.
%%writefile tmp/fig_03_sobel.cpp#define MM_OUT "tmp/fig_03_sobel.png"#include "morph.hpp"#include <iostream>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]//| label: fig-03-sobel//| fig-cap: "Operador de Sobel na imagem do leopardo: a magnitude |∇f| combina os gradientes horizontal e vertical, revelando todas as bordas. A decomposição Gx/Gy com sinal fica na trilha Python — mm::sobel devolve a magnitude já com clip."//| echo: true//| output: true mm::Image mag = mm::sobel(img_gray_crop); mm::show(std::vector<mm::Image>{img_gray_crop, mag}, MM_OUT, std::vector<std::string>{"Original", "Magnitude |grad f| (mm.sobel)"}, 2);// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray_crop, "tmp/fig_03_sobel_0.png");mm::write(mag, "tmp/fig_03_sobel_1.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_sobel.cpp
!g++-I. -std=c++17 tmp/fig_03_sobel.cpp -o tmp/fig_03_sobel \&& ./tmp/fig_03_sobel \&& test -f "tmp/fig_03_sobel.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_sobel.png"
Figura 3.18: Operador de Sobel na imagem do leopardo: a magnitude |∇f| combina os gradientes horizontal e vertical, revelando todas as bordas. A decomposição Gx/Gy com sinal fica na trilha Python — mm::sobel devolve a magnitude já com clip.
3.6.3 Operador de Prewitt
El operador de Prewitt es estructuralmente idéntico al de Sobel, pero sustituye la ponderación gaussiana \(\{1,2,1\}\) por pesos uniformes \(\{1,1,1\}\):
La magnitud y la dirección del gradiente siguen las mismas ecuaciones que las de Sobel (Ecuación 3.20 y Ecuación 3.21). La diferencia práctica es que Prewitt es ligeramente más sensible al ruido — el suavizado perpendicular uniforme pondera menos el píxel central de la línea — pero computacionalmente más simple. En imágenes con bajo ruido los resultados son equivalentes.
%%writefile tmp/fig_03_prewitt.cpp#define MM_OUT "tmp/fig_03_prewitt.png"// Compile with: g++-std=c++17-o program program.cpp morph.hpp#include "morph.hpp"#include <iostream>#include <vector>//| label: fig-03-prewitt//| fig-cap: "Operador de Prewitt: magnitude do gradiente, comparável ao Sobel mas sem a ponderação central. mm::prewitt devolve |∇f| com clip."//| echo: true//| output: trueint main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end] mm::show(std::vector<mm::Image>{img_gray_crop, mm::prewitt(img_gray_crop)}, MM_OUT, std::vector<std::string>{"Original", "Magnitude |grad f| (mm.prewitt)"}, 2);return0;}
Overwriting tmp/fig_03_prewitt.cpp
!g++-I. -std=c++17 tmp/fig_03_prewitt.cpp -o tmp/fig_03_prewitt \&& ./tmp/fig_03_prewitt \&& test -f "tmp/fig_03_prewitt.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_prewitt.png"
[1] Original
[2] Magnitude |grad f| (mm.prewitt)
try: mm.show(mm.read("tmp/fig_03_prewitt.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_prewitt.png (ver a versao Python)")
Figura 3.19: Operador de Prewitt: magnitude do gradiente, comparável ao Sobel mas sem a ponderação central. mm::prewitt devolve |∇f| com clip.
3.6.4Unsharp Masking (USM)
El Unsharp Masking es una técnica clásica de realce de nitidez originaria de la fotografía analógica, hoy ampliamente utilizada en software de edición de imágenes. La idea central es extraer los componentes de alta frecuencia de la imagen (bordes y detalles) y sumarlos de vuelta a la original con un peso \(k\):
Tabla 3.3: Etapas del Unsharp Masking.
Etapa
Operación
Descripción
1
\(\bar{f} = f * G_\sigma\)
Suaviza con gaussiana — retiene bajas frecuencias
2
\(m = f - \bar{f}\)
Máscara: diferencia = altas frecuencias (bordes)
3
\(g = f + k \cdot m\)
Suma ponderada de la máscara a la original
Sustituyendo la etapa 2 en la etapa 3, se obtiene la expresión compacta:
\[
g = f + k\,(f - f*G_\sigma) = (1+k)\,f - k\,(f*G_\sigma)
\tag{3.23}\]
El parámetro \(k\) controla la intensidad del realce:
\(k = 0\): sin realce (\(g = f\));
\(k = 1\): USM clásico — duplica la contribución de las altas frecuencias;
\(k > 1\): High Boost Filtering — amplificación más allá del doble, útil para imágenes muy borrosas.
AdvertenciaAmplificación de ruido
El USM no distingue bordes de ruido — ambos son componentes de alta frecuencia. Para \(k\) elevado, el ruido presente en la imagen se amplifica junto con los bordes. Por ello, se recomienda aplicar una leve suavización antes del USM en imágenes ruidosas, o usar un \(\sigma\) pequeño en la gaussiana.
Para ilustrar las etapas del USM, la Figura 3.20 aplica el método a un parche\(30\times30\) de la imagen del leopardo, utilizando \(\sigma=1\) y \(k=1\). Inicialmente, la imagen se suaviza mediante un filtro gaussiano. A continuación, la máscara de alta frecuencia se obtiene como la diferencia entre la imagen original y la suavizada. Finalmente, esta máscara se suma a la imagen original, reforzando bordes y detalles. La figura presenta las tres etapas del proceso y el resultado final del realce.
%%writefile tmp/fig_03_usm2.cpp#define MM_OUT "tmp/fig_03_usm2.png"#include "morph.hpp"#include <iostream>#include <vector>#include <string>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]//| label: fig-03-usm2//| fig-cap: "Realce por *Unsharp Masking* num *patch* do leopardo: mm::usm faz suavização Gaussiana, subtrai da original (máscara de alta frequência) e reintroduz a máscara realçada."//| echo: true//| output: true mm::Image patch = mm::crop(img_gray_crop, 35, 65, 45, 75); mm::Image p_suave = mm::gaussian(patch, 7, 1.0);// suavização Gaussiana mm::Image p_usm = mm::usm(patch, 1.0);// realce completo (k =1.0) mm::show(std::vector<mm::Image>{patch, p_suave, p_usm}, MM_OUT, std::vector<std::string>{"Patch original", "Suavizado (σ=1)", "Realçado USM (k=1)"}, 3);// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(patch, "tmp/fig_03_usm2_0.png");mm::write(p_suave, "tmp/fig_03_usm2_1.png");mm::write(p_usm, "tmp/fig_03_usm2_2.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_usm2.cpp
!g++-I. -std=c++17 tmp/fig_03_usm2.cpp -o tmp/fig_03_usm2 \&& ./tmp/fig_03_usm2 \&& test -f "tmp/fig_03_usm2.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_usm2.png"
[1] Patch original
[2] Suavizado (σ=1)
[3] Realçado USM (k=1)
Figura 3.20: Realce por Unsharp Masking num patch do leopardo: mm::usm faz suavização Gaussiana, subtrai da original (máscara de alta frequência) e reintroduz a máscara realçada.
La Figura 3.21 aplica el método USM a un recorte más grande de la imagen del leopardo utilizando \(\sigma=1\) y diferentes valores del factor de ganancia \(k\). En todos los casos, la máscara de alta frecuencia se obtiene mediante la diferencia entre la imagen original y su versión suavizada por filtro gaussiano. El parámetro \(k\) controla la intensidad del realce: valores menores producen un aumento sutil de nitidez, mientras que valores mayores refuerzan progresivamente bordes y detalles. Se observa que, para valores elevados de \(k\), surgen halos alrededor de los bordes y el ruido presente en la imagen comienza a amplificarse.
%%writefile tmp/fig_03_usm.cpp#define MM_OUT "tmp/fig_03_usm.png"// Compile with: g++-std=c++17-o program program.cpp -lmorph#include "morph.hpp"#include <iostream>#include <vector>#include <string>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]//| label: fig-03-usm//| fig-cap: "*Unsharp Masking* na imagem do leopardo com σ=1 e k de 0.5 a 8.0. Para k>2 surgem halos nas bordas e o ruído de fundo aparece."//| echo: true//| output: true mm::show( std::vector<mm::Image>{img_gray_crop, mm::usm(img_gray_crop, 0.5), mm::usm(img_gray_crop, 1.0), mm::usm(img_gray_crop, 3.0), mm::usm(img_gray_crop, 5.0), mm::usm(img_gray_crop, 8.0)}, MM_OUT, std::vector<std::string>{"Original", "USM k=0.5", "USM k=1.0", "USM k=3.0", "USM k=5.0", "USM k=8.0"},3 );return0;}
Overwriting tmp/fig_03_usm.cpp
!g++-I. -std=c++17 tmp/fig_03_usm.cpp -o tmp/fig_03_usm \&& ./tmp/fig_03_usm \&& test -f "tmp/fig_03_usm.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_usm.png"
try: mm.show(mm.read("tmp/fig_03_usm.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_usm.png (ver a versao Python)")
Figura 3.21: Unsharp Masking na imagem do leopardo com σ=1 e k de 0.5 a 8.0. Para k>2 surgem halos nas bordas e o ruído de fundo aparece.
3.6.5 Detector de Canny
Canny combina cuatro etapas en secuencia — suavizado Gaussiano, gradiente de Sobel, supresión de no-máximos e histéresis por doble umbral — para producir bordes finos, binarios y conectados. A diferencia de Sobel y Prewitt, el resultado no es un mapa de gradiente continuo, sino una máscara donde cada píxel es borde o no.
El parámetro central es el par de umbrales \((T_{low}, T_{high})\). Los píxeles con gradiente por encima de \(T_{high}\) son bordes seguros; por debajo de \(T_{low}\), se descartan. Los píxeles ambiguos — entre los dos umbrales — se deciden mediante histéresis: se convierten en borde si están conectados a un borde seguro, y se descartan en caso contrario. Esto evita tanto la pérdida de tramos débiles de bordes reales como la inclusión de ruido aislado. Una heurística común es \(T_{high} = 3 \times T_{low}\).
%%writefile tmp/fig_03_canny.cpp#define MM_OUT "tmp/fig_03_canny.png"// Compile with: g++-std=c++17-o output code.cpp -I<include_path>#include "morph.hpp"#include <iostream>#include <vector>#include <string>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]//| label: fig-03-canny//| fig-cap: "Detector de Canny con diferentes pares de umbral: umbrales bajos capturan más bordes (incluso ruido); umbrales altos retienen solo los bordes más fuertes."//| echo: true//| output: true mm::show( std::vector<mm::Image>{img_gray_crop, mm::canny(img_gray_crop, 30, 90), mm::canny(img_gray_crop, 60, 180), mm::canny(img_gray_crop, 120, 240)}, MM_OUT, std::vector<std::string>{"Original", "Canny (30/90)", "Canny (60/180)", "Canny (120/240)"},4 );return0;}
Overwriting tmp/fig_03_canny.cpp
!g++-I. -std=c++17 tmp/fig_03_canny.cpp -o tmp/fig_03_canny \&& ./tmp/fig_03_canny \&& test -f "tmp/fig_03_canny.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_canny.png"
try: mm.show(mm.read("tmp/fig_03_canny.png"))exceptExceptionas _e:print("figura indisponivel nesta trilha (C++): "+repr(_e) +" tmp/fig_03_canny.png (ver a versao Python)")
Figura 3.22: Detector de Canny com diferentes pares de limiar: limiares baixos capturam mais bordas (inclusive ruído); limiares altos retêm apenas as bordas mais fortes.
NotaElección de los umbrales
Una heurística común es \(T_{high} = 3 \times T_{low}\). Los valores típicos dependen del rango de gradiente de la imagen — cv2.Canny acepta valores absolutos en \([0, 255]\). Para imágenes con contraste variable, calcular los umbrales a partir de percentiles de la magnitud de Sobel es más robusto que usar valores fijos.
3.7 Filtros de Orden: Filtro de la Mediana
Los filtros de orden (order-statistic filters) reemplazan el píxel central por el valor de un percentil de la distribución de intensidades de la vecindad — a diferencia de los filtros lineales, que calculan combinaciones ponderadas. El más importante es el filtro de la mediana.
3.7.1 Ruido Impulsivo: Sal y Pimienta
El ruido sal y pimienta (salt-and-pepper noise) reemplaza píxeles aleatorios por valores extremos: 0 (pimienta, negro) o 255 (sal, blanco). Es común en la transmisión de imágenes con errores de bit y en cámaras con sensores defectuosos.
Para entender por qué fallan los filtros lineales, considere una vecindad 3×3 donde un único píxel ha sido corrompido a 255:
Tabla 3.4: Media vs. mediana con un píxel corrompido. La mediana ignora el valor atípico; la media se desplaza ~40 niveles.
Método
Cálculo
Resultado
Media
(102+98+…+255+…+101)/9
≈ 140
Mediana
{97,98,99,100,101,102,103,105,255}
101
Advertencia¿Por qué fallan los filtros de media con ruido impulsivo?
La media es sensible a los valores atípicos — un único píxel con valor 255 en una vecindad de valor ≈ 100 eleva la salida a ≈ 140, propagando el ruido por la imagen. La mediana, por ser un estimador robusto, selecciona el valor central de la distribución ordenada, descartando naturalmente los extremos sin ningún ajuste especial.
El siguiente ejemplo ilustra el comportamiento de la media y de la mediana en presencia de un píxel corrupto por ruido impulsivo. Se observa que la media está fuertemente influenciada por el valor extremo (255), produciendo una estimación distante de los valores predominantes de la vecindad. En cambio, la mediana permanece cercana al valor original de la región, evidenciando su mayor robustez frente a valores atípicos y justificando su uso en la eliminación de ruido de sal y pimienta.
La Figura 3.23 presenta el efecto del ruido sal y pimienta en diferentes densidades. El ruido fue generado reemplazando aleatoriamente una fracción de los píxeles por valores mínimos (0, pimienta) y máximos (255, sal). A medida que la densidad aumenta del 2% al 10%, crece la cantidad de píxeles corruptos, haciendo que la degradación visual sea más evidente y dificultando la percepción de los detalles de la imagen.
%%writefile tmp/fig_03_ruido.cpp#define MM_OUT "tmp/fig_03_ruido.png"#include "morph.hpp"#include <iostream>#include <vector>#include <string>#include <random>#include <algorithm>#include <filesystem>//| label: fig-03-ruido//| fig-cap: "Ruído sal e pimenta com densidades crescentes (2%, 5%, 10%): metade dos pixels corrompidos vira sal (255), metade pimenta (0)."//| echo: true//| output: truemm::Image salt_pepper(const mm::Image& img, double prob) { mm::Image out = img;int h = out.h;int w = out.w; std::mt19937 rng(42); std::uniform_int_distribution<int> dist_y(0, h -1); std::uniform_int_distribution<int> dist_x(0, w -1); std::uniform_real_distribution<double> dist_rand(0.0, 1.0);int n = static_cast<int>(prob * h * w);for (int i =0; i < n; i++) {int y = dist_y(rng);int x = dist_x(rng);if (dist_rand(rng) <0.5) out.at(y, x) =0;else out.at(y, x) =255; }return out;}int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end] mm::Image n2 = salt_pepper(img_gray_crop, 0.02); mm::Image n5 = salt_pepper(img_gray_crop, 0.05); mm::Image n10 = salt_pepper(img_gray_crop, 0.10); mm::show(std::vector<mm::Image>{img_gray_crop, n2, n5, n10}, MM_OUT, std::vector<std::string>{"Original", "Ruido 2%", "Ruido 5%", "Ruido 10%"},4);// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray_crop, "tmp/fig_03_ruido_0.png");mm::write(n2, "tmp/fig_03_ruido_1.png");mm::write(n5, "tmp/fig_03_ruido_2.png");mm::write(n10, "tmp/fig_03_ruido_3.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_ruido.cpp
!g++-I. -std=c++17 tmp/fig_03_ruido.cpp -o tmp/fig_03_ruido \&& ./tmp/fig_03_ruido \&& test -f "tmp/fig_03_ruido.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_ruido.png"
El valor mediano es aquel que ocupa la posición central cuando los \(n^2\) valores de la vecindad se ordenan. Para una ventana \(3\times3\) (\(n^2=9\) píxeles), la mediana es el 5º valor de la secuencia ordenada.
Para ilustrar, considere el mismo patch 5×5 con un píxel corrompido artificialmente en \([1,1]\):
El ejemplo confirma: incluso con el píxel corrompido a 255, la mediana devuelve el valor central correcto — el outlier ocupa la última posición en la ordenación y se descarta de forma natural.
Por basarse en la ordenación y no en la suma, la mediana posee tres propiedades fundamentales que la diferencian de los filtros lineales:
Robusta ante el ruido impulsivo — los outliers van a los extremos de la secuencia ordenada y no afectan el valor central;
Preservadora de bordes — las transiciones abruptas de intensidad se mantienen, pues la mediana selecciona un valor que ya existe en la vecindad, sin crear nuevos niveles intermedios;
No lineal — no puede expresarse como convolución, por lo que mm::conv no se aplica; se utiliza cv2.medianBlur.
A Figura 3.24 compara diferentes técnicas de remoção de ruido sal y pimienta aplicadas a una imagen con un 10% de píxeles corruptos. Se evaluaron los filtros Gaussiano, Media, Mediana, Bilateral y Morfológico (próximo capítulo), lo que permite observar la relación entre la eliminación de ruido y la preservación de detalles. En general, los filtros de media y Gaussiano reducen el ruido, pero tienden a desenfocar los bordes, mientras que la mediana presenta un mejor rendimiento para ruido impulsivo. El filtro bilateral preserva mejor los bordes, y el filtro morfológico elimina gran parte de los píxeles corruptos sin degradar excesivamente la estructura de la imagen.
%%writefile tmp/fig_03_ruido_filtros.cpp#define MM_OUT "tmp/fig_03_ruido_filtros.png"#include "morph.hpp"#include <random>#include <vector>#include <string>#include <filesystem>//| label: fig-03-ruido-filtros//| fig-cap: "Filtros para ruído sal e pimenta (10%): Gaussiano, Média e Mediana. Bilateral e morfológico (open+close) ficam só na trilha Python — bilateral não tem equivalente em morph.hpp e morfologia é do próximo capítulo."//| echo: true//| output: trueint main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]// Função para adicionar ruído sal e pimenta auto salt_pepper = [](const mm::Image& img, double prob) { mm::Image out = img;int h = out.h, w = out.w; std::mt19937 rng(42); std::uniform_int_distribution<int> dist_y(0, h -1); std::uniform_int_distribution<int> dist_x(0, w -1); std::uniform_real_distribution<double> dist_p(0.0, 1.0);for (int i =0; i < static_cast<int>(prob * h * w);++i) {int y = dist_y(rng);int x = dist_x(rng); out.at(y, x) = (dist_p(rng) <0.5) ? 0 : 255; }return out; }; mm::Image noisy = salt_pepper(img_gray_crop, 0.10); mm::Image f_gauss = mm::gaussian(noisy, 5, 1.0); mm::Image f_media = mm::blur(noisy, 5); mm::Image f_median3 = mm::median(noisy, 3); mm::Image f_median5 = mm::median(noisy, 5); mm::show( std::vector<mm::Image>{img_gray_crop, noisy, f_gauss, f_media, f_median3, f_median5}, MM_OUT, std::vector<std::string>{"Original", "Ruido 10%", "Gaussiano 5x5", "Media 5x5","Mediana 3x3", "Mediana 5x5"},3 );// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray_crop, "tmp/fig_03_ruido_filtros_0.png");mm::write(noisy, "tmp/fig_03_ruido_filtros_1.png");mm::write(f_gauss, "tmp/fig_03_ruido_filtros_2.png");mm::write(f_media, "tmp/fig_03_ruido_filtros_3.png");mm::write(f_median3, "tmp/fig_03_ruido_filtros_4.png");mm::write(f_median5, "tmp/fig_03_ruido_filtros_5.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_ruido_filtros.cpp
!g++-I. -std=c++17 tmp/fig_03_ruido_filtros.cpp -o tmp/fig_03_ruido_filtros \&& ./tmp/fig_03_ruido_filtros \&& test -f "tmp/fig_03_ruido_filtros.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_ruido_filtros.png"
[1] Original
[2] Ruido 10%
[3] Gaussiano 5x5
[4] Media 5x5
[5] Mediana 3x3
[6] Mediana 5x5
Figura 3.24: Filtros para ruído sal e pimenta (10%): Gaussiano, Média e Mediana. Bilateral e morfológico (open+close) ficam só na trilha Python — bilateral não tem equivalente em morph.hpp e morfologia é do próximo capítulo.
3.8 Aplicación Práctica: Preprocesamiento para Segmentación
En la práctica, las técnicas de este capítulo rara vez se utilizan de forma aislada. Un pipeline de preprocesamiento típico combina varias etapas en secuencia, adaptándose al tipo de imagen y a la aplicación. La Figura 3.25 ilustra un pipeline completo:
Ecualización de histograma (CLAHE): normaliza el contraste independientemente de las condiciones de iluminación;
Filtro Gaussiano: suaviza el ruido de adquisición sin destruir bordes;
Detección de bordes (Sobel/Canny): extrae estructuras relevantes para la segmentación.
NotaEl orden importa
El orden de las operaciones afecta el resultado final. En general: (1) normalización de intensidad → (2) reducción de ruido → (3) realce/segmentación. Invertir el orden puede amplificar el ruido o perder bordes antes de detectarlos.
%%writefile tmp/fig_03_pipeline.cpp#define MM_OUT "tmp/fig_03_pipeline.png"#include "morph.hpp"#include <iostream>#include <filesystem>int main() {// [pdi:state-io] auto-generated — do not edit by handmm::Image img_gray_crop = mm::_read_state("tmp/state/img_gray_crop_58.png");// [pdi:state-io:end]//| label: fig-03-pipeline//| fig-cap: "*Pipeline* de pré-processamento: equalização → Gaussiano → Canny. A trilha Python usa CLAHE no lugar da equalização global; CLAHE não tem equivalente em morph.hpp."//| echo: true//| output: true mm::Image img_eq = mm::equalize(img_gray_crop);// Etapa 1: equalização global mm::Image img_gauss = mm::gaussian(img_eq, 5, 0);// Etapa 2: Gaussiano mm::Image edges = mm::canny(img_gauss, 50, 150);// Etapa 3: Canny mm::Image edges_direct = mm::canny(img_gray_crop, 50, 150);// Canny direto, sem pré-processo mm::show( std::vector<mm::Image>{img_gray_crop, img_eq, img_gauss, edges, edges_direct}, MM_OUT, std::vector<std::string>{"Original", "1. Equalizado", "2. Gaussiano", "3. Canny (pipeline)", "Canny (direto)"},5 );// [pdi:panel-io] auto-generated — do not edit by handstd::filesystem::create_directories("tmp");mm::write(img_gray_crop, "tmp/fig_03_pipeline_0.png");mm::write(img_eq, "tmp/fig_03_pipeline_1.png");mm::write(img_gauss, "tmp/fig_03_pipeline_2.png");mm::write(edges, "tmp/fig_03_pipeline_3.png");mm::write(edges_direct, "tmp/fig_03_pipeline_4.png");// [pdi:panel-io:end]return0;}
Overwriting tmp/fig_03_pipeline.cpp
!g++-I. -std=c++17 tmp/fig_03_pipeline.cpp -o tmp/fig_03_pipeline \&& ./tmp/fig_03_pipeline \&& test -f "tmp/fig_03_pipeline.png"\|| echo "⚠ mm::show não gravou tmp/fig_03_pipeline.png"
Figura 3.25: Pipeline de pré-processamento: equalização → Gaussiano → Canny. A trilha Python usa CLAHE no lugar da equalização global; CLAHE não tem equivalente em morph.hpp.
3.9 Resumen
En este capítulo se presentaron las principales técnicas de procesamiento en el dominio espacial, desde la manipulación directa de píxeles hasta el filtrado por vecindad:
Operaciones de punto: aritméticas saturadas (mm::addm, mm::subm) y lógicas bit a bit (mm::band, mm::bor, mm::bnot) para recorte de ROI y combinación de imágenes; alpha blending (mm::blend) para fusión ponderada con peso \(\alpha \in [0,1]\).
Histograma: función discreta de distribución de intensidades; visualizado con mm::histImg y calculado con mm::hist; base para el diagnóstico tonal y para las técnicas de ecualización y especificación.
Ecualización: redistribución automática de las intensidades mediante la CDF (mm::equalize), con variante adaptativa CLAHE para el control local del contraste.
Especificación de histograma: transferencia del perfil tonal de una imagen de referencia mediante mapeo inverso de la CDF — generalización de la ecualización para distribuciones arbitrarias.
Correlación y convolución: mecanismo de ventana deslizante implementado en mm::conv (cv2.filter2D); diferenciados por la rotación de 180° del kernel — relevante solo para kernels asimétricos.
Filtros de suavizado: media (kernel uniforme, desenfoca bordes proporcionalmente al tamaño) y Gaussiano (ponderación radial, separable, sin ringing, conserva mejor los bordes).
Filtros de realce: Laplaciano (\(w_4\)/\(w_8\), segunda derivada isotrópica), Sobel (gradiente direccional de primer orden, con magnitud \(|\nabla f|\) y dirección \(\theta\)) y Unsharp Masking (amplificación de las altas frecuencias con parámetro \(k\)).
Filtro de la mediana: no lineal, robusto a valores atípicos, conserva bordes — superior a los filtros lineales para ruido sal y pimienta.
Pipeline práctico: encadenamiento CLAHE → Gaussiano → Canny como estrategia de preprocesamiento; mm::drawImgKernel para visualización didáctica de la ventana deslizante.
El Capítulo 4 abordará la morfología matemática (erosión, dilatación, apertura y cierre), explorando en profundidad las funciones mm::ero y mm::dil de la biblioteca morph.py. A continuación, el Capítulo 5 presentará el procesamiento en el dominio de la frecuencia, con enfoque en la Transformada de Fourier y en técnicas de filtrado espectral.
3.10 🤖 Uso del Gemini Notebook como Tutor Complementario
En esta edición, incentivamos el uso del Gemini Notebook como herramienta complementaria de aprendizaje. Esta herramienta de IA utiliza exclusivamente los documentos proporcionados por el autor como base de conocimiento, garantizando respuestas coherentes con el contenido del libro — incluyendo las funciones de la biblioteca morph.py y los experimentos realizados en este capítulo.
Para cada capítulo, hemos preparado un proyecto específico en la plataforma con el PDF del capítulo, los notebooks y materiales auxiliares. Sugerimos explorar especialmente:
Guía de Estudio: resumen estructurado de los conceptos, ideal para repasar antes de los exámenes;
Conversación: resuelve dudas sobre ecualización, convolución, filtros y pipelines directamente con el tutor;
Preguntas frecuentes: cuestiones típicas sobre la diferencia entre media y mediana, USM, Lapaciano vs. Sobel.
Importante🎓 Estudia con el Tutor Inteligente
Para interactuar con el contenido de este capítulo, accede al siguiente enlace. El entorno contiene materiales didácticos en diferentes formatos, generados a partir del PDF del capítulo. En la plataforma, explora especialmente las opciones Guía de Estudio y Conversación para profundizar tu comprensión.
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 con la versión que estás leyendo.
⚠️ Aviso sobre Contenido Generado por IA
La IA es una poderosa aliada en los estudios, pero el contenido generado puede contener errores o imprecisiones. Consulta siempre libros, artículos científicos y otras fuentes académicas confiables para validar la información. Siempre que sea posible, ejecuta los ejemplos prácticos proporcionados en este capítulo para verificar los resultados.
3.11 Lista de Ejercicios
(10%) Explique la diferencia entre convolución y correlación cruzada. ¿Para qué tipos de kernel los resultados son idénticos? Dé un ejemplo de kernel asimétrico (como Sobel \(G_x\)) y muestre numéricamente que los resultados difieren aplicándolo al parche 5×5 del capítulo de las dos formas.
(15%) Considere una imagen 5×5 con intensidades concentradas entre los niveles 3 y 5 (bajo contraste, 3 bits). Aplique manualmente el algoritmo de ecualización de la Tabla 3.1, completando todas las columnas de la tabla (\(k\), \(h[k]\), \(p[k]\), \(\text{cdf}[k]\), \(\text{lut}[k]\)). Verifique el resultado con mm::equalize.
(15%) Usando mm::conv, aplique el filtro de media con kernels de tamaño 3×3, 9×9 y 21×21 a la imagen del mandril. Para cada versión, calcule el PSNR (Peak Signal-to-Noise Ratio) con respecto a la original: \[\text{PSNR} = 10\log_{10}\!\left(\frac{255^2}{\text{MSE}}\right), \quad \text{MSE} = \frac{1}{MN}\sum_{i,j}(f-g)^2\] Grafique el PSNR en función del tamaño del kernel y explique qué indica la caída progresiva sobre la relación entre suavizado y pérdida de información.
(15%) Usando add_salt_pepper con densidad del 5%, aplique y compare: (a) mm::conv con media 3×3, (b) cv2.GaussianBlur con \(\sigma=1\), (c) cv2.medianBlur con ventana 3×3 y (d) cv2.medianBlur con ventana 5×5. Muestre las imágenes con mm::show en una cuadrícula 2×4 (fila 1: imágenes, fila 2: histogramas mediante mm::histImg). Explique por qué la mediana supera a los filtros lineales usando el argumento de la Tabla 3.4.
(15%) Implemente mm::conv0 usando solo operaciones NumPy vectorizadas — sin bucles en Python y sin cv2.filter2D — con el operador de stride tricks (np.lib.stride_tricks.sliding_window_view). Compare el resultado y el tiempo de ejecución con mm::conv0 (bucles) y mm::conv (cv2) para kernels 3×3 y 15×15 en la imagen del mandril.
(15%) Aplique el Unsharp Masking con \(\sigma=1\) y \(k \in \{0.5, 1.0, 2.0, 4.0\}\) usando la función usm del capítulo. Para cada valor de \(k\): (a) calcule la diferencia absoluta \(|g - f|\), (b) muestre las imágenes y las diferencias con mm::show, y (c) grafique el histograma de las diferencias con mm::histImg. Identifique a partir de cuál \(k\) los artefactos (halos y amplificación de ruido) se vuelven visualmente inaceptables.
(15%) Elija una imagen de rayos X o tomografía disponible públicamente (ej.: mediante mm::read de URL) y diseñe un pipeline de preprocesamiento con al menos 4 etapas secuenciales, justificando cada elección con base en los conceptos del capítulo. Muestre con mm::show en una cuadrícula: imagen original, cada etapa intermedia y el resultado final con sus histogramas (mm::histImg).
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 operaciones de intensidad, histograma, convolución y filtrado espacial.
Szeliski (2022) para la visión por computadora y aplicaciones prácticas de filtrado.
Bradski (2008) para la implementación práctica con OpenCV y morph.py.
3.12 💻 Parte Práctica con Ejercicios de Programación
🎯 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:
import os, urllib.requestos.makedirs("tmp/state", exist_ok=True) # artefatos de build del trayecto C++ (.cpp, binario, PNGs)url ="https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/morph/config.py"ifnot os.path.exists("config.py"): urllib.request.urlretrieve(url, "config.py")# El kernel es Python incluso en el trayecto C++: `mm` (morph.py) es usado por los# simuladores, por la exhibición de las figuras que el binario C++ genera y por el# estado mm::Image entre celdas. cpp=True descarga también el trayecto compilado# (morph.hpp + stb_image*.h), usado en el #include de las celdas %%writefile *.cpp.import configconfig.setup(testsuite=True, cpp=True)from morph import mmfrom testsuite import TestSuite
Para evaluar los tests, ejecuta TestSuite("EP03_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 un archivo, usa run_code(codigo) pasando el código como string en una variable codigo:
codigo ="""from morph import mm# ... tu código aquí ..."""TestSuite("EP03_01").run_code(codigo)
3.12.1 EP03_01 ➕ Adición Saturada de Constante
En sistemas de vigilancia por video, las cámaras en entornos con iluminación variable producen imágenes subexpuestas. El ajuste de brillo mediante adición saturada de una constante es la operación más simple para la corrección inmediata, aplicándose en tiempo real en los chips de cámaras embebidas y en pipelines de preprocesamiento de robots móviles.
Ajuste el valor de la constante k para observar el desplazamiento de brillo de la imagen y el truncamiento por saturación en el intervalo [0, 255].
0
Entrada Original (p)
Resultado Transformado (p')
Fórmula aplicada: clip(p + (0))
Figura 3.26: Simulador EP03_01: Adición Saturada de Constante (p’ = clip(p + k))
%%writefile EP03_01.cpp// your solution
Overwriting EP03_01.cpp
TestSuite("EP03_01.cpp").run()
✔️ EP03_01.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_01.cases
🔍 Probando C++: EP03_01.cpp
⚠️ EP03_01.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.2 EP03_02 🔀 Alpha Blending de Dos Imágenes
En medicina nuclear, imágenes de diferentes modalidades (tomografía computarizada y resonancia magnética) se fusionan para ayudar en el diagnóstico. La mezcla ponderada (alpha blending) es la operación fundamental de este proceso, permitiendo al radiólogo controlar interactivamente el peso de cada modalidad en la imagen mostrada.
Salida: Mostrar la matriz resultante \(L \times C\).
3.12.2.2 📌 Restricciones Computacionales
Redondeo: Aplicar round antes de la conversión a entero.
Saturación: Confinar al intervalo \([0, 255]\) con \(\text{clip}(x) = \max(0, \min(255, x))\).
Operación en float: Realizar la operación en punto flotante antes de redondear.
3.12.2.3 🧠 Fundamentación Teórica
Valor de \(\alpha\)
Resultado
\(\alpha = 1.0\)
Solo \(f_1\)
\(\alpha = 0.5\)
Media aritmética de \(f_1\) y \(f_2\)
\(\alpha = 0.0\)
Solo \(f_2\)
3.12.2.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Línea 3: Real \(\alpha\).
Líneas siguientes: Elementos de \(f_1\) (\(L\) líneas con \(C\) valores cada una).
Líneas siguientes: Elementos de \(f_2\) (\(L\) líneas con \(C\) valores cada una).
Salida:
Matriz resultante \(L \times C\).
3.12.2.5 📌 Ejemplos
Entrada
Salida
Observación
1
3
0.5
0 100 200
100 200 50
50 150 125
Media entre las dos imágenes
1
3
1.0
10 20 30
90 80 70
10 20 30
Solo \(f_1\) (alpha=1)
🔀 Simulador EP03_02: Alpha Blending de Dos Imágenesg = α·f1 + (1−α)·f2
Ajusta el parámetro de transparencia α para observar la combinación lineal ponderada píxel a píxel entre las imágenes f1 y f2.
0.50
α = 0.00 → Solo f2 | α = 0.50 → Media Ponderada Igual | α = 1.00 → Solo f1
Imagen f1
Imagen f2
Resultado g
Fórmula: clip(round(0.50 · f1 + 0.50 · f2))
Figura 3.27: Simulador EP03_02: Mezcla alfa de dos imágenes (g = α·f1 + (1−α)·f2)
%%writefile EP03_02.cpp// your solution
Overwriting EP03_02.cpp
TestSuite("EP03_02.cpp").run()
✔️ EP03_02.cases ya existe en casos/
📋 6 caso(s) cargado(s) de casos/EP03_02.cases
🔍 Probando C++: EP03_02.cpp
⚠️ EP03_02.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.3 EP03_03 🎭 Inversión de Imagen (Negativo Fotográfico)
En radiología, las imágenes de rayos X se visualizan tradicionalmente en negativo: los huesos aparecen en negro sobre fondo blanco. La operación de negativo fotográfico se aplica rutinariamente en PACS (Picture Archiving and Communication Systems) para facilitar la detección de fracturas y densidades óseas.
Dimensiones: Leer los enteros \(L\) (filas) y \(C\) (columnas).
Datos: Leer los valores enteros de la matriz original.
Mapeo: Para cada píxel \(p\), calcular el negativo:
\[p' = 255 - p\]
Salida: Mostrar la matriz resultante de \(L \times C\).
3.12.3.2 📌 Restricciones Computacionales
Sin necesidad de recorte: El resultado de \(255 - p\) con \(p \in [0, 255]\) siempre está \(\in [0, 255]\).
Tipo entero: La salida debe ser valores enteros.
Equivalencia lógica: La operación es idéntica al NOT bit a bit (mm::bnot) en imágenes de 8 bits.
3.12.3.3 🧠 Fundamentación Teórica
Píxel Original \(p\)
Píxel Negativo \(p'\)
Observación
0 (negro)
255 (blanco)
Inversión total
128 (gris medio)
127 (gris medio)
Valor central
255 (blanco)
0 (negro)
Inversión total
3.12.3.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 negativa en \(L\) filas y \(C\) columnas.
3.12.3.5 📌 Ejemplos
Entrada
Salida
Observación
1
4
0 128 200 255
255 127 55 0
Inversión de cada píxel
2
2
10 20
30 40
245 235
225 215
Matriz 2x2 invertida
🎭 Simulador EP03_03: Negativo Fotográfico (Inversión)p' = 255 − p
Observe la inversión complementaria de intensidad: los tonos oscuros se vuelven claros y los tonos claros se vuelven oscuros restando cada píxel del valor máximo de 255.
Entrada Original (p)
Negativo (p' = 255 − p)
Fórmula aplicada: p' = 255 − p
Figura 3.28: Simulador EP03_03: Inversión de Imagen — Negativo Fotográfico (p’ = 255 − p)
%%writefile EP03_03.cpp// your solution
Overwriting EP03_03.cpp
TestSuite("EP03_03.cpp").run()
✔️ EP03_03.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_03.cases
🔍 Probando C++: EP03_03.cpp
⚠️ EP03_03.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.4 EP03_04 📊 Equalización de Histograma (L bits)
En imágenes de satélite de teleobservación, la variación de iluminación a lo largo del día produce imágenes de bajo contraste. La equalización de histograma se aplica automáticamente en satélites como el Landsat para redistribuir los tonos, revelando detalles de vegetación, relieve y zonas urbanas invisibles en la imagen original.
Elija la profundidad de bits (B) y genere imágenes para analizar la dispersión dinámica del histograma y la tabla de remapeo (LUT) en tiempo real.
3 bits → 8 niveles
1 bit (2 niveles)4 bits (16 niveles)8 bits (256 niveles)
Entrada Original
Resultado Ecualizado
Histograma Original
Histograma Ecualizado
LUT (Tabla de Remapeo k → v)
lut[k] = round(cdf[k] · 7) | B=3, niveles=8
Figura 3.29: Simulador EP03_04: Ecualización de Histograma (Niveles L = 2^B)
%%writefile EP03_04.cpp// your solution
Overwriting EP03_04.cpp
TestSuite("EP03_04.cpp").run()
✔️ EP03_04.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_04.cases
🔍 Probando C++: EP03_04.cpp
⚠️ EP03_04.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.5 EP03_05 🔲 Aplicación de Máscara AND Binaria
En sistemas de inspección industrial por visión por computadora, es necesario aislar regiones de interés (ROI) en imágenes de piezas para verificar defectos de fabricación. La operación AND bit a bit con una máscara binaria es el mecanismo fundamental para recortar exactamente el área de inspección, poniendo a cero todos los píxeles fuera de ella.
Dimensiones: Leer los enteros \(L\) (filas) y \(C\) (columnas).
Datos: Leer la matriz de píxeles \(f\) (valores \(\in [0, 255]\)).
Máscara: Leer la matriz binaria \(m\) (valores: solo 0 o 255).
Mapeo: Para cada píxel \((i,j)\), aplicar el AND bit a bit:
\[
g(i,j) = f(i,j) \;\text{AND}\; m(i,j)
\]
donde \(255 =\)11111111 y \(0 =\)00000000 en binario.
Salida: Mostrar la matriz resultante \(L \times C\).
3.12.5.2 📌 Restricciones Computacionales
AND con 255:\(p \; \text{AND} \; 255 = p\) (todos los bits preservados).
AND con 0:\(p \; \text{AND} \; 0 = 0\) (todos los bits puestos a cero).
Máscara: Los únicos valores posibles en la máscara son 0 y 255.
Implementación: En Python, el AND bit a bit entre enteros usa el operador &.
3.12.5.3 🧠 Fundamentación Teórica
Píxel \(f\)
Máscara \(m\)
Resultado \(f\) AND \(m\)
cualquier \(v\)
255 (11111111)
\(v\) (preservado)
cualquier \(v\)
0 (00000000)
0 (puesto a cero)
3.12.5.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Líneas siguientes: Elementos de \(f\) (\(L\) líneas).
Líneas siguientes: Elementos de \(m\) (\(L\) líneas con valores 0 o 255).
Salida:
Matriz resultante \(L \times C\).
3.12.5.5 📌 Ejemplos
Entrada
Salida
Observación
2
3
100 150 200
50 80 120
255 255 0
0 255 255
100 150 0
0 80 120
La máscara selecciona la región
1
4
10 20 30 40
255 0 255 0
10 0 30 0
Alternado preservado/puesto a cero
⬛ Simulador EP03_05: Máscara AND Binariag = f AND m
Haz clic en las celdas de la Máscara m para alternar entre transparente (255) y bloqueante (0), aplicando la operación lógica píxel a píxel.
Imagen f (0–255)
Máscara m (Clic para Alternar)
Resultado g = f AND m
—
—conservados
—puestos a cero
—visible
Leyenda:
255
Transparente (conservado)
0
Bloqueante (puesto a cero)
g(i,j) = f(i,j) & m(i,j)
Figura 3.30: Simulador EP03_05: Aplicación de Máscara AND Binaria
%%writefile EP03_05.cpp// your solution
Overwriting EP03_05.cpp
TestSuite("EP03_05.cpp").run()
✔️ EP03_05.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_05.cases
🔍 Probando C++: EP03_05.cpp
⚠️ EP03_05.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.6 EP03_06 🌫️ Filtro de Media con Kernel N×N
En cámaras de vehículos autónomos, las imágenes capturadas bajo lluvia o niebla presentan ruido gaussiano. El filtro de media se utiliza ampliamente para su reducción en tiempo real, implementándose directamente en el ISP (Image Signal Processor) de los sensores CMOS (Complementary Metal-Oxide-Semiconductor).
Los sensores CMOS son los sensores de imagen utilizados en la mayoría de las cámaras modernas (smartphones, webcams, cámaras automotrices, etc.). Convierten la luz en señales eléctricas, y el ISP procesa estas señales en tiempo real — aplicando operaciones como reducción de ruido, balance de blancos y otros ajustes de imagen.
Tratamiento de Bordes: Los píxeles en el borde (donde la ventana \(N \times N\) sobrepasa los límites) deben copiarse directamente del original sin modificación.
Salida: Mostrar la matriz resultante \(L \times C\).
3.12.6.2 📌 Restricciones Computacionales
Radio:\(r = \lfloor N/2 \rfloor\) (mitad del kernel, entero).
Píxeles internos:\((i,j)\) con \(r \le i < L-r\) y \(r \le j < C-r\).
Redondeo: Usar redondeo matemático antes de convertir a entero.
Sin clipping: El promedio de valores \(\in [0,255]\) permanece en \([0,255]\).
3.12.6.3 🧠 Fundamentación Teórica
Tamaño \(N\)
Coeficiente
Píxeles en la ventana
Efecto
3
\(1/9 \approx 0.111\)
9
Suave
5
\(1/25 = 0.04\)
25
Medio
7
\(1/49 \approx 0.020\)
49
Fuerte
3.12.6.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Línea 3: Entero \(N\) (impar, \(N \ge 3\)).
Líneas siguientes: Elementos de la matriz original.
Píxel aislado: todos los 9 píxeles internos cuya ventana 3×3 incluye el valor 100 reciben round(100/9)=11
🔲 Simulador EP03_06: Filtro de Media con Kernel N×Ng = Media(Vecinos)
Seleccione el tamaño del kernel y pase el mouse sobre los píxeles del resultado para inspeccionar la vecindad y el cálculo de la media aritmética.
Tamaño del kernel:
Imagen Original f (7×7)Con ruido sal y pimienta
Resultado g (Filtro Suavizado)Pase el mouse para inspeccionar
Leyenda:
Ventana del Kernel
Borde (Copiado)
Píxel Inspeccionado
Pase el mouse sobre un píxel interno del resultado para ver el cálculo de la media.
Figura 3.31: Simulador EP03_06: Filtro de Media con Kernel N×N
%%writefile EP03_06.cpp// your solution
Overwriting EP03_06.cpp
TestSuite("EP03_06.cpp").run()
✔️ EP03_06.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_06.cases
🔍 Probando C++: EP03_06.cpp
⚠️ EP03_06.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.7 EP03_07 🔍 Operador Laplaciano (w4) para Realce de Bordas
En tomografías de alta resolución, la nitidez de los bordes entre tejidos es crítica para el diagnóstico. El operador Laplaciano se utiliza ampliamente en pipelines de preprocesamiento de imágenes médicas para resaltar automáticamente los contornos anatómicos antes de la segmentación, evitando la intervención manual del radiólogo.
📐 Simulador EP03_07: Operador Laplaciano (w4)g = f ∓ ∇²f
Seleccione la variante de realce y pase el mouse sobre los píxeles internos del resultado para inspeccionar la vecindad de 4 puntos y la ecuación del Laplaciano.
Variante:
① Imagen Original fEscalón con ruido leve
② Laplaciano ∇²fBordes detectados (±128 shift)
③ Resultado g = f − ∇²fPase el mouse para inspeccionar
Kernel w4 (4-Vecinos)
0
+1
0
+1
−4
+1
0
+1
0
∇²f = T + B + L + R − 4·f
Leyenda:
4-Vecinos del Kernel
Píxel Central
Borde (Copiado)
Pase el mouse sobre un píxel interno del resultado para detallar la ecuación.
Figura 3.32: Simulador EP03_07: Operador Laplaciano (w4) para Realce de Bordas
%%writefile EP03_07.cpp// your solution
Overwriting EP03_07.cpp
TestSuite("EP03_07.cpp").run()
✔️ EP03_07.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_07.cases
🔍 Probando C++: EP03_07.cpp
⚠️ EP03_07.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.8 EP03_08 🧭 Gradiente de Sobel: Gx y Gy
En robots exploradores de Marte (como el Perseverance), la detección de obstáculos se realiza en tiempo real mediante cámaras estereoscópicas. El operador de Sobel calcula el gradiente direccional de la escena y se utiliza en el algoritmo de detección de bordes para identificar rocas, fisuras y desniveles del terreno que puedan comprometer la navegación.
Raíz cuadrada: Usar \(\sqrt{G_x^2 + G_y^2}\) (no la aproximación \(|G_x| + |G_y|\)).
3.12.8.3 🧠 Fundamentación Teórica
Operador
Detecta
Coeficientes diagonales
\(G_x\)
Bordes verticales
\(\pm 1\)
\(G_y\)
Bordes horizontales
\(\pm 1\)
\(|\nabla f|\)
Todos los bordes
Combinado
3.12.8.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Líneas siguientes: Elementos de la matriz.
Salida:
Magnitud del gradiente, matriz \(L \times C\).
3.12.8.5 📌 Ejemplos
Entrada
Salida
Observación
3
3
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
Imagen nula: gradiente cero
3
3
0 0 255
0 0 255
0 0 255
0 0 0
0 255 0
0 0 0
Borde vertical central: Gx alto
🧭 Simulador EP03_08: Gradiente de Sobel (Gx y Gy)|∇f| = √(Gx² + Gy²)
Analice la descomposición horizontal (Gx) y vertical (Gy) del operador de Sobel y pase el mouse sobre los píxeles de la magnitud para inspeccionar la vecindad 3×3.
Pase el mouse sobre un píxel interno de la magnitud para ver la descomposición Gx y Gy.
Figura 3.33: Simulador EP03_08: Gradiente de Sobel (Gx y Gy)
%%writefile EP03_08.cpp// your solution
Overwriting EP03_08.cpp
TestSuite("EP03_08.cpp").run()
✔️ EP03_08.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_08.cases
🔍 Probando C++: EP03_08.cpp
⚠️ EP03_08.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.9 EP03_09 📡 Filtro de la Mediana 3×3
Las imágenes de radar de apertura sintética (SAR) utilizadas en monitoreo ambiental y militar sufren de un tipo específico de ruido llamado speckle, que posee características similares al ruido sal y pimienta. El filtro de la mediana es el método estándar para eliminar este ruido porque preserva los bordes de las estructuras mientras elimina los puntos espurios.
Salida: Mostrar la matriz filtrada \(L \times C\).
3.12.9.2 📌 Restricciones Computacionales
Ventana: Siempre \(3 \times 3 = 9\) elementos.
Mediana: El elemento central de la secuencia ordenada (índice 4 de 0 a 8).
Sin recorte: La mediana de valores en \([0, 255]\) permanece en \([0, 255]\).
No lineal: El filtro de mediana no puede expresarse como convolución lineal.
3.12.9.3 🧠 Fundamentación Teórica
Ruido
Filtro de Media
Filtro de Mediana
Sal y pimienta (0 o 255)
Dispersa el ruido
Elimina sin distorsionar bordes
Gaussiano
Reduce eficazmente
Reduce parcialmente
3.12.9.4 📦 Especificación de Entrada y Salida (VPL)
Entrada:
Línea 1: Entero \(L\).
Línea 2: Entero \(C\).
Líneas siguientes: Elementos de la matriz.
Salida:
Matriz filtrada \(L \times C\).
3.12.9.5 📌 Ejemplos
Entrada
Salida
Observación
3
3
100 100 100
100 0 100
100 100 100
100 100 100
100 100 100
100 100 100
Punto negro eliminado: mediana de 8×100+1×0 = 100
3
3
50 50 50
50 255 50
50 50 50
50 50 50
50 50 50
50 50 50
Punto blanco (sal) eliminado
📉 Simulador EP03_09: Filtro de la Mediana 3×3g = Mediana(Vecinos)
Inyecte ruido impulsivo (sal y pimienta) y pase el mouse sobre los píxeles internos del resultado para inspeccionar la ordenación del vector de vecindad y la eliminación del ruido.
Ruido de sal (255) y pimienta (0) — ~30% de los píxeles internos afectados
Imagen f — Con RuidoSal (255) y pimienta (0) visibles
Resultado g — Sin RuidoPase el mouse para inspeccionar
Vector de Vecindad 3×3 — OrdenadoPase el mouse sobre un píxel interno del resultado para visualizar
—
Leyenda:
Ventana 3×3
Píxel Central
Borde (Copiado)
Mediana
Ruido (Eliminado)
Pase el mouse sobre un píxel interno del resultado para ver el proceso de ordenación.
Figura 3.34: Simulador EP03_09: Filtro de la Mediana 3×3
%%writefile EP03_09.cpp// your solution
Overwriting EP03_09.cpp
TestSuite("EP03_09.cpp").run()
✔️ EP03_09.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP03_09.cases
🔍 Probando C++: EP03_09.cpp
⚠️ EP03_09.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.
3.12.10 EP03_10 ✨ Unsharp Masking (USM)
En los sistemas de digitalización de documentos históricos y obras de arte, la nitidez de las imágenes es fundamental para la lectura de textos manuscritos y detalles ornamentales. El Unsharp Masking (USM) es el algoritmo de realce de nitidez estándar utilizado en escáneres profesionales y software como Adobe Photoshop, controlado por el parámetro \(k\) que determina la intensidad del realce.