Coverage for python/lsst/afw/image/_image/_multiband.py: 17%

325 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-22 02:38 -0700

1# This file is part of afw. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 <http://www.gnu.org/licenses/>. 

21 

22__all__ = ["MultibandPixel", "MultibandImage", "MultibandMask", "MultibandMaskedImage"] 

23 

24import numpy as np 

25 

26from lsst.geom import Point2I, Box2I, Extent2I 

27from . import Image, ImageF, Mask, MaskPixel, PARENT, LOCAL 

28from .._maskedImage import MaskedImage, MaskedImageF 

29from ._slicing import imageIndicesToNumpy 

30from ...multiband import MultibandBase 

31 

32 

33class MultibandPixel(MultibandBase): 

34 """Multiband Pixel class 

35 

36 This represent acts as a container for a single pixel 

37 (scalar) in multiple bands. 

38 

39 Parameters 

40 ---------- 

41 singles : `sequence` 

42 Either a list of single band objects or an array of values. 

43 filters : `list` 

44 List of filter names. If `singles` is an `OrderedDict` or 

45 a `MultibandPixel` then this argument is ignored, 

46 otherwise it is required. 

47 position : `Point2I` 

48 Location of the pixel in the parent image. 

49 Unlike other objects that inherit from `MultibandBase`, 

50 `MultibandPixel` objects don't have a full `Box2I` 

51 bounding box, since they only contain a single pixel, 

52 so the bounding box cannot be inherited from the 

53 list of `singles`. 

54 """ 

55 def __init__(self, filters, singles, position): 

56 if any([arg is None for arg in [singles, filters, position]]): 

57 err = "Expected an array of `singles`, a list of `filters, and a `bbox`" 

58 raise NotImplementedError(err) 

59 

60 # Make sure that singles is an array 

61 singles = np.array(singles, copy=False) 

62 

63 super().__init__(filters, singles, bbox=Box2I(position, Extent2I(1, 1))) 

64 # In this case we want self.singles to be an array 

65 self._singles = singles 

66 

67 # Make sure that the bounding box has been setup properly 

68 assert self.getBBox().getDimensions() == Extent2I(1, 1) 

69 

70 def _getArray(self): 

71 """Data cube array in multiple bands 

72 

73 Since `self._singles` is just a 1D array, 

74 `array` just returns `self._singles`. 

75 """ 

76 return self.singles 

77 

78 def _setArray(self, value): 

79 assert value.shape == self.array.shape 

80 self._singles[:] = value 

81 

82 array = property(_getArray, _setArray) 

83 

84 def clone(self, deep=True): 

85 """Make a copy of the current instance 

86 

87 `MultibandPixel.singles` is an array, 

88 so this just makes a copy of the array 

89 (as opposed to a view of the parent array). 

90 """ 

91 if deep: 

92 singles = np.copy(self.singles) 

93 position = self.getBBox().getMin() 

94 return MultibandPixel(filters=self.filters, singles=singles, position=position) 

95 

96 result = MultibandPixel(filters=self.filters, singles=self.singles, position=self.getBBox().getMin()) 

97 result._bbox = self.getBBox() 

98 return result 

99 

100 def __getitem__(self, indices): 

101 """Get a slice of the underlying array 

102 

103 Since a `MultibandPixel` is a scalar in the 

104 spatial dimensions, it can only be indexed with 

105 a filter name, number, or slice. 

106 """ 

107 if isinstance(indices, tuple): 

108 err = ("Too many indices: " 

109 "`MultibandPixel has no spatial dimensions and " 

110 "only accepts a filterIndex") 

111 raise IndexError(err) 

112 # Make the index into a valid numpy index or slice 

113 filters, filterIndex = self._filterNamesToIndex(indices) 

114 if len(filters) == 1 and not isinstance(filterIndex, slice): 

115 # The user only requested a pixel in a single band, so return it 

116 return self.array[filterIndex[0]] 

117 

118 result = self.clone(False) 

119 result._filters = filters 

