Image rotation in java

Asked

Viewed 4,725 times

12

I need an algorithm to rotate an image in java. On the Internet I found a promising algorithm at this link, and I tidied it up and adapted it to use angles in degrees. It was like this:

public static BufferedImage rotateImage(BufferedImage rotateImage, double angle) {
    AffineTransform tx = new AffineTransform();
    tx.rotate(Math.toRadians(angle), rotateImage.getWidth() / 2.0, rotateImage.getHeight() / 2.0);

    double ytrans = tx.transform(new Point2D.Double(0.0, 0.0), null).getY();
    double xtrans = tx.transform(new Point2D.Double(0, rotateImage.getHeight()), null).getX();

    AffineTransform translationTransform = new AffineTransform();
    translationTransform.translate(-xtrans, -ytrans);
    tx.preConcatenate(translationTransform);

    return new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR).filter(rotateImage, null);
}

However, this method does not work properly for angles greater than 90 degrees (and probably for smaller than -90 as well). I solved this with a gambiarra that depends on an intermediate image:

public static BufferedImage rotateImage(BufferedImage rotateImage, double angle) {
    angle %= 360;
    if (angle < 0) angle += 360;
    int quadrants = (int) angle / 90;
    double restAngle = angle % 90;
    if (restAngle < 0) restAngle += 90;

    AffineTransform tx = new AffineTransform();
    tx.rotate(Math.toRadians(restAngle), rotateImage.getWidth() / 2.0, rotateImage.getHeight() / 2.0);

    double ytrans = tx.transform(new Point2D.Double(0.0, 0.0), null).getY();
    double xtrans = tx.transform(new Point2D.Double(0, rotateImage.getHeight()), null).getX();

    AffineTransform translationTransform = new AffineTransform();
    translationTransform.translate(-xtrans, -ytrans);
    tx.preConcatenate(translationTransform);

    BufferedImage b2 = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR).filter(rotateImage, null);

    AffineTransform fix = AffineTransform.getQuadrantRotateInstance(
            quadrants, b2.getWidth() / 2.0, b2.getHeight() / 2.0);
    return new AffineTransformOp(fix, AffineTransformOp.TYPE_BILINEAR).filter(b2, null);
}

Worked. However, I know that it is possible to solve this without needing an intermediate image. I’ve broken my head enough, and I can’t make it work any better, I’ve just had images cut out and deformed as a result. Someone has an idea?

Edit: I found a bug: It doesn’t work properly for images that aren’t square, and I don’t know why.

2 answers

13

The answer from @Rodrigodebonasartor works perfectly and the big takeaway is that the algorithm makes an adjustment considering the corners of the image that are "hidden" after the rotation, as a way to avoid that the image is truncated in those regions.

The graph of Rodrigo’s own response helps a lot in this understanding, but I’ll try to explain it in a little more detail. I will use as an example the following (square) image of Lenna:

inserir a descrição da imagem aqui

Rotation is an operation that simply transforms each pixel of the image to a given angle (see this Wikipedia page for more information). By applying a 45º rotation without translating adjustment, the result causes the upper left corner (coordinate (0,0)) and the lower left corner (coordinated (0, height)) are positioned outside the original image area, producing the following result:

inserir a descrição da imagem aqui

A code like...

Point2D ponto = tx.transform(new Point2D.Double(rotateImage.getWidth(), 0.0), null);

...simply maps the original point (in the example above, (width, 0)) according to the transformation described by tx (that could have a series of concatenated transformations - the result would be the final mapping after all transformations on the point). Note that in the code of this question this is equivalent to calculating the rotation of the point (x' = x * cosseno(angulo) - y * seno(angulo), y' = x * seno(angulo) + y * cosseno(angulo)), since the transformation tx includes only one rotation.

With an angle of 45º, the rotation of each corner of the image produces the following results:

Point2D.Double(0.0, 0.0) => Point2D.Double[127.99999999999999, -53.01933598375615]
Point2D.Double(rotateImage.getWidth(), 0.0) => Point2D.Double[309.01933598375615, 128.0]
Point2D.Double(0.0, rotateImage.getHeight()) => Point2D.Double[-53.019335983756164, 128.00000000000003]
Point2D.Double(rotateImage.getWidth(), rotateImage.getHeight()) => Point2D.Double[128.0, 309.01933598375615]

As in the Java language the coordinate system originates (coordinate (0,0)) in the upper left corner, it can be observed that the "hidden" corners (that is, outside the original area of the image because they are to the left or above the origin) have coordinates mapped in negative value. This goes for both the X-axis and the Y-axis.

Thus, what the algorithm proposed by Rodrigo does is simply check which corners are "hidden" (according to the range of the given angle) and calculate the translation difference per axis in order to "move" the excondidos corners to the visible limit of the image (i.e., close to the origin (0,0)). It’s a simple balcony, and it works.

