Coverage for python/lsst/meas/algorithms/dynamicDetection.py: 16%

178 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-13 10:34 +0000

1 

2__all__ = ["DynamicDetectionConfig", "DynamicDetectionTask"] 

3 

4import numpy as np 

5 

6from lsst.pex.config import Field, ConfigurableField 

7from lsst.pipe.base import Struct, NoWorkFound 

8 

9from .detection import SourceDetectionConfig, SourceDetectionTask 

10from .skyObjects import SkyObjectsTask 

11 

12from lsst.afw.detection import FootprintSet 

13from lsst.afw.geom import makeCdMatrix, makeSkyWcs 

14from lsst.afw.table import SourceCatalog, SourceTable 

15from lsst.meas.base import ForcedMeasurementTask 

16 

17import lsst.afw.image 

18import lsst.afw.math 

19import lsst.geom as geom 

20 

21 

22class DynamicDetectionConfig(SourceDetectionConfig): 

23 """Configuration for DynamicDetectionTask 

24 """ 

25 prelimThresholdFactor = Field(dtype=float, default=0.5, 

26 doc="Factor by which to multiply the main detection threshold " 

27 "(thresholdValue) to use for first pass (to find sky objects).") 

28 prelimNegMultiplier = Field(dtype=float, default=2.5, 

29 doc="Multiplier for the negative (relative to positive) polarity " 

30 "detections threshold to use for first pass (to find sky objects).") 

31 skyObjects = ConfigurableField(target=SkyObjectsTask, doc="Generate sky objects.") 

32 doBackgroundTweak = Field(dtype=bool, default=True, 

33 doc="Tweak background level so median PSF flux of sky objects is zero?") 

34 minFractionSources = Field(dtype=float, default=0.02, 

35 doc="Minimum fraction of the requested number of sky sources for dynamic " 

36 "detection to be considered a success. If the number of good sky sources " 

37 "identified falls below this threshold, a NoWorkFound error is raised so " 

38 "that this dataId is no longer considered in downstream processing.") 

39 doBrightPrelimDetection = Field(dtype=bool, default=True, 

40 doc="Do initial bright detection pass where footprints are grown " 

41 "by brightGrowFactor?") 

42 brightMultiplier = Field(dtype=float, default=2000.0, 

43 doc="Multiplier to apply to the prelimThresholdFactor for the " 

44 "\"bright\" detections stage (want this to be large to only " 

45 "detect the brightest sources).") 

46 brightNegFactor = Field(dtype=float, default=2.2, 

47 doc="Factor by which to multiply the threshold for the negative polatiry " 

48 "detections for the \"bright\" detections stage (this needs to be fairly " 

49 "low given the nature of the negative polarity detections in the very " 

50 "large positive polarity threshold).") 

51 brightGrowFactor = Field(dtype=int, default=40, 

52 doc="Factor by which to grow the footprints of sources detected in the " 

53 "\"bright\" detections stage (want this to be large to mask wings of " 

54 "bright sources).") 

55 brightMaskFractionMax = Field(dtype=float, default=0.95, 

56 doc="Maximum allowed fraction of masked pixes from the \"bright\" " 

57 "detection stage (to mask regions unsuitable for sky sourcess). " 

58 "If this fraction is exeeded, the detection threshold for this stage " 

59 "will be increased by bisectFactor until the fraction of masked " 

60 "pixels drops below this threshold.") 

61 bisectFactor = Field(dtype=float, default=1.2, 

62 doc="Factor by which to increase thresholds in brightMaskFractionMax loop.") 

63 

64 def setDefaults(self): 

65 SourceDetectionConfig.setDefaults(self) 

66 self.skyObjects.nSources = 1000 # For good statistics 

67 for maskStr in ["SAT"]: 

68 if maskStr not in self.skyObjects.avoidMask: 

69 self.skyObjects.avoidMask.append(maskStr) 

70 

71 

72class DynamicDetectionTask(SourceDetectionTask): 

73 """Detection of sources on an image with a dynamic threshold 

74 

75 We first detect sources using a lower threshold than normal (see config 

76 parameter ``prelimThresholdFactor``) in order to identify good sky regions 

77 (configurable ``skyObjects``). Then we perform forced PSF photometry on 

78 those sky regions. Using those PSF flux measurements and estimated errors, 

79 we set the threshold so that the stdev of the measurements matches the 

80 median estimated error. 

81 

82 Besides the usual initialisation of configurables, we also set up 

83 the forced measurement which is deliberately not represented in 

84 this Task's configuration parameters because we're using it as 

85 part of the algorithm and we don't want to allow it to be modified. 

86 """ 

