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

后端 未结 2 413
眼角桃花
眼角桃花 2021-01-27 17:25

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.

相关标签:
2条回答
  • 2021-01-27 17:52

    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">

    0 讨论(0)
  • 2021-01-27 18:00

    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.

    0 讨论(0)
提交回复
热议问题