1  Fondamenti e Primi Passi

Questo capitolo apre la Parte 1 del libro, dedicata ai fondamenti dell’Elaborazione Digitale delle Immagini (EDI). Vengono presentati la rappresentazione matematica delle immagini digitali e i principali metodi per la loro manipolazione.

Si utilizza il linguaggio C++ e la libreria morph.hpp, una porta didattica della morph.py (ZAMPIROLLI, 2025).

1.1 Obiettivi

Al termine di questo capitolo, sarai in grado di:

  • Comprendere la natura fisica e matematica dell’immagine digitale \(f(x,y)\).

  • Identificare le bande dello spettro elettromagnetico rilevanti per l’elaborazione digitale delle immagini (PDI).

  • Eseguire operazioni di base: lettura, visualizzazione e salvataggio delle immagini.

  • Accedere e modificare le intensità dei pixel singolarmente.

  • Applicare la sogliatura manuale.

  • Configurare l’ambiente di sviluppo in C++.

  • Gestire le strutture di array (std::vector) senza cadere in insidie di copia/riferimento.

1.2 Prima di iniziare: Notebooks interattivi

Questo materiale è stato realizzato secondo il concetto di Literate Programming (Programmazione Letteraria), ideato da Donald Knuth negli anni ’80 (KNUTH, 1984). Knuth — anche creatore del sistema TeX per la tipografia digitale — propose che i programmi fossero scritti come una narrazione logica per gli esseri umani, alternando codice e documentazione.

Per eseguire una cella, premere Shift + Invio oppure fare clic sul pulsante ▶️.

NotaNota sul formato

Nelle versioni renderizzate (PDF o HTML), il codice è presentato in blocchi statici a scopo di lettura e riferimento. L’esecuzione interattiva richiede l’accesso tramite Google Colab (disponibile in cima alla pagina) o in ambiente locale con VSCode o Jupyter Notebook.

1.3 Fondamenti

Lo studio dei sistemi basati su immagini comprende un ecosistema di discipline integrate che trasformano dati visivi grezzi in conoscenza strutturata. Mentre alcune aree si concentrano sulla generazione di rappresentazioni, altre si dedicano al trattamento e all’analisi di tali dati per supportare applicazioni tecnologiche complesse.

Il diagramma presentato in Figura 1.1 stabilisce la distinzione e la complementarità tra l’Elaborazione Digitale delle Immagini (EDI) e la Visione Artificiale (VA). L’EDI, evidenziata in verde, si concentra sulla trasformazione da immagine a immagine, mirando al miglioramento della qualità o alla pre-elaborazione, come la rimozione del rumore e l’accentuazione del contrasto.

Al contrario, la VA, contrassegnata in blu, si concentra sull’interpretazione del contenuto visivo per estrarre modelli o informazioni, come il riconoscimento di oggetti e gesti. La regione di intersezione illustra la sinergia tra le aree, dove l’EDI prepara i dati visivi per l’interpretazione da parte della VA. La mappa dimostra inoltre le interconnessioni di entrambe le discipline con aree quali la Robotica, la Grafica Computerizzata, l’Intelligenza Artificiale (IA) e le Neuroscienze.

Figura 1.1: Diagramma relazionale che illustra le distinzioni fondamentali, le sinergie e le interconnessioni tra EDI e VA nel contesto dei sistemi basati su immagini.

1.3.1 👁️ Visione Computazionale

  • Focus: Immagine → Modello (percorso inverso rispetto alla Computer Grafica).
  • Obiettivo: Estrarre informazioni di alto livello da immagini o video.
  • Applicazioni tipiche:
    • Robotica: rilevamento di ostacoli, localizzazione e navigazione autonoma.
    • Sorveglianza e ispezione: riconoscimento di eventi, lettura di targhe, controllo qualità.
    • Telerilevamento: analisi di immagini satellitari, mappatura ambientale.
    • Imaging medico: rilevamento di tumori, segmentazione di organi, supporto alla diagnosi.
    • Interazione uomo-computer: riconoscimento di gesti, espressioni facciali, eye tracking.
  • Relazione con altri ambiti: utilizza tecniche di Apprendimento Automatico e IA per classificare e interpretare le scene; funge da “occhi” della Robotica.

1.3.2 🖼️ Elaborazione Digitale delle Immagini (PDI)

  • Obiettivo: Immagine → Immagine (generalmente - trasformazione di un’immagine in un’altra).
  • Scopo: Migliorare la qualità visiva o estrarre caratteristiche di basso livello.
  • Applicazioni comuni:
    • Rimozione del rumore (filtri media, mediana, gaussiano).
    • Miglioramento del contrasto (equalizzazione dell’istogramma, regolazione gamma).
    • Rilevamento dei bordi (Sobel, Canny, Laplaciano).
    • Segmentazione (sogliatura, crescita delle regioni, watershed).
    • Trasformazioni geometriche (ridimensionamento, rotazione, correzione prospettica).
  • Relazione con altre aree (vedi Tabella 1.1):
    • È la base per la maggior parte dei sistemi di visione artificiale (pre-elaborazione).
    • La Computer Grafica applica spesso PDI per la post-elaborazione (es.: smussatura, miglioramento).
    • Le tecniche di IA possono ottimizzare i parametri di elaborazione (es.: apprendimento di filtri).
Tabella 1.1: Connessione tra PDI, VC e altre aree della scienza.
Área Relação com PDI e VC
Intelligenza Artificiale Fornisce modelli (reti neurali, SVM) che interpretano le uscite della VC.
Robotica Consuma dati di VC per prendere decisioni (navigazione, manipolazione).
Apprendimento Automatico Utilizza descrittori estratti da PDI/VC per addestrare classificatori.
Computer Grafica Percorso inverso: modello → immagine; spesso applica PDI per una resa realistica.
Neuroscienze Ispira modelli di PDI (es.: filtri simili alle cellule gangliari della retina).

1.4 Fasi del PDI

Le fasi del PDI sono presentate nella Figura 1.2 e possono essere interpretate come una catena di trasformazioni che riduce la ridondanza dei dati per ricercarne il significato:

  • Basso Livello: opera direttamente sui pixel dell’immagine disturbata per apportare miglioramenti e filtraggi, producendo come output un’immagine pulita o esaltata.
  • Medio Livello: riceve l’immagine trattata ed esegue la segmentazione e la descrizione, trasformando la matrice dei pixel in attributi strutturati (forma, dimensione e texture).
  • Alto Livello: utilizza la tabella degli attributi per alimentare processi di logica e intelligenza artificiale, portando alla decisione o al riconoscimento finale (come la diagnosi medica).
Figura 1.2: Rappresentazione del flusso sequenziale di elaborazione: l’output di ciascun livello diventa l’input del livello successivo.

La Figura 1.3 descrive l’intera sequenza dell’elaborazione delle immagini digitali (PDI), dall’acquisizione all’interpretazione. Il flusso inizia con l’Acquisizione dell’Immagine (1) e prosegue con il Miglioramento (2) e il Restauro (3). Successivamente, il contenuto viene isolato tramite la Segmentazione (4) e raffinato mediante la Morfologia (5). La transizione cruciale avviene nella Rappresentazione e Descrizione (6), dove gli oggetti visivi vengono convertiti in dati matematici (area, perimetro, ecc.), consentendo il Riconoscimento (7). I processi ausiliari includono l’Elaborazione dell’Immagine a Colori e la Compressione, che contribuiscono all’efficienza dell’archiviazione e dell’analisi.

Figura 1.3: Flusso dettagliato del PDI: dall’acquisizione sensoriale all’estrazione di attributi e al riconoscimento automatizzato, includendo l’elaborazione a colori e la compressione.

1.5 Formazione dell’Immagine e Spettro

Il processo di formazione di un’immagine si basa sull’interazione tra materia ed energia radiante. Essenzialmente, un’immagine viene concepita quando un sensore registra la radiazione risultante dall’interazione con un oggetto fisico. Nel contesto della visione umana e della fotografia convenzionale, questo fenomeno dipende da una sorgente luminosa che illumina la scena; le caratteristiche degli oggetti vengono quindi codificate attraverso le variazioni di intensità e colore della luce che raggiunge il sensore, come illustrato nella Figura 1.4.

Figura 1.4: Rappresentazione dello spettro visibile e della sua posizione rispetto alle altre radiazioni elettromagnetiche, evidenziando la variazione delle lunghezze d’onda da 380 nm a 750 nm.

La luce visibile occupa solo una piccola banda dello spettro elettromagnetico — tra 380 nm (viola) e 750 nm (rosso) — come illustrato nella Figura 1.5. I sensori digitali convenzionali operano in questa stessa finestra, ma apparecchiature specializzate possono captare radiazioni invisibili all’occhio umano, come l’infrarosso e i raggi X. In PDI, l’immagine formata dipende direttamente dalla sensibilità spettrale del sensore utilizzato.

Figura 1.5: (A) Spettro elettromagnetico completo in scala logaritmica, con evidenza della fascia visibile. (B) Dettaglio della luce visibile (380-750 nm) e dei suoi colori. (C) Scomposizione della luce bianca tramite prisma: la lunghezza d’onda minore subisce una rifrazione maggiore, separando UV, visibile e infrarosso.

A partire da questo processo fisico di acquisizione, diventa possibile modellare matematicamente l’immagine digitale come una funzione bidimensionale discreta, nella quale ogni punto della scena è rappresentato da campioni numerici di intensità luminosa, formalizzando così i concetti di pixel e di immagine digitale presentati nella sezione successiva.

1.6 Cos’è un’Immagine Digitale?

Un’immagine digitale è formata da una griglia di pixel (Picture Elements), dove ogni pixel è la più piccola unità elementare dell’immagine.

ConsiglioCos’è un Pixel?

Un pixel è la più piccola unità indirizzabile che compone un’immagine digitale. Ogni pixel occupa una posizione unica nella griglia e memorizza uno o più valori numerici che rappresentano la sua intensità o colore.

Rappresentazione Matematica

A differenza di una funzione continua, il dominio di un’immagine digitale è un piano rettangolare finito \(\mathbb{E} \subset \mathbb{Z}^2\), che rappresenta la griglia di campionamento. Questo dominio è indicizzato da coordinate intere:

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

Dove:

  • \(L\): rappresenta la larghezza dell’immagine (numero di colonne).
  • \(H\): rappresenta l’altezza dell’immagine (numero di righe).

