【问题标题】:Stacking rotated images into one combined image将旋转的图像堆叠成一张组合图像
【发布时间】:2017-04-28 22:11:18
【问题描述】:

我正在编写一个扑克游戏,目前正在尝试编写一个生成给定手牌图像的类。我最初只是通过将 5 张卡片中的每一张的图像并排组合来制作一张图像。结果是这样的:

然后我决定将手牌堆叠在一起并展开,这样会更好,因为一个人会拿着一手牌。

这是迄今为止我能做到的最好的:

如您所见,最后三张牌看起来应该是,但前三张牌在第三张牌的左侧被切掉。

这是我现在的代码(它不是最干净的,因为我一直试图让它工作,不管它需要什么)

private static final int CARD_WIDTH = 500;
private static final int CARD_HEIGHT = 726;
private static final double ROTATION = 20.0;

public void createImage(HandOfCards hand) throws IOException {
    int handImageWidth = (int) (2 * (Math.sin(degreesToRadian(ROTATION)) * CARD_HEIGHT + Math.cos(degreesToRadian(ROTATION)) * CARD_WIDTH)- CARD_WIDTH);
    int handImageHeight = (int) (CARD_HEIGHT + Math.sin(degreesToRadian(ROTATION)) * CARD_WIDTH); 

    BufferedImage handImage = new BufferedImage(handImageWidth, handImageHeight, BufferedImage.TYPE_INT_ARGB);
    Graphics2D graphics = (Graphics2D) handImage.getGraphics();

    int xPos = handImageWidth / 2 - CARD_WIDTH / 2;
    int yPos = 0;
    int xAnchor = CARD_WIDTH; 
    int yAnchor = CARD_HEIGHT;

    double rotation = -ROTATION;        
    for (int i = 0; i < HandOfCards.HAND_SIZE; i++) {
        if (i == 3) xAnchor = 0;

        PlayingCard card = hand.getCard(i);
        BufferedImage cardImage = ImageIO.read(new File("cardImages/" + card + ".png"));

        AffineTransform transform = new AffineTransform();
        transform.rotate(degreesToRadian(rotation), xAnchor, yAnchor);
        AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
        cardImage = transformOp.filter(cardImage, null);

        graphics.drawImage(cardImage, xPos, yPos, null);
        rotation += ROTATION / 2;
    }

private double degreesToRadian(double degrees) {
    return (degrees * Math.PI) / 180.0;
}

编辑

为了让事情更清楚,这里是只执行循环(只绘制第一张卡片)并且背景着色以显示整个图像的大小时的结果。

【问题讨论】:

    标签: java image graphics bufferedimage affinetransform


    【解决方案1】:

    documentation of AffineTransformOp#filter 中说明了您观察到的行为的原因:

    getBounds2D(BufferedImage)返回的矩形坐标不一定与该方法返回的BufferedImage坐标相同。 如果矩形的左上角坐标为负数,则不绘制矩形的这部分

    当你用这样的语句打印每张卡片的边界时

    System.out.println("Bounds: "+transformOp.getBounds2D(cardImage));
    

    你会看到边界是负数(就像人们在向左旋转卡片时所期望的那样)。

    这可以通过调整AffineTransform 以始终产生 边界并使用非null 目标图像调用filter 方法来避免(在您的情况下:使用包含手的图像 - 即所有卡片图像)。


    (这个^是问题的实际答案。剩下的部分可能会被忽略,或者被认为是我有太多空闲时间的证据)


    话虽如此,我想提出一个不同的解决方案,因为当前的方法在不同的层面上存在一些问题。


    在最高级别:为什么要创建此图像?我猜你正在实现一个纸牌游戏。而在这样的游戏中,你可能会有数百个不同的“手”。为什么要为每个手创建一个图像?

    您可以直接绘制旋转后的图像,而不是为每只手创建一个图像。粗略地说:无需将图像绘制到新图像的Graphics 中,您只需将它们绘制到您正在实际绘制手部的JPanelGraphics 中。

    但考虑到区别仅在于您正在绘制的 Graphics 对象,这很容易在以后更改(如果相应地实施),并且您可能有实际原因创建这些图像。


    在最底层:函数degreesToRadian 应完全替换为Math.toRadians


    下面是一个示例,实现为MCVE。 (这意味着它不使用HandOfCardsPlayingCards 类。相反,它对BufferedImage 对象列表进行操作。这些图像实际上是在运行时从维基百科下载的。

    这个例子的核心是RotatedPlayingCardsPainter。它允许您将(旋转的)卡片绘制成Graphics2D。当您尝试时,这种方法的一个优点可能会变得很明显:您可以使用滑块来动态更改卡片之间的角度。 (在这里为您的游戏想象一些花哨的动画......)

    (但如果您愿意,它还包含一个createImage 方法,允许您创建图像,如问题中最初所做的那样)

    当您阅读代码时,您会看到 each 卡的 AffineTransform 实例是在 createTransform 方法中创建的。在那里,我添加了一些任意的、神奇的因素来稍微改变卡片彼此的位置,并让它们看起来更像“粉丝”。

    比较这张图片,没有魔法因素

    给那个拥有魔力的人:

    我认为后者看起来更“现实”,但这可能是品味问题。


    另一个附注:直接绘制图像的一个缺点(与AffineTransformOp 方法相比)是图像的边界可能看起来参差不齐,无论过滤和抗锯齿设置如何。这是因为在图像的边界处没有什么可以插值的。在给定的程序中,使用addBorder 方法规避了这一点,该方法为图像添加了一个 1 像素的透明边框,以确保图像在旋转时看起来不错并且边框看起来很平滑。

    代码如下:

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Rectangle;
    import java.awt.RenderingHints;
    import java.awt.geom.AffineTransform;
    import java.awt.geom.Rectangle2D;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.net.URL;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    import javax.imageio.ImageIO;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JSlider;
    import javax.swing.SwingUtilities;
    
    public class RotatedPlayingCards
    {
        public static void main(String[] args)
        {
            try
            {
                List<BufferedImage> images = loadTestImages();
                SwingUtilities.invokeLater(() -> createAndShowGui(images));
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
    
        }
    
        private static void createAndShowGui(List<BufferedImage> images)
        {
            JFrame f = new JFrame();
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            RotatedPlayingCardsPanel cardsPanel = 
                new RotatedPlayingCardsPanel(images);
    
            JSlider angleDegSlider = new JSlider(0, 20, 10);
            angleDegSlider.addChangeListener(e -> {
                double rotationAngleRad = Math.toRadians(angleDegSlider.getValue());
                cardsPanel.setRotationAngleRad(rotationAngleRad);
            });
            JPanel controlPanel = new JPanel();
            controlPanel.add(angleDegSlider);
            f.getContentPane().add(controlPanel, BorderLayout.NORTH);
    
            f.getContentPane().add(cardsPanel, BorderLayout.CENTER);
            f.setSize(500,500);
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        }
    
        private static List<BufferedImage> loadTestImages() throws IOException
        {
            String basePath = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
            List<String> subPaths = Arrays.asList(
              "3/36/Playing_card_club_A.svg/480px-Playing_card_club_A.svg.png",
              "2/20/Playing_card_diamond_4.svg/480px-Playing_card_diamond_4.svg.png",
              "9/94/Playing_card_heart_7.svg/480px-Playing_card_heart_7.svg.png",
              "2/21/Playing_card_spade_8.svg/480px-Playing_card_spade_8.svg.png",
              "b/bd/Playing_card_spade_J.svg/480px-Playing_card_spade_J.svg.png",
              "0/0b/Playing_card_diamond_Q.svg/480px-Playing_card_diamond_Q.svg.png",
              "2/25/Playing_card_spade_A.svg/480px-Playing_card_spade_A.svg.png"
            );
            List<BufferedImage> result = new ArrayList<BufferedImage>();
            for (String subPath : subPaths)
            {
                String path = basePath + subPath;
                System.out.println("Loading "+path);
                BufferedImage image = ImageIO.read(new URL(path));
    
                image = scale(image, 0.3);
                image = addBorder(image);
                result.add(image);
            }
            return result;
        }
    
        // Scale the given image by the given factor
        private static BufferedImage scale(
            BufferedImage image, double factor) 
        {
            int w = (int)(image.getWidth() * factor);
            int h = (int)(image.getHeight() * factor);
            BufferedImage scaledImage = new BufferedImage(w, h, image.getType());
            Graphics2D g = scaledImage.createGraphics();
            g.setRenderingHint(
                RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.drawImage(image, 0, 0, w, h, null);
            g.dispose();
            return scaledImage;
        }
    
        // Add a 1-pixel transparent border to the given image, to avoid
        // aliasing artifacts when the image is rotated
        private static BufferedImage addBorder(
            BufferedImage image) 
        {
            int w = image.getWidth();
            int h = image.getHeight();
            BufferedImage result = new BufferedImage(w + 2, h + 2, image.getType());
            Graphics2D g = result.createGraphics();
            g.setColor(new Color(0,0,0,0));
            g.fillRect(0, 0, w + 2, h + 2);
            g.drawImage(image, 1, 1, w, h, null);
            g.dispose();
            return result;
        }    
    
    }
    
    class RotatedPlayingCardsPanel extends JPanel
    {
        private List<BufferedImage> images;
        private double rotationAngleRad;
    
        public RotatedPlayingCardsPanel(List<BufferedImage> images)
        {
            this.images = images;
            this.rotationAngleRad = Math.toRadians(10);
        }
    
        public void setRotationAngleRad(double rotationAngleRad)
        {
            this.rotationAngleRad = rotationAngleRad;
            repaint();
        }
    
        @Override
        protected void paintComponent(Graphics gr)
        {
            super.paintComponent(gr);
            Graphics2D g = (Graphics2D)gr;
            g.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
            g.setRenderingHint(
                RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    
            g.translate(200, 100);
            RotatedPlayingCardsPainter.drawImages(
                g, images, rotationAngleRad);
        }
    }
    
    
    class RotatedPlayingCardsPainter
    {
        public static BufferedImage createImage(
            List<? extends BufferedImage> images, double rotationAngleRad)
        {
            Rectangle2D bounds = computeBounds(images, rotationAngleRad);
            BufferedImage image = new BufferedImage(
                (int)bounds.getWidth(), (int)bounds.getHeight(), 
                BufferedImage.TYPE_INT_ARGB);
            Graphics2D graphics = image.createGraphics();
            graphics.setRenderingHint(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
            graphics.setRenderingHint(
                RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            graphics.translate(-bounds.getX(), -bounds.getY());
            drawImages(graphics, images, rotationAngleRad);
            graphics.dispose();
            return image;
        }
    
        public static Rectangle2D computeBounds(
            List<? extends BufferedImage> images, double rotationAngleRad)
        {
            Rectangle2D totalBounds = null;
            for (int i=0; i<images.size(); i++)
            {
                BufferedImage image = images.get(i);
                AffineTransform transform = createTransform(
                    i, images.size(), image.getWidth(), image.getHeight(), 
                    rotationAngleRad);
                Rectangle2D imageBounds = new Rectangle2D.Double(0.0, 0.0, 
                    image.getWidth(), image.getHeight());
                Rectangle2D transformedBounds = 
                    transform.createTransformedShape(imageBounds).getBounds();
                if (totalBounds == null)
                {
                    totalBounds = transformedBounds;
                }
                else
                {
                    Rectangle.union(transformedBounds, totalBounds, totalBounds);
                }
            }
            return totalBounds;
        }
    
        public static void drawImages(Graphics2D g, 
            List<? extends BufferedImage> images, double rotationAngleRad)
        {
            for (int i=0; i<images.size(); i++)
            {
                AffineTransform oldAt = g.getTransform();
                BufferedImage image = images.get(i);
                AffineTransform transform = createTransform(
                    i, images.size(), image.getWidth(), image.getHeight(), 
                    rotationAngleRad);
                g.transform(transform);
                g.drawImage(image, 0, 0, null);
                g.setTransform(oldAt);
            }
        }
    
        private static AffineTransform createTransform(
            int index, int total, double width, double height, 
            double rotationAngleRad)
        {
            double startAngleRad = (total - 1) * 0.5 * rotationAngleRad;
            double angleRad = index * rotationAngleRad - startAngleRad;
            AffineTransform transform = new AffineTransform();
    
            // A magic factor to shift the images slightly, to give 
            // them a more fan-like appearance. Just set it to 0.0
            // or remove it if you don't like it.
            double magicFactor = 0.2;
    
            double magicOffsetFactor = 
                (1.0 - index) * magicFactor * rotationAngleRad;
            double magicOffsetX = -width * magicOffsetFactor;
            double magicOffsetY = height * magicOffsetFactor;
            transform.translate(magicOffsetX, height + magicOffsetY);
            transform.rotate(angleRad);
            transform.translate(0, -height);
            return transform;
        }
    
    }
    

    【讨论】:

    • 抱歉,我现在无法阅读您的全部答案 - 我稍后再看。在我自己的解决方案中解决问题方面,当您说始终使用带有AffineTransformation 的正旋转时,这是否意味着我必须旋转 +340° 而不是旋转 -20°?
    • @KOB 否。当您打印边界时,与答案中的第一行代码一样,您将看到一些输出,例如 Bounds: java.awt.geom.Rectangle2D$Float[x=-218.15294,y=43.783157,w=718.15295,h=853.2269]。因此,当您在创建 transform 对象后调用 transform.translate(218.53717,0.0); 时,您会看到丢失的部分变得可见(但是,当然,输出图像不够宽 - 您必须使其宽 219 像素) .我没有按照你计算图像大小的数学 - 在给定的情况下这有点繁琐。
    • 好的我明白了,所以我需要将整个图像在右侧扩展218像素,然后最左边的卡片左侧的图像中会出现218像素的宽度?知道这个 218 是从哪里来的吗?我想在不对该数字进行硬编码的情况下进行这项工作,并且我已经计算出我认为该度量应该是什么,并得到了 682。
    • 我是用毕达哥拉斯定理计算出来的,即卡片从原始位置向左侧延伸的距离长度。
    • @gpasch 除了问我这个,你没有别的事可做吗? :-P 说真的:这种摇摆/图像/游戏的东西是一种爱好。通过回答这个问题,我学到了一些关于AffineTransformOp 的边界处理的知识。用这个漂亮的滑块构建 MCVE 来“展开”卡片很有趣。是的,不是那种“跳伞”或“蹦极”的乐趣,而是……有趣:-)
    猜你喜欢
    • 1970-01-01
    • 2011-10-20
    • 1970-01-01
    • 1970-01-01
    • 2020-03-05
    • 1970-01-01
    • 1970-01-01
    • 2011-01-29
    • 1970-01-01
    相关资源
    最近更新 更多