How can I avoid exceeding the max call stack size during a flood fill algorithm?

不打扰是莪最后的温柔 提交于 2020-04-30 06:31:08

问题


I am using a recursive flood fill algorithm in javascript and I am not sure how to avoid exceeding the max call stack size. This is a little project that runs in the browser.

I got the idea from here: https://guide.freecodecamp.org/algorithms/flood-fill/

I chose this algorithm because it's easy to understand and so far I like it because it's pretty quick.

x and y are the 2-d coordinates from the top-left, targetColor and newColor are each a Uint8ClampedArray, and id = ctx.createImageData(1,1); that gets its info from newColor.

function floodFill2(x, y, targetColor, newColor, id) {
  let c = ctx.getImageData(x, y, 1, 1).data;

  // if the pixel doesnt match the target color, end function   
  if (c[0] !== targetColor[0] || c[1] !== targetColor[1] || c[2] !== targetColor[2]) {
    return;
  }

  // if the pixel is already the newColor, exit function
  if (c[0] === newColor[0] && c[1] === newColor[1] && c[2] === newColor[2]) {
    // this 'probably' means we've already been here, so we should ignore the pixel
    return;
  }

  // if the fn is still alive, then change the color of the pixel
  ctx.putImageData(id, x, y);

  // check neighbors
  floodFill2(x-1, y, targetColor, newColor, id);
  floodFill2(x+1, y, targetColor, newColor, id);
  floodFill2(x, y-1, targetColor, newColor, id);
  floodFill2(x, y+1, targetColor, newColor, id);

  return;   
}

If the section is small, this code works fine. If the section is big, only a portion gets filled in and then I get the max call stack size error.

Questions

  1. Is there something that doesn't make sense in the above code? (ie. maybe an issue for code review?)
  2. If the code looks ok, is it the possible that I am simply using an algorithm that is inappropriate to flood fill a large section?

I would like to say that my hope for this question is to have a simple function similar to the one above which will work even for a very large, oddly shaped region but that I suppose is contingent on the generality of the algorithm. Like, am I trying to drive a nail with a screwdriver kind of thing?


回答1:


Use a stack or Why recursion in JavaScript sucks.

Recursion is just a lazy mans stack. Not only is it lazy, it uses more memory and is far slower than traditional stacks

To top it off (as you have discovered) In JavaScript recursion is risky as the call stack is very small and you can never know how much of the call stack has been used when your function is called.

Some bottle necks while here

Getting image data via getImageData is an intensive task for many devices. It can take just as long to get 1 pixel as getting 65000 pixels. Calling getImageData for every pixel is a very bad idea. Get all pixels once and get access to pixels directly from RAM

Use an Uint32Array so you can process a pixel in one step rather than having to check each channel in turn.

Example

Using a simple array as a stack, each item pushed to the stack is the index of a new pixel to fill. Thus rather than have to create a new execution context, a new local scope and associated variables, closure, and more. A single 64bit number takes the place of a callStack entry.

See demo for an alternative flood fill pixel search method

function floodFill(x, y, targetColor, newColor) {
     const w = ctx.canvas.width, h = ctx.canvas.height;
     const imgData = ctx.getImageData(0, 0, w, h);
     const p32 = new Uint32Array(imgData.data.buffer);

     const channelMask = 0xFFFFFF; // Masks out Alpha  NOTE order of channels is ABGR
     const cInvMask = 0xFF000000; // Mask out BGR

     const canFill = idx => (p32[idx] & channelMask) === targetColor;
     const setPixel = (idx, newColor) => p32[idx] = (p32[idx] & cInvMask) | newColor;
     const stack = [x + y * w]; // add starting pos to stack

     while (stack.length) {
         let idx = stack.pop();
         setPixel(idx, newColor);

         // for each direction check if that pixel can be filled and if so add it to the stack
         canFill(idx + 1) && stack.push(idx + 1); // check right
         canFill(idx - 1) && stack.push(idx - 1); // check left
         canFill(idx - w) && stack.push(idx - w); // check Up
         canFill(idx + w) && stack.push(idx + w); // check down
     }
     // all done when stack is empty so put pixels back to canvas and return
     ctx.putImageData(imgData,0, 0);

}

Usage

To use the function is slightly different. id is not used and the colors targetColor and newColor need to be 32bit words with the red, green, blue, alpha reversed.

For example if targetColor was yellow = [255, 255, 0] and newColor was blue =[0, 0, 255] then revers RGB for each and call fill with

 const yellow = 0xFFFF;
 const blue = 0xFF0000;
 floodFill(x, y, yellow, blue);

Note that I am matching your function and completely ignoring alpha

Inefficient algorithm

Note that this style of fill (mark up to 4 neighbors) is very inefficient as many of the pixels will be marked to fill and by the time they are popped from the stack it will already have been filled by another neighbor.

The following GIF best illustrates the problem. Filling the 4 by 3 area with green.

  1. First set the pixel green,
  2. Then push to stack if not green right, left, up, down [illustration red, orange, cyan, purple boxes]
  3. Pop bottom and set to green
  4. Repeat

