1 /*
2 * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.geometry;
17
18 import com.irurueta.algebra.AlgebraException;
19 import com.irurueta.algebra.ArrayUtils;
20 import com.irurueta.algebra.Matrix;
21 import com.irurueta.algebra.RQDecomposer;
22 import com.irurueta.algebra.SingularValueDecomposer;
23 import com.irurueta.algebra.Utils;
24 import com.irurueta.algebra.WrongSizeException;
25
26 import java.io.Serializable;
27 import java.util.Arrays;
28
29 /**
30 * This class performs affine transformations on 2D space.
31 * Affine transformations include transformations related to rotations,
32 * translations, independently scaling horizontal or vertical coordinates
33 * or skewing the coordinates axis.
34 * This class is not intended to be used on points located at infinity or
35 * at very large coordinates, since numerical instabilities may occur. For
36 * those cases use a ProjectiveTransformation2D instead.
37 */
38 @SuppressWarnings("DuplicatedCode")
39 public class AffineTransformation2D extends Transformation2D implements Serializable {
40
41 /**
42 * Constant indicating number of coordinates required in translation arrays.
43 */
44 public static final int NUM_TRANSLATION_COORDS = 2;
45
46
47 /**
48 * Constant defining number of inhomogeneous coordinates in 2D space.
49 */
50 public static final int INHOM_COORDS = 2;
51
52 /**
53 * Constant defining number of homogeneous coordinates in 2D space.
54 */
55 public static final int HOM_COORDS = 3;
56
57 /**
58 * Linear mapping.
59 */
60 private Matrix a;
61
62 /**
63 * 2D translation to be performed on geometric objects.
64 * Translation is specified using inhomogeneous coordinates.
65 */
66 private double[] translation;
67
68 /**
69 * Empty constructor.
70 * Creates transformation that has no effect.
71 */
72 public AffineTransformation2D() {
73 super();
74 try {
75 a = Matrix.identity(INHOM_COORDS, INHOM_COORDS);
76 } catch (final WrongSizeException ignore) {
77 // never happens
78 }
79 translation = new double[NUM_TRANSLATION_COORDS];
80 }
81
82 /**
83 * Creates transformation with provided linear mapping matrix.
84 *
85 * @param a linear mapping.
86 * @throws NullPointerException raised if provided rotation is null.
87 * @throws IllegalArgumentException raised if provided matrix does not have
88 * size 2x2.
89 */
90 public AffineTransformation2D(final Matrix a) {
91 setA(a);
92 translation = new double[NUM_TRANSLATION_COORDS];
93 }
94
95 /**
96 * Creates transformation with provided scale value.
97 *
98 * @param scale Scale value. Values between 0.0 and 1.0 reduce objects,
99 * values greater than 1.0 enlarge objects and negative values reverse
100 * objects.
101 */
102 public AffineTransformation2D(final double scale) {
103 final var diag = new double[INHOM_COORDS];
104 Arrays.fill(diag, scale);
105 a = Matrix.diagonal(diag);
106 translation = new double[NUM_TRANSLATION_COORDS];
107 }
108
109 /**
110 * Creates transformation with provided rotation.
111 *
112 * @param rotation a 2D rotation.
113 * @throws NullPointerException Raised if provided rotation is null.
114 */
115 public AffineTransformation2D(final Rotation2D rotation) {
116 a = rotation.asInhomogeneousMatrix();
117 translation = new double[NUM_TRANSLATION_COORDS];
118 }
119
120 /**
121 * Creates transformation with provided scale and rotation.
122 *
123 * @param scale Scale value. Values between 0.0 and 1.0 reduce objects,
124 * values greater than 1.0 enlarge objects and negative values reverse
125 * objects.
126 * @param rotation a 2D rotation.
127 * @throws NullPointerException raised if provided rotation is null.
128 */
129 public AffineTransformation2D(final double scale, final Rotation2D rotation) {
130 final var diag = new double[INHOM_COORDS];
131 Arrays.fill(diag, scale);
132 a = Matrix.diagonal(diag);
133 try {
134 a.multiply(rotation.asInhomogeneousMatrix());
135 } catch (final WrongSizeException ignore) {
136 // never happens
137 }
138 translation = new double[NUM_TRANSLATION_COORDS];
139 }
140
141 /**
142 * Creates transformation with provided affine parameters and rotation.
143 *
144 * @param params affine parameters including horizontal scaling, vertical
145 * scaling and skewness.
146 * @param rotation a 2D rotation.
147 * @throws NullPointerException raised if provided parameters are null or
148 * if provided rotation is null.
149 */
150 public AffineTransformation2D(final AffineParameters2D params, final Rotation2D rotation) {
151 a = params.asMatrix();
152 try {
153 a.multiply(rotation.asInhomogeneousMatrix());
154 } catch (final WrongSizeException ignore) {
155 // never happens
156 }
157 translation = new double[NUM_TRANSLATION_COORDS];
158 }
159
160 /**
161 * Creates transformation with provided 2D translation.
162 *
163 * @param translation array indicating 2D translation using inhomogeneous
164 * coordinates.
165 * @throws NullPointerException raised if provided array is null.
166 * @throws IllegalArgumentException raised if length of array is not equal
167 * to NUM_TRANSLATION_COORDS.
168 */
169 public AffineTransformation2D(final double[] translation) {
170 if (translation.length != NUM_TRANSLATION_COORDS) {
171 throw new IllegalArgumentException();
172 }
173
174 try {
175 a = Matrix.identity(INHOM_COORDS, INHOM_COORDS);
176 } catch (final WrongSizeException ignore) {
177 // never happens
178 }
179 this.translation = translation;
180 }
181
182 /**
183 * Creates transformation with provided linear mapping and translation.
184 *
185 * @param a linear mapping.
186 * @param translation array indicating 2D translation using inhomogeneous
187 * coordinates.
188 * @throws NullPointerException raised if provided array is null or if
189 * linear mapping is null.
190 * @throws IllegalArgumentException raised if length of array is not equal
191 * to NUM_TRANSLATION_COORDS.
192 */
193 public AffineTransformation2D(final Matrix a, final double[] translation) {
194 if (translation.length != NUM_TRANSLATION_COORDS) {
195 throw new IllegalArgumentException();
196 }
197 this.translation = translation;
198
199 setA(a);
200 }
201
202 /**
203 * Creates transformation with provided scale and translation.
204 *
205 * @param scale scale value. Values between 0.0 and 1.0 reduce objects,
206 * values greater than 1.0 enlarge objects and negative values reverse
207 * objects.
208 * @param translation array indicating 2D translation using inhomogeneous
209 * coordinates.
210 * @throws NullPointerException raised if provided translation is null
211 * @throws IllegalArgumentException raised if provided translation does not
212 * have length 2.
213 */
214 public AffineTransformation2D(final double scale, final double[] translation) {
215 if (translation.length != NUM_TRANSLATION_COORDS) {
216 throw new IllegalArgumentException();
217 }
218
219 final var diag = new double[INHOM_COORDS];
220 Arrays.fill(diag, scale);
221 a = Matrix.diagonal(diag);
222
223 this.translation = translation;
224 }
225
226 /**
227 * Creates transformation with provided rotation and translation.
228 *
229 * @param rotation a 2D rotation.
230 * @param translation array indicating 2D translation using inhomogeneous
231 * coordinates.
232 * @throws NullPointerException raised if provided rotation or translation
233 * is null.
234 * @throws IllegalArgumentException raised if provided translation does not
235 * have length 2.
236 */
237 public AffineTransformation2D(final Rotation2D rotation, final double[] translation) {
238 if (translation.length != NUM_TRANSLATION_COORDS) {
239 throw new IllegalArgumentException();
240 }
241
242 a = rotation.asInhomogeneousMatrix();
243 this.translation = translation;
244 }
245
246 /**
247 * Creates transformation with provided scale, rotation and translation
248 *
249 * @param scale Scale value. Values between 0.0 and 1.0 reduce objects,
250 * values greater than 1.0 enlarge objects and negative values reverse
251 * objects.
252 * @param rotation a 2D rotation.
253 * @param translation array indicating 2D translation using inhomogeneous
254 * coordinates.
255 * @throws NullPointerException raised if provided rotation or translation
256 * is null.
257 * @throws IllegalArgumentException raised if provided translation does not
258 * have length 2.
259 */
260 public AffineTransformation2D(final double scale, final Rotation2D rotation, final double[] translation) {
261 if (translation.length != NUM_TRANSLATION_COORDS) {
262 throw new IllegalArgumentException();
263 }
264
265 final var diag = new double[INHOM_COORDS];
266 Arrays.fill(diag, scale);
267 a = Matrix.diagonal(diag);
268 try {
269 a.multiply(rotation.asInhomogeneousMatrix());
270 } catch (final WrongSizeException ignore) {
271 // never happens
272 }
273
274 this.translation = translation;
275 }
276
277 /**
278 * Creates transformation with provided parameters, rotation and
279 * translation.
280 *
281 * @param params affine parameters including horizontal scaling, vertical
282 * scaling and skewness.
283 * @param rotation a 2D rotation.
284 * @param translation array indicating 2D translation using inhomogeneous
285 * coordinates.
286 * @throws NullPointerException raised if provided parameters, rotation or
287 * translation is null.
288 * @throws IllegalArgumentException raised if provided translation does not
289 * have length 2.
290 */
291 public AffineTransformation2D(final AffineParameters2D params, final Rotation2D rotation,
292 final double[] translation) {
293 if (translation.length != NUM_TRANSLATION_COORDS) {
294 throw new IllegalArgumentException();
295 }
296
297 a = params.asMatrix();
298 try {
299 a.multiply(rotation.asInhomogeneousMatrix());
300 } catch (final WrongSizeException ignore) {
301 // never happens
302 }
303 this.translation = translation;
304 }
305
306 /**
307 * Creates transformation by estimating its internal values using provided 3
308 * corresponding original and transformed points.
309 *
310 * @param inputPoint1 1st input point.
311 * @param inputPoint2 2nd input point.
312 * @param inputPoint3 3rd input point.
313 * @param outputPoint1 1st transformed point corresponding to 1st input
314 * point.
315 * @param outputPoint2 2nd transformed point corresponding to 2nd input
316 * point.
317 * @param outputPoint3 3rd transformed point corresponding to 3rd input
318 * point.
319 * @throws CoincidentPointsException raised if transformation cannot be
320 * estimated for some reason (point configuration degeneracy, duplicate
321 * points or numerical instabilities).
322 */
323 public AffineTransformation2D(
324 final Point2D inputPoint1, final Point2D inputPoint2, final Point2D inputPoint3,
325 final Point2D outputPoint1, final Point2D outputPoint2, final Point2D outputPoint3)
326 throws CoincidentPointsException {
327 try {
328 a = new Matrix(INHOM_COORDS, INHOM_COORDS);
329 } catch (final WrongSizeException ignore) {
330 // never happens
331 }
332 translation = new double[NUM_TRANSLATION_COORDS];
333 setTransformationFromPoints(inputPoint1, inputPoint2, inputPoint3, outputPoint1, outputPoint2, outputPoint3);
334 }
335
336 /**
337 * Creates transformation by estimating its internal values using provided 3
338 * corresponding original and transformed lines.
339 *
340 * @param inputLine1 1st input line.
341 * @param inputLine2 2nd input line.
342 * @param inputLine3 3rd input line.
343 * @param outputLine1 1st transformed line corresponding to 1st input line.
344 * @param outputLine2 2nd transformed line corresponding to 2nd input line.
345 * @param outputLine3 3rd transformed line corresponding to 3rd input line.
346 * @throws CoincidentLinesException raised if transformation cannot be
347 * estimated for some reason (line configuration degeneracy, duplicate lines
348 * or numerical instabilities).
349 */
350 public AffineTransformation2D(
351 final Line2D inputLine1, final Line2D inputLine2, final Line2D inputLine3,
352 final Line2D outputLine1, final Line2D outputLine2, final Line2D outputLine3)
353 throws CoincidentLinesException {
354 setTransformationFromLines(inputLine1, inputLine2, inputLine3, outputLine1, outputLine2, outputLine3);
355 }
356
357 /**
358 * Returns linear mapping matrix to perform affine transformation.
359 * Point transformation is computed as a * x + t, where x is a point and t
360 * is the amount of translation.
361 *
362 * @return linear mapping matrix.
363 */
364 public Matrix getA() {
365 return a;
366 }
367
368 /**
369 * Sets linear mapping matrix to perform affine transformation.
370 *
371 * @param a linear mapping matrix.
372 * @throws NullPointerException raised if provided matrix is null.
373 * @throws IllegalArgumentException raised if provided matrix does not have
374 * size 2x2.
375 */
376 public final void setA(final Matrix a) {
377 if (a == null) {
378 throw new NullPointerException();
379 }
380 if (a.getRows() != INHOM_COORDS || a.getColumns() != INHOM_COORDS) {
381 throw new IllegalArgumentException();
382 }
383
384 this.a = a;
385 }
386
387 /**
388 * Returns 2D rotation assigned to this transformation.
389 * Note: if this rotation instance is modified, its changes won't be
390 * reflected on this instance until rotation is set again.
391 *
392 * @return 2D rotation.
393 * @throws AlgebraException if for some reason rotation cannot
394 * be estimated (usually because of numerical instability).
395 */
396 public Rotation2D getRotation() throws AlgebraException {
397 // Use QR decomposition to retrieve rotation
398 final var decomposer = new RQDecomposer(a);
399 try {
400 decomposer.decompose();
401 return new Rotation2D(decomposer.getQ());
402 } catch (final InvalidRotationMatrixException ignore) {
403 return null;
404 }
405 }
406
407 /**
408 * Sets 2D rotation for this transformation.
409 *
410 * @param rotation a 2D rotation.
411 * @throws NullPointerException raised if provided rotation is null.
412 * @throws AlgebraException raised if for numerical reasons rotation cannot
413 * be set (usually because of numerical instability in parameters of this
414 * transformation).
415 */
416 public void setRotation(final Rotation2D rotation) throws AlgebraException {
417 final var rotMatrix = rotation.asInhomogeneousMatrix();
418
419 // Use QR decomposition to retrieve parameters matrix
420 final var decomposer = new RQDecomposer(a);
421 decomposer.decompose();
422 // retrieves params matrix
423 final var localA = decomposer.getR();
424 localA.multiply(rotMatrix);
425 this.a = localA;
426 }
427
428 /**
429 * Adds provided rotation to current rotation assigned to this
430 * transformation.
431 *
432 * @param rotation 2D rotation to be added.
433 * @throws AlgebraException raised if for numerical reasons rotation cannot
434 * be set (usually because of numerical instability in parameters of this
435 * transformation).
436 */
437 public void addRotation(final Rotation2D rotation) throws AlgebraException {
438 final var localRotation = getRotation();
439 localRotation.combine(rotation);
440 setRotation(localRotation);
441 }
442
443 /**
444 * Sets scale of this transformation.
445 *
446 * @param scale scale value to be set. a value between 0.0 and 1.0 indicates
447 * that objects will be reduced, a value greater than 1.0 indicates that
448 * objects will be enlarged, and a negative value indicates that objects
449 * will be reversed.
450 * @throws AlgebraException raised if for numerical reasons scale cannot
451 * be set (usually because of numerical instability in parameters of this
452 * transformation).
453 */
454 public void setScale(final double scale) throws AlgebraException {
455 final var decomposer = new RQDecomposer(a);
456 decomposer.decompose();
457 // params
458 final var localA = decomposer.getR();
459 localA.setElementAt(0, 0, scale);
460 localA.setElementAt(1, 1, scale);
461 // multiply by rotation
462 localA.multiply(decomposer.getQ());
463 this.a = localA;
464 }
465
466 /**
467 * Gets affine parameters of this instance.
468 * Affine parameters contain horizontal scale, vertical scale and skewness
469 * of axes.
470 *
471 * @return affine parameters.
472 * @throws AlgebraException raised if for numerical reasons affine
473 * parameters cannot be retrieved (usually because of numerical instability
474 * in matrix a).
475 */
476 public AffineParameters2D getParameters() throws AlgebraException {
477 final var parameters = new AffineParameters2D();
478 getParameters(parameters);
479 return parameters;
480 }
481
482 /**
483 * Computes affine parameters of this instance and stores the result in
484 * provided instance.
485 * Affine parameters contain horizontal scale, vertical scale and skewness
486 * of axes.
487 *
488 * @param result instance where affine parameters will be stored.
489 * @throws AlgebraException raised if for numerical reasons affine
490 * parameters cannot be retrieved (usually because of numerical instability
491 * in matrix a).
492 */
493 public void getParameters(final AffineParameters2D result) throws AlgebraException {
494 final var decomposer = new RQDecomposer(a);
495 decomposer.decompose();
496 final var params = decomposer.getR();
497 result.fromMatrix(params);
498 }
499
500 /**
501 * Sets affine parameters of this instance.
502 * Affine parameters contain horizontal scale, vertical scale and skewness
503 * of axes.
504 *
505 * @param parameters affine parameters to be set.
506 * @throws AlgebraException raised if for numerical reasons affine
507 * parameters cannot be set (usually because of numerical instability in
508 * current matrix a).
509 */
510 public void setParameters(final AffineParameters2D parameters) throws AlgebraException {
511 final var decomposer = new RQDecomposer(a);
512 decomposer.decompose();
513 final var params = parameters.asMatrix();
514 final var rotation = decomposer.getQ();
515
516 params.multiply(rotation);
517 a = params;
518 }
519
520 /**
521 * Returns 2D translation assigned to this transformation as an array
522 * expressed in inhomogeneous coordinates.
523 *
524 * @return 2D translation array.
525 */
526 public double[] getTranslation() {
527 return translation;
528 }
529
530 /**
531 * Sets 2D translation assigned to this transformation as an array expressed
532 * in inhomogeneous coordinates.
533 *
534 * @param translation 2D translation array.
535 * @throws IllegalArgumentException raised if provided array does not have
536 * length equal to NUM_TRANSLATION_COORDS.
537 */
538 public void setTranslation(final double[] translation) {
539 if (translation.length != NUM_TRANSLATION_COORDS) {
540 throw new IllegalArgumentException();
541 }
542
543 this.translation = translation;
544 }
545
546 /**
547 * Adds provided translation to current translation on this transformation.
548 * Provided translation must be expressed as an array of inhomogeneous
549 * coordinates.
550 *
551 * @param translation 2D translation array.
552 * @throws IllegalArgumentException raised if provided array does not have
553 * length equal to NUM_TRANSLATION_COORDS.
554 */
555 public void addTranslation(final double[] translation) {
556 ArrayUtils.sum(this.translation, translation, this.translation);
557 }
558
559 /**
560 * Returns current x coordinate translation assigned to this transformation.
561 *
562 * @return X coordinate translation.
563 */
564 public double getTranslationX() {
565 return translation[0];
566 }
567
568 /**
569 * Sets x coordinate translation to be made by this transformation.
570 *
571 * @param translationX X coordinate translation to be set.
572 */
573 public void setTranslationX(final double translationX) {
574 translation[0] = translationX;
575 }
576
577 /**
578 * Returns current y coordinate translation assigned to this transformation.
579 *
580 * @return Y coordinate translation.
581 */
582 public double getTranslationY() {
583 return translation[1];
584 }
585
586 /**
587 * Sets y coordinate translation to be made by this transformation.
588 *
589 * @param translationY Y coordinate translation to be set.
590 */
591 public void setTranslationY(final double translationY) {
592 translation[1] = translationY;
593 }
594
595 /**
596 * Sets x, y coordinates of translation to be made by this transformation.
597 *
598 * @param translationX translation x coordinate to be set.
599 * @param translationY translation y coordinate to be set.
600 */
601 public void setTranslation(final double translationX, final double translationY) {
602 translation[0] = translationX;
603 translation[1] = translationY;
604 }
605
606 /**
607 * Sets x, y, coordinates of translation to be made by this transformation.
608 *
609 * @param translation translation to be set.
610 */
611 public void setTranslation(final Point2D translation) {
612 setTranslation(translation.getInhomX(), translation.getInhomY());
613 }
614
615 /**
616 * Gets x, y coordinates of translation to be made by this transformation
617 * as a new point.
618 *
619 * @return a new point containing translation coordinates.
620 */
621 public Point2D getTranslationPoint() {
622 final var out = Point2D.create();
623 getTranslationPoint(out);
624 return out;
625 }
626
627 /**
628 * Gets x, y coordinates of translation to be made by this transformation
629 * and stores them into provided point.
630 *
631 * @param out point where translation coordinates will be stored.
632 */
633 public void getTranslationPoint(final Point2D out) {
634 out.setInhomogeneousCoordinates(translation[0], translation[1]);
635 }
636
637 /**
638 * Adds provided x coordinate to current translation assigned to this
639 * transformation.
640 *
641 * @param translationX X coordinate to be added to current translation.
642 */
643 public void addTranslationX(final double translationX) {
644 translation[0] += translationX;
645 }
646
647 /**
648 * Adds provided y coordinate to current translation assigned to this
649 * transformation.
650 *
651 * @param translationY Y coordinate to be added to current translation.
652 */
653 public void addTranslationY(final double translationY) {
654 translation[1] += translationY;
655 }
656
657 /**
658 * Adds provided coordinates to current translation assigned to this
659 * transformation.
660 *
661 * @param translationX x coordinate to be added to current translation.
662 * @param translationY y coordinate to be added to current translation.
663 */
664 public void addTranslation(final double translationX, final double translationY) {
665 translation[0] += translationX;
666 translation[1] += translationY;
667 }
668
669 /**
670 * Adds provided coordinates to current translation assigned to this
671 * transformation.
672 *
673 * @param translation x, y, coordinates to be added to current translation.
674 */
675 public void addTranslation(final Point2D translation) {
676 addTranslation(translation.getInhomX(), translation.getInhomY());
677 }
678
679 /**
680 * Represents this transformation as a 3x3 matrix.
681 * a point can be transformed as T * p, where T is the transformation matrix
682 * and p is a point expressed as an homogeneous vector.
683 *
684 * @return This transformation in matrix form.
685 */
686 @Override
687 public Matrix asMatrix() {
688 Matrix m = null;
689 try {
690 m = new Matrix(HOM_COORDS, HOM_COORDS);
691 asMatrix(m);
692 } catch (final WrongSizeException ignore) {
693 // never happens
694 }
695 return m;
696 }
697
698 /**
699 * Represents this transformation as a 3x3 matrix and stores the result in
700 * provided instance.
701 *
702 * @param m instance where transformation matrix will be stored.
703 * @throws IllegalArgumentException raised if provided instance is not a 3x3
704 * matrix.
705 */
706 @Override
707 public void asMatrix(final Matrix m) {
708 if (m.getRows() != HOM_COORDS || m.getColumns() != HOM_COORDS) {
709 throw new IllegalArgumentException();
710 }
711
712 // set rotation
713 m.setSubmatrix(0, 0, INHOM_COORDS - 1, INHOM_COORDS - 1,
714 a);
715
716 // set translation
717 m.setSubmatrix(0, HOM_COORDS - 1, translation.length - 1,
718 HOM_COORDS - 1, translation);
719
720 // set last element
721 for (var i = 0; i < INHOM_COORDS; i++) {
722 m.setElementAt(INHOM_COORDS, i, 0.0);
723 }
724 m.setElementAt(HOM_COORDS - 1, HOM_COORDS - 1, 1.0);
725 }
726
727 /**
728 * Transforms input point using this transformation and stores the result in
729 * provided output points.
730 *
731 * @param inputPoint point to be transformed.
732 * @param outputPoint instance where transformed point data will be stored.
733 */
734 @Override
735 public void transform(final Point2D inputPoint, final Point2D outputPoint) {
736 try {
737 final var coords = new double[Point2D.POINT2D_INHOMOGENEOUS_COORDINATES_LENGTH];
738 coords[0] = inputPoint.getInhomX();
739 coords[1] = inputPoint.getInhomY();
740
741 final var p = a.multiplyAndReturnNew(Matrix.newFromArray(coords, true));
742
743 outputPoint.setInhomogeneousCoordinates(p.getElementAtIndex(0) + translation[0],
744 p.getElementAtIndex(1) + translation[1]);
745 } catch (final WrongSizeException ignore) {
746 // this exception will never be raised
747 }
748 }
749
750 /**
751 * Transforms a conic using this transformation and stores the result into
752 * provided output conic.
753 *
754 * @param inputConic conic to be transformed.
755 * @param outputConic instance where data of transformed conic will be
756 * stored.
757 * @throws NonSymmetricMatrixException raised if due to numerical precision
758 * the resulting output conic matrix is not considered to be symmetric.
759 * @throws AlgebraException raised if transform cannot be computed because of
760 * numerical instabilities.
761 */
762 @Override
763 public void transform(final Conic inputConic, final Conic outputConic)
764 throws NonSymmetricMatrixException, AlgebraException {
765 // point' * conic * point = 0
766 // point' * T' * transformedConic * T * point = 0
767 // where:
768 // - transformedPoint = T * point
769
770 // Hence:
771 // transformedConic = T^-1' * conic * T^-1
772
773 inputConic.normalize();
774
775 final var c = inputConic.asMatrix();
776 final var invT = inverseAndReturnNew().asMatrix();
777 // normalize transformation matrix invT to increase accuracy
778 var norm = Utils.normF(invT);
779 invT.multiplyByScalar(1.0 / norm);
780
781 final var m = invT.transposeAndReturnNew();
782 try {
783 m.multiply(c);
784 m.multiply(invT);
785 } catch (final WrongSizeException ignore) {
786 // never happens
787 }
788
789 // normalize resulting m matrix to increase accuracy so that it can be
790 // considered symmetric
791 norm = Utils.normF(m);
792 m.multiplyByScalar(1.0 / norm);
793
794 outputConic.setParameters(m);
795 }
796
797 /**
798 * Transforms a dual conic using this transformation and stores the result
799 * into provided output dual conic.
800 *
801 * @param inputDualConic dual conic to be transformed.
802 * @param outputDualConic instance where data of transformed dual conic will
803 * be stored.
804 * @throws NonSymmetricMatrixException raised if due to numerical precision
805 * the resulting output dual conic matrix is not considered to be symmetric.
806 */
807 @Override
808 public void transform(final DualConic inputDualConic, final DualConic outputDualConic)
809 throws NonSymmetricMatrixException {
810 // line' * dualConic * line = 0
811 // line' * T^-1 * T * dualConic * T' * T^-1'* line
812
813 // Hence:
814 // transformed plane: T^-1'* line
815 // transformed dual quadric: T * dualQuadric * T'
816
817 inputDualConic.normalize();
818
819 final var dualC = inputDualConic.asMatrix();
820 final var t = asMatrix();
821 // normalize transformation matrix T to increase accuracy
822 var norm = Utils.normF(t);
823 t.multiplyByScalar(1.0 / norm);
824
825 final var transT = t.transposeAndReturnNew();
826 try {
827 t.multiply(dualC);
828 t.multiply(transT);
829 } catch (final WrongSizeException ignore) {
830 //never happens
831 }
832
833 // normalize resulting m matrix to increase accuracy so that it can be
834 // considered symmetric
835 norm = Utils.normF(t);
836 t.multiplyByScalar(1.0 / norm);
837
838 outputDualConic.setParameters(t);
839 }
840
841 /**
842 * Transforms provided input line using this transformation and stores the
843 * result into provided output line instance.
844 *
845 * @param inputLine line to be transformed.
846 * @param outputLine instance where data of transformed line will be stored.
847 * @throws AlgebraException raised if transform cannot be computed because
848 * of numerical instabilities.
849 */
850 @Override
851 public void transform(final Line2D inputLine, final Line2D outputLine) throws AlgebraException {
852 // line' * point = 0 --> line' * T^-1 * T * point
853 // (line' * T^-1)*(T*point) = (T^-1'*line)'*(T*point)
854 // where:
855 // - transformedLine = T^-1'*line
856 // - transformedPoint = T*point
857
858 inputLine.normalize();
859
860 final var invT = inverseAndReturnNew().asMatrix();
861 final var l = Matrix.newFromArray(inputLine.asArray());
862
863 // normalize transformation matrix T to increase accuracy
864 final var norm = Utils.normF(invT);
865 invT.multiplyByScalar(1.0 / norm);
866
867 invT.transpose();
868 invT.multiply(l);
869
870 outputLine.setParameters(invT.toArray());
871 }
872
873 /**
874 * Inverses this transformation.
875 *
876 * @throws AlgebraException if inverse transform cannot be computed because
877 * of numerical instabilities.
878 */
879 public void inverse() throws AlgebraException {
880 inverse(this);
881 }
882
883 /**
884 * Computes the inverse of this transformation and returns the result as a
885 * new transformation instance.
886 *
887 * @return Inverse transformation.
888 * @throws AlgebraException if inverse transform cannot be computed because
889 * of numerical instabilities.
890 */
891 public Transformation2D inverseAndReturnNew() throws AlgebraException {
892 final var result = new AffineTransformation2D();
893 inverse(result);
894 return result;
895 }
896
897 /**
898 * Computes the inverse of this transformation and stores the result in
899 * provided instance.
900 *
901 * @param result instance where inverse transformation will be stored.
902 * @throws AlgebraException if inverse transform cannot be computed because
903 * of numerical instabilities.
904 */
905 public void inverse(final AffineTransformation2D result) throws AlgebraException {
906
907 // x' = a * x + t -->
908 // a^-1 * x' = a^-1 * a * x + a^-1 * t = x + a^-1 * t -->
909 // x = a^-1 * x' - a^-1 * t
910
911 try {
912 // reverse rotation
913 final var invA = Utils.inverse(a);
914 result.a = invA;
915
916 // reverse translation
917 final var t = Matrix.newFromArray(translation, true);
918 t.multiplyByScalar(-1.0);
919
920 final var resultT = invA.multiplyAndReturnNew(t);
921 result.translation = resultT.toArray();
922 } catch (final WrongSizeException ignore) {
923 // never happens
924 }
925 }
926
927 /**
928 * Converts this transformation into a metric transformation.
929 *
930 * @return This transformation converted into a projective transformation.
931 */
932 public ProjectiveTransformation2D toProjective() {
933 return new ProjectiveTransformation2D(a, translation);
934 }
935
936 /**
937 * Combines this transformation with provided transformation.
938 * The combination is equivalent to multiplying the matrix of this
939 * transformation with the matrix of provided transformation.
940 *
941 * @param transformation Transformation to be combined with.
942 */
943 public void combine(final AffineTransformation2D transformation) {
944 combine(transformation, this);
945 }
946
947 /**
948 * Combines this transformation with provided transformation and returns
949 * the result as a new transformation instance.
950 * The combination is equivalent to multiplying the matrix of this
951 * transformation with the matrix of provided transformation.
952 *
953 * @param transformation Transformation to be combined with
954 * @return a new transformation resulting of the combination with this
955 * transformation and provided transformation.
956 */
957 public AffineTransformation2D combineAndReturnNew(final AffineTransformation2D transformation) {
958
959 final var result = new AffineTransformation2D();
960 combine(transformation, result);
961 return result;
962 }
963
964 /**
965 * Combines this transformation with provided input transformation and
966 * stores the result into provided output transformation.
967 * The combination is equivalent to multiplying the matrix of this
968 * transformation with the matrix of provided input transformation.
969 *
970 * @param inputTransformation transformation to be combined with.
971 * @param outputTransformation transformation where result will be stored.
972 */
973 private void combine(final AffineTransformation2D inputTransformation,
974 final AffineTransformation2D outputTransformation) {
975 // combination in matrix representation is:
976 // [A1 t1] * [A2 t2] = [A1*A2 + t1*0T A1*t2 + t1*1] = [A1*A2 A1*t2 + t1]
977 // [0T 1 ] [0T 1 ] [0T*A2 + 1*0T 0T*t2 + 1*1 ] [0T 1 ]
978
979 try {
980 // we do translation first, because this.rotation might change later
981 final var a1 = new Matrix(this.a);
982 final var t2 = Matrix.newFromArray(inputTransformation.translation, true);
983 // this is R1 * t2
984 a1.multiply(t2);
985
986 ArrayUtils.sum(a1.toArray(), this.translation, outputTransformation.translation);
987
988 outputTransformation.a = this.a.multiplyAndReturnNew(inputTransformation.a);
989
990 } catch (final WrongSizeException ignore) {
991 // never happens
992 }
993 }
994
995 /**
996 * Estimates this transformation internal parameters by using 3
997 * corresponding original and transformed points.
998 *
999 * @param inputPoint1 1st input point.
1000 * @param inputPoint2 2nd input point.
1001 * @param inputPoint3 3rd input point.
1002 * @param outputPoint1 1st transformed point corresponding to 1st input
1003 * point.
1004 * @param outputPoint2 2nd transformed point corresponding to 2nd input
1005 * point.
1006 * @param outputPoint3 3rd transformed point corresponding to 3rd input
1007 * point.
1008 * @throws CoincidentPointsException raised if transformation cannot be
1009 * estimated for some reason (point configuration degeneracy, duplicate
1010 * points or numerical instabilities).
1011 */
1012 public final void setTransformationFromPoints(
1013 final Point2D inputPoint1, final Point2D inputPoint2, final Point2D inputPoint3,
1014 final Point2D outputPoint1, final Point2D outputPoint2, final Point2D outputPoint3)
1015 throws CoincidentPointsException {
1016
1017 // normalize points to increase accuracy
1018 inputPoint1.normalize();
1019 inputPoint2.normalize();
1020 inputPoint3.normalize();
1021
1022 outputPoint1.normalize();
1023 outputPoint2.normalize();
1024 outputPoint3.normalize();
1025
1026 // matrix of homogeneous linear system of equations.
1027 // There are 7 unknowns and 6 equations (2 for each pair of corresponding
1028 // points)
1029 Matrix m = null;
1030 try {
1031 // build matrix initialized to zero
1032 m = new Matrix(6, 7);
1033
1034 // 1st pair of points
1035 var iX = inputPoint1.getHomX();
1036 var iY = inputPoint1.getHomY();
1037 var iW = inputPoint1.getHomW();
1038
1039 var oX = outputPoint1.getHomX();
1040 var oY = outputPoint1.getHomY();
1041 var oW = outputPoint1.getHomW();
1042
1043 var oWiX = oW * iX;
1044 var oWiY = oW * iY;
1045 var oWiW = oW * iW;
1046
1047 var oXiW = oX * iW;
1048 var oYiW = oY * iW;
1049
1050 var tmp = oWiX * oWiX + oWiY * oWiY + oWiW * oWiW;
1051 var norm = Math.sqrt(tmp + oXiW * oXiW);
1052
1053 m.setElementAt(0, 0, oWiX / norm);
1054 m.setElementAt(0, 1, oWiY / norm);
1055 m.setElementAt(0, 4, oWiW / norm);
1056 m.setElementAt(0, 6, -oXiW / norm);
1057
1058 norm = Math.sqrt(tmp + oYiW * oYiW);
1059
1060 m.setElementAt(1, 2, oWiX / norm);
1061 m.setElementAt(1, 3, oWiY / norm);
1062 m.setElementAt(1, 5, oWiW / norm);
1063 m.setElementAt(1, 6, -oYiW / norm);
1064
1065 // 2nd pair of points
1066 iX = inputPoint2.getHomX();
1067 iY = inputPoint2.getHomY();
1068 iW = inputPoint2.getHomW();
1069
1070 oX = outputPoint2.getHomX();
1071 oY = outputPoint2.getHomY();
1072 oW = outputPoint2.getHomW();
1073
1074 oWiX = oW * iX;
1075 oWiY = oW * iY;
1076 oWiW = oW * iW;
1077
1078 oXiW = oX * iW;
1079 oYiW = oY * iW;
1080
1081 tmp = oWiX * oWiX + oWiY * oWiY + oWiW * oWiW;
1082 norm = Math.sqrt(tmp + oXiW * oXiW);
1083
1084 m.setElementAt(2, 0, oWiX / norm);
1085 m.setElementAt(2, 1, oWiY / norm);
1086 m.setElementAt(2, 4, oWiW / norm);
1087 m.setElementAt(2, 6, -oXiW / norm);
1088
1089 norm = Math.sqrt(tmp + oYiW * oYiW);
1090
1091 m.setElementAt(3, 2, oWiX / norm);
1092 m.setElementAt(3, 3, oWiY / norm);
1093 m.setElementAt(3, 5, oWiW / norm);
1094 m.setElementAt(3, 6, -oYiW / norm);
1095
1096 // 3rd pair of points
1097 iX = inputPoint3.getHomX();
1098 iY = inputPoint3.getHomY();
1099 iW = inputPoint3.getHomW();
1100
1101 oX = outputPoint3.getHomX();
1102 oY = outputPoint3.getHomY();
1103 oW = outputPoint3.getHomW();
1104
1105 oWiX = oW * iX;
1106 oWiY = oW * iY;
1107 oWiW = oW * iW;
1108
1109 oXiW = oX * iW;
1110 oYiW = oY * iW;
1111
1112 tmp = oWiX * oWiX + oWiY * oWiY + oWiW * oWiW;
1113 norm = Math.sqrt(tmp + oXiW * oXiW);
1114
1115 m.setElementAt(4, 0, oWiX / norm);
1116 m.setElementAt(4, 1, oWiY / norm);
1117 m.setElementAt(4, 4, oWiW / norm);
1118 m.setElementAt(4, 6, -oXiW / norm);
1119
1120 norm = Math.sqrt(tmp + oYiW * oYiW);
1121
1122 m.setElementAt(5, 2, oWiX / norm);
1123 m.setElementAt(5, 3, oWiY / norm);
1124 m.setElementAt(5, 5, oWiW / norm);
1125 m.setElementAt(5, 6, -oYiW / norm);
1126 } catch (final WrongSizeException ignore) {
1127 // never happens
1128 }
1129
1130 // use SVD to decompose matrix m
1131 Matrix v;
1132 try {
1133 final var decomposer = new SingularValueDecomposer(m);
1134 decomposer.decompose();
1135
1136 // ensure that matrix m has enough rank and there is a unique
1137 // solution (up to scale)
1138 if (decomposer.getRank() < 6) {
1139 throw new CoincidentPointsException();
1140 }
1141 // V is 7x7
1142 v = decomposer.getV();
1143
1144 // last column of V will contain parameters of transformation
1145 final var value = v.getElementAt(6, 6);
1146 a.setElementAt(0, 0, v.getElementAt(0, 6) / value);
1147 a.setElementAt(0, 1, v.getElementAt(1, 6) / value);
1148 a.setElementAt(1, 0, v.getElementAt(2, 6) / value);
1149 a.setElementAt(1, 1, v.getElementAt(3, 6) / value);
1150
1151 translation[0] = v.getElementAt(4, 6) / value;
1152 translation[1] = v.getElementAt(5, 6) / value;
1153
1154 } catch (final AlgebraException e) {
1155 throw new CoincidentPointsException(e);
1156 }
1157 }
1158
1159 /**
1160 * Estimates this transformation internal parameters by using 3
1161 * corresponding original and transformed lines.
1162 *
1163 * @param inputLine1 1st input line.
1164 * @param inputLine2 2nd input line.
1165 * @param inputLine3 3rd input line.
1166 * @param outputLine1 1st transformed line corresponding to 1st input
1167 * line.
1168 * @param outputLine2 2nd transformed line corresponding to 2nd input
1169 * line.
1170 * @param outputLine3 3rd transformed line corresponding to 3rd input
1171 * line.
1172 * @throws CoincidentLinesException raised if transformation cannot be
1173 * estimated for some reason (line configuration degeneracy, duplicate
1174 * lines or numerical instabilities).
1175 */
1176 public final void setTransformationFromLines(
1177 final Line2D inputLine1, final Line2D inputLine2,
1178 final Line2D inputLine3, final Line2D outputLine1,
1179 final Line2D outputLine2, final Line2D outputLine3)
1180 throws CoincidentLinesException {
1181
1182 // normalize points to increase accuracy
1183 inputLine1.normalize();
1184 inputLine2.normalize();
1185 inputLine3.normalize();
1186
1187 outputLine1.normalize();
1188 outputLine2.normalize();
1189 outputLine3.normalize();
1190
1191 // matrix of homogeneous linear system of equations.
1192 // There are 7 unknowns and 6 equations (2 for each pair of corresponding
1193 // points)
1194 Matrix m = null;
1195 try {
1196 // build matrix initialized to zero
1197 m = new Matrix(6, 7);
1198
1199 // 1st pair of lines
1200 var iA = inputLine1.getA();
1201 var iB = inputLine1.getB();
1202 var iC = inputLine1.getC();
1203
1204 var oA = outputLine1.getA();
1205 var oB = outputLine1.getB();
1206 var oC = outputLine1.getC();
1207
1208 var oCiA = oC * iA;
1209 var oCiB = oC * iB;
1210
1211 var oAiA = oA * iA;
1212 var oAiB = oA * iB;
1213 var oAiC = oA * iC;
1214
1215 var oBiA = oB * iA;
1216 var oBiB = oB * iB;
1217 var oBiC = oB * iC;
1218
1219 var tmp = oCiA * oCiA + oCiB * oCiB;
1220 var norm = Math.sqrt(tmp + oAiA * oAiA + oAiB * oAiB + oAiC * oAiC);
1221
1222 m.setElementAt(0, 0, oCiA / norm);
1223 m.setElementAt(0, 1, oCiB / norm);
1224 m.setElementAt(0, 4, -oAiA / norm);
1225 m.setElementAt(0, 5, -oAiB / norm);
1226 m.setElementAt(0, 6, -oAiC / norm);
1227
1228 norm = Math.sqrt(tmp + oBiA * oBiA + oBiB * oBiB + oBiC * oBiC);
1229
1230 m.setElementAt(1, 2, oCiA / norm);
1231 m.setElementAt(1, 3, oCiB / norm);
1232 m.setElementAt(1, 4, -oBiA / norm);
1233 m.setElementAt(1, 5, -oBiB / norm);
1234 m.setElementAt(1, 6, -oBiC / norm);
1235
1236 // 2nd pair of lines
1237 iA = inputLine2.getA();
1238 iB = inputLine2.getB();
1239 iC = inputLine2.getC();
1240
1241 oA = outputLine2.getA();
1242 oB = outputLine2.getB();
1243 oC = outputLine2.getC();
1244
1245 oCiA = oC * iA;
1246 oCiB = oC * iB;
1247
1248 oAiA = oA * iA;
1249 oAiB = oA * iB;
1250 oAiC = oA * iC;
1251
1252 oBiA = oB * iA;
1253 oBiB = oB * iB;
1254 oBiC = oB * iC;
1255
1256 tmp = oCiA * oCiA + oCiB * oCiB;
1257 norm = Math.sqrt(tmp + oAiA * oAiA + oAiB * oAiB + oAiC * oAiC);
1258
1259 m.setElementAt(2, 0, oCiA / norm);
1260 m.setElementAt(2, 1, oCiB / norm);
1261 m.setElementAt(2, 4, -oAiA / norm);
1262 m.setElementAt(2, 5, -oAiB / norm);
1263 m.setElementAt(2, 6, -oAiC / norm);
1264
1265 norm = Math.sqrt(tmp + oBiA * oBiA + oBiB * oBiB + oBiC * oBiC);
1266
1267 m.setElementAt(3, 2, oCiA / norm);
1268 m.setElementAt(3, 3, oCiB / norm);
1269 m.setElementAt(3, 4, -oBiA / norm);
1270 m.setElementAt(3, 5, -oBiB / norm);
1271 m.setElementAt(3, 6, -oBiC / norm);
1272
1273 // 3rd pair of lines
1274 iA = inputLine3.getA();
1275 iB = inputLine3.getB();
1276 iC = inputLine3.getC();
1277
1278 oA = outputLine3.getA();
1279 oB = outputLine3.getB();
1280 oC = outputLine3.getC();
1281
1282 oCiA = oC * iA;
1283 oCiB = oC * iB;
1284
1285 oAiA = oA * iA;
1286 oAiB = oA * iB;
1287 oAiC = oA * iC;
1288
1289 oBiA = oB * iA;
1290 oBiB = oB * iB;
1291 oBiC = oB * iC;
1292
1293 tmp = oCiA * oCiA + oCiB * oCiB;
1294 norm = Math.sqrt(tmp + oAiA * oAiA + oAiB * oAiB + oAiC * oAiC);
1295
1296 m.setElementAt(4, 0, oCiA / norm);
1297 m.setElementAt(4, 1, oCiB / norm);
1298 m.setElementAt(4, 4, -oAiA / norm);
1299 m.setElementAt(4, 5, -oAiB / norm);
1300 m.setElementAt(4, 6, -oAiC / norm);
1301
1302 norm = Math.sqrt(tmp + oBiA * oBiA + oBiB * oBiB + oBiC * oBiC);
1303
1304 m.setElementAt(5, 2, oCiA / norm);
1305 m.setElementAt(5, 3, oCiB / norm);
1306 m.setElementAt(5, 4, -oBiA / norm);
1307 m.setElementAt(5, 5, -oBiB / norm);
1308 m.setElementAt(5, 6, -oBiC / norm);
1309 } catch (final WrongSizeException ignore) {
1310 // never happens
1311 }
1312
1313 // use SVD to decompose matrix m
1314 Matrix v;
1315 try {
1316 final var decomposer = new SingularValueDecomposer(m);
1317 decomposer.decompose();
1318
1319 // ensure that matrix m has enough rank and there is a unique
1320 // solution (up to scale)
1321 if (decomposer.getRank() < 6) {
1322 throw new CoincidentLinesException();
1323 }
1324 // V is 7x7
1325 v = decomposer.getV();
1326
1327 // last column of V will contain parameters of transformation
1328 final var value = v.getElementAt(6, 6);
1329
1330 final var invTransA = new Matrix(AffineParameters2D.INHOM_COORDS, AffineParameters2D.INHOM_COORDS);
1331 // copy former 4 elements of 7th column of V into a in row order
1332 invTransA.setSubmatrix(0, 0, 1, 1,
1333 v.getSubmatrixAsArray(0, 6, 3, 6),
1334 false);
1335 // normalize by scale value
1336 invTransA.multiplyByScalar(1.0 / value);
1337
1338 // initially a contains the inverse of its transpose, so to obtain a we need
1339 // to transpose it and invert it
1340 invTransA.transpose();
1341 final var a2 = Utils.inverse(invTransA);
1342
1343 final var invt = new Matrix(1, 2);
1344 invt.setSubmatrix(0, 0, 0, 1,
1345 v.getSubmatrixAsArray(4, 6, 5, 6),
1346 false);
1347 // normalize by scale value (we need to change sign as well)
1348 invt.multiplyByScalar(-1.0 / value);
1349 invt.transpose();
1350
1351 final var t = a2.multiplyAndReturnNew(invt);
1352
1353 this.a = a2;
1354 this.translation = t.getBuffer();
1355 } catch (final AlgebraException e) {
1356 throw new CoincidentLinesException(e);
1357 }
1358 }
1359 }