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

191 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-05 12:20 +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: galsim.Image | None = None, 

96 badpix: galsim.Image | None = None, 

97 sigma: float = 5.0, 

98 precision: float = 1.0e-6, 

99 centroid: Point2D | None = None, 

100 ) -> None: 

101 """ 

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

103 place. 

104 

105 Parameters 

106 ---------- 

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

108 Record to store measurements. 

109 image : `~galsim.Image` 

110 Image on which to perform measurements. 

111 weight : `~galsim.Image`, optional 

112 The weight image for the galaxy being measured. Can be an int or a 

113 float array. No weighting is done if None. Default is None. 

114 badpix : `~galsim.Image`, optional 

115 Image representing bad pixels, where zero indicates good pixels and 

116 any nonzero value denotes a bad pixel. No bad pixel masking is done 

117 if None. Default is None. 

118 sigma : `float`, optional 

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

120 precision : `float`, optional 

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

122 centroid : `~lsst.geom.Point2D`, optional 

123 Centroid guess for HSM adaptive moments, defaulting to the image's 

124 true center if None. Default is None. 

125 

126 Raises 

127 ------ 

128 MeasurementError 

129 Raised for errors in measurement. 

130 """ 

131 # Convert centroid to GalSim's PositionD type. 

132 guessCentroid = galsim.PositionD(centroid.x, centroid.y) 

133 

134 try: 

135 # Attempt to compute HSM moments. 

136 shape = galsim.hsm.FindAdaptiveMom( 

137 image, 

138 weight=weight, 

139 badpix=badpix, 

140 guess_sig=sigma, 

141 precision=precision, 

142 guess_centroid=guessCentroid, 

143 strict=True, 

144 round_moments=self.config.roundMoments, 

145 hsmparams=None, 

146 ) 

147 except galsim.hsm.GalSimHSMError as error: 

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

149 

150 # Retrieve computed moments sigma and centroid. 

151 determinantRadius = shape.moments_sigma 

152 centroidResult = shape.moments_centroid 

153 

154 # Subtract center if required by configuration. 

155 if self.config.subtractCenter: 

156 centroidResult.x -= centroid.getX() 

157 centroidResult.y -= centroid.getY() 

158 

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

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

161 

162 # Populate the record with the centroid results. 

163 record.set(self.centroidResultKey, centroidResult) 

164 

165 # Convert GalSim measurements to lsst measurements. 

166 try: 

167 # Create an ellipse for the shape. 

168 ellipse = afwGeom.ellipses.SeparableDistortionDeterminantRadius( 

169 e1=shape.observed_shape.e1, 

170 e2=shape.observed_shape.e2, 

171 radius=determinantRadius, 

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

173 ) 

174 # Get the quadrupole moments from the ellipse. 

175 quad = afwGeom.ellipses.Quadrupole(ellipse) 

176 except InvalidParameterError as error: 

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

178 

179 # Store the quadrupole moments in the record. 

180 record.set(self.shapeKey, quad) 

181 

182 # Store the flux if required by configuration. 

183 if self.config.addFlux: 

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

185 

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

187 # Docstring inherited. 

188 self.flagHandler.handleFailure(record) 

189 if error: 

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

191 self.log.debug( 

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

193 record.getId(), 

194 centroid.getX(), 

195 centroid.getY(), 

196 error, 

197 ) 

198 

199 

200class HsmSourceMomentsConfig(HsmMomentsConfig): 

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

202 

203 badMaskPlanes = pexConfig.ListField[str]( 

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

205 ) 

206 

207 

208@measBase.register("ext_shapeHSM_HsmSourceMoments") 

209class HsmSourceMomentsPlugin(HsmMomentsPlugin): 

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

211 

212 ConfigClass = HsmSourceMomentsConfig 

213 

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

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

216 self.centroidResultKey = afwTable.Point2DKey.addFields( 

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

218 ) 

219 self.shapeKey = afwTable.QuadrupoleKey.addFields( 

220 schema, 

221 name, 

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

223 afwTable.CoordinateType.PIXEL, 

224 ) 

225 if config.addFlux: 

226 self.fluxKey = schema.addField( 

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

228 ) 

229 

230 def measure(self, record, exposure): 

