30 import lsst.pex.config
as pexConfig
33 from .imageMapReduce
import (ImageMapReduceConfig, ImageMapper,
35 from .imagePsfMatch
import (ImagePsfMatchTask, ImagePsfMatchConfig,
36 subtractAlgorithmRegistry)
38 __all__ = [
"ZogyTask",
"ZogyConfig",
39 "ZogyMapper",
"ZogyMapReduceConfig",
40 "ZogyImagePsfMatchConfig",
"ZogyImagePsfMatchTask"]
43 """Tasks for performing the "Proper image subtraction" algorithm of 44 Zackay, et al. (2016), hereafter simply referred to as 'ZOGY (2016)'. 46 `ZogyTask` contains methods to perform the basic estimation of the 47 ZOGY diffim `D`, its updated PSF, and the variance-normalized 48 likelihood image `S_corr`. We have implemented ZOGY using the 49 proscribed methodology, computing all convolutions in Fourier space, 50 and also variants in which the convolutions are performed in real 51 (image) space. The former is faster and results in fewer artifacts 52 when the PSFs are noisy (i.e., measured, for example, via 53 `PsfEx`). The latter is presumed to be preferred as it can account for 54 masks correctly with fewer "ringing" artifacts from edge effects or 55 saturated stars, but noisy PSFs result in their own smaller 56 artifacts. Removal of these artifacts is a subject of continuing 57 research. Currently, we "pad" the PSFs when performing the 58 subtractions in real space, which reduces, but does not entirely 59 eliminate these artifacts. 61 All methods in `ZogyTask` assume template and science images are 62 already accurately photometrically and astrometrically registered. 64 `ZogyMapper` is a wrapper which runs `ZogyTask` in the 65 `ImageMapReduce` framework, computing of ZOGY diffim's on small, 66 overlapping sub-images, thereby enabling complete ZOGY diffim's which 67 account for spatially-varying noise and PSFs across the two input 68 exposures. An example of the use of this task is in the `testZogy.py` 74 """Configuration parameters for the ZogyTask 76 inImageSpace = pexConfig.Field(
79 doc=
"Perform all convolutions in real (image) space rather than Fourier space. " 80 "Currently if True, this results in artifacts when using real (noisy) PSFs." 83 padSize = pexConfig.Field(
86 doc=
"Number of pixels to pad PSFs to avoid artifacts (when inImageSpace is True)" 89 templateFluxScaling = pexConfig.Field(
92 doc=
"Template flux scaling factor (Fr in ZOGY paper)" 95 scienceFluxScaling = pexConfig.Field(
98 doc=
"Science flux scaling factor (Fn in ZOGY paper)" 101 doTrimKernels = pexConfig.Field(
104 doc=
"Trim kernels for image-space ZOGY. Speeds up convolutions and shrinks artifacts. " 105 "Subject of future research." 108 doFilterPsfs = pexConfig.Field(
111 doc=
"Filter PSFs for image-space ZOGY. Aids in reducing artifacts. " 112 "Subject of future research." 115 ignoreMaskPlanes = pexConfig.ListField(
117 default=(
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE"),
118 doc=
"Mask planes to ignore for statistics" 126 """Task to perform ZOGY proper image subtraction. See module-level documentation for 129 In all methods, im1 is R (reference, or template) and im2 is N (new, or science). 131 ConfigClass = ZogyConfig
132 _DefaultName =
"ip_diffim_Zogy" 134 def __init__(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None,
135 psf1=None, psf2=None, *args, **kwargs):
136 """Create the ZOGY task. 140 templateExposure : `lsst.afw.image.Exposure` 141 Template exposure ("Reference image" in ZOGY (2016)). 142 scienceExposure : `lsst.afw.image.Exposure` 143 Science exposure ("New image" in ZOGY (2016)). Must have already been 144 registered and photmetrically matched to template. 146 (Optional) sqrt(variance) of `templateExposure`. If `None`, it is 147 computed from the sqrt(mean) of the `templateExposure` variance image. 149 (Optional) sqrt(variance) of `scienceExposure`. If `None`, it is 150 computed from the sqrt(mean) of the `scienceExposure` variance image. 151 psf1 : 2D `numpy.array` 152 (Optional) 2D array containing the PSF image for the template. If 153 `None`, it is extracted from the PSF taken at the center of `templateExposure`. 154 psf2 : 2D `numpy.array` 155 (Optional) 2D array containing the PSF image for the science img. If 156 `None`, it is extracted from the PSF taken at the center of `scienceExposure`. 158 additional arguments to be passed to 159 `lsst.pipe.base.Task` 161 additional keyword arguments to be passed to 162 `lsst.pipe.base.Task` 164 pipeBase.Task.__init__(self, *args, **kwargs)
166 self.
setup(templateExposure=templateExposure, scienceExposure=scienceExposure,
167 sig1=sig1, sig2=sig2, psf1=psf1, psf2=psf2, *args, **kwargs)
169 def setup(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None,
170 psf1=None, psf2=None, correctBackground=False, *args, **kwargs):
171 """Set up the ZOGY task. 175 templateExposure : `lsst.afw.image.Exposure` 176 Template exposure ("Reference image" in ZOGY (2016)). 177 scienceExposure : `lsst.afw.image.Exposure` 178 Science exposure ("New image" in ZOGY (2016)). Must have already been 179 registered and photmetrically matched to template. 181 (Optional) sqrt(variance) of `templateExposure`. If `None`, it is 182 computed from the sqrt(mean) of the `templateExposure` variance image. 184 (Optional) sqrt(variance) of `scienceExposure`. If `None`, it is 185 computed from the sqrt(mean) of the `scienceExposure` variance image. 186 psf1 : 2D `numpy.array` 187 (Optional) 2D array containing the PSF image for the template. If 188 `None`, it is extracted from the PSF taken at the center of `templateExposure`. 189 psf2 : 2D `numpy.array` 190 (Optional) 2D array containing the PSF image for the science img. If 191 `None`, it is extracted from the PSF taken at the center of `scienceExposure`. 192 correctBackground : `bool` 193 (Optional) subtract sigma-clipped mean of exposures. Zogy doesn't correct 194 nonzero backgrounds (unlike AL) so subtract them here. 196 additional arguments to be passed to 197 `lsst.pipe.base.Task` 199 additional keyword arguments to be passed to 200 `lsst.pipe.base.Task` 202 if self.
template is None and templateExposure
is None:
204 if self.
science is None and scienceExposure
is None:
213 self.
statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(
214 self.config.ignoreMaskPlanes))
217 self.
im2 = self.
science.getMaskedImage().getImage().getArray()
221 def selectPsf(psf, exposure):
226 xcen = (bbox1.getBeginX() + bbox1.getEndX()) / 2.
227 ycen = (bbox1.getBeginY() + bbox1.getEndY()) / 2.
228 return exposure.getPsf().computeKernelImage(
geom.Point2D(xcen, ycen)).getArray()
238 if (pShape1[0] < pShape2[0]):
239 psf1 = np.pad(psf1, ((0, pShape2[0] - pShape1[0]), (0, 0)), mode=
'constant', constant_values=0.)
240 elif (pShape2[0] < pShape1[0]):
241 psf2 = np.pad(psf2, ((0, pShape1[0] - pShape2[0]), (0, 0)), mode=
'constant', constant_values=0.)
242 if (pShape1[1] < pShape2[1]):
243 psf1 = np.pad(psf1, ((0, 0), (0, pShape2[1] - pShape1[1])), mode=
'constant', constant_values=0.)
244 elif (pShape2[1] < pShape1[1]):
245 psf2 = np.pad(psf2, ((0, 0), (0, pShape1[1] - pShape2[1])), mode=
'constant', constant_values=0.)
248 maxLoc1 = np.unravel_index(np.argmax(psf1), psf1.shape)
249 maxLoc2 = np.unravel_index(np.argmax(psf2), psf2.shape)
251 while (maxLoc1[0] != maxLoc2[0])
or (maxLoc1[1] != maxLoc2[1]):
252 if maxLoc1[0] > maxLoc2[0]:
253 psf2[1:, :] = psf2[:-1, :]
254 elif maxLoc1[0] < maxLoc2[0]:
255 psf1[1:, :] = psf1[:-1, :]
256 if maxLoc1[1] > maxLoc2[1]:
257 psf2[:, 1:] = psf2[:, :-1]
258 elif maxLoc1[1] < maxLoc2[1]:
259 psf1[:, 1:] = psf1[:, :-1]
260 maxLoc1 = np.unravel_index(np.argmax(psf1), psf1.shape)
261 maxLoc2 = np.unravel_index(np.argmax(psf2), psf2.shape)
264 psf1[psf1 < MIN_KERNEL] = MIN_KERNEL
265 psf2[psf2 < MIN_KERNEL] = MIN_KERNEL
274 if np.isnan(self.
sig1)
or self.
sig1 == 0:
276 if np.isnan(self.
sig2)
or self.
sig2 == 0:
280 if correctBackground:
281 def _subtractImageMean(exposure):
282 """Compute the sigma-clipped mean of the image of `exposure`.""" 283 mi = exposure.getMaskedImage()
286 mean = statObj.getValue(afwMath.MEANCLIP)
287 if not np.isnan(mean):
291 _subtractImageMean(self.
science)
293 self.
Fr = self.config.templateFluxScaling
294 self.
Fn = self.config.scienceFluxScaling
297 def _computeVarianceMean(self, exposure):
298 """Compute the sigma-clipped mean of the variance image of `exposure`. 301 exposure.getMaskedImage().getMask(),
303 var = statObj.getValue(afwMath.MEANCLIP)
307 def _padPsfToSize(psf, size):
308 """Zero-pad `psf` to the dimensions given by `size`. 312 psf : 2D `numpy.array` 313 Input psf to be padded 315 Two element list containing the dimensions to pad the `psf` to 319 psf : 2D `numpy.array` 320 The padded copy of the input `psf`. 322 newArr = np.zeros(size)
323 offset = [size[0]//2 - psf.shape[0]//2 - 1, size[1]//2 - psf.shape[1]//2 - 1]
324 tmp = newArr[offset[0]:(psf.shape[0] + offset[0]), offset[1]:(psf.shape[1] + offset[1])]
329 """Compute standard ZOGY quantities used by (nearly) all methods. 331 Many of the ZOGY calculations require similar quantities, including 332 FFTs of the PSFs, and the "denominator" term (e.g. in eq. 13 of 333 ZOGY manuscript (2016). This function consolidates many of those 338 psf1 : 2D `numpy.array` 339 (Optional) Input psf of template, override if already padded 340 psf2 : 2D `numpy.array` 341 (Optional) Input psf of science image, override if already padded 342 padSize : `int`, optional 343 Number of pixels to pad the image on each side with zeroes. 347 A `lsst.pipe.base.Struct` containing: 348 - Pr : 2D `numpy.array`, the (possibly zero-padded) template PSF 349 - Pn : 2D `numpy.array`, the (possibly zero-padded) science PSF 350 - Pr_hat : 2D `numpy.array`, the FFT of `Pr` 351 - Pn_hat : 2D `numpy.array`, the FFT of `Pn` 352 - denom : 2D `numpy.array`, the denominator of equation (13) in ZOGY (2016) manuscript 353 - Fd : `float`, the relative flux scaling factor between science and template 355 psf1 = self.
im1_psf if psf1
is None else psf1
356 psf2 = self.
im2_psf if psf2
is None else psf2
357 padSize = self.
padSize if padSize
is None else padSize
360 Pr = ZogyTask._padPsfToSize(psf1, (psf1.shape[0] + padSize, psf1.shape[1] + padSize))
361 Pn = ZogyTask._padPsfToSize(psf2, (psf2.shape[0] + padSize, psf2.shape[1] + padSize))
363 psf1[np.abs(psf1) <= MIN_KERNEL] = MIN_KERNEL
364 psf2[np.abs(psf2) <= MIN_KERNEL] = MIN_KERNEL
367 Pr_hat = np.fft.fft2(Pr)
368 Pr_hat2 = np.conj(Pr_hat) * Pr_hat
369 Pn_hat = np.fft.fft2(Pn)
370 Pn_hat2 = np.conj(Pn_hat) * Pn_hat
371 denom = np.sqrt((sigN**2 * self.
Fr**2 * Pr_hat2) + (sigR**2 * self.
Fn**2 * Pn_hat2))
372 Fd = self.
Fr * self.
Fn / np.sqrt(sigN**2 * self.
Fr**2 + sigR**2 * self.
Fn**2)
374 res = pipeBase.Struct(
375 Pr=Pr, Pn=Pn, Pr_hat=Pr_hat, Pn_hat=Pn_hat, denom=denom, Fd=Fd
380 r"""Compute ZOGY diffim `D` as proscribed in ZOGY (2016) manuscript 384 debug : `bool`, optional 385 If set to True, filter the kernels by setting the edges to zero. 386 returnMatchedTemplate : `bool`, optional 387 Calculate the template image. 388 If not set, the returned template will be None. 392 In all functions, im1 is R (reference, or template) and im2 is N (new, or science) 393 Compute the ZOGY eqn. (13): 397 \widehat{D} = \frac{Fr\widehat{Pr}\widehat{N} - 398 F_n\widehat{Pn}\widehat{R}}{\sqrt{\sigma_n^2 Fr^2 399 \|\widehat{Pr}\|^2 + \sigma_r^2 F_n^2 \|\widehat{Pn}\|^2}} 401 where :math:`D` is the optimal difference image, :math:`R` and :math:`N` are the 402 reference and "new" image, respectively, :math:`Pr` and :math:`P_n` are their 403 PSFs, :math:`Fr` and :math:`Fn` are their flux-based zero-points (which we 404 will set to one here), :math:`\sigma_r^2` and :math:`\sigma_n^2` are their 405 variance, and :math:`\widehat{D}` denotes the FT of :math:`D`. 409 result : `lsst.pipe.base.Struct` 410 Result struct with components: 412 - ``D`` : 2D `numpy.array`, the proper image difference 413 - ``D_var`` : 2D `numpy.array`, the variance image for `D` 416 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
417 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
421 def _filterKernel(K, trim_amount):
424 K[:ps, :] = K[-ps:, :] = 0
425 K[:, :ps] = K[:, -ps:] = 0
428 Kr_hat = self.
Fr * preqs.Pr_hat / preqs.denom
429 Kn_hat = self.
Fn * preqs.Pn_hat / preqs.denom
430 if debug
and self.config.doTrimKernels:
433 ps = (Kn_hat.shape[1] - 80)//2
434 Kn = _filterKernel(np.fft.ifft2(Kn_hat), ps)
435 Kn_hat = np.fft.fft2(Kn)
436 Kr = _filterKernel(np.fft.ifft2(Kr_hat), ps)
437 Kr_hat = np.fft.fft2(Kr)
439 def processImages(im1, im2, doAdd=False):
441 im1[np.isinf(im1)] = np.nan
442 im1[np.isnan(im1)] = np.nanmean(im1)
443 im2[np.isinf(im2)] = np.nan
444 im2[np.isnan(im2)] = np.nanmean(im2)
446 R_hat = np.fft.fft2(im1)
447 N_hat = np.fft.fft2(im2)
449 D_hat = Kr_hat * N_hat
450 D_hat_R = Kn_hat * R_hat
456 D = np.fft.ifft2(D_hat)
457 D = np.fft.ifftshift(D.real) / preqs.Fd
460 if returnMatchedTemplate:
461 R = np.fft.ifft2(D_hat_R)
462 R = np.fft.ifftshift(R.real) / preqs.Fd
467 D, R = processImages(self.
im1, self.
im2, doAdd=
False)
469 D_var, R_var = processImages(self.
im1_var, self.
im2_var, doAdd=
True)
471 return pipeBase.Struct(D=D, D_var=D_var, R=R, R_var=R_var)
473 def _doConvolve(self, exposure, kernel, recenterKernel=False):
474 """Convolve an Exposure with a decorrelation convolution kernel. 478 exposure : `lsst.afw.image.Exposure` 479 Input exposure to be convolved. 480 kernel : `numpy.array` 481 2D `numpy.array` to convolve the image with 482 recenterKernel : `bool`, optional 483 Force the kernel center to the pixel with the maximum value. 487 A new `lsst.afw.image.Exposure` with the convolved pixels and the (possibly 492 - We optionally re-center the kernel if necessary and return the possibly 495 kernelImg = afwImage.ImageD(kernel.shape[1], kernel.shape[0])
496 kernelImg.getArray()[:, :] = kernel
499 maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
500 kern.setCtrX(maxloc[0])
501 kern.setCtrY(maxloc[1])
502 outExp = exposure.clone()
504 maxInterpolationDistance=0)
506 afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl)
507 except AttributeError:
515 """Compute ZOGY diffim `D` using image-space convlutions 517 This method is still being debugged as it results in artifacts 518 when the PSFs are noisy (see module-level docstring). Thus 519 there are several options still enabled by the `debug` flag, 520 which are disabled by defult. 525 The amount to pad the PSFs by 527 Flag to enable debugging tests and options 531 D : `lsst.afw.Exposure` 532 the proper image difference, including correct variance, 540 Kr_hat = (preqs.Pr_hat + delta) / (preqs.denom + delta)
541 Kn_hat = (preqs.Pn_hat + delta) / (preqs.denom + delta)
542 Kr = np.fft.ifft2(Kr_hat).real
543 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
544 Kn = np.fft.ifft2(Kn_hat).real
545 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
547 def _trimKernel(self, K, trim_amount):
551 K = K[ps:-ps, ps:-ps]
554 padSize = self.
padSize if padSize
is None else padSize
556 if debug
and self.config.doTrimKernels:
559 Kn = _trimKernel(Kn, padSize)
560 Kr = _trimKernel(Kr, padSize)
566 tmp = D.getMaskedImage()
567 tmp -= exp1.getMaskedImage()
569 return pipeBase.Struct(D=D, R=exp1)
571 def _setNewPsf(self, exposure, psfArr):
572 """Utility method to set an exposure's PSF when provided as a 2-d numpy.array 574 bbox = exposure.getBBox()
575 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
577 psfI = afwImage.ImageD(psfArr.shape[1], psfArr.shape[0])
578 psfI.getArray()[:, :] = psfArr
580 psfNew = measAlg.KernelPsf(psfK, center)
581 exposure.setPsf(psfNew)
585 returnMatchedTemplate=False, **kwargs):
586 """Wrapper method to compute ZOGY proper diffim 588 This method should be used as the public interface for 589 computing the ZOGY diffim. 593 inImageSpace : `bool` 594 Override config `inImageSpace` parameter 596 Override config `padSize` parameter 597 returnMatchedTemplate : `bool` 598 Include the PSF-matched template in the results Struct 600 additional keyword arguments to be passed to 601 `computeDiffimFourierSpace` or `computeDiffimImageSpace`. 605 An lsst.pipe.base.Struct containing: 606 - D : `lsst.afw.Exposure` 607 the proper image difference, including correct variance, 609 - R : `lsst.afw.Exposure` 610 If `returnMatchedTemplate` is True, the PSF-matched template 614 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
616 padSize = self.
padSize if padSize
is None else padSize
619 if returnMatchedTemplate:
624 D.getMaskedImage().getImage().getArray()[:, :] = res.D
625 D.getMaskedImage().getVariance().getArray()[:, :] = res.D_var
626 if returnMatchedTemplate:
628 R.getMaskedImage().getImage().getArray()[:, :] = res.R
629 R.getMaskedImage().getVariance().getArray()[:, :] = res.R_var
633 return pipeBase.Struct(D=D, R=R)
636 """Compute the ZOGY diffim PSF (ZOGY manuscript eq. 14) 641 Override config `padSize` parameter 643 Return the FFT of the diffim PSF (do not inverse-FFT it) 644 psf1 : 2D `numpy.array` 645 (Optional) Input psf of template, override if already padded 646 psf2 : 2D `numpy.array` 647 (Optional) Input psf of science image, override if already padded 651 Pd : 2D `numpy.array` 652 The diffim PSF (or FFT of PSF if `keepFourier=True`) 654 preqs = self.
computePrereqs(psf1=psf1, psf2=psf2, padSize=padSize)
656 Pd_hat_numerator = (self.
Fr * self.
Fn * preqs.Pr_hat * preqs.Pn_hat)
657 Pd_hat = Pd_hat_numerator / (preqs.Fd * preqs.denom)
662 Pd = np.fft.ifft2(Pd_hat)
663 Pd = np.fft.ifftshift(Pd).real
667 def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False,
668 R_hat=None, Kr_hat=None, Kr=None,
669 N_hat=None, Kn_hat=None, Kn=None):
670 """Compute the astrometric noise correction terms 672 Compute the correction for estimated astrometric noise as 673 proscribed in ZOGY (2016), section 3.3. All convolutions 674 performed either in real (image) or Fourier space. 678 xVarAst, yVarAst : `float` 679 estimated astrometric noise (variance of astrometric registration errors) 680 inImageSpace : `bool` 681 Perform all convolutions in real (image) space rather than Fourier space 682 R_hat : 2-D `numpy.array` 683 (Optional) FFT of template image, only required if `inImageSpace=False` 684 Kr_hat : 2-D `numpy.array` 685 FFT of Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=False` 686 Kr : 2-D `numpy.array` 687 Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=True`. 688 Kr is associated with the template (reference). 689 N_hat : 2-D `numpy.array` 690 FFT of science image, only required if `inImageSpace=False` 691 Kn_hat : 2-D `numpy.array` 692 FFT of Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=False` 693 Kn : 2-D `numpy.array` 694 Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=True`. 695 Kn is associated with the science (new) image. 699 VastSR, VastSN : 2-D `numpy.array` 700 Arrays containing the values in eqs. 30 and 32 of ZOGY (2016). 703 if xVarAst + yVarAst > 0:
706 S_R = S_R.getMaskedImage().getImage().getArray()
708 S_R = np.fft.ifft2(R_hat * Kr_hat)
709 gradRx, gradRy = np.gradient(S_R)
710 VastSR = xVarAst * gradRx**2. + yVarAst * gradRy**2.
714 S_N = S_N.getMaskedImage().getImage().getArray()
716 S_N = np.fft.ifft2(N_hat * Kn_hat)
717 gradNx, gradNy = np.gradient(S_N)
718 VastSN = xVarAst * gradNx**2. + yVarAst * gradNy**2.
720 return VastSR, VastSN
723 """Compute corrected likelihood image, optimal for source detection 725 Compute ZOGY S_corr image. This image can be thresholded for 726 detection without optimal filtering, and the variance image is 727 corrected to account for astrometric noise (errors in 728 astrometric registration whether systematic or due to effects 729 such as DCR). The calculations here are all performed in 730 Fourier space, as proscribed in ZOGY (2016). 734 xVarAst, yVarAst : `float` 735 estimated astrometric noise (variance of astrometric registration errors) 739 result : `lsst.pipe.base.Struct` 740 Result struct with components: 742 - ``S`` : `numpy.array`, the likelihood image S (eq. 12 of ZOGY (2016)) 743 - ``S_var`` : the corrected variance image (denominator of eq. 25 of ZOGY (2016)) 744 - ``Dpsf`` : the PSF of the diffim D, likely never to be used. 748 """Replace any NaNs or Infs with the mean of the image.""" 749 isbad = ~np.isfinite(im)
752 im[isbad] = np.nanmean(im)
755 self.
im1 = fix_nans(self.
im1)
756 self.
im2 = fix_nans(self.
im2)
761 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
762 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
767 R_hat = np.fft.fft2(self.
im1)
768 N_hat = np.fft.fft2(self.
im2)
769 D_hat = self.
Fr * preqs.Pr_hat * N_hat - self.
Fn * preqs.Pn_hat * R_hat
772 Pd_hat = self.
computeDiffimPsf(padSize=0, keepFourier=
True, psf1=psf1, psf2=psf2)
773 Pd_bar = np.conj(Pd_hat)
774 S = np.fft.ifft2(D_hat * Pd_bar)
778 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
779 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
780 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
781 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
783 Kr_hat2 = np.fft.fft2(np.fft.ifft2(Kr_hat)**2.)
784 Kn_hat2 = np.fft.fft2(np.fft.ifft2(Kn_hat)**2.)
785 var1c_hat = Kr_hat2 * np.fft.fft2(self.
im1_var)
786 var2c_hat = Kn_hat2 * np.fft.fft2(self.
im2_var)
790 R_hat=R_hat, Kr_hat=Kr_hat,
791 N_hat=N_hat, Kn_hat=Kn_hat)
793 S_var = np.sqrt(np.fft.ifftshift(np.fft.ifft2(var1c_hat + var2c_hat)) + fGradR + fGradN)
796 S = np.fft.ifftshift(np.fft.ifft2(Kn_hat * N_hat - Kr_hat * R_hat))
800 return pipeBase.Struct(S=S.real, S_var=S_var.real, Dpsf=Pd)
803 """Compute corrected likelihood image, optimal for source detection 805 Compute ZOGY S_corr image. This image can be thresholded for 806 detection without optimal filtering, and the variance image is 807 corrected to account for astrometric noise (errors in 808 astrometric registration whether systematic or due to effects 809 such as DCR). The calculations here are all performed in 814 xVarAst, yVarAst : `float` 815 estimated astrometric noise (variance of astrometric registration errors) 819 A `lsst.pipe.base.Struct` containing: 820 - S : `lsst.afw.image.Exposure`, the likelihood exposure S (eq. 12 of ZOGY (2016)), 821 including corrected variance, masks, and PSF 822 - D : `lsst.afw.image.Exposure`, the proper image difference, including correct 823 variance, masks, and PSF 828 padSize = self.
padSize if padSize
is None else padSize
832 Pd_bar = np.fliplr(np.flipud(Pd))
834 tmp = S.getMaskedImage()
839 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
840 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
841 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
842 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
844 Kr = np.fft.ifft2(Kr_hat).real
845 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
846 Kn = np.fft.ifft2(Kn_hat).real
847 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
855 Smi = S.getMaskedImage()
857 S_var = np.sqrt(var1c.getArray() + var2c.getArray() + fGradR + fGradN)
858 S.getMaskedImage().getVariance().getArray()[:, :] = S_var
862 return pipeBase.Struct(S=S, D=D)
864 def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, **kwargs):
865 """Wrapper method to compute ZOGY corrected likelihood image, optimal for 868 This method should be used as the public interface for 869 computing the ZOGY S_corr. 873 xVarAst, yVarAst : `float` 874 estimated astrometric noise (variance of astrometric registration errors) 875 inImageSpace : `bool` 876 Override config `inImageSpace` parameter 878 Override config `padSize` parameter 882 S : `lsst.afw.image.Exposure` 883 The likelihood exposure S (eq. 12 of ZOGY (2016)), 884 including corrected variance, masks, and PSF 886 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
894 S.getMaskedImage().getImage().getArray()[:, :] = res.S
895 S.getMaskedImage().getVariance().getArray()[:, :] = res.S_var
898 return pipeBase.Struct(S=S)
902 """Task to be used as an ImageMapper for performing 903 ZOGY image subtraction on a grid of subimages. 905 ConfigClass = ZogyConfig
906 _DefaultName =
'ip_diffim_ZogyMapper' 909 ImageMapper.__init__(self, *args, **kwargs)
911 def run(self, subExposure, expandedSubExposure, fullBBox, template,
913 """Perform ZOGY proper image subtraction on sub-images 915 This method performs ZOGY proper image subtraction on 916 `subExposure` using local measures for image variances and 917 PSF. `subExposure` is a sub-exposure of the science image. It 918 also requires the corresponding sub-exposures of the template 919 (`template`). The operations are actually performed on 920 `expandedSubExposure` to allow for invalid edge pixels arising 921 from convolutions, which are then removed. 925 subExposure : `lsst.afw.image.Exposure` 926 the sub-exposure of the diffim 927 expandedSubExposure : `lsst.afw.image.Exposure` 928 the expanded sub-exposure upon which to operate 929 fullBBox : `lsst.geom.Box2I` 930 the bounding box of the original exposure 931 template : `lsst.afw.image.Exposure` 932 the template exposure, from which a corresponding sub-exposure 935 additional keyword arguments propagated from 936 `ImageMapReduceTask.run`. These include: 939 Compute and return the corrected likelihood image S_corr 940 rather than the proper image difference 941 ``inImageSpace`` : `bool` 942 Perform all convolutions in real (image) space rather than 943 in Fourier space. This option currently leads to artifacts 944 when using real (measured and noisy) PSFs, thus it is set 945 to `False` by default. 946 These kwargs may also include arguments to be propagated to 947 `ZogyTask.computeDiffim` and `ZogyTask.computeScorr`. 951 result : `lsst.pipe.base.Struct` 952 Result struct with components: 954 ``subExposure``: Either the subExposure of the proper image difference ``D``, 955 or (if `doScorr==True`) the corrected likelihood exposure ``S``. 959 This `run` method accepts parameters identical to those of 960 `ImageMapper.run`, since it is called from the 961 `ImageMapperTask`. See that class for more information. 963 bbox = subExposure.getBBox()
964 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
967 imageSpace = kwargs.pop(
'inImageSpace',
False)
968 doScorr = kwargs.pop(
'doScorr',
False)
969 sigmas = kwargs.pop(
'sigmas',
None)
970 padSize = kwargs.pop(
'padSize', 7)
973 subExp2 = expandedSubExposure
976 subExp1 = template.Factory(template, expandedSubExposure.getBBox())
982 sig1, sig2 = sigmas[0], sigmas[1]
984 def _makePsfSquare(psf):
986 if psf.shape[0] < psf.shape[1]:
987 psf = np.pad(psf, ((0, psf.shape[1] - psf.shape[0]), (0, 0)), mode=
'constant',
989 elif psf.shape[0] > psf.shape[1]:
990 psf = np.pad(psf, ((0, 0), (0, psf.shape[0] - psf.shape[1])), mode=
'constant',
994 psf2 = subExp2.getPsf().computeKernelImage(center).getArray()
995 psf2 = _makePsfSquare(psf2)
997 psf1 = template.getPsf().computeKernelImage(center).getArray()
998 psf1 = _makePsfSquare(psf1)
1001 if subExp1.getDimensions()[0] < psf1.shape[0]
or subExp1.getDimensions()[1] < psf1.shape[1]:
1002 return pipeBase.Struct(subExposure=subExposure)
1004 def _filterPsf(psf):
1005 """Filter a noisy Psf to remove artifacts. Subject of future research.""" 1007 if psf.shape[0] == 41:
1010 psf[0:10, :] = psf[:, 0:10] = psf[31:41, :] = psf[:, 31:41] = 0
1015 psf1b = psf2b =
None 1016 if self.config.doFilterPsfs:
1018 psf1b = _filterPsf(psf1)
1019 psf2b = _filterPsf(psf2)
1022 if imageSpace
is True:
1023 config.inImageSpace = imageSpace
1024 config.padSize = padSize
1025 task =
ZogyTask(templateExposure=subExp1, scienceExposure=subExp2,
1026 sig1=sig1, sig2=sig2, psf1=psf1b, psf2=psf2b, config=config)
1029 res = task.computeDiffim(**kwargs)
1032 res = task.computeScorr(**kwargs)
1035 outExp = D.Factory(D, subExposure.getBBox())
1036 out = pipeBase.Struct(subExposure=outExp)
1041 """Config to be passed to ImageMapReduceTask 1043 This config targets the imageMapper to use the ZogyMapper. 1045 mapper = pexConfig.ConfigurableField(
1046 doc=
'Zogy task to run on each sub-image',
1052 """Config for the ZogyImagePsfMatchTask""" 1054 zogyConfig = pexConfig.ConfigField(
1056 doc=
'ZogyTask config to use when running on complete exposure (non spatially-varying)',
1059 zogyMapReduceConfig = pexConfig.ConfigField(
1060 dtype=ZogyMapReduceConfig,
1061 doc=
'ZogyMapReduce config to use when running Zogy on each sub-image (spatially-varying)',
1073 """Task to perform Zogy PSF matching and image subtraction. 1075 This class inherits from ImagePsfMatchTask to contain the _warper 1076 subtask and related methods. 1079 ConfigClass = ZogyImagePsfMatchConfig
1082 ImagePsfMatchTask.__init__(self, *args, **kwargs)
1084 def _computeImageMean(self, exposure):
1085 """Compute the sigma-clipped mean of the pixels image of `exposure`. 1088 statsControl.setNumSigmaClip(3.)
1089 statsControl.setNumIter(3)
1090 ignoreMaskPlanes = (
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
1091 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(ignoreMaskPlanes))
1093 exposure.getMaskedImage().getMask(),
1094 afwMath.MEANCLIP | afwMath.MEDIAN, statsControl)
1095 mn = statObj.getValue(afwMath.MEANCLIP)
1096 med = statObj.getValue(afwMath.MEDIAN)
1100 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1101 doPreConvolve=False):
1102 """Register, PSF-match, and subtract two Exposures using the ZOGY algorithm. 1104 Do the following, in order: 1105 - Warp templateExposure to match scienceExposure, if their WCSs do not already match 1106 - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures 1110 templateExposure : `lsst.afw.image.Exposure` 1111 exposure to PSF-match to scienceExposure. The exposure's mean value is subtracted 1113 scienceExposure : `lsst.afw.image.Exposure` 1114 reference Exposure. The exposure's mean value is subtracted in-place. 1116 what to do if templateExposure's and scienceExposure's WCSs do not match: 1117 - if True then warp templateExposure to match scienceExposure 1118 - if False then raise an Exception 1119 spatiallyVarying : `bool` 1120 If True, perform the operation over a grid of patches across the two exposures 1121 inImageSpace : `bool` 1122 If True, perform the Zogy convolutions in image space rather than in frequency space. 1123 doPreConvolve : `bool` 1124 ***Currently not implemented.*** If True assume we are to compute the match filter-convolved 1125 exposure which can be thresholded for detection. In the case of Zogy this would mean 1126 we compute the Scorr image. 1130 A `lsst.pipe.base.Struct` containing these fields: 1131 - subtractedExposure: subtracted Exposure 1132 - warpedExposure: templateExposure after warping to match scienceExposure (if doWarping true) 1137 self.log.
info(
"Exposure means=%f, %f; median=%f, %f:" % (mn1[0], mn2[0], mn1[1], mn2[1]))
1138 if not np.isnan(mn1[0])
and np.abs(mn1[0]) > 1:
1139 mi = templateExposure.getMaskedImage()
1141 if not np.isnan(mn2[0])
and np.abs(mn2[0]) > 1:
1142 mi = scienceExposure.getMaskedImage()
1145 self.log.
info(
'Running Zogy algorithm: spatiallyVarying=%r' % spatiallyVarying)
1147 if not self.
_validateWcs(templateExposure, scienceExposure):
1149 self.log.
info(
"Astrometrically registering template to science image")
1152 scienceExposure.getWcs())
1153 psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform)
1156 destBBox=scienceExposure.getBBox())
1158 templateExposure.setPsf(psfWarped)
1160 self.log.
error(
"ERROR: Input images not registered")
1161 raise RuntimeError(
"Input images not registered")
1164 return exp.getMaskedImage().getMask()
1167 return exp.getMaskedImage().getImage().getArray()
1169 if self.config.zogyConfig.inImageSpace:
1171 self.log.
info(
'Running Zogy algorithm: inImageSpace=%r' % inImageSpace)
1172 if spatiallyVarying:
1173 config = self.config.zogyMapReduceConfig
1175 results = task.run(scienceExposure, template=templateExposure, inImageSpace=inImageSpace,
1176 doScorr=doPreConvolve, forceEvenSized=
False)
1177 results.D = results.exposure
1184 config = self.config.zogyConfig
1185 task =
ZogyTask(scienceExposure=scienceExposure, templateExposure=templateExposure,
1187 if not doPreConvolve:
1188 results = task.computeDiffim(inImageSpace=inImageSpace)
1189 results.matchedExposure = results.R
1191 results = task.computeScorr(inImageSpace=inImageSpace)
1192 results.D = results.S
1195 mask = results.D.getMaskedImage().getMask()
1196 mask |= scienceExposure.getMaskedImage().getMask()
1197 mask |= templateExposure.getMaskedImage().getMask()
1198 results.D.getMaskedImage().getMask()[:, :] = mask
1199 badBitsNan = mask.addMaskPlane(
'UNMASKEDNAN')
1200 resultsArr = results.D.getMaskedImage().getMask().getArray()
1201 resultsArr[np.isnan(resultsArr)] |= badBitsNan
1202 resultsArr[np.isnan(scienceExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1203 resultsArr[np.isnan(templateExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1205 results.subtractedExposure = results.D
1206 results.warpedExposure = templateExposure
1210 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1211 doPreConvolve=False):
1212 raise NotImplementedError
1215 subtractAlgorithmRegistry.register(
'zogy', ZogyImagePsfMatchTask)
def _computeImageMean(self, exposure)
def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False, R_hat=None, Kr_hat=None, Kr=None, N_hat=None, Kn_hat=None, Kn=None)
def computeDiffim(self, inImageSpace=None, padSize=None, returnMatchedTemplate=False, kwargs)
def computeDiffimFourierSpace(self, debug=False, returnMatchedTemplate=False, kwargs)
Parameters to control convolution.
def __init__(self, args, kwargs)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
def computePrereqs(self, psf1=None, psf2=None, padSize=0)
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Pass parameters to a Statistics object.
def run(self, subExposure, expandedSubExposure, fullBBox, template, kwargs)
def subtractExposures(self, templateExposure, scienceExposure, doWarping=True, spatiallyVarying=True, inImageSpace=False, doPreConvolve=False)
def setup(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None, psf1=None, psf2=None, correctBackground=False, args, kwargs)
def __init__(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None, psf1=None, psf2=None, args, kwargs)
def computeScorrFourierSpace(self, xVarAst=0., yVarAst=0., kwargs)
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
A Transform obtained by putting two SkyWcs objects "back to back".
def _computeVarianceMean(self, exposure)
def computeDiffimPsf(self, padSize=0, keepFourier=False, psf1=None, psf2=None)
def _doConvolve(self, exposure, kernel, recenterKernel=False)
def _setNewPsf(self, exposure, psfArr)
def subtractMaskedImages(self, templateExposure, scienceExposure, doWarping=True, spatiallyVarying=True, inImageSpace=False, doPreConvolve=False)
def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, kwargs)
def computeDiffimImageSpace(self, padSize=None, debug=False, kwargs)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def _validateWcs(self, templateExposure, scienceExposure)
def computeScorrImageSpace(self, xVarAst=0., yVarAst=0., padSize=None, kwargs)
A kernel created from an Image.
def __init__(self, args, kwargs)