Negli esempi che abbiamo visto fino ad ora relativi all’uso delle immagini all’interno di Processing abbiamo sempre impostato a priori la grandezza della finestra del nostro sketch in base alla dimensione dell’immagine caricata.

È un metodo molto semplice ma decisamente scomodo se dobbiamo utilizzare tante immagini diverse oppure se vogliamo rendere il nostro programma universale.

Nella versione precedente di Processing era possibile utilizzare delle variabili all’interno della funzione size(). Questa opzione, però, benché fortemente osteggiata dagli sviluppatori fin dal 2009 è stata definitivamente rimossa con l’aggiornamento a Processing 3 perché impediva miglioramenti in termini di performance, velocità e compatibilità cross-platform.

Non disperate! Esiste una soluzione alternativa che, però, non è ben descritta nel reference del linguaggio. Ecco perché ho pensato fosse interessante scrivere un breve articolo a riguardo.

Ecco come fare:

  • All’interno di setup() è comunque necessario indicare una dimensione di partenza con la funzione size(). Per semplicità possiamo scrivere: size(1, 1);
  • Dopodiché aggiungiamo la seguente linea di codice: surface.setResizable(true); mi raccomando, fate attenzione alle maiuscole!
  • A questo punto, nel punto in cui abbiamo la necessità di reimpostare la dimensione è sufficiente scrivere: surface.setSize(larghezza, altezza); dove larghezza altezza sono i nostri parametri.

Ecco un esempio con un’immagine:

/*
 * Modificare la dimensione della finestra in base all'immagine caricata
 * Federico Pepe, 25.06.2017
 * http://blog.federicopepe.com/processing
*/

PImage img;

void setup() {
  size(1, 1);
  surface.setResizable(true);
  img = loadImage("immagine.jpg");
  surface.setSize(img.width, img.height);  
}

void draw() {
}

Per cambiare la dimensione a ogni click del mouse:

/*
 * Modificare la dimensione della finestra in base all'immagine caricata
 * Federico Pepe, 25.06.2017
 * http://blog.federicopepe.com/processing
*/

void setup() {
  size(100, 100);
  surface.setResizable(true);
}

void draw() {
}

void mousePressed() {
  surface.setSize(round(random(100, 500)), round(random(100, 500)));  
}

Inauguro oggi con questo post una rubrica chiamata Coding Rescue ovvero: come risolvere problemi di programmazione che mi vengono posti dai lettori del blog. Ogni tanto capita che qualcuno mi scriva privatamente perché è rimasto bloccato con un esercizio in Processing che non riesce a risolvere e mi viene chiesto di dare una mano a trovare una soluzione.

Mi piace ricevere questo genere di messaggi perché mi permette di scontrarmi con difficoltà a cui non avevo mai pensato. Dal momento che capita a tutti – sottoscritto compreso – di rimanere bloccati per giorni, penso che aiutare le persone sia un ottimo modo per allenarsi e imparare qualcosa di nuovo.

Il problema

Qualche giorno fa ho ricevuto il seguente messaggio sulla mia pagina Facebook:

creare un progettino, ovvero una funzione, che inverte (sotto sopra) l’immagine, solo all’interno di un quadrato che si crea intorno al puntatore del mouse.

Il problema è interessante perché pone una serie di sfide che, sommate, non sono per nulla semplici:

  1. Dobbiamo lavorare con un’immagine
  2. Lo script deve essere dinamico perché in relazione alla posizione del puntatore del mouse
  3. Dobbiamo invertire sotto sopra una porzione di un’immagine

Come consiglio sempre: è importante concentrarsi sui vari punti uno per volta perché, altrimenti, si rischia di non arrivare facilmente alla soluzione.

La soluzione in pseudo-codice

Prima di cominciare a scrivere il codice, proviamo a descrivere il funzionamento del programma a parole; ho sottolineato con il corsivo le parole chiave: il nostro sketch dovrà, all’avvio, caricare un’immagine. Intorno alla posizione del mouse disegneremo un quadrato e, una volta analizzati i pixel presenti all’interno di questo quadrato, creeremo e disegneremo una nuova immagine che avrà dimensione pari a quella del quadrato i cui pixel, rispetto all’originale, saranno invertiti sotto-sopra.

La soluzione: passo dopo passo

1. Carichiamo l’immagine

Per prima cosa dobbiamo ripassare come si lavora sulle immagini:

Per risolvere il problema avremo bisogno di due oggetti immagine: il primo sarà l’immagine originale mentre, la seconda, sarà l’immagine con i pixel invertiti che chiamerò flipped. Avremo inoltre bisogno di impostare una variabile che determinerà la larghezza del quadrato e, di conseguenza, la grandezza dell’immagine che andrò a creare. Per questo esercizio, ho deciso di usare la foto di un corgi.

