Эффективно ли использовать BufferedImage потокобезопасным способом? - PullRequest
0 голосов
/ 27 апреля 2019

Допустим, у меня есть этот простой javax.swing.JPanel компонент, который используется только для отображения BufferedImage.

public class Scratch extends JPanel {
    private BufferedImage image;

    public Scratch(BufferedImage image) {
        this.image = image;
    }

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

Иногда для этого компонента вызывается метод repaint(), указывающий, что Swing долженперерисовать это.Однако использование переопределенного метода paintComponent обрабатывается Swing внутренне.Поэтому я не могу точно контролировать, когда читается BufferedImage.

Теперь, допустим, у меня есть несколько алгоритмов обработки изображений, выполняемых для введенного BufferedImage.Обычно это выполняется двумя способами:

  • Считывание текущего состояния изображения и изменение его с помощью setPixel.
  • Создание копии текущего состояния изображения (интересует толькоМатрица значений RGB), выполняющая обработку изображения путем считывания исходной матрицы и изменения скопированной матрицы, а затем замены исходной матрицы копией.Так что новое состояние будет отображаться вместо исходного в пользовательском интерфейсе.

Два вопроса:

  • Какой поток будет наиболее эффективным (самым быстрым)способ выполнения обоих этих процессов?Для максимальной производительности обработки изображений.

  • Будет ли потокобезопасен вызов setPixel для исходного экземпляра из пользовательского потока, или его нужно будет вызывать в очереди событий Swing дляизбежать конфликтов с показаниями paintComponent?

Возможно, использование BufferedImage не лучший способ, в этом случае вы можете предложить и другие варианты.Но в настоящее время я хотел бы сосредоточиться на Swing с BufferedImage.

1 Ответ

2 голосов
/ 27 апреля 2019

Вы правы, вы никогда не знаете, когда будет выполнена repaint() панели.Чтобы избежать нежелательных представлений в компоненте, я обработал бы изображение в фоновом потоке.Таким образом, мне было бы все равно (конечно, я бы), сколько времени займет обработка изображения.Наконец, после обработки изображения я хотел бы поделиться им с GUI (обратно в поток EDT).

Стоит отметить, что инструмент для запуска задач в фоновом режиме в Swing - это Swing Worker. Рабочий Swing позволит вам выполнять долгосрочные задачи в фоновом режиме, а затем обновлять графический интерфейс в соответствующем потоке (EDT - поток рассылки событий).

Я создал пример, в котором кадр состоит изИзображение и кнопка «Изображение процесса».

При нажатии кнопки рабочий запускается.Он обрабатывает изображение (в моем случае он обрезает изображение до 90% ) и, наконец, «освежает» вид новым изображением, красиво и легко.

Кроме того, чтобы ответить на ваш вопрос:

Будет ли потокобезопасным вызывать setPixel для исходного экземпляра из пользовательского потока или его необходимо вызывать вКачать очередь событий, чтобы избежать конфликтов с чтением paintComponent?

Вам не нужно беспокоиться о том, какой метод вы собираетесь использовать во время задачи обработки изображения.Просто не обновляйте там компоненты свинга.Обновите их после процесса.

Предварительный просмотр:

enter image description here

Исходный код:

import java.awt.BorderLayout;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutionException;

import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

public class TestImage extends JFrame {
    private Scratch scratch;
    private JButton crop;

    public TestImage() {
        super("Process image");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout());
        try {
            BufferedImage img = loadImage();
            scratch = new Scratch(img);
            getContentPane().add(scratch, BorderLayout.CENTER);
        } catch (IOException e) {
            e.printStackTrace();
        }
        crop = new JButton("Process image");
        crop.addActionListener(e -> processImage());
        getContentPane().add(crop, BorderLayout.PAGE_END);
        setSize(500, 500);
        setLocationRelativeTo(null);
    }

    private void processImage() {
        crop.setEnabled(false);
        crop.setText("Processing image...");
        new ImageProcessorWorker(scratch, () -> {
            crop.setEnabled(true);
            crop.setText("Process image");
        }).execute();
    }

    private BufferedImage loadImage() throws IOException {
        File desktop = new File(System.getProperty("user.home"), "Desktop");
        File image = new File(desktop, "img.png");
        return ImageIO.read(image);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new TestImage().setVisible(true));
    }

    public static class Scratch extends JPanel implements ImageView {
        private static final long serialVersionUID = -5546688149216743458L;
        private BufferedImage image;

        public Scratch(BufferedImage image) {
            this.image = image;
        }

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

        @Override
        public BufferedImage getImage() {
            return image;
        }

        @Override
        public void setImage(BufferedImage img) {
            this.image = img;
            repaint(); //repaint the view after image changes
        }
    }

    public static class ImageProcessorWorker extends SwingWorker<BufferedImage, Void> {
        private ImageView view;
        private Runnable restoreTask;

        public ImageProcessorWorker(ImageView v, Runnable restoreViewTask) {
            view = v;
            restoreTask = restoreViewTask;
        }

        @Override
        protected BufferedImage doInBackground() throws Exception {
            BufferedImage image = view.getImage();
            image = crop(image, 0.9d);
            Thread.sleep(5000); // Assume it takes 5 second to process
            return image;
        }

        /*
         * Taken from
         * https://stackoverflow.com/questions/50562388/how-to-crop-image-in-java
         */
        public BufferedImage crop(BufferedImage image, double amount) throws IOException {
            BufferedImage originalImage = image;
            int height = originalImage.getHeight();
            int width = originalImage.getWidth();

            int targetWidth = (int) (width * amount);
            int targetHeight = (int) (height * amount);
            // Coordinates of the image's middle
            int xc = (width - targetWidth) / 2;
            int yc = (height - targetHeight) / 2;

            // Crop
            BufferedImage croppedImage = originalImage.getSubimage(xc, yc, targetWidth, // widht
                    targetHeight // height
            );
            return croppedImage;
        }

        @Override
        protected void done() {
            try {
                BufferedImage processedImage = get();
                view.setImage(processedImage);
                if (restoreTask != null)
                    restoreTask.run();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
            super.done();
        }

    }

    public static interface ImageView {
        BufferedImage getImage();

        void setImage(BufferedImage img);
    }
}
...