Coverage for python/lsst/meas/extensions/shapeHSM/_hsm_moments.py: 28%

203 statements  

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

1# This file is part of meas_extensions_shapeHSM. 

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 logging 

23 

24import galsim 

25import lsst.afw.geom as afwGeom 

26import lsst.afw.image as afwImage 

27import lsst.afw.math as afwMath 

28import lsst.afw.table as afwTable 

29import lsst.meas.base as measBase 

30import lsst.pex.config as pexConfig 

31import numpy as np 

32from lsst.geom import Box2I, Point2D, Point2I 

33from lsst.pex.exceptions import InvalidParameterError, NotFoundError 

34 

35__all__ = [ 

36 "HsmSourceMomentsConfig", 

37 "HsmSourceMomentsPlugin", 

38 "HsmSourceMomentsRoundConfig", 

39 "HsmSourceMomentsRoundPlugin", 

40 "HsmPsfMomentsConfig", 

41 "HsmPsfMomentsPlugin", 

42 "HsmPsfMomentsDebiasedConfig", 

43 "HsmPsfMomentsDebiasedPlugin", 

44] 

45 

46 

47class HsmMomentsConfig(measBase.SingleFramePluginConfig): 

48 """Base configuration for HSM adaptive moments measurement.""" 

49 

50 roundMoments = pexConfig.Field[bool](doc="Use round weight function?", default=False) 

51 addFlux = pexConfig.Field[bool](doc="Store measured flux?", default=False) 

52 subtractCenter = pexConfig.Field[bool](doc="Subtract starting center from x/y outputs?", default=False) 

53 

54 

55class HsmMomentsPlugin(measBase.SingleFramePlugin): 

56 """Base plugin for HSM adaptive moments measurement.""" 

57 

58 ConfigClass = HsmMomentsConfig 

59 

60 def __init__(self, config, name, schema, metadata, logName=None): 

61 if logName is None: 

62 logName = __name__ 

63 super().__init__(config, name, schema, metadata, logName=logName) 

64 

65 # Define flags for possible issues that might arise during measurement. 

66 flagDefs = measBase.FlagDefinitionList() 

67 self.FAILURE = flagDefs.addFailureFlag("General failure flag, set if anything went wrong") 

68 self.NO_PIXELS = flagDefs.add("flag_no_pixels", "No pixels to measure") 

69 self.NOT_CONTAINED = flagDefs.add( 

70 "flag_not_contained", "Center not contained in footprint bounding box" 

71 ) 

72 self.PARENT_SOURCE = flagDefs.add("flag_parent_source", "Parent source, ignored") 

73 self.GALSIM = flagDefs.add("flag_galsim", "GalSim failure") 

74 self.INVALID_PARAM = flagDefs.add("flag_invalid_param", "Invalid combination of moments") 

75 self.EDGE = flagDefs.add("flag_edge", "Variance undefined outside image edge") 

76 self.NO_PSF = flagDefs.add("flag_no_psf", "Exposure lacks PSF") 

77 

78 # Embed the flag definitions in the schema using a flag handler. 

79 self.flagHandler = measBase.FlagHandler.addFields(schema, name, flagDefs) 

80 

81 # Utilize a safe centroid extractor that uses the detection footprint 

82 # as a fallback if necessary. 

83 self.centroidExtractor = measBase.SafeCentroidExtractor(schema, name) 

84 self.log = logging.getLogger(self.logName) 

85 

86 @classmethod 

87 def getExecutionOrder(cls): 

88 return cls.SHAPE_ORDER 

89 

90 def _calculate( 

91 self, 

92 record: afwTable.SourceRecord, 

93 *, 

94 image: galsim.Image, 

95 weight_image: galsim.Image, 

96 centroid: Point2D, 

97 sigma: float = 5.0, 

98 precision: float = 1.0e-6, 

99 ) -> None: 

