示例#1
0
 def testTicket2872(self):
     """Test that CoaddPsf.getAveragePosition() is always a position at which
     we can call computeImage().
     """
     cdelt = (0.2 * afwGeom.arcseconds).asDegrees()
     wcs = afwImage.makeWcs(
         afwCoord.IcrsCoord(afwGeom.Point2D(45.0, 45.0), afwGeom.degrees),
         afwGeom.Point2D(50, 50), cdelt, 0.0, 0.0, cdelt)
     kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel()
     psf1 = measAlg.KernelPsf(kernel, afwGeom.Point2D(0, 50))
     psf2 = measAlg.KernelPsf(kernel, afwGeom.Point2D(100, 50))
     record1 = self.mycatalog.addNew()
     record1.setPsf(psf1)
     record1.setWcs(wcs)
     record1.setD(self.weightKey, 1.0)
     record1.setBBox(
         afwGeom.Box2I(afwGeom.Point2I(-40, 0), afwGeom.Point2I(40, 100)))
     record2 = self.mycatalog.addNew()
     record2.setPsf(psf2)
     record2.setWcs(wcs)
     record2.setD(self.weightKey, 1.0)
     record2.setBBox(
         afwGeom.Box2I(afwGeom.Point2I(60, 0), afwGeom.Point2I(140, 100)))
     coaddPsf = measAlg.CoaddPsf(self.mycatalog, wcs)
     naiveAvgPos = afwGeom.Point2D(50, 50)
     with self.assertRaises(pexExceptions.InvalidParameterError):
         coaddPsf.computeKernelImage(naiveAvgPos)
     # important test is that this doesn't throw:
     coaddPsf.computeKernelImage()
示例#2
0
 def testTicket2872(self):
     """Test that CoaddPsf.getAveragePosition() is always a position at which
     we can call computeImage().
     """
     schema = afwTable.ExposureTable.makeMinimalSchema()
     weightKey = schema.addField("weight",
                                 type=float,
                                 doc="photometric weight")
     catalog = afwTable.ExposureCatalog(schema)
     cdelt = (0.2 * afwGeom.arcseconds).asDegrees()
     wcs = afwImage.makeWcs(
         afwCoord.IcrsCoord(afwGeom.Point2D(45.0, 45.0), afwGeom.degrees),
         afwGeom.Point2D(50, 50), cdelt, 0.0, 0.0, cdelt)
     kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel()
     psf1 = measAlg.KernelPsf(kernel, afwGeom.Point2D(0, 50))
     psf2 = measAlg.KernelPsf(kernel, afwGeom.Point2D(100, 50))
     record1 = catalog.addNew()
     record1.setPsf(psf1)
     record1.setWcs(wcs)
     record1.setD(weightKey, 1.0)
     record1.setBBox(
         afwGeom.Box2I(afwGeom.Point2I(-40, 0), afwGeom.Point2I(40, 100)))
     record2 = catalog.addNew()
     record2.setPsf(psf2)
     record2.setWcs(wcs)
     record2.setD(weightKey, 1.0)
     record2.setBBox(
         afwGeom.Box2I(afwGeom.Point2I(60, 0), afwGeom.Point2I(140, 100)))
     coaddPsf = measAlg.CoaddPsf(catalog, wcs)
     naiveAvgPos = afwGeom.Point2D(50, 50)
     self.assertRaises(pexExceptions.InvalidParameterError,
                       coaddPsf.computeKernelImage, naiveAvgPos)
     # important test is that this doesn't throw:
     coaddPsf.computeKernelImage()
示例#3
0
 def testTicket2872(self):
     """Test that CoaddPsf.getAveragePosition() is always a position at which
     we can call computeImage().
     """
     scale = 0.2 * lsst.geom.arcseconds
     cdMatrix = afwGeom.makeCdMatrix(scale=scale)
     wcs = afwGeom.makeSkyWcs(
         crpix=lsst.geom.Point2D(50, 50),
         crval=lsst.geom.SpherePoint(45.0, 45.0, lsst.geom.degrees),
         cdMatrix=cdMatrix,
     )
     kernel = measAlg.DoubleGaussianPsf(7, 7, 2.0).getKernel()
     psf1 = measAlg.KernelPsf(kernel, lsst.geom.Point2D(0, 50))
     psf2 = measAlg.KernelPsf(kernel, lsst.geom.Point2D(100, 50))
     record1 = self.mycatalog.addNew()
     record1.setPsf(psf1)
     record1.setWcs(wcs)
     record1.setD(self.weightKey, 1.0)
     record1.setBBox(
         lsst.geom.Box2I(lsst.geom.Point2I(-40, 0),
                         lsst.geom.Point2I(40, 100)))
     record2 = self.mycatalog.addNew()
     record2.setPsf(psf2)
     record2.setWcs(wcs)
     record2.setD(self.weightKey, 1.0)
     record2.setBBox(
         lsst.geom.Box2I(lsst.geom.Point2I(60, 0),
                         lsst.geom.Point2I(140, 100)))
     coaddPsf = measAlg.CoaddPsf(self.mycatalog, wcs)
     naiveAvgPos = lsst.geom.Point2D(50, 50)
     with self.assertRaises(pexExceptions.InvalidParameterError):
         coaddPsf.computeKernelImage(naiveAvgPos)
     # important test is that this doesn't throw:
     coaddPsf.computeKernelImage(coaddPsf.getAveragePosition())
示例#4
0
def performALZCExposureCorrection(templateExposure, exposure,
                                  subtractedExposure, psfMatchingKernel, log):
    kimg = alPsfMatchingKernelToArray(psfMatchingKernel, subtractedExposure)
    # Compute the images' sigmas (sqrt of variance)
    sig1 = templateExposure.getMaskedImage().getVariance().getArray()
    sig2 = exposure.getMaskedImage().getVariance().getArray()
    sig1squared, _, _, _ = computeClippedImageStats(sig1)
    sig2squared, _, _, _ = computeClippedImageStats(sig2)
    corrKernel = computeDecorrelationKernel(kimg, sig1squared, sig2squared)
    # Eventually, use afwMath.convolve(), but for now just use scipy.
    log.info("ALZC: Convolving.")
    pci, _ = doConvolve(
        subtractedExposure.getMaskedImage().getImage().getArray(), corrKernel)
    subtractedExposure.getMaskedImage().getImage().getArray()[:, :] = pci
    log.info("ALZC: Finished with convolution.")

    # Compute the subtracted exposure's updated psf
    psf = afwPsfToArray(subtractedExposure.getPsf(),
                        subtractedExposure)  # .computeImage().getArray()
    psfc = computeCorrectedDiffimPsf(corrKernel,
                                     psf,
                                     svar=sig1squared,
                                     tvar=sig2squared)
    psfcI = afwImage.ImageD(
        subtractedExposure.getPsf().computeKernelImage().getBBox())
    psfcI.getArray()[:, :] = psfc
    psfcK = afwMath.FixedKernel(psfcI)
    psfNew = measAlg.KernelPsf(psfcK)
    subtractedExposure.setPsf(psfNew)
    return subtractedExposure, corrKernel
