lsst.meas.astrom  14.0-6-gd5b81a9
CreateWcsWithSip.cc
Go to the documentation of this file.
1 // -*- LSST-C++ -*-
2 
3 /*
4  * LSST Data Management System
5  * Copyright 2008, 2009, 2010 LSST Corporation.
6  *
7  * This product includes software developed by the
8  * LSST Project (http://www.lsst.org/).
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the LSST License Statement and
21  * the GNU General Public License along with this program. If not,
22  * see <http://www.lsstcorp.org/LegalNotices/>.
23  */
24 
25 
26 #include "Eigen/SVD"
27 #include "Eigen/Cholesky"
28 #include "Eigen/LU"
29 
32 #include "lsst/afw/geom/Angle.h"
34 #include "lsst/afw/image/TanWcs.h"
35 #include "lsst/log/Log.h"
37 
38 namespace {
39 LOG_LOGGER _log = LOG_GET("meas.astrom.sip");
40 }
41 
42 namespace lsst {
43 namespace meas {
44 namespace astrom {
45 namespace sip {
46 
47 namespace except = lsst::pex::exceptions;
48 namespace afwCoord = lsst::afw::coord;
49 namespace afwGeom = lsst::afw::geom;
50 namespace afwImg = lsst::afw::image;
51 namespace afwDet = lsst::afw::detection;
52 namespace afwMath = lsst::afw::math;
53 namespace afwTable = lsst::afw::table;
54 
55 namespace {
56 /*
57  * Given an index and a SIP order, calculate p and q for the index'th term u^p v^q
58  * (Cf. Eqn 2 in http://fits.gsfc.nasa.gov/registry/sip/SIP_distortion_v1_0.pdf)
59  */
61 indexToPQ(int const index, int const order)
62 {
63  int p = 0, q = index;
64 
65  for (int decrement = order; q >= decrement && decrement > 0; --decrement) {
66  q -= decrement;
67  p++;
68  }
69 
70  return std::make_pair(p, q);
71 }
72 
73 Eigen::MatrixXd
74 calculateCMatrix(Eigen::VectorXd const& axis1, Eigen::VectorXd const& axis2, int const order)
75 {
76  int nTerms = 0.5*order*(order+1);
77 
78  int const n = axis1.size();
79  Eigen::MatrixXd C = Eigen::MatrixXd::Zero(n, nTerms);
80  for (int i = 0; i < n; ++i) {
81  for (int j = 0; j < nTerms; ++j) {
82  std::pair<int, int> pq = indexToPQ(j, order);
83  int p = pq.first, q = pq.second;
84 
85  assert(p + q < order);
86  C(i, j) = ::pow(axis1[i], p)*::pow(axis2[i], q);
87  }
88  }
89 
90  return C;
91 }
92 
99 Eigen::VectorXd
100 leastSquaresSolve(Eigen::VectorXd b, Eigen::MatrixXd A) {
101  assert(A.rows() == b.rows());
102  Eigen::VectorXd par = A.jacobiSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b);
103  return par;
104 }
105 
106 } // anonymous namespace
107 
109 template<class MatchT>
111  std::vector<MatchT> const & matches,
112  afwImg::Wcs const& linearWcs,
113  int const order,
114  afwGeom::Box2I const& bbox,
115  int const ngrid
116 ):
117  _matches(matches),
118  _bbox(bbox),
119  _ngrid(ngrid),
120  _linearWcs(linearWcs.clone()),
121  _sipOrder(order+1),
122  _reverseSipOrder(order+2), //Higher order for reverse transform
123  _sipA(Eigen::MatrixXd::Zero(_sipOrder, _sipOrder)),
124  _sipB(Eigen::MatrixXd::Zero(_sipOrder, _sipOrder)),
125  _sipAp(Eigen::MatrixXd::Zero(_reverseSipOrder, _reverseSipOrder)),
126  _sipBp(Eigen::MatrixXd::Zero(_reverseSipOrder, _reverseSipOrder)),
127  _newWcs()
128 {
129  if (order < 2) {
130  throw LSST_EXCEPT(except::OutOfRangeError, "SIP must be at least 2nd order");
131  }
132  if (_sipOrder > 9) {
134  str(boost::format("SIP forward order %d exceeds the convention limit of 9") %
135  _sipOrder));
136  }
137  if (_reverseSipOrder > 9) {
139  str(boost::format("SIP reverse order %d exceeds the convention limit of 9") %
140  _reverseSipOrder));
141  }
142 
143  if (_matches.size() < std::size_t(_sipOrder)) {
144  throw LSST_EXCEPT(except::LengthError, "Number of matches less than requested sip order");
145  }
146 
147  if (_ngrid <= 0) {
148  _ngrid = 5*_sipOrder; // should be plenty
149  }
150 
151  /*
152  * We need a bounding box to define the region over which:
153  * The forward transformation should be valid
154  * We calculate the reverse transformartion
155  * If no BBox is provided, guess one from the input points (extrapolated a bit to allow for fact
156  * that a finite number of points won't reach to the edge of the image)
157  */
158  if (_bbox.isEmpty() && !_matches.empty() > 0) {
159  for (
160  typename std::vector<MatchT>::const_iterator ptr = _matches.begin();
161  ptr != _matches.end();
162  ++ptr
163  ) {
164  afwTable::SourceRecord const & src = *ptr->second;
165  _bbox.include(afwGeom::PointI(src.getX(), src.getY()));
166  }
167  float const borderFrac = 1/::sqrt(_matches.size()); // fractional border to add to exact BBox
168  afwGeom::Extent2I border(borderFrac*_bbox.getWidth(), borderFrac*_bbox.getHeight());
169 
170  _bbox.grow(border);
171  }
172  // Calculate the forward part of the SIP distortion
173  _calculateForwardMatrices();
174 
175  //Build a new wcs incorporating the forward sip matrix, it's all we know so far
176  afwGeom::Point2D crval = _getCrvalAsGeomPoint();
177  afwGeom::Point2D crpix = _linearWcs->getPixelOrigin();
178  Eigen::MatrixXd CD = _linearWcs->getCDMatrix();
179 
180  _newWcs = PTR(afwImg::TanWcs)(new afwImg::TanWcs(crval, crpix, CD, _sipA, _sipB, _sipAp, _sipBp));
181  // Use _newWcs to calculate the forward transformation on a grid, and derive the back transformation
182  _calculateReverseMatrices();
183 
184  //Build a new wcs incorporating both of the sip matrices
185  _newWcs = PTR(afwImg::TanWcs)(new afwImg::TanWcs(crval, crpix, CD, _sipA, _sipB, _sipAp, _sipBp));
186 
187 }
188 
189 template<class MatchT>
190 void
192 {
193  // Assumes FITS (1-indexed) coordinates.
194  afwGeom::Point2D crpix = _linearWcs->getPixelOrigin();
195 
196  // Calculate u, v and intermediate world coordinates
197  int const nPoints = _matches.size();
198  Eigen::VectorXd u(nPoints), v(nPoints), iwc1(nPoints), iwc2(nPoints);
199 
200  int i = 0;
201  for (
202  typename std::vector<MatchT>::const_iterator ptr = _matches.begin();
203  ptr != _matches.end();
204  ++ptr, ++i
205  ) {
206  afwTable::ReferenceMatch const & match = *ptr;
207 
208  // iwc: intermediate world coordinate positions of catalogue objects
209  afwCoord::IcrsCoord c = match.first->getCoord();
210  afwGeom::Point2D p = _linearWcs->skyToIntermediateWorldCoord(c);
211  iwc1[i] = p[0];
212  iwc2[i] = p[1];
213  // u and v are intermediate pixel coordinates of observed (distorted) positions
214  u[i] = match.second->getX() - crpix[0];
215  v[i] = match.second->getY() - crpix[1];
216  }
217  // Scale u and v down to [-1,,+1] in order to avoid too large numbers in the polynomials
218  double uMax = u.cwiseAbs().maxCoeff();
219  double vMax = v.cwiseAbs().maxCoeff();
220  double norm = std::max(uMax, vMax);
221  u = u/norm;
222  v = v/norm;
223 
224  // Forward transform
225  int ord = _sipOrder;
226  Eigen::MatrixXd forwardC = calculateCMatrix(u, v, ord);
227  Eigen::VectorXd mu = leastSquaresSolve(iwc1, forwardC);
228  Eigen::VectorXd nu = leastSquaresSolve(iwc2, forwardC);
229 
230  // Use mu and nu to refine CD
231 
232  // Given the implementation of indexToPQ(), the refined values
233  // of the elements of the CD matrices are in elements 1 and "_sipOrder" of mu and nu
234  // If the implementation of indexToPQ() changes, these assertions
235  // will catch that change.
236  assert ((indexToPQ(0, ord) == std::pair<int, int>(0, 0)));
237  assert ((indexToPQ(1, ord) == std::pair<int, int>(0, 1)));
238  assert ((indexToPQ(ord, ord) == std::pair<int, int>(1, 0)));
239 
240  // Scale back CD matrix
241  Eigen::Matrix2d CD;
242  CD(1,0) = nu[ord]/norm;
243  CD(1,1) = nu[1]/norm;
244  CD(0,0) = mu[ord]/norm;
245  CD(0,1) = mu[1]/norm;
246 
247  Eigen::Matrix2d CDinv = CD.inverse(); //Direct inverse OK for 2x2 matrix in Eigen
248 
249  // The zeroth elements correspond to a shift in crpix
250  crpix[0] -= mu[0]*CDinv(0,0) + nu[0]*CDinv(0,1);
251  crpix[1] -= mu[0]*CDinv(1,0) + nu[0]*CDinv(1,1);
252 
253  afwGeom::Point2D crval = _getCrvalAsGeomPoint();
254 
255  _linearWcs = std::shared_ptr<afwImg::Wcs>( new afwImg::Wcs(crval, crpix, CD));
256 
257  //Get Sip terms
258 
259  //The rest of the elements correspond to
260  //mu[i] == CD11*Apq + CD12*Bpq and
261  //nu[i] == CD21*Apq + CD22*Bpq and
262  //
263  //We solve for Apq and Bpq with the equation
264  // (Apq) = (CD11 CD12)-1 * (mu[i])
265  // (Bpq) (CD21 CD22) (nu[i])
266 
267  for(int i=1; i< mu.rows(); ++i) {
268  std::pair<int, int> pq = indexToPQ(i, ord);
269  int p = pq.first, q = pq.second;
270 
271  if(p + q > 1 && p + q < ord) {
272  Eigen::Vector2d munu(2,1);
273  munu(0) = mu(i);
274  munu(1) = nu(i);
275  Eigen::Vector2d AB = CDinv*munu;
276  // Scale back sip coefficients
277  _sipA(p,q) = AB[0]/::pow(norm,p+q);
278  _sipB(p,q) = AB[1]/::pow(norm,p+q);
279  }
280  }
281 }
282 
283 template<class MatchT>
285  int const ngrid2 = _ngrid*_ngrid;
286 
287  Eigen::VectorXd U(ngrid2), V(ngrid2);
288  Eigen::VectorXd delta1(ngrid2), delta2(ngrid2);
289 
290  int const x0 = _bbox.getMinX();
291  double const dx = _bbox.getWidth()/(double)(_ngrid - 1);
292  int const y0 = _bbox.getMinY();
293  double const dy = _bbox.getHeight()/(double)(_ngrid - 1);
294 
295  // wcs->getPixelOrigin() returns LSST-style (0-indexed) pixel coords.
296  afwGeom::Point2D crpix = _newWcs->getPixelOrigin();
297 
298  LOGL_DEBUG(_log, "_calcReverseMatrices: x0,y0 %i,%i, W,H %i,%i, ngrid %i, dx,dy %g,%g, CRPIX %g,%g",
299  x0, y0, _bbox.getWidth(), _bbox.getHeight(), _ngrid, dx, dy, crpix[0], crpix[1]);
300 
301  int k = 0;
302  for (int i = 0; i < _ngrid; ++i) {
303  double const y = y0 + i*dy;
304  for (int j = 0; j < _ngrid; ++j, ++k) {
305  double const x = x0 + j*dx;
306  double u,v;
307  // u and v are intermediate pixel coordinates on a grid of positions
308  u = x - crpix[0];
309  v = y - crpix[1];
310 
311  // U and V are the result of applying the "forward" (A,B) SIP coefficients
312  // NOTE that the "undistortPixel()" function accepts 1-indexed (FITS-style)
313  // coordinates, and here we are treating "x" and "y" as LSST-style.
314  afwGeom::Point2D xy = _newWcs->undistortPixel(afwGeom::Point2D(x + 1, y + 1));
315  // "crpix", on the other hand, is LSST-style 0-indexed, so we have to remove
316  // the FITS-style 1-index from "xy"
317  U[k] = xy[0] - 1 - crpix[0];
318  V[k] = xy[1] - 1 - crpix[1];
319 
320  if ((i == 0 || i == (_ngrid-1) || i == (_ngrid/2)) &&
321  (j == 0 || j == (_ngrid-1) || j == (_ngrid/2))) {
322  LOGL_DEBUG(_log, " x,y (%.1f, %.1f), u,v (%.1f, %.1f), U,V (%.1f, %.1f)", x, y, u, v, U[k], V[k]);
323  }
324 
325  delta1[k] = u - U[k];
326  delta2[k] = v - V[k];
327  }
328  }
329 
330  // Scale down U and V in order to avoid too large numbers in the polynomials
331  double UMax = U.cwiseAbs().maxCoeff();
332  double VMax = V.cwiseAbs().maxCoeff();
333  double norm = (UMax > VMax) ? UMax : VMax;
334  U = U/norm;
335  V = V/norm;
336 
337  // Reverse transform
338  int const ord = _reverseSipOrder;
339  Eigen::MatrixXd reverseC = calculateCMatrix(U, V, ord);
340  Eigen::VectorXd tmpA = leastSquaresSolve(delta1, reverseC);
341  Eigen::VectorXd tmpB = leastSquaresSolve(delta2, reverseC);
342 
343  assert(tmpA.rows() == tmpB.rows());
344  for(int j=0; j< tmpA.rows(); ++j) {
345  std::pair<int, int> pq = indexToPQ(j, ord);
346  int p = pq.first, q = pq.second;
347  // Scale back sip coefficients
348  _sipAp(p, q) = tmpA[j]/::pow(norm,p+q);
349  _sipBp(p, q) = tmpB[j]/::pow(norm,p+q);
350  }
351 }
352 
353 template<class MatchT>
355  assert(_newWcs.get());
356  return makeMatchStatisticsInPixels(*_newWcs, _matches, afw::math::MEDIAN).getValue();
357 }
358 
359 template<class MatchT>
361  assert(_linearWcs.get());
362  return makeMatchStatisticsInPixels(*_linearWcs, _matches, afw::math::MEDIAN).getValue();
363 }
364 
365 template<class MatchT>
367  assert(_newWcs.get());
369  *_newWcs, _matches, afw::math::MEDIAN).getValue()*afw::geom::radians;
370 }
371 
372 template<class MatchT>
374  assert(_linearWcs.get());
376  *_linearWcs, _matches, afw::math::MEDIAN).getValue()*afw::geom::radians;
377 }
378 
379 template<class MatchT>
381  afwCoord::Fk5Coord coo = _linearWcs->getSkyOrigin()->toFk5();
382  return coo.getPosition(afwGeom::degrees);
383 }
384 
385 
386 #define INSTANTIATE(MATCH) \
387  template class CreateWcsWithSip<MATCH>;
388 
391 
392 }}}}
393 
394 
double getScatterInPixels() const
Compute the median separation, in pixels, between items in this object&#39;s match list.
T empty(T... args)
AngleUnit constexpr radians
void include(Point2I const &point)
T norm(const T &x)
table::PointKey< double > crval
#define PTR(...)
T end(T... args)
#define LOGL_DEBUG(logger, message...)
lsst::afw::geom::Point2D getPosition(lsst::afw::geom::AngleUnit unit=lsst::afw::geom::degrees) const
std::shared_ptr< Record2 > second
CreateWcsWithSip(std::vector< MatchT > const &matches, afw::image::Wcs const &linearWcs, int const order, afw::geom::Box2I const &bbox=afw::geom::Box2I(), int const ngrid=0)
Construct a CreateWcsWithSip.
T make_pair(T... args)
afw::math::Statistics makeMatchStatisticsInPixels(afw::image::Wcs const &wcs, std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
Compute statistics of on-detector radial separation for a match list, in pixels.
std::shared_ptr< Record1 > first
double x
T max(T... args)
afw::geom::Angle getScatterOnSky() const
Compute the median on-sky separation between items in this object&#39;s match list.
double getValue(Property const prop=NOTHING) const
T size(T... args)
#define LSST_EXCEPT(type,...)
void grow(int buffer)
T begin(T... args)
afw::geom::Angle getLinearScatterOnSky() const
Compute the median on-sky separation between items in this object&#39;s match list,.
Measure the distortions in an image plane and express them a SIP polynomials.
afw::math::Statistics makeMatchStatisticsInRadians(afw::image::Wcs const &wcs, std::vector< MatchT > const &matchList, int const flags, afw::math::StatisticsControl const &sctrl=afw::math::StatisticsControl())
Compute statistics of on-sky radial separation for a match list, in radians.
#define INSTANTIATE(MATCH)
#define LOG_GET(logger)
clone
double getLinearScatterInPixels() const
Compute the median radial separation between items in this object&#39;s match list.
table::PointKey< double > crpix