安卓泛洪算法

12

有没有人知道一种迭代且高效的泛洪算法?

或者有没有一种实现递归floodfill算法且避免栈溢出错误的方法?

我尝试了在Flood fill using a stack中提到的一个方法,但我找不到如何处理白色和黑色图像的方法。


试试这个问题:https://dev59.com/fHM_5IYBdhLWcg3wt1rD - Mark Ransom
5个回答

34
有人将 J. Dunlap's 队列线性泛洪填充算法移植到了安卓平台这里。我已经尝试过,速度相当快。
我修改了copyImage()方法,原来使用了一个叫做Utilities的类,但作者没有提供。
public class QueueLinearFloodFiller {

    protected Bitmap image = null;
    protected int[] tolerance = new int[] { 0, 0, 0 };
    protected int width = 0;
    protected int height = 0;
    protected int[] pixels = null;
    protected int fillColor = 0;
    protected int[] startColor = new int[] { 0, 0, 0 };
    protected boolean[] pixelsChecked;
    protected Queue<FloodFillRange> ranges;

    // Construct using an image and a copy will be made to fill into,
    // Construct with BufferedImage and flood fill will write directly to
    // provided BufferedImage
    public QueueLinearFloodFiller(Bitmap img) {
        copyImage(img);
    }

    public QueueLinearFloodFiller(Bitmap img, int targetColor, int newColor) {
        useImage(img);

        setFillColor(newColor);
        setTargetColor(targetColor);
    }

    public void setTargetColor(int targetColor) {
        startColor[0] = Color.red(targetColor);
        startColor[1] = Color.green(targetColor);
        startColor[2] = Color.blue(targetColor);
    }

    public int getFillColor() {
        return fillColor;
    }

    public void setFillColor(int value) {
        fillColor = value;
    }

    public int[] getTolerance() {
        return tolerance;
    }

    public void setTolerance(int[] value) {
        tolerance = value;
    }

    public void setTolerance(int value) {
        tolerance = new int[] { value, value, value };
    }

    public Bitmap getImage() {
        return image;
    }

    public void copyImage(Bitmap img) {
        // Copy data from provided Image to a BufferedImage to write flood fill
        // to, use getImage to retrieve
        // cache data in member variables to decrease overhead of property calls
        width = img.getWidth();
        height = img.getHeight();

        image = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
        Canvas canvas = new Canvas(image);
        canvas.drawBitmap(img, 0, 0, null);

        pixels = new int[width * height];

        image.getPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
    }

    public void useImage(Bitmap img) {
        // Use a pre-existing provided BufferedImage and write directly to it
        // cache data in member variables to decrease overhead of property calls
        width = img.getWidth();
        height = img.getHeight();
        image = img;

        pixels = new int[width * height];

        image.getPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
    }

    protected void prepare() {
        // Called before starting flood-fill
        pixelsChecked = new boolean[pixels.length];
        ranges = new LinkedList<FloodFillRange>();
    }

    // Fills the specified point on the bitmap with the currently selected fill
    // color.
    // int x, int y: The starting coords for the fill
    public void floodFill(int x, int y) {
        // Setup
        prepare();

        if (startColor[0] == 0) {
            // ***Get starting color.
            int startPixel = pixels[(width * y) + x];
            startColor[0] = (startPixel >> 16) & 0xff;
            startColor[1] = (startPixel >> 8) & 0xff;
            startColor[2] = startPixel & 0xff;
        }

        // ***Do first call to floodfill.
        LinearFill(x, y);

        // ***Call floodfill routine while floodfill ranges still exist on the
        // queue
        FloodFillRange range;

        while (ranges.size() > 0) {
            // **Get Next Range Off the Queue
            range = ranges.remove();

            // **Check Above and Below Each Pixel in the Floodfill Range
            int downPxIdx = (width * (range.Y + 1)) + range.startX;
            int upPxIdx = (width * (range.Y - 1)) + range.startX;
            int upY = range.Y - 1;// so we can pass the y coord by ref
            int downY = range.Y + 1;

            for (int i = range.startX; i <= range.endX; i++) {
                // *Start Fill Upwards
                // if we're not above the top of the bitmap and the pixel above
                // this one is within the color tolerance
                if (range.Y > 0 && (!pixelsChecked[upPxIdx])
                        && CheckPixel(upPxIdx))
                    LinearFill(i, upY);

                // *Start Fill Downwards
                // if we're not below the bottom of the bitmap and the pixel
                // below this one is within the color tolerance
                if (range.Y < (height - 1) && (!pixelsChecked[downPxIdx])
                        && CheckPixel(downPxIdx))
                    LinearFill(i, downY);

                downPxIdx++;
                upPxIdx++;
            }
        }

        image.setPixels(pixels, 0, width, 1, 1, width - 1, height - 1);
    }