87 ConfigClass = DynamicDetectionConfig 

88 _DefaultName = "dynamicDetection" 

89 

90 def __init__(self, *args, **kwargs): 

91 

92 SourceDetectionTask.__init__(self, *args, **kwargs) 

93 self.makeSubtask("skyObjects") 

94 

95 # Set up forced measurement. 

96 config = ForcedMeasurementTask.ConfigClass() 

97 config.plugins.names = ['base_TransformedCentroid', 'base_PsfFlux', 'base_LocalBackground'] 

98 # We'll need the "centroid" and "psfFlux" slots 

99 for slot in ("shape", "psfShape", "apFlux", "modelFlux", "gaussianFlux", "calibFlux"): 

100 setattr(config.slots, slot, None) 

101 config.copyColumns = {} 

102 self.skySchema = SourceTable.makeMinimalSchema() 

103 self.skyMeasurement = ForcedMeasurementTask(config=config, name="skyMeasurement", parentTask=self, 

104 refSchema=self.skySchema) 

105 

106 def calculateThreshold(self, exposure, seed, sigma=None, minFractionSourcesFactor=1.0, isBgTweak=False): 

107 """Calculate new threshold 

108 

109 This is the main functional addition to the vanilla 

110 `SourceDetectionTask`. 

111 

112 We identify sky objects and perform forced PSF photometry on 

113 them. Using those PSF flux measurements and estimated errors, 

114 we set the threshold so that the stdev of the measurements 

115 matches the median estimated error. 

116 

117 Parameters 

118 ---------- 

119 exposure : `lsst.afw.image.Exposure` 

120 Exposure on which we're detecting sources. 

121 seed : `int` 

122 RNG seed to use for finding sky objects. 

123 sigma : `float`, optional 

124 Gaussian sigma of smoothing kernel; if not provided, 

125 will be deduced from the exposure's PSF. 

126 minFractionSourcesFactor : `float` 

127 Change the fraction of required sky sources from that set in 

128 ``self.config.minFractionSources`` by this factor. NOTE: this 

129 is intended for use in the background tweak pass (the detection 

130 threshold is much lower there, so many more pixels end up marked 

131 as DETECTED or DETECTED_NEGATIVE, leaving less room for sky 

132 object placement). 

133 isBgTweak : `bool` 

134 Set to ``True`` for the background tweak pass (for more helpful 

135 log messages). 

136 

137 Returns 

138 ------- 

139 result : `lsst.pipe.base.Struct` 

140 Result struct with components: 

141 

142 ``multiplicative`` 

143 Multiplicative factor to be applied to the 

144 configured detection threshold (`float`). 

145 ``additive`` 

146 Additive factor to be applied to the background 

147 level (`float`). 

148 

149 Raises 

150 ------ 

151 NoWorkFound 

152 Raised if the number of good sky sources found is less than the 

153 minimum fraction 

154 (``self.config.minFractionSources``*``minFractionSourcesFactor``) 

155 of the number requested (``self.skyObjects.config.nSources``). 

156 """ 

157 wcsIsNone = exposure.getWcs() is None 

158 if wcsIsNone: # create a dummy WCS as needed by ForcedMeasurementTask 

159 self.log.info("WCS for exposure is None. Setting a dummy WCS for dynamic detection.") 

160 exposure.setWcs(makeSkyWcs(crpix=geom.Point2D(0, 0), 

161 crval=geom.SpherePoint(0, 0, geom.degrees), 

162 cdMatrix=makeCdMatrix(scale=1e-5*geom.degrees))) 

163 fp = self.skyObjects.run(exposure.maskedImage.mask, seed) 

164 skyFootprints = FootprintSet(exposure.getBBox()) 

165 skyFootprints.setFootprints(fp) 

166 table = SourceTable.make(self.skyMeasurement.schema) 

167 catalog = SourceCatalog(table) 

168 catalog.reserve(len(skyFootprints.getFootprints())) 

169 skyFootprints.makeSources(catalog) 

