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}