120 result._singles = self._singles[filterIndex] 

121 # No need to update the bounding box, since pixels can only be sliced in the filter dimension 

122 return result 

123 

124 def _slice(self, filters, filterIndex, indices): 

125 pass 

126 

127 

128class MultibandImageBase(MultibandBase): 

129 """Multiband Image class 

130 

131 This class acts as a container for multiple `afw.Image` objects. 

132 All images must be contained in the same bounding box, 

133 and have the same data type. 

134 The data is stored in a 3D array (filters, y, x), and the single 

135 band `Image` instances have an internal array that points to the 

136 3D multiband array, so that the single band objects and multiband 

137 array are always in agreement. 

138 

139 Parameters 

140 ---------- 

141 filters : `list` 

142 List of filter names. 

143 array : 3D numpy array 

144 Array (filters, y, x) of multiband data. 

145 If this is used to initialize a `MultibandImage`, 

146 either `bbox` or `singles` is also required. 

147 singleType : `type` 

148 Type of the single band object (eg. `Image`, `Mask`) to 

149 convert the array into a tuple of single band objects 

150 that point to the image array. 

151 bbox : `Box2I` 

152 Location of the array in a larger single band image. 

153 If `bbox` is `None` then the bounding box is initialized 

154 at the origin. 

155 """ 

156 def __init__(self, filters, array, singleType, bbox=None): 

157 # Create the single band objects from the array 

158 if len(array) != len(filters): 

159 raise ValueError("`array` and `filters` must have the same length") 

160 self._array = array 

161 self._filters = tuple(filters) 

162 if bbox is None: 

163 bbox = Box2I(Point2I(0, 0), Extent2I(array.shape[2], array.shape[1])) 

164 self._bbox = bbox 

165 

166 xy0 = self.getXY0() 

167 dtype = array.dtype 

168 self._singles = tuple([singleType(array=array[n], xy0=xy0, dtype=dtype) for n in range(len(array))]) 

169 

170 # Make sure that all of the parameters have been setup appropriately 

171 assert isinstance(self._bbox, Box2I) 

172 assert len(self.singles) == len(self.filters) 

173 

174 def _getArray(self): 

175 """Data cube array in multiple bands 

176 

177 Returns 

178 ------- 

179 self._array : array 

180 The resulting 3D data cube with shape (filters, y, x). 

181 """ 

182 return self._array 

183 

184 def _setArray(self, value): 

185 """Set the values of the array""" 

186 self.array[:] = value 

187 

188 array = property(_getArray, _setArray) 

189 

190 def clone(self, deep=True): 

191 """Copy the current object 

192 

193 Parameters 

194 ---------- 

195 deep : `bool` 

196 Whether or not to make a deep copy 

197 """ 

198 if deep: 

199 array = np.copy(self.array) 

200 bbox = Box2I(self.getBBox()) 

201 else: 

202 array = self.array 

203 bbox = self.getBBox() 

204 result = type(self)(self.filters, array, bbox) 

205 return result 

206 

207 def _slice(self, filters, filterIndex, indices): 

208 """Slice the current object and return the result 

209 

210 See `MultibandBase._slice` for a list of the parameters. 

211 """ 

212 if len(indices) > 0: 

213 if len(indices) == 1: 

214 indices = indices[0] 

215 allSlices = [filterIndex, slice(None), slice(None)] 

216 sy, sx, bbox = imageIndicesToNumpy(indices, self.getBBox) 

217 if sy is not None: 

218 allSlices[-2] = sy 

219 if sx is not None: 

220 allSlices[-1] = sx 

221 array = self._array[tuple(allSlices)] 

222 

223 # Return a scalar or MultibandPixel 

224 # if the image indices are integers 

225 if bbox is None: 

226 if not isinstance(filterIndex, slice) and len(filterIndex) == 1: 

227 return array[0] 

228 result = MultibandPixel( 

229 singles=array, 

230 filters=filters, 

231 position=Point2I(sx + self.x0, sy + self.y0) 

232 ) 

