192 inputs = butlerQC.get(inputRefs)
193 inputs[
"dataId"] = str(butlerQC.quantum.dataId)
194 refObjLoader = ReferenceObjectLoader(
195 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.refCat],
196 refCats=inputs.pop(
"refCat"),
197 name=self.config.connections.refCat,
198 config=self.config.refObjLoader,
200 output = self.
run(**inputs, refObjLoader=refObjLoader)
203 butlerQC.put(output, outputRefs)
206 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
207 """Identify bright stars within an exposure using a reference catalog,
208 extract stamps around each, then preprocess them.
210 Bright star preprocessing steps are: shifting, warping and potentially
211 rotating them to the same pixel grid; computing their annular flux,
212 and; normalizing them.
216 inputExposure : `~lsst.afw.image.ExposureF`
217 The image from which bright star stamps should be extracted.
218 refObjLoader : `~lsst.meas.algorithms.ReferenceObjectLoader`, optional
219 Loader to find objects within a reference catalog.
220 dataId : `dict` or `~lsst.daf.butler.DataCoordinate`
221 The dataId of the exposure (including detector) that bright stars
222 should be extracted from.
223 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`, optional
224 Full focal plane sky correction obtained by `SkyCorrectionTask`.
228 brightStarResults : `~lsst.pipe.base.Struct`
229 Results as a struct with attributes:
232 (`~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`)
234 if self.config.doApplySkyCorr:
235 self.log.info(
"Applying sky correction to exposure %s (exposure modified in-place).", dataId)
238 self.log.info(
"Extracting bright stars from exposure %s", dataId)
240 extractedStamps = self.
extractStamps(inputExposure, refObjLoader=refObjLoader)
241 if not extractedStamps.starStamps:
242 self.log.info(
"No suitable bright star found.")
246 "Applying warp and/or shift to %i star stamps from exposure %s.",
247 len(extractedStamps.starStamps),
250 warpOutputs = self.
warpStamps(extractedStamps.starStamps, extractedStamps.pixCenters)
251 warpedStars = warpOutputs.warpedStars
252 xy0s = warpOutputs.xy0s
256 archive_element=transform,
258 gaiaGMag=extractedStamps.gMags[j],
259 gaiaId=extractedStamps.gaiaIds[j],
260 minValidAnnulusFraction=self.config.minValidAnnulusFraction,
262 for j, (warp, transform)
in enumerate(zip(warpedStars, warpOutputs.warpTransforms))
266 "Computing annular flux and normalizing %i bright stars from exposure %s.",
272 numSigmaClip=self.config.numSigmaClip,
273 numIter=self.config.numIter,
276 innerRadius, outerRadius = self.config.annularFluxRadii
277 statsFlag = stringToStatisticsProperty(self.config.annularFluxStatistic)
278 brightStarStamps = BrightStarStamps.initAndNormalize(
280 innerRadius=innerRadius,
281 outerRadius=outerRadius,
282 nb90Rots=warpOutputs.nb90Rots,
285 statsControl=statsControl,
287 badMaskPlanes=self.config.badMaskPlanes,
288 discardNanFluxObjects=(self.config.discardNanFluxStars),
291 if not brightStarStamps._stamps:
292 self.log.info(
"No normalized stamps exist for this exposure.")
294 return Struct(brightStarStamps=brightStarStamps)
320 self, inputExposure, filterName="phot_g_mean", refObjLoader=None, inputBrightStarStamps=None
322 """Identify the positions of bright stars within an input exposure using
323 a reference catalog and extract them.
327 inputExposure : `~lsst.afw.image.ExposureF`
328 The image to extract bright star stamps from.
329 filterName : `str`, optional
330 Name of the camera filter to use for reference catalog filtering.
331 refObjLoader : `~lsst.meas.algorithms.ReferenceObjectLoader`, optional
332 Loader to find objects within a reference catalog.
333 inputBrightStarStamps:
334 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`, optional
335 Provides information about the stars that have already been
336 extracted from the inputExposure in other steps of the pipeline.
337 For example, this is used in the `SubtractBrightStarsTask` to avoid
338 extracting stars that already have been extracted when running
339 `ProcessBrightStarsTask` to produce brightStarStamps.
343 result : `~lsst.pipe.base.Struct`
344 Results as a struct with attributes:
347 Postage stamps (`list`).
349 Corresponding coords to each star's center, in pixels (`list`).
351 Corresponding (Gaia) G magnitudes (`list`).
353 Corresponding unique Gaia identifiers (`np.ndarray`).
355 if refObjLoader
is None:
356 refObjLoader = self.refObjLoader
358 wcs = inputExposure.getWcs()
359 inputBBox = inputExposure.getBBox()
364 dilatationExtent = Extent2I(np.array(self.config.stampSize) // 2)
365 withinExposure = refObjLoader.loadPixelBox(
366 inputBBox.dilatedBy(dilatationExtent), wcs, filterName=filterName
368 refCat = withinExposure.refCat
369 fluxField = withinExposure.fluxField
372 fluxLimit = ((self.config.magLimit * u.ABmag).to(u.nJy)).to_value()
373 refCatBright = Table(
374 refCat.extract(
"id",
"coord_ra",
"coord_dec", fluxField, where=refCat[fluxField] > fluxLimit)
376 refCatBright[
"mag"] = (refCatBright[fluxField][:] * u.nJy).to(u.ABmag).to_value()
379 if inputBrightStarStamps
is not None:
381 existing = np.isin(refCatBright[
"id"][:], inputBrightStarStamps.getGaiaIds())
382 refCatBright = refCatBright[~existing]
388 for row, object
in enumerate(refCatBright):
389 coordSky =
SpherePoint(object[
"coord_ra"], object[
"coord_dec"], radians)
390 coordPix = wcs.skyToPixel(coordSky)
392 starStamp = self.
_getCutout(inputExposure, coordPix, self.config.stampSize.list())
396 if self.config.doRemoveDetected:
398 starStamps.append(starStamp)
399 pixCenters.append(coordPix)
402 refCatBright.remove_rows(badRows)
403 gMags = list(refCatBright[
"mag"][:])
404 ids = list(refCatBright[
"id"][:])
405 return Struct(starStamps=starStamps, pixCenters=pixCenters, gMags=gMags, gaiaIds=ids)
407 def _getCutout(self, inputExposure, coordPix: Point2D, stampSize: list[int]):
408 """Get a cutout from an input exposure, handling edge cases.
410 Generate a cutout from an input exposure centered on a given position
411 and with a given size.
412 If any part of the cutout is outside the input exposure bounding box,
413 the cutout is padded with NaNs.
417 inputExposure : `~lsst.afw.image.ExposureF`
418 The image to extract bright star stamps from.
419 coordPix : `~lsst.geom.Point2D`
420 Center of the cutout in pixel space.
421 stampSize : `list` [`int`]
422 Size of the cutout, in pixels.
426 stamp : `~lsst.afw.image.ExposureF` or `None`
427 The cutout, or `None` if the cutout is entirely outside the input
428 exposure bounding box.
432 This method is a short-term workaround until DM-40042 is implemented.
433 At that point, it should be replaced by a call to the Exposure method
434 ``getCutout``, which will handle edge cases automatically.
437 corner = Point2I(np.array(coordPix) - np.array(stampSize) / 2)
438 dimensions = Extent2I(stampSize)
439 stampBBox =
Box2I(corner, dimensions)
440 overlapBBox =
Box2I(stampBBox)
441 overlapBBox.clip(inputExposure.getBBox())
442 if overlapBBox.getArea() > 0:
444 stamp = ExposureF(bbox=stampBBox)
445 stamp.image[:] = np.nan
446 stamp.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
448 inputMI = inputExposure.maskedImage
449 overlap = inputMI.Factory(inputMI, overlapBBox)
450 stamp.maskedImage[overlapBBox] = overlap
452 stamp.setDetector(inputExposure.getDetector())
453 stamp.setWcs(inputExposure.getWcs())
459 """Replace all secondary footprints in a stamp with another mask flag.
461 This method identifies all secondary footprints in a stamp as those
462 whose ``find`` footprints do not overlap the given pixel coordinates.
463 If then sets these secondary footprints to the ``replace`` flag.
467 stamp : `~lsst.afw.image.ExposureF`
468 The postage stamp to modify.
469 coordPix : `~lsst.geom.Point2D`
470 The pixel coordinates of the central primary object.
472 The unique identifier of the central primary object.
473 find : `str`, optional
474 The mask plane to use to identify secondary footprints.
475 replace : `str`, optional
476 The mask plane to set secondary footprints to.
480 This method modifies the input ``stamp`` in-place.
483 detThreshold =
Threshold(stamp.mask.getPlaneBitMask(find), Threshold.BITMASK)
485 allFootprints = footprintSet.getFootprints()
487 secondaryFootprints = []
488 for footprint
in allFootprints:
489 if not footprint.contains(Point2I(coordPix)):
490 secondaryFootprints.append(footprint)
496 if (numPrimaryFootprints := len(allFootprints) - len(secondaryFootprints)) == 0:
498 "Could not uniquely identify central %s footprint for star %s; "
499 "found %d footprints instead.",
502 numPrimaryFootprints,
504 footprintSet.setFootprints(secondaryFootprints)
505 footprintSet.setMask(stamp.mask, replace)
508 """Warps and shifts all given stamps so they are sampled on the same
509 pixel grid and centered on the central pixel. This includes rotating
510 the stamp depending on detector orientation.
514 stamps : `Sequence` [`~lsst.afw.image.ExposureF`]
515 Image cutouts centered on a single object.
516 pixCenters : `Sequence` [`~lsst.geom.Point2D`]
517 Positions of each object's center (from the refCat) in pixels.
521 result : `~lsst.pipe.base.Struct`
522 Results as a struct with attributes:
525 Stamps of warped stars.
526 (`list` [`~lsst.afw.image.MaskedImage`])
528 The corresponding Transform from the initial star stamp
529 to the common model grid.
530 (`list` [`~lsst.afw.geom.TransformPoint2ToPoint2`])
532 Coordinates of the bottom-left pixels of each stamp,
534 (`list` [`~lsst.geom.Point2I`])
536 The number of 90 degrees rotations required to compensate for
537 detector orientation.
549 det = stamps[0].getDetector()
551 if self.config.doApplyTransform:
552 pixToTan = det.getTransform(PIXELS, TAN_PIXELS)
554 pixToTan = makeIdentityTransform()
556 possibleRots = np.array([k * np.pi / 2
for k
in range(4)])
558 yaw = det.getOrientation().getYaw()
559 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
562 warpedStars, warpTransforms, xy0s = [], [], []
563 for star, cent
in zip(stamps, pixCenters):
566 bottomLeft = Point2D(star.image.getXY0())
567 newBottomLeft = pixToTan.applyForward(bottomLeft)
568 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0] / 2)
569 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1] / 2)
571 newBottomLeft = Point2I(newBottomLeft)
573 destImage.setXY0(newBottomLeft)
574 xy0s.append(newBottomLeft)
577 newCenter = pixToTan.applyForward(cent)
579 self.
modelCenter[0] + newBottomLeft[0] - newCenter[0],
580 self.
modelCenter[1] + newBottomLeft[1] - newCenter[1],
583 shiftTransform = makeTransform(affineShift)
586 starWarper = pixToTan.then(shiftTransform)
589 goodPix = warpImage(destImage, star.getMaskedImage(), starWarper, warpCont)
591 self.log.debug(
"Warping of a star failed: no good pixel in output")
594 destImage.setXY0(0, 0)
598 destImage = rotateImageBy90(destImage, nb90Rots)
599 warpedStars.append(destImage.clone())
600 warpTransforms.append(starWarper)
601 return Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s, nb90Rots=nb90Rots)