LSSTApplications  1.1.2+25,10.0+13,10.0+132,10.0+133,10.0+224,10.0+41,10.0+8,10.0-1-g0f53050+14,10.0-1-g4b7b172+19,10.0-1-g61a5bae+98,10.0-1-g7408a83+3,10.0-1-gc1e0f5a+19,10.0-1-gdb4482e+14,10.0-11-g3947115+2,10.0-12-g8719d8b+2,10.0-15-ga3f480f+1,10.0-2-g4f67435,10.0-2-gcb4bc6c+26,10.0-28-gf7f57a9+1,10.0-3-g1bbe32c+14,10.0-3-g5b46d21,10.0-4-g027f45f+5,10.0-4-g86f66b5+2,10.0-4-gc4fccf3+24,10.0-40-g4349866+2,10.0-5-g766159b,10.0-5-gca2295e+25,10.0-6-g462a451+1
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  noiseSeedMultiplier = lsst.pex.config.Field(
45  dtype=int, default=1,
46  doc='The seed multiplier value to use for random number generation. 0 will not set seed.'
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, noiseImage=None, exposureId=None, 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] noiseImage an afw.image.ImageF used as a predictable noise replacement source
79  (for tests only)
80  @param[in] log pex.logging.Log object to use for status messages; no status messages
81  will be printed if None
82 
83  'footprints' is a dict of {id: (parent, footprint)}; when used in SFM, the ID will be the
84  source ID, but in forced photometry, this will be the reference ID, as that's what we used to
85  determine the deblend families. This routine should create HeavyFootprints for any non-Heavy
86  Footprints, and replace them in the dict. It should then create a dict of HeavyFootprints
87  containing noise, but only for parent objects, then replace all sources with noise.
88  This should ignore any footprints that lay outside the bounding box of the exposure,
89  and clip those that lie on the border.
90 
91  NOTE: as the code currently stands, the heavy footprint for a deblended object must be available
92  from the input catalog. If it is not, it cannot be reproduced here. In that case, the
93  topmost parent in the objects parent chain must be used. The heavy footprint for that source
94  is created in this class from the masked image.
95  """
96  noiseMeanVar=None
97  self.noiseSource = config.noiseSource
98  self.noiseOffset = config.noiseOffset
99  self.noiseSeedMultiplier = config.noiseSeedMultiplier
100  self.noiseGenMean = None
101  self.noiseGenStd = None
102  self.log = log
103 
104  # creates heavies, replaces all footprints with noise
105  # We need the source table to be sorted by ID to do the parent lookups
106  self.exposure = exposure
107  self.footprints = footprints
108  mi = exposure.getMaskedImage()
109  im = mi.getImage()
110  mask = mi.getMask()
111  # Add temporary Mask planes for THISDET and OTHERDET
112  self.removeplanes = []
113  bitmasks = []
114  for maskname in ['THISDET', 'OTHERDET']:
115  try:
116  # does it already exist?
117  plane = mask.getMaskPlane(maskname)
118  if self.log: self.log.logdebug('Mask plane "%s" already existed' % maskname)
119  except:
120  # if not, add it; we should delete it when done.
121  plane = mask.addMaskPlane(maskname)
122  self.removeplanes.append(maskname)
123  mask.clearMaskPlane(plane)
124  bitmask = mask.getPlaneBitMask(maskname)
125  bitmasks.append(bitmask)
126  if self.log: self.log.logdebug('Mask plane "%s": plane %i, bitmask %i = 0x%x'
127  % (maskname, plane, bitmask, bitmask))
128  self.thisbitmask,self.otherbitmask = bitmasks
129  del bitmasks
130  self.heavies = {}
131  # Start by creating HeavyFootprints for each source which has no parent
132  # and just use them for children which do not already have heavy footprints.
133  # If a heavy footprint is available for a child, we will use it. Otherwise,
134  # we use the first parent in the parent chain which has a heavy footprint,
135  # which with the one level deblender will alway be the topmost parent
136  # NOTE: heavy footprints get destroyed by the transform process in forcedPhotImage.py,
137  # so they are never available for forced measurements.
138 
139  # Create in the dict heavies = {id:heavyfootprint}
140  for id in footprints.keys():
141  fp = footprints[id]
142  if fp[1].isHeavy():
143  self.heavies[id] = afwDet.cast_HeavyFootprintF(fp[1])
144  elif fp[0] == 0:
145  self.heavies[id] = afwDet.makeHeavyFootprint(fp[1], mi)
146 
147  ### FIXME: the heavy footprint includes the mask
148  ### and variance planes, which we shouldn't need
149  ### (I don't think we ever want to modify them in
150  ### the input image). Copying them around is
151  ### wasteful.
152 
153  # We now create a noise HeavyFootprint for each source with has a heavy footprint.
154  # We'll put the noise footprints in a dict heavyNoise = {id:heavyNoiseFootprint}
155  self.heavyNoise = {}
156  noisegen = self.getNoiseGenerator(exposure, noiseImage, noiseMeanVar, exposureId=exposureId)
157  # The noiseGenMean and Std are used by the unit tests
158  self.noiseGenMean = noisegen.mean
159  self.noiseGenStd = noisegen.std
160  if self.log: self.log.logdebug('Using noise generator: %s' % (str(noisegen)))
161  for id in self.heavies.keys():
162  fp = footprints[id][1]
163  noiseFp = noisegen.getHeavyFootprint(fp)
164  self.heavyNoise[id] = noiseFp
165  # Also insert the noisy footprint into the image now.
166  # Notice that we're just inserting it into "im", ie,
167  # the Image, not the MaskedImage.
168  noiseFp.insert(im)
169  # Also set the OTHERDET bit
171 
172  def insertSource(self, id):
173  """!
174  Insert the heavy footprint of a given source into the exposure
175 
176  @param[in] id id for current source to insert from original footprint dict
177 
178  Also adjusts the mask plane to show the source of this footprint.
179  """
180  # Copy this source's pixels into the image
181  mi = self.exposure.getMaskedImage()
182  im = mi.getImage()
183  mask = mi.getMask()
184  # usedid can point either to this source, or to the first parent in the
185  # parent chain which has a heavy footprint (or to the topmost parent,
186  # which always has one)
187  usedid = id
188  while self.footprints[usedid][0] != 0 and not usedid in self.heavies.keys():
189  usedid = self.footprints[usedid][0]
190  fp = self.heavies[usedid]
191  fp.insert(im)
192  afwDet.setMaskFromFootprint(mask, fp, self.thisbitmask)
194 
195  def removeSource(self, id):
196  """!
197  Remove the heavy footprint of a given source and replace with previous noise
198 
199  @param[in] id id for current source to insert from original footprint dict
200 
201  Also restore the mask plane.
202  """
203  # remove a single source
204  # (Replace this source's pixels by noise again.)
205  # Do this by finding the source's top-level ancestor
206  mi = self.exposure.getMaskedImage()
207  im = mi.getImage()
208  mask = mi.getMask()
209 
210  # use the same algorithm as in remove Source to find the heavy noise footprint
211  # which will undo what insertSource(id) does
212  usedid = id
213  while self.footprints[usedid][0] != 0 and not usedid in self.heavies.keys():
214  usedid = self.footprints[usedid][0]
215  # Re-insert the noise pixels
216  fp = self.heavyNoise[usedid]
217  fp.insert(im)
218  # Clear the THISDET mask plane.
219  afwDet.clearMaskFromFootprint(mask, fp, self.thisbitmask)
221 
222  def end(self):
223  """!
224  End the NoiseReplacer.
225 
226  Restore original data to the exposure from the heavies dictionary
227  Restore the mask planes to their original state
228  """
229  # restores original image, cleans up temporaries
230  # (ie, replace all the top-level pixels)
231  mi = self.exposure.getMaskedImage()
232  im = mi.getImage()
233  mask = mi.getMask()
234  for id in self.footprints.keys():
235  fp = self.footprints[id][1]
236  if self.footprints[id][0] != 0:
237  continue
238  self.heavies[id].insert(im)
239  for maskname in self.removeplanes:
240  mask.removeAndClearMaskPlane(maskname, True)
241 
242  del self.removeplanes
243  del self.thisbitmask
244  del self.otherbitmask
245  del self.heavies
246  del self.heavyNoise
247 
248  def getNoiseGenerator(self, exposure, noiseImage, noiseMeanVar, exposureId=None):
249  """!
250  Generate noise image using parameters given
251  """
252  if noiseImage is not None:
253  return ImageNoiseGenerator(noiseImage)
254  rand = None
255  if self.noiseSeedMultiplier:
256  # default plugin, our seed
257  if not exposureId is None and not exposureId == 0:
258  seed = exposureId * self.noiseSeedMultiplier
259  else:
260  seed = self.noiseSeedMultiplier
261  rand = afwMath.Random(afwMath.Random.MT19937, seed)
262  if noiseMeanVar is not None:
263  try:
264  # Assume noiseMeanVar is an iterable of floats
265  noiseMean,noiseVar = noiseMeanVar
266  noiseMean = float(noiseMean)
267  noiseVar = float(noiseVar)
268  noiseStd = math.sqrt(noiseVar)
269  if self.log: self.log.logdebug('Using passed-in noise mean = %g, variance = %g -> stdev %g'
270  % (noiseMean, noiseVar, noiseStd))
271  return FixedGaussianNoiseGenerator(noiseMean, noiseStd, rand=rand)
272  except:
273  if self.log: self.log.logdebug('Failed to cast passed-in noiseMeanVar to floats: %s'
274  % (str(noiseMeanVar)))
275  offset = self.noiseOffset
276  noiseSource = self.noiseSource
277 
278  if noiseSource == 'meta':
279  # check the exposure metadata
280  meta = exposure.getMetadata()
281  # this key name correspond to estimateBackground() in detection.py
282  try:
283  bgMean = meta.getAsDouble('BGMEAN')
284  # We would have to adjust for GAIN if ip_isr didn't make it 1.0
285  noiseStd = math.sqrt(bgMean)
286  if self.log: self.log.logdebug('Using noise variance = (BGMEAN = %g) from exposure metadata'
287  % (bgMean))
288  return FixedGaussianNoiseGenerator(offset, noiseStd, rand=rand)
289  except:
290  if self.log: self.log.logdebug('Failed to get BGMEAN from exposure metadata')
291 
292  if noiseSource == 'variance':
293  if self.log: self.log.logdebug('Will draw noise according to the variance plane.')
294  var = exposure.getMaskedImage().getVariance()
295  return VariancePlaneNoiseGenerator(var, mean=offset, rand=rand)
296 
297  # Compute an image-wide clipped variance.
298  im = exposure.getMaskedImage().getImage()
299  s = afwMath.makeStatistics(im, afwMath.MEANCLIP | afwMath.STDEVCLIP)
300  noiseMean = s.getValue(afwMath.MEANCLIP)
301  noiseStd = s.getValue(afwMath.STDEVCLIP)
302  if self.log: self.log.logdebug("Measured from image: clipped mean = %g, stdev = %g"
303  % (noiseMean,noiseStd))
304  return FixedGaussianNoiseGenerator(noiseMean + offset, noiseStd, rand=rand)
305 
306 class NoiseReplacerList(list):
307  """Syntactic sugar that makes a list of NoiseReplacers (for multiple exposures)
308  behave like a single one.
309 
310  This is only used in the multifit driver, but the logic there is already pretty
311  complex, so it's nice to have this to simplify it.
312  """
313 
314  def __init__(self, exposuresById, footprintsByExp):
315  # exposuresById --- dict of {exposureId: exposure} (possibly subimages)
316  # footprintsByExp --- nested dict of {exposureId: {objId: (parent, footprint)}}
317  list.__init__(self)
318  for expId, exposure in exposuresById.iteritems():
319  self.append(NoiseReplacer(exposure, footprintsByExp[expId]), expId)
320 
321  def insertSource(self, id):
322  """Insert the original pixels for a given source (by id) into the original exposure.
323  """
324  for item in self: self.insertSource(id)
325 
326  def removeSource(self, id):
327  """Insert the noise pixels for a given source (by id) into the original exposure.
328  """
329  for item in self: self.removeSource(id)
330 
331  def end(self):
332  """Cleanup when the use of the Noise replacer is done.
333  """
334  for item in self: self.end()
335 
336 class NoiseGenerator(object):
337  """!
338  Base class for noise generators used by the "doReplaceWithNoise" routine:
339  these produce HeavyFootprints filled with noise generated in various ways.
340 
341  This is an abstract base class.
342  """
343 
344  def getHeavyFootprint(self, fp):
345  bb = fp.getBBox()
346  mim = self.getMaskedImage(bb)
347  return afwDet.makeHeavyFootprint(fp, mim)
348 
349  def getMaskedImage(self, bb):
350  im = self.getImage(bb)
351  return afwImage.MaskedImageF(im)
352  def getImage(self, bb):
353  return None
354 
356  """
357  Generates noise by cutting out a subimage from a user-supplied noise Image.
358  """
359 
360  def __init__(self, img):
361  """
362  img: an afwImage.ImageF
363  """
364  self.mim = afwImage.MaskedImageF(img)
365  self.mean = afwMath.makeStatistics(img, afwMath.MEAN)
366  self.std = afwMath.makeStatistics(img, afwMath.STDEV)
367 
368  def getMaskedImage(self, bb):
369  return self.mim
370 
372  """!
373  Generates noise using the afwMath.Random() and afwMath.randomGaussianImage() routines.
374 
375  This is an abstract base class.
376  """
377 
378  def __init__(self, rand=None):
379  if rand is None:
380  rand = afwMath.Random()
381  self.rand = rand
382 
383  def getRandomImage(self, bb):
384  # Create an Image and fill it with Gaussian noise.
385  rim = afwImage.ImageF(bb.getWidth(), bb.getHeight())
386  rim.setXY0(bb.getMinX(), bb.getMinY())
388  return rim
389 
391  """!
392  Generates Gaussian noise with a fixed mean and standard deviation.
393  """
394 
395  def __init__(self, mean, std, rand=None):
396  super(FixedGaussianNoiseGenerator, self).__init__(rand=rand)
397  self.mean = mean
398  self.std = std
399 
400  def __str__(self):
401  return 'FixedGaussianNoiseGenerator: mean=%g, std=%g' % (self.mean, self.std)
402 
403  def getImage(self, bb):
404  rim = self.getRandomImage(bb)
405  rim *= self.std
406  rim += self.mean
407  return rim
408 
410  """!
411  Generates Gaussian noise whose variance matches that of the variance plane of the image.
412  """
413 
414  def __init__(self, var, mean=None, rand=None):
415  """
416  var: an afwImage.ImageF; the variance plane.
417  mean: floating-point or afwImage.Image
418  """
419  super(VariancePlaneNoiseGenerator, self).__init__(rand=rand)
420  self.var = var
421  if mean is not None and mean == 0.:
422  mean = None
423  self.mean = mean
424 
425  def __str__(self):
426  return 'VariancePlaneNoiseGenerator: mean=' + str(self.mean)
427 
428  def getImage(self, bb):
429  rim = self.getRandomImage(bb)
430  # Use the image's variance plane to scale the noise.
431  stdev = afwImage.ImageF(self.var, bb, afwImage.LOCAL, True)
432  stdev.sqrt()
433  rim *= stdev
434  if self.mean is not None:
435  rim += self.mean
436  return rim
437 
438 
439 class DummyNoiseReplacer(object):
440  """!
441  A do-nothing standin for NoiseReplacer, used when we want to disable NoiseReplacer
442 
443  DummyNoiseReplacer has all the public methods of NoiseReplacer, but none of them do anything.
444  """
445 
446  def insertSource(self, id):
447  pass
448 
449  def removeSource(self, id):
450  pass
451 
452  def end(self):
453  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.