1  Fundamentos y Primeros Pasos

Este capítulo inaugura la Parte 1 del libro, dedicada a los fundamentos del Procesamiento Digital de Imágenes (PDI). Se presentan la representación matemática de imágenes digitales y los principales métodos para su manipulación.

Se utiliza el lenguaje C++ y la biblioteca morph.hpp, un puerto didáctico de morph.py (ZAMPIROLLI, 2025).

1.1 Objetivos

Al final de este capítulo, usted será capaz de:

  • Comprender la naturaleza física y matemática de la imagen digital \(f(x,y)\).

  • Identificar las bandas del espectro electromagnético relevantes para PDI.

  • Realizar operaciones básicas: lectura, visualización y guardado de imágenes.

  • Acceder y modificar intensidades de píxeles individualmente.

  • Aplicar umbralización manual.

  • Configurar el entorno de desarrollo en C++.

  • Manipular estructuras de matrices (std::vector) sin caer en trampas de copia/referencia.

1.2 Antes de comenzar: Notebooks interactivos

Este material fue construido bajo el concepto de Literate Programming (Programación Literaria), ideado por Donald Knuth en la década de 1980 (KNUTH, 1984). Knuth —también creador del sistema TeX para tipografía digital— propuso que los programas se escribieran como una narrativa lógica para los seres humanos, intercalando código y documentación.

Para ejecutar una celda, presione Shift + Enter o haga clic en el botón ▶️.

NotaNota sobre el formato

En las versiones renderizadas (PDF o HTML), el código se presenta en bloques estáticos con fines de lectura y referencia. La ejecución interactiva requiere el acceso a través de Google Colab (disponible en la parte superior de la página) o en un entorno local mediante VSCode o Jupyter Notebook.

1.3 Fundamentos

El estudio de sistemas basados en imágenes comprende un ecosistema de disciplinas integradas que transforman datos visuales brutos en conocimiento estructurado. Mientras algunas áreas se centran en la generación de representaciones, otras se dedican al tratamiento y análisis de estos datos para respaldar aplicaciones tecnológicas complejas.

El diagrama presentado en el Figura 1.1 establece la distinción y complementariedad entre el Procesamiento Digital de Imágenes (PDI) y la Visión por Computador (VC). El PDI, destacado en verde, se centra en la transformación de imagen a imagen, con el objetivo de mejorar la calidad o realizar un preprocesamiento, como la eliminación de ruidos y el realce de contraste.

En contraste, la VC, señalada en azul, se centra en la interpretación del contenido visual para extraer modelos o información, como el reconocimiento de objetos y gestos. La región de intersección ilustra la sinergia entre las áreas, donde el PDI prepara los datos visuales para la interpretación por parte de la VC. El mapa también demuestra las interconexiones de ambas disciplinas con áreas como Robótica, Computación Gráfica, Inteligencia Artificial (IA) y Neurociencia.

Figura 1.1: Diagrama relacional que detalla las distinciones fundamentales, sinergias e interconexiones entre PDI y VC en el contexto de sistemas basados en imágenes.

1.3.1 👁️ Visión por Computador

  • Enfoque: Imagen → Modelo (camino inverso de la Computación Gráfica).
  • Objetivo: Extraer información de alto nivel a partir de imágenes o videos.
  • Aplicaciones típicas:
    • Robótica: detección de obstáculos, localización y navegación autónoma.
    • Vigilancia e inspección: reconocimiento de eventos, lectura de placas, control de calidad.
    • Teledetección: análisis de imágenes satelitales, cartografía ambiental.
    • Imágenes médicas: detección de tumores, segmentación de órganos, ayuda al diagnóstico.
    • Interacción humano-computador: reconocimiento de gestos, expresiones faciales, seguimiento ocular.
  • Relación con otras áreas: utiliza técnicas de Aprendizaje Automático e IA para clasificar e interpretar escenas; sirve como “ojos” de la Robótica.

1.3.2 🖼️ Procesamiento Digital de Imágenes (PDI)

  • Enfoque: Imagen → Imagen (generalmente: transformación de una imagen en otra).
  • Objetivo: Mejorar la calidad visual o extraer características de bajo nivel.
  • Aplicaciones comunes:
    • Eliminación de ruido (filtros de media, mediana, gaussiano).
    • Mejora del contraste (ecualización de histograma, ajuste gamma).
    • Detección de bordes (Sobel, Canny, Laplaciano).
    • Segmentación (umbralización, crecimiento de regiones, watershed).
    • Transformaciones geométricas (redimensionamiento, rotación, corrección de perspectiva).
  • Relación con otras áreas (ver Tabla 1.1):
    • Es la base de la mayoría de los sistemas de VC (preprocesamiento).
    • La Computación Gráfica aplica frecuentemente PDI para posprocesamiento (ej.: suavizado, realce).
    • Las técnicas de IA pueden optimizar parámetros de procesamiento (ej.: aprendizaje de filtros).
Tabla 1.1: Conexión entre PDI, VC y otras áreas de la ciencia.
Área Relación con PDI y VC
Inteligencia Artificial Proporciona modelos (redes neuronales, SVM) que interpretan salidas de la VC.
Robótica Consume datos de VC para tomar decisiones (navegación, manipulación).
Aprendizaje Automático Utiliza descriptores extraídos por PDI/VC para entrenar clasificadores.
Computación Gráfica Camino inverso: modelo → imagen; muchas veces aplica PDI para renderizado realista.
Neurociencia Inspira modelos de PDI (ej.: filtros similares a células ganglionares de la retina).

1.4 Etapas del PDI

Las etapas del PDI se presentan en la Figura 1.2, que pueden comprenderse como una cadena de transformaciones que reduce la redundancia de los datos en busca de significado:

  • Bajo Nivel: Actúa directamente sobre los píxeles de la imagen ruidosa para realizar mejoras y filtrados, generando como salida una imagen limpia o realzada.
  • Nivel Medio: Recibe la imagen tratada y realiza la segmentación y descripción, transformando la matriz de píxeles en atributos estructurados (forma, tamaño y textura).
  • Alto Nivel: Utiliza la tabla de atributos para alimentar procesos de lógica e inteligencia artificial, resultando en la decisión o reconocimiento final (como el diagnóstico médico).
Figura 1.2: Representación del flujo secuencial de procesamiento: la salida de cada nivel se convierte en la entrada del nivel subsiguiente.

A Figura 1.3 detalla la secuencia completa del PDI, desde la captura hasta la interpretación. El flujo comienza en la Adquisición de la Imagen (1) y prosigue con el Mejoramiento (2) y la Restauración (3). A continuación, el contenido se aísla mediante la Segmentación (4) y se refina con la Morfología (5). La transición crucial ocurre en la Representación y Descripción (6), donde los objetos visuales se convierten en datos matemáticos (área, perímetro, etc.), lo que permite el Reconocimiento (7). Los procesos auxiliares incluyen el Procesamiento de Imagen en Color y la Compresión, que contribuyen a la eficiencia del almacenamiento y del análisis.

Figura 1.3: Flujo detallado del PDI: desde la adquisición sensorial hasta la extracción de atributos y el reconocimiento automatizado, incluyendo el procesamiento en color y la compresión.

1.5 Formación de la Imagen y el Espectro

El proceso de formación de una imagen se fundamenta en la interacción entre la materia y la energía radiante. Esencialmente, una imagen se concibe cuando un sensor registra la radiación resultante de la interacción con un objeto físico. En el contexto de la visión humana y de la fotografía convencional, este fenómeno depende de una fuente de luz que ilumine la escena; las características de los objetos se codifican entonces a través de las variaciones de intensidad y color de la luz que alcanza el sensor, tal como se ilustra en la Figura 1.4.

Figura 1.4: Representación del espectro visible y su posición en relación con las demás radiaciones electromagnéticas, destacando la variación de las longitudes de onda de 380 nm a 750 nm.

La luz visible ocupa solo una pequeña franja del espectro electromagnético — entre 380 nm (violeta) y 750 nm (rojo) — según se ilustra en Figura 1.5.. Los sensores digitales convencionales operan en esa misma ventana, pero los equipos especializados pueden captar radiaciones invisibles al ojo humano, como el infrarrojo y los rayos X. En PDI, la imagen formada depende directamente de la sensibilidad espectral del sensor utilizado.

Figura 1.5: (A) Espectro electromagnético completo en escala logarítmica, con énfasis en el rango visible. (B) Detalle de la luz visible (380-750 nm) y sus colores. (C) Descomposición de la luz blanca por el prisma: la menor longitud de onda sufre mayor refracción, separando UV, visible e infrarrojo.

A partir de ese proceso físico de adquisición, se vuelve posible modelar matemáticamente la imagen digital como una función bidimensional discreta, en la cual cada punto de la escena está representado por muestras numéricas de intensidad luminosa, formalizando así los conceptos de píxel y de imagen digital presentados en la siguiente sección.

1.6 ¿Qué es una Imagen Digital?

Una imagen digital está formada por una cuadrícula de píxeles (Picture Elements), donde cada píxel es la unidad elemental más pequeña de la imagen.

Tip¿Qué es un Píxel?

Un píxel es la unidad direccionable más pequeña que compone una imagen digital. Cada píxel ocupa una posición única en la cuadrícula y almacena uno o más valores numéricos que representan su intensidad o color.

Representación Matemática

A diferencia de una función continua, el dominio de una imagen digital es un plano rectangular finito \(\mathbb{E} \subset \mathbb{Z}^2\), que representa la cuadrícula de muestreo. Este dominio está indexado por coordenadas enteras:

\[ \mathbb{E} = \{ (x, y) \in \mathbb{Z}^2 \mid 0 \le x < L,\; 0 \le y < H \} \tag{1.1}\]

Donde:

  • \(L\): representa el ancho de la imagen (número de columnas).
  • \(H\): representa la altura de la imagen (número de filas).

La imagen digital es una función que asocia cada par de coordenadas \((x,y)\) a uno o más valores que describen la apariencia del píxel.

\[ f: \mathbb{E} \to \mathcal{V} \tag{1.2}\]

El conjunto \(\mathcal{V}\) define los valores posibles para el píxel (codominio), variando según el tipo de imagen, como se demuestra en la Tabla 1.2.

Tabla 1.2: Principales tipos de imagen digital y sus respectivos conjuntos de valores posibles para cada píxel.
Tipo de imagen \(\mathcal{V}\) (valores del píxel) Representación
Binaria \(\{0, 1\}\) o \(\{0, 255\}\) ⬛◻️
Escala de grises \(\{0, 1, \dots, 255\}\) ░▒▓█
Color (RGB) \(\{0, \dots, 255\}^3\) (triplas ordenadas de valores) 🟥🟩🟦

Ejemplo práctico: Una imagen a color en el modelo RGB puede representarse matemáticamente mediante una función que asocia tres valores de intensidad a cada píxel. Computacionalmente, esta representación corresponde a tres matrices superpuestas — los canales rojo (Red), verde (Green) y azul (Blue) — en las cuales cada elemento almacena la intensidad luminosa del respectivo canal en una posición determinada de la imagen.

Para viabilizar os experimentos prácticos de PDI e VC, este libro utiliza C++17 y un conjunto mínimo de bibliotecas orientadas a la manipulación matricial, al procesamiento de imágenes y a la visualización de resultados, presentadas a continuación.

1.7 Configuração do Ambiente

Este material utiliza C++17 e a biblioteca de cabeçalho único morph.hpp, originalmente proposta para Python em Zampirolli (2025). Aqui, é utilizada uma versão mínima e didática da morph.py, adaptada para compilação simples com g++, conforme apresentado na Tabla 1.3.

Cada célula de código é compilada e executada como um processo isolado (%%writefile arquivo.cpp seguido de g++). Diferentemente do kernel Python, não há estado persistente entre as células: as imagens produzidas por uma célula são salvas em arquivos para serem lidas pela próxima.

Os Ejercicios de Programación (EPs) apresentados al final de los capítulos pueden ser validados por el mismo módulo testsuite.py utilizado en la ruta Python, que compila la solución con g++ y compara su salida con los casos de prueba. Así, los mismos casos de prueba (.cases) pueden validar soluciones en C++, Python y otros lenguajes, tanto en los notebooks como en el Moodle/VPL.