231 """ 

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

233 results in the record in place. 

234 

235 Parameters 

236 ---------- 

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

238 The record where measurement outputs will be stored. 

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

240 The exposure containing the source which needs measurement. 

241 

242 Raises 

243 ------ 

244 MeasurementError 

245 Raised for errors in measurement. 

246 """ 

247 # Extract the centroid from the record. 

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

249 

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

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

252 

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

254 if bbox.getArea() == 0: 

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

256 

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

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

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

260 

261 # Get the trace radius of the PSF. 

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

263 

264 # Turn bounding box corners into GalSim bounds. 

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

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

267 bounds = galsim.bounds.BoundsI(xmin, xmax, ymin, ymax) 

268 

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

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

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

272 badpix &= bitValue 

273 

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

275 # of the source. 

276 imageArray = exposure.image[bbox].array 

277 

278 # Create a GalSim image using the extracted array. 

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

280 # lower-left corner. 

281 image = galsim.Image(imageArray, bounds=bounds, copy=False) 

282 

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

284 # NOTE: galsim.Image will match whatever dtype the input array is 

285 # (here int32). 

286 badpix = galsim.Image(badpix, bounds=bounds, copy=False) 

287 

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

289 self._calculate( 

290 record, 

291 image=image, 

292 weight=None, 

293 badpix=badpix, 

294 sigma=2.5 * psfSigma, 

295 precision=1.0e-6, 

296 centroid=center, 

297 ) 

298 

299 

300class HsmSourceMomentsRoundConfig(HsmSourceMomentsConfig): 

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

302 round weight function. 

303 """ 

304 

305 def setDefaults(self): 

306 super().setDefaults() 

307 self.roundMoments = True 

308 

309 def validate(self): 

310 if not self.roundMoments: 

311 raise pexConfig.FieldValidationError( 

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

313 ) 

314 super().validate() 

315 

316 

317@measBase.register("ext_shapeHSM_HsmSourceMomentsRound") 

318class HsmSourceMomentsRoundPlugin(HsmSourceMomentsPlugin): 

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

320 weight function. 

321 """ 

322 

323 ConfigClass = HsmSourceMomentsRoundConfig 

324 

325 

326class HsmPsfMomentsConfig(HsmMomentsConfig): 

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

328 

329 useSourceCentroidOffset = pexConfig.Field[bool]( 

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

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

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

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

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

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

336 default=False, 

337 ) 

338 

339 def setDefaults(self): 

340 super().setDefaults() 

341 self.subtractCenter = True 

342 

343 

344@measBase.register("ext_shapeHSM_HsmPsfMoments") 

345class HsmPsfMomentsPlugin(HsmMomentsPlugin): 

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

347 

348 ConfigClass = HsmPsfMomentsConfig 

349 _debiased = False 

350 

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

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

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

354 self.centroidResultKey = afwTable.Point2DKey.addFields( 

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

356 ) 

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

358 self.shapeKey = afwTable.QuadrupoleKey.addFields( 

359 schema, 

360 name, 

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

362 afwTable.CoordinateType.PIXEL, 

363 ) 

364 if config.addFlux: 

365 self.fluxKey = schema.addField( 

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

367 type=float, 

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

369 ) 

370 

371 def measure(self, record, exposure): 

372 """ 

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

374 results in the record in place. 

375 

376 Parameters 

377 ---------- 

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

379 The record where measurement outputs will be stored. 

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

381 The exposure containing the PSF which needs measurement. 

382 

383 Raises 

384 ------ 

385 MeasurementError 

386 Raised for errors in measurement. 

387 """ 

388 # Extract the centroid from the record. 

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

390 

391 # Retrieve the PSF from the exposure. 

392 psf = exposure.getPsf() 

393 

394 # Check that the PSF is not None. 

395 if not psf: 

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

397 

398 # Get the bounding box of the PSF. 

399 psfBBox = psf.computeImageBBox(center) 

400 

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

402 if self.config.useSourceCentroidOffset: 

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

404 # system as the pixelized image. 

405 psfImage = psf.computeImage(center) 

406 else: 

407 psfImage = psf.computeKernelImage(center) 

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

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

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

411 # pixelized image. 

412 psfImage.setXY0(psfBBox.getMin()) 

413 

414 # Get the trace radius of the PSF. 

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

416 

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

418 bbox = psfImage.getBBox(afwImage.PARENT) 

