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
46 inputExposure = cT.Input(
47 doc=
"Input exposure from which to extract bright star stamps",
49 storageClass=
"ExposureF",
50 dimensions=(
"visit",
"detector")
52 refCat = cT.PrerequisiteInput(
53 doc=
"Reference catalog that contains bright star positions",
54 name=
"gaia_dr2_20200414",
55 storageClass=
"SimpleCatalog",
56 dimensions=(
"skypix",),
60 brightStarStamps = cT.Output(
61 doc=
"Set of preprocessed postage stamps, each centered on a single bright star.",
62 name=
"brightStarStamps",
63 storageClass=
"BrightStarStamps",
64 dimensions=(
"visit",
"detector")
69 pipelineConnections=ProcessBrightStarsConnections):
70 """Configuration parameters for ProcessBrightStarsTask
72 magLimit = pexConfig.Field(
74 doc=
"Magnitude limit, in Gaia G; all stars brighter than this value will be processed",
77 stampSize = pexConfig.ListField(
79 doc=
"Size of the stamps to be extracted, in pixels",
82 modelStampBuffer = pexConfig.Field(
84 doc=
"'Buffer' factor to be applied to determine the size of the stamp the processed stars will "
85 "be saved in. This will also be the size of the extended PSF model.",
88 doRemoveDetected = pexConfig.Field(
90 doc=
"Whether DETECTION footprints, other than that for the central object, should be changed to "
94 warpingKernelName = pexConfig.ChoiceField(
99 "bilinear":
"bilinear interpolation",
100 "lanczos3":
"Lanczos kernel of order 3",
101 "lanczos4":
"Lanczos kernel of order 4",
102 "lanczos5":
"Lanczos kernel of order 5",
105 annularFluxRadii = pexConfig.ListField(
107 doc=
"Inner and outer radii of the annulus used to compute the AnnularFlux for normalization, "
111 annularFluxStatistic = pexConfig.ChoiceField(
113 doc=
"Type of statistic to use to compute annular flux.",
118 "MEANCLIP":
"clipped mean",
121 numSigmaClip = pexConfig.Field(
123 doc=
"Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
126 numIter = pexConfig.Field(
128 doc=
"Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
131 badMaskPlanes = pexConfig.ListField(
133 doc=
"Mask planes that, if set, lead to associated pixels not being included in the computation of the"
135 default=(
'BAD',
'CR',
'CROSSTALK',
'EDGE',
'NO_DATA',
'SAT',
'SUSPECT',
'UNMASKEDNAN')
137 refObjLoader = pexConfig.ConfigurableField(
138 target=LoadIndexedReferenceObjectsTask,
139 doc=
"reference object loader for astrometric calibration",
143 self.
refObjLoaderrefObjLoader.ref_dataset_name =
"gaia_dr2_20200414"
147 """The description of the parameters for this Task are detailed in
148 :lsst-task:`~lsst.pipe.base.PipelineTask`.
152 `ProcessBrightStarsTask` is used to extract, process, and store small
153 image cut-outs (or "postage stamps") around bright stars. It relies on
154 three methods, called in succession:
157 Find bright stars within the exposure using a reference catalog and
158 extract a stamp centered on each.
160 Shift and warp each stamp to remove optical distortions and sample all
161 stars on the same pixel grid.
162 `measureAndNormalize`
163 Compute the flux of an object in an annulus and normalize it. This is
164 required to normalize each bright star stamp as their central pixels
165 are likely saturated and/or contain ghosts, and cannot be used.
167 ConfigClass = ProcessBrightStarsConfig
168 _DefaultName =
"processBrightStars"
169 RunnerClass = pipeBase.ButlerInitializedTaskRunner
171 def __init__(self, butler=None, initInputs=None, *args, **kwargs):
174 self.
modelStampSizemodelStampSize = (int(self.config.stampSize[0]*self.config.modelStampBuffer),
175 int(self.config.stampSize[1]*self.config.modelStampBuffer))
184 if butler
is not None:
185 self.makeSubtask(
'refObjLoader', butler=butler)
188 """ Read position of bright stars within `inputExposure` from refCat
193 inputExposure : `afwImage.exposure.exposure.ExposureF`
194 The image from which bright star stamps should be extracted.
195 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
196 Loader to find objects within a reference catalog.
200 result : `lsst.pipe.base.Struct`
201 Result struct with components:
203 - ``starIms``: `list` of stamps
204 - ``pixCenters``: `list` of corresponding coordinates to each
205 star's center, in pixels.
206 - ``GMags``: `list` of corresponding (Gaia) G magnitudes.
207 - ``gaiaIds``: `np.ndarray` of corresponding unique Gaia
210 if refObjLoader
is None:
211 refObjLoader = self.refObjLoader
216 wcs = inputExposure.getWcs()
218 withinCalexp = refObjLoader.loadPixelBox(inputExposure.getBBox(), wcs, filterName=
"phot_g_mean")
219 refCat = withinCalexp.refCat
221 fluxLimit = ((self.config.magLimit*u.ABmag).to(u.nJy)).to_value()
222 GFluxes = np.array(refCat[
'phot_g_mean_flux'])
223 bright = GFluxes > fluxLimit
225 allGMags = [((gFlux*u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
226 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
227 selectedColumns = refCat.columns.extract(
'coord_ra',
'coord_dec', where=bright)
228 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
230 cpix = wcs.skyToPixel(sp)
232 if (cpix[0] >= self.config.stampSize[0]/2
233 and cpix[0] < inputExposure.getDimensions()[0] - self.config.stampSize[0]/2
234 and cpix[1] >= self.config.stampSize[1]/2
235 and cpix[1] < inputExposure.getDimensions()[1] - self.config.stampSize[1]/2):
236 starIm = inputExposure.getCutout(sp,
geom.Extent2I(self.config.stampSize))
237 if self.config.doRemoveDetected:
239 detThreshold = afwDetect.Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"),
240 afwDetect.Threshold.BITMASK)
241 omask = afwDetect.FootprintSet(starIm.mask, detThreshold)
242 allFootprints = omask.getFootprints()
244 for fs
in allFootprints:
246 otherFootprints.append(fs)
247 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
248 if not nbMatchingFootprints == 1:
249 self.log.warn(
"Failed to uniquely identify central DETECTION footprint for star "
250 f
"{allIds[j]}; found {nbMatchingFootprints} footprints instead.")
251 omask.setFootprints(otherFootprints)
252 omask.setMask(starIm.mask,
"BAD")
253 starIms.append(starIm)
254 pixCenters.append(cpix)
255 GMags.append(allGMags[j])
256 ids.append(allIds[j])
257 return pipeBase.Struct(starIms=starIms,
258 pixCenters=pixCenters,
263 """Warps and shifts all given stamps so they are sampled on the same
264 pixel grid and centered on the central pixel. This includes rotating
265 the stamp depending on detector orientation.
269 stamps : `collections.abc.Sequence`
270 [`afwImage.exposure.exposure.ExposureF`]
271 Image cutouts centered on a single object.
272 pixCenters : `collections.abc.Sequence` [`geom.Point2D`]
273 Positions of each object's center (as obtained from the refCat),
278 warpedStars : `list` [`afwImage.maskedImage.maskedImage.MaskedImage`]
281 warpCont = afwMath.WarpingControl(self.config.warpingKernelName)
283 bufferPix = (self.
modelStampSizemodelStampSize[0] - self.config.stampSize[0],
287 det = stamps[0].getDetector()
289 pixToTan = det.getTransform(cg.PIXELS, cg.TAN_PIXELS)
291 possibleRots = np.array([k*np.pi/2
for k
in range(4)])
293 yaw = det.getOrientation().getYaw()
294 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
298 for star, cent
in zip(stamps, pixCenters):
300 destImage = afwImage.MaskedImageF(*self.
modelStampSizemodelStampSize)
302 newBottomLeft = pixToTan.applyForward(bottomLeft)
303 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0]/2)
304 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1]/2)
308 destImage.setXY0(newBottomLeft)
311 newCenter = pixToTan.applyForward(cent)
312 shift = self.
modelCentermodelCenter[0] + newBottomLeft[0] - newCenter[0],\
313 self.
modelCentermodelCenter[1] + newBottomLeft[1] - newCenter[1]
315 shiftTransform = tFactory.makeTransform(affineShift)
318 starWarper = pixToTan.then(shiftTransform)
321 goodPix = afwMath.warpImage(destImage, star.getMaskedImage(),
322 starWarper, warpCont)
324 self.log.debug(
"Warping of a star failed: no good pixel in output")
327 destImage.setXY0(0, 0)
331 destImage = afwMath.rotateImageBy90(destImage, nb90Rots)
332 warpedStars.append(destImage.clone())
336 def run(self, inputExposure, refObjLoader=None, dataId=None):
337 """Identify bright stars within an exposure using a reference catalog,
338 extract stamps around each, then preprocess them. The preprocessing
339 steps are: shifting, warping and potentially rotating them to the same
340 pixel grid; computing their annular flux and normalizing them.
344 inputExposure : `afwImage.exposure.exposure.ExposureF`
345 The image from which bright star stamps should be extracted.
346 refObjLoader : `LoadIndexedReferenceObjectsTask`, optional
347 Loader to find objects within a reference catalog.
348 dataId : `dict` or `lsst.daf.butler.DataCoordinate`
349 The dataId of the exposure (and detector) bright stars should be
354 result : `lsst.pipe.base.Struct`
355 Result struct with component:
357 - ``brightStarStamps``: ``bSS.BrightStarStamps``
359 self.log.info(
"Extracting bright stars from exposure %s", dataId)
361 extractedStamps = self.
extractStampsextractStamps(inputExposure, refObjLoader=refObjLoader)
363 self.log.info(
"Applying warp to %i star stamps from exposure %s",
364 len(extractedStamps.starIms), dataId)
365 warpedStars = self.
warpStampswarpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
366 brightStarList = [bSS.BrightStarStamp(stamp_im=warp,
367 gaiaGMag=extractedStamps.GMags[j],
368 gaiaId=extractedStamps.gaiaIds[j])
369 for j, warp
in enumerate(warpedStars)]
371 self.log.info(
"Computing annular flux and normalizing %i bright stars from exposure %s",
372 len(warpedStars), dataId)
374 statsControl = afwMath.StatisticsControl()
375 statsControl.setNumSigmaClip(self.config.numSigmaClip)
376 statsControl.setNumIter(self.config.numIter)
377 innerRadius, outerRadius = self.config.annularFluxRadii
378 statsFlag = afwMath.stringToStatisticsProperty(self.config.annularFluxStatistic)
379 brightStarStamps = bSS.BrightStarStamps.initAndNormalize(brightStarList,
380 innerRadius=innerRadius,
381 outerRadius=outerRadius,
383 statsControl=statsControl,
385 badMaskPlanes=self.config.badMaskPlanes)
386 return pipeBase.Struct(brightStarStamps=brightStarStamps)
389 """ Read in required calexp, extract and process stamps around bright
390 stars and write them to disk.
394 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
395 Data reference to the calexp to extract bright stars from.
397 calexp = dataRef.get(
"calexp")
398 output = self.
runrun(calexp, dataId=dataRef.dataId)
400 dataRef.put(output.brightStarStamps,
"brightStarStamps")
401 return pipeBase.Struct(brightStarStamps=output.brightStarStamps)
404 inputs = butlerQC.get(inputRefs)
405 inputs[
'dataId'] = str(butlerQC.quantum.dataId)
406 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId
407 for ref
in inputRefs.refCat],
408 refCats=inputs.pop(
"refCat"),
409 config=self.config.refObjLoader)
410 output = self.
runrun(**inputs, refObjLoader=refObjLoader)
411 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)