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.
87 magLimit = pexConfig.Field(
89 doc="Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
92 stampSize = pexConfig.ListField(
94 doc=
"Size of the stamps to be extracted, in pixels",
97 modelStampBuffer = pexConfig.Field(
99 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
100 "be saved in. This will also be the size of the extended PSF model.",
103 doRemoveDetected = pexConfig.Field(
105 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to "
109 doApplyTransform = pexConfig.Field(
111 doc=
"Apply transform to bright star stamps to correct for optical distortions?",
114 warpingKernelName = pexConfig.ChoiceField(
116 doc=
"Warping kernel",
119 "bilinear":
"bilinear interpolation",
120 "lanczos3":
"Lanczos kernel of order 3",
121 "lanczos4":
"Lanczos kernel of order 4",
122 "lanczos5":
"Lanczos kernel of order 5",
125 annularFluxRadii = pexConfig.ListField(
127 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
131 annularFluxStatistic = pexConfig.ChoiceField(
133 doc=
"Type of statistic to use to compute annular flux.",
138 "MEANCLIP":
"clipped mean",
141 numSigmaClip = pexConfig.Field(
143 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
146 numIter = pexConfig.Field(
148 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
151 badMaskPlanes = pexConfig.ListField(
153 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
155 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
157 minPixelsWithinFrame = pexConfig.Field(
159 doc=
"Minimum number of pixels that must fall within the stamp boundary for the bright star to be"
160 " saved when its center is beyond the exposure boundary.",
163 doApplySkyCorr = pexConfig.Field(
165 doc=
"Apply full focal plane sky correction before extracting stars?",
168 discardNanFluxStars = pexConfig.Field(
170 doc=
"Should stars with NaN annular flux be discarded?",
173 refObjLoader = pexConfig.ConfigField(
174 dtype=LoadReferenceObjectsConfig,
175 doc=
"Reference object loader for astrometric calibration.",
180 """The description of the parameters for this Task are detailed in
181 :lsst-task:`~lsst.pipe.base.PipelineTask`.
185 initInputs : `Unknown`
187 Additional positional arguments.
189 Additional keyword arguments.
193 `ProcessBrightStarsTask` is used to extract, process,
and store small
194 image cut-outs (
or "postage stamps") around bright stars. It relies on
195 three methods, called
in succession:
198 Find bright stars within the exposure using a reference catalog
and
199 extract a stamp centered on each.
201 Shift
and warp each stamp to remove optical distortions
and sample all
202 stars on the same pixel grid.
203 `measureAndNormalize`
204 Compute the flux of an object
in an annulus
and normalize it. This
is
205 required to normalize each bright star stamp
as their central pixels
206 are likely saturated
and/
or contain ghosts,
and cannot be used.
208 ConfigClass = ProcessBrightStarsConfig
209 _DefaultName = "processBrightStars"
211 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
212 super().__init__(*args, **kwargs)
214 self.
modelStampSize = [int(self.config.stampSize[0]*self.config.modelStampBuffer),
215 int(self.config.stampSize[1]*self.config.modelStampBuffer)]
224 if butler
is not None:
225 self.makeSubtask(
'refObjLoader', butler=butler)
228 """Apply correction to the sky background level.
230 Sky corrections can be generated with the
'skyCorrection.py'
231 executable
in pipe_drivers. Because the sky model used by that
232 code extends over the entire focal plane, this can produce
233 better sky subtraction.
234 The calexp
is updated
in-place.
240 skyCorr : `lsst.afw.math.backgroundList.BackgroundList`
or `
None`,
242 Full focal plane sky correction, obtained by running
246 calexp = calexp.getMaskedImage()
247 calexp -= skyCorr.getImage()
250 """ Read position of bright stars within `inputExposure` from refCat
255 inputExposure : `afwImage.exposure.exposure.ExposureF`
256 The image
from which bright star stamps should be extracted.
258 Loader to find objects within a reference catalog.
262 result : `lsst.pipe.base.Struct`
263 Results
as a struct
with attributes:
266 List of stamps (`list`).
268 List of corresponding coordinates to each star
's center, in pixels (`list`).
270 List of corresponding (Gaia) G magnitudes (`list`).
272 Array of corresponding unique Gaia identifiers (`np.ndarray`).
274 if refObjLoader
is None:
275 refObjLoader = self.refObjLoader
280 wcs = inputExposure.getWcs()
282 inputIm = inputExposure.maskedImage
283 inputExpBBox = inputExposure.getBBox()
284 dilatationExtent =
geom.Extent2I(np.array(self.config.stampSize) - self.config.minPixelsWithinFrame)
286 withinCalexp = refObjLoader.loadPixelBox(inputExpBBox.dilatedBy(dilatationExtent), wcs,
287 filterName=
"phot_g_mean")
288 refCat = withinCalexp.refCat
290 fluxLimit = ((self.config.magLimit*u.ABmag).
to(u.nJy)).to_value()
291 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
292 bright = GFluxes > fluxLimit
294 allGMags = [((gFlux*u.nJy).
to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
295 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
296 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
297 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
299 cpix = wcs.skyToPixel(sp)
301 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
302 except InvalidParameterError:
304 bboxCorner = np.array(cpix) - np.array(self.config.stampSize)/2
308 clippedStarBBox.clip(inputExpBBox)
309 if clippedStarBBox.getArea() > 0:
312 starIm = afwImage.ExposureF(bbox=idealBBox)
313 starIm.image[:] = np.nan
314 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
316 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
317 starIm.maskedImage[clippedStarBBox] = clippedIm
319 starIm.setDetector(inputExposure.getDetector())
320 starIm.setWcs(inputExposure.getWcs())
323 if self.config.doRemoveDetected:
326 afwDetect.Threshold.BITMASK)
328 allFootprints = omask.getFootprints()
330 for fs
in allFootprints:
332 otherFootprints.append(fs)
333 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
334 if not nbMatchingFootprints == 1:
335 self.log.warning(
"Failed to uniquely identify central DETECTION footprint for star "
336 "%s; found %d footprints instead.",
337 allIds[j], nbMatchingFootprints)
338 omask.setFootprints(otherFootprints)
339 omask.setMask(starIm.mask,
"BAD")
340 starIms.append(starIm)
341 pixCenters.append(cpix)
342 GMags.append(allGMags[j])
343 ids.append(allIds[j])
344 return pipeBase.Struct(starIms=starIms,
345 pixCenters=pixCenters,
350 """Warps and shifts all given stamps so they are sampled on the same
351 pixel grid and centered on the central pixel. This includes rotating
352 the stamp depending on detector orientation.
356 stamps : `collections.abc.Sequence`
357 [`afwImage.exposure.exposure.ExposureF`]
358 Image cutouts centered on a single object.
359 pixCenters : `collections.abc.Sequence` [`
geom.Point2D`]
360 Positions of each object
's center (as obtained from the refCat),
365 result : `lsst.pipe.base.Struct`
366 Results
as a struct
with attributes:
369 List of stamps of warped stars (`list` of `afwImage.maskedImage.maskedImage.MaskedImage`).
371 List of the corresponding Transform
from the initial star stamp
374 List of coordinates of the bottom-left
375 pixels of each stamp, before rotation (`list` of `
geom.Point2I`).
377 The number of 90 degrees rotations required
378 to compensate
for detector orientation (`int`).
387 det = stamps[0].getDetector()
389 if self.config.doApplyTransform:
390 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
392 pixToTan = tFactory.makeIdentityTransform()
394 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
396 yaw = det.getOrientation().getYaw()
397 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
400 warpedStars, warpTransforms, xy0s = [], [], []
401 for star, cent
in zip(stamps, pixCenters):
405 newBottomLeft = pixToTan.applyForward(bottomLeft)
406 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
407 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
411 destImage.setXY0(newBottomLeft)
412 xy0s.append(newBottomLeft)
415 newCenter = pixToTan.applyForward(cent)
416 shift = self.
modelCenter[0] + newBottomLeft[0] - newCenter[0],\
417 self.
modelCenter[1] + newBottomLeft[1] - newCenter[1]
419 shiftTransform = tFactory.makeTransform(affineShift)
422 starWarper = pixToTan.then(shiftTransform)
426 starWarper, warpCont)
428 self.log.debug(
"Warping of a star failed: no good pixel in output")
431 destImage.setXY0(0, 0)
436 warpedStars.append(destImage.clone())
437 warpTransforms.append(starWarper)
438 return pipeBase.Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s,
442 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
443 """Identify bright stars within an exposure using a reference catalog,
444 extract stamps around each, then preprocess them. The preprocessing
445 steps are: shifting, warping and potentially rotating them to the same
446 pixel grid; computing their annular flux
and normalizing them.
450 inputExposure : `afwImage.exposure.exposure.ExposureF`
451 The image
from which bright star stamps should be extracted.
453 Loader to find objects within a reference catalog.
454 dataId : `dict`
or `lsst.daf.butler.DataCoordinate`
455 The dataId of the exposure (
and detector) bright stars should be
457 skyCorr : `lsst.afw.math.backgroundList.BackgroundList`
or `
None`,
459 Full focal plane sky correction, obtained by running
464 result : `lsst.pipe.base.Struct`
465 Results
as a struct
with attributes:
468 (`bSS.BrightStarStamps`)
470 if self.config.doApplySkyCorr:
471 self.log.info(
"Applying sky correction to exposure %s (exposure will be modified in-place).",
474 self.log.info(
"Extracting bright stars from exposure %s", dataId)
476 extractedStamps = self.
extractStamps(inputExposure, refObjLoader=refObjLoader)
477 if not extractedStamps.starIms:
478 self.log.info(
"No suitable bright star found.")
481 self.log.info(
"Applying warp and/or shift to %i star stamps from exposure %s",
482 len(extractedStamps.starIms), dataId)
483 warpOutputs = self.
warpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
484 warpedStars = warpOutputs.warpedStars
485 xy0s = warpOutputs.xy0s
486 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
487 archive_element=transform,
489 gaiaGMag=extractedStamps.GMags[j],
490 gaiaId=extractedStamps.gaiaIds[j])
491 for j, (warp, transform)
in
492 enumerate(zip(warpedStars, warpOutputs.warpTransforms))]
494 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
495 len(warpedStars), dataId)
498 statsControl.setNumSigmaClip(self.config.numSigmaClip)
499 statsControl.setNumIter(self.config.numIter)
500 innerRadius, outerRadius = self.config.annularFluxRadii
502 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
503 innerRadius=innerRadius,
504 outerRadius=outerRadius,
505 nb90Rots=warpOutputs.nb90Rots,
508 statsControl=statsControl,
510 badMaskPlanes=self.config.badMaskPlanes,
511 discardNanFluxObjects=(
512 self.config.discardNanFluxStars))
513 return pipeBase.Struct(brightStarStamps=brightStarStamps)
516 inputs = butlerQC.get(inputRefs)
517 inputs[
'dataId'] =
str(butlerQC.quantum.dataId)
519 for ref
in inputRefs.refCat],
520 refCats=inputs.pop(
"refCat"),
521 name=self.config.connections.refCat,
522 config=self.config.refObjLoader)
523 output = self.
run(**inputs, refObjLoader=refObjLoader)
525 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.
A class to manipulate images, masks, and variance as a single object.
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 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)
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.