Estratto del documento

Lezione 5 - 18/03/2026

Appunti lezione 5 – 18 marzo 2026

Le immagini in scala di grigio sono essenzialmente matrici di pixel in cui ogni singolo elemento è codificato tramite il tipo di dato unsigned char (che occupa un solo byte). Un'immagine a colori, al contrario, ha bisogno di una quantità maggiore di byte per poter veicolare l'intera informazione cromatica. Nello specifico, in un'immagine standard si usano 256 valori per ogni singolo canale; nel modello RGB questo porta ad avere 256 combinazioni possibili, generando un totale di circa 16.000.000 di colori.

(Se voglio 1000 byte, ne riceverò 1024 perché avrò la potenza del 2 più vicina al valore che mi serve. Dobbiamo capire che in informatica parliamo sempre in potenza del 2)

Per codificare un pixel a colori sono necessari quindi almeno 3 byte: uno per la componente del rosso, uno per il verde e uno per il blu. Di conseguenza, un'immagine a colori risulta essere almeno tre volte più grande in termini di memoria rispetto a una non a colori. In alcuni contesti esiste anche un quarto byte aggiuntivo per ogni pixel, andando a formare il formato RGBD. In questo caso la "D" sta per profondità (depth) e viene utilizzata per mappare ed esaltare ulteriori componenti spaziali.

Un discorso simile vale per le immagini mediche, le quali sfruttano colorazioni speciali per far risaltare particolari componenti anatomiche. È importante notare che le tecnologie di imaging che utilizzeremo non mostrano quasi mai "colori veri", ma applicano falsi colori generati appositamente per evidenziare le zone di interesse diagnostico.

A livello di gestione delle risorse, bisogna sempre ricordare che in informatica si ragiona rigorosamente per potenze del due. Se, ad esempio, richiediamo un'allocazione di 1000 byte, il sistema ce ne fornirà 1024, arrotondando automaticamente alla potenza del due più vicina utile a coprire la nostra richiesta.

Per i nostri scopi pratici, da questo momento in poi lavoreremo esclusivamente con immagini a livelli di grigio, che richiedono un solo byte per pixel. L'obiettivo finale sarà quello di creare un automatismo capace di caricare, salvare e manipolare immagini di cui non si conosce la dimensione a priori (una situazione estremamente comune per chi sviluppa software). Per superare questo ostacolo faremo ricorso alla memoria dinamica, che ci permetterà di allocare e utilizzare le risorse informatiche in modo opportuno e flessibile a seconda della reale quantità di memoria necessaria al momento dell'esecuzione.

Il caricamento di un'immagine in formato bitmap, tipicamente associata ai file con estensione .bmp, equivale a gestire una vera e propria mappa spaziale di pixel. All'interno di questa griglia, ogni singolo punto visivo viene codificato digitalmente impiegando un determinato numero di bit. Questa tipologia di formato è estremamente versatile e permette di rappresentare sia immagini in scala di grigi che immagini a colori, a seconda della quantità di informazioni che si decide di memorizzare.

Nel nostro contesto di lavoro specifico, ci concentreremo esclusivamente sulla gestione di immagini con una profondità di 8 bit. Questo significa che ogni singolo pixel della nostra immagine a livelli di grigio sarà codificato utilizzando esattamente 8 bit di memoria (ovvero 1 byte, come visto in precedenza). Di conseguenza, potremo rappresentare l'intensità luminosa di ogni pixel attraverso un valore numerico strettamente compreso tra 0 e 255, poiché matematicamente parlando abbiamo a disposizione 28=256 sfumature possibili per definire il passaggio dal nero assoluto al bianco puro.

Un concetto fondamentale da chiarire è la profondità di un'immagine, che indica il numero di bit necessari per codificare ogni singolo pixel dell'immagine stessa. Avendo ben chiara questa nozione, il nostro obiettivo finale pratico si delinea chiaramente: dobbiamo essere in grado di aprire un file, cercare le informazioni al suo interno ed elaborarle in modo efficace.

Per raggiungere questo traguardo, è essenziale comprendere la struttura interna di un file bitmap. Ogni immagine di questo tipo inizia con una specifica porzione di dati chiamata header, ovvero l'intestazione. In questa sezione preliminare non sono ancora presenti le informazioni visive vere e proprie; quindi, qui non troveremo i valori dei livelli di grigio. Si chiama intestazione proprio perché costituisce la parte iniziale del file e, nel formato standard di cui ci stiamo occupando per le immagini a 8 bit, occupa uno spazio di esattamente 1078 byte.