Nella funzione draw, per il momento, mi preoccupo solo di disegnare un quadrato intorno alla posizione del mouse.

/*
 * Coding Rescue #1 - Invertire sotto-sopra una porzione di un'immagine
 * Federico Pepe, 11.06.2017
 * http://blog.federicopepe.com/processing
 */

// Creo i due oggetti immagine
PImage img, flipped;

// Variabile che determina la larghezza del quadrato
int w = 100;

void setup() {
  size(700, 542);
  // Carico l'immagine originale e la disegno
  img = loadImage("corgi-photo.jpg");
  image(img, 0, 0);
  // Creo l'immagine flipped
  flipped = createImage(w, w, RGB);
  noFill();
}

void draw() {
  image(img, 0, 0);

  rectMode(CENTER);
  rect(mouseX, mouseY, w, w);
}

2. Carichiamo i pixel di riferimento in un array

Il prossimo passaggio sarà analizzare i pixel all’interno del quadrato disegnato e inserirli uno per uno all’interno di un array: avendo come riferimento la posizione del mouse e volendo tenere quel punto al centro del quadrato creo quattro variabili: startX, startY, endX ed endY che mi serviranno come riferimento per i cicli for.

Invece di utilizzare un array classico, decido di usare una variabile di tipo IntList questo perché si tratta di un tipo di dato più flessibile che gestisce gli elementi al suo interno in modo più dinamico e rapido. Operazioni quali l’aggiunta, la modifica, la lettura dei dati al suo interno sono più veloci e semplici da eseguire rispetto ai comuni array.

/*
 * Coding Rescue #1 - Invertire sotto-sopra una porzione di un'immagine
 * Federico Pepe, 11.06.2017
 * http://blog.federicopepe.com/processing
 */

// Creo i due oggetti immagine
PImage img, flipped;

// Variabile che determina la larghezza del quadrato
int w = 100;

// Variabile nella quale inserirò tutti i pixel analizzati
IntList arrayPixel = new IntList();

void setup() {
  size(700, 542);
  // Carico l'immagine originale e la disegno
  img = loadImage("corgi-photo.jpg");
  image(img, 0, 0);
  // Creo l'immagine flipped
  flipped = createImage(w, w, RGB);
  noFill();
}

void draw() {
  image(img, 0, 0);
  
  int startX = mouseX-w/2;
  int startY = mouseY-w/2;
  int endX = mouseX + w/2;
  int endY = mouseY + w/2;
  
  img.loadPixels();
  
  for (int y = startY; y < endY; y++) {
    for (int x = startX; x < endX; x++) {
      arrayPixel.append(get(x, y));
    }
  }
  
  rectMode(CENTER);
  rect(mouseX, mouseY, w, w);
}

Problema:

In questo momento ad ogni iterazione di draw, la variabile arrayPixel, grazie alla funzione append(), viene ingrandita di 10.000 valori (100*100). Per vedere cosa accade, provate ad aggiungere: println(arrayPixel.size());. Se provassimo a disegnare un'immagine con una dimensione variabile, il programma andrebbe in crash. È importante, dunque, svuotare l'array prima dei cicli for con la funzione clear()arrayPixel.clear();

3. Aggiungiamo i pixel analizzati nell'immagine di destinazione

Ora che i pixel contenuti nel quadrato di riferimento sono stati aggiunti all'array dobbiamo trasferirli tutti all'interno della nostra immagine di destinazione che abbiamo creato in precedenza.

Carichiamo, quindi, i pixel dell'immagine di destinazione e, attraverso un ciclo for, assegniamo a ogni pixel il pixel relativo presente all'interno dell'array:

flipped.loadPixels();
for (int i = 0; i < arrayPixel.size(); i++) {
  flipped.pixels[i] = arrayPixel.get(i);
}
flipped.updatePixels();

Aggiungendo image(flipped, 0, 0); alla fine del ciclo draw e cliccando su Run dovreste vedere in alto a sinistra un quadrato di 100 pixel contenente la stessa immagine del quadrato di riferimento disegnato intorno alla posizione del mouse.

Ora non ci resta che completare il lavoro.

4. Invertire l'immagine di destinazione sotto-sopra

Mentre lavoravo all'esercizio, arrivato a questo punto mi sono bloccato. Per invertire un'immagine sottosopra avrei dovuto modificare l'ultimo ciclo for scritto per fare in modo che i pixel venissero scritti nell'immagine di destinazione invertiti rispetto all'originale. Su questa cosa ho sbattuto la testa per qualche giorno poi è arrivata l'illuminazione: anziché andare a complicarmi la vita con i pixel, avrei potuto usare le funzioni di trasformazione.

