javabufferedimagearea

Fast Way to Convert BufferedImage Pixels with a Specific Color into an Area


I am making an image editing application using Java and would like to make a select-by-color tool. A select-by-color tool makes my variable selection, a subclass of Area, highlight pixels in the selected image based on whether or not they are a specific color, an int. In this code snippet selectedImage is the BufferedImage that is used to create the Area.

int selectedColor = selectedImage.getRGB(...based on MouseEvent...);
for(int x = 0 ; x < selectedImage.getWidth() ; x++){
    for(int y = 0 ; y < selectedImage.getHeight() ; y++){
        if(selectedImage.getRGB(x, y) == selectedColor){
            selection.add(new Area(new Rectangle(x, y, 1, 1)));
        }
    }
}

I didn't really find anything useful that resolves my problem on StackOverflow or in the Oracle Docs. I found LookUpOp, which is almost capable of what I'm looing for but it only works by converting BufferedImages and I can't turn it into an Area.

This code works, however it is too slow to be an acceptable way of making the tool work.

EDIT: I tried using the answer matt gave me, and my code looks like this: (path is a Path2D object)

int selectedColor = selectedImage.getRGB(...based on MouseEvent...);
        
for(int x = 0 ; x < selectedImage.getWidth() ; x++){
    for(int y = 0 ; y < selectedImage.getHeight() ; y++){
        if(selectedImage.getRGB(x, y) == selectedColor){
            path.append(new Rectangle(x, y, 1, 1), false);
        }
    }
}
selection = new SelectionArea(path);

But it's still too slow. My computer specs are: 3.6 gH processor 8 GB of RAM There is nothing in the SelectionArea constructor that should make it slow.


Solution

  • I made a script to profile this. I found the rgb look up and the order of iterations to be irrelevant compared to updating the Area. As you suggested a Path2D can significantly improve the results.

    package orpal;
    
    import javax.swing.*;
    import java.awt.*;
    import java.awt.event.MouseAdapter;
    import java.awt.event.MouseEvent;
    import java.awt.geom.Area;
    import java.awt.geom.Path2D;
    import java.awt.geom.Point2D;
    import java.awt.image.BufferedImage;
    import java.util.Random;
    
    public class PixelPaths {
        static int w = 1920;
        static int h = 1024;
        Random ng = new Random(42);
        BufferedImage current = generateImage();
        Area selected = new Area();
        static class Result{
            Path2D p = new Path2D.Double();
            int count = 0;
            public Result(){
    
            }
            public void add(Shape s){
                p.append(s, false);
            }
            public Area build(){
                return new Area(p);
            }
        }
    
        BufferedImage generateImage(){
            int boxes = 1500;
            int size = 25;
            BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
    
            Graphics2D g = (Graphics2D)img.getGraphics();
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, w, h);
            g.setColor(Color.BLUE);
            for(int i = 0; i<boxes; i++){
                int x = (int)(ng.nextDouble()*w);
                int y = (int)(ng.nextDouble()*h);
                int wi = (int)Math.abs(ng.nextGaussian(0, size));
                int hi = (int)Math.abs(ng.nextGaussian(0, size));
                g.fillRect(x, y, wi, hi);
            }
            g.dispose();
            return img;
        }
        public Area generateArea(Point2D pt, BufferedImage img){
            Result result = new Result();
            long start = System.nanoTime();
            int chosen = img.getRGB((int)pt.getX(), (int)pt.getY());
            for(int i = 0; i<w; i++){
                for(int j = 0; j<h; j++){
                    //int p = pixels[j*w + i];
                    int p = img.getRGB(i, j);
                    if(p == chosen){
                        result.add( new Rectangle(i, j, 1, 1) );
                    }
                }
            }
            long path2d = System.nanoTime();
            Area finished = result.build();
            long finish = System.nanoTime();
    
            System.out.println("path2d: " + (path2d - start)*1e-9 + "area: " + (finish - path2d)*1e-9  + " seconds");
            return finished;
        }
    
        public void buildGui(){
            current = generateImage();
            JLabel label = new JLabel(new ImageIcon(current)){
                @Override
                public void paintComponent(Graphics g){
                    super.paintComponent(g);
                    g.setColor(Color.WHITE);
                    ((Graphics2D)g).draw(selected);
                }
            };
            label.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    Point2D pt = e.getPoint();
                    SwingWorker<Area, BufferedImage> s = new SwingWorker<>() {
                        @Override
                        protected Area doInBackground() throws Exception {
                            BufferedImage img = generateImage();
                            publish(img);
                            return generateArea(pt, img);
                        }
                        @Override
                        public void done(){
                            try {
                                selected = get();
                                label.repaint();
                            }catch(Exception e){
                                e.printStackTrace();
                            }
                        }
                        @Override
                        public void process(java.util.List<BufferedImage> imgs){
                            current = imgs.get(0);
                            label.setIcon(new ImageIcon(current));
                        }
                    };
                    s.execute();
                }
            });
            JFrame frame = new JFrame("Vector to Path");
            frame.add(label);
            frame.pack();
            frame.setVisible(true);
        }
        public static void main(String[] args){
            PixelPaths pp = new PixelPaths();
            EventQueue.invokeLater(pp::buildGui);
        }
    }
    
    

    This went from a 4.5 second runtime to a fraction of a second for 512x512. It seemed to scale linearly with larger images. It takes about 1.3s for a 2048 x 2048.

    Changing the order of iteration, ie iterating over j in the outer loop didn't do anything for the speed of the look up/image but it did affect the area.add(new Area(...)); call going from 6s to 4s.

    I've updated the example to be somewhat interactive.