【问题标题】:Java 8 + Swing: How to Draw Flush PolygonsJava 8 + Swing:如何绘制齐平多边形
【发布时间】:2025-12-14 19:00:01
【问题描述】:

(抱歉,帖子太长了……至少它有图片?)

我编写了一个算法,通过统计生成覆盖图像且不重叠的 N 个凸多边形,从图像创建马赛克。这些多边形有 3-8 条边,每条边的角度是 45 度的倍数。这些多边形在内部存储为一个矩形,每个角都有位移。下面是一张解释其工作原理的图片:

getRight() 返回x + width - 1getBottom() 返回y + height - 1。该类旨在保持填充像素周围的紧密边界框,因此此图像中显示的坐标是正确的。请注意width >= ul + ur + 1width >= ll + lr + 1height >= ul + ll + 1height >= ur + ul + 1,否则一侧会有空像素。另请注意,角的位移可能为 0,因此表示所有像素都填充在该角中。这使得该表示可以存储 3-8 个边的凸多边形,每个边的长度至少为一个像素。

虽然用数学方法表示这些区域很好,但我想绘制它们以便我可以看到它们。使用简单的 lambda 和迭代多边形中每个像素的方法,我可以完美地渲染图像。例如,下面是Claude Monet's Woman with a Parasol,使用 99 个多边形,允许所有分割方向。

呈现此图像的代码如下所示:

public void drawOnto(Graphics graphics) {
    graphics.setColor(getColor());
    forEach(
        (i, j) -> {
            graphics.fillRect(x + i, y + j, 1, 1);
        }
    );
}

private void forEach(PerPixel algorithm) {
    for (int j = 0; j < height; ++j) {
        int nj = height - 1 - j;

        int minX;
        if (j < ul) {
            minX = ul - j;
        } else if (nj < ll) {
            minX = ll - nj;
        } else {
            minX = 0;
        }

        int maxX = width;
        if (j < ur) {
            maxX -= ur - j;
        } else if (nj < lr) {
            maxX -= lr - nj;
        }

        for (int i = minX; i < maxX; ++i) {
            algorithm.perform(i, j);
        }
    }
}