Questo blocco di 1078 byte è piuttosto capiente. In teoria, si avrebbe spazio a sufficienza persino per inserire informazioni testuali extra, come ad esempio i dati personali di un paziente. Considerando che per memorizzare una parola serve semplicemente un byte per ogni carattere ASCII (incluso lo spazio), la capienza dell'intestazione lo permetterebbe tranquillamente, anche se nella pratica comune dell'elaborazione standard questo non avviene quasi mai.

Il vero scopo di questi 1078 byte è, invece, quello di custodire le informazioni operative essenziali per descrivere le caratteristiche tecniche dell'immagine. Tra tutte queste, il dato principale e più prezioso per noi è la dimensione dell'immagine, espressa attraverso il numero di righe e di colonne. Recuperare questi due valori è un passaggio cruciale e ci ricollega a quanto detto in precedenza: solo conoscendo le dimensioni esatte della matrice sapremo quanta memoria dinamica richiedere al sistema operativo per poter allocare correttamente tutti i pixel e lavorare sull'immagine vera e propria.

Esempio

Prendiamo come esempio pratico una piccola immagine quadrata, nominata bn.bmp, formata da 4 righe e 4 colonne per un totale di 16 pixel di colore uniforme (completamente bianca). A livello di memoria, questo file occuperà i canonici 1078 byte dedicati all'header, a cui si sommeranno esattamente 16 byte necessari per memorizzare l'informazione dei livelli di grigio di ogni singolo pixel. Considerando il peso complessivo in megabyte, ci rendiamo conto che questo file è minuscolo: pesa poco più di un singolo kilobyte (ricordando che 1 KB equivale a 1024 byte).

b b b b
b b b b
b b b b
b b b b

Se applichiamo questo stesso ragionamento a un'immagine più grande, ad esempio da 256x256 pixel, avremo sempre i nostri 1078 byte fissi di intestazione, ma a questi andranno aggiunti circa 64 KB necessari per memorizzare l'intera matrice di pixel. Il peso complessivo del file si aggirerà quindi intorno ai 65 KB.

Un aspetto tecnico cruciale nella gestione di questi file riguarda l'allineamento in memoria. Il numero 1078 non è una potenza esatta del due e non è nemmeno un multiplo perfetto di 4. Per gestire questi disallineamenti ed evitare di lasciare spazi vuoti inutilizzati ("buchi"), il calcolatore effettua spesso un'operazione chiamata complementazione (o padding). Questo avviene anche quando i dati vengono salvati sull'hard disk, adattandosi a come il disco stesso è fisicamente suddiviso in settori.

Fino a questo momento abbiamo ipotizzato immagini ideali, le cui dimensioni di righe e colonne sono sempre multipli esatti di 4 (come le 4x4 o le 256x256). Tuttavia, nella realtà pratica, quando un medico ritaglia o segmenta un'immagine diagnostica, non si preoccupa certo che i lati siano multipli di 4. In questi casi interviene la complementazione per forzare l'allineamento allocando un po' di memoria in più: se si applicasse un arrotondamento per righe, un'immagine anomala da 13x17 pixel potrebbe essere trattata in memoria come una 16x17; se si arrotondasse per colonne, la stessa immagine verrebbe gestita come una 13x20.

Tornando alla struttura fisica del nostro file 4x4 ideale e tutto bianco (dove ogni pixel assume il valore massimo di 255), l'organizzazione dei dati segue un ordine rigoroso. I primi 1078 byte, partendo dal byte zero fino al byte 1077, contengono esclusivamente l'header. Subito dopo, a partire dal byte 1078 in poi, il computer inizia a scrivere sequenzialmente, byte per byte, i livelli di grigio della nostra matrice visiva. Ovviamente, per far sì che il software sappia di dover leggere una griglia esattamente di 4x4, l'informazione relativa al numero di righe e colonne deve essere dichiarata a priori, e viene scritta sempre in posizioni fisse e standardizzate all'interno di quell'intestazione iniziale.

Come funziona esattamente l’header?

Per capire come funziona esattamente l'header, è utile immaginare il file salvato sull'hard disk come un supporto continuo, molto simile a un vecchio nastro magnetico che possiede un punto di inizio e che può essere scorso linearmente fino alla fine. Quando apriamo un file, il sistema posiziona una "testina di lettura" virtuale (il puntatore del file) esattamente all'inizio del nastro. Da questa posizione zero, la testina procede solitamente in avanti leggendo i dati in sequenza.

Tuttavia, un vantaggio fondamentale della gestione dei file è la possibilità di far compiere a questa testina dei veri e propri salti. Non siamo obbligati a leggere tutto il contenuto passo dopo passo; possiamo invece istruire il nostro programma per spostare il cursore direttamente all'indirizzo specifico in cui sappiamo trovarsi l'informazione desiderata, ottimizzando così i tempi di lettura.

