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