Wednesday 7 September 2011

Find Largest Rectangle in Rotated Image


Some time ago I was working with images in Java. I wanted to rotate an image but also crop the image so you do not see any blank areas. When you rotate an image in java it does not crop the blanks areas for you. In my example I will show you how you can use my algorithm to find the best dimensions to crop based on the largest rectangle possible for the rotated image.

To get started I will first show you my original image.
Figure 1. Original image of Purple Swamphen

Now I will rotate my image by 20 degrees. My image has the black blank areas that I do not wish to have in my final image. What I want is a cropped image of the largest rectangle possible in this rotated image.
Figure 2. Rotated image by 20 degrees

There is not just one large rectangle that will fit into the rotated image. There are many possible rectangles, as seen in Figure 3.
Figure 3. Rotated image showing 2 rectangles filling the rotated image
Figure 3 shows the rotated image with 2 inner rectangles filling the inside of the rotated image. Usually we want the largest area rectangle, or possibly the widest or tallest.

Here are 2 examples showing the images we may want. Figure 4 showing the widest rectangle which also has the largest area, and Figure 5 showing the tallest rectangle.
Figure 4. Widest and Largest cropped rectangle from rotated image

Figure 5. Tallest cropped rectangle from rotated image


Here is the algorithm to calculate the largest rectangle in a rotated image.
/**
  * Return a largest Rectangle that will fit in a rotated image
  * @param imgWidth Width of image
  * @param imgHeight Height of Image
  * @param rotAngDeg Rotation angle in degrees
  * @param type 0 = Largest Area, 1 = Smallest Area, 2 = Widest, 3 = Tallest
  * @return
  */
 public static Rectangle getLargestRectangle(double imageWidth, double imageHeight, double rotAngDeg, int type) {
  Rectangle rect = null;
  
  double rotateAngleDeg = rotAngDeg % 180d;
  if (rotateAngleDeg < 0d) {
   rotateAngleDeg += 360d;
   rotateAngleDeg = rotateAngleDeg % 180d;
  }
  double imgWidth = imageWidth;
  double imgHeight = imageHeight;
  
  if (rotateAngleDeg == 0 || rotateAngleDeg == 180) {
   // Angle is 0. No change needed
   rect = new Rectangle(0,0,(int)imgWidth,(int)imgHeight);
   return rect;
  }
  
  if (rotateAngleDeg == 90) {
   // Angle is 90. Width and height swapped
   rect = new Rectangle(0,0,(int)imgHeight,(int)imgWidth);
   return rect;
  }
  
  if (rotateAngleDeg > 90) {
   // Angle > 90 therefore angle = 90 - ("+rotateAngleDeg+" - 90) = "+(90 - (rotateAngleDeg - 90))
   rotateAngleDeg = 90 - (rotateAngleDeg - 90);
  }
  
  double rotateAngle = Math.toRadians(rotateAngleDeg);
  double sinRotAng = Math.sin(rotateAngle);
  double cosRotAng = Math.cos(rotateAngle);
  double tanRotAng = Math.tan(rotateAngle);
  // Point 1 of rotated rectangle
  double x1 = sinRotAng * imgHeight;
  double y1 = 0;
  // Point 2 of rotated rectangle
  double x2 = cosRotAng * imgWidth + x1;
  double y2 = sinRotAng * imgWidth;
  // Point 3 of rotated rectangle
  double x3 = x2 - x1;
  double y3 = y2 + cosRotAng * imgHeight;
  // Point 4 of rotated rectangle
  double x4 = 0;
  double y4 = y3 - y2;
  // MidPoint of rotated image
  double midx = x2 / 2;
  double midy = y3 / 2;
  
  // Angle for new rectangle (based on image width and height)
  double imgAngle = Math.atan(imgHeight / imgWidth);
  double imgRotAngle = Math.atan(imgWidth / imgHeight);
  double tanImgAng = Math.tan(imgAngle);
  double tanImgRotAng = Math.tan(imgRotAngle);
  // X Point for new rectangle on bottom line
  double ibx1 = midy / tanImgAng + midx;
  double ibx2 = midy * tanImgAng + midx;
  
  // First intersecting lines
  // y = ax + b  ,  y = cx + d  ==>  x = (d - b) / (a - c)
  double a = y2 / x3;
  double b = tanRotAng * -x1;
  double c = -imgHeight / imgWidth;
  double d = tanImgAng * ibx1;
  
  // Intersecting point 1
  double ix1 = (d - b) / (a - c);
  double iy1 = a * ix1 + b;
  
  // Second intersecting lines
  c = -imgWidth / imgHeight;
  d = tanImgRotAng * ibx2;
  
  // Intersecting point 2
  double ix2 = (d - b) / (a - c);
  double iy2 = a * ix2 + b;
  
  // Work out smallest rectangle
  double radx1 = Math.abs(midx - ix1);
  double rady1 = Math.abs(midy - iy1);
  double radx2 = Math.abs(midx - ix2);
  double rady2 = Math.abs(midy - iy2);
  // Work out area of rectangles
  double area1 = radx1 * rady1;
  double area2 = radx2 * rady2;
  // Rectangle (x,y,width,height)
  Rectangle rect1 = new Rectangle((int)Math.round(midx-radx1),(int)Math.round(midy-rady1),
    (int)Math.round(radx1*2),(int)Math.round(rady1*2));
  
  // Rectangle (x,y,width,height)
  Rectangle rect2 = new Rectangle((int)Math.round(midx-radx2),(int)Math.round(midy-rady2),
    (int)Math.round(radx2*2),(int)Math.round(rady2*2));
  
  switch (type) {
   case 0: rect = (area1 > area2 ? rect1 : rect2); break;
   case 1: rect = (area1 < area2 ? rect1 : rect2); break;
   case 2: rect = (radx1 > radx2 ? rect1 : rect2); break;
   case 3: rect = (rady1 > rady2 ? rect1 : rect2); break;
  }
  
  return rect;
 }