170 key = catalog.getCentroidSlot().getMeasKey() 

171 for source in catalog: 

172 peaks = source.getFootprint().getPeaks() 

173 assert len(peaks) == 1 

174 source.set(key, peaks[0].getF()) 

175 # Coordinate covariance is not used, so don't bother calulating it. 

176 source.updateCoord(exposure.getWcs(), include_covariance=False) 

177 

178 # Forced photometry on sky objects 

179 self.skyMeasurement.run(catalog, exposure, catalog, exposure.getWcs()) 

180 

181 # Calculate new threshold 

182 fluxes = catalog["base_PsfFlux_instFlux"] 

183 area = catalog["base_PsfFlux_area"] 

184 bg = catalog["base_LocalBackground_instFlux"] 

185 

186 good = (~catalog["base_PsfFlux_flag"] & ~catalog["base_LocalBackground_flag"] 

187 & np.isfinite(fluxes) & np.isfinite(area) & np.isfinite(bg)) 

188 

189 minNumSources = int(self.config.minFractionSources*self.skyObjects.config.nSources) 

190 # Reduce the number of sky sources required if requested, but ensure 

191 # a minumum of 3. 

192 if minFractionSourcesFactor != 1.0: 

193 minNumSources = max(3, int(minNumSources*minFractionSourcesFactor)) 

194 if good.sum() < minNumSources: 

195 if not isBgTweak: 

196 msg = (f"Insufficient good sky source flux measurements ({good.sum()} < " 

197 f"{minNumSources}) for dynamic threshold calculation.") 

198 else: 

199 msg = (f"Insufficient good sky source flux measurements ({good.sum()} < " 

200 f"{minNumSources}) for background tweak calculation.") 

201 

202 nPix = exposure.mask.array.size 

203 badPixelMask = lsst.afw.image.Mask.getPlaneBitMask(["NO_DATA", "BAD"]) 

204 nGoodPix = np.sum(exposure.mask.array & badPixelMask == 0) 

205 if nGoodPix/nPix > 0.2: 

206 detectedPixelMask = lsst.afw.image.Mask.getPlaneBitMask(["DETECTED", "DETECTED_NEGATIVE"]) 

207 nDetectedPix = np.sum(exposure.mask.array & detectedPixelMask != 0) 

208 msg += (f" However, {nGoodPix}/{nPix} pixels are not marked NO_DATA or BAD, " 

209 "so there should be sufficient area to locate suitable sky sources. " 

210 f"Note that {nDetectedPix} of {nGoodPix} \"good\" pixels were marked " 

211 "as DETECTED or DETECTED_NEGATIVE.") 

212 raise RuntimeError(msg) 

213 raise NoWorkFound(msg) 

214 

215 if not isBgTweak: 

216 self.log.info("Number of good sky sources used for dynamic detection: %d (of %d requested).", 

217 good.sum(), self.skyObjects.config.nSources) 

218 else: 

219 self.log.info("Number of good sky sources used for dynamic detection background tweak:" 

220 " %d (of %d requested).", good.sum(), self.skyObjects.config.nSources) 

221 bgMedian = np.median((fluxes/area)[good]) 

222 

223 lq, uq = np.percentile((fluxes - bg*area)[good], [25.0, 75.0]) 

224 stdevMeas = 0.741*(uq - lq) 

225 medianError = np.median(catalog["base_PsfFlux_instFluxErr"][good]) 

226 if wcsIsNone: 

227 exposure.setWcs(None) 

228 return Struct(multiplicative=medianError/stdevMeas, additive=bgMedian) 

229 

230 def detectFootprints(self, exposure, doSmooth=True, sigma=None, clearMask=True, expId=None, 

231 background=None): 

