def run(self, scienceExposure, templateExposure, doWarping=True, spatiallyVarying=False): """Register, PSF-match, and subtract two Exposures, ``scienceExposure - templateExposure`` using the ZOGY algorithm. Parameters ---------- templateExposure : `lsst.afw.image.Exposure` exposure to be warped to scienceExposure. scienceExposure : `lsst.afw.image.Exposure` reference Exposure. doWarping : `bool` what to do if templateExposure's and scienceExposure's WCSs do not match: - if True then warp templateExposure to match scienceExposure - if False then raise an Exception spatiallyVarying : `bool` If True, perform the operation over a grid of patches across the two exposures Notes ----- Do the following, in order: - Warp templateExposure to match scienceExposure, if their WCSs do not already match - Compute subtracted exposure ZOGY image subtraction algorithm on the two exposures This is the new entry point of the task as of DM-25115. Returns ------- results : `lsst.pipe.base.Struct` containing these fields: - subtractedExposure: `lsst.afw.image.Exposure` The subtraction result. - warpedExposure: `lsst.afw.image.Exposure` or `None` templateExposure after warping to match scienceExposure """ if spatiallyVarying: raise NotImplementedError( "DM-25115 Spatially varying zogy subtraction is not implemented.") if not self._validateWcs(scienceExposure, templateExposure): if doWarping: self.log.info("Warping templateExposure to scienceExposure") xyTransform = afwGeom.makeWcsPairTransform(templateExposure.getWcs(), scienceExposure.getWcs()) psfWarped = measAlg.WarpedPsf(templateExposure.getPsf(), xyTransform) templateExposure = self._warper.warpExposure( scienceExposure.getWcs(), templateExposure, destBBox=scienceExposure.getBBox()) templateExposure.setPsf(psfWarped) else: raise RuntimeError("Input images are not registered. Consider setting doWarping=True.") config = self.config.zogyConfig task = ZogyTask(config=config) results = task.run(scienceExposure, templateExposure) results.warpedExposure = templateExposure return results
def testSameWcs(self): """Confirm that pairing two identical Wcs gives an identity transform. """ for wcs in self.wcsList: transform = makeWcsPairTransform(wcs, wcs) # check that the transform has been simplified self.assertTrue(transform.getMapping().isSimple) # check the transform outPoints1 = transform.applyForward(self.pixelPoints) outPoints2 = transform.applyInverse(outPoints1) self.assertPairListsAlmostEqual(self.pixelPoints, outPoints1) self.assertPairListsAlmostEqual(outPoints1, outPoints2)
def testGenericWcs(self): """Test that input and output points represent the same sky position. Would prefer a black-box test, but don't have the numbers for it. """ inPoints = self.pixelPoints for wcs1 in self.wcsList: for wcs2 in self.wcsList: transform = makeWcsPairTransform(wcs1, wcs2) outPoints = transform.applyForward(inPoints) inPointsRoundTrip = transform.applyInverse(outPoints) self.assertPairListsAlmostEqual(inPoints, inPointsRoundTrip) self.assertSpherePointListsAlmostEqual( wcs1.pixelToSky(inPoints), wcs2.pixelToSky(outPoints))
def testTransformBasedWarp(self): """Test warping using TransformPoint2ToPoint2 """ for interpLength in (0, 1, 2, 4): kernelName = "lanczos3" rtol = 4e-5 atol = 1e-2 warpingControl = afwMath.WarpingControl( warpingKernelName=kernelName, interpLength=interpLength, ) originalExposure = afwImage.ExposureF(originalExposurePath) originalMetadata = afwImage.DecoratedImageF( originalExposurePath).getMetadata() originalSkyWcs = afwGeom.makeSkyWcs(originalMetadata) swarpedImageName = f"medswarp1{kernelName}.fits" swarpedImagePath = os.path.join(dataDir, swarpedImageName) swarpedDecoratedImage = afwImage.DecoratedImageF(swarpedImagePath) swarpedImage = swarpedDecoratedImage.getImage() swarpedMetadata = swarpedDecoratedImage.getMetadata() warpedSkyWcs = afwGeom.makeSkyWcs(swarpedMetadata) # original image is source, warped image is destination srcToDest = afwGeom.makeWcsPairTransform(originalSkyWcs, warpedSkyWcs) afwWarpedMaskedImage = afwImage.MaskedImageF( swarpedImage.getDimensions()) originalMaskedImage = originalExposure.getMaskedImage() numGoodPix = afwMath.warpImage(afwWarpedMaskedImage, originalMaskedImage, srcToDest, warpingControl) self.assertGreater(numGoodPix, 50) afwWarpedImage = afwWarpedMaskedImage.getImage() afwWarpedImageArr = afwWarpedImage.getArray() noDataMaskArr = np.isnan(afwWarpedImageArr) self.assertImagesAlmostEqual(afwWarpedImage, swarpedImage, skipMask=noDataMaskArr, rtol=rtol, atol=atol)
def testTransformBasedWarp(self): """Test warping using TransformPoint2ToPoint2 """ for interpLength in (0, 1, 2, 4): kernelName = "lanczos3" rtol = 4e-5 atol = 1e-2 warpingControl = afwMath.WarpingControl( warpingKernelName=kernelName, interpLength=interpLength, ) originalExposure = afwImage.ExposureF(originalExposurePath) originalMetadata = afwImage.DecoratedImageF(originalExposurePath).getMetadata() originalSkyWcs = afwGeom.makeSkyWcs(originalMetadata) swarpedImageName = "medswarp1%s.fits" % (kernelName,) swarpedImagePath = os.path.join(dataDir, swarpedImageName) swarpedDecoratedImage = afwImage.DecoratedImageF(swarpedImagePath) swarpedImage = swarpedDecoratedImage.getImage() swarpedMetadata = swarpedDecoratedImage.getMetadata() warpedSkyWcs = afwGeom.makeSkyWcs(swarpedMetadata) # original image is source, warped image is destination srcToDest = afwGeom.makeWcsPairTransform(originalSkyWcs, warpedSkyWcs) afwWarpedMaskedImage = afwImage.MaskedImageF(swarpedImage.getDimensions()) originalMaskedImage = originalExposure.getMaskedImage() numGoodPix = afwMath.warpImage(afwWarpedMaskedImage, originalMaskedImage, srcToDest, warpingControl) self.assertGreater(numGoodPix, 50) afwWarpedImage = afwWarpedMaskedImage.getImage() afwWarpedImageArr = afwWarpedImage.getArray() noDataMaskArr = np.isnan(afwWarpedImageArr) self.assertImagesAlmostEqual(afwWarpedImage, swarpedImage, skipMask=noDataMaskArr, rtol=rtol, atol=atol)
def run(self, exposure, sensorRef, templateIdList=None): """Retrieve and mosaic a template coadd that overlaps the exposure where the template spans multiple tracts. The resulting template image will be an average of all the input templates from the separate tracts. The PSF on the template is created by combining the CoaddPsf on each template image into a meta-CoaddPsf. Parameters ---------- exposure: `lsst.afw.image.Exposure` an exposure for which to generate an overlapping template sensorRef : TYPE a Butler data reference that can be used to obtain coadd data templateIdList : TYPE, optional list of data ids (unused) Returns ------- result : `struct` return a pipeBase.Struct: - ``exposure`` : a template coadd exposure assembled out of patches - ``sources`` : None for this subtask """ # Table for CoaddPSF tractsSchema = afwTable.ExposureTable.makeMinimalSchema() tractKey = tractsSchema.addField('tract', type=np.int32, doc='Which tract') patchKey = tractsSchema.addField('patch', type=np.int32, doc='Which patch') weightKey = tractsSchema.addField( 'weight', type=float, doc='Weight for each tract, should be 1') tractsCatalog = afwTable.ExposureCatalog(tractsSchema) skyMap = sensorRef.get(datasetType=self.config.coaddName + "Coadd_skyMap") expWcs = exposure.getWcs() expBoxD = geom.Box2D(exposure.getBBox()) expBoxD.grow(self.config.templateBorderSize) ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter()) centralTractInfo = skyMap.findTract(ctrSkyPos) if not centralTractInfo: raise RuntimeError("No suitable tract found for central point") self.log.info("Central skyMap tract %s" % (centralTractInfo.getId(), )) skyCorners = [ expWcs.pixelToSky(pixPos) for pixPos in expBoxD.getCorners() ] tractPatchList = skyMap.findTractPatchList(skyCorners) if not tractPatchList: raise RuntimeError("No suitable tract found") self.log.info("All overlapping skyMap tracts %s" % ([a[0].getId() for a in tractPatchList])) # Move central tract to front of the list and use as the reference tracts = [tract[0].getId() for tract in tractPatchList] centralIndex = tracts.index(centralTractInfo.getId()) tracts.insert(0, tracts.pop(centralIndex)) tractPatchList.insert(0, tractPatchList.pop(centralIndex)) coaddPsf = None coaddFilter = None nPatchesFound = 0 maskedImageList = [] weightList = [] for itract, tract in enumerate(tracts): tractInfo = tractPatchList[itract][0] coaddWcs = tractInfo.getWcs() coaddBBox = geom.Box2D() for skyPos in skyCorners: coaddBBox.include(coaddWcs.skyToPixel(skyPos)) coaddBBox = geom.Box2I(coaddBBox) if itract == 0: # Define final wcs and bounding box from the reference tract finalWcs = coaddWcs finalBBox = coaddBBox patchList = tractPatchList[itract][1] for patchInfo in patchList: self.log.info('Adding patch %s from tract %s' % (patchInfo.getIndex(), tract)) # Local patch information patchSubBBox = geom.Box2I(patchInfo.getInnerBBox()) patchSubBBox.clip(coaddBBox) patchInt = int( f"{patchInfo.getIndex()[0]}{patchInfo.getIndex()[1]}") innerBBox = geom.Box2I(tractInfo._minimumBoundingBox(finalWcs)) if itract == 0: # clip to image and tract boundaries patchSubBBox.clip(finalBBox) patchSubBBox.clip(innerBBox) if patchSubBBox.getArea() == 0: self.log.debug("No ovlerap for patch %s" % patchInfo) continue patchArgDict = dict( datasetType="deepCoadd_sub", bbox=patchSubBBox, tract=tractInfo.getId(), patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]), filter=exposure.getFilter().getName()) coaddPatch = sensorRef.get(**patchArgDict) if coaddFilter is None: coaddFilter = coaddPatch.getFilter() # create full image from final bounding box exp = afwImage.ExposureF(finalBBox, finalWcs) exp.maskedImage.set( np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) exp.maskedImage.assign(coaddPatch.maskedImage, patchSubBBox) maskedImageList.append(exp.maskedImage) weightList.append(1) record = tractsCatalog.addNew() record.setPsf(coaddPatch.getPsf()) record.setWcs(coaddPatch.getWcs()) record.setPhotoCalib(coaddPatch.getPhotoCalib()) record.setBBox(patchSubBBox) record.set(tractKey, tract) record.set(patchKey, patchInt) record.set(weightKey, 1.) nPatchesFound += 1 else: # compute the exposure bounding box in a tract that is not the reference tract localBox = geom.Box2I() for skyPos in skyCorners: localBox.include( geom.Point2I( tractInfo.getWcs().skyToPixel(skyPos))) # clip to patch bounding box localBox.clip(patchSubBBox) # grow border to deal with warping at edges localBox.grow(self.config.templateBorderSize) # clip to tract inner bounding box localInnerBBox = geom.Box2I( tractInfo._minimumBoundingBox(tractInfo.getWcs())) localBox.clip(localInnerBBox) patchArgDict = dict( datasetType="deepCoadd_sub", bbox=localBox, tract=tractInfo.getId(), patch="%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]), filter=exposure.getFilter().getName()) coaddPatch = sensorRef.get(**patchArgDict) # warp to reference tract wcs xyTransform = afwGeom.makeWcsPairTransform( coaddPatch.getWcs(), finalWcs) psfWarped = WarpedPsf(coaddPatch.getPsf(), xyTransform) warped = self.warper.warpExposure(finalWcs, coaddPatch, maxBBox=finalBBox) # check if warpped image is viable if warped.getBBox().getArea() == 0: self.log.info( "No ovlerap for warped patch %s. Skipping" % patchInfo) continue warped.setPsf(psfWarped) exp = afwImage.ExposureF(finalBBox, finalWcs) exp.maskedImage.set( np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) exp.maskedImage.assign(warped.maskedImage, warped.getBBox()) maskedImageList.append(exp.maskedImage) weightList.append(1) record = tractsCatalog.addNew() record.setPsf(psfWarped) record.setWcs(finalWcs) record.setPhotoCalib(coaddPatch.getPhotoCalib()) record.setBBox(warped.getBBox()) record.set(tractKey, tract) record.set(patchKey, patchInt) record.set(weightKey, 1.) nPatchesFound += 1 if nPatchesFound == 0: raise RuntimeError("No patches found!") # Combine images from individual patches together # Do not mask any values statsFlags = afwMath.stringToStatisticsProperty(self.config.statistic) maskMap = [] statsCtrl = afwMath.StatisticsControl() statsCtrl.setNanSafe(True) statsCtrl.setWeighted(True) statsCtrl.setCalcErrorFromInputVariance(True) coaddExposure = afwImage.ExposureF(finalBBox, finalWcs) coaddExposure.maskedImage.set(np.nan, afwImage.Mask.getPlaneBitMask("NO_DATA"), np.nan) xy0 = coaddExposure.getXY0() coaddExposure.maskedImage = afwMath.statisticsStack( maskedImageList, statsFlags, statsCtrl, weightList, 0, maskMap) coaddExposure.maskedImage.setXY0(xy0) coaddPsf = CoaddPsf(tractsCatalog, finalWcs, self.config.coaddPsf.makeControl()) if coaddPsf is None: raise RuntimeError("No coadd Psf found!") coaddExposure.setPsf(coaddPsf) coaddExposure.setFilter(coaddFilter) return pipeBase.Struct(exposure=coaddExposure, sources=None)
def compareToSwarp(self, kernelName, useWarpExposure=True, useSubregion=False, useDeepCopy=False, interpLength=10, cacheSize=100000, rtol=4e-05, atol=1e-2): """Compare warpExposure to swarp for given warping kernel. Note that swarp only warps the image plane, so only test that plane. Inputs: - kernelName: name of kernel in the form used by afwImage.makeKernel - useWarpExposure: if True, call warpExposure to warp an ExposureF, else call warpImage to warp an ImageF and also call the Transform version - useSubregion: if True then the original source exposure (from which the usual test exposure was extracted) is read and the correct subregion extracted - useDeepCopy: if True then the copy of the subimage is a deep copy, else it is a shallow copy; ignored if useSubregion is False - interpLength: interpLength argument for lsst.afw.math.WarpingControl - cacheSize: cacheSize argument for lsst.afw.math.WarpingControl; 0 disables the cache 10000 gives some speed improvement but less accurate results (atol must be increased) 100000 gives better accuracy but no speed improvement in this test - rtol: relative tolerance as used by np.allclose - atol: absolute tolerance as used by np.allclose """ warpingControl = afwMath.WarpingControl( kernelName, "", # there is no point to a separate mask kernel since we aren't testing the mask plane cacheSize, interpLength, ) if useSubregion: originalFullExposure = afwImage.ExposureF(originalExposurePath) # "medsub" is a subregion of med starting at 0-indexed pixel (40, 150) of size 145 x 200 bbox = lsst.geom.Box2I(lsst.geom.Point2I(40, 150), lsst.geom.Extent2I(145, 200)) originalExposure = afwImage.ExposureF(originalFullExposure, bbox, afwImage.LOCAL, useDeepCopy) swarpedImageName = f"medsubswarp1{kernelName}.fits" else: originalExposure = afwImage.ExposureF(originalExposurePath) swarpedImageName = f"medswarp1{kernelName}.fits" swarpedImagePath = os.path.join(dataDir, swarpedImageName) swarpedDecoratedImage = afwImage.DecoratedImageF(swarpedImagePath) swarpedImage = swarpedDecoratedImage.getImage() swarpedMetadata = swarpedDecoratedImage.getMetadata() warpedWcs = afwGeom.makeSkyWcs(swarpedMetadata) if useWarpExposure: # path for saved afw-warped image afwWarpedImagePath = f"afwWarpedExposure1{kernelName}.fits" afwWarpedMaskedImage = afwImage.MaskedImageF( swarpedImage.getDimensions()) afwWarpedExposure = afwImage.ExposureF(afwWarpedMaskedImage, warpedWcs) afwMath.warpExposure(afwWarpedExposure, originalExposure, warpingControl) afwWarpedMask = afwWarpedMaskedImage.getMask() if SAVE_FITS_FILES: afwWarpedExposure.writeFits(afwWarpedImagePath) if display: afwDisplay.Display(frame=1).mtv(afwWarpedExposure, title="Warped") swarpedMaskedImage = afwImage.MaskedImageF(swarpedImage) if display: afwDisplay.Display(frame=2).mtv(swarpedMaskedImage, title="SWarped") msg = f"afw and swarp {kernelName}-warped differ (ignoring bad pixels)" try: self.assertMaskedImagesAlmostEqual(afwWarpedMaskedImage, swarpedMaskedImage, doImage=True, doMask=False, doVariance=False, skipMask=afwWarpedMask, rtol=rtol, atol=atol, msg=msg) except Exception: if SAVE_FAILED_FITS_FILES: afwWarpedExposure.writeFits(afwWarpedImagePath) print( f"Saved failed afw-warped exposure as: {afwWarpedImagePath}" ) raise else: # path for saved afw-warped image afwWarpedImagePath = f"afwWarpedImage1{kernelName}.fits" afwWarpedImage2Path = f"afwWarpedImage1{kernelName}_xyTransform.fits" afwWarpedImage = afwImage.ImageF(swarpedImage.getDimensions()) originalImage = originalExposure.getMaskedImage().getImage() originalWcs = originalExposure.getWcs() afwMath.warpImage(afwWarpedImage, warpedWcs, originalImage, originalWcs, warpingControl) if display: afwDisplay.Display(frame=1).mtv(afwWarpedImage, title="Warped") afwDisplay.Display(frame=2).mtv(swarpedImage, title="SWarped") diff = swarpedImage.Factory(swarpedImage, True) diff -= afwWarpedImage afwDisplay.Display(frame=3).mtv(diff, title="swarp - afw") if SAVE_FITS_FILES: afwWarpedImage.writeFits(afwWarpedImagePath) afwWarpedImageArr = afwWarpedImage.getArray() noDataMaskArr = np.isnan(afwWarpedImageArr) msg = f"afw and swarp {kernelName}-warped images do not match (ignoring NaN pixels)" try: self.assertImagesAlmostEqual(afwWarpedImage, swarpedImage, skipMask=noDataMaskArr, rtol=rtol, atol=atol, msg=msg) except Exception: if SAVE_FAILED_FITS_FILES: # save the image anyway afwWarpedImage.writeFits(afwWarpedImagePath) print( f"Saved failed afw-warped image as: {afwWarpedImagePath}" ) raise afwWarpedImage2 = afwImage.ImageF(swarpedImage.getDimensions()) srcToDest = afwGeom.makeWcsPairTransform(originalWcs, warpedWcs) afwMath.warpImage(afwWarpedImage2, originalImage, srcToDest, warpingControl) msg = f"afw transform-based and WCS-based {kernelName}-warped images do not match" try: self.assertImagesAlmostEqual(afwWarpedImage2, afwWarpedImage, rtol=rtol, atol=atol, msg=msg) except Exception: if SAVE_FAILED_FITS_FILES: # save the image anyway afwWarpedImage.writeFits(afwWarpedImage2) print( f"Saved failed afw-warped image as: {afwWarpedImage2Path}" ) raise
def matchExposures(self, templateExposure, scienceExposure, templateFwhmPix=None, scienceFwhmPix=None, candidateList=None, doWarping=True, convolveTemplate=True): """Warp and PSF-match an exposure to the reference. Do the following, in order: - Warp templateExposure to match scienceExposure, if doWarping True and their WCSs do not already match - Determine a PSF matching kernel and differential background model that matches templateExposure to scienceExposure - Convolve templateExposure by PSF matching kernel Parameters ---------- templateExposure : `lsst.afw.image.Exposure` Exposure to warp and PSF-match to the reference masked image scienceExposure : `lsst.afw.image.Exposure` Exposure whose WCS and PSF are to be matched to templateFwhmPix :`float` FWHM (in pixels) of the Psf in the template image (image to convolve) scienceFwhmPix : `float` FWHM (in pixels) of the Psf in the science image candidateList : `list`, optional a list of footprints/maskedImages for kernel candidates; if `None` then source detection is run. - Currently supported: list of Footprints or measAlg.PsfCandidateF doWarping : `bool` what to do if ``templateExposure`` and ``scienceExposure`` WCSs do not match: - if `True` then warp ``templateExposure`` to match ``scienceExposure`` - if `False` then raise an Exception convolveTemplate : `bool` Whether to convolve the template image or the science image: - if `True`, ``templateExposure`` is warped if doWarping, ``templateExposure`` is convolved - if `False`, ``templateExposure`` is warped if doWarping, ``scienceExposure`` is convolved Returns ------- results : `lsst.pipe.base.Struct` An `lsst.pipe.base.Struct` containing these fields: - ``matchedImage`` : the PSF-matched exposure = Warped ``templateExposure`` convolved by psfMatchingKernel. This has: - the same parent bbox, Wcs and PhotoCalib as scienceExposure - the same filter as templateExposure - no Psf (because the PSF-matching process does not compute one) - ``psfMatchingKernel`` : the PSF matching kernel - ``backgroundModel`` : differential background model - ``kernelCellSet`` : SpatialCellSet used to solve for the PSF matching kernel Raises ------ RuntimeError Raised if doWarping is False and ``templateExposure`` and ``scienceExposure`` WCSs do not match """ if not self._validateWcs(templateExposure, scienceExposure): if doWarping: self.log.info( "Astrometrically registering template to science image") templatePsf = templateExposure.getPsf() # Warp PSF before overwriting exposure xyTransform = afwGeom.makeWcsPairTransform( templateExposure.getWcs(), scienceExposure.getWcs()) psfWarped = WarpedPsf(templatePsf, xyTransform) templateExposure = self._warper.warpExposure( scienceExposure.getWcs(), templateExposure, destBBox=scienceExposure.getBBox()) templateExposure.setPsf(psfWarped) else: self.log.error("ERROR: Input images not registered") raise RuntimeError("Input images not registered") if templateFwhmPix is None: if not templateExposure.hasPsf(): self.log.warning("No estimate of Psf FWHM for template image") else: templateFwhmPix = self.getFwhmPix(templateExposure.getPsf()) self.log.info("templateFwhmPix: %s", templateFwhmPix) if scienceFwhmPix is None: if not scienceExposure.hasPsf(): self.log.warning("No estimate of Psf FWHM for science image") else: scienceFwhmPix = self.getFwhmPix(scienceExposure.getPsf()) self.log.info("scienceFwhmPix: %s", scienceFwhmPix) if convolveTemplate: kernelSize = self.makeKernelBasisList( templateFwhmPix, scienceFwhmPix)[0].getWidth() candidateList = self.makeCandidateList(templateExposure, scienceExposure, kernelSize, candidateList) results = self.matchMaskedImages(templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList, templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix) else: kernelSize = self.makeKernelBasisList( scienceFwhmPix, templateFwhmPix)[0].getWidth() candidateList = self.makeCandidateList(templateExposure, scienceExposure, kernelSize, candidateList) results = self.matchMaskedImages(scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList, templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix) psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs()) psfMatchedExposure.setFilter(templateExposure.getFilter()) psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib()) results.warpedExposure = templateExposure results.matchedExposure = psfMatchedExposure return results
def matchExposures(self, templateExposure, scienceExposure, templateFwhmPix=None, scienceFwhmPix=None, candidateList=None, doWarping=True, convolveTemplate=True): """Warp and PSF-match an exposure to the reference. Do the following, in order: - Warp templateExposure to match scienceExposure, if doWarping True and their WCSs do not already match - Determine a PSF matching kernel and differential background model that matches templateExposure to scienceExposure - Convolve templateExposure by PSF matching kernel Parameters ---------- templateExposure : `lsst.afw.image.Exposure` Exposure to warp and PSF-match to the reference masked image scienceExposure : `lsst.afw.image.Exposure` Exposure whose WCS and PSF are to be matched to templateFwhmPix :`float` FWHM (in pixels) of the Psf in the template image (image to convolve) scienceFwhmPix : `float` FWHM (in pixels) of the Psf in the science image candidateList : `list`, optional a list of footprints/maskedImages for kernel candidates; if `None` then source detection is run. - Currently supported: list of Footprints or measAlg.PsfCandidateF doWarping : `bool` what to do if ``templateExposure`` and ``scienceExposure`` WCSs do not match: - if `True` then warp ``templateExposure`` to match ``scienceExposure`` - if `False` then raise an Exception convolveTemplate : `bool` Whether to convolve the template image or the science image: - if `True`, ``templateExposure`` is warped if doWarping, ``templateExposure`` is convolved - if `False`, ``templateExposure`` is warped if doWarping, ``scienceExposure`` is convolved Returns ------- results : `lsst.pipe.base.Struct` An `lsst.pipe.base.Struct` containing these fields: - ``matchedImage`` : the PSF-matched exposure = Warped ``templateExposure`` convolved by psfMatchingKernel. This has: - the same parent bbox, Wcs and PhotoCalib as scienceExposure - the same filter as templateExposure - no Psf (because the PSF-matching process does not compute one) - ``psfMatchingKernel`` : the PSF matching kernel - ``backgroundModel`` : differential background model - ``kernelCellSet`` : SpatialCellSet used to solve for the PSF matching kernel Raises ------ RuntimeError Raised if doWarping is False and ``templateExposure`` and ``scienceExposure`` WCSs do not match """ if not self._validateWcs(templateExposure, scienceExposure): if doWarping: self.log.info("Astrometrically registering template to science image") templatePsf = templateExposure.getPsf() # Warp PSF before overwriting exposure xyTransform = afwGeom.makeWcsPairTransform(templateExposure.getWcs(), scienceExposure.getWcs()) psfWarped = WarpedPsf(templatePsf, xyTransform) templateExposure = self._warper.warpExposure(scienceExposure.getWcs(), templateExposure, destBBox=scienceExposure.getBBox()) templateExposure.setPsf(psfWarped) else: self.log.error("ERROR: Input images not registered") raise RuntimeError("Input images not registered") if templateFwhmPix is None: if not templateExposure.hasPsf(): self.log.warn("No estimate of Psf FWHM for template image") else: templateFwhmPix = self.getFwhmPix(templateExposure.getPsf()) self.log.info("templateFwhmPix: {}".format(templateFwhmPix)) if scienceFwhmPix is None: if not scienceExposure.hasPsf(): self.log.warn("No estimate of Psf FWHM for science image") else: scienceFwhmPix = self.getFwhmPix(scienceExposure.getPsf()) self.log.info("scienceFwhmPix: {}".format(scienceFwhmPix)) kernelSize = makeKernelBasisList(self.kConfig, templateFwhmPix, scienceFwhmPix)[0].getWidth() candidateList = self.makeCandidateList(templateExposure, scienceExposure, kernelSize, candidateList) if convolveTemplate: results = self.matchMaskedImages( templateExposure.getMaskedImage(), scienceExposure.getMaskedImage(), candidateList, templateFwhmPix=templateFwhmPix, scienceFwhmPix=scienceFwhmPix) else: results = self.matchMaskedImages( scienceExposure.getMaskedImage(), templateExposure.getMaskedImage(), candidateList, templateFwhmPix=scienceFwhmPix, scienceFwhmPix=templateFwhmPix) psfMatchedExposure = afwImage.makeExposure(results.matchedImage, scienceExposure.getWcs()) psfMatchedExposure.setFilter(templateExposure.getFilter()) psfMatchedExposure.setPhotoCalib(scienceExposure.getPhotoCalib()) results.warpedExposure = templateExposure results.matchedExposure = psfMatchedExposure return results
def run(self, exposure, wcs, modelPsf=None, maxBBox=None, destBBox=None, makeDirect=True, makePsfMatched=False): """Warp and optionally PSF-match exposure Parameters ---------- exposure : :cpp:class: `lsst::afw::image::Exposure` Exposure to preprocess. wcs : :cpp:class:`lsst::afw::image::Wcs` Desired WCS of temporary images. modelPsf : :cpp:class: `lsst::meas::algorithms::KernelPsf` or None Target PSF to which to match. maxBBox : :cpp:class:`lsst::afw::geom::Box2I` or None Maximum allowed parent bbox of warped exposure. If None then the warped exposure will be just big enough to contain all warped pixels; if provided then the warped exposure may be smaller, and so missing some warped pixels; ignored if destBBox is not None. destBBox: :cpp:class: `lsst::afw::geom::Box2I` or None Exact parent bbox of warped exposure. If None then maxBBox is used to determine the bbox, otherwise maxBBox is ignored. makeDirect : bool Return an exposure that has been only warped? makePsfMatched : bool Return an exposure that has been warped and PSF-matched? Returns ------- An lsst.pipe.base.Struct with the following fields: direct : :cpp:class:`lsst::afw::image::Exposure` warped exposure psfMatched : :cpp:class: `lsst::afw::image::Exposure` warped and psf-Matched temporary exposure """ if makePsfMatched and modelPsf is None: raise RuntimeError( "makePsfMatched=True, but no model PSF was provided") if not makePsfMatched and not makeDirect: self.log.warn("Neither makeDirect nor makePsfMatched requested") # Warp PSF before overwriting exposure xyTransform = afwGeom.makeWcsPairTransform(exposure.getWcs(), wcs) psfWarped = WarpedPsf(exposure.getPsf(), xyTransform) if makePsfMatched and maxBBox is not None: # grow warped region to provide sufficient area for PSF-matching pixToGrow = 2 * max(self.psfMatch.kConfig.sizeCellX, self.psfMatch.kConfig.sizeCellY) # replace with copy maxBBox = geom.Box2I(maxBBox) maxBBox.grow(pixToGrow) with self.timer("warp"): exposure = self.warper.warpExposure(wcs, exposure, maxBBox=maxBBox, destBBox=destBBox) exposure.setPsf(psfWarped) if makePsfMatched: try: exposurePsfMatched = self.psfMatch.run( exposure, modelPsf).psfMatchedExposure except Exception as e: exposurePsfMatched = None self.log.info("Cannot PSF-Match: %s" % (e)) return pipeBase.Struct( direct=exposure if makeDirect else None, psfMatched=exposurePsfMatched if makePsfMatched else None)
def compareToSwarp(self, kernelName, useWarpExposure=True, useSubregion=False, useDeepCopy=False, interpLength=10, cacheSize=100000, rtol=4e-05, atol=1e-2): """Compare warpExposure to swarp for given warping kernel. Note that swarp only warps the image plane, so only test that plane. Inputs: - kernelName: name of kernel in the form used by afwImage.makeKernel - useWarpExposure: if True, call warpExposure to warp an ExposureF, else call warpImage to warp an ImageF and also call the Transform version - useSubregion: if True then the original source exposure (from which the usual test exposure was extracted) is read and the correct subregion extracted - useDeepCopy: if True then the copy of the subimage is a deep copy, else it is a shallow copy; ignored if useSubregion is False - interpLength: interpLength argument for lsst.afw.math.WarpingControl - cacheSize: cacheSize argument for lsst.afw.math.WarpingControl; 0 disables the cache 10000 gives some speed improvement but less accurate results (atol must be increased) 100000 gives better accuracy but no speed improvement in this test - rtol: relative tolerance as used by np.allclose - atol: absolute tolerance as used by np.allclose """ warpingControl = afwMath.WarpingControl( kernelName, "", # there is no point to a separate mask kernel since we aren't testing the mask plane cacheSize, interpLength, ) if useSubregion: originalFullExposure = afwImage.ExposureF(originalExposurePath) # "medsub" is a subregion of med starting at 0-indexed pixel (40, 150) of size 145 x 200 bbox = lsst.geom.Box2I(lsst.geom.Point2I(40, 150), lsst.geom.Extent2I(145, 200)) originalExposure = afwImage.ExposureF( originalFullExposure, bbox, afwImage.LOCAL, useDeepCopy) swarpedImageName = "medsubswarp1%s.fits" % (kernelName,) else: originalExposure = afwImage.ExposureF(originalExposurePath) swarpedImageName = "medswarp1%s.fits" % (kernelName,) swarpedImagePath = os.path.join(dataDir, swarpedImageName) swarpedDecoratedImage = afwImage.DecoratedImageF(swarpedImagePath) swarpedImage = swarpedDecoratedImage.getImage() swarpedMetadata = swarpedDecoratedImage.getMetadata() warpedWcs = afwGeom.makeSkyWcs(swarpedMetadata) if useWarpExposure: # path for saved afw-warped image afwWarpedImagePath = "afwWarpedExposure1%s.fits" % (kernelName,) afwWarpedMaskedImage = afwImage.MaskedImageF( swarpedImage.getDimensions()) afwWarpedExposure = afwImage.ExposureF( afwWarpedMaskedImage, warpedWcs) afwMath.warpExposure( afwWarpedExposure, originalExposure, warpingControl) afwWarpedMask = afwWarpedMaskedImage.getMask() if SAVE_FITS_FILES: afwWarpedExposure.writeFits(afwWarpedImagePath) if display: afwDisplay.Display(frame=1).mtv(afwWarpedExposure, title="Warped") swarpedMaskedImage = afwImage.MaskedImageF(swarpedImage) if display: afwDisplay.Display(frame=2).mtv(swarpedMaskedImage, title="SWarped") msg = "afw and swarp %s-warped differ (ignoring bad pixels)" % ( kernelName,) try: self.assertMaskedImagesAlmostEqual(afwWarpedMaskedImage, swarpedMaskedImage, doImage=True, doMask=False, doVariance=False, skipMask=afwWarpedMask, rtol=rtol, atol=atol, msg=msg) except Exception: if SAVE_FAILED_FITS_FILES: afwWarpedExposure.writeFits(afwWarpedImagePath) print("Saved failed afw-warped exposure as: %s" % (afwWarpedImagePath,)) raise else: # path for saved afw-warped image afwWarpedImagePath = "afwWarpedImage1%s.fits" % (kernelName,) afwWarpedImage2Path = "afwWarpedImage1%s_xyTransform.fits" % ( kernelName,) afwWarpedImage = afwImage.ImageF(swarpedImage.getDimensions()) originalImage = originalExposure.getMaskedImage().getImage() originalWcs = originalExposure.getWcs() afwMath.warpImage(afwWarpedImage, warpedWcs, originalImage, originalWcs, warpingControl) if display: afwDisplay.Display(frame=1).mtv(afwWarpedImage, title="Warped") afwDisplay.Display(frame=2).mtv(swarpedImage, title="SWarped") diff = swarpedImage.Factory(swarpedImage, True) diff -= afwWarpedImage afwDisplay.Display(frame=3).mtv(diff, title="swarp - afw") if SAVE_FITS_FILES: afwWarpedImage.writeFits(afwWarpedImagePath) afwWarpedImageArr = afwWarpedImage.getArray() noDataMaskArr = np.isnan(afwWarpedImageArr) msg = "afw and swarp %s-warped images do not match (ignoring NaN pixels)" % \ (kernelName,) try: self.assertImagesAlmostEqual(afwWarpedImage, swarpedImage, skipMask=noDataMaskArr, rtol=rtol, atol=atol, msg=msg) except Exception: if SAVE_FAILED_FITS_FILES: # save the image anyway afwWarpedImage.writeFits(afwWarpedImagePath) print("Saved failed afw-warped image as: %s" % (afwWarpedImagePath,)) raise afwWarpedImage2 = afwImage.ImageF(swarpedImage.getDimensions()) srcToDest = afwGeom.makeWcsPairTransform(originalWcs, warpedWcs) afwMath.warpImage(afwWarpedImage2, originalImage, srcToDest, warpingControl) msg = "afw transform-based and WCS-based %s-warped images do not match" % ( kernelName,) try: self.assertImagesAlmostEqual(afwWarpedImage2, afwWarpedImage, rtol=rtol, atol=atol, msg=msg) except Exception: if SAVE_FAILED_FITS_FILES: # save the image anyway afwWarpedImage.writeFits(afwWarpedImagePath) print("Saved failed afw-warped image as: %s" % (afwWarpedImage2Path,)) raise