233 return result 

234 else: 

235 array = self._array[filterIndex] 

236 bbox = self.getBBox() 

237 result = type(self)(filters, array, bbox) 

238 

239 # Check that the image and array shapes agree 

240 imageShape = ( 

241 len(result.filters), 

242 result.getBBox().getHeight(), 

243 result.getBBox().getWidth() 

244 ) 

245 assert result.array.shape == imageShape 

246 

247 return result 

248 

249 def __setitem__(self, args, value): 

250 """Set a subset of the MultibandImage 

251 """ 

252 if not isinstance(args, tuple): 

253 indices = (args,) 

254 else: 

255 indices = args 

256 

257 # Return the single band object if the first 

258 # index is not a list or slice. 

259 filters, filterIndex = self._filterNamesToIndex(indices[0]) 

260 if len(indices) > 1: 

261 sy, sx, bbox = imageIndicesToNumpy(indices[1:], self.getBBox) 

262 else: 

263 sy = sx = slice(None) 

264 if hasattr(value, "array"): 

265 self._array[filterIndex, sy, sx] = value.array 

266 else: 

267 self._array[filterIndex, sy, sx] = value 

268 

269 def getBBox(self, origin=PARENT): 

270 """Bounding box 

271 """ 

272 if origin == PARENT: 

273 return self._bbox 

274 elif origin == LOCAL: 

275 return Box2I(Point2I(0, 0), self._bbox.getDimensions()) 

276 raise ValueError("Unrecognized origin, expected either PARENT or LOCAL") 

277 

278 

279def makeImageFromSingles(cls, filters, singles): 

280 """Construct a MultibandImage from a collection of single band images 

281 

282 Parameters 

283 ---------- 

284 filters : `list` 

285 List of filter names. 

286 singles : `list` 

287 A list of single band objects. 

288 If `array` is not `None`, then `singles` is ignored 

289 """ 

290 array = np.array([image.array for image in singles], dtype=singles[0].array.dtype) 

291 if not np.all([image.getBBox() == singles[0].getBBox() for image in singles[1:]]): 

292 raise ValueError("Single band images did not all have the same bounding box") 

293 bbox = singles[0].getBBox() 

294 return cls(filters, array, bbox) 

295 

296 

297def makeImageFromKwargs(cls, filters, filterKwargs, singleType=ImageF, **kwargs): 

298 """Build a MultibandImage from a set of keyword arguments 

299 

300 Parameters 

301 ---------- 

302 filters : `list` 

303 List of filter names. 

304 singleType : class 

305 Class of the single band objects. 

306 This is ignored unless `singles` and `array` 

307 are both `None`, in which case it is required. 

308 filterKwargs : `dict` 

309 Keyword arguments to initialize a new instance of an inherited class 

310 that are different for each filter. 

311 The keys are the names of the arguments and the values 

312 should also be dictionaries, with filter names as keys 

313 and the value of the argument for a given filter as values. 

314 kwargs : `dict` 

315 Keyword arguments to initialize a new instance of an 

316 inherited class that are the same in all bands. 

317 """ 

318 # Attempt to load a set of images 

319 singles = [] 

320 for f in filters: 

321 if filterKwargs is not None: 

322 for key, value in filterKwargs: 

323 kwargs[key] = value[f] 

324 singles.append(singleType(**kwargs)) 

325 return cls.makeImageFromSingles(filters, singles) 

326 

327 

328class MultibandImage(MultibandImageBase): 

329 """Multiband Image class 

330 

331 See `MultibandImageBase` for a description of the parameters. 

332 """ 

333 def __init__(self, filters, array, bbox=None): 

334 super().__init__(filters, array, Image, bbox) 

335 

336 @staticmethod 

337 def fromImages(filters, singles): 

338 """Construct a MultibandImage from a collection of single band images 

339 

340 see `fromSingles` for a description of parameters 

341 """ 

342 return makeImageFromSingles(MultibandImage, filters, singles) 

