24 from builtins
import range
26 import lsst.pex.exceptions
27 import lsst.afw.image
as afwImage
28 import lsst.afw.detection
as afwDet
29 import lsst.afw.geom
as afwGeom
32 from .baselineUtils
import BaselineUtilsF
as butils
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.
44 xImMax = x0 + image.getDimensions().getX()
45 yImMax = y0 + image.getDimensions().getY()
47 arr = image.getArray()
48 for span
in foot.spans:
50 if y < y0
or y > yImMax:
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]
58 newSpans.append(afwGeom.Span(y, xarray[0], xarray[-1]))
60 foot.setSpans(afwGeom.SpanSet(newSpans, normalize=
False))
61 foot.removeOrphanPeaks()
65 """Class to define plugins for the deblender.
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
72 def __init__(self, func, onReset=None, maxIterations=50, **kwargs):
73 """Initialize a deblender plugin
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``.
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.
90 Maximum number of times the deblender will reset when the current plugin
100 def run(self, debResult, log):
101 """Execute the current plugin
103 Once the plugin has finished, check to see if part of the deblender must be executed again.
105 log.trace(
"Executing %s", self.func.__name__)
106 reset = self.
func(debResult, log, **self.
kwargs)
114 return (
"<Deblender Plugin: func={0}, kwargs={1}".format(self.func.__name__, self.
kwargs))
119 def fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2):
120 """Fit a PSF + smooth background model (linear) to a small region around each peak
122 This function will iterate over all filters in deblender result but does not compare
123 results across filters.
124 DeblendedPeaks that pass the cuts have their templates modified to the PSF + background model
125 and their ``deblendedAsPsf`` property set to ``True``.
127 This will likely be replaced in the future with a function that compares the psf chi-squared cuts
128 so that peaks flagged as point sources will be considered point sources in all bands.
132 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
133 Container for the final deblender results.
135 LSST logger for logging purposes.
136 psfChisqCut*: `float`, optional
137 ``psfChisqCut1`` is the maximum chi-squared-per-degree-of-freedom allowed for a peak to
138 be considered a PSF match without recentering.
139 A fit is also made that includes terms to recenter the PSF.
140 ``psfChisqCut2`` is the same as ``psfChisqCut1`` except it determines the restriction on the
141 fit that includes recentering terms.
142 If the peak is a match for a re-centered PSF, the PSF is repositioned at the new center and
143 the peak footprint is fit again, this time to the new PSF.
144 If the resulting chi-squared-per-degree-of-freedom is less than ``psfChisqCut2b`` then it
145 passes the re-centering algorithm.
146 If the peak passes both the re-centered and fixed position cuts, the better of the two is accepted,
147 but parameters for all three psf fits are stored in the ``DebldendedPeak``.
148 The default for ``psfChisqCut1``, ``psfChisqCut2``, and ``psfChisqCut2b`` is ``1.5``.
149 tinyFootprintSize: `float`, optional
150 The PSF model is shrunk to the size that contains the original footprint.
151 If the bbox of the clipped PSF model for a peak is smaller than ``max(tinyFootprintSize,2)``
152 then ``tinyFootprint`` for the peak is set to ``True`` and the peak is not fit.
158 If any templates have been assigned to PSF point sources then ``modified`` is ``True``,
159 otherwise it is ``False``.
161 from .baseline
import CachingPsf
164 for fidx
in debResult.filters:
165 dp = debResult.deblendedParents[fidx]
166 peaks = dp.fp.getPeaks()
167 cpsf = CachingPsf(dp.psf)
170 fmask = afwImage.MaskU(dp.bb)
171 fmask.setXY0(dp.bb.getMinX(), dp.bb.getMinY())
172 dp.fp.spans.setMask(fmask, 1)
177 peakF = [pk.getF()
for pk
in peaks]
179 for pki, (pk, pkres, pkF)
in enumerate(zip(peaks, dp.peaks, peakF)):
180 log.trace(
'Filter %s, Peak %i', fidx, pki)
181 ispsf = _fitPsf(dp.fp, fmask, pk, pkF, pkres, dp.bb, peaks, peakF, log, cpsf, dp.psffwhm,
182 dp.img, dp.varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, tinyFootprintSize)
183 modified = modified
or ispsf
186 def _fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peaksF, log, psf, psffwhm,
187 img, varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b,
190 """Fit a PSF + smooth background model (linear) to a small region around a peak.
192 See fitPsfs for a more thorough description, including all parameters not described below.
196 fp: `afw.detection.Footprint`
197 Footprint containing the Peaks to model.
198 fmask: `afw.image.MaskU`
199 The Mask plane for pixels in the Footprint
200 pk: `afw.detection.PeakRecord`
201 The peak within the Footprint that we are going to fit with PSF model
202 pkF: `afw.geom.Point2D`
203 Floating point coordinates of the peak.
204 pkres: `meas.deblender.DeblendedPeak`
205 Peak results object that will hold the results.
206 fbb: `afw.geom.Box2I`
207 Bounding box of ``fp``
208 peaks: `afw.detection.PeakCatalog`
209 Catalog of peaks contained in the parent footprint.
210 peaksF: list of `afw.geom.Point2D`
211 List of floating point coordinates of all of the peaks.
212 psf: list of `afw.detection.Psf`s
213 Psf of the ``maskedImage`` for each band.
214 psffwhm: list pf `float`s
215 FWHM of the ``maskedImage``'s ``psf`` in each band.
216 img: `afw.image.ImageF`
217 The image that contains the footprint.
218 varimg: `afw.image.ImageF`
219 The variance of the image that contains the footprint.
224 Whether or not the peak matches a PSF model.
229 debugPlots = lsstDebug.Info(__name__).plots
230 debugPsf = lsstDebug.Info(__name__).psf
234 R0 = int(np.ceil(psffwhm*1.))
236 R1 = int(np.ceil(psffwhm*1.5))
237 cx, cy = pkF.getX(), pkF.getY()
238 psfimg = psf.computeImage(cx, cy)
240 R2 = R1 + min(psfimg.getWidth(), psfimg.getHeight())/2.
242 pbb = psfimg.getBBox()
244 px0, py0 = psfimg.getX0(), psfimg.getY0()
248 if not pbb.contains(afwGeom.Point2I(int(cx), int(cy))):
249 pkres.setOutOfBounds()
253 xlo = int(np.floor(cx - R1))
254 ylo = int(np.floor(cy - R1))
255 xhi = int(np.ceil(cx + R1))
256 yhi = int(np.ceil(cy + R1))
257 stampbb = afwGeom.Box2I(afwGeom.Point2I(xlo, ylo), afwGeom.Point2I(xhi, yhi))
259 xlo, xhi = stampbb.getMinX(), stampbb.getMaxX()
260 ylo, yhi = stampbb.getMinY(), stampbb.getMaxY()
261 if xlo > xhi
or ylo > yhi:
262 log.trace(
'Skipping this peak: out of bounds')
263 pkres.setOutOfBounds()
267 if min(stampbb.getWidth(), stampbb.getHeight()) <= max(tinyFootprintSize, 2):
270 log.trace(
'Skipping this peak: tiny footprint / close to edge')
271 pkres.setTinyFootprint()
276 for pk2, pkF2
in zip(peaks, peaksF):
279 if pkF.distanceSquared(pkF2) > R2**2:
281 opsfimg = psf.computeImage(pkF2.getX(), pkF2.getY())
282 if not opsfimg.getBBox().overlaps(stampbb):
284 otherpeaks.append(opsfimg)
285 log.trace(
'%i other peaks within range', len(otherpeaks))
291 NT1 = 4 + len(otherpeaks)
295 NP = (1 + yhi - ylo)*(1 + xhi - xlo)
307 ix0, iy0 = img.getX0(), img.getY0()
308 fx0, fy0 = fbb.getMinX(), fbb.getMinY()
309 fslice = (slice(ylo-fy0, yhi-fy0+1), slice(xlo-fx0, xhi-fx0+1))
310 islice = (slice(ylo-iy0, yhi-iy0+1), slice(xlo-ix0, xhi-ix0+1))
311 fmask_sub = fmask .getArray()[fslice]
312 var_sub = varimg.getArray()[islice]
313 img_sub = img.getArray()[islice]
316 psfarr = psfimg.getArray()[pbb.getMinY()-py0: 1+pbb.getMaxY()-py0,
317 pbb.getMinX()-px0: 1+pbb.getMaxX()-px0]
318 px0, px1 = pbb.getMinX(), pbb.getMaxX()
319 py0, py1 = pbb.getMinY(), pbb.getMaxY()
322 valid = (fmask_sub > 0)
323 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
324 RR = ((xx - cx)**2)[np.newaxis, :] + ((yy - cy)**2)[:, np.newaxis]
325 valid *= (RR <= R1**2)
326 valid *= (var_sub > 0)
330 log.warn(
'Skipping peak at (%.1f, %.1f): no unmasked pixels nearby', cx, cy)
331 pkres.setNoValidPixels()
335 XX, YY = np.meshgrid(xx, yy)
336 ipixes = np.vstack((XX[valid] - xlo, YY[valid] - ylo)).T
338 inpsfx = (xx >= px0)*(xx <= px1)
339 inpsfy = (yy >= py0)*(yy <= py1)
340 inpsf = np.outer(inpsfy, inpsfx)
341 indx = np.outer(inpsfy, (xx > px0)*(xx < px1))
342 indy = np.outer((yy > py0)*(yy < py1), inpsfx)
347 def _overlap(xlo, xhi, xmin, xmax):
348 assert((xlo <= xmax)
and (xhi >= xmin)
and
349 (xlo <= xhi)
and (xmin <= xmax))
350 xloclamp = max(xlo, xmin)
352 xhiclamp = min(xhi, xmax)
353 Xhi = Xlo + (xhiclamp - xloclamp)
354 assert(xloclamp >= 0)
356 return (xloclamp, xhiclamp+1, Xlo, Xhi+1)
358 A = np.zeros((NP, NT2))
362 A[:, I_sky_ramp_x] = ipixes[:, 0] + (xlo-cx)
363 A[:, I_sky_ramp_y] = ipixes[:, 1] + (ylo-cy)
366 px0, px1 = pbb.getMinX(), pbb.getMaxX()
367 py0, py1 = pbb.getMinY(), pbb.getMaxY()
368 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1)
369 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1)
370 dpx0, dpy0 = px0 - xlo, py0 - ylo
371 psf_y_slice = slice(sy3 - dpy0, sy4 - dpy0)
372 psf_x_slice = slice(sx3 - dpx0, sx4 - dpx0)
373 psfsub = psfarr[psf_y_slice, psf_x_slice]
374 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
375 A[inpsf[valid], I_psf] = psfsub[vsub]
379 oldsx = (sx1, sx2, sx3, sx4)
380 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0+1, px1-1)
381 psfsub = (psfarr[psf_y_slice, sx3 - dpx0 + 1: sx4 - dpx0 + 1] -
382 psfarr[psf_y_slice, sx3 - dpx0 - 1: sx4 - dpx0 - 1])/2.
383 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
384 A[indx[valid], I_dx] = psfsub[vsub]
386 (sx1, sx2, sx3, sx4) = oldsx
389 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0+1, py1-1)
390 psfsub = (psfarr[sy3 - dpy0 + 1: sy4 - dpy0 + 1, psf_x_slice] -
391 psfarr[sy3 - dpy0 - 1: sy4 - dpy0 - 1, psf_x_slice])/2.
392 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
393 A[indy[valid], I_dy] = psfsub[vsub]
396 for j, opsf
in enumerate(otherpeaks):
398 ino = np.outer((yy >= obb.getMinY())*(yy <= obb.getMaxY()),
399 (xx >= obb.getMinX())*(xx <= obb.getMaxX()))
400 dpx0, dpy0 = obb.getMinX() - xlo, obb.getMinY() - ylo
401 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, obb.getMinX(), obb.getMaxX())
402 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, obb.getMinY(), obb.getMaxY())
403 opsfarr = opsf.getArray()
404 psfsub = opsfarr[sy3 - dpy0: sy4 - dpy0, sx3 - dpx0: sx4 - dpx0]
405 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
406 A[ino[valid], I_opsf + j] = psfsub[vsub]
412 rw = np.ones_like(RR)
415 rw[ii] = np.maximum(0, 1. - ((rr - R0)/(R1 - R0)))
416 w = np.sqrt(rw[valid]/var_sub[valid])
418 sumr = np.sum(rw[valid])
419 log.debug(
'sumr = %g', sumr)
423 Aw = A*w[:, np.newaxis]
432 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
433 im1[ipixes[:, 1], ipixes[:, 0]] = A[:, i]
434 plt.subplot(R, C, i+1)
435 plt.imshow(im1, interpolation=
'nearest', origin=
'lower')
436 plt.subplot(R, C, NT2+1)
437 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
438 im1[ipixes[:, 1], ipixes[:, 0]] = b
439 plt.imshow(im1, interpolation=
'nearest', origin=
'lower')
440 plt.subplot(R, C, NT2+2)
441 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
442 im1[ipixes[:, 1], ipixes[:, 0]] = w
443 plt.imshow(im1, interpolation=
'nearest', origin=
'lower')
455 X1, r1, rank1, s1 = np.linalg.lstsq(Aw[:, :NT1], bw)
457 X2, r2, rank2, s2 = np.linalg.lstsq(Aw, bw)
458 except np.linalg.LinAlgError
as e:
459 log.warn(
"Failed to fit PSF to child: %s", e)
460 pkres.setPsfFitFailed()
463 log.debug(
'r1 r2 %s %s', r1, r2)
475 dof1 = sumr - len(X1)
476 dof2 = sumr - len(X2)
477 log.debug(
'dof1, dof2 %g %g', dof1, dof2)
480 if dof1 <= 0
or dof2 <= 0:
481 log.trace(
'Skipping this peak: bad DOF %g, %g', dof1, dof2)
487 log.trace(
'PSF fits: chisq/dof = %g, %g', q1, q2)
488 ispsf1 = (q1 < psfChisqCut1)
489 ispsf2 = (q2 < psfChisqCut2)
491 pkres.psfFit1 = (chisq1, dof1)
492 pkres.psfFit2 = (chisq2, dof2)
496 fdx, fdy = X2[I_dx], X2[I_dy]
501 ispsf2 = ispsf2
and (abs(dx) < 1.
and abs(dy) < 1.)
502 log.trace(
'isPSF2 -- checking derivatives: dx,dy = %g, %g -> %s', dx, dy, str(ispsf2))
504 pkres.psfFitBigDecenter =
True
509 psfimg2 = psf.computeImage(cx + dx, cy + dy)
511 pbb2 = psfimg2.getBBox()
516 if not pbb2.contains(afwGeom.Point2I(int(cx + dx), int(cy + dy))):
520 px0, py0 = psfimg2.getX0(), psfimg2.getY0()
521 psfarr = psfimg2.getArray()[pbb2.getMinY()-py0:1+pbb2.getMaxY()-py0,
522 pbb2.getMinX()-px0:1+pbb2.getMaxX()-px0]
523 px0, py0 = pbb2.getMinX(), pbb2.getMinY()
524 px1, py1 = pbb2.getMaxX(), pbb2.getMaxY()
529 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1)
530 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1)
531 dpx0, dpy0 = px0 - xlo, py0 - ylo
532 psfsub = psfarr[sy3-dpy0:sy4-dpy0, sx3-dpx0:sx4-dpx0]
533 vsub = valid[sy1-ylo:sy2-ylo, sx1-xlo:sx2-xlo]
534 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
535 inpsf = np.outer((yy >= py0)*(yy <= py1), (xx >= px0)*(xx <= px1))
536 Ab[inpsf[valid], I_psf] = psfsub[vsub]
538 Aw = Ab*w[:, np.newaxis]
540 Xb, rb, rankb, sb = np.linalg.lstsq(Aw, bw)
545 dofb = sumr - len(Xb)
547 ispsf2 = (qb < psfChisqCut2b)
550 log.trace(
'shifted PSF: new chisq/dof = %g; good? %s', qb, ispsf2)
551 pkres.psfFit3 = (chisqb, dofb)
554 if (((ispsf1
and ispsf2)
and (q2 < q1))
or
555 (ispsf2
and not ispsf1)):
559 log.debug(
'dof %g', dof)
560 log.trace(
'Keeping shifted-PSF model')
563 pkres.psfFitWithDecenter =
True
569 log.debug(
'dof %g', dof)
570 log.trace(
'Keeping unshifted PSF model')
572 ispsf = (ispsf1
or ispsf2)
576 SW, SH = 1+xhi-xlo, 1+yhi-ylo
577 psfmod = afwImage.ImageF(SW, SH)
578 psfmod.setXY0(xlo, ylo)
579 psfderivmodm = afwImage.MaskedImageF(SW, SH)
580 psfderivmod = psfderivmodm.getImage()
581 psfderivmod.setXY0(xlo, ylo)
582 model = afwImage.ImageF(SW, SH)
583 model.setXY0(xlo, ylo)
584 for i
in range(len(Xpsf)):
585 for (x, y), v
in zip(ipixes, A[:, i]*Xpsf[i]):
586 ix, iy = int(x), int(y)
587 model.set(ix, iy, model.get(ix, iy) + float(v))
588 if i
in [I_psf, I_dx, I_dy]:
589 psfderivmod.set(ix, iy, psfderivmod.get(ix, iy) + float(v))
592 psfmod.set(int(x), int(y), float(A[ii, I_psf]*Xpsf[I_psf]))
593 modelfp = afwDet.Footprint(fp.getPeaks().getSchema())
594 for (x, y)
in ipixes:
595 modelfp.addSpan(int(y+ylo), int(x+xlo), int(x+xlo))
598 pkres.psfFitDebugPsf0Img = psfimg
599 pkres.psfFitDebugPsfImg = psfmod
600 pkres.psfFitDebugPsfDerivImg = psfderivmod
601 pkres.psfFitDebugPsfModel = model
602 pkres.psfFitDebugStamp = img.Factory(img, stampbb,
True)
603 pkres.psfFitDebugValidPix = valid
604 pkres.psfFitDebugVar = varimg.Factory(varimg, stampbb,
True)
605 ww = np.zeros(valid.shape, np.float)
607 pkres.psfFitDebugWeight = ww
608 pkres.psfFitDebugRampWeight = rw
613 pkres.psfFitStampExtent = (xlo, xhi, ylo, yhi)
614 pkres.psfFitCenter = (cx, cy)
615 log.debug(
'saving chisq,dof %g %g', chisq, dof)
616 pkres.psfFitBest = (chisq, dof)
617 pkres.psfFitParams = Xpsf
618 pkres.psfFitFlux = Xpsf[I_psf]
619 pkres.psfFitNOthers = len(otherpeaks)
622 pkres.setDeblendedAsPsf()
626 log.trace(
'Deblending as PSF; setting template to PSF model')
629 psfimg = psf.computeImage(cx, cy)
631 psfimg *= Xpsf[I_psf]
632 psfimg = psfimg.convertF()
635 fpcopy = afwDet.Footprint(fp)
636 psfbb = psfimg.getBBox()
638 bb = fpcopy.getBBox()
641 psfmod = afwImage.ImageF(bb)
642 fpcopy.spans.copyImage(psfimg, psfmod)
645 pkres.setTemplate(psfmod, fpcopy)
648 pkres.setPsfTemplate(psfmod, fpcopy)
653 """Build a symmetric template for each peak in each filter
655 Given ``maskedImageF``, ``footprint``, and a ``DebldendedPeak``, creates a symmetric template
656 (``templateImage`` and ``templateFootprint``) around the peak for all peaks not flagged as
657 ``skip`` or ``deblendedAsPsf``.
661 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
662 Container for the final deblender results.
664 LSST logger for logging purposes.
665 patchEdges: `bool`, optional
666 If True and if the parent Footprint touches pixels with the ``EDGE`` bit set,
667 then grow the parent Footprint to include all symmetric templates.
672 If any peaks are not skipped or marked as point sources, ``modified`` is ``True.
673 Otherwise ``modified`` is ``False``.
677 for fidx
in debResult.filters:
678 dp = debResult.deblendedParents[fidx]
679 imbb = dp.img.getBBox()
680 log.trace(
'Creating templates for footprint at x0,y0,W,H = %i, %i, %i, %i)', dp.x0, dp.y0, dp.W, dp.H)
682 for peaki, pkres
in enumerate(dp.peaks):
683 log.trace(
'Deblending peak %i of %i', peaki, len(dp.peaks))
686 if pkres.skip
or pkres.deblendedAsPsf:
690 cx, cy = pk.getIx(), pk.getIy()
691 if not imbb.contains(afwGeom.Point2I(cx, cy)):
692 log.trace(
'Peak center is not inside image; skipping %i', pkres.pki)
693 pkres.setOutOfBounds()
695 log.trace(
'computing template for peak %i at (%i, %i)', pkres.pki, cx, cy)
696 timg, tfoot, patched = butils.buildSymmetricTemplate(dp.maskedImage, dp.fp, pk, dp.avgNoise,
699 log.trace(
'Peak %i at (%i, %i): failed to build symmetric template', pkres.pki, cx, cy)
700 pkres.setFailedSymmetricTemplate()
708 pkres.setOrigTemplate(timg, tfoot)
709 pkres.setTemplate(timg, tfoot)
713 """Adjust flux on the edges of the template footprints.
715 Using the PSF, a peak ``Footprint`` with pixels on the edge of ``footprint``
716 is grown by the ``psffwhm``*1.5 and filled in with ramped pixels.
717 The result is a new symmetric footprint template for the peaks near the edge.
721 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
722 Container for the final deblender results.
724 LSST logger for logging purposes.
725 patchEdges: `bool`, optional
726 If True and if the parent Footprint touches pixels with the ``EDGE`` bit set,
727 then grow the parent Footprint to include all symmetric templates.
732 If any peaks have their templates modified to include flux at the edges,
733 ``modified`` is ``True``.
737 for fidx
in debResult.filters:
738 dp = debResult.deblendedParents[fidx]
739 log.trace(
'Checking for significant flux at edge: sigma1=%g', dp.avgNoise)
741 for peaki, pkres
in enumerate(dp.peaks):
742 if pkres.skip
or pkres.deblendedAsPsf:
744 timg, tfoot = pkres.templateImage, pkres.templateFootprint
745 if butils.hasSignificantFluxAtEdge(timg, tfoot, 3*dp.avgNoise):
746 log.trace(
"Template %i has significant flux at edge: ramping", pkres.pki)
748 (timg2, tfoot2, patched) = _handle_flux_at_edge(log, dp.psffwhm, timg, tfoot, dp.fp,
749 dp.maskedImage, dp.x0, dp.x1,
750 dp.y0, dp.y1, dp.psf, pkres.peak,
751 dp.avgNoise, patchEdges)
752 except lsst.pex.exceptions.Exception
as exc:
753 if (isinstance(exc, lsst.pex.exceptions.InvalidParameterError)
754 and "CoaddPsf" in str(exc)):
755 pkres.setOutOfBounds()
758 pkres.setRampedTemplate(timg2, tfoot2)
761 pkres.setTemplate(timg2, tfoot2)
765 def _handle_flux_at_edge(log, psffwhm, t1, tfoot, fp, maskedImage,
766 x0, x1, y0, y1, psf, pk, sigma1, patchEdges
768 """Extend a template by the PSF to fill in the footprint.
770 Using the PSF, a footprint that touches the edge is passed to the function
771 and is grown by the psffwhm*1.5 and filled in with ramped pixels.
776 LSST logger for logging purposes.
779 t1: `afw.image.ImageF`
780 The image template that contains the footprint to extend.
781 tfoot: `afw.detection.Footprint`
782 Symmetric Footprint to extend.
783 fp: `afw.detection.Footprint`
784 Parent Footprint that is being deblended.
785 maskedImage: `afw.image.MaskedImageF`
786 Full MaskedImage containing the parent footprint ``fp``.
788 Minimum x,y for the bounding box of the footprint ``fp``.
790 Maximum x,y for the bounding box of the footprint ``fp``.
791 psf: `afw.detection.Psf`
793 pk: `afw.detection.PeakRecord`
794 The peak within the Footprint whose footprint is being extended.
796 Estimated noise level in the image.
798 If ``patchEdges==True`` and if the footprint touches pixels with the
799 ``EDGE`` bit set, then for spans whose symmetric mirror are outside the
800 image, the symmetric footprint is grown to include them and their
801 pixel values are stored.
805 t2: `afw.image.ImageF`
806 Image of the extended footprint.
807 tfoot2: `afw.detection.Footprint`
810 If the footprint touches an edge pixel, ``patched`` will be set to ``True``.
811 Otherwise ``patched`` is ``False``.
813 log.trace(
'Found significant flux at template edge.')
823 S = int((S + 0.5)/2)*2 + 1
825 tbb = tfoot.getBBox()
830 fpcopy = afwDet.Footprint(fp)
832 fpcopy.setSpans(fpcopy.spans.clippedTo(tbb))
833 fpcopy.removeOrphanPeaks()
834 padim = maskedImage.Factory(tbb)
835 fpcopy.spans.clippedTo(maskedImage.getBBox()).copyMaskedImage(maskedImage, padim)
838 edgepix = butils.getSignificantEdgePixels(t1, tfoot, -1e6)
841 xc = int((x0 + x1)/2)
842 yc = int((y0 + y1)/2)
843 psfim = psf.computeImage(afwGeom.Point2D(xc, yc))
844 pbb = psfim.getBBox()
846 lx, ly = pbb.getMinX(), pbb.getMinY()
847 psfim.setXY0(lx - xc, ly - yc)
848 pbb = psfim.getBBox()
850 Sbox = afwGeom.Box2I(afwGeom.Point2I(-S, -S), afwGeom.Extent2I(2*S+1, 2*S+1))
851 if not Sbox.contains(pbb):
853 psfim = psfim.Factory(psfim, Sbox, afwImage.PARENT,
True)
854 pbb = psfim.getBBox()
861 ramped = t1.Factory(tbb)
862 Tout = ramped.getArray()
864 tx0, ty0 = t1.getX0(), t1.getY0()
865 ox0, oy0 = ramped.getX0(), ramped.getY0()
869 for span
in edgepix.getSpans():
871 for x
in range(span.getX0(), span.getX1()+1):
872 slc = (slice(y+py0 - oy0, y+py1+1 - oy0),
873 slice(x+px0 - ox0, x+px1+1 - ox0))
874 Tout[slc] = np.maximum(Tout[slc], Tin[y-ty0, x-tx0]*P)
878 I = (padim.getImage().getArray() == 0)
879 padim.getImage().getArray()[I] = ramped.getArray()[I]
881 t2, tfoot2, patched = butils.buildSymmetricTemplate(padim, fpcopy, pk, sigma1,
True, patchEdges)
886 imbb = maskedImage.getBBox()
888 tbb = tfoot2.getBBox()
890 t2 = t2.Factory(t2, tbb, afwImage.PARENT,
True)
892 return t2, tfoot2, patched
895 """Applying median smoothing filter to the template images for every peak in every filter.
899 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
900 Container for the final deblender results.
902 LSST logger for logging purposes.
903 medianFilterHalfSize: `int`, optional
904 Half the box size of the median filter, i.e. a ``medianFilterHalfSize`` of 50 means that
905 each output pixel will be the median of the pixels in a 101 x 101-pixel box in the input image.
906 This parameter is only used when ``medianSmoothTemplate==True``, otherwise it is ignored.
911 Whether or not any templates were modified.
912 This will be ``True`` as long as there is at least one source that is not flagged as a PSF.
916 for fidx
in debResult.filters:
917 dp = debResult.deblendedParents[fidx]
918 for peaki, pkres
in enumerate(dp.peaks):
919 if pkres.skip
or pkres.deblendedAsPsf:
922 timg, tfoot = pkres.templateImage, pkres.templateFootprint
923 filtsize = medianFilterHalfsize*2 + 1
924 if timg.getWidth() >= filtsize
and timg.getHeight() >= filtsize:
925 log.trace(
'Median filtering template %i', pkres.pki)
928 inimg = timg.Factory(timg,
True)
929 butils.medianFilter(inimg, timg, medianFilterHalfsize)
931 pkres.setMedianFilteredTemplate(timg, tfoot)
933 log.trace(
'Not median-filtering template %i: size %i x %i smaller than required %i x %i',
934 pkres.pki, timg.getWidth(), timg.getHeight(), filtsize, filtsize)
935 pkres.setTemplate(timg, tfoot)
939 """Make the templates monotonic.
941 The pixels in the templates are modified such that pixels further from the peak will
942 have values smaller than those closer to the peak.
946 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
947 Container for the final deblender results.
949 LSST logger for logging purposes.
954 Whether or not any templates were modified.
955 This will be ``True`` as long as there is at least one source that is not flagged as a PSF.
959 for fidx
in debResult.filters:
960 dp = debResult.deblendedParents[fidx]
961 for peaki, pkres
in enumerate(dp.peaks):
962 if pkres.skip
or pkres.deblendedAsPsf:
965 timg, tfoot = pkres.templateImage, pkres.templateFootprint
967 log.trace(
'Making template %i monotonic', pkres.pki)
968 butils.makeMonotonic(timg, pk)
969 pkres.setTemplate(timg, tfoot)
973 """Clip non-zero spans in the template footprints for every peak in each filter.
975 Peak ``Footprint``s are clipped to the region in the image containing non-zero values
976 by dropping spans that are completely zero and moving endpoints to non-zero pixels
977 (but does not split spans that have internal zeros).
981 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
982 Container for the final deblender results.
984 LSST logger for logging purposes.
989 Whether or not any templates were modified.
990 This will be ``True`` as long as there is at least one source that is not flagged as a PSF.
994 for fidx
in debResult.filters:
995 dp = debResult.deblendedParents[fidx]
996 for peaki, pkres
in enumerate(dp.peaks):
997 if pkres.skip
or pkres.deblendedAsPsf:
1000 timg, tfoot = pkres.templateImage, pkres.templateFootprint
1002 if not tfoot.getBBox().isEmpty()
and tfoot.getBBox() != timg.getBBox(afwImage.PARENT):
1003 timg = timg.Factory(timg, tfoot.getBBox(), afwImage.PARENT,
True)
1004 pkres.setTemplate(timg, tfoot)
1008 """Weight the templates to best fit the observed image in each filter
1010 This function re-weights the templates so that their linear combination best represents
1011 the observed image in that filter.
1012 In the future it may be useful to simultaneously weight all of the filters together.
1016 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
1017 Container for the final deblender results.
1019 LSST logger for logging purposes.
1024 ``weightTemplates`` does not actually modify the ``Footprint`` templates other than
1025 to add a weight to them, so ``modified`` is always ``False``.
1028 log.trace(
'Weighting templates')
1029 for fidx
in debResult.filters:
1030 _weightTemplates(debResult.deblendedParents[fidx])
1033 def _weightTemplates(dp):
1034 """Weight the templates to best match the parent Footprint in a single filter
1036 This includes weighting both regular templates and point source templates
1040 dp: `DeblendedParent`
1041 The deblended parent to re-weight
1047 nchild = np.sum([pkres.skip
is False for pkres
in dp.peaks])
1048 A = np.zeros((dp.W*dp.H, nchild))
1049 parentImage = afwImage.ImageF(dp.bb)
1050 afwDet.copyWithinFootprintImage(dp.fp, dp.img, parentImage)
1051 b = parentImage.getArray().ravel()
1054 for pkres
in dp.peaks:
1057 childImage = afwImage.ImageF(dp.bb)
1058 afwDet.copyWithinFootprintImage(dp.fp, pkres.templateImage, childImage)
1059 A[:, index] = childImage.getArray().ravel()
1062 X1, r1, rank1, s1 = np.linalg.lstsq(A, b)
1067 for pkres
in dp.peaks:
1070 pkres.templateImage *= X1[index]
1071 pkres.setTemplateWeight(X1[index])
1075 """Remove "degenerate templates"
1077 If galaxies have substructure, such as face-on spirals, the process of identifying peaks can
1078 "shred" the galaxy into many pieces. The templates of shredded galaxies are typically quite
1079 similar because they represent the same galaxy, so we try to identify these "degenerate" peaks
1080 by looking at the inner product (in pixel space) of pairs of templates.
1081 If they are nearly parallel, we only keep one of the peaks and reject the other.
1082 If only one of the peaks is a PSF template, the other template is used,
1083 otherwise the one with the maximum template value is kept.
1087 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
1088 Container for the final deblender results.
1090 LSST logger for logging purposes.
1091 maxTempDotProd: `float`, optional
1092 All dot products between templates greater than ``maxTempDotProd`` will result in one
1093 of the templates removed.
1098 If any degenerate templates are found, ``modified`` is ``True``.
1100 log.trace(
'Looking for degnerate templates')
1103 for fidx
in debResult.filters:
1104 dp = debResult.deblendedParents[fidx]
1105 nchild = np.sum([pkres.skip
is False for pkres
in dp.peaks])
1106 indexes = [pkres.pki
for pkres
in dp.peaks
if pkres.skip
is False]
1111 A = np.zeros((nchild, nchild))
1114 for pkres
in dp.peaks:
1117 heavies.append(afwDet.makeHeavyFootprint(pkres.templateFootprint,
1118 afwImage.MaskedImageF(pkres.templateImage)))
1119 maxTemplate.append(np.max(pkres.templateImage.getArray()))
1121 for i
in range(nchild):
1122 for j
in range(i + 1):
1123 A[i, j] = heavies[i].dot(heavies[j])
1126 for i
in range(nchild):
1128 norm = A[i, i]*A[j, j]
1132 A[i, j] /= np.sqrt(norm)
1137 for i
in range(nchild):
1140 if A[i, j] > currentMax:
1141 currentMax = A[i, j]
1142 if currentMax > maxTempDotProd:
1155 reject = indexes[rejectedIndex]
1157 if dp.peaks[keep].deblendedAsPsf
and dp.peaks[reject].deblendedAsPsf
is False:
1158 keep = indexes[rejectedIndex]
1160 elif dp.peaks[keep].deblendedAsPsf
is False and dp.peaks[reject].deblendedAsPsf:
1161 reject = indexes[rejectedIndex]
1164 if maxTemplate[rejectedIndex] > maxTemplate[i]:
1165 keep = indexes[rejectedIndex]
1167 log.trace(
'Removing object with index %d : %f. Degenerate with %d' % (reject, currentMax,
1169 dp.peaks[reject].skip =
True
1170 dp.peaks[reject].degenerate =
True
1174 def apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak',
1175 strayFluxToPointSources=
'necessary', clipStrayFluxFraction=0.001,
1176 getTemplateSum=
False):
1177 """Apportion flux to all of the peak templates in each filter
1179 Divide the ``maskedImage`` flux amongst all of the templates based on the fraction of
1180 flux assigned to each ``template``.
1181 Leftover "stray flux" is assigned to peaks based on the other parameters.
1185 debResult: `lsst.meas.deblender.baseline.DeblenderResult`
1186 Container for the final deblender results.
1188 LSST logger for logging purposes.
1189 assignStrayFlux: `bool`, optional
1190 If True then flux in the parent footprint that is not covered by any of the
1191 template footprints is assigned to templates based on their 1/(1+r^2) distance.
1192 How the flux is apportioned is determined by ``strayFluxAssignment``.
1193 strayFluxAssignment: `string`, optional
1194 Determines how stray flux is apportioned.
1195 * ``trim``: Trim stray flux and do not include in any footprints
1196 * ``r-to-peak`` (default): Stray flux is assigned based on (1/(1+r^2) from the peaks
1197 * ``r-to-footprint``: Stray flux is distributed to the footprints based on 1/(1+r^2) of the
1198 minimum distance from the stray flux to footprint
1199 * ``nearest-footprint``: Stray flux is assigned to the footprint with lowest L-1 (Manhattan)
1200 distance to the stray flux
1201 strayFluxToPointSources: `string`, optional
1202 Determines how stray flux is apportioned to point sources
1203 * ``never``: never apportion stray flux to point sources
1204 * ``necessary`` (default): point sources are included only if there are no extended sources nearby
1205 * ``always``: point sources are always included in the 1/(1+r^2) splitting
1206 clipStrayFluxFraction: `float`, optional
1207 Minimum stray-flux portion.
1208 Any stray-flux portion less than ``clipStrayFluxFraction`` is clipped to zero.
1209 getTemplateSum: `bool`, optional
1210 As part of the flux calculation, the sum of the templates is calculated.
1211 If ``getTemplateSum==True`` then the sum of the templates is stored in the result
1212 (a `DeblendedFootprint`).
1217 Apportion flux always modifies the templates, so ``modified`` is always ``True``.
1218 However, this should likely be the final step and it is unlikely that
1219 any deblender plugins will be re-run.
1221 validStrayPtSrc = [
'never',
'necessary',
'always']
1222 validStrayAssign = [
'r-to-peak',
'r-to-footprint',
'nearest-footprint',
'trim']
1223 if strayFluxToPointSources
not in validStrayPtSrc:
1224 raise ValueError(((
'strayFluxToPointSources: value \"%s\" not in the set of allowed values: ') %
1225 strayFluxToPointSources) + str(validStrayPtSrc))
1226 if strayFluxAssignment
not in validStrayAssign:
1227 raise ValueError(((
'strayFluxAssignment: value \"%s\" not in the set of allowed values: ') %
1228 strayFluxAssignment) + str(validStrayAssign))
1230 for fidx
in debResult.filters:
1231 dp = debResult.deblendedParents[fidx]
1244 bb = dp.fp.getBBox()
1246 for peaki, pkres
in enumerate(dp.peaks):
1249 tmimgs.append(pkres.templateImage)
1250 tfoots.append(pkres.templateFootprint)
1252 dpsf.append(pkres.deblendedAsPsf)
1254 pkx.append(pk.getIx())
1255 pky.append(pk.getIy())
1256 ibi.append(pkres.pki)
1259 log.trace(
'Apportioning flux among %i templates', len(tmimgs))
1260 sumimg = afwImage.ImageF(bb)
1265 if strayFluxAssignment ==
'trim':
1266 assignStrayFlux =
False
1267 strayopts |= butils.STRAYFLUX_TRIM
1269 strayopts |= butils.ASSIGN_STRAYFLUX
1270 if strayFluxToPointSources ==
'necessary':
1271 strayopts |= butils.STRAYFLUX_TO_POINT_SOURCES_WHEN_NECESSARY
1272 elif strayFluxToPointSources ==
'always':
1273 strayopts |= butils.STRAYFLUX_TO_POINT_SOURCES_ALWAYS
1275 if strayFluxAssignment ==
'r-to-peak':
1278 elif strayFluxAssignment ==
'r-to-footprint':
1279 strayopts |= butils.STRAYFLUX_R_TO_FOOTPRINT
1280 elif strayFluxAssignment ==
'nearest-footprint':
1281 strayopts |= butils.STRAYFLUX_NEAREST_FOOTPRINT
1283 portions, strayflux = butils.apportionFlux(dp.maskedImage, dp.fp, tmimgs, tfoots, sumimg, dpsf,
1284 pkx, pky, strayopts, clipStrayFluxFraction)
1287 if strayFluxAssignment ==
'trim':
1288 finalSpanSet = afwGeom.SpanSet()
1290 finalSpanSet = finalSpanSet.union(foot.spans)
1291 dp.fp.setSpans(finalSpanSet)
1295 debResult.setTemplateSums(sumimg, fidx)
1299 for j, (pk, pkres)
in enumerate(zip(dp.fp.getPeaks(), dp.peaks)):
1302 pkres.setFluxPortion(portions[ii])
1307 stray = strayflux[ii]
1312 pkres.setStrayFlux(stray)
1315 for j, (pk, pkres)
in enumerate(zip(dp.fp.getPeaks(), dp.peaks)):
1319 for foot, add
in [(pkres.templateFootprint,
True), (pkres.origFootprint,
True),
1320 (pkres.strayFlux,
False)]:
1323 pks = foot.getPeaks()
def clipFootprintsToNonzero
def buildSymmetricTemplates
def makeTemplatesMonotonic
def clipFootprintToNonzeroImpl
def medianSmoothTemplates