Coverage for python/lsst/cp/pipe/defects.py: 18%
271 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-14 10:54 +0000
« prev ^ index » next coverage.py v6.4.4, created at 2022-09-14 10:54 +0000
1# This file is part of cp_pipe.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21#
22import numpy as np
24import lsst.pipe.base as pipeBase
25import lsst.pipe.base.connectionTypes as cT
27from lsstDebug import getDebugFrame
28import lsst.pex.config as pexConfig
30import lsst.afw.image as afwImage
31import lsst.afw.math as afwMath
32import lsst.afw.detection as afwDetection
33import lsst.afw.display as afwDisplay
34from lsst.afw import cameraGeom
35from lsst.geom import Box2I, Point2I
36from lsst.meas.algorithms import SourceDetectionTask
37from lsst.ip.isr import Defects
38from .utils import countMaskedPixels
40from ._lookupStaticCalibration import lookupStaticCalibration
42__all__ = ['MeasureDefectsTaskConfig', 'MeasureDefectsTask',
43 'MergeDefectsTaskConfig', 'MergeDefectsTask', ]
46class MeasureDefectsConnections(pipeBase.PipelineTaskConnections,
47 dimensions=("instrument", "exposure", "detector")):
48 inputExp = cT.Input(
49 name="defectExps",
50 doc="Input ISR-processed exposures to measure.",
51 storageClass="Exposure",
52 dimensions=("instrument", "detector", "exposure"),
53 multiple=False
54 )
55 camera = cT.PrerequisiteInput(
56 name='camera',
57 doc="Camera associated with this exposure.",
58 storageClass="Camera",
59 dimensions=("instrument", ),
60 isCalibration=True,
61 lookupFunction=lookupStaticCalibration,
62 )
64 outputDefects = cT.Output(
65 name="singleExpDefects",
66 doc="Output measured defects.",
67 storageClass="Defects",
68 dimensions=("instrument", "detector", "exposure"),
69 )
72class MeasureDefectsTaskConfig(pipeBase.PipelineTaskConfig,
73 pipelineConnections=MeasureDefectsConnections):
74 """Configuration for measuring defects from a list of exposures
75 """
77 nSigmaBright = pexConfig.Field(
78 dtype=float,
79 doc=("Number of sigma above mean for bright pixel detection. The default value was found to be"
80 " appropriate for some LSST sensors in DM-17490."),
81 default=4.8,
82 )
83 nSigmaDark = pexConfig.Field(
84 dtype=float,
85 doc=("Number of sigma below mean for dark pixel detection. The default value was found to be"
86 " appropriate for some LSST sensors in DM-17490."),
87 default=-5.0,
88 )
89 nPixBorderUpDown = pexConfig.Field(
90 dtype=int,
91 doc="Number of pixels to exclude from top & bottom of image when looking for defects.",
92 default=7,
93 )
94 nPixBorderLeftRight = pexConfig.Field(
95 dtype=int,
96 doc="Number of pixels to exclude from left & right of image when looking for defects.",
97 default=7,
98 )
99 badOnAndOffPixelColumnThreshold = pexConfig.Field(
100 dtype=int,
101 doc=("If BPC is the set of all the bad pixels in a given column (not necessarily consecutive) "
102 "and the size of BPC is at least 'badOnAndOffPixelColumnThreshold', all the pixels between the "
103 "pixels that satisfy minY (BPC) and maxY (BPC) will be marked as bad, with 'Y' being the long "
104 "axis of the amplifier (and 'X' the other axis, which for a column is a constant for all "
105 "pixels in the set BPC). If there are more than 'goodPixelColumnGapThreshold' consecutive "
106 "non-bad pixels in BPC, an exception to the above is made and those consecutive "
107 "'goodPixelColumnGapThreshold' are not marked as bad."),
108 default=50,
109 )
110 goodPixelColumnGapThreshold = pexConfig.Field(
111 dtype=int,
112 doc=("Size, in pixels, of usable consecutive pixels in a column with on and off bad pixels (see "
113 "'badOnAndOffPixelColumnThreshold')."),
114 default=30,
115 )
117 def validate(self):
118 super().validate()
119 if self.nSigmaBright < 0.0:
120 raise ValueError("nSigmaBright must be above 0.0.")
121 if self.nSigmaDark > 0.0:
122 raise ValueError("nSigmaDark must be below 0.0.")
125class MeasureDefectsTask(pipeBase.PipelineTask):
126 """Measure the defects from one exposure.
127 """
129 ConfigClass = MeasureDefectsTaskConfig
130 _DefaultName = 'cpDefectMeasure'
132 def run(self, inputExp, camera):
133 """Measure one exposure for defects.
135 Parameters
136 ----------
137 inputExp : `lsst.afw.image.Exposure`
138 Exposure to examine.
139 camera : `lsst.afw.cameraGeom.Camera`
140 Camera to use for metadata.
142 Returns
143 -------
144 results : `lsst.pipe.base.Struct`
145 Results struct containing:
147 ``outputDefects``
148 The defects measured from this exposure
149 (`lsst.ip.isr.Defects`).
150 """
151 detector = inputExp.getDetector()
153 filterName = inputExp.getFilter().physicalLabel
154 datasetType = inputExp.getMetadata().get('IMGTYPE', 'UNKNOWN')
156 if datasetType.lower() == 'dark':
157 nSigmaList = [self.config.nSigmaBright]
158 else:
159 nSigmaList = [self.config.nSigmaBright, self.config.nSigmaDark]
160 defects = self.findHotAndColdPixels(inputExp, nSigmaList)
162 msg = "Found %s defects containing %s pixels in %s"
163 self.log.info(msg, len(defects), self._nPixFromDefects(defects), datasetType)
165 defects.updateMetadata(camera=camera, detector=detector, filterName=filterName,
166 setCalibId=True, setDate=True,
167 cpDefectGenImageType=datasetType)
169 return pipeBase.Struct(
170 outputDefects=defects,
171 )
173 @staticmethod
174 def _nPixFromDefects(defects):
175 """Count pixels in a defect.
177 Parameters
178 ----------
179 defects : `lsst.ip.isr.Defects`
180 Defects to measure.
182 Returns
183 -------
184 nPix : `int`
185 Number of defect pixels.
186 """
187 nPix = 0
188 for defect in defects:
189 nPix += defect.getBBox().getArea()
190 return nPix
192 def findHotAndColdPixels(self, exp, nSigma):
193 """Find hot and cold pixels in an image.
195 Using config-defined thresholds on a per-amp basis, mask
196 pixels that are nSigma above threshold in dark frames (hot
197 pixels), or nSigma away from the clipped mean in flats (hot &
198 cold pixels).
200 Parameters
201 ----------
202 exp : `lsst.afw.image.exposure.Exposure`
203 The exposure in which to find defects.
204 nSigma : `list` [`float`]
205 Detection threshold to use. Positive for DETECTED pixels,
206 negative for DETECTED_NEGATIVE pixels.
208 Returns
209 -------
210 defects : `lsst.ip.isr.Defects`
211 The defects found in the image.
212 """
213 self._setEdgeBits(exp)
214 maskedIm = exp.maskedImage
216 # the detection polarity for afwDetection, True for positive,
217 # False for negative, and therefore True for darks as they only have
218 # bright pixels, and both for flats, as they have bright and dark pix
219 footprintList = []
221 for amp in exp.getDetector():
222 ampImg = maskedIm[amp.getBBox()].clone()
224 # crop ampImage depending on where the amp lies in the image
225 if self.config.nPixBorderLeftRight:
226 if ampImg.getX0() == 0:
227 ampImg = ampImg[self.config.nPixBorderLeftRight:, :, afwImage.LOCAL]
228 else:
229 ampImg = ampImg[:-self.config.nPixBorderLeftRight, :, afwImage.LOCAL]
230 if self.config.nPixBorderUpDown:
231 if ampImg.getY0() == 0:
232 ampImg = ampImg[:, self.config.nPixBorderUpDown:, afwImage.LOCAL]
233 else:
234 ampImg = ampImg[:, :-self.config.nPixBorderUpDown, afwImage.LOCAL]
236 if self._getNumGoodPixels(ampImg) == 0: # amp contains no usable pixels
237 continue
239 # Remove a background estimate
240 ampImg -= afwMath.makeStatistics(ampImg, afwMath.MEANCLIP, ).getValue()
242 mergedSet = None
243 for sigma in nSigma:
244 nSig = np.abs(sigma)
245 self.debugHistogram('ampFlux', ampImg, nSig, exp)
246 polarity = {-1: False, 1: True}[np.sign(sigma)]
248 threshold = afwDetection.createThreshold(nSig, 'stdev', polarity=polarity)
250 footprintSet = afwDetection.FootprintSet(ampImg, threshold)
251 footprintSet.setMask(maskedIm.mask, ("DETECTED" if polarity else "DETECTED_NEGATIVE"))
253 if mergedSet is None:
254 mergedSet = footprintSet
255 else:
256 mergedSet.merge(footprintSet)
258 footprintList += mergedSet.getFootprints()
260 self.debugView('defectMap', ampImg,
261 Defects.fromFootprintList(mergedSet.getFootprints()), exp.getDetector())
263 defects = Defects.fromFootprintList(footprintList)
264 defects = self.maskBlocksIfIntermitentBadPixelsInColumn(defects)
266 return defects
268 @staticmethod
269 def _getNumGoodPixels(maskedIm, badMaskString="NO_DATA"):
270 """Return the number of non-bad pixels in the image."""
271 nPixels = maskedIm.mask.array.size
272 nBad = countMaskedPixels(maskedIm, badMaskString)
273 return nPixels - nBad
275 def _setEdgeBits(self, exposureOrMaskedImage, maskplaneToSet='EDGE'):
276 """Set edge bits on an exposure or maskedImage.
278 Raises
279 ------
280 TypeError
281 Raised if parameter ``exposureOrMaskedImage`` is an invalid type.
282 """
283 if isinstance(exposureOrMaskedImage, afwImage.Exposure):
284 mi = exposureOrMaskedImage.maskedImage
285 elif isinstance(exposureOrMaskedImage, afwImage.MaskedImage):
286 mi = exposureOrMaskedImage
287 else:
288 t = type(exposureOrMaskedImage)
289 raise TypeError(f"Function supports exposure or maskedImage but not {t}")
291 MASKBIT = mi.mask.getPlaneBitMask(maskplaneToSet)
292 if self.config.nPixBorderLeftRight:
293 mi.mask[: self.config.nPixBorderLeftRight, :, afwImage.LOCAL] |= MASKBIT
294 mi.mask[-self.config.nPixBorderLeftRight:, :, afwImage.LOCAL] |= MASKBIT
295 if self.config.nPixBorderUpDown:
296 mi.mask[:, : self.config.nPixBorderUpDown, afwImage.LOCAL] |= MASKBIT
297 mi.mask[:, -self.config.nPixBorderUpDown:, afwImage.LOCAL] |= MASKBIT
299 def maskBlocksIfIntermitentBadPixelsInColumn(self, defects):
300 """Mask blocks in a column if there are on-and-off bad pixels
302 If there's a column with on and off bad pixels, mask all the
303 pixels in between, except if there is a large enough gap of
304 consecutive good pixels between two bad pixels in the column.
306 Parameters
307 ----------
308 defects : `lsst.ip.isr.Defects`
309 The defects found in the image so far
311 Returns
312 -------
313 defects : `lsst.ip.isr.Defects`
314 If the number of bad pixels in a column is not larger or
315 equal than self.config.badPixelColumnThreshold, the input
316 list is returned. Otherwise, the defects list returned
317 will include boxes that mask blocks of on-and-of pixels.
318 """
319 # Get the (x, y) values of each bad pixel in amp.
320 coordinates = []
321 for defect in defects:
322 bbox = defect.getBBox()
323 x0, y0 = bbox.getMinX(), bbox.getMinY()
324 deltaX0, deltaY0 = bbox.getDimensions()
325 for j in np.arange(y0, y0+deltaY0):
326 for i in np.arange(x0, x0 + deltaX0):
327 coordinates.append((i, j))
329 x, y = [], []
330 for coordinatePair in coordinates:
331 x.append(coordinatePair[0])
332 y.append(coordinatePair[1])
334 x = np.array(x)
335 y = np.array(y)
336 # Find the defects with same "x" (vertical) coordinate (column).
337 unique, counts = np.unique(x, return_counts=True)
338 multipleX = []
339 for (a, b) in zip(unique, counts):
340 if b >= self.config.badOnAndOffPixelColumnThreshold:
341 multipleX.append(a)
342 if len(multipleX) != 0:
343 defects = self._markBlocksInBadColumn(x, y, multipleX, defects)
345 return defects
347 def _markBlocksInBadColumn(self, x, y, multipleX, defects):
348 """Mask blocks in a column if number of on-and-off bad pixels is above
349 threshold.
351 This function is called if the number of on-and-off bad pixels
352 in a column is larger or equal than
353 self.config.badOnAndOffPixelColumnThreshold.
355 Parameters
356 ---------
357 x : `list`
358 Lower left x coordinate of defect box. x coordinate is
359 along the short axis if amp.
360 y : `list`
361 Lower left y coordinate of defect box. x coordinate is
362 along the long axis if amp.
363 multipleX : list
364 List of x coordinates in amp. with multiple bad pixels
365 (i.e., columns with defects).
366 defects : `lsst.ip.isr.Defects`
367 The defcts found in the image so far
369 Returns
370 -------
371 defects : `lsst.ip.isr.Defects`
372 The defects list returned that will include boxes that
373 mask blocks of on-and-of pixels.
374 """
375 with defects.bulk_update():
376 goodPixelColumnGapThreshold = self.config.goodPixelColumnGapThreshold
377 for x0 in multipleX:
378 index = np.where(x == x0)
379 multipleY = y[index] # multipleY and multipleX are in 1-1 correspondence.
380 minY, maxY = np.min(multipleY), np.max(multipleY)
381 # Next few lines: don't mask pixels in column if gap
382 # of good pixels between two consecutive bad pixels is
383 # larger or equal than 'goodPixelColumnGapThreshold'.
384 diffIndex = np.where(np.diff(multipleY) >= goodPixelColumnGapThreshold)[0]
385 if len(diffIndex) != 0:
386 limits = [minY] # put the minimum first
387 for gapIndex in diffIndex:
388 limits.append(multipleY[gapIndex])
389 limits.append(multipleY[gapIndex+1])
390 limits.append(maxY) # maximum last
391 assert len(limits)%2 == 0, 'limits is even by design, but check anyways'
392 for i in np.arange(0, len(limits)-1, 2):
393 s = Box2I(minimum=Point2I(x0, limits[i]), maximum=Point2I(x0, limits[i+1]))
394 defects.append(s)
395 else: # No gap is large enough
396 s = Box2I(minimum=Point2I(x0, minY), maximum=Point2I(x0, maxY))
397 defects.append(s)
398 return defects
400 def debugView(self, stepname, ampImage, defects, detector): # pragma: no cover
401 """Plot the defects found by the task.
403 Parameters
404 ----------
405 stepname : `str`
406 Debug frame to request.
407 ampImage : `lsst.afw.image.MaskedImage`
408 Amplifier image to display.
409 defects : `lsst.ip.isr.Defects`
410 The defects to plot.
411 detector : `lsst.afw.cameraGeom.Detector`
412 Detector holding camera geometry.
413 """
414 frame = getDebugFrame(self._display, stepname)
415 if frame:
416 disp = afwDisplay.Display(frame=frame)
417 disp.scale('asinh', 'zscale')
418 disp.setMaskTransparency(80)
419 disp.setMaskPlaneColor("BAD", afwDisplay.RED)
421 maskedIm = ampImage.clone()
422 defects.maskPixels(maskedIm, "BAD")
424 mpDict = maskedIm.mask.getMaskPlaneDict()
425 for plane in mpDict.keys():
426 if plane in ['BAD']:
427 continue
428 disp.setMaskPlaneColor(plane, afwDisplay.IGNORE)
430 disp.setImageColormap('gray')
431 disp.mtv(maskedIm)
432 cameraGeom.utils.overlayCcdBoxes(detector, isTrimmed=True, display=disp)
433 prompt = "Press Enter to continue [c]... "
434 while True:
435 ans = input(prompt).lower()
436 if ans in ('', 'c', ):
437 break
439 def debugHistogram(self, stepname, ampImage, nSigmaUsed, exp):
440 """Make a histogram of the distribution of pixel values for
441 each amp.
443 The main image data histogram is plotted in blue. Edge
444 pixels, if masked, are in red. Note that masked edge pixels
445 do not contribute to the underflow and overflow numbers.
447 Note that this currently only supports the 16-amp LSST
448 detectors.
450 Parameters
451 ----------
452 stepname : `str`
453 Debug frame to request.
454 ampImage : `lsst.afw.image.MaskedImage`
455 Amplifier image to display.
456 nSigmaUsed : `float`
457 The number of sigma used for detection
458 exp : `lsst.afw.image.exposure.Exposure`
459 The exposure in which the defects were found.
460 """
461 frame = getDebugFrame(self._display, stepname)
462 if frame:
463 import matplotlib.pyplot as plt
465 detector = exp.getDetector()
466 nX = np.floor(np.sqrt(len(detector)))
467 nY = len(detector) // nX
468 fig, ax = plt.subplots(nrows=int(nY), ncols=int(nX), sharex='col', sharey='row', figsize=(13, 10))
470 expTime = exp.getInfo().getVisitInfo().getExposureTime()
472 for (amp, a) in zip(reversed(detector), ax.flatten()):
473 mi = exp.maskedImage[amp.getBBox()]
475 # normalize by expTime as we plot in ADU/s and don't
476 # always work with master calibs
477 mi.image.array /= expTime
478 stats = afwMath.makeStatistics(mi, afwMath.MEANCLIP | afwMath.STDEVCLIP)
479 mean, sigma = stats.getValue(afwMath.MEANCLIP), stats.getValue(afwMath.STDEVCLIP)
480 # Get array of pixels
481 EDGEBIT = exp.maskedImage.mask.getPlaneBitMask("EDGE")
482 imgData = mi.image.array[(mi.mask.array & EDGEBIT) == 0].flatten()
483 edgeData = mi.image.array[(mi.mask.array & EDGEBIT) != 0].flatten()
485 thrUpper = mean + nSigmaUsed*sigma
486 thrLower = mean - nSigmaUsed*sigma
488 nRight = len(imgData[imgData > thrUpper])
489 nLeft = len(imgData[imgData < thrLower])
491 nsig = nSigmaUsed + 1.2 # add something small so the edge of the plot is out from level used
492 leftEdge = mean - nsig * nSigmaUsed*sigma
493 rightEdge = mean + nsig * nSigmaUsed*sigma
494 nbins = np.linspace(leftEdge, rightEdge, 1000)
495 ey, bin_borders, patches = a.hist(edgeData, histtype='step', bins=nbins,
496 lw=1, edgecolor='red')
497 y, bin_borders, patches = a.hist(imgData, histtype='step', bins=nbins,
498 lw=3, edgecolor='blue')
500 # Report number of entries in over- and under-flow
501 # bins, i.e. off the edges of the histogram
502 nOverflow = len(imgData[imgData > rightEdge])
503 nUnderflow = len(imgData[imgData < leftEdge])
505 # Put v-lines and textboxes in
506 a.axvline(thrUpper, c='k')
507 a.axvline(thrLower, c='k')
508 msg = f"{amp.getName()}\nmean:{mean: .2f}\n$\\sigma$:{sigma: .2f}"
509 a.text(0.65, 0.6, msg, transform=a.transAxes, fontsize=11)
510 msg = f"nLeft:{nLeft}\nnRight:{nRight}\nnOverflow:{nOverflow}\nnUnderflow:{nUnderflow}"
511 a.text(0.03, 0.6, msg, transform=a.transAxes, fontsize=11.5)
513 # set axis limits and scales
514 a.set_ylim([1., 1.7*np.max(y)])
515 lPlot, rPlot = a.get_xlim()
516 a.set_xlim(np.array([lPlot, rPlot]))
517 a.set_yscale('log')
518 a.set_xlabel("ADU/s")
519 fig.show()
520 prompt = "Press Enter or c to continue [chp]..."
521 while True:
522 ans = input(prompt).lower()
523 if ans in ("", " ", "c",):
524 break
525 elif ans in ("p", ):
526 import pdb
527 pdb.set_trace()
528 elif ans in ("h", ):
529 print("[h]elp [c]ontinue [p]db")
530 plt.close()
533class MergeDefectsConnections(pipeBase.PipelineTaskConnections,
534 dimensions=("instrument", "detector")):
535 inputDefects = cT.Input(
536 name="singleExpDefects",
537 doc="Measured defect lists.",
538 storageClass="Defects",
539 dimensions=("instrument", "detector", "exposure"),
540 multiple=True,
541 )
542 camera = cT.PrerequisiteInput(
543 name='camera',
544 doc="Camera associated with these defects.",
545 storageClass="Camera",
546 dimensions=("instrument", ),
547 isCalibration=True,
548 lookupFunction=lookupStaticCalibration,
549 )
551 mergedDefects = cT.Output(
552 name="defects",
553 doc="Final merged defects.",
554 storageClass="Defects",
555 dimensions=("instrument", "detector"),
556 multiple=False,
557 isCalibration=True,
558 )
561class MergeDefectsTaskConfig(pipeBase.PipelineTaskConfig,
562 pipelineConnections=MergeDefectsConnections):
563 """Configuration for merging single exposure defects.
564 """
566 assertSameRun = pexConfig.Field(
567 dtype=bool,
568 doc=("Ensure that all visits are from the same run? Raises if this is not the case, or "
569 "if the run key isn't found."),
570 default=False, # false because most obs_packages don't have runs. obs_lsst/ts8 overrides this.
571 )
572 ignoreFilters = pexConfig.Field(
573 dtype=bool,
574 doc=("Set the filters used in the CALIB_ID to NONE regardless of the filters on the input"
575 " images. Allows mixing of filters in the input flats. Set to False if you think"
576 " your defects might be chromatic and want to have registry support for varying"
577 " defects with respect to filter."),
578 default=True,
579 )
580 nullFilterName = pexConfig.Field(
581 dtype=str,
582 doc=("The name of the null filter if ignoreFilters is True. Usually something like NONE or EMPTY"),
583 default="NONE",
584 )
585 combinationMode = pexConfig.ChoiceField(
586 doc="Which types of defects to identify",
587 dtype=str,
588 default="FRACTION",
589 allowed={
590 "AND": "Logical AND the pixels found in each visit to form set ",
591 "OR": "Logical OR the pixels found in each visit to form set ",
592 "FRACTION": "Use pixels found in more than config.combinationFraction of visits ",
593 }
594 )
595 combinationFraction = pexConfig.RangeField(
596 dtype=float,
597 doc=("The fraction (0..1) of visits in which a pixel was found to be defective across"
598 " the visit list in order to be marked as a defect. Note, upper bound is exclusive, so use"
599 " mode AND to require pixel to appear in all images."),
600 default=0.7,
601 min=0,
602 max=1,
603 )
604 edgesAsDefects = pexConfig.Field(
605 dtype=bool,
606 doc=("Mark all edge pixels, as defined by nPixBorder[UpDown, LeftRight], as defects."
607 " Normal treatment is to simply exclude this region from the defect finding, such that no"
608 " defect will be located there."),
609 default=False,
610 )
613class MergeDefectsTask(pipeBase.PipelineTask):
614 """Merge the defects from multiple exposures.
615 """
617 ConfigClass = MergeDefectsTaskConfig
618 _DefaultName = 'cpDefectMerge'
620 def run(self, inputDefects, camera):
621 """Merge a list of single defects to find the common defect regions.
623 Parameters
624 ----------
625 inputDefects : `list` [`lsst.ip.isr.Defects`]
626 Partial defects from a single exposure.
627 camera : `lsst.afw.cameraGeom.Camera`
628 Camera to use for metadata.
630 Returns
631 -------
632 results : `lsst.pipe.base.Struct`
633 Results struct containing:
635 ``mergedDefects``
636 The defects merged from the input lists
637 (`lsst.ip.isr.Defects`).
638 """
639 detectorId = inputDefects[0].getMetadata().get('DETECTOR', None)
640 if detectorId is None:
641 raise RuntimeError("Cannot identify detector id.")
642 detector = camera[detectorId]
644 imageTypes = set()
645 for inDefect in inputDefects:
646 imageType = inDefect.getMetadata().get('cpDefectGenImageType', 'UNKNOWN')
647 imageTypes.add(imageType)
649 # Determine common defect pixels separately for each input image type.
650 splitDefects = list()
651 for imageType in imageTypes:
652 sumImage = afwImage.MaskedImageF(detector.getBBox())
653 count = 0
654 for inDefect in inputDefects:
655 if imageType == inDefect.getMetadata().get('cpDefectGenImageType', 'UNKNOWN'):
656 count += 1
657 for defect in inDefect:
658 sumImage.image[defect.getBBox()] += 1.0
659 sumImage /= count
660 nDetected = len(np.where(sumImage.getImage().getArray() > 0)[0])
661 self.log.info("Pre-merge %s pixels with non-zero detections for %s" % (nDetected, imageType))
663 if self.config.combinationMode == 'AND':
664 threshold = 1.0
665 elif self.config.combinationMode == 'OR':
666 threshold = 0.0
667 elif self.config.combinationMode == 'FRACTION':
668 threshold = self.config.combinationFraction
669 else:
670 raise RuntimeError(f"Got unsupported combinationMode {self.config.combinationMode}")
671 indices = np.where(sumImage.getImage().getArray() > threshold)
672 BADBIT = sumImage.getMask().getPlaneBitMask('BAD')
673 sumImage.getMask().getArray()[indices] |= BADBIT
674 self.log.info("Post-merge %s pixels marked as defects for %s" % (len(indices[0]), imageType))
675 partialDefect = Defects.fromMask(sumImage, 'BAD')
676 splitDefects.append(partialDefect)
678 # Do final combination of separate image types
679 finalImage = afwImage.MaskedImageF(detector.getBBox())
680 for inDefect in splitDefects:
681 for defect in inDefect:
682 finalImage.image[defect.getBBox()] += 1
683 finalImage /= len(splitDefects)
684 nDetected = len(np.where(finalImage.getImage().getArray() > 0)[0])
685 self.log.info("Pre-final merge %s pixels with non-zero detections" % (nDetected, ))
687 # This combination is the OR of all image types
688 threshold = 0.0
689 indices = np.where(finalImage.getImage().getArray() > threshold)
690 BADBIT = finalImage.getMask().getPlaneBitMask('BAD')
691 finalImage.getMask().getArray()[indices] |= BADBIT
692 self.log.info("Post-final merge %s pixels marked as defects" % (len(indices[0]), ))
694 if self.config.edgesAsDefects:
695 self.log.info("Masking edge pixels as defects.")
696 # Do the same as IsrTask.maskEdges()
697 box = detector.getBBox()
698 subImage = finalImage[box]
699 box.grow(-self.nPixBorder)
700 SourceDetectionTask.setEdgeBits(subImage, box, BADBIT)
702 merged = Defects.fromMask(finalImage, 'BAD')
703 merged.updateMetadata(camera=camera, detector=detector, filterName=None,
704 setCalibId=True, setDate=True)
706 return pipeBase.Struct(
707 mergedDefects=merged,
708 )