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 cameraGeom
as cg
45 inputExposure = cT.Input(
46 doc=
"Input exposure from which to extract bright star stamps",
48 storageClass=
"ExposureF",
49 dimensions=(
"visit",
"detector")
51 refCat = cT.PrerequisiteInput(
52 doc=
"Reference catalog that contains bright star positions",
53 name=
"gaia_dr2_20200414",
54 storageClass=
"SimpleCatalog",
55 dimensions=(
"skypix",),
59 brightStarStamps = cT.Output(
60 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
61 name=
"brightStarStamps",
62 storageClass=
"BrightStarStamps",
63 dimensions=(
"visit",
"detector")
68 pipelineConnections=ProcessBrightStarsConnections):
69 """Configuration parameters for ProcessBrightStarsTask
71 magLimit = pexConfig.Field(
73 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
76 stampSize = pexConfig.ListField(
78 doc=
"Size of the stamps to be extracted, in pixels",
81 modelStampBuffer = pexConfig.Field(
83 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
84 "be saved in. This will also be the size of the extended PSF model.",
87 warpingKernelName = pexConfig.ChoiceField(
92 "bilinear":
"bilinear interpolation",
93 "lanczos3":
"Lanczos kernel of order 3",
94 "lanczos4":
"Lanczos kernel of order 4",
95 "lanczos5":
"Lanczos kernel of order 5",
98 annularFluxRadii = pexConfig.ListField(
100 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
104 annularFluxStatistic = pexConfig.ChoiceField(
106 doc=
"Type of statistic to use to compute annular flux.",
111 "MEANCLIP":
"clipped mean",
114 numSigmaClip = pexConfig.Field(
116 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
119 numIter = pexConfig.Field(
121 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
124 badMaskPlanes = pexConfig.ListField(
126 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
128 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
130 refObjLoader = pexConfig.ConfigurableField(
131 target=LoadIndexedReferenceObjectsTask,
132 doc=
"reference object loader for astrometric calibration",
136 self.
refObjLoaderrefObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
140 """The description of the parameters for this Task are detailed in
141 :lsst-task:`~lsst.pipe.base.PipelineTask`.
145 `ProcessBrightStarsTask` is used to extract, process, and store small
146 image cut-outs (or "postage stamps") around bright stars. It relies on
147 three methods, called in succession:
150 Find bright stars within the exposure using a reference catalog and
151 extract a stamp centered on each.
153 Shift and warp each stamp to remove optical distortions and sample all
154 stars on the same pixel grid.
155 `measureAndNormalize`
156 Compute the flux of an object in an annulus and normalize it. This is
157 required to normalize each bright star stamp as their central pixels
158 are likely saturated and/or contain ghosts, and cannot be used.
160 ConfigClass = ProcessBrightStarsConfig
161 _DefaultName =
"processBrightStars"
162 RunnerClass = pipeBase.ButlerInitializedTaskRunner
164 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
167 self.
modelStampSizemodelStampSize = (int(self.config.stampSize[0]*self.config.modelStampBuffer),
168 int(self.config.stampSize[1]*self.config.modelStampBuffer))
177 if butler
is not None:
178 self.makeSubtask(
'refObjLoader', butler=butler)
181 """ Read position of bright stars within `inputExposure` from refCat
186 inputExposure : `afwImage.exposure.exposure.ExposureF`
187 The image from which bright star stamps should be extracted.
188 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
189 Loader to find objects within a reference catalog.
193 result : `lsst.pipe.base.Struct`
194 Result struct with components:
196 - ``starIms``: `list` of stamps
197 - ``pixCenters``: `list` of corresponding coordinates to each
198 star's center, in pixels.
199 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
200 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
203 if refObjLoader
is None:
204 refObjLoader = self.refObjLoader
209 wcs = inputExposure.getWcs()
211 withinCalexp = refObjLoader.loadPixelBox(inputExposure.getBBox(), wcs, filterName=
"phot_g_mean")
212 refCat = withinCalexp.refCat
214 fluxLimit = ((self.config.magLimit*u.ABmag).to(u.nJy)).to_value()
215 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
216 bright = GFluxes > fluxLimit
218 allGMags = [((gFlux*u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
219 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
220 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
221 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
223 cpix = wcs.skyToPixel(sp)
225 if (cpix[0] >= self.config.stampSize[0]/2
226 and cpix[0] < inputExposure.getDimensions()[0] - self.config.stampSize[0]/2
227 and cpix[1] >= self.config.stampSize[1]/2
228 and cpix[1] < inputExposure.getDimensions()[1] - self.config.stampSize[1]/2):
229 starIms.append(inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize)))
230 pixCenters.append(cpix)
231 GMags.append(allGMags[j])
232 ids.append(allIds[j])
233 return pipeBase.Struct(starIms=starIms,
234 pixCenters=pixCenters,
239 """Warps and shifts all given stamps so they are sampled on the same
240 pixel grid and centered on the central pixel. This includes rotating
241 the stamp depending on detector orientation.
245 stamps : `collections.abc.Sequence`
246 [`afwImage.exposure.exposure.ExposureF`]
247 Image cutouts centered on a single object.
248 pixCenters : `collections.abc.Sequence` [`geom.Point2D`]
249 Positions of each object's center (as obtained from the refCat),
254 warpedStars : `list` [`afwImage.maskedImage.maskedImage.MaskedImage`]
257 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
259 bufferPix = (self.
modelStampSizemodelStampSize[0] - self.config.stampSize[0],
263 det = stamps[0].getDetector()
265 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
267 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
269 yaw = det.getOrientation().getYaw()
270 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
274 for star, cent
in zip(stamps, pixCenters):
276 destImage = afwImage.MaskedImageF(*self.
modelStampSizemodelStampSize)
278 newBottomLeft = pixToTan.applyForward(bottomLeft)
279 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
280 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
284 destImage.setXY0(newBottomLeft)
287 newCenter = pixToTan.applyForward(cent)
288 shift = self.
modelCentermodelCenter[0] + newBottomLeft[0] - newCenter[0],\
289 self.
modelCentermodelCenter[1] + newBottomLeft[1] - newCenter[1]
291 shiftTransform = tFactory.makeTransform(affineShift)
294 starWarper = pixToTan.then(shiftTransform)
297 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
298 starWarper, warpCont)
300 self.log.debug(
"Warping of a star failed: no good pixel in output")
303 destImage.setXY0(0, 0)
307 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
308 warpedStars.append(destImage.clone())
312 def run(self, inputExposure, refObjLoader=None, dataId=None):
313 """Identify bright stars within an exposure using a reference catalog,
314 extract stamps around each, then preprocess them. The preprocessing
315 steps are: shifting, warping and potentially rotating them to the same
316 pixel grid; computing their annular flux and normalizing them.
320 inputExposure : `afwImage.exposure.exposure.ExposureF`
321 The image from which bright star stamps should be extracted.
322 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
323 Loader to find objects within a reference catalog.
324 dataId : `dict` or `lsst.daf.butler.DataCoordinate`
325 The dataId of the exposure (and detector) bright stars should be
330 result : `lsst.pipe.base.Struct`
331 Result struct with component:
333 - ``brightStarStamps``: ``bSS.BrightStarStamps``
335 self.log.info(
"Extracting bright stars from exposure %s", dataId)
337 extractedStamps = self.
extractStampsextractStamps(inputExposure, refObjLoader=refObjLoader)
339 self.log.info(
"Applying warp to %i star stamps from exposure %s",
340 len(extractedStamps.starIms), dataId)
341 warpedStars = self.
warpStampswarpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
342 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
343 gaiaGMag=extractedStamps.GMags[j],
344 gaiaId=extractedStamps.gaiaIds[j])
345 for j, warp
in enumerate(warpedStars)]
347 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
348 len(warpedStars), dataId)
350 statsControl = afwMath.StatisticsControl()
351 statsControl.setNumSigmaClip(self.config.numSigmaClip)
352 statsControl.setNumIter(self.config.numIter)
353 innerRadius, outerRadius = self.config.annularFluxRadii
354 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
355 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
356 innerRadius=innerRadius,
357 outerRadius=outerRadius,
359 statsControl=statsControl,
361 badMaskPlanes=self.config.badMaskPlanes)
362 return pipeBase.Struct(brightStarStamps=brightStarStamps)
365 """ Read in required calexp, extract and process stamps around bright
366 stars and write them to disk.
370 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
371 Data reference to the calexp to extract bright stars from.
373 calexp = dataRef.get(
"calexp")
374 output = self.
runrun(calexp, dataId=dataRef.dataId)
376 dataRef.put(output.brightStarStamps,
"brightStarStamps")
377 return pipeBase.Struct(brightStarStamps=output.brightStarStamps)
380 inputs = butlerQC.get(inputRefs)
381 inputs[
'dataId'] = str(butlerQC.quantum.dataId)
382 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
383 for ref
in inputRefs.refCat],
384 refCats=inputs.pop(
"refCat"),
385 config=self.config.refObjLoader)
386 output = self.
runrun(**inputs, refObjLoader=refObjLoader)
387 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)