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
47 dimensions=(
"instrument",
"visit",
"detector")):
48 inputExposure = cT.Input(
49 doc=
"Input exposure from which to extract bright star stamps",
51 storageClass=
"ExposureF",
52 dimensions=(
"visit",
"detector")
55 doc=
"Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
57 storageClass=
"Background",
58 dimensions=(
"instrument",
"visit",
"detector")
60 refCat = cT.PrerequisiteInput(
61 doc=
"Reference catalog that contains bright star positions",
62 name=
"gaia_dr2_20200414",
63 storageClass=
"SimpleCatalog",
64 dimensions=(
"skypix",),
68 brightStarStamps = cT.Output(
69 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
70 name=
"brightStarStamps",
71 storageClass=
"BrightStarStamps",
72 dimensions=(
"visit",
"detector")
77 if not config.doApplySkyCorr:
78 self.inputs.remove(
"skyCorr")
82 pipelineConnections=ProcessBrightStarsConnections):
83 """Configuration parameters for ProcessBrightStarsTask
85 magLimit = pexConfig.Field(
87 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
90 stampSize = pexConfig.ListField(
92 doc=
"Size of the stamps to be extracted, in pixels",
95 modelStampBuffer = pexConfig.Field(
97 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
98 "be saved in. This will also be the size of the extended PSF model.",
101 doRemoveDetected = pexConfig.Field(
103 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to "
107 doApplyTransform = pexConfig.Field(
109 doc=
"Apply transform to bright star stamps to correct for optical distortions?",
112 warpingKernelName = pexConfig.ChoiceField(
114 doc=
"Warping kernel",
117 "bilinear":
"bilinear interpolation",
118 "lanczos3":
"Lanczos kernel of order 3",
119 "lanczos4":
"Lanczos kernel of order 4",
120 "lanczos5":
"Lanczos kernel of order 5",
123 annularFluxRadii = pexConfig.ListField(
125 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
129 annularFluxStatistic = pexConfig.ChoiceField(
131 doc=
"Type of statistic to use to compute annular flux.",
136 "MEANCLIP":
"clipped mean",
139 numSigmaClip = pexConfig.Field(
141 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
144 numIter = pexConfig.Field(
146 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
149 badMaskPlanes = pexConfig.ListField(
151 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
153 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
155 minPixelsWithinFrame = pexConfig.Field(
157 doc=
"Minimum number of pixels that must fall within the stamp boundary for the bright star to be"
158 " saved when its center is beyond the exposure boundary.",
161 doApplySkyCorr = pexConfig.Field(
163 doc=
"Apply full focal plane sky correction before extracting stars?",
166 refObjLoader = pexConfig.ConfigurableField(
167 target=LoadIndexedReferenceObjectsTask,
168 doc=
"Reference object loader for astrometric calibration.",
172 self.
refObjLoaderrefObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
176 """The description of the parameters for this Task are detailed in
177 :lsst-task:`~lsst.pipe.base.PipelineTask`.
181 `ProcessBrightStarsTask` is used to extract, process, and store small
182 image cut-outs (or "postage stamps") around bright stars. It relies on
183 three methods, called in succession:
186 Find bright stars within the exposure using a reference catalog and
187 extract a stamp centered on each.
189 Shift and warp each stamp to remove optical distortions and sample all
190 stars on the same pixel grid.
191 `measureAndNormalize`
192 Compute the flux of an object in an annulus and normalize it. This is
193 required to normalize each bright star stamp as their central pixels
194 are likely saturated and/or contain ghosts, and cannot be used.
196 ConfigClass = ProcessBrightStarsConfig
197 _DefaultName =
"processBrightStars"
198 RunnerClass = pipeBase.ButlerInitializedTaskRunner
200 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
203 self.
modelStampSizemodelStampSize = [int(self.config.stampSize[0]*self.config.modelStampBuffer),
204 int(self.config.stampSize[1]*self.config.modelStampBuffer)]
213 if butler
is not None:
214 self.makeSubtask(
'refObjLoader', butler=butler)
217 """Apply correction to the sky background level.
219 Sky corrections can be generated with the 'skyCorrection.py'
220 executable in pipe_drivers. Because the sky model used by that
221 code extends over the entire focal plane, this can produce
222 better sky subtraction.
223 The calexp is updated in-place.
227 calexp : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
229 skyCorr : `lsst.afw.math.backgroundList.BackgroundList` or None,
231 Full focal plane sky correction, obtained by running
232 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
235 calexp = calexp.getMaskedImage()
236 calexp -= skyCorr.getImage()
239 """ Read position of bright stars within `inputExposure` from refCat
244 inputExposure : `afwImage.exposure.exposure.ExposureF`
245 The image from which bright star stamps should be extracted.
246 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
247 Loader to find objects within a reference catalog.
251 result : `lsst.pipe.base.Struct`
252 Result struct with components:
254 - ``starIms``: `list` of stamps
255 - ``pixCenters``: `list` of corresponding coordinates to each
256 star's center, in pixels.
257 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
258 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
261 if refObjLoader
is None:
262 refObjLoader = self.refObjLoader
267 wcs = inputExposure.getWcs()
269 inputIm = inputExposure.maskedImage
270 inputExpBBox = inputExposure.getBBox()
271 dilatationExtent =
geom.Extent2I(np.array(self.config.stampSize) - self.config.minPixelsWithinFrame)
273 withinCalexp = refObjLoader.loadPixelBox(inputExpBBox.dilatedBy(dilatationExtent), wcs,
274 filterName=
"phot_g_mean")
275 refCat = withinCalexp.refCat
277 fluxLimit = ((self.config.magLimit*u.ABmag).
to(u.nJy)).to_value()
278 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
279 bright = GFluxes > fluxLimit
281 allGMags = [((gFlux*u.nJy).
to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
282 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
283 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
284 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
286 cpix = wcs.skyToPixel(sp)
288 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
289 except InvalidParameterError:
291 bboxCorner = np.array(cpix) - np.array(self.config.stampSize)/2
295 clippedStarBBox.clip(inputExpBBox)
296 if clippedStarBBox.getArea() > 0:
299 starIm = afwImage.ExposureF(bbox=idealBBox)
300 starIm.image[:] = np.nan
301 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
303 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
304 starIm.maskedImage[clippedStarBBox] = clippedIm
306 starIm.setDetector(inputExposure.getDetector())
307 starIm.setWcs(inputExposure.getWcs())
310 if self.config.doRemoveDetected:
313 afwDetect.Threshold.BITMASK)
315 allFootprints = omask.getFootprints()
317 for fs
in allFootprints:
319 otherFootprints.append(fs)
320 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
321 if not nbMatchingFootprints == 1:
322 self.log.
warn(
"Failed to uniquely identify central DETECTION footprint for star "
323 f
"{allIds[j]}; found {nbMatchingFootprints} footprints instead.")
324 omask.setFootprints(otherFootprints)
325 omask.setMask(starIm.mask,
"BAD")
326 starIms.append(starIm)
327 pixCenters.append(cpix)
328 GMags.append(allGMags[j])
329 ids.append(allIds[j])
330 return pipeBase.Struct(starIms=starIms,
331 pixCenters=pixCenters,
336 """Warps and shifts all given stamps so they are sampled on the same
337 pixel grid and centered on the central pixel. This includes rotating
338 the stamp depending on detector orientation.
342 stamps : `collections.abc.Sequence`
343 [`afwImage.exposure.exposure.ExposureF`]
344 Image cutouts centered on a single object.
345 pixCenters : `collections.abc.Sequence` [`geom.Point2D`]
346 Positions of each object's center (as obtained from the refCat),
351 warpedStars : `list` [`afwImage.maskedImage.maskedImage.MaskedImage`]
356 bufferPix = (self.
modelStampSizemodelStampSize[0] - self.config.stampSize[0],
360 det = stamps[0].getDetector()
362 if self.config.doApplyTransform:
363 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
365 pixToTan = tFactory.makeIdentityTransform()
367 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
369 yaw = det.getOrientation().getYaw()
370 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
374 for star, cent
in zip(stamps, pixCenters):
376 destImage = afwImage.MaskedImageF(*self.
modelStampSizemodelStampSize)
378 newBottomLeft = pixToTan.applyForward(bottomLeft)
379 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
380 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
384 destImage.setXY0(newBottomLeft)
387 newCenter = pixToTan.applyForward(cent)
388 shift = self.
modelCentermodelCenter[0] + newBottomLeft[0] - newCenter[0],\
389 self.
modelCentermodelCenter[1] + newBottomLeft[1] - newCenter[1]
391 shiftTransform = tFactory.makeTransform(affineShift)
394 starWarper = pixToTan.then(shiftTransform)
398 starWarper, warpCont)
400 self.log.
debug(
"Warping of a star failed: no good pixel in output")
403 destImage.setXY0(0, 0)
408 warpedStars.append(destImage.clone())
412 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
413 """Identify bright stars within an exposure using a reference catalog,
414 extract stamps around each, then preprocess them. The preprocessing
415 steps are: shifting, warping and potentially rotating them to the same
416 pixel grid; computing their annular flux and normalizing them.
420 inputExposure : `afwImage.exposure.exposure.ExposureF`
421 The image from which bright star stamps should be extracted.
422 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
423 Loader to find objects within a reference catalog.
424 dataId : `dict` or `lsst.daf.butler.DataCoordinate`
425 The dataId of the exposure (and detector) bright stars should be
427 skyCorr : `lsst.afw.math.backgroundList.BackgroundList` or ``None``,
429 Full focal plane sky correction, obtained by running
430 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
434 result : `lsst.pipe.base.Struct`
435 Result struct with component:
437 - ``brightStarStamps``: ``bSS.BrightStarStamps``
439 if self.config.doApplySkyCorr:
440 self.log.
info(
"Applying sky correction to exposure %s (exposure will be modified in-place).",
443 self.log.
info(
"Extracting bright stars from exposure %s", dataId)
445 extractedStamps = self.
extractStampsextractStamps(inputExposure, refObjLoader=refObjLoader)
447 self.log.
info(
"Applying warp and/or shift to %i star stamps from exposure %s",
448 len(extractedStamps.starIms), dataId)
449 warpedStars = self.
warpStampswarpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
450 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
451 gaiaGMag=extractedStamps.GMags[j],
452 gaiaId=extractedStamps.gaiaIds[j])
453 for j, warp
in enumerate(warpedStars)]
455 self.log.
info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
456 len(warpedStars), dataId)
459 statsControl.setNumSigmaClip(self.config.numSigmaClip)
460 statsControl.setNumIter(self.config.numIter)
461 innerRadius, outerRadius = self.config.annularFluxRadii
463 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
464 innerRadius=innerRadius,
465 outerRadius=outerRadius,
467 statsControl=statsControl,
469 badMaskPlanes=self.config.badMaskPlanes)
470 return pipeBase.Struct(brightStarStamps=brightStarStamps)
473 """Read in required calexp, extract and process stamps around bright
474 stars and write them to disk.
478 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
479 Data reference to the calexp to extract bright stars from.
481 calexp = dataRef.get(
"calexp")
482 skyCorr = dataRef.get(
"skyCorr")
if self.config.doApplySkyCorr
else None
483 output = self.
runrun(calexp, dataId=dataRef.dataId, skyCorr=skyCorr)
485 dataRef.put(output.brightStarStamps,
"brightStarStamps")
486 return pipeBase.Struct(brightStarStamps=output.brightStarStamps)
489 inputs = butlerQC.get(inputRefs)
490 inputs[
'dataId'] = str(butlerQC.quantum.dataId)
492 for ref
in inputRefs.refCat],
493 refCats=inputs.pop(
"refCat"),
494 config=self.config.refObjLoader)
495 output = self.
runrun(**inputs, refObjLoader=refObjLoader)
496 butlerQC.put(output, outputRefs)
A Threshold is used to pass a threshold value to detection algorithms.
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Pass parameters to a Statistics object.
Parameters to control convolution.
An integer coordinate rectangle.
Point in an unspecified spherical coordinate system.
def __init__(self, *config=None)
def runDataRef(self, dataRef)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def warpStamps(self, stamps, pixCenters)
def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None)
def applySkyCorr(self, calexp, skyCorr)
def extractStamps(self, inputExposure, refObjLoader=None)
def __init__(self, butler=None, initInputs=None, *args, **kwargs)
Property stringToStatisticsProperty(std::string const property)
Conversion function to switch a string to a Property (see Statistics.h)
std::shared_ptr< ImageT > rotateImageBy90(ImageT const &image, int nQuarter)
Rotate an image by an integral number of quarter turns.
int warpImage(DestImageT &destImage, geom::SkyWcs const &destWcs, SrcImageT const &srcImage, geom::SkyWcs const &srcWcs, WarpingControl const &control, typename DestImageT::SinglePixel padValue=lsst::afw::math::edgePixel< DestImageT >(typename lsst::afw::image::detail::image_traits< DestImageT >::image_category()))
Warp an Image or MaskedImage to a new Wcs.
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.