001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.math.geometry.shape; 031 032import org.openimaj.math.geometry.point.Point2d; 033import org.openimaj.math.geometry.point.Point2dImpl; 034import org.openimaj.math.geometry.transforms.TransformUtilities; 035import org.openimaj.math.matrix.MatrixUtils; 036import org.openimaj.util.array.ArrayUtils; 037import org.openimaj.util.pair.IndependentPair; 038 039import Jama.Matrix; 040 041/** 042 * An ellipse shape 043 * 044 * @author Sina Samangooei (ss@ecs.soton.ac.uk) 045 * 046 */ 047public class Ellipse implements Shape, Cloneable { 048 private double x; 049 private double y; 050 private double major; 051 private double minor; 052 private double rotation; 053 054 /** 055 * Construct with centroid, semi-major and -minor axes, and rotation. 056 * 057 * @param x 058 * x-ordinate of centroid 059 * @param y 060 * y-ordinate of centroid 061 * @param major 062 * semi-major axis length 063 * @param minor 064 * semi-minor axis length 065 * @param rotation 066 * rotation 067 */ 068 public Ellipse(double x, double y, double major, double minor, double rotation) { 069 this.x = x; 070 this.y = y; 071 this.major = major; 072 this.minor = minor; 073 this.rotation = rotation; 074 } 075 076 @Override 077 public boolean isInside(Point2d point) { 078 // Unrotate the point relative to the center of the ellipse 079 final double cosrot = Math.cos(-rotation); 080 final double sinrot = Math.sin(-rotation); 081 final double relx = (point.getX() - x); 082 final double rely = (point.getY() - y); 083 084 final double xt = cosrot * relx - sinrot * rely; 085 final double yt = sinrot * relx + cosrot * rely; 086 087 final double ratiox = xt / major; 088 final double ratioy = yt / minor; 089 090 return ratiox * ratiox + ratioy * ratioy <= 1; 091 } 092 093 @Override 094 public Rectangle calculateRegularBoundingBox() { 095 096 // Differentiate the parametrics form of the ellipse equation to get 097 // tan(t) = -semiMinor * tan(rotation) / semiMajor (which gives us the 098 // min/max X) 099 // tan(t) = semiMinor * cot(rotation) / semiMajor (which gives us the 100 // min/max Y) 101 // 102 // We find a value for t, add PI to get another value of t, we use this 103 // to find our min/max x/y 104 105 final double[] minmaxx = new double[2]; 106 final double[] minmaxy = new double[2]; 107 final double tanrot = Math.tan(rotation); 108 final double cosrot = Math.cos(rotation); 109 final double sinrot = Math.sin(rotation); 110 111 double tx = Math.atan(-minor * tanrot / major); 112 double ty = Math.atan(minor * (1 / tanrot) / major); 113 114 minmaxx[0] = x + (major * Math.cos(tx) * cosrot - minor * Math.sin(tx) * sinrot); 115 tx += Math.PI; 116 minmaxx[1] = x + (major * Math.cos(tx) * cosrot - minor * Math.sin(tx) * sinrot); 117 minmaxy[0] = y + (major * Math.cos(ty) * sinrot + minor * Math.sin(ty) * cosrot); 118 ty += Math.PI; 119 minmaxy[1] = y + (major * Math.cos(ty) * sinrot + minor * Math.sin(ty) * cosrot); 120 121 double minx, miny, maxx, maxy; 122 minx = minmaxx[ArrayUtils.minIndex(minmaxx)]; 123 miny = minmaxy[ArrayUtils.minIndex(minmaxy)]; 124 maxx = minmaxx[ArrayUtils.maxIndex(minmaxx)]; 125 maxy = minmaxy[ArrayUtils.maxIndex(minmaxy)]; 126 127 return new Rectangle((float) minx, (float) miny, (float) (maxx - minx), (float) (maxy - miny)); 128 } 129 130 /** 131 * Calculate the oriented bounding box. This is the smallest rotated 132 * rectangle that will fit around the ellipse. 133 * 134 * @return the oriented bounding box. 135 */ 136 public Polygon calculateOrientedBoundingBox() { 137 final double minx = (-major); 138 final double miny = (-minor); 139 final double maxx = (+major); 140 final double maxy = (+minor); 141 142 Matrix corners = new Matrix(new double[][] { 143 { minx, miny, 1 }, 144 { minx, maxy, 1 }, 145 { maxx, miny, 1 }, 146 { maxx, maxy, 1 }, 147 }); 148 corners = corners.transpose(); 149 final Matrix rot = TransformUtilities.rotationMatrix(rotation); 150 final Matrix rotated = rot.times(corners); 151 final double[][] rotatedData = rotated.getArray(); 152 final double[] rx = ArrayUtils.add(rotatedData[0], this.x); 153 final double[] ry = ArrayUtils.add(rotatedData[1], this.y); 154 final Polygon ret = new Polygon(); 155 ret.points.add(new Point2dImpl((float) rx[0], (float) ry[0])); 156 ret.points.add(new Point2dImpl((float) rx[2], (float) ry[2])); 157 ret.points.add(new Point2dImpl((float) rx[3], (float) ry[3])); 158 ret.points.add(new Point2dImpl((float) rx[1], (float) ry[1])); 159 return ret; 160 } 161 162 @Override 163 public void translate(float x, float y) { 164 this.x += x; 165 this.y += y; 166 } 167 168 @Override 169 public void scale(float sc) { 170 this.x *= sc; 171 this.y *= sc; 172 this.major *= sc; 173 this.minor *= sc; 174 } 175 176 @Override 177 public void scale(Point2d centre, float sc) { 178 this.translate(-centre.getX(), -centre.getY()); 179 scale(sc); 180 this.translate(centre.getX(), centre.getY()); 181 } 182 183 @Override 184 public void scaleCentroid(float sc) { 185 this.major *= sc; 186 this.minor *= sc; 187 } 188 189 @Override 190 public Point2d calculateCentroid() { 191 return new Point2dImpl((float) x, (float) y); 192 } 193 194 @Override 195 public double calculateArea() { 196 return Math.PI * major * minor; 197 } 198 199 @Override 200 public double minX() { 201 return this.calculateRegularBoundingBox().minX(); 202 } 203 204 @Override 205 public double minY() { 206 return this.calculateRegularBoundingBox().minY(); 207 } 208 209 @Override 210 public double maxX() { 211 return this.calculateRegularBoundingBox().maxX(); 212 } 213 214 @Override 215 public double maxY() { 216 return this.calculateRegularBoundingBox().maxY(); 217 } 218 219 @Override 220 public double getWidth() { 221 return this.calculateRegularBoundingBox().getWidth(); 222 } 223 224 @Override 225 public double getHeight() { 226 return this.calculateRegularBoundingBox().getHeight(); 227 } 228 229 @Override 230 public Shape transform(Matrix transform) { 231 return this.asPolygon().transform(transform); 232 } 233 234 /** 235 * @param transform 236 * @return transformed ellipse 237 */ 238 public Matrix transformAffineCovar(Matrix transform) { 239 // Matrix translated = 240 // transform.times(TransformUtilities.translateMatrix((float)this.x, 241 // (float)this.y)); 242 // Matrix affineTransform = 243 // TransformUtilities.homographyToAffine(translated); 244 // affineTransform = affineTransform.times(1/affineTransform.get(2, 2)); 245 final Matrix affineTransform = TransformUtilities.homographyToAffine(transform, this.x, this.y); 246 247 final Matrix affineCovar = EllipseUtilities.ellipseToCovariance(this); 248 249 Matrix newTransform = new Matrix(3, 3); 250 newTransform.setMatrix(0, 1, 0, 1, affineCovar); 251 newTransform.set(2, 2, 1); 252 253 newTransform = affineTransform.times(newTransform).times(affineTransform.transpose()); 254 return newTransform; 255 } 256 257 /** 258 * @param transform 259 * @return transformed ellipse 260 */ 261 public Ellipse transformAffine(Matrix transform) { 262 final Point2d newCOG = this.calculateCentroid().transform(transform); 263 return EllipseUtilities 264 .ellipseFromCovariance(newCOG.getX(), newCOG.getY(), transformAffineCovar(transform), 1.0f); 265 } 266 267 /** 268 * Get the normalised transform matrix such that the scale of this ellipse 269 * is removed (i.e. the semi-major axis is 1) 270 * 271 * @return the transform matrix 272 */ 273 public Matrix normTransformMatrix() { 274 final double cosrot = Math.cos(rotation); 275 final double sinrot = Math.sin(rotation); 276 // 277 // double scaledMajor = 1.0; 278 // double scaledMinor = minor / major; 279 // 280 // double xMajor = cosrot * scaledMajor; 281 // double yMajor = sinrot * scaledMajor; 282 // double xMinor = -sinrot * scaledMinor; 283 // double yMinor = cosrot * scaledMinor; 284 // return new Matrix(new double[][]{ 285 // {xMajor,xMinor,this.x}, 286 // {yMajor,yMinor,this.y}, 287 // {0,0,1} 288 // }); 289 final double cosrotsq = cosrot * cosrot; 290 final double sinrotsq = sinrot * sinrot; 291 292 final double scale = Math.sqrt(major * minor); 293 294 final double majorsq = (major * major) / (scale * scale); 295 final double minorsq = (minor * minor) / (scale * scale); 296 final double Cxx = (cosrotsq / majorsq) + (sinrotsq / minorsq); 297 final double Cyy = (sinrotsq / majorsq) + (cosrotsq / minorsq); 298 final double Cxy = sinrot * cosrot * ((1 / majorsq) - (1 / minorsq)); 299 final double detC = Cxx * Cyy - (Cxy * Cxy); 300 301 Matrix cMat = new Matrix(new double[][] { 302 { Cyy / detC, -Cxy / detC }, 303 { -Cxy / detC, Cxx / detC } 304 }); 305 306 cMat = MatrixUtils.sqrt(cMat); 307 // cMat = cMat.inverse(); 308 final Matrix retMat = new Matrix(new double[][] { 309 { cMat.get(0, 0), cMat.get(0, 1), this.x }, 310 { cMat.get(1, 0), cMat.get(1, 1), this.y }, 311 { 0, 0, 1 }, 312 }); 313 return retMat; 314 } 315 316 /** 317 * Get the transform matrix required to turn points on a unit circle into 318 * the points on this ellipse. This function is used by 319 * {@link Ellipse#asPolygon} 320 * 321 * @return the transform matrix 322 */ 323 public Matrix transformMatrix() { 324 final double cosrot = Math.cos(rotation); 325 final double sinrot = Math.sin(rotation); 326 327 final double xMajor = cosrot * major; 328 final double yMajor = sinrot * major; 329 final double xMinor = -sinrot * minor; 330 final double yMinor = cosrot * minor; 331 return new Matrix(new double[][] { 332 { xMajor, xMinor, this.x }, 333 { yMajor, yMinor, this.y }, 334 { 0, 0, 1 } 335 }); 336 337 // double cosrotsq = cosrot * cosrot; 338 // double sinrotsq = sinrot * sinrot; 339 // 340 // double scale = Math.sqrt(major * minor); 341 // 342 // double majorsq = (major * major) / (scale * scale); 343 // double minorsq = (minor * minor) / (scale * scale); 344 // double Cxx = (cosrotsq / majorsq) + (sinrotsq/minorsq); 345 // double Cyy = (sinrotsq / majorsq) + (cosrotsq/minorsq); 346 // double Cxy = sinrot * cosrot * ((1/majorsq) - (1/minorsq)); 347 // double detC = Cxx*Cyy - (Cxy*Cxy); 348 // 349 // Matrix cMat = new Matrix(new double[][]{ 350 // {Cxx/detC,-Cxy/detC}, 351 // {-Cxy/detC,Cyy/detC} 352 // }); 353 // 354 // cMat = cMat.inverse(); 355 // Matrix retMat = new Matrix(new double[][]{ 356 // {cMat.get(0,0),cMat.get(0,1),this.x}, 357 // {cMat.get(1,0),cMat.get(1,1),this.y}, 358 // {0,0,1}, 359 // }); 360 // return retMat; 361 362 } 363 364 @Override 365 public Polygon asPolygon() { 366 final Polygon e = new Polygon(); 367 368 final Matrix transformMatrix = this.transformMatrix(); 369 final Point2dImpl circlePoint = new Point2dImpl(0, 0); 370 for (double t = -Math.PI; t < Math.PI; t += Math.PI / 360) { 371 circlePoint.x = (float) Math.cos(t); 372 circlePoint.y = (float) Math.sin(t); 373 e.points.add(circlePoint.transform(transformMatrix)); 374 } 375 return e; 376 } 377 378 @Override 379 public double intersectionArea(Shape that) { 380 return intersectionArea(that, 1); 381 } 382 383 @Override 384 public double intersectionArea(Shape that, int nStepsPerDimension) { 385 // Rectangle overlapping = 386 // this.calculateRegularBoundingBox().overlapping(that.calculateRegularBoundingBox()); 387 // if(overlapping==null) 388 // return 0; 389 // // if(that instanceof Ellipse) return 390 // intersectionAreaEllipse((Ellipse) that); 391 // double intersection = 0; 392 // double step = Math.max(overlapping.width, 393 // overlapping.height)/(double)nStepsPerDimention; 394 // double nReads = 0; 395 // for(float x = overlapping.x; x < overlapping.x + overlapping.width; 396 // x+=step){ 397 // for(float y = overlapping.y; y < overlapping.y + overlapping.height; 398 // y+=step){ 399 // boolean insideThis = this.isInside(new Point2dImpl(x,y)); 400 // boolean insideThat = that.isInside(new Point2dImpl(x,y)); 401 // nReads++; 402 // if(insideThis && insideThat) { 403 // intersection++; 404 // } 405 // } 406 // } 407 // 408 // return (intersection/nReads) * (overlapping.width * 409 // overlapping.height); 410 411 if (!this.calculateRegularBoundingBox().isOverlapping(that.calculateRegularBoundingBox())) { 412 return 0; 413 } 414 final Rectangle union = this.calculateRegularBoundingBox().union(that.calculateRegularBoundingBox()); 415 final float dr = (Math.min(union.width, union.height) / nStepsPerDimension); 416 417 // System.out.println("Union rectangle: " + union); 418 // System.out.println("Union step: " + dr); 419 int bua = 0; 420 int bna = 0; 421 int total = 0; 422 // compute the area 423 for (float rx = union.x; rx <= union.x + union.width; rx += dr) { 424 for (float ry = union.y; ry <= union.y + union.height; ry += dr) { 425 // compute the distance from the ellipse center 426 final Point2dImpl p = new Point2dImpl(rx, ry); 427 final boolean inThis = this.isInside(p); 428 final boolean inThat = that.isInside(p); 429 total++; 430 // compute the area 431 if (inThis && inThat) 432 bna++; 433 if (inThis || inThat) 434 bua++; 435 436 } 437 } 438 final double rectShapeProp = ((double) bua) / ((double) total); 439 final double intersectProp = ((double) bna / (double) bua) * rectShapeProp; 440 return union.calculateArea() * intersectProp; 441 } 442 443 /** 444 * @return The semi-minor axis length 445 */ 446 public double getMinor() { 447 return this.minor; 448 } 449 450 /** 451 * @return The semi-major axis length 452 */ 453 public double getMajor() { 454 return this.major; 455 } 456 457 /** 458 * @return The second moment matrix and scale factor. 459 */ 460 public IndependentPair<Matrix, Double> secondMomentsAndScale() { 461 return null; 462 } 463 464 @Override 465 public boolean equals(Object other) { 466 if (!(other instanceof Ellipse)) 467 return false; 468 final Ellipse that = (Ellipse) other; 469 return this.major == that.major && this.minor == that.minor && this.x == that.x && this.y == that.y 470 && this.rotation == that.rotation; 471 } 472 473 @Override 474 public Ellipse clone() { 475 Ellipse e; 476 try { 477 e = (Ellipse) super.clone(); 478 } catch (final CloneNotSupportedException e1) { 479 return null; 480 } 481 return e; 482 } 483 484 /** 485 * @return The rotation of the semi-major axis 486 */ 487 public double getRotation() { 488 return this.rotation; 489 } 490 491 @Override 492 public String toString() { 493 return String.format("Ellipse(x=%4.2f,y=%4.2f,major=%4.2f,minor=%4.2f,rot=%4.2f(%4.2f))", this.x, this.y, 494 this.major, this.minor, this.rotation, this.rotation * (180.0 / Math.PI)); 495 } 496 497 @Override 498 public double calculatePerimeter() { 499 // Ramanujan's second approximation 500 501 final double a = major; 502 final double b = minor; 503 final double h = ((a - b) * (a - b)) / ((a + b) * (a + b)); 504 505 return Math.PI * (a + b) * (1 + ((3 * h) / (10 + Math.sqrt(4 - 3 * h)))); 506 } 507 508 @Override 509 public RotatedRectangle minimumBoundingRectangle() { 510 return new RotatedRectangle(x, y, 2 * major, 2 * minor, rotation); 511 } 512 513 @Override 514 public boolean isConvex() { 515 return true; 516 } 517}