存储图形对象是个好主意吗?

Posted

技术标签:

【中文标题】存储图形对象是个好主意吗?【英文标题】:Is storing Graphics objects a good idea? 【发布时间】:2015-09-30 22:51:03 【问题描述】:

我目前正在用java编写一个绘图程序,旨在具有灵活和全面的功能。它源于我前一天通宵写的最后一个项目。正因为如此,它有大量的错误,我一直在一个一个地解决(例如,我只能保存空文件,我的矩形没有正确绘制,但我的圆圈可以......)。

这一次,我一直在尝试将撤消/重做功能添加到我的程序中。但是,我无法“撤消”我所做的事情。因此,我想在每次触发 mouseReleased 事件时保存我的 BufferedImage 的副本。但是,由于某些图像的分辨率为 1920x1080,我认为这样做效率不高:存储它们可能会占用千兆字节的内存。

我不能简单地用背景颜色绘制相同的东西以撤消的原因是因为我有许多不同的画笔,它们基于Math.random() 绘制,并且因为有许多不同的层(在一个层中)。

然后,我考虑将用于绘制的Graphics 对象克隆到BufferedImage。像这样:

ArrayList<Graphics> revisions = new ArrayList<Graphics>();

@Override
public void mouseReleased(MouseEvent event) 
    Graphics g = image.createGraphics();
    revisions.add(g);

我以前没有这样做过,所以我有几个问题:

这样做是否还会浪费无意义的内存,比如克隆我的BufferedImages? 我是否有其他方法可以做到这一点?

【问题讨论】:

【参考方案1】:

不,存储Graphics 对象通常是个坏主意。 :-)

原因如下:通常,Graphics 实例是短暂的,用于在某种表面上绘画或绘图(通常是 (J)ComponentBufferedImage)。它保存这些绘图操作的状态,如颜色、笔触、缩放、旋转等。但是,它不保存绘图操作或像素的结果

因此,它不会帮助您实现撤消功能。像素属于组件或图像。因此,回滚到“之前的”Graphics 对象不会将像素修改回之前的状态。

以下是一些我知道可行的方法:

使用命令“链”(命令模式)来修改图像。命令模式与撤消/重做配合得非常好(并在 Action 中的 Swing/AWT 中实现)。从原始开始依次渲染所有命令。优点:每个命令中的状态通常不是那么大,允许您在内存中有许多步骤的 undo-buffer。缺点:经过大量操作,变慢了……

对于每个操作,存储整个 BufferedImage(就像您最初所做的那样)。优点:易于实施。缺点:你会很快耗尽内存。提示:您可以序列化图像,使撤消/重做占用更少的内存,但代价是更多的处理时间。

上述组合,使用命令模式/链的想法,但在合理的情况下使用“快照”(如BufferedImages)优化渲染。这意味着您不需要为每个新操作从头开始渲染所有内容(更快)。还要将这些快照刷新/序列化到磁盘,以避免内存不足(但如果可以的话,将它们保留在内存中,以提高速度)。您还可以将命令序列化到磁盘,以实现几乎无限的撤消。优点:如果做得好,效果很好。缺点:需要一些时间才能正确。

PS:对于以上所有情况,您需要使用后台线程(如SwingWorker 或类似线程)在后台更新显示的图像、将命令/图像存储到磁盘等,以保持响应式 UI。

祝你好运! :-)

【讨论】:

awt 动作和图像序列化...立即查看 :) @Zizouz212:从AbstractAction 类开始(如果需要,请阅读命令模式!)。对于图像序列化,您可以使用ImageIO 和无损格式(如 BMP、PNG 或 TIFF)。或者,假设您的程序使用标准化颜色模型,您也可以只将 BufferedImagebyteint 支持数组存储到磁盘。 天哪,我完全忘记了赏金,我完全要把它奖励给你。我希望75分还可以,但为了记录,我给这150分。 :) 我现在感觉很糟糕...... 还有另一种选择。图像的任何两个版本都将非常相似。您可以存储两个图像之间的差异,而不是整个图像,类似于无损视频压缩算法。 我不确定它是否会在计算上更加昂贵。这可能取决于命令的作用。重播一个简单的线条绘制命令会很快,但是填充操作会花费更多,并且将复杂的过滤器应用于图像的一部分会更多。如果你想支持一个非常长的撤消队列,重播几十个命令可能会花费更多,而不是每 N 个撤消步骤存储一个“关键帧”(完整图像,具有可选的无损压缩)并应用与最后一个关键帧的差异.然后,您也可以将关键帧想法与命令重放一起使用。有趣的问题。【参考方案2】:

