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