343 

344 @staticmethod 

345 def fromKwargs(filters, filterKwargs, singleType=ImageF, **kwargs): 

346 """Build a MultibandImage from a set of keyword arguments 

347 

348 see `makeImageFromKwargs` for a description of parameters 

349 """ 

350 return makeImageFromKwargs(MultibandImage, filters, filterKwargs, singleType, **kwargs) 

351 

352 

353class MultibandMask(MultibandImageBase): 

354 """Multiband Mask class 

355 

356 See `MultibandImageBase` for a description of the parameters. 

357 """ 

358 def __init__(self, filters, array, bbox=None): 

359 super().__init__(filters, array, Mask, bbox) 

360 # Set Mask specific properties 

361 self._refMask = self._singles[0] 

362 refMask = self._refMask 

363 assert np.all([refMask.getMaskPlaneDict() == m.getMaskPlaneDict() for m in self.singles]) 

364 assert np.all([refMask.getNumPlanesMax() == m.getNumPlanesMax() for m in self.singles]) 

365 assert np.all([refMask.getNumPlanesUsed() == m.getNumPlanesUsed() for m in self.singles]) 

366 

367 @staticmethod 

368 def fromMasks(filters, singles): 

369 """Construct a MultibandImage from a collection of single band images 

370 

371 see `fromSingles` for a description of parameters 

372 """ 

373 return makeImageFromSingles(MultibandMask, filters, singles) 

374 

375 @staticmethod 

376 def fromKwargs(filters, filterKwargs, singleType=ImageF, **kwargs): 

377 """Build a MultibandImage from a set of keyword arguments 

378 

379 see `makeImageFromKwargs` for a description of parameters 

380 """ 

381 return makeImageFromKwargs(MultibandMask, filters, filterKwargs, singleType, **kwargs) 

382 

383 def getMaskPlane(self, key): 

384 """Get the bit number of a mask in the `MaskPlaneDict` 

385 

386 Each `key` in the mask plane has an associated bit value 

387 in the mask. This method returns the bit number of the 

388 `key` in the `MaskPlaneDict`. 

389 This is in contrast to `getPlaneBitMask`, which returns the 

390 value of the bit number. 

391 

392 For example, if `getMaskPlane` returns `8`, then `getPlaneBitMask` 

393 returns `256`. 

394 

395 Parameters 

396 ---------- 

397 key : `str` 

398 Name of the key in the `MaskPlaneDict` 

399 

400 Returns 

401 ------- 

402 bit : `int` 

403 Bit number for mask `key` 

404 """ 

405 return self._refMask.getMaskPlaneDict()[key] 

406 

407 def getPlaneBitMask(self, names): 

408 """Get the bit number of a mask in the `MaskPlaneDict` 

409 

410 Each `key` in the mask plane has an associated bit value 

411 in the mask. This method returns the bit number of the 

412 `key` in the `MaskPlaneDict`. 

413 This is in contrast to `getPlaneBitMask`, which returns the 

414 value of the bit number. 

415 

416 For example, if `getMaskPlane` returns `8`, then `getPlaneBitMask` 

417 returns `256`. 

418 

419 Parameters 

420 ---------- 

421 names : `str` or list of `str` 

422 Name of the key in the `MaskPlaneDict` or a list of keys. 

423 If multiple keys are used, the value returned is the integer 

424 value of the number with all of the bit values in `names`. 

425 

426 For example if `MaskPlaneDict("CR")=3` and 

427 `MaskPlaneDict("NO_DATA)=8`, then 

428 `getPlaneBitMask(("CR", "NO_DATA"))=264` 

429 

430 Returns 

431 ------- 

432 bit value : `int` 

433 Bit value for all of the combined bits described by `names`. 

434 """ 

435 return self._refMask.getPlaneBitMask(names) 

436 

437 def getNumPlanesMax(self): 

438 """Maximum number of mask planes available 

439 

440 This is required to be the same for all of the single 

441 band `Mask` objects. 

442 """ 

