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")
53 refCat = cT.PrerequisiteInput(
54 doc=
"Reference catalog that contains bright star positions",
55 name=
"gaia_dr2_20200414",
56 storageClass=
"SimpleCatalog",
57 dimensions=(
"skypix",),
61 brightStarStamps = cT.Output(
62 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
63 name=
"brightStarStamps",
64 storageClass=
"BrightStarStamps",
65 dimensions=(
"visit",
"detector")
70 pipelineConnections=ProcessBrightStarsConnections):
71 """Configuration parameters for ProcessBrightStarsTask
73 magLimit = pexConfig.Field(
75 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
78 stampSize = pexConfig.ListField(
80 doc=
"Size of the stamps to be extracted, in pixels",
83 modelStampBuffer = pexConfig.Field(
85 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
86 "be saved in. This will also be the size of the extended PSF model.",
89 doRemoveDetected = pexConfig.Field(
91 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to "
95 doApplyTransform = pexConfig.Field(
97 doc=
"Apply transform to bright star stamps to correct for optical distortions?",
100 warpingKernelName = pexConfig.ChoiceField(
102 doc=
"Warping kernel",
105 "bilinear":
"bilinear interpolation",
106 "lanczos3":
"Lanczos kernel of order 3",
107 "lanczos4":
"Lanczos kernel of order 4",
108 "lanczos5":
"Lanczos kernel of order 5",
111 annularFluxRadii = pexConfig.ListField(
113 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
117 annularFluxStatistic = pexConfig.ChoiceField(
119 doc=
"Type of statistic to use to compute annular flux.",
124 "MEANCLIP":
"clipped mean",
127 numSigmaClip = pexConfig.Field(
129 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
132 numIter = pexConfig.Field(
134 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
137 badMaskPlanes = pexConfig.ListField(
139 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
141 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
143 minPixelsWithinFrame = pexConfig.Field(
145 doc=
"Minimum number of pixels that must fall within the stamp boundary for the bright star to be"
146 " saved when its center is beyond the exposure boundary.",
149 refObjLoader = pexConfig.ConfigurableField(
150 target=LoadIndexedReferenceObjectsTask,
151 doc=
"Reference object loader for astrometric calibration.",
155 self.
refObjLoaderrefObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
159 """The description of the parameters for this Task are detailed in
160 :lsst-task:`~lsst.pipe.base.PipelineTask`.
164 `ProcessBrightStarsTask` is used to extract, process, and store small
165 image cut-outs (or "postage stamps") around bright stars. It relies on
166 three methods, called in succession:
169 Find bright stars within the exposure using a reference catalog and
170 extract a stamp centered on each.
172 Shift and warp each stamp to remove optical distortions and sample all
173 stars on the same pixel grid.
174 `measureAndNormalize`
175 Compute the flux of an object in an annulus and normalize it. This is
176 required to normalize each bright star stamp as their central pixels
177 are likely saturated and/or contain ghosts, and cannot be used.
179 ConfigClass = ProcessBrightStarsConfig
180 _DefaultName =
"processBrightStars"
181 RunnerClass = pipeBase.ButlerInitializedTaskRunner
183 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
186 self.
modelStampSizemodelStampSize = (int(self.config.stampSize[0]*self.config.modelStampBuffer),
187 int(self.config.stampSize[1]*self.config.modelStampBuffer))
196 if butler
is not None:
197 self.makeSubtask(
'refObjLoader', butler=butler)
200 """ Read position of bright stars within `inputExposure` from refCat
205 inputExposure : `afwImage.exposure.exposure.ExposureF`
206 The image from which bright star stamps should be extracted.
207 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
208 Loader to find objects within a reference catalog.
212 result : `lsst.pipe.base.Struct`
213 Result struct with components:
215 - ``starIms``: `list` of stamps
216 - ``pixCenters``: `list` of corresponding coordinates to each
217 star's center, in pixels.
218 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
219 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
222 if refObjLoader
is None:
223 refObjLoader = self.refObjLoader
228 wcs = inputExposure.getWcs()
230 inputIm = inputExposure.maskedImage
231 inputExpBBox = inputExposure.getBBox()
232 dilatationExtent =
geom.Extent2I(np.array(self.config.stampSize) - self.config.minPixelsWithinFrame)
234 withinCalexp = refObjLoader.loadPixelBox(inputExpBBox.dilatedBy(dilatationExtent), wcs,
235 filterName=
"phot_g_mean")
236 refCat = withinCalexp.refCat
238 fluxLimit = ((self.config.magLimit*u.ABmag).to(u.nJy)).to_value()
239 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
240 bright = GFluxes > fluxLimit
242 allGMags = [((gFlux*u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
243 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
244 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
245 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
247 cpix = wcs.skyToPixel(sp)
249 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
250 except InvalidParameterError:
252 bboxCorner = np.array(cpix) - np.array(self.config.stampSize)/2
256 clippedStarBBox.clip(inputExpBBox)
257 if clippedStarBBox.getArea() > 0:
260 starIm = afwImage.ExposureF(bbox=idealBBox)
261 starIm.image[:] = np.nan
262 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
264 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
265 starIm.maskedImage[clippedStarBBox] = clippedIm
267 starIm.setDetector(inputExposure.getDetector())
268 starIm.setWcs(inputExposure.getWcs())
271 if self.config.doRemoveDetected:
273 detThreshold = afwDetect.Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"),
274 afwDetect.Threshold.BITMASK)
275 omask = afwDetect.FootprintSet(starIm.mask, detThreshold)
276 allFootprints = omask.getFootprints()
278 for fs
in allFootprints:
280 otherFootprints.append(fs)
281 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
282 if not nbMatchingFootprints == 1:
283 self.log.warn(
"Failed to uniquely identify central DETECTION footprint for star "
284 f
"{allIds[j]}; found {nbMatchingFootprints} footprints instead.")
285 omask.setFootprints(otherFootprints)
286 omask.setMask(starIm.mask,
"BAD")
287 starIms.append(starIm)
288 pixCenters.append(cpix)
289 GMags.append(allGMags[j])
290 ids.append(allIds[j])
291 return pipeBase.Struct(starIms=starIms,
292 pixCenters=pixCenters,
297 """Warps and shifts all given stamps so they are sampled on the same
298 pixel grid and centered on the central pixel. This includes rotating
299 the stamp depending on detector orientation.
303 stamps : `collections.abc.Sequence`
304 [`afwImage.exposure.exposure.ExposureF`]
305 Image cutouts centered on a single object.
306 pixCenters : `collections.abc.Sequence` [`geom.Point2D`]
307 Positions of each object's center (as obtained from the refCat),
312 warpedStars : `list` [`afwImage.maskedImage.maskedImage.MaskedImage`]
315 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
317 bufferPix = (self.
modelStampSizemodelStampSize[0] - self.config.stampSize[0],
321 det = stamps[0].getDetector()
323 if self.config.doApplyTransform:
324 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
326 pixToTan = tFactory.makeIdentityTransform()
328 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
330 yaw = det.getOrientation().getYaw()
331 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
335 for star, cent
in zip(stamps, pixCenters):
337 destImage = afwImage.MaskedImageF(*self.
modelStampSizemodelStampSize)
339 newBottomLeft = pixToTan.applyForward(bottomLeft)
340 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
341 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
345 destImage.setXY0(newBottomLeft)
348 newCenter = pixToTan.applyForward(cent)
349 shift = self.
modelCentermodelCenter[0] + newBottomLeft[0] - newCenter[0],\
350 self.
modelCentermodelCenter[1] + newBottomLeft[1] - newCenter[1]
352 shiftTransform = tFactory.makeTransform(affineShift)
355 starWarper = pixToTan.then(shiftTransform)
358 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
359 starWarper, warpCont)
361 self.log.debug(
"Warping of a star failed: no good pixel in output")
364 destImage.setXY0(0, 0)
368 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
369 warpedStars.append(destImage.clone())
373 def run(self, inputExposure, refObjLoader=None, dataId=None):
374 """Identify bright stars within an exposure using a reference catalog,
375 extract stamps around each, then preprocess them. The preprocessing
376 steps are: shifting, warping and potentially rotating them to the same
377 pixel grid; computing their annular flux and normalizing them.
381 inputExposure : `afwImage.exposure.exposure.ExposureF`
382 The image from which bright star stamps should be extracted.
383 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
384 Loader to find objects within a reference catalog.
385 dataId : `dict` or `lsst.daf.butler.DataCoordinate`
386 The dataId of the exposure (and detector) bright stars should be
391 result : `lsst.pipe.base.Struct`
392 Result struct with component:
394 - ``brightStarStamps``: ``bSS.BrightStarStamps``
396 self.log.info(
"Extracting bright stars from exposure %s", dataId)
398 extractedStamps = self.
extractStampsextractStamps(inputExposure, refObjLoader=refObjLoader)
400 self.log.info(
"Applying warp and/or shift to %i star stamps from exposure %s",
401 len(extractedStamps.starIms), dataId)
402 warpedStars = self.
warpStampswarpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
403 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
404 gaiaGMag=extractedStamps.GMags[j],
405 gaiaId=extractedStamps.gaiaIds[j])
406 for j, warp
in enumerate(warpedStars)]
408 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
409 len(warpedStars), dataId)
411 statsControl = afwMath.StatisticsControl()
412 statsControl.setNumSigmaClip(self.config.numSigmaClip)
413 statsControl.setNumIter(self.config.numIter)
414 innerRadius, outerRadius = self.config.annularFluxRadii
415 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
416 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
417 innerRadius=innerRadius,
418 outerRadius=outerRadius,
420 statsControl=statsControl,
422 badMaskPlanes=self.config.badMaskPlanes)
423 return pipeBase.Struct(brightStarStamps=brightStarStamps)
426 """ Read in required calexp, extract and process stamps around bright
427 stars and write them to disk.
431 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
432 Data reference to the calexp to extract bright stars from.
434 calexp = dataRef.get(
"calexp")
435 output = self.
runrun(calexp, dataId=dataRef.dataId)
437 dataRef.put(output.brightStarStamps,
"brightStarStamps")
438 return pipeBase.Struct(brightStarStamps=output.brightStarStamps)
441 inputs = butlerQC.get(inputRefs)
442 inputs[
'dataId'] = str(butlerQC.quantum.dataId)
443 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
444 for ref
in inputRefs.refCat],
445 refCats=inputs.pop(
"refCat"),
446 config=self.config.refObjLoader)
447 output = self.
runrun(**inputs, refObjLoader=refObjLoader)
448 butlerQC.put(output, outputRefs)
def runDataRef(self, dataRef)
def run(self, inputExposure, refObjLoader=None, dataId=None)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
def warpStamps(self, stamps, pixCenters)
def extractStamps(self, inputExposure, refObjLoader=None)
def __init__(self, butler=None, initInputs=None, *args, **kwargs)