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 BufferedImage
s 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.
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.