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
baseline.py
Go to the documentation of this file.
1 import math
2 import numpy as np
3 
4 import lsst.afw.image as afwImage
5 import lsst.afw.detection as afwDet
6 import lsst.afw.geom as afwGeom
7 import lsst.afw.math as afwMath
8 
9 # Result objects; will probably go away as we push more results
10 # into earlier-created Source objects, but useful for now for
11 # hauling debugging results around.
12 
13 class PerFootprint(object):
14  '''
15  Result of deblending a single parent Footprint.
16 
17 
18  '''
19  def __init__(self, fp, peaks=None):
20  '''
21  Creates a result object for the given parent Footprint 'fp'.
22 
23  fp: Footprint
24  peaks: if specified, the list of peaks to use; default is
25  fp.getPeaks().
26  '''
27  # PerPeak objects, one per Peak
28  self.peaks = []
29  if peaks is None:
30  peaks = fp.getPeaks()
31  for pki,pk in enumerate(peaks):
32  pkres = PerPeak()
33  pkres.peak = pk
34  pkres.pki = pki
35  self.peaks.append(pkres)
36 
37  self.templateSum = None
38 
39  def setTemplateSum(self, tsum):
40  self.templateSum = tsum
41 
42 class PerPeak(object):
43  '''
44  Result of deblending a single Peak within a parent Footprint.
45 
46  There is one of these objects for each Peak in the Footprint.
47  '''
48  def __init__(self):
49  # Peak object
50  self.peak = None
51  # int, peak index number
52  self.pki = None
53  # union of all the ways of failing...
54  self.skip = False
55 
56  self.outOfBounds = False
57  self.tinyFootprint = False
58  self.noValidPixels = False
59  self.deblendedAsPsf = False
60 
61  # Field set during _fitPsf:
62  self.psfFitFailed = False
63  self.psfFitBadDof = False
64  # (chisq, dof) for PSF fit without decenter
65  self.psfFit1 = None
66  # (chisq, dof) for PSF fit with decenter
67  self.psfFit2 = None
68  # (chisq, dof) for PSF fit after applying decenter
69  self.psfFit3 = None
70  # decentered PSF fit wanted to move the center too much
71  self.psfFitBigDecenter = False
72  # was the fit with decenter better?
73  self.psfFitWithDecenter = False
74  #
75  self.psfFitR0 = None
76  self.psfFitR1 = None
77  self.psfFitStampExtent = None
78  self.psfFitCenter = None
79  self.psfFitBest = None
80  self.psfFitParams = None
81  self.psfFitFlux = None
82  self.psfFitNOthers = None
83 
84  # Things only set in _fitPsf when debugging is turned on:
85  self.psfFitDebugPsf0Img = None
86  self.psfFitDebugPsfImg = None
88  self.psfFitDebugPsfModel = None
89 
91 
92  # The actual template MaskedImage and Footprint
93  self.templateMaskedImage = None
94  self.templateFootprint = None
95 
96  # The flux assigned to this template -- a MaskedImage
97  self.fluxPortion = None
98 
99  # The stray flux assigned to this template (may be None),
100  # a HeavyFootprint
101  self.strayFlux = None
102 
103  self.hasRampedTemplate = False
104 
105  self.patched = False
106 
107  # debug -- a copy of the original symmetric template
108  self.origTemplate = None
109  self.origFootprint = None
110  # MaskedImage
111  self.rampedTemplate = None
112  # MaskedImage
114 
115  # when least-squares fitting templates, the template weight.
116  self.templateWeight = 1.0
117 
118  def __str__(self):
119  return (('Per-peak deblend result: outOfBounds: %s, ' +
120  'deblendedAsPsf: %s') %
121  (self.outOfBounds, self.deblendedAsPsf))
122 
123  @property
124  def psfFitChisq(self):
125  chisq,dof = self.psfFitBest
126  return chisq
127  @property
128  def psfFitDof(self):
129  chisq,dof = self.psfFitBest
130  return dof
131 
132  def getFluxPortion(self, strayFlux=True):
133  '''
134  Return a HeavyFootprint containing the flux apportioned to
135  this peak.
136 
137  'strayFlux': include stray flux also?
138  '''
139  if self.templateFootprint is None or self.fluxPortion is None:
140  return None
142  self.fluxPortion)
143  if strayFlux:
144  if self.strayFlux is not None:
145  heavy.normalize()
146  self.strayFlux.normalize()
147  heavy = afwDet.mergeHeavyFootprintsF(heavy, self.strayFlux)
148 
149  return heavy
150 
151  def setStrayFlux(self, stray):
152  self.strayFlux = stray
153 
154  def setFluxPortion(self, mimg):
155  self.fluxPortion = mimg
156 
157  def setTemplateWeight(self, w):
158  self.templateWeight = w
159 
160  def setPatched(self):
161  self.patched = True
162 
163  # DEBUG
164  def setOrigTemplate(self, t, tfoot):
165  self.origTemplate = t.getImage().Factory(t.getImage(), True)
166  self.origFootprint = tfoot
167  def setRampedTemplate(self, t, tfoot):
168  self.hasRampedTemplate = True
169  self.rampedTemplate = t.getImage().Factory(t.getImage(), True)
170  def setMedianFilteredTemplate(self, t, tfoot):
171  self.medianFilteredTemplate = t.getImage().Factory(t.getImage(), True)
172  def setPsfTemplate(self, tim, tfoot):
174  self.psfTemplate = tim.Factory(tim, True)
175 
176  def setOutOfBounds(self):
177  self.outOfBounds = True
178  self.skip = True
179 
180  def setTinyFootprint(self):
181  self.tinyFootprint = True
182  self.skip = True
183 
184  def setNoValidPixels(self):
185  self.noValidPixels = True
186  self.skip = True
187 
188  def setPsfFitFailed(self):
189  self.psfFitFailed = True
190 
191  def setBadPsfDof(self):
192  self.psfFitBadDof = True
193 
194  def setDeblendedAsPsf(self):
195  self.deblendedAsPsf = True
196 
198  self.failedSymmetricTemplate = True
199  self.skip = True
200 
201  def setTemplate(self, maskedImage, footprint):
202  self.templateMaskedImage = maskedImage
203  self.templateFootprint = footprint
204 
205 
206 def deblend(footprint, maskedImage, psf, psffwhm,
207  psfChisqCut1 = 1.5,
208  psfChisqCut2 = 1.5,
209  psfChisqCut2b = 1.5,
210  fitPsfs = True,
211  medianSmoothTemplate=True,
212  medianFilterHalfsize=2,
213  monotonicTemplate=True,
214  weightTemplates=False,
215  log=None, verbose=False,
216  sigma1=None,
217  maxNumberOfPeaks=0,
218  findStrayFlux=True,
219  assignStrayFlux=True,
220  strayFluxToPointSources='necessary',
221  strayFluxAssignment='r-to-peak',
222  rampFluxAtEdge=False,
223  patchEdges=False,
224  tinyFootprintSize=2,
225  getTemplateSum=False,
226  clipStrayFluxFraction=0.001,
227  clipFootprintToNonzero=True,
228  ):
229  '''
230  Deblend a single ``footprint`` in a ``maskedImage``.
231 
232  Each ``Peak`` in the ``Footprint`` will produce a deblended child.
233 
234  psfChisqCut1, psfChisqCut2: used when deciding whether a given
235  peak looks like a point source. We build a small local model of
236  background + peak + neighboring peaks. These are the
237  chi-squared-per-degree-of-freedom cuts (1=without and 2=with terms
238  allowing the peak position to move); larger values will result in
239  more peaks being classified as PSFs.
240 
241  psfChisqCut2b: if the with-decenter fit is accepted, we then
242  apply the computed dx,dy to the source position and re-fit without
243  decentering. The model is accepted if its chi-squared-per-DOF is
244  less than this value.
245 
246  maxNumberOfPeaks: if positive, only deblend the brightest
247  maxNumberOfPeaks peaks in the parent
248  '''
249  # Import C++ routines
250  import lsst.meas.deblender as deb
251  butils = deb.BaselineUtilsF
252 
253  validStrayPtSrc = ['never', 'necessary', 'always']
254  validStrayAssign = ['r-to-peak', 'r-to-footprint', 'nearest-footprint']
255  if not strayFluxToPointSources in validStrayPtSrc:
256  raise ValueError((('strayFluxToPointSources: value \"%s\" not in the '
257  + 'set of allowed values: ')
258  % strayFluxToPointSources) + str(validStrayPtSrc))
259  if not strayFluxAssignment in validStrayAssign:
260  raise ValueError((('strayFluxAssignment: value \"%s\" not in the '
261  + 'set of allowed values: ')
262  % strayFluxAssignment) + str(validStrayAssign))
263 
264  if log is None:
265  import lsst.pex.logging as pexLogging
266 
267  component = 'meas_deblender.baseline'
268  log = pexLogging.Log(pexLogging.Log.getDefaultLog(), component)
269 
270  if verbose: # pexLogging defines DEBUG in terms of "importance" == -verbosity
271  pexLogging.Trace.setVerbosity(component, -pexLogging.Log.DEBUG)
272 
273  img = maskedImage.getImage()
274  varimg = maskedImage.getVariance()
275  mask = maskedImage.getMask()
276 
277  fp = footprint
278  fp.normalize()
279  peaks = fp.getPeaks()
280  if maxNumberOfPeaks > 0:
281  peaks = peaks[:maxNumberOfPeaks]
282 
283  # Pull out the image bounds of the parent Footprint
284  imbb = img.getBBox()
285  bb = fp.getBBox()
286  if not imbb.contains(bb):
287  raise ValueError(('Footprint bounding-box %s extends outside image '
288  + 'bounding-box %s') % (str(bb), str(imbb)))
289  W,H = bb.getWidth(), bb.getHeight()
290  x0,y0 = bb.getMinX(), bb.getMinY()
291  x1,y1 = bb.getMaxX(), bb.getMaxY()
292 
293  # 'sigma1' is an estimate of the average noise level in this image.
294  if sigma1 is None:
295  # FIXME -- just within the bbox?
296  stats = afwMath.makeStatistics(varimg, mask, afwMath.MEDIAN)
297  sigma1 = math.sqrt(stats.getValue(afwMath.MEDIAN))
298  log.logdebug('Estimated sigma1 = %f' % sigma1)
299 
300  # Add the mask planes we will set.
301  for nm in ['SYMM_1SIG', 'SYMM_3SIG', 'MONOTONIC_1SIG']:
302  mask.addMaskPlane(nm)
303 
304  # get object that will hold our results
305  res = PerFootprint(fp, peaks=peaks)
306 
307  if fitPsfs:
308  # Find peaks that are well-fit by a PSF + background model.
309  _fitPsfs(fp, peaks, res, log, psf, psffwhm, img, varimg,
310  psfChisqCut1, psfChisqCut2, psfChisqCut2b,
311  tinyFootprintSize=tinyFootprintSize)
312 
313  # Create templates...
314  log.logdebug(('Creating templates for footprint at x0,y0,W,H = ' +
315  '(%i,%i, %i,%i)') % (x0,y0,W,H))
316  for peaki,pkres in enumerate(res.peaks):
317  log.logdebug('Deblending peak %i of %i' % (peaki, len(res.peaks)))
318  if pkres.skip or pkres.deblendedAsPsf:
319  continue
320  pk = pkres.peak
321  cx,cy = pk.getIx(), pk.getIy()
322  if not imbb.contains(afwGeom.Point2I(cx,cy)):
323  log.logdebug('Peak center is not inside image; skipping %i' %
324  pkres.pki)
325  pkres.setOutOfBounds()
326  continue
327  log.logdebug('computing template for peak %i at (%i,%i)' %
328  (pkres.pki, cx, cy))
329  S,patched = butils.buildSymmetricTemplate(
330  maskedImage, fp, pk, sigma1, True, patchEdges)
331  # SWIG doesn't know how to unpack a std::pair into a 2-tuple...
332  # (in this case, a nested pair)
333  t1, tfoot = S[0], S[1]
334  del S
335 
336  if t1 is None:
337  log.logdebug(('Peak %i at (%i,%i): failed to build symmetric ' +
338  'template') % (pkres.pki, cx,cy))
339  pkres.setFailedSymmetricTemplate()
340  continue
341 
342  if patched:
343  pkres.setPatched()
344 
345  # possibly save the original symmetric template
346  pkres.setOrigTemplate(t1, tfoot)
347 
348  if rampFluxAtEdge:
349  log.logdebug('Checking for significant flux at edge: sigma1=%g' % sigma1)
350  if (rampFluxAtEdge and
351  butils.hasSignificantFluxAtEdge(t1.getImage(), tfoot, 3*sigma1)):
352  log.logdebug("Template %i has significant flux at edge: ramping" % pkres.pki)
353  (t2, tfoot2, patched) = _handle_flux_at_edge(
354  log, psffwhm, t1, tfoot, fp, maskedImage, x0,x1,y0,y1,
355  psf, pk, sigma1, patchEdges)
356  pkres.setRampedTemplate(t2, tfoot2)
357  if patched:
358  pkres.setPatched()
359  t1 = t2
360  tfoot = tfoot2
361 
362  if medianSmoothTemplate:
363  filtsize = medianFilterHalfsize * 2 + 1
364  if t1.getWidth() >= filtsize and t1.getHeight() >= filtsize:
365  log.logdebug('Median filtering template %i' % pkres.pki)
366  # We want the output to go in "t1", so copy it into
367  # "inimg" for input
368  inimg = t1.Factory(t1, True)
369  butils.medianFilter(inimg, t1, medianFilterHalfsize)
370  # possible save this median-filtered template
371  pkres.setMedianFilteredTemplate(t1, tfoot)
372  else:
373  log.logdebug(('Not median-filtering template %i: size '
374  + '%i x %i smaller than required %i x %i') %
375  (pkres.pki, t1.getWidth(), t1.getHeight(),
376  filtsize, filtsize))
377 
378  if monotonicTemplate:
379  log.logdebug('Making template %i monotonic' % pkres.pki)
380  butils.makeMonotonic(t1, pk)
381 
382  if clipFootprintToNonzero:
383  tfoot.clipToNonzero(t1.getImage())
384  tfoot.normalize()
385 
386  pkres.setTemplate(t1, tfoot)
387 
388  # Prepare inputs to "apportionFlux" call.
389  # template maskedImages
390  tmimgs = []
391  # template footprints
392  tfoots = []
393  # deblended as psf
394  dpsf = []
395  # peak x,y
396  pkx = []
397  pky = []
398  # indices of valid templates
399  ibi = []
400 
401  for peaki,pkres in enumerate(res.peaks):
402  if pkres.skip:
403  continue
404  tmimgs.append(pkres.templateMaskedImage)
405  tfoots.append(pkres.templateFootprint)
406  # for stray flux...
407  dpsf.append(pkres.deblendedAsPsf)
408  pk = pkres.peak
409  pkx.append(pk.getIx())
410  pky.append(pk.getIy())
411  ibi.append(pkres.pki)
412 
413  if weightTemplates:
414  # Reweight the templates by doing a least-squares fit to the image
415  A = np.zeros((W * H, len(tmimgs)))
416  b = img.getArray()[y0:y1+1, x0:x1+1].ravel()
417 
418  for mim,i in zip(tmimgs, ibi):
419  ibb = mim.getBBox()
420  ix0,iy0 = ibb.getMinX(), ibb.getMinY()
421  pkres = res.peaks[i]
422  foot = pkres.templateFootprint
423  ima = mim.getImage().getArray()
424  for sp in foot.getSpans():
425  sy, sx0, sx1 = sp.getY(), sp.getX0(), sp.getX1()
426  imrow = ima[sy-iy0, sx0-ix0 : 1+sx1-ix0]
427  r0 = (sy-y0)*W
428  A[r0 + sx0-x0: r0+1+sx1-x0, i] = imrow
429 
430  X1,r1,rank1,s1 = np.linalg.lstsq(A, b)
431  del A
432  del b
433 
434  for mim,i,w in zip(tmimgs, ibi, X1):
435  im = mim.getImage()
436  im *= w
437  res.peaks[i].setTemplateWeight(w)
438 
439  # FIXME -- Remove templates that are too similar (via dot-product
440  # test)?
441 
442  # Now apportion flux according to the templates
443  log.logdebug('Apportioning flux among %i templates' % len(tmimgs))
444  sumimg = afwImage.ImageF(bb)
445  #.getDimensions())
446  #sumimg.setXY0(bb.getMinX(), bb.getMinY())
447 
448  strayflux = afwDet.HeavyFootprintPtrListF()
449 
450  strayopts = 0
451  if findStrayFlux:
452  strayopts |= butils.ASSIGN_STRAYFLUX
453  if strayFluxToPointSources == 'necessary':
454  strayopts |= butils.STRAYFLUX_TO_POINT_SOURCES_WHEN_NECESSARY
455  elif strayFluxToPointSources == 'always':
456  strayopts |= butils.STRAYFLUX_TO_POINT_SOURCES_ALWAYS
457 
458  if strayFluxAssignment == 'r-to-peak':
459  # this is the default
460  pass
461  elif strayFluxAssignment == 'r-to-footprint':
462  strayopts |= butils.STRAYFLUX_R_TO_FOOTPRINT
463  elif strayFluxAssignment == 'nearest-footprint':
464  strayopts |= butils.STRAYFLUX_NEAREST_FOOTPRINT
465 
466  portions = butils.apportionFlux(maskedImage, fp, tmimgs, tfoots, sumimg,
467  dpsf, pkx, pky, strayflux, strayopts,
468  clipStrayFluxFraction)
469  if getTemplateSum:
470  res.setTemplateSum(sumimg)
471 
472  # Save the apportioned fluxes
473  ii = 0
474  for j, (pk, pkres) in enumerate(zip(peaks, res.peaks)):
475  if pkres.skip:
476  continue
477  pkres.setFluxPortion(portions[ii])
478  ii += 1
479 
480  if findStrayFlux:
481  # NOTE that due to a swig bug
482  # (https://github.com/swig/swig/issues/59)
483  # we CANNOT iterate over "strayflux", but must index into it.
484  stray = strayflux[j]
485  else:
486  stray = None
487 
488  pkres.setStrayFlux(stray)
489 
490  # Set child footprints to contain the right number of peaks.
491  for j, (pk, pkres) in enumerate(zip(peaks, res.peaks)):
492  if pkres.skip:
493  continue
494 
495  for foot,add in [(pkres.templateFootprint, True),
496  (pkres.origFootprint, True),
497  (pkres.strayFlux, False)]:
498  if foot is None:
499  continue
500  pks = foot.getPeaks()
501  pks.clear()
502  if add:
503  pks.push_back(pk)
504 
505  return res
506 
507 class CachingPsf(object):
508  '''
509  In the PSF fitting code, we request PSF models for all peaks near
510  the one being fit. This was turning out to be quite expensive in
511  some cases. Here, we cache the PSF models to bring the cost down
512  closer to O(N) rather than O(N^2).
513  '''
514  def __init__(self, psf):
515  self.cache = {}
516  self.psf = psf
517  def computeImage(self, cx, cy):
518  im = self.cache.get((cx,cy), None)
519  if im is not None:
520  return im
521  im = self.psf.computeImage(afwGeom.Point2D(cx,cy))
522  self.cache[(cx,cy)] = im
523  return im
524 
525 def _fitPsfs(fp, peaks, fpres, log, psf, psffwhm, img, varimg,
526  psfChisqCut1, psfChisqCut2, psfChisqCut2b,
527  **kwargs):
528  '''
529  Fit a PSF + smooth background models to a small region around each
530  peak (plus other peaks that are nearby) in the given list of
531  peaks.
532 
533  fp: Footprint containing the Peaks to model
534  peaks: a list of all the Peak objects within this Footprint
535  fpres: a PerFootprint result object where results will be placed
536 
537  log: pex Log object
538  psf: afw PSF object
539  psffwhm: FWHM of the PSF, in pixels
540  img: umm, the Image in which this Footprint finds itself
541  varimg: Variance plane
542  psfChisqCut*: floats: cuts in chisq-per-pixel at which to accept
543  the PSF model
544 
545  Results go into the fpres.peaks objects.
546 
547  '''
548  fbb = fp.getBBox()
549  cpsf = CachingPsf(psf)
550 
551  # create mask image for pixels within the footprint
552  fmask = afwImage.MaskU(fbb)
553  fmask.setXY0(fbb.getMinX(), fbb.getMinY())
554  afwDet.setMaskFromFootprint(fmask, fp, 1)
555 
556  # pk.getF() -- retrieving the floating-point location of the peak
557  # -- actually shows up in the profile if we do it in the loop, so
558  # grab them all here.
559  peakF = [pk.getF() for pk in peaks]
560 
561  for pki,(pk,pkres,pkF) in enumerate(zip(peaks, fpres.peaks, peakF)):
562  log.logdebug('Peak %i' % pki)
563  _fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peakF, log, cpsf,
564  psffwhm, img, varimg,
565  psfChisqCut1, psfChisqCut2, psfChisqCut2b,
566  **kwargs)
567 
568 
569 def _fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peaksF, log, psf,
570  psffwhm, img, varimg,
571  psfChisqCut1, psfChisqCut2, psfChisqCut2b,
572  tinyFootprintSize=2,
573  ):
574  '''
575  Fit a PSF + smooth background model (linear) to a small region
576  around the peak.
577 
578  fp: Footprint
579  fmask: the Mask plane for pixels in the Footprint
580  pk: the Peak within the Footprint that we are going to fit a PSF model for
581  pkF: the floating-point coordinates of this peak
582  pkres: a PerPeak object that will hold the results for this peak
583  fbb: the bounding-box of this Footprint (a Box2I)
584  peaks: a list of all the Peak objects within this Footprint
585  peaksF: a python list of the floating-point (x,y) peak locations
586  log: pex Log object
587  psf: afw PSF object
588  psffwhm: FWHM of the PSF, in pixels
589  img: umm, the Image in which this Footprint finds itself
590  varimg: Variance plane
591  psfChisqCut*: floats: cuts in chisq-per-pixel at which to accept the PSF model
592 
593  '''
594  import lsstDebug
595  # my __name__ is lsst.meas.deblender.baseline
596  debugPlots = lsstDebug.Info(__name__).plots
597  debugPsf = lsstDebug.Info(__name__).psf
598 
599  # The small region is a disk out to R0, plus a ramp with
600  # decreasing weight down to R1.
601  R0 = int(math.ceil(psffwhm * 1.))
602  # ramp down to zero weight at this radius...
603  R1 = int(math.ceil(psffwhm * 1.5))
604  cx,cy = pkF.getX(), pkF.getY()
605  psfimg = psf.computeImage(cx, cy)
606  # R2: distance to neighbouring peak in order to put it
607  # into the model
608  R2 = R1 + min(psfimg.getWidth(), psfimg.getHeight())/2.
609 
610  pbb = psfimg.getBBox()
611  pbb.clip(fbb)
612  px0,py0 = psfimg.getX0(), psfimg.getY0()
613 
614  # The bounding-box of the local region we are going to fit ("stamp")
615  xlo = int(math.floor(cx - R1))
616  ylo = int(math.floor(cy - R1))
617  xhi = int(math.ceil (cx + R1))
618  yhi = int(math.ceil (cy + R1))
619  stampbb = afwGeom.Box2I(afwGeom.Point2I(xlo,ylo), afwGeom.Point2I(xhi,yhi))
620  stampbb.clip(fbb)
621  xlo,xhi = stampbb.getMinX(), stampbb.getMaxX()
622  ylo,yhi = stampbb.getMinY(), stampbb.getMaxY()
623  if xlo > xhi or ylo > yhi:
624  log.logdebug('Skipping this peak: out of bounds')
625  pkres.setOutOfBounds()
626  return
627 
628  # drop tiny footprints too?
629  if tinyFootprintSize>0 and (((xhi - xlo) < tinyFootprintSize) or
630  ((yhi - ylo) < tinyFootprintSize)):
631  log.logdebug('Skipping this peak: tiny footprint / close to edge')
632  pkres.setTinyFootprint()
633  return
634 
635  # find other peaks within range...
636  otherpeaks = []
637  for pk2,pkF2 in zip(peaks, peaksF):
638  if pk2 == pk:
639  continue
640  if pkF.distanceSquared(pkF2) > R2**2:
641  continue
642  opsfimg = psf.computeImage(pkF2.getX(), pkF2.getY())
643  if not opsfimg.getBBox(afwImage.LOCAL).overlaps(stampbb):
644  continue
645  otherpeaks.append(opsfimg)
646  log.logdebug('%i other peaks within range' % len(otherpeaks))
647 
648  # Now we are going to do a least-squares fit for the flux
649  # in this PSF, plus a decenter term, a linear sky, and
650  # fluxes of nearby sources (assumed point sources). Build
651  # up the matrix...
652  # Number of terms -- PSF flux, constant sky, X, Y, + other PSF fluxes
653  NT1 = 4 + len(otherpeaks)
654  # + PSF dx, dy
655  NT2 = NT1 + 2
656  # Number of pixels -- at most
657  NP = (1 + yhi - ylo) * (1 + xhi - xlo)
658  # indices of columns in the "A" matrix.
659  I_psf = 0
660  I_sky = 1
661  I_sky_ramp_x = 2
662  I_sky_ramp_y = 3
663  # offset of other psf fluxes:
664  I_opsf = 4
665  I_dx = NT1 + 0
666  I_dy = NT1 + 1
667 
668  # Build the matrix "A", rhs "b" and weight "w".
669 
670  ix0,iy0 = img.getX0(), img.getY0()
671  fx0,fy0 = fbb.getMinX(), fbb.getMinY()
672  fslice = (slice(ylo-fy0, yhi-fy0+1), slice(xlo-fx0, xhi-fx0+1))
673  islice = (slice(ylo-iy0, yhi-iy0+1), slice(xlo-ix0, xhi-ix0+1))
674  fmask_sub = fmask .getArray()[fslice]
675  var_sub = varimg.getArray()[islice]
676  img_sub = img .getArray()[islice]
677 
678  # Clip the PSF image to match its bbox
679  psfarr = psfimg.getArray()[pbb.getMinY()-py0: 1+pbb.getMaxY()-py0,
680  pbb.getMinX()-px0: 1+pbb.getMaxX()-px0]
681  px0,px1 = pbb.getMinX(), pbb.getMaxX()
682  py0,py1 = pbb.getMinY(), pbb.getMaxY()
683 
684  # Compute the "valid" pixels within our region-of-interest
685  valid = (fmask_sub > 0)
686  xx,yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
687  RR = ((xx - cx)**2)[np.newaxis,:] + ((yy - cy)**2)[:,np.newaxis]
688  valid *= (RR <= R1**2)
689  valid *= (var_sub > 0)
690  NP = valid.sum()
691 
692  if NP == 0:
693  log.warn('Skipping peak at (%.1f,%.1f): no unmasked pixels nearby'
694  % (cx,cy))
695  pkres.setNoValidPixels()
696  return
697 
698  # pixel coords of valid pixels
699  XX,YY = np.meshgrid(xx, yy)
700  ipixes = np.vstack((XX[valid] - xlo, YY[valid] - ylo)).T
701 
702  inpsfx = (xx >= px0) * (xx <= px1)
703  inpsfy = (yy >= py0) * (yy <= py1)
704  inpsf = np.outer(inpsfy, inpsfx)
705  indx = np.outer(inpsfy, (xx > px0) * (xx < px1))
706  indy = np.outer((yy > py0) * (yy < py1), inpsfx)
707 
708  del inpsfx
709  del inpsfy
710 
711  def _overlap(xlo, xhi, xmin, xmax):
712  assert((xlo <= xmax) and (xhi >= xmin) and
713  (xlo <= xhi) and (xmin <= xmax))
714  xloclamp = max(xlo, xmin)
715  Xlo = xloclamp - xlo
716  xhiclamp = min(xhi, xmax)
717  Xhi = Xlo + (xhiclamp - xloclamp)
718  assert(xloclamp >= 0)
719  assert(Xlo >= 0)
720  return (xloclamp, xhiclamp+1, Xlo, Xhi+1)
721 
722  A = np.zeros((NP, NT2))
723  # Constant term
724  A[:,I_sky] = 1.
725  # Sky slope terms: dx, dy
726  A[:,I_sky_ramp_x] = ipixes[:,0] + (xlo-cx)
727  A[:,I_sky_ramp_y] = ipixes[:,1] + (ylo-cy)
728 
729  # whew, grab the valid overlapping PSF pixels
730  px0,px1 = pbb.getMinX(), pbb.getMaxX()
731  py0,py1 = pbb.getMinY(), pbb.getMaxY()
732  sx1,sx2,sx3,sx4 = _overlap(xlo, xhi, px0, px1)
733  sy1,sy2,sy3,sy4 = _overlap(ylo, yhi, py0, py1)
734  dpx0,dpy0 = px0 - xlo, py0 - ylo
735  psf_y_slice = slice(sy3 - dpy0, sy4 - dpy0)
736  psf_x_slice = slice(sx3 - dpx0, sx4 - dpx0)
737  psfsub = psfarr[psf_y_slice, psf_x_slice]
738  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
739  A[inpsf[valid], I_psf] = psfsub[vsub]
740 
741  # PSF dx -- by taking the half-difference of shifted-by-one and
742  # shifted-by-minus-one.
743  oldsx = (sx1,sx2,sx3,sx4)
744  sx1,sx2,sx3,sx4 = _overlap(xlo, xhi, px0+1, px1-1)
745  psfsub = (psfarr[psf_y_slice, sx3 - dpx0 + 1: sx4 - dpx0 + 1] -
746  psfarr[psf_y_slice, sx3 - dpx0 - 1: sx4 - dpx0 - 1]) / 2.
747  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
748  A[indx[valid], I_dx] = psfsub[vsub]
749  # revert x indices...
750  (sx1,sx2,sx3,sx4) = oldsx
751 
752  # PSF dy
753  sy1,sy2,sy3,sy4 = _overlap(ylo, yhi, py0+1, py1-1)
754  psfsub = (psfarr[sy3 - dpy0 + 1: sy4 - dpy0 + 1, psf_x_slice] -
755  psfarr[sy3 - dpy0 - 1: sy4 - dpy0 - 1, psf_x_slice]) / 2.
756  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
757  A[indy[valid], I_dy] = psfsub[vsub]
758 
759  # other PSFs...
760  for j,opsf in enumerate(otherpeaks):
761  obb = opsf.getBBox()
762  ino = np.outer((yy >= obb.getMinY()) * (yy <= obb.getMaxY()),
763  (xx >= obb.getMinX()) * (xx <= obb.getMaxX()))
764  dpx0,dpy0 = obb.getMinX() - xlo, obb.getMinY() - ylo
765  sx1,sx2,sx3,sx4 = _overlap(xlo, xhi, obb.getMinX(), obb.getMaxX())
766  sy1,sy2,sy3,sy4 = _overlap(ylo, yhi, obb.getMinY(), obb.getMaxY())
767  opsfarr = opsf.getArray()
768  psfsub = opsfarr[sy3 - dpy0 : sy4 - dpy0, sx3 - dpx0: sx4 - dpx0]
769  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
770  A[ino[valid], I_opsf + j] = psfsub[vsub]
771 
772  b = img_sub[valid]
773 
774  # Weights -- from ramp and image variance map.
775  # ramp weights -- from 1 at R0 down to 0 at R1.
776  rw = np.ones_like(RR)
777  ii = (RR > R0**2)
778  rr = np.sqrt(RR[ii])
779  rw[ii] = np.maximum(0, 1. - ((rr - R0) / (R1 - R0)))
780  w = np.sqrt(rw[valid] / var_sub[valid])
781  # save the effective number of pixels
782  sumr = np.sum(rw[valid])
783  log.log(-9, 'sumr = %g' % sumr)
784 
785  del ii
786 
787  Aw = A * w[:,np.newaxis]
788  bw = b * w
789 
790  if debugPlots:
791  import pylab as plt
792  plt.clf()
793  N = NT2 + 2
794  R,C = 2, (N+1) / 2
795  for i in range(NT2):
796  im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
797  im1[ipixes[:,1], ipixes[:,0]] = A[:, i]
798  plt.subplot(R, C, i+1)
799  plt.imshow(im1, interpolation='nearest', origin='lower')
800  plt.subplot(R, C, NT2+1)
801  im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
802  im1[ipixes[:,1], ipixes[:,0]] = b
803  plt.imshow(im1, interpolation='nearest', origin='lower')
804  plt.subplot(R, C, NT2+2)
805  im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
806  im1[ipixes[:,1], ipixes[:,0]] = w
807  plt.imshow(im1, interpolation='nearest', origin='lower')
808  plt.savefig('A.png')
809 
810  # We do fits with and without the decenter (dx,dy) terms.
811  # Since the dx,dy terms are at the end of the matrix,
812  # we can do that just by trimming off those elements.
813  #
814  # The SVD can fail if there are NaNs in the matrices; this should
815  # really be handled upstream
816  try:
817  # NT1 is number of terms without dx,dy;
818  # X1 is the result without decenter
819  X1,r1,rank1,s1 = np.linalg.lstsq(Aw[:,:NT1], bw)
820  # X2 is with decenter
821  X2,r2,rank2,s2 = np.linalg.lstsq(Aw, bw)
822  except np.linalg.LinAlgError, e:
823  log.log(log.WARN, "Failed to fit PSF to child: %s" % e)
824  pkres.setPsfFitFailed()
825  return
826 
827  log.log(-9, 'r1 r2 %g %g' % (r1, r2))
828 
829  # r is weighted chi-squared = sum over pixels: ramp * (model -
830  # data)**2/sigma**2
831  if len(r1) > 0:
832  chisq1 = r1[0]
833  else:
834  chisq1 = 1e30
835  if len(r2) > 0:
836  chisq2 = r2[0]
837  else:
838  chisq2 = 1e30
839  dof1 = sumr - len(X1)
840  dof2 = sumr - len(X2)
841  log.log(-9, 'dof1, dof2 %g %g' % (dof1, dof2))
842 
843  # This can happen if we're very close to the edge (?)
844  if dof1 <= 0 or dof2 <= 0:
845  log.logdebug('Skipping this peak: bad DOF %g, %g' % (dof1, dof2))
846  pkres.setBadPsfDof()
847  return
848 
849  q1 = chisq1/dof1
850  q2 = chisq2/dof2
851  log.logdebug('PSF fits: chisq/dof = %g, %g' % (q1,q2))
852  ispsf1 = (q1 < psfChisqCut1)
853  ispsf2 = (q2 < psfChisqCut2)
854 
855  pkres.psfFit1 = (chisq1, dof1)
856  pkres.psfFit2 = (chisq2, dof2)
857 
858  # check that the fit PSF spatial derivative terms aren't too big
859  if ispsf2:
860  fdx, fdy = X2[I_dx], X2[I_dy]
861  f0 = X2[I_psf]
862  # as a fraction of the PSF flux
863  dx = fdx/f0
864  dy = fdy/f0
865  ispsf2 = ispsf2 and (abs(dx) < 1. and abs(dy) < 1.)
866  log.logdebug('isPSF2 -- checking derivatives: dx,dy = %g, %g -> %s' %
867  (dx,dy, str(ispsf2)))
868  if not ispsf2:
869  pkres.psfFitBigDecenter = True
870 
871  # Looks like a shifted PSF: try actually shifting the PSF by that amount
872  # and re-evaluate the fit.
873  if ispsf2:
874  psfimg2 = psf.computeImage(cx + dx, cy + dy)
875  # clip
876  pbb2 = psfimg2.getBBox()
877  pbb2.clip(fbb)
878  # clip image to bbox
879  px0,py0 = psfimg2.getX0(), psfimg2.getY0()
880  psfarr = psfimg2.getArray()[pbb2.getMinY()-py0: 1+pbb2.getMaxY()-py0,
881  pbb2.getMinX()-px0: 1+pbb2.getMaxX()-px0]
882  px0,py0 = pbb2.getMinX(), pbb2.getMinY()
883  px1,py1 = pbb2.getMaxX(), pbb2.getMaxY()
884 
885  # yuck! Update the PSF terms in the least-squares fit matrix.
886  Ab = A[:,:NT1]
887 
888  sx1,sx2,sx3,sx4 = _overlap(xlo, xhi, px0, px1)
889  sy1,sy2,sy3,sy4 = _overlap(ylo, yhi, py0, py1)
890  dpx0,dpy0 = px0 - xlo, py0 - ylo
891  psfsub = psfarr[sy3 - dpy0 : sy4 - dpy0, sx3 - dpx0: sx4 - dpx0]
892  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
893  xx,yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
894  inpsf = np.outer((yy >= py0) * (yy <= py1), (xx >= px0) * (xx <= px1))
895  Ab[inpsf[valid], I_psf] = psfsub[vsub]
896 
897  Aw = Ab * w[:,np.newaxis]
898  # re-solve...
899  Xb,rb,rankb,sb = np.linalg.lstsq(Aw, bw)
900  if len(rb) > 0:
901  chisqb = rb[0]
902  else:
903  chisqb = 1e30
904  dofb = sumr - len(Xb)
905  log.log(-9, 'rb, dofb %g %g' %(rb, dofb))
906  qb = chisqb / dofb
907  ispsf2 = (qb < psfChisqCut2b)
908  q2 = qb
909  X2 = Xb
910  log.logdebug('shifted PSF: new chisq/dof = %g; good? %s' %
911  (qb, ispsf2))
912  pkres.psfFit3 = (chisqb, dofb)
913 
914  # Which one do we keep?
915  if (((ispsf1 and ispsf2) and (q2 < q1)) or
916  (ispsf2 and not ispsf1)):
917  Xpsf = X2
918  chisq = chisq2
919  dof = dof2
920  log.log(-9, 'dof %g' % dof)
921  log.logdebug('Keeping shifted-PSF model')
922  cx += dx
923  cy += dy
924  pkres.psfFitWithDecenter = True
925  else:
926  # (arbitrarily set to X1 when neither fits well)
927  Xpsf = X1
928  chisq = chisq1
929  dof = dof1
930  log.log(-9, 'dof %g' % dof)
931  log.logdebug('Keeping unshifted PSF model')
932 
933  ispsf = (ispsf1 or ispsf2)
934 
935  # Save the PSF models in images for posterity.
936  if debugPsf:
937  SW,SH = 1+xhi-xlo, 1+yhi-ylo
938  psfmod = afwImage.ImageF(SW,SH)
939  psfmod.setXY0(xlo,ylo)
940  psfderivmodm = afwImage.MaskedImageF(SW,SH)
941  psfderivmod = psfderivmodm.getImage()
942  psfderivmod.setXY0(xlo,ylo)
943  model = afwImage.ImageF(SW,SH)
944  model.setXY0(xlo,ylo)
945  for i in range(len(Xpsf)):
946  for (x,y),v in zip(ipixes, A[:,i]*Xpsf[i]):
947  ix,iy = int(x),int(y)
948  model.set(ix, iy, model.get(ix,iy) + float(v))
949  if i in [I_psf, I_dx, I_dy]:
950  psfderivmod.set(ix, iy, psfderivmod.get(ix,iy) + float(v))
951  for ii in range(NP):
952  x,y = ipixes[ii,:]
953  psfmod.set(int(x),int(y), float(A[ii, I_psf] * Xpsf[I_psf]))
954  modelfp = afwDet.Footprint()
955  for (x,y) in ipixes:
956  modelfp.addSpan(int(y+ylo), int(x+xlo), int(x+xlo))
957  modelfp.normalize()
958 
959  pkres.psfFitDebugPsf0Img = psfimg
960  pkres.psfFitDebugPsfImg = psfmod
961  pkres.psfFitDebugPsfDerivImg = psfderivmod
962  pkres.psfFitDebugPsfModel = model
963  pkres.psfFitDebugStamp = img.Factory(img, stampbb, True)
964  pkres.psfFitDebugValidPix = valid # numpy array
965  pkres.psfFitDebugVar = varimg.Factory(varimg, stampbb, True)
966  ww = np.zeros(valid.shape, np.float)
967  ww[valid] = w
968  pkres.psfFitDebugWeight = ww # numpy
969  pkres.psfFitDebugRampWeight = rw
970 
971 
972 
973  # Save things we learned about this peak for posterity...
974  pkres.psfFitR0 = R0
975  pkres.psfFitR1 = R1
976  pkres.psfFitStampExtent = (xlo, xhi, ylo, yhi)
977  pkres.psfFitCenter = (cx,cy)
978  log.log(-9, 'saving chisq,dof %g %g' % (chisq, dof))
979  pkres.psfFitBest = (chisq, dof)
980  pkres.psfFitParams = Xpsf
981  pkres.psfFitFlux = Xpsf[I_psf]
982  pkres.psfFitNOthers = len(otherpeaks)
983 
984  if ispsf:
985  pkres.setDeblendedAsPsf()
986 
987  # replace the template image by the PSF + derivatives
988  # image.
989  log.logdebug('Deblending as PSF; setting template to PSF model')
990 
991  # Instantiate the PSF model and clip it to the footprint
992  psfimg = psf.computeImage(cx, cy)
993  # Scale by fit flux.
994  psfimg *= Xpsf[I_psf]
995  psfimg = psfimg.convertF()
996 
997  # Clip the Footprint to the PSF model image bbox.
998  fpcopy = afwDet.Footprint(fp)
999  psfbb = psfimg.getBBox()
1000  fpcopy.clipTo(psfbb)
1001  bb = fpcopy.getBBox()
1002 
1003  # Copy the part of the PSF model within the clipped footprint.
1004  psfmod = afwImage.MaskedImageF(bb)
1005  afwDet.copyWithinFootprintImage(fpcopy, psfimg, psfmod.getImage())
1006  # Save it as our template.
1007  fpcopy.clipToNonzero(psfmod.getImage())
1008  fpcopy.normalize()
1009  pkres.setTemplate(psfmod, fpcopy)
1010 
1011  # DEBUG
1012  pkres.setPsfTemplate(psfmod, fpcopy)
1013 
1014 
1015 def _handle_flux_at_edge(log, psffwhm, t1, tfoot, fp, maskedImage,
1016  x0,x1,y0,y1, psf, pk, sigma1, patchEdges):
1017  # Import C++ routines
1018  import lsst.meas.deblender as deb
1019  butils = deb.BaselineUtilsF
1020 
1021  log.logdebug('Found significant flux at template edge.')
1022  # Compute the max of:
1023  # -symmetric-template-clipped image * PSF
1024  # -footprint-clipped image
1025  # Ie, extend the template by the PSF and "fill in" the
1026  # footprint.
1027  # Then find the symmetric template of that image.
1028 
1029  # The size we'll grow by
1030  S = psffwhm * 1.5
1031  # make it an odd integer
1032  S = (int(S + 0.5) / 2) * 2 + 1
1033 
1034  tbb = tfoot.getBBox()
1035  tbb.grow(S)
1036 
1037  # (footprint+margin)-clipped image;
1038  # we need the pixels OUTSIDE the footprint to be 0.
1039  fpcopy = afwDet.Footprint(fp)
1040  fpcopy.clipTo(tbb)
1041  padim = t1.Factory(tbb)
1042  afwDet.copyWithinFootprintMaskedImage(fpcopy, maskedImage, padim)
1043 
1044  # find pixels on the edge of the template
1045  edgepix = butils.getSignificantEdgePixels(t1.getImage(), tfoot, -1e6)
1046 
1047  # instantiate PSF image
1048  xc = int((x0 + x1)/2)
1049  yc = int((y0 + y1)/2)
1050  psfim = psf.computeImage(afwGeom.Point2D(xc, yc))
1051  pbb = psfim.getBBox()
1052  # shift PSF image to by centered on zero
1053  lx,ly = pbb.getMinX(), pbb.getMinY()
1054  psfim.setXY0(lx - xc, ly - yc)
1055  pbb = psfim.getBBox()
1056  # clip PSF to S, if necessary
1057  Sbox = afwGeom.Box2I(afwGeom.Point2I(-S, -S),
1058  afwGeom.Extent2I(2*S+1, 2*S+1))
1059  if not Sbox.contains(pbb):
1060  # clip PSF image
1061  psfim = psfim.Factory(psfim, Sbox, afwImage.PARENT, True)
1062  pbb = psfim.getBBox()
1063  px0 = pbb.getMinX()
1064  px1 = pbb.getMaxX()
1065  py0 = pbb.getMinY()
1066  py1 = pbb.getMaxY()
1067 
1068  # Compute the ramped-down edge pixels
1069  ramped = t1.getImage().Factory(tbb)
1070  Tout = ramped.getArray()
1071  Tin = t1.getImage().getArray()
1072  tx0,ty0 = t1.getX0(), t1.getY0()
1073  ox0,oy0 = ramped.getX0(), ramped.getY0()
1074  P = psfim.getArray()
1075  P /= P.max()
1076  # For each edge pixel, Tout = max(Tout, edgepix * PSF)
1077  for span in edgepix.getSpans():
1078  y = span.getY()
1079  for x in range(span.getX0(), span.getX1()+1):
1080  slc = (slice(y+py0 - oy0, y+py1+1 - oy0),
1081  slice(x+px0 - ox0, x+px1+1 - ox0))
1082  Tout[slc] = np.maximum(Tout[slc], Tin[y-ty0, x-tx0] * P)
1083 
1084  # Fill in the "padim" (which has the right variance and
1085  # mask planes) with the ramped pixels, outside the footprint
1086  I = (padim.getImage().getArray() == 0)
1087  padim.getImage().getArray()[I] = ramped.getArray()[I]
1088 
1089  fpcopy = afwDet.growFootprint(fpcopy, S)
1090  fpcopy.normalize()
1091 
1092  rtn,patched = butils.buildSymmetricTemplate(
1093  padim, fpcopy, pk, sigma1, True, patchEdges)
1094  # silly SWIG can't unpack pairs as tuples
1095  t2, tfoot2 = rtn[0], rtn[1]
1096  del rtn
1097 
1098  # This template footprint may extend outside the parent
1099  # footprint -- or the image. Clip it.
1100  # NOTE that this may make it asymmetric, unlike normal templates.
1101  imbb = maskedImage.getBBox()
1102  tfoot2.clipTo(imbb)
1103  tbb = tfoot2.getBBox()
1104  # clip template image to bbox
1105  t2 = t2.Factory(t2, tbb, afwImage.PARENT, True)
1106 
1107  return t2, tfoot2, patched
boost::shared_ptr< Footprint > growFootprint(Footprint const &foot, int nGrow, bool left, bool right, bool up, bool down)
Grow a Footprint in at least one of the cardinal directions, returning a new Footprint.
a place to record messages and descriptions of the state of processing.
Definition: Log.h:154
An integer coordinate rectangle.
Definition: Box.h:53
double min
Definition: attributes.cc:216
double max
Definition: attributes.cc:218
A set of pixels in an Image.
Definition: Footprint.h:70
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
HeavyFootprint< ImagePixelT, MaskPixelT, VariancePixelT > makeHeavyFootprint(Footprint const &foot, lsst::afw::image::MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > const &img, HeavyFootprintCtrl const *ctrl=NULL)