想法#1,存储Graphics 对象根本行不通。 Graphics 不应被视为“持有”某些显示内存,而应被视为访问显示内存区域的句柄。在BufferedImage 的情况下,每个Graphics 对象将始终是相同给定图像内存缓冲区的句柄,因此它们都将代表相同的图像。更重要的是,您实际上无法对存储的 Graphics 执行任何操作:由于它们不存储任何内容,因此他们无法“重新存储”任何内容。

想法#2,克隆BufferedImages 是一个更好的想法,但你确实会浪费内存,并且很快就会用完。它仅有助于存储受绘图影响的图像部分,例如使用矩形区域,但它仍然需要大量内存。将这些撤消图像缓冲到磁盘可能会有所帮助,但它会使您的 UI 变慢且无响应,这不好;此外,它使您的应用程序更加复杂且容易出错

我的替代方法是将图像修改存储在一个列表中,在图像顶部从头到尾呈现。然后,撤消操作只需从列表中删除修改。

这需要您“具体化”图像修改,即通过提供执行实际绘图的void draw(Graphics gfx) 方法来创建一个实现单个修改的类。

正如您所说,随机修改会带来额外的问题。但是,关键问题是您使用Math.random() 创建随机数。相反,使用从固定种子值创建的Random 执行每个随机修改,以便(伪)随机数序列在每次调用draw() 时都相同,即每次抽奖具有完全相同的效果。 (这就是为什么它们被称为“伪随机”——生成的数字看起来是随机的,但它们与任何其他函数一样具有确定性。)

与存在内存问题的图像存储技术相比,这种技术的问题是许多修改可能会使 GUI 变慢,尤其是在修改计算量很大的情况下。为防止这种情况,最简单的方法是修复一个适当的可撤消修改列表的最大大小。如果添加新修改会超出此限制,请删除列表中最旧的修改并将其应用于支持 BufferedImage 本身。

以下简单的演示应用程序展示了这一切(以及如何)协同工作。它还包括一个不错的“重做”功能,用于重做已撤消的操作。

package ***;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.Random;
import javax.swing.*;

public final class UndoableDrawDemo
        implements Runnable

    public static void main(String[] args) 
        EventQueue.invokeLater(new UndoableDrawDemo()); // execute on EDT
    

    // holds the list of drawn modifications, rendered back to front
    private final LinkedList<ImageModification> undoable = new LinkedList<>();
    // holds the list of undone modifications for redo, last undone at end
    private final LinkedList<ImageModification> undone = new LinkedList<>();

    // maximum # of undoable modifications
    private static final int MAX_UNDO_COUNT = 4;

    private BufferedImage image;

    public UndoableDrawDemo() 
        image = new BufferedImage(600, 600, BufferedImage.TYPE_INT_RGB);
    

    public void run() 
        // create display area
        final JPanel drawPanel = new JPanel() 
            @Override
            public void paintComponent(Graphics gfx) 
                super.paintComponent(gfx);

                // display backing image
                gfx.drawImage(image, 0, 0, null);

                // and render all undoable modification
                for (ImageModification action: undoable) 
                    action.draw(gfx, image.getWidth(), image.getHeight());
                
            

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

        // create buttons for drawing new stuff, undoing and redoing it
        JButton drawButton = new JButton("Draw");
        JButton undoButton = new JButton("Undo");
        JButton redoButton = new JButton("Redo");

        drawButton.addActionListener(new ActionListener() 
            public void actionPerformed(ActionEvent e) 
                // maximum number of undo's reached?
                if (undoable.size() == MAX_UNDO_COUNT) 
                    // remove oldest undoable action and apply it to backing image
                    ImageModification first = undoable.removeFirst();

                    Graphics imageGfx = image.getGraphics();
                    first.draw(imageGfx, image.getWidth(), image.getHeight());
                    imageGfx.dispose();
                

                // add new modification
                undoable.addLast(new ExampleRandomModification());

                // we shouldn't "redo" the undone actions
                undone.clear();

                drawPanel.repaint();
            
        );

        undoButton.addActionListener(new ActionListener() 
            public void actionPerformed(ActionEvent e) 
                if (!undoable.isEmpty()) 
                    // remove last drawn modification, and append it to undone list
                    ImageModification lastDrawn = undoable.removeLast();
                    undone.addLast(lastDrawn);

                    drawPanel.repaint();
                
            
        );

        redoButton.addActionListener(new ActionListener() 
            public void actionPerformed(ActionEvent e) 
                if (!undone.isEmpty()) 
                    // remove last undone modification, and append it to drawn list again
                    ImageModification lastUndone = undone.removeLast();
                    undoable.addLast(lastUndone);

                    drawPanel.repaint();
                
            
        );

        JPanel buttonPanel = new JPanel(new FlowLayout());
        buttonPanel.add(drawButton);
        buttonPanel.add(undoButton);
        buttonPanel.add(redoButton);

        // create frame, add all content, and open it
        JFrame frame = new JFrame("Undoable Draw Demo");
        frame.getContentPane().add(drawPanel);
        frame.getContentPane().add(buttonPanel, BorderLayout.NORTH);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    

    //--- draw actions ---

    // provides the seeds for the random modifications -- not for drawing itself
    private static final Random SEEDS = new Random();

    // interface for draw modifications
    private interface ImageModification
    
        void draw(Graphics gfx, int width, int height);
    

    // example random modification, draws bunch of random lines in random color
    private static class ExampleRandomModification implements ImageModification
    
        private final long seed;

        public ExampleRandomModification() 
            // create some random seed for this modification
            this.seed = SEEDS.nextLong();
        

        @Override
        public void draw(Graphics gfx, int width, int height) 
            // create a new pseudo-random number generator with our seed...
            Random random = new Random(seed);

            // so that the random numbers generated are the same each time.
            gfx.setColor(new Color(
                    random.nextInt(256), random.nextInt(256), random.nextInt(256)));

            for (int i = 0; i < 16; i++) 
                gfx.drawLine(
                        random.nextInt(width), random.nextInt(height),
                        random.nextInt(width), random.nextInt(height));
            
        
    