443 return self._refMask.getNumPlanesMax() 

444 

445 def getNumPlanesUsed(self): 

446 """Number of mask planes used 

447 

448 This is required to be the same for all of the single 

449 band `Mask` objects. 

450 """ 

451 return self._refMask.getNumPlanesUsed() 

452 

453 def getMaskPlaneDict(self): 

454 """Dictionary of Mask Plane bit values 

455 """ 

456 return self._refMask.getMaskPlaneDict() 

457 

458 @staticmethod 

459 def clearMaskPlaneDict(): 

460 """Reset the mask plane dictionary 

461 """ 

462 Mask[MaskPixel].clearMaskPlaneDict() 

463 

464 @staticmethod 

465 def addMaskPlane(name): 

466 """Add a mask to the mask plane 

467 

468 Parameters 

469 ---------- 

470 name : `str` 

471 Name of the new mask plane 

472 

473 Returns 

474 ------- 

475 index : `int` 

476 Bit value of the mask in the mask plane. 

477 """ 

478 idx = Mask[MaskPixel].addMaskPlane(name) 

479 return idx 

480 

481 @staticmethod 

482 def removeMaskPlane(name): 

483 """Remove a mask from the mask plane 

484 

485 Parameters 

486 ---------- 

487 name : `str` 

488 Name of the mask plane to remove 

489 """ 

490 Mask[MaskPixel].removeMaskPlane(name) 

491 

492 def removeAndClearMaskPlane(self, name, removeFromDefault=False): 

493 """Remove and clear a mask from the mask plane 

494 

495 Clear all pixels of the specified mask and remove the plane from the 

496 mask plane dictionary. Also optionally remove the plane from the 

497 default dictionary. 

498 

499 Parameters 

500 ---------- 

501 name : `str` 

502 Name of the mask plane to remove 

503 removeFromDefault : `bool`, optional 

504 Whether to remove the mask plane from the default dictionary. 

505 Default is `False`. 

506 """ 

507 # Clear all masks in MultibandMask but leave in default dict for now 

508 for single in self.singles: 

509 single.removeAndClearMaskPlane(name, removeFromDefault=False) 

510 # Now remove from default dict according to removeFromDefault 

511 self._refMask.removeAndClearMaskPlane(name, removeFromDefault) 

512 

513 def clearAllMaskPlanes(self): 

514 """Clear all the pixels 

515 """ 

516 self._refMask.clearAllMaskPlanes() 

517 

518 def _getOtherMasks(self, others): 

519 """Check if two masks can be combined 

520 

521 This method checks that `self` and `others` 

522 have the same number of bands, or if 

523 others is a single value, creates a list 

524 to use for updating all of the `singles`. 

525 """ 

526 if isinstance(others, MultibandMask): 

527 if len(self.singles) != len(others.singles) or self.filters != others.filters: 

528 msg = "Both `MultibandMask` objects must have the same number of bands to combine" 

529 raise ValueError(msg) 

530 result = [s for s in others.singles] 

531 else: 

532 result = [others]*len(self.singles) 

533 return result 

534 

535 def __ior__(self, others): 

536 _others = self._getOtherMasks(others) 

537 for s, o in zip(self.singles, _others): 

538 s |= o 

539 return self 

540 

541 def __iand__(self, others): 

542 _others = self._getOtherMasks(others) 

543 for s, o in zip(self.singles, _others): 

544 s &= o 

545 return self 

546 

547 def __ixor__(self, others): 

548 _others = self._getOtherMasks(others) 

549 for s, o in zip(self.singles, _others): 

550 s ^= o 

551 return self 

552 

553 

554class MultibandTripleBase(MultibandBase): 

555 """MultibandTripleBase class 

556 

557 This is a base class inherited by multiband classes 

558 with `image`, `mask`, and `variance` objects, 

559 such as `MultibandMaskedImage` and `MultibandExposure`. 

560 

561 Parameters 

562 ---------- 

563 filters : `list` 

564 List of filter names. If `singles` is an `OrderedDict` 

565 then this argument is ignored, otherwise it is required. 

566 image : `list` or `MultibandImage` 

567 List of `Image` objects that represent the image in each band or 

568 a `MultibandImage`. 

569 Ignored if `singles` is not `None`. 

570 mask : `list` or `MultibandMask` 

571 List of `Mask` objects that represent the mask in each bandor 

572 a `MultibandMask`. 

573 Ignored if `singles` is not `None`. 

574 variance : `list` or `MultibandImage` 

575 List of `Image` objects that represent the variance in each bandor 

576 a `MultibandImage`. 

577 Ignored if `singles` is not `None`. 

578 """ 

579 def __init__(self, filters, image, mask, variance): 

580 self._filters = tuple(filters) 

581 # Convert single band images into multiband images 

582 if not isinstance(image, MultibandBase): 

583 image = MultibandImage.fromImages(filters, image) 

584 if mask is not None: 

585 mask = MultibandMask.fromMasks(filters, mask) 

586 if variance is not None: 

587 variance = MultibandImage.fromImages(filters, variance) 

588 self._image = image 

589 self._mask = mask 

590 self._variance = variance 

591 

592 self._singles = self._buildSingles(self._image, self._mask, self._variance) 

593 self._bbox = self.singles[0].getBBox() 

594 

595 def setXY0(self, xy0): 

596 """Shift the bounding box but keep the same Extent 

597 This is different than `MultibandBase.setXY0` 

598 because the multiband `image`, `mask`, and `variance` objects 

599 must all have their bounding boxes updated. 

600 Parameters 

601 ---------- 

602 xy0 : `Point2I` 

603 New minimum bounds of the bounding box 

604 """ 

605 super().setXY0(xy0) 

606 self.image.setXY0(xy0) 

607 if self.mask is not None: 

608 self.mask.setXY0(xy0) 

609 if self.variance is not None: 

610 self.variance.setXY0(xy0) 

611 

612 def shiftedTo(self, xy0): 

613 """Shift the bounding box but keep the same Extent 

614 

615 This is different than `MultibandBase.shiftedTo` 

616 because the multiband `image`, `mask`, and `variance` objects 

617 must all have their bounding boxes updated. 

618 

619 Parameters 

620 ---------- 

621 xy0 : `Point2I` 

622 New minimum bounds of the bounding box 

623 

624 Returns 

625 ------- 

626 result : `MultibandBase` 

627 A copy of the object, shifted to `xy0`. 

628 """ 

629 raise NotImplementedError("shiftedTo not implemented until DM-10781") 

630 result = self.clone(False) 

631 result._image = result.image.shiftedTo(xy0) 

632 if self.mask is not None: 

633 result._mask = result.mask.shiftedTo(xy0) 

634 if self.variance is not None: 

635 result._variance = result.variance.shiftedTo(xy0) 

636 result._bbox = result.image.getBBox() 

637 return result 

638 

639 def clone(self, deep=True): 

640 """Make a copy of the current instance 

641 """ 

642 image = self.image.clone(deep) 

643 if self.mask is not None: 

644 mask = self.mask.clone(deep) 

645 else: 

646 mask = None 

647 if self.variance is not None: 

648 variance = self.variance.clone(deep) 

649 else: 

650 variance = None 

651 return type(self)(self.filters, image, mask, variance) 

652 

653 def _slice(self, filters, filterIndex, indices): 

654 """Slice the current object and return the result 

655 

656 See `Multiband._slice` for a list of the parameters. 

657 """ 

658 image = self.image._slice(filters, filterIndex, indices) 

659 if self.mask is not None: 

660 mask = self._mask._slice(filters, filterIndex, indices) 

661 else: 

662 mask = None 

663 if self.variance is not None: 

664 variance = self._variance._slice(filters, filterIndex, indices) 

665 else: 

666 variance = None 

667 

668 # If only a single pixel is selected, return the tuple of MultibandPixels 