L’immagine digitale è una funzione che associa a ogni coppia di coordinate \((x,y)\) uno o più valori che descrivono l’aspetto del pixel.

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

L’insieme \(\mathcal{V}\) definisce i valori possibili per il pixel (codominio), variando a seconda del tipo di immagine, come dimostrato nella Tabella 1.2.

Tabella 1.2: Principali tipi di immagine digitale e i rispettivi insiemi di valori possibili per ogni pixel.
Tipo di immagine \(\mathcal{V}\) (valori del pixel) Rappresentazione
Binaria \(\{0, 1\}\) o \(\{0, 255\}\) ⬛◻️
Scala di grigi \(\{0, 1, \dots, 255\}\) ░▒▓█
A colori (RGB) \(\{0, \dots, 255\}^3\) (terne ordinate di valori) 🟥🟩🟦

Esempio pratico: Un’immagine a colori nel modello RGB può essere rappresentata matematicamente da una funzione che associa tre valori di intensità a ogni pixel. Computazionalmente, questa rappresentazione corrisponde a tre matrici sovrapposte — i canali rosso (Red), verde (Green) e blu (Blue) — nelle quali ogni elemento memorizza l’intensità luminosa del rispettivo canale in una determinata posizione dell’immagine.

Per rendere possibili gli esperimenti pratici di PDI e VC, questo libro utilizza C++17 e un insieme minimo di librerie orientate alla manipolazione matriciale, all’elaborazione delle immagini e alla visualizzazione dei risultati, presentate di seguito.

1.7 Configurazione dell’Ambiente

Questo materiale utilizza C++17 e la libreria a singolo header morph.hpp, originariamente proposta per Python in Zampirolli (2025). Qui viene utilizzata una versione minima e didattica della morph.py, adattata per una compilazione semplice con g++, come presentato in Tabella 1.3.

Ogni cella di codice viene compilata ed eseguita come un processo isolato (%%writefile file.cpp seguito da g++). Diversamente dal kernel Python, non esiste uno stato persistente tra le celle: le immagini prodotte da una cella vengono salvate in file per essere lette dalla successiva.

Gli Esercizi di Programmazione (EP) presentati alla fine dei capitoli possono essere validati dallo stesso modulo testsuite.py utilizzato nel percorso Python, che compila la soluzione con g++ e confronta l’output con i casi di test. In questo modo, gli stessi casi di test (.cases) possono validare soluzioni in C++, Python e altri linguaggi, sia nei notebook che in Moodle/VPL.

Tabella 1.3: Principali librerie e strumenti utilizzati nel percorso C++ di questo libro.
Libreria / Strumento Funzione principale
g++ (C++17) Compilazione di ogni cella di codice
morph.hpp Astrazione didattica delle operazioni di PDI
stb_image.h / stb_image_write.h Lettura/scrittura di PNG/JPEG (senza OpenCV)
testsuite.py Esecuzione e validazione automatica degli EP

1.8 Informazioni sulla morph.hpp

La morph.hpp è una versione C++ minima e didattica della morph.py: implementa le funzioni utilizzate in questo capitolo (read, gray, randomImage, show, write, threshold, drawImg), oltre alle operazioni di morfologia (dil/ero e varianti dil0/ero0/dil1/ero1) preparate per i capitoli successivi. Non è garantita una parità numerica con l’implementazione Python — il criterio è “compila e produce un’immagine plausibile”, non “risultato bit-a-bit identico a Python”.

Le librerie stb_image.h/stb_image_write.h (dominio pubblico/MIT) sono vendorizzate insieme al repository — cioè, il loro codice sorgente è già copiato all’interno del progetto stesso, invece di essere installato separatamente tramite apt install — evitando così di dipendere dai pacchetti di sistema durante la compilazione su Colab. Le operazioni dil()/ero() utilizzano cv::dilate/cv::erode quando compilate con -DMM_USE_OPENCV; senza questa flag (predefinita, anche su Moodle/VPL), si ricade sulla versione didattica equivalente (dil1/ero1) senza richiedere OpenCV.

import os, urllib.request

os.makedirs("tmp/state", exist_ok=True)  # artefatti di build della pista C++ (.cpp, binario, PNG)

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

# Il kernel è comunque Python anche nella pista C++: `mm` (morph.py) è usato dai
# simulatori, dalla visualizzazione delle figure che il binario C++ genera e dallo
# stato mm::Image tra le celle. cpp=True scarica anche la pista compilata
# (morph.hpp + stb_image*.h), usata nell'#include delle celle %%writefile *.cpp.
import config
config.setup(testsuite=True, cpp=True)
from morph import mm
from testsuite import TestSuite
✅ Ambiente pronto. Morph: 1.1.9 | OpenCV: 5.0.0 | TestSuite: 1.1.2

1.9 Fondamenti delle Matrici — attenzione alla copia dei riferimenti

Poiché un’immagine digitale può essere rappresentata da una matrice, è importante comprendere come creare e manipolare correttamente le matrici. In C++, std::vector ha semantica di valore: copiare il vettore copia i suoi dati. Pertanto, std::vector<std::vector<int>> m(3, std::vector<int>(2, 0)) crea effettivamente tre righe indipendenti — il costruttore di riempimento copia la riga-modello tre volte.

AvvisoAttenzione alla copia dei riferimenti

La trappola compare quando si usano puntatori o riferimenti invece di copie. In std::vector<int> riga(2, 0); std::vector<std::vector<int>*> m(3, &riga);, i tre elementi di m puntano alla stessa riga, e modificare (*m[0])[0] modifica tutte. Lo stesso accade con auto& riga = m[0]; (riferimento — modifica la matrice), in contrasto con auto riga = m[0]; (copia — lascia la matrice intatta).

Per visualizzare questo comportamento, si può eseguire il codice su Python Tutor (che esegue anche C++ passo dopo passo) e confrontare l’effetto di copie e di riferimenti.

Nella pratica, per l’elaborazione digitale delle immagini (PDI), si utilizza la struttura mm::Image della morph.hpp, che ha anch’essa semantica di valore: mm::Image b = a; copia i pixel, mentre mm::Image& b = a; crea solo un alias per la stessa immagine. Il codice seguente presenta diverse modalità di creazione di immagini sintetiche, i cui risultati sono mostrati nella Figura 1.6..

%%writefile tmp/fig_imagens_sinteticas.cpp
#define MM_OUT "tmp/fig_imagens_sinteticas.png"
// Compile: g++ -std=c++17 -o programa programa.cpp -I. -lpng -ljpeg
#include "morph.hpp"
#include <iostream>
#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:
 52 194  34  64 125 119 
 29  87 162 226  43  94 
150 182 153 225  92 169 
 28 150 242  41   7 149 

[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 Lettura e Visualizzazione delle Immagini

In biblioteche come OpenCV (cv::Mat), le immagini digitali sono rappresentate computazionalmente come matrici multidimensionali. In morph.hpp, la struttura mm::Image adotta un approccio più semplice: un buffer lineare di byte (std::vector<unsigned char>), con altezza, larghezza e numero di canali espliciti.

Una delle operazioni fondamentali nell’elaborazione delle immagini digitali è la lettura delle immagini.

In morph.hpp, la funzione mm::read() consente di caricare immagini sia da file locali che da URL, come illustrato nella Figura 2.7..

%%writefile tmp/fig_01_natureza.cpp
#define MM_OUT "tmp/fig_01_natureza.png"
//| label: fig-01-natureza
//| fig-cap: "Mandrill (Mandrillus sphinx) em ambiente natural. Crédito: Julien Renoult (CC BY 4.0)."
//| echo: true

#include "morph.hpp"
#include <iostream>
#include <filesystem>

int main() {
    mm::Image img = mm::read("https://upload.wikimedia.org/wikipedia/commons/8/87/Mandrillus_sphinx_339428057.jpg");

    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: Download dell’immagine per archiviazione locale

In ambienti in cui la lettura diretta degli URL non è disponibile — a causa di restrizioni di rete, policy di firewall o assenza di connettività — un’alternativa consiste nel scaricare preventivamente l’immagine nel file system locale e quindi caricarla con mm::read(). Nel percorso C++, mm::read accetta anche URL direttamente (download interno tramite curl/wget); questa alternativa copre il caso di scaricare una volta e riutilizzare il file locale. Nell’esempio seguente, il file viene salvato come 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");   // legge dal file locale
    mm::show(img, MM_OUT);                      // Esempio: Cattura RGB (file locale)
    return 0;
}
!g++ -I. tmp/ler_local.cpp -o tmp/ler_local && ./tmp/ler_local

Spiegazione

  • wget -O mandrill.png <URL> esegue il download dell’immagine e la archivia localmente con il nome specificato;
  • mm::read("mandrill.png") effettua la lettura del file direttamente dal file system, senza necessità di richieste HTTP aggiuntive;
  • questa strategia riduce la dipendenza dalla connettività durante l’esecuzione degli esperimenti ed evita download ripetuti della stessa immagine.
ConsiglioNota

Il prefisso ! è utilizzato in ambienti basati su notebook, come Jupyter Notebook, JupyterLab e Google Colab, per eseguire comandi del sistema operativo direttamente in celle di codice. In terminali convenzionali, il comando deve essere utilizzato senza il prefisso !.

Se l’utilità wget non è installata, si può utilizzare in alternativa:

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

1.11 Conversione di Tipi e Soglia

Come abbiamo visto, un’immagine a colori nello spazio RGB è rappresentata dalla funzione:

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

Cioè, per ogni pixel \((x,y)\), abbiamo tre valori \((R,G,B)\) che ne definiscono il colore.

Conversione in Scala di Grigi

Per convertire un’immagine RGB in scala di grigi (grayscale), è necessario combinare i tre canali in un unico valore di intensità \(g\), che rappresenta la luminosità percepita. Poiché l’occhio umano non è ugualmente sensibile al rosso, al verde e al blu, si utilizza una media ponderata. Lo standard ITU-R BT.601 ({ITU-R}, 2011) definisce i seguenti pesi:

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

Dopo il calcolo, il valore \(g\) viene arrotondato all’intero più vicino e adattato all’intervallo \([0, 255]\). Il risultato è una nuova immagine, ora in scala di grigi, rappresentata da:

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

Soglia (Thresholding)

A partire dall’immagine in scala di grigi \(f_{\text{grigi}}(x,y)\), un’operazione fondamentale è la soglia, che produce un’immagine binaria (solo bianco e nero). A tale scopo, si sceglie un valore di taglio \(T\) (generalmente nell’intervallo \([0,255]\)) e si definisce:

\[ f_{\text{bin}}(x,y) = \begin{cases} 255 & \text{se } f_{\text{grigi}}(x,y) > T \\[4pt] 0 & \text{altrimenti} \end{cases} \tag{1.4}\]

Esempio: Con \(T = 128\), i pixel con intensità superiore a 128 diventano bianchi (255); gli altri diventano neri (0).

La soglia è ampiamente utilizzata per segmentare gli oggetti dallo sfondo, estrarre i bordi o creare maschere binarie per l’elaborazione successiva.

Nota: Il valore 255 rappresenta il bianco massimo nelle immagini a 8 bit, mentre 0 rappresenta il nero assoluto.

Esempio pratico di conversione e soglia

La Figura 1.8 illustra i passaggi principali per trasformare un’immagine a colori in scala di grigi e, successivamente, convertirla in un’immagine binaria mediante soglia. Il codice seguente implementa questi passaggi:

%%writefile tmp/fig_01_processamento_basico.cpp
#define MM_OUT "tmp/fig_01_processamento_basico.png"
#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 = mm::_read_state("tmp/state/img_34.png");
// [pdi:state-io:end]

    // Leitura da imagem já fornecida: img

    // 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=128)"},
        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 Soglia con il metodo di Otsu

Come presentato in Equazione 1.4, la sogliatura converte un’immagine in scala di grigi in binaria utilizzando un valore di taglio \(T\). Finora abbiamo fissato \(T = 128\) manualmente.

mm::Image img_bin_fixo = mm::threshold(img_gray, 128);

Tuttavia, la scelta manuale di \(T\) non è sempre banale. La libreria mm offre un’alternativa automatica: quando la soglia non viene fornita, la funzione mm::threshold(img_gray) calcola il valore di \(T\) tramite il metodo di Otsu (OTSU, 1979). Questo metodo, che verrà dettagliato nei capitoli successivi, massimizza la varianza tra le classi dell’istogramma (frequenza di ogni tono di grigio), separando automaticamente i pixel dell’oggetto e dello sfondo.

Il codice seguente confronta la sogliatura manuale (\(T=128\)) con quella automatica (Otsu). La binarizzazione con Otsu viene eseguita con mm::threshold(img_gray); poiché l’API minima di morph.hpp non restituisce il \(T\) calcolato, il valore mostrato nell’etichetta della figura viene recuperato con cv2.threshold nella fase di visualizzazione (stesso algoritmo, stesso \(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 <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 de Otsu
    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 mostra che la soglia ottenuta mediante Otsu si adatta automaticamente all’immagine, producendo una binarizzazione più efficace rispetto a un valore fisso, soprattutto quando le intensità dell’oggetto e dello sfondo sono ben separate nell’istogramma. Questa tecnica è ampiamente utilizzata nei sistemi di visione artificiale (VC) per la binarizzazione di documenti, il rilevamento di oggetti e il pre-processing delle immagini.

ConsiglioSemplicità della libreria morph.hpp

Mentre OpenCV richiede la chiamata completa:

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

la morph.hpp astrae tutta questa complessità: basta chiamare mm::threshold(img_gray). La soglia di Otsu viene calcolata automaticamente e l’immagine binaria viene restituita direttamente. Questo approccio consente di concentrarsi sul concetto, non sui dettagli implementativi.

1.13 Accesso ai Pixel

In morph.hpp, l’immagine è un mm::Image con un buffer lineare data (riga dopo riga, canali interallacciati) e il metodo at(y, x, c) per l’accesso individuale, seguendo la convenzione matriciale riga (asse Y) e colonna (asse X): img.at(riga, colonna) per i toni di grigio e img.at(riga, colonna, canale) per ogni canale di un’immagine RGB.

Il codice della Figura 1.10 dimostra come estrarre questi valori in immagini a colori (RGB) e in toni di grigio, oltre a isolare l’intorno immediato del punto di interesse.

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

# 1. Coordinate del pixel bersaglio (riga, colonna)
r, c = 600, 800

# 2. Accesso diretto ai valori del pixel
pixel_cinza = img_gray[r, c]
print(f"Pixel bersaglio ({r}, {c}):")
print(f"  Toni di grigio (scalare) : {pixel_cinza}")
print(f"  Colorato (canali RGB)   : R={img[r, c, 0]}, G={img[r, c, 1]}, B={img[r, c, 2]}")

# 3. Intorno 3x3 attorno a (r, c); il pixel centrale va tra parentesi quadre
print("Matrice dell'intorno 3x3 (toni di grigio):")
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 posizione del pixel con un quadrato rosso vuoto (bordo di
#    3 px) su una copia dell'immagine e visualizza (equivalente al punto del matplotlib).
marcada = img.copy()
lado = 20   # metà lato del quadrato, in pixel
esp = 5     # spessore del bordo

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"Posizione del pixel ({r}, {c})")
Figura 1.10

Accesso ai pixel in C++ (mm::Image):

  • Scala di grigi: img_gray.at(r, c) restituisce un unsigned char (da 0 a 255) con l’intensità del grigio nel pixel.
  • RGB: img.at(r, c, 0), img.at(r, c, 1), img.at(r, c, 2) accedono ai canali R, G e B — un indice di canale alla volta; non esiste un vettore [R, G, B].
  • Intorno \(3\times 3\): esaminato tramite due cicli annidati su img_gray.at(r + dy, c + dx), con dy, dx in \(\{-1, 0, 1\}\) — non esiste lo slicing come in NumPy. Questa scansione è alla base per filtri spaziali e convoluzioni.
  • Non esiste plottaggio interattivo (equivalente a plt.plot): per marcare la posizione del pixel, la cella di codice seguente disegna un quadrato rosso vuoto attorno ad esso in una copia dell’immagine (marcata = img.copy(), poi marcata.at(...)) e la visualizza con mm::show.

L’indicizzazione è zero-based: (0,0) è l’angolo in alto a sinistra. La prima dimensione controlla l’altezza (righe/Y) e la seconda la larghezza (colonne/X).

1.14 Riassunto

In questo capitolo sono stati presentati i fondamenti della rappresentazione delle immagini digitali: la definizione di pixel, la strutturazione delle immagini in matrici e l’impatto del campionamento e della quantizzazione sulla qualità finale:

  • Immagine digitale = funzione \(f(x,y)\) che mappa coordinate in intensità (scalari o vettoriali).

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

  • Tipi principali: binaria (\(\mathcal{V} = \{0, 255\}\)), toni di grigio (\(\mathcal{V} = [0,255]\)) e RGB (\(\mathcal{V} = [0,255]^3\)).

  • Sogliatura converte i toni di grigio in immagine binaria; il metodo di Otsu determina automaticamente il valore di soglia massimizzando la varianza tra le classi.

  • La morph.hpp (o mm::) offre funzioni didattiche per operazioni di base dell’elaborazione delle immagini, come mm::gray(), mm::threshold(), mm::show() (sovraccaricata per 1 immagine o più).

  • Trappola della copia: std::vector ha semantica di valore, ma puntatori/riferimenti condivisi tra “righe” di una matrice reintroducono lo stesso problema di NumPy — preferisci mm::Image, che copia anch’essa per valore.

  • Accesso ai pixel tramite img.at(riga, colonna), con indicizzazione zero-based.

Il Capitolo 2 tratterà istogrammi ed equalizzazione del contrasto.

1.15 🤖 Uso del Gemini Notebook come Tutor Complementare

In questa edizione, oltre ai notebook interattivi su Google Colab, il Gemini Notebook è disponibile come strumento complementare di studio. La piattaforma utilizza esclusivamente i documenti forniti dall’autore come base di conoscenza, garantendo risposte coerenti con il contenuto del libro.

Importante🎓 Studia con il Tutor Intelligente

Accedi all’ambiente del capitolo tramite il link qui sotto ed esplora in particolare le opzioni Guida allo Studio e Conversazione per approfondire la tua comprensione.

🚀 ACCEDI AL GEMINI NOTEBOOK: CAPITOLO 01

🌐 Lingua e Linguaggio di Programmazione

Il progetto di questo capitolo nel Gemini Notebook è stato costruito esclusivamente con il testo in portoghese e gli esempi di codice in Python. Se stai studiando dall’edizione in inglese o francese, oppure seguendo il percorso in C++, le risposte del tutor potrebbero non corrispondere esattamente alla versione che stai leggendo.

⚠️ Avvertenza sul Contenuto Generato dall’IA

L’IA è un potente alleato nello studio, ma il contenuto generato può contenere errori o imprecisioni. Consulta sempre libri, articoli scientifici e altre fonti accademiche affidabili per validare le informazioni. Quando possibile, esegui gli esempi pratici forniti in questo capitolo per verificare i risultati.

Funzionalità Disponibili sulla Piattaforma

Il Gemini Notebook offre una suite avanzata di strumenti basati sull’IA per trasformare il contenuto statico del libro in un’esperienza di apprendimento dinamica e multimediale. La piattaforma utilizza tecniche di RAG (Retrieval-Augmented Generation), fondate sul lavoro di Lewis (2020), per basare le risposte strettamente sui documenti forniti e minimizzare il verificarsi di allucinazioni.

Le principali funzionalità includono:

  • Sintesi Multimodali (Audio e Video): Generazione di conversazioni naturali tra esperti nel formato di Sintesi Audio (stile podcast) e Sintesi Video, che discutono i temi centrali del capitolo, come le differenze tra PDI e VC, o l’interpretazione di trasformazioni come la sogliatura e il metodo di Otsu.
  • Visualizzazione di Strutture (Mappa Mentale e Infografica): Creazione automatica di diagrammi che collegano visivamente i concetti, ad esempio, il flusso di elaborazione dalla cattura dell’immagine digitale, passando per la conversione in toni di grigio, la sogliatura e la segmentazione binaria.
  • Strumenti di Valutazione (Test e Schede Didattiche): Generazione di Test a scelta multipla e Schede Didattiche (flashcards) per il consolidamento delle conoscenze, basati sul testo d’autore (es.: domande sulla formula di conversione RGB→grigio o sul funzionamento della soglia globale e di Otsu).
  • Supporto alla Presentazione (Slide e Relazioni): Assistenza nella strutturazione di Presentazioni di Slide e nella redazione di Relazioni tecniche, facilitando la comunicazione dei risultati di esperimenti con le immagini.
  • Analisi dei Dati (Tabella Dati): Organizzazione dei dati estratti dal testo in tabelle strutturate, aiutando la comprensione di esempi pratici, come il confronto tra diversi valori di soglia.
  • Chat Contestualizzata: Consente di porre domande dirette sul codice e sulla teoria, come: “Come implementare la conversione da RGB a toni di grigio utilizzando i pesi dello standard ITU‑R BT.601?” o “Cosa succede all’immagine binaria se scelgo una soglia T=200 invece di T=128?”.

1.16 Elenco di Esercizi

  1. (15%) Con le tue parole, definisci immagine digitale e pixel. Fornisci un esempio concreto di come un’immagine a colori (RGB) è rappresentata in forma matriciale nel computer.

  2. (15%) Spiega le differenze tra immagine binaria, toni di grigio (8 bit) e a colori RGB, indicando l’intervallo di valori possibili per ogni pixel in ciascun tipo.

  3. (20%) Considerando la formula di conversione RGB → toni di grigio dello standard ITU‑R BT.601: \[g = 0.299\,R + 0.587\,G + 0.114\,B\] Calcola il valore del pixel in toni di grigio per \((R,G,B) = (80, 180, 30)\). Arrotonda al numero intero più vicino.

  4. (20%) Che cos’è la sogliatura (thresholding)? Spiega la differenza tra scegliere una soglia \(T\) fissa (es.: \(T=128\)) e utilizzare il metodo di Otsu per la determinazione automatica della soglia. In poche parole, come sceglie la soglia il metodo di Otsu?

  5. (15%) Nel contesto della libreria didattica mm discussa nel capitolo, rispondi:

      1. (7,5%) Come si accede al valore del pixel nella posizione (riga=50, colonna=60) di un’immagine in scala di grigi img_gray?
      1. (7,5%) Qual è il vantaggio di usare mm::threshold(img_gray) senza passare la soglia? Confronta con la chiamata equivalente in OpenCV (cv::threshold).
  6. (15%) Cosa rappresentano i campi h, w e channels di un mm::Image per un’immagine RGB? Fornisci un esempio concreto con un’immagine di 640×480 pixel.

Riferimenti del Capitolo

La base teorica di questo capitolo comprende le seguenti opere di PDI e VC:

  • Gonzalez (2018) per i fondamenti di Elaborazione Digitale delle Immagini (EDI).
  • Singh (2019) per l’implementazione pratica di metodi di elaborazione e analisi delle immagini.
  • Szeliski (2022) per lo studio di VC e algoritmi fondamentali.
  • Bradski (2008) per l’applicazione della libreria OpenCV in ambiente Python.
  • Lewis (2020) per il concetto di generazione aumentata da recupero (RAG), utilizzato a supporto dell’elaborazione delle informazioni di questo materiale.

1.17 💻 Parte pratica con esercizi di programmazione

🎯 Obiettivo di questo Quaderno

Gli Esercizi di Programmazione (EPs) presentati di seguito possono essere sottomessi anche in attività Moodle (attività VPL) che forniscono feedback automatico.

Questo quaderno è stato sviluppato per superare le limitazioni d’uso di Moodle. Con esso, si deve:

  1. Sviluppare: Scrivere e modificare la propria soluzione direttamente nell’ambiente Colab.
  2. Validare: Testare il proprio codice localmente utilizzando gli stessi casi di test di Moodle.
  3. Organizzare: Salvare in modo sicuro i propri codici delle attività VPL.
  4. Valutare: Quando si è connessi a Moodle, basta copiare la propria soluzione e cliccare su Valuta in Moodle (se si è nella rete UFABC) per registrare il proprio voto ufficiale.

⚙️ Istruzioni Passo a Passo

In un ambiente di esecuzione (come VSCode, Jupyter o Colab), seguire l’ordine seguente per configurare l’ambiente e validare i propri esercizi:

Preparazione dell’Ambiente

Eseguire la cella di codice seguente per scaricare morph.py e testsuite.py dal repository della disciplina — solo se non sono già presenti nella directory locale. Con entrambi in ./, il notebook e i sottoprocessi del TestSuite trovano il modulo senza configurazioni aggiuntive di percorso.

Nota: Lo script testsuite.py cercherà automaticamente i casi di test in all/{cap}/cases su GitHub.

Scrittura del Codice

Salvare la propria soluzione in una cella di codice utilizzando il comando magico %%writefile. Il nome del file deve seguire il pattern EPX_Y.*, dove X è il capitolo, Y è l’esercizio e * è l’estensione del linguaggio.

Esempio: %%writefile EP01_01.py

Download

Scaricare morph.py e testsuite.py eseguendo la cella seguente:

import os, urllib.request

os.makedirs("tmp/state", exist_ok=True)  # artefatti di build della pista C++ (.cpp, binario, PNG)

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

# Il kernel è comunque Python anche nella pista C++: `mm` (morph.py) è usato dai
# simulatori, dalla visualizzazione delle figure che il binario C++ genera e dallo
# stato mm::Image tra le celle. cpp=True scarica anche la pista compilata
# (morph.hpp + stb_image*.h), usata nell'#include delle celle %%writefile *.cpp.
import config
config.setup(testsuite=True, cpp=True)
from morph import mm
from testsuite import TestSuite
✅ Ambiente pronto. Morph: 1.1.9 | OpenCV: 5.0.0 | TestSuite: 1.1.2

Esecuzione dei Test

Dopo aver salvato il file con la tua soluzione, esegui il comando seguente (in una nuova cella) per valutare i test automatici:

TestSuite("EP01_01.extensão").run()

Sostituisci l’estensione in base al linguaggio utilizzato:

Linguaggio Estensione
Python .py
Java .java
C .c
C++ .cpp
JavaScript .js
R .r

Come funziona: Il TestSuite scarica i casi di test da GitHub, esegue il tuo programma con ciascun input e confronta l’output con quello atteso – calcolando automaticamente il tuo voto.

Per testare codice Python direttamente, senza salvare file, usa run_code(codice) passando il codice come stringa in una variabile codice:

codice = """
from morph import mm
# ... il tuo codice qui ...
"""
TestSuite("EP04_01").run_code(codice)

⚠️ Importante: Regole e Buone Pratiche

🔹 Sull’Inserimento dei Dati

Il tuo programma deve leggere l’input standard (tastiera).

  • Python: Utilizza input().
  • Altri linguaggi: Utilizza il comando di lettura standard equivalente (cin, Scanner, ecc.).

🔹 Configurazione dell’IA su Colab

Per un migliore apprendimento, si consiglia di disattivare il completamento automatico del codice tramite IA, poiché non sarà disponibile durante le valutazioni. Ad esempio, nel browser Chrome:

  • Vai su: Strumenti > Impostazioni > IA generativa
  • Deseleziona: Abilita generazione di codice

🔹 Integrità Accademica (Plagio)

Questa risorsa di test locali si applica a ES senza variazioni. Tuttavia:

  • Individualità: Ogni studente deve sviluppare la propria soluzione.
  • Rilevamento di Similarità: Il professore utilizza strumenti che rilevano copie, anche con modifica di nomi di variabili o spazi bianchi.

1.17.1 EP01_01 📏 Tre metriche di distanza nella PDA (Processamento Digitale delle Immagini)

In questa attività, devi scrivere un programma che calcoli le tre distanze classiche nella PDA: Euclidea (L2), City-Block (L1) e Chessboard (L∞).

  • Leggi 4 numeri reali che rappresentano le coordinate: \(A_x, A_y, B_x, B_y\).
  • Calcola le tre distanze utilizzando le formule:

\[d_{\text{Euclidea}} = \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)\]

  • Stampa i tre risultati, ciascuno su una riga, formattati con due cifre decimali, nell’ordine: Euclidea, City-block, Chessboard.

📌 Importante:

  • Utilizza le funzioni matematiche standard del tuo linguaggio: math.sqrt, abs (o fabs) e max.
  • L’output deve contenere solo i numeri (uno per riga), senza testi aggiuntivi.
  • Consulta un simulatore interattivo per questo esercizio nella Figura 1.11 (grafico con trascinamento dei punti e visualizzazione delle tre metriche).

1.17.1.1 🖼️ Perché è importante? – Costo computazionale

In un’immagine 1000×1000 pixel (1 milione di pixel), calcolare la distanza di ogni pixel da un punto di riferimento richiede 1 milione di operazioni. La scelta della metrica influisce sulle prestazioni:

Metrica Operazioni per pixel Costo relativo (1M pixel) Quando utilizzarla
Euclidea (L2) 2 sottrazioni, 2 moltiplicazioni, 1 somma, 1 sqrt 🔴 Più costosa – sqrt è dispendiosa Distanza “reale” nello spazio continuo
City-block (L1) 2 sottrazioni, 2 abs, 1 somma 🟡 Moderata – senza radice quadrata Griglie, robotica, immagini binarie
Chessboard (L∞) 2 sottrazioni, 2 abs, 1 max 🟢 Più efficiente Movimenti di pezzi, morfologia

La funzione sqrt è computazionalmente più costosa rispetto ad operazioni come addizione, sottrazione, moltiplicazione e valore assoluto. Nelle CPU moderne, la differenza può essere piccola (circa 1,5× a 3×), ma nei sistemi embedded o nei cicli di milioni di iterazioni, qualsiasi guadagno conta. Per questo motivo, quando l’obiettivo è solo confrontare le distanze (es.: trovare il punto più vicino), utilizza la distanza euclidea al quadrato.

1.17.1.2 📋 Compito (specifica per VPL)

Input:
Un’unica riga con quattro numeri reali: Ax Ay Bx By

Output:
Tre righe, ciascuna con un numero reale con due cifre decimali (Euclidea, City-block, Chessboard).

1.17.1.3 📌 Esempi

Input Output Osservazione
0
0
3
4
5.00
7.00
4.00
Triangolo 3-4-5
0
0
1
1
1.41
2.00
1.00
Diagonale unitaria

Esempio di test di sqrt in Python, con timeit che isola ogni operazione:

%%writefile tmp/mm_out_1.cpp
// Benchmark: soma simples vs soma + sqrt
#include <chrono>
#include <cmath>
#include <iostream>

int main() {
    const long long N = 50000000;  // 50 milhões

    // Mide o tempo apenas da soma
    auto t0 = std::chrono::high_resolution_clock::now();
    for (long long i = 0; i < N; ++i) {
        double a = 3.0, b = 4.0;
        volatile double resultado = a + b;  // evita otimização
    }
    auto t1 = std::chrono::high_resolution_clock::now();
    double t_soma = std::chrono::duration<double>(t1 - t0).count();

    // Mide o tempo da soma + sqrt
    t0 = std::chrono::high_resolution_clock::now();
    for (long long i = 0; i < N; ++i) {
        double a = 3.0, b = 4.0;
        volatile double resultado = std::sqrt(a*a + b*b);  // evita otimização
    }
    t1 = std::chrono::high_resolution_clock::now();
    double t_sqrt = std::chrono::duration<double>(t1 - t0).count();

    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.113353 s
Soma + sqrt        : 0.156901 s
Razão (sqrt/soma)  : 1.38418x
🎮 Simulatore EP01_01: Metriche di Distanza nello Spazio Discreto Euclidea vs City-block vs Scacchiera

Clicca e trascina i punti A o B nel piano cartesiano o regola le loro coordinate qui sotto per confrontare le tre metriche di distanza in tempo reale.

📐 EUCLIDEA (L2)
5.00
√(Δx² + Δy²)
🧱 CITY-BLOCK (L1)
7.00
|Δx| + |Δy|
🏁 SCACCHIERA (L∞)
4.00
max(|Δx|, |Δy|)
👆 Trascina i punti A (Viola) o B (Arancione) sulla griglia.
Punto A
Punto B
Legenda Geometrica: Linea tratteggiata (Euclidea), Percorso ortogonale a L (City-block) ed Evidenziazione della dimensione massima (Scacchiera).
Euclidea City-block Scacchiera (Max)
Figura 1.11: Simulatore EP01_01: Distanze Euclidea, City-block e Chessboard

1.17.1.4 🐍 Python

Basta creare una normale cella di codice e inserire il codice Python. L’input può essere simulato usando input(), che funziona normalmente.

Esempio di cella:

%%writefile EP01_01.py
# Codice Python
x1,y1,x2,y2 = int(input()), int(input()), int(input()), int(input())
# Calcolo delle differenze
dx = abs(x2 - x1)
dy = abs(y2 - y1)

# 1. Distanza Euclidea (L2)
dist_euclidiana = (dx**2 + dy**2)**0.5

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

# 3. Distanza Scacchiera / Chebyshev (Linf)
dist_chessboard = max(dx, dy)

# Output formattato secondo i casi di test
print(f"{dist_euclidiana:.2f}")
print(f"{dist_city_block:.2f}")
print(f"{dist_chessboard:.2f}")
Overwriting EP01_01.py
# Aspetta che tu inserisca 4 numeri interi eseguendo questa cella.
# In Jupyter o Google Colab, la magia %run -i permette allo script di leggere dalla tastiera.
# In un terminale comune, useresti: python3 EP01_01.py (senza il '!' e senza '%run').

# %run -i EP01_01.py
# Invia 4 interi come input standard (stdin) allo script EP01_01.py usando una 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 esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_01.cases

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

📊 Risultato: 5/5 (100.0%)
🎉 Complimenti! Tutti i test sono stati superati.

1.17.1.5 ☕ Java

Per eseguire Java in Colab, è necessario utilizzare una cella con il prefisso %%writefile per salvare il codice in un file, compilarlo ed eseguirlo.

%%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 esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_01.cases

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

📊 Risultato: 5/5 (100.0%)
🎉 Complimenti! Tutti i test sono stati superati.

1.17.1.6 💻 C

Analogamente, usa %%writefile per salvare il codice, poi compila ed esegui. Per C, utilizziamo il compilatore GCC.

# Installare il compilatore GCC e gli strumenti di build
# build-essential include gcc, g++, make, ecc.
import platform, shutil, subprocess

def instalar_gcc():
    if shutil.which("gcc"):
        print("✅ GCC già disponibile."); 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 installato!")

instalar_gcc()
✅ GCC già disponibile.
%%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 il file .c generando l'eseguibile EP01_01
# -lm viene usato per linkare la libreria matematica (math.h) se necessario

!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 esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_01.cases

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

📊 Risultato: 5/5 (100.0%)
🎉 Complimenti! Tutti i test sono stati superati.

1.17.1.7 💻 C++

Analogamente al Java, usa %%writefile per salvare il codice, poi compila ed esegui. Ricorda che, in Colab, è necessario installare anche quanto segue:

# Installa il compilatore G++ per C++
# Il build-essential include g++, make, ecc.
import platform, shutil, subprocess

def instalar_gpp():
    if shutil.which("g++"):
        print("✅ G++ già disponibile."); return
    if platform.system() != "Linux":
        if platform.system() == "Darwin":
            print("⚠️ Mac: xcode-select --install")
        else:
            print("⚠️ Windows: usa 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("✅ Compilatore C++ pronto.")

instalar_gpp()
✅ G++ già disponibile.
%%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 esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_01.cases

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

📊 Risultato: 5/5 (100.0%)
🎉 Complimenti! Tutti i test sono stati superati.

1.17.1.8 🌐 JavaScript (Node.js)

Per JavaScript, usa %%writefile per creare il file ed eseguilo 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 esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_01.cases

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

📊 Risultato: 5/5 (100.0%)
🎉 Complimenti! Tutti i test sono stati superati.

1.17.1.9 📊 R

In Colab è possibile eseguire codice R direttamente utilizzando la magia %%R.
Il programma deve leggere quattro numeri (x1, y1, x2, y2) e mostrare la distanza euclidea con due cifre decimali.

Esempio di cella:

%%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))
# Installare R e Rscript
# Il pacchetto r-base installa l'ambiente R completo, inclusi Rscript
import platform, shutil, subprocess

def instalar_r():
    if shutil.which("R"):
        print("✅ R già disponibile."); 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("✅ Ambiente R pronto.")

instalar_r()
✅ R già disponibile.

Para testare nello stesso modo degli esempi precedenti, si deve utilizzare l’input standard (stdin) nel terminale o adattare il codice come segue:

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

# Output: Euclidea, City-block e Scacchiera
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 esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_01.cases

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

📊 Risultato: 5/5 (100.0%)
🎉 Complimenti! Tutti i test sono stati superati.

1.17.2 EP01_02 📊 Prestazioni Predittive — Metriche di ML in VC

In questa attività entrerai nel mondo dell’Apprendimento Automatico (Machine Learning). Il tuo obiettivo è valutare le prestazioni di un classificatore binario calcolando le metriche a partire da una Matrice di Confusione.

1.17.2.1 🧠 Perché la metrica giusta è importante?

Immagina un rilevatore di monete da R$ 1,00. L’impatto dell’errore definisce la metrica prioritaria:

Metrica Esempio Pratico Importanza in PDI/VC
Accuratezza Conteggio dei Grani Utile quando le classi sono bilanciate (es: metà dei grani difettosi, metà sani).
Precisione Sicurezza/Biometria Fondamentale per evitare Falsi Positivi (es: non consentire a un impostore di accedere a un sistema per un errore di riconoscimento).
Sensibilità Salute (Tumori) Fondamentale per evitare Falsi Negativi (es: non lasciare che un tumore passi inosservato in una radiografia).
F1-score Banconote Ideale per un equilibrio tra non rifiutare banconote autentiche e non accettare banconote false.

1.17.2.2 📊 La Matrice di Confusione

Predetta Positiva Predetta Negativa
Reale Positiva VP (Vero Positivo) FN (Falso Negativo)
Reale Negativa FP (Falso Positivo) VN (Vero Negativo)

Compito:

  1. Leggi 4 valori interi nell’ordine: VP, FN, FP, VN.
  2. Calcola le metriche utilizzando le formule:

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

\[\text{Precisione} = \frac{VP}{VP + FP}\]

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

\[\text{F1-score} = \frac{2 \times \text{Precisione} \times \text{Sensibilità}}{\text{Precisione} + \text{Sensibilità}}\]

  1. Stampa i risultati formattati con due cifre decimali, uno per riga.

📌 Importante:

  • Usa la divisione in virgola mobile per evitare risultati troncati.
  • L’ordine di uscita deve essere: Accuratezza, Precisione, Sensibilità e F1-score.
  • Consulta la simulazione interattiva di questo EP alla Figura 1.12.

1.17.2.3 📌 Esempio di Esecuzione

Input Output Osservazione
40 0.75 Accuratezza
10 0.73 Precisione
15 0.80 Sensibilità
35 0.76 F1-score

(Nota: VP=40, FN=10, FP=15, VN=35. Totale dei casi = 100)

🎮 Simulatore EP01_02: Prestazioni Predittive — Metriche di ML Matrice di Confusione & Metriche

Seleziona uno scenario predefinito o regola i cursori per osservare l'impatto in tempo reale sulla matrice di confusione e sulle metriche di valutazione.

INPUT (PARAMETRI DELLA MATRICE)
40
10
15
35
MATRICE DI CONFUSIONE DISCRETA
Pred +
Pred −
Reale +
VP
40
FN
10
Reale −
FP
15
VN
35
METRICHE CALCOLATE IN TEMPO REALE
Accuratezza 0.75
(VP + VN) / Totale
Precisione 0.73
VP / (VP + FP)
Sensibilità (Recall) 0.80
VP / (VP + FN)
F1-Score (Media Armonica) 0.76
2 · (P · R) / (P + R)
Totale Campioni: 100
Figura 1.12: Simulatore EP01_02: Prestazioni Predittive — Metriche di ML
%%writefile EP01_02.cpp
// your solution
Overwriting EP01_02.cpp
TestSuite("EP01_02.cpp").run()
✔️ EP01_02.cases esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_02.cases

🔍 Test di C++: EP01_02.cpp
⚠️ EP01_02.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.3 EP01_03 📈 Mean Average Precision (mAP) — Curva Precisione-Sensibilità

In questa attività valuterai un classificatore binario (es.: rilevamento della deforestazione in immagini satellitari, vedi dgi.inpe.br) tramite la curva Precisione-Sensibilità e la metrica mAP (Mean Average Precision). L’mAP è uno standard in competizioni come COCO (Common Objects in Context) e PASCAL VOC (Visual Object Classes) e nei modelli YOLO (You Only Look Once).

1.17.3.1 🧠 Perché l’mAP è la metrica standard?

Nella EP01_02 hai visto che la scelta della soglia altera significativamente Precisione e Sensibilità. L’mAP (Mean Average Precision) risolve questo problema: valuta il modello su più soglie (ogni soglia deve generare una matrice di confusione diversa) e riassume le prestazioni tramite l’area sotto la curva Precisione-Sensibilità (P-S).

Mentre l’F1‑Score considera un singolo punto di equilibrio, l’mAP considera l’intera curva. Più il valore è vicino a 1,0, migliore è il rilevatore su tutte le soglie e classi (es.: monete da 25, 50 centesimi e 1 euro).

Metrica Cosa riassume Limitazione
F1‑Score Equilibrio P × S su una singola soglia Dipende dalla soglia scelta
AP Area sotto la curva P-S di una classe Valida solo per una classe
mAP Media delle AP su tutte le classi Più complesso da implementare

Riferimenti: Roboflow — mAP · Video esplicativo

1.17.3.2 🔢 Come viene calcolato l’mAP — passo dopo passo

  1. Soglie fisse (usa sempre questa lista):

    limiares = [0.00, 0.09, 0.21, 0.31, 0.39, 0.52, 0.60, 0.71, 0.81, 0.89, 1.00]
  2. Per ogni soglia (t), classifica i campioni: predito = 1 se confiança ≥ t, altrimenti 0.
    Calcola VP, FP, FN, VN e ottieni Precisione((t)) e Sensibilità((t)).

  3. Costruisci la curva P-S: coppie (Sensibilità((t)), Precisione((t))), ordinate per Sensibilità crescente.

  4. Rendi monotona la Precisione:
    \[P_{\text{mono}}[i] = \max_{j \ge i} P[j]\]

  5. Calcola l’AP (area sotto la curva monotona) usando la regola del trapezio (approssimazione più accurata rispetto alla semplice somma di 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 = media delle AP di tutte le classi. In questa EP c’è solo 1 classe, quindi mAP = AP.

Nota

📐 Differenza riassunta:
La somma di Riemann approssima l’area tramite rettangoli, potendo sottostimare o sovrastimare. La regola del trapezio usa trapezi, riducendo l’errore considerando la media tra i valori agli estremi dell’intervallo, risultando generalmente più precisa per funzioni continue a tratti, come la curva Precisione-Sensibilità.

1.17.3.3 📋 Compito

Leggi un intero n (quantità di campioni). Poi leggi n righe, ciascuna con: verdade (0 o 1) e confiança (float 0.0–1.0).

Calcola e stampa, per la soglia 0.85 (indice 9 della lista):

  • Matrice di Confusione (VP, FN, FP, VN)
  • Accuratezza, Precisione, Sensibilità e F1‑Score

Successivamente, per tutte le soglie, stampa:

  • Precisioni grezze, Precisioni monotone e Sensibilità, separate da ,
  • mAP finale

1.17.3.4 📌 Importante

  • Soglia fissa per le metriche individuali: 0.85
  • Divisione sicura: se il denominatore è zero, usa 0
  • Formattazione: due cifre decimali
  • Rendi monotona da destra verso sinistra
  • La Figura 1.13 presenta una simulazione di questa domanda

1.17.3.5 📌 Esempio di Esecuzione

Input Output Atteso
7
0 0.94
1 0.80
1 0.69
0 0.67
1 0.30
1 0.15
1 0.15
# MÉTRICHE PER LA SOGLIA 0.85 #
Matrice di Confusione:
VP = 0, FN = 5
FP = 1, VN = 1

Metriche di Valutazione:
Accuratezza: 0.14
Precisione: 0.00
Sensibilità: 0.00
F1-Score: 0.00

# MÉTRICHE PER TUTTE LE SOGLIE #
Precisioni: 0.00, 0.00, 0.00, 0.50, 0.50, 0.50, 0.50, 0.50, 0.60, 0.71, 0.71
Precisioni mon.: 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71, 0.71
Sensibilità: 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 🐍 Suggerimento per calcolare l’AP (con regola del trapezio)

def calcular_AP(verdades, confiancas, limiares):
    m = len(limiares)
    precisoes = [0.0] * m
    sensibilidades = [0.0] * m
    for i in range(m):
        p, s = calcular_metricas(verdades, confiancas, limiares[i])
        precisoes[m-1-i] = p
        sensibilidades[m-1-i] = s
    prec_mono = precisoes.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):
        # Regola del trapezio: media delle altezze per la base
        area_trapezio = (prec_mono[i-1] + prec_mono[i]) / 2.0
        AP += area_trapezio * (sensibilidades[i] - sensibilidades[i-1])
    return precisoes, prec_mono, sensibilidades, AP
📊 Simulatore EP01_03: Curva Precisione-Sensibilità & mAP mAP & Curva P-S

Modifica i campioni (classe reale e confidenza) oppure scegli uno scenario predefinito per visualizzare la matrice di confusione, la curva P-S e il valore di mAP in tempo reale.

CAMPIONI (CLASSE REALE | CONFIDENZA)
# Vero Confidenza
SOGLIA PER METRICHE INDIVIDUALI
0.85
Soglie: 0.00 · 0.09 · 0.21 · 0.31 · 0.39 · 0.52 · 0.60 · 0.71 · 0.81 · 0.89 · 1.00
METRICHE ALLA SOGLIA 0.85
Pred +
Pred −
Reale +
VP
0
FN
5
Reale −
FP
1
VN
1
Accuratezza
0.14
Precisione
0.00
Sensib.
0.00
F1
0.00
CURVA PRECISIONE-SENSIBILITÀ
mAP = 0.71
Curva P-S Monotonica Area (AP)
Soglia Prec. P.mono Sensib.
Figura 1.13: Simulatore EP01_03: Mean Average Precision (mAP) e Curva P-S
%%writefile EP01_03.cpp
// your solution
Overwriting EP01_03.cpp
TestSuite("EP01_03.cpp").run()
✔️ EP01_03.cases esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_03.cases

🔍 Test di C++: EP01_03.cpp
⚠️ EP01_03.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.4 EP01_04 🖼️ Lettura e Informazioni di un’Immagine in Matrice

In questa attività, devi scrivere un programma che elabori un’immagine digitale rappresentata come una matrice di pixel in scala di grigi.

  • Leggi due interi L e C, che rappresentano il numero di righe e di colonne.
  • Leggi i L * C valori interi che compongono la matrice dell’immagine (ogni valore tra 0 e 255).
  • Calcola e stampa le seguenti informazioni:
  1. Il numero di righe.
  2. Il numero di colonne.
  3. Il valore del pixel più grande (Massimo).
  4. Il valore del pixel più piccolo (Minimo).
  5. La Media aritmetica di tutti i pixel.

📌 Importante:

  • L’output deve seguire esattamente il formato etichettato (es: Righe: X).
  • Il valore della media deve essere formattato con due cifre decimali.
  • Vedi un simulatore interattivo per questa domanda nella Figura 1.14 (griglia interattiva per la visualizzazione delle intensità e dei calcoli in tempo reale).

1.17.4.1 🧠 Perché è importante? – L’Immagine come Dato

Ogni immagine digitale è, in fondo, una struttura dati. In scala di grigi a 8 bit, ogni pixel è un valore scalare. Estrarre statistiche di base è il primo passo per:

Operazione Utilità Pratica
Massimo/Minimo Identificare se l’immagine è “slavata” (basso contrasto) o saturata.
Media Calcolare la luminosità globale della scena per regolazioni dell’esposizione.
Normalizzazione Ridimensionare i valori in intervalli come \([0, 1]\) nelle reti neurali.

1.17.4.2 📋 Attività (specifica per VPL)

Input:

La prima riga contiene l’intero L (righe).

La seconda riga contiene l’intero C (colonne).

Le righe successive contengono gli elementi della matrice.

Output:

Cinque righe formattate secondo l’esempio:

Righe: L

Colonne: C

Max: V

Min: V

Media: V.VV

1.17.4.3 📌 Esempi

Input Output Osservazione
2
3
0 128 255
50 100 200
Righe: 2
Colonne: 3
Max: 255
Min: 0
Media: 122.17
Immagine piccola ad alto contrasto
📊 Simulatore EP01_04: Statistiche Locali dei Pixel Matrice 5x5

Fai clic su qualsiasi pixel della matrice per incrementare il suo livello di grigio (passo +51) oppure usa le azioni predefinite qui sotto per osservare i limiti e la media globale.

Media Globale (µ)
0.00
Valore Max
0
Valore Min
0
Figura 1.14: Simulatore EP01_04: Statistiche dei pixel in matrice discreta 5x5
%%writefile EP01_04.cpp
// your solution
Overwriting EP01_04.cpp
TestSuite("EP01_04.cpp").run()
✔️ EP01_04.cases esiste già in casos/
📋 5 caso/i caricato/i da casos/EP01_04.cases

🔍 Test di C++: EP01_04.cpp
⚠️ EP01_04.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.5 EP01_05 🔄 Negativo di un’Immagine in Ton di Grigio

In questa attività, si deve scrivere un programma che calcoli il negativo di un’immagine digitale.

  • Leggere due interi L e C, che rappresentano il numero di righe e colonne.
  • Leggere i valori interi che compongono la matrice dell’immagine.
  • Per ogni pixel, applicare la trasformazione di inversione:

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

  • Stampare la matrice risultante, mantenendo il formato originale (L righe e C colonne).

📌 Importante:

  • I valori di ciascuna riga nell’output devono essere separati da uno spazio bianco.
  • L’output deve contenere solo i numeri della matrice risultante.
  • Vedere un simulatore interattivo per questa questione nella Figura 1.15 (confronto in tempo reale tra la matrice originale e il suo negativo).

1.17.5.1 🧠 Perché è importante? – Inversione di Intensità

Il negativo è una trasformazione lineare di base che inverte la scala di luminosità. È uno strumento essenziale per l’occhio umano per identificare dettagli chiari “nascosti” in sfondi più scuri, ed è ampiamente utilizzato in:

Applicazione Utilità
Immagini Mediche Migliora la visualizzazione di anomalie in tessuti densi (es. Radiografie).
Astronomia Evidenziare galassie e nebulose deboli contro il vuoto dello spazio.
Arti Digitali Effetti estetici e preparazione di maschere di selezione.

1.17.5.2 📋 Compito (specifica per VPL)

Input:

La prima riga contiene l’intero L.

La seconda riga contiene l’intero C.

Le righe successive contengono gli elementi della matrice.

Output:

La matrice invertita con L righe e C colonne.

1.17.5.3 📌 Esempi

Input Output Osservazione
2
3
0 128 255
50 100 255
255 127 0
205 155 55
Dove c’era 0 (nero) diventa 255 (bianco)
🌓 Simulatore EP01_05: Trasformazione del Negativo di Immagine p' = 255 - p

Fai clic sui pixel della matrice Originale (p) per modificarne i livelli di grigio (passo di +51) e osserva l'effetto dell'inversione complementare sulla matrice Negativo (255 - p).

ORIGINALE (p)
NEGATIVO (255 - p)
💡 La trasformazione del negativo mappa i toni scuri (vicini a 0) su toni chiari (vicini a 255) e viceversa, essendo utile per evidenziare dettagli scuri su sfondi chiari.
Figura 1.15: Simulatore EP01_05: Trasformazione del Negativo dell’Immagine (Inversione di Intensità)
%%writefile EP01_05.cpp
// your solution
Overwriting EP01_05.cpp
TestSuite("EP01_05.cpp").run()
✔️ EP01_05.cases esiste già in casos/
📋 7 caso/i caricato/i da casos/EP01_05.cases

🔍 Test di C++: EP01_05.cpp
⚠️ EP01_05.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.6 EP01_06 🎨 Conversione RGB → Toni di Grigio (ITU-R BT.601)

In questa attività, devi scrivere un programma che converta pixel colorati (RGB) in toni di grigio utilizzando la ponderazione fisiologica dello standard ITU-R BT.601.

  • Leggi due interi L e C, che rappresentano il numero di righe e colonne.
  • Leggi L × C triple di interi, dove ogni terna rappresenta i canali R (Rosso), G (Verde) e B (Blu) di un pixel.
  • Per ogni pixel, calcola il valore di grigio (\(g\)) usando la formula:

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

  • Stampa la matrice risultante (L righe e C colonne) contenente i valori interi convertiti.

📌 Importante:

  • Utilizza la funzione round() del tuo linguaggio per garantire l’arrotondamento corretto all’intero più vicino.
  • L’output deve contenere solo i valori di grigio, mantenendo la struttura a matrice (separati da spazio sulla riga).
  • Vedi un simulatore interattivo per questa domanda nella Figura 1.16 (regola gli slider per osservare come ogni colore contribuisce alla luminosità finale).

1.17.6.1 🧠 Perché non usare semplicemente la media?

L’occhio umano non percepisce tutti i colori con la stessa intensità. Siamo molto più sensibili al Verde che al Blu a causa della nostra evoluzione biologica. Lo standard ITU-R BT.601 utilizza pesi specifici per creare un’immagine in grigio che appaia naturalmente corretta alla nostra visione:

Canale Peso Percezione Umana
🟢 Verde 58.7% Massima sensibilità (distinzione del fogliame).
🔴 Rosso 29.9% Sensibilità media.
🔵 Blu 11.4% Bassa sensibilità (toni più scuri).

1.17.6.2 📋 Compito (specifica per VPL)

Input:

La prima riga contiene l’intero L.

La seconda riga contiene l’intero C.

Le righe successive contengono triple di interi R G B per ogni pixel.

Output:

La matrice di grigi con L righe e C colonne.

1.17.6.3 📌 Esempi

Input Output Osservazione
1
3
255 0 0 0 255 0 0 0 255
76 150 29 Nota come il Verde (150) sia più luminoso del Blu (29)
🎨 Simulatore EP01_06: Percezione del Colore & Pesi ITU-R BT.601 RGB → Scala di Grigi

Regola l'intensità dei canali Rosso (R), Verde (G) e Blu (B) per osservare come ciascuna componente contribuisce in modo ponderato al valore finale di luminanza nei livelli di grigio.

REGOLAZIONE DEI CANALI COLORE
80
180
30
RGB Originale
Toni di Grigio
💡 Il canale Verde (G) ha il peso maggiore (0.587) a causa della maggiore sensibilità spettrale del sistema visivo umano alle lunghezze d'onda del verde.
Figura 1.16: Simulatore EP01_06: Conversione RGB in Scala di Grigi (Ponderazione Percettiva ITU-R BT.601)
%%writefile EP01_06.cpp
// your solution
Overwriting EP01_06.cpp
TestSuite("EP01_06.cpp").run()
✔️ EP01_06.cases esiste già in casos/
📋 7 caso/i caricato/i da casos/EP01_06.cases

🔍 Test di C++: EP01_06.cpp
⚠️ EP01_06.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.7 EP01_07 ⚫ Soglia Manuale: Immagine Binaria

In questa attività, devi scrivere un programma che esegua la segmentazione di un’immagine tramite sogliatura (thresholding).

  • Leggi due interi L e C, che rappresentano le dimensioni della matrice.
  • Leggi un intero T, che sarà il valore della soglia (taglio).
  • Leggi i valori interi della matrice.
  • Per ogni pixel \(p\), applica la seguente regola di binarizzazione:

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

  • Stampa la matrice risultante contenente solo i valori 0 o 255.

📌 Importante:

  • Presta attenzione all’operatore: il pixel diventa bianco (255) solo se è strettamente maggiore di \(T\).
  • L’output deve mantenere la struttura della matrice (L righe e C colonne).
  • Consulta un simulatore interattivo per questo problema su Figura 1.17 (regola il cursore di \(T\) per osservare come gli oggetti vengono isolati dallo sfondo).

1.17.7.1 🧠 Cos’è la Segmentazione?

La sogliatura è il metodo più semplice per separare gli oggetti di interesse dallo sfondo dell’immagine. Trasformando i toni di grigio in bianco e nero puro, creiamo una mappa binaria che facilita il conteggio degli oggetti o l’identificazione delle forme:

Valore del Pixel (\(p\)) Condizione Risultato Finale
Scuro (\(p \le T\)) Sfondo/Rumore 0 (Nero)
Chiaro (\(p > T\)) Oggetto/In evidenza 255 (Bianco)

1.17.7.2 📋 Compito (specifica per VPL)

Input:

La prima riga contiene l’intero L.

La seconda riga contiene l’intero C.

La terza riga contiene l’intero T (soglia).

Le righe successive contengono gli elementi della matrice.

Output:

La matrice binarizzata (0 o 255) con L righe e C colonne.

1.17.7.3 📌 Esempi

Input Output Osservazione
2
4
128
0 100 128 200
50 129 255 64
0 0 0 255
0 255 255 0
Nota che il valore 128 è diventato 0 (poiché \(128 \le 128\))
🎛️ Simulatore EP01_07: Soglia Interattiva dell'Immagine Binario: 0 o 255
128
Ingresso (Scala di Grigi)
Uscita (Maschera Binaria)
Pixel con intensità maggiore di 128 (p > 128) diventano bianchi (255); altrimenti, diventano neri (0).
Figura 1.17: Simulatore EP01_07: Soglia Globale Interattiva (Binarizzazione p > T)
%%writefile EP01_07.cpp
// your solution
Overwriting EP01_07.cpp
TestSuite("EP01_07.cpp").run()
✔️ EP01_07.cases esiste già in casos/
📋 7 caso/i caricato/i da casos/EP01_07.cases

🔍 Test di C++: EP01_07.cpp
⚠️ EP01_07.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.8 EP01_08 🎨 Rimappatura per Intervallo di Intensità

In questa attività, devi scrivere un programma che applichi trasformazioni lineari distinte a diverse regioni di intensità dell’immagine.

  • Leggi due interi L e C, che rappresentano le dimensioni della matrice.
  • Leggi gli interi T (soglia), δ₁ (delta 1) e δ₂ (delta 2).
  • Leggi i valori interi della matrice.
  • Per ogni pixel \(p\), applica la regola di rimappatura condizionale:

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

  • Stampa la matrice risultante con i nuovi valori di intensità.

📌 Importante:

  • δ₁ è l’offset applicato ai pixel scuri (al di sotto della soglia).
  • δ₂ è l’offset applicato ai pixel chiari (maggiori o uguali alla soglia).
  • I casi di test garantiscono che il risultato sia sempre nell’intervallo valido da 0 a 255, quindi non è necessario gestire saturazione o arrotondamenti.
  • L’output deve mantenere la struttura di matrice (L righe e C colonne).

1.17.8.1 🧠 Trasformazione Condizionale dei Pixel

Nell’elaborazione digitale delle immagini (PDI), spesso dobbiamo trattare le regioni in modo indipendente. Questa tecnica consente, ad esempio, di schiarire solo le ombre di una fotografia (aumentando i pixel scuri) senza bruciare la luminosità delle aree già chiare, o viceversa.

Intervallo di Intensità Condizione Operazione
Pixel scuri \(p < T\) \(p + \delta_1\)
Pixel chiari \(p \ge T\) \(p + \delta_2\)

1.17.8.2 📋 Compito (specifica per VPL)

Input:

La prima riga contiene gli interi L e C.

La seconda riga contiene gli interi T, δ₁ e δ₂.

Le righe successive contengono gli elementi della matrice.

Output:

La matrice trasformata con L righe e C colonne, con valori separati da spazi.

1.17.8.3 📌 Esempi

Input Output Osservazione
2 4
128 60 -40
0 100 150 255
80 128 200 30
60 160 110 215
140 88 160 90
I pixel < 128 sommano 60. I pixel ≥ 128 sottraggono 40.
🎛️ Simulatore EP01_08: Rimappatura per Intervallo Condizionale p < T → p + δ₁ | p ≥ T → p + δ₂

Regola la soglia di separazione (T) e gli spostamenti di luminosità (δ₁ e δ₂) per applicare trasformazioni di intensità differenziate nelle regioni scure e chiare dell'immagine.

128
+60
-40
Input Originale (p)
Risultato Trasformato
Regola attiva: p < 128 → p + (+60)  |  p ≥ 128 → p + (-40)
Figura 1.18: Simulatore EP01_08: Rimappatura per Intervallo Condizionale (Luminosità e Contrasto per Soglia)
%%writefile EP01_08.cpp
// your solution
Overwriting EP01_08.cpp
TestSuite("EP01_08.cpp").run()
✔️ EP01_08.cases esiste già in casos/
📋 8 caso/i caricato/i da casos/EP01_08.cases

🔍 Test di C++: EP01_08.cpp
⚠️ EP01_08.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.9 EP01_09 🏁 Schema a Scacchiera: Matrice a Quadretti

In questa attività, devi scrivere un programma che generi un’immagine sintetica con il motivo di una scacchiera.

  • Leggi due interi L (righe) e C (colonne).
  • Genera una matrice in cui i valori alternano tra 0 (nero) e 1 (bianco).
  • La logica di riempimento deve seguire la regola di parità:
  • L’elemento nella posizione \((0,0)\) è sempre 0.
  • Un pixel nella posizione \((i, j)\) sarà 1 se la somma degli indici \((i + j)\) è dispari.
  • Un pixel nella posizione \((i, j)\) sarà 0 se la somma degli indici \((i + j)\) è pari.

📌 Importante:

  • I colori devono alternarsi correttamente sia orizzontalmente che verticalmente.
  • L’output deve essere la matrice stampata riga per riga, con gli elementi separati da uno spazio.
  • Consulta un simulatore interattivo per questo problema su Figura 1.19 (regola le dimensioni per visualizzare la costruzione della griglia e l’output testuale corrispondente).

1.17.9.1 🧠 Motivi Sintetici

Creare motivi geometrici è un esercizio fondamentale per padroneggiare la logica degli indici nelle matrici. Nell’elaborazione digitale delle immagini, il motivo a scacchiera non è solo estetico; è ampiamente utilizzato per:

Applicazione Utilità
Calibrazione della fotocamera Stimare i parametri intrinseci ed estrinseci dell’obiettivo.
Correzione della distorsione Identificare e correggere l’effetto “a barile” o “a cuscino” negli obiettivi grandangolari.
Mappatura 3D Proiettare motivi noti per ricostruire superfici in sistemi a luce strutturata.

1.17.9.2 📋 Compito (specifica per VPL)

Input:

Una riga contenente l’intero L (righe).

Una riga contenente l’intero C (colonne).

Output:

La matrice a scacchiera con L righe e C colonne, stampata con spazi tra gli elementi.

1.17.9.3 📌 Esempi

Input Output Osservazione
3
4
0 1 0 1
1 0 1 0
0 1 0 1
Nota che ogni riga inizia con l’inverso della precedente
🏁 Simulatore EP01_09: Generatore di Matrice a Scacchiera (i + j) % 2

Modifica il numero di righe (L) e colonne (C) per osservare come l'alternanza di parità delle coordinate della griglia costruisce la matrice binaria a scacchiera.

×
GRIGLIA GRAFICA
USCITA ATTESA (VALORI)
Figura 1.19: Simulatore EP01_09: Generatore di Motivo a Scacchiera (Logica di Parità (i + j) % 2)
%%writefile EP01_09.cpp
// your solution
Overwriting EP01_09.cpp
TestSuite("EP01_09.cpp").run()
✔️ EP01_09.cases esiste già in casos/
📋 7 caso/i caricato/i da casos/EP01_09.cases

🔍 Test di C++: EP01_09.cpp
⚠️ EP01_09.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.10 EP01_10 📄 Metadati: Lettura di File PGM

In questa attività, devi leggere un file immagine nel formato PGM (Portable Gray Map) ed estrarne le dimensioni dall’intestazione.

  • Il formato PGM (P2) è un file di testo semplice (ASCII) che memorizza immagini in scala di grigi.
  • Il file possiede un’intestazione strutturata nel seguente modo:
  1. Versione: L’identificatore P2.
  2. Commenti: Righe opzionali che iniziano con # (devono essere ignorate).
  3. Dimensioni: Due interi che rappresentano Larghezza e Altezza.
  4. Massimo: Un intero che rappresenta l’intensità massima (generalmente 255).
  • Dopo l’intestazione, seguono i dati dei pixel.

📌 Importante:

  • Lettura del File: Devi aprire il file indicato nell’esempio utilizzando la funzione open() di Python.
  • Ordine di Uscita: Contrariamente all’ordine presente nel file, l’output atteso deve essere nel formato di tupla: (Altezza, Larghezza, Canali).
  • Poiché i file PGM sono in scala di grigi, il numero di Canali è sempre 1.
  • Vedi un simulatore interattivo per questa domanda alla Figura 1.20 (regola le dimensioni per vedere come viene generata l’intestazione ASCII).

1.17.10.1 🧠 Comprendere il formato PGM

Il formato PGM è uno dei più semplici per l’elaborazione delle immagini. Essendo in testo puro, consente di visualizzare i metadati e persino i valori dei pixel aprendo il file in un blocco note:

Componente Esempio Significato
Numero Magico P2 Identifica che è un PGM in formato testo (ASCII).
Commento # CREATOR... Riga informativa ignorata dal processore.
Dimensioni 397 343 397 colonne (Larghezza) e 343 righe (Altezza).
Intensità 255 Definisce il valore del bianco puro (scala da 0 a 255).

1.17.10.2 📋 Compito (specifica per VPL)

Input:

Nessun input da tastiera. Il programma deve leggere il file "aula01fig03b.pgm" presente nella directory di esecuzione.

Output:

Una tupla contenente (Altezza, Larghezza, 1).

1.17.10.3 📌 Esempi

Nome del File Output Atteso Osservazione
“aula01fig03b.pgm” (343, 397, 1) Nota l’inversione dell’ordine: Altezza prima
📄 Simulatore EP01_10: Struttura del File PGM ASCII P2 & Formato (H, W, C)

Modifica le dimensioni di larghezza (W) e altezza (H) per osservare l'assemblaggio dinamico dell'intestazione PGM e il formato della tupla dell'array risultante in Python (Righe × Colonne × Canali).

DEFINIZIONI DELL'IMMAGINE
💡 Attenzione alla convenzione: L'intestazione PGM dichiara prima W H (Larghezza × Altezza), mentre l'array in Python/NumPy riporta la tupla come (H, W, C) (Righe × Colonne × Canali).
CONTENUTO DEL FILE (.PGM)
P2
# CREATOR: UFABC PDI / EP01_10
397 343
255
120 134 210 0 85 255 ...
Uscita della Funzione mm.readImg (Formato dell'Array):
(343, 397, 1)
Figura 1.20: Simulatore EP01_10: Struttura del file PGM (Intestazione ASCII P2 e mappatura a tupla Python)
NotaNota

Il file necessario per questo EP verrà scaricato automaticamente dal repository tramite il seguente codice:

%%writefile tmp/mm_out_2.cpp
// Compile with: g++ -std=c++17 -O2 program.cpp -o program $(pkg-config --cflags --libs opencv4) -lm -lstdc++fs

#include "morph.hpp"
#include <iostream>
#include <string>

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

    const std::string url = BASE_URL + "/" + file;

    // Scarica il file direttamente
    mm::Image img = mm::read(url);
    std::cout << "✅ File scaricato: " << 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
✅ File scaricato: aula01fig03b.pgm
%%writefile EP01_10.cpp
// your solution
Overwriting EP01_10.cpp
TestSuite("EP01_10.cpp").run()
✔️ EP01_10.cases esiste già in casos/
📋 1 caso/i caricato/i da casos/EP01_10.cases

🔍 Test di C++: EP01_10.cpp
⚠️ EP01_10.cpp: file vuoto (meno di 3 righe). Test saltati.

1.17.11 EP01_11 📈 Analisi di Vicinato: Filtro di Massimo 1D

In questa attività, devi implementare un semplice filtro morfologico di massimo che opera su un segnale unidimensionale (vettore).

  • Leggi un intero n, che rappresenta la dimensione del vettore.
  • Leggi i n elementi interi che compongono il vettore originale v1.
  • Crea un nuovo vettore v2, in cui ogni posizione \(i\) è il risultato del confronto tra l’elemento corrente e i suoi vicini immediati:

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

📌 Importante:

  • Bordi: Alle estremità del vettore (indici \(0\) e \(n-1\)), il vicinato ha solo due elementi (quello stesso e l’unico vicino disponibile). All’indice \(0\), confronta solo \(v1[0]\) e \(v1[1]\). All’ultimo indice, confronta solo \(v1[n-2]\) e \(v1[n-1]\).
  • Output: Stampa l’intestazione “v2:” seguita dai valori del vettore risultante, uno per riga.
  • Per un simulatore interattivo su questo problema, vedi Figura 1.21 (passa il mouse sui risultati per visualizzare la finestra di vicinato utilizzata nel calcolo).

1.17.11.1 🧠 Perché analizzare i vicini?

Nell’elaborazione delle immagini, il valore di un pixel raramente è isolato; dipende dal contesto circostante. Il Filtro di Massimo è la base dell’operazione di Dilatazione nella morfologia matematica e serve a:

Funzione Effetto Visivo
Enfatizzazione Espande strutture luminose e “ingrossa” oggetti chiari.
Rimozione del Rumore Elimina piccoli punti neri (rumore “sale e pepe” scuro).
Riempimento Chiude piccoli buchi o lacune in forme binarie.

1.17.11.2 📋 Compito (specifica per VPL)

Input:

Un intero n.

Nelle righe successive, i n elementi interi del vettore.

Output:

La stringa v2: sulla prima riga.

Nelle righe successive, ogni elemento di v2 (uno per riga).

1.17.11.3 📌 Esempi

Input Output Osservazione
5
10
20
5
30
15
v2:
20
20
30
30
30
All’indice 1: max(10, 20, 5) = 20
📈 Simulatore EP01_11: Filtro di Massimo Locale 1D Finestra 1x3

Clicca sugli elementi di v1 (Ingresso) per generare nuovi valori individuali oppure passa il mouse sulle celle di v2 (Risultato) per ispezionare la finestra locale di vicinanza.

Vettore v1 (Ingresso)
⬇️
Vettore v2 (Risultato del Massimo)
Passa il cursore del mouse su una cella del vettore v2 per analizzare la finestra di massimo locale.
Figura 1.21: Simulatore EP01_11: Filtro di Massimo Locale 1D (Vicinato 1x3 con Condizione di Bordo)
%%writefile EP01_11.cpp
// your solution
Overwriting EP01_11.cpp
TestSuite("EP01_11.cpp").run()
✔️ EP01_11.cases esiste già in casos/
📋 7 caso/i caricato/i da casos/EP01_11.cases

🔍 Test di C++: EP01_11.cpp
⚠️ EP01_11.cpp: file vuoto (meno di 3 righe). Test saltati.