22 """Extract small cutouts around bright stars, normalize and warp them to the
23 same arbitrary pixel grid.
26 __all__ = [
"ProcessBrightStarsTask"]
29 import astropy.units
as u
33 from lsst.afw import image
as afwImage
34 from lsst.afw import detection
as afwDetect
35 from lsst.afw import cameraGeom
as cg
46 inputExposure = cT.Input(
47 doc=
"Input exposure from which to extract bright star stamps",
49 storageClass=
"ExposureF",
50 dimensions=(
"visit",
"detector")
52 refCat = cT.PrerequisiteInput(
53 doc=
"Reference catalog that contains bright star positions",
54 name=
"gaia_dr2_20200414",
55 storageClass=
"SimpleCatalog",
56 dimensions=(
"skypix",),
60 brightStarStamps = cT.Output(
61 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
62 name=
"brightStarStamps",
63 storageClass=
"BrightStarStamps",
64 dimensions=(
"visit",
"detector")
69 pipelineConnections=ProcessBrightStarsConnections):
70 """Configuration parameters for ProcessBrightStarsTask
72 magLimit = pexConfig.Field(
74 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
77 stampSize = pexConfig.ListField(
79 doc=
"Size of the stamps to be extracted, in pixels",
82 modelStampBuffer = pexConfig.Field(
84 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
85 "be saved in. This will also be the size of the extended PSF model.",
88 doRemoveDetected = pexConfig.Field(
90 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to "
94 doApplyTransform = pexConfig.Field(
96 doc=
"Apply transform to bright star stamps to correct for optical distortions?",
99 warpingKernelName = pexConfig.ChoiceField(
101 doc=
"Warping kernel",
104 "bilinear":
"bilinear interpolation",
105 "lanczos3":
"Lanczos kernel of order 3",
106 "lanczos4":
"Lanczos kernel of order 4",
107 "lanczos5":
"Lanczos kernel of order 5",
110 annularFluxRadii = pexConfig.ListField(
112 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
116 annularFluxStatistic = pexConfig.ChoiceField(
118 doc=
"Type of statistic to use to compute annular flux.",
123 "MEANCLIP":
"clipped mean",
126 numSigmaClip = pexConfig.Field(
128 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
131 numIter = pexConfig.Field(
133 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
136 badMaskPlanes = pexConfig.ListField(
138 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
140 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
142 refObjLoader = pexConfig.ConfigurableField(
143 target=LoadIndexedReferenceObjectsTask,
144 doc=
"reference object loader for astrometric calibration",
148 self.
refObjLoaderrefObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
152 """The description of the parameters for this Task are detailed in
153 :lsst-task:`~lsst.pipe.base.PipelineTask`.
157 `ProcessBrightStarsTask` is used to extract, process, and store small
158 image cut-outs (or "postage stamps") around bright stars. It relies on
159 three methods, called in succession:
162 Find bright stars within the exposure using a reference catalog and
163 extract a stamp centered on each.
165 Shift and warp each stamp to remove optical distortions and sample all
166 stars on the same pixel grid.
167 `measureAndNormalize`
168 Compute the flux of an object in an annulus and normalize it. This is
169 required to normalize each bright star stamp as their central pixels
170 are likely saturated and/or contain ghosts, and cannot be used.
172 ConfigClass = ProcessBrightStarsConfig
173 _DefaultName =
"processBrightStars"
174 RunnerClass = pipeBase.ButlerInitializedTaskRunner
176 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
179 self.
modelStampSizemodelStampSize = (int(self.config.stampSize[0]*self.config.modelStampBuffer),
180 int(self.config.stampSize[1]*self.config.modelStampBuffer))
189 if butler
is not None:
190 self.makeSubtask(
'refObjLoader', butler=butler)
193 """ Read position of bright stars within `inputExposure` from refCat
198 inputExposure : `afwImage.exposure.exposure.ExposureF`
199 The image from which bright star stamps should be extracted.
200 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
201 Loader to find objects within a reference catalog.
205 result : `lsst.pipe.base.Struct`
206 Result struct with components:
208 - ``starIms``: `list` of stamps
209 - ``pixCenters``: `list` of corresponding coordinates to each
210 star's center, in pixels.
211 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
212 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
215 if refObjLoader
is None:
216 refObjLoader = self.refObjLoader
221 wcs = inputExposure.getWcs()
223 withinCalexp = refObjLoader.loadPixelBox(inputExposure.getBBox(), wcs, filterName=
"phot_g_mean")
224 refCat = withinCalexp.refCat
226 fluxLimit = ((self.config.magLimit*u.ABmag).to(u.nJy)).to_value()
227 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
228 bright = GFluxes > fluxLimit
230 allGMags = [((gFlux*u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
231 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
232 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
233 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
235 cpix = wcs.skyToPixel(sp)
237 if (cpix[0] >= self.config.stampSize[0]/2
238 and cpix[0] < inputExposure.getDimensions()[0] - self.config.stampSize[0]/2
239 and cpix[1] >= self.config.stampSize[1]/2
240 and cpix[1] < inputExposure.getDimensions()[1] - self.config.stampSize[1]/2):
241 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
242 if self.config.doRemoveDetected:
244 detThreshold = afwDetect.Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"),
245 afwDetect.Threshold.BITMASK)
246 omask = afwDetect.FootprintSet(starIm.mask, detThreshold)
247 allFootprints = omask.getFootprints()
249 for fs
in allFootprints:
251 otherFootprints.append(fs)
252 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
253 if not nbMatchingFootprints == 1:
254 self.log.warn(
"Failed to uniquely identify central DETECTION footprint for star "
255 f
"{allIds[j]}; found {nbMatchingFootprints} footprints instead.")
256 omask.setFootprints(otherFootprints)
257 omask.setMask(starIm.mask,
"BAD")
258 starIms.append(starIm)
259 pixCenters.append(cpix)
260 GMags.append(allGMags[j])
261 ids.append(allIds[j])
262 return pipeBase.Struct(starIms=starIms,
263 pixCenters=pixCenters,
268 """Warps and shifts all given stamps so they are sampled on the same
269 pixel grid and centered on the central pixel. This includes rotating
270 the stamp depending on detector orientation.
274 stamps : `collections.abc.Sequence`
275 [`afwImage.exposure.exposure.ExposureF`]
276 Image cutouts centered on a single object.
277 pixCenters : `collections.abc.Sequence` [`geom.Point2D`]
278 Positions of each object's center (as obtained from the refCat),
283 warpedStars : `list` [`afwImage.maskedImage.maskedImage.MaskedImage`]
286 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
288 bufferPix = (self.
modelStampSizemodelStampSize[0] - self.config.stampSize[0],
292 det = stamps[0].getDetector()
294 if self.config.doApplyTransform:
295 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
297 pixToTan = tFactory.makeIdentityTransform()
299 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
301 yaw = det.getOrientation().getYaw()
302 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
306 for star, cent
in zip(stamps, pixCenters):
308 destImage = afwImage.MaskedImageF(*self.
modelStampSizemodelStampSize)
310 newBottomLeft = pixToTan.applyForward(bottomLeft)
311 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
312 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
316 destImage.setXY0(newBottomLeft)
319 newCenter = pixToTan.applyForward(cent)
320 shift = self.
modelCentermodelCenter[0] + newBottomLeft[0] - newCenter[0],\
321 self.
modelCentermodelCenter[1] + newBottomLeft[1] - newCenter[1]
323 shiftTransform = tFactory.makeTransform(affineShift)
326 starWarper = pixToTan.then(shiftTransform)
329 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
330 starWarper, warpCont)
332 self.log.debug(
"Warping of a star failed: no good pixel in output")
335 destImage.setXY0(0, 0)
339 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
340 warpedStars.append(destImage.clone())
344 def run(self, inputExposure, refObjLoader=None, dataId=None):
345 """Identify bright stars within an exposure using a reference catalog,
346 extract stamps around each, then preprocess them. The preprocessing
347 steps are: shifting, warping and potentially rotating them to the same
348 pixel grid; computing their annular flux and normalizing them.
352 inputExposure : `afwImage.exposure.exposure.ExposureF`
353 The image from which bright star stamps should be extracted.
354 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
355 Loader to find objects within a reference catalog.
356 dataId : `dict` or `lsst.daf.butler.DataCoordinate`
357 The dataId of the exposure (and detector) bright stars should be
362 result : `lsst.pipe.base.Struct`
363 Result struct with component:
365 - ``brightStarStamps``: ``bSS.BrightStarStamps``
367 self.log.info(
"Extracting bright stars from exposure %s", dataId)
369 extractedStamps = self.
extractStampsextractStamps(inputExposure, refObjLoader=refObjLoader)
371 self.log.info(
"Applying warp and/or shift to %i star stamps from exposure %s",
372 len(extractedStamps.starIms), dataId)
373 warpedStars = self.
warpStampswarpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
374 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
375 gaiaGMag=extractedStamps.GMags[j],
376 gaiaId=extractedStamps.gaiaIds[j])
377 for j, warp
in enumerate(warpedStars)]
379 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
380 len(warpedStars), dataId)
382 statsControl = afwMath.StatisticsControl()
383 statsControl.setNumSigmaClip(self.config.numSigmaClip)
384 statsControl.setNumIter(self.config.numIter)
385 innerRadius, outerRadius = self.config.annularFluxRadii
386 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
387 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
388 innerRadius=innerRadius,
389 outerRadius=outerRadius,
391 statsControl=statsControl,
393 badMaskPlanes=self.config.badMaskPlanes)
394 return pipeBase.Struct(brightStarStamps=brightStarStamps)
397 """ Read in required calexp, extract and process stamps around bright
398 stars and write them to disk.
402 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
403 Data reference to the calexp to extract bright stars from.
405 calexp = dataRef.get(
"calexp")
406 output = self.
runrun(calexp, dataId=dataRef.dataId)
408 dataRef.put(output.brightStarStamps,
"brightStarStamps")
409 return pipeBase.Struct(brightStarStamps=output.brightStarStamps)
412 inputs = butlerQC.get(inputRefs)
413 inputs[
'dataId'] = str(butlerQC.quantum.dataId)
414 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
415 for ref
in inputRefs.refCat],
416 refCats=inputs.pop(
"refCat"),
417 config=self.config.refObjLoader)
418 output = self.
runrun(**inputs, refObjLoader=refObjLoader)
419 butlerQC.put(output, outputRefs)
def runDataRef(self, dataRef)
def run(self, inputExposure, refObjLoader=None, dataId=None)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def warpStamps(self, stamps, pixCenters)
def extractStamps(self, inputExposure, refObjLoader=None)
def __init__(self, butler=None, initInputs=None, *args, **kwargs)