EDI+VA · Esercizio di Programmazione

EP07_07 — ⚫ Classificazione Reale di un Mosaico di Trame tramite LBP + k-NN

7.18.7 EP07_07 ⚫ Classificazione Reale di un Mosaico di Trame tramite LBP + k-NN

Negli esercizi precedenti, il descrittore LBP (EP07_04) e il classificatore k-NN multiclasse (EP07_06) sono stati studiati separatamente, sempre a partire da dati già forniti in input — vicinanze \(3\times3\) isolate o istogrammi precedentemente estratti. In questo esercizio conclusivo del capitolo, il programma dovrà leggere un’immagine reale, nel formato PGM ASCII (P2), calcolare il descrittore LBP direttamente dai pixel e, successivamente, classificare ogni regione tramite il k-NN, riproducendo, in scala ridotta, il flusso completo di un sistema di riconoscimento delle trame. Questo approccio anticipa anche l’idea di classificazione per mosaico di regioni, legata alla segmentazione semantica studiata in un capitolo successivo.

Il simulatore interattivo dell’EP07_06 utilizzava solo tre classi (granulare, a righe e maculata) rappresentate da punti bidimensionali fittizi. In questo esercizio, si aggiunge una quarta classe, a scacchi, e i punti vengono sostituiti da istogrammi LBP estratti da un’immagine reale.

L’immagine di input è un mosaico formato da una griglia \(G\times G\) di blocchi quadrati di \(S\times S\) pixel. Ogni blocco contiene un campione di una delle quattro classi di trama sintetica del capitolo: granulare, a righe, maculata o a scacchi (motivo a scacchiera con intensità alternate). Come negli altri esercizi del libro, il caricamento dell’immagine viene eseguito dalla funzione didattica mm.readImg.

ConsiglioPerché un mosaico unico, e non più immagini?

L’input riunisce i \(G \times G\) campioni di trama in un unico file PGM, solo per semplificare la lettura dei dati ed evitare l’apertura di più file. Per l’algoritmo, ciò non modifica l’elaborazione: ogni blocco viene trattato in modo indipendente, come se fosse un’immagine isolata. L’unica eccezione è l’esclusione del bordo (punto 4 di seguito).

7.18.7.1 📋 Linee Guida di Implementazione

  1. Lettura delle dimensioni dell’immagine

    Leggere, tramite l’input standard, due righe contenenti, rispettivamente, il numero di righe \(L\) e il numero di colonne \(C\) del mosaico (entrambi multipli della dimensione del blocco \(S\), con \(L=C\)).

  2. Caricamento dell’immagine

    Utilizzare la funzione didattica

    f = mm.readImg(L, C)

    per leggere i valori di intensità \(L \times C\) (toni di grigio, uint8) del mosaico.

  3. Parametri della griglia

    Leggere l’intero \(G\) (numero di blocchi per lato) e l’intero \(S\) (dimensione del lato di ogni blocco, in pixel), soddisfacendo \(L = C = G \times S\).

  4. Calcolo del codice LBP per pixel

    Per ogni pixel interno dell’immagine (cioè che non si trova sul bordo globale di f — riga o colonna \(0\) o \(L-1\)/\(C-1\)), calcolare il codice LBP con \(P=8\) vicini e raggio \(R=1\), percorrendo i vicini in senso orario a partire dall’angolo superiore sinistro, esattamente come nell’EP07_04: [riga-1][colonna-1], [riga-1][colonna], [riga-1][colonna+1], [riga][colonna+1], [riga+1][colonna+1], [riga+1][colonna], [riga+1][colonna-1], [riga][colonna-1].

    I pixel sul bordo globale dell’immagine non hanno una vicinanza completa e devono essere ignorati (non contribuiscono a nessun istogramma). Questo include i pixel di bordo che cadono all’interno di un blocco (l’esclusione è sempre relativa al bordo dell’intera immagine, non al bordo di ogni singolo blocco).

  5. Istogramma LBP uniforme per blocco (10 contenitori)

    Per ogni blocco \((i,j)\) della griglia (\(i,j = 0,\ldots,G-1\)), accumulare, tra i suoi pixel validi (punto 4), un istogramma \(H^{(i,j)}\) di \(10\) contenitori:

    • Considerando la sequenza circolare di bit \(s_0,\ldots,s_7\) del pixel (stessa regola di transizioni dell’EP07_04): se il numero di transizioni è \(\le 2\) (pattern uniforme), il pixel contribuisce al contenitore \(\operatorname{popcount}(s_0,\ldots,s_7) \in \{0,\ldots,8\}\) (numero di bit uguali a 1);
    • In caso contrario (pattern non uniforme), il pixel contribuisce al contenitore \(9\).

    Alla fine, normalizzare l’istogramma di ogni blocco dividendolo per il numero di pixel validi in esso contenuti, ottenendo \(\hat H^{(i,j)}\), con \(\sum_{b=0}^{9} \hat H^{(i,j)}[b] = 1\).

  6. Prototipi di addestramento

    Leggere l’intero \(Ncl\) (numero di classi) seguito da \(Ncl\) nomi di classe (ordine che definisce la matrice di confusione e il criterio di parità per la votazione, come nell’EP07_06); successivamente, leggere la stringa \(M\) (metrica: euclidiana o manhattan) e l’intero dispari \(k\); infine, leggere l’intero \(N\) (numero di prototipi) e, per ciascuno, il nome della classe seguito da \(10\) valori reali (istogramma prototipo già normalizzato).

  7. Classificazione k-NN di ogni blocco

    Per ogni blocco, calcolare la distanza di \(\hat H^{(i,j)}\) da ciascuno degli \(N\) prototipi, usando la metrica \(M\) (stesse formule dell’EP07_06). Selezionare i \(k\) prototipi più vicini (criterio di parità per la distanza basato sull’ordine di lettura dei prototipi) e classificare tramite la classe maggioritaria (criterio di parità per la votazione basato sull’ordine delle classi del punto 6).

  8. Etichette reali e valutazione

    Leggere, in un’unica riga, i \(G \times G\) nomi di classe reali di ogni blocco, in ordine di lettura per riga della griglia (blocco \((0,0)\), \((0,1)\), …, \((0,G-1)\), \((1,0)\), …). Costruire la matrice di confusione \(Ncl \times Ncl\) (riga = classe reale, colonna = classe prevista) e calcolare l’accuratezza globale.

  9. Output

    Stampare, per ogni blocco (nello stesso ordine di lettura delle etichette reali del punto 8), la classe prevista. Successivamente, stampare la matrice di confusione (una riga per classe reale, nell’ordine del punto 6). Infine, stampare l’accuratezza, arrotondata a 4 cifre decimali.

