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): 396 \widehat{D} = \frac{Fr\widehat{Pr}\widehat{N} - 397 F_n\widehat{Pn}\widehat{R}}{\sqrt{\sigma_n^2 Fr^2 398 \|\widehat{Pr}\|^2 + \sigma_r^2 F_n^2 \|\widehat{Pn}\|^2}} 400 where :math:`D` is the optimal difference image, :math:`R` and :math:`N` are the 401 reference and "new" image, respectively, :math:`Pr` and :math:`P_n` are their 402 PSFs, :math:`Fr` and :math:`Fn` are their flux-based zero-points (which we 403 will set to one here), :math:`\sigma_r^2` and :math:`\sigma_n^2` are their 404 variance, and :math:`\widehat{D}` denotes the FT of :math:`D`. 408 result : `lsst.pipe.base.Struct` 409 Result struct with components: 411 - ``D`` : 2D `numpy.array`, the proper image difference 412 - ``D_var`` : 2D `numpy.array`, the variance image for `D` 415 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
416 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
420 def _filterKernel(K, trim_amount):
423 K[:ps, :] = K[-ps:, :] = 0
424 K[:, :ps] = K[:, -ps:] = 0
427 Kr_hat = self.
Fr * preqs.Pr_hat / preqs.denom
428 Kn_hat = self.
Fn * preqs.Pn_hat / preqs.denom
429 if debug
and self.config.doTrimKernels:
432 ps = (Kn_hat.shape[1] - 80)//2
433 Kn = _filterKernel(np.fft.ifft2(Kn_hat), ps)
434 Kn_hat = np.fft.fft2(Kn)
435 Kr = _filterKernel(np.fft.ifft2(Kr_hat), ps)
436 Kr_hat = np.fft.fft2(Kr)
438 def processImages(im1, im2, doAdd=False):
440 im1[np.isinf(im1)] = np.nan
441 im1[np.isnan(im1)] = np.nanmean(im1)
442 im2[np.isinf(im2)] = np.nan
443 im2[np.isnan(im2)] = np.nanmean(im2)
445 R_hat = np.fft.fft2(im1)
446 N_hat = np.fft.fft2(im2)
448 D_hat = Kr_hat * N_hat
449 D_hat_R = Kn_hat * R_hat
455 D = np.fft.ifft2(D_hat)
456 D = np.fft.ifftshift(D.real) / preqs.Fd
459 if returnMatchedTemplate:
460 R = np.fft.ifft2(D_hat_R)
461 R = np.fft.ifftshift(R.real) / preqs.Fd
466 D, R = processImages(self.
im1, self.
im2, doAdd=
False)
468 D_var, R_var = processImages(self.
im1_var, self.
im2_var, doAdd=
True)
470 return pipeBase.Struct(D=D, D_var=D_var, R=R, R_var=R_var)
472 def _doConvolve(self, exposure, kernel, recenterKernel=False):
473 """Convolve an Exposure with a decorrelation convolution kernel. 477 exposure : `lsst.afw.image.Exposure` 478 Input exposure to be convolved. 479 kernel : `numpy.array` 480 2D `numpy.array` to convolve the image with 481 recenterKernel : `bool`, optional 482 Force the kernel center to the pixel with the maximum value. 486 A new `lsst.afw.image.Exposure` with the convolved pixels and the (possibly 491 - We optionally re-center the kernel if necessary and return the possibly 494 kernelImg = afwImage.ImageD(kernel.shape[1], kernel.shape[0])
495 kernelImg.getArray()[:, :] = kernel
498 maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
499 kern.setCtrX(maxloc[0])
500 kern.setCtrY(maxloc[1])
501 outExp = exposure.clone()
503 maxInterpolationDistance=0)
505 afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl)
506 except AttributeError:
514 """Compute ZOGY diffim `D` using image-space convlutions 516 This method is still being debugged as it results in artifacts 517 when the PSFs are noisy (see module-level docstring). Thus 518 there are several options still enabled by the `debug` flag, 519 which are disabled by defult. 524 The amount to pad the PSFs by 526 Flag to enable debugging tests and options 530 D : `lsst.afw.Exposure` 531 the proper image difference, including correct variance, 539 Kr_hat = (preqs.Pr_hat + delta) / (preqs.denom + delta)
540 Kn_hat = (preqs.Pn_hat + delta) / (preqs.denom + delta)
541 Kr = np.fft.ifft2(Kr_hat).real
542 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
543 Kn = np.fft.ifft2(Kn_hat).real
544 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
546 def _trimKernel(self, K, trim_amount):
550 K = K[ps:-ps, ps:-ps]
553 padSize = self.
padSize if padSize
is None else padSize
555 if debug
and self.config.doTrimKernels:
558 Kn = _trimKernel(Kn, padSize)
559 Kr = _trimKernel(Kr, padSize)
565 tmp = D.getMaskedImage()
566 tmp -= exp1.getMaskedImage()
568 return pipeBase.Struct(D=D, R=exp1)
570 def _setNewPsf(self, exposure, psfArr):
571 """Utility method to set an exposure's PSF when provided as a 2-d numpy.array 573 bbox = exposure.getBBox()
574 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
576 psfI = afwImage.ImageD(psfArr.shape[1], psfArr.shape[0])
577 psfI.getArray()[:, :] = psfArr
579 psfNew = measAlg.KernelPsf(psfK, center)
580 exposure.setPsf(psfNew)
584 returnMatchedTemplate=False, **kwargs):
585 """Wrapper method to compute ZOGY proper diffim 587 This method should be used as the public interface for 588 computing the ZOGY diffim. 592 inImageSpace : `bool` 593 Override config `inImageSpace` parameter 595 Override config `padSize` parameter 596 returnMatchedTemplate : `bool` 597 Include the PSF-matched template in the results Struct 599 additional keyword arguments to be passed to 600 `computeDiffimFourierSpace` or `computeDiffimImageSpace`. 604 An lsst.pipe.base.Struct containing: 605 - D : `lsst.afw.Exposure` 606 the proper image difference, including correct variance, 608 - R : `lsst.afw.Exposure` 609 If `returnMatchedTemplate` is True, the PSF-matched template 613 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
615 padSize = self.
padSize if padSize
is None else padSize
618 if returnMatchedTemplate:
623 D.getMaskedImage().getImage().getArray()[:, :] = res.D
624 D.getMaskedImage().getVariance().getArray()[:, :] = res.D_var
625 if returnMatchedTemplate:
627 R.getMaskedImage().getImage().getArray()[:, :] = res.R
628 R.getMaskedImage().getVariance().getArray()[:, :] = res.R_var
632 return pipeBase.Struct(D=D, R=R)
635 """Compute the ZOGY diffim PSF (ZOGY manuscript eq. 14) 640 Override config `padSize` parameter 642 Return the FFT of the diffim PSF (do not inverse-FFT it) 643 psf1 : 2D `numpy.array` 644 (Optional) Input psf of template, override if already padded 645 psf2 : 2D `numpy.array` 646 (Optional) Input psf of science image, override if already padded 650 Pd : 2D `numpy.array` 651 The diffim PSF (or FFT of PSF if `keepFourier=True`) 653 preqs = self.
computePrereqs(psf1=psf1, psf2=psf2, padSize=padSize)
655 Pd_hat_numerator = (self.
Fr * self.
Fn * preqs.Pr_hat * preqs.Pn_hat)
656 Pd_hat = Pd_hat_numerator / (preqs.Fd * preqs.denom)
661 Pd = np.fft.ifft2(Pd_hat)
662 Pd = np.fft.ifftshift(Pd).real
666 def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False,
667 R_hat=None, Kr_hat=None, Kr=None,
668 N_hat=None, Kn_hat=None, Kn=None):
669 """Compute the astrometric noise correction terms 671 Compute the correction for estimated astrometric noise as 672 proscribed in ZOGY (2016), section 3.3. All convolutions 673 performed either in real (image) or Fourier space. 677 xVarAst, yVarAst : `float` 678 estimated astrometric noise (variance of astrometric registration errors) 679 inImageSpace : `bool` 680 Perform all convolutions in real (image) space rather than Fourier space 681 R_hat : 2-D `numpy.array` 682 (Optional) FFT of template image, only required if `inImageSpace=False` 683 Kr_hat : 2-D `numpy.array` 684 FFT of Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=False` 685 Kr : 2-D `numpy.array` 686 Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=True`. 687 Kr is associated with the template (reference). 688 N_hat : 2-D `numpy.array` 689 FFT of science image, only required if `inImageSpace=False` 690 Kn_hat : 2-D `numpy.array` 691 FFT of Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=False` 692 Kn : 2-D `numpy.array` 693 Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=True`. 694 Kn is associated with the science (new) image. 698 VastSR, VastSN : 2-D `numpy.array` 699 Arrays containing the values in eqs. 30 and 32 of ZOGY (2016). 702 if xVarAst + yVarAst > 0:
705 S_R = S_R.getMaskedImage().getImage().getArray()
707 S_R = np.fft.ifft2(R_hat * Kr_hat)
708 gradRx, gradRy = np.gradient(S_R)
709 VastSR = xVarAst * gradRx**2. + yVarAst * gradRy**2.
713 S_N = S_N.getMaskedImage().getImage().getArray()
715 S_N = np.fft.ifft2(N_hat * Kn_hat)
716 gradNx, gradNy = np.gradient(S_N)
717 VastSN = xVarAst * gradNx**2. + yVarAst * gradNy**2.
719 return VastSR, VastSN
722 """Compute corrected likelihood image, optimal for source detection 724 Compute ZOGY S_corr image. This image can be thresholded for 725 detection without optimal filtering, and the variance image is 726 corrected to account for astrometric noise (errors in 727 astrometric registration whether systematic or due to effects 728 such as DCR). The calculations here are all performed in 729 Fourier space, as proscribed in ZOGY (2016). 733 xVarAst, yVarAst : `float` 734 estimated astrometric noise (variance of astrometric registration errors) 738 result : `lsst.pipe.base.Struct` 739 Result struct with components: 741 - ``S`` : `numpy.array`, the likelihood image S (eq. 12 of ZOGY (2016)) 742 - ``S_var`` : the corrected variance image (denominator of eq. 25 of ZOGY (2016)) 743 - ``Dpsf`` : the PSF of the diffim D, likely never to be used. 747 """Replace any NaNs or Infs with the mean of the image.""" 748 isbad = ~np.isfinite(im)
751 im[isbad] = np.nanmean(im)
754 self.
im1 = fix_nans(self.
im1)
755 self.
im2 = fix_nans(self.
im2)
760 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.
im1.shape)
761 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.
im2.shape)
766 R_hat = np.fft.fft2(self.
im1)
767 N_hat = np.fft.fft2(self.
im2)
768 D_hat = self.
Fr * preqs.Pr_hat * N_hat - self.
Fn * preqs.Pn_hat * R_hat
771 Pd_hat = self.
computeDiffimPsf(padSize=0, keepFourier=
True, psf1=psf1, psf2=psf2)
772 Pd_bar = np.conj(Pd_hat)
773 S = np.fft.ifft2(D_hat * Pd_bar)
777 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
778 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
779 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
780 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
782 Kr_hat2 = np.fft.fft2(np.fft.ifft2(Kr_hat)**2.)
783 Kn_hat2 = np.fft.fft2(np.fft.ifft2(Kn_hat)**2.)
784 var1c_hat = Kr_hat2 * np.fft.fft2(self.
im1_var)
785 var2c_hat = Kn_hat2 * np.fft.fft2(self.
im2_var)
789 R_hat=R_hat, Kr_hat=Kr_hat,
790 N_hat=N_hat, Kn_hat=Kn_hat)
792 S_var = np.sqrt(np.fft.ifftshift(np.fft.ifft2(var1c_hat + var2c_hat)) + fGradR + fGradN)
795 S = np.fft.ifftshift(np.fft.ifft2(Kn_hat * N_hat - Kr_hat * R_hat))
799 return pipeBase.Struct(S=S.real, S_var=S_var.real, Dpsf=Pd)
802 """Compute corrected likelihood image, optimal for source detection 804 Compute ZOGY S_corr image. This image can be thresholded for 805 detection without optimal filtering, and the variance image is 806 corrected to account for astrometric noise (errors in 807 astrometric registration whether systematic or due to effects 808 such as DCR). The calculations here are all performed in 813 xVarAst, yVarAst : `float` 814 estimated astrometric noise (variance of astrometric registration errors) 818 A `lsst.pipe.base.Struct` containing: 819 - S : `lsst.afw.image.Exposure`, the likelihood exposure S (eq. 12 of ZOGY (2016)), 820 including corrected variance, masks, and PSF 821 - D : `lsst.afw.image.Exposure`, the proper image difference, including correct 822 variance, masks, and PSF 827 padSize = self.
padSize if padSize
is None else padSize
831 Pd_bar = np.fliplr(np.flipud(Pd))
833 tmp = S.getMaskedImage()
838 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
839 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
840 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
841 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
843 Kr = np.fft.ifft2(Kr_hat).real
844 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
845 Kn = np.fft.ifft2(Kn_hat).real
846 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
854 Smi = S.getMaskedImage()
856 S_var = np.sqrt(var1c.getArray() + var2c.getArray() + fGradR + fGradN)
857 S.getMaskedImage().getVariance().getArray()[:, :] = S_var
861 return pipeBase.Struct(S=S, D=D)
863 def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, **kwargs):
864 """Wrapper method to compute ZOGY corrected likelihood image, optimal for 867 This method should be used as the public interface for 868 computing the ZOGY S_corr. 872 xVarAst, yVarAst : `float` 873 estimated astrometric noise (variance of astrometric registration errors) 874 inImageSpace : `bool` 875 Override config `inImageSpace` parameter 877 Override config `padSize` parameter 881 S : `lsst.afw.image.Exposure` 882 The likelihood exposure S (eq. 12 of ZOGY (2016)), 883 including corrected variance, masks, and PSF 885 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
893 S.getMaskedImage().getImage().getArray()[:, :] = res.S
894 S.getMaskedImage().getVariance().getArray()[:, :] = res.S_var
897 return pipeBase.Struct(S=S)
901 """Task to be used as an ImageMapper for performing 902 ZOGY image subtraction on a grid of subimages. 904 ConfigClass = ZogyConfig
905 _DefaultName =
'ip_diffim_ZogyMapper' 908 ImageMapper.__init__(self, *args, **kwargs)
910 def run(self, subExposure, expandedSubExposure, fullBBox, template,
912 """Perform ZOGY proper image subtraction on sub-images 914 This method performs ZOGY proper image subtraction on 915 `subExposure` using local measures for image variances and 916 PSF. `subExposure` is a sub-exposure of the science image. It 917 also requires the corresponding sub-exposures of the template 918 (`template`). The operations are actually performed on 919 `expandedSubExposure` to allow for invalid edge pixels arising 920 from convolutions, which are then removed. 924 subExposure : `lsst.afw.image.Exposure` 925 the sub-exposure of the diffim 926 expandedSubExposure : `lsst.afw.image.Exposure` 927 the expanded sub-exposure upon which to operate 928 fullBBox : `lsst.afw.geom.BoundingBox` 929 the bounding box of the original exposure 930 template : `lsst.afw.image.Exposure` 931 the template exposure, from which a corresponding sub-exposure 934 additional keyword arguments propagated from 935 `ImageMapReduceTask.run`. These include: 938 Compute and return the corrected likelihood image S_corr 939 rather than the proper image difference 940 ``inImageSpace`` : `bool` 941 Perform all convolutions in real (image) space rather than 942 in Fourier space. This option currently leads to artifacts 943 when using real (measured and noisy) PSFs, thus it is set 944 to `False` by default. 945 These kwargs may also include arguments to be propagated to 946 `ZogyTask.computeDiffim` and `ZogyTask.computeScorr`. 950 result : `lsst.pipe.base.Struct` 951 Result struct with components: 953 ``subExposure``: Either the subExposure of the proper image difference ``D``, 954 or (if `doScorr==True`) the corrected likelihood exposure ``S``. 958 This `run` method accepts parameters identical to those of 959 `ImageMapper.run`, since it is called from the 960 `ImageMapperTask`. See that class for more information. 962 bbox = subExposure.getBBox()
963 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
966 imageSpace = kwargs.pop(
'inImageSpace',
False)
967 doScorr = kwargs.pop(
'doScorr',
False)
968 sigmas = kwargs.pop(
'sigmas',
None)
969 padSize = kwargs.pop(
'padSize', 7)
972 subExp2 = expandedSubExposure
975 subExp1 = template.Factory(template, expandedSubExposure.getBBox())
981 sig1, sig2 = sigmas[0], sigmas[1]
983 def _makePsfSquare(psf):
985 if psf.shape[0] < psf.shape[1]:
986 psf = np.pad(psf, ((0, psf.shape[1] - psf.shape[0]), (0, 0)), mode=
'constant',
988 elif psf.shape[0] > psf.shape[1]:
989 psf = np.pad(psf, ((0, 0), (0, psf.shape[0] - psf.shape[1])), mode=
'constant',
993 psf2 = subExp2.getPsf().computeKernelImage(center).getArray()
994 psf2 = _makePsfSquare(psf2)
996 psf1 = template.getPsf().computeKernelImage(center).getArray()
997 psf1 = _makePsfSquare(psf1)
1000 if subExp1.getDimensions()[0] < psf1.shape[0]
or subExp1.getDimensions()[1] < psf1.shape[1]:
1001 return pipeBase.Struct(subExposure=subExposure)
1003 def _filterPsf(psf):
1004 """Filter a noisy Psf to remove artifacts. Subject of future research.""" 1006 if psf.shape[0] == 41:
1009 psf[0:10, :] = psf[:, 0:10] = psf[31:41, :] = psf[:, 31:41] = 0
1014 psf1b = psf2b =
None 1015 if self.config.doFilterPsfs:
1017 psf1b = _filterPsf(psf1)
1018 psf2b = _filterPsf(psf2)
1021 if imageSpace
is True:
1022 config.inImageSpace = imageSpace
1023 config.padSize = padSize
1024 task =
ZogyTask(templateExposure=subExp1, scienceExposure=subExp2,
1025 sig1=sig1, sig2=sig2, psf1=psf1b, psf2=psf2b, config=config)
1028 res = task.computeDiffim(**kwargs)
1031 res = task.computeScorr(**kwargs)
1034 outExp = D.Factory(D, subExposure.getBBox())
1035 out = pipeBase.Struct(subExposure=outExp)
1040 """Config to be passed to ImageMapReduceTask 1042 This config targets the imageMapper to use the ZogyMapper. 1044 mapper = pexConfig.ConfigurableField(
1045 doc=
'Zogy task to run on each sub-image',
1051 """Config for the ZogyImagePsfMatchTask""" 1053 zogyConfig = pexConfig.ConfigField(
1055 doc=
'ZogyTask config to use when running on complete exposure (non spatially-varying)',
1058 zogyMapReduceConfig = pexConfig.ConfigField(
1059 dtype=ZogyMapReduceConfig,
1060 doc=
'ZogyMapReduce config to use when running Zogy on each sub-image (spatially-varying)',
1072 """Task to perform Zogy PSF matching and image subtraction. 1074 This class inherits from ImagePsfMatchTask to contain the _warper 1075 subtask and related methods. 1078 ConfigClass = ZogyImagePsfMatchConfig
1081 ImagePsfMatchTask.__init__(self, *args, **kwargs)
1083 def _computeImageMean(self, exposure):
1084 """Compute the sigma-clipped mean of the pixels image of `exposure`. 1087 statsControl.setNumSigmaClip(3.)
1088 statsControl.setNumIter(3)
1089 ignoreMaskPlanes = (
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
1090 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(ignoreMaskPlanes))
1092 exposure.getMaskedImage().getMask(),
1093 afwMath.MEANCLIP | afwMath.MEDIAN, statsControl)
1094 mn = statObj.getValue(afwMath.MEANCLIP)
1095 med = statObj.getValue(afwMath.MEDIAN)
1099 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1100 doPreConvolve=False):
1101 """Register, PSF-match, and subtract two Exposures using the ZOGY algorithm. 1103 Do the following, in order: 1104 - Warp templateExposure to match scienceExposure, if their WCSs do not already match 1105 - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures 1109 templateExposure : `lsst.afw.image.Exposure` 1110 exposure to PSF-match to scienceExposure. The exposure's mean value is subtracted 1112 scienceExposure : `lsst.afw.image.Exposure` 1113 reference Exposure. The exposure's mean value is subtracted in-place. 1115 what to do if templateExposure's and scienceExposure's WCSs do not match: 1116 - if True then warp templateExposure to match scienceExposure 1117 - if False then raise an Exception 1118 spatiallyVarying : `bool` 1119 If True, perform the operation over a grid of patches across the two exposures 1120 inImageSpace : `bool` 1121 If True, perform the Zogy convolutions in image space rather than in frequency space. 1122 doPreConvolve : `bool` 1123 ***Currently not implemented.*** If True assume we are to compute the match filter-convolved 1124 exposure which can be thresholded for detection. In the case of Zogy this would mean 1125 we compute the Scorr image. 1129 A `lsst.pipe.base.Struct` containing these fields: 1130 - subtractedExposure: subtracted Exposure 1131 - warpedExposure: templateExposure after warping to match scienceExposure (if doWarping true) 1136 self.log.
info(
"Exposure means=%f, %f; median=%f, %f:" % (mn1[0], mn2[0], mn1[1], mn2[1]))
1137 if not np.isnan(mn1[0])
and np.abs(mn1[0]) > 1:
1138 mi = templateExposure.getMaskedImage()
1140 if not np.isnan(mn2[0])
and np.abs(mn2[0]) > 1:
1141 mi = scienceExposure.getMaskedImage()
1144 self.log.
info(
'Running Zogy algorithm: spatiallyVarying=%r' % spatiallyVarying)
1146 if not self.
_validateWcs(templateExposure, scienceExposure):
1148 self.log.
info(
"Astrometrically registering template to science image")
1151 scienceExposure.getWcs())
1152 psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform)
1155 destBBox=scienceExposure.getBBox())
1157 templateExposure.setPsf(psfWarped)
1159 self.log.
error(
"ERROR: Input images not registered")
1160 raise RuntimeError(
"Input images not registered")
1163 return exp.getMaskedImage().getMask()
1166 return exp.getMaskedImage().getImage().getArray()
1168 if self.config.zogyConfig.inImageSpace:
1170 self.log.
info(
'Running Zogy algorithm: inImageSpace=%r' % inImageSpace)
1171 if spatiallyVarying:
1172 config = self.config.zogyMapReduceConfig
1174 results = task.run(scienceExposure, template=templateExposure, inImageSpace=inImageSpace,
1175 doScorr=doPreConvolve, forceEvenSized=
False)
1176 results.D = results.exposure
1183 config = self.config.zogyConfig
1184 task =
ZogyTask(scienceExposure=scienceExposure, templateExposure=templateExposure,
1186 if not doPreConvolve:
1187 results = task.computeDiffim(inImageSpace=inImageSpace)
1188 results.matchedExposure = results.R
1190 results = task.computeScorr(inImageSpace=inImageSpace)
1191 results.D = results.S
1194 mask = results.D.getMaskedImage().getMask()
1195 mask |= scienceExposure.getMaskedImage().getMask()
1196 mask |= templateExposure.getMaskedImage().getMask()
1197 results.D.getMaskedImage().getMask()[:, :] = mask
1198 badBitsNan = mask.addMaskPlane(
'UNMASKEDNAN')
1199 resultsArr = results.D.getMaskedImage().getMask().getArray()
1200 resultsArr[np.isnan(resultsArr)] |= badBitsNan
1201 resultsArr[np.isnan(scienceExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1202 resultsArr[np.isnan(templateExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1204 results.subtractedExposure = results.D
1205 results.warpedExposure = templateExposure
1209 doWarping=True, spatiallyVarying=True, inImageSpace=False,
1210 doPreConvolve=False):
1211 raise NotImplementedError
1214 subtractAlgorithmRegistry.register(
'zogy', ZogyImagePsfMatchTask)
def _computeImageMean(self, exposure)
def _computeVarAstGradients(self, xVarAst=0., yVarAst=0., inImageSpace=False, R_hat=None, Kr_hat=None, Kr=None, N_hat=None, Kn_hat=None, Kn=None)
def computeDiffim(self, inImageSpace=None, padSize=None, returnMatchedTemplate=False, kwargs)
def computeDiffimFourierSpace(self, debug=False, returnMatchedTemplate=False, kwargs)
Parameters to control convolution.
def __init__(self, args, kwargs)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
def computePrereqs(self, psf1=None, psf2=None, padSize=0)
Statistics makeStatistics(lsst::afw::math::MaskedVector< EntryT > const &mv, std::vector< WeightPixel > const &vweights, int const flags, StatisticsControl const &sctrl=StatisticsControl())
The makeStatistics() overload to handle lsst::afw::math::MaskedVector<>
Pass parameters to a Statistics object.
def run(self, subExposure, expandedSubExposure, fullBBox, template, kwargs)
def subtractExposures(self, templateExposure, scienceExposure, doWarping=True, spatiallyVarying=True, inImageSpace=False, doPreConvolve=False)
def setup(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None, psf1=None, psf2=None, correctBackground=False, args, kwargs)
def __init__(self, templateExposure=None, scienceExposure=None, sig1=None, sig2=None, psf1=None, psf2=None, args, kwargs)
def computeScorrFourierSpace(self, xVarAst=0., yVarAst=0., kwargs)
std::shared_ptr< TransformPoint2ToPoint2 > makeWcsPairTransform(SkyWcs const &src, SkyWcs const &dst)
A Transform obtained by putting two SkyWcs objects "back to back".
def _computeVarianceMean(self, exposure)
def computeDiffimPsf(self, padSize=0, keepFourier=False, psf1=None, psf2=None)
def _doConvolve(self, exposure, kernel, recenterKernel=False)
def _setNewPsf(self, exposure, psfArr)
def subtractMaskedImages(self, templateExposure, scienceExposure, doWarping=True, spatiallyVarying=True, inImageSpace=False, doPreConvolve=False)
def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, kwargs)
def computeDiffimImageSpace(self, padSize=None, debug=False, kwargs)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def _validateWcs(self, templateExposure, scienceExposure)
def computeScorrImageSpace(self, xVarAst=0., yVarAst=0., padSize=None, kwargs)
A kernel created from an Image.
def __init__(self, args, kwargs)