22__all__ = [
"DeblenderPlugin",
"fitPsfs",
"buildSymmetricTemplates",
"rampFluxAtEdge",
23 "medianSmoothTemplates",
"makeTemplatesMonotonic",
"clipFootprintsToNonzero",
24 "weightTemplates",
"reconstructTemplates",
"apportionFlux"]
35from .baselineUtils
import BaselineUtilsF
as bUtils
39 """Clips the given *Footprint* to the region in the *Image*
40 containing non-zero values.
42 The clipping drops spans that are
43 totally zero, and moves endpoints to non-zero; it does
not
44 split spans that have internal zeros.
48 xImMax = x0 + image.getDimensions().getX()
49 yImMax = y0 + image.getDimensions().getY()
51 arr = image.getArray()
52 for span
in foot.spans:
54 if y < y0
or y > yImMax:
58 xMin = spanX0
if spanX0 >= x0
else x0
59 xMax = spanX1
if spanX1 <= xImMax
else xImMax
60 xarray = np.arange(xMin, xMax+1)[arr[y-y0, xMin-x0:xMax-x0+1] != 0]
65 foot.removeOrphanPeaks()
69 """Class to define plugins for the deblender.
71 The new deblender executes a series of plugins specified by the user.
72 Each plugin defines the function to be executed, the keyword arguments
73 required by the function, and whether
or not certain portions of the
74 deblender might need to be rerun
as a result of the function.
76 def __init__(self, func, onReset=None, maxIterations=50, **kwargs):
77 """Initialize a deblender plugin
82 Function to run when the plugin is executed.
83 The function should always take
84 `debResult`, a `DeblenderResult` that stores the deblender result,
85 and `log`, an `
lsst.log`,
as the first two arguments,
as well
as
86 any additional keyword arguments (that must
87 be specified
in ``kwargs``). The function should also
return
88 ``modified``, a `bool` that tells the deblender whether
89 or not any templates have been modified by the function.
90 If ``modified==
True``, the deblender will go back to step
91 ``onReset``, unless the has already been run ``maxIterations``.
93 Index of the deblender plugin to
return to
if ``func`` modifies
94 any templates. The default
is ``
None``, which does
not re-run
97 Maximum number of times the deblender will reset when
108 def run(self, debResult, log):
109 """Execute the current plugin
111 Once the plugin has finished, check to see if part of the deblender
112 must be executed again.
114 log.trace("Executing %s", self.
func.__name__)
115 reset = self.
func(debResult, log, **self.
kwargs)
123 return (
"<Deblender Plugin: func={0}, kwargs={1}".format(self.
func.__name__, self.
kwargs))
130 """Update the peak in each band with an error
132 This function logs an error that occurs during deblending and sets the
138 Container
for the final deblender results.
140 LSST logger
for logging purposes.
142 Number of the peak that failed
144 x coordinate of the peak
146 y coordinate of the peak
148 List of filter names
for the exposures
150 Message to display
in log traceback
152 Name of the flag to set
158 log.trace("Peak %d at (%f,%f):%s", pk, cx, cy, msg)
159 for fidx, f
in enumerate(filters):
160 pkResult = debResult.deblendedParents[f].peaks[pk]
161 getattr(pkResult, flag)()
164def fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2):
165 """Fit a PSF + smooth background model (linear) to a small region
168 This function will iterate over all filters in deblender result but does
169 not compare results across filters.
170 DeblendedPeaks that
pass the cuts have their templates modified to the
171 PSF + background model
and their ``deblendedAsPsf`` property set
174 This will likely be replaced
in the future
with a function that compares
175 the psf chi-squared cuts so that peaks flagged
as point sources will be
176 considered point sources
in all bands.
181 Container
for the final deblender results.
183 LSST logger
for logging purposes.
184 psfChisqCut*: `float`, optional
185 ``psfChisqCut1``
is the maximum chi-squared-per-degree-of-freedom
186 allowed
for a peak to be considered a PSF match without recentering.
187 A fit
is also made that includes terms to recenter the PSF.
188 ``psfChisqCut2``
is the same
as ``psfChisqCut1``
except it
189 determines the restriction on the fit that includes
191 If the peak
is a match
for a re-centered PSF, the PSF
is
192 repositioned at the new center
and
193 the peak footprint
is fit again, this time to the new PSF.
194 If the resulting chi-squared-per-degree-of-freedom
is less than
195 ``psfChisqCut2b`` then it passes the re-centering algorithm.
196 If the peak passes both the re-centered
and fixed position cuts,
197 the better of the two
is accepted, but parameters
for all three psf
198 fits are stored
in the ``DebldendedPeak``.
199 The default
for ``psfChisqCut1``, ``psfChisqCut2``,
and
200 ``psfChisqCut2b``
is ``1.5``.
201 tinyFootprintSize: `float`, optional
202 The PSF model
is shrunk to the size that contains the original
203 footprint. If the bbox of the clipped PSF model
for a peak
is
204 smaller than ``max(tinyFootprintSize,2)`` then ``tinyFootprint``
for
205 the peak
is set to ``
True``
and the peak
is not fit. The default
is 2.
210 If any templates have been assigned to PSF point sources then
211 ``modified``
is ``
True``, otherwise it
is ``
False``.
213 from .baseline
import CachingPsf
216 for fidx
in debResult.filters:
217 dp = debResult.deblendedParents[fidx]
218 peaks = dp.fp.getPeaks()
222 fmask = afwImage.Mask(dp.bb)
223 fmask.setXY0(dp.bb.getMinX(), dp.bb.getMinY())
224 dp.fp.spans.setMask(fmask, 1)
229 peakF = [pk.getF()
for pk
in peaks]
231 for pki, (pk, pkres, pkF)
in enumerate(zip(peaks, dp.peaks, peakF)):
232 log.trace(
'Filter %s, Peak %i', fidx, pki)
233 ispsf =
_fitPsf(dp.fp, fmask, pk, pkF, pkres, dp.bb, peaks, peakF, log, cpsf, dp.psffwhm,
234 dp.img, dp.varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, tinyFootprintSize)
235 modified = modified
or ispsf
239def _fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peaksF, log, psf, psffwhm,
240 img, varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b,
243 r"""Fit a PSF + smooth background model (linear) to a small region
246 See fitPsfs for a more thorough description, including all
247 parameters
not described below.
252 Footprint containing the Peaks to model.
254 The Mask plane
for pixels
in the Footprint
256 The peak within the Footprint that we are going to fit
with PSF model
257 pkF: `afw.geom.Point2D`
258 Floating point coordinates of the peak.
260 Peak results object that will hold the results.
261 fbb: `afw.geom.Box2I`
262 Bounding box of ``fp``
264 Catalog of peaks contained
in the parent footprint.
265 peaksF: list of `afw.geom.Point2D`
266 List of floating point coordinates of all of the peaks.
268 Psf of the ``maskedImage``
for each band.
269 psffwhm: list pf `float`\ s
270 FWHM of the ``maskedImage``\
's ``psf`` in each band.
271 img: `afw.image.ImageF`
272 The image that contains the footprint.
273 varimg: `afw.image.ImageF`
274 The variance of the image that contains the footprint.
279 Whether or not the peak matches a PSF model.
289 R0 = int(np.ceil(psffwhm*1.))
291 R1 = int(np.ceil(psffwhm*1.5))
292 cx, cy = pkF.getX(), pkF.getY()
293 psfimg = psf.computeImage(cx, cy)
295 R2 = R1 + min(psfimg.getWidth(), psfimg.getHeight())/2.
297 pbb = psfimg.getBBox()
299 px0, py0 = psfimg.getX0(), psfimg.getY0()
304 pkres.setOutOfBounds()
308 xlo = int(np.floor(cx - R1))
309 ylo = int(np.floor(cy - R1))
310 xhi = int(np.ceil(cx + R1))
311 yhi = int(np.ceil(cy + R1))
314 xlo, xhi = stampbb.getMinX(), stampbb.getMaxX()
315 ylo, yhi = stampbb.getMinY(), stampbb.getMaxY()
316 if xlo > xhi
or ylo > yhi:
317 log.trace(
'Skipping this peak: out of bounds')
318 pkres.setOutOfBounds()
322 if min(stampbb.getWidth(), stampbb.getHeight()) <= max(tinyFootprintSize, 2):
325 log.trace(
'Skipping this peak: tiny footprint / close to edge')
326 pkres.setTinyFootprint()
331 for pk2, pkF2
in zip(peaks, peaksF):
334 if pkF.distanceSquared(pkF2) > R2**2:
336 opsfimg = psf.computeImage(pkF2.getX(), pkF2.getY())
337 if not opsfimg.getBBox().overlaps(stampbb):
339 otherpeaks.append(opsfimg)
340 log.trace(
'%i other peaks within range', len(otherpeaks))
346 NT1 = 4 + len(otherpeaks)
350 NP = (1 + yhi - ylo)*(1 + xhi - xlo)
362 ix0, iy0 = img.getX0(), img.getY0()
363 fx0, fy0 = fbb.getMinX(), fbb.getMinY()
364 fslice = (slice(ylo-fy0, yhi-fy0+1), slice(xlo-fx0, xhi-fx0+1))
365 islice = (slice(ylo-iy0, yhi-iy0+1), slice(xlo-ix0, xhi-ix0+1))
366 fmask_sub = fmask .getArray()[fslice]
367 var_sub = varimg.getArray()[islice]
368 img_sub = img.getArray()[islice]
371 psfarr = psfimg.getArray()[pbb.getMinY()-py0: 1+pbb.getMaxY()-py0,
372 pbb.getMinX()-px0: 1+pbb.getMaxX()-px0]
373 px0, px1 = pbb.getMinX(), pbb.getMaxX()
374 py0, py1 = pbb.getMinY(), pbb.getMaxY()
377 valid = (fmask_sub > 0)
378 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
379 RR = ((xx - cx)**2)[np.newaxis, :] + ((yy - cy)**2)[:, np.newaxis]
380 valid *= (RR <= R1**2)
381 valid *= (var_sub > 0)
385 log.warning(
'Skipping peak at (%.1f, %.1f): no unmasked pixels nearby', cx, cy)
386 pkres.setNoValidPixels()
390 XX, YY = np.meshgrid(xx, yy)
391 ipixes = np.vstack((XX[valid] - xlo, YY[valid] - ylo)).T
393 inpsfx = (xx >= px0)*(xx <= px1)
394 inpsfy = (yy >= py0)*(yy <= py1)
395 inpsf = np.outer(inpsfy, inpsfx)
396 indx = np.outer(inpsfy, (xx > px0)*(xx < px1))
397 indy = np.outer((yy > py0)*(yy < py1), inpsfx)
402 def _overlap(xlo, xhi, xmin, xmax):
403 assert (xlo <= xmax)
and (xhi >= xmin)
and (xlo <= xhi)
and (xmin <= xmax)
404 xloclamp = max(xlo, xmin)
406 xhiclamp = min(xhi, xmax)
407 Xhi = Xlo + (xhiclamp - xloclamp)
410 return (xloclamp, xhiclamp+1, Xlo, Xhi+1)
412 A = np.zeros((NP, NT2))
416 A[:, I_sky_ramp_x] = ipixes[:, 0] + (xlo-cx)
417 A[:, I_sky_ramp_y] = ipixes[:, 1] + (ylo-cy)
420 px0, px1 = pbb.getMinX(), pbb.getMaxX()
421 py0, py1 = pbb.getMinY(), pbb.getMaxY()
422 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1)
423 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1)
424 dpx0, dpy0 = px0 - xlo, py0 - ylo
425 psf_y_slice = slice(sy3 - dpy0, sy4 - dpy0)
426 psf_x_slice = slice(sx3 - dpx0, sx4 - dpx0)
427 psfsub = psfarr[psf_y_slice, psf_x_slice]
428 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
429 A[inpsf[valid], I_psf] = psfsub[vsub]
433 oldsx = (sx1, sx2, sx3, sx4)
434 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0+1, px1-1)
435 psfsub = (psfarr[psf_y_slice, sx3 - dpx0 + 1: sx4 - dpx0 + 1]
436 - psfarr[psf_y_slice, sx3 - dpx0 - 1: sx4 - dpx0 - 1])/2.
437 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
438 A[indx[valid], I_dx] = psfsub[vsub]
440 (sx1, sx2, sx3, sx4) = oldsx
443 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0+1, py1-1)
444 psfsub = (psfarr[sy3 - dpy0 + 1: sy4 - dpy0 + 1, psf_x_slice]
445 - psfarr[sy3 - dpy0 - 1: sy4 - dpy0 - 1, psf_x_slice])/2.
446 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
447 A[indy[valid], I_dy] = psfsub[vsub]
450 for j, opsf
in enumerate(otherpeaks):
452 ino = np.outer((yy >= obb.getMinY())*(yy <= obb.getMaxY()),
453 (xx >= obb.getMinX())*(xx <= obb.getMaxX()))
454 dpx0, dpy0 = obb.getMinX() - xlo, obb.getMinY() - ylo
455 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, obb.getMinX(), obb.getMaxX())
456 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, obb.getMinY(), obb.getMaxY())
457 opsfarr = opsf.getArray()
458 psfsub = opsfarr[sy3 - dpy0: sy4 - dpy0, sx3 - dpx0: sx4 - dpx0]
459 vsub = valid[sy1-ylo: sy2-ylo, sx1-xlo: sx2-xlo]
460 A[ino[valid], I_opsf + j] = psfsub[vsub]
466 rw = np.ones_like(RR)
469 rw[ii] = np.maximum(0, 1. - ((rr - R0)/(R1 - R0)))
470 w = np.sqrt(rw[valid]/var_sub[valid])
472 sumr = np.sum(rw[valid])
473 log.debug(
'sumr = %g', sumr)
477 Aw = A*w[:, np.newaxis]
486 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
487 im1[ipixes[:, 1], ipixes[:, 0]] = A[:, i]
488 plt.subplot(R, C, i+1)
489 plt.imshow(im1, interpolation=
'nearest', origin=
'lower')
490 plt.subplot(R, C, NT2+1)
491 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
492 im1[ipixes[:, 1], ipixes[:, 0]] = b
493 plt.imshow(im1, interpolation=
'nearest', origin=
'lower')
494 plt.subplot(R, C, NT2+2)
495 im1 = np.zeros((1+yhi-ylo, 1+xhi-xlo))
496 im1[ipixes[:, 1], ipixes[:, 0]] = w
497 plt.imshow(im1, interpolation=
'nearest', origin=
'lower')
509 X1, r1, rank1, s1 = np.linalg.lstsq(Aw[:, :NT1], bw, rcond=-1)
511 X2, r2, rank2, s2 = np.linalg.lstsq(Aw, bw, rcond=-1)
512 except np.linalg.LinAlgError
as e:
513 log.warning(
"Failed to fit PSF to child: %s", e)
514 pkres.setPsfFitFailed()
517 log.debug(
'r1 r2 %s %s', r1, r2)
529 dof1 = sumr - len(X1)
530 dof2 = sumr - len(X2)
531 log.debug(
'dof1, dof2 %g %g', dof1, dof2)
534 if dof1 <= 0
or dof2 <= 0:
535 log.trace(
'Skipping this peak: bad DOF %g, %g', dof1, dof2)
541 log.trace(
'PSF fits: chisq/dof = %g, %g', q1, q2)
542 ispsf1 = (q1 < psfChisqCut1)
543 ispsf2 = (q2 < psfChisqCut2)
545 pkres.psfFit1 = (chisq1, dof1)
546 pkres.psfFit2 = (chisq2, dof2)
550 fdx, fdy = X2[I_dx], X2[I_dy]
555 ispsf2 = ispsf2
and (abs(dx) < 1.
and abs(dy) < 1.)
556 log.trace(
'isPSF2 -- checking derivatives: dx,dy = %g, %g -> %s', dx, dy, str(ispsf2))
558 pkres.psfFitBigDecenter =
True
563 psfimg2 = psf.computeImage(cx + dx, cy + dy)
565 pbb2 = psfimg2.getBBox()
570 if not pbb2.contains(
geom.Point2I(int(cx + dx), int(cy + dy))):
574 px0, py0 = psfimg2.getX0(), psfimg2.getY0()
575 psfarr = psfimg2.getArray()[pbb2.getMinY()-py0:1+pbb2.getMaxY()-py0,
576 pbb2.getMinX()-px0:1+pbb2.getMaxX()-px0]
577 px0, py0 = pbb2.getMinX(), pbb2.getMinY()
578 px1, py1 = pbb2.getMaxX(), pbb2.getMaxY()
583 sx1, sx2, sx3, sx4 = _overlap(xlo, xhi, px0, px1)
584 sy1, sy2, sy3, sy4 = _overlap(ylo, yhi, py0, py1)
585 dpx0, dpy0 = px0 - xlo, py0 - ylo
586 psfsub = psfarr[sy3-dpy0:sy4-dpy0, sx3-dpx0:sx4-dpx0]
587 vsub = valid[sy1-ylo:sy2-ylo, sx1-xlo:sx2-xlo]
588 xx, yy = np.arange(xlo, xhi+1), np.arange(ylo, yhi+1)
589 inpsf = np.outer((yy >= py0)*(yy <= py1), (xx >= px0)*(xx <= px1))
590 Ab[inpsf[valid], I_psf] = psfsub[vsub]
592 Aw = Ab*w[:, np.newaxis]
594 Xb, rb, rankb, sb = np.linalg.lstsq(Aw, bw, rcond=-1)
599 dofb = sumr - len(Xb)
601 ispsf2 = (qb < psfChisqCut2b)
604 log.trace(
'shifted PSF: new chisq/dof = %g; good? %s', qb, ispsf2)
605 pkres.psfFit3 = (chisqb, dofb)
608 if (((ispsf1
and ispsf2)
and (q2 < q1))
609 or (ispsf2
and not ispsf1)):
613 log.debug(
'dof %g', dof)
614 log.trace(
'Keeping shifted-PSF model')
617 pkres.psfFitWithDecenter =
True
623 log.debug(
'dof %g', dof)
624 log.trace(
'Keeping unshifted PSF model')
626 ispsf = (ispsf1
or ispsf2)
630 SW, SH = 1+xhi-xlo, 1+yhi-ylo
631 psfmod = afwImage.ImageF(SW, SH)
632 psfmod.setXY0(xlo, ylo)
633 psfderivmodm = afwImage.MaskedImageF(SW, SH)
634 psfderivmod = psfderivmodm.getImage()
635 psfderivmod.setXY0(xlo, ylo)
636 model = afwImage.ImageF(SW, SH)
637 model.setXY0(xlo, ylo)
638 for i
in range(len(Xpsf)):
639 for (x, y), v
in zip(ipixes, A[:, i]*Xpsf[i]):
640 ix, iy = int(x), int(y)
641 model.set(ix, iy, model.get(ix, iy) + float(v))
642 if i
in [I_psf, I_dx, I_dy]:
643 psfderivmod.set(ix, iy, psfderivmod.get(ix, iy) + float(v))
646 psfmod.set(int(x), int(y), float(A[ii, I_psf]*Xpsf[I_psf]))
647 modelfp = afwDet.Footprint(fp.getPeaks().getSchema())
648 for (x, y)
in ipixes:
649 modelfp.addSpan(int(y+ylo), int(x+xlo), int(x+xlo))
652 pkres.psfFitDebugPsf0Img = psfimg
653 pkres.psfFitDebugPsfImg = psfmod
654 pkres.psfFitDebugPsfDerivImg = psfderivmod
655 pkres.psfFitDebugPsfModel = model
656 pkres.psfFitDebugStamp = img.Factory(img, stampbb,
True)
657 pkres.psfFitDebugValidPix = valid
658 pkres.psfFitDebugVar = varimg.Factory(varimg, stampbb,
True)
659 ww = np.zeros(valid.shape, np.float64)
661 pkres.psfFitDebugWeight = ww
662 pkres.psfFitDebugRampWeight = rw
667 pkres.psfFitStampExtent = (xlo, xhi, ylo, yhi)
668 pkres.psfFitCenter = (cx, cy)
669 log.debug(
'saving chisq,dof %g %g', chisq, dof)
670 pkres.psfFitBest = (chisq, dof)
671 pkres.psfFitParams = Xpsf
672 pkres.psfFitFlux = Xpsf[I_psf]
673 pkres.psfFitNOthers = len(otherpeaks)
676 pkres.setDeblendedAsPsf()
680 log.trace(
'Deblending as PSF; setting template to PSF model')
683 psfimg = psf.computeImage(cx, cy)
685 psfimg *= Xpsf[I_psf]
686 psfimg = psfimg.convertF()
689 fpcopy = afwDet.Footprint(fp)
690 psfbb = psfimg.getBBox()
692 bb = fpcopy.getBBox()
695 psfmod = afwImage.ImageF(bb)
696 fpcopy.spans.copyImage(psfimg, psfmod)
699 pkres.setTemplate(psfmod, fpcopy)
702 pkres.setPsfTemplate(psfmod, fpcopy)
708 """Build a symmetric template for each peak in each filter
710 Given ``maskedImageF``, ``footprint``, and a ``DebldendedPeak``, creates
711 a symmetric template (``templateImage``
and ``templateFootprint``) around
712 the peak
for all peaks
not flagged
as ``skip``
or ``deblendedAsPsf``.
717 Container
for the final deblender results.
719 LSST logger
for logging purposes.
720 patchEdges: `bool`, optional
721 If
True and if the parent Footprint touches pixels
with the
722 ``EDGE`` bit set, then grow the parent Footprint to include
723 all symmetric templates.
728 If any peaks are
not skipped
or marked
as point sources,
729 ``modified``
is ``
True. Otherwise ``modified``
is ``
False``.
733 for fidx
in debResult.filters:
734 dp = debResult.deblendedParents[fidx]
735 imbb = dp.img.getBBox()
736 log.trace(
'Creating templates for footprint at x0,y0,W,H = %i, %i, %i, %i)', dp.x0, dp.y0, dp.W, dp.H)
738 for peaki, pkres
in enumerate(dp.peaks):
739 log.trace(
'Deblending peak %i of %i', peaki, len(dp.peaks))
742 if pkres.skip
or pkres.deblendedAsPsf:
746 cx, cy = pk.getIx(), pk.getIy()
748 log.trace(
'Peak center is not inside image; skipping %i', pkres.pki)
749 pkres.setOutOfBounds()
751 log.trace(
'computing template for peak %i at (%i, %i)', pkres.pki, cx, cy)
752 timg, tfoot, patched = bUtils.buildSymmetricTemplate(dp.maskedImage, dp.fp, pk, dp.avgNoise,
755 log.trace(
'Peak %i at (%i, %i): failed to build symmetric template', pkres.pki, cx, cy)
756 pkres.setFailedSymmetricTemplate()
764 pkres.setOrigTemplate(timg, tfoot)
765 pkres.setTemplate(timg, tfoot)
770 r"""Adjust flux on the edges of the template footprints.
773 of ``footprint``
is grown by the ``psffwhm*1.5``
and filled
in
774 with ramped pixels. The result
is a new symmetric footprint
775 template
for the peaks near the edge.
780 Container
for the final deblender results.
782 LSST logger
for logging purposes.
783 patchEdges: `bool`, optional
784 If
True and if the parent Footprint touches pixels
with the
785 ``EDGE`` bit set, then grow the parent Footprint to include
786 all symmetric templates.
791 If any peaks have their templates modified to include flux at the
792 edges, ``modified``
is ``
True``.
796 for fidx
in debResult.filters:
797 dp = debResult.deblendedParents[fidx]
798 log.trace(
'Checking for significant flux at edge: sigma1=%g', dp.avgNoise)
800 for peaki, pkres
in enumerate(dp.peaks):
801 if pkres.skip
or pkres.deblendedAsPsf:
803 timg, tfoot = pkres.templateImage, pkres.templateFootprint
804 if bUtils.hasSignificantFluxAtEdge(timg, tfoot, 3*dp.avgNoise):
805 log.trace(
"Template %i has significant flux at edge: ramping", pkres.pki)
808 dp.maskedImage, dp.x0, dp.x1,
809 dp.y0, dp.y1, dp.psf, pkres.peak,
810 dp.avgNoise, patchEdges)
813 and "CoaddPsf" in str(exc)):
814 pkres.setOutOfBounds()
817 pkres.setRampedTemplate(timg2, tfoot2)
820 pkres.setTemplate(timg2, tfoot2)
826 x0, x1, y0, y1, psf, pk, sigma1, patchEdges):
827 """Extend a template by the PSF to fill in the footprint.
829 Using the PSF, a footprint that touches the edge is passed to the
830 function
and is grown by the psffwhm*1.5
and filled
in with
836 LSST logger
for logging purposes.
839 t1: `afw.image.ImageF`
840 The image template that contains the footprint to extend.
842 Symmetric Footprint to extend.
844 Parent Footprint that
is being deblended.
845 maskedImage: `afw.image.MaskedImageF`
846 Full MaskedImage containing the parent footprint ``fp``.
848 Minimum x,y
for the bounding box of the footprint ``fp``.
850 Maximum x,y
for the bounding box of the footprint ``fp``.
854 The peak within the Footprint whose footprint
is being extended.
856 Estimated noise level
in the image.
858 If ``patchEdges==
True``
and if the footprint touches pixels
with the
859 ``EDGE`` bit set, then
for spans whose symmetric mirror are outside
860 the image, the symmetric footprint
is grown to include them
and their
861 pixel values are stored.
865 t2: `afw.image.ImageF`
866 Image of the extended footprint.
870 If the footprint touches an edge pixel, ``patched`` will be set to
871 ``
True``. Otherwise ``patched``
is ``
False``.
873 log.trace('Found significant flux at template edge.')
883 S = int((S + 0.5)/2)*2 + 1
885 tbb = tfoot.getBBox()
890 fpcopy = afwDet.Footprint(fp)
892 fpcopy.setSpans(fpcopy.spans.clippedTo(tbb))
893 fpcopy.removeOrphanPeaks()
894 padim = maskedImage.Factory(tbb)
895 fpcopy.spans.clippedTo(maskedImage.getBBox()).copyMaskedImage(maskedImage, padim)
898 edgepix = bUtils.getSignificantEdgePixels(t1, tfoot, -1e6)
901 xc = int((x0 + x1)/2)
902 yc = int((y0 + y1)/2)
904 pbb = psfim.getBBox()
906 lx, ly = pbb.getMinX(), pbb.getMinY()
907 psfim.setXY0(lx - xc, ly - yc)
908 pbb = psfim.getBBox()
911 if not Sbox.contains(pbb):
913 psfim = psfim.Factory(psfim, Sbox, afwImage.PARENT,
True)
914 pbb = psfim.getBBox()
921 ramped = t1.Factory(tbb)
922 Tout = ramped.getArray()
924 tx0, ty0 = t1.getX0(), t1.getY0()
925 ox0, oy0 = ramped.getX0(), ramped.getY0()
929 for span
in edgepix.getSpans():
931 for x
in range(span.getX0(), span.getX1()+1):
932 slc = (slice(y+py0 - oy0, y+py1+1 - oy0),
933 slice(x+px0 - ox0, x+px1+1 - ox0))
934 Tout[slc] = np.maximum(Tout[slc], Tin[y-ty0, x-tx0]*P)
938 imZeros = (padim.getImage().getArray() == 0)
939 padim.getImage().getArray()[imZeros] = ramped.getArray()[imZeros]
941 t2, tfoot2, patched = bUtils.buildSymmetricTemplate(padim, fpcopy, pk, sigma1,
True, patchEdges)
946 imbb = maskedImage.getBBox()
948 tbb = tfoot2.getBBox()
950 t2 = t2.Factory(t2, tbb, afwImage.PARENT,
True)
952 return t2, tfoot2, patched
956 """Applying median smoothing filter to the template images for every
957 peak in every filter.
962 Container
for the final deblender results.
964 LSST logger
for logging purposes.
965 medianFilterHalfSize: `int`, optional
966 Half the box size of the median filter, i.e. a
967 ``medianFilterHalfSize`` of 50 means that each output pixel will
968 be the median of the pixels
in a 101 x 101-pixel box
in the input
969 image. This parameter
is only used when
970 ``medianSmoothTemplate==
True``, otherwise it
is ignored.
975 Whether
or not any templates were modified.
976 This will be ``
True``
as long
as there
is at least one source that
977 is not flagged
as a PSF.
981 for fidx
in debResult.filters:
982 dp = debResult.deblendedParents[fidx]
983 for peaki, pkres
in enumerate(dp.peaks):
984 if pkres.skip
or pkres.deblendedAsPsf:
987 timg, tfoot = pkres.templateImage, pkres.templateFootprint
988 filtsize = medianFilterHalfsize*2 + 1
989 if timg.getWidth() >= filtsize
and timg.getHeight() >= filtsize:
990 log.trace(
'Median filtering template %i', pkres.pki)
993 inimg = timg.Factory(timg,
True)
994 bUtils.medianFilter(inimg, timg, medianFilterHalfsize)
996 pkres.setMedianFilteredTemplate(timg, tfoot)
998 log.trace(
'Not median-filtering template %i: size %i x %i smaller than required %i x %i',
999 pkres.pki, timg.getWidth(), timg.getHeight(), filtsize, filtsize)
1000 pkres.setTemplate(timg, tfoot)
1005 """Make the templates monotonic.
1007 The pixels in the templates are modified such that pixels further
1008 from the peak will have values smaller than those closer to the peak.
1013 Container
for the final deblender results.
1015 LSST logger
for logging purposes.
1020 Whether
or not any templates were modified.
1021 This will be ``
True``
as long
as there
is at least one source that
1022 is not flagged
as a PSF.
1026 for fidx
in debResult.filters:
1027 dp = debResult.deblendedParents[fidx]
1028 for peaki, pkres
in enumerate(dp.peaks):
1029 if pkres.skip
or pkres.deblendedAsPsf:
1032 timg, tfoot = pkres.templateImage, pkres.templateFootprint
1034 log.trace(
'Making template %i monotonic', pkres.pki)
1035 bUtils.makeMonotonic(timg, pk)
1036 pkres.setTemplate(timg, tfoot)
1041 r"""Clip non-zero spans in the template footprints for every peak in each filter.
1043 Peak ``Footprint``\ s are clipped to the region in the image containing
1044 non-zero values by dropping spans that are completely zero
and moving
1045 endpoints to non-zero pixels (but does
not split spans that have
1051 Container
for the final deblender results.
1053 LSST logger
for logging purposes.
1058 Whether
or not any templates were modified.
1059 This will be ``
True``
as long
as there
is at least one source that
1060 is not flagged
as a PSF.
1063 for fidx
in debResult.filters:
1064 dp = debResult.deblendedParents[fidx]
1065 for peaki, pkres
in enumerate(dp.peaks):
1066 if pkres.skip
or pkres.deblendedAsPsf:
1068 timg, tfoot = pkres.templateImage, pkres.templateFootprint
1070 if not tfoot.getBBox().isEmpty()
and tfoot.getBBox() != timg.getBBox(afwImage.PARENT):
1071 timg = timg.Factory(timg, tfoot.getBBox(), afwImage.PARENT,
True)
1072 pkres.setTemplate(timg, tfoot)
1077 """Weight the templates to best fit the observed image in each filter
1079 This function re-weights the templates so that their linear combination
1080 best represents the observed image in that filter.
1081 In the future it may be useful to simultaneously weight all of the
1087 Container
for the final deblender results.
1089 LSST logger
for logging purposes.
1094 ``weightTemplates`` does
not actually modify the ``Footprint``
1095 templates other than to add a weight to them, so ``modified``
1096 is always ``
False``.
1099 log.trace(
'Weighting templates')
1100 for fidx
in debResult.filters:
1106 """Weight the templates to best match the parent Footprint in a single
1109 This includes weighting both regular templates and point source templates
1113 dp: `DeblendedParent`
1114 The deblended parent to re-weight
1120 nchild = np.sum([pkres.skip is False for pkres
in dp.peaks])
1121 A = np.zeros((dp.W*dp.H, nchild))
1122 parentImage = afwImage.ImageF(dp.bb)
1123 afwDet.copyWithinFootprintImage(dp.fp, dp.img, parentImage)
1124 b = parentImage.getArray().ravel()
1127 for pkres
in dp.peaks:
1130 childImage = afwImage.ImageF(dp.bb)
1131 afwDet.copyWithinFootprintImage(dp.fp, pkres.templateImage, childImage)
1132 A[:, index] = childImage.getArray().ravel()
1135 X1, r1, rank1, s1 = np.linalg.lstsq(A, b, rcond=-1)
1140 for pkres
in dp.peaks:
1143 pkres.templateImage *= X1[index]
1144 pkres.setTemplateWeight(X1[index])
1149 """Remove "degenerate templates"
1151 If galaxies have substructure, such as face-on spirals, the process of
1152 identifying peaks can
"shred" the galaxy into many pieces. The templates
1153 of shredded galaxies are typically quite similar because they represent
1154 the same galaxy, so we
try to identify these
"degenerate" peaks
1155 by looking at the inner product (
in pixel space) of pairs of templates.
1156 If they are nearly parallel, we only keep one of the peaks
and reject
1157 the other. If only one of the peaks
is a PSF template, the other template
1158 is used, otherwise the one
with the maximum template value
is kept.
1163 Container
for the final deblender results.
1165 LSST logger
for logging purposes.
1166 maxTempDotProd: `float`, optional
1167 All dot products between templates greater than ``maxTempDotProd``
1168 will result
in one of the templates removed.
1173 If any degenerate templates are found, ``modified``
is ``
True``.
1175 log.trace('Looking for degnerate templates')
1178 for fidx
in debResult.filters:
1179 dp = debResult.deblendedParents[fidx]
1180 nchild = np.sum([pkres.skip
is False for pkres
in dp.peaks])
1181 indexes = [pkres.pki
for pkres
in dp.peaks
if pkres.skip
is False]
1186 A = np.zeros((nchild, nchild))
1189 for pkres
in dp.peaks:
1192 heavies.append(afwDet.makeHeavyFootprint(pkres.templateFootprint,
1193 afwImage.MaskedImageF(pkres.templateImage)))
1194 maxTemplate.append(np.max(pkres.templateImage.getArray()))
1196 for i
in range(nchild):
1197 for j
in range(i + 1):
1198 A[i, j] = heavies[i].dot(heavies[j])
1201 for i
in range(nchild):
1203 norm = A[i, i]*A[j, j]
1207 A[i, j] /= np.sqrt(norm)
1212 for i
in range(nchild):
1215 if A[i, j] > currentMax:
1216 currentMax = A[i, j]
1217 if currentMax > maxTempDotProd:
1230 reject = indexes[rejectedIndex]
1231 if dp.peaks[keep].deblendedAsPsf
and dp.peaks[reject].deblendedAsPsf
is False:
1232 keep = indexes[rejectedIndex]
1234 elif dp.peaks[keep].deblendedAsPsf
is False and dp.peaks[reject].deblendedAsPsf:
1235 reject = indexes[rejectedIndex]
1238 if maxTemplate[rejectedIndex] > maxTemplate[i]:
1239 keep = indexes[rejectedIndex]
1241 log.trace(
'Removing object with index %d : %f. Degenerate with %d',
1242 reject, currentMax, keep)
1243 dp.peaks[reject].skip =
True
1244 dp.peaks[reject].degenerate =
True
1249def apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak',
1250 strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001,
1251 getTemplateSum=False):
1252 """Apportion flux to all of the peak templates in each filter
1254 Divide the ``maskedImage`` flux amongst all of the templates based
1255 on the fraction of flux assigned to each ``template``.
1256 Leftover "stray flux" is assigned to peaks based on the other parameters.
1261 Container
for the final deblender results.
1263 LSST logger
for logging purposes.
1264 assignStrayFlux: `bool`, optional
1265 If
True then flux
in the parent footprint that
is not covered by any
1266 of the template footprints
is assigned to templates based on
1267 their 1/(1+r^2) distance.
1268 How the flux
is apportioned
is determined by ``strayFluxAssignment``.
1269 strayFluxAssignment: `string`, optional
1270 Determines how stray flux
is apportioned.
1272 * ``trim``: Trim stray flux
and do
not include
in any footprints
1273 * ``r-to-peak`` (default): Stray flux
is assigned based on
1274 (1/(1+r^2)
from the peaks
1275 * ``r-to-footprint``: Stray flux
is distributed to the footprints
1276 based on 1/(1+r^2) of the minimum distance
from the stray flux
1278 * ``nearest-footprint``: Stray flux
is assigned to the footprint
1279 with lowest L-1 (Manhattan) distance to the stray flux
1281 strayFluxToPointSources: `string`, optional
1282 Determines how stray flux
is apportioned to point sources
1284 * ``never``: never apportion stray flux to point sources
1285 * ``necessary`` (default): point sources are included only
if there
1286 are no extended sources nearby
1287 * ``always``: point sources are always included
in
1288 the 1/(1+r^2) splitting
1290 clipStrayFluxFraction: `float`, optional
1291 Minimum stray-flux portion.
1292 Any stray-flux portion less than ``clipStrayFluxFraction``
is
1294 getTemplateSum: `bool`, optional
1295 As part of the flux calculation, the sum of the templates
is
1296 calculated. If ``getTemplateSum==
True`` then the sum of the
1297 templates
is stored
in the result (a `DeblendedFootprint`).
1302 Apportion flux always modifies the templates, so ``modified``
is
1303 always ``
True``. However, this should likely be the final step
and
1304 it
is unlikely that any deblender plugins will be re-run.
1306 validStrayPtSrc = ['never',
'necessary',
'always']
1307 validStrayAssign = [
'r-to-peak',
'r-to-footprint',
'nearest-footprint',
'trim']
1308 if strayFluxToPointSources
not in validStrayPtSrc:
1309 raise ValueError(((
'strayFluxToPointSources: value \"%s\" not in the set of allowed values: ') %
1310 strayFluxToPointSources) + str(validStrayPtSrc))
1311 if strayFluxAssignment
not in validStrayAssign:
1312 raise ValueError(((
'strayFluxAssignment: value \"%s\" not in the set of allowed values: ') %
1313 strayFluxAssignment) + str(validStrayAssign))
1315 for fidx
in debResult.filters:
1316 dp = debResult.deblendedParents[fidx]
1329 bb = dp.fp.getBBox()
1331 for peaki, pkres
in enumerate(dp.peaks):
1334 tmimgs.append(pkres.templateImage)
1335 tfoots.append(pkres.templateFootprint)
1337 dpsf.append(pkres.deblendedAsPsf)
1339 pkx.append(pk.getIx())
1340 pky.append(pk.getIy())
1341 ibi.append(pkres.pki)
1344 log.trace(
'Apportioning flux among %i templates', len(tmimgs))
1345 sumimg = afwImage.ImageF(bb)
1350 if strayFluxAssignment ==
'trim':
1351 assignStrayFlux =
False
1352 strayopts |= bUtils.STRAYFLUX_TRIM
1354 strayopts |= bUtils.ASSIGN_STRAYFLUX
1355 if strayFluxToPointSources ==
'necessary':
1356 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_WHEN_NECESSARY
1357 elif strayFluxToPointSources ==
'always':
1358 strayopts |= bUtils.STRAYFLUX_TO_POINT_SOURCES_ALWAYS
1360 if strayFluxAssignment ==
'r-to-peak':
1363 elif strayFluxAssignment ==
'r-to-footprint':
1364 strayopts |= bUtils.STRAYFLUX_R_TO_FOOTPRINT
1365 elif strayFluxAssignment ==
'nearest-footprint':
1366 strayopts |= bUtils.STRAYFLUX_NEAREST_FOOTPRINT
1368 portions, strayflux = bUtils.apportionFlux(dp.maskedImage, dp.fp, tmimgs, tfoots, sumimg, dpsf,
1369 pkx, pky, strayopts, clipStrayFluxFraction)
1372 if strayFluxAssignment ==
'trim':
1375 finalSpanSet = finalSpanSet.union(foot.spans)
1376 dp.fp.setSpans(finalSpanSet)
1380 debResult.setTemplateSums(sumimg, fidx)
1384 for j, (pk, pkres)
in enumerate(zip(dp.fp.getPeaks(), dp.peaks)):
1387 pkres.setFluxPortion(portions[ii])
1392 stray = strayflux[ii]
1397 pkres.setStrayFlux(stray)
1400 for j, (pk, pkres)
in enumerate(zip(dp.fp.getPeaks(), dp.peaks)):
1404 for foot, add
in [(pkres.templateFootprint,
True), (pkres.origFootprint,
True),
1405 (pkres.strayFlux,
False)]:
1408 pks = foot.getPeaks()
__init__(self, func, onReset=None, maxIterations=50, **kwargs)
run(self, debResult, log)
_handle_flux_at_edge(log, psffwhm, t1, tfoot, fp, maskedImage, x0, x1, y0, y1, psf, pk, sigma1, patchEdges)
apportionFlux(debResult, log, assignStrayFlux=True, strayFluxAssignment='r-to-peak', strayFluxToPointSources='necessary', clipStrayFluxFraction=0.001, getTemplateSum=False)
_setPeakError(debResult, log, pk, cx, cy, filters, msg, flag)
_fitPsf(fp, fmask, pk, pkF, pkres, fbb, peaks, peaksF, log, psf, psffwhm, img, varimg, psfChisqCut1, psfChisqCut2, psfChisqCut2b, tinyFootprintSize=2)
makeTemplatesMonotonic(debResult, log)
fitPsfs(debResult, log, psfChisqCut1=1.5, psfChisqCut2=1.5, psfChisqCut2b=1.5, tinyFootprintSize=2)
weightTemplates(debResult, log)
rampFluxAtEdge(debResult, log, patchEdges=False)
clipFootprintToNonzeroImpl(foot, image)
clipFootprintsToNonzero(debResult, log)
buildSymmetricTemplates(debResult, log, patchEdges=False, setOrigTemplate=True)
medianSmoothTemplates(debResult, log, medianFilterHalfsize=2)
reconstructTemplates(debResult, log, maxTempDotProd=0.5)