7.18.7.2 📌 Vincoli Computazionali

  • Descrittore fisso: \(P=8\), \(R=1\) e \(10\) contenitori (come da punto 5) sono fissi in questo esercizio — non vengono letti dall’input.
  • Esclusione del bordo globale, non del blocco: un pixel sul confine tra due blocchi, ma all’interno dell’immagine, è valido e contribuisce normalmente all’istogramma del blocco a cui appartiene.
  • Ordine di lettura come criterio di parità: sia il criterio di parità per la distanza (punto 7) che quello per la votazione (punto 7) seguono esattamente le stesse convenzioni dell’EP07_01 e dell’EP07_06.
  • Prototipi come input, non appresi: a differenza del Progetto Pratico 2, gli istogrammi di addestramento vengono forniti direttamente nell’input; il programma non deve generare trame sintetiche.

7.18.7.3 🧠 Fondamenti Teorici

Fase dell’esercizio Fase corrispondente nel capitolo
Lettura dell’immagine tramite mm.readImg Acquisizione dell’immagine nel pipeline di riconoscimento di pattern
Codice LBP per pixel (EP07_04) local_binary_pattern(immagine, P=8, R=1, method="uniform")
Istogramma di 10 contenitori per blocco Funzione descrittor_lbp del Progetto Pratico 2 (bins=10, range=(0, P+2))
Classificazione k-NN con metrica selezionabile (EP07_06) KNeighborsClassifier addestrato su X_texture
Matrice di confusione \(Ncl\times Ncl\) e accuratezza confusion_matrix e accuracy_score su yt_test

Questo esercizio evidenzia, con pixel reali invece di valori sintetici, una limitazione discussa nella sezione finale del capitolo: classi di trama visivamente distinte per un osservatore umano — come granulare e maculata — possono produrre istogrammi LBP simili quando la vicinanza considerata è piccola (\(R=1\)), poiché entrambe presentano un’alta frequenza di pattern non uniformi alla scala di un singolo pixel. La classe a scacchi, invece, avendo bordi regolari e ripetitivi, tende a essere separata con maggiore facilità. Ci si aspetta che la matrice di confusione prodotta rifletta esattamente questo pattern di confusione parziale.

7.18.7.4 📦 Specifica di Input e Output (VPL)

Input:

