Coverage for python/lsst/cp/pipe/defects.py: 18%

271 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-08-30 11:02 +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 

23 

24import lsst.pipe.base as pipeBase 

25import lsst.pipe.base.connectionTypes as cT 

26 

27from lsstDebug import getDebugFrame 

28import lsst.pex.config as pexConfig 

29 

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 

39 

40from ._lookupStaticCalibration import lookupStaticCalibration 

41 

42__all__ = ['MeasureDefectsTaskConfig', 'MeasureDefectsTask', 

43 'MergeDefectsTaskConfig', 'MergeDefectsTask', ] 

44 

45 

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 ) 

63 

64 outputDefects = cT.Output( 

65 name="singleExpDefects", 

66 doc="Output measured defects.", 

67 storageClass="Defects", 

68 dimensions=("instrument", "detector", "exposure"), 

69 ) 

70 

71 

72class MeasureDefectsTaskConfig(pipeBase.PipelineTaskConfig, 

73 pipelineConnections=MeasureDefectsConnections): 

74 """Configuration for measuring defects from a list of exposures 

75 """ 

76 

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 ) 

116 

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.") 

123 

124 

125class MeasureDefectsTask(pipeBase.PipelineTask): 

126 """Measure the defects from one exposure. 

127 """ 

128 

129 ConfigClass = MeasureDefectsTaskConfig 

130 _DefaultName = 'cpDefectMeasure' 

131 

132 def run(self, inputExp, camera): 

133 """Measure one exposure for defects. 

134 

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. 

141 

142 Returns 

143 ------- 

144 results : `lsst.pipe.base.Struct` 

145 Results struct containing: 

146 

147 ``outputDefects`` 

148 The defects measured from this exposure 

149 (`lsst.ip.isr.Defects`). 

150 """ 

151 detector = inputExp.getDetector() 

152 

153 filterName = inputExp.getFilter().physicalLabel 

154 datasetType = inputExp.getMetadata().get('IMGTYPE', 'UNKNOWN') 

155 

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) 

161 

162 msg = "Found %s defects containing %s pixels in %s" 

163 self.log.info(msg, len(defects), self._nPixFromDefects(defects), datasetType) 

164 

165 defects.updateMetadata(camera=camera, detector=detector, filterName=filterName, 

166 setCalibId=True, setDate=True, 

167 cpDefectGenImageType=datasetType) 

168 

169 return pipeBase.Struct( 

170 outputDefects=defects, 

171 ) 

172 

173 @staticmethod 

174 def _nPixFromDefects(defects): 

175 """Count pixels in a defect. 

176 

177 Parameters 

178 ---------- 

179 defects : `lsst.ip.isr.Defects` 

180 Defects to measure. 

181 

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 

191 

192 def findHotAndColdPixels(self, exp, nSigma): 

193 """Find hot and cold pixels in an image. 

194 

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). 

199 

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. 

207 

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 

215 

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 = [] 

220 

221 for amp in exp.getDetector(): 

222 ampImg = maskedIm[amp.getBBox()].clone() 

223 

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] 

235 

236 if self._getNumGoodPixels(ampImg) == 0: # amp contains no usable pixels 

237 continue 

238 

239 # Remove a background estimate 

240 ampImg -= afwMath.makeStatistics(ampImg, afwMath.MEANCLIP, ).getValue() 

241 

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)] 

247 

248 threshold = afwDetection.createThreshold(nSig, 'stdev', polarity=polarity) 

249 

250 footprintSet = afwDetection.FootprintSet(ampImg, threshold) 

251 footprintSet.setMask(maskedIm.mask, ("DETECTED" if polarity else "DETECTED_NEGATIVE")) 

252 

253 if mergedSet is None: 

254 mergedSet = footprintSet 

255 else: 

256 mergedSet.merge(footprintSet) 

257 

258 footprintList += mergedSet.getFootprints() 

259 

260 self.debugView('defectMap', ampImg, 

261 Defects.fromFootprintList(mergedSet.getFootprints()), exp.getDetector()) 

