lsst.pipe.tasks ga0be0691d8+ae2b9df503
Loading...
Searching...
No Matches
processBrightStars.py
Go to the documentation of this file.
1# This file is part of pipe_tasks.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22"""Extract bright star cutouts; normalize and warp to the same pixel grid."""
23
24__all__ = ["ProcessBrightStarsConnections", "ProcessBrightStarsConfig", "ProcessBrightStarsTask"]
25
26import astropy.units as u
27import numpy as np
28from astropy.table import Table
29from lsst.afw.cameraGeom import PIXELS, TAN_PIXELS
30from lsst.afw.detection import FootprintSet, Threshold
31from lsst.afw.geom.transformFactory import makeIdentityTransform, makeTransform
32from lsst.afw.image import Exposure, ExposureF, MaskedImageF
33from lsst.afw.math import (
34 StatisticsControl,
35 WarpingControl,
36 rotateImageBy90,
37 stringToStatisticsProperty,
38 warpImage,
39)
40from lsst.geom import AffineTransform, Box2I, Extent2I, Point2D, Point2I, SpherePoint, radians
41from lsst.meas.algorithms import LoadReferenceObjectsConfig, ReferenceObjectLoader
42from lsst.meas.algorithms.brightStarStamps import BrightStarStamp, BrightStarStamps
43from lsst.pex.config import ChoiceField, ConfigField, Field, ListField
44from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections, Struct
45from lsst.pipe.base.connectionTypes import Input, Output, PrerequisiteInput
46from lsst.utils.timer import timeMethod
47
48
49class ProcessBrightStarsConnections(PipelineTaskConnections, dimensions=("instrument", "visit", "detector")):
50 """Connections for ProcessBrightStarsTask."""
51
52 inputExposure = Input(
53 doc="Input exposure from which to extract bright star stamps.",
54 name="calexp",
55 storageClass="ExposureF",
56 dimensions=("visit", "detector"),
57 )
58 skyCorr = Input(
59 doc="Input sky correction to be subtracted from the calexp if doApplySkyCorr=True.",
60 name="skyCorr",
61 storageClass="Background",
62 dimensions=("instrument", "visit", "detector"),
63 )
64 refCat = PrerequisiteInput(
65 doc="Reference catalog that contains bright star positions",
66 name="gaia_dr3_20230707",
67 storageClass="SimpleCatalog",
68 dimensions=("skypix",),
69 multiple=True,
70 deferLoad=True,
71 )
72 brightStarStamps = Output(
73 doc="Set of preprocessed postage stamps, each centered on a single bright star.",
74 name="brightStarStamps",
75 storageClass="BrightStarStamps",
76 dimensions=("visit", "detector"),
77 )
78
79 def __init__(self, *, config=None):
80 super().__init__(config=config)
81 if not config.doApplySkyCorr:
82 self.inputs.remove("skyCorr")
83
84
85class ProcessBrightStarsConfig(PipelineTaskConfig, pipelineConnections=ProcessBrightStarsConnections):
86 """Configuration parameters for ProcessBrightStarsTask."""
87
88 magLimit = Field[float](
89 doc="Magnitude limit, in Gaia G; all stars brighter than this value will be processed.",
90 default=18,
91 )
92 stampSize = ListField[int](
93 doc="Size of the stamps to be extracted, in pixels.",
94 default=(250, 250),
95 )
96 modelStampBuffer = Field[float](
97 doc=(
98 "'Buffer' factor to be applied to determine the size of the stamp the processed stars will be "
99 "saved in. This will also be the size of the extended PSF model."
100 ),
101 default=1.1,
102 )
103 doRemoveDetected = Field[bool](
104 doc="Whether secondary DETECTION footprints (i.e., footprints of objects other than the central "
105 "primary object) should be changed to BAD.",
106 default=True,
107 )
108 doApplyTransform = Field[bool](
109 doc="Apply transform to bright star stamps to correct for optical distortions?",
110 default=True,
111 )
112 warpingKernelName = ChoiceField[str](
113 doc="Warping kernel.",
114 default="lanczos5",
115 allowed={
116 "bilinear": "bilinear interpolation",
117 "lanczos3": "Lanczos kernel of order 3",
118 "lanczos4": "Lanczos kernel of order 4",
119 "lanczos5": "Lanczos kernel of order 5",
120 },
121 )
122 annularFluxRadii = ListField[int](
123 doc="Inner and outer radii of the annulus used to compute AnnularFlux for normalization, in pixels.",
124 default=(70, 80),
125 )
126 annularFluxStatistic = ChoiceField[str](
127 doc="Type of statistic to use to compute annular flux.",
128 default="MEANCLIP",
129 allowed={
130 "MEAN": "mean",
131 "MEDIAN": "median",
132 "MEANCLIP": "clipped mean",
133 },
134 )
135 numSigmaClip = Field[float](
136 doc="Sigma for outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
137 default=4,
138 )
139 numIter = Field[int](
140 doc="Number of iterations of outlier rejection; ignored if annularFluxStatistic != 'MEANCLIP'.",
141 default=3,
142 )
143 badMaskPlanes = ListField[str](
144 doc="Mask planes that identify pixels to not include in the computation of the annular flux.",
145 default=("BAD", "CR", "CROSSTALK", "EDGE", "NO_DATA", "SAT", "SUSPECT", "UNMASKEDNAN"),
146 )
147 minValidAnnulusFraction = Field[float](
148 doc="Minumum number of valid pixels that must fall within the annulus for the bright star to be "
149 "saved for subsequent generation of a PSF.",
150 default=0.0,
151 )
152 doApplySkyCorr = Field[bool](
153 doc="Apply full focal plane sky correction before extracting stars?",
154 default=True,
155 )
156 discardNanFluxStars = Field[bool](
157 doc="Should stars with NaN annular flux be discarded?",
158 default=False,
159 )
160 refObjLoader = ConfigField[LoadReferenceObjectsConfig](
161 doc="Reference object loader for astrometric calibration.",
162 )
163
164
165class ProcessBrightStarsTask(PipelineTask):
166 """Extract bright star cutouts; normalize and warp to the same pixel grid.
167
168 This task is used to extract, process, and store small image cut-outs
169 (or "postage stamps") around bright stars. It relies on three methods,
170 called in succession:
171
172 `extractStamps`
173 Find bright stars within the exposure using a reference catalog and
174 extract a stamp centered on each.
175 `warpStamps`
176 Shift and warp each stamp to remove optical distortions and sample all
177 stars on the same pixel grid.
178 `measureAndNormalize`
179 Compute the flux of an object in an annulus and normalize it. This is
180 required to normalize each bright star stamp as their central pixels
181 are likely saturated and/or contain ghosts, and cannot be used.
182 """
183
184 ConfigClass = ProcessBrightStarsConfig
185 _DefaultName = "processBrightStars"
186
187 def __init__(self, initInputs=None, *args, **kwargs):
188 super().__init__(*args, **kwargs)
189 self.setModelStamp()
190
191 def runQuantum(self, butlerQC, inputRefs, outputRefs):
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,
199 )
200 output = self.run(**inputs, refObjLoader=refObjLoader)
201 # Only ingest stamp if it exists; prevent ingesting an empty FITS file.
202 if output:
203 butlerQC.put(output, outputRefs)
204
205 @timeMethod
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.
209
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.
213
214 Parameters
215 ----------
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`.
225
226 Returns
227 -------
228 brightStarResults : `~lsst.pipe.base.Struct`
229 Results as a struct with attributes:
230
231 ``brightStarStamps``
232 (`~lsst.meas.algorithms.brightStarStamps.BrightStarStamps`)
233 """
234 if self.config.doApplySkyCorr:
235 self.log.info("Applying sky correction to exposure %s (exposure modified in-place).", dataId)
236 self.applySkyCorr(inputExposure, skyCorr)
237
238 self.log.info("Extracting bright stars from exposure %s", dataId)
239 # Extract stamps around bright stars.
240 extractedStamps = self.extractStamps(inputExposure, refObjLoader=refObjLoader)
241 if not extractedStamps.starStamps:
242 self.log.info("No suitable bright star found.")
243 return None
244 # Warp (and shift, and potentially rotate) them.
245 self.log.info(
246 "Applying warp and/or shift to %i star stamps from exposure %s.",
247 len(extractedStamps.starStamps),
248 dataId,
249 )
250 warpOutputs = self.warpStamps(extractedStamps.starStamps, extractedStamps.pixCenters)
251 warpedStars = warpOutputs.warpedStars
252 xy0s = warpOutputs.xy0s
253 brightStarList = [
255 stamp_im=warp,
256 archive_element=transform,
257 position=xy0s[j],
258 gaiaGMag=extractedStamps.gMags[j],
259 gaiaId=extractedStamps.gaiaIds[j],
260 minValidAnnulusFraction=self.config.minValidAnnulusFraction,
261 )
262 for j, (warp, transform) in enumerate(zip(warpedStars, warpOutputs.warpTransforms))
263 ]
264 # Compute annularFlux and normalize
265 self.log.info(
266 "Computing annular flux and normalizing %i bright stars from exposure %s.",
267 len(warpedStars),
268 dataId,
269 )
270 # annularFlux statistic set-up, excluding mask planes
271 statsControl = StatisticsControl(
272 numSigmaClip=self.config.numSigmaClip,
273 numIter=self.config.numIter,
274 )
275
276 innerRadius, outerRadius = self.config.annularFluxRadii
277 statsFlag = stringToStatisticsProperty(self.config.annularFluxStatistic)
278 brightStarStamps = BrightStarStamps.initAndNormalize(
279 brightStarList,
280 innerRadius=innerRadius,
281 outerRadius=outerRadius,
282 nb90Rots=warpOutputs.nb90Rots,
283 imCenter=self.modelCenter,
284 use_archive=True,
285 statsControl=statsControl,
286 statsFlag=statsFlag,
287 badMaskPlanes=self.config.badMaskPlanes,
288 discardNanFluxObjects=(self.config.discardNanFluxStars),
289 )
290 # Do not create empty FITS files if there aren't any normalized stamps.
291 if not brightStarStamps._stamps:
292 self.log.info("No normalized stamps exist for this exposure.")
293 return None
294 return Struct(brightStarStamps=brightStarStamps)
295
296 def applySkyCorr(self, calexp, skyCorr):
297 """Apply sky correction to the input exposure.
298
299 Sky corrections can be generated using the
300 `~lsst.pipe.tasks.skyCorrection.SkyCorrectionTask`.
301 As the sky model generated via that task extends over the full focal
302 plane, this should produce a more optimal sky subtraction solution.
303
304 Parameters
305 ----------
306 calexp : `~lsst.afw.image.Exposure` or `~lsst.afw.image.MaskedImage`
307 Calibrated exposure to correct.
308 skyCorr : `~lsst.afw.math.backgroundList.BackgroundList`
309 Full focal plane sky correction from ``SkyCorrectionTask``.
310
311 Notes
312 -----
313 This method modifies the input ``calexp`` in-place.
314 """
315 if isinstance(calexp, Exposure):
316 calexp = calexp.getMaskedImage()
317 calexp -= skyCorr.getImage()
318
320 self, inputExposure, filterName="phot_g_mean", refObjLoader=None, inputBrightStarStamps=None
321 ):
322 """Identify the positions of bright stars within an input exposure using
323 a reference catalog and extract them.
324
325 Parameters
326 ----------
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.
340
341 Returns
342 -------
343 result : `~lsst.pipe.base.Struct`
344 Results as a struct with attributes:
345
346 ``starStamps``
347 Postage stamps (`list`).
348 ``pixCenters``
349 Corresponding coords to each star's center, in pixels (`list`).
350 ``gMags``
351 Corresponding (Gaia) G magnitudes (`list`).
352 ``gaiaIds``
353 Corresponding unique Gaia identifiers (`np.ndarray`).
354 """
355 if refObjLoader is None:
356 refObjLoader = self.refObjLoader
357
358 wcs = inputExposure.getWcs()
359 inputBBox = inputExposure.getBBox()
360
361 # Trim the reference catalog to only those objects within the exposure
362 # bounding box dilated by half the bright star stamp size. This ensures
363 # all stars that overlap the exposure are included.
364 dilatationExtent = Extent2I(np.array(self.config.stampSize) // 2)
365 withinExposure = refObjLoader.loadPixelBox(
366 inputBBox.dilatedBy(dilatationExtent), wcs, filterName=filterName
367 )
368 refCat = withinExposure.refCat
369 fluxField = withinExposure.fluxField
370
371 # Define ref cat bright subset: objects brighter than the mag limit.
372 fluxLimit = ((self.config.magLimit * u.ABmag).to(u.nJy)).to_value() # AB magnitudes.
373 refCatBright = Table(
374 refCat.extract("id", "coord_ra", "coord_dec", fluxField, where=refCat[fluxField] > fluxLimit)
375 )
376 refCatBright["mag"] = (refCatBright[fluxField][:] * u.nJy).to(u.ABmag).to_value() # AB magnitudes.
377
378 # Remove input bright stars (if provided) from the bright subset.
379 if inputBrightStarStamps is not None:
380 # Extract the IDs of stars that have already been extracted.
381 existing = np.isin(refCatBright["id"][:], inputBrightStarStamps.getGaiaIds())
382 refCatBright = refCatBright[~existing]
383
384 # Loop over each reference bright star, extract a stamp around it.
385 pixCenters = []
386 starStamps = []
387 badRows = []
388 for row, object in enumerate(refCatBright):
389 coordSky = SpherePoint(object["coord_ra"], object["coord_dec"], radians)
390 coordPix = wcs.skyToPixel(coordSky)
391 # TODO: Replace this method with exposure getCutout after DM-40042.
392 starStamp = self._getCutout(inputExposure, coordPix, self.config.stampSize.list())
393 if not starStamp:
394 badRows.append(row)
395 continue
396 if self.config.doRemoveDetected:
397 self._replaceSecondaryFootprints(starStamp, coordPix, object["id"])
398 starStamps.append(starStamp)
399 pixCenters.append(coordPix)
400
401 # Remove bad rows from the reference catalog; set up return data.
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)
406
407 def _getCutout(self, inputExposure, coordPix: Point2D, stampSize: list[int]):
408 """Get a cutout from an input exposure, handling edge cases.
409
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.
414
415 Parameters
416 ----------
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.
423
424 Returns
425 -------
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.
429
430 Notes
431 -----
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.
435 """
436 # TODO: Replace this method with exposure getCutout after DM-40042.
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:
443 # Create full-sized stamp with pixels initially flagged as NO_DATA.
444 stamp = ExposureF(bbox=stampBBox)
445 stamp.image[:] = np.nan
446 stamp.mask.set(inputExposure.mask.getPlaneBitMask("NO_DATA"))
447 # Restore pixels which overlap the input exposure.
448 inputMI = inputExposure.maskedImage
449 overlap = inputMI.Factory(inputMI, overlapBBox)
450 stamp.maskedImage[overlapBBox] = overlap
451 # Set detector and WCS.
452 stamp.setDetector(inputExposure.getDetector())
453 stamp.setWcs(inputExposure.getWcs())
454 else:
455 stamp = None
456 return stamp
457
458 def _replaceSecondaryFootprints(self, stamp, coordPix, objectId, find="DETECTED", replace="BAD"):
459 """Replace all secondary footprints in a stamp with another mask flag.
460
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.
464
465 Parameters
466 ----------
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.
471 objectId : `int`
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.
477
478 Notes
479 -----
480 This method modifies the input ``stamp`` in-place.
481 """
482 # Find a FootprintSet given an Image and a threshold.
483 detThreshold = Threshold(stamp.mask.getPlaneBitMask(find), Threshold.BITMASK)
484 footprintSet = FootprintSet(stamp.mask, detThreshold)
485 allFootprints = footprintSet.getFootprints()
486 # Identify secondary objects (i.e., not the central primary object).
487 secondaryFootprints = []
488 for footprint in allFootprints:
489 if not footprint.contains(Point2I(coordPix)):
490 secondaryFootprints.append(footprint)
491 # Set secondary object footprints to BAD.
492 # Note: the value of numPrimaryFootprints can only be 0 or 1. If it is
493 # 0, then the primary object was not found overlapping a footprint.
494 # This can occur for low-S/N stars, for example. Processing can still
495 # continue beyond this point in an attempt to utilize this faint flux.
496 if (numPrimaryFootprints := len(allFootprints) - len(secondaryFootprints)) == 0:
497 self.log.info(
498 "Could not uniquely identify central %s footprint for star %s; "
499 "found %d footprints instead.",
500 find,
501 objectId,
502 numPrimaryFootprints,
503 )
504 footprintSet.setFootprints(secondaryFootprints)
505 footprintSet.setMask(stamp.mask, replace)
506
507 def warpStamps(self, stamps, pixCenters):
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.
511
512 Parameters
513 ----------
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.
518
519 Returns
520 -------
521 result : `~lsst.pipe.base.Struct`
522 Results as a struct with attributes:
523
524 ``warpedStars``
525 Stamps of warped stars.
526 (`list` [`~lsst.afw.image.MaskedImage`])
527 ``warpTransforms``
528 The corresponding Transform from the initial star stamp
529 to the common model grid.
530 (`list` [`~lsst.afw.geom.TransformPoint2ToPoint2`])
531 ``xy0s``
532 Coordinates of the bottom-left pixels of each stamp,
533 before rotation.
534 (`list` [`~lsst.geom.Point2I`])
535 ``nb90Rots``
536 The number of 90 degrees rotations required to compensate for
537 detector orientation.
538 (`int`)
539 """
540 # warping control; only contains shiftingALg provided in config
541 warpCont = WarpingControl(self.config.warpingKernelName)
542 # Compare model to star stamp sizes
543 bufferPix = (
544 self.modelStampSize[0] - self.config.stampSize[0],
545 self.modelStampSize[1] - self.config.stampSize[1],
546 )
547 # Initialize detector instance (note all stars were extracted from an
548 # exposure from the same detector)
549 det = stamps[0].getDetector()
550 # Define correction for optical distortions
551 if self.config.doApplyTransform:
552 pixToTan = det.getTransform(PIXELS, TAN_PIXELS)
553 else:
554 pixToTan = makeIdentityTransform()
555 # Array of all possible rotations for detector orientation:
556 possibleRots = np.array([k * np.pi / 2 for k in range(4)])
557 # determine how many, if any, rotations are required
558 yaw = det.getOrientation().getYaw()
559 nb90Rots = np.argmin(np.abs(possibleRots - float(yaw)))
560
561 # apply transformation to each star
562 warpedStars, warpTransforms, xy0s = [], [], []
563 for star, cent in zip(stamps, pixCenters):
564 # (re)create empty destination image
565 destImage = MaskedImageF(*self.modelStampSize)
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)
570 # Convert to int
571 newBottomLeft = Point2I(newBottomLeft)
572 # Set origin and save it
573 destImage.setXY0(newBottomLeft)
574 xy0s.append(newBottomLeft)
575
576 # Define linear shifting to recenter stamps
577 newCenter = pixToTan.applyForward(cent) # center of warped star
578 shift = (
579 self.modelCenter[0] + newBottomLeft[0] - newCenter[0],
580 self.modelCenter[1] + newBottomLeft[1] - newCenter[1],
581 )
582 affineShift = AffineTransform(shift)
583 shiftTransform = makeTransform(affineShift)
584
585 # Define full transform (warp and shift)
586 starWarper = pixToTan.then(shiftTransform)
587
588 # Apply it
589 goodPix = warpImage(destImage, star.getMaskedImage(), starWarper, warpCont)
590 if not goodPix:
591 self.log.debug("Warping of a star failed: no good pixel in output")
592
593 # Arbitrarily set origin of shifted star to 0
594 destImage.setXY0(0, 0)
595
596 # Apply rotation if appropriate
597 if nb90Rots:
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)
602
603 def setModelStamp(self):
604 """Compute (model) stamp size depending on provided buffer value."""
606 int(self.config.stampSize[0] * self.config.modelStampBuffer),
607 int(self.config.stampSize[1] * self.config.modelStampBuffer),
608 ]
609 # Force stamp to be odd-sized so we have a central pixel.
610 if not self.modelStampSize[0] % 2:
611 self.modelStampSize[0] += 1
612 if not self.modelStampSize[1] % 2:
613 self.modelStampSize[1] += 1
614 # Central pixel.
615 self.modelCenter = self.modelStampSize[0] // 2, self.modelStampSize[1] // 2
_getCutout(self, inputExposure, Point2D coordPix, list[int] stampSize)
_replaceSecondaryFootprints(self, stamp, coordPix, objectId, find="DETECTED", replace="BAD")
run(self, inputExposure, refObjLoader=None, dataId=None, skyCorr=None)
extractStamps(self, inputExposure, filterName="phot_g_mean", refObjLoader=None, inputBrightStarStamps=None)