Magic Wand (Flood Fill) Question

I've created a basic magic wand program that I can't figure out what's gone wrong. I'm trying to keep the code basic without getting into crazy complicated coding. Can anyone tell me what isn't working please? The color picker function doesn't seem to be reading the image pixel color correctly, which is preventing me from seeing if the color distance or magic wand functions are working either. Thanks for any assistance.

// Variables
PImage img;
// Boolean variables for the buttons
boolean buttonSelect = false;
boolean buttonFill = false;
// picked color
color pickedColor;
boolean c = false;
int loc;

// Position of button 1
// Position of button 2

void setup() {
size(1000,900);
img = loadImage ("sunflower.jpg");
 rect(925, 200, 50, 25);
 rect(925, 400, 50, 25);
   // draw buttons

  // initialize variables if necessary
}

void draw() {

  // load image pixels: loadPixels();
  loadPixels();
  image(img, 0, 0);
  fill(random(255),random(255),random(255));
  // if button 1 is clicked and currently there is no color picked


  // if button_2 is clicked and a color is picked


}


// default function offered by Processing for Button click detection
boolean overRect(int x, int y, int width, int height)  {
  if (mouseX >= x && mouseX <= x+width && 
      mouseY >= y && mouseY <= y+height) {
    return true;
  } else {
    return false;
  }
}

void mouseClicked() {
  // if first button clicked
    if (overRect(925, 200, 50, 25)==true)
    {
      buttonSelect = true;
      rect(925,200,50,25);
    }

  if (buttonSelect == true) {
  // { select color}
  pickedColor = pickColor();
  }
  if (overRect(925, 400, 50, 25) && c==true){
     //  magic wand (select pixels with similar color)
     rect(925, 400, 50, 25);
     magicWand(pickedColor); 
     buttonFill = true;
   }
 }

   // if (overRect(first button position and size)) 
   //
   // if second button clicked


   // function to Pick A Color
color pickColor() 
{
// allocate location in pixel array based on mouse position
// for example: int loc = mouseX + mouseY*img.width;
   loc = mouseX + mouseY * img.width;
  // get the color from that pixel
     float r = red(pixels[loc]);
     float g = green(pixels[loc]);
     float b = blue(pixels[loc]);

     // return the color
     pickedColor = color(r,g,b);
     c = true;
     return pickedColor;
} 

// function for MagicWand
void magicWand(color c_selected) 
{
  // load pixels
  loadPixels();
  // get the r, g, b value for the selected color
  // use for loops to access each pixel in image
  color current;
  for (int i = 0; i < width*height; i++) {
  current = pixels[i];
  float dist = colorDist(current,c_selected);
  if (dist < 50){
    pixels[i] = color(255,0,0);  
  }
updatePixels();
}
//updatePixels();
  // get each pixel's color
  // compare the pixel's color with the selected color
  // if the distance between the two color is less than 50
  // highlight the pixel with a new color: for example, pixels[loc] = color(255,0,0);
  // update image Pixels 
}
// function to find color distance
float colorDist(color c1, color c2){
  float r1 = red(c1);
  float g1 = green(c1);
  float b1 = blue(c1);
  float r2 = red(c2);
  float g2 = blue(c2);
  float b2 = blue(c2);
  // get the r, g, b values from each color
  // compute the distance using pythagorean theorom: 
  float dist = sqrt( (r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2));
  return dist;
}

