0

I have been attempting to create a flood fill function using an initial mouseX and mouseY value. After experimenting and failing with a 4 way recursive method I found an article that suggested using an array to store the 4 way values and queue them using the .push() function. While the array has values the function would then use the .pop()function to set new values to the x and y which in turn tested and changed to the desired colour.

After attempting this method for a flood fill I am still struggling to gain any sort of performance with regards to the speed. I tried to include an additional check history function to store the history of the visited pixels and prevent any repeated x,y coordinates from being pushed back into the queue. however (when using the console to log "match found") the function appears to not find any repetitions.

I would really appreciate any guidance on how to improve upon the code I have written so far. Or perhaps a suggestion on a different method that is not to complicated to implement. I rarely use Stack Overflow and would also appreciate guidance on how to format questions and code for future reference. Thank you for any of your time.

using the sketch.js I have included, you can see that the fill function is acceptable on very small objects(the first initial square), but as the size increases the performance grinds to halt.

index.html

<!DOCTYPE html>
<html>
  <head>
    <script src="p5.min.js"></script>
    <script src="sketch.js"></script>
    <style> body {padding: 0; margin: 0;} </style>
  </head>
  <body>
  </body>
</html>

sketch.js

var colorNew = [0,255,0,255];
function setup()
{
    createCanvas(500,500);
    squares();
}

function squares(){

    background(255); 
    noFill();
    stroke(0);
    rect(125,125,10,10);
    rect(125,125,20,20);
    rect(125,125,30,30);
    rect(125,125,40,40);
    rect(125,125,50,50);
    updatePixels();
}

function getPixData(xPos,yPos){    
    colorOld = get(xPos,yPos);
};

function checkValue(xPos,yPos,oldColor){    
    var currentPix
    currentPix = get(xPos,yPos);
    if(currentPix[0] == oldColor[0] && currentPix[1] == oldColor[1] && currentPix[2] == oldColor[2] && currentPix[3] == oldColor[3]){        
        return true;
    }    
};

function checkHistory(x1,y1,historyArray){    
    for (var i = 0 ; i < historyArray.length; i+=2){        
        if(x1 == historyArray[i] && y1 == historyArray[i+1]){            
            console.log("match found");           
            return false;                    
           }else {               
           console.log(historyArray.length)
           return true;             
            }
    } 
};


function floodFill (xPos,yPos){ 
    getPixData(mouseX,mouseY);    
    var stack = [];
    var historyList = [];
    stack.push(xPos,yPos);
    historyList.push(xPos,yPos);    
    console.log(stack);

    while(stack.length> 0){
        var yPos1 = stack.pop();
        var xPos1 = stack.pop();   
        set(xPos1,yPos1,colorNew); 
        updatePixels();

        if(xPos1+1 <= width && xPos1+1 > 0 ){
            if(checkValue(xPos1+1,yPos1,colorOld) && checkHistory(xPos1+1,yPos1,historyList)){
                stack.push(xPos1+1,yPos1);
                historyList.push(xPos1+1,yPos1);
                console.log("right checked")            
            }
        }

        if(xPos1+1 <= width && xPos1+1 > 0 ){
            if(checkValue(xPos1-1,yPos1,colorOld) && checkHistory(xPos1-1,yPos1,historyList)){
                stack.push(xPos1-1,yPos1);
                historyList.push(xPos1-1,yPos1);
                console.log("left checked");            
            }
        }
        if(yPos1+1 <= height && yPos1+1 > 0 ){
            if(checkValue(xPos1,yPos1+1,colorOld) && checkHistory(xPos1,yPos1+1,historyList)){
                stack.push(xPos1,yPos1+1);
                historyList.push(xPos1,yPos1+1);
                console.log("above checked");         
            }
        }

        if(yPos1-1 <= height && yPos1-1 > 0 ){
            if(checkValue(xPos1,yPos1-1,colorOld) && checkHistory(xPos1,yPos1-1,historyList)){
                stack.push(xPos1,yPos1-1);
                historyList.push(xPos1,yPos1-1);
                console.log("below checked");
            }
        }
    }      
    console.log(historyList);     
}

function draw()
{

    if(mouseIsPressed){
        floodFill(mouseX,mouseY)
    }

} 
David
  • 27
  • 4

2 Answers2

2

The main performance bottleneck is that get(x,y) is slow. It has to call getImageData every time to look at the pixel data of the canvas, which takes about 4 ms.

Performance Dev Tools showing that each checkValue(x,y,colorOld) call takes anywhere between 5 and 12 ms to complete due to get(x,y)

The flood fill algorithm has to check each pixel once, so in a 20x50 square you'd have to check 1000 pixels. If each pixel check takes 4 ms to get the color data, that's 4 seconds!

