lsst.meas.astrom  17.0.1-6-g3b68888+1
matchOptimisticBTask.py
Go to the documentation of this file.
1 
2 __all__ = ["MatchOptimisticBTask", "MatchOptimisticBConfig",
3  "MatchTolerance"]
4 
5 import math
6 
7 import lsst.pex.config as pexConfig
8 import lsst.pipe.base as pipeBase
9 from lsst.meas.algorithms.sourceSelector import sourceSelectorRegistry
10 
11 from .setMatchDistance import setMatchDistance
12 from .matchOptimisticB import matchOptimisticB, MatchOptimisticBControl
13 
14 
16  """Stores match tolerances for use in `lsst.meas.astrom.AstrometryTask` and
17  later iterations of the matcher.
18 
19  MatchOptimsiticBTask relies on a maximum distance for matching
20  set by either the default in MatchOptimisticBConfig or the 2 sigma
21  scatter found after AstrometryTask has fit for a wcs.
22 
23  Parameters
24  ----------
25  maxMatchDist : `lsst.geom.Angle`
26  Current maximum distance to consider a match.
27  """
28 
29  def __init__(self, maxMatchDist=None):
30  self.maxMatchDist = maxMatchDist
31 
32 
33 class MatchOptimisticBConfig(pexConfig.Config):
34  """Configuration for MatchOptimisticBTask
35  """
36  maxMatchDistArcSec = pexConfig.RangeField(
37  doc="Maximum separation between reference objects and sources "
38  "beyond which they will not be considered a match (arcsec)",
39  dtype=float,
40  default=3,
41  min=0,
42  )
43  numBrightStars = pexConfig.RangeField(
44  doc="Number of bright stars to use",
45  dtype=int,
46  default=50,
47  min=2,
48  )
49  minMatchedPairs = pexConfig.RangeField(
50  doc="Minimum number of matched pairs; see also minFracMatchedPairs",
51  dtype=int,
52  default=30,
53  min=2,
54  )
55  minFracMatchedPairs = pexConfig.RangeField(
56  doc="Minimum number of matched pairs as a fraction of the smaller of "
57  "the number of reference stars or the number of good sources; "
58  "the actual minimum is the smaller of this value or minMatchedPairs",
59  dtype=float,
60  default=0.3,
61  min=0,
62  max=1,
63  )
64  maxOffsetPix = pexConfig.RangeField(
65  doc="Maximum allowed shift of WCS, due to matching (pixel). "
66  "When changing this value, the LoadReferenceObjectsConfig.pixelMargin should also be updated.",
67  dtype=int,
68  default=300,
69  max=4000,
70  )
71  maxRotationDeg = pexConfig.RangeField(
72  doc="Rotation angle allowed between sources and position reference objects (degrees)",
73  dtype=float,
74  default=1.0,
75  max=6.0,
76  )
77  allowedNonperpDeg = pexConfig.RangeField(
78  doc="Allowed non-perpendicularity of x and y (degree)",
79  dtype=float,
80  default=3.0,
81  max=45.0,
82  )
83  numPointsForShape = pexConfig.Field(
84  doc="number of points to define a shape for matching",
85  dtype=int,
86  default=6,
87  )
88  maxDeterminant = pexConfig.Field(
89  doc="maximum determinant of linear transformation matrix for a usable solution",
90  dtype=float,
91  default=0.02,
92  )
93  sourceSelector = sourceSelectorRegistry.makeField(
94  doc="How to select sources for cross-matching",
95  default="matcher"
96  )
97 
98  def setDefaults(self):
99  sourceSelector = self.sourceSelector["matcher"]
100  sourceSelector.setDefaults()
101  sourceSelector.excludePixelFlags = False
102 
103 
104 # The following block adds links to this task from the Task Documentation page.
105 # \addtogroup LSST_task_documentation
106 # \{
107 # \page measAstrom_matchOptimisticBTask
108 # \ref MatchOptimisticBTask "MatchOptimisticBTask"
109 # Match sources to reference objects
110 # \}
111 
112 
113 class MatchOptimisticBTask(pipeBase.Task):
114  """Match sources to reference objects using the Optimistic Pattern Matcher
115  B algorithm of Tabur 2007.
116  """
117  ConfigClass = MatchOptimisticBConfig
118  _DefaultName = "matchObjectsToSources"
119 
120  def __init__(self, **kwargs):
121  pipeBase.Task.__init__(self, **kwargs)
122  self.makeSubtask("sourceSelector")
123 
124  def filterStars(self, refCat):
125  """Extra filtering pass; subclass if desired.
126 
127  Parameters
128  ----------
129  refCat : `lsst.afw.table.SimpleCatalog`
130  Catalog of reference objects.
131 
132  Returns
133  -------
134  trimmedRefCat : `lsst.afw.table.SimpleCatalog`
135  Reference catalog with some filtering applied. Currently no
136  filtering is applied.
137  """
138  return refCat
139 
140  @pipeBase.timeMethod
141  def matchObjectsToSources(self, refCat, sourceCat, wcs, refFluxField,
142  match_tolerance=None):
143  """Match sources to position reference stars.
144 
145  Parameters
146  ----------
147  refCat : `lsst.afw.table.SimpleCatalog`
148  Reference catalog to match.
149  sourceCat : `lsst.afw.table.SourceCatalog`
150  Source catalog to match.
151  wcs : `lsst.afw.geom.SkyWcs`
152  Current WCS of the exposure containing the sources.
153  refFluxField : `str`
154  Name of the reference catalog filter to use.
155  match_tolerance : `lsst.meas.astrom.MatchTolerance`
156  Object containing information from previous
157  `lsst.meas.astrom.AstrometryTask` match/fit cycles for use in
158  matching. If `None` is config defaults.
159 
160  Returns
161  -------
162  matchResult : `lsst.pipe.base.Struct`
163  Result struct with components
164 
165  - ``matches`` : List of matches with distance below the maximum match
166  distance (`list` of `lsst.afw.table.ReferenceMatch`).
167  - ``useableSourceCat`` : Catalog of sources matched and suited for
168  WCS fitting (`lsst.afw.table.SourceCatalog`).
169  - ``match_tolerance`` : MatchTolerance object updated from this
170  match iteration (`lsst.meas.astrom.MatchTolerance`).
171  """
172  import lsstDebug
173  debug = lsstDebug.Info(__name__)
174 
175  preNumObj = len(refCat)
176  refCat = self.filterStars(refCat)
177  numRefObj = len(refCat)
178 
179  if self.log:
180  self.log.info("filterStars purged %d reference stars, leaving %d stars" %
181  (preNumObj - numRefObj, numRefObj))
182 
183  if match_tolerance is None:
184  match_tolerance = MatchTolerance()
185 
186  # usableSourceCat: sources that are good but may be saturated
187  numSources = len(sourceCat)
188  selectedSources = self.sourceSelector.run(sourceCat)
189  usableSourceCat = selectedSources.sourceCat
190  numUsableSources = len(usableSourceCat)
191  self.log.info("Purged %d unusable sources, leaving %d usable sources" %
192  (numSources - numUsableSources, numUsableSources))
193 
194  if len(usableSourceCat) == 0:
195  raise pipeBase.TaskError("No sources are usable")
196 
197  del sourceCat # avoid accidentally using sourceCat; use usableSourceCat or goodSourceCat from now on
198 
199  minMatchedPairs = min(self.config.minMatchedPairs,
200  int(self.config.minFracMatchedPairs * min([len(refCat), len(usableSourceCat)])))
201 
202  # match usable (possibly saturated) sources and then purge saturated sources from the match list
203  usableMatches = self._doMatch(
204  refCat=refCat,
205  sourceCat=usableSourceCat,
206  wcs=wcs,
207  refFluxField=refFluxField,
208  numUsableSources=numUsableSources,
209  minMatchedPairs=minMatchedPairs,
210  maxMatchDist=match_tolerance.maxMatchDist,
211  sourceFluxField=self.sourceSelector.fluxField,
212  verbose=debug.verbose,
213  )
214 
215  # cull non-good sources
216  matches = []
217  self._getIsGoodKeys(usableSourceCat.schema)
218  for match in usableMatches:
219  if self._isGoodTest(match.second):
220  # Append the isGood match.
221  matches.append(match)
222 
223  self.log.debug("Found %d usable matches, of which %d had good sources",
224  len(usableMatches), len(matches))
225 
226  if len(matches) == 0:
227  raise RuntimeError("Unable to match sources")
228 
229  self.log.info("Matched %d sources" % len(matches))
230  if len(matches) < minMatchedPairs:
231  self.log.warn("Number of matches is smaller than request")
232 
233  return pipeBase.Struct(
234  matches=matches,
235  usableSourceCat=usableSourceCat,
236  match_tolerance=match_tolerance,
237  )
238 
239  def _getIsGoodKeys(self, schema):
240  """Retrieve the keys needed for the isGoodTest from the source catalog
241  schema.
242 
243  Parameters
244  ----------
245  schema : `lsst.afw.table.Schema`
246  Source schema to retrieve `lsst.afw.table.Key` s from.
247  """
248  self.edgeKey = schema["base_PixelFlags_flag_edge"].asKey()
249  self.interpolatedCenterKey = schema["base_PixelFlags_flag_interpolatedCenter"].asKey()
250  self.saturatedKey = schema["base_PixelFlags_flag_saturated"].asKey()
251 
252  def _isGoodTest(self, source):
253  """Test that an object is good for use in the WCS fitter.
254 
255  This is a hard coded version of the isGood flag from the old SourceInfo
256  class that used to be part of this class.
257 
258  Parameters
259  ----------
260  source : `lsst.afw.table.SourceRecord`
261  Source to test.
262 
263  Returns
264  -------
265  isGood : `bool`
266  Source passes CCD edge and saturated tests.
267  """
268  return (not source.get(self.edgeKey) and
269  not source.get(self.interpolatedCenterKey) and
270  not source.get(self.saturatedKey))
271 
272  @pipeBase.timeMethod
273  def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs,
274  maxMatchDist, sourceFluxField, verbose):
275  """Implementation of matching sources to position reference stars.
276 
277  Unlike matchObjectsToSources, this method does not check if the sources
278  are suitable.
279 
280  Parameters
281  ----------
282  refCat : `lsst.afw.table.SimpleCatalog`
283  Catalog of reference objects.
284  sourceCat : `lsst.afw.table.SourceCatalog`
285  Catalog of detected sources.
286  wcs : `lsst.afw.geom.SkyWcs`
287  Current best WCS of the image.
288  refFluxFioeld : `str`
289  Name of flux field in refCat to use.
290  numUsableSources : `int`
291  Total number of source usable for matching.
292  mintMatchPairs : `int`
293  Minimum number of objects to match between the refCat and sourceCat
294  to consider a valid match.
295  maxMatchDist : `lsst.geom.Angle`
296  Maximum separation to considering a reference and a source a match.
297  sourceFluxField : `str`
298  Name of source catalog flux field.
299  verbose : `bool`
300  Print diagnostic information std::cout
301 
302  Returns
303  -------
304  matches : `list` of `lsst.afw.table.ReferenceMatch`
305  """
306  numSources = len(sourceCat)
307  posRefBegInd = numUsableSources - numSources
308  if maxMatchDist is None:
309  maxMatchDistArcSec = self.config.maxMatchDistArcSec
310  else:
311  maxMatchDistArcSec = min(maxMatchDist.asArcseconds(), self.config.maxMatchDistArcSec)
312  configMatchDistPix = maxMatchDistArcSec/wcs.getPixelScale().asArcseconds()
313 
314  matchControl = MatchOptimisticBControl()
315  matchControl.refFluxField = refFluxField
316  matchControl.sourceFluxField = sourceFluxField
317  matchControl.numBrightStars = self.config.numBrightStars
318  matchControl.minMatchedPairs = self.config.minMatchedPairs
319  matchControl.maxOffsetPix = self.config.maxOffsetPix
320  matchControl.numPointsForShape = self.config.numPointsForShape
321  matchControl.maxDeterminant = self.config.maxDeterminant
322 
323  for maxRotInd in range(4):
324  matchControl.maxRotationDeg = self.config.maxRotationDeg * math.pow(2.0, 0.5*maxRotInd)
325  for matchRadInd in range(3):
326  matchControl.matchingAllowancePix = configMatchDistPix * math.pow(1.25, matchRadInd)
327 
328  for angleDiffInd in range(3):
329  matchControl.allowedNonperpDeg = self.config.allowedNonperpDeg*(angleDiffInd+1)
330  matches = matchOptimisticB(
331  refCat,
332  sourceCat,
333  matchControl,
334  wcs,
335  posRefBegInd,
336  verbose,
337  )
338  if matches is not None and len(matches) > 0:
339  setMatchDistance(matches)
340  return matches
341  return matches
def matchObjectsToSources(self, refCat, sourceCat, wcs, refFluxField, match_tolerance=None)
afw::table::ReferenceMatchVector matchOptimisticB(afw::table::SimpleCatalog const &posRefCat, afw::table::SourceCatalog const &sourceCat, MatchOptimisticBControl const &control, afw::geom::SkyWcs const &wcs, int posRefBegInd=0, bool verbose=false)
Match sources to stars in a position reference catalog using optimistic pattern matching B...
def _doMatch(self, refCat, sourceCat, wcs, refFluxField, numUsableSources, minMatchedPairs, maxMatchDist, sourceFluxField, verbose)