Answers

  • edited July 2017

    There are a bunch of good ideas in this sketch and a few really strange things happening.

    For example, in draw() you are currently randomizing fill every single draw loop. Don't do that there in a sketch where you are testing a color picker, it confuses the issue (as often the color you are seeing has already been changed).

    Strategy: I would recommend breaking this into two sketches that work separately, then combining. The first sketch simply tests the magic wand, which is always on and always pours red. The second sketch simply tests a color picker, which is always on and always displays the latest color in a floating status box which is non-interactive.

    Once both work perfectly, think about combining them. At that point it should be obvious that you need at least two mouse modes -- picking and pouring. Consider triggering these modes with keyboard keys at first -- having special controls regions and adds complexity.

    Also -- could you explain how your magic wand is supposed to work? Right now it appears to be attempting to change every pixel in the image that is similar -- no matter where that pixel is. Is it supposed to operate on contiguous, spatially connected pixels?

  • The top button is supposed to activate the color picker and then when you click on a pixel in the image it selects that color. Upon pressing the bottom button it "highlights" every pixel in the image red that's less than 50 difference in color value. The redraw of the rectangle and the random fill was just to show that the color picker has been activated.

    Do you see anything wrong with the actual code in the color picker? I've tested it by having the second button draw a rectangle with that fill and it doesn't seem to be storing the color value correctly. Also, the flood fill doesn't do anything at all.

  • edited November 2016

    @CodeSloth -- like I said, break it down into multiple simpler sketches!

    There are several potential problems that I haven't tried to tease out in the more complex sketch. Just one example:

    float g2 = blue(c2);
    float b2 = blue(c2);
    

    That's going to play merry hell with your distance checking. Especially in a picture of a sunflower.

    Also, you might not want to call loadPixels every draw. Just call it when you need it (look at the loadPixels() reference).

  • Answer ✓

    I didn't go through all the code but I did minor changes to show hot to get the color picker working. Notice those changes I indicated in the code below. Few things to keep in mind:

    loadPixels should be called only after you draw the image with the image() function.

    There is a difference calling loadPixels before or after drawing your buttons. If you call them after drawing your buttons, then your buttons will also be included as part of your loadPixell array.

    If you draw your buttons in setup, then they are drawn only once. If the image takes over all the sketch area, it will cover your buttons. That is why I moved the buttons into your drawing and after drawing the image, so are are drawn on top of it.

    I hope this helps,

    Kf

    // Variables
    PImage img;
    // Boolean variables for the buttons
    boolean buttonSelect = false;
    boolean buttonFill = false;
    // picked color
    color pickedColor;
    boolean c = false;
    int loc;
    
    // Position of button 1
    // Position of button 2
    
    void setup() {
      size(1000, 900);
      img = loadImage ("fig.jpg");
    }
    
    void draw() {
    
      // load image pixels: loadPixels();
    
      image(img, 0, 0);
      loadPixels();            // ***  CHANGED
      // draw buttons
      pushStyle();             // ***  CHANGED
      fill(pickedColor);       // ***  CHANGED
      rect(925, 200, 50, 25);  // ***  CHANGED
      rect(925, 400, 50, 25);  // ***  CHANGED
      popStyle();              // ***  CHANGED
    
    
      // if button 1 is clicked and currently there is no color picked
    
    
      // if button_2 is clicked and a color is picked
    }
    
    
    
    
    // default function offered by Processing for Button click detection
    boolean overRect(int x, int y, int width, int height) {
      if (mouseX >= x && mouseX <= x+width && 
        mouseY >= y && mouseY <= y+height) {
        return true;
      } else {
        return false;
      }
    }
    
    void mouseClicked() {
    
      pickedColor=get(mouseX,mouseY); // ***  CHANGED
    
    
      // if first button clicked
      if (overRect(925, 200, 50, 25)==true)
      {
        buttonSelect = true;
        rect(925, 200, 50, 25);
      }
    
      if (buttonSelect == true) {
        // { select color}
        pickedColor = pickColor();
      }
      if (overRect(925, 400, 50, 25) && c==true) {
        //  magic wand (select pixels with similar color)
        rect(925, 400, 50, 25);
        magicWand(pickedColor); 
        buttonFill = true;
      }
    }
    
    // if (overRect(first button position and size)) 
    //
    // if second button clicked
    
    
    // function to Pick A Color
    color pickColor() 
    {
      // allocate location in pixel array based on mouse position
      // for example: int loc = mouseX + mouseY*img.width;
      loc = mouseX + mouseY * img.width;
      // get the color from that pixel
      float r = red(pixels[loc]);
      float g = green(pixels[loc]);
      float b = blue(pixels[loc]);
    
      // return the color
      pickedColor = color(r, g, b);
      c = true;
      return pickedColor;
    } 
    
    // function for MagicWand
    void magicWand(color c_selected) 
    {
      // load pixels
      loadPixels();
      // get the r, g, b value for the selected color
      // use for loops to access each pixel in image
      color current;
      for (int i = 0; i < width*height; i++) {
        current = pixels[i];
        float dist = colorDist(current, c_selected);
        if (dist < 50) {
          pixels[i] = color(255, 0, 0);
        }
        updatePixels();
      }
      //updatePixels();
      // get each pixel's color
      // compare the pixel's color with the selected color
      // if the distance between the two color is less than 50
      // highlight the pixel with a new color: for example, pixels[loc] = color(255,0,0);
      // update image Pixels
    }
    // function to find color distance
    float colorDist(color c1, color c2) {
      float r1 = red(c1);
      float g1 = green(c1);
      float b1 = blue(c1);
      float r2 = red(c2);
      float g2 = blue(c2);
      float b2 = blue(c2);
      // get the r, g, b values from each color
      // compute the distance using pythagorean theorom: 
      float dist = sqrt( (r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2));
      return dist;
    }
    
  • edited November 2016 Answer ✓

    @CodeSloth -- Try this to start. Work our your color picker in a separate simple sketch. Then combine.

    /**
     * Simple Magic Fill -- based on a sketch by CodeSloth
     * 2016-11-05 Processing 3.2.2
     * Click any pixel to highlight similar pixels. Press any key to reset.
     * https:// forum.processing.org/two/discussion/18902/magic-wand-flood-fill-question#latest
     **/ 
    PImage img;
    void setup() {
      size(600, 600);
      fill(255, 0, 0);
      img = loadImage ("https:// upload.wikimedia.org/wikipedia/commons/thumb/4/40/Sunflower_sky_backdrop.jpg/693px-Sunflower_sky_backdrop.jpg");
      image(img, 0, 0);
    }
    
    void draw() {}
    
    void mouseClicked() {
      magicFill(mouseX, mouseY);
    }
    
    void keyPressed(){
      image(img, 0, 0);
    }
    
    /**
     * magicFill marks all pixels within sensitivity of selected color.
     * Color is either found at xy coords or specified.
     **/
    void magicFill(int x, int y) {
      magicFill(get(x, y));
    }
    void magicFill(color c_selected) {
      int sensitivity = 96;
      println("magicFill:", c_selected, sensitivity);
      loadPixels();
      for (int i = 0; i < width*height; i++) {
        if ( colorDist(pixels[i], c_selected) < sensitivity) {
          pixels[i] = color(255, 0, 0);
        }
      }
      updatePixels();
    }
    
    float colorDist(color c1, color c2) {
      return dist(red(c1), green(c1), blue(c1), red(c2), green(c2), blue(c2));
    }
    

    SimpleMagicFill1 SimpleMagicFill2

  • Thank you so much, the help is greatly appreciated.

  • edited July 2017

    It has been a long time, but we have recently gotten several questions about flood filling, so...

    I am contributing a flood fill demo sketch that I did last year based on this discussion.

    Magic fill (above) changes any pixel in the image that matches the color profile. It is easy to implement -- check every pixel. Flood fill only changes pixels that are contiguous (like a paint bucket pour). It is harder to implement -- you to implement a method of "flooding" through the pixels.

    /**
     * Simple Flood Fill 2
     * 2017-07-07 Processing 3.3.5
     * Click any pixel to highlight similar nearby pixels. Press any key to reset.
     * forum.processing.org/two/discussion/18902/magic-wand-flood-fill-question
     **/
    import java.util.Queue;
    import java.util.ArrayDeque;
    Queue<PVector> queue = new ArrayDeque<PVector>();
    
    PImage img;
    void setup() {
      size(600, 600);
      fill(255, 0, 0);
      img = loadImage ("693px-Sunflower_sky_backdrop.jpg");
      // img = loadImage ("https://" + "upload.wikimedia.org/wikipedia/commons/thumb/4/40/Sunflower_sky_backdrop.jpg/693px-Sunflower_sky_backdrop.jpg");
      image(img, 0, 0);
    }
    
    void draw() {
    }
    
    void mouseClicked() {
      floodFill(mouseX, mouseY, get(mouseX, mouseY), 96);
    }
    
    void keyPressed() {
      image(img, 0, 0);
    }
    
    void floodFill(int xSeed, int ySeed, color c_selected, int sensitivity) {
      boolean[][] flooded  = new boolean[width][height];
      queue.clear();
      queue.add(new PVector(xSeed, ySeed));
    
      while (!queue.isEmpty()) {
        PVector point = queue.remove();
        int x = (int)point.x;
        int y = (int)point.y;
    
        // skip point that is out of bounds
        if (x >= 0 && x < width && y >= 0 && y < height) {
          // skip point that was previously flooded
          if (flooded[x][y]) {
            continue;
          }
          // skip and mark point that is out of color range
          if ( colorDist(get(x, y), c_selected) > sensitivity) {
            flooded[x][y] = true;
            continue;
          }
          set(x, y, color(255, 0, 0));
          flooded[x][y] = true;
          // queue flooding in four directions
          queue.add(new PVector(x - 1, y));
          queue.add(new PVector(x + 1, y));
          queue.add(new PVector(x, y - 1));
          queue.add(new PVector(x, y + 1));
        }
      }
    }
    
    float colorDist(color c1, color c2) {
      return dist(red(c1), green(c1), blue(c1), red(c2), green(c2), blue(c2));
    }
    

    The easiest naive implementation of a flood fill algorithm is recursive -- it calls itself of its four neighbors -- but this leads to stack overflow problems. One way to making the flooding manageable is to change recursion into adding points to a queue while processing the queue. See the Java implementation here:

    ...and an interesting previous discussion of queues in Processing here:

    ...and it like I forgot that @quark had already created a flood fill example. It also uses ArrayDeque, only with his only Point class instead of PVector. That example uses a scanline flood fill rather than a four way flood fill.

    https://forum.processing.org/two/discussion/14390/fill-algorithm

Sign In or Register to comment.