In particolare, dopo aver fatto una veloce ricerca, se passiamo valori negativi alla funzione scale(), otteniamo l'immagine invertita.

pushMatrix();
translate(startX, startY+w);
scale(1, -1);
image(flipped, 0, 0);
popMatrix();

Ecco che con queste poche righe di codice, siamo giunti alla fine: se non vi ricordate come funzionano, potete ripassare pushMatrix() e popMatrix() a questo indirizzo.

Risultato finale:


Quick, Draw! in Processing

A fine 2016 Google ha messo on-line Quick, Draw! uno dei suoi esperimenti di intelligenza artificiale e machine learning. Il sito chiede alle persone di disegnare delle forme: un maiale, un tubo per annaffiare il prato, una padella, ecc… e in meno di 20 secondi, attraverso una rete neurale, il computer prova a indovinare cosa è stato disegnato.

Più di 15 milioni di persone hanno partecipato al gioco e, così facendo, Google è riuscita a raccogliere un dataset di più di 50 milioni di disegni che, qualche giorno fa, è stato rilasciato pubblicamente.

Il dataset si compone di 345 categorie e, oltre ai dati vettoriali delle immagini sono inclusi anche una serie di metadati.

L’obiettivo del rilascio di questi dati è:

We’re sharing them here for developers, researchers, and artists to explore, study, and learn from.

Nel giro di poche ore sul mio feed twitter sono comparsi i primi esperimenti (i più interessanti sono quelli di @frauzufall che trovate qui) la maggior parte dei quali, però, generati con OpenFrameworks.

Non trovando nessuno che stesse utilizzando Processing, ho deciso di scrivere un piccolo programma in questo linguaggio per dare la possibilità a tutti di utilizzare questo dataset

Struttura dei dati

I file che si scaricano dal repository rilasciato da Google sono file di tipo .ndjson ovvero file con un oggetto di tipo JSON per ogni riga di file.

Ciascun oggetto è composto dai seguenti dati:

  • key_id (integer): numero univoco che identifica il disegno
  • word (string): la categoria che era stata richiesta
  • recognized (boolean): se il disegno era stato riconosciuto (true) oppure no (false)
  • timestamp (datetime): quando il disegno è stato creato
  • countrycode (string): due lettere per identificare la nazione del giocatore
  • drawing (string): un array JSON contenente i dati vettoriali del disegno
{ 
    "key_id":"5891796615823360",
    "word":"nose",
    "countrycode":"AE",
    "timestamp":"2017-03-01 20:41:36.70725 UTC",
    "recognized":true,
    "drawing":[[[129,128,129,129,130,130,131,132,132,133,133,133,133,...]]]
  }

Per semplificarmi la vita, mi sono concentrato solo sull’ultimo tipo di dato: il mio obiettivo era caricare i dati del dataset e ridisegnare le forme con Processing.

L’array contenente i dati del disegno è a sua volta strutturato in questo modo:

[ 
  [  // First stroke 
    [x0, x1, x2, x3, ...],
    [y0, y1, y2, y3, ...],
    [t0, t1, t2, t3, ...]
  ],
  [  // Second stroke
    [x0, x1, x2, x3, ...],
    [y0, y1, y2, y3, ...],
    [t0, t1, t2, t3, ...]
  ],
  ... // Additional strokes
]

Per fortuna la documentazione di Google è molto chiara: x e y sono le coordinate e t è il tempo trascorso in millisecondi dal primo punto.

Dati preprocessati

I dati contenuti nel dataset sono molti e, infatti, ciascun file pesa diverse centinaia di megabyte. Per fortuna il team di Google ha messo a disposizione anche dei file preprocessati: sono state rimosse le informazioni temporali e tutti i dati sono stati allineati e ridimensionali in un quadrato di 256×256 pixel.

I file .ndjson prepocessati sono disponibili a questo link e, come potete notare, la dimensione è notevolmente ridotta anche se si tratta, pur sempre, di almeno 40-50 megabyte per file.

Importare i dati in Processing

Primo problema: in Processing non esistono funzioni che ci permettono di lavorare su oggetti di tipo JSON differenti a meno che non siano inclusi in un array. All’inizio avevo, dunque, optato per modificare il file dei dati ma, grazie al suggerimento di un utente su Facebook, ho scoperto l’esistenza dell’oggetto BufferedReader che si usa per leggere un file una riga alla volta.

Una volta risolto questo inghippo, tutto è diventato più semplice: le linee restituite dall’oggetto sono di tipo String. Con la funzione parseJSONObject() ho convertito la stringa in un oggetto JSON.

Quick, Draw!

Quick, Draw: il dataset di Google in Processing
Vorrei conoscere chi ha disegnato questa “incudine”

