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