32 from .imageMapReduce
import (ImageMapReduceConfig, ImageMapper,
34 from .imagePsfMatch
import (ImagePsfMatchTask, ImagePsfMatchConfig,
35 subtractAlgorithmRegistry)
37 __all__ = [
"ZogyTask",
"ZogyConfig",
38 "ZogyMapper",
"ZogyMapReduceConfig",
39 "ZogyImagePsfMatchConfig",
"ZogyImagePsfMatchTask"]
42 """Tasks for performing the "Proper image subtraction" algorithm of 43 Zackay, et al. (2016), hereafter simply referred to as 'ZOGY (2016)'. 45 `ZogyTask` contains methods to perform the basic estimation of the 46 ZOGY diffim `D`, its updated PSF, and the variance-normalized 47 likelihood image `S_corr`. We have implemented ZOGY using the 48 proscribed methodology, computing all convolutions in Fourier space, 49 and also variants in which the convolutions are performed in real 50 (image) space. The former is faster and results in fewer artifacts 51 when the PSFs are noisy (i.e., measured, for example, via 52 `PsfEx`). The latter is presumed to be preferred as it can account for 53 masks correctly with fewer "ringing" artifacts from edge effects or 54 saturated stars, but noisy PSFs result in their own smaller 55 artifacts. Removal of these artifacts is a subject of continuing 56 research. Currently, we "pad" the PSFs when performing the 57 subtractions in real space, which reduces, but does not entirely 58 eliminate these artifacts. 60 All methods in `ZogyTask` assume template and science images are 61 already accurately photometrically and astrometrically registered. 63 `ZogyMapper` is a wrapper which runs `ZogyTask` in the 64 `ImageMapReduce` framework, computing of ZOGY diffim's on small, 65 overlapping sub-images, thereby enabling complete ZOGY diffim's which 66 account for spatially-varying noise and PSFs across the two input 67 exposures. An example of the use of this task is in the `testZogy.py` 73 """Configuration parameters for the ZogyTask 75 inImageSpace = pexConfig.Field(
78 doc=
"Perform all convolutions in real (image) space rather than Fourier space. " 79 "Currently if True, this results in artifacts when using real (noisy) PSFs." 82 padSize = pexConfig.Field(
85 doc=
"Number of pixels to pad PSFs to avoid artifacts (when inImageSpace is True)" 88 templateFluxScaling = pexConfig.Field(
91 doc=
"Template flux scaling factor (Fr in ZOGY paper)" 94 scienceFluxScaling = pexConfig.Field(
97 doc=
"Science flux scaling factor (Fn in ZOGY paper)" 100 doTrimKernels = pexConfig.Field(
103 doc=
"Trim kernels for image-space ZOGY. Speeds up convolutions and shrinks artifacts. " 104 "Subject of future research." 107 doFilterPsfs = pexConfig.Field(
110 doc=
"Filter PSFs for image-space ZOGY. Aids in reducing artifacts. " 111 "Subject of future research." 114 ignoreMaskPlanes = pexConfig.ListField(
116 default=(
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE"),
117 doc=
"Mask planes to ignore for statistics" 125 """Task to perform ZOGY proper image subtraction. See module-level documentation for 128 In all methods, im1 is R (reference, or template) and im2 is N (new, or science). 130 ConfigClass = ZogyConfig
131 _DefaultName =
"ip_diffim_Zogy" 133 def __init__(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None,
134 psf1=None, psf2=None, *args, **kwargs):
135 """Create the ZOGY task. 139 templateExposure : `lsst.afw.image.Exposure` 140 Template exposure ("Reference image" in ZOGY (2016)). 141 scienceExposure : `lsst.afw.image.Exposure` 142 Science exposure ("New image" in ZOGY (2016)). Must have already been 143 registered and photmetrically matched to template. 145 (Optional) sqrt(variance) of `templateExposure`. If `None`, it is 146 computed from the sqrt(mean) of the `templateExposure` variance image. 148 (Optional) sqrt(variance) of `scienceExposure`. If `None`, it is 149 computed from the sqrt(mean) of the `scienceExposure` variance image. 150 psf1 : 2D `numpy.array` 151 (Optional) 2D array containing the PSF image for the template. If 152 `None`, it is extracted from the PSF taken at the center of `templateExposure`. 153 psf2 : 2D `numpy.array` 154 (Optional) 2D array containing the PSF image for the science img. If 155 `None`, it is extracted from the PSF taken at the center of `scienceExposure`. 157 additional arguments to be passed to 158 `lsst.pipe.base.Task` 160 additional keyword arguments to be passed to 161 `lsst.pipe.base.Task` 163 pipeBase.Task.__init__(self, *args, **kwargs)
165 self.
setup(templateExposure=templateExposure, scienceExposure=scienceExposure,
166 sig1=sig1, sig2=sig2, psf1=psf1, psf2=psf2, *args, **kwargs)
168 def setup(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None,
169 psf1=None, psf2=None, correctBackground=False, *args, **kwargs):
170 """Set up the ZOGY task. 174 templateExposure : `lsst.afw.image.Exposure` 175 Template exposure ("Reference image" in ZOGY (2016)). 176 scienceExposure : `lsst.afw.image.Exposure` 177 Science exposure ("New image" in ZOGY (2016)). Must have already been 178 registered and photmetrically matched to template. 180 (Optional) sqrt(variance) of `templateExposure`. If `None`, it is 181 computed from the sqrt(mean) of the `templateExposure` variance image. 183 (Optional) sqrt(variance) of `scienceExposure`. If `None`, it is 184 computed from the sqrt(mean) of the `scienceExposure` variance image. 185 psf1 : 2D `numpy.array` 186 (Optional) 2D array containing the PSF image for the template. If 187 `None`, it is extracted from the PSF taken at the center of `templateExposure`. 188 psf2 : 2D `numpy.array` 189 (Optional) 2D array containing the PSF image for the science img. If 190 `None`, it is extracted from the PSF taken at the center of `scienceExposure`. 191 correctBackground : `bool` 192 (Optional) subtract sigma-clipped mean of exposures. Zogy doesn't correct 193 nonzero backgrounds (unlike AL) so subtract them here. 195 additional arguments to be passed to 196 `lsst.pipe.base.Task` 198 additional keyword arguments to be passed to 199 `lsst.pipe.base.Task` 201 if self.
template is None and templateExposure
is None:
203 if self.
science is None and scienceExposure
is None:
212 self.
statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(
213 self.config.ignoreMaskPlanes))
216 self.
im2 = self.
science.getMaskedImage().getImage().getArray()
220 def selectPsf(psf, exposure):
225 xcen = (bbox1.getBeginX() + bbox1.getEndX()) / 2.
226 ycen = (bbox1.getBeginY() + bbox1.getEndY()) / 2.
227 return exposure.getPsf().computeKernelImage(
afwGeom.Point2D(xcen, ycen)).getArray()
237 if (pShape1[0] < pShape2[0]):
238 psf1 = np.pad(psf1, ((0, pShape2[0] - pShape1[0]), (0, 0)), mode=
'constant', constant_values=0.)
239 elif (pShape2[0] < pShape1[0]):
240 psf2 = np.pad(psf2, ((0, pShape1[0] - pShape2[0]), (0, 0)), mode=
'constant', constant_values=0.)
241 if (pShape1[1] < pShape2[1]):
242 psf1 = np.pad(psf1, ((0, 0), (0, pShape2[1] - pShape1[1])), mode=
'constant', constant_values=0.)
243 elif (pShape2[1] < pShape1[1]):
244 psf2 = np.pad(psf2, ((0, 0), (0, pShape1[1] - pShape2[1])), mode=
'constant', constant_values=0.)
247 maxLoc1 = np.unravel_index(np.argmax(psf1), psf1.shape)
248 maxLoc2 = np.unravel_index(np.argmax(psf2), psf2.shape)
250 while (maxLoc1[0] != maxLoc2[0])
or (maxLoc1[1] != maxLoc2[1]):
251 if maxLoc1[0] > maxLoc2[0]:
252 psf2[1:, :] = psf2[:-1, :]
253 elif maxLoc1[0] < maxLoc2[0]:
254 psf1[1:, :] = psf1[:-1, :]
255 if maxLoc1[1] > maxLoc2[1]:
256 psf2[:, 1:] = psf2[:, :-1]
257 elif maxLoc1[1] < maxLoc2[1]:
258 psf1[:, 1:] = psf1[:, :-1]
259 maxLoc1 = np.unravel_index(np.argmax(psf1), psf1.shape)
260 maxLoc2 = np.unravel_index(np.argmax(psf2), psf2.shape)
263 psf1[psf1 < MIN_KERNEL] = MIN_KERNEL
264 psf2[psf2 < MIN_KERNEL] = MIN_KERNEL
273 if np.isnan(self.
sig1)
or self.
sig1 == 0:
275 if np.isnan(self.
sig2)
or self.
sig2 == 0:
279 if correctBackground:
280 def _subtractImageMean(exposure):
281 """Compute the sigma-clipped mean of the image of `exposure`.""" 282 mi = exposure.getMaskedImage()
285 mean = statObj.getValue(afwMath.MEANCLIP)
286 if not np.isnan(mean):
290 _subtractImageMean(self.
science)
292 self.
Fr = self.config.templateFluxScaling
293 self.
Fn = self.config.scienceFluxScaling
296 def _computeVarianceMean(self, exposure):
297 """Compute the sigma-clipped mean of the variance image of `exposure`. 300 exposure.getMaskedImage().getMask(),
302 var = statObj.getValue(afwMath.MEANCLIP)
306 def _padPsfToSize(psf, size):
307 """Zero-pad `psf` to the dimensions given by `size`. 311 psf : 2D `numpy.array` 312 Input psf to be padded 314 Two element list containing the dimensions to pad the `psf` to 318 psf : 2D `numpy.array` 319 The padded copy of the input `psf`. 321 newArr = np.zeros(size)
322 offset = [size[0]//2 - psf.shape[0]//2 - 1, size[1]//2 - psf.shape[1]//2 - 1]
323 tmp = newArr[offset[0]:(psf.shape[0] + offset[0]), offset[1]:(psf.shape[1] + offset[1])]
328 """Compute standard ZOGY quantities used by (nearly) all methods. 330 Many of the ZOGY calculations require similar quantities, including 331 FFTs of the PSFs, and the "denominator" term (e.g. in eq. 13 of 332 ZOGY manuscript (2016). This function consolidates many of those 337 psf1 : 2D `numpy.array` 338 (Optional) Input psf of template, override if already padded 339 psf2 : 2D `numpy.array` 340 (Optional) Input psf of science image, override if already padded 341 padSize : `int`, optional 342 Number of pixels to pad the image on each side with zeroes. 346 A `lsst.pipe.base.Struct` containing: 347 - Pr : 2D `numpy.array`, the (possibly zero-padded) template PSF 348 - Pn : 2D `numpy.array`, the (possibly zero-padded) science PSF 349 - Pr_hat : 2D `numpy.array`, the FFT of `Pr` 350 - Pn_hat : 2D `numpy.array`, the FFT of `Pn` 351 - denom : 2D `numpy.array`, the denominator of equation (13) in ZOGY (2016) manuscript 352 - Fd : `float`, the relative flux scaling factor between science and template 354 psf1 = self.
im1_psf if psf1
is None else psf1
355 psf2 = self.
im2_psf if psf2
is None else psf2
356 padSize = self.
padSize if padSize
is None else padSize
359 Pr = ZogyTask._padPsfToSize(psf1, (psf1.shape[0] + padSize, psf1.shape[1] + padSize))
360 Pn = ZogyTask._padPsfToSize(psf2, (psf2.shape[0] + padSize, psf2.shape[1] + padSize))
362 psf1[np.abs(psf1) <= MIN_KERNEL] = MIN_KERNEL
363 psf2[np.abs(psf2) <= MIN_KERNEL] = MIN_KERNEL
366 Pr_hat = np.fft.fft2(Pr)
367 Pr_hat2 = np.conj(Pr_hat) * Pr_hat
368 Pn_hat = np.fft.fft2(Pn)
369 Pn_hat2 = np.conj(Pn_hat) * Pn_hat
370 denom = np.sqrt((sigN**2 * self.
Fr**2 * Pr_hat2) + (sigR**2 * self.
Fn**2 * Pn_hat2))
371 Fd = self.
Fr * self.
Fn / np.sqrt(sigN**2 * self.
Fr**2 + sigR**2 * self.
Fn**2)
373 res = pipeBase.Struct(
374 Pr=Pr, Pn=Pn, Pr_hat=Pr_hat, Pn_hat=Pn_hat, denom=denom, Fd=Fd
379 r"""Compute ZOGY diffim `D` as proscribed in ZOGY (2016) manuscript 383 debug : `bool`, optional 384 If set to True, filter the kernels by setting the edges to zero. 385 returnMatchedTemplate : `bool`, optional 386 Calculate the template image. 387 If not set, the returned template will be None. 391 In all functions, im1 is R (reference, or template) and im2 is N (new, or science) 392 Compute the ZOGY eqn. (13): 395 \widehat{D} = \\frac{Fr\widehat{Pr}\widehat{N} - 396 F_n\widehat{Pn}\widehat{R}}{\sqrt{\sigma_n^2 Fr^2 397 \|\widehat{Pr}\|^2 + \sigma_r^2 F_n^2 \|\widehat{Pn}\|^2}} 399 where :math:`D` is the optimal difference image, :math:`R` and :math:`N` are the 400 reference and "new" image, respectively, :math:`Pr` and :math:`P_n` are their 401 PSFs, :math:`Fr` and :math:`Fn` are their flux-based zero-points (which we 402 will set to one here), :math:`\sigma_r^2` and :math:`\sigma_n^2` are their 403 variance, and :math:`\widehat{D}` denotes the FT of :math:`D`. 407 result : `lsst.pipe.base.Struct` 408 Result struct with components: 410 - ``D`` : 2D `numpy.array`, the proper image difference 411 - ``D_var`` : 2D `numpy.array`, the variance image for `D` 414 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
415 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
419 def _filterKernel(K, trim_amount):
422 K[:ps, :] = K[-ps:, :] = 0
423 K[:, :ps] = K[:, -ps:] = 0
426 Kr_hat = self.
Fr * preqs.Pr_hat / preqs.denom
427 Kn_hat = self.
Fn * preqs.Pn_hat / preqs.denom
428 if debug
and self.config.doTrimKernels:
431 ps = (Kn_hat.shape[1] - 80)//2
432 Kn = _filterKernel(np.fft.ifft2(Kn_hat), ps)
433 Kn_hat = np.fft.fft2(Kn)
434 Kr = _filterKernel(np.fft.ifft2(Kr_hat), ps)
435 Kr_hat = np.fft.fft2(Kr)
437 def processImages(im1, im2, doAdd=False):
439 im1[np.isinf(im1)] = np.nan
440 im1[np.isnan(im1)] = np.nanmean(im1)
441 im2[np.isinf(im2)] = np.nan
442 im2[np.isnan(im2)] = np.nanmean(im2)
444 R_hat = np.fft.fft2(im1)
445 N_hat = np.fft.fft2(im2)
447 D_hat = Kr_hat * N_hat
448 D_hat_R = Kn_hat * R_hat
454 D = np.fft.ifft2(D_hat)
455 D = np.fft.ifftshift(D.real) / preqs.Fd
458 if returnMatchedTemplate:
459 R = np.fft.ifft2(D_hat_R)
460 R = np.fft.ifftshift(R.real) / preqs.Fd
465 D, R = processImages(self.
im1, self.
im2, doAdd=
False)
467 D_var, R_var = processImages(self.
im1_var, self.
im2_var, doAdd=
True)
469 return pipeBase.Struct(D=D, D_var=D_var, R=R, R_var=R_var)
471 def _doConvolve(self, exposure, kernel, recenterKernel=False):
472 """Convolve an Exposure with a decorrelation convolution kernel. 476 exposure : `lsst.afw.image.Exposure` 477 Input exposure to be convolved. 478 kernel : `numpy.array` 479 2D `numpy.array` to convolve the image with 480 recenterKernel : `bool`, optional 481 Force the kernel center to the pixel with the maximum value. 485 A new `lsst.afw.image.Exposure` with the convolved pixels and the (possibly 490 - We optionally re-center the kernel if necessary and return the possibly 493 kernelImg = afwImage.ImageD(kernel.shape[1], kernel.shape[0])
494 kernelImg.getArray()[:, :] = kernel
497 maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
498 kern.setCtrX(maxloc[0])
499 kern.setCtrY(maxloc[1])
500 outExp = exposure.clone()
502 maxInterpolationDistance=0)
504 afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl)
505 except AttributeError:
513 """Compute ZOGY diffim `D` using image-space convlutions 515 This method is still being debugged as it results in artifacts 516 when the PSFs are noisy (see module-level docstring). Thus 517 there are several options still enabled by the `debug` flag, 518 which are disabled by defult. 523 The amount to pad the PSFs by 525 Flag to enable debugging tests and options 529 D : `lsst.afw.Exposure` 530 the proper image difference, including correct variance, 538 Kr_hat = (preqs.Pr_hat + delta) / (preqs.denom + delta)
539 Kn_hat = (preqs.Pn_hat + delta) / (preqs.denom + delta)
540 Kr = np.fft.ifft2(Kr_hat).real
541 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
542 Kn = np.fft.ifft2(Kn_hat).real
543 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
545 def _trimKernel(self, K, trim_amount):
549 K = K[ps:-ps, ps:-ps]
552 padSize = self.
padSize if padSize
is None else padSize
554 if debug
and self.config.doTrimKernels:
557 Kn = _trimKernel(Kn, padSize)
558 Kr = _trimKernel(Kr, padSize)
564 tmp = D.getMaskedImage()
565 tmp -= exp1.getMaskedImage()
567 return pipeBase.Struct(D=D, R=exp1)
569 def _setNewPsf(self, exposure, psfArr):
570 """Utility method to set an exposure's PSF when provided as a 2-d numpy.array 572 bbox = exposure.getBBox()
573 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
575 psfI = afwImage.ImageD(psfArr.shape[1], psfArr.shape[0])
576 psfI.getArray()[:, :] = psfArr
578 psfNew = measAlg.KernelPsf(psfK, center)
579 exposure.setPsf(psfNew)
583 returnMatchedTemplate=False, **kwargs):
584 """Wrapper method to compute ZOGY proper diffim 586 This method should be used as the public interface for 587 computing the ZOGY diffim. 591 inImageSpace : `bool` 592 Override config `inImageSpace` parameter 594 Override config `padSize` parameter 595 returnMatchedTemplate : `bool` 596 Include the PSF-matched template in the results Struct 598 additional keyword arguments to be passed to 599 `computeDiffimFourierSpace` or `computeDiffimImageSpace`. 603 An lsst.pipe.base.Struct containing: 604 - D : `lsst.afw.Exposure` 605 the proper image difference, including correct variance, 607 - R : `lsst.afw.Exposure` 608 If `returnMatchedTemplate` is True, the PSF-matched template 612 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
614 padSize = self.
padSize if padSize
is None else padSize
617 if returnMatchedTemplate:
622 D.getMaskedImage().getImage().getArray()[:, :] = res.D
623 D.getMaskedImage().getVariance().getArray()[:, :] = res.D_var
624 if returnMatchedTemplate:
626 R.getMaskedImage().getImage().getArray()[:, :] = res.R
627 R.getMaskedImage().getVariance().getArray()[:, :] = res.R_var
631 return pipeBase.Struct(D=D, R=R)
634 """Compute the ZOGY diffim PSF (ZOGY manuscript eq. 14) 639 Override config `padSize` parameter 641 Return the FFT of the diffim PSF (do not inverse-FFT it) 642 psf1 : 2D `numpy.array` 643 (Optional) Input psf of template, override if already padded 644 psf2 : 2D `numpy.array` 645 (Optional) Input psf of science image, override if already padded 649 Pd : 2D `numpy.array` 650 The diffim PSF (or FFT of PSF if `keepFourier=True`) 652 preqs = self.
computePrereqs(psf1=psf1, psf2=psf2, padSize=padSize)
654 Pd_hat_numerator = (self.
Fr * self.
Fn * preqs.Pr_hat * preqs.Pn_hat)
655 Pd_hat = Pd_hat_numerator / (preqs.Fd * preqs.denom)
660 Pd = np.fft.ifft2(Pd_hat)
661 Pd = np.fft.ifftshift(Pd).real
665 def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False,
666 R_hat=None, Kr_hat=None, Kr=None,
667 N_hat=None, Kn_hat=None, Kn=None):
668 """Compute the astrometric noise correction terms 670 Compute the correction for estimated astrometric noise as 671 proscribed in ZOGY (2016), section 3.3. All convolutions 672 performed either in real (image) or Fourier space. 676 xVarAst, yVarAst : `float` 677 estimated astrometric noise (variance of astrometric registration errors) 678 inImageSpace : `bool` 679 Perform all convolutions in real (image) space rather than Fourier space 680 R_hat : 2-D `numpy.array` 681 (Optional) FFT of template image, only required if `inImageSpace=False` 682 Kr_hat : 2-D `numpy.array` 683 FFT of Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=False` 684 Kr : 2-D `numpy.array` 685 Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=True`. 686 Kr is associated with the template (reference). 687 N_hat : 2-D `numpy.array` 688 FFT of science image, only required if `inImageSpace=False` 689 Kn_hat : 2-D `numpy.array` 690 FFT of Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=False` 691 Kn : 2-D `numpy.array` 692 Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=True`. 693 Kn is associated with the science (new) image. 697 VastSR, VastSN : 2-D `numpy.array` 698 Arrays containing the values in eqs. 30 and 32 of ZOGY (2016). 701 if xVarAst + yVarAst > 0:
704 S_R = S_R.getMaskedImage().getImage().getArray()
706 S_R = np.fft.ifft2(R_hat * Kr_hat)
707 gradRx, gradRy = np.gradient(S_R)
708 VastSR = xVarAst * gradRx**2. + yVarAst * gradRy**2.
712 S_N = S_N.getMaskedImage().getImage().getArray()
714 S_N = np.fft.ifft2(N_hat * Kn_hat)
715 gradNx, gradNy = np.gradient(S_N)
716 VastSN = xVarAst * gradNx**2. + yVarAst * gradNy**2.
718 return VastSR, VastSN
721 """Compute corrected likelihood image, optimal for source detection 723 Compute ZOGY S_corr image. This image can be thresholded for 724 detection without optimal filtering, and the variance image is 725 corrected to account for astrometric noise (errors in 726 astrometric registration whether systematic or due to effects 727 such as DCR). The calculations here are all performed in 728 Fourier space, as proscribed in ZOGY (2016). 732 xVarAst, yVarAst : `float` 733 estimated astrometric noise (variance of astrometric registration errors) 737 result : `lsst.pipe.base.Struct` 738 Result struct with components: 740 - ``S`` : `numpy.array`, the likelihood image S (eq. 12 of ZOGY (2016)) 741 - ``S_var`` : the corrected variance image (denominator of eq. 25 of ZOGY (2016)) 742 - ``Dpsf`` : the PSF of the diffim D, likely never to be used. 746 """Replace any NaNs or Infs with the mean of the image.""" 747 isbad = ~np.isfinite(im)
750 im[isbad] = np.nanmean(im)
753 self.
im1 = fix_nans(self.
im1)
754 self.
im2 = fix_nans(self.
im2)
759 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
760 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
765 R_hat = np.fft.fft2(self.
im1)
766 N_hat = np.fft.fft2(self.
im2)
767 D_hat = self.
Fr * preqs.Pr_hat * N_hat - self.
Fn * preqs.Pn_hat * R_hat
770 Pd_hat = self.
computeDiffimPsf(padSize=0, keepFourier=
True, psf1=psf1, psf2=psf2)
771 Pd_bar = np.conj(Pd_hat)
772 S = np.fft.ifft2(D_hat * Pd_bar)
776 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
777 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
778 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
779 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
781 Kr_hat2 = np.fft.fft2(np.fft.ifft2(Kr_hat)**2.)
782 Kn_hat2 = np.fft.fft2(np.fft.ifft2(Kn_hat)**2.)
783 var1c_hat = Kr_hat2 * np.fft.fft2(self.
im1_var)
784 var2c_hat = Kn_hat2 * np.fft.fft2(self.
im2_var)
788 R_hat=R_hat, Kr_hat=Kr_hat,
789 N_hat=N_hat, Kn_hat=Kn_hat)
791 S_var = np.sqrt(np.fft.ifftshift(np.fft.ifft2(var1c_hat + var2c_hat)) + fGradR + fGradN)
794 S = np.fft.ifftshift(np.fft.ifft2(Kn_hat * N_hat - Kr_hat * R_hat))
798 return pipeBase.Struct(S=S.real, S_var=S_var.real, Dpsf=Pd)
801 """Compute corrected likelihood image, optimal for source detection 803 Compute ZOGY S_corr image. This image can be thresholded for 804 detection without optimal filtering, and the variance image is 805 corrected to account for astrometric noise (errors in 806 astrometric registration whether systematic or due to effects 807 such as DCR). The calculations here are all performed in 812 xVarAst, yVarAst : `float` 813 estimated astrometric noise (variance of astrometric registration errors) 817 A `lsst.pipe.base.Struct` containing: 818 - S : `lsst.afw.image.Exposure`, the likelihood exposure S (eq. 12 of ZOGY (2016)), 819 including corrected variance, masks, and PSF 820 - D : `lsst.afw.image.Exposure`, the proper image difference, including correct 821 variance, masks, and PSF 826 padSize = self.
padSize if padSize
is None else padSize
830 Pd_bar = np.fliplr(np.flipud(Pd))
832 tmp = S.getMaskedImage()
837 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
838 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
839 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
840 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
842 Kr = np.fft.ifft2(Kr_hat).real
843 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
844 Kn = np.fft.ifft2(Kn_hat).real
845 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
853 Smi = S.getMaskedImage()
855 S_var = np.sqrt(var1c.getArray() + var2c.getArray() + fGradR + fGradN)
856 S.getMaskedImage().getVariance().getArray()[:, :] = S_var
860 return pipeBase.Struct(S=S, D=D)
862 def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, **kwargs):
863 """Wrapper method to compute ZOGY corrected likelihood image, optimal for 866 This method should be used as the public interface for 867 computing the ZOGY S_corr. 871 xVarAst, yVarAst : `float` 872 estimated astrometric noise (variance of astrometric registration errors) 873 inImageSpace : `bool` 874 Override config `inImageSpace` parameter 876 Override config `padSize` parameter 880 S : `lsst.afw.image.Exposure` 881 The likelihood exposure S (eq. 12 of ZOGY (2016)), 882 including corrected variance, masks, and PSF 884 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
892 S.getMaskedImage().getImage().getArray()[:, :] = res.S
893 S.getMaskedImage().getVariance().getArray()[:, :] = res.S_var
896 return pipeBase.Struct(S=S)
900 """Task to be used as an ImageMapper for performing 901 ZOGY image subtraction on a grid of subimages. 903 ConfigClass = ZogyConfig
904 _DefaultName =
'ip_diffim_ZogyMapper' 907 ImageMapper.__init__(self, *args, **kwargs)
909 def run(self, subExposure, expandedSubExposure, fullBBox, template,
911 """Perform ZOGY proper image subtraction on sub-images 913 This method performs ZOGY proper image subtraction on 914 `subExposure` using local measures for image variances and 915 PSF. `subExposure` is a sub-exposure of the science image. It 916 also requires the corresponding sub-exposures of the template 917 (`template`). The operations are actually performed on 918 `expandedSubExposure` to allow for invalid edge pixels arising 919 from convolutions, which are then removed. 923 subExposure : `lsst.afw.image.Exposure` 924 the sub-exposure of the diffim 925 expandedSubExposure : `lsst.afw.image.Exposure` 926 the expanded sub-exposure upon which to operate 927 fullBBox : `lsst.afw.geom.BoundingBox` 928 the bounding box of the original exposure 929 template : `lsst.afw.image.Exposure` 930 the template exposure, from which a corresponding sub-exposure 933 additional keyword arguments propagated from 934 `ImageMapReduceTask.run`. These include: 937 Compute and return the corrected likelihood image S_corr 938 rather than the proper image difference 939 ``inImageSpace`` : `bool` 940 Perform all convolutions in real (image) space rather than 941 in Fourier space. This option currently leads to artifacts 942 when using real (measured and noisy) PSFs, thus it is set 943 to `False` by default. 944 These kwargs may also include arguments to be propagated to 945 `ZogyTask.computeDiffim` and `ZogyTask.computeScorr`. 949 result : `lsst.pipe.base.Struct` 950 Result struct with components: 952 ``subExposure``: Either the subExposure of the proper image difference ``D``, 953 or (if `doScorr==True`) the corrected likelihood exposure ``S``. 957 This `run` method accepts parameters identical to those of 958 `ImageMapper.run`, since it is called from the 959 `ImageMapperTask`. See that class for more information. 961 bbox = subExposure.getBBox()
962 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
965 imageSpace = kwargs.pop(
'inImageSpace',
False)
966 doScorr = kwargs.pop(
'doScorr',
False)
967 sigmas = kwargs.pop(
'sigmas',
None)
968 padSize = kwargs.pop(
'padSize', 7)
971 subExp2 = expandedSubExposure
974 subExp1 = template.Factory(template, expandedSubExposure.getBBox())
980 sig1, sig2 = sigmas[0], sigmas[1]
982 def _makePsfSquare(psf):
984 if psf.shape[0] < psf.shape[1]:
985 psf = np.pad(psf, ((0, psf.shape[1] - psf.shape[0]), (0, 0)), mode=
'constant',
987 elif psf.shape[0] > psf.shape[1]:
988 psf = np.pad(psf, ((0, 0), (0, psf.shape[0] - psf.shape[1])), mode=
'constant',
992 psf2 = subExp2.getPsf().computeKernelImage(center).getArray()
993 psf2 = _makePsfSquare(psf2)
995 psf1 = template.getPsf().computeKernelImage(center).getArray()
996 psf1 = _makePsfSquare(psf1)
999 if subExp1.getDimensions()[0] < psf1.shape[0]
or subExp1.getDimensions()[1] < psf1.shape[1]:
1000 return pipeBase.Struct(subExposure=subExposure)
1002 def _filterPsf(psf):
1003 """Filter a noisy Psf to remove artifacts. Subject of future research.""" 1005 if psf.shape[0] == 41:
1008 psf[0:10, :] = psf[:, 0:10] = psf[31:41, :] = psf[:, 31:41] = 0
1013 psf1b = psf2b =
None 1014 if self.config.doFilterPsfs:
1016 psf1b = _filterPsf(psf1)
1017 psf2b = _filterPsf(psf2)
1020 if imageSpace
is True:
1021 config.inImageSpace = imageSpace
1022 config.padSize = padSize
1023 task =
ZogyTask(templateExposure=subExp1, scienceExposure=subExp2,
1024 sig1=sig1, sig2=sig2, psf1=psf1b, psf2=psf2b, config=config)
1027 res = task.computeDiffim(**kwargs)
1030 res = task.computeScorr(**kwargs)
1033 outExp = D.Factory(D, subExposure.getBBox())
1034 out = pipeBase.Struct(subExposure=outExp)
1039 """Config to be passed to ImageMapReduceTask 1041 This config targets the imageMapper to use the ZogyMapper. 1043 mapper = pexConfig.ConfigurableField(
1044 doc=
'Zogy task to run on each sub-image',
1050 """Config for the ZogyImagePsfMatchTask""" 1052 zogyConfig = pexConfig.ConfigField(
1054 doc=
'ZogyTask config to use when running on complete exposure (non spatially-varying)',
1057 zogyMapReduceConfig = pexConfig.ConfigField(
1058 dtype=ZogyMapReduceConfig,
1059 doc=
'ZogyMapReduce config to use when running Zogy on each sub-image (spatially-varying)',
1071 """Task to perform Zogy PSF matching and image subtraction. 1073 This class inherits from ImagePsfMatchTask to contain the _warper 1074 subtask and related methods. 1077 ConfigClass = ZogyImagePsfMatchConfig
1080 ImagePsfMatchTask.__init__(self, *args, **kwargs)
1082 def _computeImageMean(self, exposure):
1083 """Compute the sigma-clipped mean of the pixels image of `exposure`. 1086 statsControl.setNumSigmaClip(3.)
1087 statsControl.setNumIter(3)
1088 ignoreMaskPlanes = (
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
1089 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(ignoreMaskPlanes))
1091 exposure.getMaskedImage().getMask(),
1092 afwMath.MEANCLIP | afwMath.MEDIAN, statsControl)
1093 mn = statObj.getValue(afwMath.MEANCLIP)
1094 med = statObj.getValue(afwMath.MEDIAN)
1098 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1099 doPreConvolve=False):
1100 """Register, PSF-match, and subtract two Exposures using the ZOGY algorithm. 1102 Do the following, in order: 1103 - Warp templateExposure to match scienceExposure, if their WCSs do not already match 1104 - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures 1108 templateExposure : `lsst.afw.image.Exposure` 1109 exposure to PSF-match to scienceExposure. The exposure's mean value is subtracted 1111 scienceExposure : `lsst.afw.image.Exposure` 1112 reference Exposure. The exposure's mean value is subtracted in-place. 1114 what to do if templateExposure's and scienceExposure's WCSs do not match: 1115 - if True then warp templateExposure to match scienceExposure 1116 - if False then raise an Exception 1117 spatiallyVarying : `bool` 1118 If True, perform the operation over a grid of patches across the two exposures 1119 inImageSpace : `bool` 1120 If True, perform the Zogy convolutions in image space rather than in frequency space. 1121 doPreConvolve : `bool` 1122 ***Currently not implemented.*** If True assume we are to compute the match filter-convolved 1123 exposure which can be thresholded for detection. In the case of Zogy this would mean 1124 we compute the Scorr image. 1128 A `lsst.pipe.base.Struct` containing these fields: 1129 - subtractedExposure: subtracted Exposure 1130 - warpedExposure: templateExposure after warping to match scienceExposure (if doWarping true) 1135 self.log.
info(
"Exposure means=%f, %f; median=%f, %f:" % (mn1[0], mn2[0], mn1[1], mn2[1]))
1136 if not np.isnan(mn1[0])
and np.abs(mn1[0]) > 1:
1137 mi = templateExposure.getMaskedImage()
1139 if not np.isnan(mn2[0])
and np.abs(mn2[0]) > 1:
1140 mi = scienceExposure.getMaskedImage()
1143 self.log.
info(
'Running Zogy algorithm: spatiallyVarying=%r' % spatiallyVarying)
1145 if not self.
_validateWcs(templateExposure, scienceExposure):
1147 self.log.
info(
"Astrometrically registering template to science image")
1150 scienceExposure.getWcs())
1151 psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform)
1154 destBBox=scienceExposure.getBBox())
1156 templateExposure.setPsf(psfWarped)
1158 self.log.
error(
"ERROR: Input images not registered")
1159 raise RuntimeError(
"Input images not registered")
1162 return exp.getMaskedImage().getMask()
1165 return exp.getMaskedImage().getImage().getArray()
1167 if self.config.zogyConfig.inImageSpace:
1169 self.log.
info(
'Running Zogy algorithm: inImageSpace=%r' % inImageSpace)
1170 if spatiallyVarying:
1171 config = self.config.zogyMapReduceConfig
1173 results = task.run(scienceExposure, template=templateExposure, inImageSpace=inImageSpace,
1174 doScorr=doPreConvolve, forceEvenSized=
False)
1175 results.D = results.exposure
1182 config = self.config.zogyConfig
1183 task =
ZogyTask(scienceExposure=scienceExposure, templateExposure=templateExposure,
1185 if not doPreConvolve:
1186 results = task.computeDiffim(inImageSpace=inImageSpace)
1187 results.matchedExposure = results.R
1189 results = task.computeScorr(inImageSpace=inImageSpace)
1190 results.D = results.S
1193 mask = results.D.getMaskedImage().getMask()
1194 mask |= scienceExposure.getMaskedImage().getMask()
1195 mask |= templateExposure.getMaskedImage().getMask()
1196 results.D.getMaskedImage().getMask()[:, :] = mask
1197 badBitsNan = mask.addMaskPlane(
'UNMASKEDNAN')
1198 resultsArr = results.D.getMaskedImage().getMask().getArray()
1199 resultsArr[np.isnan(resultsArr)] |= badBitsNan
1200 resultsArr[np.isnan(scienceExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1201 resultsArr[np.isnan(templateExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1203 results.subtractedExposure = results.D
1204 results.warpedExposure = templateExposure
1208 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1209 doPreConvolve=False):
1210 raise NotImplementedError
1213 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.
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)