    // Finds the furthermost left and right boundaries of the fill area
    // on a given y coordinate, starting from a given x coordinate, filling as
    // it goes.
    // Adds the resulting horizontal range to the queue of floodfill ranges,
    // to be processed in the main loop.

    // int x, int y: The starting coords
    protected void LinearFill(int x, int y) {
        // ***Find Left Edge of Color Area
        int lFillLoc = x; // the location to check/fill on the left
        int pxIdx = (width * y) + x;

        while (true) {
            // **fill with the color
            pixels[pxIdx] = fillColor;

            // **indicate that this pixel has already been checked and filled
            pixelsChecked[pxIdx] = true;

            // **de-increment
            lFillLoc--; // de-increment counter
            pxIdx--; // de-increment pixel index

            // **exit loop if we're at edge of bitmap or color area
            if (lFillLoc < 0 || (pixelsChecked[pxIdx]) || !CheckPixel(pxIdx)) {
                break;
            }
        }

        lFillLoc++;

        // ***Find Right Edge of Color Area
        int rFillLoc = x; // the location to check/fill on the left

        pxIdx = (width * y) + x;

        while (true) {
            // **fill with the color
            pixels[pxIdx] = fillColor;

            // **indicate that this pixel has already been checked and filled
            pixelsChecked[pxIdx] = true;

            // **increment
            rFillLoc++; // increment counter
            pxIdx++; // increment pixel index

            // **exit loop if we're at edge of bitmap or color area
            if (rFillLoc >= width || pixelsChecked[pxIdx] || !CheckPixel(pxIdx)) {
                break;
            }
        }

        rFillLoc--;

        // add range to queue
        FloodFillRange r = new FloodFillRange(lFillLoc, rFillLoc, y);

        ranges.offer(r);
    }

    // Sees if a pixel is within the color tolerance range.
    protected boolean CheckPixel(int px) {
        int red = (pixels[px] >>> 16) & 0xff;
        int green = (pixels[px] >>> 8) & 0xff;
        int blue = pixels[px] & 0xff;

        return (red >= (startColor[0] - tolerance[0])
                && red <= (startColor[0] + tolerance[0])
                && green >= (startColor[1] - tolerance[1])
                && green <= (startColor[1] + tolerance[1])
                && blue >= (startColor[2] - tolerance[2]) && blue <= (startColor[2] + tolerance[2]));
    }

    // Represents a linear range to be filled and branched from.
    protected class FloodFillRange {
        public int startX;
        public int endX;
        public int Y;

        public FloodFillRange(int startX, int endX, int y) {
            this.startX = startX;
            this.endX = endX;
            this.Y = y;
        }
    }
}

如果您不想让UI等待图像填充完成,也可以使用线程。

public class FloodFillThread extends Thread {
    ProgressDialog mProgressDialog;
    Bitmap mBitmap;
    int mTargetColor;
    int mNewColor;
    Point mPoint;
    Runnable mCallback;

    public FloodFillThread(ProgressDialog pd, Runnable callback, Bitmap bitmap,
            Point pt, int targetColor, int newColor) {
        mBitmap = bitmap;
        mPoint = pt;
        mTargetColor = targetColor;
        mNewColor = newColor;
        mProgressDialog = pd;
        mCallback = callback;
    }

    @Override
    public void run() {
        QueueLinearFloodFiller filler = new QueueLinearFloodFiller(mBitmap, mTargetColor, mNewColor);
        filler.setTolerance(10);
        filler.floodFill(mPoint.x, mPoint.y);

        handler.sendEmptyMessage(0);
    }

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            mProgressDialog.dismiss();
            mCallback.run();
        }
    };
}

谢谢,我应该如何将 Runnable callBack 传递给 FloodFillThread 构造函数?我的意思是,我应该如何从我的活动中使用您的代码。 - Arash
它已经解决了。它非常快。我在我的Activity类中实现了Runnable,并将this传递给构造函数。 - Arash
7
似乎QueueLinearFloodFiller存在一个问题 - 如果将其与已经填充了一种颜色的位图一起使用,它无法填充整个位图。问题在下面几行代码中: image.getPixels(pixels, 0, width, 1, 1, width - 1, height - 1); image.setPixels(pixels, 0, width, 1, 1, width - 1, height - 1);应该是:image.getPixels(pixels, 0, width, 0, 0, width, height); image.setPixels(pixels, 0, width, 0, 0, width, height); - Fedir Tsapana
2
有没有办法也填充边缘?我的意思是,这个算法在填充区域和区域“边界”之间留下了一些空白。 - Tristan G
有没有办法也填充边缘?我的意思是,这个算法在填充区域和区域“边界”之间留下了一些空白。 - Vigneshwaran T
显示剩余9条评论

