LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
assembleCoadd.py
Go to the documentation of this file.
1 from __future__ import division, absolute_import
2 from __future__ import print_function
3 from builtins import zip
4 from builtins import range
5 #
6 # LSST Data Management System
7 # Copyright 2008-2016 AURA/LSST.
8 #
9 # This product includes software developed by the
10 # LSST Project (http://www.lsst.org/).
11 #
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the LSST License Statement and
23 # the GNU General Public License along with this program. If not,
24 # see <https://www.lsstcorp.org/LegalNotices/>.
25 #
26 import numpy
27 import lsst.pex.config as pexConfig
28 import lsst.pex.exceptions as pexExceptions
29 import lsst.afw.detection as afwDetect
30 import lsst.afw.geom as afwGeom
31 import lsst.afw.image as afwImage
32 import lsst.afw.math as afwMath
33 import lsst.afw.table as afwTable
34 import lsst.afw.detection as afwDet
35 import lsst.coadd.utils as coaddUtils
36 import lsst.pipe.base as pipeBase
37 import lsst.meas.algorithms as measAlg
38 from .coaddBase import CoaddBaseTask, SelectDataIdContainer
39 from .interpImage import InterpImageTask
40 from .matchBackgrounds import MatchBackgroundsTask
41 from .scaleZeroPoint import ScaleZeroPointTask
42 from .coaddHelpers import groupPatchExposures, getGroupDataRef
43 from lsst.meas.algorithms import SourceDetectionTask
44 
45 __all__ = ["AssembleCoaddTask", "SafeClipAssembleCoaddTask"]
46 
47 
48 class AssembleCoaddConfig(CoaddBaseTask.ConfigClass):
49  """!
50 \anchor AssembleCoaddConfig_
51 
52 \brief Configuration parameters for the \ref AssembleCoaddTask_ "AssembleCoaddTask"
53  """
54  subregionSize = pexConfig.ListField(
55  dtype=int,
56  doc="Width, height of stack subregion size; "
57  "make small enough that a full stack of images will fit into memory at once.",
58  length=2,
59  default=(2000, 2000),
60  )
61  doSigmaClip = pexConfig.Field(
62  dtype=bool,
63  doc="Perform sigma clipped outlier rejection? If False then compute a simple mean.",
64  default=True,
65  )
66  sigmaClip = pexConfig.Field(
67  dtype=float,
68  doc="Sigma for outlier rejection; ignored if doSigmaClip false.",
69  default=3.0,
70  )
71  clipIter = pexConfig.Field(
72  dtype=int,
73  doc="Number of iterations of outlier rejection; ignored if doSigmaClip false.",
74  default=2,
75  )
76  scaleZeroPoint = pexConfig.ConfigurableField(
77  target=ScaleZeroPointTask,
78  doc="Task to adjust the photometric zero point of the coadd temp exposures",
79  )
80  doInterp = pexConfig.Field(
81  doc="Interpolate over NaN pixels? Also extrapolate, if necessary, but the results are ugly.",
82  dtype=bool,
83  default=True,
84  )
85  interpImage = pexConfig.ConfigurableField(
86  target=InterpImageTask,
87  doc="Task to interpolate (and extrapolate) over NaN pixels",
88  )
89  matchBackgrounds = pexConfig.ConfigurableField(
90  target=MatchBackgroundsTask,
91  doc="Task to match backgrounds",
92  )
93  maxMatchResidualRatio = pexConfig.Field(
94  doc="Maximum ratio of the mean squared error of the background matching model to the variance "
95  "of the difference in backgrounds",
96  dtype=float,
97  default=1.1
98  )
99  maxMatchResidualRMS = pexConfig.Field(
100  doc="Maximum RMS of residuals of the background offset fit in matchBackgrounds.",
101  dtype=float,
102  default=1.0
103  )
104  doWrite = pexConfig.Field(
105  doc="Persist coadd?",
106  dtype=bool,
107  default=True,
108  )
109  doMatchBackgrounds = pexConfig.Field(
110  doc="Match backgrounds of coadd temp exposures before coadding them? "
111  "If False, the coadd temp expsosures must already have been background subtracted or matched",
112  dtype=bool,
113  default=True,
114  )
115  autoReference = pexConfig.Field(
116  doc="Automatically select the coadd temp exposure to use as a reference for background matching? "
117  "Ignored if doMatchBackgrounds false. "
118  "If False you must specify the reference temp exposure as the data Id",
119  dtype=bool,
120  default=True,
121  )
122  maskPropagationThresholds = pexConfig.DictField(
123  keytype=str,
124  itemtype=float,
125  doc=("Threshold (in fractional weight) of rejection at which we propagate a mask plane to "
126  "the coadd; that is, we set the mask bit on the coadd if the fraction the rejected frames "
127  "would have contributed exceeds this value."),
128  default={"SAT": 0.1},
129  )
130  removeMaskPlanes = pexConfig.ListField(dtype=str, default=["CROSSTALK", "NOT_DEBLENDED"],
131  doc="Mask planes to remove before coadding")
132  #
133  # N.b. These configuration options only set the bitplane config.brightObjectMaskName
134  # To make this useful you *must* also configure the flags.pixel algorithm, for example
135  # by adding
136  # config.measurement.plugins["base_PixelFlags"].masksFpCenter.append("BRIGHT_OBJECT")
137  # config.measurement.plugins["base_PixelFlags"].masksFpAnywhere.append("BRIGHT_OBJECT")
138  # to your measureCoaddSources.py and forcedPhotCoadd.py config overrides
139  #
140  doMaskBrightObjects = pexConfig.Field(dtype=bool, default=False,
141  doc="Set mask and flag bits for bright objects?")
142  brightObjectMaskName = pexConfig.Field(dtype=str, default="BRIGHT_OBJECT",
143  doc="Name of mask bit used for bright objects")
144 
145  def setDefaults(self):
146  CoaddBaseTask.ConfigClass.setDefaults(self)
147  self.badMaskPlanes = ["NO_DATA", "BAD", "CR", ]
148 
149 
150 # \addtogroup LSST_task_documentation
151 # \{
152 # \page AssembleCoaddTask
153 # \ref AssembleCoaddTask_ "AssembleCoaddTask"
154 # \copybrief AssembleCoaddTask
155 # \}
156 
157 class AssembleCoaddTask(CoaddBaseTask):
158  """!
159 \anchor AssembleCoaddTask_
160 
161 \brief Assemble a coadded image from a set of coadded temporary exposures.
162 
163 \section pipe_tasks_assembleCoadd_Contents Contents
164  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Purpose
165  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Initialize
166  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Run
167  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Config
168  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Debug
169  - \ref pipe_tasks_assembleCoadd_AssembleCoaddTask_Example
170 
171 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Purpose Description
172 
173 \copybrief AssembleCoaddTask_
174 
175 We want to assemble a coadded image from a set of coadded temporary exposures (coaddTempExps).
176 Each input coaddTempExp covers a patch on the sky and corresponds to a single run/visit/exposure of the
177 covered patch. We provide the task with a list of coaddTempExps (selectDataList) from which it selects
178 coaddTempExps that cover the specified patch (pointed at by dataRef).
179 Each coaddTempExp that goes into a coadd will typically have an independent photometric zero-point.
180 Therefore, we must scale each coaddTempExp to set it to a common photometric zeropoint. By default, each
181 coaddTempExp has backgrounds and hence will require config.doMatchBackgrounds=True.
182 When background matching is enabled, the task may be configured to automatically select a reference exposure
183 (config.autoReference=True). If this is not done, we require that the input dataRef provides access to a
184 coaddTempExp (dataset type coaddName + 'Coadd_tempExp') which is used as the reference exposure.
185 The coadd is computed as a mean with optional outlier rejection.
186 Criteria for outlier rejection are set in \ref AssembleCoaddConfig. Finally, coaddTempExps can have bad 'NaN'
187 pixels which received no input from the source calExps. We interpolate over these bad (NaN) pixels.
188 
189 AssembleCoaddTask uses several sub-tasks. These are
190 <DL>
191  <DT>\ref ScaleZeroPointTask_ "ScaleZeroPointTask"</DT>
192  <DD> create and use an imageScaler object to scale the photometric zeropoint for each coaddTempExp</DD>
193  <DT>\ref MatchBackgroundsTask_ "MatchBackgroundsTask"</DT>
194  <DD> match background in a coaddTempExp to a reference exposure (and select the reference exposure if one is
195  not provided).</DD>
196  <DT>\ref InterpImageTask_ "InterpImageTask"</DT>
197  <DD>interpolate across bad pixels (NaN) in the final coadd</DD>
198 </DL>
199 You can retarget these subtasks if you wish.
200 
201 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Initialize Task initialization
202 \copydoc \_\_init\_\_
203 
204 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Run Invoking the Task
205 \copydoc run
206 
207 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Config Configuration parameters
208 See \ref AssembleCoaddConfig_
209 
210 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Debug Debug variables
211 The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
212 flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
213 AssembleCoaddTask has no debug variables of its own. Some of the subtasks may support debug variables. See
214 the documetation for the subtasks for further information.
215 
216 \section pipe_tasks_assembleCoadd_AssembleCoaddTask_Example A complete example of using AssembleCoaddTask
217 
218 AssembleCoaddTask assembles a set of warped coaddTempExp images into a coadded image. The AssembleCoaddTask
219 can be invoked by running assembleCoadd.py with the flag '--legacyCoadd'. Usage of assembleCoadd.py expects
220 a data reference to the tract patch and filter to be coadded (specified using
221 '--id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]') along with a list of
222 coaddTempExps to attempt to coadd (specified using
223 '--selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]'). Only the coaddTempExps
224 that cover the specified tract and patch will be coadded. A list of the available optional
225 arguments can be obtained by calling assembleCoadd.py with the --help command line argument:
226 \code
227 assembleCoadd.py --help
228 \endcode
229 To demonstrate usage of the AssembleCoaddTask in the larger context of multi-band processing, we will generate
230 the HSC-I & -R band coadds from HSC engineering test data provided in the ci_hsc package. To begin, assuming
231 that the lsst stack has been already set up, we must set up the obs_subaru and ci_hsc packages.
232 This defines the environment variable $CI_HSC_DIR and points at the location of the package. The raw HSC
233 data live in the $CI_HSC_DIR/raw directory. To begin assembling the coadds, we must first
234 <DL>
235  <DT>processCcd</DT>
236  <DD> process the individual ccds in $CI_HSC_RAW to produce calibrated exposures</DD>
237  <DT>makeSkyMap</DT>
238  <DD> create a skymap that covers the area of the sky present in the raw exposures</DD>
239  <DT>makeCoaddTempExp</DT>
240  <DD> warp the individual calibrated exposures to the tangent plane of the coadd</DD>
241 </DL>
242 We can perform all of these steps by running
243 \code
244 $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
245 \endcode
246 This will produce warped coaddTempExps for each visit. To coadd the warped data, we call assembleCoadd.py as
247 follows:
248 \code
249 assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 --selectId visit=903986 ccd=100 --selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 --selectId visit=903988 ccd=24\endcode
250 that will process the HSC-I band data. The results are written in $CI_HSC_DIR/DATA/deepCoadd-results/HSC-I
251 You may also choose to run:
252 \code
253 scons warp-903334 warp-903336 warp-903338 warp-903342 warp-903344 warp-903346
254 assembleCoadd.py --legacyCoadd $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R --selectId visit=903334 ccd=16 --selectId visit=903334 ccd=22 --selectId visit=903334 ccd=23 --selectId visit=903334 ccd=100 --selectId visit=903336 ccd=17 --selectId visit=903336 ccd=24 --selectId visit=903338 ccd=18 --selectId visit=903338 ccd=25 --selectId visit=903342 ccd=4 --selectId visit=903342 ccd=10 --selectId visit=903342 ccd=100 --selectId visit=903344 ccd=0 --selectId visit=903344 ccd=5 --selectId visit=903344 ccd=11 --selectId visit=903346 ccd=1 --selectId visit=903346 ccd=6 --selectId visit=903346 ccd=12
255 \endcode
256 to generate the coadd for the HSC-R band if you are interested in following multiBand Coadd processing as
257 discussed in \ref pipeTasks_multiBand (but note that normally, one would use the
258 \ref SafeClipAssembleCoaddTask_ "SafeClipAssembleCoaddTask" rather than AssembleCoaddTask to make the coadd.
259  """
260  ConfigClass = AssembleCoaddConfig
261  _DefaultName = "assembleCoadd"
262 
263  def __init__(self, *args, **kwargs):
264  """!
265  \brief Initialize the task. Create the \ref InterpImageTask "interpImage",
266  \ref MatchBackgroundsTask "matchBackgrounds", & \ref ScaleZeroPointTask "scaleZeroPoint" subtasks.
267  """
268  CoaddBaseTask.__init__(self, *args, **kwargs)
269  self.makeSubtask("interpImage")
270  self.makeSubtask("matchBackgrounds")
271  self.makeSubtask("scaleZeroPoint")
272 
273  if self.config.doMaskBrightObjects:
274  mask = afwImage.MaskU()
275  try:
276  self.brightObjectBitmask = 1 << mask.addMaskPlane(self.config.brightObjectMaskName)
277  except pexExceptions.LsstCppException:
278  raise RuntimeError("Unable to define mask plane for bright objects; planes used are %s" %
279  mask.getMaskPlaneDict().keys())
280  del mask
281 
282  @pipeBase.timeMethod
283  def run(self, dataRef, selectDataList=[]):
284  """!
285  \brief Assemble a coadd from a set of coaddTempExp
286 
287  Coadd a set of coaddTempExps. Compute weights to be applied to each coaddTempExp and find scalings to
288  match the photometric zeropoint to a reference coaddTempExp. Optionally, match backgrounds across
289  coaddTempExps if the background has not already been removed. Assemble the coaddTempExps using
290  \ref assemble. Interpolate over NaNs and optionally write the coadd to disk. Return the coadded
291  exposure.
292 
293  \anchor runParams
294  \param[in] dataRef: Data reference defining the patch for coaddition and the reference coaddTempExp
295  (if config.autoReference=False). Used to access the following data products:
296  - [in] self.config.coaddName + "Coadd_skyMap"
297  - [in] self.config.coaddName + "Coadd_tempExp" (optionally)
298  - [out] self.config.coaddName + "Coadd"
299  \param[in] selectDataList[in]: List of data references to coaddTempExps. Data to be coadded will be
300  selected from this list based on overlap with the patch defined by dataRef.
301 
302  \return a pipeBase.Struct with fields:
303  - coaddExposure: coadded exposure
304  """
305  skyInfo = self.getSkyInfo(dataRef)
306  calExpRefList = self.selectExposures(dataRef, skyInfo, selectDataList=selectDataList)
307  if len(calExpRefList) == 0:
308  self.log.warn("No exposures to coadd")
309  return
310  self.log.info("Coadding %d exposures", len(calExpRefList))
311 
312  tempExpRefList = self.getTempExpRefList(dataRef, calExpRefList)
313  inputData = self.prepareInputs(tempExpRefList)
314  self.log.info("Found %d %s", len(inputData.tempExpRefList), self.getTempExpDatasetName())
315  if len(inputData.tempExpRefList) == 0:
316  self.log.warn("No coadd temporary exposures found")
317  return
318  if self.config.doMatchBackgrounds:
319  refImageScaler = self.getBackgroundReferenceScaler(dataRef)
320  inputData = self.backgroundMatching(inputData, dataRef, refImageScaler)
321  if len(inputData.tempExpRefList) == 0:
322  self.log.warn("No valid background models")
323  return
324 
325  coaddExp = self.assemble(skyInfo, inputData.tempExpRefList, inputData.imageScalerList,
326  inputData.weightList,
327  inputData.backgroundInfoList if self.config.doMatchBackgrounds else None,
328  doClip=self.config.doSigmaClip)
329  if self.config.doMatchBackgrounds:
330  self.addBackgroundMatchingMetadata(coaddExp, inputData.tempExpRefList,
331  inputData.backgroundInfoList)
332 
333  if self.config.doInterp:
334  self.interpImage.run(coaddExp.getMaskedImage(), planeName="NO_DATA")
335  # The variance must be positive; work around for DM-3201.
336  varArray = coaddExp.getMaskedImage().getVariance().getArray()
337  varArray[:] = numpy.where(varArray > 0, varArray, numpy.inf)
338 
339  if self.config.doMaskBrightObjects:
340  brightObjectMasks = self.readBrightObjectMasks(dataRef)
341  self.setBrightObjectMasks(coaddExp, dataRef.dataId, brightObjectMasks)
342 
343  if self.config.doWrite:
344  self.writeCoaddOutput(dataRef, coaddExp)
345 
346  return pipeBase.Struct(coaddExposure=coaddExp)
347 
348  def getTempExpRefList(self, patchRef, calExpRefList):
349  """!
350  \brief Generate list of coaddTempExp data references corresponding to exposures that lie within the
351  patch to be coadded.
352 
353  \param[in] patchRef: Data reference for patch
354  \param[in] calExpRefList: List of data references for input calexps
355  \return List of coaddTempExp data references
356  """
357  butler = patchRef.getButler()
358  groupData = groupPatchExposures(patchRef, calExpRefList, self.getCoaddDatasetName(),
359  self.getTempExpDatasetName())
360  tempExpRefList = [getGroupDataRef(butler, self.getTempExpDatasetName(), g, groupData.keys) for
361  g in groupData.groups.keys()]
362  return tempExpRefList
363 
364  def getBackgroundReferenceScaler(self, dataRef):
365  """!
366  \brief Construct an image scaler for the background reference frame
367 
368  Each coaddTempExp has a different background level. A reference background level must be chosen before
369  coaddition. If config.autoReference=True, \ref backgroundMatching will pick the reference level and
370  this routine is a no-op and None is returned. Otherwise, use the
371  \ref ScaleZeroPointTask_ "scaleZeroPoint" subtask to compute an imageScaler object for the provided
372  reference image and return it.
373 
374  \param[in] dataRef: Data reference for the background reference frame, or None
375  \return image scaler, or None
376  """
377  if self.config.autoReference:
378  return None
379 
380  # We've been given the data reference
381  dataset = self.getTempExpDatasetName()
382  if not dataRef.datasetExists(dataset):
383  raise RuntimeError("Could not find reference exposure %s %s." % (dataset, dataRef.dataId))
384 
385  refExposure = dataRef.get(self.getTempExpDatasetName(), immediate=True)
386  refImageScaler = self.scaleZeroPoint.computeImageScaler(
387  exposure=refExposure,
388  dataRef=dataRef,
389  )
390  return refImageScaler
391 
392  def prepareInputs(self, refList):
393  """!
394  \brief Prepare the input warps for coaddition by measuring the weight for each warp and the scaling
395  for the photometric zero point.
396 
397  Each coaddTempExp has its own photometric zeropoint and background variance. Before coadding these
398  coaddTempExps together, compute a scale factor to normalize the photometric zeropoint and compute the
399  weight for each coaddTempExp.
400 
401  \param[in] refList: List of data references to tempExp
402  \return Struct:
403  - tempExprefList: List of data references to tempExp
404  - weightList: List of weightings
405  - imageScalerList: List of image scalers
406  """
407  statsCtrl = afwMath.StatisticsControl()
408  statsCtrl.setNumSigmaClip(self.config.sigmaClip)
409  statsCtrl.setNumIter(self.config.clipIter)
410  statsCtrl.setAndMask(self.getBadPixelMask())
411  statsCtrl.setNanSafe(True)
412 
413  # compute tempExpRefList: a list of tempExpRef that actually exist
414  # and weightList: a list of the weight of the associated coadd tempExp
415  # and imageScalerList: a list of scale factors for the associated coadd tempExp
416  tempExpRefList = []
417  weightList = []
418  imageScalerList = []
419  tempExpName = self.getTempExpDatasetName()
420  for tempExpRef in refList:
421  if not tempExpRef.datasetExists(tempExpName):
422  self.log.warn("Could not find %s %s; skipping it", tempExpName, tempExpRef.dataId)
423  continue
424 
425  tempExp = tempExpRef.get(tempExpName, immediate=True)
426  maskedImage = tempExp.getMaskedImage()
427  imageScaler = self.scaleZeroPoint.computeImageScaler(
428  exposure=tempExp,
429  dataRef=tempExpRef,
430  )
431  try:
432  imageScaler.scaleMaskedImage(maskedImage)
433  except Exception as e:
434  self.log.warn("Scaling failed for %s (skipping it): %s", tempExpRef.dataId, e)
435  continue
436  statObj = afwMath.makeStatistics(maskedImage.getVariance(), maskedImage.getMask(),
437  afwMath.MEANCLIP, statsCtrl)
438  meanVar, meanVarErr = statObj.getResult(afwMath.MEANCLIP)
439  weight = 1.0 / float(meanVar)
440  if not numpy.isfinite(weight):
441  self.log.warn("Non-finite weight for %s: skipping", tempExpRef.dataId)
442  continue
443  self.log.info("Weight of %s %s = %0.3f", tempExpName, tempExpRef.dataId, weight)
444 
445  del maskedImage
446  del tempExp
447 
448  tempExpRefList.append(tempExpRef)
449  weightList.append(weight)
450  imageScalerList.append(imageScaler)
451 
452  return pipeBase.Struct(tempExpRefList=tempExpRefList, weightList=weightList,
453  imageScalerList=imageScalerList)
454 
455  def backgroundMatching(self, inputData, refExpDataRef=None, refImageScaler=None):
456  """!
457  \brief Perform background matching on the prepared inputs
458 
459  Each coaddTempExp has a different background level that must be normalized to a reference level
460  before coaddition. If no reference is provided, the background matcher selects one. If the background
461  matching is performed sucessfully, recompute the weight to be applied to the coaddTempExp to be
462  consistent with the scaled background.
463 
464  \param[in] inputData: Struct from prepareInputs() with tempExpRefList, weightList, imageScalerList
465  \param[in] refExpDataRef: Data reference for background reference tempExp, or None
466  \param[in] refImageScaler: Image scaler for background reference tempExp, or None
467  \return Struct:
468  - tempExprefList: List of data references to tempExp
469  - weightList: List of weightings
470  - imageScalerList: List of image scalers
471  - backgroundInfoList: result from background matching
472  """
473  try:
474  backgroundInfoList = self.matchBackgrounds.run(
475  expRefList=inputData.tempExpRefList,
476  imageScalerList=inputData.imageScalerList,
477  refExpDataRef=refExpDataRef if not self.config.autoReference else None,
478  refImageScaler=refImageScaler,
479  expDatasetType=self.getTempExpDatasetName(),
480  ).backgroundInfoList
481  except Exception as e:
482  self.log.fatal("Cannot match backgrounds: %s", e)
483  raise pipeBase.TaskError("Background matching failed.")
484 
485  newWeightList = []
486  newTempExpRefList = []
487  newBackgroundStructList = []
488  newScaleList = []
489  # the number of good backgrounds may be < than len(tempExpList)
490  # sync these up and correct the weights
491  for tempExpRef, bgInfo, scaler, weight in zip(inputData.tempExpRefList, backgroundInfoList,
492  inputData.imageScalerList, inputData.weightList):
493  if not bgInfo.isReference:
494  # skip exposure if it has no backgroundModel
495  # or if fit was bad
496  if (bgInfo.backgroundModel is None):
497  self.log.info("No background offset model available for %s: skipping", tempExpRef.dataId)
498  continue
499  try:
500  varianceRatio = bgInfo.matchedMSE / bgInfo.diffImVar
501  except Exception as e:
502  self.log.info("MSE/Var ratio not calculable (%s) for %s: skipping",
503  e, tempExpRef.dataId)
504  continue
505  if not numpy.isfinite(varianceRatio):
506  self.log.info("MSE/Var ratio not finite (%.2f / %.2f) for %s: skipping",
507  bgInfo.matchedMSE, bgInfo.diffImVar, tempExpRef.dataId)
508  continue
509  elif (varianceRatio > self.config.maxMatchResidualRatio):
510  self.log.info("Bad fit. MSE/Var ratio %.2f > %.2f for %s: skipping",
511  varianceRatio, self.config.maxMatchResidualRatio, tempExpRef.dataId)
512  continue
513  elif (bgInfo.fitRMS > self.config.maxMatchResidualRMS):
514  self.log.info("Bad fit. RMS %.2f > %.2f for %s: skipping",
515  bgInfo.fitRMS, self.config.maxMatchResidualRMS, tempExpRef.dataId)
516  continue
517  newWeightList.append(1 / (1 / weight + bgInfo.fitRMS**2))
518  newTempExpRefList.append(tempExpRef)
519  newBackgroundStructList.append(bgInfo)
520  newScaleList.append(scaler)
521 
522  return pipeBase.Struct(tempExpRefList=newTempExpRefList, weightList=newWeightList,
523  imageScalerList=newScaleList, backgroundInfoList=newBackgroundStructList)
524 
525  def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgInfoList=None,
526  altMaskList=None, doClip=False, mask=None):
527  """!
528  \anchor AssembleCoaddTask.assemble_
529 
530  \brief Assemble a coadd from input warps
531 
532  Assemble the coadd using the provided list of coaddTempExps. Since the full coadd covers a patch (a
533  large area), the assembly is performed over small areas on the image at a time in order to
534  conserve memory usage. Iterate over subregions within the outer bbox of the patch using
535  \ref assembleSubregion to mean-stack the corresponding subregions from the coaddTempExps (with outlier
536  rejection if config.doSigmaClip=True). Set the edge bits the the coadd mask based on the weight map.
537 
538  \param[in] skyInfo: Patch geometry information, from getSkyInfo
539  \param[in] tempExpRefList: List of data references to tempExp
540  \param[in] imageScalerList: List of image scalers
541  \param[in] weightList: List of weights
542  \param[in] bgInfoList: List of background data from background matching, or None
543  \param[in] altMaskList: List of alternate masks to use rather than those stored with tempExp, or None
544  \param[in] doClip: Use clipping when codding?
545  \param[in] mask: Mask to ignore when coadding
546  \return coadded exposure
547  """
548  tempExpName = self.getTempExpDatasetName()
549  self.log.info("Assembling %s %s", len(tempExpRefList), tempExpName)
550  if mask is None:
551  mask = self.getBadPixelMask()
552 
553  statsCtrl = afwMath.StatisticsControl()
554  statsCtrl.setNumSigmaClip(self.config.sigmaClip)
555  statsCtrl.setNumIter(self.config.clipIter)
556  statsCtrl.setAndMask(mask)
557  statsCtrl.setNanSafe(True)
558  statsCtrl.setWeighted(True)
559  statsCtrl.setCalcErrorFromInputVariance(True)
560  for plane, threshold in self.config.maskPropagationThresholds.items():
561  bit = afwImage.MaskU.getMaskPlane(plane)
562  statsCtrl.setMaskPropagationThreshold(bit, threshold)
563 
564  if doClip:
565  statsFlags = afwMath.MEANCLIP
566  else:
567  statsFlags = afwMath.MEAN
568 
569  if bgInfoList is None:
570  bgInfoList = [None]*len(tempExpRefList)
571 
572  if altMaskList is None:
573  altMaskList = [None]*len(tempExpRefList)
574 
575  coaddExposure = afwImage.ExposureF(skyInfo.bbox, skyInfo.wcs)
576  coaddExposure.setCalib(self.scaleZeroPoint.getCalib())
577  coaddExposure.getInfo().setCoaddInputs(self.inputRecorder.makeCoaddInputs())
578  self.assembleMetadata(coaddExposure, tempExpRefList, weightList)
579  coaddMaskedImage = coaddExposure.getMaskedImage()
580  subregionSizeArr = self.config.subregionSize
581  subregionSize = afwGeom.Extent2I(subregionSizeArr[0], subregionSizeArr[1])
582  for subBBox in _subBBoxIter(skyInfo.bbox, subregionSize):
583  try:
584  self.assembleSubregion(coaddExposure, subBBox, tempExpRefList, imageScalerList,
585  weightList, bgInfoList, altMaskList, statsFlags, statsCtrl)
586  except Exception as e:
587  self.log.fatal("Cannot compute coadd %s: %s", subBBox, e)
588 
589  coaddUtils.setCoaddEdgeBits(coaddMaskedImage.getMask(), coaddMaskedImage.getVariance())
590 
591  return coaddExposure
592 
593  def assembleMetadata(self, coaddExposure, tempExpRefList, weightList):
594  """!
595  \brief Set the metadata for the coadd
596 
597  This basic implementation simply sets the filter from the
598  first input.
599 
600  \param[in] coaddExposure: The target image for the coadd
601  \param[in] tempExpRefList: List of data references to tempExp
602  \param[in] weightList: List of weights
603  """
604  assert len(tempExpRefList) == len(weightList), "Length mismatch"
605  tempExpName = self.getTempExpDatasetName()
606  # We load a single pixel of each coaddTempExp, because we just want to get at the metadata
607  # (and we need more than just the PropertySet that contains the header), which is not possible
608  # with the current butler (see #2777).
609  tempExpList = [tempExpRef.get(tempExpName + "_sub",
611  imageOrigin="LOCAL", immediate=True) for tempExpRef in tempExpRefList]
612  numCcds = sum(len(tempExp.getInfo().getCoaddInputs().ccds) for tempExp in tempExpList)
613 
614  coaddExposure.setFilter(tempExpList[0].getFilter())
615  coaddInputs = coaddExposure.getInfo().getCoaddInputs()
616  coaddInputs.ccds.reserve(numCcds)
617  coaddInputs.visits.reserve(len(tempExpList))
618 
619  for tempExp, weight in zip(tempExpList, weightList):
620  self.inputRecorder.addVisitToCoadd(coaddInputs, tempExp, weight)
621  coaddInputs.visits.sort()
622  if self.config.doPsfMatch:
623  psf = self.config.modelPsf.apply()
624  else:
625  psf = measAlg.CoaddPsf(coaddInputs.ccds, coaddExposure.getWcs())
626  coaddExposure.setPsf(psf)
627  apCorrMap = measAlg.makeCoaddApCorrMap(coaddInputs.ccds, coaddExposure.getBBox(afwImage.PARENT),
628  coaddExposure.getWcs())
629  coaddExposure.getInfo().setApCorrMap(apCorrMap)
630 
631  def assembleSubregion(self, coaddExposure, bbox, tempExpRefList, imageScalerList, weightList,
632  bgInfoList, altMaskList, statsFlags, statsCtrl):
633  """!
634  \brief Assemble the coadd for a sub-region.
635 
636  For each coaddTempExp, check for (and swap in) an alternative mask if one is passed. If background
637  matching is enabled, add the background and background variance from each coaddTempExp. Remove mask
638  planes listed in config.removeMaskPlanes, Finally, mean-stack
639  the actual exposures using \ref afwMath.statisticsStack "statisticsStack" with outlier rejection if
640  config.doSigmaClip=True. Assign the stacked subregion back to the coadd.
641 
642  \param[in] coaddExposure: The target image for the coadd
643  \param[in] bbox: Sub-region to coadd
644  \param[in] tempExpRefList: List of data reference to tempExp
645  \param[in] imageScalerList: List of image scalers
646  \param[in] weightList: List of weights
647  \param[in] bgInfoList: List of background data from background matching
648  \param[in] altMaskList: List of alternate masks to use rather than those stored with tempExp, or None
649  \param[in] statsFlags: Statistic for coadd
650  \param[in] statsCtrl: Statistics control object for coadd
651  """
652  self.log.debug("Computing coadd over %s", bbox)
653  tempExpName = self.getTempExpDatasetName()
654  coaddMaskedImage = coaddExposure.getMaskedImage()
655  maskedImageList = afwImage.vectorMaskedImageF() # [] is rejected by afwMath.statisticsStack
656  for tempExpRef, imageScaler, bgInfo, altMask in zip(tempExpRefList, imageScalerList, bgInfoList,
657  altMaskList):
658  exposure = tempExpRef.get(tempExpName + "_sub", bbox=bbox)
659  maskedImage = exposure.getMaskedImage()
660 
661  if altMask:
662  altMaskSub = altMask.Factory(altMask, bbox, afwImage.PARENT)
663  maskedImage.getMask().swap(altMaskSub)
664  imageScaler.scaleMaskedImage(maskedImage)
665 
666  if self.config.doMatchBackgrounds and not bgInfo.isReference:
667  backgroundModel = bgInfo.backgroundModel
668  backgroundImage = backgroundModel.getImage() if \
669  self.matchBackgrounds.config.usePolynomial else \
670  backgroundModel.getImageF()
671  backgroundImage.setXY0(coaddMaskedImage.getXY0())
672  maskedImage += backgroundImage.Factory(backgroundImage, bbox, afwImage.PARENT, False)
673  var = maskedImage.getVariance()
674  var += (bgInfo.fitRMS)**2
675 
676  if self.config.removeMaskPlanes:
677  mask = maskedImage.getMask()
678  for maskPlane in self.config.removeMaskPlanes:
679  try:
680  mask &= ~mask.getPlaneBitMask(maskPlane)
681  except Exception as e:
682  self.log.warn("Unable to remove mask plane %s: %s", maskPlane, e.message)
683 
684  maskedImageList.append(maskedImage)
685 
686  with self.timer("stack"):
687  coaddSubregion = afwMath.statisticsStack(
688  maskedImageList, statsFlags, statsCtrl, weightList)
689 
690  coaddMaskedImage.assign(coaddSubregion, bbox)
691 
692  def addBackgroundMatchingMetadata(self, coaddExposure, tempExpRefList, backgroundInfoList):
693  """!
694  \brief Add metadata from the background matching to the coadd
695 
696  \param[in] coaddExposure: Coadd
697  \param[in] tempExpRefList: List of data references for temp exps to go into coadd
698  \param[in] backgroundInfoList: List of background info, results from background matching
699  """
700  self.log.info("Adding exposure information to metadata")
701  metadata = coaddExposure.getMetadata()
702  metadata.addString("CTExp_SDQA1_DESCRIPTION",
703  "Background matching: Ratio of matchedMSE / diffImVar")
704  for ind, (tempExpRef, backgroundInfo) in enumerate(zip(tempExpRefList, backgroundInfoList)):
705  tempExpStr = '&'.join('%s=%s' % (k, v) for k, v in tempExpRef.dataId.items())
706  if backgroundInfo.isReference:
707  metadata.addString("ReferenceExp_ID", tempExpStr)
708  else:
709  metadata.addString("CTExp_ID_%d" % (ind), tempExpStr)
710  metadata.addDouble("CTExp_SDQA1_%d" % (ind),
711  backgroundInfo.matchedMSE/backgroundInfo.diffImVar)
712  metadata.addDouble("CTExp_SDQA2_%d" % (ind),
713  backgroundInfo.fitRMS)
714 
715  def readBrightObjectMasks(self, dataRef):
716  """Returns None on failure"""
717  try:
718  return dataRef.get("brightObjectMask", immediate=True)
719  except Exception as e:
720  self.log.warn("Unable to read brightObjectMask for %s: %s", dataRef.dataId, e)
721  return None
722 
723  def setBrightObjectMasks(self, exposure, dataId, brightObjectMasks):
724  """Set the bright object masks
725 
726  exposure: Exposure under consideration
727  dataId: Data identifier dict for patch
728  brightObjectMasks: afwTable of bright objects to mask
729  """
730  #
731  # Check the metadata specifying the tract/patch/filter
732  #
733  if brightObjectMasks is None:
734  self.log.warn("Unable to apply bright object mask: none supplied")
735  return
736  self.log.info("Applying %d bright object masks to %s", len(brightObjectMasks), dataId)
737  md = brightObjectMasks.table.getMetadata()
738  for k in dataId:
739  if not md.exists(k):
740  self.log.warn("Expected to see %s in metadata", k)
741  else:
742  if md.get(k) != dataId[k]:
743  self.log.warn("Expected to see %s == %s in metadata, saw %s", k, md.get(k), dataId[k])
744 
745  mask = exposure.getMaskedImage().getMask()
746  wcs = exposure.getWcs()
747  plateScale = wcs.pixelScale().asArcseconds()
748 
749  for rec in brightObjectMasks:
750  center = afwGeom.PointI(wcs.skyToPixel(rec.getCoord()))
751  radius = rec["radius"].asArcseconds()/plateScale # convert to pixels
752 
753  foot = afwDetect.Footprint(center, radius, exposure.getBBox())
755 
756  @classmethod
758  """!
759  \brief Create an argument parser
760  """
761  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
762  parser.add_id_argument("--id", cls.ConfigClass().coaddName + "Coadd_tempExp",
763  help="data ID, e.g. --id tract=12345 patch=1,2",
764  ContainerClass=AssembleCoaddDataIdContainer)
765  parser.add_id_argument("--selectId", "calexp", help="data ID, e.g. --selectId visit=6789 ccd=0..9",
766  ContainerClass=SelectDataIdContainer)
767  return parser
768 
769 
770 def _subBBoxIter(bbox, subregionSize):
771  """!
772  \brief Iterate over subregions of a bbox
773 
774  \param[in] bbox: bounding box over which to iterate: afwGeom.Box2I
775  \param[in] subregionSize: size of sub-bboxes
776 
777  \return subBBox: next sub-bounding box of size subregionSize or smaller;
778  each subBBox is contained within bbox, so it may be smaller than subregionSize at the edges of bbox,
779  but it will never be empty
780  """
781  if bbox.isEmpty():
782  raise RuntimeError("bbox %s is empty" % (bbox,))
783  if subregionSize[0] < 1 or subregionSize[1] < 1:
784  raise RuntimeError("subregionSize %s must be nonzero" % (subregionSize,))
785 
786  for rowShift in range(0, bbox.getHeight(), subregionSize[1]):
787  for colShift in range(0, bbox.getWidth(), subregionSize[0]):
788  subBBox = afwGeom.Box2I(bbox.getMin() + afwGeom.Extent2I(colShift, rowShift), subregionSize)
789  subBBox.clip(bbox)
790  if subBBox.isEmpty():
791  raise RuntimeError("Bug: empty bbox! bbox=%s, subregionSize=%s, colShift=%s, rowShift=%s" %
792  (bbox, subregionSize, colShift, rowShift))
793  yield subBBox
794 
795 
796 class AssembleCoaddDataIdContainer(pipeBase.DataIdContainer):
797  """!
798  \brief A version of lsst.pipe.base.DataIdContainer specialized for assembleCoadd.
799  """
800 
801  def makeDataRefList(self, namespace):
802  """!
803  \brief Make self.refList from self.idList.
804 
805  Interpret the config.doMatchBackgrounds, config.autoReference,
806  and whether a visit/run supplied.
807  If a visit/run is supplied, config.autoReference is automatically set to False.
808  if config.doMatchBackgrounds == false, then a visit/run will be ignored if accidentally supplied.
809 
810  """
811  keysCoadd = namespace.butler.getKeys(datasetType=namespace.config.coaddName + "Coadd",
812  level=self.level)
813  keysCoaddTempExp = namespace.butler.getKeys(datasetType=namespace.config.coaddName + "Coadd_tempExp",
814  level=self.level)
815 
816  if namespace.config.doMatchBackgrounds:
817  if namespace.config.autoReference: # matcher will pick it's own reference image
818  datasetType = namespace.config.coaddName + "Coadd"
819  validKeys = keysCoadd
820  else:
821  datasetType = namespace.config.coaddName + "Coadd_tempExp"
822  validKeys = keysCoaddTempExp
823  else: # bkg subtracted coadd
824  datasetType = namespace.config.coaddName + "Coadd"
825  validKeys = keysCoadd
826 
827  for dataId in self.idList:
828  # tract and patch are required
829  for key in validKeys:
830  if key not in dataId:
831  raise RuntimeError("--id must include " + key)
832 
833  for key in dataId: # check if users supplied visit/run
834  if (key not in keysCoadd) and (key in keysCoaddTempExp): # user supplied a visit/run
835  if namespace.config.autoReference:
836  # user probably meant: autoReference = False
837  namespace.config.autoReference = False
838  datasetType = namespace.config.coaddName + "Coadd_tempExp"
839  print("Switching config.autoReference to False; applies only to background Matching.")
840  break
841 
842  dataRef = namespace.butler.dataRef(
843  datasetType=datasetType,
844  dataId=dataId,
845  )
846  self.refList.append(dataRef)
847 
848 
849 def countMaskFromFootprint(mask, footprint, bitmask, ignoreMask):
850  """!
851  \brief Function to count the number of pixels with a specific mask in a footprint.
852 
853  Find the intersection of mask & footprint. Count all pixels in the mask that are in the intersection that
854  have bitmask set but do not have ignoreMask set. Return the count.
855 
856  \param[in] mask: mask to define intersection region by.
857  \parma[in] footprint: footprint to define the intersection region by.
858  \param[in] bitmask: specific mask that we wish to count the number of occurances of.
859  \param[in] ignoreMask: pixels to not consider.
860  \return count of number of pixels in footprint with specified mask.
861  """
862  bbox = footprint.getBBox()
863  bbox.clip(mask.getBBox(afwImage.PARENT))
864  fp = afwImage.MaskU(bbox)
865  subMask = mask.Factory(mask, bbox, afwImage.PARENT)
866  afwDet.setMaskFromFootprint(fp, footprint, bitmask)
867  return numpy.logical_and((subMask.getArray() & fp.getArray()) > 0,
868  (subMask.getArray() & ignoreMask) == 0).sum()
869 
870 
872  """!
873 \anchor SafeClipAssembleCoaddConfig
874 
875 \brief Configuration parameters for the SafeClipAssembleCoaddTask
876  """
877  clipDetection = pexConfig.ConfigurableField(
878  target=SourceDetectionTask,
879  doc="Detect sources on difference between unclipped and clipped coadd")
880  minClipFootOverlap = pexConfig.Field(
881  doc="Minimum fractional overlap of clipped footprint with visit DETECTED to be clipped",
882  dtype=float,
883  default=0.6
884  )
885  minClipFootOverlapSingle = pexConfig.Field(
886  doc="Minimum fractional overlap of clipped footprint with visit DETECTED to be "
887  "clipped when only one visit overlaps",
888  dtype=float,
889  default=0.5
890  )
891  minClipFootOverlapDouble = pexConfig.Field(
892  doc="Minimum fractional overlap of clipped footprints with visit DETECTED to be "
893  "clipped when two visits overlap",
894  dtype=float,
895  default=0.45
896  )
897  maxClipFootOverlapDouble = pexConfig.Field(
898  doc="Maximum fractional overlap of clipped footprints with visit DETECTED when "
899  "considering two visits",
900  dtype=float,
901  default=0.15
902  )
903  minBigOverlap = pexConfig.Field(
904  doc="Minimum number of pixels in footprint to use DETECTED mask from the single visits "
905  "when labeling clipped footprints",
906  dtype=int,
907  default=100
908  )
909 
910  def setDefaults(self):
911  # The numeric values for these configuration parameters were empirically determined, future work
912  # may further refine them.
913  AssembleCoaddConfig.setDefaults(self)
914  self.clipDetection.doTempLocalBackground = False
915  self.clipDetection.reEstimateBackground = False
916  self.clipDetection.returnOriginalFootprints = False
917  self.clipDetection.thresholdPolarity = "both"
918  self.clipDetection.thresholdValue = 2
919  self.clipDetection.nSigmaToGrow = 2
920  self.clipDetection.minPixels = 4
921  self.clipDetection.isotropicGrow = True
922  self.clipDetection.thresholdType = "pixel_stdev"
923  self.sigmaClip = 1.5
924  self.clipIter = 3
925 
926 ## \addtogroup LSST_task_documentation
927 ## \{
928 ## \page SafeClipAssembleCoaddTask
929 ## \ref SafeClipAssembleCoaddTask_ "SafeClipAssembleCoaddTask"
930 ## \copybrief SafeClipAssembleCoaddTask
931 ## \}
932 
933 
935  """!
936  \anchor SafeClipAssembleCoaddTask_
937 
938  \brief Assemble a coadded image from a set of coadded temporary exposures, being careful to clip & flag areas
939  with potential artifacts.
940 
941  \section pipe_tasks_assembleCoadd_Contents Contents
942  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Purpose
943  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Initialize
944  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Run
945  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Config
946  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Debug
947  - \ref pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Example
948 
949  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Purpose Description
950 
951  \copybrief SafeClipAssembleCoaddTask
952 
953  Read the documentation for \ref AssembleCoaddTask_ "AssembleCoaddTask" first since
954  SafeClipAssembleCoaddTask subtasks that task.
955  In \ref AssembleCoaddTask_ "AssembleCoaddTask", we compute the coadd as an clipped mean (i.e. we clip
956  outliers).
957  The problem with doing this is that when computing the coadd PSF at a given location, individual visit
958  PSFs from visits with outlier pixels contribute to the coadd PSF and cannot be treated correctly.
959  In this task, we correct for this behavior by creating a new badMaskPlane 'CLIPPED'.
960  We populate this plane on the input coaddTempExps and the final coadd where i. difference imaging suggests
961  that there is an outlier and ii. this outlier appears on only one or two images.
962  Such regions will not contribute to the final coadd.
963  Furthermore, any routine to determine the coadd PSF can now be cognizant of clipped regions.
964  Note that the algorithm implemented by this task is preliminary and works correctly for HSC data.
965  Parameter modifications and or considerable redesigning of the algorithm is likley required for other
966  surveys.
967 
968  SafeClipAssembleCoaddTask uses a \ref SourceDetectionTask_ "clipDetection" subtask and also sub-classes
969  \ref AssembleCoaddTask_ "AssembleCoaddTask". You can retarget the
970  \ref SourceDetectionTask_ "clipDetection" subtask if you wish.
971 
972  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Initialize Task initialization
973  \copydoc \_\_init\_\_
974 
975  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Run Invoking the Task
976  \copydoc run
977 
978  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Config Configuration parameters
979  See \ref SafeClipAssembleCoaddConfig
980 
981  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Debug Debug variables
982  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
983  flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py
984  files.
985  SafeClipAssembleCoaddTask has no debug variables of its own. The \ref SourceDetectionTask_ "clipDetection"
986  subtasks may support debug variables. See the documetation for \ref SourceDetectionTask_ "clipDetection"
987  for further information.
988 
989  \section pipe_tasks_assembleCoadd_SafeClipAssembleCoaddTask_Example A complete example of using SafeClipAssembleCoaddTask
990 
991  SafeClipAssembleCoaddTask assembles a set of warped coaddTempExp images into a coadded image.
992  The SafeClipAssembleCoaddTask is invoked by running assembleCoadd.py <em>without</em> the flag
993  '--legacyCoadd'.
994  Usage of assembleCoadd.py expects a data reference to the tract patch and filter to be coadded
995  (specified using '--id = [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]') along
996  with a list of coaddTempExps to attempt to coadd (specified using
997  '--selectId [KEY=VALUE1[^VALUE2[^VALUE3...] [KEY=VALUE1[^VALUE2[^VALUE3...] ...]]').
998  Only the coaddTempExps that cover the specified tract and patch will be coadded.
999  A list of the available optional arguments can be obtained by calling assembleCoadd.py with the --help
1000  command line argument:
1001  \code
1002  assembleCoadd.py --help
1003  \endcode
1004  To demonstrate usage of the SafeClipAssembleCoaddTask in the larger context of multi-band processing, we
1005  will generate the HSC-I & -R band coadds from HSC engineering test data provided in the ci_hsc package. To
1006  begin, assuming that the lsst stack has been already set up, we must set up the obs_subaru and ci_hsc
1007  packages.
1008  This defines the environment variable $CI_HSC_DIR and points at the location of the package. The raw HSC
1009  data live in the $CI_HSC_DIR/raw directory. To begin assembling the coadds, we must first
1010  <DL>
1011  <DT>processCcd</DT>
1012  <DD> process the individual ccds in $CI_HSC_RAW to produce calibrated exposures</DD>
1013  <DT>makeSkyMap</DT>
1014  <DD> create a skymap that covers the area of the sky present in the raw exposures</DD>
1015  <DT>makeCoaddTempExp</DT>
1016  <DD> warp the individual calibrated exposures to the tangent plane of the coadd</DD>
1017  </DL>
1018  We can perform all of these steps by running
1019  \code
1020  $CI_HSC_DIR scons warp-903986 warp-904014 warp-903990 warp-904010 warp-903988
1021  \endcode
1022  This will produce warped coaddTempExps for each visit. To coadd the wraped data, we call assembleCoadd.py
1023  as follows:
1024  \code
1025  assembleCoadd.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I --selectId visit=903986 ccd=16 --selectId visit=903986 ccd=22 --selectId visit=903986 ccd=23 --selectId visit=903986 ccd=100 --selectId visit=904014 ccd=1 --selectId visit=904014 ccd=6 --selectId visit=904014 ccd=12 --selectId visit=903990 ccd=18 --selectId visit=903990 ccd=25 --selectId visit=904010 ccd=4 --selectId visit=904010 ccd=10 --selectId visit=904010 ccd=100 --selectId visit=903988 ccd=16 --selectId visit=903988 ccd=17 --selectId visit=903988 ccd=23 --selectId visit=903988 ccd=24
1026  \endcode
1027  This will process the HSC-I band data. The results are written in $CI_HSC_DIR/DATA/deepCoadd-results/HSC-I
1028  You may also choose to run:
1029  \code
1030  scons warp-903334 warp-903336 warp-903338 warp-903342 warp-903344 warp-903346
1031  assembleCoadd.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R --selectId visit=903334 ccd=16 --selectId visit=903334 ccd=22 --selectId visit=903334 ccd=23 --selectId visit=903334 ccd=100 --selectId visit=903336 ccd=17 --selectId visit=903336 ccd=24 --selectId visit=903338 ccd=18 --selectId visit=903338 ccd=25 --selectId visit=903342 ccd=4 --selectId visit=903342 ccd=10 --selectId visit=903342 ccd=100 --selectId visit=903344 ccd=0 --selectId visit=903344 ccd=5 --selectId visit=903344 ccd=11 --selectId visit=903346 ccd=1 --selectId visit=903346 ccd=6 --selectId visit=903346 ccd=12
1032  \endcode
1033  to generate the coadd for the HSC-R band if you are interested in following multiBand Coadd processing as
1034  discussed in \ref pipeTasks_multiBand.
1035  """
1036  ConfigClass = SafeClipAssembleCoaddConfig
1037  _DefaultName = "safeClipAssembleCoadd"
1038 
1039  def __init__(self, *args, **kwargs):
1040  """!
1041  \brief Initialize the task and make the \ref SourceDetectionTask_ "clipDetection" subtask.
1042  """
1043  AssembleCoaddTask.__init__(self, *args, **kwargs)
1044  schema = afwTable.SourceTable.makeMinimalSchema()
1045  self.makeSubtask("clipDetection", schema=schema)
1046 
1047  def assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList, *args, **kwargs):
1048  """!
1049  \brief Assemble the coadd for a region
1050 
1051  Compute the difference of coadds created with and without outlier rejection to identify coadd pixels
1052  that have outlier values in some individual visits. Detect clipped regions on the difference image and
1053  mark these regions on the one or two individual coaddTempExps where they occur if there is significant
1054  overlap between the clipped region and a source.
1055  This leaves us with a set of footprints from the difference image that have been identified as having
1056  occured on just one or two individual visits. However, these footprints were generated from a
1057  difference image. It is conceivable for a large diffuse source to have become broken up into multiple
1058  footprints acrosss the coadd difference in this process.
1059  Determine the clipped region from all overlapping footprints from the detected sources in each visit -
1060  these are big footprints.
1061  Combine the small and big clipped footprints and mark them on a new bad mask plane
1062  Generate the coadd using \ref AssembleCoaddTask.assemble_ "AssembleCoaddTask.assemble" without outlier
1063  removal. Clipped footprints will no longer make it into the coadd because they are marked in the new
1064  bad mask plane.
1065 
1066  N.b. *args and **kwargs are passed but ignored in order to match the call signature expected by the
1067  parent task.
1068 
1069  @param skyInfo: Patch geometry information, from getSkyInfo
1070  @param tempExpRefList: List of data reference to tempExp
1071  @param imageScalerList: List of image scalers
1072  @param weightList: List of weights
1073  @param bgModelList: List of background models from background matching
1074  return coadd exposure
1075  """
1076  exp = self.buildDifferenceImage(skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList)
1077  mask = exp.getMaskedImage().getMask()
1078  mask.addMaskPlane("CLIPPED")
1079 
1080  result = self.detectClip(exp, tempExpRefList)
1081 
1082  self.log.info('Found %d clipped objects', len(result.clipFootprints))
1083 
1084  # Go to individual visits for big footprints
1085  maskClipValue = mask.getPlaneBitMask("CLIPPED")
1086  maskDetValue = mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")
1087  bigFootprints = self.detectClipBig(result.tempExpClipList, result.clipFootprints, result.clipIndices,
1088  maskClipValue, maskDetValue)
1089 
1090  # Create mask of the current clipped footprints
1091  maskClip = mask.Factory(mask.getBBox(afwImage.PARENT))
1092  afwDet.setMaskFromFootprintList(maskClip, result.clipFootprints, maskClipValue)
1093 
1094  maskClipBig = maskClip.Factory(mask.getBBox(afwImage.PARENT))
1095  afwDet.setMaskFromFootprintList(maskClipBig, bigFootprints, maskClipValue)
1096  maskClip |= maskClipBig
1097 
1098  # Assemble coadd from base class, but ignoring CLIPPED pixels (doClip is false)
1099  badMaskPlanes = self.config.badMaskPlanes[:]
1100  badMaskPlanes.append("CLIPPED")
1101  badPixelMask = afwImage.MaskU.getPlaneBitMask(badMaskPlanes)
1102  coaddExp = AssembleCoaddTask.assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1103  bgModelList, result.tempExpClipList,
1104  doClip=False,
1105  mask=badPixelMask)
1106 
1107  # Set the coadd CLIPPED mask from the footprints since currently pixels that are masked
1108  # do not get propagated
1109  maskExp = coaddExp.getMaskedImage().getMask()
1110  maskExp |= maskClip
1111 
1112  return coaddExp
1113 
1114  def buildDifferenceImage(self, skyInfo, tempExpRefList, imageScalerList, weightList, bgModelList):
1115  """!
1116  \brief Return an exposure that contains the difference between and unclipped and clipped coadds.
1117 
1118  Generate a difference image between clipped and unclipped coadds.
1119  Compute the difference image by subtracting an outlier-clipped coadd from an outlier-unclipped coadd.
1120  Return the difference image.
1121 
1122  @param skyInfo: Patch geometry information, from getSkyInfo
1123  @param tempExpRefList: List of data reference to tempExp
1124  @param imageScalerList: List of image scalers
1125  @param weightList: List of weights
1126  @param bgModelList: List of background models from background matching
1127  @return Difference image of unclipped and clipped coadd wrapped in an Exposure
1128  """
1129  # Build the unclipped coadd
1130  coaddMean = AssembleCoaddTask.assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1131  bgModelList, doClip=False)
1132 
1133  # Build the clipped coadd
1134  coaddClip = AssembleCoaddTask.assemble(self, skyInfo, tempExpRefList, imageScalerList, weightList,
1135  bgModelList, doClip=True)
1136 
1137  coaddDiff = coaddMean.getMaskedImage().Factory(coaddMean.getMaskedImage())
1138  coaddDiff -= coaddClip.getMaskedImage()
1139  exp = afwImage.ExposureF(coaddDiff)
1140  exp.setPsf(coaddMean.getPsf())
1141  return exp
1142 
1143  def detectClip(self, exp, tempExpRefList):
1144  """!
1145  \brief Detect clipped regions on an exposure and set the mask on the individual tempExp masks
1146 
1147  Detect footprints in the difference image after smoothing the difference image with a Gaussian kernal.
1148  Identify footprints that overlap with one or two input coaddTempExps by comparing the computed overlap
1149  fraction to thresholds set in the config.
1150  A different threshold is applied depending on the number of overlapping visits (restricted to one or
1151  two).
1152  If the overlap exceeds the thresholds, the footprint is considered "CLIPPED" and is marked as such on
1153  the coaddTempExp.
1154  Return a struct with the clipped footprints, the indices of the coaddTempExps that end up overlapping
1155  with the clipped footprints and a list of new masks for the coaddTempExps.
1156 
1157  \param[in] exp: Exposure to run detection on
1158  \param[in] tempExpRefList: List of data reference to tempExp
1159  \return struct containing:
1160  - clippedFootprints: list of clipped footprints
1161  - clippedIndices: indices for each clippedFootprint in tempExpRefList
1162  - tempExpClipList: list of new masks for tempExp
1163  """
1164  mask = exp.getMaskedImage().getMask()
1165  maskClipValue = mask.getPlaneBitMask("CLIPPED")
1166  maskDetValue = mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")
1167  fpSet = self.clipDetection.detectFootprints(exp, doSmooth=True, clearMask=True)
1168  # Merge positive and negative together footprints together
1169  fpSet.positive.merge(fpSet.negative)
1170  footprints = fpSet.positive
1171  self.log.info('Found %d potential clipped objects', len(footprints.getFootprints()))
1172  ignoreMask = self.getBadPixelMask()
1173 
1174  clipFootprints = []
1175  clipIndices = []
1176 
1177  # build a list with a mask for each visit which can be modified with clipping information
1178  tempExpClipList = [tmpExpRef.get(self.getTempExpDatasetName(),
1179  immediate=True).getMaskedImage().getMask() for
1180  tmpExpRef in tempExpRefList]
1181 
1182  for footprint in footprints.getFootprints():
1183  nPixel = footprint.getArea()
1184  overlap = [] # hold the overlap with each visit
1185  maskList = [] # which visit mask match
1186  indexList = [] # index of visit in global list
1187  for i, tmpExpMask in enumerate(tempExpClipList):
1188  # Determine the overlap with the footprint
1189  ignore = countMaskFromFootprint(tmpExpMask, footprint, ignoreMask, 0x0)
1190  overlapDet = countMaskFromFootprint(tmpExpMask, footprint, maskDetValue, ignoreMask)
1191  totPixel = nPixel - ignore
1192 
1193  # If we have more bad pixels than detection skip
1194  if ignore > overlapDet or totPixel <= 0.5*nPixel or overlapDet == 0:
1195  continue
1196  overlap.append(overlapDet/float(totPixel))
1197  maskList.append(tmpExpMask)
1198  indexList.append(i)
1199 
1200  overlap = numpy.array(overlap)
1201  if not len(overlap):
1202  continue
1203 
1204  keep = False # Should this footprint be marked as clipped?
1205  keepIndex = [] # Which tempExps does the clipped footprint belong to
1206 
1207  # If footprint only has one overlap use a lower threshold
1208  if len(overlap) == 1:
1209  if overlap[0] > self.config.minClipFootOverlapSingle:
1210  keep = True
1211  keepIndex = [0]
1212  else:
1213  # This is the general case where only visit should be clipped
1214  clipIndex = numpy.where(overlap > self.config.minClipFootOverlap)[0]
1215  if len(clipIndex) == 1:
1216  keep = True
1217  keepIndex = [clipIndex[0]]
1218 
1219  # Test if there are clipped objects that overlap two different visits
1220  clipIndex = numpy.where(overlap > self.config.minClipFootOverlapDouble)[0]
1221  if len(clipIndex) == 2 and len(overlap) > 3:
1222  clipIndexComp = numpy.where(overlap < self.config.minClipFootOverlapDouble)[0]
1223  if numpy.max(overlap[clipIndexComp]) < self.config.maxClipFootOverlapDouble:
1224  keep = True
1225  keepIndex = clipIndex
1226 
1227  if not keep:
1228  continue
1229 
1230  for index in keepIndex:
1231  afwDet.setMaskFromFootprint(maskList[index], footprint, maskClipValue)
1232 
1233  clipIndices.append(numpy.array(indexList)[keepIndex])
1234  clipFootprints.append(footprint)
1235 
1236  return pipeBase.Struct(clipFootprints=clipFootprints, clipIndices=clipIndices,
1237  tempExpClipList=tempExpClipList)
1238 
1239  def detectClipBig(self, tempExpClipList, clipFootprints, clipIndices, maskClipValue, maskDetValue):
1240  """!
1241  \brief Find footprints from individual tempExp footprints for large footprints.
1242 
1243  Identify big footprints composed of many sources in the coadd difference that may have originated in a
1244  large diffuse source in the coadd. We do this by indentifying all clipped footprints that overlap
1245  significantly with each source in all the coaddTempExps.
1246 
1247  \param[in] tempExpClipList: List of tempExp masks with clipping information
1248  \param[in] clipFootprints: List of clipped footprints
1249  \param[in] clipIndices: List of which entries in tempExpClipList each footprint belongs to
1250  \param[in] maskClipValue: Mask value of clipped pixels
1251  \param[in] maskClipValue: Mask value of detected pixels
1252  \return list of big footprints
1253  """
1254  bigFootprintsCoadd = []
1255  ignoreMask = self.getBadPixelMask()
1256  for index, tmpExpMask in enumerate(tempExpClipList):
1257 
1258  # Create list of footprints from the DETECTED pixels
1259  maskVisitDet = tmpExpMask.Factory(tmpExpMask, tmpExpMask.getBBox(afwImage.PARENT),
1260  afwImage.PARENT, True)
1261  maskVisitDet &= maskDetValue
1262  visitFootprints = afwDet.FootprintSet(maskVisitDet, afwDet.Threshold(1))
1263 
1264  # build a mask of clipped footprints that are in this visit
1265  clippedFootprintsVisit = []
1266  for foot, clipIndex in zip(clipFootprints, clipIndices):
1267  if index not in clipIndex:
1268  continue
1269  clippedFootprintsVisit.append(foot)
1270  maskVisitClip = maskVisitDet.Factory(maskVisitDet.getBBox(afwImage.PARENT))
1271  afwDet.setMaskFromFootprintList(maskVisitClip, clippedFootprintsVisit, maskClipValue)
1272 
1273  bigFootprintsVisit = []
1274  for foot in visitFootprints.getFootprints():
1275  if foot.getArea() < self.config.minBigOverlap:
1276  continue
1277  nCount = countMaskFromFootprint(maskVisitClip, foot, maskClipValue, ignoreMask)
1278  if nCount > self.config.minBigOverlap:
1279  bigFootprintsVisit.append(foot)
1280  bigFootprintsCoadd.append(foot)
1281 
1282  # Update single visit masks
1283  maskVisitClip.clearAllMaskPlanes()
1284  afwDet.setMaskFromFootprintList(maskVisitClip, bigFootprintsVisit, maskClipValue)
1285  tmpExpMask |= maskVisitClip
1286 
1287  return bigFootprintsCoadd
def __init__
Initialize the task and make the clipDetection subtask.
def run
Assemble a coadd from a set of coaddTempExp.
def getBackgroundReferenceScaler
Construct an image scaler for the background reference frame.
void setCoaddEdgeBits(lsst::afw::image::Mask< lsst::afw::image::MaskPixel > &coaddMask, lsst::afw::image::Image< WeightPixelT > const &weightMap)
set edge bits of coadd mask based on weight map
def addBackgroundMatchingMetadata
Add metadata from the background matching to the coadd.
A Threshold is used to pass a threshold value to detection algorithms.
Definition: Threshold.h:44
def detectClipBig
Find footprints from individual tempExp footprints for large footprints.
void swap(Mask< PixelT > &a, Mask< PixelT > &b)
Definition: Mask.cc:524
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
def _subBBoxIter
Iterate over subregions of a bbox.
An integer coordinate rectangle.
Definition: Box.h:53
Pass parameters to a Statistics objectA class to pass parameters which control how the stats are calc...
Definition: Statistics.h:92
def assembleSubregion
Assemble the coadd for a sub-region.
def assembleMetadata
Set the metadata for the coadd.
CoaddPsf is the Psf derived to be used for non-PSF-matched Coadd images.
Definition: CoaddPsf.h:45
def backgroundMatching
Perform background matching on the prepared inputs.
Configuration parameters for the SafeClipAssembleCoaddTask.
Assemble a coadded image from a set of coadded temporary exposures.
A set of pixels in an Image.
Definition: Footprint.h:62
Assemble a coadded image from a set of coadded temporary exposures, being careful to clip &amp; flag area...
def buildDifferenceImage
Return an exposure that contains the difference between and unclipped and clipped coadds...
def _makeArgumentParser
Create an argument parser.
A set of Footprints, associated with a MaskedImage.
Definition: FootprintSet.h:55
def detectClip
Detect clipped regions on an exposure and set the mask on the individual tempExp masks.
def prepareInputs
Prepare the input warps for coaddition by measuring the weight for each warp and the scaling for the ...
def makeDataRefList
Make self.refList from self.idList.
MaskT setMaskFromFootprint(lsst::afw::image::Mask< MaskT > *mask, Footprint const &footprint, MaskT const bitmask)
OR bitmask into all the Mask&#39;s pixels that are in the Footprint.
Statistics makeStatistics(afwImage::Mask< afwImage::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl)
Specialization to handle Masks.
Definition: Statistics.cc:1107
Configuration parameters for the AssembleCoaddTask.
MaskT setMaskFromFootprintList(lsst::afw::image::Mask< MaskT > *mask, boost::shared_ptr< std::vector< boost::shared_ptr< Footprint >> const > const &footprints, MaskT const bitmask)
OR bitmask into all the Mask&#39;s pixels which are in the set of Footprints.
def countMaskFromFootprint
Function to count the number of pixels with a specific mask in a footprint.
def assemble
Assemble a coadd from input warps.
A version of lsst.pipe.base.DataIdContainer specialized for assembleCoadd.
lsst::afw::image::MaskedImage< PixelT >::Ptr statisticsStack(lsst::afw::image::MaskedImage< PixelT > const &image, Property flags, char dimension, StatisticsControl const &sctrl=StatisticsControl())
A function to compute statistics on the rows or columns of an image.
def getTempExpRefList
Generate list of coaddTempExp data references corresponding to exposures that lie within the patch to...