LSST Applications g0b6bd0c080+a72a5dd7e6,g1182afd7b4+2a019aa3bb,g17e5ecfddb+2b8207f7de,g1d67935e3f+06cf436103,g38293774b4+ac198e9f13,g396055baef+6a2097e274,g3b44f30a73+6611e0205b,g480783c3b1+98f8679e14,g48ccf36440+89c08d0516,g4b93dc025c+98f8679e14,g5c4744a4d9+a302e8c7f0,g613e996a0d+e1c447f2e0,g6c8d09e9e7+25247a063c,g7271f0639c+98f8679e14,g7a9cd813b8+124095ede6,g9d27549199+a302e8c7f0,ga1cf026fa3+ac198e9f13,ga32aa97882+7403ac30ac,ga786bb30fb+7a139211af,gaa63f70f4e+9994eb9896,gabf319e997+ade567573c,gba47b54d5d+94dc90c3ea,gbec6a3398f+06cf436103,gc6308e37c7+07dd123edb,gc655b1545f+ade567573c,gcc9029db3c+ab229f5caf,gd01420fc67+06cf436103,gd877ba84e5+06cf436103,gdb4cecd868+6f279b5b48,ge2d134c3d5+cc4dbb2e3f,ge448b5faa6+86d1ceac1d,gecc7e12556+98f8679e14,gf3ee170dca+25247a063c,gf4ac96e456+ade567573c,gf9f5ea5b4d+ac198e9f13,gff490e6085+8c2580be5c,w.2022.27
LSST Data Management Base Package
isrTask.py
Go to the documentation of this file.
1# This file is part of ip_isr.
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
22import math
23import numpy
24
25import lsst.geom
26import lsst.afw.image as afwImage
27import lsst.afw.math as afwMath
28import lsst.pex.config as pexConfig
29import lsst.pipe.base as pipeBase
30import lsst.pipe.base.connectionTypes as cT
31
32from contextlib import contextmanager
33from lsstDebug import getDebugFrame
34
35from lsst.afw.cameraGeom import NullLinearityType, ReadoutCorner
36from lsst.afw.display import getDisplay
37from lsst.daf.persistence import ButlerDataRef
38from lsst.daf.persistence.butler import NoResults
39from lsst.meas.algorithms.detection import SourceDetectionTask
40from lsst.utils.timer import timeMethod
41
42from . import isrFunctions
43from . import isrQa
44from . import linearize
45from .defects import Defects
46
47from .assembleCcdTask import AssembleCcdTask
48from .crosstalk import CrosstalkTask, CrosstalkCalib
49from .fringe import FringeTask
50from .isr import maskNans
51from .masking import MaskingTask
52from .overscan import OverscanCorrectionTask
53from .straylight import StrayLightTask
54from .vignette import VignetteTask
55from .ampOffset import AmpOffsetTask
56from lsst.daf.butler import DimensionGraph
57
58
59__all__ = ["IsrTask", "IsrTaskConfig", "RunIsrTask", "RunIsrConfig"]
60
61
62def crosstalkSourceLookup(datasetType, registry, quantumDataId, collections):
63 """Lookup function to identify crosstalkSource entries.
64
65 This should return an empty list under most circumstances. Only
66 when inter-chip crosstalk has been identified should this be
67 populated.
68
69 Parameters
70 ----------
71 datasetType : `str`
72 Dataset to lookup.
73 registry : `lsst.daf.butler.Registry`
74 Butler registry to query.
75 quantumDataId : `lsst.daf.butler.ExpandedDataCoordinate`
76 Data id to transform to identify crosstalkSources. The
77 ``detector`` entry will be stripped.
78 collections : `lsst.daf.butler.CollectionSearch`
79 Collections to search through.
80
81 Returns
82 -------
83 results : `list` [`lsst.daf.butler.DatasetRef`]
84 List of datasets that match the query that will be used as
85 crosstalkSources.
86 """
87 newDataId = quantumDataId.subset(DimensionGraph(registry.dimensions, names=["instrument", "exposure"]))
88 results = set(registry.queryDatasets(datasetType, collections=collections, dataId=newDataId,
89 findFirst=True))
90 # In some contexts, calling `.expanded()` to expand all data IDs in the
91 # query results can be a lot faster because it vectorizes lookups. But in
92 # this case, expandDataId shouldn't need to hit the database at all in the
93 # steady state, because only the detector record is unknown and those are
94 # cached in the registry.
95 return [ref.expanded(registry.expandDataId(ref.dataId, records=newDataId.records)) for ref in results]
96
97
98class IsrTaskConnections(pipeBase.PipelineTaskConnections,
99 dimensions={"instrument", "exposure", "detector"},
100 defaultTemplates={}):
101 ccdExposure = cT.Input(
102 name="raw",
103 doc="Input exposure to process.",
104 storageClass="Exposure",
105 dimensions=["instrument", "exposure", "detector"],
106 )
107 camera = cT.PrerequisiteInput(
108 name="camera",
109 storageClass="Camera",
110 doc="Input camera to construct complete exposures.",
111 dimensions=["instrument"],
112 isCalibration=True,
113 )
114
115 crosstalk = cT.PrerequisiteInput(
116 name="crosstalk",
117 doc="Input crosstalk object",
118 storageClass="CrosstalkCalib",
119 dimensions=["instrument", "detector"],
120 isCalibration=True,
121 minimum=0, # can fall back to cameraGeom
122 )
123 crosstalkSources = cT.PrerequisiteInput(
124 name="isrOverscanCorrected",
125 doc="Overscan corrected input images.",
126 storageClass="Exposure",
127 dimensions=["instrument", "exposure", "detector"],
128 deferLoad=True,
129 multiple=True,
130 lookupFunction=crosstalkSourceLookup,
131 minimum=0, # not needed for all instruments, no config to control this
132 )
133 bias = cT.PrerequisiteInput(
134 name="bias",
135 doc="Input bias calibration.",
136 storageClass="ExposureF",
137 dimensions=["instrument", "detector"],
138 isCalibration=True,
139 )
140 dark = cT.PrerequisiteInput(
141 name='dark',
142 doc="Input dark calibration.",
143 storageClass="ExposureF",
144 dimensions=["instrument", "detector"],
145 isCalibration=True,
146 )
147 flat = cT.PrerequisiteInput(
148 name="flat",
149 doc="Input flat calibration.",
150 storageClass="ExposureF",
151 dimensions=["instrument", "physical_filter", "detector"],
152 isCalibration=True,
153 )
154 ptc = cT.PrerequisiteInput(
155 name="ptc",
156 doc="Input Photon Transfer Curve dataset",
157 storageClass="PhotonTransferCurveDataset",
158 dimensions=["instrument", "detector"],
159 isCalibration=True,
160 )
161 fringes = cT.PrerequisiteInput(
162 name="fringe",
163 doc="Input fringe calibration.",
164 storageClass="ExposureF",
165 dimensions=["instrument", "physical_filter", "detector"],
166 isCalibration=True,
167 minimum=0, # only needed for some bands, even when enabled
168 )
169 strayLightData = cT.PrerequisiteInput(
170 name='yBackground',
171 doc="Input stray light calibration.",
172 storageClass="StrayLightData",
173 dimensions=["instrument", "physical_filter", "detector"],
174 deferLoad=True,
175 isCalibration=True,
176 minimum=0, # only needed for some bands, even when enabled
177 )
178 bfKernel = cT.PrerequisiteInput(
179 name='bfKernel',
180 doc="Input brighter-fatter kernel.",
181 storageClass="NumpyArray",
182 dimensions=["instrument"],
183 isCalibration=True,
184 minimum=0, # can use either bfKernel or newBFKernel
185 )
186 newBFKernel = cT.PrerequisiteInput(
187 name='brighterFatterKernel',
188 doc="Newer complete kernel + gain solutions.",
189 storageClass="BrighterFatterKernel",
190 dimensions=["instrument", "detector"],
191 isCalibration=True,
192 minimum=0, # can use either bfKernel or newBFKernel
193 )
194 defects = cT.PrerequisiteInput(
195 name='defects',
196 doc="Input defect tables.",
197 storageClass="Defects",
198 dimensions=["instrument", "detector"],
199 isCalibration=True,
200 )
201 linearizer = cT.PrerequisiteInput(
202 name='linearizer',
203 storageClass="Linearizer",
204 doc="Linearity correction calibration.",
205 dimensions=["instrument", "detector"],
206 isCalibration=True,
207 minimum=0, # can fall back to cameraGeom
208 )
209 opticsTransmission = cT.PrerequisiteInput(
210 name="transmission_optics",
211 storageClass="TransmissionCurve",
212 doc="Transmission curve due to the optics.",
213 dimensions=["instrument"],
214 isCalibration=True,
215 )
216 filterTransmission = cT.PrerequisiteInput(
217 name="transmission_filter",
218 storageClass="TransmissionCurve",
219 doc="Transmission curve due to the filter.",
220 dimensions=["instrument", "physical_filter"],
221 isCalibration=True,
222 )
223 sensorTransmission = cT.PrerequisiteInput(
224 name="transmission_sensor",
225 storageClass="TransmissionCurve",
226 doc="Transmission curve due to the sensor.",
227 dimensions=["instrument", "detector"],
228 isCalibration=True,
229 )
230 atmosphereTransmission = cT.PrerequisiteInput(
231 name="transmission_atmosphere",
232 storageClass="TransmissionCurve",
233 doc="Transmission curve due to the atmosphere.",
234 dimensions=["instrument"],
235 isCalibration=True,
236 )
237 illumMaskedImage = cT.PrerequisiteInput(
238 name="illum",
239 doc="Input illumination correction.",
240 storageClass="MaskedImageF",
241 dimensions=["instrument", "physical_filter", "detector"],
242 isCalibration=True,
243 )
244
245 outputExposure = cT.Output(
246 name='postISRCCD',
247 doc="Output ISR processed exposure.",
248 storageClass="Exposure",
249 dimensions=["instrument", "exposure", "detector"],
250 )
251 preInterpExposure = cT.Output(
252 name='preInterpISRCCD',
253 doc="Output ISR processed exposure, with pixels left uninterpolated.",
254 storageClass="ExposureF",
255 dimensions=["instrument", "exposure", "detector"],
256 )
257 outputOssThumbnail = cT.Output(
258 name="OssThumb",
259 doc="Output Overscan-subtracted thumbnail image.",
260 storageClass="Thumbnail",
261 dimensions=["instrument", "exposure", "detector"],
262 )
263 outputFlattenedThumbnail = cT.Output(
264 name="FlattenedThumb",
265 doc="Output flat-corrected thumbnail image.",
266 storageClass="Thumbnail",
267 dimensions=["instrument", "exposure", "detector"],
268 )
269
270 def __init__(self, *, config=None):
271 super().__init__(config=config)
272
273 if config.doBias is not True:
274 self.prerequisiteInputs.remove("bias")
275 if config.doLinearize is not True:
276 self.prerequisiteInputs.remove("linearizer")
277 if config.doCrosstalk is not True:
278 self.prerequisiteInputs.remove("crosstalkSources")
279 self.prerequisiteInputs.remove("crosstalk")
280 if config.doBrighterFatter is not True:
281 self.prerequisiteInputs.remove("bfKernel")
282 self.prerequisiteInputs.remove("newBFKernel")
283 if config.doDefect is not True:
284 self.prerequisiteInputs.remove("defects")
285 if config.doDark is not True:
286 self.prerequisiteInputs.remove("dark")
287 if config.doFlat is not True:
288 self.prerequisiteInputs.remove("flat")
289 if config.doFringe is not True:
290 self.prerequisiteInputs.remove("fringes")
291 if config.doStrayLight is not True:
292 self.prerequisiteInputs.remove("strayLightData")
293 if config.usePtcGains is not True and config.usePtcReadNoise is not True:
294 self.prerequisiteInputs.remove("ptc")
295 if config.doAttachTransmissionCurve is not True:
296 self.prerequisiteInputs.remove("opticsTransmission")
297 self.prerequisiteInputs.remove("filterTransmission")
298 self.prerequisiteInputs.remove("sensorTransmission")
299 self.prerequisiteInputs.remove("atmosphereTransmission")
300 else:
301 if config.doUseOpticsTransmission is not True:
302 self.prerequisiteInputs.remove("opticsTransmission")
303 if config.doUseFilterTransmission is not True:
304 self.prerequisiteInputs.remove("filterTransmission")
305 if config.doUseSensorTransmission is not True:
306 self.prerequisiteInputs.remove("sensorTransmission")
307 if config.doUseAtmosphereTransmission is not True:
308 self.prerequisiteInputs.remove("atmosphereTransmission")
309 if config.doIlluminationCorrection is not True:
310 self.prerequisiteInputs.remove("illumMaskedImage")
311
312 if config.doWrite is not True:
313 self.outputs.remove("outputExposure")
314 self.outputs.remove("preInterpExposure")
315 self.outputs.remove("outputFlattenedThumbnail")
316 self.outputs.remove("outputOssThumbnail")
317 if config.doSaveInterpPixels is not True:
318 self.outputs.remove("preInterpExposure")
319 if config.qa.doThumbnailOss is not True:
320 self.outputs.remove("outputOssThumbnail")
321 if config.qa.doThumbnailFlattened is not True:
322 self.outputs.remove("outputFlattenedThumbnail")
323
324
325class IsrTaskConfig(pipeBase.PipelineTaskConfig,
326 pipelineConnections=IsrTaskConnections):
327 """Configuration parameters for IsrTask.
328
329 Items are grouped in the order in which they are executed by the task.
330 """
331 datasetType = pexConfig.Field(
332 dtype=str,
333 doc="Dataset type for input data; users will typically leave this alone, "
334 "but camera-specific ISR tasks will override it",
335 default="raw",
336 )
337
338 fallbackFilterName = pexConfig.Field(
339 dtype=str,
340 doc="Fallback default filter name for calibrations.",
341 optional=True
342 )
343 useFallbackDate = pexConfig.Field(
344 dtype=bool,
345 doc="Pass observation date when using fallback filter.",
346 default=False,
347 )
348 expectWcs = pexConfig.Field(
349 dtype=bool,
350 default=True,
351 doc="Expect input science images to have a WCS (set False for e.g. spectrographs)."
352 )
353 fwhm = pexConfig.Field(
354 dtype=float,
355 doc="FWHM of PSF in arcseconds.",
356 default=1.0,
357 )
358 qa = pexConfig.ConfigField(
359 dtype=isrQa.IsrQaConfig,
360 doc="QA related configuration options.",
361 )
362
363 # Image conversion configuration
364 doConvertIntToFloat = pexConfig.Field(
365 dtype=bool,
366 doc="Convert integer raw images to floating point values?",
367 default=True,
368 )
369
370 # Saturated pixel handling.
371 doSaturation = pexConfig.Field(
372 dtype=bool,
373 doc="Mask saturated pixels? NB: this is totally independent of the"
374 " interpolation option - this is ONLY setting the bits in the mask."
375 " To have them interpolated make sure doSaturationInterpolation=True",
376 default=True,
377 )
378 saturatedMaskName = pexConfig.Field(
379 dtype=str,
380 doc="Name of mask plane to use in saturation detection and interpolation",
381 default="SAT",
382 )
383 saturation = pexConfig.Field(
384 dtype=float,
385 doc="The saturation level to use if no Detector is present in the Exposure (ignored if NaN)",
386 default=float("NaN"),
387 )
388 growSaturationFootprintSize = pexConfig.Field(
389 dtype=int,
390 doc="Number of pixels by which to grow the saturation footprints",
391 default=1,
392 )
393
394 # Suspect pixel handling.
395 doSuspect = pexConfig.Field(
396 dtype=bool,
397 doc="Mask suspect pixels?",
398 default=False,
399 )
400 suspectMaskName = pexConfig.Field(
401 dtype=str,
402 doc="Name of mask plane to use for suspect pixels",
403 default="SUSPECT",
404 )
405 numEdgeSuspect = pexConfig.Field(
406 dtype=int,
407 doc="Number of edge pixels to be flagged as untrustworthy.",
408 default=0,
409 )
410 edgeMaskLevel = pexConfig.ChoiceField(
411 dtype=str,
412 doc="Mask edge pixels in which coordinate frame: DETECTOR or AMP?",
413 default="DETECTOR",
414 allowed={
415 'DETECTOR': 'Mask only the edges of the full detector.',
416 'AMP': 'Mask edges of each amplifier.',
417 },
418 )
419
420 # Initial masking options.
421 doSetBadRegions = pexConfig.Field(
422 dtype=bool,
423 doc="Should we set the level of all BAD patches of the chip to the chip's average value?",
424 default=True,
425 )
426 badStatistic = pexConfig.ChoiceField(
427 dtype=str,
428 doc="How to estimate the average value for BAD regions.",
429 default='MEANCLIP',
430 allowed={
431 "MEANCLIP": "Correct using the (clipped) mean of good data",
432 "MEDIAN": "Correct using the median of the good data",
433 },
434 )
435
436 # Overscan subtraction configuration.
437 doOverscan = pexConfig.Field(
438 dtype=bool,
439 doc="Do overscan subtraction?",
440 default=True,
441 )
442 overscan = pexConfig.ConfigurableField(
443 target=OverscanCorrectionTask,
444 doc="Overscan subtraction task for image segments.",
445 )
446 overscanFitType = pexConfig.ChoiceField(
447 dtype=str,
448 doc="The method for fitting the overscan bias level.",
449 default='MEDIAN',
450 allowed={
451 "POLY": "Fit ordinary polynomial to the longest axis of the overscan region",
452 "CHEB": "Fit Chebyshev polynomial to the longest axis of the overscan region",
453 "LEG": "Fit Legendre polynomial to the longest axis of the overscan region",
454 "NATURAL_SPLINE": "Fit natural spline to the longest axis of the overscan region",
455 "CUBIC_SPLINE": "Fit cubic spline to the longest axis of the overscan region",
456 "AKIMA_SPLINE": "Fit Akima spline to the longest axis of the overscan region",
457 "MEAN": "Correct using the mean of the overscan region",
458 "MEANCLIP": "Correct using a clipped mean of the overscan region",
459 "MEDIAN": "Correct using the median of the overscan region",
460 "MEDIAN_PER_ROW": "Correct using the median per row of the overscan region",
461 },
462 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface."
463 " This option will no longer be used, and will be removed after v20.")
464 )
465 overscanOrder = pexConfig.Field(
466 dtype=int,
467 doc=("Order of polynomial or to fit if overscan fit type is a polynomial, "
468 "or number of spline knots if overscan fit type is a spline."),
469 default=1,
470 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface."
471 " This option will no longer be used, and will be removed after v20.")
472 )
473 overscanNumSigmaClip = pexConfig.Field(
474 dtype=float,
475 doc="Rejection threshold (sigma) for collapsing overscan before fit",
476 default=3.0,
477 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface."
478 " This option will no longer be used, and will be removed after v20.")
479 )
480 overscanIsInt = pexConfig.Field(
481 dtype=bool,
482 doc="Treat overscan as an integer image for purposes of overscan.FitType=MEDIAN"
483 " and overscan.FitType=MEDIAN_PER_ROW.",
484 default=True,
485 deprecated=("Please configure overscan via the OverscanCorrectionConfig interface."
486 " This option will no longer be used, and will be removed after v20.")
487 )
488 # These options do not get deprecated, as they define how we slice up the
489 # image data.
490 overscanNumLeadingColumnsToSkip = pexConfig.Field(
491 dtype=int,
492 doc="Number of columns to skip in overscan, i.e. those closest to amplifier",
493 default=0,
494 )
495 overscanNumTrailingColumnsToSkip = pexConfig.Field(
496 dtype=int,
497 doc="Number of columns to skip in overscan, i.e. those farthest from amplifier",
498 default=0,
499 )
500 overscanMaxDev = pexConfig.Field(
501 dtype=float,
502 doc="Maximum deviation from the median for overscan",
503 default=1000.0, check=lambda x: x > 0
504 )
505 overscanBiasJump = pexConfig.Field(
506 dtype=bool,
507 doc="Fit the overscan in a piecewise-fashion to correct for bias jumps?",
508 default=False,
509 )
510 overscanBiasJumpKeyword = pexConfig.Field(
511 dtype=str,
512 doc="Header keyword containing information about devices.",
513 default="NO_SUCH_KEY",
514 )
515 overscanBiasJumpDevices = pexConfig.ListField(
516 dtype=str,
517 doc="List of devices that need piecewise overscan correction.",
518 default=(),
519 )
520 overscanBiasJumpLocation = pexConfig.Field(
521 dtype=int,
522 doc="Location of bias jump along y-axis.",
523 default=0,
524 )
525
526 # Amplifier to CCD assembly configuration
527 doAssembleCcd = pexConfig.Field(
528 dtype=bool,
529 default=True,
530 doc="Assemble amp-level exposures into a ccd-level exposure?"
531 )
532 assembleCcd = pexConfig.ConfigurableField(
533 target=AssembleCcdTask,
534 doc="CCD assembly task",
535 )
536
537 # General calibration configuration.
538 doAssembleIsrExposures = pexConfig.Field(
539 dtype=bool,
540 default=False,
541 doc="Assemble amp-level calibration exposures into ccd-level exposure?"
542 )
543 doTrimToMatchCalib = pexConfig.Field(
544 dtype=bool,
545 default=False,
546 doc="Trim raw data to match calibration bounding boxes?"
547 )
548
549 # Bias subtraction.
550 doBias = pexConfig.Field(
551 dtype=bool,
552 doc="Apply bias frame correction?",
553 default=True,
554 )
555 biasDataProductName = pexConfig.Field(
556 dtype=str,
557 doc="Name of the bias data product",
558 default="bias",
559 )
560 doBiasBeforeOverscan = pexConfig.Field(
561 dtype=bool,
562 doc="Reverse order of overscan and bias correction.",
563 default=False
564 )
565
566 # Variance construction
567 doVariance = pexConfig.Field(
568 dtype=bool,
569 doc="Calculate variance?",
570 default=True
571 )
572 gain = pexConfig.Field(
573 dtype=float,
574 doc="The gain to use if no Detector is present in the Exposure (ignored if NaN)",
575 default=float("NaN"),
576 )
577 readNoise = pexConfig.Field(
578 dtype=float,
579 doc="The read noise to use if no Detector is present in the Exposure",
580 default=0.0,
581 )
582 doEmpiricalReadNoise = pexConfig.Field(
583 dtype=bool,
584 default=False,
585 doc="Calculate empirical read noise instead of value from AmpInfo data?"
586 )
587 usePtcReadNoise = pexConfig.Field(
588 dtype=bool,
589 default=False,
590 doc="Use readnoise values from the Photon Transfer Curve?"
591 )
592 maskNegativeVariance = pexConfig.Field(
593 dtype=bool,
594 default=True,
595 doc="Mask pixels that claim a negative variance? This likely indicates a failure "
596 "in the measurement of the overscan at an edge due to the data falling off faster "
597 "than the overscan model can account for it."
598 )
599 negativeVarianceMaskName = pexConfig.Field(
600 dtype=str,
601 default="BAD",
602 doc="Mask plane to use to mark pixels with negative variance, if `maskNegativeVariance` is True.",
603 )
604 # Linearization.
605 doLinearize = pexConfig.Field(
606 dtype=bool,
607 doc="Correct for nonlinearity of the detector's response?",
608 default=True,
609 )
610
611 # Crosstalk.
612 doCrosstalk = pexConfig.Field(
613 dtype=bool,
614 doc="Apply intra-CCD crosstalk correction?",
615 default=False,
616 )
617 doCrosstalkBeforeAssemble = pexConfig.Field(
618 dtype=bool,
619 doc="Apply crosstalk correction before CCD assembly, and before trimming?",
620 default=False,
621 )
622 crosstalk = pexConfig.ConfigurableField(
623 target=CrosstalkTask,
624 doc="Intra-CCD crosstalk correction",
625 )
626
627 # Masking options.
628 doDefect = pexConfig.Field(
629 dtype=bool,
630 doc="Apply correction for CCD defects, e.g. hot pixels?",
631 default=True,
632 )
633 doNanMasking = pexConfig.Field(
634 dtype=bool,
635 doc="Mask non-finite (NAN, inf) pixels?",
636 default=True,
637 )
638 doWidenSaturationTrails = pexConfig.Field(
639 dtype=bool,
640 doc="Widen bleed trails based on their width?",
641 default=True
642 )
643
644 # Brighter-Fatter correction.
645 doBrighterFatter = pexConfig.Field(
646 dtype=bool,
647 default=False,
648 doc="Apply the brighter-fatter correction?"
649 )
650 brighterFatterLevel = pexConfig.ChoiceField(
651 dtype=str,
652 default="DETECTOR",
653 doc="The level at which to correct for brighter-fatter.",
654 allowed={
655 "AMP": "Every amplifier treated separately.",
656 "DETECTOR": "One kernel per detector",
657 }
658 )
659 brighterFatterMaxIter = pexConfig.Field(
660 dtype=int,
661 default=10,
662 doc="Maximum number of iterations for the brighter-fatter correction"
663 )
664 brighterFatterThreshold = pexConfig.Field(
665 dtype=float,
666 default=1000,
667 doc="Threshold used to stop iterating the brighter-fatter correction. It is the "
668 "absolute value of the difference between the current corrected image and the one "
669 "from the previous iteration summed over all the pixels."
670 )
671 brighterFatterApplyGain = pexConfig.Field(
672 dtype=bool,
673 default=True,
674 doc="Should the gain be applied when applying the brighter-fatter correction?"
675 )
676 brighterFatterMaskListToInterpolate = pexConfig.ListField(
677 dtype=str,
678 doc="List of mask planes that should be interpolated over when applying the brighter-fatter "
679 "correction.",
680 default=["SAT", "BAD", "NO_DATA", "UNMASKEDNAN"],
681 )
682 brighterFatterMaskGrowSize = pexConfig.Field(
683 dtype=int,
684 default=0,
685 doc="Number of pixels to grow the masks listed in config.brighterFatterMaskListToInterpolate "
686 "when brighter-fatter correction is applied."
687 )
688
689 # Dark subtraction.
690 doDark = pexConfig.Field(
691 dtype=bool,
692 doc="Apply dark frame correction?",
693 default=True,
694 )
695 darkDataProductName = pexConfig.Field(
696 dtype=str,
697 doc="Name of the dark data product",
698 default="dark",
699 )
700
701 # Camera-specific stray light removal.
702 doStrayLight = pexConfig.Field(
703 dtype=bool,
704 doc="Subtract stray light in the y-band (due to encoder LEDs)?",
705 default=False,
706 )
707 strayLight = pexConfig.ConfigurableField(
708 target=StrayLightTask,
709 doc="y-band stray light correction"
710 )
711
712 # Flat correction.
713 doFlat = pexConfig.Field(
714 dtype=bool,
715 doc="Apply flat field correction?",
716 default=True,
717 )
718 flatDataProductName = pexConfig.Field(
719 dtype=str,
720 doc="Name of the flat data product",
721 default="flat",
722 )
723 flatScalingType = pexConfig.ChoiceField(
724 dtype=str,
725 doc="The method for scaling the flat on the fly.",
726 default='USER',
727 allowed={
728 "USER": "Scale by flatUserScale",
729 "MEAN": "Scale by the inverse of the mean",
730 "MEDIAN": "Scale by the inverse of the median",
731 },
732 )
733 flatUserScale = pexConfig.Field(
734 dtype=float,
735 doc="If flatScalingType is 'USER' then scale flat by this amount; ignored otherwise",
736 default=1.0,
737 )
738 doTweakFlat = pexConfig.Field(
739 dtype=bool,
740 doc="Tweak flats to match observed amplifier ratios?",
741 default=False
742 )
743
744 # Amplifier normalization based on gains instead of using flats
745 # configuration.
746 doApplyGains = pexConfig.Field(
747 dtype=bool,
748 doc="Correct the amplifiers for their gains instead of applying flat correction",
749 default=False,
750 )
751 usePtcGains = pexConfig.Field(
752 dtype=bool,
753 doc="Use the gain values from the Photon Transfer Curve?",
754 default=False,
755 )
756 normalizeGains = pexConfig.Field(
757 dtype=bool,
758 doc="Normalize all the amplifiers in each CCD to have the same median value.",
759 default=False,
760 )
761
762 # Fringe correction.
763 doFringe = pexConfig.Field(
764 dtype=bool,
765 doc="Apply fringe correction?",
766 default=True,
767 )
768 fringe = pexConfig.ConfigurableField(
769 target=FringeTask,
770 doc="Fringe subtraction task",
771 )
772 fringeAfterFlat = pexConfig.Field(
773 dtype=bool,
774 doc="Do fringe subtraction after flat-fielding?",
775 default=True,
776 )
777
778 # Amp offset correction.
779 doAmpOffset = pexConfig.Field(
780 doc="Calculate and apply amp offset corrections?",
781 dtype=bool,
782 default=False,
783 )
784 ampOffset = pexConfig.ConfigurableField(
785 doc="Amp offset correction task.",
786 target=AmpOffsetTask,
787 )
788
789 # Initial CCD-level background statistics options.
790 doMeasureBackground = pexConfig.Field(
791 dtype=bool,
792 doc="Measure the background level on the reduced image?",
793 default=False,
794 )
795
796 # Camera-specific masking configuration.
797 doCameraSpecificMasking = pexConfig.Field(
798 dtype=bool,
799 doc="Mask camera-specific bad regions?",
800 default=False,
801 )
802 masking = pexConfig.ConfigurableField(
803 target=MaskingTask,
804 doc="Masking task."
805 )
806
807 # Interpolation options.
808 doInterpolate = pexConfig.Field(
809 dtype=bool,
810 doc="Interpolate masked pixels?",
811 default=True,
812 )
813 doSaturationInterpolation = pexConfig.Field(
814 dtype=bool,
815 doc="Perform interpolation over pixels masked as saturated?"
816 " NB: This is independent of doSaturation; if that is False this plane"
817 " will likely be blank, resulting in a no-op here.",
818 default=True,
819 )
820 doNanInterpolation = pexConfig.Field(
821 dtype=bool,
822 doc="Perform interpolation over pixels masked as NaN?"
823 " NB: This is independent of doNanMasking; if that is False this plane"
824 " will likely be blank, resulting in a no-op here.",
825 default=True,
826 )
827 doNanInterpAfterFlat = pexConfig.Field(
828 dtype=bool,
829 doc=("If True, ensure we interpolate NaNs after flat-fielding, even if we "
830 "also have to interpolate them before flat-fielding."),
831 default=False,
832 )
833 maskListToInterpolate = pexConfig.ListField(
834 dtype=str,
835 doc="List of mask planes that should be interpolated.",
836 default=['SAT', 'BAD'],
837 )
838 doSaveInterpPixels = pexConfig.Field(
839 dtype=bool,
840 doc="Save a copy of the pre-interpolated pixel values?",
841 default=False,
842 )
843
844 # Default photometric calibration options.
845 fluxMag0T1 = pexConfig.DictField(
846 keytype=str,
847 itemtype=float,
848 doc="The approximate flux of a zero-magnitude object in a one-second exposure, per filter.",
849 default=dict((f, pow(10.0, 0.4*m)) for f, m in (("Unknown", 28.0),
850 ))
851 )
852 defaultFluxMag0T1 = pexConfig.Field(
853 dtype=float,
854 doc="Default value for fluxMag0T1 (for an unrecognized filter).",
855 default=pow(10.0, 0.4*28.0)
856 )
857
858 # Vignette correction configuration.
859 doVignette = pexConfig.Field(
860 dtype=bool,
861 doc=("Compute and attach the validPolygon defining the unvignetted region to the exposure "
862 "according to vignetting parameters?"),
863 default=False,
864 )
865 doMaskVignettePolygon = pexConfig.Field(
866 dtype=bool,
867 doc=("Add a mask bit for pixels within the vignetted region. Ignored if doVignette "
868 "is False"),
869 default=True,
870 )
871 vignetteValue = pexConfig.Field(
872 dtype=float,
873 doc="Value to replace image array pixels with in the vignetted region? Ignored if None.",
874 optional=True,
875 default=None,
876 )
877 vignette = pexConfig.ConfigurableField(
878 target=VignetteTask,
879 doc="Vignetting task.",
880 )
881
882 # Transmission curve configuration.
883 doAttachTransmissionCurve = pexConfig.Field(
884 dtype=bool,
885 default=False,
886 doc="Construct and attach a wavelength-dependent throughput curve for this CCD image?"
887 )
888 doUseOpticsTransmission = pexConfig.Field(
889 dtype=bool,
890 default=True,
891 doc="Load and use transmission_optics (if doAttachTransmissionCurve is True)?"
892 )
893 doUseFilterTransmission = pexConfig.Field(
894 dtype=bool,
895 default=True,
896 doc="Load and use transmission_filter (if doAttachTransmissionCurve is True)?"
897 )
898 doUseSensorTransmission = pexConfig.Field(
899 dtype=bool,
900 default=True,
901 doc="Load and use transmission_sensor (if doAttachTransmissionCurve is True)?"
902 )
903 doUseAtmosphereTransmission = pexConfig.Field(
904 dtype=bool,
905 default=True,
906 doc="Load and use transmission_atmosphere (if doAttachTransmissionCurve is True)?"
907 )
908
909 # Illumination correction.
910 doIlluminationCorrection = pexConfig.Field(
911 dtype=bool,
912 default=False,
913 doc="Perform illumination correction?"
914 )
915 illuminationCorrectionDataProductName = pexConfig.Field(
916 dtype=str,
917 doc="Name of the illumination correction data product.",
918 default="illumcor",
919 )
920 illumScale = pexConfig.Field(
921 dtype=float,
922 doc="Scale factor for the illumination correction.",
923 default=1.0,
924 )
925 illumFilters = pexConfig.ListField(
926 dtype=str,
927 default=[],
928 doc="Only perform illumination correction for these filters."
929 )
930
931 # Write the outputs to disk. If ISR is run as a subtask, this may not
932 # be needed.
933 doWrite = pexConfig.Field(
934 dtype=bool,
935 doc="Persist postISRCCD?",
936 default=True,
937 )
938
939 def validate(self):
940 super().validate()
941 if self.doFlatdoFlat and self.doApplyGainsdoApplyGains:
942 raise ValueError("You may not specify both doFlat and doApplyGains")
943 if self.doBiasBeforeOverscandoBiasBeforeOverscan and self.doTrimToMatchCalibdoTrimToMatchCalib:
944 raise ValueError("You may not specify both doBiasBeforeOverscan and doTrimToMatchCalib")
945 if self.doSaturationInterpolationdoSaturationInterpolation and self.saturatedMaskNamesaturatedMaskName not in self.maskListToInterpolatemaskListToInterpolate:
946 self.maskListToInterpolatemaskListToInterpolate.append(self.saturatedMaskNamesaturatedMaskName)
947 if not self.doSaturationInterpolationdoSaturationInterpolation and self.saturatedMaskNamesaturatedMaskName in self.maskListToInterpolatemaskListToInterpolate:
948 self.maskListToInterpolatemaskListToInterpolate.remove(self.saturatedMaskNamesaturatedMaskName)
949 if self.doNanInterpolationdoNanInterpolation and "UNMASKEDNAN" not in self.maskListToInterpolatemaskListToInterpolate:
950 self.maskListToInterpolatemaskListToInterpolate.append("UNMASKEDNAN")
951
952
953class IsrTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
954 """Apply common instrument signature correction algorithms to a raw frame.
955
956 The process for correcting imaging data is very similar from
957 camera to camera. This task provides a vanilla implementation of
958 doing these corrections, including the ability to turn certain
959 corrections off if they are not needed. The inputs to the primary
960 method, `run()`, are a raw exposure to be corrected and the
961 calibration data products. The raw input is a single chip sized
962 mosaic of all amps including overscans and other non-science
963 pixels. The method `runDataRef()` identifies and defines the
964 calibration data products, and is intended for use by a
965 `lsst.pipe.base.cmdLineTask.CmdLineTask` and takes as input only a
967 subclassed for different camera, although the most camera specific
968 methods have been split into subtasks that can be redirected
969 appropriately.
970
971 The __init__ method sets up the subtasks for ISR processing, using
972 the defaults from `lsst.ip.isr`.
973
974 Parameters
975 ----------
976 args : `list`
977 Positional arguments passed to the Task constructor.
978 None used at this time.
979 kwargs : `dict`, optional
980 Keyword arguments passed on to the Task constructor.
981 None used at this time.
982 """
983 ConfigClass = IsrTaskConfig
984 _DefaultName = "isr"
985
986 def __init__(self, **kwargs):
987 super().__init__(**kwargs)
988 self.makeSubtask("assembleCcd")
989 self.makeSubtask("crosstalk")
990 self.makeSubtask("strayLight")
991 self.makeSubtask("fringe")
992 self.makeSubtask("masking")
993 self.makeSubtask("overscan")
994 self.makeSubtask("vignette")
995 self.makeSubtask("ampOffset")
996
997 def runQuantum(self, butlerQC, inputRefs, outputRefs):
998 inputs = butlerQC.get(inputRefs)
999
1000 try:
1001 inputs['detectorNum'] = inputRefs.ccdExposure.dataId['detector']
1002 except Exception as e:
1003 raise ValueError("Failure to find valid detectorNum value for Dataset %s: %s." %
1004 (inputRefs, e))
1005
1006 inputs['isGen3'] = True
1007
1008 detector = inputs['ccdExposure'].getDetector()
1009
1010 if self.config.doCrosstalk is True:
1011 # Crosstalk sources need to be defined by the pipeline
1012 # yaml if they exist.
1013 if 'crosstalk' in inputs and inputs['crosstalk'] is not None:
1014 if not isinstance(inputs['crosstalk'], CrosstalkCalib):
1015 inputs['crosstalk'] = CrosstalkCalib.fromTable(inputs['crosstalk'])
1016 else:
1017 coeffVector = (self.config.crosstalk.crosstalkValues
1018 if self.config.crosstalk.useConfigCoefficients else None)
1019 crosstalkCalib = CrosstalkCalib().fromDetector(detector, coeffVector=coeffVector)
1020 inputs['crosstalk'] = crosstalkCalib
1021 if inputs['crosstalk'].interChip and len(inputs['crosstalk'].interChip) > 0:
1022 if 'crosstalkSources' not in inputs:
1023 self.log.warning("No crosstalkSources found for chip with interChip terms!")
1024
1025 if self.doLinearizedoLinearize(detector) is True:
1026 if 'linearizer' in inputs:
1027 if isinstance(inputs['linearizer'], dict):
1028 linearizer = linearize.Linearizer(detector=detector, log=self.log)
1029 linearizer.fromYaml(inputs['linearizer'])
1030 self.log.warning("Dictionary linearizers will be deprecated in DM-28741.")
1031 elif isinstance(inputs['linearizer'], numpy.ndarray):
1032 linearizer = linearize.Linearizer(table=inputs.get('linearizer', None),
1033 detector=detector,
1034 log=self.log)
1035 self.log.warning("Bare lookup table linearizers will be deprecated in DM-28741.")
1036 else:
1037 linearizer = inputs['linearizer']
1038 linearizer.log = self.log
1039 inputs['linearizer'] = linearizer
1040 else:
1041 inputs['linearizer'] = linearize.Linearizer(detector=detector, log=self.log)
1042 self.log.warning("Constructing linearizer from cameraGeom information.")
1043
1044 if self.config.doDefect is True:
1045 if "defects" in inputs and inputs['defects'] is not None:
1046 # defects is loaded as a BaseCatalog with columns
1047 # x0, y0, width, height. Masking expects a list of defects
1048 # defined by their bounding box
1049 if not isinstance(inputs["defects"], Defects):
1050 inputs["defects"] = Defects.fromTable(inputs["defects"])
1051
1052 # Load the correct style of brighter-fatter kernel, and repack
1053 # the information as a numpy array.
1054 if self.config.doBrighterFatter:
1055 brighterFatterKernel = inputs.pop('newBFKernel', None)
1056 if brighterFatterKernel is None:
1057 brighterFatterKernel = inputs.get('bfKernel', None)
1058
1059 if brighterFatterKernel is not None and not isinstance(brighterFatterKernel, numpy.ndarray):
1060 # This is a ISR calib kernel
1061 detName = detector.getName()
1062 level = brighterFatterKernel.level
1063
1064 # This is expected to be a dictionary of amp-wise gains.
1065 inputs['bfGains'] = brighterFatterKernel.gain
1066 if self.config.brighterFatterLevel == 'DETECTOR':
1067 if level == 'DETECTOR':
1068 if detName in brighterFatterKernel.detKernels:
1069 inputs['bfKernel'] = brighterFatterKernel.detKernels[detName]
1070 else:
1071 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
1072 elif level == 'AMP':
1073 self.log.warning("Making DETECTOR level kernel from AMP based brighter "
1074 "fatter kernels.")
1075 brighterFatterKernel.makeDetectorKernelFromAmpwiseKernels(detName)
1076 inputs['bfKernel'] = brighterFatterKernel.detKernels[detName]
1077 elif self.config.brighterFatterLevel == 'AMP':
1078 raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented")
1079
1080 if self.config.doFringe is True and self.fringe.checkFilter(inputs['ccdExposure']):
1081 expId = inputs['ccdExposure'].info.id
1082 inputs['fringes'] = self.fringe.loadFringes(inputs['fringes'],
1083 expId=expId,
1084 assembler=self.assembleCcd
1085 if self.config.doAssembleIsrExposures else None)
1086 else:
1087 inputs['fringes'] = pipeBase.Struct(fringes=None)
1088
1089 if self.config.doStrayLight is True and self.strayLight.checkFilter(inputs['ccdExposure']):
1090 if 'strayLightData' not in inputs:
1091 inputs['strayLightData'] = None
1092
1093 outputs = self.runrun(**inputs)
1094 butlerQC.put(outputs, outputRefs)
1095
1096 def readIsrData(self, dataRef, rawExposure):
1097 """Retrieve necessary frames for instrument signature removal.
1098
1099 Pre-fetching all required ISR data products limits the IO
1100 required by the ISR. Any conflict between the calibration data
1101 available and that needed for ISR is also detected prior to
1102 doing processing, allowing it to fail quickly.
1103
1104 Parameters
1105 ----------
1107 Butler reference of the detector data to be processed
1108 rawExposure : `afw.image.Exposure`
1109 The raw exposure that will later be corrected with the
1110 retrieved calibration data; should not be modified in this
1111 method.
1112
1113 Returns
1114 -------
1115 result : `lsst.pipe.base.Struct`
1116 Result struct with components (which may be `None`):
1117 - ``bias``: bias calibration frame (`afw.image.Exposure`)
1118 - ``linearizer``: functor for linearization
1120 - ``crosstalkSources``: list of possible crosstalk sources (`list`)
1121 - ``dark``: dark calibration frame (`afw.image.Exposure`)
1122 - ``flat``: flat calibration frame (`afw.image.Exposure`)
1123 - ``bfKernel``: Brighter-Fatter kernel (`numpy.ndarray`)
1124 - ``defects``: list of defects (`lsst.ip.isr.Defects`)
1125 - ``fringes``: `lsst.pipe.base.Struct` with components:
1126 - ``fringes``: fringe calibration frame (`afw.image.Exposure`)
1127 - ``seed``: random seed derived from the ccdExposureId for random
1128 number generator (`uint32`).
1129 - ``opticsTransmission``: `lsst.afw.image.TransmissionCurve`
1130 A ``TransmissionCurve`` that represents the throughput of the
1131 optics, to be evaluated in focal-plane coordinates.
1132 - ``filterTransmission`` : `lsst.afw.image.TransmissionCurve`
1133 A ``TransmissionCurve`` that represents the throughput of the
1134 filter itself, to be evaluated in focal-plane coordinates.
1135 - ``sensorTransmission`` : `lsst.afw.image.TransmissionCurve`
1136 A ``TransmissionCurve`` that represents the throughput of the
1137 sensor itself, to be evaluated in post-assembly trimmed
1138 detector coordinates.
1139 - ``atmosphereTransmission`` : `lsst.afw.image.TransmissionCurve`
1140 A ``TransmissionCurve`` that represents the throughput of the
1141 atmosphere, assumed to be spatially constant.
1142 - ``strayLightData`` : `object`
1143 An opaque object containing calibration information for
1144 stray-light correction. If `None`, no correction will be
1145 performed.
1146 - ``illumMaskedImage`` : illumination correction image
1148
1149 Raises
1150 ------
1151 NotImplementedError :
1152 Raised if a per-amplifier brighter-fatter kernel is requested by
1153 the configuration.
1154 """
1155 try:
1156 dateObs = rawExposure.getInfo().getVisitInfo().getDate()
1157 dateObs = dateObs.toPython().isoformat()
1158 except RuntimeError:
1159 self.log.warning("Unable to identify dateObs for rawExposure.")
1160 dateObs = None
1161
1162 ccd = rawExposure.getDetector()
1163 filterLabel = rawExposure.getFilter()
1164 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
1165 rawExposure.mask.addMaskPlane("UNMASKEDNAN") # needed to match pre DM-15862 processing.
1166 biasExposure = (self.getIsrExposuregetIsrExposure(dataRef, self.config.biasDataProductName)
1167 if self.config.doBias else None)
1168 # immediate=True required for functors and linearizers are functors
1169 # see ticket DM-6515
1170 linearizer = (dataRef.get("linearizer", immediate=True)
1171 if self.doLinearizedoLinearize(ccd) else None)
1172 if linearizer is not None and not isinstance(linearizer, numpy.ndarray):
1173 linearizer.log = self.log
1174 if isinstance(linearizer, numpy.ndarray):
1175 linearizer = linearize.Linearizer(table=linearizer, detector=ccd)
1176
1177 crosstalkCalib = None
1178 if self.config.doCrosstalk:
1179 try:
1180 crosstalkCalib = dataRef.get("crosstalk", immediate=True)
1181 except NoResults:
1182 coeffVector = (self.config.crosstalk.crosstalkValues
1183 if self.config.crosstalk.useConfigCoefficients else None)
1184 crosstalkCalib = CrosstalkCalib().fromDetector(ccd, coeffVector=coeffVector)
1185 crosstalkSources = (self.crosstalk.prepCrosstalk(dataRef, crosstalkCalib)
1186 if self.config.doCrosstalk else None)
1187
1188 darkExposure = (self.getIsrExposuregetIsrExposure(dataRef, self.config.darkDataProductName)
1189 if self.config.doDark else None)
1190 flatExposure = (self.getIsrExposuregetIsrExposure(dataRef, self.config.flatDataProductName,
1191 dateObs=dateObs)
1192 if self.config.doFlat else None)
1193
1194 brighterFatterKernel = None
1195 brighterFatterGains = None
1196 if self.config.doBrighterFatter is True:
1197 try:
1198 # Use the new-style cp_pipe version of the kernel if it exists
1199 # If using a new-style kernel, always use the self-consistent
1200 # gains, i.e. the ones inside the kernel object itself
1201 brighterFatterKernel = dataRef.get("brighterFatterKernel")
1202 brighterFatterGains = brighterFatterKernel.gain
1203 self.log.info("New style brighter-fatter kernel (brighterFatterKernel) loaded")
1204 except NoResults:
1205 try: # Fall back to the old-style numpy-ndarray style kernel if necessary.
1206 brighterFatterKernel = dataRef.get("bfKernel")
1207 self.log.info("Old style brighter-fatter kernel (bfKernel) loaded")
1208 except NoResults:
1209 brighterFatterKernel = None
1210 if brighterFatterKernel is not None and not isinstance(brighterFatterKernel, numpy.ndarray):
1211 # If the kernel is not an ndarray, it's the cp_pipe version
1212 # so extract the kernel for this detector, or raise an error
1213 if self.config.brighterFatterLevel == 'DETECTOR':
1214 if brighterFatterKernel.detKernels:
1215 brighterFatterKernel = brighterFatterKernel.detKernels[ccd.getName()]
1216 else:
1217 raise RuntimeError("Failed to extract kernel from new-style BF kernel.")
1218 else:
1219 # TODO DM-15631 for implementing this
1220 raise NotImplementedError("Per-amplifier brighter-fatter correction not implemented")
1221
1222 defectList = (dataRef.get("defects")
1223 if self.config.doDefect else None)
1224 expId = rawExposure.info.id
1225 fringeStruct = (self.fringe.readFringes(dataRef, expId=expId, assembler=self.assembleCcd
1226 if self.config.doAssembleIsrExposures else None)
1227 if self.config.doFringe and self.fringe.checkFilter(rawExposure)
1228 else pipeBase.Struct(fringes=None))
1229
1230 if self.config.doAttachTransmissionCurve:
1231 opticsTransmission = (dataRef.get("transmission_optics")
1232 if self.config.doUseOpticsTransmission else None)
1233 filterTransmission = (dataRef.get("transmission_filter")
1234 if self.config.doUseFilterTransmission else None)
1235 sensorTransmission = (dataRef.get("transmission_sensor")
1236 if self.config.doUseSensorTransmission else None)
1237 atmosphereTransmission = (dataRef.get("transmission_atmosphere")
1238 if self.config.doUseAtmosphereTransmission else None)
1239 else:
1240 opticsTransmission = None
1241 filterTransmission = None
1242 sensorTransmission = None
1243 atmosphereTransmission = None
1244
1245 if self.config.doStrayLight:
1246 strayLightData = self.strayLight.readIsrData(dataRef, rawExposure)
1247 else:
1248 strayLightData = None
1249
1250 illumMaskedImage = (self.getIsrExposuregetIsrExposure(dataRef,
1251 self.config.illuminationCorrectionDataProductName).getMaskedImage()
1252 if (self.config.doIlluminationCorrection
1253 and physicalFilter in self.config.illumFilters)
1254 else None)
1255
1256 # Struct should include only kwargs to run()
1257 return pipeBase.Struct(bias=biasExposure,
1258 linearizer=linearizer,
1259 crosstalk=crosstalkCalib,
1260 crosstalkSources=crosstalkSources,
1261 dark=darkExposure,
1262 flat=flatExposure,
1263 bfKernel=brighterFatterKernel,
1264 bfGains=brighterFatterGains,
1265 defects=defectList,
1266 fringes=fringeStruct,
1267 opticsTransmission=opticsTransmission,
1268 filterTransmission=filterTransmission,
1269 sensorTransmission=sensorTransmission,
1270 atmosphereTransmission=atmosphereTransmission,
1271 strayLightData=strayLightData,
1272 illumMaskedImage=illumMaskedImage
1273 )
1274
1275 @timeMethod
1276 def run(self, ccdExposure, *, camera=None, bias=None, linearizer=None,
1277 crosstalk=None, crosstalkSources=None,
1278 dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None,
1279 fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None,
1280 sensorTransmission=None, atmosphereTransmission=None,
1281 detectorNum=None, strayLightData=None, illumMaskedImage=None,
1282 isGen3=False,
1283 ):
1284 """Perform instrument signature removal on an exposure.
1285
1286 Steps included in the ISR processing, in order performed, are:
1287 - saturation and suspect pixel masking
1288 - overscan subtraction
1289 - CCD assembly of individual amplifiers
1290 - bias subtraction
1291 - variance image construction
1292 - linearization of non-linear response
1293 - crosstalk masking
1294 - brighter-fatter correction
1295 - dark subtraction
1296 - fringe correction
1297 - stray light subtraction
1298 - flat correction
1299 - masking of known defects and camera specific features
1300 - vignette calculation
1301 - appending transmission curve and distortion model
1302
1303 Parameters
1304 ----------
1305 ccdExposure : `lsst.afw.image.Exposure`
1306 The raw exposure that is to be run through ISR. The
1307 exposure is modified by this method.
1308 camera : `lsst.afw.cameraGeom.Camera`, optional
1309 The camera geometry for this exposure. Required if
1310 one or more of ``ccdExposure``, ``bias``, ``dark``, or
1311 ``flat`` does not have an associated detector.
1312 bias : `lsst.afw.image.Exposure`, optional
1313 Bias calibration frame.
1314 linearizer : `lsst.ip.isr.linearize.LinearizeBase`, optional
1315 Functor for linearization.
1316 crosstalk : `lsst.ip.isr.crosstalk.CrosstalkCalib`, optional
1317 Calibration for crosstalk.
1318 crosstalkSources : `list`, optional
1319 List of possible crosstalk sources.
1320 dark : `lsst.afw.image.Exposure`, optional
1321 Dark calibration frame.
1322 flat : `lsst.afw.image.Exposure`, optional
1323 Flat calibration frame.
1325 Photon transfer curve dataset, with, e.g., gains
1326 and read noise.
1327 bfKernel : `numpy.ndarray`, optional
1328 Brighter-fatter kernel.
1329 bfGains : `dict` of `float`, optional
1330 Gains used to override the detector's nominal gains for the
1331 brighter-fatter correction. A dict keyed by amplifier name for
1332 the detector in question.
1333 defects : `lsst.ip.isr.Defects`, optional
1334 List of defects.
1335 fringes : `lsst.pipe.base.Struct`, optional
1336 Struct containing the fringe correction data, with
1337 elements:
1338 - ``fringes``: fringe calibration frame (`afw.image.Exposure`)
1339 - ``seed``: random seed derived from the ccdExposureId for random
1340 number generator (`uint32`)
1341 opticsTransmission: `lsst.afw.image.TransmissionCurve`, optional
1342 A ``TransmissionCurve`` that represents the throughput of the,
1343 optics, to be evaluated in focal-plane coordinates.
1344 filterTransmission : `lsst.afw.image.TransmissionCurve`
1345 A ``TransmissionCurve`` that represents the throughput of the
1346 filter itself, to be evaluated in focal-plane coordinates.
1347 sensorTransmission : `lsst.afw.image.TransmissionCurve`
1348 A ``TransmissionCurve`` that represents the throughput of the
1349 sensor itself, to be evaluated in post-assembly trimmed detector
1350 coordinates.
1351 atmosphereTransmission : `lsst.afw.image.TransmissionCurve`
1352 A ``TransmissionCurve`` that represents the throughput of the
1353 atmosphere, assumed to be spatially constant.
1354 detectorNum : `int`, optional
1355 The integer number for the detector to process.
1356 isGen3 : bool, optional
1357 Flag this call to run() as using the Gen3 butler environment.
1358 strayLightData : `object`, optional
1359 Opaque object containing calibration information for stray-light
1360 correction. If `None`, no correction will be performed.
1361 illumMaskedImage : `lsst.afw.image.MaskedImage`, optional
1362 Illumination correction image.
1363
1364 Returns
1365 -------
1366 result : `lsst.pipe.base.Struct`
1367 Result struct with component:
1368 - ``exposure`` : `afw.image.Exposure`
1369 The fully ISR corrected exposure.
1370 - ``outputExposure`` : `afw.image.Exposure`
1371 An alias for `exposure`
1372 - ``ossThumb`` : `numpy.ndarray`
1373 Thumbnail image of the exposure after overscan subtraction.
1374 - ``flattenedThumb`` : `numpy.ndarray`
1375 Thumbnail image of the exposure after flat-field correction.
1376
1377 Raises
1378 ------
1379 RuntimeError
1380 Raised if a configuration option is set to True, but the
1381 required calibration data has not been specified.
1382
1383 Notes
1384 -----
1385 The current processed exposure can be viewed by setting the
1386 appropriate lsstDebug entries in the `debug.display`
1387 dictionary. The names of these entries correspond to some of
1388 the IsrTaskConfig Boolean options, with the value denoting the
1389 frame to use. The exposure is shown inside the matching
1390 option check and after the processing of that step has
1391 finished. The steps with debug points are:
1392
1393 doAssembleCcd
1394 doBias
1395 doCrosstalk
1396 doBrighterFatter
1397 doDark
1398 doFringe
1399 doStrayLight
1400 doFlat
1401
1402 In addition, setting the "postISRCCD" entry displays the
1403 exposure after all ISR processing has finished.
1404
1405 """
1406
1407 if isGen3 is True:
1408 # Gen3 currently cannot automatically do configuration overrides.
1409 # DM-15257 looks to discuss this issue.
1410 # Configure input exposures;
1411
1412 ccdExposure = self.ensureExposureensureExposure(ccdExposure, camera, detectorNum)
1413 bias = self.ensureExposureensureExposure(bias, camera, detectorNum)
1414 dark = self.ensureExposureensureExposure(dark, camera, detectorNum)
1415 flat = self.ensureExposureensureExposure(flat, camera, detectorNum)
1416 else:
1417 if isinstance(ccdExposure, ButlerDataRef):
1418 return self.runDataRefrunDataRef(ccdExposure)
1419
1420 ccd = ccdExposure.getDetector()
1421 filterLabel = ccdExposure.getFilter()
1422 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
1423
1424 if not ccd:
1425 assert not self.config.doAssembleCcd, "You need a Detector to run assembleCcd."
1426 ccd = [FakeAmp(ccdExposure, self.config)]
1427
1428 # Validate Input
1429 if self.config.doBias and bias is None:
1430 raise RuntimeError("Must supply a bias exposure if config.doBias=True.")
1431 if self.doLinearizedoLinearize(ccd) and linearizer is None:
1432 raise RuntimeError("Must supply a linearizer if config.doLinearize=True for this detector.")
1433 if self.config.doBrighterFatter and bfKernel is None:
1434 raise RuntimeError("Must supply a kernel if config.doBrighterFatter=True.")
1435 if self.config.doDark and dark is None:
1436 raise RuntimeError("Must supply a dark exposure if config.doDark=True.")
1437 if self.config.doFlat and flat is None:
1438 raise RuntimeError("Must supply a flat exposure if config.doFlat=True.")
1439 if self.config.doDefect and defects is None:
1440 raise RuntimeError("Must supply defects if config.doDefect=True.")
1441 if (self.config.doFringe and physicalFilter in self.fringe.config.filters
1442 and fringes.fringes is None):
1443 # The `fringes` object needs to be a pipeBase.Struct, as
1444 # we use it as a `dict` for the parameters of
1445 # `FringeTask.run()`. The `fringes.fringes` `list` may
1446 # not be `None` if `doFringe=True`. Otherwise, raise.
1447 raise RuntimeError("Must supply fringe exposure as a pipeBase.Struct.")
1448 if (self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters
1449 and illumMaskedImage is None):
1450 raise RuntimeError("Must supply an illumcor if config.doIlluminationCorrection=True.")
1451
1452 # Begin ISR processing.
1453 if self.config.doConvertIntToFloat:
1454 self.log.info("Converting exposure to floating point values.")
1455 ccdExposure = self.convertIntToFloatconvertIntToFloat(ccdExposure)
1456
1457 if self.config.doBias and self.config.doBiasBeforeOverscan:
1458 self.log.info("Applying bias correction.")
1459 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1460 trimToFit=self.config.doTrimToMatchCalib)
1461 self.debugViewdebugView(ccdExposure, "doBias")
1462
1463 # Amplifier level processing.
1464 overscans = []
1465 for amp in ccd:
1466 # if ccdExposure is one amp,
1467 # check for coverage to prevent performing ops multiple times
1468 if ccdExposure.getBBox().contains(amp.getBBox()):
1469 # Check for fully masked bad amplifiers,
1470 # and generate masks for SUSPECT and SATURATED values.
1471 badAmp = self.maskAmplifiermaskAmplifier(ccdExposure, amp, defects)
1472
1473 if self.config.doOverscan and not badAmp:
1474 # Overscan correction on amp-by-amp basis.
1475 overscanResults = self.overscanCorrectionoverscanCorrection(ccdExposure, amp)
1476 self.log.debug("Corrected overscan for amplifier %s.", amp.getName())
1477 if overscanResults is not None and \
1478 self.config.qa is not None and self.config.qa.saveStats is True:
1479 if isinstance(overscanResults.overscanFit, float):
1480 qaMedian = overscanResults.overscanFit
1481 qaStdev = float("NaN")
1482 else:
1483 qaStats = afwMath.makeStatistics(overscanResults.overscanFit,
1484 afwMath.MEDIAN | afwMath.STDEVCLIP)
1485 qaMedian = qaStats.getValue(afwMath.MEDIAN)
1486 qaStdev = qaStats.getValue(afwMath.STDEVCLIP)
1487
1488 self.metadata[f"FIT MEDIAN {amp.getName()}"] = qaMedian
1489 self.metadata[f"FIT STDEV {amp.getName()}"] = qaStdev
1490 self.log.debug(" Overscan stats for amplifer %s: %f +/- %f",
1491 amp.getName(), qaMedian, qaStdev)
1492
1493 # Residuals after overscan correction
1494 qaStatsAfter = afwMath.makeStatistics(overscanResults.overscanImage,
1495 afwMath.MEDIAN | afwMath.STDEVCLIP)
1496 qaMedianAfter = qaStatsAfter.getValue(afwMath.MEDIAN)
1497 qaStdevAfter = qaStatsAfter.getValue(afwMath.STDEVCLIP)
1498
1499 self.metadata[f"RESIDUAL MEDIAN {amp.getName()}"] = qaMedianAfter
1500 self.metadata[f"RESIDUAL STDEV {amp.getName()}"] = qaStdevAfter
1501 self.log.debug(" Overscan stats for amplifer %s after correction: %f +/- %f",
1502 amp.getName(), qaMedianAfter, qaStdevAfter)
1503
1504 ccdExposure.getMetadata().set('OVERSCAN', "Overscan corrected")
1505 else:
1506 if badAmp:
1507 self.log.warning("Amplifier %s is bad.", amp.getName())
1508 overscanResults = None
1509
1510 overscans.append(overscanResults if overscanResults is not None else None)
1511 else:
1512 self.log.info("Skipped OSCAN for %s.", amp.getName())
1513
1514 if self.config.doCrosstalk and self.config.doCrosstalkBeforeAssemble:
1515 self.log.info("Applying crosstalk correction.")
1516 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1517 crosstalkSources=crosstalkSources, camera=camera)
1518 self.debugViewdebugView(ccdExposure, "doCrosstalk")
1519
1520 if self.config.doAssembleCcd:
1521 self.log.info("Assembling CCD from amplifiers.")
1522 ccdExposure = self.assembleCcd.assembleCcd(ccdExposure)
1523
1524 if self.config.expectWcs and not ccdExposure.getWcs():
1525 self.log.warning("No WCS found in input exposure.")
1526 self.debugViewdebugView(ccdExposure, "doAssembleCcd")
1527
1528 ossThumb = None
1529 if self.config.qa.doThumbnailOss:
1530 ossThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1531
1532 if self.config.doBias and not self.config.doBiasBeforeOverscan:
1533 self.log.info("Applying bias correction.")
1534 isrFunctions.biasCorrection(ccdExposure.getMaskedImage(), bias.getMaskedImage(),
1535 trimToFit=self.config.doTrimToMatchCalib)
1536 self.debugViewdebugView(ccdExposure, "doBias")
1537
1538 if self.config.doVariance:
1539 for amp, overscanResults in zip(ccd, overscans):
1540 if ccdExposure.getBBox().contains(amp.getBBox()):
1541 self.log.debug("Constructing variance map for amplifer %s.", amp.getName())
1542 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1543 if overscanResults is not None:
1544 self.updateVarianceupdateVariance(ampExposure, amp,
1545 overscanImage=overscanResults.overscanImage,
1546 ptcDataset=ptc)
1547 else:
1548 self.updateVarianceupdateVariance(ampExposure, amp,
1549 overscanImage=None,
1550 ptcDataset=ptc)
1551 if self.config.qa is not None and self.config.qa.saveStats is True:
1552 qaStats = afwMath.makeStatistics(ampExposure.getVariance(),
1553 afwMath.MEDIAN | afwMath.STDEVCLIP)
1554 self.metadata[f"ISR VARIANCE {amp.getName()} MEDIAN"] = \
1555 qaStats.getValue(afwMath.MEDIAN)
1556 self.metadata[f"ISR VARIANCE {amp.getName()} STDEV"] = \
1557 qaStats.getValue(afwMath.STDEVCLIP)
1558 self.log.debug(" Variance stats for amplifer %s: %f +/- %f.",
1559 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1560 qaStats.getValue(afwMath.STDEVCLIP))
1561 if self.config.maskNegativeVariance:
1562 self.maskNegativeVariancemaskNegativeVariance(ccdExposure)
1563
1564 if self.doLinearizedoLinearize(ccd):
1565 self.log.info("Applying linearizer.")
1566 linearizer.applyLinearity(image=ccdExposure.getMaskedImage().getImage(),
1567 detector=ccd, log=self.log)
1568
1569 if self.config.doCrosstalk and not self.config.doCrosstalkBeforeAssemble:
1570 self.log.info("Applying crosstalk correction.")
1571 self.crosstalk.run(ccdExposure, crosstalk=crosstalk,
1572 crosstalkSources=crosstalkSources, isTrimmed=True)
1573 self.debugViewdebugView(ccdExposure, "doCrosstalk")
1574
1575 # Masking block. Optionally mask known defects, NAN/inf pixels,
1576 # widen trails, and do anything else the camera needs. Saturated and
1577 # suspect pixels have already been masked.
1578 if self.config.doDefect:
1579 self.log.info("Masking defects.")
1580 self.maskDefectmaskDefect(ccdExposure, defects)
1581
1582 if self.config.numEdgeSuspect > 0:
1583 self.log.info("Masking edges as SUSPECT.")
1584 self.maskEdgesmaskEdges(ccdExposure, numEdgePixels=self.config.numEdgeSuspect,
1585 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
1586
1587 if self.config.doNanMasking:
1588 self.log.info("Masking non-finite (NAN, inf) value pixels.")
1589 self.maskNanmaskNan(ccdExposure)
1590
1591 if self.config.doWidenSaturationTrails:
1592 self.log.info("Widening saturation trails.")
1593 isrFunctions.widenSaturationTrails(ccdExposure.getMaskedImage().getMask())
1594
1595 if self.config.doCameraSpecificMasking:
1596 self.log.info("Masking regions for camera specific reasons.")
1597 self.masking.run(ccdExposure)
1598
1599 if self.config.doBrighterFatter:
1600 # We need to apply flats and darks before we can interpolate, and
1601 # we need to interpolate before we do B-F, but we do B-F without
1602 # the flats and darks applied so we can work in units of electrons
1603 # or holes. This context manager applies and then removes the darks
1604 # and flats.
1605 #
1606 # We also do not want to interpolate values here, so operate on
1607 # temporary images so we can apply only the BF-correction and roll
1608 # back the interpolation.
1609 interpExp = ccdExposure.clone()
1610 with self.flatContextflatContext(interpExp, flat, dark):
1611 isrFunctions.interpolateFromMask(
1612 maskedImage=interpExp.getMaskedImage(),
1613 fwhm=self.config.fwhm,
1614 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1615 maskNameList=list(self.config.brighterFatterMaskListToInterpolate)
1616 )
1617 bfExp = interpExp.clone()
1618
1619 self.log.info("Applying brighter-fatter correction using kernel type %s / gains %s.",
1620 type(bfKernel), type(bfGains))
1621 bfResults = isrFunctions.brighterFatterCorrection(bfExp, bfKernel,
1622 self.config.brighterFatterMaxIter,
1623 self.config.brighterFatterThreshold,
1624 self.config.brighterFatterApplyGain,
1625 bfGains)
1626 if bfResults[1] == self.config.brighterFatterMaxIter:
1627 self.log.warning("Brighter-fatter correction did not converge, final difference %f.",
1628 bfResults[0])
1629 else:
1630 self.log.info("Finished brighter-fatter correction in %d iterations.",
1631 bfResults[1])
1632 image = ccdExposure.getMaskedImage().getImage()
1633 bfCorr = bfExp.getMaskedImage().getImage()
1634 bfCorr -= interpExp.getMaskedImage().getImage()
1635 image += bfCorr
1636
1637 # Applying the brighter-fatter correction applies a
1638 # convolution to the science image. At the edges this
1639 # convolution may not have sufficient valid pixels to
1640 # produce a valid correction. Mark pixels within the size
1641 # of the brighter-fatter kernel as EDGE to warn of this
1642 # fact.
1643 self.log.info("Ensuring image edges are masked as EDGE to the brighter-fatter kernel size.")
1644 self.maskEdgesmaskEdges(ccdExposure, numEdgePixels=numpy.max(bfKernel.shape) // 2,
1645 maskPlane="EDGE")
1646
1647 if self.config.brighterFatterMaskGrowSize > 0:
1648 self.log.info("Growing masks to account for brighter-fatter kernel convolution.")
1649 for maskPlane in self.config.brighterFatterMaskListToInterpolate:
1650 isrFunctions.growMasks(ccdExposure.getMask(),
1651 radius=self.config.brighterFatterMaskGrowSize,
1652 maskNameList=maskPlane,
1653 maskValue=maskPlane)
1654
1655 self.debugViewdebugView(ccdExposure, "doBrighterFatter")
1656
1657 if self.config.doDark:
1658 self.log.info("Applying dark correction.")
1659 self.darkCorrectiondarkCorrection(ccdExposure, dark)
1660 self.debugViewdebugView(ccdExposure, "doDark")
1661
1662 if self.config.doFringe and not self.config.fringeAfterFlat:
1663 self.log.info("Applying fringe correction before flat.")
1664 self.fringe.run(ccdExposure, **fringes.getDict())
1665 self.debugViewdebugView(ccdExposure, "doFringe")
1666
1667 if self.config.doStrayLight and self.strayLight.check(ccdExposure):
1668 self.log.info("Checking strayLight correction.")
1669 self.strayLight.run(ccdExposure, strayLightData)
1670 self.debugViewdebugView(ccdExposure, "doStrayLight")
1671
1672 if self.config.doFlat:
1673 self.log.info("Applying flat correction.")
1674 self.flatCorrectionflatCorrection(ccdExposure, flat)
1675 self.debugViewdebugView(ccdExposure, "doFlat")
1676
1677 if self.config.doApplyGains:
1678 self.log.info("Applying gain correction instead of flat.")
1679 if self.config.usePtcGains:
1680 self.log.info("Using gains from the Photon Transfer Curve.")
1681 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains,
1682 ptcGains=ptc.gain)
1683 else:
1684 isrFunctions.applyGains(ccdExposure, self.config.normalizeGains)
1685
1686 if self.config.doFringe and self.config.fringeAfterFlat:
1687 self.log.info("Applying fringe correction after flat.")
1688 self.fringe.run(ccdExposure, **fringes.getDict())
1689
1690 if self.config.doVignette:
1691 if self.config.doMaskVignettePolygon:
1692 self.log.info("Constructing, attaching, and masking vignette polygon.")
1693 else:
1694 self.log.info("Constructing and attaching vignette polygon.")
1695 self.vignettePolygonvignettePolygon = self.vignette.run(
1696 exposure=ccdExposure, doUpdateMask=self.config.doMaskVignettePolygon,
1697 vignetteValue=self.config.vignetteValue, log=self.log)
1698
1699 if self.config.doAttachTransmissionCurve:
1700 self.log.info("Adding transmission curves.")
1701 isrFunctions.attachTransmissionCurve(ccdExposure, opticsTransmission=opticsTransmission,
1702 filterTransmission=filterTransmission,
1703 sensorTransmission=sensorTransmission,
1704 atmosphereTransmission=atmosphereTransmission)
1705
1706 flattenedThumb = None
1707 if self.config.qa.doThumbnailFlattened:
1708 flattenedThumb = isrQa.makeThumbnail(ccdExposure, isrQaConfig=self.config.qa)
1709
1710 if self.config.doIlluminationCorrection and physicalFilter in self.config.illumFilters:
1711 self.log.info("Performing illumination correction.")
1712 isrFunctions.illuminationCorrection(ccdExposure.getMaskedImage(),
1713 illumMaskedImage, illumScale=self.config.illumScale,
1714 trimToFit=self.config.doTrimToMatchCalib)
1715
1716 preInterpExp = None
1717 if self.config.doSaveInterpPixels:
1718 preInterpExp = ccdExposure.clone()
1719
1720 # Reset and interpolate bad pixels.
1721 #
1722 # Large contiguous bad regions (which should have the BAD mask
1723 # bit set) should have their values set to the image median.
1724 # This group should include defects and bad amplifiers. As the
1725 # area covered by these defects are large, there's little
1726 # reason to expect that interpolation would provide a more
1727 # useful value.
1728 #
1729 # Smaller defects can be safely interpolated after the larger
1730 # regions have had their pixel values reset. This ensures
1731 # that the remaining defects adjacent to bad amplifiers (as an
1732 # example) do not attempt to interpolate extreme values.
1733 if self.config.doSetBadRegions:
1734 badPixelCount, badPixelValue = isrFunctions.setBadRegions(ccdExposure)
1735 if badPixelCount > 0:
1736 self.log.info("Set %d BAD pixels to %f.", badPixelCount, badPixelValue)
1737
1738 if self.config.doInterpolate:
1739 self.log.info("Interpolating masked pixels.")
1740 isrFunctions.interpolateFromMask(
1741 maskedImage=ccdExposure.getMaskedImage(),
1742 fwhm=self.config.fwhm,
1743 growSaturatedFootprints=self.config.growSaturationFootprintSize,
1744 maskNameList=list(self.config.maskListToInterpolate)
1745 )
1746
1747 self.roughZeroPointroughZeroPoint(ccdExposure)
1748
1749 # correct for amp offsets within the CCD
1750 if self.config.doAmpOffset:
1751 self.log.info("Correcting amp offsets.")
1752 self.ampOffset.run(ccdExposure)
1753
1754 if self.config.doMeasureBackground:
1755 self.log.info("Measuring background level.")
1756 self.measureBackgroundmeasureBackground(ccdExposure, self.config.qa)
1757
1758 if self.config.qa is not None and self.config.qa.saveStats is True:
1759 for amp in ccd:
1760 ampExposure = ccdExposure.Factory(ccdExposure, amp.getBBox())
1761 qaStats = afwMath.makeStatistics(ampExposure.getImage(),
1762 afwMath.MEDIAN | afwMath.STDEVCLIP)
1763 self.metadata[f"ISR BACKGROUND {amp.getName()} MEDIAN"] = qaStats.getValue(afwMath.MEDIAN)
1764 self.metadata[f"ISR BACKGROUND {amp.getName()} STDEV"] = \
1765 qaStats.getValue(afwMath.STDEVCLIP)
1766 self.log.debug(" Background stats for amplifer %s: %f +/- %f",
1767 amp.getName(), qaStats.getValue(afwMath.MEDIAN),
1768 qaStats.getValue(afwMath.STDEVCLIP))
1769
1770 self.debugViewdebugView(ccdExposure, "postISRCCD")
1771
1772 return pipeBase.Struct(
1773 exposure=ccdExposure,
1774 ossThumb=ossThumb,
1775 flattenedThumb=flattenedThumb,
1776
1777 preInterpExposure=preInterpExp,
1778 outputExposure=ccdExposure,
1779 outputOssThumbnail=ossThumb,
1780 outputFlattenedThumbnail=flattenedThumb,
1781 )
1782
1783 @timeMethod
1784 def runDataRef(self, sensorRef):
1785 """Perform instrument signature removal on a ButlerDataRef of a Sensor.
1786
1787 This method contains the `CmdLineTask` interface to the ISR
1788 processing. All IO is handled here, freeing the `run()` method
1789 to manage only pixel-level calculations. The steps performed
1790 are:
1791 - Read in necessary detrending/isr/calibration data.
1792 - Process raw exposure in `run()`.
1793 - Persist the ISR-corrected exposure as "postISRCCD" if
1794 config.doWrite=True.
1795
1796 Parameters
1797 ----------
1799 DataRef of the detector data to be processed
1800
1801 Returns
1802 -------
1803 result : `lsst.pipe.base.Struct`
1804 Result struct with component:
1805 - ``exposure`` : `afw.image.Exposure`
1806 The fully ISR corrected exposure.
1807
1808 Raises
1809 ------
1810 RuntimeError
1811 Raised if a configuration option is set to True, but the
1812 required calibration data does not exist.
1813
1814 """
1815 self.log.info("Performing ISR on sensor %s.", sensorRef.dataId)
1816
1817 ccdExposure = sensorRef.get(self.config.datasetType)
1818
1819 camera = sensorRef.get("camera")
1820 isrData = self.readIsrDatareadIsrData(sensorRef, ccdExposure)
1821
1822 result = self.runrun(ccdExposure, camera=camera, **isrData.getDict())
1823
1824 if self.config.doWrite:
1825 sensorRef.put(result.exposure, "postISRCCD")
1826 if result.preInterpExposure is not None:
1827 sensorRef.put(result.preInterpExposure, "postISRCCD_uninterpolated")
1828 if result.ossThumb is not None:
1829 isrQa.writeThumbnail(sensorRef, result.ossThumb, "ossThumb")
1830 if result.flattenedThumb is not None:
1831 isrQa.writeThumbnail(sensorRef, result.flattenedThumb, "flattenedThumb")
1832
1833 return result
1834
1835 def getIsrExposure(self, dataRef, datasetType, dateObs=None, immediate=True):
1836 """Retrieve a calibration dataset for removing instrument signature.
1837
1838 Parameters
1839 ----------
1840
1842 DataRef of the detector data to find calibration datasets
1843 for.
1844 datasetType : `str`
1845 Type of dataset to retrieve (e.g. 'bias', 'flat', etc).
1846 dateObs : `str`, optional
1847 Date of the observation. Used to correct butler failures
1848 when using fallback filters.
1849 immediate : `Bool`
1850 If True, disable butler proxies to enable error handling
1851 within this routine.
1852
1853 Returns
1854 -------
1855 exposure : `lsst.afw.image.Exposure`
1856 Requested calibration frame.
1857
1858 Raises
1859 ------
1860 RuntimeError
1861 Raised if no matching calibration frame can be found.
1862 """
1863 try:
1864 exp = dataRef.get(datasetType, immediate=immediate)
1865 except Exception as exc1:
1866 if not self.config.fallbackFilterName:
1867 raise RuntimeError("Unable to retrieve %s for %s: %s." % (datasetType, dataRef.dataId, exc1))
1868 try:
1869 if self.config.useFallbackDate and dateObs:
1870 exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName,
1871 dateObs=dateObs, immediate=immediate)
1872 else:
1873 exp = dataRef.get(datasetType, filter=self.config.fallbackFilterName, immediate=immediate)
1874 except Exception as exc2:
1875 raise RuntimeError("Unable to retrieve %s for %s, even with fallback filter %s: %s AND %s." %
1876 (datasetType, dataRef.dataId, self.config.fallbackFilterName, exc1, exc2))
1877 self.log.warning("Using fallback calibration from filter %s.", self.config.fallbackFilterName)
1878
1879 if self.config.doAssembleIsrExposures:
1880 exp = self.assembleCcd.assembleCcd(exp)
1881 return exp
1882
1883 def ensureExposure(self, inputExp, camera=None, detectorNum=None):
1884 """Ensure that the data returned by Butler is a fully constructed exp.
1885
1886 ISR requires exposure-level image data for historical reasons, so if we
1887 did not recieve that from Butler, construct it from what we have,
1888 modifying the input in place.
1889
1890 Parameters
1891 ----------
1892 inputExp : `lsst.afw.image.Exposure`, `lsst.afw.image.DecoratedImageU`,
1893 or `lsst.afw.image.ImageF`
1894 The input data structure obtained from Butler.
1895 camera : `lsst.afw.cameraGeom.camera`, optional
1896 The camera associated with the image. Used to find the appropriate
1897 detector if detector is not already set.
1898 detectorNum : `int`, optional
1899 The detector in the camera to attach, if the detector is not
1900 already set.
1901
1902 Returns
1903 -------
1904 inputExp : `lsst.afw.image.Exposure`
1905 The re-constructed exposure, with appropriate detector parameters.
1906
1907 Raises
1908 ------
1909 TypeError
1910 Raised if the input data cannot be used to construct an exposure.
1911 """
1912 if isinstance(inputExp, afwImage.DecoratedImageU):
1914 elif isinstance(inputExp, afwImage.ImageF):
1916 elif isinstance(inputExp, afwImage.MaskedImageF):
1917 inputExp = afwImage.makeExposure(inputExp)
1918 elif isinstance(inputExp, afwImage.Exposure):
1919 pass
1920 elif inputExp is None:
1921 # Assume this will be caught by the setup if it is a problem.
1922 return inputExp
1923 else:
1924 raise TypeError("Input Exposure is not known type in isrTask.ensureExposure: %s." %
1925 (type(inputExp), ))
1926
1927 if inputExp.getDetector() is None:
1928 if camera is None or detectorNum is None:
1929 raise RuntimeError('Must supply both a camera and detector number when using exposures '
1930 'without a detector set.')
1931 inputExp.setDetector(camera[detectorNum])
1932
1933 return inputExp
1934
1935 def convertIntToFloat(self, exposure):
1936 """Convert exposure image from uint16 to float.
1937
1938 If the exposure does not need to be converted, the input is
1939 immediately returned. For exposures that are converted to use
1940 floating point pixels, the variance is set to unity and the
1941 mask to zero.
1942
1943 Parameters
1944 ----------
1945 exposure : `lsst.afw.image.Exposure`
1946 The raw exposure to be converted.
1947
1948 Returns
1949 -------
1950 newexposure : `lsst.afw.image.Exposure`
1951 The input ``exposure``, converted to floating point pixels.
1952
1953 Raises
1954 ------
1955 RuntimeError
1956 Raised if the exposure type cannot be converted to float.
1957
1958 """
1959 if isinstance(exposure, afwImage.ExposureF):
1960 # Nothing to be done
1961 self.log.debug("Exposure already of type float.")
1962 return exposure
1963 if not hasattr(exposure, "convertF"):
1964 raise RuntimeError("Unable to convert exposure (%s) to float." % type(exposure))
1965
1966 newexposure = exposure.convertF()
1967 newexposure.variance[:] = 1
1968 newexposure.mask[:] = 0x0
1969
1970 return newexposure
1971
1972 def maskAmplifier(self, ccdExposure, amp, defects):
1973 """Identify bad amplifiers, saturated and suspect pixels.
1974
1975 Parameters
1976 ----------
1977 ccdExposure : `lsst.afw.image.Exposure`
1978 Input exposure to be masked.
1980 Catalog of parameters defining the amplifier on this
1981 exposure to mask.
1982 defects : `lsst.ip.isr.Defects`
1983 List of defects. Used to determine if the entire
1984 amplifier is bad.
1985
1986 Returns
1987 -------
1988 badAmp : `Bool`
1989 If this is true, the entire amplifier area is covered by
1990 defects and unusable.
1991
1992 """
1993 maskedImage = ccdExposure.getMaskedImage()
1994
1995 badAmp = False
1996
1997 # Check if entire amp region is defined as a defect
1998 # NB: need to use amp.getBBox() for correct comparison with current
1999 # defects definition.
2000 if defects is not None:
2001 badAmp = bool(sum([v.getBBox().contains(amp.getBBox()) for v in defects]))
2002
2003 # In the case of a bad amp, we will set mask to "BAD"
2004 # (here use amp.getRawBBox() for correct association with pixels in
2005 # current ccdExposure).
2006 if badAmp:
2007 dataView = afwImage.MaskedImageF(maskedImage, amp.getRawBBox(),
2008 afwImage.PARENT)
2009 maskView = dataView.getMask()
2010 maskView |= maskView.getPlaneBitMask("BAD")
2011 del maskView
2012 return badAmp
2013
2014 # Mask remaining defects after assembleCcd() to allow for defects that
2015 # cross amplifier boundaries. Saturation and suspect pixels can be
2016 # masked now, though.
2017 limits = dict()
2018 if self.config.doSaturation and not badAmp:
2019 limits.update({self.config.saturatedMaskName: amp.getSaturation()})
2020 if self.config.doSuspect and not badAmp:
2021 limits.update({self.config.suspectMaskName: amp.getSuspectLevel()})
2022 if math.isfinite(self.config.saturation):
2023 limits.update({self.config.saturatedMaskName: self.config.saturation})
2024
2025 for maskName, maskThreshold in limits.items():
2026 if not math.isnan(maskThreshold):
2027 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2028 isrFunctions.makeThresholdMask(
2029 maskedImage=dataView,
2030 threshold=maskThreshold,
2031 growFootprints=0,
2032 maskName=maskName
2033 )
2034
2035 # Determine if we've fully masked this amplifier with SUSPECT and
2036 # SAT pixels.
2037 maskView = afwImage.Mask(maskedImage.getMask(), amp.getRawDataBBox(),
2038 afwImage.PARENT)
2039 maskVal = maskView.getPlaneBitMask([self.config.saturatedMaskName,
2040 self.config.suspectMaskName])
2041 if numpy.all(maskView.getArray() & maskVal > 0):
2042 badAmp = True
2043 maskView |= maskView.getPlaneBitMask("BAD")
2044
2045 return badAmp
2046
2047 def overscanCorrection(self, ccdExposure, amp):
2048 """Apply overscan correction in place.
2049
2050 This method does initial pixel rejection of the overscan
2051 region. The overscan can also be optionally segmented to
2052 allow for discontinuous overscan responses to be fit
2053 separately. The actual overscan subtraction is performed by
2054 the `lsst.ip.isr.isrFunctions.overscanCorrection` function,
2055 which is called here after the amplifier is preprocessed.
2056
2057 Parameters
2058 ----------
2059 ccdExposure : `lsst.afw.image.Exposure`
2060 Exposure to have overscan correction performed.
2061 amp : `lsst.afw.cameraGeom.Amplifer`
2062 The amplifier to consider while correcting the overscan.
2063
2064 Returns
2065 -------
2066 overscanResults : `lsst.pipe.base.Struct`
2067 Result struct with components:
2068 - ``imageFit`` : scalar or `lsst.afw.image.Image`
2069 Value or fit subtracted from the amplifier image data.
2070 - ``overscanFit`` : scalar or `lsst.afw.image.Image`
2071 Value or fit subtracted from the overscan image data.
2072 - ``overscanImage`` : `lsst.afw.image.Image`
2073 Image of the overscan region with the overscan
2074 correction applied. This quantity is used to estimate
2075 the amplifier read noise empirically.
2076
2077 Raises
2078 ------
2079 RuntimeError
2080 Raised if the ``amp`` does not contain raw pixel information.
2081
2082 See Also
2083 --------
2084 lsst.ip.isr.isrFunctions.overscanCorrection
2085 """
2086 if amp.getRawHorizontalOverscanBBox().isEmpty():
2087 self.log.info("ISR_OSCAN: No overscan region. Not performing overscan correction.")
2088 return None
2089
2090 statControl = afwMath.StatisticsControl()
2091 statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT"))
2092
2093 # Determine the bounding boxes
2094 dataBBox = amp.getRawDataBBox()
2095 oscanBBox = amp.getRawHorizontalOverscanBBox()
2096 dx0 = 0
2097 dx1 = 0
2098
2099 prescanBBox = amp.getRawPrescanBBox()
2100 if (oscanBBox.getBeginX() > prescanBBox.getBeginX()): # amp is at the right
2101 dx0 += self.config.overscanNumLeadingColumnsToSkip
2102 dx1 -= self.config.overscanNumTrailingColumnsToSkip
2103 else:
2104 dx0 += self.config.overscanNumTrailingColumnsToSkip
2105 dx1 -= self.config.overscanNumLeadingColumnsToSkip
2106
2107 # Determine if we need to work on subregions of the amplifier
2108 # and overscan.
2109 imageBBoxes = []
2110 overscanBBoxes = []
2111
2112 if ((self.config.overscanBiasJump
2113 and self.config.overscanBiasJumpLocation)
2114 and (ccdExposure.getMetadata().exists(self.config.overscanBiasJumpKeyword)
2115 and ccdExposure.getMetadata().getScalar(self.config.overscanBiasJumpKeyword) in
2116 self.config.overscanBiasJumpDevices)):
2117 if amp.getReadoutCorner() in (ReadoutCorner.LL, ReadoutCorner.LR):
2118 yLower = self.config.overscanBiasJumpLocation
2119 yUpper = dataBBox.getHeight() - yLower
2120 else:
2121 yUpper = self.config.overscanBiasJumpLocation
2122 yLower = dataBBox.getHeight() - yUpper
2123
2124 imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin(),
2125 lsst.geom.Extent2I(dataBBox.getWidth(), yLower)))
2126 overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, 0),
2127 lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
2128 yLower)))
2129
2130 imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin() + lsst.geom.Extent2I(0, yLower),
2131 lsst.geom.Extent2I(dataBBox.getWidth(), yUpper)))
2132 overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, yLower),
2133 lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
2134 yUpper)))
2135 else:
2136 imageBBoxes.append(lsst.geom.Box2I(dataBBox.getBegin(),
2137 lsst.geom.Extent2I(dataBBox.getWidth(), dataBBox.getHeight())))
2138 overscanBBoxes.append(lsst.geom.Box2I(oscanBBox.getBegin() + lsst.geom.Extent2I(dx0, 0),
2139 lsst.geom.Extent2I(oscanBBox.getWidth() - dx0 + dx1,
2140 oscanBBox.getHeight())))
2141
2142 # Perform overscan correction on subregions, ensuring saturated
2143 # pixels are masked.
2144 for imageBBox, overscanBBox in zip(imageBBoxes, overscanBBoxes):
2145 ampImage = ccdExposure.maskedImage[imageBBox]
2146 overscanImage = ccdExposure.maskedImage[overscanBBox]
2147
2148 overscanArray = overscanImage.image.array
2149 median = numpy.ma.median(numpy.ma.masked_where(overscanImage.mask.array, overscanArray))
2150 bad = numpy.where(numpy.abs(overscanArray - median) > self.config.overscanMaxDev)
2151 overscanImage.mask.array[bad] = overscanImage.mask.getPlaneBitMask("SAT")
2152
2153 statControl = afwMath.StatisticsControl()
2154 statControl.setAndMask(ccdExposure.mask.getPlaneBitMask("SAT"))
2155
2156 overscanResults = self.overscan.run(ampImage.getImage(), overscanImage, amp)
2157
2158 # Measure average overscan levels and record them in the metadata.
2159 levelStat = afwMath.MEDIAN
2160 sigmaStat = afwMath.STDEVCLIP
2161
2162 sctrl = afwMath.StatisticsControl(self.config.qa.flatness.clipSigma,
2163 self.config.qa.flatness.nIter)
2164 metadata = ccdExposure.getMetadata()
2165 ampNum = amp.getName()
2166 # if self.config.overscanFitType in ("MEDIAN", "MEAN", "MEANCLIP"):
2167 if isinstance(overscanResults.overscanFit, float):
2168 metadata[f"ISR_OSCAN_LEVEL{ampNum}"] = overscanResults.overscanFit
2169 metadata[f"ISR_OSCAN_SIGMA{ampNum}"] = 0.0
2170 else:
2171 stats = afwMath.makeStatistics(overscanResults.overscanFit, levelStat | sigmaStat, sctrl)
2172 metadata[f"ISR_OSCAN_LEVEL{ampNum}"] = stats.getValue(levelStat)
2173 metadata[f"ISR_OSCAN_SIGMA%{ampNum}"] = stats.getValue(sigmaStat)
2174
2175 return overscanResults
2176
2177 def updateVariance(self, ampExposure, amp, overscanImage=None, ptcDataset=None):
2178 """Set the variance plane using the gain and read noise
2179
2180 The read noise is calculated from the ``overscanImage`` if the
2181 ``doEmpiricalReadNoise`` option is set in the configuration; otherwise
2182 the value from the amplifier data is used.
2183
2184 Parameters
2185 ----------
2186 ampExposure : `lsst.afw.image.Exposure`
2187 Exposure to process.
2188 amp : `lsst.afw.table.AmpInfoRecord` or `FakeAmp`
2189 Amplifier detector data.
2190 overscanImage : `lsst.afw.image.MaskedImage`, optional.
2191 Image of overscan, required only for empirical read noise.
2192 ptcDataset : `lsst.ip.isr.PhotonTransferCurveDataset`, optional
2193 PTC dataset containing the gains and read noise.
2194
2195
2196 Raises
2197 ------
2198 RuntimeError
2199 Raised if either ``usePtcGains`` of ``usePtcReadNoise``
2200 are ``True``, but ptcDataset is not provided.
2201
2202 Raised if ```doEmpiricalReadNoise`` is ``True`` but
2203 ``overscanImage`` is ``None``.
2204
2205 See also
2206 --------
2207 lsst.ip.isr.isrFunctions.updateVariance
2208 """
2209 maskPlanes = [self.config.saturatedMaskName, self.config.suspectMaskName]
2210 if self.config.usePtcGains:
2211 if ptcDataset is None:
2212 raise RuntimeError("No ptcDataset provided to use PTC gains.")
2213 else:
2214 gain = ptcDataset.gain[amp.getName()]
2215 self.log.info("Using gain from Photon Transfer Curve.")
2216 else:
2217 gain = amp.getGain()
2218
2219 if math.isnan(gain):
2220 gain = 1.0
2221 self.log.warning("Gain set to NAN! Updating to 1.0 to generate Poisson variance.")
2222 elif gain <= 0:
2223 patchedGain = 1.0
2224 self.log.warning("Gain for amp %s == %g <= 0; setting to %f.",
2225 amp.getName(), gain, patchedGain)
2226 gain = patchedGain
2227
2228 if self.config.doEmpiricalReadNoise and overscanImage is None:
2229 raise RuntimeError("Overscan is none for EmpiricalReadNoise.")
2230
2231 if self.config.doEmpiricalReadNoise and overscanImage is not None:
2233 stats.setAndMask(overscanImage.mask.getPlaneBitMask(maskPlanes))
2234 readNoise = afwMath.makeStatistics(overscanImage, afwMath.STDEVCLIP, stats).getValue()
2235 self.log.info("Calculated empirical read noise for amp %s: %f.",
2236 amp.getName(), readNoise)
2237 elif self.config.usePtcReadNoise:
2238 if ptcDataset is None:
2239 raise RuntimeError("No ptcDataset provided to use PTC readnoise.")
2240 else:
2241 readNoise = ptcDataset.noise[amp.getName()]
2242 self.log.info("Using read noise from Photon Transfer Curve.")
2243 else:
2244 readNoise = amp.getReadNoise()
2245
2246 isrFunctions.updateVariance(
2247 maskedImage=ampExposure.getMaskedImage(),
2248 gain=gain,
2249 readNoise=readNoise,
2250 )
2251
2252 def maskNegativeVariance(self, exposure):
2253 """Identify and mask pixels with negative variance values.
2254
2255 Parameters
2256 ----------
2257 exposure : `lsst.afw.image.Exposure`
2258 Exposure to process.
2259
2260 See Also
2261 --------
2262 lsst.ip.isr.isrFunctions.updateVariance
2263 """
2264 maskPlane = exposure.getMask().getPlaneBitMask(self.config.negativeVarianceMaskName)
2265 bad = numpy.where(exposure.getVariance().getArray() <= 0.0)
2266 exposure.mask.array[bad] |= maskPlane
2267
2268 def darkCorrection(self, exposure, darkExposure, invert=False):
2269 """Apply dark correction in place.
2270
2271 Parameters
2272 ----------
2273 exposure : `lsst.afw.image.Exposure`
2274 Exposure to process.
2275 darkExposure : `lsst.afw.image.Exposure`
2276 Dark exposure of the same size as ``exposure``.
2277 invert : `Bool`, optional
2278 If True, re-add the dark to an already corrected image.
2279
2280 Raises
2281 ------
2282 RuntimeError
2283 Raised if either ``exposure`` or ``darkExposure`` do not
2284 have their dark time defined.
2285
2286 See Also
2287 --------
2288 lsst.ip.isr.isrFunctions.darkCorrection
2289 """
2290 expScale = exposure.getInfo().getVisitInfo().getDarkTime()
2291 if math.isnan(expScale):
2292 raise RuntimeError("Exposure darktime is NAN.")
2293 if darkExposure.getInfo().getVisitInfo() is not None \
2294 and not math.isnan(darkExposure.getInfo().getVisitInfo().getDarkTime()):
2295 darkScale = darkExposure.getInfo().getVisitInfo().getDarkTime()
2296 else:
2297 # DM-17444: darkExposure.getInfo.getVisitInfo() is None
2298 # so getDarkTime() does not exist.
2299 self.log.warning("darkExposure.getInfo().getVisitInfo() does not exist. Using darkScale = 1.0.")
2300 darkScale = 1.0
2301
2302 isrFunctions.darkCorrection(
2303 maskedImage=exposure.getMaskedImage(),
2304 darkMaskedImage=darkExposure.getMaskedImage(),
2305 expScale=expScale,
2306 darkScale=darkScale,
2307 invert=invert,
2308 trimToFit=self.config.doTrimToMatchCalib
2309 )
2310
2311 def doLinearize(self, detector):
2312 """Check if linearization is needed for the detector cameraGeom.
2313
2314 Checks config.doLinearize and the linearity type of the first
2315 amplifier.
2316
2317 Parameters
2318 ----------
2319 detector : `lsst.afw.cameraGeom.Detector`
2320 Detector to get linearity type from.
2321
2322 Returns
2323 -------
2324 doLinearize : `Bool`
2325 If True, linearization should be performed.
2326 """
2327 return self.config.doLinearize and \
2328 detector.getAmplifiers()[0].getLinearityType() != NullLinearityType
2329
2330 def flatCorrection(self, exposure, flatExposure, invert=False):
2331 """Apply flat correction in place.
2332
2333 Parameters
2334 ----------
2335 exposure : `lsst.afw.image.Exposure`
2336 Exposure to process.
2337 flatExposure : `lsst.afw.image.Exposure`
2338 Flat exposure of the same size as ``exposure``.
2339 invert : `Bool`, optional
2340 If True, unflatten an already flattened image.
2341
2342 See Also
2343 --------
2344 lsst.ip.isr.isrFunctions.flatCorrection
2345 """
2346 isrFunctions.flatCorrection(
2347 maskedImage=exposure.getMaskedImage(),
2348 flatMaskedImage=flatExposure.getMaskedImage(),
2349 scalingType=self.config.flatScalingType,
2350 userScale=self.config.flatUserScale,
2351 invert=invert,
2352 trimToFit=self.config.doTrimToMatchCalib
2353 )
2354
2355 def saturationDetection(self, exposure, amp):
2356 """Detect and mask saturated pixels in config.saturatedMaskName.
2357
2358 Parameters
2359 ----------
2360 exposure : `lsst.afw.image.Exposure`
2361 Exposure to process. Only the amplifier DataSec is processed.
2363 Amplifier detector data.
2364
2365 See Also
2366 --------
2367 lsst.ip.isr.isrFunctions.makeThresholdMask
2368 """
2369 if not math.isnan(amp.getSaturation()):
2370 maskedImage = exposure.getMaskedImage()
2371 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2372 isrFunctions.makeThresholdMask(
2373 maskedImage=dataView,
2374 threshold=amp.getSaturation(),
2375 growFootprints=0,
2376 maskName=self.config.saturatedMaskName,
2377 )
2378
2379 def saturationInterpolation(self, exposure):
2380 """Interpolate over saturated pixels, in place.
2381
2382 This method should be called after `saturationDetection`, to
2383 ensure that the saturated pixels have been identified in the
2384 SAT mask. It should also be called after `assembleCcd`, since
2385 saturated regions may cross amplifier boundaries.
2386
2387 Parameters
2388 ----------
2389 exposure : `lsst.afw.image.Exposure`
2390 Exposure to process.
2391
2392 See Also
2393 --------
2394 lsst.ip.isr.isrTask.saturationDetection
2395 lsst.ip.isr.isrFunctions.interpolateFromMask
2396 """
2397 isrFunctions.interpolateFromMask(
2398 maskedImage=exposure.getMaskedImage(),
2399 fwhm=self.config.fwhm,
2400 growSaturatedFootprints=self.config.growSaturationFootprintSize,
2401 maskNameList=list(self.config.saturatedMaskName),
2402 )
2403
2404 def suspectDetection(self, exposure, amp):
2405 """Detect and mask suspect pixels in config.suspectMaskName.
2406
2407 Parameters
2408 ----------
2409 exposure : `lsst.afw.image.Exposure`
2410 Exposure to process. Only the amplifier DataSec is processed.
2412 Amplifier detector data.
2413
2414 See Also
2415 --------
2416 lsst.ip.isr.isrFunctions.makeThresholdMask
2417
2418 Notes
2419 -----
2420 Suspect pixels are pixels whose value is greater than
2421 amp.getSuspectLevel(). This is intended to indicate pixels that may be
2422 affected by unknown systematics; for example if non-linearity
2423 corrections above a certain level are unstable then that would be a
2424 useful value for suspectLevel. A value of `nan` indicates that no such
2425 level exists and no pixels are to be masked as suspicious.
2426 """
2427 suspectLevel = amp.getSuspectLevel()
2428 if math.isnan(suspectLevel):
2429 return
2430
2431 maskedImage = exposure.getMaskedImage()
2432 dataView = maskedImage.Factory(maskedImage, amp.getRawBBox())
2433 isrFunctions.makeThresholdMask(
2434 maskedImage=dataView,
2435 threshold=suspectLevel,
2436 growFootprints=0,
2437 maskName=self.config.suspectMaskName,
2438 )
2439
2440 def maskDefect(self, exposure, defectBaseList):
2441 """Mask defects using mask plane "BAD", in place.
2442
2443 Parameters
2444 ----------
2445 exposure : `lsst.afw.image.Exposure`
2446 Exposure to process.
2447 defectBaseList : `lsst.ip.isr.Defects` or `list` of
2449 List of defects to mask.
2450
2451 Notes
2452 -----
2453 Call this after CCD assembly, since defects may cross amplifier
2454 boundaries.
2455 """
2456 maskedImage = exposure.getMaskedImage()
2457 if not isinstance(defectBaseList, Defects):
2458 # Promotes DefectBase to Defect
2459 defectList = Defects(defectBaseList)
2460 else:
2461 defectList = defectBaseList
2462 defectList.maskPixels(maskedImage, maskName="BAD")
2463
2464 def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR'):
2465 """Mask edge pixels with applicable mask plane.
2466
2467 Parameters
2468 ----------
2469 exposure : `lsst.afw.image.Exposure`
2470 Exposure to process.
2471 numEdgePixels : `int`, optional
2472 Number of edge pixels to mask.
2473 maskPlane : `str`, optional
2474 Mask plane name to use.
2475 level : `str`, optional
2476 Level at which to mask edges.
2477 """
2478 maskedImage = exposure.getMaskedImage()
2479 maskBitMask = maskedImage.getMask().getPlaneBitMask(maskPlane)
2480
2481 if numEdgePixels > 0:
2482 if level == 'DETECTOR':
2483 boxes = [maskedImage.getBBox()]
2484 elif level == 'AMP':
2485 boxes = [amp.getBBox() for amp in exposure.getDetector()]
2486
2487 for box in boxes:
2488 # This makes a bbox numEdgeSuspect pixels smaller than the
2489 # image on each side
2490 subImage = maskedImage[box]
2491 box.grow(-numEdgePixels)
2492 # Mask pixels outside box
2493 SourceDetectionTask.setEdgeBits(
2494 subImage,
2495 box,
2496 maskBitMask)
2497
2498 def maskAndInterpolateDefects(self, exposure, defectBaseList):
2499 """Mask and interpolate defects using mask plane "BAD", in place.
2500
2501 Parameters
2502 ----------
2503 exposure : `lsst.afw.image.Exposure`
2504 Exposure to process.
2505 defectBaseList : `lsst.ip.isr.Defects` or `list` of
2507 List of defects to mask and interpolate.
2508
2509 See Also
2510 --------
2511 lsst.ip.isr.isrTask.maskDefect
2512 """
2513 self.maskDefectmaskDefect(exposure, defectBaseList)
2514 self.maskEdgesmaskEdges(exposure, numEdgePixels=self.config.numEdgeSuspect,
2515 maskPlane="SUSPECT", level=self.config.edgeMaskLevel)
2516 isrFunctions.interpolateFromMask(
2517 maskedImage=exposure.getMaskedImage(),
2518 fwhm=self.config.fwhm,
2519 growSaturatedFootprints=0,
2520 maskNameList=["BAD"],
2521 )
2522
2523 def maskNan(self, exposure):
2524 """Mask NaNs using mask plane "UNMASKEDNAN", in place.
2525
2526 Parameters
2527 ----------
2528 exposure : `lsst.afw.image.Exposure`
2529 Exposure to process.
2530
2531 Notes
2532 -----
2533 We mask over all non-finite values (NaN, inf), including those
2534 that are masked with other bits (because those may or may not be
2535 interpolated over later, and we want to remove all NaN/infs).
2536 Despite this behaviour, the "UNMASKEDNAN" mask plane is used to
2537 preserve the historical name.
2538 """
2539 maskedImage = exposure.getMaskedImage()
2540
2541 # Find and mask NaNs
2542 maskedImage.getMask().addMaskPlane("UNMASKEDNAN")
2543 maskVal = maskedImage.getMask().getPlaneBitMask("UNMASKEDNAN")
2544 numNans = maskNans(maskedImage, maskVal)
2545 self.metadata["NUMNANS"] = numNans
2546 if numNans > 0:
2547 self.log.warning("There were %d unmasked NaNs.", numNans)
2548
2549 def maskAndInterpolateNan(self, exposure):
2550 """"Mask and interpolate NaN/infs using mask plane "UNMASKEDNAN",
2551 in place.
2552
2553 Parameters
2554 ----------
2555 exposure : `lsst.afw.image.Exposure`
2556 Exposure to process.
2557
2558 See Also
2559 --------
2560 lsst.ip.isr.isrTask.maskNan
2561 """
2562 self.maskNanmaskNan(exposure)
2563 isrFunctions.interpolateFromMask(
2564 maskedImage=exposure.getMaskedImage(),
2565 fwhm=self.config.fwhm,
2566 growSaturatedFootprints=0,
2567 maskNameList=["UNMASKEDNAN"],
2568 )
2569
2570 def measureBackground(self, exposure, IsrQaConfig=None):
2571 """Measure the image background in subgrids, for quality control.
2572
2573 Parameters
2574 ----------
2575 exposure : `lsst.afw.image.Exposure`
2576 Exposure to process.
2577 IsrQaConfig : `lsst.ip.isr.isrQa.IsrQaConfig`
2578 Configuration object containing parameters on which background
2579 statistics and subgrids to use.
2580 """
2581 if IsrQaConfig is not None:
2582 statsControl = afwMath.StatisticsControl(IsrQaConfig.flatness.clipSigma,
2583 IsrQaConfig.flatness.nIter)
2584 maskVal = exposure.getMaskedImage().getMask().getPlaneBitMask(["BAD", "SAT", "DETECTED"])
2585 statsControl.setAndMask(maskVal)
2586 maskedImage = exposure.getMaskedImage()
2587 stats = afwMath.makeStatistics(maskedImage, afwMath.MEDIAN | afwMath.STDEVCLIP, statsControl)
2588 skyLevel = stats.getValue(afwMath.MEDIAN)
2589 skySigma = stats.getValue(afwMath.STDEVCLIP)
2590 self.log.info("Flattened sky level: %f +/- %f.", skyLevel, skySigma)
2591 metadata = exposure.getMetadata()
2592 metadata["SKYLEVEL"] = skyLevel
2593 metadata["SKYSIGMA"] = skySigma
2594
2595 # calcluating flatlevel over the subgrids
2596 stat = afwMath.MEANCLIP if IsrQaConfig.flatness.doClip else afwMath.MEAN
2597 meshXHalf = int(IsrQaConfig.flatness.meshX/2.)
2598 meshYHalf = int(IsrQaConfig.flatness.meshY/2.)
2599 nX = int((exposure.getWidth() + meshXHalf) / IsrQaConfig.flatness.meshX)
2600 nY = int((exposure.getHeight() + meshYHalf) / IsrQaConfig.flatness.meshY)
2601 skyLevels = numpy.zeros((nX, nY))
2602
2603 for j in range(nY):
2604 yc = meshYHalf + j * IsrQaConfig.flatness.meshY
2605 for i in range(nX):
2606 xc = meshXHalf + i * IsrQaConfig.flatness.meshX
2607
2608 xLLC = xc - meshXHalf
2609 yLLC = yc - meshYHalf
2610 xURC = xc + meshXHalf - 1
2611 yURC = yc + meshYHalf - 1
2612
2613 bbox = lsst.geom.Box2I(lsst.geom.Point2I(xLLC, yLLC), lsst.geom.Point2I(xURC, yURC))
2614 miMesh = maskedImage.Factory(exposure.getMaskedImage(), bbox, afwImage.LOCAL)
2615
2616 skyLevels[i, j] = afwMath.makeStatistics(miMesh, stat, statsControl).getValue()
2617
2618 good = numpy.where(numpy.isfinite(skyLevels))
2619 skyMedian = numpy.median(skyLevels[good])
2620 flatness = (skyLevels[good] - skyMedian) / skyMedian
2621 flatness_rms = numpy.std(flatness)
2622 flatness_pp = flatness.max() - flatness.min() if len(flatness) > 0 else numpy.nan
2623
2624 self.log.info("Measuring sky levels in %dx%d grids: %f.", nX, nY, skyMedian)
2625 self.log.info("Sky flatness in %dx%d grids - pp: %f rms: %f.",
2626 nX, nY, flatness_pp, flatness_rms)
2627
2628 metadata["FLATNESS_PP"] = float(flatness_pp)
2629 metadata["FLATNESS_RMS"] = float(flatness_rms)
2630 metadata["FLATNESS_NGRIDS"] = '%dx%d' % (nX, nY)
2631 metadata["FLATNESS_MESHX"] = IsrQaConfig.flatness.meshX
2632 metadata["FLATNESS_MESHY"] = IsrQaConfig.flatness.meshY
2633
2634 def roughZeroPoint(self, exposure):
2635 """Set an approximate magnitude zero point for the exposure.
2636
2637 Parameters
2638 ----------
2639 exposure : `lsst.afw.image.Exposure`
2640 Exposure to process.
2641 """
2642 filterLabel = exposure.getFilter()
2643 physicalFilter = isrFunctions.getPhysicalFilter(filterLabel, self.log)
2644
2645 if physicalFilter in self.config.fluxMag0T1:
2646 fluxMag0 = self.config.fluxMag0T1[physicalFilter]
2647 else:
2648 self.log.warning("No rough magnitude zero point defined for filter %s.", physicalFilter)
2649 fluxMag0 = self.config.defaultFluxMag0T1
2650
2651 expTime = exposure.getInfo().getVisitInfo().getExposureTime()
2652 if not expTime > 0: # handle NaN as well as <= 0
2653 self.log.warning("Non-positive exposure time; skipping rough zero point.")
2654 return
2655
2656 self.log.info("Setting rough magnitude zero point for filter %s: %f",
2657 physicalFilter, 2.5*math.log10(fluxMag0*expTime))
2658 exposure.setPhotoCalib(afwImage.makePhotoCalibFromCalibZeroPoint(fluxMag0*expTime, 0.0))
2659
2660 @contextmanager
2661 def flatContext(self, exp, flat, dark=None):
2662 """Context manager that applies and removes flats and darks,
2663 if the task is configured to apply them.
2664
2665 Parameters
2666 ----------
2668 Exposure to process.
2670 Flat exposure the same size as ``exp``.
2671 dark : `lsst.afw.image.Exposure`, optional
2672 Dark exposure the same size as ``exp``.
2673
2674 Yields
2675 ------
2677 The flat and dark corrected exposure.
2678 """
2679 if self.config.doDark and dark is not None:
2680 self.darkCorrectiondarkCorrection(exp, dark)
2681 if self.config.doFlat:
2682 self.flatCorrectionflatCorrection(exp, flat)
2683 try:
2684 yield exp
2685 finally:
2686 if self.config.doFlat:
2687 self.flatCorrectionflatCorrection(exp, flat, invert=True)
2688 if self.config.doDark and dark is not None:
2689 self.darkCorrectiondarkCorrection(exp, dark, invert=True)
2690
2691 def debugView(self, exposure, stepname):
2692 """Utility function to examine ISR exposure at different stages.
2693
2694 Parameters
2695 ----------
2696 exposure : `lsst.afw.image.Exposure`
2697 Exposure to view.
2698 stepname : `str`
2699 State of processing to view.
2700 """
2701 frame = getDebugFrame(self._display, stepname)
2702 if frame:
2703 display = getDisplay(frame)
2704 display.scale('asinh', 'zscale')
2705 display.mtv(exposure)
2706 prompt = "Press Enter to continue [c]... "
2707 while True:
2708 ans = input(prompt).lower()
2709 if ans in ("", "c",):
2710 break
2711
2712
2714 """A Detector-like object that supports returning gain and saturation level
2715
2716 This is used when the input exposure does not have a detector.
2717
2718 Parameters
2719 ----------
2720 exposure : `lsst.afw.image.Exposure`
2721 Exposure to generate a fake amplifier for.
2722 config : `lsst.ip.isr.isrTaskConfig`
2723 Configuration to apply to the fake amplifier.
2724 """
2725
2726 def __init__(self, exposure, config):
2727 self._bbox_bbox = exposure.getBBox(afwImage.LOCAL)
2728 self._RawHorizontalOverscanBBox_RawHorizontalOverscanBBox = lsst.geom.Box2I()
2729 self._gain_gain = config.gain
2730 self._readNoise_readNoise = config.readNoise
2731 self._saturation_saturation = config.saturation
2732
2733 def getBBox(self):
2734 return self._bbox_bbox
2735
2736 def getRawBBox(self):
2737 return self._bbox_bbox
2738
2740 return self._RawHorizontalOverscanBBox_RawHorizontalOverscanBBox
2741
2742 def getGain(self):
2743 return self._gain_gain
2744
2745 def getReadNoise(self):
2746 return self._readNoise_readNoise
2747
2748 def getSaturation(self):
2749 return self._saturation_saturation
2750
2752 return float("NaN")
2753
2754
2755class RunIsrConfig(pexConfig.Config):
2756 isr = pexConfig.ConfigurableField(target=IsrTask, doc="Instrument signature removal")
2757
2758
2759class RunIsrTask(pipeBase.CmdLineTask):
2760 """Task to wrap the default IsrTask to allow it to be retargeted.
2761
2762 The standard IsrTask can be called directly from a command line
2763 program, but doing so removes the ability of the task to be
2764 retargeted. As most cameras override some set of the IsrTask
2765 methods, this would remove those data-specific methods in the
2766 output post-ISR images. This wrapping class fixes the issue,
2767 allowing identical post-ISR images to be generated by both the
2768 processCcd and isrTask code.
2769 """
2770 ConfigClass = RunIsrConfig
2771 _DefaultName = "runIsr"
2772
2773 def __init__(self, *args, **kwargs):
2774 super().__init__(*args, **kwargs)
2775 self.makeSubtask("isr")
2776
2777 def runDataRef(self, dataRef):
2778 """
2779 Parameters
2780 ----------
2782 data reference of the detector data to be processed
2783
2784 Returns
2785 -------
2786 result : `pipeBase.Struct`
2787 Result struct with component:
2788
2789 - exposure : `lsst.afw.image.Exposure`
2790 Post-ISR processed exposure.
2791 """
2792 return self.isr.runDataRef(dataRef)
table::Key< int > type
Definition: Detector.cc:163
An immutable representation of a camera.
Definition: Camera.h:43
A representation of a detector in a mosaic camera.
Definition: Detector.h:185
Encapsulate information about a bad portion of a detector.
Definition: Defect.h:41
A class to contain the data, WCS, and other information needed to describe an image of the sky.
Definition: Exposure.h:72
A class to represent a 2-dimensional array of pixels.
Definition: Image.h:51
Represent a 2-dimensional array of bitmask pixels.
Definition: Mask.h:77
A class to manipulate images, masks, and variance as a single object.
Definition: MaskedImage.h:73
A spatially-varying transmission curve as a function of wavelength.
Pass parameters to a Statistics object.
Definition: Statistics.h:92
A custom container class for records, based on std::vector.
Definition: Catalog.h:98
An integer coordinate rectangle.
Definition: Box.h:55
def getRawHorizontalOverscanBBox(self)
Definition: isrTask.py:2739
def __init__(self, exposure, config)
Definition: isrTask.py:2726
def __init__(self, *config=None)
Definition: isrTask.py:270
def flatCorrection(self, exposure, flatExposure, invert=False)
Definition: isrTask.py:2330
def maskAndInterpolateNan(self, exposure)
Definition: isrTask.py:2549
def saturationInterpolation(self, exposure)
Definition: isrTask.py:2379
def runDataRef(self, sensorRef)
Definition: isrTask.py:1784
def maskNan(self, exposure)
Definition: isrTask.py:2523
def maskAmplifier(self, ccdExposure, amp, defects)
Definition: isrTask.py:1972
def debugView(self, exposure, stepname)
Definition: isrTask.py:2691
def ensureExposure(self, inputExp, camera=None, detectorNum=None)
Definition: isrTask.py:1883
def getIsrExposure(self, dataRef, datasetType, dateObs=None, immediate=True)
Definition: isrTask.py:1835
def maskNegativeVariance(self, exposure)
Definition: isrTask.py:2252
def saturationDetection(self, exposure, amp)
Definition: isrTask.py:2355
def maskDefect(self, exposure, defectBaseList)
Definition: isrTask.py:2440
def __init__(self, **kwargs)
Definition: isrTask.py:986
def runQuantum(self, butlerQC, inputRefs, outputRefs)
Definition: isrTask.py:997
def maskEdges(self, exposure, numEdgePixels=0, maskPlane="SUSPECT", level='DETECTOR')
Definition: isrTask.py:2464
def overscanCorrection(self, ccdExposure, amp)
Definition: isrTask.py:2047
def measureBackground(self, exposure, IsrQaConfig=None)
Definition: isrTask.py:2570
def roughZeroPoint(self, exposure)
Definition: isrTask.py:2634
def maskAndInterpolateDefects(self, exposure, defectBaseList)
Definition: isrTask.py:2498
def readIsrData(self, dataRef, rawExposure)
Definition: isrTask.py:1096
def run(self, ccdExposure, *camera=None, bias=None, linearizer=None, crosstalk=None, crosstalkSources=None, dark=None, flat=None, ptc=None, bfKernel=None, bfGains=None, defects=None, fringes=pipeBase.Struct(fringes=None), opticsTransmission=None, filterTransmission=None, sensorTransmission=None, atmosphereTransmission=None, detectorNum=None, strayLightData=None, illumMaskedImage=None, isGen3=False)
Definition: isrTask.py:1283
def doLinearize(self, detector)
Definition: isrTask.py:2311
def flatContext(self, exp, flat, dark=None)
Definition: isrTask.py:2661
def convertIntToFloat(self, exposure)
Definition: isrTask.py:1935
def suspectDetection(self, exposure, amp)
Definition: isrTask.py:2404
def updateVariance(self, ampExposure, amp, overscanImage=None, ptcDataset=None)
Definition: isrTask.py:2177
def darkCorrection(self, exposure, darkExposure, invert=False)
Definition: isrTask.py:2268
def __init__(self, *args, **kwargs)
Definition: isrTask.py:2773
def runDataRef(self, dataRef)
Definition: isrTask.py:2777
daf::base::PropertyList * list
Definition: fits.cc:913
daf::base::PropertySet * set
Definition: fits.cc:912
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
std::shared_ptr< PhotoCalib > makePhotoCalibFromCalibZeroPoint(double instFluxMag0, double instFluxMag0Err)
Construct a PhotoCalib from the deprecated Calib-style instFluxMag0/instFluxMag0Err values.
Definition: PhotoCalib.cc:613
MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > * makeMaskedImage(typename std::shared_ptr< Image< ImagePixelT > > image, typename std::shared_ptr< Mask< MaskPixelT > > mask=Mask< MaskPixelT >(), typename std::shared_ptr< Image< VariancePixelT > > variance=Image< VariancePixelT >())
A function to return a MaskedImage of the correct type (cf.
Definition: MaskedImage.h:1240
std::shared_ptr< Exposure< ImagePixelT, MaskPixelT, VariancePixelT > > makeExposure(MaskedImage< ImagePixelT, MaskPixelT, VariancePixelT > &mimage, std::shared_ptr< geom::SkyWcs const > wcs=std::shared_ptr< geom::SkyWcs const >())
A function to return an Exposure of the correct type (cf.
Definition: Exposure.h:454
Statistics makeStatistics(lsst::afw::image::Image< Pixel > const &img, lsst::afw::image::Mask< image::MaskPixel > const &msk, int const flags, StatisticsControl const &sctrl=StatisticsControl())
Handle a watered-down front-end to the constructor (no variance)
Definition: Statistics.h:359
def checkFilter(exposure, filterList, log)
def crosstalkSourceLookup(datasetType, registry, quantumDataId, collections)
Definition: isrTask.py:62
size_t maskNans(afw::image::MaskedImage< PixelT > const &mi, afw::image::MaskPixel maskVal, afw::image::MaskPixel allow=0)
Mask NANs in an image.
Definition: Isr.cc:35
def getDebugFrame(debugDisplay, name)
Definition: lsstDebug.py:95