p5 suggests a few performance optimization techniques to process data, one of them being not to check the pixels of the image frequently. It suggests caching the pixels ahead of time. Luckily, p5 does this for you already.

p5 creates an array of all colors of the image in the pixels array. Instead of using get to look at the pixel data you can use the pixel array. The documentation provides code for how to set/get the color data from point x,y:

function getPixData(x,y){
  var color = [];

  for (var i = 0; i < d; i++) {
    for (var j = 0; j < d; j++) {
      // loop over
      idx = 4 * ((y * d + j) * width * d + (x * d + i));
      color[0] = pixels[idx];
      color[1] = pixels[idx+1];
      color[2] = pixels[idx+2];
      color[3] = pixels[idx+3];
    }
  }

  return color;
};

function setPixData(x, y, colorNew) {
  for (var i = 0; i < d; i++) {
    for (var j = 0; j < d; j++) {
      // loop over
      idx = 4 * ((y * d + j) * width * d + (x * d + i));
      pixels[idx] = colorNew[0];
      pixels[idx+1] = colorNew[1];
      pixels[idx+2] = colorNew[2];
      pixels[idx+3] = colorNew[3];
    }
  }
}

If you replace your references of get(x,y) and set(x,y) with these two methods, and call loadPixels in the setup method, you'll see a tremendous speed improvement. And don't forget to call updatePixels at the end of the method.

function floodFill (xPos,yPos){
    colorOld = getPixData(xPos, yPos);
    var stack = [];
    var historyList = [];
    stack.push(xPos,yPos);
    historyList.push(xPos,yPos);
    console.log(stack);

    while(stack.length> 0){
        var yPos1 = stack.pop();
        var xPos1 = stack.pop();
        setPixData(xPos1,yPos1,colorNew);

        if(xPos1+1 <= width && xPos1+1 > 0 ){
            if(checkValue(xPos1+1,yPos1,colorOld) && checkHistory(xPos1+1,yPos1,historyList)){
                stack.push(xPos1+1,yPos1);
                historyList.push(xPos1+1,yPos1);
                console.log("right checked")
            }
        }

        if(xPos1+1 <= width && xPos1+1 > 0 ){
            if(checkValue(xPos1-1,yPos1,colorOld) && checkHistory(xPos1-1,yPos1,historyList)){
                stack.push(xPos1-1,yPos1);
                historyList.push(xPos1-1,yPos1);
                console.log("left checked");
            }
        }
        if(yPos1+1 <= height && yPos1+1 > 0 ){
            if(checkValue(xPos1,yPos1+1,colorOld) && checkHistory(xPos1,yPos1+1,historyList)){
                stack.push(xPos1,yPos1+1);
                historyList.push(xPos1,yPos1+1);
                console.log("above checked");
            }
        }

        if(yPos1-1 <= height && yPos1-1 > 0 ){
            if(checkValue(xPos1,yPos1-1,colorOld) && checkHistory(xPos1,yPos1-1,historyList)){
                stack.push(xPos1,yPos1-1);
                historyList.push(xPos1,yPos1-1);
                console.log("below checked");
            }
        }
    }

    updatePixels();
    console.log(historyList);
}

Also, I'd recommend changing the draw function to use mouseClicked instead of mousePressed as mousePressed will continually run while the mouse is held down whereas mouseClicked will run only once after the user has let the mouse up.

Steven Lambert
  • 5,571
  • 2
  • 29
  • 46
  • Thank you for taking time to look over my code, apologies for the wrong response earlier as that comment was meant for another response. After reviewing the code and inserting 2 new functions getPix() and setPix() which use the formula stated above. The setPix( ) function on its own made a tiny improvement. When using the new getPix(); function I am now getting a "paused before potential out-of-memory crash" error in the console. ( what is the best way to show the edited code without altering the orignal question ? ) – David Mar 12 '18 at 18:09
  • @David I think your problem is a combination of what this answer and what my answer talks about. My answer mentions that you're adding extra pixels to your history. My guess is this is the cause of your memory issue. – Kevin Workman Mar 12 '18 at 21:01
0

Something is definitely fishy with how your code behaves. A couple observations:

The history array is much larger than it should be. In theory you're only adding each pixel once, right? But check the size of the history array and notice that it is much larger than the number of pixels. Since you add both the x and y values to the array, for a 10x10 rectangle you should have 200 elements in the array. But you end up with much more.

I'd also avoid adding the x and y components to the array like that. Instead, have each index contain a point object. Something like this:

stack.push({x:xPos,y:yPos});

This doesn't actually fix your problem, but it will make debugging easier. Your code definitely contains a bug. I'll try to keep looking more later, but hopefully that gets you on the right track.

Kevin Workman
  • 41,537
  • 9
  • 68
  • 107