100 """ 

101 Calculate adaptive moments using GalSim's HSM and modify the record in 

102 place. 

103 

104 Parameters 

105 ---------- 

106 record : `~lsst.afw.table.SourceRecord` 

107 Record to store measurements. 

108 image : `~galsim.Image` 

109 Image on which to perform measurements. 

110 weight_image : `~galsim.Image` 

111 The combined badpix/weight image for input to galsim HSM code. 

112 centroid : `~lsst.geom.Point2D` 

113 Centroid guess for HSM adaptive moments. 

114 sigma : `float`, optional 

115 Estimate of object's Gaussian sigma in pixels. Default is 5.0. 

116 precision : `float`, optional 

117 Precision for HSM adaptive moments. Default is 1.0e-6. 

118 

119 Raises 

120 ------ 

121 MeasurementError 

122 Raised for errors in measurement. 

123 """ 

124 # Convert centroid to GalSim's PositionD type. 

125 guessCentroid = galsim._PositionD(centroid.x, centroid.y) 

126 try: 

127 # Attempt to compute HSM moments. 

128 

129 # Use galsim c++/python interface directly. 

130 shape = galsim.hsm.ShapeData( 

131 image_bounds=galsim._BoundsI(0, 0, 1, 1), 

132 observed_shape=galsim._Shear(0j), 

133 psf_shape=galsim._Shear(0j), 

134 moments_centroid=galsim._PositionD(0, 0), 

135 ) 

136 hsmparams = galsim.hsm.HSMParams.default 

137 

138 # TODO: DM-42047 Change to public API when an optimized 

139 # version is available. 

140 galsim._galsim.FindAdaptiveMomView( 

141 shape._data, 

142 image._image, 

143 weight_image._image, 

144 float(sigma), 

145 float(precision), 

146 guessCentroid._p, 

147 bool(self.config.roundMoments), 

148 hsmparams._hsmp, 

149 ) 

150 

151 except RuntimeError as error: 

152 raise measBase.MeasurementError(str(error), self.GALSIM.number) 

153 

154 # Retrieve computed moments sigma and centroid. 

155 determinantRadius = shape.moments_sigma 

156 centroidResult = shape.moments_centroid 

157 

158 # Subtract center if required by configuration. 

159 if self.config.subtractCenter: 

160 centroidResult.x -= centroid.getX() 

161 centroidResult.y -= centroid.getY() 

162 

163 # Convert GalSim's `galsim.PositionD` to `lsst.geom.Point2D`. 

164 centroidResult = Point2D(centroidResult.x, centroidResult.y) 

165 

166 # Populate the record with the centroid results. 

167 record.set(self.centroidResultKey, centroidResult) 

168 

169 # Convert GalSim measurements to lsst measurements. 

170 try: 

171 # Create an ellipse for the shape. 

172 observed_shape = shape.observed_shape 

173 ellipse = afwGeom.ellipses.SeparableDistortionDeterminantRadius( 

174 e1=observed_shape.e1, 

175 e2=observed_shape.e2, 

176 radius=determinantRadius, 

177 normalize=True, # Fail if |e|>1. 

178 ) 

179 # Get the quadrupole moments from the ellipse. 

180 quad = afwGeom.ellipses.Quadrupole(ellipse) 

181 except InvalidParameterError as error: 

182 raise measBase.MeasurementError(error, self.INVALID_PARAM.number) 

183 

184 # Store the quadrupole moments in the record. 

185 record.set(self.shapeKey, quad) 

186 

187 # Store the flux if required by configuration. 

188 if self.config.addFlux: 

189 record.set(self.fluxKey, shape.moments_amp) 

190 

191 def fail(self, record, error=None): 

192 # Docstring inherited. 

193 self.flagHandler.handleFailure(record) 

194 if error: 

195 centroid = self.centroidExtractor(record, self.flagHandler) 

196 self.log.debug( 

197 "Failed to measure shape for %d at (%f, %f): %s", 

198 record.getId(), 

199 centroid.getX(), 

200 centroid.getY(), 

201 error, 

202 ) 

203 

204 

205class HsmSourceMomentsConfig(HsmMomentsConfig): 

206 """Configuration for HSM adaptive moments measurement for sources.""" 

207 

208 badMaskPlanes = pexConfig.ListField[str]( 

209 doc="Mask planes used to reject bad pixels.", default=["BAD", "SAT"] 

210 ) 

211 

212 

213@measBase.register("ext_shapeHSM_HsmSourceMoments") 

214class HsmSourceMomentsPlugin(HsmMomentsPlugin): 

215 """Plugin for HSM adaptive moments measurement for sources.""" 