Avendo accesso all’oggetto JSON, l’unico passaggio da fare era estrarre dall’array drawing i dati x e y e assegnarli ai vertici di una forma per poterla disegnare a schermo.

Per farli ho usato un’insieme di funzioni di cui parleremo in futuro.

Per i più curiosi, il codice è disponibile su questo repository su GitHub.


In questo post vedremo come unire le nozioni che abbiamo appreso relativamente al caricamento e all’ utilizzo delle immagini in Processing e alle possibilità creative che abbiamo a disposizione lavorando sui singoli pixel per imparare a processore le immagini a nostro piacimento.

Riprendiamo l’immagine del gatto che abbiamo già usato in precedenza e carichiamola all’interno di un nuovo sketch. Invece di utilizzare la funzione image() per mostrarla nella finestra, questa volta andremo a caricare tutti i pixel che compongono l’immagine.

/*
 * Processare le immagini in Processing
 * Federico Pepe, 29.04.2017
 * http://blog.federicopepe.com/processing
 */

PImage img;

void setup() {
  size(640, 536);
  img = loadImage("cat-300572_640.jpg");
}

void draw() {
  // Carichiamo i pixel della finestra
  loadPixels();
  // Carichiamo i pixel dell'immagine
  img.loadPixels();
  for(int y = 0; y < height; y++) {
    for(int x = 0; x < width; x++) {
      int pos = x + y * width;
      
      float r = red(img.pixels[pos]);
      float g = green(img.pixels[pos]);
      float b = blue(img.pixels[pos]);
      
      pixels[pos] = color(r, g, b);
    }
  }
  updatePixels();
}

Analizziamo velocemente il codice qui sopra concentrandoci, in particolare, sulle cose che non abbiamo mai visto prima: come indicato nei commenti, oltre a caricare i pixel della finestra dobbiamo caricare anche quelli relativi all'immagine. Per farlo utilizziamo img.loadPixels(); dove img fa riferimento al nome della variabile che abbiamo dichiarato all'inizio del programma.

Con due loop for andiamo a caricare ogni singolo pixel e, utilizzando le funzioni red()green()blue() otteniamo i valori R, G e B. Attraverso pixels[pos] = color(r, g, b); assegniamo i valori al pixel sullo schermo.

Il nostro programma, in pratica, lavora come segue: che valori di rossoverdeblu ha il pixel nell'immagine che ha coordinate x = 0, y = 0? Una volta ottenuti, applica quei valori al pixel con posizione x = 0 e y = 0 della finestra. Dopodiché, il ciclo for prosegue e analizzerà il pixel con coordinate x = 1 e y = 0 e avanti così.

Con l'updatePixel() finale aggiorniamo tutti i valori presenti nell'array della finestra. Il risultato finale sarà, dunque, vedere visualizzata sullo schermo l'immagine.

La domanda che sorge spontanea è: perché dovrei scrivere tutte queste righe di codice quando avrei potuto usare la funzione image() e risparmiare un sacco di fatica? La risposta è semplice: avendo accesso ai dati grezzi dell'immagine possiamo modificarli a nostro piacimento.

Proviamo, ad esempio, ad aggiungere la riga in grassetto al nostro codice:

r = constrain(r, 0, 100);
 
pixels[pos] = color(r, g, b);

Il risultato sarà che il valore del rosso non potrà avere un valore compreso tra 0 e 255 come previsto normalmente ma sarà limitato a valori compresi tra 0, 100 grazie alla funzione constrain.

Processare le immagini in Processing

Siamo riusciti a creare il nostro primo filtro personalizzato per le immagini. Ora possiamo dare sfogo alla nostra fantasia.

Facciamo un altro esperimento: rimpiazziamo l'ultima riga di codice che abbiamo aggiunto con quella seguente:

r = map(mouseX, 0, width, 0, 255);

Il rosso ora è controllato dalla posizione x del mouse.

Usare un solo ciclo for

Prima di concludere questo post ci tengo a fare una precisazione: volendo è possibile utilizzare un solo ciclo for per ottenere lo stesso effetto riducendo, così, le righe di codice del nostro programma:

for(int i = 0; i < pixels.length; i++)

Se decidiamo di intraprendere questa strada dobbiamo eliminare la variabile pos e sostituirla con i:

float r = red(img.pixels[i]);

Come avevamo discusso nel post relativo all'array di pixel la differenza tra i due approcci dipende se consideriamo l'immagine un array monodimensionale o bidimensionale.


Avevo parlato di pixel in uno dei primissimi post su Processing in questo blog. Grazie alle competenze che abbiamo acquisito nelle ultime settimane, possiamo fare un ulteriore passo in avanti.

