【问题标题】:Using BufferedImage in a thread-safe way efficiently?以线程安全的方式有效地使用 BufferedImage?
【发布时间】:2019-04-27 10:36:20
【问题描述】:

假设我有一个简单的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值矩阵感兴趣),通过读取原始矩阵并修改复制的矩阵来进行图像处理,然后用副本替换原始矩阵。以便将新状态而不是原始状态呈现到 UI 中。

两个问题:

  • 执行这两个进程的最有效(最快)线程安全方式是什么?以获得最大的图像处理性能。

  • 从自定义线程调用原始实例上的setPixel 是线程安全的,还是需要在 Swing 事件队列中调用它以避免与 paintComponent 读取冲突?

也许使用BufferedImage 不是最好的方法,在这种情况下,您也可以建议其他选项。但我目前想通过BufferedImage 专注于 Swing。

【问题讨论】:

  • 你指的是底层WritableRastersetPixel方法吗?除此之外,最终目标是什么并不完全清楚:您是否要避免绘制图像的“中间”状态?除此之外:关于图像的“跟踪”,事情在内部变得更加复杂(参见github.com/JetBrains/jdk8u_jdk/blob/master/src/share/classes/… - 粗略:图像是否保存在 VRAM 中)。
  • 此外,当前的答案基本上提出了“双缓冲”:创建一个修改过的 new 图像,并在最后简单地“交换”旧图像和新图像.这是一个可行的选择吗?
  • @Marco13 我说的是BufferedImage 对象的setPixel。但是,我可能还没有发现更有效的技术。最终目标是达到最高性能。我可以使用自己的线程调用setPixel 还是应该将处理算法排队到Swing。显示中间状态不是问题。但是对副本进行处理并用新图像替换旧图像(或矩阵)是首选方式,因为通常有复杂的算法需要创建新图像(例如作为膨胀和腐蚀)。
  • BufferedImage 类没有 setPixel 方法 - 你的意思是 setRGB 吗? (setPixel 仅存在于 WritableRaster 中)。在谈论最大性能时,需要考虑一些细节。我假设您不是在谈论实际绘制最终图像的性能(即 drawImage 调用是否需要 20 或 50 毫秒),甚至可能不是关于 setPixels 调用是否需要50 或 100ms,但主要是关于像素的原始处理。那么最好知道首选的像素格式 - 我猜是int[] rgb 数组?然后我会写一些提示
  • A BufferedImage 没有太多可变状态(除了它的像素数据)。你不能改变它的尺寸、颜色模型/样品模型等等。任何此类更改只能通过创建新的BufferedImage 实例来实现。因此,如果图像在更新过程中为repainted,将会发生的“最糟糕”的事情是,您会看到一些新的像素值与一些旧的像素值混合在一起。这可以通过在完成处理后强制重新绘制来轻松解决。您也可以在处理过程中忽略任何重绘,基本上是暂时“分离”图像。

标签: java swing thread-safety jpanel bufferedimage


【解决方案1】:

你是对的,你永远不知道面板的repaint() 什么时候会被执行。为了避免组件中出现任何不需要的视图,我会在后台线程中处理图像。这样,我不会太在意(当然我会)图像处理需要多长时间。最后,在处理完图像后,我会将它分享给 GUI(返回 EDT 线程)。

值得一提的是,在 Swing 中在后台运行任务的工具是 Swing Worker. Swing worker 将允许您在后台执行长时间任务,然后在适当的线程中更新 GUI(EDT - 事件调度线程)。

我创建了一个示例,其中框架由图像和“处理图像”按钮组成。

当按下按钮时,worker 启动。它处理图像(在我的例子中将图像裁剪为 90%),最后用新图像“刷新”视图,既美观又简单。

另外,为了回答您的问题:

在原始实例上调用 setPixel 是否是线程安全的? 自定义线程还是需要在 Swing 事件队列中调用 避免与paintComponent读取冲突?

您不必担心在图像处理任务期间将使用什么方法。只是,不要在那里更新摆动组件。完成后更新它们。

预览:

源代码:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-30
    • 2011-01-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多