Java 8 + Swing:如何绘制Flush多边形

Java 8 + Swing: How to Draw Flush Polygons

(很抱歉,这封长信…至少有照片?)

我已经写了一个算法,通过统计生成n个凸多边形来从一个图像创建一个马赛克,这些凸多边形覆盖了没有重叠的图像。这些多边形有3-8个边,每边有一个45度的倍数角。这些多边形内部存储为一个矩形,每个角都有位移。下面是一张图片,说明了这是如何工作的:

enter image description here

getRight()返回EDCOX1〔1〕,EDCOX1〔2〕返回EDCOX1〔3〕。该类被设计为在填充的像素周围保持一个紧密的包围盒,因此该图像中所示的坐标是正确的。请注意,EDOCX1为4,EDOCX1为5,EDOCX1为6,EDCOX1为7,或者侧为空像素。还要注意的是,角位移是可能的0,从而指示所有像素都填充在该角落中。这使该表示能够存储3-8个边凸多边形,每个边的长度至少为一个像素。

虽然用数学方法表示这些区域很好,但我想画出它们,这样我就能看到它们。使用简单的lambda和迭代多边形中每个像素的方法,我可以完美地渲染图像。举个例子,下面是克劳德·莫奈(ClaudeMonet)的女人,她用一把阳伞,使用99个多边形,允许所有方向的分裂。

enter image description here

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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()来绘制一个像素。最后,我希望能够开发出其他方法来渲染这些多边形,而不是按原样绘制它们(例如,对由多边形中心表示的Voronoi细分执行加权插值)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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()方法时,它不会填充所有像素!以下是用这种不同方法绘制的相同马赛克:

enter image description here

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

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

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

  • MCE

    如果上面的代码片段和图片不足以帮助解释这个问题,那么我添加了一个最小的、完整的、可验证的示例来演示我上面描述的行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    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"));
        }
    }


    我花了一整天的时间研究它,我似乎对此有了一个解决办法。线索是在Shape类的文档中找到的,文档内容如下:

    Definition of insideness: A point is considered to lie inside a Shape if and only if:

    • it lies completely inside theShape boundary or

    • it lies exactly on the Shape boundary and the space immediately adjacent to the point in the increasing X direction is entirely inside the boundary or

    • it lies exactly on a horizontal boundary segment and the space immediately adjacent to the point in the increasing Y direction is inside the boundary.

    实际上,此文本有点误导性;第三种情况会覆盖第二种情况(即,即使Shape底部水平边界段中的像素向右有一个填充点,它仍然不会被填充)。用图形表示,下面的Polygon不会绘制出x'ed像素:

    enter image description here

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

    最简单的方法是使用CAMICKR的方法:使用EDCOX1,4,EDCX1,5。至少在我的45度多棱凸壳的情况下,drawPolygon()精确地画直线到顶点(并且可能也适用于其他情况),因此将填充EDCOX1×4忽略的像素。然而,EDCOX1的4Ω和EDCOX1(5)都不会画出一个像素EDOCX1×2,所以必须编写一个特殊的例子来处理这个问题。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    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]);
                }
            }
        }
    }