0

I have a BufferedImage whit some shapes drawn down on it, I'm trying to fill those whit a random color; I have made a method whit recursion to do this job, and it does it, but:

  1. It's painfully slow
  2. It requires a lot of stack memory (I had to scale it up to 1GB to avoid problems)

I heard that is always possible to avoid recursion but I can't come up whit a more efficient way to do it, any help would be appreciated

The method returns an ArrayList of int[] which represent all the pixel that I have to fill (then I color those pixel by pixel) It start from a random point that hasn't be colored yet.

I would like to have a result similar to the one that we can have using the instrument "fill" on Microsoft Paint

Here's the code of the method that find all points in a section:

ArrayList<int[]> populatesSection(ArrayList<int[]> section, int[] is) {
  int[][] neighbors=new int[4][2];
  int i;

  neighbors[0][0]=is[0];
  neighbors[0][1]=is[1]+1;

  neighbors[1][0]=is[0]-1;
  neighbors[1][1]=is[1];

  neighbors[2][0]=is[0];
  neighbors[2][1]=is[1]-1;

  neighbors[3][0]=is[0]+1;
  neighbors[3][1]=is[1];


  toBeColored.remove(contains(toBeColored, is));
  section.add(is);
  

  for(i=0;i<neighbors.length;i++)
    if(isValid(neighbors[i]) && contains(toBeColored, neighbors[i])>=0 && contains(section, neighbors[i])<0)
      populatesSection(section, neighbors[i]);

  return section;
}

initial BufferedImage:

Example of initial BufferedImage

colored BufferedImage:

Example of the colored BufferedImage

c0der
  • 18,467
  • 6
  • 33
  • 65
  • I think coloring pixel by pixel is already slow, even without recursion. Look into how you can maybe fill a certain range of pixels within a set boundary. Nonetheless, you could transform your recursive approach to an iterative one, using a ```while``` loop and your "own stack" or you could use [memoization](https://dzone.com/articles/memoization-make-recursive-algorithms-efficient) with a hash table or array – NeonFire Aug 18 '21 at 17:14
  • This can be done by scanning the image in raster order, I believe with Time and Space complexity of O(n log log log ... log n ). – Mark Lavin Aug 18 '21 at 17:18
  • did you tried to use stream to iterate for all pixel and do that coloring inside the stream –  Aug 18 '21 at 20:17

3 Answers3

2

I borrowed the flood method from camickr'a answer and enhanced it to auto flood fill the entire image.
I also took the long flood-related calculations off the EDT by performing it on a SwingWorker:

import java.awt.*;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.util.*;
import javax.imageio.ImageIO;
import javax.swing.*;

public class FloodFill extends JPanel {

    private final BufferedImage image;
    private static final Color background = Color.WHITE;
    private final Random rnd = new Random();

    FloodFill(BufferedImage image) {
        this.image = image;
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(image.getWidth(), image.getHeight());
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.drawImage(image, 0,0, this);
    }

    private void autoFloodFill(){
        //take long process off the EDT by delegating it to a worker
        new FloodFillWorker().execute();
    }

    private Optional<Point> findBackgrounPoint() {

        int backgroundRGB = background.getRGB();

        for (int x = 0; x < image.getWidth(); x++) {
            for (int y = 0; y < image.getHeight(); y++) {
                int imageRGB = image.getRGB(x, y);
                if(imageRGB == backgroundRGB)
                    return Optional.of(new Point(x, y));
            }
        }
        return Optional.empty();
    }

    private Color randomColor() {
        return new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
    }

    class FloodFillWorker extends SwingWorker<Void, Void>{

        //todo set sleep to 0
        private final long SLEEP = 500; //used to slow sown for demo purposed.

        @Override
        protected Void doInBackground() throws Exception {

            Optional<Point> backgroundPoint = findBackgrounPoint();
            while(backgroundPoint.isPresent()){
                floodFill(backgroundPoint.get(), randomColor());
                SwingUtilities.invokeLater(()-> repaint());  //invoke repaint on EDT
                backgroundPoint = findBackgrounPoint(); //find next point
                Thread.sleep(SLEEP);
            }
            return null;
        }

        private void floodFill(Point point, Color replacement)  {

            int width = image.getWidth();
            int height = image.getHeight();
            int targetRGB = image.getRGB(point.x, point.y);
            int replacementRGB = replacement.getRGB();

            Queue<Point> queue = new LinkedList<>();
            queue.add( point );

            while (!queue.isEmpty()){

                Point p = queue.remove();

                int imageRGB = image.getRGB(p.x, p.y);
                if (imageRGB != targetRGB) { continue;  }

                //Update the image and check surrounding pixels
                image.setRGB(p.x, p.y, replacementRGB);

                if (p.x > 0) {
                    queue.add( new Point(p.x - 1, p.y) );
                }
                if (p.x +1 < width) {
                    queue.add( new Point(p.x + 1, p.y) );
                }
                if (p.y > 0) {
                    queue.add( new Point(p.x, p.y - 1) );
                }
                if (p.y +1 < height) {
                    queue.add( new Point(p.x, p.y + 1) );
                }
            }
        }
    }

    public static void main(String[] args)  throws Exception    {

        String imageAdress = "https://i.stack.imgur.com/to4SE.png";
        BufferedImage image = ImageIO.read(new URL(imageAdress));

        FloodFill ff = new FloodFill(image);

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(ff);
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible( true );
        ff.autoFloodFill();
    }
}

A slowed down demo:

enter image description here


Run it on line

c0der
  • 18,467
  • 6
  • 33
  • 65
  • (1+) nice example. However, you should not dispose a Graphics object that is passed to the painting method. You should only dispose of Graphics objects that your painting code creates. I also question if the code should be executed in the SwingWorker since you are changing the state of your component by changing the pixels of the image.. It is my understanding that all updates to Swing components should be done on the EDT. Your code invokes repaint() so the component could be painting itself as you are updating the image. – camickr Aug 19 '21 at 20:17
  • @camickr Good point about the repaint, thank you. I changed it to `SwingUtilities.invokeLater(()-> repaint()`. Using publish() and process() would have been better but it would also make this demo more difficult to follow. I don't really understand why not dispose the g object. I'll need to look into it. – c0der Aug 20 '21 at 04:13
  • *I don't really understand why not dispose the g object* The paint() method will create a Graphics object and pass it to the three painting methods. See: [A closer look at the painting mechanism]. (https://docs.oracle.com/javase/tutorial/uiswing/painting/closer.html). The paint() method will then dispose of the Graphics: object: https://github.com/openjdk/jdk/blob/master/src/java.desktop/share/classes/javax/swing/JComponent.java. In this example it will not cause a problem. but if you don't create the Graphics objects then don't dispose of the object as it may be used elsewhere. – camickr Aug 20 '21 at 14:09
  • @camickr I did not know that. Thank you !. Searching for information about it, on SO, I found explanations mainly in comments. I think a Q and A may help others. Consider posting this helpful information as an [answer](https://stackoverflow.com/questions/68864382/swing-custom-painting-should-graphic-object-be-disposed) – c0der Aug 20 '21 at 15:10
1

Here is a flood fill method I found on the web a long time ago:

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.*;
import java.util.LinkedList;
import java.util.Queue;
import javax.imageio.ImageIO;
import javax.swing.*;

public class ImageUtil
{
    public static void floodFill(BufferedImage image, Point point, Color target, Color replacement)
    {
        int width = image.getWidth() - 1;
        int height = image.getHeight() - 1;
        int targetRGB = target.getRGB();
        int replacementRGB = replacement.getRGB();

        Queue<Point> queue = new LinkedList<Point>();
        queue.add( point );

        while (!queue.isEmpty())
        {
            Point p = queue.remove();
            int imageRGB = image.getRGB(p.x, p.y);
            Color imageColor = new Color(imageRGB);

            if (imageRGB != targetRGB) continue;

            //  Update the image and check surrounding pixels

            image.setRGB(p.x, p.y, replacementRGB);

            if (p.x > 0) queue.add( new Point(p.x - 1, p.y) );
            if (p.x < width) queue.add( new Point(p.x + 1, p.y) );
            if (p.y > 0) queue.add( new Point(p.x, p.y - 1) );
            if (p.y < height) queue.add( new Point(p.x, p.y + 1) );
        }
    }

    public static void main(String[] args)
        throws Exception
    {
        if (args.length != 1) {
            System.err.println("ERROR: Pass filename as argument.");
            return;
        }

        String fileName = args[0];

        BufferedImage image = ImageIO.read( new File( fileName ) );

        JLabel north = new JLabel( new ImageIcon( fileName ) );
        JLabel south = new JLabel( new ImageIcon( image ) );

        north.addMouseListener( new MouseAdapter()
        {
            @Override
            public void mousePressed(MouseEvent e)
            {
                try
                {
                    BufferedImage image = ImageIO.read( new File( fileName ) );
                    int rgb = image.getRGB(e.getX(), e.getY());
                    Color target = new Color( rgb );

                    floodFill(image, e.getPoint(), target, Color.ORANGE);

                    south.setIcon( new ImageIcon(image) );
                }
                catch (Exception e2) {}
            }
        });

        JLabel label = new JLabel("Click on above image for flood fill");

        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(north, BorderLayout.NORTH);
        frame.add(label);
        frame.add(south, BorderLayout.SOUTH);
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible( true );
    }
}

I'll let you decide if it is any more efficient.

camickr
  • 321,443
  • 19
  • 166
  • 288
1

You'd typically use a Queue to implement a flood fill, seeded each time you detect a background (in your case white) pixel during a raster scan of the image.

static int[] xd = {-1, 0, 1,  0};
static int[] yd = { 0, 1, 0, -1};

static BufferedImage colorImage(BufferedImage im, Color background, Color[] colors) throws Exception
{       
    im = ensureImageType(im, BufferedImage.TYPE_INT_ARGB);
    
    int width = im.getWidth();
    int height = im.getHeight();
    
    int[] pix = ((DataBufferInt)im.getRaster().getDataBuffer()).getData();
    
    Queue<Integer> q = new LinkedList<>();      
    for (int i = 0, r = 0; i < width * height; i++) 
    {
        if(pix[i] == background.getRGB())
        {
            q.add(i);
            pix[i] = colors[r++ % colors.length].getRGB();
            
            while(!q.isEmpty())
            {
                int pos = q.poll();
                int x = pos % width;
                int y = pos / width;
    
                for(int j = 0; j < 4; j++)
                {                       
                    int xn = x + xd[j];
                    if(xn >= 0 && xn < width)
                    {
                        int yn = y + yd[j];
                        if(yn >= 0 && yn < height)
                        {
                            int npos = yn * width + xn;
                            if(pix[npos] == background.getRGB())
                            {
                                q.add(npos);
                                pix[npos] = pix[i];
                            }
                        }
                    }
                }
            }
        }
    }

    return im;
}

With the helper class:

static BufferedImage ensureImageType(BufferedImage im, int imageType)
{
    if (im.getType() != imageType)
    {
        BufferedImage nim = new BufferedImage(im.getWidth(), im.getHeight(), imageType);
        Graphics g = nim.getGraphics();
        g.drawImage(im, 0, 0, null);
        g.dispose();
        im = nim;
    }
    return im;
}

Test:

Color[] colors = {Color.BLUE, Color.RED, Color.GREEN, Color.ORANGE,                  
                  Color.PINK, Color.CYAN, Color.MAGENTA, Color.YELLOW};

BufferedImage im = ImageIO.read(new File("to4SE.png"));         
im = colorImage(im, Color.WHITE, colors);       
ImageIO.write(im, "png", new File("color.png"));

Output:

enter image description here

RaffleBuffle
  • 5,396
  • 1
  • 9
  • 16