Le funzioni che abbiamo usato fino a oggi ci hanno permesso di disegnare linee e forme sullo schermo o, come visto di recente, di mostrare un’immagine. Queste funzioni che, all’apparenza, sembrano eseguire operazioni molto semplici, in realtà nascondono un principio molto complesso: stabilire se ciascun pixel sullo schermo deve essere accesso o spento e, se acceso, il colore che deve rappresentare.

Array di pixel

Siamo abituati a pensare ai pixel come ad una griglia sullo schermo; quindi ad un array bidimensionale.

In Processing, però, il valore di ciascuno di essi viene salvato in un classico array monodimensionale.

È possibile accedere a queste informazioni e modificarle a nostro piacimento? Ovviamente si, utilizzando le funzioni loadPixels()updatePixels().

loadPixels() e updatePixels()

Con la prima funzione stiamo dicendo a Processing che intendiamo lavorare sull’array di pixel e che, quindi, deve caricarlo in una variabile. updatePixels(), invece, la usiamo quando abbiamo apportato tutte le modifiche e vogliamo che il programma aggiorni le informazioni sullo schermo.

Facciamo un esempio pratico:

/*
 * Array di pixel: loadPixels() e updatePixels()
 * Federico Pepe, 26.03.2017
 * http://blog.federicopepe.com/processing
 */

void setup() {
  size(500, 500);
  loadPixels();  
 
  for (int i = 0; i < pixels.length; i++) {
    float rand = random(255);
    color c = color(0, 0, rand);
    pixels[i] = c;
  }
  
  updatePixels();
}

In questo programma carichiamo tutti i valori di una finestra di 500x500 pixel. Con il ciclo for partiamo dal primo valore presente nel nostro array e arriviamo fino all'ultimo, identificato, per comodità, dalla funzione pixel.length e assegniamo a ciascuno un colore casuale float rand = random(255); nella scala dei blu color c = color(0, 0, rand);.

Assegniamo il nuovo colore al pixel pixels[i] = c; e, una volta usciti dal loop, chiediamo a Processing di aggiornare e mostrare tutte le modifiche che abbiamo effettuato updatePixels();.

Il risultato sarà il seguente:

Pixel Array Blu

Modificare un pixel conoscendone la posizione x e y

A questo punto, immagino, potrà esservi sorta spontaneamente una domanda: è possibile accedere a un determinato pixel conoscendone la posizione x e y all'interno della finestra come siamo stati abituati a fare fino a ora?

Riprendendo l'immagine della griglia più sopra, proviamo a capire come accedere al pixel 19: la sua posizione è x = 5, y = 2. A questo punto è importante ricordare che, per questi valori, partiamo a contare da 0 mentre, per la larghezza della finestra, da 1.

Ora dobbiamo sommare il valore di x al valore di y moltiplicato alla larghezza.

La formula è, dunque: x + (y * width)

5 + (2 * 7) = 19

I conti tornano!

Proviamo a modificare il codice di prima come segue:

/*
 * Array di pixel: loadPixels() e updatePixels()
 * Federico Pepe, 26.03.2017
 * http://blog.federicopepe.com/processing
 */

void setup() {
  size(500, 500);
  loadPixels();  
  color c;
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      float rand = random(255);
      int pos = x + y * width;
      if(x % 2 == 0) {
        c = color(0, 0, rand);
      } else {
        c = color(rand, 0, 0);
      }
      pixels[pos] = c;
    }
  }
  
  updatePixels();
}

Zoomando l'immagine noterete che una riga è rossa e una riga è, invece, blu. Il prossimo passo sarà applicare quello che abbiamo appena imparato su un'immagine. Provate a pensare a tutte le operazioni che potremmo compiere andando a modificare individualmente ogni singolo pixel.


Proseguiamo l’approfondimento su Processing e immagini. Nell’era di Instagram siamo abituati a cambiare l’aspetto delle nostre foto prima di condividerle con gli altri. Le funzioni tint()filter() – già incluse all’interno del linguaggio – ci consentono di alterare le immagini a nostro piacimento.

La funzione tint()

Questa funzione consente di alterare il colore generale delle foto. L’effetto non è permanente tanto che invocando la funzione noTint() è possibile ritornare all’originale.

I parametri accettati vanno da uno a quattro come specificato nel reference. Prendendo quest’immagine di partenza –chi non ama i gatti? – facciamo qualche esempio:

Immagine di partenza, presa da Pixabay (CC)
Passiamo alla funzione un solo parametro per applicare un filtro “grigio”:

/*
 * Immagini: tint(), filter() e masking
 * Federico Pepe, 19.03.2017
 * http://blog.federicopepe.com/processing
*/

PImage immagine;

void setup() {
  size(640, 535);
  immagine = loadImage("cat.jpg");
  tint(100);
  image(immagine, 0, 0);
}

void draw() {
 
}

