def testGaussianFunction2D(self): """Note: Assumes GaussianFunction1D is correct (tested elsewhere).""" errMsg = "{} = {} != {} for pos1={}, pos2={}, x={}, y={}, sigma1={}, sigma2={}, angle={}" areaMsg = "%s area = %s != 1.0 for sigma1=%s, sigma2=%s" f = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) f1 = afwMath.GaussianFunction1D(1.0) f2 = afwMath.GaussianFunction1D(1.0) for sigma1 in (0.1, 1.0, 3.0): for sigma2 in (0.1, 1.0, 3.0): for angle in (0.0, 0.4, 1.1): sinNegAngle = math.sin(-angle) cosNegAngle = math.cos(-angle) f.setParameters((sigma1, sigma2, angle)) g = f.clone() f1.setParameters((sigma1, )) f2.setParameters((sigma2, )) fSum = 0.0 delta1 = sigma1 / 5.0 delta2 = sigma2 / 5.0 for pos1 in np.arange(-sigma1 * 5, sigma1 * 5.01, delta1): for pos2 in np.arange(-sigma2 * 5.0, sigma2 * 5.01, delta2): x = (cosNegAngle * pos1) + (sinNegAngle * pos2) y = (-sinNegAngle * pos1) + (cosNegAngle * pos2) predVal = f1(pos1) * f2(pos2) fSum += predVal msg = errMsg.format( type(f).__name__, f(x, y), predVal, pos1, pos2, x, y, sigma1, sigma2, angle) self.assertFloatsAlmostEqual(f(x, y), predVal, msg=msg, atol=self.atol, rtol=None) msg = errMsg.format( type(g).__name__, g(x, y), predVal, pos1, pos2, x, y, sigma1, sigma2, angle) + "; clone" self.assertFloatsAlmostEqual(g(x, y), predVal, msg=msg, atol=self.atol, rtol=None) approxArea = fSum * delta1 * delta2 msg = areaMsg % (type(f).__name__, approxArea, sigma1, sigma2) # approxArea is very approximate, so we need a high # tolerance threshold. self.assertFloatsAlmostEqual(approxArea, 1.0, msg=msg, atol=1e-6, rtol=None)
def testZeroSizeKernel(self): """Creating a kernel with width or height < 1 should raise an exception. Note: this ignores the default constructors, which produce kernels with height = width = 0. The default constructors are only intended to support persistence, not to produce useful kernels. """ gaussFunc2D = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) gaussFunc1D = afwMath.GaussianFunction1D(1.0) zeroPoint = lsst.geom.Point2I(0, 0) for kWidth in (-1, 0, 1): for kHeight in (-1, 0, 1): if (kHeight > 0) and (kWidth > 0): continue if (kHeight >= 0) and (kWidth >= 0): # don't try to create an image with negative dimensions blankImage = afwImage.ImageF( lsst.geom.Extent2I(kWidth, kHeight)) self.assertRaises( Exception, afwMath.FixedKernel, blankImage) self.assertRaises( Exception, afwMath.AnalyticKernel, kWidth, kHeight, gaussFunc2D) self.assertRaises(Exception, afwMath.SeparableKernel, kWidth, kHeight, gaussFunc1D, gaussFunc1D) self.assertRaises( Exception, afwMath.DeltaFunctionKernel, kWidth, kHeight, zeroPoint)
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["doSmooth"] = doSmooth sigma = psf.computeShape(psf.getAveragePosition()).getDeterminantRadius() self.metadata["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["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 smooth_gauss(masked_image, sigma, nsigma=7.0): """ Smooth image with a Gaussian kernel. Parameters ---------- masked_image : lsst.afw.image.imageLib.MaskedImageF Masked image object to be smoothed sigma : float Standard deviation of Gaussian nsigma : float, optional Number of sigma for kernel width Returns ------- convolved_image : lsst.afw.image.imageLib.MaskedImageF The convolved masked image """ width = (int(sigma*nsigma + 0.5) // 2)*2 + 1 # make sure it is odd gauss_func = afwMath.GaussianFunction1D(sigma) gauss_kern = afwMath.SeparableKernel(width, width, gauss_func, gauss_func) convolved_image = masked_image.Factory(masked_image.getBBox()) afwMath.convolve(convolved_image, masked_image, gauss_kern, afwMath.ConvolutionControl()) return convolved_image
def testGaussianFunction1D(self): def basicGaussian(x, sigma): return (1.0 / (sigma * math.sqrt(2 * math.pi))) * math.exp( -x**2 / (2.0 * sigma**2)) f = afwMath.GaussianFunction1D(1.0) for xsigma in (0.1, 1.0, 3.0): f.setParameters((xsigma, )) g = f.clone() xdelta = xsigma / 10.0 fSum = 0.0 for x in np.arange(-xsigma * 20, xsigma * 20.01, xdelta): predVal = basicGaussian(x, xsigma) fSum += predVal msg = f"{type(f).__name__} = {f(x)} != {predVal} for x={x}, xsigma={xsigma}" self.assertFloatsAlmostEqual(f(x), predVal, msg=msg, atol=self.atol, rtol=None) msg += "; clone" self.assertFloatsAlmostEqual(g(x), predVal, msg=msg, atol=self.atol, rtol=None) approxArea = fSum * xdelta msg = f"{type(f).__name__} area = {approxArea} != 1.0 for xsigma={xsigma}" self.assertFloatsAlmostEqual(approxArea, 1.0, msg=msg, atol=self.atol, rtol=None)
def testGaussianFunction1D(self): """A test for GaussianFunction1D""" def basicGaussian(x, sigma): return (1.0 / (sigma * math.sqrt(2 * math.pi))) * math.exp( -x**2 / (2.0 * sigma**2)) f = afwMath.GaussianFunction1D(1.0) for xsigma in (0.1, 1.0, 3.0): f.setParameters((xsigma, )) g = f.clone() xdelta = xsigma / 10.0 fSum = 0.0 for x in numpy.arange(-xsigma * 20, xsigma * 20.01, xdelta): predVal = basicGaussian(x, xsigma) fSum += predVal if not numpy.allclose(predVal, f(x)): self.fail("%s = %s != %s for x=%s, xsigma=%s" % \ (type(f).__name__, f(x), predVal, x, xsigma)) if not numpy.allclose(predVal, g(x)): self.fail("%s = %s != %s for x=%s, xsigma=%s; clone" % \ (type(f).__name__, f(x), predVal, x, xsigma)) approxArea = fSum * xdelta if not numpy.allclose(approxArea, 1.0): self.fail("%s area = %s != 1.0 for xsigma=%s" % \ (type(f).__name__, approxArea, xsigma))
def testSpatiallyVaryingSeparableConvolve(self): """Test convolution with a spatially varying SeparableKernel """ kWidth = 7 kHeight = 6 # create spatial model sFunc = afwMath.PolynomialFunction2D(1) minSigma = 0.1 maxSigma = 3.0 # spatial parameters are a list of entries, one per kernel parameter; # each entry is a list of spatial parameters sParams = ( (minSigma, (maxSigma - minSigma) / self.width, 0.0), (minSigma, 0.0, (maxSigma - minSigma) / self.height), (0.0, 0.0, 0.0), ) gaussFunc1 = afwMath.GaussianFunction1D(1.0) gaussFunc2 = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) separableKernel = afwMath.SeparableKernel(kWidth, kHeight, gaussFunc1, gaussFunc1, sFunc) analyticKernel = afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc2, sFunc) separableKernel.setSpatialParameters(sParams[0:2]) analyticKernel.setSpatialParameters(sParams) self.runStdTest( separableKernel, refKernel=analyticKernel, kernelDescr="Spatially Varying Gaussian Separable Kernel")
def convolveImage(self, maskedImage, psf, doSmooth=True): """Convolve the image with the PSF This differs from the standard SourceDetectionTask convolveImage in that it allows the user to specify a scaling to the sigma. For GOTO coadds, we found that convolving by the sigma blurred the image too much, meaning that some peaks were missed. By reducing the width of the Gaussian kernel, the smoothing is reduced. 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? 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() # This is the only thing that differs from SourceDetectionTask: sigma *= self.config.scaleSigma self.metadata.set("sigma", sigma) if not doSmooth: middle = maskedImage.Factory(maskedImage) 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 testMakeBadKernels(self): """Attempt to make various invalid kernels; make sure the constructor shows an exception """ kWidth = 4 kHeight = 3 gaussFunc1 = afwMath.GaussianFunction1D(1.0) gaussFunc2 = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) spFunc = afwMath.PolynomialFunction2D(1) kernelList = [] kernelList.append(afwMath.FixedKernel( afwImage.ImageD(lsst.geom.Extent2I(kWidth, kHeight), 0.1))) kernelList.append(afwMath.FixedKernel( afwImage.ImageD(lsst.geom.Extent2I(kWidth, kHeight), 0.2))) for numKernelParams in (2, 4): spFuncList = [] for ii in range(numKernelParams): spFuncList.append(spFunc.clone()) try: afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc2, spFuncList) self.fail("Should have failed with wrong # of spatial functions") except pexExcept.Exception: pass for numKernelParams in (1, 3): spFuncList = [] for ii in range(numKernelParams): spFuncList.append(spFunc.clone()) try: afwMath.LinearCombinationKernel(kernelList, spFuncList) self.fail("Should have failed with wrong # of spatial functions") except pexExcept.Exception: pass kParamList = [0.2]*numKernelParams try: afwMath.LinearCombinationKernel(kernelList, kParamList) self.fail("Should have failed with wrong # of kernel parameters") except pexExcept.Exception: pass try: afwMath.SeparableKernel( kWidth, kHeight, gaussFunc1, gaussFunc1, spFuncList) self.fail("Should have failed with wrong # of spatial functions") except pexExcept.Exception: pass for pointX in range(-1, kWidth+2): for pointY in range(-1, kHeight+2): if (0 <= pointX < kWidth) and (0 <= pointY < kHeight): continue try: afwMath.DeltaFunctionKernel( kWidth, kHeight, lsst.geom.Point2I(pointX, pointY)) self.fail("Should have failed with point not on kernel") except pexExcept.Exception: pass
def psf(self, exposure, sources): """Measure the PSF @param exposure Exposure to process @param sources Measured sources on exposure """ assert exposure, "No exposure provided" assert sources, "No sources provided" psfPolicy = self.config['psf'] selName = psfPolicy['selectName'] selPolicy = psfPolicy['select'].getPolicy() algName = psfPolicy['algorithmName'] algPolicy = psfPolicy['algorithm'].getPolicy() self.log.log(self.log.INFO, "Measuring PSF") # # Run an extra detection step to mask out faint stars # if False: print "RHL is cleaning faint sources" import lsst.afw.math as afwMath sigma = 1.0 gaussFunc = afwMath.GaussianFunction1D(sigma) gaussKernel = afwMath.SeparableKernel(15, 15, gaussFunc, gaussFunc) im = exposure.getMaskedImage().getImage() convolvedImage = im.Factory(im.getDimensions()) afwMath.convolve(convolvedImage, im, gaussKernel) del im fs = afwDet.makeFootprintSet(convolvedImage, afwDet.createThreshold(4, "stdev")) fs = afwDet.makeFootprintSet(fs, 3, True) fs.setMask(exposure.getMaskedImage().getMask(), "DETECTED") starSelector = measAlg.makeStarSelector(selName, selPolicy) psfCandidateList = starSelector.selectStars(exposure, sources) psfDeterminer = measAlg.makePsfDeterminer(algName, algPolicy) psf, cellSet = psfDeterminer.determinePsf(exposure, psfCandidateList) # The PSF candidates contain a copy of the source, and so we need to explicitly propagate new flags for cand in psfCandidateList: cand = measAlg.cast_PsfCandidateF(cand) src = cand.getSource() if src.getFlagForDetection() & measAlg.Flags.PSFSTAR: ident = src.getId() src = sources[ident] assert src.getId() == ident src.setFlagForDetection(src.getFlagForDetection() | measAlg.Flags.PSFSTAR) exposure.setPsf(psf) return psf, cellSet
def testSeparableKernel(self): """Test SeparableKernel using a Gaussian function """ kWidth = 5 kHeight = 8 pol = pexPolicy.Policy() additionalData = dafBase.PropertySet() loc = dafPersist.LogicalLocation( os.path.join(testPath, "data", "kernel4.boost")) persistence = dafPersist.Persistence.getPersistence(pol) gaussFunc1 = afwMath.GaussianFunction1D(1.0) k = afwMath.SeparableKernel(kWidth, kHeight, gaussFunc1, gaussFunc1) fArr = np.zeros(shape=[k.getWidth(), k.getHeight()], dtype=float) np.zeros(shape=[k.getWidth(), k.getHeight()], dtype=float) gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) for xsigma in (0.1, 1.0, 3.0): gaussFunc1.setParameters((xsigma, )) for ysigma in (0.1, 1.0, 3.0): gaussFunc.setParameters((xsigma, ysigma, 0.0)) # compute array of function values and normalize for row in range(k.getHeight()): y = row - k.getCtrY() for col in range(k.getWidth()): x = col - k.getCtrX() fArr[col, row] = gaussFunc(x, y) fArr /= fArr.sum() k.setKernelParameters((xsigma, ysigma)) storageList = dafPersist.StorageList() storage = persistence.getPersistStorage("XmlStorage", loc) storageList.append(storage) persistence.persist(k, storageList, additionalData) storageList2 = dafPersist.StorageList() storage2 = persistence.getRetrieveStorage("XmlStorage", loc) storageList2.append(storage2) x = persistence.unsafeRetrieve("SeparableKernel", storageList2, additionalData) k2 = afwMath.SeparableKernel.swigConvert(x) self.kernelCheck(k, k2) kImage = afwImage.ImageD(k2.getDimensions()) k2.computeImage(kImage, True) kArr = kImage.getArray().transpose() if not np.allclose(fArr, kArr): self.fail( "%s = %s != %s for xsigma=%s, ysigma=%s" % (k2.__class__.__name__, kArr, fArr, xsigma, ysigma))
def testGaussianFunction2D(self): """A test for GaussianFunction2D Assumes GaussianFunction1D is correct (tested elsewhere) """ f = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) f1 = afwMath.GaussianFunction1D(1.0) f2 = afwMath.GaussianFunction1D(1.0) for sigma1 in (0.1, 1.0, 3.0): for sigma2 in (0.1, 1.0, 3.0): for angle in (0.0, 0.4, 1.1): sinNegAngle = math.sin(-angle) cosNegAngle = math.cos(-angle) f.setParameters((sigma1, sigma2, angle)) g = f.clone() f1.setParameters((sigma1, )) f2.setParameters((sigma2, )) fSum = 0.0 delta1 = sigma1 / 5.0 delta2 = sigma2 / 5.0 for pos1 in numpy.arange(-sigma1 * 5, sigma1 * 5.01, delta1): for pos2 in numpy.arange(-sigma2 * 5.0, sigma2 * 5.01, delta2): x = (cosNegAngle * pos1) + (sinNegAngle * pos2) y = (-sinNegAngle * pos1) + (cosNegAngle * pos2) predVal = f1(pos1) * f2(pos2) fSum += predVal if not numpy.allclose(predVal, f(x, y)): self.fail( "%s = %s != %s for pos1=%s, pos2=%s, x=%s, y=%s, sigma1=%s, sigma2=%s, angle=%s" % \ (type(f).__name__, f(x, y), predVal, pos1, pos2, x, y, sigma1, sigma2, angle)) if not numpy.allclose(predVal, g(x, y)): self.fail( "%s = %s != %s for pos1=%s, pos2=%s, x=%s, y=%s, sigma1=%s, sigma2=%s, angle=%s; clone" % \ (type(g).__name__, g(x, y), predVal, pos1, pos2, x, y, sigma1, sigma2, angle)) approxArea = fSum * delta1 * delta2 if not numpy.allclose(approxArea, 1.0): self.fail("%s area = %s != 1.0 for sigma1=%s, sigma2=%s" % \ (type(f).__name__, approxArea, sigma1, sigma2))
def findBrightestStarRHL(exp, minArea=100, maxRadialOffset=1500): import lsst.afw.detection as afwDetect import lsst.afw.image as afwImage import lsst.afw.math as afwMath cexp = exp.clone() sigma = 4 ksize = 1 + 4*int(sigma + 1) gauss1d = afwMath.GaussianFunction1D(sigma) kernel = afwMath.SeparableKernel(ksize, ksize, gauss1d, gauss1d) convolvedImage = cexp.maskedImage.Factory(cexp.maskedImage.getBBox()) afwMath.convolve(convolvedImage, cexp.maskedImage, kernel) bbox = geom.BoxI(geom.PointI(ksize//2, ksize//2), geom.PointI(cexp.getWidth() - ksize//2 - 1, cexp.getHeight() - ksize//2 - 1)) tmp = cexp.maskedImage[bbox] cexp.image *= 0 tmp[bbox, afwImage.PARENT] = convolvedImage[bbox] del convolvedImage; del tmp if False: defectBBoxes = [ geom.BoxI(geom.PointI(548, 3562), geom.PointI(642, 3999)), geom.BoxI(geom.PointI(474, 3959), geom.PointI(1056, 3999)), geom.BoxI(geom.PointI(500, 0), geom.PointI(680, 40)), ] for bbox in defectBBoxes: cexp.maskedImage[bbox] = 0 threshold = 1000 feet = afwDetect.FootprintSet(cexp.maskedImage, afwDetect.Threshold(threshold), "DETECTED").getFootprints() maxVal = None centroid = None for foot in feet: peak = foot.peaks[0] if minArea > 0 and foot.getArea() < minArea: continue x, y = peak.getCentroid() if np.hypot(x - 2036, y - 2000) > maxRadialOffset: continue if maxVal is None or peak.getPeakValue() > maxVal: maxVal = peak.getPeakValue() centroid = peak.getCentroid() return centroid
def testSeparableKernel(self): """Test SeparableKernel using a Gaussian function """ kWidth = 5 kHeight = 8 gaussFunc1 = afwMath.GaussianFunction1D(1.0) kernel = afwMath.SeparableKernel(kWidth, kHeight, gaussFunc1, gaussFunc1) self.basicTests(kernel, 2) kernelResized = self.verifyResized(kernel) self.basicTests(kernelResized, 2) fArr = np.zeros(shape=[kernel.getWidth(), kernel.getHeight()], dtype=float) gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) for xsigma in (0.1, 1.0, 3.0): gaussFunc1.setParameters((xsigma, )) for ysigma in (0.1, 1.0, 3.0): gaussFunc.setParameters((xsigma, ysigma, 0.0)) # compute array of function values and normalize for row in range(kernel.getHeight()): y = row - kernel.getCtrY() for col in range(kernel.getWidth()): x = col - kernel.getCtrX() fArr[col, row] = gaussFunc(x, y) fArr /= fArr.sum() kernel.setKernelParameters((xsigma, ysigma)) kImage = afwImage.ImageD(kernel.getDimensions()) kernel.computeImage(kImage, True) kArr = kImage.getArray().transpose() if not np.allclose(fArr, kArr): self.fail("%s = %s != %s for xsigma=%s, ysigma=%s" % (kernel.__class__.__name__, kArr, fArr, xsigma, ysigma)) kernelClone = kernel.clone() errStr = self.compareKernels(kernel, kernelClone) if errStr: self.fail(errStr) kernel.setKernelParameters((1.2, 0.6)) errStr = self.compareKernels(kernel, kernelClone) if not errStr: self.fail( "Clone was modified by changing original's kernel parameters") self.verifyCache(kernel, hasCache=True)
def testSeparableConvolve(self): """Test convolve of a separable kernel with a spatially invariant Gaussian function """ kWidth = 7 kHeight = 6 gaussFunc1 = afwMath.GaussianFunction1D(1.0) gaussFunc2 = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) separableKernel = afwMath.SeparableKernel(kWidth, kHeight, gaussFunc1, gaussFunc1) analyticKernel = afwMath.AnalyticKernel(kWidth, kHeight, gaussFunc2) self.runStdTest( separableKernel, refKernel = analyticKernel, kernelDescr = "Gaussian Separable Kernel (compared to AnalyticKernel equivalent)")
def smoothExp(exp, fwhm, display=False): import lsst.afw.math as afwMath smoothedScienceExposure = exp.clone() gauss = afwMath.GaussianFunction1D(fwhm) kSize = int(2 * fwhm + 0.5) + 1 kernel = afwMath.SeparableKernel(kSize, kSize, gauss, gauss) afwMath.convolve(smoothedScienceExposure.getMaskedImage(), exp.getMaskedImage(), kernel) if display: import lsst.afw.display.ds9 as ds9 ds9.mtv(exp, title="science", frame=1) ds9.mtv(smoothedScienceExposure, title="smoothed", frame=2) return smoothedScienceExposure
def smooth_gauss(masked_image, sigma=2.0, nsigma=7.0, inplace=False, use_scipy=False, **kwargs): """ Smooth image with a Gaussian kernel. Parameters ---------- masked_image : lsst.afw.image.imageLib.MaskedImageF Masked image object to be smoothed sigma : float Standard deviation of Gaussian nsigma : float, optional Number of sigma for kernel width inplace : bool, optional If True, smooth the image inplace. Else, return a clone of the masked image. use_scipy : bool, optional If True, perform smoothing using scipy.ndimage. Returns ------- convolved_image : lsst.afw.image.imageLib.MaskedImageF The convolved masked image """ if use_scipy: img_arr = get_image_ndarray(masked_image) img_arr_smooth = ndi.gaussian_filter( img_arr, sigma, truncate=nsigma, **kwargs) if inplace: convolved_image = masked_image else: convolved_image = masked_image.Factory(masked_image.getBBox()) convolved_image.getImage().getArray()[:] = img_arr_smooth else: width = (int(sigma*nsigma + 0.5) // 2)*2 + 1 # make sure it is odd gauss_func = afwMath.GaussianFunction1D(sigma) gauss_kern = afwMath.SeparableKernel( width, width, gauss_func, gauss_func) convolved_image = masked_image.Factory(masked_image.getBBox()) afwMath.convolve(convolved_image, masked_image, gauss_kern, afwMath.ConvolutionControl()) if inplace: img_arr_smooth = convolved_image.getImage().getArray() masked_image.getImage().getArray()[:] = img_arr_smooth return convolved_image
def getSeparableKernel(kSize, imSize, spOrder): """Return spatially varying separable kernel: a pair of 1-d Gaussians @param kSize: kernel size (scalar; height = width) @param x, y imSize: image size """ gaussFunc = afwMath.GaussianFunction1D(1) polyFunc = afwMath.PolynomialFunction2D(spOrder) kernel = afwMath.SeparableKernel(kSize, kSize, gaussFunc, gaussFunc, polyFunc) minSigma = 0.1 maxSigma = 3.0 spParams = getSpatialParameters(2, polyFunc) spParams[0][0:3] = [minSigma, (maxSigma - minSigma) / float(imSize[0]), 0.0] spParams[1][0:3] = [minSigma, 0.0, (maxSigma - minSigma) / float(imSize[0])] kernel.setSpatialParameters(spParams); return kernel
def testCast(self): for instance in (afwMath.Chebyshev1Function1F(2), afwMath.GaussianFunction1F(1.0), afwMath.LanczosFunction1F(3), afwMath.NullFunction1F(), afwMath.PolynomialFunction1F(2)): Class = type(instance) base = instance.clone() self.assertEqual(type(base), afwMath.Function1F) derived = Class.cast(base) self.assertEqual(type(derived), Class) for instance in (afwMath.Chebyshev1Function1D(2), afwMath.GaussianFunction1D(1.0), afwMath.LanczosFunction1D(3), afwMath.NullFunction1D(), afwMath.PolynomialFunction1D(2)): Class = type(instance) base = instance.clone() self.assertEqual(type(base), afwMath.Function1D) derived = Class.cast(base) self.assertEqual(type(derived), Class) for instance in (afwMath.Chebyshev1Function2F(2), afwMath.GaussianFunction2F(1.0, 1.0), afwMath.DoubleGaussianFunction2F(1.0), afwMath.LanczosFunction2F(3), afwMath.NullFunction2F(), afwMath.PolynomialFunction2F(2)): Class = type(instance) base = instance.clone() self.assertEqual(type(base), afwMath.Function2F) derived = Class.cast(base) self.assertEqual(type(derived), Class) for instance in (afwMath.Chebyshev1Function2D(2), afwMath.GaussianFunction2D(1.0, 1.0), afwMath.DoubleGaussianFunction2D(1.0), afwMath.LanczosFunction2D(3), afwMath.NullFunction2D(), afwMath.PolynomialFunction2D(2)): Class = type(instance) base = instance.clone() self.assertEqual(type(base), afwMath.Function2D) derived = Class.cast(base) self.assertEqual(type(derived), Class)
def testSeparableKernel(self): """Test SeparableKernel using a Gaussian function """ kWidth = 5 kHeight = 8 gaussFunc1 = afwMath.GaussianFunction1D(1.0) k = afwMath.SeparableKernel(kWidth, kHeight, gaussFunc1, gaussFunc1) center = k.getCtr() fArr = np.zeros(shape=[k.getWidth(), k.getHeight()], dtype=float) np.zeros(shape=[k.getWidth(), k.getHeight()], dtype=float) gaussFunc = afwMath.GaussianFunction2D(1.0, 1.0, 0.0) for xsigma in (0.1, 1.0, 3.0): gaussFunc1.setParameters((xsigma, )) for ysigma in (0.1, 1.0, 3.0): gaussFunc.setParameters((xsigma, ysigma, 0.0)) # compute array of function values and normalize for row in range(k.getHeight()): y = row - center.getY() for col in range(k.getWidth()): x = col - center.getX() fArr[col, row] = gaussFunc(x, y) fArr /= fArr.sum() k.setKernelParameters((xsigma, ysigma)) with lsst.utils.tests.getTempFilePath(".fits") as filename: k.writeFits(filename) k2 = afwMath.SeparableKernel.readFits(filename) self.kernelCheck(k, k2) kImage = afwImage.ImageD(k2.getDimensions()) k2.computeImage(kImage, True) kArr = kImage.getArray().transpose() if not np.allclose(fArr, kArr): self.fail(f"{k2.__class__.__name__} = {kArr} != {fArr} " f"for xsigma={xsigma}, ysigma={ysigma}")
def testSVSeparableKernel(self): """Test spatially varying SeparableKernel using a Gaussian function Just tests cloning. """ kWidth = 5 kHeight = 8 # spatial model spFunc = afwMath.PolynomialFunction2D(1) # spatial parameters are a list of entries, one per kernel parameter; # each entry is a list of spatial parameters sParams = ( (1.0, 1.0, 0.0), (1.0, 0.0, 1.0), ) gaussFunc = afwMath.GaussianFunction1D(1.0) kernel = afwMath.SeparableKernel( kWidth, kHeight, gaussFunc, gaussFunc, spFunc) kernel.setSpatialParameters(sParams) kernelClone = kernel.clone() errStr = self.compareKernels(kernel, kernelClone) if errStr: self.fail(errStr) newSParams = ( (0.1, 0.2, 0.5), (0.1, 0.5, 0.2), ) kernel.setSpatialParameters(newSParams) errStr = self.compareKernels(kernel, kernelClone) if not errStr: self.fail( "Clone was modified by changing original's spatial parameters")
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
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