11

最高排名答案(由Shubhadeep Chaudhuri提供)不能处理透明背景。为了实现这一点,您需要添加一个alpha检查。以下更改是需要的:

更新私有成员

protected int[] tolerance = new int[] { 0, 0, 0, 0 };
protected int[] startColor = new int[] { 0, 0, 0, 0 };

更新方法

public void setTargetColor(int targetColor) {
    /*Same as before....*/
    startColor[3] = Color.alpha(targetColor);
}

public void setTolerance(int value) {
    tolerance = new int[] { value, value, value, value };
}

protected boolean CheckPixel(int px) {
    int red = (pixels[px] >>> 16) & 0xff;
    int green = (pixels[px] >>> 8) & 0xff;
    int blue = pixels[px] & 0xff;
    int alpha = (Color.alpha(pixels[px]));

    return (red >= (startColor[0] - tolerance[0]) && red <= (startColor[0] + tolerance[0])
            && green >= (startColor[1] - tolerance[1]) && green <= (startColor[1] + tolerance[1])
            && blue >= (startColor[2] - tolerance[2]) && blue <= (startColor[2] + tolerance[2])
            && alpha >= (startColor[3] - tolerance[3]) && alpha <= (startColor[3] + tolerance[3]));
}

检查 alpha 通道,这个更新是最好的解决方案。 但仅对于填充 alpha 通道,只需调用 bitmap.setHasAlpha(true) 而不需要此更新。 - Sora Takayama

7
这个算法对我很有效。
private void FloodFill(Bitmap bmp, Point pt, int targetColor, int replacementColor) 
{
    Queue<Point> q = new LinkedList<Point>();
    q.add(pt);
    while (q.size() > 0) {
        Point n = q.poll();
        if (bmp.getPixel(n.x, n.y) != targetColor)
            continue;

        Point w = n, e = new Point(n.x + 1, n.y);
        while ((w.x > 0) && (bmp.getPixel(w.x, w.y) == targetColor)) {
            bmp.setPixel(w.x, w.y, replacementColor);
            if ((w.y > 0) && (bmp.getPixel(w.x, w.y - 1) == targetColor))
                q.add(new Point(w.x, w.y - 1));
            if ((w.y < bmp.getHeight() - 1)
                    && (bmp.getPixel(w.x, w.y + 1) == targetColor))
                q.add(new Point(w.x, w.y + 1));
            w.x--;
        }
        while ((e.x < bmp.getWidth() - 1)
                && (bmp.getPixel(e.x, e.y) == targetColor)) {
            bmp.setPixel(e.x, e.y, replacementColor);

            if ((e.y > 0) && (bmp.getPixel(e.x, e.y - 1) == targetColor))
                q.add(new Point(e.x, e.y - 1));
            if ((e.y < bmp.getHeight() - 1)
                    && (bmp.getPixel(e.x, e.y + 1) == targetColor))
                q.add(new Point(e.x, e.y + 1));
            e.x++;
        }
    }
}

但它消耗了大量的内存并使应用程序变慢。 - Satyajitsinh Raijada
1
请告诉我在主活动中如何调用此函数,以及如何在ImageView中使用它并应用OnTouch事件来填充区域等等... 请向我解释实现细节,我真的需要它。谢谢。 - Zia Ur Rahman

5

我让上面的算法跑得更快了。
使用getPixels()setPixels()而不是重复调用getPixel()

但这里有一个权衡。
这个算法需要为一个数组int [bmp.width * bmp.height]使用更多的内存空间。

private void floodFill_array(Bitmap bmp, Point pt, int targetColor, int replacementColor)
{
    if(targetColor == replacementColor)
        return;

    int width, height;
    int[] arrPixels;

    width = bmp.getWidth();
    height = bmp.getHeight();

    arrPixels = new int[width*height];
    bmp.getPixels(arrPixels, 0, width, 0, 0, width, height);

    Queue<Point> q = new LinkedList<Point>();
    q.add(pt);

    while (q.size() > 0) {
        Point n = q.poll();
        if (arrPixels[width*n.y + n.x] != targetColor)
            continue;

        Point w = n, e = new Point(n.x + 1, n.y);
        while ((w.x > 0) && (arrPixels[width*w.y + w.x] == targetColor)) {

            arrPixels[width*w.y + w.x] = replacementColor;  // setPixel

            if ((w.y > 0) && (arrPixels[width*(w.y-1) + w.x] == targetColor))
                q.add(new Point(w.x, w.y - 1));
            if ((w.y < height - 1)
                    && (arrPixels[width*(w.y+1) + w.x] == targetColor))
                q.add(new Point(w.x, w.y + 1));
            w.x--;
        }

        while ((e.x < width - 1)
                && (arrPixels[width*e.y + e.x] == targetColor)) {

            arrPixels[width*e.y + e.x] = replacementColor;  // setPixel

            if ((e.y > 0) && (arrPixels[width*(e.y-1) + e.x] == targetColor))
                q.add(new Point(e.x, e.y - 1));
            if ((e.y < height - 1)
                    && (arrPixels[width*(e.y+1) + e.x] == targetColor))
                q.add(new Point(e.x, e.y + 1));
            e.x++;
        }
    }

    bmp.setPixels(arrPixels, 0, width, 0, 0, width, height);
}

