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 inputExposure = cT.Input(
48 doc=
"Input exposure from which to extract bright star stamps",
50 storageClass=
"ExposureF",
51 dimensions=(
"visit",
"detector")
54 doc=
"Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True",
56 storageClass=
"Background",
57 dimensions=(
"instrument",
"visit",
"detector")
59 refCat = cT.PrerequisiteInput(
60 doc=
"Reference catalog that contains bright star positions",
61 name=
"gaia_dr2_20200414",
62 storageClass=
"SimpleCatalog",
63 dimensions=(
"skypix",),
67 brightStarStamps = cT.Output(
68 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
69 name=
"brightStarStamps",
70 storageClass=
"BrightStarStamps",
71 dimensions=(
"visit",
"detector")
76 if not config.doApplySkyCorr:
77 self.inputs.remove(
"skyCorr")
81 pipelineConnections=ProcessBrightStarsConnections):
82 """Configuration parameters for ProcessBrightStarsTask
84 magLimit = pexConfig.Field(
86 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
89 stampSize = pexConfig.ListField(
91 doc=
"Size of the stamps to be extracted, in pixels",
94 modelStampBuffer = pexConfig.Field(
96 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
97 "be saved in. This will also be the size of the extended PSF model.",
100 doRemoveDetected = pexConfig.Field(
102 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to "
106 doApplyTransform = pexConfig.Field(
108 doc=
"Apply transform to bright star stamps to correct for optical distortions?",
111 warpingKernelName = pexConfig.ChoiceField(
113 doc=
"Warping kernel",
116 "bilinear":
"bilinear interpolation",
117 "lanczos3":
"Lanczos kernel of order 3",
118 "lanczos4":
"Lanczos kernel of order 4",
119 "lanczos5":
"Lanczos kernel of order 5",
122 annularFluxRadii = pexConfig.ListField(
124 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
128 annularFluxStatistic = pexConfig.ChoiceField(
130 doc=
"Type of statistic to use to compute annular flux.",
135 "MEANCLIP":
"clipped mean",
138 numSigmaClip = pexConfig.Field(
140 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
143 numIter = pexConfig.Field(
145 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
148 badMaskPlanes = pexConfig.ListField(
150 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
152 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
154 minPixelsWithinFrame = pexConfig.Field(
156 doc=
"Minimum number of pixels that must fall within the stamp boundary for the bright star to be"
157 " saved when its center is beyond the exposure boundary.",
160 doApplySkyCorr = pexConfig.Field(
162 doc=
"Apply full focal plane sky correction before extracting stars?",
165 refObjLoader = pexConfig.ConfigurableField(
166 target=LoadIndexedReferenceObjectsTask,
167 doc=
"Reference object loader for astrometric calibration.",
171 self.
refObjLoaderrefObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
175 """The description of the parameters for this Task are detailed in
176 :lsst-task:`~lsst.pipe.base.PipelineTask`.
180 `ProcessBrightStarsTask` is used to extract, process, and store small
181 image cut-outs (or "postage stamps") around bright stars. It relies on
182 three methods, called in succession:
185 Find bright stars within the exposure using a reference catalog and
186 extract a stamp centered on each.
188 Shift and warp each stamp to remove optical distortions and sample all
189 stars on the same pixel grid.
190 `measureAndNormalize`
191 Compute the flux of an object in an annulus and normalize it. This is
192 required to normalize each bright star stamp as their central pixels
193 are likely saturated and/or contain ghosts, and cannot be used.
195 ConfigClass = ProcessBrightStarsConfig
196 _DefaultName =
"processBrightStars"
197 RunnerClass = pipeBase.ButlerInitializedTaskRunner
199 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
202 self.
modelStampSizemodelStampSize = [int(self.config.stampSize[0]*self.config.modelStampBuffer),
203 int(self.config.stampSize[1]*self.config.modelStampBuffer)]
212 if butler
is not None:
213 self.makeSubtask(
'refObjLoader', butler=butler)
216 """Apply correction to the sky background level.
218 Sky corrections can be generated with the 'skyCorrection.py'
219 executable in pipe_drivers. Because the sky model used by that
220 code extends over the entire focal plane, this can produce
221 better sky subtraction.
222 The calexp is updated in-place.
226 calexp : `lsst.afw.image.Exposure` or `lsst.afw.image.MaskedImage`
228 skyCorr : `lsst.afw.math.backgroundList.BackgroundList` or None,
230 Full focal plane sky correction, obtained by running
231 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
233 if isinstance(calexp, afwImage.Exposure):
234 calexp = calexp.getMaskedImage()
235 calexp -= skyCorr.getImage()
238 """ Read position of bright stars within `inputExposure` from refCat
243 inputExposure : `afwImage.exposure.exposure.ExposureF`
244 The image from which bright star stamps should be extracted.
245 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
246 Loader to find objects within a reference catalog.
250 result : `lsst.pipe.base.Struct`
251 Result struct with components:
253 - ``starIms``: `list` of stamps
254 - ``pixCenters``: `list` of corresponding coordinates to each
255 star's center, in pixels.
256 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
257 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
260 if refObjLoader
is None:
261 refObjLoader = self.refObjLoader
266 wcs = inputExposure.getWcs()
268 inputIm = inputExposure.maskedImage
269 inputExpBBox = inputExposure.getBBox()
270 dilatationExtent =
geom.Extent2I(np.array(self.config.stampSize) - self.config.minPixelsWithinFrame)
272 withinCalexp = refObjLoader.loadPixelBox(inputExpBBox.dilatedBy(dilatationExtent), wcs,
273 filterName=
"phot_g_mean")
274 refCat = withinCalexp.refCat
276 fluxLimit = ((self.config.magLimit*u.ABmag).to(u.nJy)).to_value()
277 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
278 bright = GFluxes > fluxLimit
280 allGMags = [((gFlux*u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
281 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
282 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
283 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
285 cpix = wcs.skyToPixel(sp)
287 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
288 except InvalidParameterError:
290 bboxCorner = np.array(cpix) - np.array(self.config.stampSize)/2
294 clippedStarBBox.clip(inputExpBBox)
295 if clippedStarBBox.getArea() > 0:
298 starIm = afwImage.ExposureF(bbox=idealBBox)
299 starIm.image[:] = np.nan
300 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
302 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
303 starIm.maskedImage[clippedStarBBox] = clippedIm
305 starIm.setDetector(inputExposure.getDetector())
306 starIm.setWcs(inputExposure.getWcs())
309 if self.config.doRemoveDetected:
311 detThreshold = afwDetect.Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"),
312 afwDetect.Threshold.BITMASK)
313 omask = afwDetect.FootprintSet(starIm.mask, detThreshold)
314 allFootprints = omask.getFootprints()
316 for fs
in allFootprints:
318 otherFootprints.append(fs)
319 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
320 if not nbMatchingFootprints == 1:
321 self.log.warn(
"Failed to uniquely identify central DETECTION footprint for star "
322 f
"{allIds[j]}; found {nbMatchingFootprints} footprints instead.")
323 omask.setFootprints(otherFootprints)
324 omask.setMask(starIm.mask,
"BAD")
325 starIms.append(starIm)
326 pixCenters.append(cpix)
327 GMags.append(allGMags[j])
328 ids.append(allIds[j])
329 return pipeBase.Struct(starIms=starIms,
330 pixCenters=pixCenters,
335 """Warps and shifts all given stamps so they are sampled on the same
336 pixel grid and centered on the central pixel. This includes rotating
337 the stamp depending on detector orientation.
341 stamps : `collections.abc.Sequence`
342 [`afwImage.exposure.exposure.ExposureF`]
343 Image cutouts centered on a single object.
344 pixCenters : `collections.abc.Sequence` [`geom.Point2D`]
345 Positions of each object's center (as obtained from the refCat),
350 warpedStars : `list` [`afwImage.maskedImage.maskedImage.MaskedImage`]
353 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
355 bufferPix = (self.
modelStampSizemodelStampSize[0] - self.config.stampSize[0],
359 det = stamps[0].getDetector()
361 if self.config.doApplyTransform:
362 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
364 pixToTan = tFactory.makeIdentityTransform()
366 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
368 yaw = det.getOrientation().getYaw()
369 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
373 for star, cent
in zip(stamps, pixCenters):
375 destImage = afwImage.MaskedImageF(*self.
modelStampSizemodelStampSize)
377 newBottomLeft = pixToTan.applyForward(bottomLeft)
378 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
379 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
383 destImage.setXY0(newBottomLeft)
386 newCenter = pixToTan.applyForward(cent)
387 shift = self.
modelCentermodelCenter[0] + newBottomLeft[0] - newCenter[0],\
388 self.
modelCentermodelCenter[1] + newBottomLeft[1] - newCenter[1]
390 shiftTransform = tFactory.makeTransform(affineShift)
393 starWarper = pixToTan.then(shiftTransform)
396 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
397 starWarper, warpCont)
399 self.log.debug(
"Warping of a star failed: no good pixel in output")
402 destImage.setXY0(0, 0)
406 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
407 warpedStars.append(destImage.clone())
411 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
412 """Identify bright stars within an exposure using a reference catalog,
413 extract stamps around each, then preprocess them. The preprocessing
414 steps are: shifting, warping and potentially rotating them to the same
415 pixel grid; computing their annular flux and normalizing them.
419 inputExposure : `afwImage.exposure.exposure.ExposureF`
420 The image from which bright star stamps should be extracted.
421 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
422 Loader to find objects within a reference catalog.
423 dataId : `dict` or `lsst.daf.butler.DataCoordinate`
424 The dataId of the exposure (and detector) bright stars should be
426 skyCorr : `lsst.afw.math.backgroundList.BackgroundList` or ``None``,
428 Full focal plane sky correction, obtained by running
429 `lsst.pipe.drivers.skyCorrection.SkyCorrectionTask`.
433 result : `lsst.pipe.base.Struct`
434 Result struct with component:
436 - ``brightStarStamps``: ``bSS.BrightStarStamps``
438 if self.config.doApplySkyCorr:
439 self.log.info(
"Applying sky correction to exposure %s (exposure will be modified in-place).",
442 self.log.info(
"Extracting bright stars from exposure %s", dataId)
444 extractedStamps = self.
extractStampsextractStamps(inputExposure, refObjLoader=refObjLoader)
446 self.log.info(
"Applying warp and/or shift to %i star stamps from exposure %s",
447 len(extractedStamps.starIms), dataId)
448 warpedStars = self.
warpStampswarpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
449 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
450 gaiaGMag=extractedStamps.GMags[j],
451 gaiaId=extractedStamps.gaiaIds[j])
452 for j, warp
in enumerate(warpedStars)]
454 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
455 len(warpedStars), dataId)
457 statsControl = afwMath.StatisticsControl()
458 statsControl.setNumSigmaClip(self.config.numSigmaClip)
459 statsControl.setNumIter(self.config.numIter)
460 innerRadius, outerRadius = self.config.annularFluxRadii
461 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
462 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
463 innerRadius=innerRadius,
464 outerRadius=outerRadius,
466 statsControl=statsControl,
468 badMaskPlanes=self.config.badMaskPlanes)
469 return pipeBase.Struct(brightStarStamps=brightStarStamps)
472 """Read in required calexp, extract and process stamps around bright
473 stars and write them to disk.
477 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
478 Data reference to the calexp to extract bright stars from.
480 calexp = dataRef.get(
"calexp")
481 skyCorr = dataRef.get(
"skyCorr")
if self.config.doApplySkyCorr
else None
482 output = self.
runrun(calexp, dataId=dataRef.dataId, skyCorr=skyCorr)
484 dataRef.put(output.brightStarStamps,
"brightStarStamps")
485 return pipeBase.Struct(brightStarStamps=output.brightStarStamps)
488 inputs = butlerQC.get(inputRefs)
489 inputs[
'dataId'] = str(butlerQC.quantum.dataId)
490 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
491 for ref
in inputRefs.refCat],
492 refCats=inputs.pop(
"refCat"),
493 config=self.config.refObjLoader)
494 output = self.
runrun(**inputs, refObjLoader=refObjLoader)
495 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)