669 if isinstance(image, MultibandPixel): 

670 if mask is not None: 

671 assert isinstance(mask, MultibandPixel) 

672 if variance is not None: 

673 assert isinstance(variance, MultibandPixel) 

674 return (image, mask, variance) 

675 

676 result = type(self)(filters=filters, image=image, mask=mask, variance=variance) 

677 assert all([r.getBBox() == result._bbox for r in [result._mask, result._variance]]) 

678 return result 

679 

680 def _verifyUpdate(self, image=None, mask=None, variance=None): 

681 """Check that the new image, mask, or variance is valid 

682 

683 This basically means checking that the update to the 

684 property matches the current bounding box and inherits 

685 from the `MultibandBase` class. 

686 """ 

687 for prop in [image, mask, variance]: 

688 if prop is not None: 

689 if prop.getBBox() != self.getBBox(): 

690 raise ValueError("Bounding box does not match the current class") 

691 if not isinstance(prop, MultibandBase): 

692 err = "image, mask, and variance should all inherit from the MultibandBase class" 

693 raise ValueError(err) 

694 

695 @property 

696 def image(self): 

697 """The image of the MultibandMaskedImage""" 

698 return self._image 

699 

700 @property 

701 def mask(self): 

702 """The mask of the MultibandMaskedImage""" 

703 return self._mask 

704 

705 @property 

706 def variance(self): 

707 """The variance of the MultibandMaskedImage""" 

708 return self._variance 

709 

710 def getBBox(self, origin=PARENT): 

711 """Bounding box 

712 """ 

713 if origin == PARENT: 

714 return self._bbox 

715 elif origin == LOCAL: 

716 return Box2I(Point2I(0, 0), self._bbox.getDimensions()) 

717 raise ValueError("Unrecognized origin, expected either PARENT or LOCAL") 

718 

719 

720def tripleFromSingles(cls, filters, singles, **kwargs): 

721 """Construct a MultibandTriple from a collection of single band objects 

722 

723 Parameters 

724 ---------- 

725 filters : `list` 

726 List of filter names. 

727 singles : `list` 

728 A list of single band objects. 

729 If `array` is not `None`, then `singles` is ignored 

730 """ 

731 if not np.all([single.getBBox() == singles[0].getBBox() for single in singles[1:]]): 

732 raise ValueError("Single band images did not all have the same bounding box") 

733 image = MultibandImage.fromImages(filters, [s.image for s in singles]) 

734 mask = MultibandMask.fromMasks(filters, [s.mask for s in singles]) 

735 variance = MultibandImage.fromImages(filters, [s.variance for s in singles]) 

736 return cls(filters, image, mask, variance, **kwargs) 

737 

738 

739def tripleFromArrays(cls, filters, image, mask, variance, bbox=None): 

740 """Construct a MultibandTriple from a set of arrays 

741 

742 Parameters 

743 ---------- 

744 filters : `list` 

745 List of filter names. 

746 image : array 

747 Array of image values 

748 mask : array 

749 Array of mask values 

750 variance : array 

751 Array of variance values 

752 bbox : `Box2I` 

753 Location of the array in a larger single band image. 

754 This argument is ignored if `singles` is not `None`. 

755 """ 

756 if bbox is None: 

757 bbox = Box2I(Point2I(0, 0), Extent2I(image.shape[1], image.shape[0])) 

758 mImage = MultibandImage(filters, image, bbox) 

759 if mask is not None: 

760 mMask = MultibandMask(filters, mask, bbox) 

761 else: 

762 mMask = None 

763 if variance is not None: 

764 mVariance = MultibandImage(filters, variance, bbox) 

765 else: 

766 mVariance = None 

767 return cls(filters, mImage, mMask, mVariance) 

768 

769 

770def makeTripleFromKwargs(cls, filters, filterKwargs, singleType, **kwargs): 

