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