232 """Detect footprints with a dynamic threshold 

233 

234 This varies from the vanilla ``detectFootprints`` method because we 

235 do detection three times: first with a high threshold to detect 

236 "bright" (both positive and negative, the latter to identify very 

237 over-subtracted regions) sources for which we grow the DETECTED and 

238 DETECTED_NEGATIVE masks significantly to account for wings. Second, 

239 with a low threshold to mask all non-empty regions of the image. These 

240 two masks are combined and used to identify regions of sky 

241 uncontaminated by objects. A final round of detection is then done 

242 with the new calculated threshold. 

243 

244 Parameters 

245 ---------- 

246 exposure : `lsst.afw.image.Exposure` 

247 Exposure to process; DETECTED{,_NEGATIVE} mask plane will be 

248 set in-place. 

249 doSmooth : `bool`, optional 

250 If True, smooth the image before detection using a Gaussian 

251 of width ``sigma``. 

252 sigma : `float`, optional 

253 Gaussian Sigma of PSF (pixels); used for smoothing and to grow 

254 detections; if `None` then measure the sigma of the PSF of the 

255 ``exposure``. 

256 clearMask : `bool`, optional 

257 Clear both DETECTED and DETECTED_NEGATIVE planes before running 

258 detection. 

259 expId : `int`, optional 

260 Exposure identifier, used as a seed for the random number 

261 generator. If absent, the seed will be the sum of the image. 

262 background : `lsst.afw.math.BackgroundList`, optional 

263 Background that was already subtracted from the exposure; will be 

264 modified in-place if ``reEstimateBackground=True``. 

265 

266 Returns 

267 ------- 

268 resutls : `lsst.pipe.base.Struct` 

269 The results `~lsst.pipe.base.Struct` contains: 

270 

271 ``positive`` 

272 Positive polarity footprints. 

273 (`lsst.afw.detection.FootprintSet` or `None`) 

274 ``negative`` 

275 Negative polarity footprints. 

276 (`lsst.afw.detection.FootprintSet` or `None`) 

277 ``numPos`` 

278 Number of footprints in positive or 0 if detection polarity was 

279 negative. (`int`) 

280 ``numNeg`` 

281 Number of footprints in negative or 0 if detection polarity was 

282 positive. (`int`) 

283 ``background`` 

284 Re-estimated background. `None` or the input ``background`` 

285 if ``reEstimateBackground==False``. 

286 (`lsst.afw.math.BackgroundList`) 

287 ``factor`` 

288 Multiplication factor applied to the configured detection 

289 threshold. (`float`) 

290 ``prelim`` 

291 Results from preliminary detection pass. 

292 (`lsst.pipe.base.Struct`) 

293 """ 

294 maskedImage = exposure.maskedImage 

295 

296 if clearMask: 

297 self.clearMask(maskedImage.mask) 

298 else: 

299 oldDetected = maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(["DETECTED", 

300 "DETECTED_NEGATIVE"]) 

301 nPix = maskedImage.mask.array.size 

302 badPixelMask = lsst.afw.image.Mask.getPlaneBitMask(["NO_DATA", "BAD"]) 

303 nGoodPix = np.sum(maskedImage.mask.array & badPixelMask == 0) 

304 self.log.info("Number of good data pixels (i.e. not NO_DATA or BAD): {} ({:.1f}% of total)". 

305 format(nGoodPix, 100*nGoodPix/nPix)) 

306 

307 with self.tempWideBackgroundContext(exposure): 

308 # Could potentially smooth with a wider kernel than the PSF in 

309 # order to better pick up the wings of stars and galaxies, but for 

310 # now sticking with the PSF as that's more simple. 

311 psf = self.getPsf(exposure, sigma=sigma) 

312 convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth) 

313 

314 if self.config.doBrightPrelimDetection: 

315 brightDetectedMask = self._computeBrightDetectionMask(maskedImage, convolveResults) 

316 

317 middle = convolveResults.middle 

318 sigma = convolveResults.sigma 

319 prelim = self.applyThreshold( 

320 middle, maskedImage.getBBox(), factor=self.config.prelimThresholdFactor, 

321 factorNeg=self.config.prelimNegMultiplier*self.config.prelimThresholdFactor 

322 ) 

323 self.finalizeFootprints( 

324 maskedImage.mask, prelim, sigma, factor=self.config.prelimThresholdFactor, 

325 factorNeg=self.config.prelimNegMultiplier*self.config.prelimThresholdFactor 

326 ) 

327 if self.config.doBrightPrelimDetection: 

328 # Combine prelim and bright detection masks for multiplier 

329 # determination. 

330 maskedImage.mask.array |= brightDetectedMask 

331 

332 # Calculate the proper threshold 

333 # seed needs to fit in a C++ 'int' so pybind doesn't choke on it 

334 seed = (expId if expId is not None else int(maskedImage.image.array.sum())) % (2**31 - 1) 

