001/*******************************************************************************
002 * This software is provided as a supplement to the authors' textbooks on digital
003 *  image processing published by Springer-Verlag in various languages and editions.
004 * Permission to use and distribute this software is granted under the BSD 2-Clause 
005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 
006 * Copyright (c) 2006-2016 Wilhelm Burger, Mark J. Burge. All rights reserved. 
007 * Visit http://imagingbook.com for additional details.
008 *******************************************************************************/
009
010package imagingbook.pub.fd;
011
012import imagingbook.lib.math.Arithmetic;
013import imagingbook.lib.math.Complex;
014
015import java.awt.geom.Path2D;
016import java.awt.geom.Point2D;
017
018import org.apache.commons.math3.analysis.UnivariateFunction;
019import org.apache.commons.math3.optim.MaxEval;
020import org.apache.commons.math3.optim.nonlinear.scalar.GoalType;
021import org.apache.commons.math3.optim.univariate.BrentOptimizer;
022import org.apache.commons.math3.optim.univariate.SearchInterval;
023import org.apache.commons.math3.optim.univariate.UnivariateObjectiveFunction;
024import org.apache.commons.math3.optim.univariate.UnivariateOptimizer;
025import org.apache.commons.math3.optim.univariate.UnivariatePointValuePair;
026
027/**
028 * This is the abstract super-class for Fourier descriptors. It cannot
029 * be instantiated.
030 * @author W. Burger
031 * @version 2015/08/13
032 */
033public abstract class FourierDescriptor implements Cloneable {
034
035        static int minReconstructionSamples = 50;
036
037        protected Complex[] g;  // complex-valued samples (used only for display purposes)
038        protected Complex[] G;  // complex-valued DFT spectrum
039        protected double reconstructionScale = 1.0;             // remembers original scale after normalization
040
041        // ----------------------------------------------------------------
042
043        public double getReconstructionScale() {
044                return reconstructionScale;
045        }
046
047        /**
048         * Truncates this Fourier descriptor to the {@code Mp} lowest-frequency coefficients.
049         * For example, the original Fourier descriptor with 10 coefficients a,b,...,j
050         * <pre>
051         * m    = 0 1 2 3 4 5 6 7 8 9
052         * G[m] = a b c d e f g h i j
053         * </pre>
054         * becomes (with {@code P} = 3)
055         * <pre>
056         * m    = 0 1 2 3 4 5 6
057         * G[m] = a b c d h i j
058         * </pre>
059         * @param Mp number of coefficients to remain
060         * @return a new (truncated) instance of {@link FourierDescriptor}
061         */
062        public FourierDescriptor truncate(int Mp) {
063                FourierDescriptor fd = this.clone();
064                fd.truncateSelf(Mp);
065                return fd;
066        }
067
068        private void truncateSelf(int Mp) {
069                int M = G.length;
070                if (Mp > 0 && Mp < M) {
071                        Complex[] Gnew = new Complex[Mp];
072                        for (int m = 0; m < Mp; m++) {
073                                if (m <= Mp / 2)
074                                        Gnew[m] = G[m];
075                                else
076                                        Gnew[m] = G[M - Mp + m];
077                        }
078                        G = Gnew;
079                }
080        }
081
082        public FourierDescriptor clone() {
083                FourierDescriptor fd2 = null;
084                try {
085                        fd2 = (FourierDescriptor) super.clone();
086                } catch (CloneNotSupportedException e) {
087                        e.printStackTrace();
088                }
089                fd2.g = Complex.duplicate(this.g);
090                fd2.G = Complex.duplicate(this.G);
091                return fd2;
092        }
093
094        public int getMaxNegHarmonic() {
095                return -(G.length - 1)/2;
096        }
097
098        public int getMaxPosHarmonic() {
099                return G.length/2;
100        }
101
102        public int getMaxCoefficientPairs() {
103                return (G.length - 1)/2;
104        }
105
106        // ----------------------------------------------------------------
107
108        protected static Complex[] makeComplex(Point2D[] points) {
109                int N = points.length;
110                Complex[] samples = new Complex[N];
111                for (int i = 0; i < N; i++) {
112                        samples[i] = new Complex(points[i]);
113                }
114                return samples;
115        }
116
117        // ------------------------------------------------------------------
118
119        public Complex[] getSamples() {
120                return g;
121        }
122
123        public Complex[] getCoefficients() {
124                return G;
125        }
126
127        public int size() {
128                return G.length;        // = M
129        }
130
131        public int getCoefficientIndex(int m) {
132                return Arithmetic.mod(m, G.length);
133        }
134
135        public Complex getCoefficient(int m) {
136                int mm = Arithmetic.mod(m, G.length);
137                return new Complex(G[mm]);
138        }
139
140        public void setCoefficient(int m, Complex z) {
141                int mm = Arithmetic.mod(m, G.length);
142                G[mm] = new Complex(z);
143        }
144
145        public void setCoefficient(int m, double a, double b) {
146                int mm = Arithmetic.mod(m, G.length);
147                G[mm] = new Complex(a, b);
148        }
149
150        // ------------------------------------------------------------------
151
152        /**
153         * Calculates a reconstruction from the full DFT spectrum with N samples.
154         * 
155         * @param N number of samples
156         * @return reconstructed contour points
157         */
158        public Complex[] getReconstruction(int N) {
159                Complex[] S = new Complex[N];
160                for (int i = 0; i < N; i++) {
161                        double t = (double) i / N;
162                        S[i] = getReconstructionPoint(t);
163                }
164                return S;
165        }
166
167
168        /**
169         * Calculate a reconstruction from the partial DFT spectrum with N sample
170         * points and using Mp coefficient pairs.
171         * 
172         * @param N number of samples
173         * @param Mp number of coefficient pairs
174         * @return reconstructed contour points
175         */
176        public Complex[] getReconstruction(int N, int Mp) {
177                Complex[] S = new Complex[N];
178                Mp = Math.min(Mp, -getMaxNegHarmonic());
179                for (int i = 0; i < N; i++) {
180                        double t = (double) i / N;
181                        S[i] = getReconstructionPoint(t, -Mp, +Mp);
182                }
183                return S;
184        }
185
186
187        /**
188         * Reconstructs a single spatial point from the complete FD
189         * at the fractional path position t in [0,1].
190         * 
191         * @param t path position
192         * @return single contour point
193         */
194        public Complex getReconstructionPoint(double t) {
195                int mm = getMaxNegHarmonic();
196                int mp = getMaxPosHarmonic();
197                return getReconstructionPoint(t, mm, mp);
198        }
199
200        public Complex getReconstructionPoint(double t, int Mp) {
201                return getReconstructionPoint(t, -Mp, Mp);
202        }
203
204
205        /**
206         * Reconstructs a single spatial point from this FD using
207         * coefficients [mm,...,mp] = [m-,...,m+] at the fractional path position t in [0,1].
208         * 
209         * @param t path position
210         * @param mm most negative frequency index
211         * @param mp most positive frequency index
212         * @return single contour point
213         */
214        private Complex getReconstructionPoint(double t, int mm, int mp) {
215                double x = G[0].re();
216                double y = G[0].im();
217                for (int m = mm; m <= mp; m++) {
218                        if (m != 0) {
219                                Complex Gm = getCoefficient(m);
220                                double A = reconstructionScale * Gm.re();
221                                double B = reconstructionScale * Gm.im();
222                                double phi = 2 * Math.PI * m * t;
223                                double sinPhi = Math.sin(phi);
224                                double cosPhi = Math.cos(phi);
225                                x = x + A * cosPhi - B * sinPhi;
226                                y = y + A * sinPhi + B * cosPhi;
227                        }
228                }
229                return new Complex(x, y);
230        }
231
232        // -----------------------------------------------------------------------
233
234
235        public Path2D makeEllipse(Complex G1, Complex G2, int m, double xOffset, double yOffset) {
236                Path2D path = new Path2D.Float();
237                int recPoints = Math.max(minReconstructionSamples, G.length * 3);
238                for (int i = 0; i < recPoints; i++) {
239                        double t = (double) i / recPoints;
240                        Complex p1 = this.getEllipsePoint(G1, G2, m, t);
241                        double xt = p1.re();
242                        double yt = p1.im();
243                        if (i == 0) {
244                                path.moveTo(xt + xOffset, yt + yOffset);
245                        }
246                        else {
247                                path.lineTo(xt + xOffset, yt + yOffset);
248                        }
249                }
250                path.closePath();
251                return path;
252        }
253
254
255        /**
256         * Get the reconstructed point for two DFT coefficients G1, G2 at a given
257         * position t.
258         * @param G1 first coefficient
259         * @param G2 second coefficient
260         * @param m frequency number
261         * @param t contour position
262         * @return reconstructed point
263         */
264        public Complex getEllipsePoint(Complex G1, Complex G2, int m, double t) {
265                Complex p1 = getReconstructionPoint(G1, -m, t);
266                Complex p2 = getReconstructionPoint(G2, m, t);
267                return p1.add(p2);
268        }
269
270
271        /**
272         * Returns the spatial point reconstructed from a single
273         * DFT coefficient 'Gm' with frequency 'm' at 
274         * position 't' in [0,1].
275         * 
276         * @param Gm single DFT coefficient
277         * @param m frequency index
278         * @param t contour position
279         * @return reconstructed point
280         */
281        private Complex getReconstructionPoint(Complex Gm, int m, double t) {
282                double wm = 2 * Math.PI * m;
283                double Am = Gm.re();
284                double Bm = Gm.im();
285                double cost = Math.cos(wm * t);
286                double sint = Math.sin(wm * t);
287                double xt = Am * cost - Bm * sint;
288                double yt = Bm * cost + Am * sint;
289                return new Complex(xt, yt);
290        }
291
292
293        /**
294         * Reconstructs the shape using all FD pairs.
295         * 
296         * @return reconstructed shape
297         */
298        public Path2D makeFourierPairsReconstruction() {
299                int M = G.length;
300                return  makeFourierPairsReconstruction(M/2);
301        }
302
303        /**
304         * Reconstructs the shape obtained from FD-pairs 0,...,Mp as a polygon (path).
305         * 
306         * @param Mp number of Fourier coefficient pairs
307         * @return reconstructed shape
308         */
309        public Path2D makeFourierPairsReconstruction(int Mp) {
310                int M = G.length;
311                Mp = Math.min(Mp, M/2);
312                int recPoints = Math.max(minReconstructionSamples, G.length * 3);
313                Path2D path = new Path2D.Float();
314                for (int i = 0; i < recPoints; i++) {
315                        double t = (double) i / recPoints;
316                        Complex pt = new Complex(getCoefficient(0));    // assumes that coefficient 0 is never scaled
317                        // calculate a particular reconstruction point 
318                        for (int m = 1; m <= Mp; m++) {
319                                Complex ep = getEllipsePoint(getCoefficient(-m), getCoefficient(m), m, t);
320                                pt = pt.add(ep.mult(reconstructionScale));
321                        }
322                        double xt = pt.re(); 
323                        double yt = pt.im(); 
324                        if (i == 0) {
325                                path.moveTo(xt, yt);
326                        }
327                        else {
328                                path.lineTo(xt, yt);
329                        }
330                }
331                path.closePath();
332                return path;
333        }
334
335
336        public int getMaxDftMagnitudeIndex() {
337                double maxMag = -1;
338                int maxIdx = -1;
339                for (int i=0; i<G.length; i++) {
340                        double mag = G[i].abs();
341                        if (mag > maxMag) {
342                                maxMag = mag;
343                                maxIdx = i;
344                        }
345                }
346                return maxIdx;
347        }
348
349        public double getMaxDftMagnitude() {
350                int maxIdx = getMaxDftMagnitudeIndex();
351                return G[maxIdx].abs();
352        }
353
354        // Invariance -----------------------------------------------------
355
356        public FourierDescriptor[] makeInvariant() {
357                int Mp = getMaxCoefficientPairs();
358                return makeInvariant(Mp);
359        }
360
361        public FourierDescriptor[] makeInvariant(int Mp) {
362                makeScaleInvariant(Mp);
363                FourierDescriptor[] fdAB = makeStartPointInvariant(Mp); // = [fdA, fdB]
364                fdAB[0].makeRotationInvariant(Mp);      // works destructively!
365                fdAB[1].makeRotationInvariant(Mp);
366                return fdAB;
367        }
368
369        public FourierDescriptor[] makeStartPointInvariant() {
370                int Mp = getMaxCoefficientPairs();
371                return makeStartPointInvariant(Mp);
372        }
373
374        private FourierDescriptor[] makeStartPointInvariant(int Mp) {
375                double phiA = getStartPointPhase(Mp);
376                double phiB = phiA + Math.PI;
377                FourierDescriptor fdA = clone();
378                FourierDescriptor fdB = clone();
379                fdA.shiftStartPointPhase(phiA, Mp);
380                fdB.shiftStartPointPhase(phiB, Mp);
381                return new FourierDescriptor[] {fdA, fdB};
382        }
383
384        // -----------------------------------------------------------------
385
386        /**
387         * Sets the zero (DC) coefficient to zero.
388         */
389        public void makeTranslationInvariant() {
390                G[0] = new Complex(0,0);
391        }
392
393
394        /**
395         * Normalizes this descriptor destructively to the L2 norm of G, 
396         * keeps G_0 untouched.
397         * 
398         * @return the scale factor used for normalization
399         */
400        public double makeScaleInvariant() {
401                double s = 0;
402                for (int m = 1; m < G.length; m++) {
403                        s = s + G[m].abs2();
404                }
405                // scale coefficients
406                double norm = Math.sqrt(s);
407                reconstructionScale = norm;             // keep for later reconstruction
408                double scale = 1 / norm;
409                for (int m = 1; m < G.length; m++) {
410                        G[m] =  G[m].mult(scale);
411                }
412                return scale;
413        }
414
415
416        /**
417         * Normalizes the L2 norm of the sub-vector (G_{-Mp}, ..., G_{Mp}),
418         * keeps G_0 untouched.
419         * 
420         * @param Mp most positive/negative frequency index
421         * @return normalized coefficient sub-vector
422         */
423        private double makeScaleInvariant(int Mp) {
424                double s = 0;
425                for (int m = 1; m <= Mp; m++) {
426                        s = s + getCoefficient(-m).abs2() + getCoefficient(m).abs2();
427                }
428                // scale Fourier coefficients:
429                double norm = Math.sqrt(s);
430                reconstructionScale = norm;             // keep for later reconstruction
431                double scale = 1 / norm;
432                for (int m = 1; m <= Mp; m++) {
433                        setCoefficient(-m, getCoefficient(-m).mult(scale));
434                        setCoefficient( m, getCoefficient( m).mult(scale));
435                }
436                return scale;
437        }
438
439        
440        
441        public double makeRotationInvariant() { // works destructively.
442                int Mp = getMaxCoefficientPairs();
443                return makeRotationInvariant(Mp);
444        }
445
446        private double makeRotationInvariant(int Mp) {
447                Complex z = new Complex(0,0);
448                for (int m = 1; m <= Mp; m++) {
449                        Complex Gm = getCoefficient(-m);
450                        Complex Gp = getCoefficient(+m);
451                        double w = 1.0 / m;
452                        z = z.add(Gm.mult(w));
453                        z = z.add(Gp.mult(w));
454                }
455                double beta = z.arg();
456                for (int m = 1; m <= Mp; m++) {
457                        setCoefficient(-m, getCoefficient(-m).rotate(-beta));
458                        setCoefficient( m, getCoefficient( m).rotate(-beta));
459                }
460                return beta;
461        }
462
463        /**
464         * For testing: apply shape rotation to this FourierDescriptor (phi in radians)
465         * 
466         * @param phi rotation angle
467         */
468        public void rotate(double phi) {
469                rotate(G, phi);
470        }
471
472        /**
473         * For testing: apply shape rotation to this FourierDescriptor (phi in radians)
474         * 
475         * @param C complex point
476         * @param phi angle
477         */
478        private void rotate(Complex[] C, double phi) {
479                Complex rot = new Complex(phi);
480                for (int m = 1; m < G.length; m++) {
481                        C[m] = C[m].mult(rot);
482                }
483        }
484
485        /**
486         * Apply a particular start-point phase shift
487         * 
488         * @param phi start point phase
489         * @param Mp most positive/negative frequency index
490         */
491        private void shiftStartPointPhase(double phi, int Mp) {
492                Mp = Math.min(Mp, G.length/2);
493                for (int m = -Mp; m <= Mp; m++) {
494                        if (m != 0) {
495                                setCoefficient(m, getCoefficient(m).rotate(m * phi));
496                        }
497                }
498        }
499
500
501        /**
502         * Calculates the 'canonical' start point. This version uses 
503         * (a) a coarse search for a global maximum of fp() and subsequently 
504         * (b) a numerical optimization using Brent's method
505         * (implemented with Apache Commons Math).
506         * 
507         * @param Mp number of Fourier coefficient pairs
508         * @return start point phase
509         */
510        public double getStartPointPhase(int Mp) {
511                Mp = Math.min(Mp, (G.length-1)/2);
512                UnivariateFunction fp =  new TargetFunction(Mp);
513                // search for the global maximum in coarse steps
514                double cmax = Double.NEGATIVE_INFINITY;
515                int kmax = -1;
516                int K = 25;     // number of steps over 180 degrees
517                for (int k = 0; k < K; k++) {
518                        final double phi = Math.PI * k / K;     // phase to evaluate
519                        final double c = fp.value(phi);
520                        if (c > cmax) {
521                                cmax = c;
522                                kmax = k;
523                        }
524                }
525                // optimize using previous and next point as the bracket.
526                double minPhi = Math.PI * (kmax - 1) / K;
527                double maxPhi = Math.PI * (kmax + 1) / K;       
528
529                UnivariateOptimizer optimizer = new BrentOptimizer(1E-4, 1E-6);
530                int maxIter = 20;
531                UnivariatePointValuePair result = optimizer.optimize(
532                                new MaxEval(maxIter),
533                                new UnivariateObjectiveFunction(fp),
534                                GoalType.MAXIMIZE,
535                                new SearchInterval(minPhi, maxPhi)
536                                );
537                double phi0 = result.getPoint();
538                return phi0;    // the canonical start point phase
539        }
540
541        /**
542         * This inner class defines the target function for calculating the
543         * canonical start point phase. {@link UnivariateFunction} is defined in the
544         * Apache Common Maths framework.
545         */
546        private class TargetFunction implements UnivariateFunction {
547                final int Mp;
548
549                TargetFunction(int Mp) {
550                        this.Mp = Mp;
551                }
552
553                /** 
554                 * The value returned is the sum of the cross products of the FD pairs,
555                 * with all coefficients rotated to the given start point phase phi.
556                 * TODO: This could be made a lot more efficient!
557                 */
558                public double value(double phi) {
559                        double sum = 0;
560                        for (int m = 1; m <= Mp; m++) {
561                                Complex Gm = getCoefficient(-m).rotate(-m * phi);
562                                Complex Gp = getCoefficient( m).rotate( m * phi);
563                                sum = sum + Gp.crossProduct(Gm);
564                        }
565                        return sum;
566                }
567        }
568
569
570        public double distanceComplex(FourierDescriptor fd2) {
571                return distanceComplex(fd2, G.length/2);
572        }
573
574
575        public double distanceComplex(FourierDescriptor fd2, int Mp) {
576                FourierDescriptor fd1 = this;
577                Mp = Math.min(Mp, G.length/2);
578                double sum = 0;
579                for (int m = -Mp; m <= Mp; m++) {
580                        if (m != 0) {
581                                Complex G1m = fd1.getCoefficient(m);
582                                Complex G2m = fd2.getCoefficient(m);
583                                double dRe = G1m.re() - G2m.re();
584                                double dIm = G1m.im() - G2m.im();
585                                sum = sum + dRe * dRe + dIm * dIm;
586                        }
587                }
588                return Math.sqrt(sum);
589        }
590
591
592        public double distanceMagnitude(FourierDescriptor fd2) {
593                int Mp = getMaxCoefficientPairs();
594                return distanceMagnitude(fd2, Mp);
595        }
596
597
598        public double distanceMagnitude(FourierDescriptor fd2, int Mp) {
599                FourierDescriptor fd1 = this;
600                Mp = Math.min(Mp, G.length/2);
601                double sum = 0;
602                for (int m = -Mp; m <= Mp; m++) {
603                        if (m != 0) {
604                                double mag1 = fd1.getCoefficient(m).abs();
605                                double mag2 = fd2.getCoefficient(m).abs();
606                                double dmag = mag2 - mag1;
607                                sum = sum + (dmag * dmag);
608                        }
609                }
610                return Math.sqrt(sum);
611        }
612
613}