Archivi tag: coding rescue

Coding Rescue #3 – Cambiare la tonalità di alcuni pixel

Negli ultimi giorni ho ricevuto diverse richieste di aiuto per problemi di codice in Processing; ecco la soluzione a uno dei problemi che mi sono stati posti.

Ho deciso di chiamare questo Coding Rescuecambiare la tonalità di alcuni pixel.

Il problema

Il quesito era piuttosto articolato:

  • Il programma deve caricare un’immagine dal disco.
  • Il programma decide se modificare o no il pixel in base ad un valore casuale.
  • Il processo di trasformazione viene controllato dal click  del mouse: un primo click avvia la trasformazione che, con un successivo click, viene messa in pausa. Un ulteriore click fa ripartire la trasformazione e così via.
  • I pixel devono essere modificati di continuo.
  • Il compito deve essere svolto mediante la funzione creaImmagine(), che accetta in ingresso un oggetto di tipo PImage e rende in uscita un oggetto di tipo PImage, che sarà l’immagine modificata come da specifiche, e la funzione calcolaPixel(), che accetta in ingresso un oggetto di tipo color e rende in uscita un oggetto di tipo color, che sarà il pixel modificato come da specifiche.
  • creaImmagine() crea una nuova immagine utilizzando su ciascun pixel la funzione calcolaPixel().
  • calcolaPixel() calcola il nuovo pixel nel seguente modo: siano r, g, b i livelli di canale di un pixel. Sia treshold un valore numerico posto inizialmente a 0.5. Sia r un valore casuale compreso tra 0 e 1, generato per ogni pixel. Se r>soglia, allora la funzione lascia il pixel inalterato, in caso contrario il programma calcola i nuovi valori rm, gm, bm (che definiscono il pixel
    modificato) nel seguente modo:

    • rm = 0.393∗r+0.769∗g+0.189∗b
    • gm = 0.349∗r+0.686∗g+0.168∗b
    • bm = 0.272∗r+0.534∗g+0.131∗b

    In ogni caso i valori finali di r, g, b devono essere valori leciti, cioè compresi tra 0 e 255.

  • Quando l’utente preme il tasto “+” la soglia viene incrementata di 0.1 mentre, con la pressione del tasto “-” la soglia viene decrementata di 0.1.

La soluzione


/*
 * Coding Rescue #3 - Cambiare la tonalità di alcuni pixel
 * Federico Pepe, 21.01.2018
 * http://blog.federicopepe.com/processing
 */

// Creo le variabili necessarie;
float threshold = 0.5;
boolean work = true;
PImage original, edited;

void setup() {
  // Nella funzione di setup carico il file dall'hard disk;
  size(1, 1);
  surface.setResizable(true);
  selectInput("Select a file to process:", "fileSelected");
}

void draw() {
  if (original != null) {
    edited = original.copy();
    /*
     * Se la variabile work, il cui valore dipende dal click del mouse è true, mostro
     * l'immagine originale, altrimenti avvio la modifica
     */
    if (work) {
      image(edited, 0, 0);
    } else {
      image(creaImmagine(edited), 0, 0);
    }
  }
}

// Funzione per gestire il caricamento dei file da HD
// https://processing.org/reference/selectInput_.html
void fileSelected(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
  } else {
    println("User selected " + selection.getAbsolutePath());
    original = loadImage(selection.getAbsolutePath());
    surface.setSize(original.width, original.height);
  }
}

// Funzione creaImmagine che, come da specifiche, accetta in ingresso un oggetto PImage
// e restituisce un oggetto PImage;
PImage creaImmagine(PImage img) {
  // Carico i pixel in un array
  img.loadPixels();
  for (int y = 0; y < img.height; y++) {
    for (int x = 0; x < img.width; x++) {
      int loc = x + y * img.width;
      // Ottengo i valori R, G, B di ciascun pixel;
      float r = red(img.pixels[loc]);
      float g = green(img.pixels[loc]);
      float b = blue(img.pixels[loc]);
      // Lancio la funziona calcolaPixel
      img.pixels[loc] = calcolaPixel(color(r, g, b));
    }
  }
  img.updatePixels();
  return img;
}

color calcolaPixel(color c) {
  // Valore casuale, come da specifiche
  float r = random(0, 1);

  if (r > threshold) {
    // Se r è maggiore della soglia, restituisco il colore originale
    return c;
  } else {
    // Altrimenti calcolo il nuovo valore del pixel come richiesto
    // la funzione constrain mi assicura che il valore calcolato sia compreso tra 0 e 255;
    float rm = constrain(0.393*red(c)+0.769*green(c)+0.189*blue(c), 0, 255);
    float gm = constrain(0.349*red(c)+0.686*green(c)+0.168*blue(c), 0, 255);
    float bm = constrain(0.272*red(c)+0.534*green(c)+0.131*blue(c), 0, 255);
    // Restituisco i nuovi valori
    return color(rm, gm, bm);
  }
}

// Con la funzione keyPressed determino la pressione dei tasti + e -
void keyPressed() {
  if (key == '+') {
    threshold += 0.1;
  }
  if (key == '-') {
    threshold -= 0.1;
  }
  // Utilizzo la funzione round (di seguito) per arrotondare il valore a 2 decimali;
  round(threshold, 2);
}
// Alla pressione del mouse, cambio il valore di work da true a false e viceversa
void mouseClicked() {
  work = !work;
}
// Funzione utile per limitare il numero di decimali in un float
// https://stackoverflow.com/questions/9627182/how-do-i-limit-decimal-precision-in-processing
float round(float number, float decimal) {
  return (float)(round((number*pow(10, decimal))))/pow(10, decimal);
} 

Coding Rescue #1 – Invertire sotto-sopra una porzione di un’immagine

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: