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