LSST Applications  21.0.0+04719a4bac,21.0.0-1-ga51b5d4+f5e6047307,21.0.0-11-g2b59f77+a9c1acf22d,21.0.0-11-ga42c5b2+86977b0b17,21.0.0-12-gf4ce030+76814010d2,21.0.0-13-g1721dae+760e7a6536,21.0.0-13-g3a573fe+768d78a30a,21.0.0-15-g5a7caf0+f21cbc5713,21.0.0-16-g0fb55c1+b60e2d390c,21.0.0-19-g4cded4ca+71a93a33c0,21.0.0-2-g103fe59+bb20972958,21.0.0-2-g45278ab+04719a4bac,21.0.0-2-g5242d73+3ad5d60fb1,21.0.0-2-g7f82c8f+8babb168e8,21.0.0-2-g8f08a60+06509c8b61,21.0.0-2-g8faa9b5+616205b9df,21.0.0-2-ga326454+8babb168e8,21.0.0-2-gde069b7+5e4aea9c2f,21.0.0-2-gecfae73+1d3a86e577,21.0.0-2-gfc62afb+3ad5d60fb1,21.0.0-25-g1d57be3cd+e73869a214,21.0.0-3-g357aad2+ed88757d29,21.0.0-3-g4a4ce7f+3ad5d60fb1,21.0.0-3-g4be5c26+3ad5d60fb1,21.0.0-3-g65f322c+e0b24896a3,21.0.0-3-g7d9da8d+616205b9df,21.0.0-3-ge02ed75+a9c1acf22d,21.0.0-4-g591bb35+a9c1acf22d,21.0.0-4-g65b4814+b60e2d390c,21.0.0-4-gccdca77+0de219a2bc,21.0.0-4-ge8a399c+6c55c39e83,21.0.0-5-gd00fb1e+05fce91b99,21.0.0-6-gc675373+3ad5d60fb1,21.0.0-64-g1122c245+4fb2b8f86e,21.0.0-7-g04766d7+cd19d05db2,21.0.0-7-gdf92d54+04719a4bac,21.0.0-8-g5674e7b+d1bd76f71f,master-gac4afde19b+a9c1acf22d,w.2021.13
LSST Data Management Base Package
quickFrameMeasurement.py
Go to the documentation of this file.
1 # This file is part of pipe_tasks.
2 #
3 # Developed for the LSST Data Management System.
4 # This product includes software developed by the LSST Project
5 # (https://www.lsst.org).
6 # See the COPYRIGHT file at the top-level directory of this distribution
7 # for details of code ownership.
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
18 #
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <https://www.gnu.org/licenses/>.
21 
22 import numpy as np
23 import lsst.afw.detection as afwDetect
24 import lsst.afw.table as afwTable
25 import lsst.meas.base as measBase
26 import lsst.daf.base as dafBase
27 import lsst.pipe.base as pipeBase
28 import lsst.pex.config as pexConfig
29 from lsst.meas.base import MeasurementError
30 from lsst.meas.algorithms.installGaussianPsf import InstallGaussianPsfTask
31 
32 
33 class QuickFrameMeasurementTaskConfig(pexConfig.Config):
34  """Config class for the QuickFrameMeasurementTask.
35  """
36  installPsf = pexConfig.ConfigurableField(
37  target=InstallGaussianPsfTask,
38  doc="Task for installing an initial PSF",
39  )
40  maxNonRoundness = pexConfig.Field(
41  dtype=float,
42  doc="Ratio of xx to yy (or vice versa) above which to cut, in order to exclude spectra",
43  default=5.,
44  )
45  maxExtendedness = pexConfig.Field(
46  dtype=float,
47  doc="Max absolute value of xx and yy above which to cut, in order to exclude large/things",
48  default=100,
49  )
50  doExtendednessCut = pexConfig.Field(
51  dtype=bool,
52  doc="Apply the extendeness cut, as definted by maxExtendedness",
53  default=False,
54  )
55  centroidPixelPercentile = pexConfig.Field(
56  dtype=float,
57  doc="The image's percentile value which the centroid must be greater than to pass the final peak"
58  " check. Ignored if doCheckCentroidPixelValue is False",
59  default=90,
60  )
61  doCheckCentroidPixelValue = pexConfig.Field(
62  dtype=bool,
63  doc="Check that the centroid found is actually in the centroidPixelPercentile percentile of the"
64  " image? Set to False for donut images.",
65  default=True,
66  )
67  initialPsfWidth = pexConfig.Field(
68  dtype=float,
69  doc="Guess at the initial PSF FWHM in pixels.",
70  default=10,
71  )
72  nSigmaDetection = pexConfig.Field(
73  dtype=float,
74  doc="Number of sigma for the detection limit.",
75  default=20,
76  )
77  nPixMinDetection = pexConfig.Field(
78  dtype=int,
79  doc="Minimum number of pixels in a detected source.",
80  default=10,
81  )
82 
83  def setDefaults(self):
84  super().setDefaults()
85  self.installPsfinstallPsf.fwhm = self.initialPsfWidthinitialPsfWidth
86 
87 
88 class QuickFrameMeasurementTask(pipeBase.Task):
89  """WARNING: An experimental new task with changable API! Do not rely on yet!
90 
91  This task finds the centroid of the brightest source in a given CCD-image
92  and returns its centroid and a rough estimate of the seeing/PSF.
93 
94  It is designed for speed, such that it can be used in observing scripts
95  to provide pointing offsets, allowing subsequent pointings to place
96  a source at an exact pixel position.
97 
98  The approach taken here is deliberately sub-optimal in the detection and
99  measurement sense, with all optimisation being done for speed and robustness
100  of the result.
101 
102  A small set of unit tests exist for this task, which run automatically
103  if afwdata is setup. These, however, are stricky unit tests, and will not
104  catch algorithmic regressions. TODO: DM-29038 exists to merge a regression
105  real test which runs against 1,000 LATISS images, but is therefore slow
106  and requires access to the data.
107 
108  Parameters
109  ----------
110  config : lsst.pipe.tasks.quickFrameMeasurement.QuickFrameMeasurementTaskConfig
111  Configuration class for the QuickFrameMeasurementTask.
112 
113  display : lsst.afw.display.Display, optional
114  The display to use for showing the images, detections and centroids.
115 
116  Returns
117  -------
118  result : lsst.pipe.base.Struct()
119  Return strucure containing whether the task was successful, the main
120  source's centroid, its the aperture fluxes, the ixx and iyy of the
121  source, and the median ixx, iyy of the detections in the exposure.
122  See run() method for further details.
123 
124  Raises
125  ------
126  This task should *never* raise, as the run() method is enclosed in an
127  except Exception block, so that it will never fail during observing.
128  Failure modes should be limited to returning a return Struct() with the same
129  structure as the success case, with all value set to np.nan but with
130  result.success=False.
131  """
132  ConfigClass = QuickFrameMeasurementTaskConfig
133  _DefaultName = 'quickFrameMeasurementTask'
134 
135  def __init__(self, config, *, display=None, **kwargs):
136  super().__init__(config=config, **kwargs)
137  self.makeSubtask("installPsf")
138 
139  self.displaydisplay = None
140  if display:
141  self.displaydisplay = display
142 
143  self.centroidNamecentroidName = "base_SdssCentroid"
144  self.shapeNameshapeName = "base_SdssShape"
145  self.schemaschema = afwTable.SourceTable.makeMinimalSchema()
146  self.schemaschema.getAliasMap().set("slot_Centroid", self.centroidNamecentroidName)
147  self.schemaschema.getAliasMap().set("slot_Shape", self.shapeNameshapeName)
148  self.controlcontrol = measBase.SdssCentroidControl()
149  self.centroidercentroider = measBase.SdssCentroidAlgorithm(self.controlcontrol, self.centroidNamecentroidName, self.schemaschema)
150  self.sdssShapesdssShape = measBase.SdssShapeControl()
151  self.shapershaper = measBase.SdssShapeAlgorithm(self.sdssShapesdssShape, self.shapeNameshapeName, self.schemaschema)
152  self.apFluxControlapFluxControl = measBase.ApertureFluxControl()
153  md = dafBase.PropertySet()
154  self.apFluxerapFluxer = measBase.CircularApertureFluxAlgorithm(self.apFluxControlapFluxControl, "aperFlux",
155  self.schemaschema, md)
156 
157  self.tabletable = afwTable.SourceTable.make(self.schemaschema) # make sure to call this last!
158 
159  @staticmethod
160  def detectObjectsInExp(exp, nSigma, nPixMin, grow=0):
161  """Run a very basic but fast threshold-based object detection on an exposure
162  Return the footPrintSet for the objects in a postISR exposure.
163 
164  Parameters
165  ----------
166  exp : lsst.afw.image.Exposure
167  Image in which to detect objects.
168 
169  nSigma : float
170  nSigma above image's stddev at which to set the detection threshold.
171 
172  nPixMin : int
173  Minimum number of pixels for detection.
174 
175  grow : int
176  Grow the detected footprint by this many pixels.
177 
178  Returns
179  -------
180  footPrintSet : lsst.afw.detection.FootprintSet
181  FootprintSet containing the detections.
182  """
183  threshold = afwDetect.Threshold(nSigma, afwDetect.Threshold.STDEV)
184  footPrintSet = afwDetect.FootprintSet(exp.getMaskedImage(), threshold, "DETECTED", nPixMin)
185  if grow > 0:
186  isotropic = True
187  footPrintSet = afwDetect.FootprintSet(footPrintSet, grow, isotropic)
188  return footPrintSet
189 
190  @staticmethod
191  def checkResult(exp, centroid, srcNum, percentile):
192  """Perform a final check that centroid location is actually bright.
193 
194  Parameters
195  ----------
196  exp : lsst.afw.image.Exposure
197  centroid : `tuple` of `float`
198  Location of the centroid in pixel coordinates
199 
200  scrNum : int
201  Number of the source in the source catalog. Only used if the check
202  is failed, for debug purposes.
203 
204  percentile : float
205  Image's percentile above which the pixel containing the centroid
206  must be in order to pass the check.
207 
208  Raises
209  ------
210  ValueError
211  Raised if the centroid's pixel is not above the percentile threshold
212  """
213  threshold = np.percentile(exp.image.array, percentile)
214  pixelValue = exp.image[centroid]
215  if pixelValue < threshold:
216  msg = (f"Final centroid pixel value check failed: srcNum {srcNum} at {centroid}"
217  f" has central pixel = {pixelValue:3f} <"
218  f" {percentile} percentile of image = {threshold:3f}")
219  raise ValueError(msg)
220  return
221 
222  @staticmethod
223  def _calcMedianXxYy(objData):
224  """Return the median ixx and iyy for object in the image.
225  """
226  medianXx = np.nanmedian([element['xx'] for element in objData.values()])
227  medianYy = np.nanmedian([element['xx'] for element in objData.values()])
228  return medianXx, medianYy
229 
230  def _calcBrightestObjSrcNum(self, objData):
231  """Find the brightest source which passes the cuts among the sources.
232 
233  Parameters
234  ----------
235  objData : `dict` of `dict`
236  Dictionary, keyed by source number, containing the measurements.
237 
238  Returns
239  -------
240  srcNum : int
241  The source number of the brightest source which passes the cuts.
242  """
243  max70, max70srcNum = -1, -1
244  max25, max25srcNum = -1, -1
245 
246  for srcNum in sorted(objData.keys()): # srcNum not contiguous so don't use a list comp
247  # skip flag used rather than continue statements so we have all the
248  # metrics computed for debug purposes as this task is whack-a-mole
249  skip = False
250  xx = objData[srcNum]['xx']
251  yy = objData[srcNum]['yy']
252 
253  xx = max(xx, 1e-9) # need to protect against division by zero
254  yy = max(yy, 1e-9) # because we don't `continue` on zero moments
255 
256  if self.config.doExtendednessCut:
257  if xx > self.config.maxExtendedness or yy > self.config.maxExtendedness:
258  skip = True
259 
260  nonRoundness = xx/yy
261  nonRoundness = max(nonRoundness, 1/nonRoundness)
262  if nonRoundness > self.config.maxNonRoundness:
263  skip = True
264 
265  if self.log.isDebugEnabled():
266  text = f"src {srcNum}: {objData[srcNum]['xCentroid']:.0f}, {objData[srcNum]['yCentroid']:.0f}"
267  text += f" - xx={xx:.1f}, yy={yy:.1f}, nonRound={nonRoundness:.1f}"
268  text += f" - ap70={objData[srcNum]['apFlux70']:,.0f}"
269  text += f" - ap25={objData[srcNum]['apFlux25']:,.0f}"
270  text += f" - skip={skip}"
271  self.log.debug(text)
272 
273  if skip:
274  continue
275 
276  ap70 = objData[srcNum]['apFlux70']
277  ap25 = objData[srcNum]['apFlux25']
278  if ap70 > max70:
279  max70 = ap70
280  max70srcNum = srcNum
281  if ap25 > max25:
282  max25 = ap25
283  max25srcNum = srcNum
284  if max70srcNum != max25srcNum:
285  self.log.warn("WARNING! Max apFlux70 for different object than with max apFlux25")
286 
287  if max70srcNum >= 0: # starts as -1, return None if nothing is acceptable
288  return max70srcNum
289  return None
290 
291  def _measureFp(self, fp, exp):
292  """Run the measurements on a footprint.
293 
294  Parameters
295  ----------
296  fp : lsst.afw.detection.Footprint
297  The footprint to measure.
298 
299  exp : lsst.afw.image.Exposure
300  The footprint's parent exposure.
301 
302  Returns
303  -------
304  src : lsst.afw.table.SourceRecord
305  The source record containing the measurements.
306  """
307  src = self.tabletable.makeRecord()
308  src.setFootprint(fp)
309  self.centroidercentroider.measure(src, exp)
310  self.shapershaper.measure(src, exp)
311  self.apFluxerapFluxer.measure(src, exp)
312  return src
313 
314  def _getDataFromSrcRecord(self, src):
315  """Extract the shapes and centroids from a source record.
316 
317  Parameters
318  ----------
319  src : lsst.afw.table.SourceRecord
320  The source record from which to extract the measurements.
321 
322  Returns
323  -------
324  srcData : lsst.pipe.base.Struct
325  The struct containing the extracted measurements.
326  """
327  pScale = self.plateScaleplateScale
328  xx = np.sqrt(src['base_SdssShape_xx'])*2.355*pScale # 2.355 for FWHM, pScale for platescale from exp
329  yy = np.sqrt(src['base_SdssShape_yy'])*2.355*pScale
330  xCentroid = src['base_SdssCentroid_x']
331  yCentroid = src['base_SdssCentroid_y']
332  # apFluxes available: 70, 50, 35, 25, 17, 12 9, 6, 4.5, 3
333  apFlux70 = src['aperFlux_70_0_instFlux']
334  apFlux25 = src['aperFlux_25_0_instFlux']
335  return pipeBase.Struct(xx=xx,
336  yy=yy,
337  xCentroid=xCentroid,
338  yCentroid=yCentroid,
339  apFlux70=apFlux70,
340  apFlux25=apFlux25)
341 
342  @staticmethod
343  def _getDataFromFootprintOnly(fp, exp):
344  """Get the shape, centroid and flux from a footprint.
345 
346  Parameters
347  ----------
348  fp : lsst.afw.detection.Footprint
349  The footprint to measure.
350  exp : lsst.afw.image.Exposure
351  The footprint's parent exposure.
352 
353  Returns
354  -------
355  srcData : lsst.pipe.base.Struct
356  The struct containing the extracted measurements.
357  """
358  xx = fp.getShape().getIxx()
359  yy = fp.getShape().getIyy()
360  xCentroid, yCentroid = fp.getCentroid()
361  apFlux70 = np.sum(exp[fp.getBBox()].image.array)
362  apFlux25 = np.sum(exp[fp.getBBox()].image.array)
363  return pipeBase.Struct(xx=xx,
364  yy=yy,
365  xCentroid=xCentroid,
366  yCentroid=yCentroid,
367  apFlux70=apFlux70,
368  apFlux25=apFlux25)
369 
370  @staticmethod
371  def _measurementResultToDict(measurementResult):
372  """Convenience function to repackage measurement results to a dict.
373 
374  Parameters
375  ----------
376  measurementResult : lsst.afw.table.SourceRecord
377  The source record to convert to a dict.
378 
379  Returns
380  -------
381  objData : `dict`
382  The dict containing the extracted data.
383  """
384  objData = {}
385  objData['xx'] = measurementResult.xx
386  objData['yy'] = measurementResult.yy
387  objData['xCentroid'] = measurementResult.xCentroid
388  objData['yCentroid'] = measurementResult.yCentroid
389  objData['apFlux70'] = measurementResult.apFlux70
390  objData['apFlux25'] = measurementResult.apFlux25
391  return objData
392 
393  @staticmethod
394  def _makeEmptyReturnStruct():
395  """Make the default/template return struct, with defaults to False/nan.
396 
397  Returns
398  -------
399  objData : lsst.pipe.base.Struct
400  The default template return structure.
401  """
402  result = pipeBase.Struct()
403  result.success = False
404  result.brightestObjCentroid = (np.nan, np.nan)
405  result.brightestObj_xXyY = (np.nan, np.nan)
406  result.brightestObjApFlux70 = np.nan
407  result.brightestObjApFlux25 = np.nan
408  result.medianXxYy = (np.nan, np.nan)
409  return result
410 
411  def run(self, exp, doDisplay=False):
412  """Calculate position, flux and shape of the brightest star in an image.
413 
414  Given an an assembled (and at least minimally ISRed exposure),
415  quickly and robustly calculate the centroid of the
416  brightest star in the image.
417 
418  Parameters
419  ----------
420  exp : lsst.afw.image.Exposure
421  The exposure in which to find and measure the brightest star.
422 
423  doDisplay : bool
424  Display the image and found sources. A diplay object must have
425  been passed to the task constructor.
426 
427  Returns
428  -------
429  result : lsst.pipe.base.Struct
430  Struct containing:
431  Whether the task ran successfully and found the object (bool)
432  The object's centroid (float, float)
433  The object's ixx, iyy (float, float)
434  The object's 70 pixel aperture flux (float)
435  The object's 25 pixel aperture flux (float)
436  The images's median ixx, iyy (float, float)
437  If unsuccessful, the success field is False and all other results
438  are np.nan of the expected shape.
439 
440  Notes
441  -----
442  Because of this task's involvement in observing scripts, the run method
443  should *never* raise. Failure modes are noted by returning a Struct with
444  the same structure as the success case, with all value set to np.nan and
445  result.success=False.
446  """
447  try:
448  result = self._run_run(exp=exp, doDisplay=doDisplay)
449  return result
450  except Exception as e:
451  self.log.warn(f"Failed to find main source centroid {e}")
452  result = self._makeEmptyReturnStruct_makeEmptyReturnStruct()
453  return result
454 
455  def _run(self, exp, doDisplay=False):
456  """The actual run method, called by run()
457 
458  Behaviour is documented in detail in the main run().
459  """
460  self.plateScaleplateScale = exp.getWcs().getPixelScale().asArcseconds()
461  median = np.nanmedian(exp.image.array)
462  exp.image -= median # is put back later
463  self.installPsf.run(exp)
464  sources = self.detectObjectsInExpdetectObjectsInExp(exp, nSigma=self.config.nSigmaDetection,
465  nPixMin=self.config.nPixMinDetection)
466 
467  if doDisplay:
468  if self.displaydisplay is None:
469  raise RuntimeError("Display failed as no display provided during init()")
470  self.displaydisplay.mtv(exp)
471 
472  fpSet = sources.getFootprints()
473  self.log.info(f"Found {len(fpSet)} sources in exposure")
474 
475  objData = {}
476  nMeasured = 0
477 
478  for srcNum, fp in enumerate(fpSet):
479  try:
480  src = self._measureFp_measureFp(fp, exp)
481  result = self._getDataFromSrcRecord_getDataFromSrcRecord(src)
482  except MeasurementError:
483  try:
484  # gets shape and centroid from footprint
485  result = self._getDataFromFootprintOnly_getDataFromFootprintOnly(fp, exp)
486  except MeasurementError as e:
487  self.log.info(f"Skipped measuring source {srcNum}: {e}")
488  continue
489  objData[srcNum] = self._measurementResultToDict_measurementResultToDict(result)
490  nMeasured += 1
491 
492  self.log.info(f"Measured {nMeasured} of {len(fpSet)} sources in exposure")
493 
494  medianXxYy = self._calcMedianXxYy_calcMedianXxYy(objData)
495 
496  brightestObjSrcNum = self._calcBrightestObjSrcNum_calcBrightestObjSrcNum(objData)
497  if brightestObjSrcNum is None:
498  raise RuntimeError("No sources in image passed cuts")
499 
500  x = objData[brightestObjSrcNum]['xCentroid']
501  y = objData[brightestObjSrcNum]['yCentroid']
502  brightestObjCentroid = (x, y)
503  xx = objData[brightestObjSrcNum]['xx']
504  yy = objData[brightestObjSrcNum]['yy']
505  brightestObjApFlux70 = objData[brightestObjSrcNum]['apFlux70']
506  brightestObjApFlux25 = objData[brightestObjSrcNum]['apFlux25']
507 
508  exp.image += median # put background back in
509  if self.config.doCheckCentroidPixelValue:
510  self.checkResultcheckResult(exp, brightestObjCentroid, brightestObjSrcNum,
511  self.config.centroidPixelPercentile)
512 
513  result = self._makeEmptyReturnStruct_makeEmptyReturnStruct()
514  result.success = True
515  result.brightestObjCentroid = brightestObjCentroid
516  result.brightestObj_xXyY = (xx, yy)
517  result.brightestObjApFlux70 = brightestObjApFlux70
518  result.brightestObjApFlux25 = brightestObjApFlux25
519  result.medianXxYy = medianXxYy
520  return result
int max
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:53
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:43
Class for storing generic metadata.
Definition: PropertySet.h:67
daf::base::PropertySet * set
Definition: fits.cc:912
def mtv(data, frame=None, title="", wcs=None, *args, **kwargs)
Definition: ds9.py:92
def measure(mi, x, y, size, statistic, stats)
Definition: fringe.py:512