Tabla 1.3: Principales bibliotecas y herramientas utilizadas en la ruta C++ de este libro.
Biblioteca / Herramienta Función principal
g++ (C++17) Compilación de cada celda de código
morph.hpp Abstracción didáctica de operaciones de PDI
stb_image.h / stb_image_write.h Lectura/escritura de PNG/JPEG (sin OpenCV)
testsuite.py Ejecución y validación automática de los EPs

1.8 Sobre el morph.hpp

La morph.hpp es una versión C++ mínima y didáctica de la morph.py: implementa las funciones usadas en este capítulo (read, gray, randomImage, show, write, threshold, drawImg), además de operaciones de morfología (dil/ero y variantes dil0/ero0/dil1/ero1) preparadas para los capítulos siguientes. No hay paridad numérica garantizada con la implementación Python — el criterio es “compila y produce una imagen plausible”, no “resultado bit a bit idéntico al Python”.

Las bibliotecas stb_image.h/stb_image_write.h (dominio público/MIT) están vendorizadas junto al repositorio — es decir, su código fuente ya viene copiado dentro del propio proyecto, en lugar de instalado separadamente mediante apt install — lo que evita depender de paquetes del sistema durante la compilación en Colab. Las operaciones dil()/ero() usan cv::dilate/cv::erode cuando se compilan con -DMM_USE_OPENCV; sin esa flag (por defecto, incluso en Moodle/VPL), se recurre a la versión didáctica equivalente (dil1/ero1) sin requerir OpenCV.

import os, urllib.request

os.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"
if not 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 config
config.setup(testsuite=True, cpp=True)
from morph import mm
from testsuite import TestSuite
✅ Entorno listo. Morph: 1.1.9 | OpenCV: 5.0.0 | TestSuite: 1.1.2

1.9 Fundamentos de Matrices — atención a la copia de referencias

Como una imagen digital puede representarse mediante una matriz, es importante comprender cómo crear y manipular matrices correctamente. En C++, std::vector tiene semántica de valor: copiar el vector copia sus datos. Por ello, std::vector<std::vector<int>> m(3, std::vector<int>(2, 0)) crea de hecho tres filas independientes — el constructor de relleno copia la fila modelo tres veces.

AdvertenciaAtención a la copia de referencias

La trampa aparece cuando se usan punteros o referencias en lugar de copias. En std::vector<int> linha(2, 0); std::vector<std::vector<int>*> m(3, &linha);, los tres elementos de m apuntan a la misma fila, y modificar (*m[0])[0] altera todas. Lo mismo ocurre con auto& linha = m[0]; (referencia — modifica la matriz), en contraste con auto linha = m[0]; (copia — deja la matriz intacta).

Para visualizar este comportamiento, se puede ejecutar el código en Python Tutor (que también ejecuta C++ paso a paso) y comparar el efecto de copias y de referencias.

En la práctica, para el procesamiento digital de imágenes (PDI), se utiliza la estructura mm::Image de la morph.hpp, que también tiene semántica de valor: mm::Image b = a; copia los píxeles, mientras que mm::Image& b = a; crea solo un alias para la misma imagen. El siguiente código presenta diferentes formas de creación de imágenes sintéticas, cuyos resultados se muestran en la Figura 1.6..

%%writefile tmp/fig_imagens_sinteticas.cpp
#define MM_OUT "tmp/fig_imagens_sinteticas.png"
// Compile: g++ -std=c++17 -O2 program.cpp -o program $(pkg-config --cflags --libs opencv4) -lm
#include "morph.hpp"
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>

//| quarto-raw: false
//| label: fig-imagens-sinteticas
//| fig-cap: "Exemplos de imagens sintéticas representadas matricialmente."
//| echo: true
//| output: true