示例#5
0
    def testKernelPsf(self):
        """Test creating a Psf from a Kernel"""

        x,y = 10.4999, 10.4999
        ksize = 15
        sigma1 = 1
        #
        # Make a PSF from that kernel
        #
        kPsf = measAlg.KernelPsf(afwMath.AnalyticKernel(ksize, ksize,
                                                        afwMath.GaussianFunction2D(sigma1, sigma1)))

        kIm = kPsf.computeImage(afwGeom.Point2D(x, y))
        #
        # And now via the dgPsf model
        #
        dgPsf = measAlg.DoubleGaussianPsf(ksize, ksize, sigma1)
        dgIm = dgPsf.computeImage(afwGeom.Point2D(x, y))
        #
        # Check that they're the same
        #
        diff = type(kIm)(kIm, True); diff -= dgIm
        stats = afwMath.makeStatistics(diff, afwMath.MAX | afwMath.MIN)
        self.assertAlmostEqual(stats.getValue(afwMath.MAX), 0.0, places=16)
        self.assertAlmostEqual(stats.getValue(afwMath.MIN), 0.0, places=16)

        if display:
            mos = displayUtils.Mosaic()
            mos.setBackground(-0.1)
            ds9.mtv(mos.makeMosaic([kIm, dgIm, diff], mode="x"), frame=1)
示例#6
0
 def makeHSCExposure(self, galData, psfData, pixScale, variance):
     ny, nx = galData.shape
     exposure = afwImg.ExposureF(nx, ny)
     exposure.getMaskedImage().getImage().getArray()[:, :] = galData
     exposure.getMaskedImage().getVariance().getArray()[:, :] = variance
     #Set the PSF
     ngridPsf = psfData.shape[0]
     psfLsst = afwImg.ImageF(ngridPsf, ngridPsf)
     psfLsst.getArray()[:, :] = psfData
     psfLsst = psfLsst.convertD()
     kernel = afwMath.FixedKernel(psfLsst)
     kernelPSF = meaAlg.KernelPsf(kernel)
     exposure.setPsf(kernelPSF)
     #prepare the wcs
     #Rotation
     cdelt = (pixScale * afwGeom.arcseconds)
     CD = afwGeom.makeCdMatrix(cdelt, afwGeom.Angle(0.))  #no rotation
     #wcs
     crval = afwGeom.SpherePoint(afwGeom.Angle(0., afwGeom.degrees),
                                 afwGeom.Angle(0., afwGeom.degrees))
     #crval   =   afwCoord.IcrsCoord(0.*afwGeom.degrees, 0.*afwGeom.degrees) # hscpipe6
     crpix = afwGeom.Point2D(0.0, 0.0)
     dataWcs = afwGeom.makeSkyWcs(crpix, crval, CD)
     exposure.setWcs(dataWcs)
     #prepare the frc
     dataCalib = afwImg.makePhotoCalibFromCalibZeroPoint(63095734448.0194)
     exposure.setPhotoCalib(dataCalib)
     return exposure
