22"""Extract small cutouts around bright stars, normalize and warp them to the
23same arbitrary pixel grid.
26__all__ = [
"ProcessBrightStarsTask"]
29import astropy.units
as u
33from lsst.afw import image
as afwImage
34from lsst.afw import detection
as afwDetect
44from lsst.utils.timer
import timeMethod
48 dimensions=(
"instrument",
"visit",
"detector")):
49 inputExposure = cT.Input(
50 doc=
"Input exposure from which to extract bright star stamps",
52 storageClass=
"ExposureF",
53 dimensions=(
"visit",
"detector")
56 doc=
"Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
58 storageClass=
"Background",
59 dimensions=(
"instrument",
"visit",
"detector")
61 refCat = cT.PrerequisiteInput(
62 doc=
"Reference catalog that contains bright star positions",
63 name=
"gaia_dr2_20200414",
64 storageClass=
"SimpleCatalog",
65 dimensions=(
"skypix",),
69 brightStarStamps = cT.Output(
70 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
71 name=
"brightStarStamps",
72 storageClass=
"BrightStarStamps",
73 dimensions=(
"visit",
"detector")
78 if not config.doApplySkyCorr:
79 self.inputs.remove(
"skyCorr")
83 pipelineConnections=ProcessBrightStarsConnections):
84 """Configuration parameters for ProcessBrightStarsTask
86 magLimit = pexConfig.Field(
88 doc="Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
91 stampSize = pexConfig.ListField(
93 doc=
"Size of the stamps to be extracted, in pixels",
96 modelStampBuffer = pexConfig.Field(
98 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
99 "be saved in. This will also be the size of the extended PSF model.",
102 doRemoveDetected = pexConfig.Field(
104 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to "
108 doApplyTransform = pexConfig.Field(
110 doc=
"Apply transform to bright star stamps to correct for optical distortions?",
113 warpingKernelName = pexConfig.ChoiceField(
115 doc=
"Warping kernel",
118 "bilinear":
"bilinear interpolation",
119 "lanczos3":
"Lanczos kernel of order 3",
120 "lanczos4":
"Lanczos kernel of order 4",
121 "lanczos5":
"Lanczos kernel of order 5",
124 annularFluxRadii = pexConfig.ListField(
126 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
130 annularFluxStatistic = pexConfig.ChoiceField(
132 doc=
"Type of statistic to use to compute annular flux.",
137 "MEANCLIP":
"clipped mean",
140 numSigmaClip = pexConfig.Field(
142 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
145 numIter = pexConfig.Field(
147 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
150 badMaskPlanes = pexConfig.ListField(
152 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
154 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
156 minPixelsWithinFrame = pexConfig.Field(
158 doc=
"Minimum number of pixels that must fall within the stamp boundary for the bright star to be"
159 " saved when its center is beyond the exposure boundary.",
162 doApplySkyCorr = pexConfig.Field(
164 doc=
"Apply full focal plane sky correction before extracting stars?",
167 discardNanFluxStars = pexConfig.Field(
169 doc=
"Should stars with NaN annular flux be discarded?",
172 refObjLoader = pexConfig.ConfigField(
173 dtype=LoadReferenceObjectsConfig,
174 doc=
"Reference object loader for astrometric calibration.",
178 self.
refObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
182 """The description of the parameters for this Task are detailed in
183 :lsst-task:`~lsst.pipe.base.PipelineTask`.
187 `ProcessBrightStarsTask` is used to extract, process,
and store small
188 image cut-outs (
or "postage stamps") around bright stars. It relies on
189 three methods, called
in succession:
192 Find bright stars within the exposure using a reference catalog
and
193 extract a stamp centered on each.
195 Shift
and warp each stamp to remove optical distortions
and sample all
196 stars on the same pixel grid.
197 `measureAndNormalize`
198 Compute the flux of an object
in an annulus
and normalize it. This
is
199 required to normalize each bright star stamp
as their central pixels
200 are likely saturated
and/
or contain ghosts,
and cannot be used.
202 ConfigClass = ProcessBrightStarsConfig
203 _DefaultName = "processBrightStars"
205 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
208 self.
modelStampSize = [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.
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 : `lsst.meas.algorithms.ReferenceObjectLoader`, 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.warning(
"Failed to uniquely identify central DETECTION footprint for star "
328 "%s; found %d footprints instead.",
329 allIds[j], nbMatchingFootprints)
330 omask.setFootprints(otherFootprints)
331 omask.setMask(starIm.mask,
"BAD")
332 starIms.append(starIm)
333 pixCenters.append(cpix)
334 GMags.append(allGMags[j])
335 ids.append(allIds[j])
336 return pipeBase.Struct(starIms=starIms,
337 pixCenters=pixCenters,
342 """Warps and shifts all given stamps so they are sampled on the same
343 pixel grid and centered on the central pixel. This includes rotating
344 the stamp depending on detector orientation.
348 stamps : `collections.abc.Sequence`
349 [`afwImage.exposure.exposure.ExposureF`]
350 Image cutouts centered on a single object.
351 pixCenters : `collections.abc.Sequence` [`
geom.Point2D`]
352 Positions of each object
's center (as obtained from the refCat),
357 result : `lsst.pipe.base.Struct`
358 Result struct
with components:
361 `list` [`afwImage.maskedImage.maskedImage.MaskedImage`] of
362 stamps of warped stars
363 - ``warpTransforms``:
364 `list` [`afwGeom.TransformPoint2ToPoint2`] of
365 the corresponding Transform
from the initial star stamp to
366 the common model grid
368 `list` [`
geom.Point2I`] of coordinates of the bottom-left
369 pixels of each stamp, before rotation
370 - ``nb90Rots``: `int`, the number of 90 degrees rotations required
371 to compensate
for detector orientation
374 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
380 det = stamps[0].getDetector()
382 if self.config.doApplyTransform:
383 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
385 pixToTan = tFactory.makeIdentityTransform()
387 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
389 yaw = det.getOrientation().getYaw()
390 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
393 warpedStars, warpTransforms, xy0s = [], [], []
394 for star, cent
in zip(stamps, pixCenters):
398 newBottomLeft = pixToTan.applyForward(bottomLeft)
399 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
400 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
404 destImage.setXY0(newBottomLeft)
405 xy0s.append(newBottomLeft)
408 newCenter = pixToTan.applyForward(cent)
409 shift = self.
modelCenter[0] + newBottomLeft[0] - newCenter[0],\
410 self.
modelCenter[1] + newBottomLeft[1] - newCenter[1]
412 shiftTransform = tFactory.makeTransform(affineShift)
415 starWarper = pixToTan.then(shiftTransform)
418 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
419 starWarper, warpCont)
421 self.log.debug(
"Warping of a star failed: no good pixel in output")
424 destImage.setXY0(0, 0)
428 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
429 warpedStars.append(destImage.clone())
430 warpTransforms.append(starWarper)
431 return pipeBase.Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s,
435 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
436 """Identify bright stars within an exposure using a reference catalog,
437 extract stamps around each, then preprocess them. The preprocessing
438 steps are: shifting, warping and potentially rotating them to the same
439 pixel grid; computing their annular flux
and normalizing them.
443 inputExposure : `afwImage.exposure.exposure.ExposureF`
444 The image
from which bright star stamps should be extracted.
445 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
446 Loader to find objects within a reference catalog.
447 dataId : `dict`
or `lsst.daf.butler.DataCoordinate`
448 The dataId of the exposure (
and detector) bright stars should be
450 skyCorr : `lsst.afw.math.backgroundList.BackgroundList`
or ``
None``,
452 Full focal plane sky correction, obtained by running
453 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
457 result : `lsst.pipe.base.Struct`
458 Result struct
with component:
460 - ``brightStarStamps``: ``bSS.BrightStarStamps``
462 if self.config.doApplySkyCorr:
463 self.log.info(
"Applying sky correction to exposure %s (exposure will be modified in-place).",
466 self.log.info(
"Extracting bright stars from exposure %s", dataId)
468 extractedStamps = self.
extractStamps(inputExposure, refObjLoader=refObjLoader)
469 if not extractedStamps.starIms:
470 self.log.info(
"No suitable bright star found.")
473 self.log.info(
"Applying warp and/or shift to %i star stamps from exposure %s",
474 len(extractedStamps.starIms), dataId)
475 warpOutputs = self.
warpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
476 warpedStars = warpOutputs.warpedStars
477 xy0s = warpOutputs.xy0s
478 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
479 archive_element=transform,
481 gaiaGMag=extractedStamps.GMags[j],
482 gaiaId=extractedStamps.gaiaIds[j])
483 for j, (warp, transform)
in
484 enumerate(zip(warpedStars, warpOutputs.warpTransforms))]
486 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
487 len(warpedStars), dataId)
489 statsControl = afwMath.StatisticsControl()
490 statsControl.setNumSigmaClip(self.config.numSigmaClip)
491 statsControl.setNumIter(self.config.numIter)
492 innerRadius, outerRadius = self.config.annularFluxRadii
493 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
494 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
495 innerRadius=innerRadius,
496 outerRadius=outerRadius,
497 nb90Rots=warpOutputs.nb90Rots,
500 statsControl=statsControl,
502 badMaskPlanes=self.config.badMaskPlanes,
503 discardNanFluxObjects=(
504 self.config.discardNanFluxStars))
505 return pipeBase.Struct(brightStarStamps=brightStarStamps)
508 inputs = butlerQC.get(inputRefs)
509 inputs[
'dataId'] =
str(butlerQC.quantum.dataId)
510 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
511 for ref
in inputRefs.refCat],
512 refCats=inputs.pop(
"refCat"),
513 config=self.config.refObjLoader)
514 output = self.
run(**inputs, refObjLoader=refObjLoader)
516 butlerQC.put(output, outputRefs)
def __init__(self, *config=None)
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)