This question already has an answer here:
Rotate BufferedImage with transparent background
(1 answer)
Closed 2 years ago.
I want to rotate a BufferedImage by an angle in radians. I used the following code. matrixImage is a matrix of integers where foreground pixels have 1 as value while background pixels have 0 as value. The new BufferedImage is correctly rotated but the extra borders are black. The new image is bigger than the original one and the new parts are black. I want that all the background pixels of the new image are white. I tried the solution proposed at Rotate BufferedImage and remove black bound, but I noticed that during the rotation the image changes.
bufferedImage = matrix2BufferedImage(matrixImage);
AffineTransform transform = new AffineTransform();
transform.rotate(radians, bufferedImage.getWidth() / 2, bufferedImage.getHeight() / 2);
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
bufferedImage = op.filter(bufferedImage, null);
I solved using the following code for rotating
private BufferedImage rotateImage(BufferedImage sourceImage, double angle) {
AffineTransform transform = new AffineTransform();
transform.rotate(angle, sourceImage.getWidth() / 2, sourceImage.getHeight() / 2);
AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
BufferedImage destImage = op.filter(sourceImage, null);
Graphics2D g2d = destImage.createGraphics();
g2d.drawRenderedImage(sourceImage, transform);
g2d.dispose();
return destImage;
}
Then, I binarised the buffered image with the following code
int value=binarized.getRGB(x,y);
if(value==0)
value=-1;
output[y][x] = ((0xFFFFFF & value) == 0xFFFFFF) ? (byte) 0 : 1;
Thanks for suggesting me the right post!
Related
I'll start of by showing examples of what's wrong then I'll explain how, and finally I'll ask my question.
This is the picture I want to rotate.
I am rotating it 90 degrees and 270 degrees, on multiple occasions and then combiningthose into a big buffered-image.
The code I am using to rotate a single bufferedImage is this:
public static BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = new BufferedImage(w, h, img.getType());
Graphics2D g = dimg.createGraphics();
g.rotate(Math.toRadians(angle), w/2, h/2);
g.drawImage(img, null, 0, 0);
return dimg;
}
The out come of the rotation looks something like this.
The reason those black bars are these is because in the code you can clearly see I create a separate buffered-image which will be the final image.
Which uses the original width and hight, since the image is rotated the with and height switch so I compensated for this by changing BufferedImage dimg = new BufferedImage(w, h, img.getType()); to BufferedImage dimg = new BufferedImage(h, w, img.getType());.
I though it would be logical that this would solve my problem.
But I was wrong now the rotational outcome is this.
So from this point on is where I have no clue why it's doing this.
I might just be overlooking a tiny thing, or it's a common error even though I can't find any instance of this occurring.
So here is my question to you, why does it do this? And how do I fix this.
The image isn't square. If you rotate it by 90°, then you will create a gap that you need to fill.
Solutions:
Make sure the image is square
"Rotate" the size: When you rotate by 90° or 270°, you need to create a target image with swapped width and height (i.e. 200x100 -> 100x200)
Crop the image. Good in your case since scaling will make the arrow look bad but it might be out of center
Scale the image. If it's 609x579, scale it down to 579x579 (scaling down will usually look a little bit better).
Find the border color and fill the gap with the border color after the rotation
I figured it out.
The thing I was doing in the start was rotating the host image (dimg),
and then drawing the original image to it.
I could just as well have tried to fit a square in a circle my earlier rotation actually makes no sense at all.
So what I need to do is first create the host, draw the image to the host, the rotate the host and return it as the final image.
public static BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth();
int h = img.getHeight();
BufferedImage dimg = new BufferedImage(w, h, img.getType());
Graphics2D g = dimg.createGraphics();
g.drawImage(img, null, 0, 0); //Draw before rotating
g.rotate(Math.toRadians(angle), w/2, h/2); //Rotating after drawing
return dimg;
}
I hope this helps out some other people as well
if you want to use a similar code as first code
this may help ( if you remove the comments and debug lines (such as painting the background) it has only the translate((W-w)/2,(H-h)/2) line in addition )
// do not forget to import static java.lang.Math.*
public static BufferedImage rotate(BufferedImage img, int angle) {
int w = img.getWidth(null);
int h = img.getHeight(null);
double rad = toRadians(angle);
double eps = 1e-3;
int W=(int)(abs(cos(rad))*w+abs(sin(rad))*h-eps)+1;//W after rotation(calculated by using a little geometry )
int H=(int)(abs(sin(rad))*w+abs(cos(rad))*h-eps)+1;//H after rotation
//you may use max value ( diameter of the rectangle ) instead of dynamic value but in that case you must be careful of the black edges ( in this case red edges )
// if 90 is not a divisor of angle then you can't fit a rectangle with that angle in another one so the red edges are inevitable
// but with calculated W and H this edges are minimum
BufferedImage dimg = new BufferedImage(W,H, BufferedImage.TYPE_INT_RGB);// you can change it to any type you want it's just a sample
Graphics2D g = dimg.createGraphics();
g.setColor(Color.RED); // background color of red for displaying the red edges when image is not completely fit
g.fillRect(0, 0, W, H);
int x=(W-w)/2;
int y=(H-h)/2;
g.translate(x, y); // moving dimg center to img center ( this was what first code lack in )
g.rotate(-rad, w/2, h/2); // now rotating dimg around the center of img ( which is now same as center of dimg )
// we rotate dimg by -rad and draw img normally , it's like rotating img by rad instead of dimg by -rad
g.drawImage(img,null,0,0); // and drawing
return dimg;
}
I've tried using affine transform to scale down a BufferedImage, But I only manage to scale up the image and not make it smaller like I need to.
Here's my scale up code.
public BufferedImage Scale(){
BufferedImage after = new BufferedImage(before.getWidth(), before.getHeight(), BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(-2.0, -2.0);
AffineTransformOp scale = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
after = scale.filter(before, after);
return after;
}
To scale down, use a scale in the range (0.0, 1.0), instead of negatives.
When you apply a scaling Affine Transform with scale (xScale, yScale), the new dimensions are (imgWidth*xScale, imgHeight*yScale).
If you draw this onto a graphics context, you can specify a new width and height in the drawImage call.
This is my function to resize images.
The quality is not photoshop but it's acceptable.
What's not acceptable is the behaviour on indexed png.
We expect that if we scale down an image with a 256 colors palette with a transparent index we would get a resized image with same transparency, but this it not the case.
So we did the resize on a new ARGB image and then we reduce it to 256 colors. The problem is how to "reintroduce" the transparent pixel index.
private static BufferedImage internalResize(BufferedImage source, int destWidth, int destHeight) {
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
double xScale = ((double) destWidth) / (double) sourceWidth;
double yScale = ((double) destHeight) / (double) sourceHeight;
Graphics2D g2d = null;
BufferedImage resizedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TRANSLUCENT);
log.debug("resizing image to w:" + destWidth + " h:" + destHeight);
try {
g2d = resizedImage.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
AffineTransform at = AffineTransform.getScaleInstance(xScale, yScale);
g2d.drawRenderedImage(source, at);
} finally {
if (g2d != null)
g2d.dispose();
}
//doesn't keep the transparency
if (source.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
log.debug("reducing to color-indexed image");
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED);
try {
Graphics g = indexedImage.createGraphics();
g.drawImage(resizedImage, 0, 0, null);
} finally {
if (g != null)
g.dispose();
}
System.err.println("source" + ((IndexColorModel) source.getColorModel()).getTransparentPixel()
+ " " + ((IndexColorModel) indexedImage.getColorModel()).getTransparentPixel());
return indexedImage;
}
return resizedImage;
}
Try changing
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED);
to
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel) source.getColorModel());
Even if that specifically doesn't help you (which it might not if the resizing, for whatever reason, changes what specific color values are indexed), the fact that you can create a new BufferedImage with a given IndexColorModel will probably be quite useful for you.
http://download.oracle.com/javase/6/docs/api/java/awt/image/BufferedImage.html#BufferedImage%28int,%20int,%20int,%20java.awt.image.IndexColorModel%29
EDIT: Just noticed that your resizedImage constructor should probably use BufferedImage.TYPE_INT_ARGB rather than BufferedImage.TRANSLUCENT. Not sure if that will change how it works, but BufferedImage.TRANSLUCENT isn't supposed to be passed to that form of the constructor. http://download.oracle.com/javase/1,5.0/docs/api/java/awt/image/BufferedImage.html#BufferedImage%28int,%20int,%20int%29
Anyway, maybe try something like this:
DirectColorModel resizedModel = (DirectColorModel) resizedImage.getColorModel();
int numPixels = resizedImage.getWidth() * resizedImage.getHeight();
byte[numPixels] reds;
byte[numPixels] blues;
byte[numPixels] greens;
byte[numPixels] alphas;
int curIndex = 0;
int curPixel;
for (int i = 0; i < resizedImage.getWidth(); i++)
{
for (int j = 0; j < resizedImage.getHeight(); j++)
{
curPixel = resizedImage.getRGB(i, j);
reds[curIndex] = resizedModel.getRed(curPixel);
blues[curIndex]= resizedModel.getBlue(curPixel);
greens[curIndex] = resizedModel.getGreen(curPixel);
alphas[curIndex] = resizedModel.getAlpha(curPixel);
curIndex++;
}
}
BufferedImage indexedImage = new BufferedImage(destWidth, destHeight, BufferedImage.TYPE_BYTE_INDEXED, new IndexColorModel(resizedModel.pixel_bits, numPixels, reds, blues, greens, alphas));
Don't know if this will actually work, though.
Indexed images with transparency are a hack. They only work under certain conditions and resizing isn't one of them.
An image with transparency doesn't just have fully opaque and fully transparent pixels. In particular at a irregularly shaped borders, there are many pixels with partial transparency. If you save it in a format with indexed colors where a single color is used for transparent pixels, you have to decide what color the background will have. All pixels with partial transparency are then blended between their color and the background color (according to their transparency) and become fully opaque. Only the fully transparent pixel are assigned the transparent pseudo color.
If such an image is displayed against a background with differnt color, an ugly border will become apparent. It's an artifact of the inadequate transparency handling.
When you resize the image, you introduce more artifacts. The color of a new pixels is usually blended from several neighboring pixels. If some are transparent and some are opaque, the result is a partially transparent pixel. When you save it, the partially transparent pixel is blended against the background color and becomes opaque. As a result, the opaque area (and the associated artifacts) grow with each resize (or most other image manipulations).
Whatever programming language or graphics library you use, the artifacts will grow and the result will become worse. I recommend you use a ARGB buffer and save the image as a non-indexed PNG file.
Java ImageIO correctly displays this black & white image http://www.jthink.net/jaikoz/scratch/black.gif but when I try and resize it using this code
public static BufferedImage resize2D(Image srcImage, int size)
{
int w = srcImage.getWidth(null);
int h = srcImage.getHeight(null);
// Determine the scaling required to get desired result.
float scaleW = (float) size / (float) w;
float scaleH = (float) size / (float) h;
MainWindow.logger.finest("Image Resizing to size:" + size + " w:" + w + ":h:" + h + ":scaleW:" + scaleW + ":scaleH" + scaleH);
//Create an image buffer in which to paint on, create as an opaque Rgb type image, it doesn't matter what type
//the original image is we want to convert to the best type for displaying on screen regardless
BufferedImage bi = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
// Set the scale.
AffineTransform tx = new AffineTransform();
tx.scale(scaleW, scaleH);
// Paint image.
Graphics2D g2d = bi.createGraphics();
g2d.setComposite(AlphaComposite.Src);
g2d.drawImage(srcImage, tx, null);
g2d.dispose();
return bi;
}
I just end up with a black image. Im trying to make the image smaller (a thumbnail) but even if I resize it larger for test purposes it still ends up as a black square.
Other images resize okay, anyone know what is the problem with the gif/and or Java Bug
Here is the string representation of the ColorModel of the linked image when loaded through ImageIO:
IndexColorModel: #pixelBits = 1 numComponents = 4 color space = java.awt.color.ICC_ColorSpace#1572e449 transparency = 2 transIndex = 1 has alpha = true isAlphaPre = false
If I understand this correctly, you have one bit per pixel, where a 0 bit is opaque black and a 1 bit is transparent. Your BufferedImage is initially all black, so drawing a mixture of black and transparent pixels onto it will have no effect.
Although you are using AlphaComposite.Src this will not help as the R/G/B values for the transparent palette entry read as zero (I am not sure whether this is encoded in the GIF or just the default in the JDK.)
You can work around it by:
Initializing the BufferedImage with all-white pixels
Using AlphaComposite.SrcOver
So the last part of your resize2D implementation would become:
// Paint image.
Graphics2D g2d = bi.createGraphics();
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, size, size);
g2d.setComposite(AlphaComposite.SrcOver);
g2d.drawImage(srcImage, tx, null);
Try this:
BufferedImage bi = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
That makes it work. Of course, the question is why..?
I am trying to use JAI to perform a rotate task on an image. I can get this working no problem. However, there is severe loss of midtones in the image. The image can be rotated in photoshop without this lack of contrast.
Please see the following 3 images stacked next to each other here, to see what I mean;
http://imgur.com/SYPhZ.jpg
The top image is the original, the middle is rotated in photoshop to prove that it can be done, and the bottom is from the result of my code.
To see the actual images, please see here;
Before rotate: http://imgur.com/eiAOO.jpg
After rotate : http://imgur.com/TTUKS.jpg
You can see the issue most clearly if you load the images in two different tabs, and flick between them.
In terms of code, I load the image as follows;
public void testIt() throws Exception {
File source = new File("c:\\STRIP.jpg");
FileInputStream fis = new FileInputStream(source);
BufferedImage sourceImage = ImageIO.read(fis);
fis.close();
BufferedImage rotatedImage = doRotate(sourceImage, 15);
FileOutputStream output = new FileOutputStream("c:\\STRIP_ROTATED.jpg");
ImageIO.write(rotatedImage, "JPEG", output);
}
and then here is the rotate function;
public BufferedImage doRotate(BufferedImage input, int angle) {
int width = input.getWidth();
int height = input.getHeight();
double radians = Math.toRadians(angle / 10.0);
// Rotate about the input image's centre
AffineTransform rotate = AffineTransform.getRotateInstance(radians, width / 2.0, height / 2.0);
Shape rect = new Rectangle(width, height);
// Work out how big the rotated image would be..
Rectangle bounds = rotate.createTransformedShape(rect).getBounds();
// Shift the rotated image into the centre of the new bounds
rotate.preConcatenate(
AffineTransform.getTranslateInstance((bounds.width - width) / 2.0, (bounds.height - height) / 2.0));
BufferedImage output = new BufferedImage(bounds.width, bounds.height, input.getType());
Graphics2D g2d = (Graphics2D) output.getGraphics();
// Fill the background with white
g2d.setColor(Color.WHITE);
g2d.fill(new Rectangle(width, height));
RenderingHints hints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHints(hints);
g2d.drawImage(input, rotate, null);
return output;
}
This is apparently a bug in JAI that has existed for a while:
The earliest mention that I was able to find of this issue appears here. That original article points to an old jai-core issue here. Having read that resolution, it appears that there is a root bug that is still open and described here.
Whether or not all of that detective work is relevant to your application, it may be possible to construct a color space that is more tolerant than the default that JAI is using for your test code.
In the absolute worst case, you could write the pixel traversal yourself to create a rotated image. That isn't the optimal solution but I mention it for completeness if you absolutely need a solution to this problem today.