【讨论】:

除了所采取的操作之外,您还可以每 X 步存储完整的图像,而不是最大撤消深度。这将允许快速绘制(因为您只会重播自上次快照以来的操作),同时允许无限撤消。 伟大的工作实际上为此实现了一个演示应用程序!不过,您可能已经使用了 Swing EditUndoManager 类。 :-)【参考方案3】:

大多数游戏(或程序)只保存必要的部分,这就是你应该做的。

一个矩形可以用宽度、高度、背景颜色、描边、轮廓等表示。所以你可以只保存这些参数而不是实际的矩形。 "矩形颜色:红色宽度:100 高度100"

对于程序的随机方面(画笔上的随机颜色),您可以保存种子或保存结果。 “随机种子:1023920”

如果程序允许用户导入图像,那么您应该复制并保存图像。

填充和效果(缩放/变换/发光)都可以像形状一样由参数表示。例如。 "缩放比例:2" "旋转角度:30"

所以你将所有这些参数保存在一个列表中,然后当你需要撤消时,你可以将参数标记为已删除(但实际上不要删除它们,因为你也希望能够重做)。然后,您可以擦除整个画布并根据参数减去标记为已删除的参数重新创建图像。

*对于像线条这样的东西,你可以将它们的位置存储在一个列表中。

【讨论】:

我喜欢这个答案,因为它充实并添加了一些关于如何继续在每个绘图命令上存储参数的细节。这与我描述为“命令链”的想法基本相同。【参考方案4】:

您将想要尝试压缩您的图像(使用 PNG 是一个好的开始,它有一些不错的过滤器以及真正有用的 zlib 压缩)。我认为最好的方法是

在修改之前复制图像 修改它 将副本与修改后的新图像进行比较 对于您未更改的每个像素,将该像素设为黑色透明像素。

这应该在 PNG 中压缩得非常非常好。尝试黑白,看看是否有区别(我认为不会有区别,但请确保将 rgb 值设置为相同的值,而不仅仅是 alpha 值,这样压缩效果会更好)。

将图像裁剪到更改的部分可能会获得更好的性能,但考虑到压缩(以及您现在必须保存并记住这一事实),我不确定您从中获得了多少偏移量)。

然后,由于您有一个 Alpha 通道,如果它们撤消,您只需将撤消图像放回当前图像的顶部即可。

【讨论】:

您可以将图像与上一张图像进行异或,以去除所有压缩前未更改的像素。

以上是关于存储图形对象是个好主意吗?的主要内容,如果未能解决你的问题,请参考以下文章

将设备标识符存储在 iOS 应用程序的钥匙串中是个好主意吗

将数以亿计的小图像存储到键/值存储或其他 nosql 数据库是个好主意吗?

Cassandra Scaling:为多节点 Cassandra DB 使用通用挂载是个好主意吗?

动态 SQL 表是个好主意吗?

在 2 个不同的存储库中管理 Azure DEVOPS Git DEV 和发布分支是个好主意吗?

typedef 指针是个好主意吗?