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 self.metadata[
"validStarCount"] = len(brightStarStamps)
293 if not brightStarStamps._stamps:
294 self.log.info(
"No normalized stamps exist for this exposure.")
296 return Struct(brightStarStamps=brightStarStamps)
322 self, inputExposure, filterName="phot_g_mean", refObjLoader=None, inputBrightStarStamps=None
324 """Identify the positions of bright stars within an input exposure using
325 a reference catalog and extract them.
329 inputExposure : `~lsst.afw.image.ExposureF`
330 The image to extract bright star stamps from.
331 filterName : `str`, optional
332 Name of the camera filter to use for reference catalog filtering.
333 refObjLoader : `~lsst.meas.algorithms.ReferenceObjectLoader`, optional
334 Loader to find objects within a reference catalog.
335 inputBrightStarStamps:
336 `~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`, optional
337 Provides information about the stars that have already been
338 extracted from the inputExposure in other steps of the pipeline.
339 For example, this is used in the `SubtractBrightStarsTask` to avoid
340 extracting stars that already have been extracted when running
341 `ProcessBrightStarsTask` to produce brightStarStamps.
345 result : `~lsst.pipe.base.Struct`
346 Results as a struct with attributes:
349 Postage stamps (`list`).
351 Corresponding coords to each star's center, in pixels (`list`).
353 Corresponding (Gaia) G magnitudes (`list`).
355 Corresponding unique Gaia identifiers (`np.ndarray`).
357 if refObjLoader
is None:
358 refObjLoader = self.refObjLoader
360 wcs = inputExposure.getWcs()
361 inputBBox = inputExposure.getBBox()
366 dilatationExtent = Extent2I(np.array(self.config.stampSize) // 2)
367 withinExposure = refObjLoader.loadPixelBox(
368 inputBBox.dilatedBy(dilatationExtent), wcs, filterName=filterName
370 refCat = withinExposure.refCat
371 fluxField = withinExposure.fluxField
374 fluxLimit = ((self.config.magLimit * u.ABmag).to(u.nJy)).to_value()
375 refCatBright = Table(
376 refCat.extract(
"id",
"coord_ra",
"coord_dec", fluxField, where=refCat[fluxField] > fluxLimit)
378 refCatBright[
"mag"] = (refCatBright[fluxField][:] * u.nJy).to(u.ABmag).to_value()
381 if inputBrightStarStamps
is not None:
383 existing = np.isin(refCatBright[
"id"][:], inputBrightStarStamps.getGaiaIds())
384 refCatBright = refCatBright[~existing]
390 for row, object
in enumerate(refCatBright):
391 coordSky =
SpherePoint(object[
"coord_ra"], object[
"coord_dec"], radians)
392 coordPix = wcs.skyToPixel(coordSky)
394 starStamp = self.
_getCutout(inputExposure, coordPix, self.config.stampSize.list())
398 if self.config.doRemoveDetected:
400 starStamps.append(starStamp)
401 pixCenters.append(coordPix)
404 refCatBright.remove_rows(badRows)
405 gMags = list(refCatBright[
"mag"][:])
406 ids = list(refCatBright[
"id"][:])
412 self.metadata[
"allStarCount"] = len(starStamps)
413 return Struct(starStamps=starStamps, pixCenters=pixCenters, gMags=gMags, gaiaIds=ids)
415 def _getCutout(self, inputExposure, coordPix: Point2D, stampSize: list[int]):
416 """Get a cutout from an input exposure, handling edge cases.
418 Generate a cutout from an input exposure centered on a given position
419 and with a given size.
420 If any part of the cutout is outside the input exposure bounding box,
421 the cutout is padded with NaNs.
425 inputExposure : `~lsst.afw.image.ExposureF`
426 The image to extract bright star stamps from.
427 coordPix : `~lsst.geom.Point2D`
428 Center of the cutout in pixel space.
429 stampSize : `list` [`int`]
430 Size of the cutout, in pixels.
434 stamp : `~lsst.afw.image.ExposureF` or `None`
435 The cutout, or `None` if the cutout is entirely outside the input
436 exposure bounding box.
440 This method is a short-term workaround until DM-40042 is implemented.
441 At that point, it should be replaced by a call to the Exposure method
442 ``getCutout``, which will handle edge cases automatically.
445 corner = Point2I(np.array(coordPix) - np.array(stampSize) / 2)
446 dimensions = Extent2I(stampSize)
447 stampBBox =
Box2I(corner, dimensions)
448 overlapBBox =
Box2I(stampBBox)
449 overlapBBox.clip(inputExposure.getBBox())
450 if overlapBBox.getArea() > 0:
452 stamp = ExposureF(bbox=stampBBox)
453 stamp.image[:] = np.nan
454 stamp.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
456 inputMI = inputExposure.maskedImage
457 overlap = inputMI.Factory(inputMI, overlapBBox)
458 stamp.maskedImage[overlapBBox] = overlap
460 stamp.setDetector(inputExposure.getDetector())
461 stamp.setWcs(inputExposure.getWcs())
467 """Replace all secondary footprints in a stamp with another mask flag.
469 This method identifies all secondary footprints in a stamp as those
470 whose ``find`` footprints do not overlap the given pixel coordinates.
471 If then sets these secondary footprints to the ``replace`` flag.
475 stamp : `~lsst.afw.image.ExposureF`
476 The postage stamp to modify.
477 coordPix : `~lsst.geom.Point2D`
478 The pixel coordinates of the central primary object.
480 The unique identifier of the central primary object.
481 find : `str`, optional
482 The mask plane to use to identify secondary footprints.
483 replace : `str`, optional
484 The mask plane to set secondary footprints to.
488 This method modifies the input ``stamp`` in-place.
491 detThreshold =
Threshold(stamp.mask.getPlaneBitMask(find), Threshold.BITMASK)
493 allFootprints = footprintSet.getFootprints()
495 secondaryFootprints = []
496 for footprint
in allFootprints:
497 if not footprint.contains(Point2I(coordPix)):
498 secondaryFootprints.append(footprint)
504 if (numPrimaryFootprints := len(allFootprints) - len(secondaryFootprints)) == 0:
506 "Could not uniquely identify central %s footprint for star %s; "
507 "found %d footprints instead.",
510 numPrimaryFootprints,
512 footprintSet.setFootprints(secondaryFootprints)
513 footprintSet.setMask(stamp.mask, replace)
516 """Warps and shifts all given stamps so they are sampled on the same
517 pixel grid and centered on the central pixel. This includes rotating
518 the stamp depending on detector orientation.
522 stamps : `Sequence` [`~lsst.afw.image.ExposureF`]
523 Image cutouts centered on a single object.
524 pixCenters : `Sequence` [`~lsst.geom.Point2D`]
525 Positions of each object's center (from the refCat) in pixels.
529 result : `~lsst.pipe.base.Struct`
530 Results as a struct with attributes:
533 Stamps of warped stars.
534 (`list` [`~lsst.afw.image.MaskedImage`])
536 The corresponding Transform from the initial star stamp
537 to the common model grid.
538 (`list` [`~lsst.afw.geom.TransformPoint2ToPoint2`])
540 Coordinates of the bottom-left pixels of each stamp,
542 (`list` [`~lsst.geom.Point2I`])
544 The number of 90 degrees rotations required to compensate for
545 detector orientation.
557 det = stamps[0].getDetector()
559 if self.config.doApplyTransform:
560 pixToTan = det.getTransform(PIXELS, TAN_PIXELS)
562 pixToTan = makeIdentityTransform()
564 possibleRots = np.array([k * np.pi / 2
for k
in range(4)])
566 yaw = det.getOrientation().getYaw()
567 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
570 warpedStars, warpTransforms, xy0s = [], [], []
571 for star, cent
in zip(stamps, pixCenters):
574 bottomLeft = Point2D(star.image.getXY0())
575 newBottomLeft = pixToTan.applyForward(bottomLeft)
576 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0] / 2)
577 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1] / 2)
579 newBottomLeft = Point2I(newBottomLeft)
581 destImage.setXY0(newBottomLeft)
582 xy0s.append(newBottomLeft)
585 newCenter = pixToTan.applyForward(cent)
587 self.
modelCenter[0] + newBottomLeft[0] - newCenter[0],
588 self.
modelCenter[1] + newBottomLeft[1] - newCenter[1],
591 shiftTransform = makeTransform(affineShift)
594 starWarper = pixToTan.then(shiftTransform)
597 goodPix = warpImage(destImage, star.getMaskedImage(), starWarper, warpCont)
599 self.log.debug(
"Warping of a star failed: no good pixel in output")
602 destImage.setXY0(0, 0)
606 destImage = rotateImageBy90(destImage, nb90Rots)
607 warpedStars.append(destImage.clone())
608 warpTransforms.append(starWarper)
609 return Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s, nb90Rots=nb90Rots)