When a location that already is on the stack is added it is inset (just for illustration purpose)

Note that when all pixels are green there are still 6 items on the stack that still need to be popped. I estimate on average you will be processing 1.6 times the number of pixels needed. For a large image 2000sq thats 2million (alot of) pixels

Using an array stack rather than call stack means

  • No more call stack overflows
  • Inherently faster code.
  • Allows for many optimizations

Demo

The demo is a slightly different version as your logic has some problems. It still uses a stack, but limits the number of entries pushed to the stack to be equal to the number of unique columns in the fill area.

  • Includes alpha in the pixel fill test and pixel write color. Simplifying the pixel read and write code.
  • Checks against the edges of the canvas rather than filling outside the canvas width (looping back AKA asteroids style)
  • Reads target color from the canvas at the first x,y pixel
  • Fills columns from the top most pixel in each column and only branching left or right if the previous left or right pixel was not the target color. This reduces the number of pixels to push the stack by orders of magnitude.

Click to flood fill

function floodFill(x, y, newColor) {
    var left, right, leftEdge, rightEdge;
    const w = ctx.canvas.width, h = ctx.canvas.height, pixels = w * h;
    const imgData = ctx.getImageData(0, 0, w, h);
    const p32 = new Uint32Array(imgData.data.buffer);
    const stack = [x + y * w]; // add starting pos to stack
    const targetColor = p32[stack[0]];
    if (targetColor === newColor || targetColor === undefined) { return } // avoid endless loop
    while (stack.length) {
        let idx = stack.pop();
        while(idx >= w && p32[idx - w] === targetColor) { idx -= w }; // move to top edge
        right = left = false;   
        leftEdge = (idx % w) === 0;          
        rightEdge = ((idx +1) % w) === 0;
        while (p32[idx] === targetColor) {
            p32[idx] = newColor;
            if(!leftEdge) {
                if (p32[idx - 1] === targetColor) { // check left
                    if (!left) {        
                        stack.push(idx - 1);  // found new column to left
                        left = true;  // 
                    }
                } else if (left) { left = false }
            }
            if(!rightEdge) {
                if (p32[idx + 1] === targetColor) {
                    if (!right) {
                        stack.push(idx + 1); // new column to right
                        right = true;
                    }
                } else if (right) { right = false }
            }
            idx += w;
        }
    }
    ctx.putImageData(imgData,0, 0);
    return;
}




var w = canvas.width;
var h = canvas.height;
const ctx = canvas.getContext("2d");
var i = 400;
const fillCol = 0xFF0000FF
const randI = v => Math.random() * v | 0;
ctx.fillStyle = "#FFF";
ctx.fillRect(0, 0, w, h);


ctx.fillStyle = "#000";
while(i--) {
    ctx.fillRect(randI(w), randI(h), 20, 20);
    ctx.fillRect(randI(w), randI(h), 50, 20);
    ctx.fillRect(randI(w), randI(h), 10, 60);    
    ctx.fillRect(randI(w), randI(h), 180, 2);
    ctx.fillRect(randI(w), randI(h), 2, 182);
    ctx.fillRect(randI(w), randI(h), 80, 6);
    ctx.fillRect(randI(w), randI(h), 6, 82);  
    ctx.fillRect(randI(w), randI(h), randI(40), randI(40));          
}
i = 400;
ctx.fillStyle = "#888";
while(i--) {
    ctx.fillRect(randI(w), randI(h), randI(40), randI(40));      
    ctx.fillRect(randI(w), randI(h), randI(4), randI(140)); 
}  
var fillIdx = 0;
const fillColors = [0xFFFF0000,0xFFFFFF00,0xFF00FF00,0xFF00FFFF,0xFF0000FF,0xFFFF00FF];

canvas.addEventListener("click",(e) => {
    floodFill(e.pageX | 0, e.pageY | 0, fillColors[(fillIdx++) % fillColors.length]); 
});
canvas {
	   position: absolute;
	   top: 0px;
	   left: 0px;
	}
<canvas id="canvas" width="2048" height="2048">



回答2:


Flood fill is a problematic process with respect to stack size requirements (be it the system stack or one managed on the heap): in the worst case you will need a recursion depth on the order of the image size. Such cases can occur when you binarize random noise, they are not so improbable.

There is a version of flood filling that is based on filling whole horizontal runs in a single go (https://en.wikipedia.org/wiki/Flood_fill#Scanline_fill). It is advisable in general because it roughly divides the recursion depth by the average length of the runs and is faster in the "normal" cases. Anyway, it doesn't solve the worst-case issue.

There is also an interesting truly stackless algorithm as described here: https://en.wikipedia.org/wiki/Flood_fill#Fixed-memory_method_(right-hand_fill_method). But the implementation looks cumbersome.



来源:https://stackoverflow.com/questions/59833738/how-can-i-avoid-exceeding-the-max-call-stack-size-during-a-flood-fill-algorithm

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!