216 

217 ConfigClass = HsmSourceMomentsConfig 

218 

219 def __init__(self, config, name, schema, metadata, logName=None): 

220 super().__init__(config, name, schema, metadata, logName=logName) 

221 self.centroidResultKey = afwTable.Point2DKey.addFields( 

222 schema, name, "Centroid of the source via the HSM shape algorithm", "pixel" 

223 ) 

224 self.shapeKey = afwTable.QuadrupoleKey.addFields( 

225 schema, 

226 name, 

227 "Adaptive moments of the source via the HSM shape algorithm", 

228 afwTable.CoordinateType.PIXEL, 

229 ) 

230 if config.addFlux: 

231 self.fluxKey = schema.addField( 

232 schema.join(name, "Flux"), type=float, doc="Flux of the source via the HSM shape algorithm" 

233 ) 

234 

235 def measure(self, record, exposure): 

236 """ 

237 Measure adaptive moments of sources given an exposure and set the 

238 results in the record in place. 

239 

240 Parameters 

241 ---------- 

242 record : `~lsst.afw.table.SourceRecord` 

243 The record where measurement outputs will be stored. 

244 exposure : `~lsst.afw.image.Exposure` 

245 The exposure containing the source which needs measurement. 

246 

247 Raises 

248 ------ 

249 MeasurementError 

250 Raised for errors in measurement. 

251 """ 

252 # Extract the centroid from the record. 

253 center = self.centroidExtractor(record, self.flagHandler) 

254 

255 # Get the bounding box of the source's footprint. 

256 bbox = record.getFootprint().getBBox() 

257 

258 # Check that the bounding box has non-zero area. 

259 if bbox.getArea() == 0: 

260 raise measBase.MeasurementError(self.NO_PIXELS.doc, self.NO_PIXELS.number) 

261 

262 # Ensure that the centroid is within the bounding box. 

263 if not bbox.contains(Point2I(center)): 

264 raise measBase.MeasurementError(self.NOT_CONTAINED.doc, self.NOT_CONTAINED.number) 

265 

266 # Get the trace radius of the PSF. 

267 psfSigma = exposure.getPsf().computeShape(center).getTraceRadius() 

268 

269 # Turn bounding box corners into GalSim bounds. 

270 xmin, xmax = bbox.getMinX(), bbox.getMaxX() 

271 ymin, ymax = bbox.getMinY(), bbox.getMaxY() 

272 bounds = galsim._BoundsI(xmin, xmax, ymin, ymax) 

273 

274 # Get the `lsst.meas.base` mask for bad pixels. 

275 badpix = exposure.mask[bbox].array.copy() 

276 bitValue = exposure.mask.getPlaneBitMask(self.config.badMaskPlanes) 

277 badpix &= bitValue 

278 

279 # Extract the numpy array underlying the image within the bounding box 

280 # of the source. 

281 imageArray = exposure.image[bbox].array 

282 

283 # Create a GalSim image using the extracted array. 

284 # NOTE: GalSim's HSM uses the FITS convention of 1,1 for the 

285 # lower-left corner. 

286 image = galsim._Image(imageArray, bounds, None) 

287 

288 # Convert the mask of bad pixels to a format suitable for galsim. 

289 gd = badpix == 0 

290 badpix[gd] = 1 

291 badpix[~gd] = 0 

292 

293 weight_image = galsim._Image(badpix, bounds, None) 

294 

295 # Call the internal method to calculate adaptive moments using GalSim. 

296 self._calculate( 

297 record, 

298 image=image, 

299 weight_image=weight_image, 

300 sigma=2.5 * psfSigma, 

301 precision=1.0e-6, 

302 centroid=center, 

303 ) 

304 

305 

306class HsmSourceMomentsRoundConfig(HsmSourceMomentsConfig): 

307 """Configuration for HSM adaptive moments measurement for sources using 

308 round weight function. 

309 """ 

310 

311 def setDefaults(self): 

312 super().setDefaults() 

313 self.roundMoments = True 

314 

315 def validate(self): 

316 if not self.roundMoments: 

317 raise pexConfig.FieldValidationError( 

318 self.roundMoments, self, "roundMoments should be set to `True`." 

319 ) 