335 threshResults = self.calculateThreshold(exposure, seed, sigma=sigma) 

336 factor = threshResults.multiplicative 

337 self.log.info("Modifying configured detection threshold by factor %f to %f", 

338 factor, factor*self.config.thresholdValue) 

339 

340 # Blow away preliminary (low threshold) detection mask 

341 self.clearMask(maskedImage.mask) 

342 if not clearMask: 

343 maskedImage.mask.array |= oldDetected 

344 

345 # Rinse and repeat thresholding with new calculated threshold 

346 results = self.applyThreshold(middle, maskedImage.getBBox(), factor) 

347 results.prelim = prelim 

348 results.background = background if background is not None else lsst.afw.math.BackgroundList() 

349 if self.config.doTempLocalBackground: 

350 self.applyTempLocalBackground(exposure, middle, results) 

351 self.finalizeFootprints(maskedImage.mask, results, sigma, factor=factor) 

352 

353 self.clearUnwantedResults(maskedImage.mask, results) 

354 

355 if self.config.reEstimateBackground: 

356 self.reEstimateBackground(maskedImage, results.background) 

357 

358 self.display(exposure, results, middle) 

359 

360 if self.config.doBackgroundTweak: 

361 # Re-do the background tweak after any temporary backgrounds have 

362 # been restored. 

363 # 

364 # But we want to keep any large-scale background (e.g., scattered 

365 # light from bright stars) from being selected for sky objects in 

366 # the calculation, so do another detection pass without either the 

367 # local or wide temporary background subtraction; the DETECTED 

368 # pixels will mark the area to ignore. 

369 originalMask = maskedImage.mask.array.copy() 

370 try: 

371 self.clearMask(exposure.mask) 

372 convolveResults = self.convolveImage(maskedImage, psf, doSmooth=doSmooth) 

373 tweakDetResults = self.applyThreshold(convolveResults.middle, maskedImage.getBBox(), factor) 

374 self.finalizeFootprints(maskedImage.mask, tweakDetResults, sigma, factor=factor) 

375 bgLevel = self.calculateThreshold(exposure, seed, sigma=sigma, minFractionSourcesFactor=0.5, 

376 isBgTweak=True).additive 

377 finally: 

378 maskedImage.mask.array[:] = originalMask 

379 self.tweakBackground(exposure, bgLevel, results.background) 

380 

381 return results 

382 

383 def tweakBackground(self, exposure, bgLevel, bgList=None): 

384 """Modify the background by a constant value 

385 

386 Parameters 

387 ---------- 

388 exposure : `lsst.afw.image.Exposure` 

389 Exposure for which to tweak background. 

390 bgLevel : `float` 

391 Background level to remove 

392 bgList : `lsst.afw.math.BackgroundList`, optional 

393 List of backgrounds to append to. 

394 

395 Returns 

396 ------- 

397 bg : `lsst.afw.math.BackgroundMI` 

398 Constant background model. 

399 """ 

400 self.log.info("Tweaking background by %f to match sky photometry", bgLevel) 

401 exposure.image -= bgLevel 

402 bgStats = lsst.afw.image.MaskedImageF(1, 1) 

403 bgStats.set(bgLevel, 0, bgLevel) 

404 bg = lsst.afw.math.BackgroundMI(exposure.getBBox(), bgStats) 

405 bgData = (bg, lsst.afw.math.Interpolate.LINEAR, lsst.afw.math.REDUCE_INTERP_ORDER, 

406 lsst.afw.math.ApproximateControl.UNKNOWN, 0, 0, False) 

407 if bgList is not None: 

408 bgList.append(bgData) 

409 return bg 

410 

411 def _computeBrightDetectionMask(self, maskedImage, convolveResults): 

