1 from __future__
import absolute_import, division, print_function
28 import lsst.meas.algorithms
as measAlg
31 import lsst.pipe.base
as pipeBase
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.Task.__init__` 161 additional keyword arguments to be passed to 162 `lsst.pipe.base.task.Task.__init__` 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.Task.__init__` 199 additional keyword arguments to be passed to 200 `lsst.pipe.base.task.Task.__init__` 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(
afwGeom.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 345 A lsst.pipe.base.Struct containing: 346 - Pr : 2D `numpy.array`, the (possibly zero-padded) template PSF 347 - Pn : 2D `numpy.array`, the (possibly zero-padded) science PSF 348 - Pr_hat : 2D `numpy.array`, the FFT of `Pr` 349 - Pn_hat : 2D `numpy.array`, the FFT of `Pn` 350 - denom : 2D `numpy.array`, the denominator of equation (13) in ZOGY (2016) manuscript 351 - Fd : `float`, the relative flux scaling factor between science and template 353 psf1 = self.
im1_psf if psf1
is None else psf1
354 psf2 = self.
im2_psf if psf2
is None else psf2
355 padSize = self.
padSize if padSize
is None else padSize
358 Pr = ZogyTask._padPsfToSize(psf1, (psf1.shape[0] + padSize, psf1.shape[1] + padSize))
359 Pn = ZogyTask._padPsfToSize(psf2, (psf2.shape[0] + padSize, psf2.shape[1] + padSize))
361 psf1[np.abs(psf1) <= MIN_KERNEL] = MIN_KERNEL
362 psf2[np.abs(psf2) <= MIN_KERNEL] = MIN_KERNEL
365 Pr_hat = np.fft.fft2(Pr)
366 Pr_hat2 = np.conj(Pr_hat) * Pr_hat
367 Pn_hat = np.fft.fft2(Pn)
368 Pn_hat2 = np.conj(Pn_hat) * Pn_hat
369 denom = np.sqrt((sigN**2 * self.
Fr**2 * Pr_hat2) + (sigR**2 * self.
Fn**2 * Pn_hat2))
370 Fd = self.
Fr * self.
Fn / np.sqrt(sigN**2 * self.
Fr**2 + sigR**2 * self.
Fn**2)
372 res = pipeBase.Struct(
373 Pr=Pr, Pn=Pn, Pr_hat=Pr_hat, Pn_hat=Pn_hat, denom=denom, Fd=Fd
379 """Compute ZOGY diffim `D` as proscribed in ZOGY (2016) manuscript 381 Compute the ZOGY eqn. (13): 383 \widehat{D} = \frac{Fr\widehat{Pr}\widehat{N} - 384 F_n\widehat{Pn}\widehat{R}}{\sqrt{\sigma_n^2 Fr^2 385 |\widehat{Pr}|^2 + \sigma_r^2 F_n^2 |\widehat{Pn}|^2}} 387 where $D$ is the optimal difference image, $R$ and $N$ are the 388 reference and "new" image, respectively, $Pr$ and $P_n$ are their 389 PSFs, $Fr$ and $Fn$ are their flux-based zero-points (which we 390 will set to one here), $\sigma_r^2$ and $\sigma_n^2$ are their 391 variance, and $\widehat{D}$ denotes the FT of $D$. 395 A `lsst.pipe.base.Struct` containing: 396 - D : 2D `numpy.array`, the proper image difference 397 - D_var : 2D `numpy.array`, the variance image for `D` 400 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
401 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
405 def _filterKernel(K, trim_amount):
408 K[:ps, :] = K[-ps:, :] = 0
409 K[:, :ps] = K[:, -ps:] = 0
412 Kr_hat = self.
Fr * preqs.Pr_hat / preqs.denom
413 Kn_hat = self.
Fn * preqs.Pn_hat / preqs.denom
414 if debug
and self.config.doTrimKernels:
417 ps = (Kn_hat.shape[1] - 80)//2
418 Kn = _filterKernel(np.fft.ifft2(Kn_hat), ps)
419 Kn_hat = np.fft.fft2(Kn)
420 Kr = _filterKernel(np.fft.ifft2(Kr_hat), ps)
421 Kr_hat = np.fft.fft2(Kr)
423 def processImages(im1, im2, doAdd=False):
425 im1[np.isinf(im1)] = np.nan
426 im1[np.isnan(im1)] = np.nanmean(im1)
427 im2[np.isinf(im2)] = np.nan
428 im2[np.isnan(im2)] = np.nanmean(im2)
430 R_hat = np.fft.fft2(im1)
431 N_hat = np.fft.fft2(im2)
433 D_hat = Kr_hat * N_hat
434 D_hat_R = Kn_hat * R_hat
440 D = np.fft.ifft2(D_hat)
441 D = np.fft.ifftshift(D.real) / preqs.Fd
444 if returnMatchedTemplate:
445 R = np.fft.ifft2(D_hat_R)
446 R = np.fft.ifftshift(R.real) / preqs.Fd
451 D, R = processImages(self.
im1, self.
im2, doAdd=
False)
453 D_var, R_var = processImages(self.
im1_var, self.
im2_var, doAdd=
True)
455 return pipeBase.Struct(D=D, D_var=D_var, R=R, R_var=R_var)
457 def _doConvolve(self, exposure, kernel, recenterKernel=False):
458 """! Convolve an Exposure with a decorrelation convolution kernel. 462 exposure : `lsst.afw.image.Exposure` to be convolved. 463 kernel : 2D `numpy.array` to convolve the image with 467 A new `lsst.afw.image.Exposure` with the convolved pixels and the (possibly 472 - We optionally re-center the kernel if necessary and return the possibly 475 kernelImg = afwImage.ImageD(kernel.shape[1], kernel.shape[0])
476 kernelImg.getArray()[:, :] = kernel
479 maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
480 kern.setCtrX(maxloc[0])
481 kern.setCtrY(maxloc[1])
482 outExp = exposure.clone()
484 maxInterpolationDistance=0)
486 afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl)
487 except AttributeError:
495 """Compute ZOGY diffim `D` using image-space convlutions 497 This method is still being debugged as it results in artifacts 498 when the PSFs are noisy (see module-level docstring). Thus 499 there are several options still enabled by the `debug` flag, 500 which are disabled by defult. 504 padSize : `int`, the amount to pad the PSFs by 505 debug : `bool`, flag to enable debugging tests and options 509 D : `lsst.afw.Exposure` 510 the proper image difference, including correct variance, 518 Kr_hat = (preqs.Pr_hat + delta) / (preqs.denom + delta)
519 Kn_hat = (preqs.Pn_hat + delta) / (preqs.denom + delta)
520 Kr = np.fft.ifft2(Kr_hat).real
521 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
522 Kn = np.fft.ifft2(Kn_hat).real
523 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
525 def _trimKernel(self, K, trim_amount):
529 K = K[ps:-ps, ps:-ps]
532 padSize = self.
padSize if padSize
is None else padSize
534 if debug
and self.config.doTrimKernels:
537 Kn = _trimKernel(Kn, padSize)
538 Kr = _trimKernel(Kr, padSize)
544 tmp = D.getMaskedImage()
545 tmp -= exp1.getMaskedImage()
547 return pipeBase.Struct(D=D, R=exp1)
549 def _setNewPsf(self, exposure, psfArr):
550 """Utility method to set an exposure's PSF when provided as a 2-d numpy.array 552 bbox = exposure.getBBox()
553 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
555 psfI = afwImage.ImageD(psfArr.shape[1], psfArr.shape[0])
556 psfI.getArray()[:, :] = psfArr
558 psfNew = measAlg.KernelPsf(psfK, center)
559 exposure.setPsf(psfNew)
563 returnMatchedTemplate=False, **kwargs):
564 """Wrapper method to compute ZOGY proper diffim 566 This method should be used as the public interface for 567 computing the ZOGY diffim. 571 inImageSpace : `bool` 572 Override config `inImageSpace` parameter 574 Override config `padSize` parameter 575 returnMatchedTemplate : bool 576 Include the PSF-matched template in the results Struct 578 additional keyword arguments to be passed to 579 `computeDiffimFourierSpace` or `computeDiffimImageSpace`. 583 An lsst.pipe.base.Struct containing: 584 - D : `lsst.afw.Exposure` 585 the proper image difference, including correct variance, 587 - R : `lsst.afw.Exposure` 588 If `returnMatchedTemplate` is True, the PSF-matched template 592 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
594 padSize = self.
padSize if padSize
is None else padSize
597 if returnMatchedTemplate:
602 D.getMaskedImage().getImage().getArray()[:, :] = res.D
603 D.getMaskedImage().getVariance().getArray()[:, :] = res.D_var
604 if returnMatchedTemplate:
606 R.getMaskedImage().getImage().getArray()[:, :] = res.R
607 R.getMaskedImage().getVariance().getArray()[:, :] = res.R_var
611 return pipeBase.Struct(D=D, R=R)
614 """Compute the ZOGY diffim PSF (ZOGY manuscript eq. 14) 619 Override config `padSize` parameter 621 Return the FFT of the diffim PSF (do not inverse-FFT it) 622 psf1 : 2D `numpy.array` 623 (Optional) Input psf of template, override if already padded 624 psf2 : 2D `numpy.array` 625 (Optional) Input psf of science image, override if already padded 629 Pd : 2D `numpy.array`, the diffim PSF (or FFT of PSF if `keepFourier=True`) 631 preqs = self.
computePrereqs(psf1=psf1, psf2=psf2, padSize=padSize)
633 Pd_hat_numerator = (self.
Fr * self.
Fn * preqs.Pr_hat * preqs.Pn_hat)
634 Pd_hat = Pd_hat_numerator / (preqs.Fd * preqs.denom)
639 Pd = np.fft.ifft2(Pd_hat)
640 Pd = np.fft.ifftshift(Pd).real
644 def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False,
645 R_hat=None, Kr_hat=None, Kr=None,
646 N_hat=None, Kn_hat=None, Kn=None):
647 """Compute the astrometric noise correction terms 649 Compute the correction for estimated astrometric noise as 650 proscribed in ZOGY (2016), section 3.3. All convolutions 651 performed either in real (image) or Fourier space. 655 xVarAst, yVarAst : `float` 656 estimated astrometric noise (variance of astrometric registration errors) 657 inImageSpace : `bool` 658 Perform all convolutions in real (image) space rather than Fourier space 659 R_hat : 2-D `numpy.array` 660 (Optional) FFT of template image, only required if `inImageSpace=False` 661 Kr_hat : 2-D `numpy.array` 662 FFT of Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=False` 663 Kr : 2-D `numpy.array` 664 Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=True`. 665 Kr is associated with the template (reference). 666 N_hat : 2-D `numpy.array` 667 FFT of science image, only required if `inImageSpace=False` 668 Kn_hat : 2-D `numpy.array` 669 FFT of Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=False` 670 Kn : 2-D `numpy.array` 671 Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=True`. 672 Kn is associated with the science (new) image. 676 VastSR, VastSN : 2-D `numpy.array`s containing the values in eqs. 30 and 32 of 680 if xVarAst + yVarAst > 0:
683 S_R = S_R.getMaskedImage().getImage().getArray()
685 S_R = np.fft.ifft2(R_hat * Kr_hat)
686 gradRx, gradRy = np.gradient(S_R)
687 VastSR = xVarAst * gradRx**2. + yVarAst * gradRy**2.
691 S_N = S_N.getMaskedImage().getImage().getArray()
693 S_N = np.fft.ifft2(N_hat * Kn_hat)
694 gradNx, gradNy = np.gradient(S_N)
695 VastSN = xVarAst * gradNx**2. + yVarAst * gradNy**2.
697 return VastSR, VastSN
700 """Compute corrected likelihood image, optimal for source detection 702 Compute ZOGY S_corr image. This image can be thresholded for 703 detection without optimal filtering, and the variance image is 704 corrected to account for astrometric noise (errors in 705 astrometric registration whether systematic or due to effects 706 such as DCR). The calculations here are all performed in 707 Fourier space, as proscribed in ZOGY (2016). 711 xVarAst, yVarAst : `float` 712 estimated astrometric noise (variance of astrometric registration errors) 716 A lsst.pipe.base.Struct containing: 717 - S : `numpy.array`, the likelihood image S (eq. 12 of ZOGY (2016)) 718 - S_var : the corrected variance image (denominator of eq. 25 of ZOGY (2016)) 719 - Dpsf : the PSF of the diffim D, likely never to be used. 723 """Replace any NaNs or Infs with the mean of the image.""" 724 isbad = ~np.isfinite(im)
727 im[isbad] = np.nanmean(im)
730 self.
im1 = fix_nans(self.
im1)
731 self.
im2 = fix_nans(self.
im2)
736 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
737 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
742 R_hat = np.fft.fft2(self.
im1)
743 N_hat = np.fft.fft2(self.
im2)
744 D_hat = self.
Fr * preqs.Pr_hat * N_hat - self.
Fn * preqs.Pn_hat * R_hat
747 Pd_hat = self.
computeDiffimPsf(padSize=0, keepFourier=
True, psf1=psf1, psf2=psf2)
748 Pd_bar = np.conj(Pd_hat)
749 S = np.fft.ifft2(D_hat * Pd_bar)
753 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
754 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
755 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
756 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
758 Kr_hat2 = np.fft.fft2(np.fft.ifft2(Kr_hat)**2.)
759 Kn_hat2 = np.fft.fft2(np.fft.ifft2(Kn_hat)**2.)
760 var1c_hat = Kr_hat2 * np.fft.fft2(self.
im1_var)
761 var2c_hat = Kn_hat2 * np.fft.fft2(self.
im2_var)
765 R_hat=R_hat, Kr_hat=Kr_hat,
766 N_hat=N_hat, Kn_hat=Kn_hat)
768 S_var = np.sqrt(np.fft.ifftshift(np.fft.ifft2(var1c_hat + var2c_hat)) + fGradR + fGradN)
771 S = np.fft.ifftshift(np.fft.ifft2(Kn_hat * N_hat - Kr_hat * R_hat))
775 return pipeBase.Struct(S=S.real, S_var=S_var.real, Dpsf=Pd)
778 """Compute corrected likelihood image, optimal for source detection 780 Compute ZOGY S_corr image. This image can be thresholded for 781 detection without optimal filtering, and the variance image is 782 corrected to account for astrometric noise (errors in 783 astrometric registration whether systematic or due to effects 784 such as DCR). The calculations here are all performed in 789 xVarAst, yVarAst : `float` 790 estimated astrometric noise (variance of astrometric registration errors) 795 - S : `lsst.afw.image.Exposure`, the likelihood exposure S (eq. 12 of ZOGY (2016)), 796 including corrected variance, masks, and PSF 797 - D : `lsst.afw.image.Exposure`, the proper image difference, including correct 798 variance, masks, and PSF 803 padSize = self.
padSize if padSize
is None else padSize
807 Pd_bar = np.fliplr(np.flipud(Pd))
809 tmp = S.getMaskedImage()
814 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
815 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
816 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
817 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
819 Kr = np.fft.ifft2(Kr_hat).real
820 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
821 Kn = np.fft.ifft2(Kn_hat).real
822 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
830 Smi = S.getMaskedImage()
832 S_var = np.sqrt(var1c.getArray() + var2c.getArray() + fGradR + fGradN)
833 S.getMaskedImage().getVariance().getArray()[:, :] = S_var
837 return pipeBase.Struct(S=S, D=D)
839 def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, **kwargs):
840 """Wrapper method to compute ZOGY corrected likelihood image, optimal for 843 This method should be used as the public interface for 844 computing the ZOGY S_corr. 848 xVarAst, yVarAst : float 849 estimated astrometric noise (variance of astrometric registration errors) 851 Override config `inImageSpace` parameter 853 Override config `padSize` parameter 857 S : lsst.afw.image.Exposure, the likelihood exposure S (eq. 12 of ZOGY (2016)), 858 including corrected variance, masks, and PSF 860 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
868 S.getMaskedImage().getImage().getArray()[:, :] = res.S
869 S.getMaskedImage().getVariance().getArray()[:, :] = res.S_var
872 return pipeBase.Struct(S=S)
876 """Task to be used as an ImageMapper for performing 877 ZOGY image subtraction on a grid of subimages. 879 ConfigClass = ZogyConfig
880 _DefaultName =
'ip_diffim_ZogyMapper' 883 ImageMapper.__init__(self, *args, **kwargs)
885 def run(self, subExposure, expandedSubExposure, fullBBox, template,
887 """Perform ZOGY proper image subtraction on sub-images 889 This method performs ZOGY proper image subtraction on 890 `subExposure` using local measures for image variances and 891 PSF. `subExposure` is a sub-exposure of the science image. It 892 also requires the corresponding sub-exposures of the template 893 (`template`). The operations are actually performed on 894 `expandedSubExposure` to allow for invalid edge pixels arising 895 from convolutions, which are then removed. 899 subExposure : lsst.afw.image.Exposure 900 the sub-exposure of the diffim 901 expandedSubExposure : lsst.afw.image.Exposure 902 the expanded sub-exposure upon which to operate 903 fullBBox : lsst.afw.geom.BoundingBox 904 the bounding box of the original exposure 905 template : lsst.afw.image.Exposure 906 the template exposure, from which a corresponding sub-exposure 909 additional keyword arguments propagated from 910 `ImageMapReduceTask.run`. These include: 912 Compute and return the corrected likelihood image S_corr 913 rather than the proper image difference 914 - inImageSpace : bool 915 Perform all convolutions in real (image) space rather than 916 in Fourier space. This option currently leads to artifacts 917 when using real (measured and noisy) PSFs, thus it is set 918 to `False` by default. 919 These kwargs may also include arguments to be propagated to 920 `ZogyTask.computeDiffim` and `ZogyTask.computeScorr`. 924 A `lsst.pipe.base.Struct` containing the result of the 925 `subExposure` processing, labelled 'subExposure'. In this case 926 it is either the subExposure of the proper image difference D, 927 or (if `doScorr==True`) the corrected likelihood exposure `S`. 931 This `run` method accepts parameters identical to those of 932 `ImageMapper.run`, since it is called from the 933 `ImageMapperTask`. See that class for more information. 935 bbox = subExposure.getBBox()
936 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
939 imageSpace = kwargs.pop(
'inImageSpace',
False)
940 doScorr = kwargs.pop(
'doScorr',
False)
941 sigmas = kwargs.pop(
'sigmas',
None)
942 padSize = kwargs.pop(
'padSize', 7)
945 subExp2 = expandedSubExposure
948 subExp1 = template.Factory(template, expandedSubExposure.getBBox())
954 sig1, sig2 = sigmas[0], sigmas[1]
956 def _makePsfSquare(psf):
958 if psf.shape[0] < psf.shape[1]:
959 psf = np.pad(psf, ((0, psf.shape[1] - psf.shape[0]), (0, 0)), mode=
'constant',
961 elif psf.shape[0] > psf.shape[1]:
962 psf = np.pad(psf, ((0, 0), (0, psf.shape[0] - psf.shape[1])), mode=
'constant',
966 psf2 = subExp2.getPsf().computeKernelImage(center).getArray()
967 psf2 = _makePsfSquare(psf2)
969 psf1 = template.getPsf().computeKernelImage(center).getArray()
970 psf1 = _makePsfSquare(psf1)
973 if subExp1.getDimensions()[0] < psf1.shape[0]
or subExp1.getDimensions()[1] < psf1.shape[1]:
974 return pipeBase.Struct(subExposure=subExposure)
977 """Filter a noisy Psf to remove artifacts. Subject of future research.""" 979 if psf.shape[0] == 41:
982 psf[0:10, :] = psf[:, 0:10] = psf[31:41, :] = psf[:, 31:41] = 0
988 if self.config.doFilterPsfs:
990 psf1b = _filterPsf(psf1)
991 psf2b = _filterPsf(psf2)
994 if imageSpace
is True:
995 config.inImageSpace = imageSpace
996 config.padSize = padSize
997 task =
ZogyTask(templateExposure=subExp1, scienceExposure=subExp2,
998 sig1=sig1, sig2=sig2, psf1=psf1b, psf2=psf2b, config=config)
1001 res = task.computeDiffim(**kwargs)
1004 res = task.computeScorr(**kwargs)
1007 outExp = D.Factory(D, subExposure.getBBox())
1008 out = pipeBase.Struct(subExposure=outExp)
1013 """Config to be passed to ImageMapReduceTask 1015 This config targets the imageMapper to use the ZogyMapper. 1017 mapper = pexConfig.ConfigurableField(
1018 doc=
'Zogy task to run on each sub-image',
1024 """Config for the ZogyImagePsfMatchTask""" 1026 zogyConfig = pexConfig.ConfigField(
1028 doc=
'ZogyTask config to use when running on complete exposure (non spatially-varying)',
1031 zogyMapReduceConfig = pexConfig.ConfigField(
1032 dtype=ZogyMapReduceConfig,
1033 doc=
'ZogyMapReduce config to use when running Zogy on each sub-image (spatially-varying)',
1045 """Task to perform Zogy PSF matching and image subtraction. 1047 This class inherits from ImagePsfMatchTask to contain the _warper 1048 subtask and related methods. 1051 ConfigClass = ZogyImagePsfMatchConfig
1054 ImagePsfMatchTask.__init__(self, *args, **kwargs)
1056 def _computeImageMean(self, exposure):
1057 """Compute the sigma-clipped mean of the pixels image of `exposure`. 1060 statsControl.setNumSigmaClip(3.)
1061 statsControl.setNumIter(3)
1062 ignoreMaskPlanes = (
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
1063 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(ignoreMaskPlanes))
1065 exposure.getMaskedImage().getMask(),
1066 afwMath.MEANCLIP | afwMath.MEDIAN, statsControl)
1067 mn = statObj.getValue(afwMath.MEANCLIP)
1068 med = statObj.getValue(afwMath.MEDIAN)
1072 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1073 doPreConvolve=False):
1074 """Register, PSF-match, and subtract two Exposures using the ZOGY algorithm. 1076 Do the following, in order: 1077 - Warp templateExposure to match scienceExposure, if their WCSs do not already match 1078 - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures 1082 templateExposure : `lsst.afw.image.Exposure` 1083 exposure to PSF-match to scienceExposure. The exposure's mean value is subtracted 1085 scienceExposure : `lsst.afw.image.Exposure` 1086 reference Exposure. The exposure's mean value is subtracted in-place. 1088 what to do if templateExposure's and scienceExposure's WCSs do not match: 1089 - if True then warp templateExposure to match scienceExposure 1090 - if False then raise an Exception 1091 spatiallyVarying : bool 1092 If True, perform the operation over a grid of patches across the two exposures 1093 inImageSpace : `bool` 1094 If True, perform the Zogy convolutions in image space rather than in frequency space. 1095 doPreConvolve : `bool` 1096 ***Currently not implemented.*** If True assume we are to compute the match filter-convolved 1097 exposure which can be thresholded for detection. In the case of Zogy this would mean 1098 we compute the Scorr image. 1102 A `lsst.pipe.base.Struct` containing these fields: 1103 - subtractedExposure: subtracted Exposure 1104 - warpedExposure: templateExposure after warping to match scienceExposure (if doWarping true) 1109 self.log.info(
"Exposure means=%f, %f; median=%f, %f:" % (mn1[0], mn2[0], mn1[1], mn2[1]))
1110 if not np.isnan(mn1[0])
and np.abs(mn1[0]) > 1:
1111 mi = templateExposure.getMaskedImage()
1113 if not np.isnan(mn2[0])
and np.abs(mn2[0]) > 1:
1114 mi = scienceExposure.getMaskedImage()
1117 self.log.info(
'Running Zogy algorithm: spatiallyVarying=%r' % spatiallyVarying)
1119 if not self.
_validateWcs(templateExposure, scienceExposure):
1121 self.log.info(
"Astrometrically registering template to science image")
1124 scienceExposure.getWcs())
1125 psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform)
1126 templateExposure = self.
_warper.warpExposure(scienceExposure.getWcs(),
1128 destBBox=scienceExposure.getBBox())
1130 templateExposure.setPsf(psfWarped)
1132 self.log.error(
"ERROR: Input images not registered")
1133 raise RuntimeError(
"Input images not registered")
1136 return exp.getMaskedImage().getMask()
1139 return exp.getMaskedImage().getImage().getArray()
1141 if self.config.zogyConfig.inImageSpace:
1143 self.log.info(
'Running Zogy algorithm: inImageSpace=%r' % inImageSpace)
1144 if spatiallyVarying:
1145 config = self.config.zogyMapReduceConfig
1147 results = task.run(scienceExposure, template=templateExposure, inImageSpace=inImageSpace,
1148 doScorr=doPreConvolve, forceEvenSized=
False)
1149 results.D = results.exposure
1156 config = self.config.zogyConfig
1157 task =
ZogyTask(scienceExposure=scienceExposure, templateExposure=templateExposure,
1159 if not doPreConvolve:
1160 results = task.computeDiffim(inImageSpace=inImageSpace)
1161 results.matchedExposure = results.R
1163 results = task.computeScorr(inImageSpace=inImageSpace)
1164 results.D = results.S
1167 mask = results.D.getMaskedImage().getMask()
1168 mask |= scienceExposure.getMaskedImage().getMask()
1169 mask |= templateExposure.getMaskedImage().getMask()
1170 results.D.getMaskedImage().getMask()[:, :] = mask
1171 badBitsNan = mask.addMaskPlane(
'UNMASKEDNAN')
1172 resultsArr = results.D.getMaskedImage().getMask().getArray()
1173 resultsArr[np.isnan(resultsArr)] |= badBitsNan
1174 resultsArr[np.isnan(scienceExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1175 resultsArr[np.isnan(templateExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1177 results.subtractedExposure = results.D
1178 results.warpedExposure = templateExposure
1182 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1183 doPreConvolve=False):
1184 raise NotImplementedError
1187 subtractAlgorithmRegistry.register(
'zogy', ZogyImagePsfMatchTask)
def _computeImageMean(self, exposure)
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
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)
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)