lsst.meas.astrom  14.0-7-g0d69b06+3
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 #include <cmath>
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/geom/SkyWcs.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 double const MAX_DISTANCE_CRPIX_TO_BBOXCTR = 1000;
58 
59 /*
60  * Given an index and a SIP order, calculate p and q for the index'th term u^p v^q
61  * (Cf. Eqn 2 in http://fits.gsfc.nasa.gov/registry/sip/SIP_distortion_v1_0.pdf)
62  */
64 indexToPQ(int const index, int const order)
65 {
66  int p = 0, q = index;
67 
68  for (int decrement = order; q >= decrement && decrement > 0; --decrement) {
69  q -= decrement;
70  p++;
71  }
72 
73  return std::make_pair(p, q);
74 }
75 
76 Eigen::MatrixXd
77 calculateCMatrix(Eigen::VectorXd const& axis1, Eigen::VectorXd const& axis2, int const order)
78 {
79  int nTerms = 0.5*order*(order+1);
80 
81  int const n = axis1.size();
82  Eigen::MatrixXd C = Eigen::MatrixXd::Zero(n, nTerms);
83  for (int i = 0; i < n; ++i) {
84  for (int j = 0; j < nTerms; ++j) {
85  std::pair<int, int> pq = indexToPQ(j, order);
86  int p = pq.first, q = pq.second;
87 
88  assert(p + q < order);
89  C(i, j) = ::pow(axis1[i], p)*::pow(axis2[i], q);
90  }
91  }
92 
93  return C;
94 }
95 
102 Eigen::VectorXd
103 leastSquaresSolve(Eigen::VectorXd b, Eigen::MatrixXd A) {
104  assert(A.rows() == b.rows());
105  Eigen::VectorXd par = A.jacobiSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b);
106  return par;
107 }
108 
109 } // anonymous namespace
110 
112 template<class MatchT>
114  std::vector<MatchT> const & matches,
115  afw::geom::SkyWcs const& linearWcs,
116  int const order,
117  afwGeom::Box2I const& bbox,
118  int const ngrid
119 ):
120  _matches(matches),
121  _bbox(bbox),
122  _ngrid(ngrid),
123  _linearWcs(std::make_shared<afw::geom::SkyWcs>(linearWcs)),
124  _sipOrder(order+1),
125  _reverseSipOrder(order+2), //Higher order for reverse transform
126  _sipA(Eigen::MatrixXd::Zero(_sipOrder, _sipOrder)),
127  _sipB(Eigen::MatrixXd::Zero(_sipOrder, _sipOrder)),
128  _sipAp(Eigen::MatrixXd::Zero(_reverseSipOrder, _reverseSipOrder)),
129  _sipBp(Eigen::MatrixXd::Zero(_reverseSipOrder, _reverseSipOrder)),
130  _newWcs()
131 {
132  if (order < 2) {
133  throw LSST_EXCEPT(except::OutOfRangeError, "SIP must be at least 2nd order");
134  }
135  if (_sipOrder > 9) {
137  str(boost::format("SIP forward order %d exceeds the convention limit of 9") %
138  _sipOrder));
139  }
140  if (_reverseSipOrder > 9) {
142  str(boost::format("SIP reverse order %d exceeds the convention limit of 9") %
143  _reverseSipOrder));
144  }
145 
146  if (_matches.size() < std::size_t(_sipOrder)) {
147  throw LSST_EXCEPT(except::LengthError, "Number of matches less than requested sip order");
148  }
149 
150  if (_ngrid <= 0) {
151  _ngrid = 5*_sipOrder; // should be plenty
152  }
153 
154  /*
155  * We need a bounding box to define the region over which:
156  * The forward transformation should be valid
157  * We calculate the reverse transformartion
158  * If no BBox is provided, guess one from the input points (extrapolated a bit to allow for fact
159  * that a finite number of points won't reach to the edge of the image)
160  */
161  if (_bbox.isEmpty() && !_matches.empty() > 0) {
162  for (
163  typename std::vector<MatchT>::const_iterator ptr = _matches.begin();
164  ptr != _matches.end();
165  ++ptr
166  ) {
167  afwTable::SourceRecord const & src = *ptr->second;
168  _bbox.include(afwGeom::PointI(src.getX(), src.getY()));
169  }
170  float const borderFrac = 1/::sqrt(_matches.size()); // fractional border to add to exact BBox
171  afwGeom::Extent2I border(borderFrac*_bbox.getWidth(), borderFrac*_bbox.getHeight());
172 
173  _bbox.grow(border);
174  }
175 
176  // If crpix is too far from the center of the fit bbox, move it to the center to improve the fit
177  auto const initialCrpix = _linearWcs->getPixelOrigin();
178  auto const bboxCenter = afw::geom::Box2D(_bbox).getCenter();
179  if (std::hypot(initialCrpix[0] - bboxCenter[0], initialCrpix[1] - bboxCenter[1]) >
180  MAX_DISTANCE_CRPIX_TO_BBOXCTR) {
181  LOGL_DEBUG(_log,
182  "_linearWcs crpix = %d, %d farther than %f from bbox center; shifting to center %d, %d",
183  initialCrpix[0], initialCrpix[1], MAX_DISTANCE_CRPIX_TO_BBOXCTR, bboxCenter[0],
184  bboxCenter[1]);
185  _linearWcs = _linearWcs->getTanWcs(bboxCenter);
186  }
187 
188  // Calculate the forward part of the SIP distortion
189  _calculateForwardMatrices();
190 
191  // Build a new wcs incorporating the forward SIP matrix, it's all we know so far
192  auto const crval = _linearWcs->getSkyOrigin();
193  auto const crpix = _linearWcs->getPixelOrigin();
194  Eigen::MatrixXd cdMatrix = _linearWcs->getCdMatrix();
195  _newWcs = afw::geom::makeTanSipWcs(crpix, crval, cdMatrix, _sipA, _sipB);
196 
197  // Use _newWcs to calculate the forward transformation on a grid, and derive the back transformation
198  _calculateReverseMatrices();
199 
200  // Build a new wcs incorporating all SIP matrices
201  _newWcs = afw::geom::makeTanSipWcs(crpix, crval, cdMatrix, _sipA, _sipB, _sipAp, _sipBp);
202 }
203 
204 template<class MatchT>
205 void
207 {
208  // Assumes FITS (1-indexed) coordinates.
209  afwGeom::Point2D crpix = _linearWcs->getPixelOrigin();
210 
211  // Calculate u, v and intermediate world coordinates
212  int const nPoints = _matches.size();
213  Eigen::VectorXd u(nPoints), v(nPoints), iwc1(nPoints), iwc2(nPoints);
214 
215  int i = 0;
216  auto linearIwcToSky = getIntermediateWorldCoordsToSky(*_linearWcs);
217  for (
218  typename std::vector<MatchT>::const_iterator ptr = _matches.begin();
219  ptr != _matches.end();
220  ++ptr, ++i
221  ) {
222  afwTable::ReferenceMatch const & match = *ptr;
223 
224  // iwc: intermediate world coordinate positions of catalogue objects
225  afwCoord::IcrsCoord c = match.first->getCoord();
226  afwGeom::Point2D p = linearIwcToSky->applyInverse(c);
227  iwc1[i] = p[0];
228  iwc2[i] = p[1];
229  // u and v are intermediate pixel coordinates of observed (distorted) positions
230  u[i] = match.second->getX() - crpix[0];
231  v[i] = match.second->getY() - crpix[1];
232  }
233  // Scale u and v down to [-1,,+1] in order to avoid too large numbers in the polynomials
234  double uMax = u.cwiseAbs().maxCoeff();
235  double vMax = v.cwiseAbs().maxCoeff();
236  double norm = std::max(uMax, vMax);
237  u = u/norm;
238  v = v/norm;
239 
240  // Forward transform
241  int ord = _sipOrder;
242  Eigen::MatrixXd forwardC = calculateCMatrix(u, v, ord);
243  Eigen::VectorXd mu = leastSquaresSolve(iwc1, forwardC);
244  Eigen::VectorXd nu = leastSquaresSolve(iwc2, forwardC);
245 
246  // Use mu and nu to refine CD
247 
248  // Given the implementation of indexToPQ(), the refined values
249  // of the elements of the CD matrices are in elements 1 and "_sipOrder" of mu and nu
250  // If the implementation of indexToPQ() changes, these assertions
251  // will catch that change.
252  assert ((indexToPQ(0, ord) == std::pair<int, int>(0, 0)));
253  assert ((indexToPQ(1, ord) == std::pair<int, int>(0, 1)));
254  assert ((indexToPQ(ord, ord) == std::pair<int, int>(1, 0)));
255 
256  // Scale back CD matrix
257  Eigen::Matrix2d CD;
258  CD(1,0) = nu[ord]/norm;
259  CD(1,1) = nu[1]/norm;
260  CD(0,0) = mu[ord]/norm;
261  CD(0,1) = mu[1]/norm;
262 
263  Eigen::Matrix2d CDinv = CD.inverse(); //Direct inverse OK for 2x2 matrix in Eigen
264 
265  // The zeroth elements correspond to a shift in crpix
266  crpix[0] -= mu[0]*CDinv(0,0) + nu[0]*CDinv(0,1);
267  crpix[1] -= mu[0]*CDinv(1,0) + nu[0]*CDinv(1,1);
268 
269  auto const crval = _linearWcs->getSkyOrigin();
270 
271  _linearWcs = afw::geom::makeSkyWcs(crpix, crval, CD);
272 
273  //Get Sip terms
274 
275  //The rest of the elements correspond to
276  //mu[i] == CD11*Apq + CD12*Bpq and
277  //nu[i] == CD21*Apq + CD22*Bpq and
278  //
279  //We solve for Apq and Bpq with the equation
280  // (Apq) = (CD11 CD12)-1 * (mu[i])
281  // (Bpq) (CD21 CD22) (nu[i])
282 
283  for(int i=1; i< mu.rows(); ++i) {
284  std::pair<int, int> pq = indexToPQ(i, ord);
285  int p = pq.first, q = pq.second;
286 
287  if(p + q > 1 && p + q < ord) {
288  Eigen::Vector2d munu(2,1);
289  munu(0) = mu(i);
290  munu(1) = nu(i);
291  Eigen::Vector2d AB = CDinv*munu;
292  // Scale back sip coefficients
293  _sipA(p,q) = AB[0]/::pow(norm,p+q);
294  _sipB(p,q) = AB[1]/::pow(norm,p+q);
295  }
296  }
297 }
298 
299 template<class MatchT>
301  int const ngrid2 = _ngrid*_ngrid;
302 
303  Eigen::VectorXd U(ngrid2), V(ngrid2);
304  Eigen::VectorXd delta1(ngrid2), delta2(ngrid2);
305 
306  int const x0 = _bbox.getMinX();
307  double const dx = _bbox.getWidth()/(double)(_ngrid - 1);
308  int const y0 = _bbox.getMinY();
309  double const dy = _bbox.getHeight()/(double)(_ngrid - 1);
310 
311  // wcs->getPixelOrigin() returns LSST-style (0-indexed) pixel coords.
312  afwGeom::Point2D crpix = _newWcs->getPixelOrigin();
313 
314  LOGL_DEBUG(_log, "_calcReverseMatrices: x0,y0 %i,%i, W,H %i,%i, ngrid %i, dx,dy %g,%g, CRPIX %g,%g",
315  x0, y0, _bbox.getWidth(), _bbox.getHeight(), _ngrid, dx, dy, crpix[0], crpix[1]);
316 
317  auto tanWcs = _newWcs->getTanWcs(_newWcs->getPixelOrigin());
318  auto applySipAB = afwGeom::makeWcsPairTransform(*_newWcs, *tanWcs);
319  int k = 0;
320  for (int i = 0; i < _ngrid; ++i) {
321  double const y = y0 + i*dy;
322  std::vector<afwGeom::Point2D> beforeSipABPoints;
323  beforeSipABPoints.reserve(_ngrid);
324  for (int j = 0; j < _ngrid; ++j) {
325  double const x = x0 + j*dx;
326  beforeSipABPoints.emplace_back(afwGeom::Point2D(x, y));
327  }
328  auto const afterSipABPoints = applySipAB->applyForward(beforeSipABPoints);
329  for (int j = 0; j < _ngrid; ++j, ++k) {
330  double const x = x0 + j*dx;
331  double u,v;
332  // u and v are intermediate pixel coordinates on a grid of positions
333  u = x - crpix[0];
334  v = y - crpix[1];
335 
336  // U and V are the result of applying the "forward" (A,B) SIP coefficients
337  afwGeom::Point2D const xy = afterSipABPoints[j];
338  U[k] = xy[0] - crpix[0];
339  V[k] = xy[1] - crpix[1];
340 
341  if ((i == 0 || i == (_ngrid-1) || i == (_ngrid/2)) &&
342  (j == 0 || j == (_ngrid-1) || j == (_ngrid/2))) {
343  LOGL_DEBUG(_log, " x,y (%.1f, %.1f), u,v (%.1f, %.1f), U,V (%.1f, %.1f)", x, y, u, v, U[k], V[k]);
344  }
345 
346  delta1[k] = u - U[k];
347  delta2[k] = v - V[k];
348  }
349  }
350 
351  // Scale down U and V in order to avoid too large numbers in the polynomials
352  double UMax = U.cwiseAbs().maxCoeff();
353  double VMax = V.cwiseAbs().maxCoeff();
354  double norm = (UMax > VMax) ? UMax : VMax;
355  U = U/norm;
356  V = V/norm;
357 
358  // Reverse transform
359  int const ord = _reverseSipOrder;
360  Eigen::MatrixXd reverseC = calculateCMatrix(U, V, ord);
361  Eigen::VectorXd tmpA = leastSquaresSolve(delta1, reverseC);
362  Eigen::VectorXd tmpB = leastSquaresSolve(delta2, reverseC);
363 
364  assert(tmpA.rows() == tmpB.rows());
365  for(int j=0; j< tmpA.rows(); ++j) {
366  std::pair<int, int> pq = indexToPQ(j, ord);
367  int p = pq.first, q = pq.second;
368  // Scale back sip coefficients
369  _sipAp(p, q) = tmpA[j]/::pow(norm,p+q);
370  _sipBp(p, q) = tmpB[j]/::pow(norm,p+q);
371  }
372 }
373 
374 template<class MatchT>
376  assert(_newWcs.get());
377  return makeMatchStatisticsInPixels(*_newWcs, _matches, afw::math::MEDIAN).getValue();
378 }
379 
380 template<class MatchT>
382  assert(_linearWcs.get());
383  return makeMatchStatisticsInPixels(*_linearWcs, _matches, afw::math::MEDIAN).getValue();
384 }
385 
386 template<class MatchT>
388  assert(_newWcs.get());
390  *_newWcs, _matches, afw::math::MEDIAN).getValue()*afw::geom::radians;
391 }
392 
393 template<class MatchT>
395  assert(_linearWcs.get());
397  *_linearWcs, _matches, afw::math::MEDIAN).getValue()*afw::geom::radians;
398 }
399 
400 
401 #define INSTANTIATE(MATCH) \
402  template class CreateWcsWithSip<MATCH>;
403 
406 
407 }}}}
408 
409 
double getScatterInPixels() const
Compute the median separation, in pixels, between items in this object&#39;s match list.
T empty(T... args)
std::shared_ptr< SkyWcs > makeTanSipWcs(Point2D const &crpix, coord::IcrsCoord const &crval, Eigen::Matrix2d const &cdMatrix, Eigen::MatrixXd const &sipA, Eigen::MatrixXd const &sipB)
AngleUnit constexpr radians
CreateWcsWithSip(std::vector< MatchT > const &matches, afw::geom::SkyWcs const &linearWcs, int const order, afw::geom::Box2I const &bbox=afw::geom::Box2I(), int const ngrid=0)
Construct a CreateWcsWithSip.
void include(Point2I const &point)
T norm(const T &x)
STL namespace.
T end(T... args)
#define LOGL_DEBUG(logger, message...)
std::shared_ptr< Record2 > second
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
afw::math::Statistics makeMatchStatisticsInPixels(afw::geom::SkyWcs 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.
Point2D const getCenter() const
T make_pair(T... args)
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 hypot(T... args)
T size(T... args)
#define LSST_EXCEPT(type,...)
afw::math::Statistics makeMatchStatisticsInRadians(afw::geom::SkyWcs 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.
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.
#define INSTANTIATE(MATCH)
#define LOG_GET(logger)
std::shared_ptr< TransformPoint2ToIcrsCoord > getIntermediateWorldCoordsToSky(SkyWcs const &wcs, bool simplify=true)
T reserve(T... args)
double getLinearScatterInPixels() const
Compute the median radial separation between items in this object&#39;s match list.
T emplace_back(T... args)