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.geometry.mappings.linear; 011 012import imagingbook.lib.settings.PrintPrecision; 013import imagingbook.pub.geometry.mappings.WarpParameters; 014 015import java.awt.geom.Point2D; 016 017import org.apache.commons.math3.linear.DecompositionSolver; 018import org.apache.commons.math3.linear.MatrixUtils; 019import org.apache.commons.math3.linear.RealMatrix; 020import org.apache.commons.math3.linear.RealVector; 021import org.apache.commons.math3.linear.SingularValueDecomposition; 022 023 024/** 025 * 2015-02-16: Added preliminary constructor for more than 4 point pairs 026 * (overdetermined, least-squares). 027 * 028 * @author WB 029 * 030 */ 031public class ProjectiveMapping extends LinearMapping implements WarpParameters { 032 033 034 public static ProjectiveMapping makeMapping (Point2D[] P, Point2D[] Q) { 035 int minLen = Math.min(P.length, Q.length); 036 if (minLen < 1) { 037 throw new IllegalArgumentException("cannot create a mapping from zero points"); 038 } 039 else if (minLen <= 2) { // TODO: special case for minLen == 2? 040 return new Translation(P, Q); 041 } 042 else if (minLen <= 3) { 043 return new AffineMapping(P, Q); 044 } 045 else { 046 return new ProjectiveMapping(P, Q); 047 } 048 } 049 050 // creates the identity mapping: 051 public ProjectiveMapping() { 052 super(); 053 } 054 055 public ProjectiveMapping( 056 double a00, double a01, double a02, 057 double a10, double a11, double a12, 058 double a20, double a21, 059 boolean inv) { 060 super(a00, a01, a02, a10, a11, a12, a20, a21, 1, inv); 061 } 062 063 public ProjectiveMapping(LinearMapping lm) { 064 super(lm); 065 //this.normalize(); // needed?? 066 } 067 068 // creates the projective mapping from the unit square S to 069 // the arbitrary quadrilateral P given by points p0,...,p3: 070 public ProjectiveMapping(Point2D p0, Point2D p1, Point2D p2, Point2D p3) { 071 super(); 072 double x0 = p0.getX(), x1 = p1.getX(), x2 = p2.getX(), x3 = p3.getX(); 073 double y0 = p0.getY(), y1 = p1.getY(), y2 = p2.getY(), y3 = p3.getY(); 074 double S = (x1-x2)*(y3-y2) - (x3-x2)*(y1-y2); 075 // TODO: check S for zero value and throw exception 076 a20 = ((x0-x1+x2-x3)*(y3-y2)-(y0-y1+y2-y3)*(x3-x2)) / S; 077 a21 = ((y0-y1+y2-y3)*(x1-x2)-(x0-x1+x2-x3)*(y1-y2)) / S; 078 a00 = x1 - x0 + a20*x1; 079 a01 = x3 - x0 + a21*x3; 080 a02 = x0; 081 a10 = y1 - y0 + a20*y1; 082 a11 = y3 - y0 + a21*y3; 083 a12 = y0; 084 } 085 086 087 /** 088 * Creates a new {@link ProjectiveMapping} between arbitrary quadrilaterals P, Q. 089 * @param p0 point 1 of source quad P. 090 * @param p1 point 2 of source quad P. 091 * @param p2 point 3 of source quad P. 092 * @param p3 point 4 of source quad P. 093 * @param q0 point 1 of target quad Q. 094 * @param q1 point 2 of target quad Q. 095 * @param q2 point 3 of target quad Q. 096 * @param q3 point 4 of target quad Q. 097 */ 098 public ProjectiveMapping( 099 Point2D p0, Point2D p1, Point2D p2, Point2D p3, 100 Point2D q0, Point2D q1, Point2D q2, Point2D q3) { 101 super(); // initialized to identity 102 ProjectiveMapping T1 = new ProjectiveMapping(p0, p1, p2, p3); 103 ProjectiveMapping T2 = new ProjectiveMapping(q0, q1, q2, q3); 104 ProjectiveMapping T1i = T1.invert(); 105 ProjectiveMapping T12 = T1i.concat(T2); 106 this.concatDestructive(T12); // transfer T12 -> this 107 } 108 109 /** 110 * Creates a new {@link ProjectiveMapping} between arbitrary quadrilaterals P, Q. 111 * @param P source quad. 112 * @param Q target quad. 113 */ 114 public ProjectiveMapping(Point2D[] P, Point2D[] Q) { 115 this(P[0], P[1], P[2], P[3], Q[0], Q[1], Q[2], Q[3]); 116 } 117 118 /** 119 * Constructor for more than 4 point pairs, finds a least-squares solution 120 * for the homography parameters. 121 * NOTE: this is UNFINISHED code! 122 * @param P sequence of points (source) 123 * @param Q sequence of points (target) 124 * @param dummy unused (only to avoid duplicate signature) 125 */ 126 public ProjectiveMapping(Point2D[] P, Point2D[] Q, boolean dummy) { 127 final int n = P.length; 128 double[] ba = new double[2 * n]; 129 double[][] Ma = new double[2 * n][]; 130 for (int i = 0; i < n; i++) { 131 double x = P[i].getX(); 132 double y = P[i].getY(); 133 double u = Q[i].getX(); 134 double v = Q[i].getY(); 135 ba[2 * i + 0] = u; 136 ba[2 * i + 1] = v; 137 Ma[2 * i + 0] = new double[] { x, y, 1, 0, 0, 0, -u * x, -u * y }; 138 Ma[2 * i + 1] = new double[] { 0, 0, 0, x, y, 1, -v * x, -v * y }; 139 } 140 141 RealMatrix M = MatrixUtils.createRealMatrix(Ma); 142 RealVector b = MatrixUtils.createRealVector(ba); 143 DecompositionSolver solver = new SingularValueDecomposition(M).getSolver(); 144 RealVector h = solver.solve(b); 145 a00 = h.getEntry(0); 146 a01 = h.getEntry(1); 147 a02 = h.getEntry(2); 148 a10 = h.getEntry(3); 149 a11 = h.getEntry(4); 150 a12 = h.getEntry(5); 151 a20 = h.getEntry(6); 152 a21 = h.getEntry(7); 153 a22 = 1; 154 } 155 156 // ----------------------------------------------------------- 157 158 159 public ProjectiveMapping concat(ProjectiveMapping B) { 160 ProjectiveMapping A = new ProjectiveMapping(this); 161 A.concatDestructive(B); 162 return A; 163 } 164 165 public ProjectiveMapping invert() { 166 ProjectiveMapping pm = new ProjectiveMapping(this); 167 pm.invertDestructive(); 168 return pm; 169 } 170 171 void normalize() { 172 // scales the matrix such that a22 becomes 1 173 // TODO: check a22 for zero value and throw exception 174 a00 = a00/a22; a01 = a01/a22; a02 = a02/a22; 175 a10 = a10/a22; a11 = a11/a22; a12 = a12/a22; 176 a20 = a20/a22; a21 = a21/a22; a22 = 1; 177 } 178 179 @Override 180 public ProjectiveMapping duplicate() { 181 return (ProjectiveMapping) this.clone(); 182 } 183 184 // Warp parameter support ------------------------------------- 185 186 public int getWarpParameterCount() { 187 return 8; 188 } 189 190 public double[] getWarpParameters() { 191 double[] p = new double[] { 192 a00 - 1, a01, 193 a10, a11 - 1, 194 a20, a21, 195 a02, a12, 196 }; 197 return p; 198 } 199 200// p[0] = M3x3[0][0] - 1; // = a 201// p[1] = M3x3[0][1]; // = b 202// p[2] = M3x3[1][0]; // = c 203// p[3] = M3x3[1][1] - 1; // = d 204// p[4] = M3x3[2][0]; // = e 205// p[5] = M3x3[2][1]; // = f 206// p[6] = M3x3[0][2]; // = tx 207// p[7] = M3x3[1][2]; // = ty 208 209 210 public void setWarpParameters(double[] p) { 211 a00 = p[0] + 1; a01 = p[1]; a02 = p[6]; 212 a10 = p[2]; a11 = p[3] + 1; a12 = p[7]; 213 a20 = p[4]; a21 = p[5]; a22 = 1; 214 } 215 216 217 public double[][] getWarpJacobian(double[] xy) { 218 // see Baker 2003 "20 Years" Part 1, Eq. 99 (p. 46) 219 final double x = xy[0]; 220 final double y = xy[1]; 221 double a = a00 * x + a01 * y + a02; // = alpha 222 double b = a10 * x + a11 * y + a12; // = beta 223 double c = a20 * x + a21 * y + 1; // = gamma 224 double cc = c * c; 225 // TODO: check c for zero-value and throw exception, make more efficient 226 return new double[][] 227 {{x/c, y/c, 0, 0, -(x*a)/cc, -(y*a)/cc, 1/c, 0 }, 228 {0, 0, x/c, y/c, -(x*b)/cc, -(y*b)/cc, 0, 1/c}}; 229 } 230 231 // for testing only ----------------------------------------------------------------- 232 233 public static void main(String[] args) { 234 PrintPrecision.set(6); 235 236 // book example: 237 Point2D[] A = new Point2D[] { 238 new Point2D.Double(2,5), 239 new Point2D.Double(4,6), 240 new Point2D.Double(7,9), 241 new Point2D.Double(5,9), 242 new Point2D.Double(5.2,9.1) // 5 points, overdetermined! 243 }; 244 245 Point2D[] B = new Point2D[] { 246 new Point2D.Double(4,3), 247 new Point2D.Double(5,2), 248 new Point2D.Double(9,3), 249 new Point2D.Double(7,5), 250 new Point2D.Double(7,4.9) // 5 points, overdetermined! 251 }; 252 253 ProjectiveMapping pm = new ProjectiveMapping(A, B, true); 254 255 System.out.println("\nprojective mapping = " + pm.toString()); 256 257 for (int i = 0; i < A.length; i++) { 258 Point2D Bi = pm.applyTo(A[i]); 259 System.out.println(A[i].toString() + " -> " + Bi.toString()); 260 } 261 262 System.out.println(); 263 ProjectiveMapping pmi = pm.invert(); 264 System.out.println("\ninverse projective mapping = " + pmi.toString()); 265 for (int i = 0; i < B.length; i++) { 266 Point2D Ai = pmi.applyTo(B[i]); 267 System.out.println(B[i].toString() + " -> " + Ai.toString()); 268 } 269 } 270 271 272}