1 from __future__
import absolute_import, division, print_function
2 from future
import standard_library
3 standard_library.install_aliases()
28 import lsst.afw.image
as afwImage
29 import lsst.afw.geom
as afwGeom
30 import lsst.meas.algorithms
as measAlg
31 import lsst.afw.math
as afwMath
32 import lsst.pex.config
as pexConfig
33 import lsst.pipe.base
as pipeBase
36 from .imageMapReduce
import (ImageMapReduceConfig, ImageMapper,
38 from .imagePsfMatch
import (ImagePsfMatchTask, ImagePsfMatchConfig,
39 subtractAlgorithmRegistry)
41 __all__ = [
"ZogyTask",
"ZogyConfig",
42 "ZogyMapper",
"ZogyMapReduceConfig",
43 "ZogyImagePsfMatchConfig",
"ZogyImagePsfMatchTask"]
46 """Tasks for performing the "Proper image subtraction" algorithm of
47 Zackay, et al. (2016), hereafter simply referred to as 'ZOGY (2016)'.
49 `ZogyTask` contains methods to perform the basic estimation of the
50 ZOGY diffim `D`, its updated PSF, and the variance-normalized
51 likelihood image `S_corr`. We have implemented ZOGY using the
52 proscribed methodology, computing all convolutions in Fourier space,
53 and also variants in which the convolutions are performed in real
54 (image) space. The former is faster and results in fewer artifacts
55 when the PSFs are noisy (i.e., measured, for example, via
56 `PsfEx`). The latter is presumed to be preferred as it can account for
57 masks correctly with fewer "ringing" artifacts from edge effects or
58 saturated stars, but noisy PSFs result in their own smaller
59 artifacts. Removal of these artifacts is a subject of continuing
60 research. Currently, we "pad" the PSFs when performing the
61 subtractions in real space, which reduces, but does not entirely
62 eliminate these artifacts.
64 All methods in `ZogyTask` assume template and science images are
65 already accurately photometrically and astrometrically registered.
67 `ZogyMapper` is a wrapper which runs `ZogyTask` in the
68 `ImageMapReduce` framework, computing of ZOGY diffim's on small,
69 overlapping sub-images, thereby enabling complete ZOGY diffim's which
70 account for spatially-varying noise and PSFs across the two input
71 exposures. An example of the use of this task is in the `testZogy.py`
77 """Configuration parameters for the ZogyTask
79 inImageSpace = pexConfig.Field(
82 doc=
"Perform all convolutions in real (image) space rather than Fourier space. "
83 "Currently if True, this results in artifacts when using real (noisy) PSFs."
86 padSize = pexConfig.Field(
89 doc=
"Number of pixels to pad PSFs to avoid artifacts (when inImageSpace is True)"
92 templateFluxScaling = pexConfig.Field(
95 doc=
"Template flux scaling factor (Fr in ZOGY paper)"
98 scienceFluxScaling = pexConfig.Field(
101 doc=
"Science flux scaling factor (Fn in ZOGY paper)"
104 doTrimKernels = pexConfig.Field(
107 doc=
"Trim kernels for image-space ZOGY. Speeds up convolutions and shrinks artifacts. "
108 "Subject of future research."
111 doFilterPsfs = pexConfig.Field(
114 doc=
"Filter PSFs for image-space ZOGY. Aids in reducing artifacts. "
115 "Subject of future research."
118 ignoreMaskPlanes = pexConfig.ListField(
120 default=(
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE"),
121 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:
211 self.statsControl.setNumSigmaClip(3.)
212 self.statsControl.setNumIter(3)
213 self.statsControl.setAndMask(afwImage.Mask\
214 .getPlaneBitMask(self.config.ignoreMaskPlanes))
216 self.
im1 = self.template.getMaskedImage().getImage().getArray()
217 self.
im2 = self.science.getMaskedImage().getImage().getArray()
218 self.
im1_var = self.template.getMaskedImage().getVariance().getArray()
219 self.
im2_var = self.science.getMaskedImage().getVariance().getArray()
221 def selectPsf(psf, exposure):
225 bbox1 = self.template.getBBox()
226 xcen = (bbox1.getBeginX() + bbox1.getEndX()) / 2.
227 ycen = (bbox1.getBeginY() + bbox1.getEndY()) / 2.
228 return exposure.getPsf().computeKernelImage(afwGeom.Point2D(xcen, ycen)).getArray()
234 pShape1 = self.im1_psf.shape
235 pShape2 = self.im2_psf.shape
236 if (pShape1[0] < pShape2[0]):
237 self.
im1_psf = np.pad(self.
im1_psf, ((0, pShape2[0] - pShape1[0]), (0, 0)),
238 mode=
'constant', constant_values=0.)
239 elif (pShape2[0] < pShape1[0]):
240 self.
im2_psf = np.pad(self.
im2_psf, ((0, pShape1[0] - pShape2[0]), (0, 0)),
241 mode=
'constant', constant_values=0.)
242 if (pShape1[1] < pShape2[1]):
243 self.
im1_psf = np.pad(self.
im1_psf, ((0, 0), (0, pShape2[1] - pShape1[1])),
244 mode=
'constant', constant_values=0.)
245 elif (pShape2[1] < pShape1[1]):
246 self.
im2_psf = np.pad(self.
im2_psf, ((0, 0), (0, pShape1[1] - pShape2[1])),
247 mode=
'constant', constant_values=0.)
253 if correctBackground:
254 def _subtractImageMean(exposure):
255 """Compute the sigma-clipped mean of the image of `exposure`."""
256 mi = exposure.getMaskedImage()
257 statObj = afwMath.makeStatistics(mi.getImage(), mi.getMask(),
259 mean = statObj.getValue(afwMath.MEANCLIP)
260 if not np.isnan(mean):
264 _subtractImageMean(self.
science)
266 self.
Fr = self.config.templateFluxScaling
267 self.
Fn = self.config.scienceFluxScaling
271 """Compute the sigma-clipped mean of the variance image of `exposure`.
273 statObj = afwMath.makeStatistics(exposure.getMaskedImage().getVariance(),
274 exposure.getMaskedImage().getMask(),
276 var = statObj.getValue(afwMath.MEANCLIP)
281 """Zero-pad `psf` to the dimensions given by `size`.
285 psf : 2D `numpy.array`
286 Input psf to be padded
288 Two element list containing the dimensions to pad the `psf` to
292 psf : 2D `numpy.array`
293 The padded copy of the input `psf`.
295 newArr = np.zeros(size)
296 offset = [size[0]//2 - psf.shape[0]//2 - 1, size[1]//2 - psf.shape[1]//2 - 1]
297 tmp = newArr[offset[0]:(psf.shape[0] + offset[0]), offset[1]:(psf.shape[1] + offset[1])]
302 """Compute standard ZOGY quantities used by (nearly) all methods.
304 Many of the ZOGY calculations require similar quantities, including
305 FFTs of the PSFs, and the "denominator" term (e.g. in eq. 13 of
306 ZOGY manuscript (2016). This function consolidates many of those
311 psf1 : 2D `numpy.array`
312 (Optional) Input psf of template, override if already padded
313 psf2 : 2D `numpy.array`
314 (Optional) Input psf of science image, override if already padded
318 A lsst.pipe.base.Struct containing:
319 - Pr : 2D `numpy.array`, the (possibly zero-padded) template PSF
320 - Pn : 2D `numpy.array`, the (possibly zero-padded) science PSF
321 - Pr_hat : 2D `numpy.array`, the FFT of `Pr`
322 - Pn_hat : 2D `numpy.array`, the FFT of `Pn`
323 - denom : 2D `numpy.array`, the denominator of equation (13) in ZOGY (2016) manuscript
324 - Fd : `float`, the relative flux scaling factor between science and template
326 psf1 = self.
im1_psf if psf1
is None else psf1
327 psf2 = self.
im2_psf if psf2
is None else psf2
328 padSize = self.
padSize if padSize
is None else padSize
331 Pr = ZogyTask._padPsfToSize(psf1, (psf1.shape[0] + padSize, psf1.shape[1] + padSize))
332 Pn = ZogyTask._padPsfToSize(psf2, (psf2.shape[0] + padSize, psf2.shape[1] + padSize))
335 Pr_hat = np.fft.fft2(Pr)
336 Pr_hat2 = np.conj(Pr_hat) * Pr_hat
337 Pn_hat = np.fft.fft2(Pn)
338 Pn_hat2 = np.conj(Pn_hat) * Pn_hat
339 denom = np.sqrt((sigN**2 * self.
Fr**2 * Pr_hat2) + (sigR**2 * self.
Fn**2 * Pn_hat2))
340 Fd = self.
Fr*self.
Fn / np.sqrt(sigN**2 * self.
Fr**2 + sigR**2 * self.
Fn**2)
342 res = pipeBase.Struct(
343 Pr=Pr, Pn=Pn, Pr_hat=Pr_hat, Pn_hat=Pn_hat, denom=denom, Fd=Fd
349 """Compute ZOGY diffim `D` as proscribed in ZOGY (2016) manuscript
351 Compute the ZOGY eqn. (13):
353 \widehat{D} = \frac{Fr\widehat{Pr}\widehat{N} -
354 F_n\widehat{Pn}\widehat{R}}{\sqrt{\sigma_n^2 Fr^2
355 |\widehat{Pr}|^2 + \sigma_r^2 F_n^2 |\widehat{Pn}|^2}}
357 where $D$ is the optimal difference image, $R$ and $N$ are the
358 reference and "new" image, respectively, $Pr$ and $P_n$ are their
359 PSFs, $Fr$ and $Fn$ are their flux-based zero-points (which we
360 will set to one here), $\sigma_r^2$ and $\sigma_n^2$ are their
361 variance, and $\widehat{D}$ denotes the FT of $D$.
365 A `lsst.pipe.base.Struct` containing:
366 - D : 2D `numpy.array`, the proper image difference
367 - D_var : 2D `numpy.array`, the variance image for `D`
370 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.im1.shape)
371 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.im2.shape)
375 def _filterKernel(K, trim_amount):
378 K[:ps, :] = K[-ps:, :] = 0
379 K[:, :ps] = K[:, -ps:] = 0
382 Kr_hat = self.
Fr * preqs.Pr_hat / preqs.denom
383 Kn_hat = self.
Fn * preqs.Pn_hat / preqs.denom
384 if debug
and self.config.doTrimKernels:
387 ps = (Kn_hat.shape[1] - 80)//2
388 Kn = _filterKernel(np.fft.ifft2(Kn_hat), ps)
389 Kn_hat = np.fft.fft2(Kn)
390 Kr = _filterKernel(np.fft.ifft2(Kr_hat), ps)
391 Kr_hat = np.fft.fft2(Kr)
393 def processImages(im1, im2, doAdd=False):
395 im1[np.isinf(im1)] = np.nan
396 im1[np.isnan(im1)] = np.nanmean(im1)
397 im2[np.isinf(im2)] = np.nan
398 im2[np.isnan(im2)] = np.nanmean(im2)
400 R_hat = np.fft.fft2(im1)
401 N_hat = np.fft.fft2(im2)
403 D_hat = Kr_hat * N_hat
404 D_hat_R = Kn_hat * R_hat
410 D = np.fft.ifft2(D_hat)
411 D = np.fft.ifftshift(D.real) / preqs.Fd
414 if returnMatchedTemplate:
415 R = np.fft.ifft2(D_hat_R)
416 R = np.fft.ifftshift(R.real) / preqs.Fd
421 D, R = processImages(self.
im1, self.
im2, doAdd=
False)
423 D_var, R_var = processImages(self.
im1_var, self.
im2_var, doAdd=
True)
425 return pipeBase.Struct(D=D, D_var=D_var, R=R, R_var=R_var)
428 """! Convolve an Exposure with a decorrelation convolution kernel.
432 exposure : `lsst.afw.image.Exposure` to be convolved.
433 kernel : 2D `numpy.array` to convolve the image with
437 A new `lsst.afw.image.Exposure` with the convolved pixels and the (possibly
442 - We optionally re-center the kernel if necessary and return the possibly
445 kernelImg = afwImage.ImageD(kernel.shape[1], kernel.shape[0])
446 kernelImg.getArray()[:, :] = kernel
447 kern = afwMath.FixedKernel(kernelImg)
449 maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
450 kern.setCtrX(maxloc[0])
451 kern.setCtrY(maxloc[1])
452 outExp = exposure.clone()
453 convCntrl = afwMath.ConvolutionControl(doNormalize=
False, doCopyEdge=
False,
454 maxInterpolationDistance=0)
456 afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl)
459 afwMath.convolve(outExp, exposure, kern, convCntrl)
464 """Compute ZOGY diffim `D` using image-space convlutions
466 This method is still being debugged as it results in artifacts
467 when the PSFs are noisy (see module-level docstring). Thus
468 there are several options still enabled by the `debug` flag,
469 which are disabled by defult.
473 padSize : `int`, the amount to pad the PSFs by
474 debug : `bool`, flag to enable debugging tests and options
478 D : `lsst.afw.Exposure`
479 the proper image difference, including correct variance,
487 Kr_hat = (preqs.Pr_hat + delta) / (preqs.denom + delta)
488 Kn_hat = (preqs.Pn_hat + delta) / (preqs.denom + delta)
489 Kr = np.fft.ifft2(Kr_hat).real
490 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
491 Kn = np.fft.ifft2(Kn_hat).real
492 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
494 def _trimKernel(self, K, trim_amount):
498 K = K[ps:-ps, ps:-ps]
501 padSize = self.
padSize if padSize
is None else padSize
503 if debug
and self.config.doTrimKernels:
506 Kn = _trimKernel(Kn, padSize)
507 Kr = _trimKernel(Kr, padSize)
513 tmp = D.getMaskedImage()
514 tmp -= exp1.getMaskedImage()
516 return pipeBase.Struct(D=D, R=exp1)
519 """Utility method to set an exposure's PSF when provided as a 2-d numpy.array
521 bbox = exposure.getBBox()
522 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
523 center = afwGeom.Point2D(center[0], center[1])
524 psfI = afwImage.ImageD(psfArr.shape[1], psfArr.shape[0])
525 psfI.getArray()[:, :] = psfArr
526 psfK = afwMath.FixedKernel(psfI)
527 psfNew = measAlg.KernelPsf(psfK, center)
528 exposure.setPsf(psfNew)
532 returnMatchedTemplate=
False, **kwargs):
533 """Wrapper method to compute ZOGY proper diffim
535 This method should be used as the public interface for
536 computing the ZOGY diffim.
540 inImageSpace : `bool`
541 Override config `inImageSpace` parameter
543 Override config `padSize` parameter
544 returnMatchedTemplate : bool
545 Include the PSF-matched template in the results Struct
547 additional keyword arguments to be passed to
548 `computeDiffimFourierSpace` or `computeDiffimImageSpace`.
552 An lsst.pipe.base.Struct containing:
553 - D : `lsst.afw.Exposure`
554 the proper image difference, including correct variance,
556 - R : `lsst.afw.Exposure`
557 If `returnMatchedTemplate` is True, the PSF-matched template
561 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
563 padSize = self.
padSize if padSize
is None else padSize
566 if returnMatchedTemplate:
570 D = self.science.clone()
571 D.getMaskedImage().getImage().getArray()[:, :] = res.D
572 D.getMaskedImage().getVariance().getArray()[:, :] = res.D_var
573 if returnMatchedTemplate:
574 R = self.science.clone()
575 R.getMaskedImage().getImage().getArray()[:, :] = res.R
576 R.getMaskedImage().getVariance().getArray()[:, :] = res.R_var
580 return pipeBase.Struct(D=D, R=R)
583 """Compute the ZOGY diffim PSF (ZOGY manuscript eq. 14)
588 Override config `padSize` parameter
590 Return the FFT of the diffim PSF (do not inverse-FFT it)
591 psf1 : 2D `numpy.array`
592 (Optional) Input psf of template, override if already padded
593 psf2 : 2D `numpy.array`
594 (Optional) Input psf of science image, override if already padded
598 Pd : 2D `numpy.array`, the diffim PSF (or FFT of PSF if `keepFourier=True`)
600 preqs = self.
computePrereqs(psf1=psf1, psf2=psf2, padSize=padSize)
602 Pd_hat_numerator = (self.
Fr * self.
Fn * preqs.Pr_hat * preqs.Pn_hat)
603 Pd_hat = Pd_hat_numerator / (preqs.Fd * preqs.denom)
608 Pd = np.fft.ifft2(Pd_hat)
609 Pd = np.fft.ifftshift(Pd).real
614 R_hat=
None, Kr_hat=
None, Kr=
None,
615 N_hat=
None, Kn_hat=
None, Kn=
None):
616 """Compute the astrometric noise correction terms
618 Compute the correction for estimated astrometric noise as
619 proscribed in ZOGY (2016), section 3.3. All convolutions
620 performed either in real (image) or Fourier space.
624 xVarAst, yVarAst : `float`
625 estimated astrometric noise (variance of astrometric registration errors)
626 inImageSpace : `bool`
627 Perform all convolutions in real (image) space rather than Fourier space
628 R_hat : 2-D `numpy.array`
629 (Optional) FFT of template image, only required if `inImageSpace=False`
630 Kr_hat : 2-D `numpy.array`
631 FFT of Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=False`
632 Kr : 2-D `numpy.array`
633 Kr kernel (eq. 28 of ZOGY (2016)), only required if `inImageSpace=True`.
634 Kr is associated with the template (reference).
635 N_hat : 2-D `numpy.array`
636 FFT of science image, only required if `inImageSpace=False`
637 Kn_hat : 2-D `numpy.array`
638 FFT of Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=False`
639 Kn : 2-D `numpy.array`
640 Kn kernel (eq. 29 of ZOGY (2016)), only required if `inImageSpace=True`.
641 Kn is associated with the science (new) image.
645 VastSR, VastSN : 2-D `numpy.array`s containing the values in eqs. 30 and 32 of
649 if xVarAst + yVarAst > 0:
652 S_R = S_R.getMaskedImage().getImage().getArray()
654 S_R = np.fft.ifft2(R_hat * Kr_hat)
655 gradRx, gradRy = np.gradient(S_R)
656 VastSR = xVarAst * gradRx**2. + yVarAst * gradRy**2.
660 S_N = S_N.getMaskedImage().getImage().getArray()
662 S_N = np.fft.ifft2(N_hat * Kn_hat)
663 gradNx, gradNy = np.gradient(S_N)
664 VastSN = xVarAst * gradNx**2. + yVarAst * gradNy**2.
666 return VastSR, VastSN
669 """Compute corrected likelihood image, optimal for source detection
671 Compute ZOGY S_corr image. This image can be thresholded for
672 detection without optimal filtering, and the variance image is
673 corrected to account for astrometric noise (errors in
674 astrometric registration whether systematic or due to effects
675 such as DCR). The calculations here are all performed in
676 Fourier space, as proscribed in ZOGY (2016).
680 xVarAst, yVarAst : `float`
681 estimated astrometric noise (variance of astrometric registration errors)
685 A lsst.pipe.base.Struct containing:
686 - S : `numpy.array`, the likelihood image S (eq. 12 of ZOGY (2016))
687 - S_var : the corrected variance image (denominator of eq. 25 of ZOGY (2016))
688 - Dpsf : the PSF of the diffim D, likely never to be used.
691 psf1 = ZogyTask._padPsfToSize(self.
im1_psf, self.im1.shape)
692 psf2 = ZogyTask._padPsfToSize(self.
im2_psf, self.im2.shape)
697 R_hat = np.fft.fft2(self.
im1)
698 N_hat = np.fft.fft2(self.
im2)
699 D_hat = self.
Fr * preqs.Pr_hat * N_hat - self.
Fn * preqs.Pn_hat * R_hat
702 Pd_hat = self.
computeDiffimPsf(padSize=0, keepFourier=
True, psf1=psf1, psf2=psf2)
703 Pd_bar = np.conj(Pd_hat)
704 S = np.fft.ifft2(D_hat * Pd_bar)
708 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
709 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
710 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
711 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
713 Kr_hat2 = np.fft.fft2(np.fft.ifft2(Kr_hat)**2.)
714 Kn_hat2 = np.fft.fft2(np.fft.ifft2(Kn_hat)**2.)
715 var1c_hat = Kr_hat2 * np.fft.fft2(self.
im1_var)
716 var2c_hat = Kn_hat2 * np.fft.fft2(self.
im2_var)
720 R_hat=R_hat, Kr_hat=Kr_hat,
721 N_hat=N_hat, Kn_hat=Kn_hat)
723 S_var = np.sqrt(np.fft.ifftshift(np.fft.ifft2(var1c_hat + var2c_hat)) + fGradR + fGradN)
726 S = np.fft.ifftshift(np.fft.ifft2(Kn_hat * N_hat - Kr_hat * R_hat))
730 return pipeBase.Struct(S=S.real, S_var=S_var.real, Dpsf=Pd)
733 """Compute corrected likelihood image, optimal for source detection
735 Compute ZOGY S_corr image. This image can be thresholded for
736 detection without optimal filtering, and the variance image is
737 corrected to account for astrometric noise (errors in
738 astrometric registration whether systematic or due to effects
739 such as DCR). The calculations here are all performed in
744 xVarAst, yVarAst : `float`
745 estimated astrometric noise (variance of astrometric registration errors)
750 - S : `lsst.afw.image.Exposure`, the likelihood exposure S (eq. 12 of ZOGY (2016)),
751 including corrected variance, masks, and PSF
752 - D : `lsst.afw.image.Exposure`, the proper image difference, including correct
753 variance, masks, and PSF
758 padSize = self.
padSize if padSize
is None else padSize
762 Pd_bar = np.fliplr(np.flipud(Pd))
764 tmp = S.getMaskedImage()
769 Pn_hat2 = np.conj(preqs.Pn_hat) * preqs.Pn_hat
770 Kr_hat = self.
Fr * self.
Fn**2. * np.conj(preqs.Pr_hat) * Pn_hat2 / preqs.denom**2.
771 Pr_hat2 = np.conj(preqs.Pr_hat) * preqs.Pr_hat
772 Kn_hat = self.
Fn * self.
Fr**2. * np.conj(preqs.Pn_hat) * Pr_hat2 / preqs.denom**2.
774 Kr = np.fft.ifft2(Kr_hat).real
775 Kr = np.roll(np.roll(Kr, -1, 0), -1, 1)
776 Kn = np.fft.ifft2(Kn_hat).real
777 Kn = np.roll(np.roll(Kn, -1, 0), -1, 1)
778 var1c, _ = self.
_doConvolve(self.template.getMaskedImage().getVariance(), Kr**2.)
779 var2c, _ = self.
_doConvolve(self.science.getMaskedImage().getVariance(), Kn**2.)
785 Smi = S.getMaskedImage()
787 S_var = np.sqrt(var1c.getArray() + var2c.getArray() + fGradR + fGradN)
788 S.getMaskedImage().getVariance().getArray()[:, :] = S_var
792 return pipeBase.Struct(S=S, D=D)
794 def computeScorr(self, xVarAst=0., yVarAst=0., inImageSpace=None, padSize=0, **kwargs):
795 """Wrapper method to compute ZOGY corrected likelihood image, optimal for
798 This method should be used as the public interface for
799 computing the ZOGY S_corr.
803 xVarAst, yVarAst : float
804 estimated astrometric noise (variance of astrometric registration errors)
806 Override config `inImageSpace` parameter
808 Override config `padSize` parameter
812 S : lsst.afw.image.Exposure, the likelihood exposure S (eq. 12 of ZOGY (2016)),
813 including corrected variance, masks, and PSF
815 inImageSpace = self.config.inImageSpace
if inImageSpace
is None else inImageSpace
822 S = self.science.clone()
823 S.getMaskedImage().getImage().getArray()[:, :] = res.S
824 S.getMaskedImage().getVariance().getArray()[:, :] = res.S_var
827 return pipeBase.Struct(S=S)
831 """Task to be used as an ImageMapper for performing
832 ZOGY image subtraction on a grid of subimages.
834 ConfigClass = ZogyConfig
835 _DefaultName =
'ip_diffim_ZogyMapper'
838 ImageMapper.__init__(self, *args, **kwargs)
840 def run(self, subExposure, expandedSubExposure, fullBBox, template,
842 """Perform ZOGY proper image subtraction on sub-images
844 This method performs ZOGY proper image subtraction on
845 `subExposure` using local measures for image variances and
846 PSF. `subExposure` is a sub-exposure of the science image. It
847 also requires the corresponding sub-exposures of the template
848 (`template`). The operations are actually performed on
849 `expandedSubExposure` to allow for invalid edge pixels arising
850 from convolutions, which are then removed.
854 subExposure : lsst.afw.image.Exposure
855 the sub-exposure of the diffim
856 expandedSubExposure : lsst.afw.image.Exposure
857 the expanded sub-exposure upon which to operate
858 fullBBox : lsst.afw.geom.BoundingBox
859 the bounding box of the original exposure
860 template : lsst.afw.image.Exposure
861 the template exposure, from which a corresponding sub-exposure
864 additional keyword arguments propagated from
865 `ImageMapReduceTask.run`. These include:
867 Compute and return the corrected likelihood image S_corr
868 rather than the proper image difference
869 - inImageSpace : bool
870 Perform all convolutions in real (image) space rather than
871 in Fourier space. This option currently leads to artifacts
872 when using real (measured and noisy) PSFs, thus it is set
873 to `False` by default.
874 These kwargs may also include arguments to be propagated to
875 `ZogyTask.computeDiffim` and `ZogyTask.computeScorr`.
879 A `lsst.pipe.base.Struct` containing the result of the
880 `subExposure` processing, labelled 'subExposure'. In this case
881 it is either the subExposure of the proper image difference D,
882 or (if `doScorr==True`) the corrected likelihood exposure `S`.
886 This `run` method accepts parameters identical to those of
887 `ImageMapper.run`, since it is called from the
888 `ImageMapperTask`. See that class for more information.
890 bbox = subExposure.getBBox()
891 center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
892 center = afwGeom.Point2D(center[0], center[1])
894 imageSpace = kwargs.pop(
'inImageSpace',
False)
895 doScorr = kwargs.pop(
'doScorr',
False)
896 sigmas = kwargs.pop(
'sigmas',
None)
897 padSize = kwargs.pop(
'padSize', 7)
900 subExp2 = expandedSubExposure
903 subExp1 = template.Factory(template, expandedSubExposure.getBBox())
909 sig1, sig2 = sigmas[0], sigmas[1]
911 def _makePsfSquare(psf):
913 if psf.shape[0] < psf.shape[1]:
914 psf = np.pad(psf, ((0, psf.shape[1] - psf.shape[0]), (0, 0)), mode=
'constant',
916 elif psf.shape[0] > psf.shape[1]:
917 psf = np.pad(psf, ((0, 0), (0, psf.shape[0] - psf.shape[1])), mode=
'constant',
921 psf2 = subExp2.getPsf().computeKernelImage(center).getArray()
922 psf2 = _makePsfSquare(psf2)
924 psf1 = template.getPsf().computeKernelImage(center).getArray()
925 psf1 = _makePsfSquare(psf1)
928 if subExp1.getDimensions()[0] < psf1.shape[0]
or subExp1.getDimensions()[1] < psf1.shape[1]:
929 return pipeBase.Struct(subExposure=subExposure)
932 """Filter a noisy Psf to remove artifacts. Subject of future research."""
934 if psf.shape[0] == 41:
937 psf[0:10, :] = psf[:, 0:10] = psf[31:41, :] = psf[:, 31:41] = 0
943 if self.config.doFilterPsfs:
945 psf1b = _filterPsf(psf1)
946 psf2b = _filterPsf(psf2)
949 if imageSpace
is True:
950 config.inImageSpace = imageSpace
951 config.padSize = padSize
952 task =
ZogyTask(templateExposure=subExp1, scienceExposure=subExp2,
953 sig1=sig1, sig2=sig2, psf1=psf1b, psf2=psf2b, config=config)
956 res = task.computeDiffim(**kwargs)
959 res = task.computeScorr(**kwargs)
962 outExp = D.Factory(D, subExposure.getBBox())
963 out = pipeBase.Struct(subExposure=outExp)
968 """Config to be passed to ImageMapReduceTask
970 This config targets the imageMapper to use the ZogyMapper.
972 mapper = pexConfig.ConfigurableField(
973 doc=
'Zogy task to run on each sub-image',
979 """Config for the ZogyImagePsfMatchTask"""
981 zogyConfig = pexConfig.ConfigField(
983 doc=
'ZogyTask config to use when running on complete exposure (non spatially-varying)',
986 zogyMapReduceConfig = pexConfig.ConfigField(
987 dtype=ZogyMapReduceConfig,
988 doc=
'ZogyMapReduce config to use when running Zogy on each sub-image (spatially-varying)',
992 self.zogyMapReduceConfig.gridStepX = self.zogyMapReduceConfig.gridStepY = 19
993 self.zogyMapReduceConfig.cellSizeX = self.zogyMapReduceConfig.cellSizeY = 20
994 self.zogyMapReduceConfig.borderSizeX = self.zogyMapReduceConfig.borderSizeY = 6
995 self.zogyMapReduceConfig.reducer.reduceOperation =
'average'
999 """Task to perform Zogy PSF matching and image subtraction.
1001 This class inherits from ImagePsfMatchTask to contain the _warper
1002 subtask and related methods.
1005 ConfigClass = ZogyImagePsfMatchConfig
1008 ImagePsfMatchTask.__init__(self, *args, **kwargs)
1011 """Compute the sigma-clipped mean of the pixels image of `exposure`.
1013 statsControl = afwMath.StatisticsControl()
1014 statsControl.setNumSigmaClip(3.)
1015 statsControl.setNumIter(3)
1016 ignoreMaskPlanes = (
"INTRP",
"EDGE",
"DETECTED",
"SAT",
"CR",
"BAD",
"NO_DATA",
"DETECTED_NEGATIVE")
1017 statsControl.setAndMask(afwImage.Mask.getPlaneBitMask(ignoreMaskPlanes))
1018 statObj = afwMath.makeStatistics(exposure.getMaskedImage().getImage(),
1019 exposure.getMaskedImage().getMask(),
1020 afwMath.MEANCLIP | afwMath.MEDIAN, statsControl)
1021 mn = statObj.getValue(afwMath.MEANCLIP)
1022 med = statObj.getValue(afwMath.MEDIAN)
1026 doWarping=
True, spatiallyVarying=
True, inImageSpace=
False,
1027 doPreConvolve=
False):
1028 """Register, PSF-match, and subtract two Exposures using the ZOGY algorithm.
1030 Do the following, in order:
1031 - Warp templateExposure to match scienceExposure, if their WCSs do not already match
1032 - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures
1036 templateExposure : `lsst.afw.image.Exposure`
1037 exposure to PSF-match to scienceExposure. The exposure's mean value is subtracted
1039 scienceExposure : `lsst.afw.image.Exposure`
1040 reference Exposure. The exposure's mean value is subtracted in-place.
1042 what to do if templateExposure's and scienceExposure's WCSs do not match:
1043 - if True then warp templateExposure to match scienceExposure
1044 - if False then raise an Exception
1045 spatiallyVarying : bool
1046 If True, perform the operation over a grid of patches across the two exposures
1047 inImageSpace : `bool`
1048 If True, perform the Zogy convolutions in image space rather than in frequency space.
1049 doPreConvolve : `bool`
1050 ***Currently not implemented.*** If True assume we are to compute the match filter-convolved
1051 exposure which can be thresholded for detection. In the case of Zogy this would mean
1052 we compute the Scorr image.
1056 A `lsst.pipe.base.Struct` containing these fields:
1057 - subtractedExposure: subtracted Exposure
1058 - warpedExposure: templateExposure after warping to match scienceExposure (if doWarping true)
1063 self.log.info(
"Exposure means=%f, %f; median=%f, %f:" % (mn1[0], mn2[0], mn1[1], mn2[1]))
1064 if not np.isnan(mn1[0])
and np.abs(mn1[0]) > 1:
1065 mi = templateExposure.getMaskedImage()
1067 if not np.isnan(mn2[0])
and np.abs(mn2[0]) > 1:
1068 mi = scienceExposure.getMaskedImage()
1071 self.log.info(
'Running Zogy algorithm: spatiallyVarying=%r' % spatiallyVarying)
1073 if not self._validateWcs(templateExposure, scienceExposure):
1075 self.log.info(
"Astrometrically registering template to science image")
1077 xyTransform = afwImage.XYTransformFromWcsPair(scienceExposure.getWcs(),
1078 templateExposure.getWcs())
1079 psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform)
1080 templateExposure = self._warper.warpExposure(scienceExposure.getWcs(),
1082 destBBox=scienceExposure.getBBox())
1084 templateExposure.setPsf(psfWarped)
1086 self.log.error(
"ERROR: Input images not registered")
1087 raise RuntimeError(
"Input images not registered")
1090 return exp.getMaskedImage().getMask()
1093 return exp.getMaskedImage().getImage().getArray()
1095 if spatiallyVarying:
1096 config = self.config.zogyMapReduceConfig
1097 task = ImageMapReduceTask(config=config)
1098 results = task.run(scienceExposure, template=templateExposure, inImageSpace=inImageSpace,
1099 doScorr=doPreConvolve, forceEvenSized=
True)
1100 results.D = results.exposure
1107 config = self.config.zogyConfig
1108 task =
ZogyTask(scienceExposure=scienceExposure, templateExposure=templateExposure,
1110 if not doPreConvolve:
1111 results = task.computeDiffim(inImageSpace=inImageSpace)
1112 results.matchedExposure = results.R
1114 results = task.computeScorr(inImageSpace=inImageSpace)
1115 results.D = results.S
1118 mask = results.D.getMaskedImage().getMask()
1119 mask |= scienceExposure.getMaskedImage().getMask()
1120 mask |= templateExposure.getMaskedImage().getMask()
1121 results.D.getMaskedImage().getMask()[:, :] = mask
1122 badBitsNan = mask.addMaskPlane(
'UNMASKEDNAN')
1123 resultsArr = results.D.getMaskedImage().getMask().getArray()
1124 resultsArr[np.isnan(resultsArr)] |= badBitsNan
1125 resultsArr[np.isnan(scienceExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1126 resultsArr[np.isnan(templateExposure.getMaskedImage().getImage().getArray())] |= badBitsNan
1128 results.subtractedExposure = results.D
1129 results.warpedExposure = templateExposure
1133 doWarping=
True, spatiallyVarying=
True, inImageSpace=
False,
1134 doPreConvolve=
False):
1135 raise NotImplementedError
1138 subtractAlgorithmRegistry.register(
'zogy', ZogyImagePsfMatchTask)
def _doConvolve
Convolve an Exposure with a decorrelation convolution kernel.
def computeDiffimImageSpace
def _computeVarAstGradients
def computeDiffimFourierSpace
def computeScorrImageSpace
def computeScorrFourierSpace