Search code examples
javaswinggraphicsgraphics2dantialiasing

Graphics2D - Antialias a Nearest-Neighbor along pixel borders


I have a (small) BufferedImage wich needs to be enlarged using nearest neighbor interpolation and then drawn to a Graphics2D. The Image has 1-bit alpha information and rendering it with antialiasing on and this code

AffineTransform oldT = g.getTransform();
Paint oldP = g.getPaint();
int w = img.getWidth(), h = img.getHeight();
g.transform(at);
g.setPaint(new TexturePaint(img, new Rectangle2D.Double(0, 0, w, h)));
g.fillRect(0, 0, w, h);
g.setPaint(oldP);
g.setTransform(oldT);

Where img is theBufferedImage to be rendered using at as an AffineTransform. The antialiasing activated by

g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

produces good results, but only for the border of the entire image; looking very strange:

enter image description here

Notice the top border is actually the image border while the right is a transition from opaque to transparent pixel.

Is there a best practice to achieve the same antialiasing within the image as is applied to the border? Note that other interpolation methods than NearestNeighbor are unacceptable.

Thanks in advance for any hints.

The rendering shouldn't take too long since it is part of a paintComponent method, but preprocessing the BufferedImage is possible. The AffineTransform is (very) variable, though.


EDIT

I have achieved a small improvement by using a two-step method:

AffineTransform oldT = g.getTransform();
Paint oldP = g.getPaint();
int w = img.getWidth(), h = img.getHeight(), texw = (int) (w*oldT.getScaleX()), texh = (int) (h *  oldT.getScaleY());
BufferedImage tex = new BufferedImage(texw, texh, BufferedImage.TYPE_INT_ARGB);
Graphics2D tg = tex.createGraphics();
tg.scale(oldT.getScaleX(), oldT.getScaleY());
tg.drawImage(img, 0, 0, this);
g.transform(at);
g.scale(1/oldT.getScaleX(), 1/oldT.getScaleY());
g.setPaint(new TexturePaint(tex, new Rectangle2D.Double(0, 0, texw, texh)));
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.fillRect(0, 0, texw, texh);
g.setPaint(oldP);
g.setTransform(oldT);

Resulting in this image:

enter image description here

This is still not perfect, though. I tried VALUE_INTERPOLATION_BICUBIC, but this is just incredibly slow and essentially produces the same result. I hope there is a way the get the exact same antialiasing effect, since it is still irritating.


Solution

  • A fairly complicated solution with a lot of tweaking and testing put into provides a very good quality by rendering each pixel as a shape reading its color and transparency from the BufferedImage:

    import java.awt.AlphaComposite;
    import java.awt.Color;
    import java.awt.Composite;
    import java.awt.Graphics2D;
    import java.awt.geom.AffineTransform;
    import java.awt.geom.Path2D;
    import java.awt.image.BufferedImage;
    
    public class VectorizedImage {
        private final BufferedImage img;
    
        public VectorizedImage(BufferedImage img) {
            this.img = img;
        }
    
        public void draw(Graphics2D g) {
            Color oldC = g.getColor();
            Composite oldCo = g.getComposite();
            AlphaComposite comp = AlphaComposite.SrcOver;
            double lap = 1/Math.sqrt(g.getTransform().getDeterminant()); // This deals with inter-pixel borders letting the background shine through
            Path2D pixel = new Path2D.Double();
            pixel.moveTo(0, 0);
            pixel.lineTo(0, 1);
            pixel.lineTo(1, 1);
            pixel.lineTo(1, 0);
            pixel.lineTo(0, 0);
            pixel.transform(AffineTransform.getScaleInstance(1+lap, 1+lap));
            for (int i = 0; i < img.getWidth(); i++)
                for (int j = 0; j < img.getHeight(); j++) {
                    g.setColor(new Color(img.getRGB(i, j)));
                    g.setComposite(comp.derive(img.getAlphaRaster().getSampleFloat(i, j, 0) / 255));
                    g.fill(pixel.createTransformedShape(AffineTransform.getTranslateInstance(i, j)));
            }
            g.setColor(oldC);
            g.setComposite(oldCo);
        }
    }
    

    The result is this nicely rendered edge:
    enter image description here