Измените четыре точки прямоугольника в правильном порядке

Соотношение сторон = высота/ширина всегда> 1 (в большинстве случаев даже> 2), поэтому должно быть ясно/точно, как бы я хотел повернуть.


У меня есть объект RotatedRect в OpenCV/Java.
Я могу получить массив из 4 объектов типа Point, а Point определяет значения x/y.

Теперь я хотел бы отсортировать эти 4 точки так, чтобы верхняя левая точка была первым элементом массива, а затем по часовой стрелке, чтобы верхняя нижняя точка была четвертым элементом.

Я предполагаю, что прямоугольники не сильно вращаются (только несколько небольших углов), например

В примере я указал, какую точку я бы обозначил как верхний левый (TL).

Как это сделать?

Вам не нужно указывать мне что-то конкретно об OpenCV и т.д., Просто предположим, что у вас будет два массива

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

Тогда как точка n -th имеет координаты (x[n-1], y[n-1]). Затем я могу сделать это специально для OpenCV.

Ответ 1

Ответ

Существует очень простое решение, если вы знаете, что:

  • -45 < roundedRect.angle < 45
  • roundedRect.size.height > roundedRect.size.width

Если это так, то точки в порядке по часовой стрелке будут ВСЕГДА в следующем порядке:

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

В стороне, если это не навредит вашей программе слишком много, точки доставляются в порядке против часовой стрелки, начиная с верхнего левого... тогда вам не нужно было бы переупорядочивать/сортировать.

Другие случаи:

  • height > width && 135 < roundedRect.angle < 225
    • Порядок по часовой стрелке, начиная с левого верхнего угла, 2,3,0,1
    • Порядок против часовой стрелки слева вверху 2,1,0,3.
  • width > height && -135 < roundedRect.angle < -45
    • Порядок по часовой стрелке, начиная с верхнего левого угла, 3,2,1,0
    • Порядок против часовой стрелки слева вверху 3,0,1,2
  • width > height && 45 < roundedRect.angle < 135
    • Порядок по часовой стрелке, начиная с левого верхнего угла, 1,0,3,2
    • Порядок против часовой стрелки слева вверху 1,2,3,0

Остальные случаи будут означать, что прямоугольник больше слева направо, чем сверху вниз, что не может произойти в вашем сценарии. Кроме того, если угол находится за пределами этих диапазонов, вы можете добавить или вычесть 360 последовательно, чтобы получить угол в одном из этих диапазонов.


Объяснение

(Tl; дг)

Мы знаем это, как OpenCV вычисляет значения этих точек. Вы можете понять это с небольшим экспериментом. Вот небольшая программа, которую я написал, которая демонстрирует это:

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();
        }
    }
}

Ответ 2

РЕДАКТИРОВАТЬ:. Если вы можете предположить, что прямоугольник не был сильно повернут, вы можете прямо идти вперед и находить верхнюю левую точку, вычисляя расстояние от начала координат по формуле length = ((y1-y2) ^ 2 +   (x1-x2) ^ 2) ^ (0,5), причем начало координат (0,0). Точка с наименьшим расстоянием будет левым верхним. И затем вы можете продолжить использование шагов, которые я привел ниже.

Если вы не можете этого догадаться, есть еще один способ более элегантно, как только вы определили верхнюю левую точку прямоугольника (и, следовательно, первые три шага остались прежними). Как только вы определили верхний левый:

  • Узнайте расстояние от верхней левой точки до остальных трех точек с использованием формулы Пифагора, длина = ((y1-y2) ^ 2 + (X1-x2) ^ 2) ^ (0,5)
  • Теперь у вас есть три длины, соответствующие длине каждого вершины из верхней левой точки.
  • Положение вершин можно легко найти как (в по часовой стрелке):

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

Вам не нужно использовать условия.

ПРИМЕЧАНИЕ.. Это выполняется до тех пор, пока поддерживается сохранение высоты всегда больше, чем ширина.

Ответ 3

Найдите 2 точки с наивысшими значениями y, один из которых всегда является TL в вашем определении (ширина < высота и малые углы (не выше 45 °!).

Сортируйте свой массив в порядке убывания для ваших значений y и получите элемент со вторым самым большим значением y.

Если эта точка имеет самое низкое значение x, она определяет ваше правое изображение (1). Еще одна точка с наивысшим значением - ваш TL и определяет ваше левое изображение (2).

Теперь вы получаете по часовой стрелке, где TL - ваш первый элемент.

В случае (1): измените положение последних двух элементов вашего отсортированного массива В случае (2): измените положение первых двух элементов.

Это верно из-за вашего определения, но я не могу объяснить это математически.

Ответ 4

Я просто попробовал один подход. Я не уверен, что это "проще" или каким-либо другим образом "лучше", чем ваше, но я отправлю его здесь в качестве предложения:

Вы можете вычислить центр прямоугольника. Затем вы можете перемещать прямоугольник так, чтобы его центр находился в начале координат. Затем вы можете вычислить угол, который каждая точка имеет на оси x, используя метод Math.atan2. Здесь аккуратно: он возвращает угол в диапазоне -PI... + PI, который точно соответствует вашему желаемому порядку: верхняя левая точка будет иметь "самый отрицательный" угол, а нижняя левая точка будет иметь "наиболее позитивных".

Это описание только для иллюстрации. Некоторые из этих шагов (особенно "перемещение" прямоугольника) не должны выполняться явно.

Однако: В этом примере вы можете установить угловые точки с помощью щелчков мыши. Каждая точка будет помечена индексом и углом, как описано выше. Когда будет установлена ​​четвертая точка, точки будут соответственно переупорядочены, и будут показаны полученные индексы/углы.

Насколько я вижу, результаты, похоже, являются тем, что вы намеревались вычислить.

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) { }


}

Ответ 5

Поскольку вы специально запрашиваете порядок точек из структуры данных RotatedRect, этот порядок предсказуем и никогда не изменяется (если вы не обновите библиотеку, а разработчики как-то не захотели изменить этот код → очень маловероятно).

Приказ, который я получил, довольно странный и выглядит следующим образом:

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

В исходный код OpenCV вы можете видеть, как создается этот список точек из RotatedRect на основе его центра и угла:

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 (после комментариев):

Если вы понимаете, что угловой порядок зависит от угла. Легче получить заказ, основанный на формуле Пифагора, как это было сказано ранее Сунилом.

В зависимости от того, какой знак будут иметь переменные a и b, порядок будет другим. Эти признаки зависят от результатов cos() и sin(), которые, в свою очередь, зависят от угла. Таким образом, у вас есть 4 комбинации знаков (+a, +b), (-a ,-b), (+a, -b), (-a, +b). Это даст, если будет моя теория, 4 разных точечных ордера.

Вы можете получить верхний левый угол, пройдя все расстояния до (0,0). Вы получите либо минимум, либо 2 (равные) минимальные расстояния. Если вы получаете 2 минимума, вы выбрали один из них как верхний левый прямоугольник: тот, у кого меньшая координата x, имеет смысл, на мой взгляд, и в соответствии с вашим рисунком. Этот же процесс можно использовать для других углов прямоугольника.

// 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) ) );          
}