def runStdTest(self, kernel, refKernel=None, kernelDescr="", rtol=1.0e-05, atol=1e-08, maxInterpDist=10): """Assert that afwMath::convolve gives the same result as reference convolution for a given kernel. Inputs: - kernel: convolution kernel - refKernel: kernel to use for refConvolve (if None then kernel is used) - kernelDescr: description of kernel - rtol: relative tolerance (see below) - atol: absolute tolerance (see below) - maxInterpDist: maximum allowed distance for linear interpolation during convolution rtol and atol are positive, typically very small numbers. The relative difference (rtol * abs(b)) and the absolute difference "atol" are added together to compare against the absolute difference between "a" and "b". """ if VERBOSITY > 0: print "Test convolution with", kernelDescr convControl = afwMath.ConvolutionControl() convControl.setMaxInterpolationDistance(maxInterpDist) # verify dimension assertions: # - output image dimensions = input image dimensions # - input image width and height >= kernel width and height # Note: the assertion kernel size > 0 is tested elsewhere for inWidth in (kernel.getWidth() - 1, self.width-1, self.width, self.width + 1): for inHeight in (kernel.getHeight() - 1, self.width-1, self.width, self.width + 1): if (inWidth == self.width) and (inHeight == self.height): continue inMaskedImage = afwImage.MaskedImageF(afwGeom.Extent2I(inWidth, inHeight)) self.assertRaises(Exception, afwMath.convolve, self.cnvMaskedImage, inMaskedImage, kernel) for doNormalize in (True,): # (False, True): convControl.setDoNormalize(doNormalize) for doCopyEdge in (False,): # (False, True): convControl.setDoCopyEdge(doCopyEdge) self.runBasicTest(kernel, convControl=convControl, refKernel=refKernel, kernelDescr=kernelDescr, rtol=rtol, atol=atol) # verify that basicConvolve does not write to edge pixels self.runBasicConvolveEdgeTest(kernel, kernelDescr)
def runBasicConvolveEdgeTest(self, kernel, kernelDescr): """Verify that basicConvolve does not write to edge pixels for this kind of kernel """ fullBox = afwGeom.Box2I( afwGeom.Point2I(0, 0), ShiftedBBox.getDimensions(), ) goodBox = kernel.shrinkBBox(fullBox) cnvMaskedImage = afwImage.MaskedImageF(FullMaskedImage, ShiftedBBox, afwImage.LOCAL, True) cnvMaskedImageCopy = afwImage.MaskedImageF(cnvMaskedImage, fullBox, afwImage.LOCAL, True) cnvMaskedImageCopyViewOfGoodRegion = afwImage.MaskedImageF( cnvMaskedImageCopy, goodBox, afwImage.LOCAL, False) # convolve with basicConvolve, which should leave the edge pixels alone convControl = afwMath.ConvolutionControl() mathDetail.basicConvolve(cnvMaskedImage, self.maskedImage, kernel, convControl) # reset the good region to the original convolved image; # this should reset the entire convolved image to its original self cnvMaskedImageGoodView = afwImage.MaskedImageF(cnvMaskedImage, goodBox, afwImage.LOCAL, False) cnvMaskedImageGoodView <<= cnvMaskedImageCopyViewOfGoodRegion # assert that these two are equal cnvImMaskVarArr = cnvMaskedImage.getArrays() desCnvImMaskVarArr = cnvMaskedImageCopy.getArrays() errStr = imTestUtils.maskedImagesDiffer(cnvImMaskVarArr, desCnvImMaskVarArr, doVariance=True, rtol=0, atol=0) shortKernelDescr = kernelDescr.translate(NullTranslator, GarbageChars) if errStr: cnvMaskedImage.writeFits("actBasicConvolve%s" % (shortKernelDescr, )) cnvMaskedImageCopy.writeFits("desBasicConvolve%s" % (shortKernelDescr, )) self.fail("basicConvolve(MaskedImage, kernel=%s) wrote to edge pixels:\n%s" % \ (kernelDescr, errStr))
def testBad(self): ti = afwImage.MaskedImageF(geom.Extent2I(100, 100)) ti.getVariance().set(0.1) ti[50, 50, afwImage.LOCAL] = (1., 0x0, 1.) sKernel = self.makeSpatialKernel(2) si = afwImage.MaskedImageF(ti.getDimensions()) convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(True) afwMath.convolve(si, ti, sKernel, convolutionControl) bbox = geom.Box2I(geom.Point2I(25, 25), geom.Point2I(75, 75)) si = afwImage.MaskedImageF(si, bbox, origin=afwImage.LOCAL) ti = afwImage.MaskedImageF(ti, bbox, origin=afwImage.LOCAL) kc = ipDiffim.KernelCandidateF(50., 50., ti, si, self.ps) badGaussian = afwMath.GaussianFunction2D(1., 1., 0.) badKernel = afwMath.AnalyticKernel(self.ksize, self.ksize, badGaussian) basisList = [] basisList.append(badKernel) badSpatialKernelFunction = afwMath.PolynomialFunction2D(0) badSpatialKernel = afwMath.LinearCombinationKernel( basisList, badSpatialKernelFunction) badSpatialKernel.setSpatialParameters([[ 1, ]]) sBg = afwMath.PolynomialFunction2D(1) bgCoeffs = [10., 10., 10.] sBg.setParameters(bgCoeffs) # must be initialized bskv = ipDiffim.BuildSingleKernelVisitorF(self.kList, self.ps) bskv.processCandidate(kc) self.assertEqual(kc.isInitialized(), True) askv = ipDiffim.AssessSpatialKernelVisitorF(badSpatialKernel, sBg, self.ps) askv.processCandidate(kc) self.assertEqual(askv.getNProcessed(), 1) self.assertEqual(askv.getNRejected(), 1) self.assertEqual(kc.getStatus(), afwMath.SpatialCellCandidate.BAD)
def setUp(self): FWHM = 5 psf = algorithms.DoubleGaussianPsf( 15, 15, FWHM / (2 * math.sqrt(2 * math.log(2)))) mi = afwImage.MaskedImageF(lsst.geom.ExtentI(100, 100)) self.xc, self.yc, self.instFlux = 45, 55, 1000.0 mi.image[self.xc, self.yc, afwImage.LOCAL] = self.instFlux cnvImage = mi.Factory(mi.getDimensions()) afwMath.convolve(cnvImage, mi, psf.getKernel(), afwMath.ConvolutionControl()) self.exp = afwImage.makeExposure(cnvImage) self.exp.setPsf(psf) if display and False: afwDisplay.Display(frame=0).mtv(self.exp, title=self._testMethodName + ": image")
def setUp(self): self.config = ipDiffim.ImagePsfMatchTask.ConfigClass() self.config.kernel.name = "AL" self.subconfig = self.config.kernel.active self.kSize = self.subconfig.kernelSize # gaussian reference kernel self.gSize = self.kSize self.gaussFunction = afwMath.GaussianFunction2D(2, 3) self.gaussKernel = afwMath.AnalyticKernel(self.gSize, self.gSize, self.gaussFunction) if defDataDir: defImagePath = os.path.join(defDataDir, "DC3a-Sim", "sci", "v5-e0", "v5-e0-c011-a00.sci.fits") self.templateImage = afwImage.MaskedImageF(defImagePath) self.scienceImage = self.templateImage.Factory(self.templateImage.getDimensions()) convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(False) afwMath.convolve(self.scienceImage, self.templateImage, self.gaussKernel, convolutionControl)
def _doConvolve(exposure, kernel): """! Convolve an Exposure with a decorrelation convolution kernel. @param exposure Input afw.image.Exposure to be convolved. @param kernel Input 2-d numpy.array to convolve the image with @return a new Exposure with the convolved pixels and the (possibly re-centered) kernel. @note We re-center the kernel if necessary and return the possibly re-centered kernel """ 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]) outExp = exposure.clone() # Do this to keep WCS, PSF, masks, etc. convCntrl = afwMath.ConvolutionControl(False, True, 0) afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl) return outExp, kern
def runBasicConvolveEdgeTest(self, kernel, kernelDescr): """Verify that basicConvolve does not write to edge pixels for this kind of kernel """ fullBox = afwGeom.Box2I( afwGeom.Point2I(0, 0), ShiftedBBox.getDimensions(), ) goodBox = kernel.shrinkBBox(fullBox) cnvMaskedImage = afwImage.MaskedImageF( FullMaskedImage, ShiftedBBox, afwImage.LOCAL, True) cnvMaskedImageCopy = afwImage.MaskedImageF( cnvMaskedImage, fullBox, afwImage.LOCAL, True) cnvMaskedImageCopyViewOfGoodRegion = afwImage.MaskedImageF( cnvMaskedImageCopy, goodBox, afwImage.LOCAL, False) # convolve with basicConvolve, which should leave the edge pixels alone convControl = afwMath.ConvolutionControl() mathDetail.basicConvolve( cnvMaskedImage, self.maskedImage, kernel, convControl) # reset the good region to the original convolved image; # this should reset the entire convolved image to its original self cnvMaskedImageGoodView = afwImage.MaskedImageF( cnvMaskedImage, goodBox, afwImage.LOCAL, False) cnvMaskedImageGoodView[:] = cnvMaskedImageCopyViewOfGoodRegion # assert that these two are equal msg = "basicConvolve(MaskedImage, kernel=%s) wrote to edge pixels" % ( kernelDescr,) try: self.assertMaskedImagesAlmostEqual(cnvMaskedImage, cnvMaskedImageCopy, doVariance=True, rtol=0, atol=0, msg=msg) except Exception: # write out the images, then fail shortKernelDescr = self.removeGarbageChars(kernelDescr) cnvMaskedImage.writeFits( "actBasicConvolve%s" % (shortKernelDescr,)) cnvMaskedImageCopy.writeFits( "desBasicConvolve%s" % (shortKernelDescr,)) raise
def _doConvolve(self, exposure, kernel, recenterKernel=False): """! Convolve an Exposure with a decorrelation convolution kernel. Parameters ---------- exposure : lsst.afw.image.Exposure to be convolved. kernel : 2D numpy.array to convolve the image with Returns ------- A new lsst.afw.image.Exposure with the convolved pixels and the (possibly re-centered) kernel. Notes ----- - We optionally re-center the kernel if necessary and return the possibly re-centered kernel """ kernelImg = afwImage.ImageD(kernel.shape[0], kernel.shape[1]) kernelImg.getArray()[:, :] = kernel kern = afwMath.FixedKernel(kernelImg) if recenterKernel: maxloc = np.unravel_index(np.argmax(kernel), kernel.shape) kern.setCtrX(maxloc[0]) kern.setCtrY(maxloc[1]) outExp = exposure.clone() # Do this to keep WCS, PSF, masks, etc. convCntrl = afwMath.ConvolutionControl(doNormalize=False, doCopyEdge=False, maxInterpolationDistance=0) try: afwMath.convolve(outExp.getMaskedImage(), exposure.getMaskedImage(), kern, convCntrl) except: # Allow exposure to actually be an image/maskedImage afwMath.convolve(outExp, exposure, kern, convCntrl) return outExp, kern
def testAutoCorrelation(orderMake, orderFit, inMi=None, display=False): config = ipDiffim.ImagePsfMatchTask.ConfigClass() config.kernel.name = "AL" subconfig = config.kernel.active subconfig.fitForBackground = True stride = 100 if inMi is None: width = 512 height = 2048 inMi = afwImage.MaskedImageF(afwGeom.Extent2I(width, height)) for j in num.arange(stride // 2, height, stride): j = int(j) for i in num.arange(stride // 2, width, stride): i = int(i) inMi._set((i - 1, j - 1), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i - 1, j + 0), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i - 1, j + 1), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i + 0, j - 1), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i + 0, j + 0), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i + 0, j + 1), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i + 1, j - 1), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i + 1, j + 0), (100., 0x0, 1.), afwImage.LOCAL) inMi._set((i + 1, j + 1), (100., 0x0, 1.), afwImage.LOCAL) addNoise(inMi) kSize = subconfig.kernelSize basicGaussian1 = afwMath.GaussianFunction2D(2., 2., 0.) basicKernel1 = afwMath.AnalyticKernel(kSize, kSize, basicGaussian1) basicGaussian2 = afwMath.GaussianFunction2D(5., 3., 0.5 * num.pi) basicKernel2 = afwMath.AnalyticKernel(kSize, kSize, basicGaussian2) basisList = [] basisList.append(basicKernel1) basisList.append(basicKernel2) spatialKernelFunction = afwMath.PolynomialFunction2D(orderMake) spatialKernel = afwMath.LinearCombinationKernel(basisList, spatialKernelFunction) kCoeffs = [[ 1.0 for x in range(1, spatialKernelFunction.getNParameters() + 1) ], [ 0.01 * x for x in range(1, spatialKernelFunction.getNParameters() + 1) ]] spatialKernel.setSpatialParameters(kCoeffs) cMi = afwImage.MaskedImageF(inMi.getDimensions()) convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(True) afwMath.convolve(cMi, inMi, spatialKernel, convolutionControl) if display: afwDisplay.Display(frame=1).mtv(inMi.getImage()) afwDisplay.Display(frame=2).mtv(inMi.getVariance()) afwDisplay.Display(frame=3).mtv(cMi.getImage()) afwDisplay.Display(frame=4).mtv(cMi.getVariance()) subconfig.spatialKernelOrder = orderFit subconfig.sizeCellX = stride subconfig.sizeCellY = stride psfmatch = ipDiffim.ImagePsfMatchTask(config=config) candList = psfmatch.makeCandidateList(afwImage.ExposureF(inMi), afwImage.ExposureF(cMi), kSize) result = psfmatch.subtractMaskedImages(inMi, cMi, candList) spatialKernel = result.psfMatchingKernel kernelCellSet = result.kernelCellSet makeAutoCorrelation(kernelCellSet, spatialKernel, makePlot=True)
def testGaussianWithNoise(self): # Convolve a real image with a gaussian and try and recover # it. Add noise and perform the same test. gsize = self.ps["kernelSize"] gaussFunction = afwMath.GaussianFunction2D(2, 3) gaussKernel = afwMath.AnalyticKernel(gsize, gsize, gaussFunction) kImageIn = afwImage.ImageD(geom.Extent2I(gsize, gsize)) kSumIn = gaussKernel.computeImage(kImageIn, False) imX, imY = self.templateExposure2.getMaskedImage().getDimensions() smi = afwImage.MaskedImageF(geom.Extent2I(imX, imY)) convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(False) afwMath.convolve(smi, self.templateExposure2.getMaskedImage(), gaussKernel, convolutionControl) bbox = gaussKernel.shrinkBBox(smi.getBBox(afwImage.LOCAL)) tmi2 = afwImage.MaskedImageF(self.templateExposure2.getMaskedImage(), bbox, origin=afwImage.LOCAL) smi2 = afwImage.MaskedImageF(smi, bbox, origin=afwImage.LOCAL) kc = ipDiffim.KernelCandidateF(self.x02, self.y02, tmi2, smi2, self.ps) kList = ipDiffim.makeKernelBasisList(self.subconfig) kc.build(kList) self.assertEqual(kc.isInitialized(), True) kImageOut = kc.getImage() soln = kc.getKernelSolution(ipDiffim.KernelCandidateF.RECENT) self.assertAlmostEqual(soln.getKsum(), kSumIn) # 8.7499380640430563e-06 != 0.0 within 7 places self.assertAlmostEqual(soln.getBackground(), 0.0, 4) for j in range(kImageOut.getHeight()): for i in range(kImageOut.getWidth()): # in the outskirts of the kernel, the ratio can get screwed because of low S/N # e.g. 7.45817359824e-09 vs. 1.18062529402e-08 # in the guts of the kernel it should look closer if kImageIn[i, j, afwImage.LOCAL] > 1e-4: # sigh, too bad this sort of thing fails.. # 0.99941584433815966 != 1.0 within 3 places self.assertAlmostEqual( kImageOut[i, j, afwImage.LOCAL] / kImageIn[i, j, afwImage.LOCAL], 1.0, 2) # now repeat with noise added; decrease precision of comparison self.addNoise(smi2) kc = ipDiffim.KernelCandidateF(self.x02, self.y02, tmi2, smi2, self.ps) kList = ipDiffim.makeKernelBasisList(self.subconfig) kc.build(kList) self.assertEqual(kc.isInitialized(), True) kImageOut = kc.getImage() soln = kc.getKernelSolution(ipDiffim.KernelCandidateF.RECENT) self.assertAlmostEqual(soln.getKsum(), kSumIn, 3) if not self.ps.get("fitForBackground"): self.assertEqual(soln.getBackground(), 0.0) for j in range(kImageOut.getHeight()): for i in range(kImageOut.getWidth()): if kImageIn[i, j, afwImage.LOCAL] > 1e-2: self.assertAlmostEqual(kImageOut[i, j, afwImage.LOCAL], kImageIn[i, j, afwImage.LOCAL], 2)
def _makeAndTestUncorrectedDiffim(self): """Create the (un-decorrelated) diffim, and verify that its variance is too low. """ # Create the matching kernel. We used Gaussian PSFs for im1 and im2, so we can compute the "expected" # matching kernel sigma. psf1pos = self.im1ex.getPsf().getAveragePosition() psf2pos = self.im2ex.getPsf().getAveragePosition() psf1_sig = self.im1ex.getPsf().computeShape( psf1pos).getDeterminantRadius() psf2_sig = self.im2ex.getPsf().computeShape( psf2pos).getDeterminantRadius() sig_match = np.sqrt((psf1_sig**2. - psf2_sig**2.)) # Sanity check - make sure PSFs are correct. self.assertFloatsAlmostEqual(sig_match, np.sqrt((self.psf1_sigma**2. - self.psf2_sigma**2.)), rtol=2e-5) # mKernel = measAlg.SingleGaussianPsf(31, 31, sig_match) x0 = np.arange(-16, 16, 1) y0 = x0.copy() x0im, y0im = np.meshgrid(x0, y0) matchingKernel = singleGaussian2d(x0im, y0im, -1., -1., sigma_x=sig_match, sigma_y=sig_match) kernelImg = afwImage.ImageD(matchingKernel.shape[0], matchingKernel.shape[1]) kernelImg.getArray()[:, :] = matchingKernel mKernel = afwMath.FixedKernel(kernelImg) # Create the matched template by convolving the template with the matchingKernel matched_im2ex = self.im2ex.clone() convCntrl = afwMath.ConvolutionControl(False, True, 0) afwMath.convolve(matched_im2ex.getMaskedImage(), self.im2ex.getMaskedImage(), mKernel, convCntrl) # Expected (ideal) variance of difference image expected_var = self.svar + self.tvar if verbose: print('EXPECTED VARIANCE:', expected_var) # Create the diffim (uncorrected) # Uncorrected diffim exposure - variance plane is wrong (too low) tmp_diffExp = self.im1ex.getMaskedImage().clone() tmp_diffExp -= matched_im2ex.getMaskedImage() var = self._computeVarianceMean(tmp_diffExp) self.assertLess(var, expected_var) # Uncorrected diffim exposure - variance is wrong (too low) - same as above but on pixels diffExp = self.im1ex.clone() tmp = diffExp.getMaskedImage() tmp -= matched_im2ex.getMaskedImage() var = self._computePixelVariance(diffExp.getMaskedImage()) self.assertLess(var, expected_var) # Uncorrected diffim exposure - variance plane is wrong (too low) mn = self._computeVarianceMean(diffExp.getMaskedImage()) self.assertLess(mn, expected_var) if verbose: print('UNCORRECTED VARIANCE:', var, mn) return diffExp, mKernel, expected_var
def testPeakLikelihoodFlux(self): """Test measurement with PeakLikelihoodFlux """ # make and measure a series of exposures containing just one star, approximately centered bbox = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(100, 101)) kernelWidth = 35 var = 100 fwhm = 3.0 sigma = fwhm / FwhmPerSigma convolutionControl = afwMath.ConvolutionControl() psf = afwDetection.GaussianPsf(kernelWidth, kernelWidth, sigma) psfKernel = psf.getLocalKernel() psfImage = psf.computeKernelImage() sumPsfSq = numpy.sum(psfImage.getArray()**2) psfSqArr = psfImage.getArray()**2 for flux in (1000, 10000): ctrInd = afwGeom.Point2I(50, 51) ctrPos = afwGeom.Point2D(ctrInd) kernelBBox = psfImage.getBBox() kernelBBox.shift(afwGeom.Extent2I(ctrInd)) # compute predicted flux error unshMImage = makeFakeImage(bbox, [ctrPos], [flux], fwhm, var) # filter image by PSF unshFiltMImage = afwImage.MaskedImageF(unshMImage.getBBox()) afwMath.convolve(unshFiltMImage, unshMImage, psfKernel, convolutionControl) # compute predicted flux = value of image at peak / sum(PSF^2) # this is a sanity check of the algorithm, as much as anything predFlux = unshFiltMImage.getImage().get(ctrInd[0], ctrInd[1]) / sumPsfSq self.assertLess(abs(flux - predFlux), flux * 0.01) # compute predicted flux error based on filtered pixels # = sqrt(value of filtered variance at peak / sum(PSF^2)^2) predFluxErr = math.sqrt(unshFiltMImage.getVariance().get( ctrInd[0], ctrInd[1])) / sumPsfSq # compute predicted flux error based on unfiltered pixels # = sqrt(sum(unfiltered variance * PSF^2)) / sum(PSF^2) # and compare to that derived from filtered pixels; # again, this is a test of the algorithm varView = afwImage.ImageF(unshMImage.getVariance(), kernelBBox) varArr = varView.getArray() unfiltPredFluxErr = math.sqrt(numpy.sum( varArr * psfSqArr)) / sumPsfSq self.assertLess(abs(unfiltPredFluxErr - predFluxErr), predFluxErr * 0.01) for fracOffset in (afwGeom.Extent2D(0, 0), afwGeom.Extent2D(0.2, -0.3)): adjCenter = ctrPos + fracOffset if fracOffset == (0, 0): maskedImage = unshMImage filteredImage = unshFiltMImage else: maskedImage = makeFakeImage(bbox, [adjCenter], [flux], fwhm, var) # filter image by PSF filteredImage = afwImage.MaskedImageF( maskedImage.getBBox()) afwMath.convolve(filteredImage, maskedImage, psfKernel, convolutionControl) exp = afwImage.makeExposure(filteredImage) exp.setPsf(psf) control = measBase.PeakLikelihoodFluxControl() plugin, cat = makePluginAndCat( measBase.PeakLikelihoodFluxAlgorithm, "test", control, centroid="centroid") source = cat.makeRecord() source.set("centroid_x", adjCenter.getX()) source.set("centroid_y", adjCenter.getY()) plugin.measure(source, exp) measFlux = source.get("test_flux") measFluxErr = source.get("test_fluxSigma") self.assertLess(abs(measFlux - flux), flux * 0.003) self.assertLess(abs(measFluxErr - predFluxErr), predFluxErr * 0.2) # try nearby points and verify that the flux is smaller; # this checks that the sub-pixel shift is performed in the correct direction for dx in (-0.2, 0, 0.2): for dy in (-0.2, 0, 0.2): if dx == dy == 0: continue offsetCtr = afwGeom.Point2D(adjCenter[0] + dx, adjCenter[1] + dy) source = cat.makeRecord() source.set("centroid_x", offsetCtr.getX()) source.set("centroid_y", offsetCtr.getY()) plugin.measure(source, exp) self.assertLess(source.get("test_flux"), measFlux) # source so near edge of image that PSF does not overlap exposure should result in failure for edgePos in ( (1, 50), (50, 1), (50, bbox.getHeight() - 1), (bbox.getWidth() - 1, 50), ): source = cat.makeRecord() source.set("centroid_x", edgePos[0]) source.set("centroid_y", edgePos[1]) self.assertRaises( lsst.pex.exceptions.RangeError, plugin.measure, source, exp, ) # no PSF should result in failure: flags set noPsfExposure = afwImage.ExposureF(filteredImage) source = cat.makeRecord() source.set("centroid_x", edgePos[0]) source.set("centroid_y", edgePos[1]) self.assertRaises( lsst.pex.exceptions.InvalidParameterError, plugin.measure, source, noPsfExposure, )
def matchMaskedImages(self, templateMaskedImage, scienceMaskedImage, candidateList, templateFwhmPix=None, scienceFwhmPix=None): """PSF-match a MaskedImage (templateMaskedImage) to a reference MaskedImage (scienceMaskedImage). Do the following, in order: - Determine a PSF matching kernel and differential background model that matches templateMaskedImage to scienceMaskedImage - Convolve templateMaskedImage by the PSF matching kernel Parameters ---------- templateMaskedImage : `lsst.afw.image.MaskedImage` masked image to PSF-match to the reference masked image; must be warped to match the reference masked image scienceMaskedImage : `lsst.afw.image.MaskedImage` maskedImage whose PSF is 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 Returns ------- result : `callable` An `lsst.pipe.base.Struct` containing these fields: - psfMatchedMaskedImage: the PSF-matched masked image = ``templateMaskedImage`` convolved with psfMatchingKernel. This has the same xy0, dimensions and wcs as ``scienceMaskedImage``. - psfMatchingKernel: the PSF matching kernel - backgroundModel: differential background model - kernelCellSet: SpatialCellSet used to solve for the PSF matching kernel Raises ------ RuntimeError Raised if input images have different dimensions """ import lsstDebug display = lsstDebug.Info(__name__).display displayTemplate = lsstDebug.Info(__name__).displayTemplate displaySciIm = lsstDebug.Info(__name__).displaySciIm displaySpatialCells = lsstDebug.Info(__name__).displaySpatialCells maskTransparency = lsstDebug.Info(__name__).maskTransparency if not maskTransparency: maskTransparency = 0 if display: afwDisplay.setDefaultMaskTransparency(maskTransparency) if not candidateList: raise RuntimeError( "Candidate list must be populated by makeCandidateList") if not self._validateSize(templateMaskedImage, scienceMaskedImage): self.log.error("ERROR: Input images different size") raise RuntimeError("Input images different size") if display and displayTemplate: disp = afwDisplay.Display(frame=lsstDebug.frame) disp.mtv(templateMaskedImage, title="Image to convolve") lsstDebug.frame += 1 if display and displaySciIm: disp = afwDisplay.Display(frame=lsstDebug.frame) disp.mtv(scienceMaskedImage, title="Image to not convolve") lsstDebug.frame += 1 kernelCellSet = self._buildCellSet(templateMaskedImage, scienceMaskedImage, candidateList) if display and displaySpatialCells: diffimUtils.showKernelSpatialCells(scienceMaskedImage, kernelCellSet, symb="o", ctype=afwDisplay.CYAN, ctypeUnused=afwDisplay.YELLOW, ctypeBad=afwDisplay.RED, size=4, frame=lsstDebug.frame, title="Image to not convolve") lsstDebug.frame += 1 if templateFwhmPix and scienceFwhmPix: self.log.info("Matching Psf FWHM %.2f -> %.2f pix", templateFwhmPix, scienceFwhmPix) if self.kConfig.useBicForKernelBasis: tmpKernelCellSet = self._buildCellSet(templateMaskedImage, scienceMaskedImage, candidateList) nbe = diffimTools.NbasisEvaluator(self.kConfig, templateFwhmPix, scienceFwhmPix) bicDegrees = nbe(tmpKernelCellSet, self.log) basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, basisDegGauss=bicDegrees[0], metadata=self.metadata) del tmpKernelCellSet else: basisList = self.makeKernelBasisList(templateFwhmPix, scienceFwhmPix, metadata=self.metadata) spatialSolution, psfMatchingKernel, backgroundModel = self._solve( kernelCellSet, basisList) psfMatchedMaskedImage = afwImage.MaskedImageF( templateMaskedImage.getBBox()) convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(False) afwMath.convolve(psfMatchedMaskedImage, templateMaskedImage, psfMatchingKernel, convolutionControl) return pipeBase.Struct( matchedImage=psfMatchedMaskedImage, psfMatchingKernel=psfMatchingKernel, backgroundModel=backgroundModel, kernelCellSet=kernelCellSet, )
def run(self, exposure, referencePsfModel, kernelSum=1.0): """Psf-match an exposure to a model Psf Parameters ---------- exposure : `lsst.afw.image.Exposure` Exposure to Psf-match to the reference Psf model; it must return a valid PSF model via exposure.getPsf() referencePsfModel : `lsst.afw.detection.Psf` The Psf model to match to kernelSum : `float`, optional A multipicative factor to apply to the kernel sum (default=1.0) Returns ------- result : `struct` - ``psfMatchedExposure`` : the Psf-matched Exposure. This has the same parent bbox, Wcs, PhotoCalib and Filter as the input Exposure but no Psf. In theory the Psf should equal referencePsfModel but the match is likely not exact. - ``psfMatchingKernel`` : the spatially varying Psf-matching kernel - ``kernelCellSet`` : SpatialCellSet used to solve for the Psf-matching kernel - ``referencePsfModel`` : Validated and/or modified reference model used Raises ------ RuntimeError if the Exposure does not contain a Psf model """ if not exposure.hasPsf(): raise RuntimeError("exposure does not contain a Psf model") maskedImage = exposure.getMaskedImage() self.log.info("compute Psf-matching kernel") result = self._buildCellSet(exposure, referencePsfModel) kernelCellSet = result.kernelCellSet referencePsfModel = result.referencePsfModel # TODO: This should be evaluated at (or close to) the center of the # exposure's bounding box in DM-32756. sciAvgPos = exposure.getPsf().getAveragePosition() modelAvgPos = referencePsfModel.getAveragePosition() fwhmScience = exposure.getPsf().computeShape(sciAvgPos).getDeterminantRadius()*sigma2fwhm fwhmModel = referencePsfModel.computeShape(modelAvgPos).getDeterminantRadius()*sigma2fwhm basisList = makeKernelBasisList(self.kConfig, fwhmScience, fwhmModel, metadata=self.metadata) spatialSolution, psfMatchingKernel, backgroundModel = self._solve(kernelCellSet, basisList) if psfMatchingKernel.isSpatiallyVarying(): sParameters = np.array(psfMatchingKernel.getSpatialParameters()) sParameters[0][0] = kernelSum psfMatchingKernel.setSpatialParameters(sParameters) else: kParameters = np.array(psfMatchingKernel.getKernelParameters()) kParameters[0] = kernelSum psfMatchingKernel.setKernelParameters(kParameters) self.log.info("Psf-match science exposure to reference") psfMatchedExposure = afwImage.ExposureF(exposure.getBBox(), exposure.getWcs()) psfMatchedExposure.info.id = exposure.info.id psfMatchedExposure.setFilter(exposure.getFilter()) psfMatchedExposure.setPhotoCalib(exposure.getPhotoCalib()) psfMatchedExposure.getInfo().setVisitInfo(exposure.getInfo().getVisitInfo()) psfMatchedExposure.setPsf(referencePsfModel) psfMatchedMaskedImage = psfMatchedExposure.getMaskedImage() # Normalize the psf-matching kernel while convolving since its magnitude is meaningless # when PSF-matching one model to another. convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(True) afwMath.convolve(psfMatchedMaskedImage, maskedImage, psfMatchingKernel, convolutionControl) self.log.info("done") return pipeBase.Struct(psfMatchedExposure=psfMatchedExposure, psfMatchingKernel=psfMatchingKernel, kernelCellSet=kernelCellSet, metadata=self.metadata, )
def brighterFatterCorrection(self, exposure, kernel, maxIter, threshold, applyGain): """Apply brighter fatter correction in place for the image This correction takes a kernel that has been derived from flat field images to redistribute the charge. The gradient of the kernel is the deflection field due to the accumulated charge. Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x) using the following equation: Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y)))) To evaluate the derivative term we expand it as follows: 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y))) + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) ) Because we use the measured counts instead of the incident counts we apply the correction iteratively to reconstruct the original counts and the correction. We stop iterating when the summed difference between the current corrected image and the one from the previous iteration is below the threshold. We do not require convergence because the number of iterations is too large a computational cost. How we define the threshold still needs to be evaluated, the current default was shown to work reasonably well on a small set of images. For more information on the method see DocuShare Document-19407. The edges as defined by the kernel are not corrected because they have spurious values due to the convolution. """ self.log.info("Applying brighter fatter correction") image = exposure.getMaskedImage().getImage() # The image needs to be units of electrons/holes with self.gainContext(exposure, image, applyGain): kLx = numpy.shape(kernel)[0] kLy = numpy.shape(kernel)[1] kernelImage = afwImage.ImageD(kernel.astype(numpy.float64)) tempImage = image.clone() nanIndex = numpy.isnan(tempImage.getArray()) tempImage.getArray()[nanIndex] = 0. outImage = afwImage.ImageF(image.getDimensions()) corr = numpy.zeros_like(image.getArray()) prev_image = numpy.zeros_like(image.getArray()) convCntrl = afwMath.ConvolutionControl(False, True, 1) fixedKernel = afwMath.FixedKernel(kernelImage) # Define boundary by convolution region. The region that the correction will be # calculated for is one fewer in each dimension because of the second derivative terms. startX = kLx/2 endX = -kLx/2 startY = kLy/2 endY = -kLy/2 for iteration in range(maxIter): afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl) tmpArray = tempImage.getArray() outArray = outImage.getArray() # First derivative term gradTmp = numpy.gradient(tmpArray[startY:endY,startX:endX]) gradOut = numpy.gradient(outArray[startY:endY,startX:endX]) first = (gradTmp[0]*gradOut[0] + gradTmp[1]*gradOut[1]) # Second derivative term diffOut20 = numpy.gradient(gradOut[0]) diffOut21 = numpy.gradient(gradOut[1]) second = tmpArray[startY:endY, startX:endX]*(diffOut20[0] + diffOut21[1]) corr[startY:endY, startX:endX] = 0.5*(first + second) # reset tmp image and apply correction tmpArray[:,:] = image.getArray()[:,:] tmpArray[nanIndex] = 0. tmpArray[startY:endY, startX:endX] += corr[startY:endY,startX:endX] if iteration > 0: diff = numpy.sum(numpy.abs(prev_image - tmpArray)) if diff < threshold: break prev_image[:,:] = tmpArray[:,:] if iteration == maxIter -1: self.log.warn("Brighter fatter correction did not converge, final difference %f" % diff) self.log.info("Finished brighter fatter in %d iterations" % (iteration)) image.getArray()[startY:endY, startX:endX] += corr[startY:endY, startX:endX]
def measure(self): """Detect and measure sources""" mi = self.exposure.getMaskedImage() # # We do a pretty good job of interpolating, so don't propagagate the convolved CR/INTRP bits # (we'll keep them for the original CR/INTRP pixels) # savedMask = mi.getMask().Factory(mi.getMask(), True) saveBits = savedMask.getPlaneBitMask("CR") | \ savedMask.getPlaneBitMask("BAD") | \ savedMask.getPlaneBitMask("INTRP") # Bits to not convolve savedMask &= saveBits msk = mi.getMask(); msk &= ~saveBits; del msk # Clear the saved bits # # Smooth image # cnvImage = mi.Factory(mi.getBBox(afwImage.PARENT)) afwMath.convolve(cnvImage, mi, self.psf.getKernel(), afwMath.ConvolutionControl()) msk = cnvImage.getMask(); msk |= savedMask; del msk # restore the saved bits threshold = afwDetection.Threshold(3, afwDetection.Threshold.STDEV) # # Only search the part of the frame that was PSF-smoothed # llc = afwGeom.PointI(self.psf.getKernel().getWidth()/2, self.psf.getKernel().getHeight()/2) urc = afwGeom.PointI(cnvImage.getWidth() - 1, cnvImage.getHeight() - 1) - afwGeom.ExtentI(llc[0], llc[1]); middle = cnvImage.Factory(cnvImage, afwGeom.BoxI(llc, urc), afwImage.LOCAL) ds = afwDetection.FootprintSetF(middle, threshold, "DETECTED") del middle # # ds only searched the middle but it belongs to the entire MaskedImage # ds.setRegion(mi.getBBox(afwImage.PARENT)) # # We want to grow the detections into the edge by at least one pixel so that it sees the EDGE bit # grow, isotropic = 1, False ds = afwDetection.FootprintSetF(ds, grow, isotropic) ds.setMask(mi.getMask(), "DETECTED") # # Reinstate the saved (e.g. BAD) (and also the DETECTED | EDGE) bits in the unsmoothed image # savedMask <<= cnvImage.getMask() msk = mi.getMask(); msk |= savedMask; del msk del savedMask; savedMask = None #msk = mi.getMask(); msk &= ~0x10; del msk # XXXX if self.display: ds9.mtv(mi, frame = 0, lowOrderBits = True) ds9.mtv(cnvImage, frame = 1) objects = ds.getFootprints() # # Time to actually measure # msPolicy = policy.Policy.createPolicy(policy.DefaultPolicyFile("meas_algorithms", "examples/measureSources.paf")) msPolicy = msPolicy.getPolicy("measureSources") measureSources = measAlg.makeMeasureSources(self.exposure, msPolicy) self.sourceList = afwDetection.SourceSet() for i in range(len(objects)): source = afwDetection.Source() self.sourceList.append(source) source.setId(i) source.setFlagForDetection(source.getFlagForDetection() | measAlg.Flags.BINNED1); try: measureSources.apply(source, objects[i]) except Exception, e: try: print e except Exception, ee: print ee
class SourceDetectionTask(pipeBase.Task): """ Detect positive and negative sources on an exposure and return a new SourceCatalog. """ ConfigClass = SourceDetectionConfig _DefaultName = "sourceDetection" def __init__(self, schema=None, **kwds): """Create the detection task. Most arguments are simply passed onto pipe_base.Task. If schema is not None, it will be used to register a 'flags.negative' flag field that will be set for negative detections. """ pipeBase.Task.__init__(self, **kwds) if schema is not None: self.negativeFlagKey = schema.addField( "flags.negative", type="Flag", doc="set if source was detected as significantly negative") else: if self.config.thresholdPolarity == "both": self.log.log(self.log.WARN, "Detection polarity set to 'both', but no flag will be "\ "set to distinguish between positive and negative detections") self.negativeFlagKey = None @pipeBase.timeMethod def makeSourceCatalog(self, table, exposure, doSmooth=True, sigma=None, clearMask=True): """Run source detection and create a SourceCatalog. To avoid dealing with sources and tables, use detectFootprints() to just get the FootprintSets. @param table lsst.afw.table.SourceTable object that will be used to created the SourceCatalog. @param exposure Exposure to process; DETECTED mask plane will be set in-place. @param doSmooth if True, smooth the image before detection using a Gaussian of width sigma @param sigma sigma of PSF (pixels); used for smoothing and to grow detections; if None then measure the sigma of the PSF of the exposure @param clearMask Clear DETECTED{,_NEGATIVE} planes before running detection @return a Struct with: sources -- an lsst.afw.table.SourceCatalog object fpSets --- Struct returned by detectFootprints @raise pipe_base TaskError if sigma=None, doSmooth=True and the exposure has no PSF """ if self.negativeFlagKey is not None and self.negativeFlagKey not in table.getSchema( ): raise ValueError("Table has incorrect Schema") fpSets = self.detectFootprints(exposure=exposure, doSmooth=doSmooth, sigma=sigma, clearMask=clearMask) sources = afwTable.SourceCatalog(table) table.preallocate(fpSets.numPos + fpSets.numNeg) # not required, but nice if fpSets.negative: fpSets.negative.makeSources(sources) if self.negativeFlagKey: for record in sources: record.set(self.negativeFlagKey, True) if fpSets.positive: fpSets.positive.makeSources(sources) return pipeBase.Struct(sources=sources, fpSets=fpSets) @pipeBase.timeMethod def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True): """Detect footprints. @param exposure Exposure to process; DETECTED{,_NEGATIVE} mask plane will be set in-place. @param doSmooth if True, smooth the image before detection using a Gaussian of width sigma @param sigma sigma of PSF (pixels); used for smoothing and to grow detections; if None then measure the sigma of the PSF of the exposure @param clearMask Clear both DETECTED and DETECTED_NEGATIVE planes before running detection @return a lsst.pipe.base.Struct with fields: - positive: lsst.afw.detection.FootprintSet with positive polarity footprints (may be None) - negative: lsst.afw.detection.FootprintSet with negative polarity footprints (may be None) - numPos: number of footprints in positive or 0 if detection polarity was negative - numNeg: number of footprints in negative or 0 if detection polarity was positive - background: re-estimated background. None if reEstimateBackground==False @raise pipe_base TaskError if sigma=None and the exposure has no PSF """ try: import lsstDebug display = lsstDebug.Info(__name__).display except ImportError, e: try: display except NameError: display = False if exposure is None: raise RuntimeException("No exposure for detection") maskedImage = exposure.getMaskedImage() region = maskedImage.getBBox(afwImage.PARENT) if clearMask: mask = maskedImage.getMask() mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")) del mask if sigma is None: psf = exposure.getPsf() if psf is None: raise pipeBase.TaskError( "exposure has no PSF; must specify sigma") shape = psf.computeShape() sigma = shape.getDeterminantRadius() self.metadata.set("sigma", sigma) self.metadata.set("doSmooth", doSmooth) if not doSmooth: convolvedImage = maskedImage.Factory(maskedImage) middle = convolvedImage else: # smooth using a Gaussian (which is separate, hence fast) of width sigma # make a SingleGaussian (separable) kernel with the 'sigma' psf = exposure.getPsf() kWidth = (int(sigma * 7 + 0.5) / 2) * 2 + 1 # make sure it is odd self.metadata.set("smoothingKernelWidth", kWidth) gaussFunc = afwMath.GaussianFunction1D(sigma) gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc) convolvedImage = maskedImage.Factory( maskedImage.getBBox(afwImage.PARENT)) afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl()) # # Only search psf-smooth part of frame # goodBBox = gaussKernel.shrinkBBox( convolvedImage.getBBox(afwImage.PARENT)) middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False) # # Mark the parts of the image outside goodBBox as EDGE # self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE")) fpSets = pipeBase.Struct(positive=None, negative=None) if self.config.thresholdPolarity != "negative": fpSets.positive = self.thresholdImage(middle, "positive") if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive": fpSets.negative = self.thresholdImage(middle, "negative") for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")): fpSet = getattr(fpSets, polarity) if fpSet is None: continue fpSet.setRegion(region) if self.config.nSigmaToGrow > 0: nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5) self.metadata.set("nGrow", nGrow) fpSet = afwDet.FootprintSet(fpSet, nGrow, False) fpSet.setMask(maskedImage.getMask(), maskName) if not self.config.returnOriginalFootprints: setattr(fpSets, polarity, fpSet) fpSets.numPos = len(fpSets.positive.getFootprints() ) if fpSets.positive is not None else 0 fpSets.numNeg = len(fpSets.negative.getFootprints() ) if fpSets.negative is not None else 0 if self.config.thresholdPolarity != "negative": self.log.log( self.log.INFO, "Detected %d positive sources to %g sigma." % (fpSets.numPos, self.config.thresholdValue)) fpSets.background = None if self.config.reEstimateBackground: mi = exposure.getMaskedImage() bkgd = getBackground(mi, self.config.background) if self.config.adjustBackground: self.log.log( self.log.WARN, "Fiddling the background by %g" % self.config.adjustBackground) bkgd += self.config.adjustBackground fpSets.background = bkgd self.log.log( self.log.INFO, "Resubtracting the background after object detection") mi -= bkgd.getImageF() del mi if self.config.thresholdPolarity == "positive": if self.config.reEstimateBackground: mask = maskedImage.getMask() mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE") del mask fpSets.negative = None else: self.log.log( self.log.INFO, "Detected %d negative sources to %g %s" % (fpSets.numNeg, self.config.thresholdValue, ("DN" if self.config.thresholdType == "value" else "sigma"))) if display: ds9.mtv(exposure, frame=0, title="detection") if convolvedImage and display and display > 1: ds9.mtv(convolvedImage, frame=1, title="PSF smoothed") if middle and display and display > 1: ds9.mtv(middle, frame=2, title="middle") return fpSets
def convolveImage(self, maskedImage, psf, doSmooth=True): """Convolve the image with the PSF We convolve the image with a Gaussian approximation to the PSF, because this is separable and therefore fast. It's technically a correlation rather than a convolution, but since we use a symmetric Gaussian there's no difference. The convolution can be disabled with ``doSmooth=False``. If we do convolve, we mask the edges as ``EDGE`` and return the convolved image with the edges removed. This is because we can't convolve the edges because the kernel would extend off the image. Parameters ---------- maskedImage : `lsst.afw.image.MaskedImage` Image to convolve. psf : `lsst.afw.detection.Psf` PSF to convolve with (actually with a Gaussian approximation to it). doSmooth : `bool` Actually do the convolution? Set to False when running on e.g. a pre-convolved image, or a mask plane. Return Struct contents ---------------------- middle : `lsst.afw.image.MaskedImage` Convolved image, without the edges. sigma : `float` Gaussian sigma used for the convolution. """ self.metadata.set("doSmooth", doSmooth) sigma = psf.computeShape().getDeterminantRadius() self.metadata.set("sigma", sigma) if not doSmooth: middle = maskedImage.Factory(maskedImage, deep=True) return pipeBase.Struct(middle=middle, sigma=sigma) # Smooth using a Gaussian (which is separable, hence fast) of width sigma # Make a SingleGaussian (separable) kernel with the 'sigma' kWidth = self.calculateKernelSize(sigma) self.metadata.set("smoothingKernelWidth", kWidth) gaussFunc = afwMath.GaussianFunction1D(sigma) gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc) convolvedImage = maskedImage.Factory(maskedImage.getBBox()) afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl()) # # Only search psf-smoothed part of frame # goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox()) middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False) # # Mark the parts of the image outside goodBBox as EDGE # self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE")) return pipeBase.Struct(middle=middle, sigma=sigma)
def testPeakLikelihoodFlux(self): """Test measurement with PeakLikelihoodFlux """ # make mp: a flux measurer measControl = measAlg.PeakLikelihoodFluxControl() schema = afwTable.SourceTable.makeMinimalSchema() mp = measAlg.MeasureSourcesBuilder().addAlgorithm(measControl).build( schema) # make and measure a series of exposures containing just one star, approximately centered bbox = afwGeom.Box2I(afwGeom.Point2I(0, 0), afwGeom.Extent2I(100, 101)) kernelWidth = 35 var = 100 fwhm = 3.0 sigma = fwhm / FwhmPerSigma convolutionControl = afwMath.ConvolutionControl() psf = measAlg.SingleGaussianPsf(kernelWidth, kernelWidth, sigma) psfKernel = psf.getLocalKernel() psfImage = psf.computeKernelImage() sumPsfSq = numpy.sum(psfImage.getArray()**2) psfSqArr = psfImage.getArray()**2 for flux in (1000, 10000): ctrInd = afwGeom.Point2I(50, 51) ctrPos = afwGeom.Point2D(ctrInd) kernelBBox = psfImage.getBBox(afwImage.PARENT) kernelBBox.shift(afwGeom.Extent2I(ctrInd)) # compute predicted flux error unshMImage = makeFakeImage(bbox, [ctrPos], [flux], fwhm, var) # filter image by PSF unshFiltMImage = afwImage.MaskedImageF( unshMImage.getBBox(afwImage.PARENT)) afwMath.convolve(unshFiltMImage, unshMImage, psfKernel, convolutionControl) # compute predicted flux = value of image at peak / sum(PSF^2) # this is a sanity check of the algorithm, as much as anything predFlux = unshFiltMImage.getImage().get(ctrInd[0], ctrInd[1]) / sumPsfSq self.assertLess(abs(flux - predFlux), flux * 0.01) # compute predicted flux error based on filtered pixels # = sqrt(value of filtered variance at peak / sum(PSF^2)^2) predFluxErr = math.sqrt(unshFiltMImage.getVariance().get( ctrInd[0], ctrInd[1])) / sumPsfSq # compute predicted flux error based on unfiltered pixels # = sqrt(sum(unfiltered variance * PSF^2)) / sum(PSF^2) # and compare to that derived from filtered pixels; # again, this is a test of the algorithm varView = afwImage.ImageF(unshMImage.getVariance(), kernelBBox) varArr = varView.getArray() unfiltPredFluxErr = math.sqrt(numpy.sum( varArr * psfSqArr)) / sumPsfSq self.assertLess(abs(unfiltPredFluxErr - predFluxErr), predFluxErr * 0.01) for fracOffset in (afwGeom.Extent2D(0, 0), afwGeom.Extent2D(0.2, -0.3)): adjCenter = ctrPos + fracOffset if fracOffset == (0, 0): maskedImage = unshMImage filteredImage = unshFiltMImage else: maskedImage = makeFakeImage(bbox, [adjCenter], [flux], fwhm, var) # filter image by PSF filteredImage = afwImage.MaskedImageF( maskedImage.getBBox(afwImage.PARENT)) afwMath.convolve(filteredImage, maskedImage, psfKernel, convolutionControl) exposure = afwImage.makeExposure(filteredImage) exposure.setPsf(psf) table = afwTable.SourceTable.make(schema) source = table.makeRecord() mp.apply(source, exposure, afwGeom.Point2D(*adjCenter)) measFlux = source.get(measControl.name) measFluxErr = source.get(measControl.name + ".err") self.assertFalse(source.get(measControl.name + ".flags")) self.assertLess(abs(measFlux - flux), flux * 0.003) self.assertLess(abs(measFluxErr - predFluxErr), predFluxErr * 0.2) # try nearby points and verify that the flux is smaller; # this checks that the sub-pixel shift is performed in the correct direction for dx in (-0.2, 0, 0.2): for dy in (-0.2, 0, 0.2): if dx == dy == 0: continue offsetCtr = afwGeom.Point2D(adjCenter[0] + dx, adjCenter[1] + dy) table = afwTable.SourceTable.make(schema) source = table.makeRecord() mp.apply(source, exposure, offsetCtr) offsetFlux = source.get(measControl.name) self.assertLess(offsetFlux, measFlux) # source so near edge of image that PSF does not overlap exposure should result in failure for edgePos in ( (1, 50), (50, 1), (50, bbox.getHeight() - 1), (bbox.getWidth() - 1, 50), ): table = afwTable.SourceTable.make(schema) source = table.makeRecord() mp.apply(source, exposure, afwGeom.Point2D(*edgePos)) self.assertTrue(source.get(measControl.name + ".flags")) # no PSF should result in failure: flags set noPsfExposure = afwImage.ExposureF(filteredImage) table = afwTable.SourceTable.make(schema) source = table.makeRecord() mp.apply(source, noPsfExposure, afwGeom.Point2D(*adjCenter)) self.assertTrue(source.get(measControl.name + ".flags"))
def testDetection(self): """Test object detection""" # # Fix defects # # Mask known bad pixels # measAlgorithmsDir = lsst.utils.getPackageDir('meas_algorithms') badPixels = defects.policyToBadRegionList(os.path.join(measAlgorithmsDir, "policy/BadPixels.paf")) # did someone lie about the origin of the maskedImage? If so, adjust bad pixel list if self.XY0.getX() != self.mi.getX0() or self.XY0.getY() != self.mi.getY0(): dx = self.XY0.getX() - self.mi.getX0() dy = self.XY0.getY() - self.mi.getY0() for bp in badPixels: bp.shift(-dx, -dy) algorithms.interpolateOverDefects(self.mi, self.psf, badPixels) # # Subtract background # bgGridSize = 64 # was 256 ... but that gives only one region and the spline breaks bctrl = afwMath.BackgroundControl(afwMath.Interpolate.NATURAL_SPLINE) bctrl.setNxSample(int(self.mi.getWidth()/bgGridSize) + 1) bctrl.setNySample(int(self.mi.getHeight()/bgGridSize) + 1) backobj = afwMath.makeBackground(self.mi.getImage(), bctrl) self.mi.getImage()[:] -= backobj.getImageF() # # Remove CRs # crConfig = algorithms.FindCosmicRaysConfig() algorithms.findCosmicRays(self.mi, self.psf, 0, pexConfig.makePolicy(crConfig)) # # We do a pretty good job of interpolating, so don't propagagate the convolved CR/INTRP bits # (we'll keep them for the original CR/INTRP pixels) # savedMask = self.mi.getMask().Factory(self.mi.getMask(), True) saveBits = savedMask.getPlaneBitMask("CR") | \ savedMask.getPlaneBitMask("BAD") | \ savedMask.getPlaneBitMask("INTRP") # Bits to not convolve savedMask &= saveBits msk = self.mi.getMask() msk &= ~saveBits # Clear the saved bits del msk # # Smooth image # psf = algorithms.DoubleGaussianPsf(15, 15, self.FWHM/(2*math.sqrt(2*math.log(2)))) cnvImage = self.mi.Factory(self.mi.getBBox()) kernel = psf.getKernel() afwMath.convolve(cnvImage, self.mi, kernel, afwMath.ConvolutionControl()) msk = cnvImage.getMask() msk |= savedMask # restore the saved bits del msk threshold = afwDetection.Threshold(3, afwDetection.Threshold.STDEV) # # Only search the part of the frame that was PSF-smoothed # llc = lsst.geom.PointI(psf.getKernel().getWidth()//2, psf.getKernel().getHeight()//2) urc = lsst.geom.PointI(cnvImage.getWidth() - llc[0] - 1, cnvImage.getHeight() - llc[1] - 1) middle = cnvImage.Factory(cnvImage, lsst.geom.BoxI(llc, urc), afwImage.LOCAL) ds = afwDetection.FootprintSet(middle, threshold, "DETECTED") del middle # # Reinstate the saved (e.g. BAD) (and also the DETECTED | EDGE) bits in the unsmoothed image # savedMask[:] = cnvImage.getMask() msk = self.mi.getMask() msk |= savedMask del msk del savedMask if display: disp = afwDisplay.Display(frame=2) disp.mtv(self.mi, title=self._testMethodName + ": image") afwDisplay.Display(frame=3).mtv(cnvImage, title=self._testMethodName + ": cnvImage") # # Time to actually measure # schema = afwTable.SourceTable.makeMinimalSchema() sfm_config = measBase.SingleFrameMeasurementConfig() sfm_config.plugins = ["base_SdssCentroid", "base_CircularApertureFlux", "base_PsfFlux", "base_SdssShape", "base_GaussianFlux", "base_PixelFlags"] sfm_config.slots.centroid = "base_SdssCentroid" sfm_config.slots.shape = "base_SdssShape" sfm_config.slots.psfFlux = "base_PsfFlux" sfm_config.slots.gaussianFlux = None sfm_config.slots.apFlux = "base_CircularApertureFlux_3_0" sfm_config.slots.modelFlux = "base_GaussianFlux" sfm_config.slots.calibFlux = None sfm_config.plugins["base_SdssShape"].maxShift = 10.0 sfm_config.plugins["base_CircularApertureFlux"].radii = [3.0] task = measBase.SingleFrameMeasurementTask(schema, config=sfm_config) measCat = afwTable.SourceCatalog(schema) # detect the sources and run with the measurement task ds.makeSources(measCat) self.exposure.setPsf(self.psf) task.run(measCat, self.exposure) self.assertGreater(len(measCat), 0) for source in measCat: if source.get("base_PixelFlags_flag_edge"): continue if display: disp.dot("+", source.getX(), source.getY())
def makeFakeKernelSet(sizeCell=128, nCell=3, deltaFunctionCounts=1.e4, tGaussianWidth=1.0, addNoise=True, bgValue=100., display=False): """Generate test template and science images with sources. Parameters ---------- sizeCell : `int`, optional Size of the square spatial cells in pixels. nCell : `int`, optional Number of adjacent spatial cells in both direction in both images. deltaFunctionCounts : `float`, optional Flux value for the template image sources. tGaussianWidth : `float`, optional Sigma of the generated Gaussian PSF sources in the template image. addNoise : `bool`, optional If `True`, Poisson noise is added to both the generated template and science images. bgValue : `float`, optional Background level to be added to the generated science image. display : `bool`, optional If `True` displays the generated template and science images by `lsst.afw.display.Display`. Notes ----- - The generated images consist of adjacent ``nCell x nCell`` cells, each of pixel size ``sizeCell x sizeCell``. - The sources in the science image are generated by convolving the template by ``sKernel``. ``sKernel`` is a spatial `LinearCombinationKernel` of hard wired kernel bases functions. The linear combination has first order polynomial spatial dependence with polynomial parameters from ``fakeCoeffs()``. - The template image sources are generated in the center of each spatial cell from one pixel, set to `deltaFunctionCounts` counts, then convolved by a 2D Gaussian with sigma of `tGaussianWidth` along each axis. - The sources are also returned in ``kernelCellSet`` each source is "detected" exactly at the center of a cell. Returns ------- tMi : `lsst.afw.image.MaskedImage` Generated template image. sMi : `lsst.afw.image.MaskedImage` Generated science image. sKernel : `lsst.afw.math.LinearCombinationKernel` The spatial kernel used to generate the sources in the science image. kernelCellSet : `lsst.afw.math.SpatialCellSet` Cell grid of `lsst.afw.math.SpatialCell` instances, containing `lsst.ip.diffim.KernelCandidate` instances around all the generated sources in the science image. configFake : `lsst.ip.diffim.ImagePsfMatchConfig` Config instance used in the image generation. """ from . import imagePsfMatch configFake = imagePsfMatch.ImagePsfMatchConfig() configFake.kernel.name = "AL" subconfigFake = configFake.kernel.active subconfigFake.alardNGauss = 1 subconfigFake.alardSigGauss = [ 2.5, ] subconfigFake.alardDegGauss = [ 2, ] subconfigFake.sizeCellX = sizeCell subconfigFake.sizeCellY = sizeCell subconfigFake.spatialKernelOrder = 1 subconfigFake.spatialModelType = "polynomial" subconfigFake.singleKernelClipping = False # variance is a hack subconfigFake.spatialKernelClipping = False # variance is a hack if bgValue > 0.0: subconfigFake.fitForBackground = True psFake = pexConfig.makePropertySet(subconfigFake) basisList = makeKernelBasisList(subconfigFake) kSize = subconfigFake.kernelSize # This sets the final extent of each convolved delta function gaussKernelWidth = sizeCell // 2 # This sets the scale over which pixels are correlated in the # spatial convolution; should be at least as big as the kernel you # are trying to fit for spatialKernelWidth = kSize # Number of bad pixels due to convolutions border = (gaussKernelWidth + spatialKernelWidth) // 2 # Make a fake image with a matrix of delta functions totalSize = nCell * sizeCell + 2 * border tim = afwImage.ImageF(geom.Extent2I(totalSize, totalSize)) for x in range(nCell): for y in range(nCell): tim[x * sizeCell + sizeCell // 2 + border - 1, y * sizeCell + sizeCell // 2 + border - 1, afwImage.LOCAL] = deltaFunctionCounts # Turn this into stars with a narrow width; conserve counts gaussFunction = afwMath.GaussianFunction2D(tGaussianWidth, tGaussianWidth) gaussKernel = afwMath.AnalyticKernel(gaussKernelWidth, gaussKernelWidth, gaussFunction) cim = afwImage.ImageF(tim.getDimensions()) convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(True) afwMath.convolve(cim, tim, gaussKernel, convolutionControl) tim = cim # Trim off border pixels bbox = gaussKernel.shrinkBBox(tim.getBBox(afwImage.LOCAL)) tim = afwImage.ImageF(tim, bbox, afwImage.LOCAL) # Now make a science image which is this convolved with some # spatial function. Use input basis list. polyFunc = afwMath.PolynomialFunction2D(1) kCoeffs = fakeCoeffs() nToUse = min(len(kCoeffs), len(basisList)) # Make the full convolved science image sKernel = afwMath.LinearCombinationKernel(basisList[:nToUse], polyFunc) sKernel.setSpatialParameters(kCoeffs[:nToUse]) sim = afwImage.ImageF(tim.getDimensions()) convolutionControl = afwMath.ConvolutionControl() convolutionControl.setDoNormalize(True) afwMath.convolve(sim, tim, sKernel, convolutionControl) # Get the good subregion bbox = sKernel.shrinkBBox(sim.getBBox(afwImage.LOCAL)) # Add background sim += bgValue # Watch out for negative values tim += 2 * np.abs(np.min(tim.getArray())) # Add noise? if addNoise: sim = makePoissonNoiseImage(sim) tim = makePoissonNoiseImage(tim) # And turn into MaskedImages sim = afwImage.ImageF(sim, bbox, afwImage.LOCAL) svar = afwImage.ImageF(sim, True) smask = afwImage.Mask(sim.getDimensions()) smask.set(0x0) sMi = afwImage.MaskedImageF(sim, smask, svar) tim = afwImage.ImageF(tim, bbox, afwImage.LOCAL) tvar = afwImage.ImageF(tim, True) tmask = afwImage.Mask(tim.getDimensions()) tmask.set(0x0) tMi = afwImage.MaskedImageF(tim, tmask, tvar) if display: import lsst.afw.display as afwDisplay afwDisplay.Display(frame=1).mtv(tMi) afwDisplay.Display(frame=2).mtv(sMi) # Finally, make a kernelSet from these 2 images kernelCellSet = afwMath.SpatialCellSet( geom.Box2I(geom.Point2I(0, 0), geom.Extent2I(sizeCell * nCell, sizeCell * nCell)), sizeCell, sizeCell) stampHalfWidth = 2 * kSize for x in range(nCell): for y in range(nCell): xCoord = x * sizeCell + sizeCell // 2 yCoord = y * sizeCell + sizeCell // 2 p0 = geom.Point2I(xCoord - stampHalfWidth, yCoord - stampHalfWidth) p1 = geom.Point2I(xCoord + stampHalfWidth, yCoord + stampHalfWidth) bbox = geom.Box2I(p0, p1) tsi = afwImage.MaskedImageF(tMi, bbox, origin=afwImage.LOCAL) ssi = afwImage.MaskedImageF(sMi, bbox, origin=afwImage.LOCAL) kc = diffimLib.makeKernelCandidate(xCoord, yCoord, tsi, ssi, psFake) kernelCellSet.insertCandidate(kc) tMi.setXY0(0, 0) sMi.setXY0(0, 0) return tMi, sMi, sKernel, kernelCellSet, configFake
def run(self, sensorRef, templateIdList=None): """Subtract an image from a template coadd and measure the result Steps include: - warp template coadd to match WCS of image - PSF match image to warped template - subtract image from PSF-matched, warped template - persist difference image - detect sources - measure sources @param sensorRef: sensor-level butler data reference, used for the following data products: Input only: - calexp - psf - ccdExposureId - ccdExposureId_bits - self.config.coaddName + "Coadd_skyMap" - self.config.coaddName + "Coadd" Input or output, depending on config: - self.config.coaddName + "Diff_subtractedExp" Output, depending on config: - self.config.coaddName + "Diff_matchedExp" - self.config.coaddName + "Diff_src" @return pipe_base Struct containing these fields: - subtractedExposure: exposure after subtracting template; the unpersisted version if subtraction not run but detection run None if neither subtraction nor detection run (i.e. nothing useful done) - subtractRes: results of subtraction task; None if subtraction not run - sources: detected and possibly measured sources; None if detection not run """ self.log.info("Processing %s" % (sensorRef.dataId)) # initialize outputs and some intermediate products subtractedExposure = None subtractRes = None selectSources = None kernelSources = None controlSources = None diaSources = None # We make one IdFactory that will be used by both icSrc and src datasets; # I don't know if this is the way we ultimately want to do things, but at least # this ensures the source IDs are fully unique. expBits = sensorRef.get("ccdExposureId_bits") expId = int(sensorRef.get("ccdExposureId")) idFactory = afwTable.IdFactory.makeSource(expId, 64 - expBits) # Retrieve the science image we wish to analyze exposure = sensorRef.get("calexp", immediate=True) if self.config.doAddCalexpBackground: mi = exposure.getMaskedImage() mi += sensorRef.get("calexpBackground").getImage() if not exposure.hasPsf(): raise pipeBase.TaskError("Exposure has no psf") sciencePsf = exposure.getPsf() subtractedExposureName = self.config.coaddName + "Diff_differenceExp" templateExposure = None # Stitched coadd exposure templateSources = None # Sources on the template image if self.config.doSubtract: template = self.getTemplate.run(exposure, sensorRef, templateIdList=templateIdList) templateExposure = template.exposure templateSources = template.sources # compute scienceSigmaOrig: sigma of PSF of science image before pre-convolution scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius() # sigma of PSF of template image before warping templateSigma = templateExposure.getPsf().computeShape( ).getDeterminantRadius() # if requested, convolve the science exposure with its PSF # (properly, this should be a cross-correlation, but our code does not yet support that) # compute scienceSigmaPost: sigma of science exposure with pre-convolution, if done, # else sigma of original science exposure if self.config.doPreConvolve: convControl = afwMath.ConvolutionControl() # cannot convolve in place, so make a new MI to receive convolved image srcMI = exposure.getMaskedImage() destMI = srcMI.Factory(srcMI.getDimensions()) srcPsf = sciencePsf if self.config.useGaussianForPreConvolution: # convolve with a simplified PSF model: a double Gaussian kWidth, kHeight = sciencePsf.getLocalKernel( ).getDimensions() preConvPsf = SingleGaussianPsf(kWidth, kHeight, scienceSigmaOrig) else: # convolve with science exposure's PSF model preConvPsf = srcPsf afwMath.convolve(destMI, srcMI, preConvPsf.getLocalKernel(), convControl) exposure.setMaskedImage(destMI) scienceSigmaPost = scienceSigmaOrig * math.sqrt(2) else: scienceSigmaPost = scienceSigmaOrig # If requested, find sources in the image if self.config.doSelectSources: if not sensorRef.datasetExists("src"): self.log.warn( "Src product does not exist; running detection, measurement, selection" ) # Run own detection and measurement; necessary in nightly processing selectSources = self.subtract.getSelectSources( exposure, sigma=scienceSigmaPost, doSmooth=not self.doPreConvolve, idFactory=idFactory, ) else: self.log.info("Source selection via src product") # Sources already exist; for data release processing selectSources = sensorRef.get("src") # Number of basis functions nparam = len( makeKernelBasisList( self.subtract.config.kernel.active, referenceFwhmPix=scienceSigmaPost * FwhmPerSigma, targetFwhmPix=templateSigma * FwhmPerSigma)) if self.config.doAddMetrics: # Modify the schema of all Sources kcQa = KernelCandidateQa(nparam) selectSources = kcQa.addToSchema(selectSources) if self.config.kernelSourcesFromRef: # match exposure sources to reference catalog astromRet = self.astrometer.loadAndMatch( exposure=exposure, sourceCat=selectSources) matches = astromRet.matches elif templateSources: # match exposure sources to template sources mc = afwTable.MatchControl() mc.findOnlyClosest = False matches = afwTable.matchRaDec(templateSources, selectSources, 1.0 * afwGeom.arcseconds, mc) else: raise RuntimeError( "doSelectSources=True and kernelSourcesFromRef=False," + "but template sources not available. Cannot match science " + "sources with template sources. Run process* on data from " + "which templates are built.") kernelSources = self.sourceSelector.selectStars( exposure, selectSources, matches=matches).starCat random.shuffle(kernelSources, random.random) controlSources = kernelSources[::self.config.controlStepSize] kernelSources = [ k for i, k in enumerate(kernelSources) if i % self.config.controlStepSize ] if self.config.doSelectDcrCatalog: redSelector = DiaCatalogSourceSelectorTask( DiaCatalogSourceSelectorConfig( grMin=self.sourceSelector.config.grMax, grMax=99.999)) redSources = redSelector.selectStars( exposure, selectSources, matches=matches).starCat controlSources.extend(redSources) blueSelector = DiaCatalogSourceSelectorTask( DiaCatalogSourceSelectorConfig( grMin=-99.999, grMax=self.sourceSelector.config.grMin)) blueSources = blueSelector.selectStars( exposure, selectSources, matches=matches).starCat controlSources.extend(blueSources) if self.config.doSelectVariableCatalog: varSelector = DiaCatalogSourceSelectorTask( DiaCatalogSourceSelectorConfig(includeVariable=True)) varSources = varSelector.selectStars( exposure, selectSources, matches=matches).starCat controlSources.extend(varSources) self.log.info( "Selected %d / %d sources for Psf matching (%d for control sample)" % (len(kernelSources), len(selectSources), len(controlSources))) allresids = {} if self.config.doUseRegister: self.log.info("Registering images") if templateSources is None: # Run detection on the template, which is # temporarily background-subtracted templateSources = self.subtract.getSelectSources( templateExposure, sigma=templateSigma, doSmooth=True, idFactory=idFactory) # Third step: we need to fit the relative astrometry. # wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources) warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs, exposure.getWcs(), exposure.getBBox()) templateExposure = warpedExp # Create debugging outputs on the astrometric # residuals as a function of position. Persistence # not yet implemented; expected on (I believe) #2636. if self.config.doDebugRegister: # Grab matches to reference catalog srcToMatch = {x.second.getId(): x.first for x in matches} refCoordKey = wcsResults.matches[0].first.getTable( ).getCoordKey() inCentroidKey = wcsResults.matches[0].second.getTable( ).getCentroidKey() sids = [m.first.getId() for m in wcsResults.matches] positions = [ m.first.get(refCoordKey) for m in wcsResults.matches ] residuals = [ m.first.get(refCoordKey).getOffsetFrom( wcsResults.wcs.pixelToSky( m.second.get(inCentroidKey))) for m in wcsResults.matches ] allresids = dict(zip(sids, zip(positions, residuals))) cresiduals = [ m.first.get(refCoordKey).getTangentPlaneOffset( wcsResults.wcs.pixelToSky( m.second.get(inCentroidKey))) for m in wcsResults.matches ] colors = numpy.array([ -2.5 * numpy.log10(srcToMatch[x].get("g")) + 2.5 * numpy.log10(srcToMatch[x].get("r")) for x in sids if x in srcToMatch.keys() ]) dlong = numpy.array([ r[0].asArcseconds() for s, r in zip(sids, cresiduals) if s in srcToMatch.keys() ]) dlat = numpy.array([ r[1].asArcseconds() for s, r in zip(sids, cresiduals) if s in srcToMatch.keys() ]) idx1 = numpy.where( colors < self.sourceSelector.config.grMin) idx2 = numpy.where( (colors >= self.sourceSelector.config.grMin) & (colors <= self.sourceSelector.config.grMax)) idx3 = numpy.where( colors > self.sourceSelector.config.grMax) rms1Long = IqrToSigma * \ (numpy.percentile(dlong[idx1], 75)-numpy.percentile(dlong[idx1], 25)) rms1Lat = IqrToSigma * (numpy.percentile(dlat[idx1], 75) - numpy.percentile(dlat[idx1], 25)) rms2Long = IqrToSigma * \ (numpy.percentile(dlong[idx2], 75)-numpy.percentile(dlong[idx2], 25)) rms2Lat = IqrToSigma * (numpy.percentile(dlat[idx2], 75) - numpy.percentile(dlat[idx2], 25)) rms3Long = IqrToSigma * \ (numpy.percentile(dlong[idx3], 75)-numpy.percentile(dlong[idx3], 25)) rms3Lat = IqrToSigma * (numpy.percentile(dlat[idx3], 75) - numpy.percentile(dlat[idx3], 25)) self.log.info("Blue star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx1]), rms1Long, numpy.median(dlat[idx1]), rms1Lat)) self.log.info( "Green star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx2]), rms2Long, numpy.median(dlat[idx2]), rms2Lat)) self.log.info("Red star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx3]), rms3Long, numpy.median(dlat[idx3]), rms3Lat)) self.metadata.add("RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1])) self.metadata.add("RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2])) self.metadata.add("RegisterRedLongOffsetMedian", numpy.median(dlong[idx3])) self.metadata.add("RegisterBlueLongOffsetStd", rms1Long) self.metadata.add("RegisterGreenLongOffsetStd", rms2Long) self.metadata.add("RegisterRedLongOffsetStd", rms3Long) self.metadata.add("RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1])) self.metadata.add("RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2])) self.metadata.add("RegisterRedLatOffsetMedian", numpy.median(dlat[idx3])) self.metadata.add("RegisterBlueLatOffsetStd", rms1Lat) self.metadata.add("RegisterGreenLatOffsetStd", rms2Lat) self.metadata.add("RegisterRedLatOffsetStd", rms3Lat) # warp template exposure to match exposure, # PSF match template exposure to exposure, # then return the difference # Return warped template... Construct sourceKernelCand list after subtract self.log.info("Subtracting images") subtractRes = self.subtract.subtractExposures( templateExposure=templateExposure, scienceExposure=exposure, candidateList=kernelSources, convolveTemplate=self.config.convolveTemplate, doWarping=not self.config.doUseRegister) subtractedExposure = subtractRes.subtractedExposure if self.config.doWriteMatchedExp: sensorRef.put(subtractRes.matchedExposure, self.config.coaddName + "Diff_matchedExp") if self.config.doDetection: self.log.info("Computing diffim PSF") if subtractedExposure is None: subtractedExposure = sensorRef.get(subtractedExposureName) # Get Psf from the appropriate input image if it doesn't exist if not subtractedExposure.hasPsf(): if self.config.convolveTemplate: subtractedExposure.setPsf(exposure.getPsf()) else: if templateExposure is None: template = self.getTemplate.run( exposure, sensorRef, templateIdList=templateIdList) subtractedExposure.setPsf(template.exposure.getPsf()) # If doSubtract is False, then subtractedExposure was fetched from disk (above), thus it may have # already been decorrelated. Thus, we do not do decorrelation if doSubtract is False. if self.config.doDecorrelation and self.config.doSubtract: decorrResult = self.decorrelate.run(exposure, templateExposure, subtractedExposure, subtractRes.psfMatchingKernel) subtractedExposure = decorrResult.correctedExposure if self.config.doDetection: self.log.info("Running diaSource detection") # Erase existing detection mask planes mask = subtractedExposure.getMaskedImage().getMask() mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")) table = afwTable.SourceTable.make(self.schema, idFactory) table.setMetadata(self.algMetadata) results = self.detection.makeSourceCatalog( table=table, exposure=subtractedExposure, doSmooth=not self.config.doPreConvolve) if self.config.doMerge: fpSet = results.fpSets.positive fpSet.merge(results.fpSets.negative, self.config.growFootprint, self.config.growFootprint, False) diaSources = afwTable.SourceCatalog(table) fpSet.makeSources(diaSources) self.log.info("Merging detections into %d sources" % (len(diaSources))) else: diaSources = results.sources if self.config.doMeasurement: self.log.info("Running diaSource measurement") if not self.config.doDipoleFitting: self.measurement.run(diaSources, subtractedExposure) else: if self.config.doSubtract: self.measurement.run(diaSources, subtractedExposure, exposure, subtractRes.matchedExposure) else: self.measurement.run(diaSources, subtractedExposure, exposure) # Match with the calexp sources if possible if self.config.doMatchSources: if sensorRef.datasetExists("src"): # Create key,val pair where key=diaSourceId and val=sourceId matchRadAsec = self.config.diaSourceMatchRadius matchRadPixel = matchRadAsec / exposure.getWcs( ).pixelScale().asArcseconds() srcMatches = afwTable.matchXy(sensorRef.get("src"), diaSources, matchRadPixel) srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId()) for srcMatch in srcMatches]) self.log.info("Matched %d / %d diaSources to sources" % (len(srcMatchDict), len(diaSources))) else: self.log.warn( "Src product does not exist; cannot match with diaSources" ) srcMatchDict = {} # Create key,val pair where key=diaSourceId and val=refId refAstromConfig = AstrometryConfig() refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec refAstrometer = AstrometryTask(refAstromConfig) astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources) refMatches = astromRet.matches if refMatches is None: self.log.warn( "No diaSource matches with reference catalog") refMatchDict = {} else: self.log.info( "Matched %d / %d diaSources to reference catalog" % (len(refMatches), len(diaSources))) refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId()) for refMatch in refMatches]) # Assign source Ids for diaSource in diaSources: sid = diaSource.getId() if sid in srcMatchDict: diaSource.set("srcMatchId", srcMatchDict[sid]) if sid in refMatchDict: diaSource.set("refMatchId", refMatchDict[sid]) if diaSources is not None and self.config.doWriteSources: sensorRef.put(diaSources, self.config.coaddName + "Diff_diaSrc") if self.config.doAddMetrics and self.config.doSelectSources: self.log.info("Evaluating metrics and control sample") kernelCandList = [] for cell in subtractRes.kernelCellSet.getCellList(): for cand in cell.begin(False): # include bad candidates kernelCandList.append(cand) # Get basis list to build control sample kernels basisList = kernelCandList[0].getKernel( KernelCandidateF.ORIG).getKernelList() controlCandList = \ diffimTools.sourceTableToCandidateList(controlSources, subtractRes.warpedExposure, exposure, self.config.subtract.kernel.active, self.config.subtract.kernel.active.detectionConfig, self.log, doBuild=True, basisList=basisList) kcQa.apply(kernelCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel, dof=nparam) kcQa.apply(controlCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel) if self.config.doDetection: kcQa.aggregate(selectSources, self.metadata, allresids, diaSources) else: kcQa.aggregate(selectSources, self.metadata, allresids) sensorRef.put(selectSources, self.config.coaddName + "Diff_kernelSrc") if self.config.doWriteSubtractedExp: sensorRef.put(subtractedExposure, subtractedExposureName) self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources) return pipeBase.Struct( subtractedExposure=subtractedExposure, subtractRes=subtractRes, sources=diaSources, )
def _testImages(self): """Check that the variance of the corrected diffim matches the theoretical value. """ # Create the matching kernel. We used Gaussian PSFs for im1 and im2, so we can compute the "expected" # matching kernel sigma. psf1_sig = self.im1ex.getPsf().computeShape().getDeterminantRadius() psf2_sig = self.im2ex.getPsf().computeShape().getDeterminantRadius() sig_match = np.sqrt((psf1_sig**2. - psf2_sig**2.)) # Sanity check - make sure PSFs are correct. self.assertClose(sig_match, np.sqrt((self.psf1_sigma**2. - self.psf2_sigma**2.)), rtol=1e-5) # mKernel = measAlg.SingleGaussianPsf(31, 31, sig_match) x0 = np.arange(-16, 16, 1) y0 = x0.copy() x0im, y0im = np.meshgrid(x0, y0) matchingKernel = singleGaussian2d(x0im, y0im, -1., -1., sigma_x=sig_match, sigma_y=sig_match) kernelImg = afwImage.ImageD(matchingKernel.shape[0], matchingKernel.shape[1]) kernelImg.getArray()[:, :] = matchingKernel mKernel = afwMath.FixedKernel(kernelImg) # Create the matched template by convolving the template with the matchingKernel matched_im2ex = self.im2ex.clone() convCntrl = afwMath.ConvolutionControl(False, True, 0) afwMath.convolve(matched_im2ex.getMaskedImage(), self.im2ex.getMaskedImage(), mKernel, convCntrl) # Expected (ideal) variance of difference image expected_var = self.svar + self.tvar print('Expected variance:', expected_var) # Uncorrected diffim exposure - variance plane is wrong (too low) tmp_diffExp = self.im1ex.getMaskedImage().clone() tmp_diffExp -= matched_im2ex.getMaskedImage() var = self._computeVarianceMean(tmp_diffExp) self.assertLess(var, expected_var) # Create the diffim (uncorrected) diffExp = self.im1ex.clone() tmp = diffExp.getMaskedImage() tmp -= matched_im2ex.getMaskedImage() # Uncorrected diffim exposure - variance is wrong (too low) - same as above but on pixels var = self._computePixelVariance(diffExp.getMaskedImage()) self.assertLess(var, expected_var) # Uncorrected diffim exposure - variance plane is wrong (too low) mn = self._computeVarianceMean(diffExp.getMaskedImage()) self.assertLess(mn, expected_var) print('UNCORRECTED VARIANCE:', var, mn) task = DecorrelateALKernelTask() decorrResult = task.run(self.im1ex, self.im2ex, diffExp, mKernel) corrected_diffExp = decorrResult.correctedExposure # Corrected diffim - variance should be close to expected. # We set the tolerance a bit higher here since the simulated images have many bright stars var = self._computePixelVariance(corrected_diffExp.getMaskedImage()) self.assertClose(var, expected_var, rtol=0.05) # Check statistics of variance plane in corrected diffim mn = self._computeVarianceMean(corrected_diffExp.getMaskedImage()) print('CORRECTED VARIANCE:', var, mn) self.assertClose(mn, expected_var, rtol=0.02) self.assertClose(var, mn, rtol=0.05)
def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True): """!Detect footprints. \param exposure Exposure to process; DETECTED{,_NEGATIVE} mask plane will be set in-place. \param doSmooth if True, smooth the image before detection using a Gaussian of width sigma \param sigma sigma of PSF (pixels); used for smoothing and to grow detections; if None then measure the sigma of the PSF of the exposure \param clearMask Clear both DETECTED and DETECTED_NEGATIVE planes before running detection \return a lsst.pipe.base.Struct with fields: - positive: lsst.afw.detection.FootprintSet with positive polarity footprints (may be None) - negative: lsst.afw.detection.FootprintSet with negative polarity footprints (may be None) - numPos: number of footprints in positive or 0 if detection polarity was negative - numNeg: number of footprints in negative or 0 if detection polarity was positive - background: re-estimated background. None if reEstimateBackground==False \throws lsst.pipe.base.TaskError if sigma=None and the exposure has no PSF """ try: import lsstDebug display = lsstDebug.Info(__name__).display except ImportError: try: display except NameError: display = False if exposure is None: raise RuntimeError("No exposure for detection") maskedImage = exposure.getMaskedImage() region = maskedImage.getBBox() if clearMask: mask = maskedImage.getMask() mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE")) del mask if self.config.doTempLocalBackground: tempBgRes = self.tempLocalBackground.run(maskedImage) tempLocalBkgdImage = tempBgRes.background.getImage() if sigma is None: psf = exposure.getPsf() if psf is None: raise pipeBase.TaskError( "exposure has no PSF; must specify sigma") shape = psf.computeShape() sigma = shape.getDeterminantRadius() self.metadata.set("sigma", sigma) self.metadata.set("doSmooth", doSmooth) if not doSmooth: convolvedImage = maskedImage.Factory(maskedImage) middle = convolvedImage else: # smooth using a Gaussian (which is separate, hence fast) of width sigma # make a SingleGaussian (separable) kernel with the 'sigma' psf = exposure.getPsf() kWidth = (int(sigma * 7 + 0.5) // 2) * 2 + 1 # make sure it is odd self.metadata.set("smoothingKernelWidth", kWidth) gaussFunc = afwMath.GaussianFunction1D(sigma) gaussKernel = afwMath.SeparableKernel(kWidth, kWidth, gaussFunc, gaussFunc) convolvedImage = maskedImage.Factory(maskedImage.getBBox()) afwMath.convolve(convolvedImage, maskedImage, gaussKernel, afwMath.ConvolutionControl()) # # Only search psf-smooth part of frame # goodBBox = gaussKernel.shrinkBBox(convolvedImage.getBBox()) middle = convolvedImage.Factory(convolvedImage, goodBBox, afwImage.PARENT, False) # # Mark the parts of the image outside goodBBox as EDGE # self.setEdgeBits(maskedImage, goodBBox, maskedImage.getMask().getPlaneBitMask("EDGE")) fpSets = pipeBase.Struct(positive=None, negative=None) if self.config.thresholdPolarity != "negative": fpSets.positive = self.thresholdImage(middle, "positive") if self.config.reEstimateBackground or self.config.thresholdPolarity != "positive": fpSets.negative = self.thresholdImage(middle, "negative") for polarity, maskName in (("positive", "DETECTED"), ("negative", "DETECTED_NEGATIVE")): fpSet = getattr(fpSets, polarity) if fpSet is None: continue fpSet.setRegion(region) if self.config.nSigmaToGrow > 0: nGrow = int((self.config.nSigmaToGrow * sigma) + 0.5) self.metadata.set("nGrow", nGrow) fpSet = afwDet.FootprintSet(fpSet, nGrow, self.config.isotropicGrow) fpSet.setMask(maskedImage.getMask(), maskName) if not self.config.returnOriginalFootprints: setattr(fpSets, polarity, fpSet) fpSets.numPos = len(fpSets.positive.getFootprints() ) if fpSets.positive is not None else 0 fpSets.numNeg = len(fpSets.negative.getFootprints() ) if fpSets.negative is not None else 0 if self.config.thresholdPolarity != "negative": self.log.log( self.log.INFO, "Detected %d positive sources to %g sigma." % (fpSets.numPos, self.config.thresholdValue * self.config.includeThresholdMultiplier)) if self.config.doTempLocalBackground: maskedImage += tempLocalBkgdImage fpSets.background = None if self.config.reEstimateBackground: mi = exposure.getMaskedImage() bkgd = self.background.fitBackground(mi) if self.config.adjustBackground: self.log.log( self.log.WARN, "Fiddling the background by %g" % self.config.adjustBackground) bkgd += self.config.adjustBackground fpSets.background = bkgd self.log.log( self.log.INFO, "Resubtracting the background after object detection") mi -= bkgd.getImageF() del mi if self.config.thresholdPolarity == "positive": if self.config.reEstimateBackground: mask = maskedImage.getMask() mask &= ~mask.getPlaneBitMask("DETECTED_NEGATIVE") del mask fpSets.negative = None else: self.log.log( self.log.INFO, "Detected %d negative sources to %g %s" % (fpSets.numNeg, self.config.thresholdValue, ("DN" if self.config.thresholdType == "value" else "sigma"))) if display: ds9.mtv(exposure, frame=0, title="detection") x0, y0 = exposure.getXY0() def plotPeaks(fps, ctype): if fps is None: return with ds9.Buffering(): for fp in fps.getFootprints(): for pp in fp.getPeaks(): ds9.dot("+", pp.getFx() - x0, pp.getFy() - y0, ctype=ctype) plotPeaks(fpSets.positive, "yellow") plotPeaks(fpSets.negative, "red") if convolvedImage and display and display > 1: ds9.mtv(convolvedImage, frame=1, title="PSF smoothed") return fpSets
def brighterFatterCorrection(exposure, kernel, maxIter, threshold, applyGain): """Apply brighter fatter correction in place for the image. Parameters ---------- exposure : `lsst.afw.image.Exposure` Exposure to have brighter-fatter correction applied. Modified by this method. kernel : `numpy.ndarray` Brighter-fatter kernel to apply. maxIter : scalar Number of correction iterations to run. threshold : scalar Convergence threshold in terms of the sum of absolute deviations between an iteration and the previous one. applyGain : `Bool` If True, then the exposure values are scaled by the gain prior to correction. Returns ------- diff : `float` Final difference between iterations achieved in correction. iteration : `int` Number of iterations used to calculate correction. Notes ----- This correction takes a kernel that has been derived from flat field images to redistribute the charge. The gradient of the kernel is the deflection field due to the accumulated charge. Given the original image I(x) and the kernel K(x) we can compute the corrected image Ic(x) using the following equation: Ic(x) = I(x) + 0.5*d/dx(I(x)*d/dx(int( dy*K(x-y)*I(y)))) To evaluate the derivative term we expand it as follows: 0.5 * ( d/dx(I(x))*d/dx(int(dy*K(x-y)*I(y))) + I(x)*d^2/dx^2(int(dy* K(x-y)*I(y))) ) Because we use the measured counts instead of the incident counts we apply the correction iteratively to reconstruct the original counts and the correction. We stop iterating when the summed difference between the current corrected image and the one from the previous iteration is below the threshold. We do not require convergence because the number of iterations is too large a computational cost. How we define the threshold still needs to be evaluated, the current default was shown to work reasonably well on a small set of images. For more information on the method see DocuShare Document-19407. The edges as defined by the kernel are not corrected because they have spurious values due to the convolution. """ image = exposure.getMaskedImage().getImage() # The image needs to be units of electrons/holes with gainContext(exposure, image, applyGain): kLx = numpy.shape(kernel)[0] kLy = numpy.shape(kernel)[1] kernelImage = afwImage.ImageD(kLx, kLy) kernelImage.getArray()[:, :] = kernel tempImage = image.clone() nanIndex = numpy.isnan(tempImage.getArray()) tempImage.getArray()[nanIndex] = 0. outImage = afwImage.ImageF(image.getDimensions()) corr = numpy.zeros_like(image.getArray()) prev_image = numpy.zeros_like(image.getArray()) convCntrl = afwMath.ConvolutionControl(False, True, 1) fixedKernel = afwMath.FixedKernel(kernelImage) # Define boundary by convolution region. The region that the correction will be # calculated for is one fewer in each dimension because of the second derivative terms. # NOTE: these need to use integer math, as we're using start:end as numpy index ranges. startX = kLx // 2 endX = -kLx // 2 startY = kLy // 2 endY = -kLy // 2 for iteration in range(maxIter): afwMath.convolve(outImage, tempImage, fixedKernel, convCntrl) tmpArray = tempImage.getArray() outArray = outImage.getArray() with numpy.errstate(invalid="ignore", over="ignore"): # First derivative term gradTmp = numpy.gradient(tmpArray[startY:endY, startX:endX]) gradOut = numpy.gradient(outArray[startY:endY, startX:endX]) first = (gradTmp[0] * gradOut[0] + gradTmp[1] * gradOut[1])[1:-1, 1:-1] # Second derivative term diffOut20 = numpy.diff(outArray, 2, 0)[startY:endY, startX + 1:endX - 1] diffOut21 = numpy.diff(outArray, 2, 1)[startY + 1:endY - 1, startX:endX] second = tmpArray[startY + 1:endY - 1, startX + 1:endX - 1] * (diffOut20 + diffOut21) corr[startY + 1:endY - 1, startX + 1:endX - 1] = 0.5 * (first + second) tmpArray[:, :] = image.getArray()[:, :] tmpArray[nanIndex] = 0. tmpArray[startY:endY, startX:endX] += corr[startY:endY, startX:endX] if iteration > 0: diff = numpy.sum(numpy.abs(prev_image - tmpArray)) if diff < threshold: break prev_image[:, :] = tmpArray[:, :] image.getArray()[startY + 1:endY - 1, startX + 1:endX - 1] += \ corr[startY + 1:endY - 1, startX + 1:endX - 1] return diff, iteration