Here is some code you want want to use for your own testing. It uses JAI for rotating and cropping but really you can use any library for this.

    public static void main(String[] args) throws IOException {
  
     BufferedImage src = ImageIO.read(new File("MyTestImage.jpg"));
     String outType = "jpg";
     int imgWidth = 1280;
     int imgHeight = 851;

     double rotateAngle = 20d;
     BufferedImage rotImg = rotateImage(src, rotateAngle);
     ImageIO.write(rotImg, outType, new File("RotateTest."+outType));
         
     Rectangle rectLargest = getLargestRectangle(imgWidth,imgHeight,rotateAngle,0);
     Rectangle rectTallest = getLargestRectangle(imgWidth,imgHeight,rotateAngle,3);
         
     BufferedImage rotCropImage = cropImage(rotImg, (int)rectLargest.getX(), (int)rectLargest.getY(), (int)rectLargest.getWidth(), (int)rectLargest.getHeight());
     ImageIO.write(rotCropImage, outType, new File("RotateCropTest_LG."+outType));

     rotCropImage = cropImage(rotImg, (int)rectTallest.getX(), (int)rectTallest.getY(), (int)rectTallest.getWidth(), (int)rectTallest.getHeight());
     ImageIO.write(rotCropImage, outType, new File("RotateCropTest_TA."+outType));
    }
        
    public static BufferedImage rotateImage(BufferedImage image, double angle)
    {
     // Gets the angle (converting it to radians).
     float rangle = (float)Math.toRadians(angle);
     // Gets the rotation center.
     float centerX = 0f; float centerY = 0f;
     centerX = image.getWidth()/2f;
     centerY = image.getHeight()/2f;
 
     // Rotates the original image.
     ParameterBlock pb = new ParameterBlock();
     pb.addSource(image);
     pb.add(centerX);
     pb.add(centerY);
     pb.add(rangle);
     pb.add(new InterpolationBilinear());
     // Creates a new, rotated image and uses it on the DisplayJAI component
     RenderedOp result = JAI.create("rotate", pb);
     return result.getAsBufferedImage();
    }
    
    public static BufferedImage cropImage(BufferedImage image, int topLeftX, int topLeftY, int width, int height)
    {
     // Crops the original image.
     ParameterBlock pb = new ParameterBlock();
     pb.addSource(image);
     pb.add((float)topLeftX);
     pb.add((float)topLeftY);
     pb.add((float)width);
     pb.add((float)height);
     // Creates a new, rotated image and uses it on the DisplayJAI component
     RenderedOp result = JAI.create("crop", pb);
     return result.getAsBufferedImage();
    }


4 comments:

  1. The widest and biggest in area crop of your image rotated by 20° has roughly these dimensions:
    1191 x 471 pix (with an area of ~5.6 mio pixels)

    Your crop has these dimensions:
    880 x 585 pix (with an area of ~5.1 mio pixels)

    Your assumptions about the optimal solution seem not to be correct. See
    http://stackoverflow.com/questions/5789239/calculate-largest-rectangle-in-a-rotated-rectangle#7519376
    for a correct solution of the maximal area.

    ReplyDelete
    Replies
    1. Here is the proof:
      http://tinypic.com/r/6s48bn/5

      (Note that it has been resized to 1023px × 405px due to restrictions of tinypic.com; the original size was 1192 x 472)

      Delete
  2. Hi from another Aussie developer :P

    Just thought you might like to know - your blog post here has been making the rounds on Stack Overflow - myself and several others have been searching for a solution to this problem. Your result here was close, however I ended up using a different algorithm that works slightly better - see http://stackoverflow.com/questions/16702966/rotate-image-and-crop-out-black-borders

    ReplyDelete
  3. Thanks guys for your comments. I did this work a number of years ago and did not find a good solution at the time so came up with this algorithm myself. Glad to see there is an interest and a better solution out there. Hope this work helps get you started on your way.

    ReplyDelete