lsst.meas.deblender  13.0-13-g8b7e855+29
plugins.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # See COPYRIGHT file.
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 <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 import numpy as np
24 from builtins import range
25 
26 import lsst.pex.exceptions
27 import lsst.afw.image as afwImage
28 import lsst.afw.detection as afwDet
29 import lsst.afw.geom as afwGeom
30 
31 # Import C++ routines
32 from .baselineUtils import BaselineUtilsF as butils
33 
34 
35 def clipFootprintToNonzeroImpl(foot, image):
36  '''
37  Clips the given *Footprint* to the region in the *Image*
38  containing non-zero values. The clipping drops spans that are
39  totally zero, and moves endpoints to non-zero; it does not
40  split spans that have internal zeros.
41  '''
42  x0 = image.getX0()
43  y0 = image.getY0()
44  xImMax = x0 + image.getDimensions().getX()
45  yImMax = y0 + image.getDimensions().getY()
46  newSpans = []
47  arr = image.getArray()
48  for span in foot.spans:
49  y = span.getY()
50  if y < y0 or y > yImMax:
51  continue
52  spanX0 = span.getX0()
53  spanX1 = span.getX1()
54  xMin = spanX0 if spanX0 >= x0 else x0
55  xMax = spanX1 if spanX1 <= xImMax else xImMax
56  xarray = np.arange(xMin, xMax+1)[arr[y-y0, xMin-x0:xMax-x0+1] != 0]
57  if len(xarray) > 0:
58  newSpans.append(afwGeom.Span(y, xarray[0], xarray[-1]))
59  # Time to update the SpanSet
60  foot.setSpans(afwGeom.SpanSet(newSpans, normalize=False))
61  foot.removeOrphanPeaks()
62 
63 
64 class DeblenderPlugin(object):
65  """Class to define plugins for the deblender.
66 
67  The new deblender executes a series of plugins specified by the user.
68  Each plugin defines the function to be executed, the keyword arguments required by the function,
69  and whether or not certain portions of the deblender might need to be rerun as a result of
70  the function.
71  """
72  def __init__(self, func, onReset=None, maxIterations=50, **kwargs):
73  """Initialize a deblender plugin
74 
75  Parameters
76  ----------
77  func: `function`
78  Function to run when the plugin is executed. The function should always take
79  `debResult`, a `DeblenderResult` that stores the deblender result, and
80  `log`, an `lsst.log`, as the first two arguments, as well as any additional
81  keyword arguments (that must be specified in ``kwargs``).
82  The function should also return ``modified``, a `bool` that tells the deblender whether
83  or not any templates have been modified by the function.
84  If ``modified==True``, the deblender will go back to step ``onReset``,
85  unless the has already been run ``maxIterations``.
86  onReset: `int`
87  Index of the deblender plugin to return to if ``func`` modifies any templates.
88  The default is ``None``, which does not re-run any plugins.
89  maxIterations: `int`
90  Maximum number of times the deblender will reset when the current plugin
91  returns ``True``.
92  """
93  self.func = func
94  self.kwargs = kwargs
95  self.onReset = onReset
96  self.maxIterations = maxIterations
97  self.kwargs = kwargs
98  self.iterations = 0
99 
100  def run(self, debResult, log):
101  """Execute the current plugin
102 
103  Once the plugin has finished, check to see if part of the deblender must be executed again.
104  """
105  log.trace("Executing %s", self.func.__name__)
106  reset = self.func(debResult, log, **self.kwargs)
107  if reset:
108  self.iterations += 1
109  if self.iterations < self.maxIterations:
110  return self.onReset
111  return None
112 
113  def __str__(self):
114  return ("<Deblender Plugin: func={0}, kwargs={1}".format(self.func.__name__, self.kwargs))
115  def __repr__(self):
116  return self.__str__()
117 
118 
119 def fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2):
120  """Fit a PSF + smooth background model (linear) to a small region around each peak
121 
122  This function will iterate over all filters in deblender result but does not compare
123  results across filters.
124  DeblendedPeaks that pass the cuts have their templates modified to the PSF + background model
125  and their ``deblendedAsPsf`` property set to ``True``.
126 
127  This will likely be replaced in the future with a function that compares the psf chi-squared cuts
128  so that peaks flagged as point sources will be considered point sources in all bands.
129 
130  Parameters
131  ----------
132  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
133  Container for the final deblender results.
134  log: `log.Log`
135  LSST logger for logging purposes.
136  psfChisqCut*: `float`, optional
137  ``psfChisqCut1`` is the maximum chi-squared-per-degree-of-freedom allowed for a peak to
138  be considered a PSF match without recentering.
139  A fit is also made that includes terms to recenter the PSF.
140  ``psfChisqCut2`` is the same as ``psfChisqCut1`` except it determines the restriction on the
141  fit that includes recentering terms.
142  If the peak is a match for a re-centered PSF, the PSF is repositioned at the new center and
143  the peak footprint is fit again, this time to the new PSF.
144  If the resulting chi-squared-per-degree-of-freedom is less than ``psfChisqCut2b`` then it
145  passes the re-centering algorithm.
146  If the peak passes both the re-centered and fixed position cuts, the better of the two is accepted,
147  but parameters for all three psf fits are stored in the ``DebldendedPeak``.
148  The default for ``psfChisqCut1``, ``psfChisqCut2``, and ``psfChisqCut2b`` is ``1.5``.
149  tinyFootprintSize: `float`, optional
150  The PSF model is shrunk to the size that contains the original footprint.
151  If the bbox of the clipped PSF model for a peak is smaller than ``max(tinyFootprintSize,2)``
152  then ``tinyFootprint`` for the peak is set to ``True`` and the peak is not fit.
153  The default is 2.
154 
155  Returns
156  -------
157  modified: `bool`
158  If any templates have been assigned to PSF point sources then ``modified`` is ``True``,
159  otherwise it is ``False``.
160  """
161  from .baseline import CachingPsf
162  modified = False
163  # Loop over all of the filters to build the PSF
164  for fidx in debResult.filters:
165  dp = debResult.deblendedParents[fidx]
166  peaks = dp.fp.getPeaks()
167  cpsf = CachingPsf(dp.psf)
168 
169  # create mask image for pixels within the footprint
170  fmask = afwImage.Mask(dp.bb)
171  fmask.setXY0(dp.bb.getMinX(), dp.bb.getMinY())
172  dp.fp.spans.setMask(fmask, 1)
173 
174  # pk.getF() -- retrieving the floating-point location of the peak
175  # -- actually shows up in the profile if we do it in the loop, so
176  # grab them all here.
177  peakF = [pk.getF() for pk in peaks]
178 
179  for pki, (pk, pkres, pkF) in enumerate(zip(peaks, dp.peaks, peakF)):
180  log.trace('Filter %s, Peak %i', fidx, pki)
181  ispsf = _fitPsf(dp.fp, fmask, pk, pkF, pkres, dp.bb, peaks, peakF, log, cpsf, dp.psffwhm,
182  dp.img, dp.varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, tinyFootprintSize)
183  modified = modified or ispsf
184  return modified
185 
186 def _fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peaksF, log, psf, psffwhm,
187  img, varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b,
188  tinyFootprintSize=2,
189  ):
190  """Fit a PSF + smooth background model (linear) to a small region around a peak.
191 
192  See fitPsfs for a more thorough description, including all parameters not described below.
193 
194  Parameters
195  ----------
196  fp: `afw.detection.Footprint`
197  Footprint containing the Peaks to model.
198  fmask: `afw.image.Mask`
199  The Mask plane for pixels in the Footprint
200  pk: `afw.detection.PeakRecord`
201  The peak within the Footprint that we are going to fit with PSF model
202  pkF: `afw.geom.Point2D`
203  Floating point coordinates of the peak.
204  pkres: `meas.deblender.DeblendedPeak`
205  Peak results object that will hold the results.
206  fbb: `afw.geom.Box2I`
207  Bounding box of ``fp``
208  peaks: `afw.detection.PeakCatalog`
209  Catalog of peaks contained in the parent footprint.
210  peaksF: list of `afw.geom.Point2D`
211  List of floating point coordinates of all of the peaks.
212  psf: list of `afw.detection.Psf`s
213  Psf of the ``maskedImage`` for each band.
214  psffwhm: list pf `float`s
215  FWHM of the ``maskedImage``'s ``psf`` in each band.
216  img: `afw.image.ImageF`
217  The image that contains the footprint.
218  varimg: `afw.image.ImageF`
219  The variance of the image that contains the footprint.
220 
221  Results
222  -------
223  ispsf: `bool`
224  Whether or not the peak matches a PSF model.
225  """
226  import lsstDebug
227 
228  # my __name__ is lsst.meas.deblender.baseline
229  debugPlots = lsstDebug.Info(__name__).plots
230  debugPsf = lsstDebug.Info(__name__).psf
231 
232  # The small region is a disk out to R0, plus a ramp with
233  # decreasing weight down to R1.
234  R0 = int(np.ceil(psffwhm*1.))
235  # ramp down to zero weight at this radius...
236  R1 = int(np.ceil(psffwhm*1.5))
237  cx, cy = pkF.getX(), pkF.getY()
238  psfimg = psf.computeImage(cx, cy)
239  # R2: distance to neighbouring peak in order to put it into the model
240  R2 = R1 + min(psfimg.getWidth(), psfimg.getHeight())/2.
241 
242  pbb = psfimg.getBBox()
243  pbb.clip(fbb)
244  px0, py0 = psfimg.getX0(), psfimg.getY0()
245 
246  # Make sure we haven't been given a substitute PSF that's nowhere near where we want, as may occur if
247  # "Cannot compute CoaddPsf at point (xx,yy); no input images at that point."
248  if not pbb.contains(afwGeom.Point2I(int(cx), int(cy))):
249  pkres.setOutOfBounds()
250  return
251 
252  # The bounding-box of the local region we are going to fit ("stamp")
253  xlo = int(np.floor(cx - R1))
254  ylo = int(np.floor(cy - R1))
255  xhi = int(np.ceil(cx + R1))
256  yhi = int(np.ceil(cy + R1))
257  stampbb = afwGeom.Box2I(afwGeom.Point2I(xlo, ylo), afwGeom.Point2I(xhi, yhi))
258  stampbb.clip(fbb)
259  xlo, xhi = stampbb.getMinX(), stampbb.getMaxX()
260  ylo, yhi = stampbb.getMinY(), stampbb.getMaxY()
261  if xlo > xhi or ylo > yhi:
262  log.trace('Skipping this peak: out of bounds')
263  pkres.setOutOfBounds()
264  return
265 
266  # drop tiny footprints too?
267  if min(stampbb.getWidth(), stampbb.getHeight()) <= max(tinyFootprintSize, 2):
268  # Minimum size limit of 2 comes from the "PSF dx" calculation, which involves shifting the PSF
269  # by one pixel to the left and right.
270  log.trace('Skipping this peak: tiny footprint / close to edge')
271  pkres.setTinyFootprint()
272  return
273 
274  # find other peaks within range...
275  otherpeaks = []
276  for pk2, pkF2 in zip(peaks, peaksF):
277  if pk2 == pk:
278  continue
279  if pkF.distanceSquared(pkF2) > R2**2:
280  continue
281  opsfimg = psf.computeImage(pkF2.getX(), pkF2.getY())
282  if not opsfimg.getBBox().overlaps(stampbb):
283  continue
284  otherpeaks.append(opsfimg)
285  log.trace('%i other peaks within range', len(otherpeaks))
286 
287  # Now we are going to do a least-squares fit for the flux in this
288  # PSF, plus a decenter term, a linear sky, and fluxes of nearby
289  # sources (assumed point sources). Build up the matrix...
290  # Number of terms -- PSF flux, constant sky, X, Y, + other PSF fluxes
291  NT1 = 4 + len(otherpeaks)
292  # + PSF dx, dy
293  NT2 = NT1 + 2
294  # Number of pixels -- at most
295  NP = (1 + yhi - ylo)*(1 + xhi - xlo)
296  # indices of columns in the "A" matrix.
297  I_psf = 0
298  I_sky = 1
299  I_sky_ramp_x = 2
300  I_sky_ramp_y = 3
301  # offset of other psf fluxes:
302  I_opsf = 4
303  I_dx = NT1 + 0
304  I_dy = NT1 + 1
305 
306  # Build the matrix "A", rhs "b" and weight "w".
307  ix0, iy0 = img.getX0(), img.getY0()
308  fx0, fy0 = fbb.getMinX(), fbb.getMinY()
309  fslice = (slice(ylo-fy0, yhi-fy0+1), slice(xlo-fx0, xhi-fx0+1))
310  islice = (slice(ylo-iy0, yhi-iy0+1), slice(xlo-ix0, xhi-ix0+1))
311  fmask_sub = fmask .getArray()[fslice]
312  var_sub = varimg.getArray()[islice]
313  img_sub = img.getArray()[islice]
314 
315  # Clip the PSF image to match its bbox
316  psfarr = psfimg.getArray()[pbb.getMinY()-py0: 1+pbb.getMaxY()-py0,
317  pbb.getMinX()-px0: 1+pbb.getMaxX()-px0]
318  px0, px1 = pbb.getMinX(), pbb.getMaxX()
319  py0, py1 = pbb.getMinY(), pbb.getMaxY()
320 
321  # Compute the "valid" pixels within our region-of-interest
322  valid = (fmask_sub > 0)
323  xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
324  RR = ((xx - cx)**2)[np.newaxis, :] + ((yy - cy)**2)[:, np.newaxis]
325  valid *= (RR <= R1**2)
326  valid *= (var_sub > 0)
327  NP = valid.sum()
328 
329  if NP == 0:
330  log.warn('Skipping peak at (%.1f, %.1f): no unmasked pixels nearby', cx, cy)
331  pkres.setNoValidPixels()
332  return
333 
334  # pixel coords of valid pixels
335  XX, YY = np.meshgrid(xx, yy)
336  ipixes = np.vstack((XX[valid] - xlo, YY[valid] - ylo)).T
337 
338  inpsfx = (xx >= px0)*(xx <= px1)
339  inpsfy = (yy >= py0)*(yy <= py1)
340  inpsf = np.outer(inpsfy, inpsfx)
341  indx = np.outer(inpsfy, (xx > px0)*(xx < px1))
342  indy = np.outer((yy > py0)*(yy < py1), inpsfx)
343 
344  del inpsfx
345  del inpsfy
346 
347  def _overlap(xlo, xhi, xmin, xmax):
348  assert((xlo <= xmax) and (xhi >= xmin) and
349  (xlo <= xhi) and (xmin <= xmax))
350  xloclamp = max(xlo, xmin)
351  Xlo = xloclamp - xlo
352  xhiclamp = min(xhi, xmax)
353  Xhi = Xlo + (xhiclamp - xloclamp)
354  assert(xloclamp >= 0)
355  assert(Xlo >= 0)
356  return (xloclamp, xhiclamp+1, Xlo, Xhi+1)
357 
358  A = np.zeros((NP, NT2))
359  # Constant term
360  A[:, I_sky] = 1.
361  # Sky slope terms: dx, dy
362  A[:, I_sky_ramp_x] = ipixes[:, 0] + (xlo-cx)
363  A[:, I_sky_ramp_y] = ipixes[:, 1] + (ylo-cy)
364 
365  # whew, grab the valid overlapping PSF pixels
366  px0, px1 = pbb.getMinX(), pbb.getMaxX()
367  py0, py1 = pbb.getMinY(), pbb.getMaxY()
368  sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1)
369  sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1)
370  dpx0, dpy0 = px0 - xlo, py0 - ylo
371  psf_y_slice = slice(sy3 - dpy0, sy4 - dpy0)
372  psf_x_slice = slice(sx3 - dpx0, sx4 - dpx0)
373  psfsub = psfarr[psf_y_slice, psf_x_slice]
374  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
375  A[inpsf[valid], I_psf] = psfsub[vsub]
376 
377  # PSF dx -- by taking the half-difference of shifted-by-one and
378  # shifted-by-minus-one.
379  oldsx = (sx1, sx2, sx3, sx4)
380  sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0+1, px1-1)
381  psfsub = (psfarr[psf_y_slice, sx3 - dpx0 + 1: sx4 - dpx0 + 1] -
382  psfarr[psf_y_slice, sx3 - dpx0 - 1: sx4 - dpx0 - 1])/2.
383  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
384  A[indx[valid], I_dx] = psfsub[vsub]
385  # revert x indices...
386  (sx1, sx2, sx3, sx4) = oldsx
387 
388  # PSF dy
389  sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0+1, py1-1)
390  psfsub = (psfarr[sy3 - dpy0 + 1: sy4 - dpy0 + 1, psf_x_slice] -
391  psfarr[sy3 - dpy0 - 1: sy4 - dpy0 - 1, psf_x_slice])/2.
392  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
393  A[indy[valid], I_dy] = psfsub[vsub]
394 
395  # other PSFs...
396  for j, opsf in enumerate(otherpeaks):
397  obb = opsf.getBBox()
398  ino = np.outer((yy >= obb.getMinY())*(yy <= obb.getMaxY()),
399  (xx >= obb.getMinX())*(xx <= obb.getMaxX()))
400  dpx0, dpy0 = obb.getMinX() - xlo, obb.getMinY() - ylo
401  sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, obb.getMinX(), obb.getMaxX())
402  sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, obb.getMinY(), obb.getMaxY())
403  opsfarr = opsf.getArray()
404  psfsub = opsfarr[sy3 - dpy0: sy4 - dpy0, sx3 - dpx0: sx4 - dpx0]
405  vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
406  A[ino[valid], I_opsf + j] = psfsub[vsub]
407 
408  b = img_sub[valid]
409 
410  # Weights -- from ramp and image variance map.
411  # Ramp weights -- from 1 at R0 down to 0 at R1.
412  rw = np.ones_like(RR)
413  ii = (RR > R0**2)
414  rr = np.sqrt(RR[ii])
415  rw[ii] = np.maximum(0, 1. - ((rr - R0)/(R1 - R0)))
416  w = np.sqrt(rw[valid]/var_sub[valid])
417  # save the effective number of pixels
418  sumr = np.sum(rw[valid])
419  log.debug('sumr = %g', sumr)
420 
421  del ii
422 
423  Aw = A*w[:, np.newaxis]
424  bw = b*w
425 
426  if debugPlots:
427  import pylab as plt
428  plt.clf()
429  N = NT2 + 2
430  R, C = 2, (N+1)/2
431  for i in range(NT2):
432  im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
433  im1[ipixes[:, 1], ipixes[:, 0]] = A[:, i]
434  plt.subplot(R, C, i+1)
435  plt.imshow(im1, interpolation='nearest', origin='lower')
436  plt.subplot(R, C, NT2+1)
437  im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
438  im1[ipixes[:, 1], ipixes[:, 0]] = b
439  plt.imshow(im1, interpolation='nearest', origin='lower')
440  plt.subplot(R, C, NT2+2)
441  im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
442  im1[ipixes[:, 1], ipixes[:, 0]] = w
443  plt.imshow(im1, interpolation='nearest', origin='lower')
444  plt.savefig('A.png')
445 
446  # We do fits with and without the decenter (dx,dy) terms.
447  # Since the dx,dy terms are at the end of the matrix,
448  # we can do that just by trimming off those elements.
449  #
450  # The SVD can fail if there are NaNs in the matrices; this should
451  # really be handled upstream
452  try:
453  # NT1 is number of terms without dx,dy;
454  # X1 is the result without decenter
455  X1, r1, rank1, s1 = np.linalg.lstsq(Aw[:, :NT1], bw)
456  # X2 is with decenter
457  X2, r2, rank2, s2 = np.linalg.lstsq(Aw, bw)
458  except np.linalg.LinAlgError as e:
459  log.warn("Failed to fit PSF to child: %s", e)
460  pkres.setPsfFitFailed()
461  return
462 
463  log.debug('r1 r2 %s %s', r1, r2)
464 
465  # r is weighted chi-squared = sum over pixels: ramp * (model -
466  # data)**2/sigma**2
467  if len(r1) > 0:
468  chisq1 = r1[0]
469  else:
470  chisq1 = 1e30
471  if len(r2) > 0:
472  chisq2 = r2[0]
473  else:
474  chisq2 = 1e30
475  dof1 = sumr - len(X1)
476  dof2 = sumr - len(X2)
477  log.debug('dof1, dof2 %g %g', dof1, dof2)
478 
479  # This can happen if we're very close to the edge (?)
480  if dof1 <= 0 or dof2 <= 0:
481  log.trace('Skipping this peak: bad DOF %g, %g', dof1, dof2)
482  pkres.setBadPsfDof()
483  return
484 
485  q1 = chisq1/dof1
486  q2 = chisq2/dof2
487  log.trace('PSF fits: chisq/dof = %g, %g', q1, q2)
488  ispsf1 = (q1 < psfChisqCut1)
489  ispsf2 = (q2 < psfChisqCut2)
490 
491  pkres.psfFit1 = (chisq1, dof1)
492  pkres.psfFit2 = (chisq2, dof2)
493 
494  # check that the fit PSF spatial derivative terms aren't too big
495  if ispsf2:
496  fdx, fdy = X2[I_dx], X2[I_dy]
497  f0 = X2[I_psf]
498  # as a fraction of the PSF flux
499  dx = fdx/f0
500  dy = fdy/f0
501  ispsf2 = ispsf2 and (abs(dx) < 1. and abs(dy) < 1.)
502  log.trace('isPSF2 -- checking derivatives: dx,dy = %g, %g -> %s', dx, dy, str(ispsf2))
503  if not ispsf2:
504  pkres.psfFitBigDecenter = True
505 
506  # Looks like a shifted PSF: try actually shifting the PSF by that amount
507  # and re-evaluate the fit.
508  if ispsf2:
509  psfimg2 = psf.computeImage(cx + dx, cy + dy)
510  # clip
511  pbb2 = psfimg2.getBBox()
512  pbb2.clip(fbb)
513 
514  # Make sure we haven't been given a substitute PSF that's nowhere near where we want, as may occur if
515  # "Cannot compute CoaddPsf at point (xx,yy); no input images at that point."
516  if not pbb2.contains(afwGeom.Point2I(int(cx + dx), int(cy + dy))):
517  ispsf2 = False
518  else:
519  # clip image to bbox
520  px0, py0 = psfimg2.getX0(), psfimg2.getY0()
521  psfarr = psfimg2.getArray()[pbb2.getMinY()-py0:1+pbb2.getMaxY()-py0,
522  pbb2.getMinX()-px0:1+pbb2.getMaxX()-px0]
523  px0, py0 = pbb2.getMinX(), pbb2.getMinY()
524  px1, py1 = pbb2.getMaxX(), pbb2.getMaxY()
525 
526  # yuck! Update the PSF terms in the least-squares fit matrix.
527  Ab = A[:, :NT1]
528 
529  sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1)
530  sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1)
531  dpx0, dpy0 = px0 - xlo, py0 - ylo
532  psfsub = psfarr[sy3-dpy0:sy4-dpy0, sx3-dpx0:sx4-dpx0]
533  vsub = valid[sy1-ylo:sy2-ylo, sx1-xlo:sx2-xlo]
534  xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
535  inpsf = np.outer((yy >= py0)*(yy <= py1), (xx >= px0)*(xx <= px1))
536  Ab[inpsf[valid], I_psf] = psfsub[vsub]
537 
538  Aw = Ab*w[:, np.newaxis]
539  # re-solve...
540  Xb, rb, rankb, sb = np.linalg.lstsq(Aw, bw)
541  if len(rb) > 0:
542  chisqb = rb[0]
543  else:
544  chisqb = 1e30
545  dofb = sumr - len(Xb)
546  qb = chisqb/dofb
547  ispsf2 = (qb < psfChisqCut2b)
548  q2 = qb
549  X2 = Xb
550  log.trace('shifted PSF: new chisq/dof = %g; good? %s', qb, ispsf2)
551  pkres.psfFit3 = (chisqb, dofb)
552 
553  # Which one do we keep?
554  if (((ispsf1 and ispsf2) and (q2 < q1)) or
555  (ispsf2 and not ispsf1)):
556  Xpsf = X2
557  chisq = chisq2
558  dof = dof2
559  log.debug('dof %g', dof)
560  log.trace('Keeping shifted-PSF model')
561  cx += dx
562  cy += dy
563  pkres.psfFitWithDecenter = True
564  else:
565  # (arbitrarily set to X1 when neither fits well)
566  Xpsf = X1
567  chisq = chisq1
568  dof = dof1
569  log.debug('dof %g', dof)
570  log.trace('Keeping unshifted PSF model')
571 
572  ispsf = (ispsf1 or ispsf2)
573 
574  # Save the PSF models in images for posterity.
575  if debugPsf:
576  SW, SH = 1+xhi-xlo, 1+yhi-ylo
577  psfmod = afwImage.ImageF(SW, SH)
578  psfmod.setXY0(xlo, ylo)
579  psfderivmodm = afwImage.MaskedImageF(SW, SH)
580  psfderivmod = psfderivmodm.getImage()
581  psfderivmod.setXY0(xlo, ylo)
582  model = afwImage.ImageF(SW, SH)
583  model.setXY0(xlo, ylo)
584  for i in range(len(Xpsf)):
585  for (x, y), v in zip(ipixes, A[:, i]*Xpsf[i]):
586  ix, iy = int(x), int(y)
587  model.set(ix, iy, model.get(ix, iy) + float(v))
588  if i in [I_psf, I_dx, I_dy]:
589  psfderivmod.set(ix, iy, psfderivmod.get(ix, iy) + float(v))
590  for ii in range(NP):
591  x, y = ipixes[ii, :]
592  psfmod.set(int(x), int(y), float(A[ii, I_psf]*Xpsf[I_psf]))
593  modelfp = afwDet.Footprint(fp.getPeaks().getSchema())
594  for (x, y) in ipixes:
595  modelfp.addSpan(int(y+ylo), int(x+xlo), int(x+xlo))
596  modelfp.normalize()
597 
598  pkres.psfFitDebugPsf0Img = psfimg
599  pkres.psfFitDebugPsfImg = psfmod
600  pkres.psfFitDebugPsfDerivImg = psfderivmod
601  pkres.psfFitDebugPsfModel = model
602  pkres.psfFitDebugStamp = img.Factory(img, stampbb, True)
603  pkres.psfFitDebugValidPix = valid # numpy array
604  pkres.psfFitDebugVar = varimg.Factory(varimg, stampbb, True)
605  ww = np.zeros(valid.shape, np.float)
606  ww[valid] = w
607  pkres.psfFitDebugWeight = ww # numpy
608  pkres.psfFitDebugRampWeight = rw
609 
610  # Save things we learned about this peak for posterity...
611  pkres.psfFitR0 = R0
612  pkres.psfFitR1 = R1
613  pkres.psfFitStampExtent = (xlo, xhi, ylo, yhi)
614  pkres.psfFitCenter = (cx, cy)
615  log.debug('saving chisq,dof %g %g', chisq, dof)
616  pkres.psfFitBest = (chisq, dof)
617  pkres.psfFitParams = Xpsf
618  pkres.psfFitFlux = Xpsf[I_psf]
619  pkres.psfFitNOthers = len(otherpeaks)
620 
621  if ispsf:
622  pkres.setDeblendedAsPsf()
623 
624  # replace the template image by the PSF + derivatives
625  # image.
626  log.trace('Deblending as PSF; setting template to PSF model')
627 
628  # Instantiate the PSF model and clip it to the footprint
629  psfimg = psf.computeImage(cx, cy)
630  # Scale by fit flux.
631  psfimg *= Xpsf[I_psf]
632  psfimg = psfimg.convertF()
633 
634  # Clip the Footprint to the PSF model image bbox.
635  fpcopy = afwDet.Footprint(fp)
636  psfbb = psfimg.getBBox()
637  fpcopy.clipTo(psfbb)
638  bb = fpcopy.getBBox()
639 
640  # Copy the part of the PSF model within the clipped footprint.
641  psfmod = afwImage.ImageF(bb)
642  fpcopy.spans.copyImage(psfimg, psfmod)
643  # Save it as our template.
644  clipFootprintToNonzeroImpl(fpcopy, psfmod)
645  pkres.setTemplate(psfmod, fpcopy)
646 
647  # DEBUG
648  pkres.setPsfTemplate(psfmod, fpcopy)
649 
650  return ispsf
651 
652 def buildSymmetricTemplates(debResult, log, patchEdges=False, setOrigTemplate=True):
653  """Build a symmetric template for each peak in each filter
654 
655  Given ``maskedImageF``, ``footprint``, and a ``DebldendedPeak``, creates a symmetric template
656  (``templateImage`` and ``templateFootprint``) around the peak for all peaks not flagged as
657  ``skip`` or ``deblendedAsPsf``.
658 
659  Parameters
660  ----------
661  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
662  Container for the final deblender results.
663  log: `log.Log`
664  LSST logger for logging purposes.
665  patchEdges: `bool`, optional
666  If True and if the parent Footprint touches pixels with the ``EDGE`` bit set,
667  then grow the parent Footprint to include all symmetric templates.
668 
669  Returns
670  -------
671  modified: `bool`
672  If any peaks are not skipped or marked as point sources, ``modified`` is ``True.
673  Otherwise ``modified`` is ``False``.
674  """
675  modified = False
676  # Create the Templates for each peak in each filter
677  for fidx in debResult.filters:
678  dp = debResult.deblendedParents[fidx]
679  imbb = dp.img.getBBox()
680  log.trace('Creating templates for footprint at x0,y0,W,H = %i, %i, %i, %i)', dp.x0, dp.y0, dp.W, dp.H)
681 
682  for peaki, pkres in enumerate(dp.peaks):
683  log.trace('Deblending peak %i of %i', peaki, len(dp.peaks))
684  # TODO: Check debResult to see if the peak is deblended as a point source
685  # when comparing all bands, not just a single band
686  if pkres.skip or pkres.deblendedAsPsf:
687  continue
688  modified = True
689  pk = pkres.peak
690  cx, cy = pk.getIx(), pk.getIy()
691  if not imbb.contains(afwGeom.Point2I(cx, cy)):
692  log.trace('Peak center is not inside image; skipping %i', pkres.pki)
693  pkres.setOutOfBounds()
694  continue
695  log.trace('computing template for peak %i at (%i, %i)', pkres.pki, cx, cy)
696  timg, tfoot, patched = butils.buildSymmetricTemplate(dp.maskedImage, dp.fp, pk, dp.avgNoise,
697  True, patchEdges)
698  if timg is None:
699  log.trace('Peak %i at (%i, %i): failed to build symmetric template', pkres.pki, cx, cy)
700  pkres.setFailedSymmetricTemplate()
701  continue
702 
703  if patched:
704  pkres.setPatched()
705 
706  # possibly save the original symmetric template
707  if setOrigTemplate:
708  pkres.setOrigTemplate(timg, tfoot)
709  pkres.setTemplate(timg, tfoot)
710  return modified
711 
712 def rampFluxAtEdge(debResult, log, patchEdges=False):
713  """Adjust flux on the edges of the template footprints.
714 
715  Using the PSF, a peak ``Footprint`` with pixels on the edge of ``footprint``
716  is grown by the ``psffwhm``*1.5 and filled in with ramped pixels.
717  The result is a new symmetric footprint template for the peaks near the edge.
718 
719  Parameters
720  ----------
721  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
722  Container for the final deblender results.
723  log: `log.Log`
724  LSST logger for logging purposes.
725  patchEdges: `bool`, optional
726  If True and if the parent Footprint touches pixels with the ``EDGE`` bit set,
727  then grow the parent Footprint to include all symmetric templates.
728 
729  Returns
730  -------
731  modified: `bool`
732  If any peaks have their templates modified to include flux at the edges,
733  ``modified`` is ``True``.
734  """
735  modified = False
736  # Loop over all filters
737  for fidx in debResult.filters:
738  dp = debResult.deblendedParents[fidx]
739  log.trace('Checking for significant flux at edge: sigma1=%g', dp.avgNoise)
740 
741  for peaki, pkres in enumerate(dp.peaks):
742  if pkres.skip or pkres.deblendedAsPsf:
743  continue
744  timg, tfoot = pkres.templateImage, pkres.templateFootprint
745  if butils.hasSignificantFluxAtEdge(timg, tfoot, 3*dp.avgNoise):
746  log.trace("Template %i has significant flux at edge: ramping", pkres.pki)
747  try:
748  (timg2, tfoot2, patched) = _handle_flux_at_edge(log, dp.psffwhm, timg, tfoot, dp.fp,
749  dp.maskedImage, dp.x0, dp.x1,
750  dp.y0, dp.y1, dp.psf, pkres.peak,
751  dp.avgNoise, patchEdges)
752  except lsst.pex.exceptions.Exception as exc:
753  if (isinstance(exc, lsst.pex.exceptions.InvalidParameterError)
754  and "CoaddPsf" in str(exc)):
755  pkres.setOutOfBounds()
756  continue
757  raise
758  pkres.setRampedTemplate(timg2, tfoot2)
759  if patched:
760  pkres.setPatched()
761  pkres.setTemplate(timg2, tfoot2)
762  modified = True
763  return modified
764 
765 def _handle_flux_at_edge(log, psffwhm, t1, tfoot, fp, maskedImage,
766  x0, x1, y0, y1, psf, pk, sigma1, patchEdges
767  ):
768  """Extend a template by the PSF to fill in the footprint.
769 
770  Using the PSF, a footprint that touches the edge is passed to the function
771  and is grown by the psffwhm*1.5 and filled in with ramped pixels.
772 
773  Parameters
774  ----------
775  log: `log.Log`
776  LSST logger for logging purposes.
777  psffwhm: `float`
778  PSF FWHM in pixels.
779  t1: `afw.image.ImageF`
780  The image template that contains the footprint to extend.
781  tfoot: `afw.detection.Footprint`
782  Symmetric Footprint to extend.
783  fp: `afw.detection.Footprint`
784  Parent Footprint that is being deblended.
785  maskedImage: `afw.image.MaskedImageF`
786  Full MaskedImage containing the parent footprint ``fp``.
787  x0,y0: `init`
788  Minimum x,y for the bounding box of the footprint ``fp``.
789  x1,y1: `int`
790  Maximum x,y for the bounding box of the footprint ``fp``.
791  psf: `afw.detection.Psf`
792  PSF of the image.
793  pk: `afw.detection.PeakRecord`
794  The peak within the Footprint whose footprint is being extended.
795  sigma1: `float`
796  Estimated noise level in the image.
797  patchEdges: `bool`
798  If ``patchEdges==True`` and if the footprint touches pixels with the
799  ``EDGE`` bit set, then for spans whose symmetric mirror are outside the
800  image, the symmetric footprint is grown to include them and their
801  pixel values are stored.
802 
803  Results
804  -------
805  t2: `afw.image.ImageF`
806  Image of the extended footprint.
807  tfoot2: `afw.detection.Footprint`
808  Extended Footprint.
809  patched: `bool`
810  If the footprint touches an edge pixel, ``patched`` will be set to ``True``.
811  Otherwise ``patched`` is ``False``.
812  """
813  log.trace('Found significant flux at template edge.')
814  # Compute the max of:
815  # -symmetric-template-clipped image * PSF
816  # -footprint-clipped image
817  # Ie, extend the template by the PSF and "fill in" the footprint.
818  # Then find the symmetric template of that image.
819 
820  # The size we'll grow by
821  S = psffwhm*1.5
822  # make it an odd integer
823  S = int((S + 0.5)/2)*2 + 1
824 
825  tbb = tfoot.getBBox()
826  tbb.grow(S)
827 
828  # (footprint+margin)-clipped image;
829  # we need the pixels OUTSIDE the footprint to be 0.
830  fpcopy = afwDet.Footprint(fp)
831  fpcopy.dilate(S)
832  fpcopy.setSpans(fpcopy.spans.clippedTo(tbb))
833  fpcopy.removeOrphanPeaks()
834  padim = maskedImage.Factory(tbb)
835  fpcopy.spans.clippedTo(maskedImage.getBBox()).copyMaskedImage(maskedImage, padim)
836 
837  # find pixels on the edge of the template
838  edgepix = butils.getSignificantEdgePixels(t1, tfoot, -1e6)
839 
840  # instantiate PSF image
841  xc = int((x0 + x1)/2)
842  yc = int((y0 + y1)/2)
843  psfim = psf.computeImage(afwGeom.Point2D(xc, yc))
844  pbb = psfim.getBBox()
845  # shift PSF image to be centered on zero
846  lx, ly = pbb.getMinX(), pbb.getMinY()
847  psfim.setXY0(lx - xc, ly - yc)
848  pbb = psfim.getBBox()
849  # clip PSF to S, if necessary
850  Sbox = afwGeom.Box2I(afwGeom.Point2I(-S, -S), afwGeom.Extent2I(2*S+1, 2*S+1))
851  if not Sbox.contains(pbb):
852  # clip PSF image
853  psfim = psfim.Factory(psfim, Sbox, afwImage.PARENT, True)
854  pbb = psfim.getBBox()
855  px0 = pbb.getMinX()
856  px1 = pbb.getMaxX()
857  py0 = pbb.getMinY()
858  py1 = pbb.getMaxY()
859 
860  # Compute the ramped-down edge pixels
861  ramped = t1.Factory(tbb)
862  Tout = ramped.getArray()
863  Tin = t1.getArray()
864  tx0, ty0 = t1.getX0(), t1.getY0()
865  ox0, oy0 = ramped.getX0(), ramped.getY0()
866  P = psfim.getArray()
867  P /= P.max()
868  # For each edge pixel, Tout = max(Tout, edgepix * PSF)
869  for span in edgepix.getSpans():
870  y = span.getY()
871  for x in range(span.getX0(), span.getX1()+1):
872  slc = (slice(y+py0 - oy0, y+py1+1 - oy0),
873  slice(x+px0 - ox0, x+px1+1 - ox0))
874  Tout[slc] = np.maximum(Tout[slc], Tin[y-ty0, x-tx0]*P)
875 
876  # Fill in the "padim" (which has the right variance and
877  # mask planes) with the ramped pixels, outside the footprint
878  I = (padim.getImage().getArray() == 0)
879  padim.getImage().getArray()[I] = ramped.getArray()[I]
880 
881  t2, tfoot2, patched = butils.buildSymmetricTemplate(padim, fpcopy, pk, sigma1, True, patchEdges)
882 
883  # This template footprint may extend outside the parent
884  # footprint -- or the image. Clip it.
885  # NOTE that this may make it asymmetric, unlike normal templates.
886  imbb = maskedImage.getBBox()
887  tfoot2.clipTo(imbb)
888  tbb = tfoot2.getBBox()
889  # clip template image to bbox
890  t2 = t2.Factory(t2, tbb, afwImage.PARENT, True)
891 
892  return t2, tfoot2, patched
893 
894 def medianSmoothTemplates(debResult, log, medianFilterHalfsize=2):
895  """Applying median smoothing filter to the template images for every peak in every filter.
896 
897  Parameters
898  ----------
899  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
900  Container for the final deblender results.
901  log: `log.Log`
902  LSST logger for logging purposes.
903  medianFilterHalfSize: `int`, optional
904  Half the box size of the median filter, i.e. a ``medianFilterHalfSize`` of 50 means that
905  each output pixel will be the median of the pixels in a 101 x 101-pixel box in the input image.
906  This parameter is only used when ``medianSmoothTemplate==True``, otherwise it is ignored.
907 
908  Returns
909  -------
910  modified: `bool`
911  Whether or not any templates were modified.
912  This will be ``True`` as long as there is at least one source that is not flagged as a PSF.
913  """
914  modified = False
915  # Loop over all filters
916  for fidx in debResult.filters:
917  dp = debResult.deblendedParents[fidx]
918  for peaki, pkres in enumerate(dp.peaks):
919  if pkres.skip or pkres.deblendedAsPsf:
920  continue
921  modified = True
922  timg, tfoot = pkres.templateImage, pkres.templateFootprint
923  filtsize = medianFilterHalfsize*2 + 1
924  if timg.getWidth() >= filtsize and timg.getHeight() >= filtsize:
925  log.trace('Median filtering template %i', pkres.pki)
926  # We want the output to go in "t1", so copy it into
927  # "inimg" for input
928  inimg = timg.Factory(timg, True)
929  butils.medianFilter(inimg, timg, medianFilterHalfsize)
930  # possible save this median-filtered template
931  pkres.setMedianFilteredTemplate(timg, tfoot)
932  else:
933  log.trace('Not median-filtering template %i: size %i x %i smaller than required %i x %i',
934  pkres.pki, timg.getWidth(), timg.getHeight(), filtsize, filtsize)
935  pkres.setTemplate(timg, tfoot)
936  return modified
937 
938 def makeTemplatesMonotonic(debResult, log):
939  """Make the templates monotonic.
940 
941  The pixels in the templates are modified such that pixels further from the peak will
942  have values smaller than those closer to the peak.
943 
944  Parameters
945  ----------
946  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
947  Container for the final deblender results.
948  log: `log.Log`
949  LSST logger for logging purposes.
950 
951  Returns
952  -------
953  modified: `bool`
954  Whether or not any templates were modified.
955  This will be ``True`` as long as there is at least one source that is not flagged as a PSF.
956  """
957  modified = False
958  # Loop over all filters
959  for fidx in debResult.filters:
960  dp = debResult.deblendedParents[fidx]
961  for peaki, pkres in enumerate(dp.peaks):
962  if pkres.skip or pkres.deblendedAsPsf:
963  continue
964  modified = True
965  timg, tfoot = pkres.templateImage, pkres.templateFootprint
966  pk = pkres.peak
967  log.trace('Making template %i monotonic', pkres.pki)
968  butils.makeMonotonic(timg, pk)
969  pkres.setTemplate(timg, tfoot)
970  return modified
971 
972 def clipFootprintsToNonzero(debResult, log):
973  """Clip non-zero spans in the template footprints for every peak in each filter.
974 
975  Peak ``Footprint``s are clipped to the region in the image containing non-zero values
976  by dropping spans that are completely zero and moving endpoints to non-zero pixels
977  (but does not split spans that have internal zeros).
978 
979  Parameters
980  ----------
981  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
982  Container for the final deblender results.
983  log: `log.Log`
984  LSST logger for logging purposes.
985 
986  Returns
987  -------
988  modified: `bool`
989  Whether or not any templates were modified.
990  This will be ``True`` as long as there is at least one source that is not flagged as a PSF.
991  """
992  modified = False
993  # Loop over all filters
994  for fidx in debResult.filters:
995  dp = debResult.deblendedParents[fidx]
996  for peaki, pkres in enumerate(dp.peaks):
997  if pkres.skip or pkres.deblendedAsPsf:
998  continue
999  modified = True
1000  timg, tfoot = pkres.templateImage, pkres.templateFootprint
1001  clipFootprintToNonzeroImpl(tfoot, timg)
1002  if not tfoot.getBBox().isEmpty() and tfoot.getBBox() != timg.getBBox(afwImage.PARENT):
1003  timg = timg.Factory(timg, tfoot.getBBox(), afwImage.PARENT, True)
1004  pkres.setTemplate(timg, tfoot)
1005  return False
1006 
1007 def weightTemplates(debResult, log):
1008  """Weight the templates to best fit the observed image in each filter
1009 
1010  This function re-weights the templates so that their linear combination best represents
1011  the observed image in that filter.
1012  In the future it may be useful to simultaneously weight all of the filters together.
1013 
1014  Parameters
1015  ----------
1016  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
1017  Container for the final deblender results.
1018  log: `log.Log`
1019  LSST logger for logging purposes.
1020 
1021  Returns
1022  -------
1023  modified: `bool`
1024  ``weightTemplates`` does not actually modify the ``Footprint`` templates other than
1025  to add a weight to them, so ``modified`` is always ``False``.
1026  """
1027  # Weight the templates by doing a least-squares fit to the image
1028  log.trace('Weighting templates')
1029  for fidx in debResult.filters:
1030  _weightTemplates(debResult.deblendedParents[fidx])
1031  return False
1032 
1033 def _weightTemplates(dp):
1034  """Weight the templates to best match the parent Footprint in a single filter
1035 
1036  This includes weighting both regular templates and point source templates
1037 
1038  Parameter
1039  ---------
1040  dp: `DeblendedParent`
1041  The deblended parent to re-weight
1042 
1043  Returns
1044  -------
1045  None
1046  """
1047  nchild = np.sum([pkres.skip is False for pkres in dp.peaks])
1048  A = np.zeros((dp.W*dp.H, nchild))
1049  parentImage = afwImage.ImageF(dp.bb)
1050  afwDet.copyWithinFootprintImage(dp.fp, dp.img, parentImage)
1051  b = parentImage.getArray().ravel()
1052 
1053  index = 0
1054  for pkres in dp.peaks:
1055  if pkres.skip:
1056  continue
1057  childImage = afwImage.ImageF(dp.bb)
1058  afwDet.copyWithinFootprintImage(dp.fp, pkres.templateImage, childImage)
1059  A[:, index] = childImage.getArray().ravel()
1060  index += 1
1061 
1062  X1, r1, rank1, s1 = np.linalg.lstsq(A, b)
1063  del A
1064  del b
1065 
1066  index = 0
1067  for pkres in dp.peaks:
1068  if pkres.skip:
1069  continue
1070  pkres.templateImage *= X1[index]
1071  pkres.setTemplateWeight(X1[index])
1072  index += 1
1073 
1074 def reconstructTemplates(debResult, log, maxTempDotProd=0.5):
1075  """Remove "degenerate templates"
1076 
1077  If galaxies have substructure, such as face-on spirals, the process of identifying peaks can
1078  "shred" the galaxy into many pieces. The templates of shredded galaxies are typically quite
1079  similar because they represent the same galaxy, so we try to identify these "degenerate" peaks
1080  by looking at the inner product (in pixel space) of pairs of templates.
1081  If they are nearly parallel, we only keep one of the peaks and reject the other.
1082  If only one of the peaks is a PSF template, the other template is used,
1083  otherwise the one with the maximum template value is kept.
1084 
1085  Parameters
1086  ----------
1087  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
1088  Container for the final deblender results.
1089  log: `log.Log`
1090  LSST logger for logging purposes.
1091  maxTempDotProd: `float`, optional
1092  All dot products between templates greater than ``maxTempDotProd`` will result in one
1093  of the templates removed.
1094 
1095  Returns
1096  -------
1097  modified: `bool`
1098  If any degenerate templates are found, ``modified`` is ``True``.
1099  """
1100  log.trace('Looking for degnerate templates')
1101 
1102  foundReject = False
1103  for fidx in debResult.filters:
1104  dp = debResult.deblendedParents[fidx]
1105  nchild = np.sum([pkres.skip is False for pkres in dp.peaks])
1106  indexes = [pkres.pki for pkres in dp.peaks if pkres.skip is False]
1107 
1108  # We build a matrix that stores the dot product between templates.
1109  # We convert the template images to HeavyFootprints because they already have a method
1110  # to compute the dot product.
1111  A = np.zeros((nchild, nchild))
1112  maxTemplate = []
1113  heavies = []
1114  for pkres in dp.peaks:
1115  if pkres.skip:
1116  continue
1117  heavies.append(afwDet.makeHeavyFootprint(pkres.templateFootprint,
1118  afwImage.MaskedImageF(pkres.templateImage)))
1119  maxTemplate.append(np.max(pkres.templateImage.getArray()))
1120 
1121  for i in range(nchild):
1122  for j in range(i + 1):
1123  A[i, j] = heavies[i].dot(heavies[j])
1124 
1125  # Normalize the dot products to get the cosine of the angle between templates
1126  for i in range(nchild):
1127  for j in range(i):
1128  norm = A[i, i]*A[j, j]
1129  if norm <= 0:
1130  A[i, j] = 0
1131  else:
1132  A[i, j] /= np.sqrt(norm)
1133 
1134  # Iterate over pairs of objects and find the maximum non-diagonal element of the matrix.
1135  # Exit the loop once we find a single degenerate pair greater than the threshold.
1136  rejectedIndex = -1
1137  for i in range(nchild):
1138  currentMax = 0.
1139  for j in range(i):
1140  if A[i, j] > currentMax:
1141  currentMax = A[i, j]
1142  if currentMax > maxTempDotProd:
1143  foundReject = True
1144  rejectedIndex = j
1145 
1146  if foundReject:
1147  break
1148 
1149  del A
1150 
1151  # If one of the objects is identified as a PSF keep the other one, otherwise keep the one
1152  # with the maximum template value
1153  if foundReject:
1154  keep = indexes[i]
1155  reject = indexes[rejectedIndex]
1156  exitLoop = False
1157  if dp.peaks[keep].deblendedAsPsf and dp.peaks[reject].deblendedAsPsf is False:
1158  keep = indexes[rejectedIndex]
1159  reject = indexes[i]
1160  elif dp.peaks[keep].deblendedAsPsf is False and dp.peaks[reject].deblendedAsPsf:
1161  reject = indexes[rejectedIndex]
1162  keep = indexes[i]
1163  else:
1164  if maxTemplate[rejectedIndex] > maxTemplate[i]:
1165  keep = indexes[rejectedIndex]
1166  reject = indexes[i]
1167  log.trace('Removing object with index %d : %f. Degenerate with %d' % (reject, currentMax,
1168  keep))
1169  dp.peaks[reject].skip = True
1170  dp.peaks[reject].degenerate = True
1171 
1172  return foundReject
1173 
1174 def apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak',
1175  strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001,
1176  getTemplateSum=False):
1177  """Apportion flux to all of the peak templates in each filter
1178 
1179  Divide the ``maskedImage`` flux amongst all of the templates based on the fraction of
1180  flux assigned to each ``template``.
1181  Leftover "stray flux" is assigned to peaks based on the other parameters.
1182 
1183  Parameters
1184  ----------
1185  debResult: `lsst.meas.deblender.baseline.DeblenderResult`
1186  Container for the final deblender results.
1187  log: `log.Log`
1188  LSST logger for logging purposes.
1189  assignStrayFlux: `bool`, optional
1190  If True then flux in the parent footprint that is not covered by any of the
1191  template footprints is assigned to templates based on their 1/(1+r^2) distance.
1192  How the flux is apportioned is determined by ``strayFluxAssignment``.
1193  strayFluxAssignment: `string`, optional
1194  Determines how stray flux is apportioned.
1195  * ``trim``: Trim stray flux and do not include in any footprints
1196  * ``r-to-peak`` (default): Stray flux is assigned based on (1/(1+r^2) from the peaks
1197  * ``r-to-footprint``: Stray flux is distributed to the footprints based on 1/(1+r^2) of the
1198  minimum distance from the stray flux to footprint
1199  * ``nearest-footprint``: Stray flux is assigned to the footprint with lowest L-1 (Manhattan)
1200  distance to the stray flux
1201  strayFluxToPointSources: `string`, optional
1202  Determines how stray flux is apportioned to point sources
1203  * ``never``: never apportion stray flux to point sources
1204  * ``necessary`` (default): point sources are included only if there are no extended sources nearby
1205  * ``always``: point sources are always included in the 1/(1+r^2) splitting
1206  clipStrayFluxFraction: `float`, optional
1207  Minimum stray-flux portion.
1208  Any stray-flux portion less than ``clipStrayFluxFraction`` is clipped to zero.
1209  getTemplateSum: `bool`, optional
1210  As part of the flux calculation, the sum of the templates is calculated.
1211  If ``getTemplateSum==True`` then the sum of the templates is stored in the result
1212  (a `DeblendedFootprint`).
1213 
1214  Returns
1215  -------
1216  modified: `bool`
1217  Apportion flux always modifies the templates, so ``modified`` is always ``True``.
1218  However, this should likely be the final step and it is unlikely that
1219  any deblender plugins will be re-run.
1220  """
1221  validStrayPtSrc = ['never', 'necessary', 'always']
1222  validStrayAssign = ['r-to-peak', 'r-to-footprint', 'nearest-footprint', 'trim']
1223  if strayFluxToPointSources not in validStrayPtSrc:
1224  raise ValueError((('strayFluxToPointSources: value \"%s\" not in the set of allowed values: ') %
1225  strayFluxToPointSources) + str(validStrayPtSrc))
1226  if strayFluxAssignment not in validStrayAssign:
1227  raise ValueError((('strayFluxAssignment: value \"%s\" not in the set of allowed values: ') %
1228  strayFluxAssignment) + str(validStrayAssign))
1229 
1230  for fidx in debResult.filters:
1231  dp = debResult.deblendedParents[fidx]
1232  # Prepare inputs to "apportionFlux" call.
1233  # template maskedImages
1234  tmimgs = []
1235  # template footprints
1236  tfoots = []
1237  # deblended as psf
1238  dpsf = []
1239  # peak x,y
1240  pkx = []
1241  pky = []
1242  # indices of valid templates
1243  ibi = []
1244  bb = dp.fp.getBBox()
1245 
1246  for peaki, pkres in enumerate(dp.peaks):
1247  if pkres.skip:
1248  continue
1249  tmimgs.append(pkres.templateImage)
1250  tfoots.append(pkres.templateFootprint)
1251  # for stray flux...
1252  dpsf.append(pkres.deblendedAsPsf)
1253  pk = pkres.peak
1254  pkx.append(pk.getIx())
1255  pky.append(pk.getIy())
1256  ibi.append(pkres.pki)
1257 
1258  # Now apportion flux according to the templates
1259  log.trace('Apportioning flux among %i templates', len(tmimgs))
1260  sumimg = afwImage.ImageF(bb)
1261  # .getDimensions())
1262  # sumimg.setXY0(bb.getMinX(), bb.getMinY())
1263 
1264  strayopts = 0
1265  if strayFluxAssignment == 'trim':
1266  assignStrayFlux = False
1267  strayopts |= butils.STRAYFLUX_TRIM
1268  if assignStrayFlux:
1269  strayopts |= butils.ASSIGN_STRAYFLUX
1270  if strayFluxToPointSources == 'necessary':
1271  strayopts |= butils.STRAYFLUX_TO_POINT_SOURCES_WHEN_NECESSARY
1272  elif strayFluxToPointSources == 'always':
1273  strayopts |= butils.STRAYFLUX_TO_POINT_SOURCES_ALWAYS
1274 
1275  if strayFluxAssignment == 'r-to-peak':
1276  # this is the default
1277  pass
1278  elif strayFluxAssignment == 'r-to-footprint':
1279  strayopts |= butils.STRAYFLUX_R_TO_FOOTPRINT
1280  elif strayFluxAssignment == 'nearest-footprint':
1281  strayopts |= butils.STRAYFLUX_NEAREST_FOOTPRINT
1282 
1283  portions, strayflux = butils.apportionFlux(dp.maskedImage, dp.fp, tmimgs, tfoots, sumimg, dpsf,
1284  pkx, pky, strayopts, clipStrayFluxFraction)
1285 
1286  # Shrink parent to union of children
1287  if strayFluxAssignment == 'trim':
1288  finalSpanSet = afwGeom.SpanSet()
1289  for foot in tfoots:
1290  finalSpanSet = finalSpanSet.union(foot.spans)
1291  dp.fp.setSpans(finalSpanSet)
1292 
1293  # Store the template sum in the deblender result
1294  if getTemplateSum:
1295  debResult.setTemplateSums(sumimg, fidx)
1296 
1297  # Save the apportioned fluxes
1298  ii = 0
1299  for j, (pk, pkres) in enumerate(zip(dp.fp.getPeaks(), dp.peaks)):
1300  if pkres.skip:
1301  continue
1302  pkres.setFluxPortion(portions[ii])
1303 
1304  if assignStrayFlux:
1305  # NOTE that due to a swig bug (https://github.com/swig/swig/issues/59)
1306  # we CANNOT iterate over "strayflux", but must index into it.
1307  stray = strayflux[ii]
1308  else:
1309  stray = None
1310  ii += 1
1311 
1312  pkres.setStrayFlux(stray)
1313 
1314  # Set child footprints to contain the right number of peaks.
1315  for j, (pk, pkres) in enumerate(zip(dp.fp.getPeaks(), dp.peaks)):
1316  if pkres.skip:
1317  continue
1318 
1319  for foot, add in [(pkres.templateFootprint, True), (pkres.origFootprint, True),
1320  (pkres.strayFlux, False)]:
1321  if foot is None:
1322  continue
1323  pks = foot.getPeaks()
1324  pks.clear()
1325  if add:
1326  pks.append(pk)
1327  return True
def medianSmoothTemplates(debResult, log, medianFilterHalfsize=2)
Definition: plugins.py:894
def clipFootprintToNonzeroImpl(foot, image)
Definition: plugins.py:35
def clipFootprintsToNonzero(debResult, log)
Definition: plugins.py:972
def fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2)
Definition: plugins.py:119
def reconstructTemplates(debResult, log, maxTempDotProd=0.5)
Definition: plugins.py:1074
def rampFluxAtEdge(debResult, log, patchEdges=False)
Definition: plugins.py:712
def apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak', strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001, getTemplateSum=False)
Definition: plugins.py:1176
def buildSymmetricTemplates(debResult, log, patchEdges=False, setOrigTemplate=True)
Definition: plugins.py:652
def weightTemplates(debResult, log)
Definition: plugins.py:1007
def __init__(self, func, onReset=None, maxIterations=50, kwargs)
Definition: plugins.py:72
def makeTemplatesMonotonic(debResult, log)
Definition: plugins.py:938