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}