Java 8 + Swing: how to draw polygons

(Sorry for the long post ... at least does it have photos?)

I wrote an algorithm that mosaics an image, statistically generating N convex polygons that cover the image without overlapping. These polygons have a distance between 3-8 sides, and each side has an angle multiple of 45 degrees. These polygons are stored internally as a rectangle with offsets for each corner. Below is an image explaining how it works:

enter image description here

getRight()

returns x + width - 1

and getBottom()

returns y + height - 1

. The class is designed to maintain a tight bounding box around filled pixels, so the coordinates shown in this image are correct. Note that width >= ul + ur + 1

, width >= ll + lr + 1

, height >= ul + ll + 1

and height >= ur + ul + 1

, or on the side of the pixels will be empty. Note also that the angular offset is possibly 0, which indicates that all pixels are filled in that corner. This allows this view to store 3-8-sided convex polygons, each side of which is at least one pixel in length.

While it is good to represent these regions mathematically, I want to draw them so I can see them. Using a simple lambda and a method that iterates over each pixel in the polygon, I can make the image perfect. For example, below is Claude Monet's Woman with an Umbrella using 99 polygons allowing all explode directions.

enter image description here

The code displaying this image looks like this:

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

      

However, this is not ideal for many reasons. First, the concept of polygon graphing is now part of the class itself; better to allow other classes whose job it is to represent these polygons. Second, it causes many, many calls fillRect()

to draw a single pixel. Finally, I want to be able to develop other methods for rendering these polygons than to draw them as is (for example, performing weighted interpolation on Voronoi tessellation represented by the centers of the polygons ).

All of this points to a creation java.awt.Polygon

that represents the vertices of a polygon (which I named Region

to distinguish from a class Polygon

). No problems; I wrote a method to create Polygon

that has the corners above, no duplicates, to handle cases where the offset is 0 or that there is only one pixel on the side:

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

      

The problem is that when I try to use this Polygon

with a method Graphics.fillPolygon()

, it doesn't fill all the pixels! Below is the same mosaic as this method:

enter image description here

So, I have several related questions about this behavior:

  • Why Polygon

    doesn't the class fill all those pixels, even though the angles are simple multiples of 45 degrees?

  • How can I consistently encode this defect (applied to my application) in my renderers to use the getPolygon()

    as-is method ? I don't want to change the vertices that it outputs because they are needed to accurately calculate the center of mass.


MCE

If the above code snippets and images are not enough to help explain the problem, I have added a minimal, complete, and tested example that demonstrates the behavior described above.

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

      

+3


source to share


1 answer


I've been working on this all day and I seem to fix it. The clue was found in the documentation for Shape

which reads:

Definition of persistence : a point is considered to be inside the form if and only if:

  • it lies entirely within the interface, or

  • it lies exactly on the border of the Shape, and the space immediately adjacent to the point in the ascending X direction is entirely within the border, or

  • lies exactly on the horizontal boundary segment, and the space immediately adjacent to the ascending Y point is within the boundary.

Actually, this text is a little misleading; the third case overrides the second (that is, even if the pixel in the horizontal bounding segment at the bottom a Shape

has a filled point on the right, it will still not be filled). Pictured Polygon

below will not draw x'ed out pixels:

enter image description here



Red, green and blue pixels are part of Polygon

; the rest are not. Blue pixels fall under the first case, green pixels fall under the second case, and red pixels fall under the third case. Note that all the right and bottom pixels along the convex hull are NOT drawn. To make them draw, you need to move the vertices to orange pixels as shown to make a new right-most / bottom-most part of the convex hull.

The easiest way to do this is using the camickr method: use both fillPolygon()

and drawPolygon()

. At least in the case of my 45-edged convex hulls, drawPolygon()

precisely draws lines to the vertices (and probably for other cases), and thus fills in the pixels it fillPolygon()

skips. However fillPolygon()

, neither drawPolygon()

will nor will draw a single pixel Polygon

, so a special case needs to be handled to handle that.

The actual solution I worked out while trying to understand the definition of persistence above was to create another one Polygon

with altered angles as shown in the picture. It has the advantage (?) Of calling the drawing library only once and automatically handles the special case. This is probably not entirely optimal, but here is the code I used for any consideration:

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

      

0


source







All Articles