如果您想使用“容差”选项,请使用下面的代码。
int minR, maxR, minG, maxG, minB, maxB;  // instance values

private void floodFill_array(Bitmap bmp, Point pt, int targetColor, int replacementColor, int tolerance)
    {
        if(targetColor == replacementColor)
            return;

        /* tolerable values */
        minR = ((targetColor & 0xFF0000) >> 16) - tolerance;
        if(minR < 0) minR = 0;
        else minR = minR << 16;
        maxR = ((targetColor & 0xFF0000) >> 16) + tolerance;
        if(maxR > 0xFF) maxR = 0xFF0000;
        else maxR = maxR << 16;

        minG = ((targetColor & 0x00FF00) >> 8) - tolerance;
        if(minG < 0) minG = 0;
        else minG = minG << 8;
        maxG = ((targetColor & 0x00FF00) >> 8) + tolerance;
        if(maxG > 0xFF) maxG = 0x00FF00;
        else maxG = maxG << 8;

        minB = (targetColor & 0x0000FF) - tolerance;
        if(minB < 0) minB = 0;
        maxB = (targetColor & 0x0000FF) + tolerance;
        if(maxB > 0xFF) maxB = 0x0000FF;
        /* tolerable values */

        int width, height;
        int[] arrPixels;

        width = bmp.getWidth();
        height = bmp.getHeight();

        arrPixels = new int[width*height];
        bmp.getPixels(arrPixels, 0, width, 0, 0, width, height);

        Queue<Point> q = new LinkedList<Point>();
        q.add(pt);

        while (q.size() > 0) {

            Point n = q.poll();

            if(!isTolerable(arrPixels[width*n.y + n.x]))
                continue;

            Point w = n, e = new Point(n.x + 1, n.y);
            while ((w.x > 0) && isTolerable(arrPixels[width*w.y + w.x])) {
                arrPixels[width*w.y + w.x] = replacementColor;  // setPixel

                if ((w.y > 0) && isTolerable(arrPixels[width*(w.y-1) + w.x]))
                    q.add(new Point(w.x, w.y - 1));

                if ((w.y < height - 1) && isTolerable(arrPixels[width*(w.y+1) + w.x]))
                    q.add(new Point(w.x, w.y + 1));

                w.x--;
            }

            while ((e.x < width - 1) && isTolerable(arrPixels[width*e.y + e.x])) {
                arrPixels[width*e.y + e.x] = replacementColor;  // setPixel

                if ((e.y > 0) && isTolerable(arrPixels[width*(e.y-1) + e.x]))
                    q.add(new Point(e.x, e.y - 1));

                if ((e.y < height - 1) && isTolerable(arrPixels[width*(e.y+1) + e.x]))
                    q.add(new Point(e.x, e.y + 1));

                e.x++;
            }
        }

        bmp.setPixels(arrPixels, 0, width, 0, 0, width, height);
    }


    /** 
     * If the passed color is tolerable, return true.
     */
    private boolean isTolerable(int currentColor){
        int r = currentColor & 0xFF0000;
        int g = currentColor & 0x00FF00;
        int b = currentColor & 0x0000FF;

        if(r<minR || r>maxR || g<minG || g>maxG || b<minB || b>maxB)
            return false;   // less than or grater than tolerable values
        else
            return true;
    }

1
最好将FloodFill()作为返回Bitmap对象的方法。将其作为自定义View类的方法。你知道自定义View类有onDraw(Canvas canvas)方法。在onDraw(Canvas c)中,使用以下方式调用FloodFill()
//resize the picture that you want to fill to fit the whole display.
bitmap=Bitmap.createScaledBitmap(bitmap, canvas.getWidth(),canvas.getHeight(), true);

/*get the same bitmap flood fill it. we reassign the bit map to iself to keep it for the next and another flood fill 

currently_selected_point is a variable/object of type Point.
use the Point object to save your onTouchEvent(Event event) finger coordinates.

I used the Color.RED as a replacement color.

the target color is the color of the selected point currently_selected_point.
*/
bitmap=FloodFill(bitmap, currently_selected_point, bitmap.getPixel(currently_selected_point.x, currently_selected_point.y), Color.RED);
        canvas.drawBitmap(bitmap,0,0, paint);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接