L
C
[matrice L x C dell'immagine]
G S
Ncl nome_classe_1 ... nome_classe_Ncl
M k
N
nome_classe h0 h1 ... h9      (ripetuta N volte)
etichetta(0,0) etichetta(0,1) ... etichetta(G-1,G-1)

Output:

  • \(G \times G\) righe con la classe prevista per ogni blocco, nell’ordine di lettura della griglia.
  • \(Ncl\) righe con la matrice di confusione (una riga per classe reale, valori separati da spazi).
  • Ultima riga: Acuracia: <valore>.

7.18.7.5 📌 Esempio (verifica manuale)

Per verificare l’implementazione del descrittore prima di testarla su un mosaico completo, si consideri un’immagine \(6\times6\) omogenea, con tutti i pixel di intensità \(100\), trattata come un unico blocco (\(G=1\), \(S=6\)). Poiché ogni pixel interno ha gli 8 vicini con intensità uguale a quella del centro (\(g_p \ge g_c\) in tutti i casi), tutti i bit \(s_p\) valgono 1, il numero di transizioni è \(0\) (uniforme) e il contenitore è \(\operatorname{popcount}(11111111)=8\). L’istogramma dell’unico blocco è, quindi, 0 0 0 0 0 0 0 0 1 0.

Input (riepilogo) Output Osservazione
6
6
[36 valori uguali a 100]
1 6
2 uniforme altro
euclidiana 1
2
uniforme 0 0 0 0 0 0 0 0 1 0
altro 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1
uniforme
uniforme
1 0
0 0
Acuracia: 1.0000
La distanza del blocco al prototipo uniforme è esattamente \(0\); la classe altro non appare nell’etichetta reale, quindi la sua riga nella matrice di confusione è nulla.

7.18.7.6 📌 File di Riferimento (.pgm)

Per il debug locale, due mosaici di test nel formato ASCII P2 sono messi a disposizione (allegati a questa consegna; quando li si integra nel repository del capitolo, salvarli in all/cap07/dati/EP07/):

  • 📥 Caso 1 — Mosaico semplice (Caso1_Mosaico_Simples.pgm): griglia \(2\times2\) di blocchi di \(24\times24\) pixel, un campione di ciascuna delle quattro classi, con basso rumore — utile per validare la lettura dell’immagine e la logica di classificazione in uno scenario controllato.
  • 📥 Caso 2 — Mosaico misto (Caso2_Mosaico_Misto.pgm): griglia \(3\times3\) di blocchi di \(16\times16\) pixel, con classi ripetute e maggiore variabilità — scenario in cui la confusione tra granulare e maculata discussa nei Fondamenti Teorici tende a manifestarsi.

La Figura 7.27 mostra i due mosaici, per un’ispezione visiva prima dell’implementazione.

import os
import urllib.request
import numpy as np

def garantir_e_baixar_arquivo(nome_arquivo):
    diretorio_local = "dados/EP07"
    caminho_local = os.path.join(diretorio_local, nome_arquivo)
    
    # Creare la directory locale se non esiste
    if not os.path.exists(diretorio_local):
        os.makedirs(diretorio_local)
        
    # Se il file non esiste localmente, lo scarica dal repository remoto
    if not os.path.exists(caminho_local):
        url_base = "https://raw.githubusercontent.com/fzampirolli/"
        url_base += "pdi-vc/master/all/cap07/dados/EP07"
        url_arquivo = f"{url_base}/{nome_arquivo}"
        print(f"Scaricamento {nome_arquivo} da GitHub...")
        try:
            urllib.request.urlretrieve(url_arquivo, caminho_local)
        except Exception as e:
            raise IOError(f"Erro ao baixar {nome_arquivo} do GitHub. ",
                          "Verifique a conexão ou a URL. Detalhes: {e}")
            
    return caminho_local

def ler_pgm_p2(caminho):
    with open(caminho) as f:
        linhas = [l for l in f.read().split() if l]
    assert linhas[0] == "P2"
    C, L = int(linhas[1]), int(linhas[2])
    maxv = int(linhas[3])
    valores = list(map(int, linhas[4:4 + L * C]))
    return np.array(valores, dtype=np.uint8).reshape(L, C)

# Garantisce il download e ottiene il percorso corretto
arq_caso1 = garantir_e_baixar_arquivo("Caso1_Mosaico_Simples.pgm")
arq_caso2 = garantir_e_baixar_arquivo("Caso2_Mosaico_Misto.pgm")

# Legge le matrici PGM
caso1 = ler_pgm_p2(arq_caso1)
caso2 = ler_pgm_p2(arq_caso2)

mm.show(
    [caso1, caso2],
    titles=[
        "Caso 1: Mosaico Semplice\n(blocchi 2x2, 1 campione/classe)",
        "Caso 2: Mosaico Misto\n(blocchi 3x3, classi ripetute)",
    ],
    cols=2,
    figsize=(8, 4),
)
Figura 7.27: Mosaici di riferimento (formato PGM ASCII) usati nell’EP07_07. Caso 1: griglia 2x2 con un campione di ogni classe. Caso 2: griglia 3x3 con classi ripetute e maggiore variabilità.
🎮 Simulatore EP07_07: Classificazione del Mosaico tramite LBP + k-NN ⚫ pipeline completo

Mosaico 3x3 di blocchi 12x12 (L=C=36). LBP (P=8,R=1) calcolato pixel per pixel, con esclusione del bordo globale. Regola k e la metrica e osserva la classificazione di ciascun blocco rispetto a 8 prototipi (2 per classe).

⚠️ Texture sintetiche generate da codice, non i file .pgm reali di EP07_07. Usa questo simulatore per capire il flusso dell'algoritmo, non come riferimento di difficoltà tra le classi.
1
Figura 7.28: Simulatore EP07_07: Classificazione di un Mosaico di Trame tramite LBP + k-NN
%%writefile EP07_07.py
# Codice Python
Overwriting EP07_07.py
TestSuite("EP07_07.py").run()
✔️ EP07_07.cases esiste già in casos/
📋 2 caso/i caricato/i da casos/EP07_07.cases

🔍 Test di Python: EP07_07.py
⚠️ EP07_07.py: file vuoto (meno di 3 righe). Test saltati.