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