LSSTApplications  8.0.0.0+107,8.0.0.1+13,9.1+18,9.2,master-g084aeec0a4,master-g0aced2eed8+6,master-g15627eb03c,master-g28afc54ef9,master-g3391ba5ea0,master-g3d0fb8ae5f,master-g4432ae2e89+36,master-g5c3c32f3ec+17,master-g60f1e072bb+1,master-g6a3ac32d1b,master-g76a88a4307+1,master-g7bce1f4e06+57,master-g8ff4092549+31,master-g98e65bf68e,master-ga6b77976b1+53,master-gae20e2b580+3,master-gb584cd3397+53,master-gc5448b162b+1,master-gc54cf9771d,master-gc69578ece6+1,master-gcbf758c456+22,master-gcec1da163f+63,master-gcf15f11bcc,master-gd167108223,master-gf44c96c709
LSSTDataManagementBasePackage
snapCombine.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008, 2009, 2010, 2011 LSST Corporation.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 import numpy as num
23 import lsst.pex.config as pexConfig
24 import lsst.daf.base as dafBase
25 import lsst.afw.image as afwImage
26 import lsst.afw.table as afwTable
27 import lsst.pipe.base as pipeBase
28 from lsst.coadd.utils import addToCoadd, setCoaddEdgeBits
29 from lsst.ip.diffim import SnapPsfMatchTask
30 from lsst.meas.algorithms import SourceDetectionTask, SourceMeasurementTask
31 import lsst.meas.algorithms as measAlg
32 
33 from .repair import RepairTask
34 from .calibrate import InitialPsfConfig
35 
36 class SnapCombineConfig(pexConfig.Config):
37  doRepair = pexConfig.Field(
38  dtype = bool,
39  doc = "Repair images (CR reject and interpolate) before combining",
40  default = True,
41  )
42  repairPsfFwhm = pexConfig.Field(
43  dtype = float,
44  doc = "Psf FWHM (pixels) used to detect CRs",
45  default = 2.5,
46  )
47  doDiffIm = pexConfig.Field(
48  dtype = bool,
49  doc = "Perform difference imaging before combining",
50  default = False,
51  )
52  doPsfMatch = pexConfig.Field(
53  dtype = bool,
54  doc = "Perform PSF matching for difference imaging (ignored if doDiffIm false)",
55  default = True,
56  )
57  doMeasurement = pexConfig.Field(
58  dtype = bool,
59  doc = "Measure difference sources (ignored if doDiffIm false)",
60  default = True,
61  )
62  badMaskPlanes = pexConfig.ListField(
63  dtype = str,
64  doc = "Mask planes that, if set, the associated pixels are not included in the combined exposure; "
65  "DETECTED excludes cosmic rays",
66  default = ("DETECTED",),
67  )
68  averageKeys = pexConfig.ListField(
69  dtype = str,
70  doc = "List of float metadata keys to average when combining snaps, e.g. float positions and dates; "
71  "non-float data must be handled by overriding the fixMetadata method",
72  optional = True,
73 
74  )
75  sumKeys = pexConfig.ListField(
76  dtype = str,
77  doc = "List of float or int metadata keys to sum when combining snaps, e.g. exposure time; "
78  "non-float, non-int data must be handled by overriding the fixMetadata method",
79  optional = True,
80  )
81 
82  repair = pexConfig.ConfigurableField(target = RepairTask, doc = "")
83  diffim = pexConfig.ConfigurableField(target = SnapPsfMatchTask, doc = "")
84  detection = pexConfig.ConfigurableField(target = SourceDetectionTask, doc = "")
85  initialPsf = pexConfig.ConfigField(dtype = InitialPsfConfig, doc = "")
86  measurement = pexConfig.ConfigurableField(target = SourceMeasurementTask, doc = "")
87 
88  def setDefaults(self):
89  self.detection.thresholdPolarity = "both"
90 
91  def validate(self):
92  if self.detection.thresholdPolarity != "both":
93  raise ValueError("detection.thresholdPolarity must be 'both' for SnapCombineTask")
94 
95 class SnapCombineTask(pipeBase.Task):
96  ConfigClass = SnapCombineConfig
97  _DefaultName = "snapCombine"
98 
99  def __init__(self, *args, **kwargs):
100  pipeBase.Task.__init__(self, *args, **kwargs)
101  self.makeSubtask("repair")
102  self.makeSubtask("diffim")
103  self.schema = afwTable.SourceTable.makeMinimalSchema()
105  self.makeSubtask("detection", schema=self.schema)
106  if self.config.doMeasurement:
107  self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
108 
109  @pipeBase.timeMethod
110  def run(self, snap0, snap1, defects=None):
111  """Combine two snaps
112 
113  @param[in] snap0: snapshot exposure 0
114  @param[in] snap1: snapshot exposure 1
115  @defects[in] defect list (for repair task)
116  @return a pipe_base Struct with fields:
117  - exposure: snap-combined exposure
118  - sources: detected sources, or None if detection not performed
119  """
120  # initialize optional outputs
121  sources = None
122 
123  if self.config.doRepair:
124  self.log.info("snapCombine repair")
125  psf = self.makeInitialPsf(snap0, fwhmPix=self.config.repairPsfFwhm)
126  snap0.setPsf(psf)
127  snap1.setPsf(psf)
128  self.repair.run(snap0, defects=defects, keepCRs=False)
129  self.repair.run(snap1, defects=defects, keepCRs=False)
130  self.display('repair0', exposure=snap0)
131  self.display('repair1', exposure=snap1)
132 
133  if self.config.doDiffIm:
134  if self.config.doPsfMatch:
135  self.log.info("snapCombine psfMatch")
136  diffRet = self.diffim.run(snap0, snap1, "subtractExposures")
137  diffExp = diffRet.subtractedImage
138 
139  # Measure centroid and width of kernel; dependent on ticket #1980
140  # Useful diagnostic for the degree of astrometric shift between snaps.
141  diffKern = diffRet.psfMatchingKernel
142  width, height = diffKern.getDimensions()
143  # TBD...
144  #psfAttr = measAlg.PsfAttributes(diffKern, width//2, height//2)
145 
146  else:
147  diffExp = afwImage.ExposureF(snap0, True)
148  diffMi = diffExp.getMaskedImage()
149  diffMi -= snap1.getMaskedImage()
150 
151  psf = self.makeInitialPsf(snap0)
152  diffExp.setPsf(psf)
153  table = afwTable.SourceTable.make(self.schema)
154  table.setMetadata(self.algMetadata)
155  detRet = self.detection.makeSourceCatalog(table, diffExp)
156  sources = detRet.sources
157  fpSets = detRet.fpSets
158  if self.config.doMeasurement:
159  self.measurement.measure(diffExp, sources)
160 
161  mask0 = snap0.getMaskedImage().getMask()
162  mask1 = snap1.getMaskedImage().getMask()
163  fpSets.positive.setMask(mask0, "DETECTED")
164  fpSets.negative.setMask(mask1, "DETECTED")
165 
166  maskD = diffExp.getMaskedImage().getMask()
167  fpSets.positive.setMask(maskD, "DETECTED")
168  fpSets.negative.setMask(maskD, "DETECTED_NEGATIVE")
169 
170  combinedExp = self.addSnaps(snap0, snap1)
171 
172  return pipeBase.Struct(
173  exposure = combinedExp,
174  sources = sources,
175  )
176 
177  def addSnaps(self, snap0, snap1):
178  """Add two snap exposures together, returning a new exposure
179 
180  @param[in] snap0 snap exposure 0
181  @param[in] snap1 snap exposure 1
182  @return combined exposure
183  """
184  self.log.info("snapCombine addSnaps")
185 
186  combinedExp = snap0.Factory(snap0, True)
187  combinedMi = combinedExp.getMaskedImage()
188  combinedMi.set(0)
189 
190  weightMap = combinedMi.getImage().Factory(combinedMi.getBBox())
191  weight = 1.0
192  badPixelMask = afwImage.MaskU.getPlaneBitMask(self.config.badMaskPlanes)
193  addToCoadd(combinedMi, weightMap, snap0.getMaskedImage(), badPixelMask, weight)
194  addToCoadd(combinedMi, weightMap, snap1.getMaskedImage(), badPixelMask, weight)
195 
196  # pre-scaling the weight map instead of post-scaling the combinedMi saves a bit of time
197  # because the weight map is a simple Image instead of a MaskedImage
198  weightMap *= 0.5 # so result is sum of both images, instead of average
199  combinedMi /= weightMap
200  setCoaddEdgeBits(combinedMi.getMask(), weightMap)
201 
202  # note: none of the inputs has a valid Calib object, so that is not touched
203  # Filter was already copied
204 
205  combinedMetadata = combinedExp.getMetadata()
206  metadata0 = snap0.getMetadata()
207  metadata1 = snap1.getMetadata()
208  self.fixMetadata(combinedMetadata, metadata0, metadata1)
209 
210  return combinedExp
211 
212  def fixMetadata(self, combinedMetadata, metadata0, metadata1):
213  """Fix the metadata of the combined exposure (in place)
214 
215  This implementation handles items specified by config.averageKeys and config.sumKeys,
216  which have data type restrictions. To handle other data types (such as sexagesimal
217  positions and ISO dates) you must supplement this method with your own code.
218 
219  @param[in,out] combinedMetadata metadata of combined exposure;
220  on input this is a deep copy of metadata0 (a PropertySet)
221  @param[in] metadata0 metadata of snap0 (a PropertySet)
222  @param[in] metadata1 metadata of snap1 (a PropertySet)
223 
224  @note the inputs are presently PropertySets due to ticket #2542. However, in some sense
225  they are just PropertyLists that are missing some methods. In particular: comments and order
226  are preserved if you alter an existing value with set(key, value).
227  """
228  keyDoAvgList = []
229  if self.config.averageKeys:
230  keyDoAvgList += [(key, 1) for key in self.config.averageKeys]
231  if self.config.sumKeys:
232  keyDoAvgList += [(key, 0) for key in self.config.sumKeys]
233  for key, doAvg in keyDoAvgList:
234  opStr = "average" if doAvg else "sum"
235  try:
236  val0 = metadata0.get(key)
237  val1 = metadata1.get(key)
238  except Exception:
239  self.log.warn("Could not %s metadata %r: missing from one or both exposures" % (opStr, key,))
240  continue
241 
242  try:
243  combinedVal = val0 + val1
244  if doAvg:
245  combinedVal /= 2.0
246  except Exception:
247  self.log.warn("Could not %s metadata %r: value %r and/or %r not numeric" % \
248  (opStr, key, val0, val1))
249  continue
250 
251  combinedMetadata.set(key, combinedVal)
252 
253  def makeInitialPsf(self, exposure, fwhmPix=None):
254  """Initialise the detection procedure by setting the PSF and WCS
255 
256  @param exposure Exposure to process
257  @return PSF, WCS
258  """
259  assert exposure, "No exposure provided"
260  wcs = exposure.getWcs()
261  assert wcs, "No wcs in exposure"
262 
263  if fwhmPix is None:
264  fwhmPix = self.config.initialPsf.fwhm / wcs.pixelScale().asArcseconds()
265 
266  size = self.config.initialPsf.size
267  model = self.config.initialPsf.model
268  self.log.info("installInitialPsf fwhm=%s pixels; size=%s pixels" % (fwhmPix, size))
269  psfCls = getattr(measAlg, model + "Psf")
270  psf = psfCls(size, size, fwhmPix/(2.0*num.sqrt(2*num.log(2.0))))
271  return psf
Class for storing ordered metadata with comments.
Definition: PropertyList.h:81
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
lsst::afw::geom::Box2I addToCoadd(lsst::afw::image::Image< CoaddPixelT > &coadd, lsst::afw::image::Image< WeightPixelT > &weightMap, lsst::afw::image::Image< CoaddPixelT > const &image, WeightPixelT weight)
add good pixels from an image to a coadd and associated weight map
Definition: addToCoadd.cc:126