目录
- 前言
- 原生实现(错误方法)
- 精确实现(数学解)
- 参考
前言
遇到一个需要计算一般椭圆(斜椭圆)的外接矩形坐标的问题,在此记录一下
已知椭圆的中心点坐标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;
// 创建椭圆 ellipse
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(centerX, centerY, centerX + majorRadius, centerY + minorRadius);
我们再创建AffineTransform对象,将ellipse进行旋转变换,就能得到最终的椭圆,再通过Shape对象的getBounds2D()方法,可以直接得到外接矩形。
// 旋转椭圆得到 transformedEllipse
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;
// 创建椭圆 ellipse
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(centerX, centerY, centerX + 108, centerY + 207);
g2.draw(ellipse);
// 旋转椭圆得到 transformedEllipse
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()得到的外接矩形并不是精确的,。我们看看源码描述:
/**
* Returns a high precision and more accurate bounding box of
* the {@code Shape} than the {@code getBounds} method.
* Note that there is no guarantee that the returned
* {@link Rectangle2D} is the smallest bounding box that encloses
* the {@code Shape}, only that the {@code Shape} lies
* entirely within the indicated {@code Rectangle2D}. The
* bounding box returned by this method is usually tighter than that
* returned by the {@code getBounds} method and never fails due
* to overflow problems since the return value can be an instance of
* the {@code Rectangle2D} that uses double precision values to
* store the dimensions.
*
* <p>
* Note that the
* <a href="{@docRoot}/java.desktop/java/awt/Shape.html#def_insideness">
* definition of insideness</a> can lead to situations where points
* on the defining outline of the {@code shape} may not be considered
* contained in the returned {@code bounds} object, but only in cases
* where those points are also not considered contained in the original
* {@code shape}.
* </p>
* <p>
* If a {@code point} is inside the {@code shape} according to the
* {@link #contains(Point2D p) contains(point)} method, then it must
* be inside the returned {@code Rectangle2D} bounds object according
* to the {@link #contains(Point2D p) contains(point)} method of the
* {@code bounds}. Specifically:
* </p>
* <p>
* {@code shape.contains(p)} requires {@code bounds.contains(p)}
* </p>
* <p>
* If a {@code point} is not inside the {@code shape}, then it might
* still be contained in the {@code bounds} object:
* </p>
* <p>
* {@code bounds.contains(p)} does not imply {@code shape.contains(p)}
* </p>
* @return an instance of {@code Rectangle2D} that is a
* high-precision bounding box of the {@code Shape}.
* @see #getBounds
* @since 1.2
*/
public Rectangle2D getBounds2D();
大意为:
返回Shape的高精度且比getBounds方法更精确的边界框。请注意,不能保证返回的Rectangle2D是包围该形状的最小边界框,只能保证该形状完全位于指示的Rectangle 2D内。此方法返回的边界框通常比getBounds方法返回的更紧,并且从不因溢出问题而失败,因为返回值可以是使用双精度值来存储尺寸的Rectangle2D的实例。
事实上,如果直接生成不旋转的椭圆,通过getBounds2D()方法是可以找到准确的外接矩形的。
但是java.awt.geom没有考虑到一般椭圆(斜椭圆)的情况。
精确实现(数学解)
其实椭圆的外接矩形有数学解,我们通过还原椭圆一般式的参数,从而可以直接求外接矩形坐标。
中心点位于原点时,椭圆一般方程为:Ax^2 + Bxy + Cy^2 + F=0
因此可以通过已知短轴,长轴,旋转角,确定一般方程的参数:
/**
* 计算一般椭圆(斜椭圆)的参数A,B,C,F
* 中心点位于原点的椭圆一般方程为:Ax^2+Bxy+Cy^2+F=0
* @param majorRadius 长轴
* @param minorRadius 短轴
* @param angle 旋转角
* @return
*/
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};
}
因此可以计算中心点位于原点时,外接矩形的坐标:
/**
* 计算中心点位于原点的一般椭圆的外接矩形坐标
* @param A
* @param B
* @param C
* @param F
* @return
*/
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};
}
中心点位于原点的椭圆外接矩形能算了,原来的椭圆的外接矩形其实就是按照中心点平移罢了:
/**
* 计算一般椭圆的外接矩形实际坐标
* 根据一般椭圆的实际中心点坐标,短轴,长轴,旋转角参数,计算一般椭圆的外接矩形实际坐标
* @param majorRadius 长轴
* @param minorRadius 短轴
* @param angle 旋转角
* @param centerX 中心点横坐标
* @param centerY 中心点纵坐标
* @return
*/
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);
// 创建椭圆 ellipse
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrameFromCenter(centerX, centerY, centerX + majorRadius, centerY + minorRadius);
g2.draw(ellipse);
// 旋转椭圆得到 transformedEllipse
AffineTransform transform = new AffineTransform();
transform.rotate(Math.toRadians(angle), centerX, centerY);
Shape transformedEllipse = transform.createTransformedShape(ellipse);
// 绘制旋转后的椭圆
g2.draw(transformedEllipse);
// 绘制旋转后的椭圆的外接矩形
// g2.draw(transformedEllipse.getBounds2D());
// 绘制真正的外接矩形
g2.draw(circumscribedRectangle);
}
public Dimension getPreferredSize() {
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
/**
* 计算一般椭圆(斜椭圆)的参数A,B,C,F
* 中心点位于原点的椭圆一般方程为:Ax^2+Bxy+Cy^2+F=0
* @param majorRadius 长轴
* @param minorRadius 短轴
* @param angle 旋转角
* @return
*/
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};
}
/**
* 计算中心点位于原点的一般椭圆的外接矩形坐标
* @param A
* @param B
* @param C
* @param F
* @return
*/
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};
}
/**
* 计算一般椭圆的外接矩形实际坐标
* 根据一般椭圆的实际中心点坐标,短轴,长轴,旋转角参数,计算一般椭圆的外接矩形实际坐标
* @param majorRadius 长轴
* @param minorRadius 短轴
* @param angle 旋转角
* @param centerX 中心点横坐标
* @param centerY 中心点纵坐标
* @return
*/
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);
});
}
}
运行一下:
可以看到,数学解是成功的。
参考
https://zhuanlan.zhihu.com/p/82184417