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
noiseReplacer.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2008, 2009, 2010, 2014 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 
24 import lsst.afw.detection as afwDet
25 import lsst.afw.math as afwMath
26 import lsst.afw.image as afwImage
27 import lsst.pex.config
28 
29 class NoiseReplacerConfig(lsst.pex.config.Config):
30  noiseSource = lsst.pex.config.ChoiceField(
31  doc='How to choose mean and variance of the Gaussian noise we generate?',
32  dtype=str,
33  allowed={
34  'measure': 'Measure clipped mean and variance from the whole image',
35  'meta': 'Mean = 0, variance = the "BGMEAN" metadata entry',
36  'variance': "Mean = 0, variance = the image's variance",
37  },
38  default='measure', optional=False
39  )
40  noiseOffset = lsst.pex.config.Field(
41  dtype=float, optional=False, default=0.,
42  doc='Add ann offset to the generated noise.'
43  )
44  noiseSeed = lsst.pex.config.Field(
45  dtype=int, default=0,
46  doc='The seed value to use for random number generation.'
47  )
48 
49 class NoiseReplacer(object):
50  """!
51  Class that handles replacing sources with noise during measurement.
52 
53  When measuring a source (or the children associated with a parent source), this class is used
54  to replace its neighbors with noise, using the deblender's definition of the sources as stored
55  in HeavyFootprints attached to the SourceRecords. The algorithm works as follows:
56  - We start by replacing all pixels that are in source Footprints with artificially
57  generated noise (__init__).
58  - When we are about to measure a particular source, we add it back in, by inserting that source's
59  HeavyFootprint (from the deblender) into the image.
60  - When we are done measuring that source, we again replace the HeavyFootprint with (the same)
61  artificial noise.
62  - After measuring all sources, we return the image to its original state.
63 
64  This is a functional copy of the code in the older ReplaceWithNoiseTask, but with a slightly different
65  API needed for the new measurement framework; note that it is not a Task, as the lifetime of a
66  NoiseReplacer now corresponds to a single exposure, not an entire processing run.
67  """
68 
69  ConfigClass = NoiseReplacerConfig
70 
71  def __init__(self, config, exposure, footprints, log=None):
72  """!
73  Initialize the NoiseReplacer.
74 
75  @param[in] config instance of NoiseReplacerConfig
76  @param[in,out] exposure Exposure to be noise replaced. (All sources replaced on return)
77  @param[in] footprints dict of {id: (parent, footprint)};
78  @param[in] log pex.logging.Log object to use for status messages; no status messages
79  will be printed if None
80 
81  'footprints' is a dict of {id: (parent, footprint)}; when used in SFM, the ID will be the
82  source ID, but in forced photometry, this will be the reference ID, as that's what we used to
83  determine the deblend families. This routine should create HeavyFootprints for any non-Heavy
84  Footprints, and replace them in the dict. It should then create a dict of HeavyFootprints
85  containing noise, but only for parent objects, then replace all sources with noise.
86  This should ignore any footprints that lay outside the bounding box of the exposure,
87  and clip those that lie on the border.
88 
89  NOTE: as the code currently stands, the heavy footprint for a deblended object must be available
90  from the input catalog. If it is not, it cannot be reproduced here. In that case, the
91  topmost parent in the objects parent chain must be used. The heavy footprint for that source
92  is created in this class from the masked image.
93  """
94  noiseImage=None
95  noiseMeanVar=None
96  self.noiseSource = config.noiseSource
97  self.noiseOffset = config.noiseOffset
98  self.noiseSeed = config.noiseSeed
99  self.noiseGenMean = None
100  self.noiseGenStd = None
101  self.log = log
102 
103  # creates heavies, replaces all footprints with noise
104  # We need the source table to be sorted by ID to do the parent lookups
105  self.exposure = exposure
106  self.footprints = footprints
107  mi = exposure.getMaskedImage()
108  im = mi.getImage()
109  mask = mi.getMask()
110  # Add temporary Mask planes for THISDET and OTHERDET
111  self.removeplanes = []
112  bitmasks = []
113  for maskname in ['THISDET', 'OTHERDET']:
114  try:
115  # does it already exist?
116  plane = mask.getMaskPlane(maskname)
117  if self.log: self.log.logdebug('Mask plane "%s" already existed' % maskname)
118  except:
119  # if not, add it; we should delete it when done.
120  plane = mask.addMaskPlane(maskname)
121  self.removeplanes.append(maskname)
122  mask.clearMaskPlane(plane)
123  bitmask = mask.getPlaneBitMask(maskname)
124  bitmasks.append(bitmask)
125  if self.log: self.log.logdebug('Mask plane "%s": plane %i, bitmask %i = 0x%x'
126  % (maskname, plane, bitmask, bitmask))
127  self.thisbitmask,self.otherbitmask = bitmasks
128  del bitmasks
129  self.heavies = {}
130  # Start by creating HeavyFootprints for each source which has no parent
131  # and just use them for children which do not already have heavy footprints.
132  # If a heavy footprint is available for a child, we will use it. Otherwise,
133  # we use the first parent in the parent chain which has a heavy footprint,
134  # which with the one level deblender will alway be the topmost parent
135  # NOTE: heavy footprints get destroyed by the transform process in forcedPhotImage.py,
136  # so they are never available for forced measurements.
137 
138  # Create in the dict heavies = {id:heavyfootprint}
139  for id in footprints.keys():
140  fp = footprints[id]
141  if fp[1].isHeavy():
142  self.heavies[id] = afwDet.cast_HeavyFootprintF(fp[1])
143  elif fp[0] == 0:
144  self.heavies[id] = afwDet.makeHeavyFootprint(fp[1], mi)
145 
146  ### FIXME: the heavy footprint includes the mask
147  ### and variance planes, which we shouldn't need
148  ### (I don't think we ever want to modify them in
149  ### the input image). Copying them around is
150  ### wasteful.
151 
152  # We now create a noise HeavyFootprint for each source with has a heavy footprint.
153  # We'll put the noise footprints in a dict heavyNoise = {id:heavyNoiseFootprint}
154  self.heavyNoise = {}
155  noisegen = self.getNoiseGenerator(exposure, noiseImage, noiseMeanVar)
156  # The noiseGenMean and Std are used by the unit tests
157  self.noiseGenMean = noisegen.mean
158  self.noiseGenStd = noisegen.std
159  if self.log: self.log.logdebug('Using noise generator: %s' % (str(noisegen)))
160  for id in self.heavies.keys():
161  fp = footprints[id][1]
162  noiseFp = noisegen.getHeavyFootprint(fp)
163  self.heavyNoise[id] = noiseFp
164  # Also insert the noisy footprint into the image now.
165  # Notice that we're just inserting it into "im", ie,
166  # the Image, not the MaskedImage.
167  noiseFp.insert(im)
168  # Also set the OTHERDET bit
170 
171  def insertSource(self, id):
172  """!
173  Insert the heavy footprint of a given source into the exposure
174 
175  @param[in] id id for current source to insert from original footprint dict
176 
177  Also adjusts the mask plane to show the source of this footprint.
178  """
179  # Copy this source's pixels into the image
180  mi = self.exposure.getMaskedImage()
181  im = mi.getImage()
182  mask = mi.getMask()
183  # usedid can point either to this source, or to the first parent in the
184  # parent chain which has a heavy footprint (or to the topmost parent,
185  # which always has one)
186  usedid = id
187  while self.footprints[usedid][0] != 0 and not usedid in self.heavies.keys():
188  usedid = self.footprints[usedid][0]
189  fp = self.heavies[usedid]
190  fp.insert(im)
191  afwDet.setMaskFromFootprint(mask, fp, self.thisbitmask)
193 
194  def removeSource(self, id):
195  """!
196  Remove the heavy footprint of a given source and replace with previous noise
197 
198  @param[in] id id for current source to insert from original footprint dict
199 
200  Also restore the mask plane.
201  """
202  # remove a single source
203  # (Replace this source's pixels by noise again.)
204  # Do this by finding the source's top-level ancestor
205  mi = self.exposure.getMaskedImage()
206  im = mi.getImage()
207  mask = mi.getMask()
208 
209  # use the same algorithm as in remove Source to find the heavy noise footprint
210  # which will undo what insertSource(id) does
211  usedid = id
212  while self.footprints[usedid][0] != 0 and not usedid in self.heavies.keys():
213  usedid = self.footprints[usedid][0]
214  # Re-insert the noise pixels
215  fp = self.heavyNoise[usedid]
216  fp.insert(im)
217  # Clear the THISDET mask plane.
218  afwDet.clearMaskFromFootprint(mask, fp, self.thisbitmask)
220 
221  def end(self):
222  """!
223  End the NoiseReplacer.
224 
225  Restore original data to the exposure from the heavies dictionary
226  Restore the mask planes to their original state
227  """
228  # restores original image, cleans up temporaries
229  # (ie, replace all the top-level pixels)
230  mi = self.exposure.getMaskedImage()
231  im = mi.getImage()
232  mask = mi.getMask()
233  for id in self.footprints.keys():
234  fp = self.footprints[id][1]
235  if self.footprints[id][0] != 0:
236  continue
237  self.heavies[id].insert(im)
238  for maskname in self.removeplanes:
239  mask.removeAndClearMaskPlane(maskname, True)
240 
241  del self.removeplanes
242  del self.thisbitmask
243  del self.otherbitmask
244  del self.heavies
245  del self.heavyNoise
246 
247  def getNoiseGenerator(self, exposure, noiseImage, noiseMeanVar):
248  """!
249  Generate noise image using parameters given
250  """
251  if noiseImage is not None:
252  return ImageNoiseGenerator(noiseImage)
253  rand = None
254  if self.noiseSeed:
255  # default plugin, our seed
256  rand = afwMath.Random(afwMath.Random.MT19937, self.noiseSeed)
257  if noiseMeanVar is not None:
258  try:
259  # Assume noiseMeanVar is an iterable of floats
260  noiseMean,noiseVar = noiseMeanVar
261  noiseMean = float(noiseMean)
262  noiseVar = float(noiseVar)
263  noiseStd = math.sqrt(noiseVar)
264  if self.log: self.log.logdebug('Using passed-in noise mean = %g, variance = %g -> stdev %g'
265  % (noiseMean, noiseVar, noiseStd))
266  return FixedGaussianNoiseGenerator(noiseMean, noiseStd, rand=rand)
267  except:
268  if self.log: self.log.logdebug('Failed to cast passed-in noiseMeanVar to floats: %s'
269  % (str(noiseMeanVar)))
270  offset = self.noiseOffset
271  noiseSource = self.noiseSource
272 
273  if noiseSource == 'meta':
274  # check the exposure metadata
275  meta = exposure.getMetadata()
276  # this key name correspond to estimateBackground() in detection.py
277  try:
278  bgMean = meta.getAsDouble('BGMEAN')
279  # We would have to adjust for GAIN if ip_isr didn't make it 1.0
280  noiseStd = math.sqrt(bgMean)
281  if self.log: self.log.logdebug('Using noise variance = (BGMEAN = %g) from exposure metadata'
282  % (bgMean))
283  return FixedGaussianNoiseGenerator(offset, noiseStd, rand=rand)
284  except:
285  if self.log: self.log.logdebug('Failed to get BGMEAN from exposure metadata')
286 
287  if noiseSource == 'variance':
288  if self.log: self.log.logdebug('Will draw noise according to the variance plane.')
289  var = exposure.getMaskedImage().getVariance()
290  return VariancePlaneNoiseGenerator(var, mean=offset, rand=rand)
291 
292  # Compute an image-wide clipped variance.
293  im = exposure.getMaskedImage().getImage()
294  s = afwMath.makeStatistics(im, afwMath.MEANCLIP | afwMath.STDEVCLIP)
295  noiseMean = s.getValue(afwMath.MEANCLIP)
296  noiseStd = s.getValue(afwMath.STDEVCLIP)
297  if self.log: self.log.logdebug("Measured from image: clipped mean = %g, stdev = %g"
298  % (noiseMean,noiseStd))
299  return FixedGaussianNoiseGenerator(noiseMean + offset, noiseStd, rand=rand)
300 
301 class NoiseReplacerList(list):
302  """Syntactic sugar that makes a list of NoiseReplacers (for multiple exposures)
303  behave like a single one.
304 
305  This is only used in the multifit driver, but the logic there is already pretty
306  complex, so it's nice to have this to simplify it.
307  """
308 
309  def __init__(self, exposuresById, footprintsByExp):
310  # exposuresById --- dict of {exposureId: exposure} (possibly subimages)
311  # footprintsByExp --- nested dict of {exposureId: {objId: (parent, footprint)}}
312  list.__init__(self)
313  for expId, exposure in exposuresById.iteritems():
314  self.append(NoiseReplacer(exposure, footprintsByExp[expId]))
315 
316  def insertSource(self, id):
317  """Insert the original pixels for a given source (by id) into the original exposure.
318  """
319  for item in self: self.insertSource(id)
320 
321  def removeSource(self, id):
322  """Insert the noise pixels for a given source (by id) into the original exposure.
323  """
324  for item in self: self.removeSource(id)
325 
326  def end(self):
327  """Cleanup when the use of the Noise replacer is done.
328  """
329  for item in self: self.end()
330 
331 class NoiseGenerator(object):
332  """!
333  Base class for noise generators used by the "doReplaceWithNoise" routine:
334  these produce HeavyFootprints filled with noise generated in various ways.
335 
336  This is an abstract base class.
337  """
338 
339  def getHeavyFootprint(self, fp):
340  bb = fp.getBBox()
341  mim = self.getMaskedImage(bb)
342  return afwDet.makeHeavyFootprint(fp, mim)
343 
344  def getMaskedImage(self, bb):
345  im = self.getImage(bb)
346  return afwImage.MaskedImageF(im)
347  def getImage(self, bb):
348  return None
349 
351  """
352  Generates noise by cutting out a subimage from a user-supplied noise Image.
353  """
354 
355  def __init__(self, img):
356  """
357  img: an afwImage.ImageF
358  """
359  self.mim = afwImage.MaskedImageF(img)
360 
361  def getMaskedImage(self, bb):
362  return self.mim
363 
365  """!
366  Generates noise using the afwMath.Random() and afwMath.randomGaussianImage() routines.
367 
368  This is an abstract base class.
369  """
370 
371  def __init__(self, rand=None):
372  if rand is None:
373  rand = afwMath.Random()
374  self.rand = rand
375 
376  def getRandomImage(self, bb):
377  # Create an Image and fill it with Gaussian noise.
378  rim = afwImage.ImageF(bb.getWidth(), bb.getHeight())
379  rim.setXY0(bb.getMinX(), bb.getMinY())
381  return rim
382 
384  """!
385  Generates Gaussian noise with a fixed mean and standard deviation.
386  """
387 
388  def __init__(self, mean, std, rand=None):
389  super(FixedGaussianNoiseGenerator, self).__init__(rand=rand)
390  self.mean = mean
391  self.std = std
392 
393  def __str__(self):
394  return 'FixedGaussianNoiseGenerator: mean=%g, std=%g' % (self.mean, self.std)
395 
396  def getImage(self, bb):
397  rim = self.getRandomImage(bb)
398  rim *= self.std
399  rim += self.mean
400  return rim
401 
403  """!
404  Generates Gaussian noise whose variance matches that of the variance plane of the image.
405  """
406 
407  def __init__(self, var, mean=None, rand=None):
408  """
409  var: an afwImage.ImageF; the variance plane.
410  mean: floating-point or afwImage.Image
411  """
412  super(VariancePlaneNoiseGenerator, self).__init__(rand=rand)
413  self.var = var
414  if mean is not None and mean == 0.:
415  mean = None
416  self.mean = mean
417 
418  def __str__(self):
419  return 'VariancePlaneNoiseGenerator: mean=' + str(self.mean)
420 
421  def getImage(self, bb):
422  rim = self.getRandomImage(bb)
423  # Use the image's variance plane to scale the noise.
424  stdev = afwImage.ImageF(self.var, bb, afwImage.LOCAL, True)
425  stdev.sqrt()
426  rim *= stdev
427  if self.mean is not None:
428  rim += self.mean
429  return rim
430 
431 
432 class DummyNoiseReplacer(object):
433  """!
434  A do-nothing standin for NoiseReplacer, used when we want to disable NoiseReplacer
435 
436  DummyNoiseReplacer has all the public methods of NoiseReplacer, but none of them do anything.
437  """
438 
439  def insertSource(self, id):
440  pass
441 
442  def removeSource(self, id):
443  pass
444 
445  def end(self):
446  pass
def __init__
Initialize the NoiseReplacer.
Base class for noise generators used by the &quot;doReplaceWithNoise&quot; routine: these produce HeavyFootprin...
void randomGaussianImage(ImageT *image, Random &rand)
Definition: RandomImage.cc:155
MaskT clearMaskFromFootprint(lsst::afw::image::Mask< MaskT > *mask, Footprint const &footprint, MaskT const bitmask)
(AND ~bitmask) all the Mask&#39;s pixels that are in the Footprint; that is, set to zero in the Mask-inte...
A do-nothing standin for NoiseReplacer, used when we want to disable NoiseReplacer.
Generates Gaussian noise whose variance matches that of the variance plane of the image...
Generates Gaussian noise with a fixed mean and standard deviation.
Class that handles replacing sources with noise during measurement.
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:1023
def getNoiseGenerator
Generate noise image using parameters given.
heavyNoise
FIXME: the heavy footprint includes the mask and variance planes, which we shouldn&#39;t need (I don&#39;t th...
def insertSource
Insert the heavy footprint of a given source into the exposure.
def removeSource
Remove the heavy footprint of a given source and replace with previous noise.
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=NULL)
Generates noise using the afwMath.Random() and afwMath.randomGaussianImage() routines.