412 """Perform an initial bright source detection pass. 

413 

414 Perform an initial bright object detection pass using a high detection 

415 threshold. The footprints in this pass are grown significantly more 

416 than is typical to account for wings around bright sources. The 

417 negative polarity detections in this pass help in masking severely 

418 over-subtracted regions. 

419 

420 A maximum fraction of masked pixel from this pass is ensured via 

421 the config ``brightMaskFractionMax``. If the masked pixel fraction is 

422 above this value, the detection thresholds here are increased by 

423 ``bisectFactor`` in a while loop until the detected masked fraction 

424 falls below this value. 

425 

426 Parameters 

427 ---------- 

428 maskedImage : `lsst.afw.image.MaskedImage` 

429 Masked image on which to run the detection. 

430 convolveResults : `lsst.pipe.base.Struct` 

431 The results of the self.convolveImage function with attributes: 

432 

433 ``middle`` 

434 Convolved image, without the edges 

435 (`lsst.afw.image.MaskedImage`). 

436 ``sigma`` 

437 Gaussian sigma used for the convolution (`float`). 

438 

439 Returns 

440 ------- 

441 brightDetectedMask : `numpy.ndarray` 

442 Boolean array representing the union of the bright detection pass 

443 DETECTED and DETECTED_NEGATIVE masks. 

444 """ 

445 # Initialize some parameters. 

446 brightPosFactor = ( 

447 self.config.prelimThresholdFactor*self.config.brightMultiplier/self.config.bisectFactor 

448 ) 

449 brightNegFactor = self.config.brightNegFactor/self.config.bisectFactor 

450 nPix = 1 

451 nPixDet = 1 

452 nPixDetNeg = 1 

453 brightMaskFractionMax = self.config.brightMaskFractionMax 

454 

455 # Loop until masked fraction is smaller than 

456 # brightMaskFractionMax, increasing the thresholds by 

457 # config.bisectFactor on each iteration (rarely necessary 

458 # for current defaults). 

459 while nPixDetNeg/nPix > brightMaskFractionMax or nPixDet/nPix > brightMaskFractionMax: 

460 self.clearMask(maskedImage.mask) 

461 brightPosFactor *= self.config.bisectFactor 

462 brightNegFactor *= self.config.bisectFactor 

463 prelimBright = self.applyThreshold(convolveResults.middle, maskedImage.getBBox(), 

464 factor=brightPosFactor, factorNeg=brightNegFactor) 

465 self.finalizeFootprints( 

466 maskedImage.mask, prelimBright, convolveResults.sigma*self.config.brightGrowFactor, 

467 factor=brightPosFactor, factorNeg=brightNegFactor 

468 ) 

469 # Check that not too many pixels got masked. 

470 nPix = maskedImage.mask.array.size 

471 nPixDet = countMaskedPixels(maskedImage, "DETECTED") 

472 self.log.info("Number (%) of bright DETECTED pix: {} ({:.1f}%)". 

473 format(nPixDet, 100*nPixDet/nPix)) 

474 nPixDetNeg = countMaskedPixels(maskedImage, "DETECTED_NEGATIVE") 

475 self.log.info("Number (%) of bright DETECTED_NEGATIVE pix: {} ({:.1f}%)". 

476 format(nPixDetNeg, 100*nPixDetNeg/nPix)) 

477 if nPixDetNeg/nPix > brightMaskFractionMax or nPixDet/nPix > brightMaskFractionMax: 

478 self.log.warn("Too high a fraction (%.1f > %.1f) of pixels were masked with current " 

479 "\"bright\" detection round thresholds. Increasing by a factor of %f " 

480 "and trying again.", max(nPixDetNeg, nPixDet)/nPix, 

481 brightMaskFractionMax, self.config.bisectFactor) 

482 

483 # Save the mask planes from the "bright" detection round, then 

484 # clear them before moving on to the "prelim" detection phase. 

485 brightDetectedMask = (maskedImage.mask.array 

486 & maskedImage.mask.getPlaneBitMask(["DETECTED", "DETECTED_NEGATIVE"])) 

487 self.clearMask(maskedImage.mask) 

488 return brightDetectedMask 

489 

490 

491def countMaskedPixels(maskedIm, maskPlane): 

492 """Count the number of pixels in a given mask plane. 

493 

494 Parameters 

495 ---------- 

496 maskedIm : `lsst.afw.image.MaskedImage` 

497 Masked image to examine. 

498 maskPlane : `str` 

499 Name of the mask plane to examine. 

500 

501 Returns 

502 ------- 

503 nPixMasked : `int` 

504 Number of pixels with ``maskPlane`` bit set. 

505 """ 

506 maskBit = maskedIm.mask.getPlaneBitMask(maskPlane) 

507 nPixMasked = np.sum(np.bitwise_and(maskedIm.mask.array, maskBit))/maskBit 

508 return nPixMasked