However, to make the code a little more streamlined (and potentially simpler to understand), simply calculate the rotation for the four corners of the image and choose, among the results, the lowest negative values for the X and Y axes.

Here’s an example:

public static BufferedImage rotateImage(BufferedImage rotateImage, double angle) {
    AffineTransform tx = new AffineTransform();
    tx.rotate(Math.toRadians(angle), rotateImage.getWidth() / 2.0, rotateImage.getHeight() / 2.0);

    // Rotaciona as coordenadas dos cantos da imagem
    Point2D[] aCorners = new Point2D[4];
    aCorners[0] = tx.transform(new Point2D.Double(0.0, 0.0), null);
    aCorners[1] = tx.transform(new Point2D.Double(rotateImage.getWidth(), 0.0), null);
    aCorners[2] = tx.transform(new Point2D.Double(0.0, rotateImage.getHeight()), null);
    aCorners[3] = tx.transform(new Point2D.Double(rotateImage.getWidth(), rotateImage.getHeight()), null);

    // Obtém o valor de translação para cada eixo com um canto "escondido"
    double dTransX = 0;
    double dTransY = 0;
    for(int i = 0; i < 4; i++) {
        if(aCorners[i].getX() < 0 && aCorners[i].getX() < dTransX)
            dTransX = aCorners[i].getX();
        if(aCorners[i].getY() < 0 && aCorners[i].getY() < dTransY)
            dTransY = aCorners[i].getY();
    }

    // Aplica a translação para evitar cortes na imagem
    AffineTransform translationTransform = new AffineTransform();
    translationTransform.translate(-dTransX, -dTransY);
    tx.preConcatenate(translationTransform);

    return new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR).filter(rotateImage, null);
}

As in Rodrigo’s answer, this algorithm also correctly adjusts the image to avoid clippings:

inserir a descrição da imagem aqui

In the case of the previous example image, it is easy to notice that the necessary amount of translation is the same both in the X axis and in the Y axis (53 pixels approximately), but this is due to the angle used (45º) and the fact that the image is square. As the proposed algorithm checks the amounts individually per axis, it works correctly for other examples, as below (rotation of 260º and rectangular image - note that there are two negative values for the Y axis, but the algorithm chooses the smallest = -104 approximately):

inserir a descrição da imagem aquiinserir a descrição da imagem aqui

Point2D.Double(0.0, 0.0) => Point2D.Double[123.93876331951267, 328.9969705899713]
Point2D.Double(rotateImage.getWidth(), 0.0) => Point2D.Double[54.47949225274054, -64.92613061491193]
Point2D.Double(0.0, rotateImage.getHeight()) => Point2D.Double[345.52050774725944, 289.926130614912]
Point2D.Double(rotateImage.getWidth(), rotateImage.getHeight()) => Point2D.Double[276.06123668048735, -103.99697058997123]

Translation required: in X = [0.0], in Y = [-103.99697058997123]

  • 1

    Okay, I like the answer. Unfortunately I can’t accept both answers, so I’m going to think a little bit to decide which one I accept.

  • 1

    Thanks. I only offered an additional explanation. The original idea of the algorithm is from @Rodrigodebonasartor. :)

9


From what I’ve tested here, so it works:

public static BufferedImage rotateImage(BufferedImage rotateImage, double angle) {
    angle %= 360;
    if (angle < 0) angle += 360;

    AffineTransform tx = new AffineTransform();
    tx.rotate(Math.toRadians(angle), rotateImage.getWidth() / 2.0, rotateImage.getHeight() / 2.0);

    double ytrans = 0;
    double xtrans = 0;
    if( angle <= 90 ){
        xtrans = tx.transform(new Point2D.Double(0, rotateImage.getHeight()), null).getX();
        ytrans = tx.transform(new Point2D.Double(0.0, 0.0), null).getY();
    }
    else if( angle <= 180 ){
        xtrans = tx.transform(new Point2D.Double(rotateImage.getWidth(), rotateImage.getHeight()), null).getX();
        ytrans = tx.transform(new Point2D.Double(0, rotateImage.getHeight()), null).getY();
    }
    else if( angle <= 270 ){
        xtrans = tx.transform(new Point2D.Double(rotateImage.getWidth(), 0), null).getX();
        ytrans = tx.transform(new Point2D.Double(rotateImage.getWidth(), rotateImage.getHeight()), null).getY();
    }
    else{
        xtrans = tx.transform(new Point2D.Double(0, 0), null).getX();
        ytrans = tx.transform(new Point2D.Double(rotateImage.getWidth(), 0), null).getY();
    }

    AffineTransform translationTransform = new AffineTransform();
    translationTransform.translate(-xtrans, -ytrans);
    tx.preConcatenate(translationTransform);

    return new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR).filter(rotateImage, null);
}

Follow an image to help understand the logic: Rotação

  • 1

    Okay, I like the answer. Unfortunately I can’t accept both answers, so I’m going to think a little bit to decide which one I accept.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.