示例#7
0
    def makeExposure(imgArray, psfArray, imgVariance):
        """! Convert an image numpy.array and corresponding PSF numpy.array into an exposure.

        Add the (constant) variance plane equal to `imgVariance`.

        @param imgArray 2-d numpy.array containing the image
        @param psfArray 2-d numpy.array containing the PSF image
        @param imgVariance variance of input image
        @return a new exposure containing the image, PSF and desired variance plane
        """
        # All this code to convert the template image array/psf array into an exposure.
        bbox = geom.Box2I(
            geom.Point2I(0, 0),
            geom.Point2I(imgArray.shape[1] - 1, imgArray.shape[0] - 1))
        im1ex = afwImage.ExposureD(bbox)
        im1ex.getMaskedImage().getImage().getArray()[:, :] = imgArray
        im1ex.getMaskedImage().getVariance().getArray()[:, :] = imgVariance
        psfBox = geom.Box2I(geom.Point2I(-12, -12),
                            geom.Point2I(12, 12))  # a 25x25 pixel psf
        psf = afwImage.ImageD(psfBox)
        psfBox.shift(geom.Extent2I(size[0] // 2, size[1] // 2))
        im1_psf_sub = psfArray[psfBox.getMinX():psfBox.getMaxX() + 1,
                               psfBox.getMinY():psfBox.getMaxY() + 1]
        psf.getArray()[:, :] = im1_psf_sub
        psfK = afwMath.FixedKernel(psf)
        psfNew = measAlg.KernelPsf(psfK)
        im1ex.setPsf(psfNew)
        wcs = makeWcs()
        im1ex.setWcs(wcs)
        return im1ex
示例#8
0
    def makeExposure(imgArray, psfArray, imgVariance):
        """Convert an image and corresponding PSF into an exposure.

        Set the (constant) variance plane equal to ``imgVariance``.

        Parameters
        ----------
        imgArray : `numpy.ndarray`
            2D array containing the image.
        psfArray : `numpy.ndarray`
            2D array containing the PSF image.
        imgVariance : `float` or `numpy.ndarray`
            Set the variance plane to this value. If an array, must be broadcastable to ``imgArray.shape``.

        Returns
        -------
        im1ex : `lsst.afw.image.Exposure`
            The new exposure.
        """
        # All this code to convert the template image array/psf array into an exposure.
        bbox = geom.Box2I(geom.Point2I(0, 0), geom.Point2I(imgArray.shape[1] - 1, imgArray.shape[0] - 1))
        im1ex = afwImage.ExposureD(bbox)
        im1ex.image.array[:, :] = imgArray
        im1ex.variance.array[:, :] = imgVariance
        psfBox = geom.Box2I(geom.Point2I(-12, -12), geom.Point2I(12, 12))  # a 25x25 pixel psf
        psf = afwImage.ImageD(psfBox)
        psfBox.shift(geom.Extent2I(-(-size[0]//2), -(-size[1]//2)))  # -N//2 != -(N//2) for odd numbers
        im1_psf_sub = psfArray[psfBox.getMinY():psfBox.getMaxY() + 1, psfBox.getMinX():psfBox.getMaxX() + 1]
        psf.getArray()[:, :] = im1_psf_sub
        psfK = afwMath.FixedKernel(psf)
        psfNew = measAlg.KernelPsf(psfK)
        im1ex.setPsf(psfNew)
        wcs = makeWcs()
        im1ex.setWcs(wcs)
        return im1ex
示例#9
0
def doALdecorrelation(alTaskResult,
                      sig1squared=None,
                      sig2squared=None,
                      preConvKernel=None):
    kimg = alPsfMatchingKernelToArray(alTaskResult.psfMatchingKernel,
                                      alTaskResult.subtractedExposure)

    if preConvKernel is not None and kimg.shape[0] < preConvKernel.shape[0]:
        # This is likely brittle and may only work if both kernels are odd-shaped.
        #kimg[np.abs(kimg) < 1e-4] = np.sign(kimg)[np.abs(kimg) < 1e-4] * 1e-8
        #kimg -= kimg[0, 0]
        padSize0 = preConvKernel.shape[0] // 2 - kimg.shape[0] // 2
        padSize1 = preConvKernel.shape[1] // 2 - kimg.shape[1] // 2
        kimg = np.pad(kimg, ((padSize0, padSize0), (padSize1, padSize1)),
                      mode='constant',
                      constant_values=0)
        #kimg /= kimg.sum()
        #preConvKernel = preConvKernel[padSize0:-padSize0, padSize1:-padSize1]

    if sig1squared is None:
        sig1squared = computeVarianceMean(im1)
    if sig2squared is None:
        sig2squared = computeVarianceMean(im2)
    pck = computeDecorrelationKernel(kimg,
                                     sig1squared,
                                     sig2squared,
                                     preConvKernel=preConvKernel,
                                     delta=0.)
    #return kimg, preConvKernel, pck
    diffim, _ = doConvolve(alTaskResult.subtractedExposure,
                           pck,
                           use_scipy=False)

    # For some reason, border areas of img and variance planes can become infinite. Fix it.
    img = diffim.getMaskedImage().getImage().getArray()
    img[~np.isfinite(img)] = np.nan
    img = diffim.getMaskedImage().getVariance().getArray()
    img[~np.isfinite(img)] = np.nan
    # TBD: also need to update the mask as it is not (apparently) set correctly.

    psf = afwPsfToArray(alTaskResult.subtractedExposure.getPsf(),
                        img=alTaskResult.subtractedExposure)
    # NOTE! Need to compute the updated PSF including preConvKernel !!! This doesn't do it:
    psfc = computeCorrectedDiffimPsf(kimg,
                                     psf,
                                     tvar=sig1squared,
                                     svar=sig2squared)
    psfcI = afwImage.ImageD(psfc.shape[0], psfc.shape[1])
    psfcI.getArray()[:, :] = psfc
    psfcK = afwMath.FixedKernel(psfcI)
    psfNew = measAlg.KernelPsf(psfcK)
    diffim.setPsf(psfNew)

    alTaskResult.decorrelatedDiffim = diffim
    alTaskResult.preConvKernel = preConvKernel
    alTaskResult.decorrelationKernel = pck
    alTaskResult.kappaImg = kimg
    return alTaskResult
示例#10
0
 def _setNewPsf(self, exposure, psfArr):
     """Utility method to set an exposure's PSF when provided as a 2-d numpy.array
     """
     psfI = afwImage.ImageD(psfArr.shape[0], psfArr.shape[1])
     psfI.getArray()[:, :] = psfArr
     psfK = afwMath.FixedKernel(psfI)
     psfNew = measAlg.KernelPsf(psfK)
     exposure.setPsf(psfNew)
     return exposure
def makePsf(size, sigma1, mult1, sigma2, mult2):
    """
    make a Gaussian with one or two components.  Always square of dimensions size x size
    """
    array0 = makeGaussianArray(size, sigma1)
    array0 *= mult1
    array1 = makeGaussianArray(size, sigma2)
    array1 *= mult2
    kernel = lsst.afw.math.FixedKernel(lsst.afw.image.ImageD(array0 + array1))
    return measAlg.KernelPsf(kernel)
    def runMeasurement(self, algorithmName, imageid, x, y, v):
        """Run the measurement algorithm on an image"""
        # load the test image
        imgFile = os.path.join(self.dataDir, "image.%d.fits" % imageid)
        img = afwImage.ImageF(imgFile)
        img -= self.bkgd
        nx, ny = img.getWidth(), img.getHeight()
        msk = afwImage.Mask(geom.Extent2I(nx, ny), 0x0)
        var = afwImage.ImageF(geom.Extent2I(nx, ny), v)
        mimg = afwImage.MaskedImageF(img, msk, var)
        msk.getArray()[:] = np.where(np.fabs(img.getArray()) < 1.0e-8, msk.getPlaneBitMask("BAD"), 0)

        # Put it in a bigger image, in case it matters
        big = afwImage.MaskedImageF(self.offset + mimg.getDimensions())
        big.getImage().set(0)
        big.getMask().set(0)
        big.getVariance().set(v)
        subBig = afwImage.MaskedImageF(big, geom.Box2I(big.getXY0() + self.offset, mimg.getDimensions()))
        subBig <<= mimg
        mimg = big
        mimg.setXY0(self.xy0)

        exposure = afwImage.makeExposure(mimg)
        cdMatrix = np.array([1.0/(2.53*3600.0), 0.0, 0.0, 1.0/(2.53*3600.0)])
        cdMatrix.shape = (2, 2)
        exposure.setWcs(afwGeom.makeSkyWcs(crpix=geom.Point2D(1.0, 1.0),
                                           crval=geom.SpherePoint(0, 0, geom.degrees),
                                           cdMatrix=cdMatrix))

        # load the corresponding test psf
        psfFile = os.path.join(self.dataDir, "psf.%d.fits" % imageid)
        psfImg = afwImage.ImageD(psfFile)
        psfImg -= self.bkgd

        kernel = afwMath.FixedKernel(psfImg)
        kernelPsf = algorithms.KernelPsf(kernel)
        exposure.setPsf(kernelPsf)

        # perform the shape measurement
        msConfig = base.SingleFrameMeasurementConfig()
        alg = base.SingleFramePlugin.registry[algorithmName].PluginClass.AlgClass
        control = base.SingleFramePlugin.registry[algorithmName].PluginClass.ConfigClass().makeControl()
        msConfig.algorithms.names = [algorithmName]
        # Note: It is essential to remove the floating point part of the position for the
        # Algorithm._apply.  Otherwise, when the PSF is realised it will have been warped
        # to account for the sub-pixel offset and we won't get *exactly* this PSF.
        plugin, table = makePluginAndCat(alg, algorithmName, control, centroid="centroid")
        center = geom.Point2D(int(x), int(y)) + geom.Extent2D(self.offset + geom.Extent2I(self.xy0))
        source = table.makeRecord()
        source.set("centroid_x", center.getX())
        source.set("centroid_y", center.getY())
        source.setFootprint(afwDetection.Footprint(afwGeom.SpanSet(exposure.getBBox(afwImage.PARENT))))
        plugin.measure(source, exposure)

        return source
示例#13
0
def createImageAndKernel(sigma, psfSize, image):
    function = afwMath.GaussianFunction2D(sigma, sigma)
    kernel = afwMath.AnalyticKernel(psfSize, psfSize, function)
    psf = measAlg.KernelPsf(kernel)

    cim = afwImage.ImageF(image.getDimensions())
    afwMath.convolve(cim, image, kernel, True)

    # Trim off the border pixels
    bbox = kernel.shrinkBBox(cim.getBBox(afwImage.LOCAL))
    cim = afwImage.ImageF(cim, bbox, afwImage.LOCAL)
    cim.setXY0(0, 0)

    return cim, psf
示例#14
0
 def _growPsf(exp, extraPix=(2, 3)):
     bbox = exp.getBBox()
     center = ((bbox.getBeginX() + bbox.getEndX()) // 2., (bbox.getBeginY() + bbox.getEndY()) // 2.)
     center = geom.Point2D(center[0], center[1])
     kern = exp.getPsf().computeKernelImage(center).convertF()
     kernSize = kern.getDimensions()
     paddedKern = afwImage.ImageF(kernSize[0] + extraPix[0], kernSize[1] + extraPix[1])
     bboxToPlace = geom.Box2I(geom.Point2I((kernSize[0] + extraPix[0] - kern.getWidth()) // 2,
                                           (kernSize[1] + extraPix[1] - kern.getHeight()) // 2),
                              kern.getDimensions())
     paddedKern.assign(kern, bboxToPlace)
     fixedKern = afwMath.FixedKernel(paddedKern.convertD())
     psfNew = measAlg.KernelPsf(fixedKern, center)
     exp.setPsf(psfNew)
     return exp
示例#15
0
    def makeKernelPsfFromArray(A):
        """Create a non spatially varying PSF from a `numpy.ndarray`.

        Parameters
        ----------
        A : `numpy.ndarray`
            2D array to use as the new psf image. The pixels are copied.

        Returns
        -------
        psfNew : `lsst.meas.algorithms.KernelPsf`
            The constructed PSF.
        """
        psfImg = afwImage.ImageD(A.astype(np.float64, copy=True), deep=False)
        psfNew = measAlg.KernelPsf(afwMath.FixedKernel(psfImg))
        return psfNew
示例#16
0
    def getCoaddPsf(self, exposure):
        import lsst.afw.table as afwTable
        import lsst.afw.image as afwImage
        import lsst.afw.math as afwMath
        import lsst.afw.geom as afwGeom
        import lsst.meas.algorithms as measAlg

        schema = afwTable.ExposureTable.makeMinimalSchema()
        schema.addField("weight", type="D", doc="Coadd weight")
        mycatalog = afwTable.ExposureCatalog(schema)

        wcsref = exposure.getWcs()
        extentX = int(exposure.getWidth() * 0.05)
        extentY = int(exposure.getHeight() * 0.05)
        ind = 0
        for x in np.linspace(extentX, exposure.getWidth() - extentX, 10):
            for y in np.linspace(extentY, exposure.getHeight() - extentY, 10):
                x = int(x)
                y = int(y)
                image = self.getImage(x, y)

                psf = afwImage.ImageD(image.shape[0], image.shape[1])
                psf.getArray()[:, :] = image
                psfK = afwMath.FixedKernel(psf)
                psf = measAlg.KernelPsf(psfK)

                record = mycatalog.getTable().makeRecord()
                record.setPsf(psf)
                record.setWcs(wcsref)

                bbox = afwGeom.Box2I(
                    afwGeom.Point2I(
                        int(np.floor(x - extentX)) - 5,
                        int(np.floor(y - extentY)) - 5),
                    afwGeom.Point2I(
                        int(np.floor(x + extentX)) + 5,
                        int(np.floor(y + extentY)) + 5))
                record.setBBox(bbox)
                record['weight'] = 1.0
                record['id'] = ind
                ind += 1
                mycatalog.append(record)

        # create the coaddpsf
        psf = measAlg.CoaddPsf(mycatalog, wcsref, 'weight')
        return psf
示例#17
0
    def testKernelPsf(self):
        """Test creating a Psf from a Kernel.
        """
        x, y = 10.4999, 10.4999
        ksize = 15
        sigma1 = 1
        #
        # Make a PSF from that kernel
        #
        kPsf = measAlg.KernelPsf(
            afwMath.AnalyticKernel(ksize, ksize,
                                   afwMath.GaussianFunction2D(sigma1, sigma1)))

        kIm = kPsf.computeImage(lsst.geom.Point2D(x, y))
        #
        # And now via the dgPsf model
        #
        dgPsf = measAlg.DoubleGaussianPsf(ksize, ksize, sigma1)
        dgIm = dgPsf.computeImage(lsst.geom.Point2D(x, y))
        #
        # Check that they're the same
        #
        diff = type(kIm)(kIm, True)
        diff -= dgIm
        stats = afwMath.makeStatistics(diff, afwMath.MAX | afwMath.MIN)
        self.assertAlmostEqual(stats.getValue(afwMath.MAX), 0.0, places=16)
        self.assertAlmostEqual(stats.getValue(afwMath.MIN), 0.0, places=16)

        for pad in [-2, 4, 0]:
            resizedKPsf = kPsf.resized(ksize + pad, ksize + pad)
            self.assertEqual(resizedKPsf.computeBBox().getDimensions(),
                             lsst.geom.Extent2I(ksize + pad, ksize + pad))
            self.assertEqual(resizedKPsf.getKernel().getKernelParameters(),
                             kPsf.getKernel().getKernelParameters())
            self._compareKernelImages(kPsf, resizedKPsf)
        if display:
            mos = afwDisplay.utils.Mosaic()
            mos.setBackground(-0.1)
            afwDisplay.Display(frame=1).mtv(mos.makeMosaic([kIm, dgIm, diff],
                                                           mode="x"),
                                            title=self._testMethodName +
                                            ": mosaic")
示例#18
0
    def makeLsstExposure(galData, psfData, pixScale, variance):
        """
        make an LSST exposure object

        Parameters:
            galData (ndarray):  array of galaxy image
            psfData (ndarray):  array of PSF image
            pixScale (float):   pixel scale
            variance (float):   noise variance

        Returns:
            exposure:   LSST exposure object
        """
        if not with_lsst:
            raise ImportError('Do not have lsstpipe!')
        ny, nx = galData.shape
        exposure = afwImg.ExposureF(nx, ny)
        exposure.getMaskedImage().getImage().getArray()[:, :] = galData
        exposure.getMaskedImage().getVariance().getArray()[:, :] = variance
        #Set the PSF
        ngridPsf = psfData.shape[0]
        psfLsst = afwImg.ImageF(ngridPsf, ngridPsf)
        psfLsst.getArray()[:, :] = psfData
        psfLsst = psfLsst.convertD()
        kernel = afwMath.FixedKernel(psfLsst)
        kernelPSF = meaAlg.KernelPsf(kernel)
        exposure.setPsf(kernelPSF)
        #prepare the wcs
        #Rotation
        cdelt = (pixScale * afwGeom.arcseconds)
        CD = afwGeom.makeCdMatrix(cdelt, afwGeom.Angle(0.))  #no rotation
        #wcs
        crval = afwGeom.SpherePoint(afwGeom.Angle(0., afwGeom.degrees),
                                    afwGeom.Angle(0., afwGeom.degrees))
        #crval   =   afwCoord.IcrsCoord(0.*afwGeom.degrees, 0.*afwGeom.degrees) # hscpipe6
        crpix = afwGeom.Point2D(0.0, 0.0)
        dataWcs = afwGeom.makeSkyWcs(crpix, crval, CD)
        exposure.setWcs(dataWcs)
        #prepare the frc
        dataCalib = afwImg.makePhotoCalibFromCalibZeroPoint(63095734448.0194)
        exposure.setPhotoCalib(dataCalib)
        return exposure
示例#19
0
def performALZCExposureCorrection(templateExposure, exposure,
                                  subtractedExposure, psfMatchingKernel, log):
    import lsst.afw.image as afwImage
    import lsst.meas.algorithms as measAlg
    import lsst.afw.math as afwMath

    spatialKernel = psfMatchingKernel
    kimg = afwImage.ImageD(spatialKernel.getDimensions())
    bbox = subtractedExposure.getBBox()
    xcen = (bbox.getBeginX() + bbox.getEndX()) / 2.
    ycen = (bbox.getBeginY() + bbox.getEndY()) / 2.
    spatialKernel.computeImage(kimg, True, xcen, ycen)
    # Compute the images' sigmas (sqrt of variance)
    sig1 = templateExposure.getMaskedImage().getVariance().getArray()
    sig2 = exposure.getMaskedImage().getVariance().getArray()
    sig1squared, _ = computeClippedImageStats(sig1)
    sig2squared, _ = computeClippedImageStats(sig2)
    sig1 = np.sqrt(sig1squared)
    sig2 = np.sqrt(sig2squared)
    corrKernel = computeCorrectionKernelALZC(kimg.getArray(),
                                             sig1=sig1,
                                             sig2=sig2)
    # Eventually, use afwMath.convolve(), but for now just use scipy.
    log.info("ALZC: Convolving.")
    pci, _ = doConvolve(
        subtractedExposure.getMaskedImage().getImage().getArray(), corrKernel)
    subtractedExposure.getMaskedImage().getImage().getArray()[:, :] = pci
    log.info("ALZC: Finished with convolution.")

    # Compute the subtracted exposure's updated psf
    psf = subtractedExposure.getPsf().computeImage().getArray()
    psfc = computeCorrectedDiffimPsfALZC(corrKernel, psf, sig1=sig1, sig2=sig2)
    psfcI = afwImage.ImageD(
        subtractedExposure.getPsf().computeImage().getBBox())
    psfcI.getArray()[:, :] = psfc
    psfcK = afwMath.FixedKernel(psfcI)
    psfNew = measAlg.KernelPsf(psfcK)
    subtractedExposure.setPsf(psfNew)
    return subtractedExposure, corrKernel
示例#20
0
    def asAfwExposure(self):
        import lsst.afw.image as afwImage
        import lsst.afw.math as afwMath
        import lsst.afw.geom as afwGeom
        import lsst.meas.algorithms as measAlg

        bbox = afwGeom.Box2I(
            afwGeom.Point2I(0, 0),
            afwGeom.Point2I(self.im.shape[0] - 1, self.im.shape[1] - 1))
        im1ex = afwImage.ExposureF(bbox)
        im1ex.getMaskedImage().getImage().getArray()[:, :] = self.im
        im1ex.getMaskedImage().getVariance().getArray()[:, :] = self.var
        psfShape = self.psf.shape[0] // 2
        psfBox = afwGeom.Box2I(afwGeom.Point2I(-psfShape, -psfShape),
                               afwGeom.Point2I(psfShape, psfShape))
        psf = afwImage.ImageD(psfBox)
        psf.getArray()[:, :] = self.psf
        psfK = afwMath.FixedKernel(psf)
        psfNew = measAlg.KernelPsf(psfK)
        im1ex.setPsf(psfNew)
        wcs = makeWcs(naxis1=self.im.shape[0], naxis2=self.im.shape[1])
        im1ex.setWcs(wcs)
        return im1ex
示例#21
0
    def run(self,
            subExposure,
            expandedSubExposure,
            fullBBox,
            template,
            science,
            alTaskResult=None,
            psfMatchingKernel=None,
            preConvKernel=None,
            returnDiffimPsf=False,
            **kwargs):
        """Perform decorrelation operation on `subExposure`, using
        `expandedSubExposure` to allow for invalid edge pixels arising from
        convolutions.

        This method performs A&L decorrelation on `subExposure` using
        local measures for image variances and PSF. `subExposure` is a
        sub-exposure of the non-decorrelated A&L diffim. It also
        requires the corresponding sub-exposures of the template
        (`template`) and science (`science`) exposures.

        Parameters
        ----------
        subExposure : afw.Exposure
            the sub-exposure of the diffim
        expandedSubExposure : afw.Exposure
            the expanded sub-exposure upon which to operate
        fullBBox : afwGeom.BoundingBox
            the bounding box of the original exposure
        template : afw.Exposure
            the corresponding sub-exposure of the template exposure
        science : afw.Exposure
            the corresponding sub-exposure of the science exposure
        alTaskResult : pipeBase.Struct
            the result of A&L image differencing on `science` and
            `template`, importantly containing the resulting
            `psfMatchingKernel`. Can be `None`, only if
            `psfMatchingKernel` is not `None`.
        psfMatchingKernel : Alternative parameter for passing the
            A&L `psfMatchingKernel` directly.
        kwargs :
            additional keyword arguments propagated from
            `ImageMapReduceTask.run`.

        Returns
        -------
        A `pipeBase.Struct containing the result of the `subExposure`
        processing, labelled 'subExposure'. It also returns the
        'decorrelationKernel', although that currently is not used.

        Notes
        -----
        This `run` method accepts parameters identical to those of
        `ImageMapperSubtask.run`, since it is called from the
        `ImageMapperTask`.  See that class for more information.
        """
        templateExposure = template  # input template
        scienceExposure = science  # input science image
        if alTaskResult is None and psfMatchingKernel is None:
            raise ValueError(
                'Both alTaskResult and psfMatchingKernel cannot be None')
        psfMatchingKernel = alTaskResult.psfMatchingKernel if alTaskResult is not None else psfMatchingKernel

        # subExp and expandedSubExp are subimages of the (un-decorrelated) diffim!
        # So here we compute corresponding subimages of templateExposure and scienceExposure
        subExp2 = scienceExposure.Factory(scienceExposure,
                                          expandedSubExposure.getBBox())
        subExp1 = templateExposure.Factory(templateExposure,
                                           expandedSubExposure.getBBox())

        # Prevent too much log INFO verbosity from DecorrelateALKernelTask.run
        #logLevel = self.log.getLevel()
        #self.log.setLevel(lsst.log.WARN)
        #res = ipDiffim.DecorrelateALKernelTask.run(self, subExp2, subExp1, expandedSubExposure,
        #                                           psfMatchingKernel)
        svar = ipDiffim.DecorrelateALKernelTask.computeVarianceMean(
            self, subExp2)
        tvar = ipDiffim.DecorrelateALKernelTask.computeVarianceMean(
            self, subExp1)

        kimg = afwImage.ImageD(psfMatchingKernel.getDimensions())
        bbox = subExposure.getBBox()
        xcen = (bbox.getBeginX() + bbox.getEndX()) / 2.
        ycen = (bbox.getBeginY() + bbox.getEndY()) / 2.
        psfMatchingKernel.computeImage(kimg, True, xcen, ycen)
        kernel = ipDiffim.DecorrelateALKernelTask._computeDecorrelationKernel(
            kappa=kimg.getArray(), svar=svar, tvar=tvar)

        if not returnDiffimPsf:
            kernelImg = afwImage.ImageD(kernel.shape[0], kernel.shape[1])
            kernelImg.getArray()[:, :] = kernel
            kern = afwMath.FixedKernel(kernelImg)
            maxloc = np.unravel_index(np.argmax(kernel), kernel.shape)
            kern.setCtrX(maxloc[0])
            kern.setCtrY(maxloc[1])

        else:  # Compute the subtracted exposure's updated psf
            psf = subExposure.getPsf().computeImage(afwGeom.Point2D(
                xcen, ycen)).getArray()
            psfc = ipDiffim.DecorrelateALKernelTask.computeCorrectedDiffimPsf(
                kernel, psf, svar=svar, tvar=tvar)
            psfcI = afwImage.ImageD(psfc.shape[0], psfc.shape[1])
            psfcI.getArray()[:, :] = psfc
            kern = afwMath.FixedKernel(psfcI)

        psf = measAlg.KernelPsf(kern)
        out = pipeBase.Struct(psf=psf, bbox=subExposure.getBBox())
        return out
示例#22
0
def arrayToAfwPsf(array):
    psfcK = arrayToAfwKernel(array)
    psfNew = measAlg.KernelPsf(psfcK)
    return psfNew
示例#23
0
    def run(self, exposure, templateExposure, subtractedExposure, psfMatchingKernel,
            preConvKernel=None, xcen=None, ycen=None, svar=None, tvar=None):
        """Perform decorrelation of an image difference exposure.

        Decorrelates the diffim due to the convolution of the templateExposure with the
        A&L PSF matching kernel. Currently can accept a spatially varying matching kernel but in
        this case it simply uses a static kernel from the center of the exposure. The decorrelation
        is described in [DMTN-021, Equation 1](http://dmtn-021.lsst.io/#equation-1), where
        `exposure` is I_1; templateExposure is I_2; `subtractedExposure` is D(k);
        `psfMatchingKernel` is kappa; and svar and tvar are their respective
        variances (see below).

        Parameters
        ----------
        exposure : `lsst.afw.image.Exposure`
            The original science exposure (before `preConvKernel` applied) used for PSF matching.
        templateExposure : `lsst.afw.image.Exposure`
            The original template exposure (before matched to the science exposure
            by `psfMatchingKernel`) warped into the science exposure dimensions. Always the PSF of the
            `templateExposure` should be matched to the PSF of `exposure`, see notes below.
        subtractedExposure :
            the subtracted exposure produced by
            `ip_diffim.ImagePsfMatchTask.subtractExposures()`. The `subtractedExposure` must
            inherit its PSF from `exposure`, see notes below.
        psfMatchingKernel :
            An (optionally spatially-varying) PSF matching kernel produced
            by `ip_diffim.ImagePsfMatchTask.subtractExposures()`
        preConvKernel :
            if not None, then the `exposure` was pre-convolved with this kernel
        xcen : `float`, optional
            X-pixel coordinate to use for computing constant matching kernel to use
            If `None` (default), then use the center of the image.
        ycen : `float`, optional
            Y-pixel coordinate to use for computing constant matching kernel to use
            If `None` (default), then use the center of the image.
        svar : `float`, optional
            Image variance for science image
            If `None` (default) then compute the variance over the entire input science image.
        tvar : `float`, optional
            Image variance for template image
            If `None` (default) then compute the variance over the entire input template image.

        Returns
        -------
        result : `lsst.pipe.base.Struct`
            - ``correctedExposure`` : the decorrelated diffim

        Notes
        -----
        The `subtractedExposure` is NOT updated. The returned `correctedExposure` has an updated but
        spatially fixed PSF. It is calculated as the center of image PSF corrected by the center of
        image matching kernel.

        In this task, it is _always_ the `templateExposure` that was matched to the `exposure`
        by `psfMatchingKernel`. Swap arguments accordingly if actually the science exposure was matched
        to a co-added template. In this case, tvar > svar typically occurs.

        The `templateExposure` and `exposure` image dimensions must be the same.

        Here we currently convert a spatially-varying matching kernel into a constant kernel,
        just by computing it at the center of the image (tickets DM-6243, DM-6244).

        We are also using a constant accross-the-image measure of sigma (sqrt(variance)) to compute
        the decorrelation kernel.

        TODO DM-23857 As part of the spatially varying correction implementation
        consider whether returning a Struct is still necessary.
        """
        spatialKernel = psfMatchingKernel
        kimg = afwImage.ImageD(spatialKernel.getDimensions())
        bbox = subtractedExposure.getBBox()
        if xcen is None:
            xcen = (bbox.getBeginX() + bbox.getEndX()) / 2.
        if ycen is None:
            ycen = (bbox.getBeginY() + bbox.getEndY()) / 2.
        self.log.info("Using matching kernel computed at (%d, %d)", xcen, ycen)
        spatialKernel.computeImage(kimg, True, xcen, ycen)

        if svar is None:
            svar = self.computeVarianceMean(exposure)
        if tvar is None:
            tvar = self.computeVarianceMean(templateExposure)
        self.log.info("Variance (science, template): (%f, %f)", svar, tvar)

        # Should not happen unless entire image has been masked, which could happen
        # if this is a small subimage of the main exposure. In this case, just return a full NaN
        # exposure
        if np.isnan(svar) or np.isnan(tvar):
            # Double check that one of the exposures is all NaNs
            if (np.all(np.isnan(exposure.image.array))
                    or np.all(np.isnan(templateExposure.image.array))):
                self.log.warn('Template or science image is entirely NaNs: skipping decorrelation.')
                outExposure = subtractedExposure.clone()
                return pipeBase.Struct(correctedExposure=outExposure, )

        # The maximal correction value converges to sqrt(tvar/svar).
        # Correction divergence warning if the correction exceeds 4 orders of magnitude.
        tOverSVar = tvar/svar
        if tOverSVar > 1e8:
            self.log.warn("Diverging correction: science image variance is much smaller than template"
                          f", tvar/svar:{tOverSVar:.2e}")

        oldVarMean = self.computeVarianceMean(subtractedExposure)
        self.log.info("Variance (uncorrected diffim): %f", oldVarMean)

        if preConvKernel is not None:
            self.log.info('Using a pre-convolution kernel as part of decorrelation correction.')
            kimg2 = afwImage.ImageD(preConvKernel.getDimensions())
            preConvKernel.computeImage(kimg2, False)
            pckArr = kimg2.array

        kArr = kimg.array
        diffExpArr = subtractedExposure.image.array
        psfImg = subtractedExposure.getPsf().computeKernelImage(geom.Point2D(xcen, ycen))
        psfDim = psfImg.getDimensions()
        psfArr = psfImg.array

        # Determine the common shape
        if preConvKernel is None:
            self.computeCommonShape(kArr.shape, psfArr.shape, diffExpArr.shape)
            corrft = self.computeCorrection(kArr, svar, tvar)
        else:
            self.computeCommonShape(pckArr.shape, kArr.shape,
                                    psfArr.shape, diffExpArr.shape)
            corrft = self.computeCorrection(kArr, svar, tvar, preConvArr=pckArr)

        diffExpArr = self.computeCorrectedImage(corrft, diffExpArr)
        corrPsfArr = self.computeCorrectedDiffimPsf(corrft, psfArr)

        psfcI = afwImage.ImageD(psfDim)
        psfcI.array = corrPsfArr
        psfcK = afwMath.FixedKernel(psfcI)
        psfNew = measAlg.KernelPsf(psfcK)

        correctedExposure = subtractedExposure.clone()
        correctedExposure.image.array[...] = diffExpArr  # Allow for numpy type casting
        # The subtracted exposure variance plane is already correlated, we cannot propagate
        # it through another convolution; instead we need to use the uncorrelated originals
        # The whitening should scale it to svar + tvar on average
        varImg = correctedExposure.variance.array
        # Allow for numpy type casting
        varImg[...] = exposure.variance.array + templateExposure.variance.array
        correctedExposure.setPsf(psfNew)

        newVarMean = self.computeVarianceMean(correctedExposure)
        self.log.info(f"Variance (corrected diffim): {newVarMean:.2e}")

        # TODO DM-23857 As part of the spatially varying correction implementation
        # consider whether returning a Struct is still necessary.
        return pipeBase.Struct(correctedExposure=correctedExposure, )
示例#24
0
    def run(self,
            exposure,
            templateExposure,
            subtractedExposure,
            psfMatchingKernel,
            xcen=None,
            ycen=None,
            svar=None,
            tvar=None):
        """! Perform decorrelation of an image difference exposure.

        Decorrelates the diffim due to the convolution of the templateExposure with the
        A&L PSF matching kernel. Currently can accept a spatially varying matching kernel but in
        this case it simply uses a static kernel from the center of the exposure. The decorrelation
        is described in [DMTN-021, Equation 1](http://dmtn-021.lsst.io/#equation-1), where
        `exposure` is I_1; templateExposure is I_2; `subtractedExposure` is D(k);
        `psfMatchingKernel` is kappa; and svar and tvar are their respective
        variances (see below).

        @param[in] exposure the science afwImage.Exposure used for PSF matching
        @param[in] templateExposure the template afwImage.Exposure used for PSF matching
        @param[in] subtractedExposure the subtracted exposure produced by
        `ip_diffim.ImagePsfMatchTask.subtractExposures()`
        @param[in] psfMatchingKernel an (optionally spatially-varying) PSF matching kernel produced
        by `ip_diffim.ImagePsfMatchTask.subtractExposures()`
        @param[in] xcen X-pixel coordinate to use for computing constant matching kernel to use
        If `None` (default), then use the center of the image.
        @param[in] ycen Y-pixel coordinate to use for computing constant matching kernel to use
        If `None` (default), then use the center of the image.
        @param[in] svar image variance for science image
        If `None` (default) then compute the variance over the entire input science image.
        @param[in] tvar image variance for template image
        If `None` (default) then compute the variance over the entire input template image.

        @return a `pipeBase.Struct` containing:
            * `correctedExposure`: the decorrelated diffim
            * `correctionKernel`: the decorrelation correction kernel (which may be ignored)

        @note The `subtractedExposure` is NOT updated
        @note The returned `correctedExposure` has an updated PSF as well.
        @note Here we currently convert a spatially-varying matching kernel into a constant kernel,
        just by computing it at the center of the image (tickets DM-6243, DM-6244).
        @note We are also using a constant accross-the-image measure of sigma (sqrt(variance)) to compute
        the decorrelation kernel.
        @note Still TBD (ticket DM-6580): understand whether the convolution is correctly modifying
        the variance plane of the new subtractedExposure.
        """
        self.log.info("Starting.")
        kimg = None

        spatialKernel = psfMatchingKernel
        kimg = afwImage.ImageD(spatialKernel.getDimensions())
        bbox = subtractedExposure.getBBox()
        if xcen is None:
            xcen = (bbox.getBeginX() + bbox.getEndX()) / 2.
        if ycen is None:
            ycen = (bbox.getBeginY() + bbox.getEndY()) / 2.
        self.log.info("Using matching kernel computed at (%d, %d)" %
                      (xcen, ycen))
        spatialKernel.computeImage(kimg, True, xcen, ycen)

        if False:  # debug code to save spatially varying kernel for analysis
            import pickle
            import gzip
            pickle.dump(spatialKernel, gzip.GzipFile('spatialKernel.p.gz',
                                                     'wb'))

        if svar is None:
            svar = self.computeVarianceMean(exposure)
        self.log.info("Variance (science): %f" % svar)
        if tvar is None:
            tvar = self.computeVarianceMean(templateExposure)
        self.log.info("Variance (template): %f" % tvar)

        var = self.computeVarianceMean(subtractedExposure)
        self.log.info("Variance (uncorrected diffim): %f" % var)

        corrKernel = DecorrelateALKernelTask._computeDecorrelationKernel(
            kimg.getArray(), svar, tvar)
        self.log.info("Convolving.")
        correctedExposure, corrKern = DecorrelateALKernelTask._doConvolve(
            subtractedExposure, corrKernel)
        self.log.info("Updating correctedExposure and its PSF.")

        # Compute the subtracted exposure's updated psf
        psf = subtractedExposure.getPsf().computeImage().getArray()
        psfc = DecorrelateALKernelTask.computeCorrectedDiffimPsf(corrKernel,
                                                                 psf,
                                                                 svar=svar,
                                                                 tvar=tvar)
        psfcI = afwImage.ImageD(psfc.shape[0], psfc.shape[1])
        psfcI.getArray()[:, :] = psfc
        psfcK = afwMath.FixedKernel(psfcI)
        psfNew = measAlg.KernelPsf(psfcK)
        correctedExposure.setPsf(psfNew)
        self.log.info("Complete.")

        var = self.computeVarianceMean(correctedExposure)
        self.log.info("Variance (corrected diffim): %f" % var)

        return pipeBase.Struct(correctedExposure=correctedExposure,
                               correctionKernel=corrKern)
def makeBiaxialGaussianPsf(sizex, sizey, sigma1, sigma2, theta):
    kernel = afwMath.AnalyticKernel(
        sizex, sizey, afwMath.GaussianFunction2D(sigma1, sigma2, theta))
    return measAlg.KernelPsf(kernel)
    tot_images[filter_name].addNoise(noise)
    tot_images[filter_name].write('%s/image_%s.fits' %
                                  (output_dir, filter_name))

psf_bounds = galsim.BoundsI(0, 41, 0, 41)
psf_image = galsim.ImageF(psf_bounds, scale=scale)
psf.drawImage(image=psf_image)
psf_image.write('%s/psf_image.fits' % output_dir)

# Read in PSF
lsst_psf_image = afwImage.ImageF('%s/psf_image.fits' % output_dir)
bbox = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(41, 41))
lsst_psf_image = lsst_psf_image[bbox].convertD()
lsst_psf_image = afwMath.offsetImage(lsst_psf_image, -0.5, -0.5)
kernel = afwMath.FixedKernel(lsst_psf_image)
kernelPsf = measAlg.KernelPsf(kernel)

# Exposure setup
wcs = afwImage.makeWcs(
    afwCoord.Coord(0. * afwGeom.degrees, 0. * afwGeom.degrees),
    afwGeom.Point2D(0.0, 0.0), 0.168 * 3600, 0.0, 0.0, 0.168 * 3600)
calib = afwImage.Calib()
calib.setFluxMag0(1e12)

# Config and task setup
measure_config = SingleFrameMeasurementTask.ConfigClass()
measure_config.load(
    '/tigress/rea3/lsst/DM-8059/obs_subaru/config/apertures.py')
measure_config.load('/tigress/rea3/lsst/DM-8059/obs_subaru/config/kron.py')

measure_config.plugins.names |= [
    def run(self, exposure, templateExposure, subtractedExposure, psfMatchingKernel,
            preConvKernel=None, xcen=None, ycen=None, svar=None, tvar=None):
        """Perform decorrelation of an image difference exposure.

        Decorrelates the diffim due to the convolution of the templateExposure with the
        A&L PSF matching kernel. Currently can accept a spatially varying matching kernel but in
        this case it simply uses a static kernel from the center of the exposure. The decorrelation
        is described in [DMTN-021, Equation 1](http://dmtn-021.lsst.io/#equation-1), where
        `exposure` is I_1; templateExposure is I_2; `subtractedExposure` is D(k);
        `psfMatchingKernel` is kappa; and svar and tvar are their respective
        variances (see below).

        Parameters
        ----------
        exposure : `lsst.afw.image.Exposure`
            The science afwImage.Exposure used for PSF matching
        templateExposure : `lsst.afw.image.Exposure`
            The template exposure used for PSF matching
        subtractedExposure :
            the subtracted exposure produced by
            `ip_diffim.ImagePsfMatchTask.subtractExposures()`
        psfMatchingKernel :
            An (optionally spatially-varying) PSF matching kernel produced
            by `ip_diffim.ImagePsfMatchTask.subtractExposures()`
        preConvKernel :
            if not None, then the `exposure` was pre-convolved with this kernel
        xcen : `float`, optional
            X-pixel coordinate to use for computing constant matching kernel to use
            If `None` (default), then use the center of the image.
        ycen : `float`, optional
            Y-pixel coordinate to use for computing constant matching kernel to use
            If `None` (default), then use the center of the image.
        svar : `float`, optional
            image variance for science image
            If `None` (default) then compute the variance over the entire input science image.
        tvar : `float`, optional
            Image variance for template image
            If `None` (default) then compute the variance over the entire input template image.

        Returns
        -------
        result : `Struct`
            a `lsst.pipe.base.Struct` containing:

            - ``correctedExposure`` : the decorrelated diffim
            - ``correctionKernel`` : the decorrelation correction kernel (which may be ignored)

        Notes
        -----
        The `subtractedExposure` is NOT updated

        The returned `correctedExposure` has an updated PSF as well.

        Here we currently convert a spatially-varying matching kernel into a constant kernel,
        just by computing it at the center of the image (tickets DM-6243, DM-6244).

        We are also using a constant accross-the-image measure of sigma (sqrt(variance)) to compute
        the decorrelation kernel.

        Still TBD (ticket DM-6580): understand whether the convolution is correctly modifying
        the variance plane of the new subtractedExposure.
        """
        spatialKernel = psfMatchingKernel
        kimg = afwImage.ImageD(spatialKernel.getDimensions())
        bbox = subtractedExposure.getBBox()
        if xcen is None:
            xcen = (bbox.getBeginX() + bbox.getEndX()) / 2.
        if ycen is None:
            ycen = (bbox.getBeginY() + bbox.getEndY()) / 2.
        self.log.info("Using matching kernel computed at (%d, %d)", xcen, ycen)
        spatialKernel.computeImage(kimg, True, xcen, ycen)

        if svar is None:
            svar = self.computeVarianceMean(exposure)
        if tvar is None:
            tvar = self.computeVarianceMean(templateExposure)
        self.log.info("Variance (science, template): (%f, %f)", svar, tvar)

        # Should not happen unless entire image has been masked, which could happen
        # if this is a small subimage of the main exposure. In this case, just return a full NaN
        # exposure
        if np.isnan(svar) or np.isnan(tvar):
            # Double check that one of the exposures is all NaNs
            if (np.all(np.isnan(exposure.getMaskedImage().getImage().getArray())) or
                    np.all(np.isnan(templateExposure.getMaskedImage().getImage().getArray()))):
                self.log.warn('Template or science image is entirely NaNs: skipping decorrelation.')
                outExposure = subtractedExposure.clone()
                return pipeBase.Struct(correctedExposure=outExposure, correctionKernel=None)

        var = self.computeVarianceMean(subtractedExposure)
        self.log.info("Variance (uncorrected diffim): %f", var)

        pck = None
        if preConvKernel is not None:
            self.log.info('Using a pre-convolution kernel as part of decorrelation.')
            kimg2 = afwImage.ImageD(preConvKernel.getDimensions())
            preConvKernel.computeImage(kimg2, False)
            pck = kimg2.getArray()
        corrKernel = DecorrelateALKernelTask._computeDecorrelationKernel(kimg.getArray(), svar, tvar,
                                                                         pck)
        correctedExposure, corrKern = DecorrelateALKernelTask._doConvolve(subtractedExposure, corrKernel)

        # Compute the subtracted exposure's updated psf
        psf = subtractedExposure.getPsf().computeKernelImage(afwGeom.Point2D(xcen, ycen)).getArray()
        psfc = DecorrelateALKernelTask.computeCorrectedDiffimPsf(corrKernel, psf, svar=svar, tvar=tvar)
        psfcI = afwImage.ImageD(psfc.shape[0], psfc.shape[1])
        psfcI.getArray()[:, :] = psfc
        psfcK = afwMath.FixedKernel(psfcI)
        psfNew = measAlg.KernelPsf(psfcK)
        correctedExposure.setPsf(psfNew)

        var = self.computeVarianceMean(correctedExposure)
        self.log.info("Variance (corrected diffim): %f", var)

        return pipeBase.Struct(correctedExposure=correctedExposure, correctionKernel=corrKern)