但是,由于许多原因,这并不理想。首先,图形表示多边形的概念现在是类本身的一部分;最好允许其他专注于表示这些多边形的类。其次,这需要多次调用fillRect() 来绘制单个像素。最后,我希望能够开发出其他方法来渲染这些多边形,而不是按原样绘制它们(例如,performing weighted interpolation over the Voronoi tessellation represented by the polygons' centers)。

所有这些都指向生成一个表示多边形顶点的java.awt.Polygon(我将其命名为Region 以区别于Polygon 类)。没问题;我写了一个方法来生成一个Polygon,它上面的角没有重复,以处理位移为0或一侧只有一个像素的情况:

public Polygon getPolygon() {
    int[] xes = {
        x + ul,
        getRight() - ur,
        getRight(),
        getRight(),
        getRight() - lr,
        x + ll,
        x,
        x
    };
    int[] yes = {
        y,
        y,
        y + ur,
        getBottom() - lr,
        getBottom(),
        getBottom(),
        getBottom() - ll,
        y + ul
    };

    int[] keptXes = new int[8];
    int[] keptYes = new int[8];
    int length = 0;
    for (int i = 0; i < 8; ++i) {
        if (
            length == 0 ||
            keptXes[length - 1] != xes[i] ||
            keptYes[length - 1] != yes[i]
        ) {
            keptXes[length] = xes[i];
            keptYes[length] = yes[i];
            length++;
        }
    }

    return new Polygon(keptXes, keptYes, length);
}

问题是,当我尝试使用 PolygonGraphics.fillPolygon() 方法时,它并没有填充所有像素!下面是使用这种不同方法渲染的相同马赛克:

所以我有一些关于这种行为的相关问题:

  1. 为什么Polygon 类没有填充所有这些像素,即使角度是 45 度的简单倍数?

  2. 如何在我的渲染器中始终如一地围绕这个缺陷(就我的应用程序而言)进行编码,以便我可以按原样使用我的getPolygon() 方法?我不想更改它输出的顶点,因为我需要它们精确地进行质心计算。


MCE

如果上面的代码 sn-ps 和图片不足以帮助解释问题,我添加了一个 Minimal, Complete, and Verifiable Example 来演示我上面描述的行为。

package com.sadakatsu.mce;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Polygon;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;

public class Main {
    @FunctionalInterface
    private static interface PerPixel {
        void perform(int x, int y);
    }

    private static class Region {
        private int height;
        private int ll;
        private int lr;
        private int width;
        private int ul;
        private int ur;
        private int x;
        private int y;

        public Region(
            int x,
            int y,
            int width,
            int height,
            int ul,
            int ur,
            int ll,
            int lr
        ) {
            if (
                width < 0 || width <= ll + lr || width <= ul + ur ||
                height < 0 || height <= ul + ll || height <= ur + lr ||
                ul < 0 ||
                ur < 0 ||
                ll < 0 ||
                lr < 0
            ) {
                throw new IllegalArgumentException();
            }

            this.height = height;
            this.ll = ll;
            this.lr = lr;
            this.width = width;
            this.ul = ul;
            this.ur = ur;
            this.x = x;
            this.y = y;
        }

        public Color getColor() {
            return Color.BLACK;
        }

        public int getBottom() {
            return y + height - 1;
        }

        public int getRight() {
            return x + width - 1;
        }

        public Polygon getPolygon() {
            int[] xes = {
                x + ul,
                getRight() - ur,
                getRight(),
                getRight(),
                getRight() - lr,
                x + ll,
                x,
                x
            };
            int[] yes = {
                y,
                y,
                y + ur,
                getBottom() - lr,
                getBottom(),
                getBottom(),
                getBottom() - ll,
                y + ul
            };

            int[] keptXes = new int[8];
            int[] keptYes = new int[8];
            int length = 0;
            for (int i = 0; i < 8; ++i) {
                if (
                    length == 0 ||
                    keptXes[length - 1] != xes[i] ||
                    keptYes[length - 1] != yes[i]
                ) {
                    keptXes[length] = xes[i];
                    keptYes[length] = yes[i];
                    length++;
                }
            }

            return new Polygon(keptXes, keptYes, length);
        }

        public void drawOnto(Graphics graphics) {
            graphics.setColor(getColor());
            forEach(
                (i, j) -> {
                    graphics.fillRect(x + i, y + j, 1, 1);
                }
            );
        }

        private void forEach(PerPixel algorithm) {
            for (int j = 0; j < height; ++j) {
                int nj = height - 1 - j;

                int minX;
                if (j < ul) {
                    minX = ul - j;
                } else if (nj < ll) {
                    minX = ll - nj;
                } else {
                    minX = 0;
                }

                int maxX = width;
                if (j < ur) {
                    maxX -= ur - j;
                } else if (nj < lr) {
                    maxX -= lr - nj;
                }

                for (int i = minX; i < maxX; ++i) {
                    algorithm.perform(i, j);
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        int width = 10;
        int height = 8;

        Region region = new Region(0, 0, 10, 8, 2, 3, 4, 1);

        BufferedImage image = new BufferedImage(
            width,
            height,
            BufferedImage.TYPE_3BYTE_BGR
        );
        Graphics graphics = image.getGraphics();
        graphics.setColor(Color.WHITE);
        graphics.fillRect(0, 0, width, height);
        region.drawOnto(graphics);
        ImageIO.write(image, "PNG", new File("expected.png"));

        image = new BufferedImage(
            width,
            height,
            BufferedImage.TYPE_3BYTE_BGR
        );
        graphics = image.getGraphics();
        graphics.setColor(Color.WHITE);
        graphics.fillRect(0, 0, width, height);
        graphics.setColor(Color.BLACK);
        graphics.fillPolygon(region.getPolygon());
        ImageIO.write(image, "PNG", new File("got.png"));
    }
}

【问题讨论】:

  • 这是我一整天看到的最酷的东西 :-) +1
  • 在我看来,your 代码中多边形内部的定义与 java.awt.Polygon 的定义不同;两者在它们的参考框架内都是正确的,它们只是不同的定义。您将需要从您的定义转换为 Polygon 的;如果您想使用 Polygon,则无法解决...
  • @Durandal:确实如此。我的问题是我还没有找到关于这种方法如何工作的解释。如果我能学会押韵和理由来解释为什么有时会产生相同的结果而有时却不会,我将能够取得进步。到目前为止,我还没有找到这个解释。
  • @peeskillet: :) 当它完全可用时,我会将这个应用程序 Mosaic 放到我的 GitHub (github.com/sadakatsu) 上。一旦它启动,你就可以玩它。我希望这将在 1-2 周内完成。
  • 覆盖 JPanel 的 getPreferredSIze,然后所有坐标都来自 getHeight/weight incl。在调整大小时重新绘制(忘记了完美的像素,例如将 10 除以 3:-),所有块都只是形状,将所有对象放入数组,在paintCOMponent 内部循环对象数组,这里有一些代码示例关于跨度>

标签: java swing polygon


【解决方案1】:

我花了一整天的时间来解决这个问题,而且我似乎已经解决了这个问题。线索是在Shape 类的文档中找到的,内容如下:

内部定义:当且仅当:

  • 它完全位于形状边界内或

  • 它正好位于 Shape 边界上,并且在增加的 X 方向上紧邻该点的空间完全在边界内或

  • 它恰好位于水平边界段上,并且在 Y 方向增加的点上紧邻的空间在边界内。

其实这段文字有点误导;第三种情况覆盖第二种情况(即,即使Shape 底部的水平边界段中的像素在其右侧有一个填充点,它仍然不会被填充)。如图所示,下面的Polygon 不会绘制出x的像素:

红色、绿色和蓝色像素是Polygon 的一部分;其余的都不是。蓝色像素属于第一种情况,绿色像素属于第二种情况,红色像素属于第三种情况。请注意,未绘制凸包上的所有最右边和最低像素。要绘制它们,您必须将顶点移动到橙色像素,如图所示,以制作凸包的新最右侧/最底部部分。

最简单的方法是使用 camickr 的方法:同时使用 fillPolygon()drawPolygon()。至少在我的 45 度多边凸包的情况下,drawPolygon() 将线条准确地绘制到顶点(可能也适用于其他情况),因此将填充fillPolygon() 错过的像素。但是,fillPolygon()drawPolygon() 都不会绘制单像素 Polygon,因此必须编写一个特殊情况来处理它。

为了理解上面的insideness 定义,我开发的实际解决方案是创建一个不同的Polygon,并使用修改后的角,如图所示。它的好处是(?)只调用一次绘图库并自动处理特殊情况。它实际上可能不是最佳的,但这是我供任何人考虑的代码:

package com.sadakatsu.mosaic.renderer;

import java.awt.Polygon;
import java.util.Arrays;

import com.sadakatsu.mosaic.Region;

public class RegionPolygon extends Polygon {
    public RegionPolygon(Region region) {
        int bottom = region.getBottom();
        int ll = region.getLL();
        int lr = region.getLR();
        int right = region.getRight();
        int ul = region.getUL();
        int ur = region.getUR();
        int x = region.getX();
        int y = region.getY();

        int[] xes = {
            x + ul,
            right - ur + 1,
            right + 1,
            right + 1,
            right - lr,
            x + ll + 1,
            x,
            x
        };

        int[] yes = {
            y,
            y,
            y + ur,
            bottom - lr,
            bottom + 1,
            bottom + 1,
            bottom - ll,
            y + ul
        };

        npoints = 0;
        xpoints = new int[xes.length];
        ypoints = new int[xes.length];
        for (int i = 0; i < xes.length; ++i) {
            if (
                i == 0 ||
                xpoints[npoints - 1] != xes[i] ||
                ypoints[npoints - 1] != yes[i]
            ) {
                addPoint(xes[i], yes[i]);
            }
        }
    }
}

【讨论】: