15

The aspect-ratio=height/width is always >1 (for most cases even >2), so it should be clear/precise how I'd like to rotate.


I have a RotatedRect object in OpenCV / Java.
I can get an Array of it with 4 Objects of Type Point and Point defines x/y-values.

Now I'd like to sort those 4 Points so that the top-left Point is the first element of the Array and then clockwise, so that the top-bottom point is the fourth element.

I'm assuming that the Rectangles aren't rotated much (just some small angles), e.g.

I've indicated in the example which point I'd refer to as top left (TL).

How to do it?

You don't need to tell me specifically for OpenCV etc., just assume you'd have two arrays

int[] x = new int[4];
int[] y = new int[4];

Whereas the n-th Point has the coordinates (x[n-1], y[n-1]). I can then do it for OpenCV specifically myself.

double-beep
  • 5,031
  • 17
  • 33
  • 41
tim
  • 9,896
  • 20
  • 81
  • 137
  • Is one of the small sides of the rectangle always the upper edge? – Steve Benett Mar 27 '14 at 19:06
  • Yes exactly. And I'm as well assuming that the angles are small, which means like shown in the image – tim Mar 27 '14 at 19:09
  • In image space, what are you rotating relative to what? – Engineer2021 Mar 27 '14 at 19:13
  • Is it ever possible for the angle to be 45 degrees? I assume not because then the it's ambiguous whether you want the top or the left corner – durron597 Mar 27 '14 at 19:13
  • @staticx: Im not quite sure what you are refering to. I've got a rectangle-object as shown and the four corner points unordered and just want to sort them as I supposed. ||durron597: I assume not as well. But I would not see any problems with it. Maybe I should have mentioned that the rectangle is always higher then its width (aspect-ratio > 1), i.e. it would still be unique/distinct which is the top-left point :-) – tim Mar 27 '14 at 19:20
  • Where's 0,0? You haven't given us enough information – Engineer2021 Mar 27 '14 at 19:21
  • Sorry... added into the picture. Should be clear now!? – tim Mar 27 '14 at 19:24
  • Um, with small enough angles, top vertices have smaller `y` coordinate, and left of those is the one with smaller `x`. Do I miss something? –  Mar 27 '14 at 19:46
  • True, sounds good to me. I'll be thinking about it. It would even be the case for bigger angles depending on the aspect ration, wouldn't it? – tim Mar 27 '14 at 19:48
  • Yes, but not too big of course. –  Mar 27 '14 at 19:51

5 Answers5

6

Answer

There is an extremely easy solution if you know that:

  1. -45 < roundedRect.angle < 45
  2. roundedRect.size.height > roundedRect.size.width

If that is true, then the points, in clockwise order, will ALWAYS be in this order:

pts[0], pts[3], pts[2], pts[1]

As an aside, if it doesn't harm your program too much, the points are delivered in counterclockwise order, starting with the top left... then you wouldn't have to do any reordering / sorting.

Other cases:

  • height > width && 135 < roundedRect.angle < 225
    • The clockwise order starting from the top left is 2,3,0,1
    • The counterclockwise order from top left is 2,1,0,3.
  • width > height && -135 < roundedRect.angle < -45
    • The clockwise order starting from the top left is 3,2,1,0
    • The counterclockwise order from top left is 3,0,1,2
  • width > height && 45 < roundedRect.angle < 135
    • The clockwise order starting from the top left is 1,0,3,2
    • The counterclockwise order from top left is 1,2,3,0

The remaining cases would all imply that the rectangle is bigger from left to right than it is from top to bottom, which cannot happen in your scenario. Also, if the angle is outside these ranges, you can add or subtract 360 successively to get an angle in one of these ranges.


Explanation

(tl;dr)

We know this from how OpenCV calculates the values of those points. You can figure this out with a little experimentation. Here's a little program I wrote that demonstrates it:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

import org.opencv.core.Point;
import org.opencv.core.RotatedRect;
import org.opencv.core.Size;

public class TestFrame extends JFrame {
    public static void main(String... args) {
        final TestFrame frame = new TestFrame();
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                frame.setVisible(true);
            }
        });
    }

    private RectComponent rect;

    public TestFrame() {
        JPanel containerPane = new JPanel(new BorderLayout());
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        rect = new RectComponent();
        containerPane.add(rect);
        setContentPane(containerPane);
        setSize(400,400);
        new Timer(100, rect).start();
    }

    public class RectComponent extends JComponent implements ActionListener {
        private RotatedRect rect = new RotatedRect(new Point(0,0), new Size(1, 3), 0);

        private final Point[] pts = new Point[4];

        @Override
        protected void paintComponent(Graphics g) {
            rect.points(pts);
            printPoints();
            Dimension size = getSize();
            drawRectLine(g, pts[0], pts[1], size);
            drawRectLine(g, pts[1], pts[2], size);
            drawRectLine(g, pts[2], pts[3], size);
            drawRectLine(g, pts[0], pts[3], size);
        }

        private void printPoints() {
            System.out.format("A: %d, TL: %s, TR: %s, BR: %s, BL%s%n",
                    (int) (rect.angle + (rect.angle < 0 ? -1e-6 : 1e-6)), // Stupid doubles, stupid rounding error
                    pointToString(pts[0]),
                    pointToString(pts[3]),
                    pointToString(pts[2]),
                    pointToString(pts[1]));
        }

        private String pointToString(Point p) {
            return String.format("{%.2f,%.2f}",p.x, p.y);
        }

        private void drawRectLine(Graphics g, Point left, Point right, Dimension size) {
            g.drawLine(scale(left.x, size.width), scale(left.y, size.height),
                    scale(right.x, size.width), scale(right.y, size.height));
        }


        private int scale(double value, int coord) {
            return (int) (value * coord) / 4 + coord / 2;
        }


        @Override
        public void actionPerformed(ActionEvent e) {
            rect.angle += 1;
            if(rect.angle > 44) rect.angle = -44;
            repaint();
        }
    }
}
durron597
  • 31,968
  • 17
  • 99
  • 158
  • Really? Oh well... That sounds very good indeed. All the manual calculations not necessary would be awesome :-) – tim Mar 27 '14 at 20:29
  • 1
    @tim Yeah I mean, I saw everyone going nuts with math and I thought "there has got to be a better way than that, they wouldn't randomize the order of the points" So I sketched it out, then wrote the above program ;) – durron597 Mar 27 '14 at 20:32
  • Maybe it's because I did not clearly state that OpenCV might have it's own order internally already... Ill look into it, thanks for looking it up. EDIT: Verified! WORKING. How nice. thanks for this nice piece of code you've put up there. Would have taken ages for me to do this :D – tim Mar 27 '14 at 20:40
  • The problem I'm right now still seeing is: Im using `findContours` which returns me the contour and then I'm using `Imgproc.minAreaRect` to calculate the rotated rect for the found rectangular contour... I just ran my program and even though it's a rectangular like shown above, the rotated rect still gives me angles of somethnig like -80° and I'm wondering why and how opencv sees the angle... – tim Mar 27 '14 at 21:01
  • I've given this image into findContours: http://imgur.com/g77dXzX then used minAreaRect to calculate my rotated rect and it gives me the angle of -73°. Don't understand why Opencv does this... so then I might not be able to use your approach, do I? – tim Mar 27 '14 at 21:06
  • See here: Even the bounding-box rectangle is `width < height`, see: http://i.imgur.com/Nk19IaP.png As you can see from the drawed lines, P[0] is bottom right and then clockwise... EDIT: This one gives me even other results: http://i.imgur.com/jHiJQA1.png – tim Mar 27 '14 at 21:16
  • No, because `RotatedRect` doesnt have those fields, see http://docs.opencv.org/java/org/opencv/core/RotatedRect.html – tim Mar 27 '14 at 21:21
  • 1
    @tim yes it does, it's stored in the `size` field. Please print `rect2.size.width` and `rect2.size.height` – durron597 Mar 27 '14 at 21:22
  • Oh well... how have I missed that... Yes you are right then about width and height... http://i.imgur.com/TPA2pAr.png But I dont know how to cope with it then now!? As posted in the original question, I thought that in this case the height = long side and the width = short side... :( Do you know what to do about it? Or maybe I should really stick to the general mathematical approach as I never know what OpenCV does internally when converting a findContour-result to a RotatedRect :( – tim Mar 27 '14 at 21:25
  • Okay thanks, I'll implement it. Sounds good. But still I do not undertand how OpenCV manages it internally!? I mean... when looking at my test image I somehow imagine it's only rotated some degrees to the right (clockwise), but somehow OpenCV's results differ from that – tim Mar 27 '14 at 21:37
3

EDIT: If you are free to assume that the rectangle has not been rotated by much, you can directly go ahead and find the top-left point by calculating the distance from the origin using the formula length = ((y1-y2)^2 + (x1-x2)^2)^(0.5) above with origin being (0,0). The point with the smallest distance will be the top left. And then you can proceed using the steps I have given below.

If you cannot assume that, there is another way of proceeding more elegantly once you have identified the top left point of the rectangle (and hence the first three steps remain the same). Once you have identified the top left:

  1. Find out the distance from the top left point to the other three points using the Pythagorean formula, length = ((y1-y2)^2 + (x1-x2)^2)^(0.5)
  2. You now have three lengths corresponding to the length of each vertex from the top-left point.
  3. The position of the vertices can be easily found as (going in clockwise order):

    shortest distance = top right point 
    longest distance = bottom right point 
    middle distance = bottom left point
    

You do not need to use if conditions.

NOTE: This holds as long as the condition that the height is always greater than the width is maintained.

ucsunil
  • 7,378
  • 1
  • 27
  • 32
2

Search for the 2 points with the highest y-values, one of these is always the TL in your definition (width < height and small angles(not higher then 45°!).

Sort your array in descending order for your y-values and get the element with the 2nd heighest y-value.

If this point has the lowest x-value it defines your right picture (1). Else the point with the highest value is your TL and defines your left picture (2).

Now you get the clockwise order where the TL is your first element.

In case (1): Change the position of the last 2 elemnts of your sorted array In case (2): Change the position of the first 2 elemnts.

This is true because of your definition but I can't explain it in a proper mathematically way.

Steve Benett
  • 12,843
  • 7
  • 59
  • 79
1

I just tried out one approach. I'm not sure whether it's "simpler" or in any other way "better" than yours, but I'll post it here as a suggestion:

You can compute the center of the rectangle. Then you can move the rectangle, so that its center is at the origin. Then you can compute the angle that each point has to the x-axis, using the Math.atan2 method. The neat thing here is: It returns the angle in the range -PI ... +PI, which exactly matches your desired ordering: The upper left point will have the "most negative" angle, and the lower left point will have the "most positive".

This description is just for illustration. Some of these steps (particularly "moving" the rectangle) do not have to be done explicitly.

However: In this example, you can set the corner points via mouse clicks. Each point will be labeled with its index and the angle, as described above. When the fourth point is set, then the points will be reordered accordingly, and the resulting indices/angles will be shown.

As far as I can see, the results seem to be what you intended to compute.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class RectanglePointReorderingTest
{
    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 RectanglePointReorderingPanel());
        f.setSize(800, 800);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    static Point2D computeCenter(List<Point2D> points)
    {
        double x = 0;
        double y = 0;
        for (Point2D p : points)
        {
            x += p.getX();
            y += p.getY();
        }
        x /= points.size();
        y /= points.size();
        return new Point2D.Double(x, y);
    }

    static double computeAngle(Point2D center, Point2D point)
    {
        double dx = point.getX() - center.getX();
        double dy = point.getY() - center.getY();
        double angleRad = Math.atan2(dy, dx);
        return angleRad;
    }

    static Comparator<Point2D> createComparator(Point2D center)
    {
        final Point2D finalCenter = 
            new Point2D.Double(center.getX(), center.getY());
        return new Comparator<Point2D>()
        {
            @Override
            public int compare(Point2D p0, Point2D p1)
            {
                double angle0 = computeAngle(finalCenter, p0);
                double angle1 = computeAngle(finalCenter, p1);
                return Double.compare(angle0, angle1);
            }
        };
    }    

    static void sortPoints(List<Point2D> points)
    {
        Collections.sort(points, createComparator(computeCenter(points)));
    }

}


class RectanglePointReorderingPanel extends JPanel implements MouseListener
{
    private List<Point2D> points = new ArrayList<Point2D>();

    public RectanglePointReorderingPanel()
    {
        addMouseListener(this);
    }

    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;

        g.setColor(Color.BLACK);
        if (points.size() < 4)
        {
            g.drawString("Click to create points", 20, 20);
        }
        else
        {
            g.drawString("Sorted points. Click again to clear.", 20, 20);
        }
        for (int i=0; i<points.size(); i++)
        {
            Point2D point = points.get(i);
            double x = point.getX();
            double y = point.getY();
            g.setColor(Color.RED);
            g.fill(new Ellipse2D.Double(x-5,y-5,10,10));

            g.setColor(Color.BLACK);

            double angleRad = 
                RectanglePointReorderingTest.computeAngle(
                    RectanglePointReorderingTest.computeCenter(points), point);
            String angleString = String.valueOf((int)Math.toDegrees(angleRad));
            g.drawString(String.valueOf(i)+" ("+angleString+")", (int)x+5, (int)y+5);


        }
    }

    @Override
    public void mouseClicked(MouseEvent e)
    {
        if (points.size() == 4)
        {
            points.clear();
            repaint();
        }
        else
        {
            points.add(e.getPoint());
            if (points.size() == 4)
            {
                RectanglePointReorderingTest.sortPoints(points);
            }
            repaint();
        }
    }


    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) { }

    @Override
    public void mousePressed(MouseEvent e) { }

    @Override
    public void mouseReleased(MouseEvent e) { }


}
Marco13
  • 53,703
  • 9
  • 80
  • 159
1

Because you are specifically asking for the order of the points from the RotatedRect data structure, that order is predictable and never changes (unless you update the library and the developers somehow needed to change that code -> very unlikely).

The order I got is quite odd and is the following:

point[0] - bottom left
point[1] - top left
point[2] - top right
point[3] - bottom right

You can see in OpenCV source code how that list of points from the RotatedRect is created, based on its center and angle:

public void points(Point pt[])
    {
        double _angle = angle * Math.PI / 180.0;
        double b = (double) Math.cos(_angle) * 0.5f;
        double a = (double) Math.sin(_angle) * 0.5f;

        pt[0] = new Point(
                center.x - a * size.height - b * size.width,
                center.y + b * size.height - a * size.width);

        pt[1] = new Point(
                center.x + a * size.height - b * size.width,
                center.y - b * size.height - a * size.width);

        pt[2] = new Point(
                2 * center.x - pt[0].x,
                2 * center.y - pt[0].y);

        pt[3] = new Point(
                2 * center.x - pt[1].x,
                2 * center.y - pt[1].y);
    }

EDIT (after comments):

If you realize that the corner order depends from the angle. It is easier to get the order based on the Pythagorean formula like it was said before by Sunil.

Depending which sign the variables a and b will have, the order will be different. Those signs depend from the cos() and sin() results which in turn depend from the angle. So you have 4 combinations of signs (+a, +b), (-a ,-b), (+a, -b), (-a, +b). These will give, if my theory stands, the 4 different point orders.

You can get the top-left corner by getting all points distance to (0,0). You will either get one minimum or 2 (equal) minimum distances. If you get 2 minimums, you chose one as the top-left rectangle corner: the one with smaller x coordinate is what makes more sense in my opinion and according to your drawing. The same process can be used for the other rectangle corners.

// Distance (x1, y1) to (x2, y2) = abs( sqrt( (x2-x1)^2 + (y2-y1)^2 ) )
// Note:This is a quite literal translation of the formula, there are more efficient ways.
public static final double pointsDist(Point pt1, Point pt2){        
   return  Math.abs( Math.sqrt( Math.pow((double) (pt2.x - pt1.x), 2) + Math.pow((double) (pt2.y - pt1.y), 2) ) );          
}
Rui Marques
  • 8,567
  • 3
  • 60
  • 91
  • I am double checking this order because it is strange. Let me know if this is the order you have. – Rui Marques Mar 27 '14 at 22:49
  • Yeah please recheck it because when you compare it to answer #1, it seems your answer isn't correct. – tim Mar 27 '14 at 22:51
  • 1
    I assumed the order would always be the same but maybe it depends from the angle similar to how @durron597 presented. The source code is that one, points come from playing with center and angle. – Rui Marques Mar 27 '14 at 22:57
  • Yeah probably it has got something to do with `findContour` as well, or the conversation from the contour to a RotatedRect, somewhere inbetween :-) Thanks though. Always nice to get some further insight as I'm keen on learning the new stuff! – tim Mar 27 '14 at 22:59
  • I do not think it depends from findContour. findContour will create the RotetedRect by providing its center and angle only. The list of points is then derived from those. – Rui Marques Mar 27 '14 at 23:01
  • Sorry, you might be right, makes sense to me. And where to look this up in the source how the treatment for different angles etc is? Because when I see it correctly, your excerpt doesnt help me with it, does it? – tim Mar 27 '14 at 23:08
  • 1
    The "treatment" is right there in the code, depending which sign the variables 'a' and 'b' will have, the order will be different, I think. Those signs depend from the cos() and sin() results which depend from the angle. – Rui Marques Mar 27 '14 at 23:12
  • 1
    So you have 4 combinations of signs (+a, +b), (-a ,-b), (+a, -b), (-a, +b). These will probably give the 4 different point orders. Similar to how @durron597 said, but maybe he got the intervals a bit wrong. – Rui Marques Mar 27 '14 at 23:16
  • @RuiMarques I got my intervals from empirical testing, not attempting to figure it out from their source code. Getting the angle from the object is much faster than doing math. – durron597 Mar 28 '14 at 12:06