// Copyright 2003 David Hilvert , // /* This file is part of the Anti-Lamenessing Engine. The Anti-Lamenessing Engine is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. The Anti-Lamenessing Engine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with the Anti-Lamenessing Engine; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * d3/align.h: Handle alignment of view pyramids. * * XXX: this class assumes that the view volume is a right rectangular-based * pyramid, which holds for most photographic situations. * * XXX: this class assumes that the horizontal and vertical angles of the view * volume pyramid are proportional to the horizontal and vertical pixel * resolutions. */ #ifndef __d3align_h__ #define __d3align_h__ #include "point.h" #include "et.h" #include "pt.h" #include "tfile.h" class align { static ale_pos _init_angle; static et *alignment_array; static int _vp_adjust; static int _vo_adjust; static tload_t *tload; static tsave_t *tsave; /* * Estimate the point at which the pyramidal axis passes through the * plane, based on the given 2-dimensional transformation and the given * point set indicating the corners of the quadrilateral of * intersection. (Both transformation and point set alone are * sufficient for calculation.) * * Using the point set, the desired point is exactly the point of * intersection between line segments drawn between opposite corners of * the quadrilateral of intersection. The algebraic details of this * calculation are desribed below. * * If we represent the desired point by (e1, e2), and the four corners * of the quadrilateral of intersection by (a1, a2), ..., (d1, d2) in * clockwise (or counter-clockwise) order, then we get the two-equation * vector system (implicitly four equations) * * (e1, e2) = (a1, a2) + x[(c1, c2) - (a1, a2)] * = (b1, b2) + y[(d1, d2) - (b1, b2)] * * Solving for x in terms of y, we get the two-equation system * * x = (b1 - yb1 + yd1 - a1) / (c1 - a1) * = (b2 - yb2 + yd2 - a2) / (c2 - a2) * * And * * y = (c1b2 - c1a2 - a1b2 - c2b1 + c2a1 - a2b1) * /(-c2b1 + c2d1 + a2b1 - a2d1 + c1b2 - c1d2 - a1b2 + a1d2) * * However, it's much easier just to project the center point of the * source image onto the quadrilateral of intersection using the * given 2-dimensional transformation. */ static d2::point axis_intersection(d2::point a, d2::point b, d2::point c, d2::point d, d2::transformation t) { #if 0 ale_pos y = (c[0]*b[1] - c[0]*a[1] - a[0]*b[1] - c[1]*b[0] + c[1]*a[0] - a[1]*b[0]) /(-c[1]*b[0] + c[1]*d[0] + a[1]*b[0] - a[1]*d[0] + c[0]*b[1] - c[0]*d[1] - a[0]*b[1] + a[0]*d[1]); return b + y * (d - b); #else return t.transform_scaled(d2::point((t.scaled_height() - 1) / 2, (t.scaled_width() - 1) / 2)); #endif } /* * gd_position is a gradient-descent automatic positioning algorithm * used to determine the location of the view pyramid apex. * * Starting from an initial camera position ESTIMATE, this method * determines the angles between each pair of legs in the view volume * (there are six pairs). The sum of squares of the differences * between the thusly determined angles and the known desired angles is * then used as an error metric, and a kind of gradient descent is used * to find a position for which this metric is minimized. */ static point gd_position(point estimate, d2::point a, d2::point b, d2::point c, d2::point d, d2::transformation t) { ale_pos w = t.scaled_width(); ale_pos h = t.scaled_height(); /* * The desired diagonal angle is given (as init_angle). * Calculate the desired side angles as follows: * * The distance to the apex is * * D = sqrt(h*h + w*w) / (2 * tan(init_angle/2)) * * Then * * desired_h_angle = 2 * arctan(h / (2 * sqrt(D*D + w*w/4))) * desired_w_angle = 2 * arctan(w / (2 * sqrt(D*D + h*h/4))) */ ale_pos D = (ale_pos) sqrt(h * h + w * w) / (ale_pos) (2 * tan(_init_angle/2)); ale_pos desired_h_angle = (double) (2 * atan((double) ((double) h / (2 * sqrt((double) (D*D) + (double) (w*w)/4))))); ale_pos desired_w_angle = (double) (2 * atan((double) ((double) w / (2 * sqrt((double) (D*D) + (double) (h*h)/4))))); ale_pos estimate_h1_angle = estimate.anglebetw(a, b); ale_pos estimate_h2_angle = estimate.anglebetw(c, d); ale_pos estimate_w1_angle = estimate.anglebetw(a, d); ale_pos estimate_w2_angle = estimate.anglebetw(b, c); ale_pos estimate_d1_angle = estimate.anglebetw(a, c); ale_pos estimate_d2_angle = estimate.anglebetw(b, d); ale_pos error = sqrt(pow(estimate_h1_angle - desired_h_angle, 2) + pow(estimate_h2_angle - desired_h_angle, 2) + pow(estimate_w1_angle - desired_w_angle, 2) + pow(estimate_w2_angle - desired_w_angle, 2) + pow(estimate_d1_angle - _init_angle , 2) + pow(estimate_d2_angle - _init_angle , 2)); /* * Vary the magnitude by which each coordinate of the position * can be changed at each step. */ ale_pos view_angle = _init_angle; for (ale_pos magnitude = estimate[2] / 2; magnitude >= 1; magnitude /= 2) { /* * Continue searching at this magnitude while error < * old_error. (Initialize old_error accordingly.) */ ale_pos old_error = error * 2; while(old_error > error) { // ale_pos D = sqrt(h * h + w * w) // / (2 * tan(view_angle/2)); // ale_pos desired_h_angle = 2 * atan(h / (2 * sqrt(D*D + w*w/4))); // ale_pos desired_w_angle = 2 * atan(w / (2 * sqrt(D*D + h*h/4))); // fprintf(stderr, "."); // fprintf(stderr, "estimate: [%f %f %f %f %f %f]\n", // estimate_h1_angle, // estimate_h2_angle, // estimate_w1_angle, // estimate_w2_angle, // estimate_d1_angle, // estimate_d2_angle); // // fprintf(stderr, "desired : [%f %f %f]\n", // desired_h_angle, // desired_w_angle, // view_angle); old_error = error; for (ale_pos c0 = -magnitude; c0 <= magnitude; c0 += magnitude) for (ale_pos c1 = -magnitude; c1 <= magnitude; c1 += magnitude) for (ale_pos c2 = -magnitude; c2 <= magnitude; c2 += magnitude) for (ale_pos c3 = -magnitude; c3 <= magnitude; c3 += magnitude) { if (c3 > 10) c3 = 10; // fprintf(stderr, "[%f %f %f]\n", c0, c1, c2); estimate[0] += c0; estimate[1] += c1; estimate[2] += c2; // view_angle += c3 / 30; ale_pos D = (ale_pos) sqrt(h * h + w * w) / (ale_pos) (2 * tan(view_angle/2)); ale_pos desired_h_angle = 2 * atan((double) h / (2 * sqrt((double) (D*D) + (double) (w*w)/4))); ale_pos desired_w_angle = 2 * atan((double) w / (2 * sqrt((double) D*D + (double) h*h/4))); estimate_h1_angle = estimate.anglebetw(a, b); estimate_h2_angle = estimate.anglebetw(c, d); estimate_w1_angle = estimate.anglebetw(a, d); estimate_w2_angle = estimate.anglebetw(b, c); estimate_d1_angle = estimate.anglebetw(a, c); estimate_d2_angle = estimate.anglebetw(b, d); ale_pos perturbed_error = sqrt(pow(estimate_h1_angle - desired_h_angle, 2) + pow(estimate_h2_angle - desired_h_angle, 2) + pow(estimate_w1_angle - desired_w_angle, 2) + pow(estimate_w2_angle - desired_w_angle, 2) + pow(estimate_d1_angle - view_angle , 2) + pow(estimate_d2_angle - view_angle , 2)); if (perturbed_error < error) { error = perturbed_error; } else { estimate[0] -= c0; estimate[1] -= c1; estimate[2] -= c2; // view_angle -= c3 / 30; } } } } // fprintf(stderr, "error %f\n", error); return estimate; } public: /* * Get alignment for frame N. */ static et of(unsigned int n) { assert (n < d2::image_rw::count()); assert (alignment_array); return alignment_array[n]; } static double angle_of(unsigned int n) { return _init_angle; } static pt projective(unsigned int n) { return pt(d2::align::of(n), of(n), angle_of(n)); } static void set_tload(tload_t *t) { tload = t; } static void set_tsave(tsave_t *t) { tsave = t; } static void write_alignments() { int count = d2::image_rw::count(); if (count > 0) tsave_first(tsave, projective(0)); for (int i = 1; i < count; i++) { tsave_next(tsave, projective(i)); } } static void vp_adjust() { _vp_adjust = 1; } static void vp_noadjust() { _vp_adjust = 0; } static void vo_adjust() { _vo_adjust = 1; } static void vo_noadjust() { _vo_adjust = 0; } /* * Set the initial estimated diagonal viewing angle of the view * pyramid. This is the angle formed, e.g., between the top-left and * bottom-right legs of the view pyramid. */ static void init_angle(ale_pos a) { _init_angle = a; } /* * Initialize d3 transformations from d2 transformations. * * Here are three possible approaches for calculating camera position * based on a known diagonal viewing angle and known area of * intersection of the view volume with a fixed plane: * * (1) divide the rectangular-based pyramidal view volume into two * tetrahedra. A coordinate system is selected so that the triangle of * intersection between one of the tetrahedra and the fixed plane has * coordinates (0, 0, 0), (1, 0, 0), and (a, b, 0), for some (a, b). * The law of cosines is then used to derive three equations * associating the three known angles at the apex with the lengths of * the edges of the tetrahedron. The solution of this system of * equations gives the coordinates of the apex in terms of a, b, and * the known angles. These coordinates are then transformed back to * the original coordinate system. * * (2) The gradient descent approach taken by the method gd_position(). * * (3) Assume that the camera is aimed (roughly) perpendicular to the * plane. In this case, the pyramidal axis forms with each adjacent * pyramidal edge, in combination with a segment in the plane, a right * triangle. The distance of the camera from the plane is d/(2 * * tan(theta/2)), where d is the distance between opposite corners of * the quadrilateral of intersection and theta is the angle between * opposite pairs of edges of the view volume. * * As an initial approach to the problem, we use (3) followed by (2). * * After position is estimated, we determine orientation from position. * In order to do this, we determine the point at which the axis of * the view pyramid passes through the plane, as described in the * method axis_intersection(). */ static void init_from_d2() { assert (alignment_array == NULL); /* * Initialize the alignment array. */ alignment_array = (et *) malloc (d2::image_rw::count() * sizeof(et)); assert (alignment_array); if (!alignment_array) { fprintf(stderr, "\n\n*** Unable to allocate memory for 3D alignment array. ***\n\n"); exit(1); } /* * Iterate over input frames */ for (unsigned int i = 0; i < d2::image_rw::count(); i++) { /* * Obtain the four mapped corners of the quadrilateral * of intersection. */ d2::transformation t = d2::align::of(i); d2::point a = t.transform_scaled(d2::point(0, 0)); d2::point b = t.transform_scaled(d2::point(t.scaled_height() - 1, 0)); d2::point c = t.transform_scaled(d2::point(t.scaled_height() - 1, t.scaled_width() - 1)); d2::point d = t.transform_scaled(d2::point(0, t.scaled_width() - 1)); /* * Estimate the point at which the pyramidal axis * passes through the plane. We denote the point as * 'e'. */ d2::point e = axis_intersection(a, b, c, d, t); /* * Determine the average distance between opposite * corners, and calculate the distance to the camera * based on method (3) described above. */ ale_pos dist1 = sqrt((a[0] - c[0]) * (a[0] - c[0]) + (a[1] - c[1]) * (a[1] - c[1])); ale_pos dist2 = sqrt((b[0] - d[0]) * (b[0] - d[0]) + (b[1] - d[1]) * (b[1] - d[1])); ale_pos avg_dist = (dist1 + dist2) / 2; ale_pos tangent = tan(_init_angle / 2); ale_pos distance = avg_dist / (2 * tangent); /* * Set the position of the camera based on the estimate * from method (3). This is used as the initial * position for method (2). We assume that the camera * is placed so that its 0 and 1 coordinates match those * of e. */ point estimate; estimate[0] = e[0]; estimate[1] = e[1]; estimate[2] = distance; // fprintf(stderr, "position (n=%d) %f %f %f\n", i, estimate[0], estimate[1], estimate[2]); /* * Perform method (2). */ estimate = gd_position(estimate, a, b, c, d, t); // fprintf(stderr, "."); /* * Assign transformation values based on the output of * method (2), by modifying transformation parameters * from the identity transformation. */ alignment_array[i] = et::identity(); // fprintf(stderr, "position (n=%d) %f %f %f\n", i, estimate[0], estimate[1], estimate[2]); /* * Modify translation values, if desired. */ if (_vp_adjust) { alignment_array[i].modify_translation(0, -estimate[0]); alignment_array[i].modify_translation(1, -estimate[1]); alignment_array[i].modify_translation(2, -estimate[2]); } else { /* * Establish a default translation value. */ alignment_array[i].modify_translation(2, -1); } /* * Load any transformations. */ if (i == 0) { int is_default; pt p = tload_first(tload, projective(i), &is_default); alignment_array[i] = p.e(); _init_angle = p.view_angle(); } else { int is_default; pt p = tload_next(tload, projective(i), &is_default); alignment_array[i] = p.e(); if (p.view_angle() != _init_angle) { fprintf(stderr, "Error: variation in view angle is not currently supported.\n"); exit(1); } } /* * Assert that the z-axis translation is nonzero. This * is important for determining rotation values. */ assert (estimate[2] != 0); /* * Check whether rotation adjustment must proceed. */ if (!_vo_adjust) { continue; } /* * Modify rotation values * * The axis of the view pyramid should be mapped onto * the z-axis. Hence, the point of intersection * between the world-coordinates xy-plane and the axis * of the pyramid should occur at (0, 0) in local x-y * coordinates. * * If we temporarily assume no rotation about the * z-axis (e2 == 0), then the Euler angles (e0, e1, e2) * used to determine rotation give us: * * x' = x cos e1 - (z cos e0 - y sin e0) sin e1 * y' = y cos e0 + z sin e0 * z' = (z cos e0 - y sin e0) cos e1 + x sin e1 * * Setting x' = y' = 0, we get: * * e0 = arctan (-y/z) * e1 = arctan (x/(z cos e0 - y sin e0)) * [optionally add pi to achieve z' < 0] * * To set e2, taking T to be the 3D transformation as a * function of e2, we first ensure that * * tan(T(e2, a)[1] / T(e2, a)[0]) * == * tan(width / height) * * by assigning * * e2 += tan(width / height) * - tan(T(e2, a)[1] / T(e2, a)[0]) * * Once this condition is satisfied, we conditionally * assign * * e2 += pi * * to ensure that * * T(e2, a)[0] < 0 * T(e2, a)[1] < 0 * * which places the transformed point in the lower-left * quadrant. This is correct, since point A is mapped * from (0, 0), the most negative point in the image. */ point e_translated = alignment_array[i](e); // fprintf(stderr, "axis-intersection (n=%d) e1 %f e0 %f e_t1 %f e_t0 %f e_t2 %f\n", // i, // e[1], // e[0], // alignment_array[i](e)[1], // alignment_array[i](e)[0], // alignment_array[i](e)[2]); // // fprintf(stderr, "camera origin (n=%d) o1 %f o0 %f o2 %f o_t1 %f o_t0 %f o_t2 %f\n", // i, // estimate[1], // estimate[0], // estimate[2], // alignment_array[i](estimate)[1], // alignment_array[i](estimate)[0], // alignment_array[i](estimate)[2]); ale_pos e0 = atan(-e_translated[1] / e_translated[2]); ale_pos e1 = atan((double) e_translated[0] / ((double) e_translated[2] * cos(e0) - (double) e_translated[1] * sin(e0))); alignment_array[i].modify_rotation(0, e0); alignment_array[i].modify_rotation(1, e1); if (alignment_array[i](e)[2] > 0) alignment_array[i].modify_rotation(1, M_PI); // fprintf(stderr, "axis-intersection (n=%d) e1 %f e0 %f e_t1 %f e_t0 %f e_t2 %f\n", // i, // e[1], // e[0], // alignment_array[i](e)[1], // alignment_array[i](e)[0], // alignment_array[i](e)[2]); // // fprintf(stderr, "camera origin (n=%d) o_t1 %f o_t0 %f o_t2 %f\n", // i, // alignment_array[i](estimate)[1], // alignment_array[i](estimate)[0], // alignment_array[i](estimate)[2]); point a_transformed = alignment_array[i](a); ale_pos e2 = atan((t.scaled_width() - 1) / (t.scaled_height() - 1)) - atan(a_transformed[1] / a_transformed[0]); // fprintf(stderr, "a components (n=%d) a1 %f a0 %f\n", i, a[1], a[0]); // fprintf(stderr, "e2 components (n=%d) tw %f th %f a_t1 %f a_t0 %f\n", i, // t.scaled_width(), t.scaled_height(), a_transformed[1], a_transformed[0]); alignment_array[i].modify_rotation(2, e2); // fprintf(stderr, "rotation (n=%d) e0 %f e1 %f e2 %f\n", i, e0, e1, e2); if (alignment_array[i](a)[0] > 0) { e2 += M_PI; alignment_array[i].modify_rotation(2, M_PI); // fprintf(stderr, "adding 2pi to e2."); } // fprintf(stderr, "rotation (n=%d) e0 %f e1 %f e2 %f\n", i, e0, e1, e2); // fprintf(stderr, "(0, 0) output (n=%d) a1 %f a0 %f a_t1 %f a_t0 %f\n", // i, // a[1], // a[0], // alignment_array[i](a)[1], // alignment_array[i](a)[0]); assert (alignment_array[i](a)[0] < 0); assert (alignment_array[i](a)[1] < 0); } } static void adjust_translation(unsigned int n, int d, ale_pos x) { alignment_array[n].modify_translation(d, x); } static void adjust_rotation(unsigned int n, int d, ale_pos x) { alignment_array[n].modify_rotation(d, x); } static void set_translation(unsigned int n, int d, ale_pos x) { alignment_array[n].set_translation(d, x); } static void set_rotation(unsigned int n, int d, ale_pos x) { alignment_array[n].set_rotation(d, x); } static void adjust_view_angle(ale_pos x) { _init_angle += x; } static void set_view_angle(ale_pos x) { _init_angle = x; } }; #endif