320 super().validate() 

321 

322 

323@measBase.register("ext_shapeHSM_HsmSourceMomentsRound") 

324class HsmSourceMomentsRoundPlugin(HsmSourceMomentsPlugin): 

325 """Plugin for HSM adaptive moments measurement for sources using round 

326 weight function. 

327 """ 

328 

329 ConfigClass = HsmSourceMomentsRoundConfig 

330 

331 

332class HsmPsfMomentsConfig(HsmMomentsConfig): 

333 """Configuration for HSM adaptive moments measurement for PSFs.""" 

334 

335 useSourceCentroidOffset = pexConfig.Field[bool]( 

336 doc="If True, then draw the PSF to be measured in the coordinate " 

337 "system of the original image (the PSF model origin - which is " 

338 "commonly the PSF centroid - may end up near a pixel edge or corner). " 

339 "If False, then draw the PSF to be measured in a shifted coordinate " 

340 "system such that the PSF model origin lands precisely in the center " 

341 "of the central pixel of the PSF image.", 

342 default=False, 

343 ) 

344 

345 def setDefaults(self): 

346 super().setDefaults() 

347 self.subtractCenter = True 

348 

349 

350@measBase.register("ext_shapeHSM_HsmPsfMoments") 

351class HsmPsfMomentsPlugin(HsmMomentsPlugin): 

352 """Plugin for HSM adaptive moments measurement for PSFs.""" 

353 

354 ConfigClass = HsmPsfMomentsConfig 

355 _debiased = False 

356 

357 def __init__(self, config, name, schema, metadata, logName=None): 

358 super().__init__(config, name, schema, metadata, logName=logName) 

359 docPrefix = "Debiased centroid" if self._debiased else "Centroid" 

360 self.centroidResultKey = afwTable.Point2DKey.addFields( 

361 schema, name, docPrefix + " of the PSF via the HSM shape algorithm", "pixel" 

362 ) 

363 docPrefix = "Debiased adaptive" if self._debiased else "Adaptive" 

364 self.shapeKey = afwTable.QuadrupoleKey.addFields( 

365 schema, 

366 name, 

367 docPrefix + " moments of the PSF via the HSM shape algorithm", 

368 afwTable.CoordinateType.PIXEL, 

369 ) 

370 if config.addFlux: 

371 self.fluxKey = schema.addField( 

372 schema.join(name, "Flux"), 

373 type=float, 

374 doc="Flux of the PSF via the HSM shape algorithm", 

375 ) 

376 

377 def measure(self, record, exposure): 

378 """ 

379 Measure adaptive moments of the PSF given an exposure and set the 

380 results in the record in place. 

381 

382 Parameters 

383 ---------- 

384 record : `~lsst.afw.table.SourceRecord` 

385 The record where measurement outputs will be stored. 

386 exposure : `~lsst.afw.image.Exposure` 

387 The exposure containing the PSF which needs measurement. 

388 

389 Raises 

390 ------ 

391 MeasurementError 

392 Raised for errors in measurement. 

393 """ 

394 # Extract the centroid from the record. 

395 center = self.centroidExtractor(record, self.flagHandler) 

396 

397 # Retrieve the PSF from the exposure. 

398 psf = exposure.getPsf() 

399 

400 # Check that the PSF is not None. 

401 if not psf: 

402 raise measBase.MeasurementError(self.NO_PSF.doc, self.NO_PSF.number) 

403 

404 # Get the bounding box of the PSF. 

405 psfBBox = psf.computeImageBBox(center) 

406 

407 # Two methods for getting PSF image evaluated at the source centroid: 

408 if self.config.useSourceCentroidOffset: 

409 # 1. Using `computeImage()` returns an image in the same coordinate 

410 # system as the pixelized image. 

411 psfImage = psf.computeImage(center) 

412 else: 

413 psfImage = psf.computeKernelImage(center) 

414 # 2. Using `computeKernelImage()` to return an image does not 

415 # retain any information about the original bounding box of the 

416 # PSF. We therefore reset the origin to be the same as the 

417 # pixelized image. 

418 psfImage.setXY0(psfBBox.getMin()) 

419 

420 # Get the trace radius of the PSF. 

421 psfSigma = psf.computeShape(center).getTraceRadius() 

