27 import lsst.meas.algorithms
as measAlg
30 import lsst.pipe.base
as pipeBase
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.Task.__init__` 160 additional keyword arguments to be passed to 161 `lsst.pipe.base.task.Task.__init__` 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.Task.__init__` 198 additional keyword arguments to be passed to 199 `lsst.pipe.base.task.Task.__init__` 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 344 A lsst.pipe.base.Struct containing: 345 - Pr : 2D `numpy.array`, the (possibly zero-padded) template PSF 346 - Pn : 2D `numpy.array`, the (possibly zero-padded) science PSF 347 - Pr_hat : 2D `numpy.array`, the FFT of `Pr` 348 - Pn_hat : 2D `numpy.array`, the FFT of `Pn` 349 - denom : 2D `numpy.array`, the denominator of equation (13) in ZOGY (2016) manuscript 350 - Fd : `float`, the relative flux scaling factor between science and template 352 psf1 = self.
im1_psf if psf1
is None else psf1
353 psf2 = self.
im2_psf if psf2
is None else psf2
354 padSize = self.
padSize if padSize
is None else padSize
357 Pr = ZogyTask._padPsfToSize(psf1, (psf1.shape[0] + padSize, psf1.shape[1] + padSize))
358 Pn = ZogyTask._padPsfToSize(psf2, (psf2.shape[0] + padSize, psf2.shape[1] + padSize))
360 psf1[np.abs(psf1) <= MIN_KERNEL] = MIN_KERNEL
361 psf2[np.abs(psf2) <= MIN_KERNEL] = MIN_KERNEL
364 Pr_hat = np.fft.fft2(Pr)
365 Pr_hat2 = np.conj(Pr_hat) * Pr_hat
366 Pn_hat = np.fft.fft2(Pn)
367 Pn_hat2 = np.conj(Pn_hat) * Pn_hat
368 denom = np.sqrt((sigN**2 * self.
Fr**2 * Pr_hat2) + (sigR**2 * self.
Fn**2 * Pn_hat2))
369 Fd = self.
Fr * self.
Fn / np.sqrt(sigN**2 * self.
Fr**2 + sigR**2 * self.
Fn**2)
371 res = pipeBase.Struct(
372 Pr=Pr, Pn=Pn, Pr_hat=Pr_hat, Pn_hat=Pn_hat, denom=denom, Fd=Fd
378 """Compute ZOGY diffim `D` as proscribed in ZOGY (2016) manuscript 380 Compute the ZOGY eqn. (13): 382 \widehat{D} = \frac{Fr\widehat{Pr}\widehat{N} - 383 F_n\widehat{Pn}\widehat{R}}{\sqrt{\sigma_n^2 Fr^2 384 |\widehat{Pr}|^2 + \sigma_r^2 F_n^2 |\widehat{Pn}|^2}} 386 where $D$ is the optimal difference image, $R$ and $N$ are the 387 reference and "new" image, respectively, $Pr$ and $P_n$ are their 388 PSFs, $Fr$ and $Fn$ are their flux-based zero-points (which we 389 will set to one here), $\sigma_r^2$ and $\sigma_n^2$ are their 390 variance, and $\widehat{D}$ denotes the FT of $D$. 394 A `lsst.pipe.base.Struct` containing: 395 - D : 2D `numpy.array`, the proper image difference 396 - D_var : 2D `numpy.array`, the variance image for `D` 399 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
400 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
404 def _filterKernel(K, trim_amount):
407 K[:ps, :] = K[-ps:, :] = 0
408 K[:, :ps] = K[:, -ps:] = 0
411 Kr_hat = self.
Fr * preqs.Pr_hat / preqs.denom
412 Kn_hat = self.
Fn * preqs.Pn_hat / preqs.denom
413 if debug
and self.config.doTrimKernels:
416 ps = (Kn_hat.shape[1] - 80)//2
417 Kn = _filterKernel(np.fft.ifft2(Kn_hat), ps)
418 Kn_hat = np.fft.fft2(Kn)
419 Kr = _filterKernel(np.fft.ifft2(Kr_hat), ps)
420 Kr_hat = np.fft.fft2(Kr)
422 def processImages(im1, im2, doAdd=False):
424 im1[np.isinf(im1)] = np.nan
425 im1[np.isnan(im1)] = np.nanmean(im1)
426 im2[np.isinf(im2)] = np.nan
427 im2[np.isnan(im2)] = np.nanmean(im2)
429 R_hat = np.fft.fft2(im1)
430 N_hat = np.fft.fft2(im2)
432 D_hat = Kr_hat * N_hat
433 D_hat_R = Kn_hat * R_hat
439 D = np.fft.ifft2(D_hat)
440 D = np.fft.ifftshift(D.real) / preqs.Fd
443 if returnMatchedTemplate:
444 R = np.fft.ifft2(D_hat_R)
445 R = np.fft.ifftshift(R.real) / preqs.Fd
450 D, R = processImages(self.
im1, self.
im2, doAdd=
False)
452 D_var, R_var = processImages(self.
im1_var, self.
im2_var, doAdd=
True)
454 return pipeBase.Struct(D=D, D_var=D_var, R=R, R_var=R_var)
456 def _doConvolve(self, exposure, kernel, recenterKernel=False):
457 """! Convolve an Exposure with a decorrelation convolution kernel. 461 exposure : `lsst.afw.image.Exposure` to be convolved. 462 kernel : 2D `numpy.array` to convolve the image with 466 A new `lsst.afw.image.Exposure` with the convolved pixels and the (possibly 471 - We optionally re-center the kernel if necessary and return the possibly 474 kernelImg = afwImage.ImageD(kernel.shape[1], kernel.shape[0])
475 kernelImg.getArray()[:, :] = kernel
478 maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
479 kern.setCtrX(maxloc[0])
480 kern.setCtrY(maxloc[1])
481 outExp = exposure.clone()
483 maxInterpolationDistance=0)
485 afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl)
486 except AttributeError:
494 """Compute ZOGY diffim `D` using image-space convlutions 496 This method is still being debugged as it results in artifacts 497 when the PSFs are noisy (see module-level docstring). Thus 498 there are several options still enabled by the `debug` flag, 499 which are disabled by defult. 503 padSize : `int`, the amount to pad the PSFs by 504 debug : `bool`, flag to enable debugging tests and options 508 D : `lsst.afw.Exposure` 509 the proper image difference, including correct variance, 517 Kr_hat = (preqs.Pr_hat + delta) / (preqs.denom + delta)
518 Kn_hat = (preqs.Pn_hat + delta) / (preqs.denom + delta)
519 Kr = np.fft.ifft2(Kr_hat).real
520 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
521 Kn = np.fft.ifft2(Kn_hat).real
522 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
524 def _trimKernel(self, K, trim_amount):
528 K = K[ps:-ps, ps:-ps]
531 padSize = self.
padSize if padSize
is None else padSize
533 if debug
and self.config.doTrimKernels:
536 Kn = _trimKernel(Kn, padSize)
537 Kr = _trimKernel(Kr, padSize)
543 tmp = D.getMaskedImage()
544 tmp -= exp1.getMaskedImage()
546 return pipeBase.Struct(D=D, R=exp1)
548 def _setNewPsf(self, exposure, psfArr):
549 """Utility method to set an exposure's PSF when provided as a 2-d numpy.array 551 bbox = exposure.getBBox()
552 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
554 psfI = afwImage.ImageD(psfArr.shape[1], psfArr.shape[0])
555 psfI.getArray()[:, :] = psfArr
557 psfNew = measAlg.KernelPsf(psfK, center)
558 exposure.setPsf(psfNew)
562 returnMatchedTemplate=False, **kwargs):
563 """Wrapper method to compute ZOGY proper diffim 565 This method should be used as the public interface for 566 computing the ZOGY diffim. 570 inImageSpace : `bool` 571 Override config `inImageSpace` parameter 573 Override config `padSize` parameter 574 returnMatchedTemplate : bool 575 Include the PSF-matched template in the results Struct 577 additional keyword arguments to be passed to 578 `computeDiffimFourierSpace` or `computeDiffimImageSpace`. 582 An lsst.pipe.base.Struct containing: 583 - D : `lsst.afw.Exposure` 584 the proper image difference, including correct variance, 586 - R : `lsst.afw.Exposure` 587 If `returnMatchedTemplate` is True, the PSF-matched template 591 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
593 padSize = self.
padSize if padSize
is None else padSize
596 if returnMatchedTemplate:
601 D.getMaskedImage().getImage().getArray()[:, :] = res.D
602 D.getMaskedImage().getVariance().getArray()[:, :] = res.D_var
603 if returnMatchedTemplate:
605 R.getMaskedImage().getImage().getArray()[:, :] = res.R
606 R.getMaskedImage().getVariance().getArray()[:, :] = res.R_var
610 return pipeBase.Struct(D=D, R=R)
613 """Compute the ZOGY diffim PSF (ZOGY manuscript eq. 14) 618 Override config `padSize` parameter 620 Return the FFT of the diffim PSF (do not inverse-FFT it) 621 psf1 : 2D `numpy.array` 622 (Optional) Input psf of template, override if already padded 623 psf2 : 2D `numpy.array` 624 (Optional) Input psf of science image, override if already padded 628 Pd : 2D `numpy.array`, the diffim PSF (or FFT of PSF if `keepFourier=True`) 630 preqs = self.
computePrereqs(psf1=psf1, psf2=psf2, padSize=padSize)
632 Pd_hat_numerator = (self.
Fr * self.
Fn * preqs.Pr_hat * preqs.Pn_hat)
633 Pd_hat = Pd_hat_numerator / (preqs.Fd * preqs.denom)
638 Pd = np.fft.ifft2(Pd_hat)
639 Pd = np.fft.ifftshift(Pd).real
643 def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False,
644 R_hat=None, Kr_hat=None, Kr=None,
645 N_hat=None, Kn_hat=None, Kn=None):
646 """Compute the astrometric noise correction terms 648 Compute the correction for estimated astrometric noise as 649 proscribed in ZOGY (2016), section 3.3. All convolutions 650 performed either in real (image) or Fourier space. 654 xVarAst, yVarAst : `float` 655 estimated astrometric noise (variance of astrometric registration errors) 656 inImageSpace : `bool` 657 Perform all convolutions in real (image) space rather than Fourier space 658 R_hat : 2-D `numpy.array` 659 (Optional) FFT of template image, only required if `inImageSpace=False` 660 Kr_hat : 2-D `numpy.array` 661 FFT of Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=False` 662 Kr : 2-D `numpy.array` 663 Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=True`. 664 Kr is associated with the template (reference). 665 N_hat : 2-D `numpy.array` 666 FFT of science image, only required if `inImageSpace=False` 667 Kn_hat : 2-D `numpy.array` 668 FFT of Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=False` 669 Kn : 2-D `numpy.array` 670 Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=True`. 671 Kn is associated with the science (new) image. 675 VastSR, VastSN : 2-D `numpy.array`s containing the values in eqs. 30 and 32 of 679 if xVarAst + yVarAst > 0:
682 S_R = S_R.getMaskedImage().getImage().getArray()
684 S_R = np.fft.ifft2(R_hat * Kr_hat)
685 gradRx, gradRy = np.gradient(S_R)
686 VastSR = xVarAst * gradRx**2. + yVarAst * gradRy**2.
690 S_N = S_N.getMaskedImage().getImage().getArray()
692 S_N = np.fft.ifft2(N_hat * Kn_hat)
693 gradNx, gradNy = np.gradient(S_N)
694 VastSN = xVarAst * gradNx**2. + yVarAst * gradNy**2.
696 return VastSR, VastSN
699 """Compute corrected likelihood image, optimal for source detection 701 Compute ZOGY S_corr image. This image can be thresholded for 702 detection without optimal filtering, and the variance image is 703 corrected to account for astrometric noise (errors in 704 astrometric registration whether systematic or due to effects 705 such as DCR). The calculations here are all performed in 706 Fourier space, as proscribed in ZOGY (2016). 710 xVarAst, yVarAst : `float` 711 estimated astrometric noise (variance of astrometric registration errors) 715 A lsst.pipe.base.Struct containing: 716 - S : `numpy.array`, the likelihood image S (eq. 12 of ZOGY (2016)) 717 - S_var : the corrected variance image (denominator of eq. 25 of ZOGY (2016)) 718 - Dpsf : the PSF of the diffim D, likely never to be used. 722 """Replace any NaNs or Infs with the mean of the image.""" 723 isbad = ~np.isfinite(im)
726 im[isbad] = np.nanmean(im)
729 self.
im1 = fix_nans(self.
im1)
730 self.
im2 = fix_nans(self.
im2)
735 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
736 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
741 R_hat = np.fft.fft2(self.
im1)
742 N_hat = np.fft.fft2(self.
im2)
743 D_hat = self.
Fr * preqs.Pr_hat * N_hat - self.
Fn * preqs.Pn_hat * R_hat
746 Pd_hat = self.
computeDiffimPsf(padSize=0, keepFourier=
True, psf1=psf1, psf2=psf2)
747 Pd_bar = np.conj(Pd_hat)
748 S = np.fft.ifft2(D_hat * Pd_bar)
752 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
753 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
754 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
755 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
757 Kr_hat2 = np.fft.fft2(np.fft.ifft2(Kr_hat)**2.)
758 Kn_hat2 = np.fft.fft2(np.fft.ifft2(Kn_hat)**2.)
759 var1c_hat = Kr_hat2 * np.fft.fft2(self.
im1_var)
760 var2c_hat = Kn_hat2 * np.fft.fft2(self.
im2_var)
764 R_hat=R_hat, Kr_hat=Kr_hat,
765 N_hat=N_hat, Kn_hat=Kn_hat)
767 S_var = np.sqrt(np.fft.ifftshift(np.fft.ifft2(var1c_hat + var2c_hat)) + fGradR + fGradN)
770 S = np.fft.ifftshift(np.fft.ifft2(Kn_hat * N_hat - Kr_hat * R_hat))
774 return pipeBase.Struct(S=S.real, S_var=S_var.real, Dpsf=Pd)
777 """Compute corrected likelihood image, optimal for source detection 779 Compute ZOGY S_corr image. This image can be thresholded for 780 detection without optimal filtering, and the variance image is 781 corrected to account for astrometric noise (errors in 782 astrometric registration whether systematic or due to effects 783 such as DCR). The calculations here are all performed in 788 xVarAst, yVarAst : `float` 789 estimated astrometric noise (variance of astrometric registration errors) 794 - S : `lsst.afw.image.Exposure`, the likelihood exposure S (eq. 12 of ZOGY (2016)), 795 including corrected variance, masks, and PSF 796 - D : `lsst.afw.image.Exposure`, the proper image difference, including correct 797 variance, masks, and PSF 802 padSize = self.
padSize if padSize
is None else padSize
806 Pd_bar = np.fliplr(np.flipud(Pd))
808 tmp = S.getMaskedImage()
813 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
814 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
815 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
816 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
818 Kr = np.fft.ifft2(Kr_hat).real
819 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
820 Kn = np.fft.ifft2(Kn_hat).real
821 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
829 Smi = S.getMaskedImage()
831 S_var = np.sqrt(var1c.getArray() + var2c.getArray() + fGradR + fGradN)
832 S.getMaskedImage().getVariance().getArray()[:, :] = S_var
836 return pipeBase.Struct(S=S, D=D)
838 def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, **kwargs):
839 """Wrapper method to compute ZOGY corrected likelihood image, optimal for 842 This method should be used as the public interface for 843 computing the ZOGY S_corr. 847 xVarAst, yVarAst : float 848 estimated astrometric noise (variance of astrometric registration errors) 850 Override config `inImageSpace` parameter 852 Override config `padSize` parameter 856 S : lsst.afw.image.Exposure, the likelihood exposure S (eq. 12 of ZOGY (2016)), 857 including corrected variance, masks, and PSF 859 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
867 S.getMaskedImage().getImage().getArray()[:, :] = res.S
868 S.getMaskedImage().getVariance().getArray()[:, :] = res.S_var
871 return pipeBase.Struct(S=S)
875 """Task to be used as an ImageMapper for performing 876 ZOGY image subtraction on a grid of subimages. 878 ConfigClass = ZogyConfig
879 _DefaultName =
'ip_diffim_ZogyMapper' 882 ImageMapper.__init__(self, *args, **kwargs)
884 def run(self, subExposure, expandedSubExposure, fullBBox, template,
886 """Perform ZOGY proper image subtraction on sub-images 888 This method performs ZOGY proper image subtraction on 889 `subExposure` using local measures for image variances and 890 PSF. `subExposure` is a sub-exposure of the science image. It 891 also requires the corresponding sub-exposures of the template 892 (`template`). The operations are actually performed on 893 `expandedSubExposure` to allow for invalid edge pixels arising 894 from convolutions, which are then removed. 898 subExposure : lsst.afw.image.Exposure 899 the sub-exposure of the diffim 900 expandedSubExposure : lsst.afw.image.Exposure 901 the expanded sub-exposure upon which to operate 902 fullBBox : lsst.afw.geom.BoundingBox 903 the bounding box of the original exposure 904 template : lsst.afw.image.Exposure 905 the template exposure, from which a corresponding sub-exposure 908 additional keyword arguments propagated from 909 `ImageMapReduceTask.run`. These include: 911 Compute and return the corrected likelihood image S_corr 912 rather than the proper image difference 913 - inImageSpace : bool 914 Perform all convolutions in real (image) space rather than 915 in Fourier space. This option currently leads to artifacts 916 when using real (measured and noisy) PSFs, thus it is set 917 to `False` by default. 918 These kwargs may also include arguments to be propagated to 919 `ZogyTask.computeDiffim` and `ZogyTask.computeScorr`. 923 A `lsst.pipe.base.Struct` containing the result of the 924 `subExposure` processing, labelled 'subExposure'. In this case 925 it is either the subExposure of the proper image difference D, 926 or (if `doScorr==True`) the corrected likelihood exposure `S`. 930 This `run` method accepts parameters identical to those of 931 `ImageMapper.run`, since it is called from the 932 `ImageMapperTask`. See that class for more information. 934 bbox = subExposure.getBBox()
935 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
938 imageSpace = kwargs.pop(
'inImageSpace',
False)
939 doScorr = kwargs.pop(
'doScorr',
False)
940 sigmas = kwargs.pop(
'sigmas',
None)
941 padSize = kwargs.pop(
'padSize', 7)
944 subExp2 = expandedSubExposure
947 subExp1 = template.Factory(template, expandedSubExposure.getBBox())
953 sig1, sig2 = sigmas[0], sigmas[1]
955 def _makePsfSquare(psf):
957 if psf.shape[0] < psf.shape[1]:
958 psf = np.pad(psf, ((0, psf.shape[1] - psf.shape[0]), (0, 0)), mode=
'constant',
960 elif psf.shape[0] > psf.shape[1]:
961 psf = np.pad(psf, ((0, 0), (0, psf.shape[0] - psf.shape[1])), mode=
'constant',
965 psf2 = subExp2.getPsf().computeKernelImage(center).getArray()
966 psf2 = _makePsfSquare(psf2)
968 psf1 = template.getPsf().computeKernelImage(center).getArray()
969 psf1 = _makePsfSquare(psf1)
972 if subExp1.getDimensions()[0] < psf1.shape[0]
or subExp1.getDimensions()[1] < psf1.shape[1]:
973 return pipeBase.Struct(subExposure=subExposure)
976 """Filter a noisy Psf to remove artifacts. Subject of future research.""" 978 if psf.shape[0] == 41:
981 psf[0:10, :] = psf[:, 0:10] = psf[31:41, :] = psf[:, 31:41] = 0
987 if self.config.doFilterPsfs:
989 psf1b = _filterPsf(psf1)
990 psf2b = _filterPsf(psf2)
993 if imageSpace
is True:
994 config.inImageSpace = imageSpace
995 config.padSize = padSize
996 task =
ZogyTask(templateExposure=subExp1, scienceExposure=subExp2,
997 sig1=sig1, sig2=sig2, psf1=psf1b, psf2=psf2b, config=config)
1000 res = task.computeDiffim(**kwargs)
1003 res = task.computeScorr(**kwargs)
1006 outExp = D.Factory(D, subExposure.getBBox())
1007 out = pipeBase.Struct(subExposure=outExp)
1012 """Config to be passed to ImageMapReduceTask 1014 This config targets the imageMapper to use the ZogyMapper. 1016 mapper = pexConfig.ConfigurableField(
1017 doc=
'Zogy task to run on each sub-image',
1023 """Config for the ZogyImagePsfMatchTask""" 1025 zogyConfig = pexConfig.ConfigField(
1027 doc=
'ZogyTask config to use when running on complete exposure (non spatially-varying)',
1030 zogyMapReduceConfig = pexConfig.ConfigField(
1031 dtype=ZogyMapReduceConfig,
1032 doc=
'ZogyMapReduce config to use when running Zogy on each sub-image (spatially-varying)',
1044 """Task to perform Zogy PSF matching and image subtraction. 1046 This class inherits from ImagePsfMatchTask to contain the _warper 1047 subtask and related methods. 1050 ConfigClass = ZogyImagePsfMatchConfig
1053 ImagePsfMatchTask.__init__(self, *args, **kwargs)
1055 def _computeImageMean(self, exposure):
1056 """Compute the sigma-clipped mean of the pixels image of `exposure`. 1059 statsControl.setNumSigmaClip(3.)
1060 statsControl.setNumIter(3)
1061 ignoreMaskPlanes = (
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
1062 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(ignoreMaskPlanes))
1064 exposure.getMaskedImage().getMask(),
1065 afwMath.MEANCLIP | afwMath.MEDIAN, statsControl)
1066 mn = statObj.getValue(afwMath.MEANCLIP)
1067 med = statObj.getValue(afwMath.MEDIAN)
1071 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1072 doPreConvolve=False):
1073 """Register, PSF-match, and subtract two Exposures using the ZOGY algorithm. 1075 Do the following, in order: 1076 - Warp templateExposure to match scienceExposure, if their WCSs do not already match 1077 - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures 1081 templateExposure : `lsst.afw.image.Exposure` 1082 exposure to PSF-match to scienceExposure. The exposure's mean value is subtracted 1084 scienceExposure : `lsst.afw.image.Exposure` 1085 reference Exposure. The exposure's mean value is subtracted in-place. 1087 what to do if templateExposure's and scienceExposure's WCSs do not match: 1088 - if True then warp templateExposure to match scienceExposure 1089 - if False then raise an Exception 1090 spatiallyVarying : bool 1091 If True, perform the operation over a grid of patches across the two exposures 1092 inImageSpace : `bool` 1093 If True, perform the Zogy convolutions in image space rather than in frequency space. 1094 doPreConvolve : `bool` 1095 ***Currently not implemented.*** If True assume we are to compute the match filter-convolved 1096 exposure which can be thresholded for detection. In the case of Zogy this would mean 1097 we compute the Scorr image. 1101 A `lsst.pipe.base.Struct` containing these fields: 1102 - subtractedExposure: subtracted Exposure 1103 - warpedExposure: templateExposure after warping to match scienceExposure (if doWarping true) 1108 self.log.info(
"Exposure means=%f, %f; median=%f, %f:" % (mn1[0], mn2[0], mn1[1], mn2[1]))
1109 if not np.isnan(mn1[0])
and np.abs(mn1[0]) > 1:
1110 mi = templateExposure.getMaskedImage()
1112 if not np.isnan(mn2[0])
and np.abs(mn2[0]) > 1:
1113 mi = scienceExposure.getMaskedImage()
1116 self.log.info(
'Running Zogy algorithm: spatiallyVarying=%r' % spatiallyVarying)
1118 if not self.
_validateWcs(templateExposure, scienceExposure):
1120 self.log.info(
"Astrometrically registering template to science image")
1123 scienceExposure.getWcs())
1124 psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform)
1125 templateExposure = self.
_warper.warpExposure(scienceExposure.getWcs(),
1127 destBBox=scienceExposure.getBBox())
1129 templateExposure.setPsf(psfWarped)
1131 self.log.error(
"ERROR: Input images not registered")
1132 raise RuntimeError(
"Input images not registered")
1135 return exp.getMaskedImage().getMask()
1138 return exp.getMaskedImage().getImage().getArray()
1140 if self.config.zogyConfig.inImageSpace:
1142 self.log.info(
'Running Zogy algorithm: inImageSpace=%r' % inImageSpace)
1143 if spatiallyVarying:
1144 config = self.config.zogyMapReduceConfig
1146 results = task.run(scienceExposure, template=templateExposure, inImageSpace=inImageSpace,
1147 doScorr=doPreConvolve, forceEvenSized=
False)
1148 results.D = results.exposure
1155 config = self.config.zogyConfig
1156 task =
ZogyTask(scienceExposure=scienceExposure, templateExposure=templateExposure,
1158 if not doPreConvolve:
1159 results = task.computeDiffim(inImageSpace=inImageSpace)
1160 results.matchedExposure = results.R
1162 results = task.computeScorr(inImageSpace=inImageSpace)
1163 results.D = results.S
1166 mask = results.D.getMaskedImage().getMask()
1167 mask |= scienceExposure.getMaskedImage().getMask()
1168 mask |= templateExposure.getMaskedImage().getMask()
1169 results.D.getMaskedImage().getMask()[:, :] = mask
1170 badBitsNan = mask.addMaskPlane(
'UNMASKEDNAN')
1171 resultsArr = results.D.getMaskedImage().getMask().getArray()
1172 resultsArr[np.isnan(resultsArr)] |= badBitsNan
1173 resultsArr[np.isnan(scienceExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1174 resultsArr[np.isnan(templateExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1176 results.subtractedExposure = results.D
1177 results.warpedExposure = templateExposure
1181 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1182 doPreConvolve=False):
1183 raise NotImplementedError
1186 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)
Configuration for image-to-image Psf matching.
def computeDiffim(self, inImageSpace=None, padSize=None, returnMatchedTemplate=False, kwargs)
def computeDiffimFourierSpace(self, debug=False, returnMatchedTemplate=False, kwargs)
def __init__(self, args, kwargs)
Psf-match two MaskedImages or Exposures using the sources in the images.
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())
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)
def _computeVarianceMean(self, exposure)
def computeDiffimPsf(self, padSize=0, keepFourier=False, psf1=None, psf2=None)
def _doConvolve(self, exposure, kernel, recenterKernel=False)
Convolve an Exposure with a decorrelation convolution kernel.
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)
def _validateWcs(self, templateExposure, scienceExposure)
Return True if the WCS of the two Exposures have the same origin and extent.
def computeScorrImageSpace(self, xVarAst=0., yVarAst=0., padSize=None, kwargs)
def __init__(self, args, kwargs)