Passiamo a tint() tre parametri: tint(255, 255, 50);. È possibile usare tre valori RGB che HSB in base alla funzione colorMode() precedentemente impostata.

Processing use tint()

Se avete un background in web design, sappiate che Processing accetta anche valori esadecimali tint(#FF55EE);

Processing tint hexadecimal

È possibile anche sfruttare il parametro della trasparenza alpha.

tint(#FF55EE, 70);
image(immagine, 0, 0);
tint(255, 255, 50, 50);
image(immagine, 0, 0);

Questo codice darà il seguente risultato:

Processing tint e trasparenza

Aggiungere un filtro con la funzione filter()

Oltre alla funzione tint(), Processing ha al suo interno la funzione filter() che, come i comuni programmi di grafica come Adobe Photoshop o GIMP, permette di applicare dei filtri alle foto.

I parametri sono otto e devono essere sempre indicati in maiuscolo: THRESHOLD, GRAY, INVERT, POSTERIZE, BLUR, OPAQUE, ERODE e DILATE. Alcuni di essi accettano un secondo parametro mentre altri no.

Mentre la funzione tint() doveva essere dichiarata prima di mostrare l’immagine, la funzione filter() è inserita successivamente, come indicato nel seguente esempio:

/*
 * Immagini: tint(), filter() e masking
 * Federico Pepe, 19.03.2017
 * http://blog.federicopepe.com/processing
*/

PImage immagine;

void setup() {
  size(640, 535);
  immagine = loadImage("cat.jpg");
  tint(100);
  image(immagine, 0, 0);
}

void draw() {
 
}

THRESHOLD

Converte i pixel di un’immagine in bianco o nero in base alla soglia impostata che può assumere un valore compreso tra 0 e 1. Se non viene indicato, il valore di default è 0.5.

BLUR

Applica un filtro di sfocatura gaussiana. Il secondo parametro indica il raggio della sfocatura.

POSTERIZE

Limita ciascun canale dell’immagine al numero di colori passati come parametro. I valori accettati vanno da 2 a 255.

GRAY

Converte i colori dell’immagine nel loro valore corrispondente in scala di grigi.

OPAQUE

Imposta il canale trasparenza (alpha) opaco.

INVERT

Inverte il valore di ciascun pixel.

ERODE e DILATE

Il primo riduce le aree illuminate mentre il secondo le aumenta.

Le funzioni che abbiamo visto oggi sono solo l’inizio; prossimamente vedremo come creare i nostri filtri personalizzati andando a lavorare sui singoli pixel.


In tutti gli sketch che abbiamo creato fino a oggi abbiamo sempre utilizzato Processing per generare forme sullo schermo. Con questo post, invece, cominceremo ad ampliare i nostri orizzonti e inizieremo a utilizzare nei nostri programmi immagini esterne.

A differenza dei programmi di editing più comuni – penso per esempio a Photoshop o Pixelmator – con Processing possiamo lavorare in modo più preciso e dettagliato su ogni singola foto andando a regolare, a nostro piacimento, ogni pixel.

Le immagini sono dati

Le immagini digitali non sono altro che un insieme di dati, ovvero numeri. Per cercare di spiegarlo nel modo più semplice possibile, ciascun dato rappresenta i valori di rosso, verde e blu (RGB) di un punto preciso all’interno di una griglia.

Di fatto possiamo immaginare ogni foto come una griglia di pixel dove ciascun pixel avrà diversi valori di rosso, verde e blu. Con la programmazione e un po’ di fantasia possiamo avere accesso a tutti questi dati e usarli a nostro piacimento.

PImage, loadImage() e image()

Abbiamo già parlato di tipi di dati e classi. Processing ha al suo interno la classe PImage la cui funzione principale è quella di permetterci di caricare e visualizzare un’immagine.

La funzione loadImage(), che accetta un parametro di tipo String, carica il file all’interno della memoria. Nell’esempio riportato in seguito, il parametro passato alla funzione è il nome del file comprensivo di estensione. La funzione andrà a cercare se quel file è presente all’interno della sottocartella data presente all’interno del nostro sketch.

Per chi non lo ricordasse, in questo post abbiamo già trattato come caricare file esterni all’interno del nostro sketch in Processing.

Mettiamo tutto insieme:

/*
 * Utilizzare immagini in Processing: PImage, loadimage() e image()
 * Federico Pepe, 12.03.2017
 * http://blog.federicopepe.com/processing
*/

PImage immagine;

void setup() {
  size(500, 500);
  immagine = loadImage("logo_federicopepe.png");
}

void draw() {
  image(immagine, 0, 0);
}

Ho creato una variabile di tipo PImage chiamata immagine; attraverso la funzione loadImage() carico il file chiamato logo_federicopepe.png che, in precedenza, avevo già spostato all’interno della cartella data e, infine, con la funzione image() visualizzo l’immagine facendo in modo che il bordo in alto a sinistra corrisponda alle coordinate 0, 0.

Ecco il risultato:

loadImage(), image() e PImage

L’immagine viene visualizzata correttamente perché il file originale è di 500 x 500 pixel. Per completezza condivido anche lo screenshot della cartella dello sketch.

La cartella dello sketch che contiene l'immagine

Processing accetta i seguenti tipi di file: GIF, JPG, TGA PNG.

Un paio di precisazioni

Abbiamo detto che PImage è una classe. Perché non abbiamo mai chiamato un constructor per inizializzarla? La funzione loadImage() in questo caso ha la funzione del constructor restituendo una nuova istanza dell’oggetto PImage generata dal file che carichiamo.

Se quest’ultima frase vi sembra arabo, consiglio di ripassare i post dedicati alla programmazione ad oggetti:

Come illustrato nel reference la funzione image() richiede un minimo di tre parametri: immagine da caricare, posizione x e y. È possibile passare alla funzione due parametri ulteriori per specificare la larghezza e l’altezza dell’immagine: un metodo comodo e veloce per ridimensionare le foto:

/*
 * Utilizzare immagini in Processing: PImage, loadimage() e image()
 * Federico Pepe, 12.03.2017
 * http://blog.federicopepe.com/processing
*/

PImage immagine;

void setup() {
  size(500, 500);
  immagine = loadImage("logo_federicopepe.png");
}

void draw() {
  // Utilizzo due parametri aggiuntivi per ridimensionare la foto
  image(immagine, 0, 0, 100, 100);
}

Immagine ridimensionata


Oggi sfrutteremo Processing per creare dei semplici visual sincronizzati con la musica attraverso il protocollo MIDI. Il software musicale che userò per fare gli esempi è Ableton Live 9 ma è possibile usare qualsiasi Digital Audio Workstation in grado di inviare messaggi MIDI.

Lo schema delle connessioni, dunque, sarà: Ableton Live gestirà la musica e invierà, attraverso dei messaggi MIDI, delle informazioni che Processing acquisirà in input per generare i visual.

MIDI e the MidiBus

Perché ho deciso di sfruttare il protocollo MIDI? Le ragioni sono molto semplici:

  1. è uno standard per le applicazioni musicali
  2. è un protocollo flessibile
  3. è semplice creare, inviare e monitorare i messaggi MIDI.
  4. rende molto semplice la sincronizzazione

Per questioni di spazio, non mi dilungherò in questo post sulla spiegazione di come funziona questo protocollo; trattandosi di uno standard sviluppato negli anni ’80, ci sono numerose risorse on-line che vi permettono di approfondire.

Libreria: the MidiBus

Anche se Java supporta nativamente il MIDI attraverso il pacchetto java.sound.midi, per lavorare con questi tipi di messaggi in Processing consiglio di installare una libreria.

Processing Contribution Manager MIDI Bus

Aprite Processing, andate nel menu Tool > Add new tool. Nel pannello Libraries digitate MIDI, selezionate The Midi Bus e cliccate sul pulsante Install in basso a destra.

macOS: IAC Driver

Di norma per inviare e ricevere messaggi MIDI è necessario dotarsi di una scheda e dei relativi cablaggi. Nel nostro caso, però, dovendo inviare e ricevere messaggi tra due applicazioni possiamo sfruttare lo IAC Driver: una scheda virtuale che ci permette di gestire tutte le connessioni internamente al computer.

Nota: Per Windows esistono delle applicazioni da installare che hanno la stessa funzione del driver IAC. Con una veloce ricerca su Google ho trovato LoopBe1 e MIDI Yoke che non ho provato personalmente ma sono consigliate su diversi forum.

Per configurare lo IAC Driver su macOS aprite Applicazioni > Utility > Configurazione MIDI Audio. Cliccate su Finestra > Mostra Studio MIDI ⌘2

Studio MIDI macOS IAC Driver

Fate doppio click su IAC Driver e mettete la spunta su Il dispositivo è acceso.

mac OS accendere IAC Driver

Ora che abbiamo attivato il canale di comunicazione interno per inviare e ricevere messaggi MIDI, possiamo procedere con Processing.

Ricezione messaggi MIDI in Processing

Apriamo un nuovo sketch e verifichiamo subito che Processing veda lo IAC Driver.

/*
 * Processing e Ableton: sincronizzare musica e visual via MIDI
 * Federico Pepe, 05.03.2017
 * http://blog.federicopepe.com/processing
*/

import themidibus.*;

void setup() {
  size(100, 100);
  background(0);

  MidiBus.list(); 
}

void draw() {
  
}

Con queste poche righe di codice abbiamo importato la libreria all’interno del nostro programma e attraverso il comando Midibus.list() possiamo verificare nella console tutte le periferiche MIDI attive.

Questo è il risultato che ottengo lanciando il programma sul mio Mac. Ovviamente potrebbe cambiare sul vostro; l’importante è che sia presente IAC Bus.

Available MIDI Devices:
----------Input----------
[0] "IAC Bus 1"
[1] "Real Time Sequencer"
----------Output----------
[0] "Gervill"
[1] "IAC Bus 1"
[2] "Real Time Sequencer"

Progetto Ableton Live: invio del messaggio

Apriamo Ableton Live e creiamo un nuovo progetto. Apriamo le preferenze e verifichiamo che il driver IAC sia selezionato come scheda sia per l’input che per l’output.

Ableton Live 9 MIDI Preferences

Nella prima traccia MIDI nella parte di input/output selezioniamo MIDI ToIAC Driver. In questo modo tutti i messaggi che saranno presenti su questa traccia saranno inviati come output al driver e, di conseguenza, saranno ricevuti da Processing.

Ableton MIDI Out Tracks

Creiamo una Clip MIDI nel primo slot contenente un C3 lungo 1/4. Assicuriamoci che la clip sia il loop quando premiamo il tasto play su di essa.

MIDI Clip

Ricezione del messaggio

Torniamo su Processing e modifichiamo il nostro programma come segue:

/*
 * Processing e Ableton: sincronizzare musica e visual via MIDI
 * Federico Pepe, 05.03.2017
 * http://blog.federicopepe.com/processing
*/

import themidibus.*;

MidiBus myBus;

void setup() {
  size(100, 100);
  background(0);

  MidiBus.list(); 
  
  myBus = new MidiBus(this, 0, 1);
}

void draw() {
  
}

void noteOn(int channel, int pitch, int velocity) {
  println();
  println("Note On:" + " Channel: "+channel + " Pitch: "+pitch + " Velocity: "+velocity);
  println("--------");
}

Rispetto al codice scritto in precedenza, abbiamo aggiunto e inizializzato un oggetto di tipo MidiBus: myBus = new MidiBus(this, 0, 1);. In questa fase è fondamentale inserire i parametri corretti: il primo è il parent a cui la libreria è collegata, il secondo è l’input e l’ultimo l’output. I dati sono facilmente individuabili grazie al comando .list() dato in precedenza.

Abbiamo, infine, aggiunto una funzione che è presente all’interno della libreria: noteOn() e che restituisce i valori MIDI: canale, pitch e velocity di tutte le note ricevute.

Verifichiamo il funzionamento

Facciamo partire la nostra clip su Ableton Live e avviamo il nostro sketch su Processing. Se in console appare un messaggio come quello qui di seguito, siamo riusciti nel nostro intento:

Available MIDI Devices:
----------Input----------
[0] "IAC Bus 1"
[1] "Real Time Sequencer"
----------Output----------
[0] "Gervill"
[1] "IAC Bus 1"
[2] "Real Time Sequencer"

Note On: Channel: 0 Pitch: 60 Velocity: 127
--------

Processing sta ricevendo tramite IAC Driver un messaggio Note On con i seguenti parametri:

  • Canale: 0
  • Pitch: 60 (che corrisponde al C3)
  • Velocity: 127

Adesso, se andiamo a modificare la clip su Ableton inserendo nuovi messaggi, possiamo verificare che anche questi siano ricevuti da Processing.

Creare i visual

A questo punto non ci resta che sviluppare in Processing i nostri visual sfruttando i parametri MIDI inviati da Ableton. Nell’esempio molto semplice che trovate di seguito, viene disegnato un quadrato al centro dello schermo la cui dimensione dipende dal valore di pitch ricevuto via MIDI. In aggiunta a quanto visto in questo post, sfrutto la funzione noteOff() per impostare la grandezza del quadrato a 0. Di certo non è il metodo più elegante ma di sicuro funziona.

/*
 * Processing e Ableton: sincronizzare musica e visual via MIDI
 * Federico Pepe, 05.03.2017
 * http://blog.federicopepe.com/processing
*/

import themidibus.*;

MidiBus myBus;

int size;

void setup() {
  size(400, 400);
  background(0);

  MidiBus.list(); 
  
  myBus = new MidiBus(this, 0, 1);
}

void draw() {
  background(0);
  rectMode(CENTER);
  rect(width/2, height/2, size, size);
}

void noteOn(int channel, int pitch, int velocity) {
  println();
  println("Note On:" + " Channel: "+channel + " Pitch: "+pitch + " Velocity: "+velocity);
  println("--------");
  size = pitch;
}

void noteOff(int channel, int pitch, int velocity) {
  size = 0;
}

Ed ecco il risultato: