目录
- 前言
- 原生实现(错误方法)
- 精确实现(数学解)
- 最小外接矩形
- 参考
前言
遇到一个需要计算一般椭圆(斜椭圆)的外接矩形坐标的问题,在此记录一下
已知椭圆的中心点坐标centerX centerY,椭圆的长轴,短轴majorRadius minorRadius,和旋转角度 angle。
按理说java有原生的计算外接矩形的函数,先看看 java.awt.geom
怎么实现的。
原生实现(错误方法)
java.awt.geom提供了 Ellipse2D对象,我们通过Ellipse2D对象的 setFrameFromCenter 方法可以直接创建相应尺寸的椭圆:
double majorRadius = 108;
double minorRadius = 207;
double centerX = 836;
double centerY = 473;
double angle = 45.5;
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(centerX, centerY, centerX + majorRadius, centerY + minorRadius);
我们再创建AffineTransform对象,将ellipse进行旋转变换,就能得到最终的椭圆,再通过Shape对象的getBounds2D()方法,可以直接得到外接矩形。
AffineTransform transform = new AffineTransform();
transform.rotate(Math.toRadians(45.5), centerX, centerY);
Shape transformedEllipse = transform.createTransformedShape(ellipse);
Rectangle2D bounds2D = transformedEllipse.getBounds2D();
为了更直观展示,我们通过 Graphics2D 把图像画出来。
完整代码如下:
import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
public class BoundingBoxUtil2 {
static class DrawFrame extends JFrame {
public DrawFrame() {
add(new DrawComponent());
pack();
}
}
static class DrawComponent extends JComponent {
private static final int DEFAULT_WIDTH = 2000;
private static final int DEFAULT_HEIGHT = 1000;
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
double majorRadius = 108;
double minorRadius = 207;
double centerX = 836;
double centerY = 473;
double angle = 45.5;
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(centerX, centerY, centerX + 108, centerY + 207);
g2.draw(ellipse);
AffineTransform transform = new AffineTransform();
transform.rotate(Math.toRadians(45.5), centerX, centerY);
Shape transformedEllipse = transform.createTransformedShape(ellipse);
g2.draw(transformedEllipse);
g2.draw(transformedEllipse.getBounds2D());
}
public Dimension getPreferredSize() {
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new DrawFrame();
frame.setTitle("DrawTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
运行结果如下:
可以看到这种方法是不行的。
如果真的这么简单就好了,可以看到getBounds2D()得到的外接矩形并不是精确的,。我们看看源码描述:
public Rectangle2D getBounds2D();
大意为:
返回Shape的高精度且比getBounds方法更精确的边界框。请注意,不能保证返回的Rectangle2D是包围该形状的最小边界框,只能保证该形状完全位于指示的Rectangle 2D内。此方法返回的边界框通常比getBounds方法返回的更紧,并且从不因溢出问题而失败,因为返回值可以是使用双精度值来存储尺寸的Rectangle2D的实例。
事实上,如果直接生成不旋转的椭圆,通过getBounds2D()方法是可以找到准确的外接矩形的。
但是java.awt.geom没有考虑到一般椭圆(斜椭圆)的情况。
精确实现(数学解)
其实椭圆的外接矩形有数学解,我们通过还原椭圆一般式的参数,从而可以直接求外接矩形坐标。
中心点位于原点时,椭圆一般方程为:Ax^2 + Bxy + Cy^2 + F=0
因此可以通过已知短轴,长轴,旋转角,确定一般方程的参数:
public static double[] getEllipseParam(double majorRadius, double minorRadius, double angle) {
double a = majorRadius;
double b = minorRadius;
double sinTheta = Math.sin(-angle);
double cosTheta = Math.cos(-angle);
double A = Math.pow(a, 2) * Math.pow(sinTheta, 2) + Math.pow(b, 2) * Math.pow(cosTheta, 2);
double B = 2 * (Math.pow(a, 2) - Math.pow(b, 2)) * sinTheta * cosTheta;
double C = Math.pow(a, 2) * Math.pow(cosTheta, 2) + Math.pow(b, 2) * Math.pow(sinTheta, 2);
double F = -1 * Math.pow(a, 2) * Math.pow(b, 2);
return new double[]{A, B, C, F};
}
因此可以计算中心点位于原点时,外接矩形的坐标:
public static Point2D[] calculateRectangle(double A, double B, double C, double F) {
double y = Math.sqrt(4 * A * F / (Math.pow(B, 2) - 4 * A * C));
double y1 = -1 * Math.abs(y);
double y2 = Math.abs(y);
double x = Math.sqrt(4 * C * F / (Math.pow(B, 2) - 4 * C * A));
double x1 = -1 * Math.abs(x);
double x2 = Math.abs(x);
Point2D p1 = new Point2D.Double(x1, y1);
Point2D p2 = new Point2D.Double(x2, y2);
return new Point2D[]{p1, p2};
}
中心点位于原点的椭圆外接矩形能算了,原来的椭圆的外接矩形其实就是按照中心点平移罢了:
public static Point2D[] getCircumscribedRectangle(double majorRadius, double minorRadius, double angle, double centerX, double centerY) {
double[] param = getEllipseParam(majorRadius, minorRadius, angle);
Point2D[] points = calculateRectangle(param[0], param[1], param[2], param[3]);
Point2D p1 = new Point2D.Double(centerX + points[0].getX(), centerY + points[0].getY());
Point2D p2 = new Point2D.Double(centerX + points[1].getX(), centerY + points[1].getY());
return new Point2D[] { p1, p2 };
}
这样就能求得一般椭圆的外接矩形坐标了。
为了方便展示做一下绘图,完整代码如下:
import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
public class BoundingBoxUtil2 {
static class DrawFrame extends JFrame {
public DrawFrame() {
add(new DrawComponent());
pack();
}
}
static class DrawComponent extends JComponent {
private static final int DEFAULT_WIDTH = 2000;
private static final int DEFAULT_HEIGHT = 1000;
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
double majorRadius = 108;
double minorRadius = 207;
double centerX = 836;
double centerY = 473;
double angle = 45.5;
Point2D[] rectangle = getCircumscribedRectangle(majorRadius, minorRadius, Math.toRadians(angle), centerX, centerY);
double x1 = rectangle[0].getX();
double y1 = rectangle[0].getY();
double x2 = rectangle[1].getX();
double y2 = rectangle[1].getY();
double width = x2 - x1;
double height = y2 - y1;
Rectangle2D circumscribedRectangle = new Rectangle2D.Double();
circumscribedRectangle.setRect(x1, y1, width, height);
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(centerX, centerY, centerX + majorRadius, centerY + minorRadius);
g2.draw(ellipse);
AffineTransform transform = new AffineTransform();
transform.rotate(Math.toRadians(angle), centerX, centerY);
Shape transformedEllipse = transform.createTransformedShape(ellipse);
g2.draw(transformedEllipse);
g2.draw(circumscribedRectangle);
}
public Dimension getPreferredSize() {
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
public static double[] getEllipseParam(double majorRadius, double minorRadius, double angle) {
double a = majorRadius;
double b = minorRadius;
double sinTheta = Math.sin(-angle);
double cosTheta = Math.cos(-angle);
double A = Math.pow(a, 2) * Math.pow(sinTheta, 2) + Math.pow(b, 2) * Math.pow(cosTheta, 2);
double B = 2 * (Math.pow(a, 2) - Math.pow(b, 2)) * sinTheta * cosTheta;
double C = Math.pow(a, 2) * Math.pow(cosTheta, 2) + Math.pow(b, 2) * Math.pow(sinTheta, 2);
double F = -1 * Math.pow(a, 2) * Math.pow(b, 2);
return new double[]{A, B, C, F};
}
public static Point2D[] calculateRectangle(double A, double B, double C, double F) {
double y = Math.sqrt(4 * A * F / (Math.pow(B, 2) - 4 * A * C));
double y1 = -1 * Math.abs(y);
double y2 = Math.abs(y);
double x = Math.sqrt(4 * C * F / (Math.pow(B, 2) - 4 * C * A));
double x1 = -1 * Math.abs(x);
double x2 = Math.abs(x);
Point2D p1 = new Point2D.Double(x1, y1);
Point2D p2 = new Point2D.Double(x2, y2);
return new Point2D[]{p1, p2};
}
public static Point2D[] getCircumscribedRectangle(double majorRadius, double minorRadius, double angle, double centerX, double centerY) {
double[] param = getEllipseParam(majorRadius, minorRadius, angle);
Point2D[] points = calculateRectangle(param[0], param[1], param[2], param[3]);
Point2D p1 = new Point2D.Double(centerX + points[0].getX(), centerY + points[0].getY());
Point2D p2 = new Point2D.Double(centerX + points[1].getX(), centerY + points[1].getY());
return new Point2D[] { p1, p2 };
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new DrawFrame();
frame.setTitle("DrawTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
运行一下:
可以看到,数学解是成功的。
最小外接矩形
最小外接矩形其实很简单
假设椭圆没有旋转时,外接矩形四个点的坐标其实就是椭圆中心点,按照分别像左上,右上,右下,左下四个方向平移的结果,其中x平移短轴的距离,y平移长轴的距离。
如果椭圆旋转angle角度了,那么相应的四个点也跟着旋转四个角度就行
核心代码如下:
public static Point2D.Double[] getMinimumBoundingRectangle(double majorRadius, double minorRadius, double angle, double centerX, double centerY) {
double width = majorRadius * 2;
double height = minorRadius * 2;
double x1 = centerX - (width / 2);
double y1 = centerY - (height / 2);
double x2 = centerX + (width / 2);
double y2 = centerY - (height / 2);
double x3 = centerX + (width / 2);
double y3 = centerY + (height / 2);
double x4 = centerX - (width / 2);
double y4 = centerY + (height / 2);
double rotatedX1 = centerX + (x1 - centerX) * Math.cos(angle) - (y1 - centerY) * Math.sin(angle);
double rotatedY1 = centerY + (x1 - centerX) * Math.sin(angle) + (y1 - centerY) * Math.cos(angle);
double rotatedX2 = centerX + (x2 - centerX) * Math.cos(angle) - (y2 - centerY) * Math.sin(angle);
double rotatedY2 = centerY + (x2 - centerX) * Math.sin(angle) + (y2 - centerY) * Math.cos(angle);
double rotatedX3 = centerX + (x3 - centerX) * Math.cos(angle) - (y3 - centerY) * Math.sin(angle);
double rotatedY3 = centerY + (x3 - centerX) * Math.sin(angle) + (y3 - centerY) * Math.cos(angle);
double rotatedX4 = centerX + (x4 - centerX) * Math.cos(angle) - (y4 - centerY) * Math.sin(angle);
double rotatedY4 = centerY + (x4 - centerX) * Math.sin(angle) + (y4 - centerY) * Math.cos(angle);
Point2D.Double point1 = new Point2D.Double(rotatedX1, rotatedY1);
Point2D.Double point2 = new Point2D.Double(rotatedX2, rotatedY2);
Point2D.Double point3 = new Point2D.Double(rotatedX3, rotatedY3);
Point2D.Double point4 = new Point2D.Double(rotatedX4, rotatedY4);
return new Point2D.Double[] { point1, point2, point3, point4 };
}
我们完整代码如下:
package org.example.project_fragments.cv;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.*;
import java.util.List;
public class BoundingBoxUtil2 {
static class DrawFrame extends JFrame {
public DrawFrame() {
add(new DrawComponent());
pack();
}
}
static class DrawComponent extends JComponent {
private static final int DEFAULT_WIDTH = 2000;
private static final int DEFAULT_HEIGHT = 1000;
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
Rectangle2D image = new Rectangle2D.Double();
image.setRect(1, 1, 1440, 1000);
g2.draw(image);
int majorRadius = 108;
int minorRadius = 207;
int centerX = 836;
int centerY = 473;
double angle = 45.5;
Point2D.Double[] rectangle = getCircumscribedRectangle(majorRadius, minorRadius, Math.toRadians(angle), centerX, centerY);
double x1 = rectangle[0].getX();
double y1 = rectangle[0].getY();
double x2 = rectangle[1].getX();
double y2 = rectangle[1].getY();
double width = x2 - x1;
double height = y2 - y1;
Rectangle2D circumscribedRectangle = new Rectangle2D.Double();
circumscribedRectangle.setRect(x1, y1, width, height);
g2.draw(circumscribedRectangle);
Point2D.Double[] minimumBoundingRectangle = getMinimumBoundingRectangle(majorRadius, minorRadius, Math.toRadians(angle), centerX, centerY);
Polygon polygon = new Polygon();
for (Point2D.Double point : minimumBoundingRectangle) {
polygon.addPoint((int) point.getX(), (int) point.getY());
}
g2.drawPolygon(polygon);
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(centerX, centerY, centerX + majorRadius, centerY + minorRadius);
AffineTransform transform = new AffineTransform();
transform.rotate(Math.toRadians(angle), centerX, centerY);
Shape transformedEllipse = transform.createTransformedShape(ellipse);
g2.draw(transformedEllipse);
}
public Dimension getPreferredSize() {
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
public static double[] getEllipseParam(double majorRadius, double minorRadius, double angle) {
double a = majorRadius;
double b = minorRadius;
double sinTheta = Math.sin(-angle);
double cosTheta = Math.cos(-angle);
double A = Math.pow(a, 2) * Math.pow(sinTheta, 2) + Math.pow(b, 2) * Math.pow(cosTheta, 2);
double B = 2 * (Math.pow(a, 2) - Math.pow(b, 2)) * sinTheta * cosTheta;
double C = Math.pow(a, 2) * Math.pow(cosTheta, 2) + Math.pow(b, 2) * Math.pow(sinTheta, 2);
double F = -1 * Math.pow(a, 2) * Math.pow(b, 2);
return new double[]{A, B, C, F};
}
public static Point2D.Double[] calculateRectangle(double A, double B, double C, double F) {
double y = Math.sqrt(4 * A * F / (Math.pow(B, 2) - 4 * A * C));
double y1 = -1 * Math.abs(y);
double y2 = Math.abs(y);
double x = Math.sqrt(4 * C * F / (Math.pow(B, 2) - 4 * C * A));
double x1 = -1 * Math.abs(x);
double x2 = Math.abs(x);
Point2D.Double p1 = new Point2D.Double(x1, y1);
Point2D.Double p2 = new Point2D.Double(x2, y2);
return new Point2D.Double[]{p1, p2};
}
public static Point2D.Double[] getCircumscribedRectangle(double majorRadius, double minorRadius, double angle, double centerX, double centerY) {
double[] param = getEllipseParam(majorRadius, minorRadius, angle);
Point2D.Double[] points = calculateRectangle(param[0], param[1], param[2], param[3]);
Point2D.Double p1 = new Point2D.Double(centerX + points[0].getX(), centerY + points[0].getY());
Point2D.Double p2 = new Point2D.Double(centerX + points[1].getX(), centerY + points[1].getY());
return new Point2D.Double[] { p1, p2 };
}
public static Point2D.Double[] getMinimumBoundingRectangle(double majorRadius, double minorRadius, double angle, double centerX, double centerY) {
double width = majorRadius * 2;
double height = minorRadius * 2;
double x1 = centerX - (width / 2);
double y1 = centerY - (height / 2);
double x2 = centerX + (width / 2);
double y2 = centerY - (height / 2);
double x3 = centerX + (width / 2);
double y3 = centerY + (height / 2);
double x4 = centerX - (width / 2);
double y4 = centerY + (height / 2);
double rotatedX1 = centerX + (x1 - centerX) * Math.cos(angle) - (y1 - centerY) * Math.sin(angle);
double rotatedY1 = centerY + (x1 - centerX) * Math.sin(angle) + (y1 - centerY) * Math.cos(angle);
double rotatedX2 = centerX + (x2 - centerX) * Math.cos(angle) - (y2 - centerY) * Math.sin(angle);
double rotatedY2 = centerY + (x2 - centerX) * Math.sin(angle) + (y2 - centerY) * Math.cos(angle);
double rotatedX3 = centerX + (x3 - centerX) * Math.cos(angle) - (y3 - centerY) * Math.sin(angle);
double rotatedY3 = centerY + (x3 - centerX) * Math.sin(angle) + (y3 - centerY) * Math.cos(angle);
double rotatedX4 = centerX + (x4 - centerX) * Math.cos(angle) - (y4 - centerY) * Math.sin(angle);
double rotatedY4 = centerY + (x4 - centerX) * Math.sin(angle) + (y4 - centerY) * Math.cos(angle);
Point2D.Double point1 = new Point2D.Double(rotatedX1, rotatedY1);
Point2D.Double point2 = new Point2D.Double(rotatedX2, rotatedY2);
Point2D.Double point3 = new Point2D.Double(rotatedX3, rotatedY3);
Point2D.Double point4 = new Point2D.Double(rotatedX4, rotatedY4);
return new Point2D.Double[] { point1, point2, point3, point4 };
}
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new DrawFrame();
frame.setTitle("DrawTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
运行:
可以看到我们得到了最小外接矩形。
参考
https://zhuanlan.zhihu.com/p/82184417
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)