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 discardNanFluxStars = pexConfig.Field(
168 doc=
"Should stars with NaN annular flux be discarded?",
171 refObjLoader = pexConfig.ConfigurableField(
172 target=LoadIndexedReferenceObjectsTask,
173 doc=
"Reference object loader for astrometric calibration.",
177 self.
refObjLoaderrefObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
181 """The description of the parameters for this Task are detailed in
182 :lsst-task:`~lsst.pipe.base.PipelineTask`.
186 `ProcessBrightStarsTask` is used to extract, process, and store small
187 image cut-outs (or "postage stamps") around bright stars. It relies on
188 three methods, called in succession:
191 Find bright stars within the exposure using a reference catalog and
192 extract a stamp centered on each.
194 Shift and warp each stamp to remove optical distortions and sample all
195 stars on the same pixel grid.
196 `measureAndNormalize`
197 Compute the flux of an object in an annulus and normalize it. This is
198 required to normalize each bright star stamp as their central pixels
199 are likely saturated and/or contain ghosts, and cannot be used.
201 ConfigClass = ProcessBrightStarsConfig
202 _DefaultName =
"processBrightStars"
203 RunnerClass = pipeBase.ButlerInitializedTaskRunner
205 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
208 self.
modelStampSizemodelStampSize = [int(self.config.stampSize[0]*self.config.modelStampBuffer),
209 int(self.config.stampSize[1]*self.config.modelStampBuffer)]
218 if butler
is not None:
219 self.makeSubtask(
'refObjLoader', butler=butler)
222 """Apply correction to the sky background level.
224 Sky corrections can be generated with the 'skyCorrection.py'
225 executable in pipe_drivers. Because the sky model used by that
226 code extends over the entire focal plane, this can produce
227 better sky subtraction.
228 The calexp is updated in-place.
232 calexp : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
234 skyCorr : `lsst.afw.math.backgroundList.BackgroundList` or None,
236 Full focal plane sky correction, obtained by running
237 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
239 if isinstance(calexp, afwImage.Exposure):
240 calexp = calexp.getMaskedImage()
241 calexp -= skyCorr.getImage()
244 """ Read position of bright stars within `inputExposure` from refCat
249 inputExposure : `afwImage.exposure.exposure.ExposureF`
250 The image from which bright star stamps should be extracted.
251 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
252 Loader to find objects within a reference catalog.
256 result : `lsst.pipe.base.Struct`
257 Result struct with components:
259 - ``starIms``: `list` of stamps
260 - ``pixCenters``: `list` of corresponding coordinates to each
261 star's center, in pixels.
262 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
263 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
266 if refObjLoader
is None:
267 refObjLoader = self.refObjLoader
272 wcs = inputExposure.getWcs()
274 inputIm = inputExposure.maskedImage
275 inputExpBBox = inputExposure.getBBox()
276 dilatationExtent =
geom.Extent2I(np.array(self.config.stampSize) - self.config.minPixelsWithinFrame)
278 withinCalexp = refObjLoader.loadPixelBox(inputExpBBox.dilatedBy(dilatationExtent), wcs,
279 filterName=
"phot_g_mean")
280 refCat = withinCalexp.refCat
282 fluxLimit = ((self.config.magLimit*u.ABmag).to(u.nJy)).to_value()
283 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
284 bright = GFluxes > fluxLimit
286 allGMags = [((gFlux*u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
287 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
288 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
289 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
291 cpix = wcs.skyToPixel(sp)
293 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
294 except InvalidParameterError:
296 bboxCorner = np.array(cpix) - np.array(self.config.stampSize)/2
300 clippedStarBBox.clip(inputExpBBox)
301 if clippedStarBBox.getArea() > 0:
304 starIm = afwImage.ExposureF(bbox=idealBBox)
305 starIm.image[:] = np.nan
306 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
308 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
309 starIm.maskedImage[clippedStarBBox] = clippedIm
311 starIm.setDetector(inputExposure.getDetector())
312 starIm.setWcs(inputExposure.getWcs())
315 if self.config.doRemoveDetected:
317 detThreshold = afwDetect.Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"),
318 afwDetect.Threshold.BITMASK)
319 omask = afwDetect.FootprintSet(starIm.mask, detThreshold)
320 allFootprints = omask.getFootprints()
322 for fs
in allFootprints:
324 otherFootprints.append(fs)
325 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
326 if not nbMatchingFootprints == 1:
327 self.log.warn(
"Failed to uniquely identify central DETECTION footprint for star "
328 f
"{allIds[j]}; found {nbMatchingFootprints} footprints instead.")
329 omask.setFootprints(otherFootprints)
330 omask.setMask(starIm.mask,
"BAD")
331 starIms.append(starIm)
332 pixCenters.append(cpix)
333 GMags.append(allGMags[j])
334 ids.append(allIds[j])
335 return pipeBase.Struct(starIms=starIms,
336 pixCenters=pixCenters,
341 """Warps and shifts all given stamps so they are sampled on the same
342 pixel grid and centered on the central pixel. This includes rotating
343 the stamp depending on detector orientation.
347 stamps : `collections.abc.Sequence`
348 [`afwImage.exposure.exposure.ExposureF`]
349 Image cutouts centered on a single object.
350 pixCenters : `collections.abc.Sequence` [`geom.Point2D`]
351 Positions of each object's center (as obtained from the refCat),
356 result : `lsst.pipe.base.Struct`
357 Result struct with components:
360 `list` [`afwImage.maskedImage.maskedImage.MaskedImage`] of
361 stamps of warped stars
362 - ``warpTransforms``:
363 `list` [`afwGeom.TransformPoint2ToPoint2`] of
364 the corresponding Transform from the initial star stamp to
365 the common model grid
367 `list` [`geom.Point2I`] of coordinates of the bottom-left
368 pixels of each stamp, before rotation
369 - ``nb90Rots``: `int`, the number of 90 degrees rotations required
370 to compensate for detector orientation
373 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
375 bufferPix = (self.
modelStampSizemodelStampSize[0] - self.config.stampSize[0],
379 det = stamps[0].getDetector()
381 if self.config.doApplyTransform:
382 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
384 pixToTan = tFactory.makeIdentityTransform()
386 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
388 yaw = det.getOrientation().getYaw()
389 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
392 warpedStars, warpTransforms, xy0s = [], [], []
393 for star, cent
in zip(stamps, pixCenters):
395 destImage = afwImage.MaskedImageF(*self.
modelStampSizemodelStampSize)
397 newBottomLeft = pixToTan.applyForward(bottomLeft)
398 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
399 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
403 destImage.setXY0(newBottomLeft)
404 xy0s.append(newBottomLeft)
407 newCenter = pixToTan.applyForward(cent)
408 shift = self.
modelCentermodelCenter[0] + newBottomLeft[0] - newCenter[0],\
409 self.
modelCentermodelCenter[1] + newBottomLeft[1] - newCenter[1]
411 shiftTransform = tFactory.makeTransform(affineShift)
414 starWarper = pixToTan.then(shiftTransform)
417 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
418 starWarper, warpCont)
420 self.log.debug(
"Warping of a star failed: no good pixel in output")
423 destImage.setXY0(0, 0)
427 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
428 warpedStars.append(destImage.clone())
429 warpTransforms.append(starWarper)
430 return pipeBase.Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s,
434 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
435 """Identify bright stars within an exposure using a reference catalog,
436 extract stamps around each, then preprocess them. The preprocessing
437 steps are: shifting, warping and potentially rotating them to the same
438 pixel grid; computing their annular flux and normalizing them.
442 inputExposure : `afwImage.exposure.exposure.ExposureF`
443 The image from which bright star stamps should be extracted.
444 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
445 Loader to find objects within a reference catalog.
446 dataId : `dict` or `lsst.daf.butler.DataCoordinate`
447 The dataId of the exposure (and detector) bright stars should be
449 skyCorr : `lsst.afw.math.backgroundList.BackgroundList` or ``None``,
451 Full focal plane sky correction, obtained by running
452 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
456 result : `lsst.pipe.base.Struct`
457 Result struct with component:
459 - ``brightStarStamps``: ``bSS.BrightStarStamps``
461 if self.config.doApplySkyCorr:
462 self.log.info(
"Applying sky correction to exposure %s (exposure will be modified in-place).",
465 self.log.info(
"Extracting bright stars from exposure %s", dataId)
467 extractedStamps = self.
extractStampsextractStamps(inputExposure, refObjLoader=refObjLoader)
468 if not extractedStamps.starIms:
469 self.log.info(
"No suitable bright star found.")
472 self.log.info(
"Applying warp and/or shift to %i star stamps from exposure %s",
473 len(extractedStamps.starIms), dataId)
474 warpOutputs = self.
warpStampswarpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
475 warpedStars = warpOutputs.warpedStars
476 xy0s = warpOutputs.xy0s
477 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
478 archive_element=transform,
480 gaiaGMag=extractedStamps.GMags[j],
481 gaiaId=extractedStamps.gaiaIds[j])
482 for j, (warp, transform)
in
483 enumerate(zip(warpedStars, warpOutputs.warpTransforms))]
485 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
486 len(warpedStars), dataId)
488 statsControl = afwMath.StatisticsControl()
489 statsControl.setNumSigmaClip(self.config.numSigmaClip)
490 statsControl.setNumIter(self.config.numIter)
491 innerRadius, outerRadius = self.config.annularFluxRadii
492 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
493 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
494 innerRadius=innerRadius,
495 outerRadius=outerRadius,
496 nb90Rots=warpOutputs.nb90Rots,
499 statsControl=statsControl,
501 badMaskPlanes=self.config.badMaskPlanes,
502 discardNanFluxObjects=(
503 self.config.discardNanFluxStars))
504 return pipeBase.Struct(brightStarStamps=brightStarStamps)
507 """Read in required calexp, extract and process stamps around bright
508 stars and write them to disk.
512 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
513 Data reference to the calexp to extract bright stars from.
515 calexp = dataRef.get(
"calexp")
516 skyCorr = dataRef.get(
"skyCorr")
if self.config.doApplySkyCorr
else None
517 output = self.
runrun(calexp, dataId=dataRef.dataId, skyCorr=skyCorr)
519 dataRef.put(output.brightStarStamps,
"brightStarStamps")
520 return pipeBase.Struct(brightStarStamps=output.brightStarStamps)
523 inputs = butlerQC.get(inputRefs)
524 inputs[
'dataId'] = str(butlerQC.quantum.dataId)
525 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
526 for ref
in inputRefs.refCat],
527 refCats=inputs.pop(
"refCat"),
528 config=self.config.refObjLoader)
529 output = self.
runrun(**inputs, refObjLoader=refObjLoader)
531 butlerQC.put(output, outputRefs)
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)