All'interno del nostro pacchetto di 1078 byte, le dimensioni dell'immagine sono salvate seguendo un ordine rigoroso: viene registrato prima il numero delle colonne (ovvero la larghezza) e subito dopo il numero delle righe (l'altezza). Ognuna di queste due informazioni chiave richiede uno spazio di memoria pari a 4 byte consecutivi. Per poter estrarre questi dati e utilizzarli per allocare dinamicamente la nostra matrice, ci basterà ordinare alla testina di lettura di saltare direttamente al byte 18. Nello specifico, il blocco che va dal byte 18 al byte 21 contiene il numero delle colonne, mentre i successivi byte che vanno dal 22 al 25 custodiscono il numero delle righe.

Un'ulteriore particolarità da tenere a mente quando si lavora con questi formati riguarda l'ordine in cui vengono memorizzati i pixel veri e propri (i livelli di grigio) subito dopo l'header. Nello standard bitmap, chi ha progettato il formato ha deciso di non seguire la nostra logica di lettura visiva abituale, che partirebbe dall'alto scendendo per righe. Al contrario, l'informazione viene storicamente organizzata a partire dal basso verso l'alto (una struttura nota come bottom-up).

Colonne Righe

L'intestazione del file non è un blocco unico, ma è divisa in tre "pacchettini" logici. Per far leggere questa struttura a un programma, dobbiamo dire al computer esattamente quanti byte leggere e in quale ordine. Ciascuno di questi campi viene denominato in un certo modo proprio perché ci si ricordi di che cosa stiamo parlando.

  1. Primo pacchettino: File Header (14 byte) Contiene le informazioni base del file.
    • Byte 0-1 (2 byte): La firma del file (di solito i caratteri "B" ed "M").
    • Byte 2-5 (4 byte): La dimensione totale del file.
    • Byte 6-9 (4 byte): Spazio riservato (di solito zeri).
    • Byte 10-13 (4 byte): L'Offset, ovvero l'indirizzo esatto in cui finisce tutto l'header e iniziano i pixel veri e propri (nel nostro caso, varrà proprio 1078).
  2. Secondo pacchettino: Info Header (40 byte) Contiene i dettagli geometrici dell'immagine. È qui che troviamo righe e colonne!
    • Byte 14-17 (4 byte): La dimensione di questo pacchettino (40 byte).
    • Byte 18-21 (4 byte): Le colonne (larghezza dell'immagine).
    • Byte 22-25 (4 byte): Le righe (altezza dell'immagine).
    • Byte 26-27 (2 byte): Piani colore (vale sempre 1).
    • Byte 28-29 (2 byte): Profondità di colore (nel nostro caso varrà 8, a indicare gli 8 bit).
    • ...e altri 24 byte per informazioni su compressione e risoluzione.
  3. Terzo pacchettino: Color Palette (1024 byte) La tabella che associa i 256 valori possibili (da 0 a 255) a specifiche sfumature di grigio.

Tutto ciò che abbiamo descritto fino ad ora riguarda l'organizzazione fisica dei dati all'interno del file binario salvato sull'hard disk. Adesso, però, dobbiamo trasferire questi concetti nella logica del nostro codice in C++. L'obiettivo pratico è scrivere un programma capace di aprire il file, mappare queste informazioni fisiche e salvarle in apposite variabili all'interno della memoria del computer.

Il primo passo che il nostro codice dovrà compiere è aprire il file e istruire la testina di lettura per farle compiere quel famoso salto fino al byte 18, dove risiede il numero delle colonne della matrice. Per gestire queste operazioni si utilizzano specifiche funzioni del C++ dedicate ai file binari, le quali possiedono una caratteristica comportamentale fondamentale: l'avanzamento automatico. Ogni volta che il programma esegue un'istruzione di lettura, il puntatore interno (la nostra "testina") non resta fermo, ma si sposta in automatico da sinistra verso destra di una quantità di byte esattamente pari al campo di dati appena letto.

Facciamo un calcolo esatto per visualizzare il meccanismo in azione. Se posizioniamo il cursore al byte 18 e gli diciamo di leggere un blocco di 4 byte (ovvero i byte 18, 19, 20 e 21), al termine della lettura la testina si posizionerà automaticamente all'inizio del byte 22 (e non al 23, ricordando che il byte 18 fa parte dei quattro byte letti). Questo comportamento del linguaggio è estremamente comodo, perché ci prepara già il terreno per il passaggio successivo senza dover calcolare un nuovo salto.

Sfruttando questo automatismo, la sequenza delle nostre istruzioni diventa molto fluida: effettuiamo la prima lettura di 4 byte a partire dal byte 18 per estrarre la larghezza (le colonne, C); la testina avanza da sola al byte 22; a questo punto ci basta eseguire un'identica istruzione di lettura per altri 4 byte e acquisiamo immediatamente l'altezza (le righe, R). Così facendo, rispettiamo perfettamente l'ordine in cui i dati sono stati impacchettati nell'intestazione del file bitmap.

Allocazione dinamica della matrice

Dopo aver compreso come estrarre le dimensioni dell'immagine dall'header, possiamo tornare a concentrarci sull'allocazione dinamica della memoria, affrontando il passaggio verso una struttura dati più complessa. Finora, infatti, abbiamo considerato soltanto l'allocazione di un semplice vettore monodimensionale. In quello scenario di base, il procedimento operativo consiste nel dichiarare una variabile di tipo puntatore (come ad esempio un puntatore a intero, indicato sintatticamente con int *p) e, successivamente, assegnare a tale puntatore un blocco di memoria adeguato. Questa assegnazione avviene tipicamente richiedendo spazio al sistema operativo tramite una chiamata alla funzione malloc nel linguaggio C, oppure sfruttando l'operatore new se si sta programmando in C++.

Il nostro nuovo obiettivo pratico è prendere questa logica, perfettamente rodata per la singola dimensione, ed estenderla per gestire il caso bidimensionale. In altre parole, dobbiamo capire come manipolare i puntatori e le funzioni di allocazione per creare un'impalcatura informatica in grado di ospitare non più una semplice lista di dati, ma un'intera matrice composta da righe e colonne, esattamente ciò che ci serve per mappare e caricare in memoria tutti i pixel della nostra immagine.

Per la matrice

Per il vettore

int *p; int **P;

p = P =

Qui è contenuto qualcosa che è funzione del numero di byte che voglio leggere e di quante sono le componenti.

for (i=0; i<R; i++) {

}

La variabile puntatore p svolge un ruolo cruciale: funge da vero e proprio ponte di controllo tra due aree di memoria ben distinte. Da una parte abbiamo l'area statica (lo stack, dove risiede fisicamente la variabile del puntatore), e dall'altra l'area dinamica (l'heap, dove vengono effettivamente stivati i dati). Affinché questo meccanismo funzioni correttamente nel caso di un vettore, il sistema deve esplorare l'heap fino a trovare un blocco di byte rigorosamente adiacenti e contigui, grande abbastanza da soddisfare la nostra richiesta di spazio.

Passando da un vettore monodimensionale a una matrice, questa logica di base rimane valida ma subisce una necessaria evoluzione strutturale. La nostra variabile di controllo diventa un puntatore a puntatore, indicato dalla sintassi int **P. L'aggiunta di una seconda dimensione richiede un livello di logica in più nel codice: l'allocazione non avviene più in un singolo passaggio, ma deve essere obbligatoriamente spezzata in due fasi distinte.

Prendiamo come esempio pratico la volontà di allocare una matrice di dimensioni 2x4. Il primo passo consiste nell'allocare dinamicamente quello che possiamo immaginare come un "vettore colonna" (o vettore principale). Questo vettore speciale non conterrà i numeri veri e propri della nostra immagine, ma avrà un numero di scomparti esattamente pari al numero delle righe della matrice (in questo caso, 2). O

Anteprima
Vedrai una selezione di 5 pagine su 20
Lezione 5 Sistemi per la riabilitazione e la terapia assistita Pag. 1 Lezione 5 Sistemi per la riabilitazione e la terapia assistita Pag. 2
Anteprima di 5 pagg. su 20.
Scarica il documento per vederlo tutto.
Lezione 5 Sistemi per la riabilitazione e la terapia assistita Pag. 6
Anteprima di 5 pagg. su 20.
Scarica il documento per vederlo tutto.
Lezione 5 Sistemi per la riabilitazione e la terapia assistita Pag. 11
Anteprima di 5 pagg. su 20.
Scarica il documento per vederlo tutto.
Lezione 5 Sistemi per la riabilitazione e la terapia assistita Pag. 16
1 su 20
D/illustrazione/soddisfatti o rimborsati
Acquista con carta o PayPal
Scarica i documenti tutte le volte che vuoi
Dettagli
SSD
Scienze mediche MED/34 Medicina fisica e riabilitativa

I contenuti di questa pagina costituiscono rielaborazioni personali del Publisher rebeccasanteramo di informazioni apprese con la frequenza delle lezioni di Sistemi per la riabilitazione e la terapia assistita e studio autonomo di eventuali libri di riferimento in preparazione dell'esame finale o della tesi. Non devono intendersi come materiale ufficiale dell'università Politecnico di Bari o del prof Albino Vito.
Appunti correlati Invia appunti e guadagna

Domande e risposte

Hai bisogno di aiuto?
Chiedi alla community