int main() {
    // Criando uma imagem preta (zeros) de 4, 6 pixels
    mm::Image img_preta(4, 6);
    img_preta.at(0, 0) = 255;  // pixel branco no canto superior esquerdo

    // Criando uma imagem branca (255) de 4, 6 pixels
    mm::Image img_branca(4, 6);
    std::fill(img_branca.data.begin(), img_branca.data.end(), 255);
    img_branca.at(3, 5) = 0;  // pixel preto no canto inferior direito

    // Criando uma imagem aleatória para testes (ruído)
    mm::Image img_random = mm::randomImage(4, 6, 255);

    std::cout << "Matriz aleatória gerada:\n";
    std::cout << mm::drawImg(img_random) << "\n";

    mm::show(
        std::vector<mm::Image>{img_preta, img_branca, img_random},
        MM_OUT,
        std::vector<std::string>{
            "Predominantemente preta\n(com 1 pixel branco em (0,0))",
            "Predominantemente branca\n(com 1 pixel preto em (3,5))",
            "Imagem aleatória\n(simulação de ruído)"
        },
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_preta, "tmp/fig_imagens_sinteticas_0.png");
mm::write(img_branca, "tmp/fig_imagens_sinteticas_1.png");
mm::write(img_random, "tmp/fig_imagens_sinteticas_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_imagens_sinteticas.cpp
!g++ -I. -std=c++17 tmp/fig_imagens_sinteticas.cpp -o tmp/fig_imagens_sinteticas \
  && ./tmp/fig_imagens_sinteticas \
  && test -f "tmp/fig_imagens_sinteticas.png" \
  || echo "⚠ mm::show não gravou tmp/fig_imagens_sinteticas.png"
Matriz aleatória gerada:
 64 119 128  75 231 237 
149  43 242 200 106 116 
 48 100 235  34 136 158 
253 225 153 125 241  35 

[1] Predominantemente preta
(com 1 pixel branco em (0,0))
[2] Predominantemente branca
(com 1 pixel preto em (3,5))
[3] Imagem aleatória
(simulação de ruído)
try:
    mm.show(
        [
            mm.read("tmp/fig_imagens_sinteticas_0.png"),
            mm.read("tmp/fig_imagens_sinteticas_1.png"),
            mm.read("tmp/fig_imagens_sinteticas_2.png"),
        ],
        titles=[
            'Predominantemente preta\n(com 1 pixel branco em (0,0))',
            'Predominantemente branca\n(com 1 pixel preto em (3,5))',
            'Imagem aleatória\n(simulação de ruído)',
        ],
        cols=3,
        axis=True,
        figsize=(9, 3),
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_imagens_sinteticas_0.png (ver a versao Python)")
Figura 1.6: Exemplos de imagens sintéticas representadas matricialmente.

1.10 Leyendo y Mostrando Imágenes

En bibliotecas como OpenCV (cv::Mat), las imágenes digitales se representan computacionalmente como matrices multidimensionales. En morph.hpp, la estructura mm::Image adopta un enfoque más simple: un buffer lineal de bytes (std::vector<unsigned char>), con altura, ancho y número de canales explícitos.

Una de las operaciones fundamentales en PDI es la lectura de imágenes.

En morph.hpp, la función mm::read() permite cargar imágenes tanto de archivos locales como de URLs, como se ilustra en Figura 2.7..

%%writefile tmp/fig_01_natureza.cpp
#define MM_OUT "tmp/fig_01_natureza.png"
#include "morph.hpp"
#include <iostream>
#include <string>
#include <filesystem>

int main() {
    //| label: fig-01-natureza
    //| fig-cap: "Mandrill (Mandrillus sphinx) em ambiente natural. Crédito: Julien Renoult (CC BY 4.0)."
    //| echo: true

    std::string url = "https://upload.wikimedia.org/wikipedia/commons/8/87/Mandrillus_sphinx_339428057.jpg";

    mm::Image img = mm::read(url);

    std::cout << "Dimensões (H, W, Canais): " << img.h << ", " << img.w << ", " << img.channels << "\n";
    std::cout << "Tipo de dado: uint8\n";

    mm::show(img, MM_OUT, "Exemplo: Captura RGB");

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img, "tmp/state/img_34.png");
// [pdi:state-io:end]
return 0;
}
Overwriting tmp/fig_01_natureza.cpp
!g++ -I. -std=c++17 tmp/fig_01_natureza.cpp -o tmp/fig_01_natureza \
  && ./tmp/fig_01_natureza \
  && test -f "tmp/fig_01_natureza.png" \
  || echo "⚠ mm::show não gravou tmp/fig_01_natureza.png"
Dimensões (H, W, Canais): 1365, 2048, 3
Tipo de dado: uint8
Exemplo: Captura RGB
try:
    mm.show(mm.read("tmp/fig_01_natureza.png"))
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_01_natureza.png (ver a versao Python)")
Figura 1.7: Mandrill (Mandrillus sphinx) em ambiente natural. Crédito: Julien Renoult (CC BY 4.0).

Alternativa: Descarga de la Imagen para Almacenamiento Local

En entornos donde la lectura directa de URLs no esté disponible —debido a restricciones de red, políticas de firewall o ausencia de conectividad— una alternativa consiste en descargar previamente la imagen al sistema de archivos local y luego cargarla con mm::read(). En la ruta de C++, mm::read también acepta URLs directamente (descarga interna vía curl/wget); esta alternativa cubre el caso de descargar una vez y reutilizar el archivo local. En el siguiente ejemplo, el archivo se guarda como mandrill.png.

!wget -O mandrill.png \
    https://upload.wikimedia.org/wikipedia/commons/8/87/Mandrillus_sphinx_339428057.jpg
%%writefile tmp/ler_local.cpp
#define MM_OUT "tmp/mandrill_local.png"
#include "morph.hpp"

int main() {
    mm::Image img = mm::read("mandrill.png");   // lee del archivo local
    mm::show(img, MM_OUT);                      // Ejemplo: Captura RGB (archivo local)
    return 0;
}
!g++ -I. tmp/ler_local.cpp -o tmp/ler_local && ./tmp/ler_local

Explicación

  • wget -O mandrill.png <URL> realiza la descarga de la imagen y la almacena localmente con el nombre especificado;
  • mm::read("mandrill.png") efectúa la lectura del archivo directamente desde el sistema de archivos, sin necesidad de solicitudes HTTP adicionales;
  • esta estrategia reduce la dependencia de conectividad durante la ejecución de los experimentos y evita descargas repetidas de la misma imagen.
TipNota

El prefijo ! se utiliza en entornos basados en notebooks, como Jupyter Notebook, JupyterLab y Google Colab, para ejecutar comandos del sistema operativo directamente en celdas de código. En terminales convencionales, el comando debe utilizarse sin el prefijo !.

Si la utilidad wget no está instalada, se puede utilizar alternativamente:

!curl -o mandrill.png \
    https://upload.wikimedia.org/wikipedia/commons/8/87/Mandrillus_sphinx_339428057.jpg

1.11 Conversión de Tipos y Umbralización

Como vimos, una imagen a color en el espacio RGB se representa mediante la función:

\[ f: \mathbb{E} \to \{0,1,\dots,255\}^3 \]

Es decir, para cada píxel \((x,y)\), tenemos tres valores \((R,G,B)\) que definen su color.

Conversión a Escala de Grises

Para convertir una imagen RGB a escala de grises (grayscale), es necesario combinar los tres canales en un único valor de intensidad \(g\), que representa el brillo percibido. Como el ojo humano no es igualmente sensible al rojo, verde y azul, se utiliza una media ponderada. El estándar ITU-R BT.601 ({ITU-R}, 2011) define los siguientes pesos:

\[ g = 0.299\,R + 0.587\,G + 0.114\,B \tag{1.3}\]

Tras el cálculo, el valor \(g\) se redondea al entero más cercano y se ajusta al intervalo \([0, 255]\). El resultado es una nueva imagen, ahora en escala de grises, representada por:

\[ f_{\text{gris}}: \mathbb{E} \to \{0,1,\dots,255\} \]

Umbralización (Thresholding)

A partir de la imagen en escala de grises \(f_{\text{gris}}(x,y)\), una operación fundamental es la umbralización, que produce una imagen binaria (solo blanco y negro). Para ello, se elige un valor de corte \(T\) (generalmente en el intervalo \([0,255]\)) y se define:

\[ f_{\text{bin}}(x,y) = \begin{cases} 255 & \text{si } f_{\text{gris}}(x,y) > T \\[4pt] 0 & \text{en caso contrario} \end{cases} \tag{1.4}\]

Ejemplo: Con \(T = 128\), los píxeles con intensidad superior a 128 se vuelven blancos (255); los demás se vuelven negros (0).

La umbralización se usa ampliamente para segmentar objetos del fondo, extraer bordes o crear máscaras binarias para el procesamiento posterior.

Nota: El valor 255 representa el blanco máximo en imágenes de 8 bits, mientras que 0 representa el negro absoluto.

Ejemplo práctico de conversión y umbralización

La Figura 1.8 ilustra los principales pasos para transformar una imagen a color en escala de grises y, posteriormente, convertirla en una imagen binaria mediante umbralización. El siguiente código implementa estas etapas:

%%writefile tmp/fig_01_processamento_basico.cpp
#define MM_OUT "tmp/fig_01_processamento_basico.png"
#include "morph.hpp"
#include <iostream>
#include <filesystem>

int main() {
// [pdi:state-io] auto-generated — do not edit by hand
mm::Image img = mm::_read_state("tmp/state/img_34.png");
// [pdi:state-io:end]

    // 1. Converter para Tons de Cinza
    mm::Image img_gray = mm::gray(img);

    // 2. Aplicar limiar (Pixels > 128 tornam-se 255, outros 0)
    int limiar = 128;
    mm::Image img_binaria = mm::threshold(img_gray, limiar);

    // Uso da nova função
    mm::show(
        std::vector<mm::Image>{img, img_gray, img_binaria}, 
        MM_OUT,
        std::vector<std::string>{"Original", "Tons de Cinza", "Binária (T=" + std::to_string(limiar) + ")"},
        3
    );

    
// [pdi:state-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp/state");
mm::write(img_gray, "tmp/state/img_gray_38.png");
// [pdi:state-io:end]

// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img, "tmp/fig_01_processamento_basico_0.png");
mm::write(img_gray, "tmp/fig_01_processamento_basico_1.png");
mm::write(img_binaria, "tmp/fig_01_processamento_basico_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_01_processamento_basico.cpp
!g++ -I. -std=c++17 tmp/fig_01_processamento_basico.cpp -o tmp/fig_01_processamento_basico \
  && ./tmp/fig_01_processamento_basico \
  && test -f "tmp/fig_01_processamento_basico.png" \
  || echo "⚠ mm::show não gravou tmp/fig_01_processamento_basico.png"
[1] Original
[2] Tons de Cinza
[3] Binária (T=128)
try:
    mm.show(
        [
            mm.read("tmp/fig_01_processamento_basico_0.png"),
            mm.read("tmp/fig_01_processamento_basico_1.png"),
            mm.read("tmp/fig_01_processamento_basico_2.png"),
        ],
        titles=[
            'Original',
            'Tons de Cinza',
            'Binária (T=128)',
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_01_processamento_basico_0.png (ver a versao Python)")
Figura 1.8: Processamento básico de imagens: (a) imagem original, (b) imagem em tons de cinza, (c) imagem binarizada por limiar (T=128).

1.12 Umbralización por el método de Otsu

Tal como se presentó en la Ecuación 1.4, la umbralización convierte una imagen en tonos de gris a binaria usando un valor de corte \(T\). Hasta ahora fijamos \(T = 128\) manualmente.

mm::Image img_bin_fijo = mm::threshold(img_gris, 128);

Sin embargo, la elección manual de \(T\) no siempre es trivial. La biblioteca mm ofrece una alternativa automática: cuando el umbral no se proporciona, la función mm::threshold(img_gris) calcula el valor de \(T\) mediante el método de Otsu (OTSU, 1979). Este método, que se detallará en capítulos futuros, maximiza la varianza entre clases del histograma (frecuencia de cada tono de gris), separando automáticamente los píxeles de objeto y fondo.

El código siguiente compara la umbralización manual (\(T=128\)) con la automática (Otsu). La binarización por Otsu se realiza con mm::threshold(img_gris); dado que la API mínima de morph.hpp no devuelve el \(T\) calculado, el valor mostrado en la etiqueta de la figura se recupera con cv2.threshold en la etapa de visualización (mismo algoritmo, mismo \(T\)).

%%writefile tmp/fig_01_otsu.cpp
#define MM_OUT "tmp/fig_01_otsu.png"
//| quarto-raw: false
//| label: fig-01-otsu
//| fig-cap: "Comparação entre limiarização manual (T=128) e automática (Otsu) sobre a imagem em tons de cinza."
//| 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 hand
mm::Image img_gray = mm::_read_state("tmp/state/img_gray_38.png");
// [pdi:state-io:end]

    // Limiarização com T fixo (manual)
    int T_fixo = 128;
    mm::Image img_bin_fixo = mm::threshold(img_gray, T_fixo);

    // Limiarização pelo método de Otsu (T automático)
    int T_otsu = mm::otsu(img_gray);
    mm::Image img_bin_otsu = mm::threshold(img_gray);   // mesmo T, Otsu quando o argumento é omitido
    std::cout << "Limiar calculado por Otsu: T = " << T_otsu << "\n";
    // ou simplesmente:
    // img_bin_otsu = mm::threshold(img_gray);

    // Exibição lado a lado
    mm::show(
        std::vector<mm::Image>{img_gray, img_bin_fixo, img_bin_otsu}, 
        MM_OUT,
        std::vector<std::string>{"Tons de Cinza", "Binária (T=128)", "Binária (Otsu, T=" + std::to_string(T_otsu) + ")"},
        3
    );

    
// [pdi:panel-io] auto-generated — do not edit by hand
std::filesystem::create_directories("tmp");
mm::write(img_gray, "tmp/fig_01_otsu_0.png");
mm::write(img_bin_fixo, "tmp/fig_01_otsu_1.png");
mm::write(img_bin_otsu, "tmp/fig_01_otsu_2.png");
// [pdi:panel-io:end]
return 0;
}
Overwriting tmp/fig_01_otsu.cpp
!g++ -I. -std=c++17 tmp/fig_01_otsu.cpp -o tmp/fig_01_otsu \
  && ./tmp/fig_01_otsu \
  && test -f "tmp/fig_01_otsu.png" \
  || echo "⚠ mm::show não gravou tmp/fig_01_otsu.png"
Limiar calculado por Otsu: T = 95
[1] Tons de Cinza
[2] Binária (T=128)
[3] Binária (Otsu, T=95)
try:
    T_otsu = mm.otsu(mm.read("tmp/fig_01_otsu_0.png", grayscale=True))
    mm.show(
        [
            mm.read("tmp/fig_01_otsu_0.png"),
            mm.read("tmp/fig_01_otsu_1.png"),
            mm.read("tmp/fig_01_otsu_2.png"),
        ],
        titles=[
            'Tons de Cinza',
            'Binária (T=128)',
            f"Binária (Otsu, T={T_otsu})",
        ],
        cols=3,
    )
except Exception as _e:
    print("figura indisponivel nesta trilha (C++): " + repr(_e) + " tmp/fig_01_otsu_0.png (ver a versao Python)")
Figura 1.9: Comparação entre limiarização manual (T=128) e automática (Otsu) sobre a imagem em tons de cinza.

La Figura 1.9 muestra que el umbral obtenido por Otsu se adapta automáticamente a la imagen, resultando en una binarización más eficiente que un valor fijo, especialmente cuando las intensidades del objeto y del fondo están bien separadas en el histograma. Esta técnica es ampliamente utilizada en sistemas de VC para la binarización de documentos, detección de objetos y preprocesamiento de imágenes.

TipSimplicidad de la biblioteca morph.hpp

Mientras que OpenCV exige la llamada completa:

cv::Mat img_bin;
double T_otsu = cv::threshold(img_gray, img_bin, 0, 255,
                              cv::THRESH_BINARY | cv::THRESH_OTSU);

morph.hpp abstrae toda esa complejidad: basta con llamar a mm::threshold(img_gray). El umbral de Otsu se calcula automáticamente y la imagen binaria se devuelve directamente. Este enfoque permite concentrarse en el concepto, no en los detalles de implementación.

1.13 Acceso a Píxeles

En morph.hpp, la imagen es un mm::Image con un buffer lineal data (fila tras fila, canales intercalados) y el método at(y, x, c) para acceso individual, siguiendo la convención matricial fila (eje Y) y columna (eje X): img.at(fila, columna) para tonos de gris y img.at(fila, columna, canal) para cada canal de una imagen RGB.

El código de la Figura 1.10 demuestra cómo extraer esos valores en imágenes de color (RGB) y en tonos de gris, además de aislar la vecindad inmediata del punto de interés.

# Not yet ported to this language in this version — conceptual reference in Python.

# 1. Coordenadas del píxel objetivo (fila, columna)
r, c = 600, 800

# 2. Acceso directo a los valores del píxel
pixel_cinza = img_gray[r, c]
print(f"Píxel objetivo ({r}, {c}):")
print(f"  Tons de gris (escalar) : {pixel_cinza}")
print(f"  Color (canales RGB)   : R={img[r, c, 0]}, G={img[r, c, 1]}, B={img[r, c, 2]}")

# 3. Vecindario 3x3 alrededor de (r, c); el píxel central va entre corchetes
print("Matriz de vecindario 3x3 (tons de gris):")
for dy in range(-1, 2):
    linha = ""
    for dx in range(-1, 2):
        v = img_gray[r + dy, c + dx]
        if dy == 0 and dx == 0:
            linha += f"[{v:3d}]"
        else:
            linha += f" {v:3d} "
    print("  " + linha)
# 4. Marca la posición del píxel con un cuadrado rojo hueco (borde de
#    3 px) en una copia de la imagen y muestra (equivalente al punto de matplotlib).
marcada = img.copy()
lado = 20   # medio-lado del cuadrado, en píxeles
esp = 5     # grosor del borde

for x in range(c - lado, c + lado + 1):
    for w in range(esp):
        marcada[r - lado + w, x, 0] = 255
        marcada[r - lado + w, x, 1] = 0
        marcada[r - lado + w, x, 2] = 0
        marcada[r + lado - w, x, 0] = 255
        marcada[r + lado - w, x, 1] = 0
        marcada[r + lado - w, x, 2] = 0

for y in range(r - lado, r + lado + 1):
    for w in range(esp):
        marcada[y, c - lado + w, 0] = 255
        marcada[y, c - lado + w, 1] = 0
        marcada[y, c - lado + w, 2] = 0
        marcada[y, c + lado - w, 0] = 255
        marcada[y, c + lado - w, 1] = 0
        marcada[y, c + lado - w, 2] = 0

mm.show(marcada, title=f"Ubicación del píxel ({r}, {c})")
Figura 1.10

Acceso a píxeles en C++ (mm::Image):

  • Escala de grises: img_gray.at(r, c) devuelve un unsigned char (0 a 255) con la intensidad de gris en el píxel.
  • RGB: img.at(r, c, 0), img.at(r, c, 1), img.at(r, c, 2) acceden a los canales R, G y B — un índice de canal a la vez; no hay vector [R, G, B].
  • Vecindario \(3\times 3\): se recorre mediante dos bucles anidados sobre img_gray.at(r + dy, c + dx), con dy, dx en \(\{-1, 0, 1\}\) — no hay segmentación (slicing) como en NumPy. Este barrido es la base para filtros espaciales y convoluciones.
  • No hay gráficos interactivos (equivalente a plt.plot): para marcar la posición del píxel, la celda de código siguiente dibuja un cuadrado rojo hueco alrededor de él en una copia de la imagen (marcada = img.copy(), luego marcada.at(...)) y la muestra con mm::show.

La indexación es basada en cero: (0,0) es la esquina superior izquierda. La primera dimensión controla la altura (filas/Y) y la segunda la anchura (columnas/X).

1.14 Resumen

En este capítulo se presentaron los fundamentos de la representación de imágenes digitales: la definición de píxel, la estructuración de imágenes en matrices y el impacto del muestreo y la cuantificación en la calidad final:

  • Imagen digital = función \(f(x,y)\) que mapea coordenadas a intensidades (escalares o vectoriales).

  • Dominio: conjunto finito \(\mathbb{E} = \{(x,y) \in \mathbb{Z}^2 \mid 0 \le x < L,\; 0 \le y < H\}\).

  • Tipos principales: binaria (\(\mathcal{V} = \{0, 255\}\)), escala de grises (\(\mathcal{V} = [0,255]\)) y RGB (\(\mathcal{V} = [0,255]^3\)).

  • Umbralización convierte la escala de grises en binaria; el método de Otsu determina el valor de corte automáticamente maximizando la varianza entre clases.

  • A morph.hpp (o mm::) ofrece funciones didácticas para operaciones básicas de PDI, como mm::gray(), mm::threshold(), mm::show() (sobrecargada para 1 imagen o varias).

  • Trampa de copia: std::vector tiene semántica de valor, pero punteros/referencias compartidos entre “filas” de una matriz reintroducen el mismo problema del NumPy — prefiera mm::Image, que también copia por valor.

  • Acceso a píxeles mediante img.at(fila, columna), con indexación basada en cero.

El Capítulo 2 abordará histogramas y ecualización de contraste.

1.15 🤖 Uso del Gemini Notebook como Tutor Complementario

En esta edición, además de los notebooks interactivos en Google Colab, el Gemini Notebook está disponible como herramienta complementaria de estudio. La plataforma utiliza exclusivamente los documentos proporcionados por el autor como base de conocimiento, garantizando respuestas coherentes con el contenido del libro.

Importante🎓 Estudia con el Tutor Inteligente

Accede al entorno del capítulo a través del enlace a continuación y explora especialmente las opciones Guía de Estudio y Conversación para profundizar tu comprensión.

🚀 ACCEDER A GEMINI NOTEBOOK: CAPÍTULO 01

🌐 Idioma y Lenguaje de Programació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 a 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.

Funcionalidades Disponibles en la Plataforma

Gemini Notebook ofrece una suite avanzada de herramientas basadas en IA para transformar el contenido estático del libro en una experiencia de aprendizaje dinámica y multimedia. La plataforma utiliza técnicas de RAG (Retrieval-Augmented Generation), fundamentadas en el trabajo de Lewis (2020), para basar las respuestas estrictamente en los documentos proporcionados y minimizar la ocurrencia de alucinaciones.

Las principales funcionalidades incluyen:

  • Resúmenes Multimodales (Audio y Video): Generación de conversaciones naturales entre expertos en el formato de Resumen de Audio (estilo podcast) y Resumen de Video, discutiendo los temas centrales del capítulo, como las diferencias entre PDI y VC, o la interpretación de transformaciones como la umbralización y el método de Otsu.
  • Visualización de Estructuras (Mapa Mental e Infografía): Creación automática de diagramas que conectan visualmente los conceptos, por ejemplo, el flujo de procesamiento desde la captura de la imagen digital, pasando por la conversión a tonos de gris, umbralización y segmentación binaria.
  • Herramientas de Evaluación (Prueba y Tarjetas Didácticas): Generación de Pruebas de opción múltiple y Tarjetas Didácticas (flashcards) para la fijación de conocimientos, basadas en el texto autoral (ej.: preguntas sobre la fórmula de conversión RGB→gris o sobre el funcionamiento del umbral global y de Otsu).
  • Apoyo a la Presentación (Diapositivas e Informes): Ayuda en la estructuración de Presentaciones de Diapositivas y en la redacción de Informes técnicos, facilitando la comunicación de resultados de experimentos con imágenes.
  • Análisis de Datos (Tabla de Datos): Organización de datos extraídos del texto en tablas estructuradas, ayudando en la comprensión de ejemplos prácticos, como la comparación entre diferentes valores de umbral.
  • Chat Contextualizado: Permite el cuestionamiento directo sobre el código y la teoría, como: “¿Cómo implementar la conversión de RGB a tonos de gris usando los pesos del estándar ITU‑R BT.601?” o “¿Qué sucede con la imagen binaria si elijo un umbral T=200 en lugar de T=128?”.

1.16 Lista de Ejercicios

  1. (15%) Con sus propias palabras, defina imagen digital y píxel. Dé un ejemplo concreto de cómo una imagen a color (RGB) se representa matricialmente en el computador.

  2. (15%) Explique las diferencias entre imagen binaria, escala de grises (8 bits) y color RGB, indicando el rango de valores posibles para cada píxel en cada tipo.

  3. (20%) Considerando la fórmula de conversión RGB → escala de grises del estándar ITU‑R BT.601: \[g = 0.299\,R + 0.587\,G + 0.114\,B\] Calcule el valor del píxel en escala de grises para \((R,G,B) = (80, 180, 30)\). Redondee al entero más cercano.

  4. (20%) ¿Qué es umbralización (thresholding)? Explique la diferencia entre elegir un umbral \(T\) fijo (ej.: \(T=128\)) y utilizar el método de Otsu para la determinación automática del umbral. En pocas palabras, ¿cómo elige el umbral el método de Otsu?

  5. (15%) En el contexto de la biblioteca didáctica mm discutida en el capítulo, responda:

      1. (7,5%) ¿Cómo se accede al valor del píxel en la posición (fila=50, columna=60) de una imagen en tonos de gris img_gray?
      1. (7,5%) ¿Cuál es la ventaja de usar mm::threshold(img_gray) sin pasar el umbral? Compare con la llamada equivalente en OpenCV (cv::threshold).
  6. (15%) ¿Qué representan los campos h, w y channels de un mm::Image para una imagen RGB? Dé un ejemplo concreto con una imagen de 640×480 píxeles.

Referencias del Capítulo

La fundamentación teórica de este capítulo comprende las siguientes obras de PDI y VC:

  • Gonzalez (2018) para los fundamentos de Procesamiento Digital de Imágenes (PDI).
  • Singh (2019) para la implementación práctica de métodos de procesamiento y análisis de imágenes.
  • Szeliski (2022) para el estudio de VC y algoritmos fundamentales.
  • Bradski (2008) para la aplicación de la biblioteca OpenCV en el entorno Python.
  • Lewis (2020) para el concepto de generación aumentada por recuperación (RAG), utilizado en el soporte al procesamiento de la información de este material.

# Aquí va la traducción del contenido en portugués al español
# (Este bloque de código se mantiene sin cambios según las reglas)

1.17 💻 Parte Práctica con Ejercicios de Programación

🎯 Objetivo de este Cuaderno

Los Ejercicios de Programación (EPs) presentados a continuación también pueden enviarse en actividades del Moodle (actividades VPL) que proporcionan feedback automático.

Este cuaderno fue desarrollado para superar limitaciones de uso del Moodle. Con él, se debe:

  1. Desarrollar: Escribir y editar tu solución directamente en el entorno Colab.
  2. Validar: Probar tu código localmente utilizando los mismos casos de prueba del Moodle.
  3. Organizar: Guardar tus códigos de las actividades VPL de forma segura.
  4. Evaluar: Cuando estés conectado al Moodle, basta con copiar tu solución y hacer clic en Evaluar en el Moodle (si estás en la red de la UFABC) para registrar tu nota oficial.

⚙️ Instrucciones Paso a Paso

En un entorno de ejecución (como VSCode, Jupyter o Colab), siga el orden a continuación para configurar el entorno y validar sus ejercicios:

Preparación del Entorno

Ejecute la celda de código a continuación para descargar morph.py y testsuite.py del repositorio de la disciplina — solo si aún no existen en el directorio local. Con ambos en ./, el notebook y los subprocesos del TestSuite encuentran el módulo sin configuraciones adicionales de ruta.

Nota: El script testsuite.py buscará automáticamente los casos de prueba en all/{cap}/cases en GitHub.

Escribiendo el Código

Guarde su solución en una celda de código usando el comando mágico %%writefile. El nombre del archivo debe seguir el patrón EPX_Y.*, donde X es el capítulo, Y es el ejercicio y * es la extensión del lenguaje.

Ejemplo: %%writefile EP01_01.py

Descarga

Descargue morph.py y testsuite.py ejecutando la celda a continuación:

import os, urllib.request

os.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"
if not 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 config
config.setup(testsuite=True, cpp=True)
from morph import mm
from testsuite import TestSuite
✅ Entorno listo. Morph: 1.1.9 | OpenCV: 5.0.0 | TestSuite: 1.1.2

Ejecutando los Testes

Después de guardar el archivo con tu solución, ejecuta el comando a continuación (en una nueva celda) para evaluar los testes automáticos:

TestSuite("EP01_01.extensión").run()

Reemplaza la extensión según el lenguaje usado:

Lenguaje Extensión
Python .py
Java .java
C .c
C++ .cpp
JavaScript .js
R .r

Cómo funciona: El TestSuite descarga los casos de prueba de GitHub, ejecuta tu programa con cada entrada y compara la salida con la esperada – calculando automáticamente tu nota.

Para probar código Python directamente, sin guardar 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("EP04_01").run_code(codigo)

⚠️ Importante: Reglas y Buenas Prácticas

🔹 Sobre la Entrada de Datos

Su programa debe leer la entrada estándar (teclado).

  • Python: Utilice input().
  • Otros lenguajes: Utilice el comando de lectura estándar equivalente (cin, Scanner, etc.).

🔹 Configuración de IA en Colab

Para un mejor aprendizaje, se recomienda desactivar el autocompletado de código por IA, ya que no estará disponible durante las evaluaciones. Por ejemplo, en el navegador Chrome:

  • Vaya a: Herramientas > Configuración > IA generativa
  • Desmarque: Habilitar generación de código

🔹 Integridad Académica (Plagio)

Este recurso de pruebas locales se aplica a EPs sin variaciones. Sin embargo:

  • Individualidad: Cada estudiante debe desarrollar su propia solución.
  • Detección de Similitud: El profesor utiliza herramientas que detectan copias, incluso con cambios de nombres de variables o espacios en blanco.

1.17.1 EP01_01 📏 Tres métricas de distancia en PDI

En esta actividad, debes escribir un programa que calcule las tres distancias clásicas en PDI: Euclidiana (L2), City‑Block (L1) y Chessboard (L∞).

  • Lee 4 números reales que representan las coordenadas: \(A_x, A_y, B_x, B_y\).
  • Calcula las tres distancias utilizando las fórmulas:

\[d_{\text{Euclidiana}} = \sqrt{(B_x - A_x)^2 + (B_y - A_y)^2}\]

\[d_{\text{City-block}} = |B_x - A_x| + |B_y - A_y|\]

\[d_{\text{Chessboard}} = \max\big(|B_x - A_x|,\; |B_y - A_y|\big)\]

  • Imprime los tres resultados, cada uno en una línea, formateados con dos cifras decimales, en el orden: Euclidiana, City‑block, Chessboard.

📌 Importante:

  • Utiliza las funciones matemáticas estándar de tu lenguaje: math.sqrt, abs (o fabs) y max.
  • La salida debe contener solo los números (uno por línea), sin textos adicionales.
  • Consulta un simulador interactivo para esta cuestión en la Figura 1.11 (gráfico con arrastre de los puntos y visualización de las tres métricas).

1.17.1.1 🖼️ ¿Por qué es importante? – Costo computacional

En una imagen 1000×1000 píxeles (1 millón de píxeles), calcular la distancia de cada píxel a un punto de referencia exige 1 millón de operaciones. La elección de la métrica afecta el rendimiento:

Métrica Operaciones por píxel Costo relativo (1M píxeles) Cuándo usar
Euclidiana (L2) 2 restas, 2 multiplicaciones, 1 suma, 1 sqrt 🔴 Más costosa – sqrt es cara Distancia “real” en el espacio continuo
City‑block (L1) 2 restas, 2 abs, 1 suma 🟡 Moderada – sin raíz cuadrada Cuadrículas, robótica, imágenes binarias
Chessboard (L∞) 2 restas, 2 abs, 1 max 🟢 Más eficiente Movimientos de piezas, morfología

La función sqrt es computacionalmente más cara que operaciones como suma, resta, multiplicación y valor absoluto. En CPUs modernas, la diferencia puede ser pequeña (alrededor de 1,5× a 3×), pero en sistemas embebidos o en bucles de millones de iteraciones, cualquier ganancia importa. Por eso, cuando el objetivo es solo comparar distancias (p. ej.: encontrar el punto más cercano), usa la distancia euclidiana al cuadrado.

1.17.1.2 📋 Tarea (especificación para VPL)

Entrada:
Una única línea con cuatro números reales: Ax Ay Bx By

Salida:
Tres líneas, cada una con un número real de dos cifras decimales (Euclidiana, City‑block, Chessboard).

1.17.1.3 📌 Ejemplos

Entrada Salida Observación
0
0
3
4
5.00
7.00
4.00
Triángulo 3‑4‑5
0
0
1
1
1.41
2.00
1.00
Diagonal unitaria

Ejemplo de prueba de sqrt en Python, con timeit aislando cada operación:

%%writefile tmp/mm_out_1.cpp
#include <iostream>
#include <cmath>
#include <chrono>

// Tiempo en segundos de una lambda
template <typename F>
double timeit(F&& f, int N) {
    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) f();
    auto end = std::chrono::high_resolution_clock::now();
    return std::chrono::duration<double>(end - start).count();
}

int main() {
    const int N = 50'000'000;

    // Solo suma
    auto apenas_soma = []() -> double {
        double a = 3.0, b = 4.0;
        return a + b;
    };

    // Suma y raíz cuadrada
    auto soma_e_sqrt = []() -> double {
        double a = 3.0, b = 4.0;
        return std::sqrt(a*a + b*b);
    };

    double t_soma = timeit(apenas_soma, N);
    double t_sqrt = timeit(soma_e_sqrt, N);

    std::cout.precision(3);
    std::cout << std::fixed;
    std::cout << "Soma simples       : " << t_soma << " s\n";
    std::cout << "Soma + sqrt        : " << t_sqrt << " s\n";
    std::cout << "Razão (sqrt/soma)  : " << (t_sqrt / t_soma) << "x\n";

    return 0;
}
Overwriting tmp/mm_out_1.cpp
!g++ -I. -std=c++17 tmp/mm_out_1.cpp -o tmp/mm_out_1 \
  && ./tmp/mm_out_1
Soma simples       : 0.123 s
Soma + sqrt        : 0.208 s
Razão (sqrt/soma)  : 1.685x
🎮 Simulador EP01_01: Métricas de Distancia en el Espacio Discreto Euclidiana vs City-block vs Chessboard

Haz clic y arrastra los puntos A o B en el plano cartesiano o ajusta sus coordenadas abajo para comparar las tres métricas de distancia en tiempo real.

📐 EUCLIDIANA (L2)
5.00
√(Δx² + Δy²)
🧱 CITY-BLOCK (L1)
7.00
|Δx| + |Δy|
🏁 CHESSBOARD (L∞)
4.00
max(|Δx|, |Δy|)
👆 Arrastra los puntos A (Morado) o B (Naranja) en la cuadrícula.
Punto A
Punto B
Leyenda Geométrica: Línea discontinua (Euclidiana), Camino ortogonal en L (City-block) y Resalte de la dimensión máxima (Chessboard).
Euclidiana City-block Chessboard (Máx)
Figura 1.11: Simulador EP01_01: Distancias Euclidiana, City-block y Chessboard

1.17.1.4 🐍 Python

Basta crear una celda de código normal e insertar el código Python. La entrada se puede simular usando input(), que funciona normalmente.

Ejemplo de celda:

%%writefile EP01_01.py
# Código Python
x1,y1,x2,y2 = int(input()), int(input()), int(input()), int(input())
# Cálculo de las diferencias
dx = abs(x2 - x1)
dy = abs(y2 - y1)

# 1. Distancia Euclidiana (L2)
dist_euclidiana = (dx**2 + dy**2)**0.5

# 2. Distancia City-block / Manhattan (L1)
dist_city_block = dx + dy

# 3. Distancia Chebyshev / Tablero de ajedrez (Linf)
dist_chessboard = max(dx, dy)

# Salida formateada según los casos de prueba
print(f"{dist_euclidiana:.2f}")
print(f"{dist_city_block:.2f}")
print(f"{dist_chessboard:.2f}")
Overwriting EP01_01.py
# Espera que usted escriba 4 números enteros al ejecutar esta celda.
# En Jupyter o Google Colab, la magia %run -i permite que el script lea del teclado.
# En la terminal común, usted usaría: python3 EP01_01.py (sin el '!' y sin '%run').

# %run -i EP01_01.py
# Envía 4 enteros como entrada estándar (stdin) al script EP01_01.py usando un pipe
!echo -e "0\n0\n4\n4" | python3 EP01_01.py
5.66
8.00
4.00
TestSuite("EP01_01.py").run()
✔️ EP01_01.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_01.cases

🔍 Probando Python: EP01_01.py
✔️ Caso 1: OK
✔️ Caso 2: OK
✔️ Caso 3: OK
✔️ Caso 4: OK
✔️ Caso 5: OK

📊 Resultado: 5/5 (100.0%)
🎉 ¡Felicidades! Todas las pruebas pasaron.

1.17.1.5 ☕ Java

Para ejecutar Java en Colab, debes usar una celda con el prefijo %%writefile para guardar el código en un archivo, compilar y ejecutar.

%%writefile EP01_01.java
import java.util.Scanner;

class EP01_01 {
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        
        double x1 = s.nextDouble(), y1 = s.nextDouble();
        double x2 = s.nextDouble(), y2 = s.nextDouble();
        
        double dx = Math.abs(x2 - x1);
        double dy = Math.abs(y2 - y1);
        
        System.out.printf("%.2f\n", Math.sqrt(dx*dx + dy*dy));
        System.out.printf("%.2f\n", dx + dy);
        System.out.printf("%.2f\n", Math.max(dx, dy));
    }
}
Overwriting EP01_01.java
!javac EP01_01.java
!echo -e "0\n0\n4\n4" | java EP01_01
5,66
8,00
4,00
TestSuite("EP01_01.java").run()
✔️ EP01_01.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_01.cases

🔍 Probando Java: EP01_01.java
✔️ Caso 1: OK
✔️ Caso 2: OK
✔️ Caso 3: OK
✔️ Caso 4: OK
✔️ Caso 5: OK

📊 Resultado: 5/5 (100.0%)
🎉 ¡Felicidades! Todas las pruebas pasaron.

1.17.1.6 💻 C

De forma similar, use %%writefile para guardar el código, luego compile y ejecute. Para C, utilizamos el compilador GCC.

# Instalar el compilador GCC y herramientas de build
# El build-essential incluye gcc, g++, make, etc.
import platform, shutil, subprocess

def instalar_gcc():
    if shutil.which("gcc"):
        print("✅ GCC ya disponible."); return
    if platform.system() != "Linux":
        print("⚠️ Mac: xcode-select --install | Windows: WSL o MinGW"); return
    try: import google.colab; cmd = ["apt-get", "install", "-y", "build-essential"]
    except ImportError:       cmd = ["sudo", "apt-get", "install", "-y", "build-essential"]
    subprocess.run(cmd, check=True)
    print("✅ ¡build-essential instalado!")

instalar_gcc()
✅ GCC ya disponible.
%%writefile EP01_01.c
#include <stdio.h>
#include <math.h>

int main() {
    double x1, y1, x2, y2;
    if (scanf("%lf %lf %lf %lf", &x1, &y1, &x2, &y2) != 4) return 0;

    double dx = fabs(x2 - x1);
    double dy = fabs(y2 - y1);

    // Euclidiana, City-block e Chessboard
    printf("%.2f\n", sqrt(dx * dx + dy * dy));
    printf("%.2f\n", dx + dy);
    printf("%.2f\n", fmax(dx, dy));

    return 0;
}
Overwriting EP01_01.c
# Compila el archivo .c generando el ejecutable EP01_01
# -lm se usa para enlazar la biblioteca matemática (math.h) si es necesario

!gcc EP01_01.c -o EP01_01 -lm
!echo -e "0\n0\n4\n4" | ./EP01_01
5.66
8.00
4.00
TestSuite("EP01_01.c").run()
✔️ EP01_01.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_01.cases

🔍 Probando C: EP01_01.c
✔️ Caso 1: OK
✔️ Caso 2: OK
✔️ Caso 3: OK
✔️ Caso 4: OK
✔️ Caso 5: OK

📊 Resultado: 5/5 (100.0%)
🎉 ¡Felicidades! Todas las pruebas pasaron.

1.17.1.7 💻 C++

De manera similar a Java, usa %%writefile para guardar el código, luego compila y ejecuta. Recuerda que, en Colab, también es necesario instalar lo siguiente:

# Instalar el compilador G++ para C++
# El build-essential incluye el g++, make, etc.
import platform, shutil, subprocess

def instalar_gpp():
    if shutil.which("g++"):
        print("✅ G++ ya disponible."); return
    if platform.system() != "Linux":
        if platform.system() == "Darwin":
            print("⚠️ Mac: xcode-select --install")
        else:
            print("⚠️ Windows: use WSL o MinGW (https://www.mingw-w64.org)")
        return
    try: import google.colab; cmd = ["apt-get", "install", "-y", "build-essential"]
    except ImportError:       cmd = ["sudo", "apt-get", "install", "-y", "build-essential"]
    subprocess.run(cmd, check=True)
    print("✅ Compilador C++ listo.")

instalar_gpp()
✅ G++ ya disponible.
%%writefile EP01_01.cpp
#include <iostream>
#include <iomanip>
#include <cmath>
#include <algorithm>

int main() {
    double x1, y1, x2, y2;
    if (!(std::cin >> x1 >> y1 >> x2 >> y2)) return 0;

    double dx = std::abs(x2 - x1);
    double dy = std::abs(y2 - y1);

    std::cout << std::fixed << std::setprecision(2);
    
    // Euclidiana, City-block e Chessboard
    std::cout << std::sqrt(dx*dx + dy*dy) << std::endl;
    std::cout << (dx + dy) << std::endl;
    std::cout << std::max(dx, dy) << std::endl;

    return 0;
}
Overwriting EP01_01.cpp
!g++ EP01_01.cpp -o EP01_01
!echo -e "0\n0\n4\n4" | ./EP01_01
5.66
8.00
4.00
TestSuite("EP01_01.cpp").run()
✔️ EP01_01.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_01.cases

🔍 Probando C++: EP01_01.cpp
✔️ Caso 1: OK
✔️ Caso 2: OK
✔️ Caso 3: OK
✔️ Caso 4: OK
✔️ Caso 5: OK

📊 Resultado: 5/5 (100.0%)
🎉 ¡Felicidades! Todas las pruebas pasaron.

1.17.1.8 🌐 JavaScript (Node.js)

Para JavaScript, use %%writefile para crear el archivo y ejecútalo con Node:

%%writefile EP01_01.js
function escreva(s) { try { document.write(s + "<br>"); } catch(e) { console.log(s); } }

process.stdin.once('data', data => {
  const valores = data.toString().trim().split(/\s+/);
  const x1 = parseFloat(valores[0]);
  const y1 = parseFloat(valores[1]);
  const x2 = parseFloat(valores[2]);
  const y2 = parseFloat(valores[3]);

  const dx = Math.abs(x2 - x1);
  const dy = Math.abs(y2 - y1);

  // Euclidiana, City-block e Chessboard
  escreva(Math.sqrt(dx * dx + dy * dy).toFixed(2));
  escreva((dx + dy).toFixed(2));
  escreva(Math.max(dx, dy).toFixed(2));
});
Overwriting EP01_01.js
TestSuite("EP01_01.js").run()
✔️ EP01_01.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_01.cases

🔍 Probando Node.js: EP01_01.js
✔️ Caso 1: OK
✔️ Caso 2: OK
✔️ Caso 3: OK
✔️ Caso 4: OK
✔️ Caso 5: OK

📊 Resultado: 5/5 (100.0%)
🎉 ¡Felicidades! Todas las pruebas pasaron.

1.17.1.9 📊 R

En Colab, se puede ejecutar código R directamente utilizando la magia %%R.
El programa debe leer cuatro números (x1, y1, x2, y2) y mostrar la distancia euclidiana con dos decimales.

Ejemplo de celda:

%%R
dados <- scan(file = "stdin", n = 4, quiet = TRUE)
x1 <- dados[1]; y1 <- dados[2]; x2 <- dados[3]; y2 <- dados[4]
dist <- sqrt((x2 - x1)^2 + (y2 - y1)^2)
cat(sprintf("%.2f", dist))
# Instalar R y Rscript
# r-base instala el entorno R completo, incluido Rscript
import platform, shutil, subprocess

def instalar_r():
    if shutil.which("R"):
        print("✅ R ya disponible."); return
    if platform.system() != "Linux":
        if platform.system() == "Darwin":
            print("⚠️ Mac: https://cran.r-project.org/bin/macosx/")
        else:
            print("⚠️ Windows: https://cran.r-project.org/bin/windows/base/")
        return
    try: import google.colab; cmd = ["apt-get", "install", "-y", "r-base"]
    except ImportError:       cmd = ["sudo", "apt-get", "install", "-y", "r-base"]
    subprocess.run(cmd, check=True)
    print("✅ Entorno R listo.")

instalar_r()
✅ R ya disponible.

Para probar de la misma manera que en los ejemplos anteriores, se debe usar la entrada estándar (stdin) en la terminal o adaptar el código según lo siguiente:

%%writefile EP01_01.r
dados <- scan(file = "stdin", n = 4, quiet = TRUE)
dx <- abs(dados[3] - dados[1])
dy <- abs(dados[4] - dados[2])

# Salidas: Euclidiana, City-block y Chessboard
cat(sprintf("%.2f\n%.2f\n%.2f\n", 
    sqrt(dx^2 + dy^2), 
    dx + dy, 
    max(dx, dy)))
Overwriting EP01_01.r
!echo -e "0\n0\n4\n4" | Rscript EP01_01.r
5.66
8.00
4.00
TestSuite("EP01_01.r").run()
✔️ EP01_01.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_01.cases

🔍 Probando R: EP01_01.r
✔️ Caso 1: OK
✔️ Caso 2: OK
✔️ Caso 3: OK
✔️ Caso 4: OK
✔️ Caso 5: OK

📊 Resultado: 5/5 (100.0%)
🎉 ¡Felicidades! Todas las pruebas pasaron.

1.17.2 EP01_02 📊 Desempeño Predictivo — Métricas de ML en VC

En esta actividad, entrarás al mundo del Aprendizaje Automático (Machine Learning). Tu objetivo es evaluar el desempeño de un clasificador binario calculando métricas a partir de una Matriz de Confusión.

1.17.2.1 🧠 ¿Por qué importa la métrica correcta?

Imagina un detector de monedas de R$ 1,00. El impacto del error define la métrica prioritaria:

Métrica Ejemplo Práctico Importancia en PDI/VC
Exactitud Conteo de Granos Útil cuando las clases están equilibradas (ej: la mitad de los granos con defecto, la mitad sanos).
Precisión Seguridad/Biometría Crucial para evitar Falsos Positivos (ej: no permitir que un impostor acceda a un sistema por error de reconocimiento).
Sensibilidad Salud (Tumores) Crucial para evitar Falsos Negativos (ej: no dejar que un tumor pase desapercibido en un examen de Rayos-X).
Puntuación F1 Billetes de Dinero Ideal para un equilibrio entre no rechazar billetes verdaderos y no aceptar billetes falsos.

1.17.2.2 📊 La Matriz de Confusión

Predicha Positiva Predicha Negativa
Real Positiva VP (Verdadero Positivo) FN (Falso Negativo)
Real Negativa FP (Falso Positivo) VN (Verdadero Negativo)

Tarea:

  1. Lee 4 valores enteros en el orden: VP, FN, FP, VN.
  2. Calcula las métricas utilizando las fórmulas:

\[\text{Exactitud} = \frac{VP + VN}{VP + VN + FP + FN}\]

\[\text{Precisión} = \frac{VP}{VP + FP}\]

\[\text{Sensibilidad (Recall)} = \frac{VP}{VP + FN}\]

\[\text{Puntuación F1} = \frac{2 \times \text{Precisión} \times \text{Sensibilidad}}{\text{Precisión} + \text{Sensibilidad}}\]

  1. Imprime los resultados formateados con dos decimales, uno por línea.

📌 Importante:

  • Usa división en punto flotante para evitar resultados truncados.
  • El orden de salida debe ser: Exactitud, Precisión, Sensibilidad y Puntuación F1.
  • Consulta la simulación interactiva de este EP en la Figura 1.12.

1.17.2.3 📌 Ejemplo de Ejecución

Entrada Salida Observación
40 0.75 Exactitud
10 0.73 Precisión
15 0.80 Sensibilidad
35 0.76 Puntuación F1

(Nota: VP=40, FN=10, FP=15, VN=35. Total de casos = 100)

🎮 Simulador EP01_02: Desempeño Predictivo — Métricas de ML Matriz de Confusión & Métricas

Seleccione un escenario predefinido o ajuste los controles deslizantes para observar el impacto en tiempo real en la matriz de confusión y en las métricas de evaluación.

ENTRADAS (PARÁMETROS DE LA MATRIZ)
40
10
15
35
MATRIZ DE CONFUSIÓN DISCRETA
Pred +
Pred −
Real +
VP
40
FN
10
Real −
FP
15
VN
35
MÉTRICAS CALCULADAS EN TIEMPO REAL
Exactitud 0.75
(VP + VN) / Total
Precisión 0.73
VP / (VP + FP)
Sensibilidad (Recall) 0.80
VP / (VP + FN)
Puntuación F1 (Media Armónica) 0.76
2 · (P · R) / (P + R)
Total de Muestras: 100
Figura 1.12: Simulador EP01_02: Rendimiento Predictivo — Métricas de ML
%%writefile EP01_02.cpp
// your solution
Overwriting EP01_02.cpp
TestSuite("EP01_02.cpp").run()
✔️ EP01_02.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_02.cases

🔍 Probando C++: EP01_02.cpp
⚠️ EP01_02.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.3 EP01_03 📈 Mean Average Precision (mAP) — Curva Precisión‑Sensibilidad

En esta actividad, evaluará un clasificador binario (p. ej., detección de deforestación en imágenes de satélite, ver dgi.inpe.br) mediante la curva Precisión‑Sensibilidad y la métrica mAP (Mean Average Precision). El mAP es estándar en competencias como COCO (Common Objects in Context) y PASCAL VOC (Visual Object Classes) y de los modelos YOLO (You Only Look Once).

1.17.3.1 🧠 ¿Por qué el mAP es la métrica‑estándar?

En la EP01_02 vio que la elección del umbral altera significativamente la Precisión y la Sensibilidad. El mAP (Mean Average Precision) resuelve esto: evalúa el modelo en varios umbrales (cada umbral debe generar una matriz de confusión diferente) y resume el desempeño mediante el área bajo la curva Precisión‑Sensibilidad (P‑S).

Mientras que el F1‑Score observa un único punto de equilibrio, el mAP considera la curva completa. Cuanto más cercano a 1,0, mejor es el detector en todos los umbrales y clases (p. ej., monedas de 25, 50 y 1 real).

Métrica Qué resume Limitación
F1‑Score Equilibrio P × S en un único umbral Depende del umbral elegido
AP Área bajo la curva P‑S de una clase Válida solo para una clase
mAP Promedio de las APs sobre todas las clases Más complejo de implementar

Referencias: Roboflow — mAP · Vídeo explicativo

1.17.3.2 🔢 Cómo se calcula el mAP — paso a paso

  1. Umbrales fijos (use siempre esta lista):

    umbrales = [0.00, 0.09, 0.21, 0.31, 0.39, 0.52, 0.60, 0.71, 0.81, 0.89, 1.00]
  2. Para cada umbral (t), clasifique las muestras: predicho = 1 si confianza ≥ t, sino 0.
    Calcule VP, FP, FN, VN y obtenga Precisión((t)) y Sensibilidad((t)).

  3. Construya la curva P‑S: pares (Sensibilidad((t)), Precisión((t))), ordenados por Sensibilidad creciente.

  4. Monotonice la Precisión:
    \[P_{\text{mono}}[i] = \max_{j \ge i} P[j]\]

  5. Calcule la AP (área bajo la curva monotónica) usando la regla del trapecio (aproximación más precisa que la simple suma de Riemann):
    \[AP = \sum_{i=1}^{m-1} \frac{P_{\text{mono}}[i-1] + P_{\text{mono}}[i]}{2} \cdot (S[i] - S[i-1])\]

  6. mAP = promedio de las APs de todas las clases. En este EP hay solo 1 clase, por lo tanto mAP = AP.

Nota

📐 Diferencia resumida:
La suma de Riemann aproxima el área mediante rectángulos, pudiendo subestimar o sobrestimar. La regla del trapecio usa trapecios, reduciendo el error al considerar el promedio entre los valores en los extremos del intervalo, siendo generalmente más precisa para funciones suaves por partes, como la curva Precisión‑Sensibilidad.

1.17.3.3 📋 Tarea

Lea un entero n (cantidad de muestras). Luego lea n líneas, cada una con: verdad (0 o 1) y confianza (float 0.0–1.0).

Calcule e imprima, para el umbral 0.85 (índice 9 de la lista):

  • Matriz de Confusión (VP, FN, FP, VN)
  • Exactitud, Precisión, Sensibilidad y F1‑Score

A continuación, para todos los umbrales, imprima:

  • Precisiones crudas, Precisiones monotónicas y Sensibilidades, separadas por ,
  • mAP final

1.17.3.4 📌 Importante

  • Umbral fijo para las métricas individuales: 0.85
  • División segura: si el denominador es cero, use 0
  • Formato: dos decimales
  • Monotonice de atrás hacia adelante
  • La Figura 1.13 presenta una simulación de esta cuestión

1.17.3.5 📌 Ejemplo de Ejecución

Entrada Salida Esperada
7
0 0.94
1 0.80
1 0.69
0 0.67
1 0.30
1 0.15
1 0.15
# MÉTRICAS PARA EL UMBRAL 0.85 #
Matriz de Confusión:
VP = 0, FN = 5
FP = 1, VN = 1

Métricas de Evaluación:
Exactitud: 0.14
Precisión: 0.00
Sensibilidad: 0.00
F1-Score: 0.00

# MÉTRICAS PARA TODOS LOS UMBRALES #
Precisiones: 0.00, 0.00, 0.00, 0.50, 0.50, 0.50, 0.50, 0.50, 0.60, 0.71, 0.71
Precisiones mon.: 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71
Sensibilidades: 0.00, 0.00, 0.00, 0.20, 0.40, 0.40, 0.40, 0.40, 0.60, 1.00, 1.00
mAP: 0.71

1.17.3.6 🐍 Consejo para calcular el AP (con regla del trapecio)

def calcular_AP(verdades, confianzas, umbrales):
    m = len(umbrales)
    precisiones = [0.0] * m
    sensibilidades = [0.0] * m
    for i in range(m):
        p, s = calcular_metricas(verdades, confianzas, umbrales[i])
        precisiones[m-1-i] = p
        sensibilidades[m-1-i] = s
    prec_mono = precisiones.copy()
    for i in range(m-2, -1, -1):
        if prec_mono[i] < prec_mono[i+1]:
            prec_mono[i] = prec_mono[i+1]
    AP = 0.0
    for i in range(1, m):
        # Regla del trapecio: promedio de las alturas por la base
        area_trapecio = (prec_mono[i-1] + prec_mono[i]) / 2.0
        AP += area_trapecio * (sensibilidades[i] - sensibilidades[i-1])
    return precisiones, prec_mono, sensibilidades, AP
📊 Simulador EP01_03: Curva Precisión-Sensibilidad & mAP mAP & Curva P-S

Edite las muestras (clase real y confianza) o elija un escenario predefinido para visualizar la matriz de confusión, la curva P-S y el valor de mAP en tiempo real.

MUESTRAS (CLASE REAL | CONFIANZA)
# Verdad Confianza
UMBRAL PARA MÉTRICAS INDIVIDUALES
0.85
Umbrales: 0.00 · 0.09 · 0.21 · 0.31 · 0.39 · 0.52 · 0.60 · 0.71 · 0.81 · 0.89 · 1.00
MÉTRICAS EN EL UMBRAL 0.85
Pred +
Pred −
Real +
VP
0
FN
5
Real −
FP
1
VN
1
Exactitud
0.14
Precisión
0.00
Sensib.
0.00
F1
0.00
CURVA PRECISIÓN-SENSIBILIDAD
mAP = 0.71
Curva P-S Monotónica Área (AP)
Umbral Prec. P.mono Sensib.
Figura 1.13: Simulador EP01_03: Precisión Media Promedio (mAP) y Curva P-S
%%writefile EP01_03.cpp
// your solution
Overwriting EP01_03.cpp
TestSuite("EP01_03.cpp").run()
✔️ EP01_03.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_03.cases

🔍 Probando C++: EP01_03.cpp
⚠️ EP01_03.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.4 EP01_04 🖼️ Lectura e Información de una Imagen en Matriz

En esta actividad, debes escribir un programa que procese una imagen digital representada como una matriz de píxeles en tonos de gris.

  • Lee dos enteros L y C, que representan el número de filas y columnas.
  • Lee los L * C valores enteros que componen la matriz de la imagen (cada valor entre 0 y 255).
  • Calcula e imprime la siguiente información:
  1. El número de filas.
  2. El número de columnas.
  3. El valor del píxel más grande (Máximo).
  4. El valor del píxel más pequeño (Mínimo).
  5. La Media aritmética de todos los píxeles.

📌 Importante:

  • La salida debe seguir exactamente el formato etiquetado (ej.: Filas: X).
  • El valor de la media debe formatearse con dos decimales.
  • Consulta un simulador interactivo para esta pregunta en la Figura 1.14 (cuadrícula interactiva para visualización de intensidades y cálculos en tiempo real).

1.17.4.1 🧠 ¿Por qué es importante? – La Imagen como Datos

Toda imagen digital es, en el fondo, una estructura de datos. En tonos de gris de 8 bits, cada píxel es un valor escalar. Extraer estadísticas básicas es el primer paso para:

Operación Utilidad Práctica
Máximo/Mínimo Identificar si la imagen está “lavada” (bajo contraste) o saturada.
Media Calcular el brillo global de la escena para ajustes de exposición.
Normalización Redimensionar los valores a intervalos como \([0, 1]\) en redes neuronales.

1.17.4.2 📋 Tarea (especificación para VPL)

Entrada:

La primera línea contiene el entero L (filas).

La segunda línea contiene el entero C (columnas).

Las líneas siguientes contienen los elementos de la matriz.

Salida:

Cinco líneas formateadas según el ejemplo:

Filas: L

Columnas: C

Max: V

Min: V

Media: V.VV

1.17.4.3 📌 Ejemplos

Entrada Salida Observación
2
3
0 128 255
50 100 200
Filas: 2
Columnas: 3
Max: 255
Min: 0
Media: 122.17
Imagen pequeña de alto contraste
📊 Simulador EP01_04: Estadísticas Locales de Píxeles Matriz 5x5

Haz clic sobre cualquier píxel de la matriz para incrementar su nivel de gris (paso de +51) o utiliza las acciones predefinidas abajo para observar los límites y la media global.

Media Global (µ)
0.00
Valor Máx
0
Valor Mín
0
Figura 1.14: Simulador EP01_04: Estadísticas de Píxeles en Matriz Discreta 5x5
%%writefile EP01_04.cpp
// your solution
Overwriting EP01_04.cpp
TestSuite("EP01_04.cpp").run()
✔️ EP01_04.cases ya existe en casos/
📋 5 caso(s) cargado(s) de casos/EP01_04.cases

🔍 Probando C++: EP01_04.cpp
⚠️ EP01_04.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.5 EP01_05 🔄 Negativo de una Imagen en Tonos de Gris

En esta actividad, se debe escribir un programa que calcule el negativo de una imagen digital.

  • Lea dos enteros L y C, que representan el número de filas y columnas.
  • Lea los valores enteros que componen la matriz de la imagen.
  • Para cada píxel, aplique la transformación de inversión:

\[pixel_{negativo} = 255 - pixel_{original}\]

  • Imprima la matriz resultante, manteniendo el formato original (L filas y C columnas).

📌 Importante:

  • Los valores de cada fila en la salida deben estar separados por un espacio en blanco.
  • La salida debe contener solo los números de la matriz resultante.
  • Consulte un simulador interactivo para esta cuestión en la Figura 1.15 (comparación en tiempo real entre la matriz original y su negativo).

1.17.5.1 🧠 ¿Por qué es importante? – Inversión de Intensidad

El negativo es una transformación lineal básica que invierte la escala de brillo. Es una herramienta esencial para que el ojo humano identifique detalles claros que están “ocultos” en fondos más oscuros, siendo ampliamente utilizada en:

Aplicación Utilidad
Imágenes Médicas Mejora la visualización de anomalías en tejidos densos (ej: Rayos-X).
Astronomía Destacar galaxias y nebulosas tenues contra el vacío del espacio.
Artes Digitales Efectos estéticos y preparación de máscaras de selección.

1.17.5.2 📋 Tarea (especificación para VPL)

Entrada:

La primera línea contiene el entero L.

La segunda línea contiene el entero C.

Las líneas siguientes contienen los elementos de la matriz.

Salida:

La matriz invertida con L filas y C columnas.

1.17.5.3 📌 Ejemplos

Entrada Salida Observación
2
3
0 128 255
50 100 255
255 127 0
205 155 55
Donde era 0 (negro) se convierte en 255 (blanco)
🌓 Simulador EP01_05: Transformación de Negativo de Imagen p' = 255 - p

Haz clic en los píxeles de la matriz Original (p) para cambiar sus niveles de gris (paso de +51) y observa el efecto de la inversión complementaria en la matriz Negativo (255 - p).

ORIGINAL (p)
NEGATIVO (255 - p)
💡 La transformación de negativo mapea tonos oscuros (cercanos a 0) a tonos claros (cercanos a 255) y viceversa, siendo útil para resaltar detalles oscuros sobre fondos claros.
Figura 1.15: Simulador EP01_05: Transformación de Negativo de Imagen (Inversión de Intensidad)
%%writefile EP01_05.cpp
// your solution
Overwriting EP01_05.cpp
TestSuite("EP01_05.cpp").run()
✔️ EP01_05.cases ya existe en casos/
📋 7 caso(s) cargado(s) de casos/EP01_05.cases

🔍 Probando C++: EP01_05.cpp
⚠️ EP01_05.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.6 EP01_06 🎨 Conversión RGB → Escala de Grises (ITU-R BT.601)

En esta actividad, debes escribir un programa que convierta píxeles de color (RGB) a escala de grises utilizando la ponderación fisiológica del estándar ITU-R BT.601.

  • Lee dos enteros L y C, que representan el número de filas y columnas.
  • Lee L × C tripletas de enteros, donde cada tripleta representa los canales R (Red), G (Green) y B (Blue) de un píxel.
  • Para cada píxel, calcula el valor de gris (\(g\)) usando la fórmula:

\[g = \text{round}(0.299 \times R + 0.587 \times G + 0.114 \times B)\]

  • Imprime la matriz resultante (L filas y C columnas) que contenga los valores enteros convertidos.

📌 Importante:

  • Utiliza la función round() de tu lenguaje para garantizar el redondeo correcto al entero más cercano.
  • La salida debe contener únicamente los valores de gris, manteniendo la estructura de matriz (separados por espacios en la línea).
  • Consulta un simulador interactivo para esta pregunta en la Figura 1.16 (ajusta los deslizadores para ver cómo contribuye cada color al brillo final).

1.17.6.1 🧠 ¿Por qué no usar solo el promedio?

El ojo humano no percibe todos los colores con la misma intensidad. Somos mucho más sensibles al Verde que al Azul debido a nuestra evolución biológica. El estándar ITU-R BT.601 utiliza pesos específicos para crear una imagen en gris que parezca naturalmente correcta para nuestra visión:

Canal Peso Percepción Humana
🟢 Verde 58.7% Máxima sensibilidad (distinción de follajes).
🔴 Rojo 29.9% Sensibilidad media.
🔵 Azul 11.4% Baja sensibilidad (tonos más oscuros).

1.17.6.2 📋 Tarea (especificación para VPL)

Entrada:

La primera línea contiene el entero L.

La segunda línea contiene el entero C.

Las líneas siguientes contienen tripletas de enteros R G B para cada píxel.

Salida:

La matriz de grises con L filas y C columnas.

1.17.6.3 📌 Ejemplos

Entrada Salida Observación
1
3
255 0 0 0 255 0 0 0 255
76 150 29 Observa cómo el Verde (150) es más brillante que el Azul (29)
🎨 Simulador EP01_06: Percepción de Color & Pesos ITU-R BT.601 RGB → Escala de grises

Ajusta la intensidad de los canales Rojo (R), Verde (G) y Azul (B) para observar cómo cada componente contribuye ponderadamente al valor final de luminancia en niveles de gris.

AJUSTE DE LOS CANALES DE COLOR
80
180
30
RGB original
Tonos de gris
💡 El canal Verde (G) posee el mayor peso (0.587) debido a la mayor sensibilidad espectral del sistema visual humano a las longitudes de onda del verde.
Figura 1.16: Simulador EP01_06: Conversión RGB a Escala de Grises (Ponderación Perceptual ITU-R BT.601)
%%writefile EP01_06.cpp
// your solution
Overwriting EP01_06.cpp
TestSuite("EP01_06.cpp").run()
✔️ EP01_06.cases ya existe en casos/
📋 7 caso(s) cargado(s) de casos/EP01_06.cases

🔍 Probando C++: EP01_06.cpp
⚠️ EP01_06.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.7 EP01_07 ⚫ Umbralización Manual: Imagen Binaria

En esta actividad, debes escribir un programa que realice la segmentación de una imagen mediante la umbralización (thresholding).

  • Lee dos enteros L y C, que representan las dimensiones de la matriz.
  • Lee un entero T, que será el valor del umbral (corte).
  • Lee los valores enteros de la matriz.
  • Para cada píxel \(p\), aplica la siguiente regla de binarización:

\[\text{resultado} = \begin{cases} 255 & \text{si } p > T \\ 0 & \text{si } p \le T \end{cases}\]

  • Imprime la matriz resultante que contenga solo los valores 0 o 255.

📌 Importante:

  • Presta atención al operador: el píxel solo se vuelve blanco (255) si es estrictamente mayor que \(T\).
  • La salida debe mantener la estructura de matriz (L filas y C columnas).
  • Consulta un simulador interactivo para esta pregunta en la Figura 1.17 (ajusta el control deslizante de \(T\) para observar cómo los objetos se aíslan del fondo).

1.17.7.1 🧠 ¿Qué es la Segmentación?

La umbralización es el método más simple para separar objetos de interés del fondo de la imagen. Al transformar tonos de gris en blanco y negro puro, creamos un mapa binario que facilita el conteo de objetos o la identificación de formas:

Valor del Píxel (\(p\)) Condición Resultado Final
Oscuro (\(p \le T\)) Fondo/Ruido 0 (Negro)
Claro (\(p > T\)) Objeto/Destacado 255 (Blanco)

1.17.7.2 📋 Tarea (especificación para VPL)

Entrada:

La primera línea contiene el entero L.

La segunda línea contiene el entero C.

La tercera línea contiene el entero T (umbral).

Las líneas siguientes contienen los elementos de la matriz.

Salida:

La matriz binarizada (0 o 255) con L filas y C columnas.

1.17.7.3 📌 Ejemplos

Entrada Salida Observación
2
4
128
0 100 128 200
50 129 255 64
0 0 0 255
0 255 255 0
Observa que el valor 128 se convirtió en 0 (pues \(128 \le 128\))
🎛️ Simulador EP01_07: Binarización Interactiva de Imagen Binario: 0 o 255
128
Entrada (Escala de Grises)
Salida (Máscara Binaria)
Píxeles con intensidad mayor que 128 (p > 128) se vuelven blancos (255); de lo contrario, se vuelven negros (0).
Figura 1.17: Simulador EP01_07: Umbralización Global Interactiva (Binarización p > T)
%%writefile EP01_07.cpp
// your solution
Overwriting EP01_07.cpp
TestSuite("EP01_07.cpp").run()
✔️ EP01_07.cases ya existe en casos/
📋 7 caso(s) cargado(s) de casos/EP01_07.cases

🔍 Probando C++: EP01_07.cpp
⚠️ EP01_07.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.8 EP01_08 🎨 Remapeamento por Rango de Intensidad

En esta actividad, debes escribir un programa que aplique transformaciones lineales distintas a diferentes regiones de intensidad de la imagen.

  • Lee dos enteros L y C, que representan las dimensiones de la matriz.
  • Lee los enteros T (umbral), δ₁ (delta 1) y δ₂ (delta 2).
  • Lee los valores enteros de la matriz.
  • Para cada píxel \(p\), aplica la regla de remapeo condicional:

\[\text{resultado} = \begin{cases} p + \delta_1 & \text{si } p < T \\ p + \delta_2 & \text{si } p \ge T \end{cases}\]

  • Imprime la matriz resultante con los nuevos valores de intensidad.

📌 Importante:

  • δ₁ es el offset aplicado a los píxeles oscuros (por debajo del umbral).
  • δ₂ es el offset aplicado a los píxeles claros (mayores o iguales al umbral).
  • Los casos de prueba garantizan que el resultado siempre estará en el rango válido de 0 a 255, por lo tanto, no es necesario manejar saturación ni redondeos.
  • La salida debe mantener la estructura de matriz (L filas y C columnas).

1.17.8.1 🧠 Transformación Condicional de Píxeles

En Procesamiento Digital de Imágenes (PDI), con frecuencia necesitamos tratar regiones de forma independiente. Esta técnica permite, por ejemplo, aclarar solo las sombras de una fotografía (aumentando los píxeles oscuros) sin estallar el brillo de las áreas ya claras, o viceversa.

Rango de Intensidad Condición Operación
Píxeles Oscuros \(p < T\) \(p + \delta_1\)
Píxeles Claros \(p \ge T\) \(p + \delta_2\)

1.17.8.2 📋 Tarea (especificación para VPL)

Entrada:

La primera línea contiene los enteros L y C.

La segunda línea contiene los enteros T, δ₁ y δ₂.

Las líneas siguientes contienen los elementos de la matriz.

Salida:

La matriz transformada con L filas y C columnas, con valores separados por espacios.

1.17.8.3 📌 Ejemplos

Entrada Salida Observación
2 4
128 60 -40
0 100 150 255
80 128 200 30
60 160 110 215
140 88 160 90
Píxeles < 128 suman 60. Píxeles ≥ 128 restan 40.
🎛️ Simulador EP01_08: Remapeo por Rango Condicional p < T → p + δ₁ | p ≥ T → p + δ₂

Ajuste el umbral de separación (T) y los desplazamientos de brillo (δ₁ y δ₂) para aplicar transformaciones de intensidad diferenciadas en las regiones oscuras y claras de la imagen.

128
+60
-40
Entrada Original (p)
Resultado Transformado
Regla activa: p < 128 → p + (+60)  |  p ≥ 128 → p + (-40)
Figura 1.18: Simulador EP01_08: Remapeo por Rango Condicional (Brillo y Contraste por Umbral)
%%writefile EP01_08.cpp
// your solution
Overwriting EP01_08.cpp
TestSuite("EP01_08.cpp").run()
✔️ EP01_08.cases ya existe en casos/
📋 8 caso(s) cargado(s) de casos/EP01_08.cases

🔍 Probando C++: EP01_08.cpp
⚠️ EP01_08.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.9 EP01_09 🏁 Patrón de Tablero: Matriz de Ajedrez

En esta actividad, debes escribir un programa que genere una imagen sintética con el patrón de un tablero de ajedrez.

  • Lee dos enteros L (filas) y C (columnas).
  • Genera una matriz donde los valores alternan entre 0 (negro) y 1 (blanco).
  • La lógica de relleno debe seguir la regla de paridad:
  • El elemento en la posición \((0,0)\) siempre es 0.
  • Un píxel en la posición \((i, j)\) será 1 si la suma de los índices \((i + j)\) es impar.
  • Un píxel en la posición \((i, j)\) será 0 si la suma de los índices \((i + j)\) es par.

📌 Importante:

  • Los colores deben alternar correctamente tanto horizontal como verticalmente.
  • La salida debe ser la matriz impresa línea por línea, con los elementos separados por un espacio.
  • Consulta un simulador interactivo para esta cuestión en la Figura 1.19 (ajusta las dimensiones para visualizar la construcción de la malla y la salida textual correspondiente).

1.17.9.1 🧠 Patrones Sintéticos

Crear patrones geométricos es un ejercicio fundamental para dominar la lógica de índices en matrices. En Procesamiento Digital de Imágenes, el patrón de ajedrez no solo es estético; también se utiliza ampliamente para:

Aplicación Utilidad
Calibración de Cámara Estimar parámetros intrínsecos y extrínsecos de la lente.
Corrección de Distorsión Identificar y corregir el efecto de “barril” o “cojín” en lentes gran angulares.
Mapeo 3D Proyectar patrones conocidos para reconstruir superficies en sistemas de luz estructurada.

1.17.9.2 📋 Tarea (especificación para VPL)

Entrada:

Una línea que contiene el entero L (filas).

Una línea que contiene el entero C (columnas).

Salida:

La matriz de ajedrez con L filas y C columnas, impresa con espacios entre los elementos.

1.17.9.3 📌 Ejemplos

Entrada Salida Observación
3
4
0 1 0 1
1 0 1 0
0 1 0 1
Observa que cada fila comienza con el inverso de la anterior
🏁 Simulador EP01_09: Generador de Matriz de Tablero de Ajedrez (i + j) % 2

Cambia el número de filas (L) y columnas (C) para observar cómo la alternancia de paridad de las coordenadas de la cuadrícula construye la matriz binaria de tablero de ajedrez.

×
CUADRÍCULA GRÁFICA
SALIDA ESPERADA (VALORES)
Figura 1.19: Simulador EP01_09: Generador de Patrón de Cuadros (Lógica de Paridad (i + j) % 2)
%%writefile EP01_09.cpp
// your solution
Overwriting EP01_09.cpp
TestSuite("EP01_09.cpp").run()
✔️ EP01_09.cases ya existe en casos/
📋 7 caso(s) cargado(s) de casos/EP01_09.cases

🔍 Probando C++: EP01_09.cpp
⚠️ EP01_09.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.10 EP01_10 📄 Metadatos: Lectura de Archivo PGM

En esta actividad, debes leer un archivo de imagen en el formato PGM (Portable Gray Map) y extraer sus dimensiones a partir del encabezado.

  • El formato PGM (P2) es un archivo de texto simple (ASCII) que almacena imágenes en tonos de gris.
  • El archivo posee un encabezado estructurado de la siguiente forma:
  1. Versión: El identificador P2.
  2. Comentarios: Líneas opcionales que comienzan con # (deben ignorarse).
  3. Dimensiones: Dos enteros que representan Ancho y Alto.
  4. Máximo: Un entero que representa la intensidad máxima (generalmente 255).
  • Después del encabezado, siguen los datos de los píxeles.

📌 Importante:

  • Lectura de Archivo: Debes abrir el archivo indicado en el ejemplo utilizando la función open() de Python.
  • Orden de Salida: A diferencia del orden presente en el archivo, la salida esperada debe estar en el formato de tupla: (Altura, Ancho, Canales).
  • Como los archivos PGM son en tonos de gris, el número de Canales es siempre 1.
  • Ver un simulador interactivo para esta cuestión en la Figura 1.20 (ajusta las dimensiones para ver cómo se genera el encabezado ASCII).

1.17.10.1 🧠 Entendiendo el formato PGM

El formato PGM es uno de los más simples para el procesamiento de imágenes. Por ser en texto puro, permite visualizar los metadatos e incluso los valores de los píxeles abriendo el archivo en un bloc de notas:

Componente Ejemplo Significado
Número Mágico P2 Identifica que es un PGM en formato de texto (ASCII).
Comentario # CREATOR... Línea informativa ignorada por el procesador.
Dimensiones 397 343 397 columnas (Ancho) y 343 filas (Altura).
Intensidad 255 Define el valor del blanco puro (escala de 0 a 255).

1.17.10.2 📋 Tarea (especificación para VPL)

Entrada:

Ninguna entrada mediante teclado. El programa debe leer el archivo "aula01fig03b.pgm" presente en el directorio de ejecución.

Salida:

Una tupla que contenga (Altura, Ancho, 1).

1.17.10.3 📌 Ejemplos

Nombre del Archivo Salida Esperada Observación
“aula01fig03b.pgm” (343, 397, 1) Nota la inversión del orden: Altura primero
📄 Simulador EP01_10: Estructura del Archivo PGM ASCII P2 & Formato (H, W, C)

Cambie las dimensiones de ancho (W) y alto (H) para observar el ensamblaje dinámico del encabezado PGM y el formato de la tupla del array resultante en Python (Filas × Columnas × Canales).

DEFINICIONES DE LA IMAGEN
💡 Atención a la convención: El encabezado PGM declara primero W H (Ancho × Alto), mientras que el array en Python/NumPy reporta la tupla como (H, W, C) (Filas × Columnas × Canales).
CONTENIDO DEL ARCHIVO (.PGM)
P2
# CREADOR: UFABC PDI / EP01_10
397 343
255
120 134 210 0 85 255 ...
Salida de la función mm.readImg (Formato del Array):
(343, 397, 1)
Figura 1.20: Simulador EP01_10: Estructura de Archivo PGM (Encabezado ASCII P2 y Mapeo a Tupla Python)
NotaNota

El archivo necesario para este EP se descargará automáticamente del repositorio mediante el siguiente código:

%%writefile tmp/mm_out_2.cpp
#include <iostream>
#include <string>
// No matter how much we try, C++ does not have a direct equivalent for
// downloading files with urllib. We'll simulate the structure.

int main() {
    // Constants
    const std::string BASE_URL = "https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap01/imagens";
    const std::string file = "aula01fig03b.pgm";

    // Simulating the download process (no actual download in C++ standard library)
    std::cout << "Simulating download of: " << file << "\n";

    // Simulating the loop over files
    std::string url = BASE_URL + "/" + file;
    std::cout << "URL: " << url << "\n";
    std::cout << "✅ Arquivo baixado: " << file << "\n";

    return 0;
}
Overwriting tmp/mm_out_2.cpp
!g++ -I. -std=c++17 tmp/mm_out_2.cpp -o tmp/mm_out_2 \
  && ./tmp/mm_out_2
Simulating download of: aula01fig03b.pgm
URL: https://raw.githubusercontent.com/fzampirolli/pdi-vc/master/all/cap01/imagens/aula01fig03b.pgm
✅ Arquivo baixado: aula01fig03b.pgm
%%writefile EP01_10.cpp
// your solution
Overwriting EP01_10.cpp
TestSuite("EP01_10.cpp").run()
✔️ EP01_10.cases ya existe en casos/
📋 1 caso(s) cargado(s) de casos/EP01_10.cases

🔍 Probando C++: EP01_10.cpp
⚠️ EP01_10.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.

1.17.11 EP01_11 📈 Análisis de Vecindad: Filtro de Máximo 1D

En esta actividad, debes implementar un filtro morfológico simple de máximo que opera sobre una señal unidimensional (vector).

  • Lee un entero n, que representa el tamaño del vector.
  • Lee los n elementos enteros que componen el vector original v1.
  • Crea un nuevo vector v2, donde cada posición \(i\) es el resultado de la comparación entre el elemento actual y sus vecinos inmediatos:

\[v2[i] = \max(v1[i-1],\; v1[i],\; v1[i+1])\]

📌 Importante:

  • Bordes: En los extremos del vector (índices \(0\) y \(n-1\)), la vecindad posee solo dos elementos (el propio y el único vecino disponible). En el índice \(0\), compara únicamente \(v1[0]\) y \(v1[1]\). En el último índice, compara solo \(v1[n-2]\) y \(v1[n-1]\).
  • Salida: Imprime el encabezado “v2:” seguido de los valores del vector resultante, uno por línea.
  • Consulta un simulador interactivo para esta pregunta en la Figura 1.21 (pasa el mouse sobre los resultados para visualizar la ventana de vecindad utilizada en el cálculo).

1.17.11.1 🧠 ¿Por qué analizar vecinos?

En procesamiento de imágenes, el valor de un píxel rara vez es aislado; depende del contexto que lo rodea. El Filtro de Máximo es la base de la operación de Dilatación en morfología matemática, y sirve para:

Función Efecto Visual
Realce Expande estructuras brillantes y “engorda” objetos claros.
Eliminación de Ruido Elimina pequeños puntos negros (ruido de “sal y pimienta” oscuro).
Relleno Cierra pequeños agujeros o huecos en formas binarias.

1.17.11.2 📋 Tarea (especificación para VPL)

Entrada:

Un entero n.

En las líneas siguientes, los n elementos enteros del vector.

Salida:

La cadena v2: en la primera línea.

En las líneas siguientes, cada elemento de v2 (uno por línea).

1.17.11.3 📌 Ejemplos

Entrada Salida Observación
5
10
20
5
30
15
v2:
20
20
30
30
30
En el índice 1: max(10, 20, 5) = 20
📈 Simulador EP01_11: Filtro de Máximo Local 1D Janela 1x3

Clique nos elementos de v1 (Entrada) para gerar novos valores individuais ou passe o mouse sobre as células de v2 (Resultado) para inspecionar a janela local de vizinhança.

Vetor v1 (Entrada)
⬇️
Vetor v2 (Resultado do Máximo)
Passe o cursor do mouse sobre uma célula do vetor v2 para analisar a janela de máximo local.
Figura 1.21: Simulador EP01_11: Filtro de Máximo Local 1D (Vecindad 1x3 con Condición de Borde)
%%writefile EP01_11.cpp
// your solution
Overwriting EP01_11.cpp
TestSuite("EP01_11.cpp").run()
✔️ EP01_11.cases ya existe en casos/
📋 7 caso(s) cargado(s) de casos/EP01_11.cases

🔍 Probando C++: EP01_11.cpp
⚠️ EP01_11.cpp: archivo vacío (menos de 3 líneas). Pruebas omitidas.