422 

423 # Get the bounding box in the parent coordinate system. 

424 bbox = psfImage.getBBox(afwImage.PARENT) 

425 

426 # Turn bounding box corners into GalSim bounds. 

427 xmin, xmax = bbox.getMinX(), bbox.getMaxX() 

428 ymin, ymax = bbox.getMinY(), bbox.getMaxY() 

429 bounds = galsim._BoundsI(xmin, xmax, ymin, ymax) 

430 

431 # Adjust the psfImage for noise as needed, and retrieve the mask of bad 

432 # pixels. 

433 badpix = self._adjustNoise(psfImage, psfBBox, exposure, record, bounds) 

434 

435 # Extract the numpy array underlying the PSF image. 

436 imageArray = psfImage.array 

437 

438 # Create a GalSim image using the PSF image array. 

439 image = galsim._Image(imageArray, bounds, None) 

440 

441 if badpix is not None: 

442 gd = badpix == 0 

443 badpix[gd] = 1 

444 badpix[~gd] = 0 

445 

446 weight_image = galsim._Image(badpix, bounds, None) 

447 else: 

448 arr = np.ones(imageArray.shape, dtype=np.int32) 

449 weight_image = galsim._Image(arr, bounds, None) 

450 

451 # Decide on the centroid position based on configuration. 

452 if self.config.useSourceCentroidOffset: 

453 # If the source centroid offset should be used, use the source 

454 # centroid. 

455 centroid = center 

456 else: 

457 # Otherwise, use the center of the bounding box of psfImage. 

