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.",
179 """The description of the parameters for this Task are detailed in
180 :lsst-task:`~lsst.pipe.base.PipelineTask`.
184 `ProcessBrightStarsTask` is used to extract, process,
and store small
185 image cut-outs (
or "postage stamps") around bright stars. It relies on
186 three methods, called
in succession:
189 Find bright stars within the exposure using a reference catalog
and
190 extract a stamp centered on each.
192 Shift
and warp each stamp to remove optical distortions
and sample all
193 stars on the same pixel grid.
194 `measureAndNormalize`
195 Compute the flux of an object
in an annulus
and normalize it. This
is
196 required to normalize each bright star stamp
as their central pixels
197 are likely saturated
and/
or contain ghosts,
and cannot be used.
199 ConfigClass = ProcessBrightStarsConfig
200 _DefaultName = "processBrightStars"
202 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
205 self.
modelStampSize = [int(self.config.stampSize[0]*self.config.modelStampBuffer),
206 int(self.config.stampSize[1]*self.config.modelStampBuffer)]
215 if butler
is not None:
216 self.makeSubtask(
'refObjLoader', butler=butler)
219 """Apply correction to the sky background level.
221 Sky corrections can be generated with the
'skyCorrection.py'
222 executable
in pipe_drivers. Because the sky model used by that
223 code extends over the entire focal plane, this can produce
224 better sky subtraction.
225 The calexp
is updated
in-place.
231 skyCorr : `lsst.afw.math.backgroundList.BackgroundList`
or None,
233 Full focal plane sky correction, obtained by running
234 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
236 if isinstance(calexp, afwImage.Exposure):
237 calexp = calexp.getMaskedImage()
238 calexp -= skyCorr.getImage()
241 """ Read position of bright stars within `inputExposure` from refCat
246 inputExposure : `afwImage.exposure.exposure.ExposureF`
247 The image
from which bright star stamps should be extracted.
248 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
249 Loader to find objects within a reference catalog.
253 result : `lsst.pipe.base.Struct`
254 Result struct
with components:
256 - ``starIms``: `list` of stamps
257 - ``pixCenters``: `list` of corresponding coordinates to each
258 star
's center, in pixels.
259 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
260 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
263 if refObjLoader
is None:
264 refObjLoader = self.refObjLoader
269 wcs = inputExposure.getWcs()
271 inputIm = inputExposure.maskedImage
272 inputExpBBox = inputExposure.getBBox()
273 dilatationExtent =
geom.Extent2I(np.array(self.config.stampSize) - self.config.minPixelsWithinFrame)
275 withinCalexp = refObjLoader.loadPixelBox(inputExpBBox.dilatedBy(dilatationExtent), wcs,
276 filterName=
"phot_g_mean")
277 refCat = withinCalexp.refCat
279 fluxLimit = ((self.config.magLimit*u.ABmag).to(u.nJy)).to_value()
280 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
281 bright = GFluxes > fluxLimit
283 allGMags = [((gFlux*u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
284 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
285 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
286 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
288 cpix = wcs.skyToPixel(sp)
290 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
291 except InvalidParameterError:
293 bboxCorner = np.array(cpix) - np.array(self.config.stampSize)/2
297 clippedStarBBox.clip(inputExpBBox)
298 if clippedStarBBox.getArea() > 0:
301 starIm = afwImage.ExposureF(bbox=idealBBox)
302 starIm.image[:] = np.nan
303 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
305 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
306 starIm.maskedImage[clippedStarBBox] = clippedIm
308 starIm.setDetector(inputExposure.getDetector())
309 starIm.setWcs(inputExposure.getWcs())
312 if self.config.doRemoveDetected:
314 detThreshold = afwDetect.Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"),
315 afwDetect.Threshold.BITMASK)
316 omask = afwDetect.FootprintSet(starIm.mask, detThreshold)
317 allFootprints = omask.getFootprints()
319 for fs
in allFootprints:
321 otherFootprints.append(fs)
322 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
323 if not nbMatchingFootprints == 1:
324 self.log.warning(
"Failed to uniquely identify central DETECTION footprint for star "
325 "%s; found %d footprints instead.",
326 allIds[j], nbMatchingFootprints)
327 omask.setFootprints(otherFootprints)
328 omask.setMask(starIm.mask,
"BAD")
329 starIms.append(starIm)
330 pixCenters.append(cpix)
331 GMags.append(allGMags[j])
332 ids.append(allIds[j])
333 return pipeBase.Struct(starIms=starIms,
334 pixCenters=pixCenters,
339 """Warps and shifts all given stamps so they are sampled on the same
340 pixel grid and centered on the central pixel. This includes rotating
341 the stamp depending on detector orientation.
345 stamps : `collections.abc.Sequence`
346 [`afwImage.exposure.exposure.ExposureF`]
347 Image cutouts centered on a single object.
348 pixCenters : `collections.abc.Sequence` [`
geom.Point2D`]
349 Positions of each object
's center (as obtained from the refCat),
354 result : `lsst.pipe.base.Struct`
355 Result struct
with components:
358 `list` [`afwImage.maskedImage.maskedImage.MaskedImage`] of
359 stamps of warped stars
360 - ``warpTransforms``:
361 `list` [`afwGeom.TransformPoint2ToPoint2`] of
362 the corresponding Transform
from the initial star stamp to
363 the common model grid
365 `list` [`
geom.Point2I`] of coordinates of the bottom-left
366 pixels of each stamp, before rotation
367 - ``nb90Rots``: `int`, the number of 90 degrees rotations required
368 to compensate
for detector orientation
371 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
377 det = stamps[0].getDetector()
379 if self.config.doApplyTransform:
380 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
382 pixToTan = tFactory.makeIdentityTransform()
384 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
386 yaw = det.getOrientation().getYaw()
387 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
390 warpedStars, warpTransforms, xy0s = [], [], []
391 for star, cent
in zip(stamps, pixCenters):
395 newBottomLeft = pixToTan.applyForward(bottomLeft)
396 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
397 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
401 destImage.setXY0(newBottomLeft)
402 xy0s.append(newBottomLeft)
405 newCenter = pixToTan.applyForward(cent)
406 shift = self.
modelCenter[0] + newBottomLeft[0] - newCenter[0],\
407 self.
modelCenter[1] + newBottomLeft[1] - newCenter[1]
409 shiftTransform = tFactory.makeTransform(affineShift)
412 starWarper = pixToTan.then(shiftTransform)
415 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
416 starWarper, warpCont)
418 self.log.debug(
"Warping of a star failed: no good pixel in output")
421 destImage.setXY0(0, 0)
425 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
426 warpedStars.append(destImage.clone())
427 warpTransforms.append(starWarper)
428 return pipeBase.Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s,
432 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
433 """Identify bright stars within an exposure using a reference catalog,
434 extract stamps around each, then preprocess them. The preprocessing
435 steps are: shifting, warping and potentially rotating them to the same
436 pixel grid; computing their annular flux
and normalizing them.
440 inputExposure : `afwImage.exposure.exposure.ExposureF`
441 The image
from which bright star stamps should be extracted.
442 refObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional
443 Loader to find objects within a reference catalog.
444 dataId : `dict`
or `lsst.daf.butler.DataCoordinate`
445 The dataId of the exposure (
and detector) bright stars should be
447 skyCorr : `lsst.afw.math.backgroundList.BackgroundList`
or ``
None``,
449 Full focal plane sky correction, obtained by running
450 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
454 result : `lsst.pipe.base.Struct`
455 Result struct
with component:
457 - ``brightStarStamps``: ``bSS.BrightStarStamps``
459 if self.config.doApplySkyCorr:
460 self.log.info(
"Applying sky correction to exposure %s (exposure will be modified in-place).",
463 self.log.info(
"Extracting bright stars from exposure %s", dataId)
465 extractedStamps = self.
extractStamps(inputExposure, refObjLoader=refObjLoader)
466 if not extractedStamps.starIms:
467 self.log.info(
"No suitable bright star found.")
470 self.log.info(
"Applying warp and/or shift to %i star stamps from exposure %s",
471 len(extractedStamps.starIms), dataId)
472 warpOutputs = self.
warpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
473 warpedStars = warpOutputs.warpedStars
474 xy0s = warpOutputs.xy0s
475 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
476 archive_element=transform,
478 gaiaGMag=extractedStamps.GMags[j],
479 gaiaId=extractedStamps.gaiaIds[j])
480 for j, (warp, transform)
in
481 enumerate(zip(warpedStars, warpOutputs.warpTransforms))]
483 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
484 len(warpedStars), dataId)
486 statsControl = afwMath.StatisticsControl()
487 statsControl.setNumSigmaClip(self.config.numSigmaClip)
488 statsControl.setNumIter(self.config.numIter)
489 innerRadius, outerRadius = self.config.annularFluxRadii
490 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
491 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
492 innerRadius=innerRadius,
493 outerRadius=outerRadius,
494 nb90Rots=warpOutputs.nb90Rots,
497 statsControl=statsControl,
499 badMaskPlanes=self.config.badMaskPlanes,
500 discardNanFluxObjects=(
501 self.config.discardNanFluxStars))
502 return pipeBase.Struct(brightStarStamps=brightStarStamps)
505 inputs = butlerQC.get(inputRefs)
506 inputs[
'dataId'] =
str(butlerQC.quantum.dataId)
507 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
508 for ref
in inputRefs.refCat],
509 refCats=inputs.pop(
"refCat"),
510 name=self.config.connections.refCat,
511 config=self.config.refObjLoader)
512 output = self.
run(**inputs, refObjLoader=refObjLoader)
514 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)