250 """Read the position of bright stars within an input exposure using a
251 refCat and extract them.
255 inputExposure : `~lsst.afw.image.ExposureF`
256 The image from which bright star stamps should be extracted.
257 refObjLoader : `~lsst.meas.algorithms.ReferenceObjectLoader`, optional
258 Loader to find objects within a reference catalog.
262 result : `~lsst.pipe.base.Struct`
263 Results as a struct with attributes:
266 Postage stamps (`list`).
268 Corresponding coords to each star's center, in pixels (`list`).
270 Corresponding (Gaia) G magnitudes (`list`).
272 Corresponding unique Gaia identifiers (`np.ndarray`).
274 if refObjLoader
is None:
275 refObjLoader = self.refObjLoader
280 wcs = inputExposure.getWcs()
282 inputIm = inputExposure.maskedImage
283 inputExpBBox = inputExposure.getBBox()
286 dilatationExtent = Extent2I(np.array(self.config.stampSize) // 2)
288 withinCalexp = refObjLoader.loadPixelBox(
289 inputExpBBox.dilatedBy(dilatationExtent),
291 filterName=
"phot_g_mean",
293 refCat = withinCalexp.refCat
295 fluxLimit = ((self.config.magLimit * u.ABmag).to(u.nJy)).to_value()
296 GFluxes = np.array(refCat[
"phot_g_mean_flux"])
297 bright = GFluxes > fluxLimit
299 allGMags = [((gFlux * u.nJy).to(u.ABmag)).to_value()
for gFlux
in GFluxes[bright]]
300 allIds = refCat.columns.extract(
"id", where=bright)[
"id"]
301 selectedColumns = refCat.columns.extract(
"coord_ra",
"coord_dec", where=bright)
302 for j, (ra, dec)
in enumerate(zip(selectedColumns[
"coord_ra"], selectedColumns[
"coord_dec"])):
304 cpix = wcs.skyToPixel(sp)
306 starIm = inputExposure.getCutout(sp, Extent2I(self.config.stampSize))
307 except InvalidParameterError:
309 bboxCorner = np.array(cpix) - np.array(self.config.stampSize) / 2
311 idealBBox =
Box2I(Point2I(bboxCorner), Extent2I(self.config.stampSize))
312 clippedStarBBox =
Box2I(idealBBox)
313 clippedStarBBox.clip(inputExpBBox)
314 if clippedStarBBox.getArea() > 0:
317 starIm = ExposureF(bbox=idealBBox)
318 starIm.image[:] = np.nan
319 starIm.mask.set(inputExposure.mask.getPlaneBitMask(
"NO_DATA"))
321 clippedIm = inputIm.Factory(inputIm, clippedStarBBox)
322 starIm.maskedImage[clippedStarBBox] = clippedIm
324 starIm.setDetector(inputExposure.getDetector())
325 starIm.setWcs(inputExposure.getWcs())
328 if self.config.doRemoveDetected:
330 detThreshold =
Threshold(starIm.mask.getPlaneBitMask(
"DETECTED"), Threshold.BITMASK)
332 allFootprints = omask.getFootprints()
334 for fs
in allFootprints:
335 if not fs.contains(Point2I(cpix)):
336 otherFootprints.append(fs)
337 nbMatchingFootprints = len(allFootprints) - len(otherFootprints)
338 if not nbMatchingFootprints == 1:
340 "Failed to uniquely identify central DETECTION footprint for star "
341 "%s; found %d footprints instead.",
343 nbMatchingFootprints,
345 omask.setFootprints(otherFootprints)
346 omask.setMask(starIm.mask,
"BAD")
347 starIms.append(starIm)
348 pixCenters.append(cpix)
349 GMags.append(allGMags[j])
350 ids.append(allIds[j])
351 return Struct(starIms=starIms, pixCenters=pixCenters, GMags=GMags, gaiaIds=ids)
354 """Warps and shifts all given stamps so they are sampled on the same
355 pixel grid and centered on the central pixel. This includes rotating
356 the stamp depending on detector orientation.
360 stamps : `Sequence` [`~lsst.afw.image.ExposureF`]
361 Image cutouts centered on a single object.
362 pixCenters : `Sequence` [`~lsst.geom.Point2D`]
363 Positions of each object's center (from the refCat) in pixels.
367 result : `~lsst.pipe.base.Struct`
368 Results as a struct with attributes:
371 Stamps of warped stars.
372 (`list` [`~lsst.afw.image.MaskedImage`])
374 The corresponding Transform from the initial star stamp
375 to the common model grid.
376 (`list` [`~lsst.afw.geom.TransformPoint2ToPoint2`])
378 Coordinates of the bottom-left pixels of each stamp,
380 (`list` [`~lsst.geom.Point2I`])
382 The number of 90 degrees rotations required to compensate for
383 detector orientation.
395 det = stamps[0].getDetector()
397 if self.config.doApplyTransform:
398 pixToTan = det.getTransform(PIXELS, TAN_PIXELS)
400 pixToTan = makeIdentityTransform()
402 possibleRots = np.array([k * np.pi / 2
for k
in range(4)])
404 yaw = det.getOrientation().getYaw()
405 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
408 warpedStars, warpTransforms, xy0s = [], [], []
409 for star, cent
in zip(stamps, pixCenters):
412 bottomLeft = Point2D(star.image.getXY0())
413 newBottomLeft = pixToTan.applyForward(bottomLeft)
414 newBottomLeft.setX(newBottomLeft.getX() - bufferPix[0] / 2)
415 newBottomLeft.setY(newBottomLeft.getY() - bufferPix[1] / 2)
417 newBottomLeft = Point2I(newBottomLeft)
419 destImage.setXY0(newBottomLeft)
420 xy0s.append(newBottomLeft)
423 newCenter = pixToTan.applyForward(cent)
425 self.
modelCenter[0] + newBottomLeft[0] - newCenter[0],
426 self.
modelCenter[1] + newBottomLeft[1] - newCenter[1],
429 shiftTransform = makeTransform(affineShift)
432 starWarper = pixToTan.then(shiftTransform)
435 goodPix = warpImage(destImage, star.getMaskedImage(), starWarper, warpCont)
437 self.log.debug(
"Warping of a star failed: no good pixel in output")
440 destImage.setXY0(0, 0)
444 destImage = rotateImageBy90(destImage, nb90Rots)
445 warpedStars.append(destImage.clone())
446 warpTransforms.append(starWarper)
447 return Struct(warpedStars=warpedStars, warpTransforms=warpTransforms, xy0s=xy0s, nb90Rots=nb90Rots)
450 def run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None):
451 """Identify bright stars within an exposure using a reference catalog,
452 extract stamps around each, then preprocess them. The preprocessing
453 steps are: shifting, warping and potentially rotating them to the same
454 pixel grid; computing their annular flux and normalizing them.
458 inputExposure : `~lsst.afw.image.ExposureF`
459 The image from which bright star stamps should be extracted.
460 refObjLoader : `~lsst.meas.algorithms.ReferenceObjectLoader`, optional
461 Loader to find objects within a reference catalog.
462 dataId : `dict` or `~lsst.daf.butler.DataCoordinate`
463 The dataId of the exposure (and detector) bright stars should be
465 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`, optional
466 Full focal plane sky correction obtained by `SkyCorrectionTask`.
470 result : `~lsst.pipe.base.Struct`
471 Results as a struct with attributes:
474 (`~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`)
476 if self.config.doApplySkyCorr:
478 "Applying sky correction to exposure %s (exposure will be modified in-place).", dataId
481 self.log.info(
"Extracting bright stars from exposure %s", dataId)
483 extractedStamps = self.
extractStamps(inputExposure, refObjLoader=refObjLoader)
484 if not extractedStamps.starIms:
485 self.log.info(
"No suitable bright star found.")
489 "Applying warp and/or shift to %i star stamps from exposure %s.",
490 len(extractedStamps.starIms),
493 warpOutputs = self.
warpStamps(extractedStamps.starIms, extractedStamps.pixCenters)
494 warpedStars = warpOutputs.warpedStars
495 xy0s = warpOutputs.xy0s
499 archive_element=transform,
501 gaiaGMag=extractedStamps.GMags[j],
502 gaiaId=extractedStamps.gaiaIds[j],
503 minValidAnnulusFraction=self.config.minValidAnnulusFraction,
505 for j, (warp, transform)
in enumerate(zip(warpedStars, warpOutputs.warpTransforms))
509 "Computing annular flux and normalizing %i bright stars from exposure %s.",
515 statsControl.setNumSigmaClip(self.config.numSigmaClip)
516 statsControl.setNumIter(self.config.numIter)
517 innerRadius, outerRadius = self.config.annularFluxRadii
518 statsFlag = stringToStatisticsProperty(self.config.annularFluxStatistic)
519 brightStarStamps = BrightStarStamps.initAndNormalize(
521 innerRadius=innerRadius,
522 outerRadius=outerRadius,
523 nb90Rots=warpOutputs.nb90Rots,
526 statsControl=statsControl,
528 badMaskPlanes=self.config.badMaskPlanes,
529 discardNanFluxObjects=(self.config.discardNanFluxStars),
531 return Struct(brightStarStamps=brightStarStamps)
534 inputs = butlerQC.get(inputRefs)
535 inputs[
"dataId"] = str(butlerQC.quantum.dataId)
536 refObjLoader = ReferenceObjectLoader(
537 dataIds=[ref.datasetRef.dataId
for ref
in inputRefs.refCat],
538 refCats=inputs.pop(
"refCat"),
539 name=self.config.connections.refCat,
540 config=self.config.refObjLoader,
542 output = self.
run(**inputs, refObjLoader=refObjLoader)
544 butlerQC.put(output, outputRefs)