Graphics2D - Antialias a Nearest-Neighbor along pixel borders - java

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:
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:
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.

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:

Related

Rotate BufferedImage with transparent background

I have an image with transparent background. I'd like to rotate this image to a specific angle and keep the transparent background for the resulting image. For this purpose I use the following method:
public static BufferedImage rotateImage(BufferedImage image, double angle, Color backgroundColor) {
System.out.println(image.getType());
double theta = Math.toRadians(angle);
double sin = Math.abs(Math.sin(theta));
double cos = Math.abs(Math.cos(theta));
int w = image.getWidth();
int h = image.getHeight();
int newW = (int) Math.floor(w * cos + h * sin);
int newH = (int) Math.floor(h * cos + w * sin);
BufferedImage tmp = new BufferedImage(newW, newH, image.getType());
Graphics2D g2d = tmp.createGraphics();
if (backgroundColor != null) {
g2d.setColor(backgroundColor);
g2d.fillRect(0, 0, newW, newH);
}
g2d.fillRect(0, 0, newW, newH);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.translate((newW - w) / 2, (newH - h) / 2);
g2d.rotate(theta, w / 2, h / 2);
g2d.drawImage(image, 0, 0, null);
g2d.dispose();
return tmp;
}
I invoke it with background=null:
BufferedImage image = ImageIO.read(file);
rotateImage(image, 4, null);
ImageIO.write(bi, "PNG", new File("image.png"));
but the background of the resulting image.png is WHITE. What am I doing wrong and how to properly keep the transparent background for image.png?
I'm a bit puzzled about the behavior of Graphics.drawImage(). Maybe somebody else can comment about it.
However, Graphics2D.drawRenderedImage() works a treat. It takes an AffineTransform to control the rotation. The below example nicely works. You probably have additional requirement about the final image size and the location of the rotated image.
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
public class ImageRotation {
public static void main(String[] args) {
ImageRotation rotation = new ImageRotation();
rotation.rotate("input.png", 45, "output.png");
}
public void rotate(String inputImageFilename, double angle, String outputImageFilename) {
try {
BufferedImage inputImage = ImageIO.read(new File(inputImageFilename));
BufferedImage outputImage = rotateImage(inputImage, angle);
ImageIO.write(outputImage, "PNG", new File(outputImageFilename));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private BufferedImage rotateImage(BufferedImage sourceImage, double angle) {
int width = sourceImage.getWidth();
int height = sourceImage.getHeight();
BufferedImage destImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = destImage.createGraphics();
AffineTransform transform = new AffineTransform();
transform.rotate(angle / 180 * Math.PI, width / 2 , height / 2);
g2d.drawRenderedImage(sourceImage, transform);
g2d.dispose();
return destImage;
}
}
Update
While the above code works for most PNGs, it does not work for the image that alexanoid is using. I've analyzed the image:
It's a grayscale image without a color palette (PNG color type 0) .
It uses simple transparency with a 2 byte long tRNS chunk.
As far as I can tell that's perfectly legal. However, ImageIO does not implement this combination. If the image has no palette, it simply ignores the tRNS chunk and therefore ignores the transparency information. That's most likely a bug.
You basically have two options now:
Look for an alternative library to read PNG files.
Fix the transparency after you have read the PNG file. This only works if know that the image used the particular problematic format.
Input and output for working PNG files
Input image:
Ouptput Image:

BufferedImage fill rectangle with transparent pixels

I have a BufferedImage and I am trying to fill a rectangle with transparent pixels. The problem is, instead of replacing the original pixels, the transparent pixels just go on top and do nothing. How can I get rid of the original pixel completely? The code works fine for any other opaque colors.
public static BufferedImage[] slice(BufferedImage img, int slices) {
BufferedImage[] ret = new BufferedImage[slices];
for (int i = 0; i < slices; i++) {
ret[i] = copyImage(img);
Graphics2D g2d = ret[i].createGraphics();
g2d.setColor(new Color(255, 255, 255, 0));
for(int j = i; j < img.getHeight(); j += slices)
g2d.fill(new Rectangle(0, j, img.getWidth(), slices - 1));
g2d.dispose();
}
return ret;
}
public static BufferedImage copyImage(BufferedImage source){
BufferedImage b = new BufferedImage(source.getWidth(), source.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics g = b.getGraphics();
g.drawImage(source, 0, 0, null);
g.dispose();
return b;
}
Using AlphaComposite, you have at least two options:
Either, use AlphaComposite.CLEAR as suggested, and just fill a rectangle in any color, and the result will be a completely transparent rectangle:
Graphics2D g = ...;
g.setComposite(AlphaComposite.Clear);
g.fillRect(x, y, w, h);
Or, you can use AlphaComposite.SRC, and paint in a transparent (or semi-transparent if you like) color. This will replace whatever color/transparency that is at the destination, and the result will be a rectangle with exactly the color specified:
Graphics2D g = ...;
g.setComposite(AlphaComposite.Src);
g.setColor(new Color(0x00000000, true);
g.fillRect(x, y, w, h);
The first approach is probably faster and easier if you want to just erase what is at the destination. The second is more flexible, as it allows replacing areas with semi-transparency or even gradients or other images.
PS: (As Josh says in the linked answer) Don't forget to reset the composite after you're done, to the default AlphaComposite.SrcOver, if you plan to do more painting using the same Graphics2D object.

Drawing fully transparent "white" in Java BufferedImage

This might sound like a bit of strange title, but bear with me, there is a reason:
I am trying to generate a white glow around a text on a gray background.
To generate the glow, I created a new BufferedImage that's bigger than the text, then I drew the text in white onto the canvas of the image and ran a Gaussian Blur over the image via a ConvolveOp, hoping for something like this:
At first I was a bit surprised when the glow turned out darker than the gray background of the text:
But after a bit of thinking, I understood the problem:
The convolution operates on each color channel (R, G, B, and A) independently to calculate the blurred image. The transparent background of the picture has color value 0x00000000, i.e. a fully transparent black! So, when the convolution filter runs over the image, it not only blends the alpha value, but also mixes the black into the RGB values of the white pixels. This is why the glow comes out dark.
To fix this, I need to initialize the image to 0x00FFFFFF, i.e. a fully transparent white instead, but if I just set that color and fill a rectangle with it, it simply does nothing as Java says "well, it's a fully transparent rectangle that you're drawing! That's not going to change the image... Let me optimize that away for you... Done... You're welcome.".
If I instead set the color to 0x01FFFFFF, i.e. an almost fully transparent white, it does draw the rectangle and the glow looks beautiful, except I end up with a very faint white box around it...
Is there a way I can initialize the image to 0x00FFFFFF everywhere?
UPDATE:
I found one way, but it's probably as non-optimal as you can get:
I draw an opaque white rectangle onto the image and then I run a RescaleOp over the image that sets all alpha values to 0. This works, but it's probably a terrible approach as far as performance goes.
Can I do better somehow?
PS: I'm also open to entirely different suggestions for creating such a glow effect
The main reason why the glow appeared darker with your initial approach is most likely that you did not use an image with a premultiplied alpha component. The JavaDoc of ConvolveOp contains some information about how the alpha component is treated during a convolution.
You could work around this with an "almost fully transparent white". But alternatively, you may simply use an image with premultiplied alpha, i.e. one with the type TYPE_INT_ARGB_PRE.
Here is a MCVE that draws a panel with some text, and some pulsing glow around the text (remove the timer and set a fixed radius to remove the pulse - I couldn't resist playing around a little here ...).
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class TextGlowTest
{
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
createAndShowGUI();
}
});
}
private static void createAndShowGUI()
{
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(new TextGlowPanel());
f.setSize(300,200);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
}
class TextGlowPanel extends JPanel
{
private BufferedImage image;
private int radius = 1;
TextGlowPanel()
{
Timer t = new Timer(50, new ActionListener()
{
long startMillis = -1;
#Override
public void actionPerformed(ActionEvent e)
{
if (startMillis == -1)
{
startMillis = System.currentTimeMillis();
}
long d = System.currentTimeMillis() - startMillis;
double s = d / 1000.0;
radius = (int)(1 + 15 * (Math.sin(s * 3) * 0.5 + 0.5));
repaint();
}
});
t.start();
}
#Override
protected void paintComponent(Graphics gr)
{
super.paintComponent(gr);
gr.setColor(Color.GRAY);
int w = getWidth();
int h = getHeight();
gr.fillRect(0, 0, w, h);
if (image == null || image.getWidth() != w || image.getHeight() != h)
{
// Must be prmultiplied!
image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE);
}
Graphics2D g = image.createGraphics();
Font font = g.getFont().deriveFont(70.0f).deriveFont(Font.BOLD);
g.setFont(font);
g.setComposite(AlphaComposite.Src);
g.setColor(new Color(255,255,255,0));
g.fillRect(0,0,w,h);
g.setComposite(AlphaComposite.SrcOver);
g.setColor(new Color(255,255,255,0));
g.fillRect(0,0,w,h);
g.setColor(Color.WHITE);
g.drawString("Glow!", 50, 100);
image = getGaussianBlurFilter(radius, true).filter(image, null);
image = getGaussianBlurFilter(radius, false).filter(image, null);
g.dispose();
g = image.createGraphics();
g.setFont(font);
g.setColor(Color.BLUE);
g.drawString("Glow!", 50, 100);
g.dispose();
gr.drawImage(image, 0, 0, null);
}
// From
// http://www.java2s.com/Code/Java/Advanced-Graphics/GaussianBlurDemo.htm
public static ConvolveOp getGaussianBlurFilter(
int radius, boolean horizontal)
{
if (radius < 1)
{
throw new IllegalArgumentException("Radius must be >= 1");
}
int size = radius * 2 + 1;
float[] data = new float[size];
float sigma = radius / 3.0f;
float twoSigmaSquare = 2.0f * sigma * sigma;
float sigmaRoot = (float) Math.sqrt(twoSigmaSquare * Math.PI);
float total = 0.0f;
for (int i = -radius; i <= radius; i++)
{
float distance = i * i;
int index = i + radius;
data[index] =
(float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
total += data[index];
}
for (int i = 0; i < data.length; i++)
{
data[i] /= total;
}
Kernel kernel = null;
if (horizontal)
{
kernel = new Kernel(size, 1, data);
}
else
{
kernel = new Kernel(1, size, data);
}
return new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
}
}
I've found that clearRect should paint a transparent color.
g.setBackground(new Color(0x00FFFFFF, true));
g.clearRect(0, 0, img.getWidth(), img.getHeight());
You should also be able to force the BufferedImage to fill with a transparent color by setting the pixel data directly.
public static void forceFill(BufferedImage img, int rgb) {
for(int x = 0; x < img.getWidth(); x++) {
for(int y = 0; y < img.getHeight(); y++) {
img.setRGB(x, y, rgb);
}
}
}
It is not clearly documented but I tested it and setRGB appears to accept an ARGB value.

Why this RadialGradientPaint is drawn incorrectly?

I'm drawing small circles with radial gradient. Example result (r = 10 px):
But when I decrease the radius, strange things begin to happen. If r = 5px:
Scaled to match 10px image:
And it gets even worse if r = 2px (again, scaled):
As you see, the center of the gradient is consistently off (down and to the right) from center of circle.
The code I used to generate the above images (SSCCE, compilable&runnable):
import java.awt.image.*;
import javax.imageio.ImageIO;
import java.io.File;
import java.awt.*;
import java.awt.geom.*;
public class Test {
public static void main(String[] args) throws Exception {
float r = 2;
BufferedImage img = new BufferedImage((int) r*4, (int) r*4, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
RenderingHints rh = new RenderingHints(null);
rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
rh.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
rh.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
rh.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.addRenderingHints(rh);
g.setBackground(Color.WHITE);
g.clearRect(0, 0, (int) r*4, (int) r*4);
g.setPaint(new RadialGradientPaint(
r*2,r*2,r*2,
new float[] {0.1f,0.35f},
new Color[] {Color.RED, Color.GREEN}
));
g.fill(new Ellipse2D.Float(r,r,r*2,r*2));
g.dispose();
ImageIO.write(img, "png", new File("out.png"));
}
}
I tried switching rendering hints, but the result doesn't change. What could be causing this problem?
EDIT:
Offsetting RadialGradientPaint by 0.5f top-left results in correct image (r=2):
"Fixed" code:
g.setPaint(new RadialGradientPaint(
r*2-0.5f, r*2-0.5f, r*2,
new float[] {0.1f,0.35f},
new Color[] {Color.RED, Color.GREEN}
));
The problem has to do with the fact that the height and width of your circle is an even number. You used to have g.fill(new Ellipse2D.Float(r,r,r*2,r*2));
as part of your code. This made it so that the width and height of your circle was always even. This causes problems because computers are not perfect and its not possible to draw a pixel exactly in the center of a circle with even dimensions. To fix this, I added one to where you drew your circle. The working code follows:
import java.awt.image.*;
import javax.imageio.ImageIO;
import java.io.File;
import java.awt.*;
import java.awt.geom.*;
public class Test {
public static void main(String[] args) throws Exception {
float r = 2;
BufferedImage img = new BufferedImage((int) r*4 + 1, (int) r*4 + 1, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = img.createGraphics();
RenderingHints rh = new RenderingHints(null);
rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
rh.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
rh.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
rh.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.addRenderingHints(rh);
g.setBackground(Color.WHITE);
g.clearRect(0, 0, (int) r*4 + 1, (int) r*4 + 1);
g.setPaint(new RadialGradientPaint(
r*2,r*2,r*2,
new float[] {0.1f,0.35f},
new Color[] {Color.RED, Color.GREEN}
));
g.fill(new Ellipse2D.Float(r,r,r*2 + 1,r*2 + 1));
g.dispose();
ImageIO.write(img, "png", new File("out.png"));
}
}

Cursor with Semi-Transparency / Anti-Aliasing

I'm trying to create a custom cursor with the following code:
BufferedImage cursor = new BufferedImage(30, 30, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D) cursor.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.BLACK);
g.drawOval(0, 0, 26, 26);
jframe.getContentPane().setCursor(Toolkit.getDefaultToolkit().createCustomCursor(
cursor, new Point(13, 13), "cursor"));
Without Anti-Aliasing this looks really bad, but once I try to activate Anti-Aliasing, the pixels that are supposed to be semi transparent seem to become completely solid - as if the JFrame cursor cannot handle semi-transparency.
I've tried loading the BufferedImage from a .png file, but the results are the same.
I do not want to draw the cursor manually on my canvas because it has a slight input lag compared to this method.
All I want to do is have a Cursor that can support semi-transparency.
Any idea on how I can achieve this?
Thanks in advance.
This one is subtle (I've been stuck for a couple of years :) but I've cracked it today.
The cursor appears to only support on/off for transparency, so drawing first to a TYPE_INT_RGB image then copying that to a TYPE_INT_ARGB (then fixing the transparency) works.
Not sure if it's optimal, but it doesn't matter too much for small images like this.
int size = 32;
/*
* we need two buffered images as the cursor only supports on/off for alpha
*
* so we need to draw to an image without alpha support
* then draw that to one with alpha support
* then make "white" transparent
*/
BufferedImage image = new BufferedImage(size, size,
BufferedImage.TYPE_INT_RGB);
BufferedImage image2 = new BufferedImage(size, size,
BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
Graphics2D g2 = image2.createGraphics();
g.setColor(Color.white);
g.fillRect(0, 0, size, size);
// turn on anti-aliasing.
g.setStroke(new BasicStroke(4.0f)); // 4-pixel lines
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(new Color(0.5f, 0f, 0f));
g.drawOval(3, 3, size-7, size-7);
g2.drawImage(image, 0, 0, null, null);
for (int y = 0 ; y < size ; y++) {
for (int x = 0 ; x < size ; x++) {
int rgb = image.getRGB(x, y);
int blue = rgb & 0xff;
int green = (rgb & 0xff00) >> 8;
int red = (rgb & 0xff0000) >> 16;
//int alpha = (rgb & 0xff000000) >> 24;
if (red == 255 && green == 255 && blue == 255) {
// make white transparent
image2.setRGB(x, y, 0);
}
}
}
eraserCursor = Toolkit.getDefaultToolkit().createCustomCursor(
image2, new Point(size / 2, size / 2), "eraserCursor");
In my code, to achieve translucent image, I use this snippet:
g.setComposite(AlphaComposite.SrcOver.derive(0.8f));
g.drawImage(image, 0, 0, null);
g.setComposite(AlphaComposite.SrcOver);
See whether it works for you...

Categories