458 centroid = Point2D(psfBBox.getMin() + psfBBox.getDimensions() // 2) 

459 

460 # Call the internal method to calculate adaptive moments using GalSim. 

461 self._calculate( 

462 record, 

463 image=image, 

464 weight_image=weight_image, 

465 sigma=psfSigma, 

466 centroid=centroid, 

467 ) 

468 

469 def _adjustNoise(self, *args) -> None: 

470 """A noop in the base class, returning None for the bad pixel mask. 

471 This method is designed to be overridden in subclasses.""" 

472 pass 

473 

474 

475class HsmPsfMomentsDebiasedConfig(HsmPsfMomentsConfig): 

476 """Configuration for debiased HSM adaptive moments measurement for PSFs.""" 

477 

478 noiseSource = pexConfig.ChoiceField[str]( 

479 doc="Noise source. How to choose variance of the zero-mean Gaussian noise added to image.", 

480 allowed={ 

481 "meta": "variance = the 'BGMEAN' metadata entry", 

482 "variance": "variance = the image's variance plane", 

483 }, 

484 default="variance", 

485 ) 

486 seedOffset = pexConfig.Field[int](doc="Seed offset for random number generator.", default=0) 

487 badMaskPlanes = pexConfig.ListField[str]( 

488 doc="Mask planes used to reject bad pixels.", default=["BAD", "SAT"] 

489 ) 

490 

491 def setDefaults(self): 

492 super().setDefaults() 

493 self.useSourceCentroidOffset = True 

494 

495 

496@measBase.register("ext_shapeHSM_HsmPsfMomentsDebiased") 

497class HsmPsfMomentsDebiasedPlugin(HsmPsfMomentsPlugin): 

498 """Plugin for debiased HSM adaptive moments measurement for PSFs.""" 

499 

500 ConfigClass = HsmPsfMomentsDebiasedConfig 

501 _debiased = True 

502 

503 @classmethod 

504 def getExecutionOrder(cls): 

505 # Since the standard execution order increases in steps of 1, it's 

506 # safer to keep the increase by hand to less than 1. The exact value 

507 # does not matter. 

508 return cls.FLUX_ORDER + 0.1 

509 

510 def _adjustNoise( 

511 self, 

512 psfImage: afwImage.Image, 

513 psfBBox: Box2I, 

514 exposure: afwImage.Exposure, 

515 record: afwTable.SourceRecord, 

516 bounds: galsim.bounds.BoundsI, 

517 ) -> np.ndarray: 

518 """ 

519 Adjusts noise in the PSF image and updates the bad pixel mask based on 

520 exposure data. This method modifies `psfImage` in place and returns a 

521 newly created `badpix` mask. 

522 

523 Parameters 

524 ---------- 

525 psfImage : `~lsst.afw.image.Image` 

526 The PSF image to be adjusted. This image is modified in place. 

527 psfBBox : `~lsst.geom.Box2I` 

528 The bounding box of the PSF. 

529 exposure : `~lsst.afw.image.Exposure` 

530 The exposure object containing relevant metadata and mask 

531 information. 

532 record : `~lsst.afw.table.SourceRecord` 

533 The source record where measurement outputs will be stored. May be 

534 modified in place to set flags. 

535 bounds : `~galsim.bounds.BoundsI` 

536 The bounding box of the PSF as a GalSim bounds object. 

537 

538 Returns 

539 ------- 

540 badpix : `~np.ndarray` 

541 Numpy image array (np.int32) representing bad pixels, where zero 

542 indicates good pixels and any nonzero value denotes a bad pixel. 

543 

544 Raises 

545 ------ 

546 MeasurementError 

547 If there's an issue during the noise adjustment process. 

548 FatalAlgorithmError 

549 If BGMEAN is not present in the metadata when using the meta noise 

550 source. 

551 """ 

552 # Psf image crossing exposure edge is fine if we're getting the 

553 # variance from metadata, but not okay if we're getting the 

554 # variance from the variance plane. In both cases, set the EDGE 

555 # flag, but only fail hard if using variance plane. 

556 overlap = psfImage.getBBox() 

557 overlap.clip(exposure.getBBox()) 

558 if overlap != psfImage.getBBox(): 

559 self.flagHandler.setValue(record, self.EDGE.number, True) 

560 if self.config.noiseSource == "variance": 

561 self.flagHandler.setValue(record, self.FAILURE.number, True) 

562 raise measBase.MeasurementError(self.EDGE.doc, self.EDGE.number) 

563 

564 # Match PSF flux to source. 

565 psfImage *= record.getPsfInstFlux() 

566 

567 # Add Gaussian noise to image in 4 steps: 

568 # 1. Initialize the noise image and random number generator. 

569 noise = afwImage.Image(psfImage.getBBox(), dtype=psfImage.dtype, initialValue=0.0) 

570 seed = record.getId() + self.config.seedOffset 

571 rand = afwMath.Random("MT19937", seed) 

572 

573 # 2. Generate Gaussian noise image. 

574 afwMath.randomGaussianImage(noise, rand) 

575 

576 # 3. Determine the noise scaling based on the noise source. 

577 if self.config.noiseSource == "meta": 

578 # Retrieve BGMEAN from the exposure metadata. 

579 try: 

580 bgmean = exposure.getMetadata().getAsDouble("BGMEAN") 

581 except NotFoundError as error: 

582 raise measBase.FatalAlgorithmError(str(error)) 

583 # Scale the noise by the square root of the background mean. 

584 noise *= np.sqrt(bgmean) 

585 elif self.config.noiseSource == "variance": 

586 # Get the variance image from the exposure and restrict to the 

587 # PSF bounding box. 

588 var = afwImage.Image(exposure.variance[psfImage.getBBox()], dtype=psfImage.dtype, deep=True) 

589 # Scale the noise by the square root of the variance. 

590 var.sqrt() # In-place square root. 

591 noise *= var 

592 

593 # 4. Add the scaled noise to the PSF image. 

594 psfImage += noise 

595 

596 # Masking is needed for debiased PSF moments. 

597 badpix = afwImage.Mask(psfBBox) 

598 # NOTE: We repeat the `overlap` calculation in the two lines below to 

599 # align with the old C++ version and minimize potential discrepancies. 

600 # There's zero chance this will be a time sink, and using the bbox from 

601 # the image that's about to be cropped seems safer than using the bbox 

602 # from a different image, even if they're nominally supposed to have 

603 # the same bounds. 

604 overlap = badpix.getBBox() 

605 overlap.clip(exposure.getBBox()) 

606 badpix[overlap] = exposure.mask[overlap] 

607 # Pull out the numpy view of the badpix mask image. 

608 badpix = badpix.array 

609 

610 bitValue = exposure.mask.getPlaneBitMask(self.config.badMaskPlanes) 

611 badpix &= bitValue 

612 

613 return badpix