419 

420 # Turn bounding box corners into GalSim bounds. 

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

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

423 bounds = galsim.bounds.BoundsI(xmin, xmax, ymin, ymax) 

424 

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

426 # pixels. 

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

428 

429 # Extract the numpy array underlying the PSF image. 

430 imageArray = psfImage.array 

431 

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

433 image = galsim.Image(imageArray, bounds=bounds, copy=False) 

434 

435 # Decide on the centroid position based on configuration. 

436 if self.config.useSourceCentroidOffset: 

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

438 # centroid. 

439 centroid = center 

440 else: 

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

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

443 

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

445 self._calculate( 

446 record, 

447 image=image, 

448 weight=None, 

449 badpix=badpix, 

450 sigma=psfSigma, 

451 centroid=centroid, 

452 ) 

453 

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

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

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

457 pass 

458 

459 

460class HsmPsfMomentsDebiasedConfig(HsmPsfMomentsConfig): 

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

462 

463 noiseSource = pexConfig.ChoiceField[str]( 

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

465 allowed={ 

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

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

468 }, 

469 default="variance", 

470 ) 

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

472 badMaskPlanes = pexConfig.ListField[str]( 

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

474 ) 

475 

476 def setDefaults(self): 

477 super().setDefaults() 

478 self.useSourceCentroidOffset = True 

479 

480 

481@measBase.register("ext_shapeHSM_HsmPsfMomentsDebiased") 

482class HsmPsfMomentsDebiasedPlugin(HsmPsfMomentsPlugin): 

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

484 

485 ConfigClass = HsmPsfMomentsDebiasedConfig 

486 _debiased = True 

487 

488 @classmethod 

489 def getExecutionOrder(cls): 

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

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

492 # does not matter. 

493 return cls.FLUX_ORDER + 0.1 

494 

495 def _adjustNoise( 

496 self, 

497 psfImage: afwImage.Image, 

498 psfBBox: Box2I, 

499 exposure: afwImage.Exposure, 

500 record: afwTable.SourceRecord, 

501 bounds: galsim.bounds.BoundsI, 

502 ) -> galsim.Image: 

503 """ 

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

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

506 newly created `badpix` mask. 

507 

508 Parameters 

509 ---------- 

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

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

512 psfBBox : `~lsst.geom.Box2I` 

513 The bounding box of the PSF. 

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

515 The exposure object containing relevant metadata and mask 

516 information. 

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

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

519 modified in place to set flags. 

520 bounds : `~galsim.bounds.BoundsI` 

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

522 

523 Returns 

524 ------- 

525 badpix : `~galSim.Image` 

526 Image representing bad pixels, where zero indicates good pixels and 

527 any nonzero value denotes a bad pixel. 

528 

529 Raises 

530 ------ 

531 MeasurementError 

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

533 FatalAlgorithmError 

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

535 source. 

536 """ 

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

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

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

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

541 overlap = psfImage.getBBox() 

542 overlap.clip(exposure.getBBox()) 

543 if overlap != psfImage.getBBox(): 

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

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

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

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

548 

549 # Match PSF flux to source. 

550 psfImage *= record.getPsfInstFlux() 

551 

552 # Add Gaussian noise to image in 4 steps: 

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

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

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

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

557 

558 # 2. Generate Gaussian noise image. 

559 afwMath.randomGaussianImage(noise, rand) 

560 

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

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

563 # Retrieve BGMEAN from the exposure metadata. 

564 try: 

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

566 except NotFoundError as error: 

567 raise measBase.FatalAlgorithmError(str(error)) 

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

569 noise *= np.sqrt(bgmean) 

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

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

572 # PSF bounding box. 

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

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

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

576 noise *= var 

577 

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

579 psfImage += noise 

580 

581 # Masking is needed for debiased PSF moments. 

582 badpix = afwImage.Mask(psfBBox) 

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

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

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

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

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

588 # the same bounds. 

589 overlap = badpix.getBBox() 

590 overlap.clip(exposure.getBBox()) 

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

592 badpix = badpix.array 

593 

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

595 badpix &= bitValue 

596 

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

598 # NOTE: galsim.Image will match whatever dtype the input array is 

599 # (here int32). 

600 badpix = galsim.Image(badpix, bounds=bounds, copy=False) 

601 

602 return badpix