Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting touch coordinates not accurate in ImageView FloodFill Algorithm

I'm trying to use a Fill Flood algorithm for make a Cube Painting tool for an app.

This is the code of the algorithm:

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<>();
}

// 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;
    }
}

}

This is the part where I handle the touch event on my ImageView:

filler.setTolerance(150);

    imagen.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            int x = (int)motionEvent.getX();
            int y = (int)motionEvent.getY();
            filler.prepare();
            filler.floodFill(x, y);
            imagen.setImageBitmap(filler.getImage());
            return false;
        }
    });

The problem is that the coordinates are not accurate. I mean, wherever I touch the image it gets painted in other part in which I haven't.

Do I need to treat my event coordinates before sending them to the Filler algorithm? I have also tried a full-screen image and this keeps happening.

like image 360
Nacho Ramos Sánchez Avatar asked Jan 13 '17 22:01

Nacho Ramos Sánchez


People also ask

How to fill multiple colors boundary using flood fill algorithm?

In this method, a point or seed which is inside region is selected. This point is called a seed point. Then four connected approaches or eight connected approaches is used to fill with specified color. The flood fill algorithm has many characters similar to boundary fill. But this method is more suitable for filling multiple colors boundary.

Do you have first-hand experience with the flood fill algorithm?

If you've used the bucket fill tool in a paint or photo editing program like Photoshop or Gimp, then you already have first-hand experience with the flood fill algorithm. I'm going to demonstrate it with this image of a sweatshirt that keeps coming up in my targeted ads. Find the code for this post here. How does the flood fill algorithm work?

What is seed point in flood fill algorithm?

This point is called a seed point. Then four connected approaches or eight connected approaches is used to fill with specified color. The flood fill algorithm has many characters similar to boundary fill.

How to increase the accuracy of spot coordinates?

You can change the Project Units and/or the Units formatting of the Spot Coordinate type. Increase the decimal places to match or deeper. That'll help see the same degree of accuracy claimed in the source file. Your values are off by quite a lot (very nearly 10x) and that suggests a link scaling issue to me.


1 Answers

Your situation reminded me this blog post as it might be the same problem he fixed there. He had to take into account the transformation the bitmap went through before it was put into the ImageView.

Instead of using just getX() and getY() on the event in his OnTouchListener he used getPointerCoords(event)[0] and getPointerCoords(event)[1] respectively and created this method:

 final float[] getPointerCoords(MotionEvent e)
{
    final int index = e.getActionIndex();
    final float[] coords = new float[] { e.getX(index), e.getY(index) };
    Matrix matrix = new Matrix();
    getImageMatrix().invert(matrix); //his drawable view extends ImageView 
                                  //so it has access to the getImageMatrix.
    matrix.postTranslate(getScrollX(), getScrollY());
    matrix.mapPoints(coords);
    return coords;
}

Note that if indeed that's a solution for you - you can use the getImageMatrix() method on your imagen variable inside the method, or save the matrix into a final variable and use it in the OnTouchListener.

Also note that if that was the problem - you should also take the transformation into account when setting up the FillFlood to construct the dimensions accordingly (it would mean your previous attempt resulted in a too big/small a matrix compared to the original bitmap).

And all in one for you (one way to use this solution):

final Matrix transformationMatrix = new Matrix();
imagen.getImageMatrix().invert(transformationMatrix);
transformationMatrix.postTranslate(imagen.getScrollX(), imagen.getScrollY());
imagen.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        float[] transformedCoords = getPointerCoords(event);
        int x = (int)transformedCoords[0];
        int y = (int)transformedCoords[1];
        filler.prepare();
        filler.floodFill(x, y);
        imagen.setImageBitmap(filler.getImage());
        return false;
    }

    final float[] getPointerCoords(MotionEvent e) {
        final int index = e.getActionIndex();
        final float[] coords = new float[] { e.getX(index), e.getY(index) };
        transformationMatrix.mapPoints(coords);
        return coords;
    }
});

Hope that solves it.

like image 118
et_l Avatar answered Nov 09 '22 05:11

et_l