771 """Build a MultibandImage from a set of keyword arguments 

772 

773 Parameters 

774 ---------- 

775 filters : `list` 

776 List of filter names. 

777 singleType : `class` 

778 Class of the single band objects. 

779 This is ignored unless `singles` and `array` 

780 are both `None`, in which case it is required. 

781 filterKwargs : `dict` 

782 Keyword arguments to initialize a new instance of an inherited class 

783 that are different for each filter. 

784 The keys are the names of the arguments and the values 

785 should also be dictionaries, with filter names as keys 

786 and the value of the argument for a given filter as values. 

787 kwargs : `dict` 

788 Keyword arguments to initialize a new instance of an inherited 

789 class that are the same in all bands. 

790 """ 

791 # Attempt to load a set of images 

792 singles = [] 

793 for f in filters: 

794 if filterKwargs is not None: 

795 for key, value in filterKwargs: 

796 kwargs[key] = value[f] 

797 singles.append(singleType(**kwargs)) 

798 return tripleFromSingles(cls, filters, singles) 

799 

800 

801class MultibandMaskedImage(MultibandTripleBase): 

802 """MultibandMaskedImage class 

803 

804 This class acts as a container for multiple `afw.MaskedImage` objects. 

805 All masked images must have the same bounding box, and the associated 

806 images must all have the same data type. 

807 The `image`, `mask`, and `variance` are all stored separately into 

808 a `MultibandImage`, `MultibandMask`, and `MultibandImage` respectively, 

809 which each have their own internal 3D arrays (filter, y, x). 

810 

811 See `MultibandTripleBase` for parameter definitions. 

812 """ 

813 def __init__(self, filters, image=None, mask=None, variance=None): 

814 super().__init__(filters, image, mask, variance) 

815 

816 @staticmethod 

817 def fromImages(filters, singles): 

818 """Construct a MultibandImage from a collection of single band images 

819 

820 see `tripleFromImages` for a description of parameters 

821 """ 

822 return tripleFromSingles(MultibandMaskedImage, filters, singles) 

823 

824 @staticmethod 

825 def fromArrays(filters, image, mask, variance, bbox=None): 

826 """Construct a MultibandMaskedImage from a collection of arrays 

827 

828 see `tripleFromArrays` for a description of parameters 

829 """ 

830 return tripleFromArrays(MultibandMaskedImage, filters, image, mask, variance, bbox) 

831 

832 @staticmethod 

833 def fromKwargs(filters, filterKwargs, singleType=MaskedImageF, **kwargs): 

834 """Build a MultibandImage from a set of keyword arguments 

835 

836 see `makeTripleFromKwargs` for a description of parameters 

837 """ 

838 return makeTripleFromKwargs(MultibandMaskedImage, filters, filterKwargs, singleType, **kwargs) 

839 

840 def _buildSingles(self, image=None, mask=None, variance=None): 

841 """Make a new list of single band objects 

842 

843 Parameters 

844 ---------- 

845 image : `MultibandImage` 

846 `MultibandImage` object that represent the image in each band. 

847 mask : `MultibandMask` 

848 `MultibandMask` object that represent the mask in each band. 

849 variance : `MultibandImage` 

850 `MultibandImage` object that represent the variance in each band. 

851 

852 Returns 

853 ------- 

854 singles : `tuple` 

855 Tuple of `MaskedImage` objects for each band, 

856 where the `image`, `mask`, and `variance` of each `single` 

857 point to the multiband objects. 

858 """ 

859 singles = [] 

860 if image is None: 

861 image = self.image 

862 if mask is None: 

863 mask = self.mask 

864 if variance is None: 

865 variance = self.variance 

866 

867 dtype = image.array.dtype 

868 singles = [] 

869 for f in self.filters: 

870 _image = image[f] 

871 if mask is not None: 

872 _mask = mask[f] 

873 else: 

874 _mask = None 

875 if variance is not None: 

876 _variance = variance[f] 

877 else: 

878 _variance = None 

879 singles.append(MaskedImage(image=_image, mask=_mask, variance=_variance, dtype=dtype)) 

880 return tuple(singles)