262 

263 defects = Defects.fromFootprintList(footprintList) 

264 defects = self.maskBlocksIfIntermitentBadPixelsInColumn(defects) 

265 

266 return defects 

267 

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 

274 

275 def _setEdgeBits(self, exposureOrMaskedImage, maskplaneToSet='EDGE'): 

276 """Set edge bits on an exposure or maskedImage. 

277 

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}") 

290 

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 

298 

299 def maskBlocksIfIntermitentBadPixelsInColumn(self, defects): 

300 """Mask blocks in a column if there are on-and-off bad pixels 

301 

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. 

305 

306 Parameters 

307 ---------- 

308 defects : `lsst.ip.isr.Defects` 

309 The defects found in the image so far 

310 

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)) 

328 

329 x, y = [], [] 

330 for coordinatePair in coordinates: 

331 x.append(coordinatePair[0]) 

332 y.append(coordinatePair[1]) 

333 

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) 

344 

345 return defects 

346 

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. 

350 

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. 

354 

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 

368 

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 

399 

400 def debugView(self, stepname, ampImage, defects, detector): # pragma: no cover 

401 """Plot the defects found by the task. 

402 

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) 

420 

421 maskedIm = ampImage.clone() 

422 defects.maskPixels(maskedIm, "BAD") 

423 

424 mpDict = maskedIm.mask.getMaskPlaneDict() 

425 for plane in mpDict.keys(): 

426 if plane in ['BAD']: 

427 continue 

428 disp.setMaskPlaneColor(plane, afwDisplay.IGNORE) 

429 

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 

438 

439 def debugHistogram(self, stepname, ampImage, nSigmaUsed, exp): 

440 """Make a histogram of the distribution of pixel values for 

441 each amp. 

442 

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. 

446 

447 Note that this currently only supports the 16-amp LSST 

448 detectors. 

449 

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 

464 

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)) 

469 

470 expTime = exp.getInfo().getVisitInfo().getExposureTime() 

471 

472 for (amp, a) in zip(reversed(detector), ax.flatten()): 

473 mi = exp.maskedImage[amp.getBBox()] 

474 

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() 

484 

485 thrUpper = mean + nSigmaUsed*sigma 

486 thrLower = mean - nSigmaUsed*sigma 

487 

488 nRight = len(imgData[imgData > thrUpper]) 

489 nLeft = len(imgData[imgData < thrLower]) 

490 

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') 

499 

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]) 

504 

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) 

512 

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() 

531 

532 

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 ) 

550 

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 ) 

559 

560 

561class MergeDefectsTaskConfig(pipeBase.PipelineTaskConfig, 

562 pipelineConnections=MergeDefectsConnections): 

563 """Configuration for merging single exposure defects. 

564 """ 

565 

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 ) 

611 

612 

613class MergeDefectsTask(pipeBase.PipelineTask): 

614 """Merge the defects from multiple exposures. 

615 """ 

616 

617 ConfigClass = MergeDefectsTaskConfig 

618 _DefaultName = 'cpDefectMerge' 

619 

620 def run(self, inputDefects, camera): 

621 """Merge a list of single defects to find the common defect regions. 

622 

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. 

629 

630 Returns 

631 ------- 

632 results : `lsst.pipe.base.Struct` 

633 Results struct containing: 

634 

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] 

643 

644 imageTypes = set() 

645 for inDefect in inputDefects: 

646 imageType = inDefect.getMetadata().get('cpDefectGenImageType', 'UNKNOWN') 

647 imageTypes.add(imageType) 

648 

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)) 

662 

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) 

677 

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, )) 

686 

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]), )) 

693 

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) 

701 

702 merged = Defects.fromMask(finalImage, 'BAD') 

703 merged.updateMetadata(camera=camera, detector=detector, filterName=None, 

704 setCalibId=True, setDate=True) 

705 

706 return pipeBase.Struct( 

707 mergedDefects=merged, 

708 )