22"""Extract bright star cutouts; normalize and warp to the same pixel grid."""
24__all__ = [
"ProcessBrightStarsTask"]
26import astropy.units
as u
36 stringToStatisticsProperty,
39from lsst.geom import AffineTransform, Box2I, Extent2I, Point2D, Point2I, SpherePoint, radians
44from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
45from lsst.pipe.base.connectionTypes
import Input, Output, PrerequisiteInput
46from lsst.utils.timer
import timeMethod
50 """Connections for ProcessBrightStarsTask."""
52 inputExposure = Input(
53 doc=
"Input exposure from which to extract bright star stamps",
55 storageClass=
"ExposureF",
56 dimensions=(
"visit",
"detector"),
59 doc=
"Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
61 storageClass=
"Background",
62 dimensions=(
"instrument",
"visit",
"detector"),
64 refCat = PrerequisiteInput(
65 doc=
"Reference catalog that contains bright star positions",
66 name=
"gaia_dr2_20200414",
67 storageClass=
"SimpleCatalog",
68 dimensions=(
"skypix",),
72 brightStarStamps = Output(
73 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
74 name=
"brightStarStamps",
75 storageClass=
"BrightStarStamps",
76 dimensions=(
"visit",
"detector"),
81 if not config.doApplySkyCorr:
82 self.inputs.remove(
"skyCorr")
86 """Configuration parameters for ProcessBrightStarsTask."""
90 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be processed.",
95 doc=
"Size of the stamps to be extracted, in pixels.",
101 "'Buffer' factor to be applied to determine the size of the stamp the processed stars will be "
102 "saved in. This will also be the size of the extended PSF model."
108 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to BAD.",
113 doc=
"Apply transform to bright star stamps to correct for optical distortions?",
118 doc=
"Warping kernel",
121 "bilinear":
"bilinear interpolation",
122 "lanczos3":
"Lanczos kernel of order 3",
123 "lanczos4":
"Lanczos kernel of order 4",
124 "lanczos5":
"Lanczos kernel of order 5",
129 doc=
"Inner and outer radii of the annulus used to compute AnnularFlux for normalization, in pixels.",
134 doc=
"Type of statistic to use to compute annular flux.",
139 "MEANCLIP":
"clipped mean",
144 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
149 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
154 doc=
"Mask planes that identify pixels to not include in the computation of the annular flux.",
155 default=(
"BAD",
"CR",
"CROSSTALK",
"EDGE",
"NO_DATA",
"SAT",
"SUSPECT",
"UNMASKEDNAN"),
159 doc=
"Minumum number of valid pixels that must fall within the annulus for the bright star to be "
160 "saved for subsequent generation of a PSF.",
165 doc=
"Apply full focal plane sky correction before extracting stars?",
170 doc=
"Should stars with NaN annular flux be discarded?",
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.
209 ConfigClass = ProcessBrightStarsConfig
210 _DefaultName = "processBrightStars"
212 def __init__(self, initInputs=None, *args, **kwargs):
216 int(self.config.stampSize[0] * self.config.modelStampBuffer),
217 int(self.config.stampSize[1] * self.config.modelStampBuffer),
228 """Apply correction to the sky background level.
230 Sky corrections can be generated using the ``SkyCorrectionTask``.
231 As the sky model generated there extends over the full focal plane,
232 this should produce a more optimal sky subtraction solution.
238 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`, optional
239 Full focal plane sky correction
from ``SkyCorrectionTask``.
243 This method modifies the input ``calexp``
in-place.
245 if isinstance(calexp, Exposure):
246 calexp = calexp.getMaskedImage()
247 calexp -= skyCorr.getImage()
250 """Read the position of bright stars within an input exposure using a
251 refCat and extract them.
255 inputExposure : `~lsst.afw.image.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 Postage stamps (`list`).
268 Corresponding coords to each star
's center, in pixels (`list`).
270 Corresponding (Gaia) G magnitudes (`list`).
272 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()
286 dilatationExtent = Extent2I(np.array(self.config.stampSize) // 2)
288 withinCalexp = refObjLoader.loadPixelBox(
289 inputExpBBox.dilatedBy(dilatationExtent),
291 filterName=
"phot_g_mean",
293 refCat = withinCalexp.refCat
295 fluxLimit = ((self.config.magLimit * u.ABmag).
to(u.nJy)).to_value()
296 GFluxes = np.array(refCat[
"phot_g_mean_flux"])
297 bright = GFluxes > fluxLimit
299 allGMags = [((gFlux * u.nJy).
to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
300 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
301 selectedColumns = refCat.columns.extract(
"coord_ra",
"coord_dec", where=bright)
302 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
304 cpix = wcs.skyToPixel(sp)
306 starIm = inputExposure.getCutout(sp, Extent2I(self.config.stampSize))
307 except InvalidParameterError:
309 bboxCorner = np.array(cpix) - np.array(self.config.stampSize) / 2
311 idealBBox =
Box2I(Point2I(bboxCorner), Extent2I(self.config.stampSize))
312 clippedStarBBox =
Box2I(idealBBox)
313 clippedStarBBox.clip(inputExpBBox)
314 if clippedStarBBox.getArea() > 0:
317 starIm = ExposureF(bbox=idealBBox)
318 starIm.image[:] = np.nan
319 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
321 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
322 starIm.maskedImage[clippedStarBBox] = clippedIm
324 starIm.setDetector(inputExposure.getDetector())
325 starIm.setWcs(inputExposure.getWcs())
328 if self.config.doRemoveDetected:
330 detThreshold =
Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"), Threshold.BITMASK)
332 allFootprints = omask.getFootprints()
334 for fs
in allFootprints:
335 if not fs.contains(Point2I(cpix)):
336 otherFootprints.append(fs)
337 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
338 if not nbMatchingFootprints == 1:
340 "Failed to uniquely identify central DETECTION footprint for star "
341 "%s; found %d footprints instead.",
343 nbMatchingFootprints,
345 omask.setFootprints(otherFootprints)
346 omask.setMask(starIm.mask,
"BAD")
347 starIms.append(starIm)
348 pixCenters.append(cpix)
349 GMags.append(allGMags[j])
350 ids.append(allIds[j])
351 return Struct(starIms=starIms, pixCenters=pixCenters, GMags=GMags, gaiaIds=ids)
354 """Warps and shifts all given stamps so they are sampled on the same
355 pixel grid and centered on the central pixel. This includes rotating
356 the stamp depending on detector orientation.
360 stamps : `Sequence` [`~lsst.afw.image.ExposureF`]
361 Image cutouts centered on a single object.
363 Positions of each object
's center (from the refCat) in pixels.
367 result : `~lsst.pipe.base.Struct`
368 Results as a struct
with attributes:
371 Stamps of warped stars.
374 The corresponding Transform
from the initial star stamp
375 to the common model grid.
378 Coordinates of the bottom-left pixels of each stamp,
382 The number of 90 degrees rotations required to compensate
for
383 detector orientation.
395 det = stamps[0].getDetector()
397 if self.config.doApplyTransform:
398 pixToTan = det.getTransform(PIXELS, TAN_PIXELS)
400 pixToTan = makeIdentityTransform()
402 possibleRots = np.array([k * np.pi / 2
for k
in range(4)])
404 yaw = det.getOrientation().getYaw()
405 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
408 warpedStars, warpTransforms, xy0s = [], [], []
409 for star, cent
in zip(stamps, pixCenters):
412 bottomLeft = Point2D(star.image.getXY0())
413 newBottomLeft = pixToTan.applyForward(bottomLeft)
414 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0] / 2)
415 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1] / 2)
417 newBottomLeft = Point2I(newBottomLeft)
419 destImage.setXY0(newBottomLeft)
420 xy0s.append(newBottomLeft)
423 newCenter = pixToTan.applyForward(cent)
425 self.
modelCenter[0] + newBottomLeft[0] - newCenter[0],
426 self.
modelCenter[1] + newBottomLeft[1] - newCenter[1],
429 shiftTransform = makeTransform(affineShift)
432 starWarper = pixToTan.then(shiftTransform)
435 goodPix = warpImage(destImage, star.getMaskedImage(), starWarper, warpCont)
437 self.log.debug(
"Warping of a star failed: no good pixel in output")
440 destImage.setXY0(0, 0)
444 destImage = rotateImageBy90(destImage, nb90Rots)
445 warpedStars.append(destImage.clone())
446 warpTransforms.append(starWarper)
447 return Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s, nb90Rots=nb90Rots)
450 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
451 """Identify bright stars within an exposure using a reference catalog,
452 extract stamps around each, then preprocess them. The preprocessing
453 steps are: shifting, warping and potentially rotating them to the same
454 pixel grid; computing their annular flux
and normalizing them.
458 inputExposure : `~lsst.afw.image.ExposureF`
459 The image
from which bright star stamps should be extracted.
461 Loader to find objects within a reference catalog.
462 dataId : `dict`
or `~lsst.daf.butler.DataCoordinate`
463 The dataId of the exposure (
and detector) bright stars should be
465 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`, optional
466 Full focal plane sky correction obtained by `SkyCorrectionTask`.
470 result : `~lsst.pipe.base.Struct`
471 Results
as a struct
with attributes:
476 if self.config.doApplySkyCorr:
478 "Applying sky correction to exposure %s (exposure will be modified in-place).", dataId
481 self.log.info(
"Extracting bright stars from exposure %s", dataId)
483 extractedStamps = self.
extractStamps(inputExposure, refObjLoader=refObjLoader)
484 if not extractedStamps.starIms:
485 self.log.info(
"No suitable bright star found.")
489 "Applying warp and/or shift to %i star stamps from exposure %s.",
490 len(extractedStamps.starIms),
493 warpOutputs = self.
warpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
494 warpedStars = warpOutputs.warpedStars
495 xy0s = warpOutputs.xy0s
499 archive_element=transform,
501 gaiaGMag=extractedStamps.GMags[j],
502 gaiaId=extractedStamps.gaiaIds[j],
503 minValidAnnulusFraction=self.config.minValidAnnulusFraction,
505 for j, (warp, transform)
in enumerate(zip(warpedStars, warpOutputs.warpTransforms))
509 "Computing annular flux and normalizing %i bright stars from exposure %s.",
515 statsControl.setNumSigmaClip(self.config.numSigmaClip)
516 statsControl.setNumIter(self.config.numIter)
517 innerRadius, outerRadius = self.config.annularFluxRadii
518 statsFlag = stringToStatisticsProperty(self.config.annularFluxStatistic)
519 brightStarStamps = BrightStarStamps.initAndNormalize(
521 innerRadius=innerRadius,
522 outerRadius=outerRadius,
523 nb90Rots=warpOutputs.nb90Rots,
526 statsControl=statsControl,
528 badMaskPlanes=self.config.badMaskPlanes,
529 discardNanFluxObjects=(self.config.discardNanFluxStars),
531 return Struct(brightStarStamps=brightStarStamps)
534 inputs = butlerQC.get(inputRefs)
535 inputs[
"dataId"] = str(butlerQC.quantum.dataId)
537 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.refCat],
538 refCats=inputs.pop(
"refCat"),
539 name=self.config.connections.refCat,
540 config=self.config.refObjLoader,
542 output = self.
run(**inputs, refObjLoader=refObjLoader)
544 butlerQC.put(output, outputRefs)
afw::table::PointKey< int > dimensions
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.
__init__(self, *config=None)
runQuantum(self, butlerQC, inputRefs, outputRefs)
run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None)
extractStamps(self, inputExposure, refObjLoader=None)
applySkyCorr(self, calexp, skyCorr)
__init__(self, initInputs=None, *args, **kwargs)
warpStamps(self, stamps, pixCenters)