23 __all__ = [
'FindDefectsTask',
24 'FindDefectsTaskConfig', ]
30 import lsst.pex.config
as pexConfig
42 from .utils
import NonexistentDatasetTaskDataIdContainer, SingleVisitListTaskRunner, countMaskedPixels, \
47 """Config class for defect finding"""
49 isrForFlats = pexConfig.ConfigurableField(
51 doc=
"Task to perform instrumental signature removal",
53 isrForDarks = pexConfig.ConfigurableField(
55 doc=
"Task to perform instrumental signature removal",
57 isrMandatoryStepsFlats = pexConfig.ListField(
59 doc=(
"isr operations that must be performed for valid results when using flats."
60 " Raises if any of these are False"),
61 default=[
'doAssembleCcd',
'doFringe']
63 isrMandatoryStepsDarks = pexConfig.ListField(
65 doc=(
"isr operations that must be performed for valid results when using darks. "
66 "Raises if any of these are False"),
67 default=[
'doAssembleCcd',
'doFringe']
69 isrForbiddenStepsFlats = pexConfig.ListField(
71 doc=(
"isr operations that must NOT be performed for valid results when using flats."
72 " Raises if any of these are True"),
73 default=[
'doBrighterFatter',
'doUseOpticsTransmission',
74 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
76 isrForbiddenStepsDarks = pexConfig.ListField(
78 doc=(
"isr operations that must NOT be performed for valid results when using darks."
79 " Raises if any of these are True"),
80 default=[
'doBrighterFatter',
'doUseOpticsTransmission',
81 'doUseFilterTransmission',
'doUseSensorTransmission',
'doUseAtmosphereTransmission']
83 isrDesirableSteps = pexConfig.ListField(
85 doc=(
"isr operations that it is advisable to perform, but are not mission-critical."
86 " WARNs are logged for any of these found to be False."),
89 ccdKey = pexConfig.Field(
91 doc=
"The key by which to pull a detector from a dataId, e.g. 'ccd' or 'detector'",
94 imageTypeKey = pexConfig.Field(
96 doc=
"The key for the butler to use by which to check whether images are darks or flats",
99 mode = pexConfig.ChoiceField(
100 doc=(
"Use single master calibs (flat and dark) for finding defects, or a list of raw visits?"
101 " If MASTER, a single visit number should be supplied, for which the corresponding master flat"
102 " and dark will be used. If VISITS, the list of visits will be used, treating the flats and "
103 " darks as appropriate, depending on their image types, as determined by their imageType from"
104 " config.imageTypeKey"),
108 "VISITS":
"Calculate defects from a list of raw visits",
109 "MASTER":
"Use the corresponding master calibs from the specified visit to measure defects",
112 nSigmaBright = pexConfig.Field(
114 doc=(
"Number of sigma above mean for bright pixel detection. The default value was found to be",
115 " appropriate for some LSST sensors in DM-17490."),
118 nSigmaDark = pexConfig.Field(
120 doc=(
"Number of sigma below mean for dark pixel detection. The default value was found to be",
121 " appropriate for some LSST sensors in DM-17490."),
124 nPixBorderUpDown = pexConfig.Field(
126 doc=
"Number of pixels to exclude from top & bottom of image when looking for defects.",
129 nPixBorderLeftRight = pexConfig.Field(
131 doc=
"Number of pixels to exclude from left & right of image when looking for defects.",
134 badOnAndOffPixelColumnThreshold = pexConfig.Field(
136 doc=(
"If BPC is the set of all the bad pixels in a given column (not necessarily consecutive) ",
137 "and the size of BPC is at least 'badOnAndOffPixelColumnThreshold', all the pixels between the ",
138 "pixels that satisfy minY (BPC) and maxY (BPC) will be marked as bad, with 'Y' being the long ",
139 "axis of the amplifier (and 'X' the other axis, which for a column is a constant for all ",
140 "pixels in the set BPC). If there are more than 'goodPixelColumnGapThreshold' consecutive ",
141 "non-bad pixels in BPC, an exception to the above is made and those consecutive ",
142 "'goodPixelColumnGapThreshold' are not marked as bad."),
145 goodPixelColumnGapThreshold = pexConfig.Field(
147 doc=(
"Size, in pixels, of usable consecutive pixels in a column with on and off bad pixels (see ",
148 "'badOnAndOffPixelColumnThreshold')."),
151 edgesAsDefects = pexConfig.Field(
153 doc=(
"Mark all edge pixels, as defined by nPixBorder[UpDown, LeftRight], as defects."
154 " Normal treatment is to simply exclude this region from the defect finding, such that no"
155 " defect will be located there."),
158 assertSameRun = pexConfig.Field(
160 doc=(
"Ensure that all visits are from the same run? Raises if this is not the case, or"
161 "if the run key isn't found."),
164 ignoreFilters = pexConfig.Field(
166 doc=(
"Set the filters used in the CALIB_ID to NONE regardless of the filters on the input"
167 " images. Allows mixing of filters in the input flats. Set to False if you think"
168 " your defects might be chromatic and want to have registry support for varying"
169 " defects with respect to filter."),
172 nullFilterName = pexConfig.Field(
174 doc=(
"The name of the null filter if ignoreFilters is True. Usually something like NONE or EMPTY"),
177 combinationMode = pexConfig.ChoiceField(
178 doc=
"Which types of defects to identify",
182 "AND":
"Logical AND the pixels found in each visit to form set ",
183 "OR":
"Logical OR the pixels found in each visit to form set ",
184 "FRACTION":
"Use pixels found in more than config.combinationFraction of visits ",
187 combinationFraction = pexConfig.RangeField(
189 doc=(
"The fraction (0..1) of visits in which a pixel was found to be defective across"
190 " the visit list in order to be marked as a defect. Note, upper bound is exclusive, so use"
191 " mode AND to require pixel to appear in all images."),
196 makePlots = pexConfig.Field(
198 doc=(
"Plot histograms for each visit for each amp (one plot per detector) and the final"
199 " defects overlaid on the sensor."),
202 writeAs = pexConfig.ChoiceField(
203 doc=
"Write the output file as ASCII or FITS table",
207 "ASCII":
"Write the output as an ASCII file",
208 "FITS":
"Write the output as an FITS table",
209 "BOTH":
"Write the output as both a FITS table and an ASCII file",
215 """Task for finding defects in sensors.
217 The task has two modes of operation, defect finding in raws and in
218 master calibrations, which work as follows.
220 Master calib defect finding
221 ----------------------------
223 A single visit number is supplied, for which the corresponding flat & dark
224 will be used. This is because, at present at least, there is no way to pass
225 a calibration exposure ID from the command line to a command line task.
227 The task retrieves the corresponding dark and flat exposures for the
228 supplied visit. If a flat is available the task will (be able to) look
229 for both bright and dark defects. If only a dark is found then only bright
230 defects will be sought.
232 All pixels above/below the specified nSigma which lie with the specified
233 borders for flats/darks are identified as defects.
235 Raw visit defect finding
236 ------------------------
238 A list of exposure IDs are supplied for defect finding. The task will
239 detect bright pixels in the dark frames, if supplied, and bright & dark
240 pixels in the flats, if supplied, i.e. if you only supply darks you will
241 only be given bright defects. This is done automatically from the imageType
242 of the exposure, so the input exposure list can be a mix.
244 As with the master calib detection, all pixels above/below the specified
245 nSigma which lie with the specified borders for flats/darks are identified
246 as defects. Then, a post-processing step is done to merge these detections,
247 with pixels appearing in a fraction [0..1] of the images are kept as defects
248 and those appearing below that occurrence-threshold are discarded.
251 RunnerClass = SingleVisitListTaskRunner
252 ConfigClass = FindDefectsTaskConfig
253 _DefaultName =
"findDefects"
256 pipeBase.CmdLineTask.__init__(self, *args, **kwargs)
257 self.makeSubtask(
"isrForFlats")
258 self.makeSubtask(
"isrForDarks")
261 self.config.isrForbiddenStepsFlats, self.config.isrDesirableSteps)
263 self.config.isrForbiddenStepsDarks, self.config.isrDesirableSteps)
268 def _makeArgumentParser(cls):
269 """Augment argument parser for the FindDefectsTask."""
271 parser.add_argument(
"--visitList", dest=
"visitList", nargs=
"*",
272 help=(
"List of visits to use. Same for each detector."
273 " Uses the normal 0..10:3^234 syntax"))
274 parser.add_id_argument(
"--id", datasetType=
"newDefects",
275 ContainerClass=NonexistentDatasetTaskDataIdContainer,
276 help=
"The ccds to use, e.g. --id ccd=0..100")
281 """Run the defect finding task.
283 Find the defects, as described in the main task docstring, from a
284 dataRef and a list of visit(s).
288 dataRef : `lsst.daf.persistence.ButlerDataRef`
289 dataRef for the detector for the visits to be fit.
290 visitList : `list` [`int`]
291 List of visits to be processed. If config.mode == 'VISITS' then the
292 list of visits is used. If config.mode == 'MASTER' then the length
293 of visitList must be one, and the corresponding master calibrations
298 result : `lsst.pipe.base.Struct`
299 Result struct with Components:
301 - ``defects`` : `lsst.meas.algorithms.Defect`
302 The defects found by the task.
303 - ``exitStatus`` : `int`
307 detNum = dataRef.dataId[self.config.ccdKey]
308 self.log.
info(
"Calculating defects using %s visits for detector %s" % (visitList, detNum))
310 defectLists = {
'dark': [],
'flat': []}
315 if self.config.mode ==
'MASTER':
316 if len(visitList) > 1:
317 raise RuntimeError(f
"Must only specify one visit when using mode MASTER, got {visitList}")
318 dataRef.dataId[
'expId'] = visitList[0]
320 for datasetType
in defectLists.keys():
321 exp = dataRef.get(datasetType)
323 filters.add(exp.getFilter().getName())
326 msg =
"Found %s defects containing %s pixels in master %s"
328 defectLists[datasetType].
append(defects)
329 if self.config.makePlots:
331 defects, datasetType)
332 midTime /= len(defectLists.keys())
334 elif self.config.mode ==
'VISITS':
335 butler = dataRef.getButler()
337 if self.config.assertSameRun:
340 raise RuntimeError(f
"Got data from runs {runs} with assertSameRun==True")
342 for visit
in visitList:
343 imageType = butler.queryMetadata(
'raw', self.config.imageTypeKey, dataId={
'expId': visit})[0]
344 imageType = imageType.lower()
345 dataRef.dataId[
'expId'] = visit
347 if imageType ==
'flat':
348 exp = self.isrForFlats.
runDataRef(dataRef).exposure
350 defectLists[
'flat'].
append(defects)
352 filters.add(exp.getFilter().getName())
354 elif imageType ==
'dark':
355 exp = self.isrForDarks.
runDataRef(dataRef).exposure
357 defectLists[
'dark'].
append(defects)
359 filters.add(exp.getFilter().getName())
362 raise RuntimeError(f
"Failed on imageType {imageType}. Only flats and darks supported")
364 msg =
"Found %s defects containing %s pixels in visit %s"
367 if self.config.makePlots:
370 midTime /= len(visitList)
372 msg =
"Combining %s defect sets from darks for detector %s"
373 self.log.
info(msg, len(defectLists[
'dark']), detNum)
375 self.config.combinationMode)
376 msg =
"Combining %s defect sets from flats for detector %s"
377 self.log.
info(msg, len(defectLists[
'flat']), detNum)
379 self.config.combinationMode)
381 msg =
"Combining bright and dark defect sets for detector %s"
382 self.log.
info(msg, detNum)
383 brightDarkPostMerge = [mergedDefectsFromDarks, mergedDefectsFromFlats]
386 self.
_writeData(dataRef, allDefects, midTime, filters)
388 self.log.
info(
"Finished finding defects in detector %s" % detNum)
389 return pipeBase.Struct(defects=allDefects, exitStatus=0)
391 def _getNsigmaForPlot(self, imageType):
392 assert imageType
in [
'flat',
'dark']
393 nSig = self.config.nSigmaBright
if imageType ==
'flat' else self.config.nSigmaDark
397 def _nPixFromDefects(defect):
398 """Count the number of pixels in a defect object."""
401 nPix += d.getBBox().getArea()
404 def _writeData(self, dataRef, defects, midTime, filters):
405 """Write the data out to the defect file.
409 dataRef : `lsst.daf.persistence.ButlerDataRef`
410 dataRef for the detector for defects to be written.
411 defects : `lsst.meas.algorithms.Defect`
412 The defects to be written.
414 date =
dafBase.DateTime(midTime, dafBase.DateTime.MJD).toPython().isoformat()
419 if not self.config.ignoreFilters:
422 filt = self.config.nullFilterName
424 CALIB_ID = f
"detectorName={detName} detector={detNum} calibDate={date} ccd={detNum} filter={filt}"
426 raftName = detName.split(
"_")[0]
427 CALIB_ID += f
" raftName={raftName}"
431 now = dafBase.DateTime.now().toPython()
432 mdOriginal = defects.getMetadata()
433 mdSupplemental = {
"INSTRUME": instrumentName,
434 "DETECTOR": dataRef.dataId[
'detector'],
436 "CALIB_ID": CALIB_ID,
437 "CALIB_CREATION_DATE": now.date().isoformat(),
438 "CALIB_CREATION_TIME": now.time().isoformat()}
440 mdOriginal.update(mdSupplemental)
444 templateFilename = dataRef.getUri(write=
True)
445 baseDirName = os.path.dirname(templateFilename)
447 dirName = os.path.join(baseDirName, instrumentName,
"defects", detName.lower())
448 if not os.path.exists(dirName):
452 filename = os.path.join(dirName, date)
454 msg =
"Writing defects to %s in format: %s"
455 self.log.
info(msg, os.path.splitext(filename)[0], self.config.writeAs)
456 if self.config.writeAs
in [
'FITS',
'BOTH']:
457 defects.writeFits(filename)
458 if self.config.writeAs
in [
'ASCII',
'BOTH']:
459 wroteTo = defects.writeText(filename)
460 assert(os.path.splitext(wroteTo)[0] == os.path.splitext(filename)[0])
464 def _filterSetToFilterString(filters):
465 return "~".join([f
for f
in filters])
468 def _getDetectorNumber(dataRef):
469 dataRefDetNum = dataRef.dataId[
'detector']
470 camera = dataRef.get(
'camera')
471 detectorDetNum = camera[dataRef.dataId[
'detector']].getId()
472 assert dataRefDetNum == detectorDetNum
476 def _getInstrumentName(dataRef):
477 camera = dataRef.get(
'camera')
478 return camera.getName()
481 def _getDetectorName(dataRef):
482 camera = dataRef.get(
'camera')
483 return camera[dataRef.dataId[
'detector']].getName()
486 def _getMjd(exp, timescale=dafBase.DateTime.UTC):
487 vi = exp.getInfo().getVisitInfo()
488 dateObs = vi.getDate()
489 mjd = dateObs.get(dafBase.DateTime.MJD)
493 def _getRunListFromVisits(butler, visitList):
494 """Return the set of runs for the visits in visitList."""
496 for visit
in visitList:
497 runs.add(butler.queryMetadata(
'raw',
'run', dataId={
'expId': visit})[0])
500 def _postProcessDefectSets(self, defectList, imageDimensions, mode):
501 """Combine a list of defects to make a single defect object.
503 AND, OR or use percentage of visits in which defects appear
508 defectList : `list` [`lsst.meas.algorithms.Defect`]
509 The lList of defects to merge.
510 imageDimensions : `tuple` [`int`]
511 The size of the image.
513 The combination mode to use, either 'AND', 'OR' or 'FRACTION'
517 defects : `lsst.meas.algorithms.Defect`
518 The defect set resulting from the merge.
525 if len(defectList) == 1:
528 sumImage = afwImage.MaskedImageF(imageDimensions)
529 for defects
in defectList:
530 for defect
in defects:
531 sumImage.image[defect.getBBox()] += 1
532 sumImage /= len(defectList)
534 nDetected = len(np.where(sumImage.image.array > 0)[0])
535 self.log.
info(
"Pre-merge %s pixels with non-zero detections" % nDetected)
538 indices = np.where(sumImage.image.array > 0)
542 elif mode ==
'FRACTION':
543 threshold = self.config.combinationFraction
545 raise RuntimeError(f
"Got unsupported combinationMode {mode}")
546 indices = np.where(sumImage.image.array >= threshold)
548 BADBIT = sumImage.mask.getPlaneBitMask(
'BAD')
549 sumImage.mask.array[indices] |= BADBIT
551 self.log.
info(
"Post-merge %s pixels marked as defects" % len(indices[0]))
553 if self.config.edgesAsDefects:
554 self.log.
info(
"Masking edge pixels as defects in addition to previously identified defects")
557 defects = measAlg.Defects.fromMask(sumImage,
'BAD')
561 def _getNumGoodPixels(maskedIm, badMaskString="NO_DATA"):
562 """Return the number of non-bad pixels in the image."""
563 nPixels = maskedIm.mask.array.size
565 return nPixels - nBad
568 """Find hot and cold pixels in an image.
570 Using config-defined thresholds on a per-amp basis, mask pixels
571 that are nSigma above threshold in dark frames (hot pixels),
572 or nSigma away from the clipped mean in flats (hot & cold pixels).
576 exp : `lsst.afw.image.exposure.Exposure`
577 The exposure in which to find defects.
579 The image type, either 'dark' or 'flat'.
581 If true, update exp with hot and cold pixels.
583 cold: DETECTED_NEGATIVE
587 defects : `lsst.meas.algorithms.Defect`
588 The defects found in the image.
590 assert imageType
in [
'flat',
'dark']
593 maskedIm = exp.maskedImage
598 polarities = {
'dark': [
True],
'flat': [
True,
False]}[imageType]
602 for amp
in exp.getDetector():
603 ampImg = maskedIm[amp.getBBox()].
clone()
606 if self.config.nPixBorderLeftRight:
607 if ampImg.getX0() == 0:
608 ampImg = ampImg[self.config.nPixBorderLeftRight:, :, afwImage.LOCAL]
610 ampImg = ampImg[:-self.config.nPixBorderLeftRight, :, afwImage.LOCAL]
611 if self.config.nPixBorderUpDown:
612 if ampImg.getY0() == 0:
613 ampImg = ampImg[:, self.config.nPixBorderUpDown:, afwImage.LOCAL]
615 ampImg = ampImg[:, :-self.config.nPixBorderUpDown, afwImage.LOCAL]
623 for polarity
in polarities:
624 nSig = self.config.nSigmaBright
if polarity
else self.config.nSigmaDark
625 threshold = afwDetection.createThreshold(nSig,
'stdev', polarity=polarity)
627 footprintSet = afwDetection.FootprintSet(ampImg, threshold)
629 footprintSet.setMask(maskedIm.mask, (
"DETECTED" if polarity
else "DETECTED_NEGATIVE"))
631 if mergedSet
is None:
632 mergedSet = footprintSet
634 mergedSet.merge(footprintSet)
636 footprintList += mergedSet.getFootprints()
638 defects = measAlg.Defects.fromFootprintList(footprintList)
644 """Mask blocks in a column if there are on-and-off bad pixels
646 If there's a column with on and off bad pixels, mask all the pixels in between,
647 except if there is a large enough gap of consecutive good pixels between two
648 bad pixels in the column.
652 defects: `lsst.meas.algorithms.Defect`
653 The defects found in the image so far
657 defects: `lsst.meas.algorithms.Defect`
658 If the number of bad pixels in a column is not larger or equal than
659 self.config.badPixelColumnThreshold, the iput list is returned. Otherwise,
660 the defects list returned will include boxes that mask blocks of on-and-of
665 for defect
in defects:
666 bbox = defect.getBBox()
667 x0, y0 = bbox.getMinX(), bbox.getMinY()
668 deltaX0, deltaY0 = bbox.getDimensions()
669 for j
in np.arange(y0, y0+deltaY0):
670 for i
in np.arange(x0, x0 + deltaX0):
671 coordinates.append((i, j))
674 for coordinatePair
in coordinates:
675 x.append(coordinatePair[0])
676 y.append(coordinatePair[1])
681 unique, counts = np.unique(x, return_counts=
True)
683 for (a, b)
in zip(unique, counts):
684 if b >= self.config.badOnAndOffPixelColumnThreshold:
686 if len(multipleX) != 0:
691 def _markBlocksInBadColumn(self, x, y, multipleX, defects):
692 """Mask blocks in a column if number of on-and-off bad pixels is above threshold.
694 This function is called if the number of on-and-off bad pixels in a column
695 is larger or equal than self.config.badOnAndOffPixelColumnThreshold.
700 Lower left x coordinate of defect box. x coordinate is along the short axis if amp.
703 Lower left y coordinate of defect box. x coordinate is along the long axis if amp.
706 List of x coordinates in amp. with multiple bad pixels (i.e., columns with defects).
708 defects: `lsst.meas.algorithms.Defect`
709 The defcts found in the image so far
713 defects: `lsst.meas.algorithms.Defect`
714 The defects list returned that will include boxes that mask blocks
717 goodPixelColumnGapThreshold = self.config.goodPixelColumnGapThreshold
719 index = np.where(x == x0)
721 minY, maxY = np.min(multipleY), np.max(multipleY)
724 diffIndex = np.where(np.diff(multipleY) >= goodPixelColumnGapThreshold)[0]
725 if len(diffIndex) != 0:
727 for gapIndex
in diffIndex:
728 limits.append(multipleY[gapIndex])
729 limits.append(multipleY[gapIndex+1])
731 assert len(limits)%2 == 0,
'limits is even by design, but check anyways'
732 for i
in np.arange(0, len(limits)-1, 2):
742 def _setEdgeBits(self, exposureOrMaskedImage, maskplaneToSet='EDGE'):
743 """Set edge bits on an exposure or maskedImage.
748 Raised if parameter ``exposureOrMaskedImage`` is an invalid type.
751 mi = exposureOrMaskedImage.maskedImage
753 mi = exposureOrMaskedImage
755 t =
type(exposureOrMaskedImage)
756 raise TypeError(f
"Function supports exposure or maskedImage but not {t}")
758 MASKBIT = mi.mask.getPlaneBitMask(maskplaneToSet)
759 if self.config.nPixBorderLeftRight:
760 mi.mask[: self.config.nPixBorderLeftRight, :, afwImage.LOCAL] |= MASKBIT
761 mi.mask[-self.config.nPixBorderLeftRight:, :, afwImage.LOCAL] |= MASKBIT
762 if self.config.nPixBorderUpDown:
763 mi.mask[:, : self.config.nPixBorderUpDown, afwImage.LOCAL] |= MASKBIT
764 mi.mask[:, -self.config.nPixBorderUpDown:, afwImage.LOCAL] |= MASKBIT
766 def _plot(self, dataRef, exp, visit, nSig, defects, imageType):
767 """Plot the defects and pixel histograms.
771 dataRef : `lsst.daf.persistence.ButlerDataRef`
772 dataRef for the detector.
773 exp : `lsst.afw.image.exposure.Exposure`
774 The exposure in which the defects were found.
778 The number of sigma used for detection
779 defects : `lsst.meas.algorithms.Defect`
782 The type of image, either 'dark' or 'flat'.
784 Currently only for LSST sensors. Plots are written to the path
785 given by the butler for the ``cpPipePlotRoot`` dataset type.
787 import matplotlib.pyplot
as plt
788 from matplotlib.backends.backend_pdf
import PdfPages
790 afwDisplay.setDefaultBackend(
"matplotlib")
791 plt.interactive(
False)
793 dirname = dataRef.getUri(datasetType=
'cpPipePlotRoot', write=
True)
794 if not os.path.exists(dirname):
797 detNum = exp.getDetector().getId()
798 nAmps = len(exp.getDetector())
800 if self.config.mode ==
"MASTER":
801 filename = f
"defectPlot_det{detNum}_master-{imageType}_for-exp{visit}.pdf"
802 elif self.config.mode ==
"VISITS":
803 filename = f
"defectPlot_det{detNum}_{imageType}_exp{visit}.pdf"
805 filenameFull = os.path.join(dirname, filename)
807 with warnings.catch_warnings():
808 msg =
"Matplotlib is currently using agg, which is a non-GUI backend, so cannot show the figure."
809 warnings.filterwarnings(
"ignore", message=msg)
810 with PdfPages(filenameFull)
as pdfPages:
817 self.log.
info(
"Wrote plot(s) to %s" % filenameFull)
819 def _plotDefects(self, exp, visit, defects, imageType):
820 """Plot the defects found by the task.
824 exp : `lsst.afw.image.exposure.Exposure`
825 The exposure in which the defects were found.
828 defects : `lsst.meas.algorithms.Defect`
831 The type of image, either 'dark' or 'flat'.
833 expCopy = exp.clone()
835 maskedIm = expCopy.maskedImage
837 defects.maskPixels(expCopy.maskedImage,
"BAD")
838 detector = expCopy.getDetector()
840 disp = afwDisplay.Display(0, reopenPlot=
True, dpi=200)
842 if imageType ==
"flat":
844 ampIm = maskedIm.image[amp.getBBox()]
847 mpDict = maskedIm.mask.getMaskPlaneDict()
848 for plane
in mpDict.keys():
851 disp.setMaskPlaneColor(plane, afwDisplay.IGNORE)
853 disp.scale(
'asinh',
'zscale')
854 disp.setMaskTransparency(80)
855 disp.setMaskPlaneColor(
"BAD", afwDisplay.RED)
857 disp.setImageColormap(
'gray')
858 title = (f
"Detector: {detector.getName()[-3:]} {detector.getSerial()}"
859 f
", Type: {imageType}, visit: {visit}")
860 disp.mtv(maskedIm, title=title)
862 cameraGeom.utils.overlayCcdBoxes(detector, isTrimmed=
True, display=disp)
864 def _plotAmpHistogram(self, dataRef, exp, visit, nSigmaUsed):
866 Make a histogram of the distribution of pixel values for each amp.
868 The main image data histogram is plotted in blue. Edge pixels,
869 if masked, are in red. Note that masked edge pixels do not contribute
870 to the underflow and overflow numbers.
872 Note that this currently only supports the 16-amp LSST detectors.
876 dataRef : `lsst.daf.persistence.ButlerDataRef`
877 dataRef for the detector.
878 exp : `lsst.afw.image.exposure.Exposure`
879 The exposure in which the defects were found.
883 The number of sigma used for detection
885 import matplotlib.pyplot
as plt
887 detector = exp.getDetector()
889 if len(detector) != 16:
890 raise RuntimeError(
"Plotting currently only supported for 16 amp detectors")
891 fig, ax = plt.subplots(nrows=4, ncols=4, sharex=
'col', sharey=
'row', figsize=(13, 10))
893 expTime = exp.getInfo().getVisitInfo().getExposureTime()
895 for (amp, a)
in zip(reversed(detector), ax.flatten()):
896 mi = exp.maskedImage[amp.getBBox()]
899 mi.image.array /= expTime
901 mean, sigma = stats.getValue(afwMath.MEANCLIP), stats.getValue(afwMath.STDEVCLIP)
904 EDGEBIT = exp.maskedImage.mask.getPlaneBitMask(
"EDGE")
905 imgData = mi.image.array[(mi.mask.array & EDGEBIT) == 0].flatten()
906 edgeData = mi.image.array[(mi.mask.array & EDGEBIT) != 0].flatten()
908 thrUpper = mean + nSigmaUsed*sigma
909 thrLower = mean - nSigmaUsed*sigma
911 nRight = len(imgData[imgData > thrUpper])
912 nLeft = len(imgData[imgData < thrLower])
914 nsig = nSigmaUsed + 1.2
915 leftEdge = mean - nsig * nSigmaUsed*sigma
916 rightEdge = mean + nsig * nSigmaUsed*sigma
917 nbins = np.linspace(leftEdge, rightEdge, 1000)
918 ey, bin_borders, patches = a.hist(edgeData, histtype=
'step', bins=nbins, lw=1, edgecolor=
'red')
919 y, bin_borders, patches = a.hist(imgData, histtype=
'step', bins=nbins, lw=3, edgecolor=
'blue')
922 nOverflow = len(imgData[imgData > rightEdge])
923 nUnderflow = len(imgData[imgData < leftEdge])
926 a.axvline(thrUpper, c=
'k')
927 a.axvline(thrLower, c=
'k')
928 msg = f
"{amp.getName()}\nmean:{mean: .2f}\n$\\sigma$:{sigma: .2f}"
929 a.text(0.65, 0.6, msg, transform=a.transAxes, fontsize=11)
930 msg = f
"nLeft:{nLeft}\nnRight:{nRight}\nnOverflow:{nOverflow}\nnUnderflow:{nUnderflow}"
931 a.text(0.03, 0.6, msg, transform=a.transAxes, fontsize=11.5)
934 a.set_ylim([1., 1.7*np.max(y)])
935 lPlot, rPlot = a.get_xlim()
936 a.set_xlim(np.array([lPlot, rPlot]))
938 a.set_xlabel(
"ADU/s")