Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# 

2# LSST Data Management System 

3# 

4# Copyright 2008-2017 AURA/LSST. 

5# 

6# This product includes software developed by the 

7# LSST Project (http://www.lsst.org/). 

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 LSST License Statement and 

20# the GNU General Public License along with this program. If not, 

21# see <https://www.lsstcorp.org/LegalNotices/>. 

22# 

23"""Support for image defects""" 

24 

25__all__ = ("Defects",) 

26 

27import logging 

28import itertools 

29import collections.abc 

30import contextlib 

31import numpy as np 

32import copy 

33import datetime 

34import math 

35import numbers 

36import os.path 

37import warnings 

38import astropy.table 

39 

40import lsst.geom 

41import lsst.afw.table 

42import lsst.afw.detection 

43import lsst.afw.image 

44import lsst.afw.geom 

45from lsst.daf.base import PropertyList 

46 

47from . import Defect 

48 

49log = logging.getLogger(__name__) 

50 

51SCHEMA_NAME_KEY = "DEFECTS_SCHEMA" 

52SCHEMA_VERSION_KEY = "DEFECTS_SCHEMA_VERSION" 

53 

54 

55class Defects(collections.abc.MutableSequence): 

56 """Collection of `lsst.meas.algorithms.Defect`. 

57 

58 Parameters 

59 ---------- 

60 defectList : iterable of `lsst.meas.algorithms.Defect` 

61 or `lsst.geom.BoxI`, optional 

62 Collections of defects to apply to the image. 

63 metadata : `lsst.daf.base.PropertyList`, optional 

64 Metadata to associate with the defects. Will be copied and 

65 overwrite existing metadata, if any. If not supplied the existing 

66 metadata will be reset. 

67 normalize_on_init : `bool` 

68 If True, normalization is applied to the defects in ``defectList`` to 

69 remove duplicates, eliminate overlaps, etc. 

70 

71 Notes 

72 ----- 

73 Defects are stored within this collection in a "reduced" or "normalized" 

74 form: rather than simply storing the bounding boxes which are added to the 

75 collection, we eliminate overlaps and duplicates. This normalization 

76 procedure may introduce overhead when adding many new defects; it may be 

77 temporarily disabled using the `Defects.bulk_update` context manager if 

78 necessary. 

79 """ 

80 

81 _OBSTYPE = "defects" 

82 """The calibration type used for ingest.""" 

83 

84 def __init__(self, defectList=None, metadata=None, *, normalize_on_init=True): 

85 self._defects = [] 

86 

87 if defectList is not None: 

88 self._bulk_update = True 

89 for d in defectList: 

90 self.append(d) 

91 self._bulk_update = False 

92 

93 if normalize_on_init: 

94 self._normalize() 

95 

96 if metadata is not None: 96 ↛ 97line 96 didn't jump to line 97, because the condition on line 96 was never true

97 self._metadata = metadata 

98 else: 

99 self.setMetadata() 

100 

101 def _check_value(self, value): 

102 """Check that the supplied value is a `~lsst.meas.algorithms.Defect` 

103 or can be converted to one. 

104 

105 Parameters 

106 ---------- 

107 value : `object` 

108 Value to check. 

109 

110 Returns 

111 ------- 

112 new : `~lsst.meas.algorithms.Defect` 

113 Either the supplied value or a new object derived from it. 

114 

115 Raises 

116 ------ 

117 ValueError 

118 Raised if the supplied value can not be converted to 

119 `~lsst.meas.algorithms.Defect` 

120 """ 

121 if isinstance(value, Defect): 

122 pass 

123 elif isinstance(value, lsst.geom.BoxI): 

124 value = Defect(value) 

125 elif isinstance(value, lsst.geom.PointI): 

126 value = Defect(lsst.geom.Box2I(value, lsst.geom.Extent2I(1, 1))) 

127 elif isinstance(value, lsst.afw.image.DefectBase): 

128 value = Defect(value.getBBox()) 

129 else: 

130 raise ValueError(f"Defects must be of type Defect, BoxI, or PointI, not '{value!r}'") 

131 return value 

132 

133 def __len__(self): 

134 return len(self._defects) 

135 

136 def __getitem__(self, index): 

137 return self._defects[index] 

138 

139 def __setitem__(self, index, value): 

140 """Can be given a `~lsst.meas.algorithms.Defect` or a `lsst.geom.BoxI` 

141 """ 

142 self._defects[index] = self._check_value(value) 

143 self._normalize() 

144 

145 def __iter__(self): 

146 return iter(self._defects) 

147 

148 def __delitem__(self, index): 

149 del self._defects[index] 

150 

151 def __eq__(self, other): 

152 """Compare if two `Defects` are equal. 

153 

154 Two `Defects` are equal if their bounding boxes are equal and in 

155 the same order. Metadata content is ignored. 

156 """ 

157 if not isinstance(other, self.__class__): 157 ↛ 158line 157 didn't jump to line 158, because the condition on line 157 was never true

158 return False 

159 

160 # checking the bboxes with zip() only works if same length 

161 if len(self) != len(other): 161 ↛ 162line 161 didn't jump to line 162, because the condition on line 161 was never true

162 return False 

163 

164 # Assume equal if bounding boxes are equal 

165 for d1, d2 in zip(self, other): 

166 if d1.getBBox() != d2.getBBox(): 166 ↛ 167line 166 didn't jump to line 167, because the condition on line 166 was never true

167 return False 

168 

169 return True 

170 

171 def __str__(self): 

172 return "Defects(" + ",".join(str(d.getBBox()) for d in self) + ")" 

173 

174 def _normalize(self): 

175 """Recalculate defect bounding boxes for efficiency. 

176 

177 Notes 

178 ----- 

179 Ideally, this would generate the provably-minimal set of bounding 

180 boxes necessary to represent the defects. At present, however, that 

181 doesn't happen: see DM-24781. In the cases of substantial overlaps or 

182 duplication, though, this will produce a much reduced set. 

183 """ 

184 # In bulk-update mode, normalization is a no-op. 

185 if self._bulk_update: 

186 return 

187 

188 # work out the minimum and maximum bounds from all defect regions. 

189 minX, minY, maxX, maxY = float('inf'), float('inf'), float('-inf'), float('-inf') 

190 for defect in self: 

191 bbox = defect.getBBox() 

192 minX = min(minX, bbox.getMinX()) 

193 minY = min(minY, bbox.getMinY()) 

194 maxX = max(maxX, bbox.getMaxX()) 

195 maxY = max(maxY, bbox.getMaxY()) 

196 

197 region = lsst.geom.Box2I(lsst.geom.Point2I(minX, minY), 

198 lsst.geom.Point2I(maxX, maxY)) 

199 

200 mi = lsst.afw.image.MaskedImageF(region) 

201 self.maskPixels(mi, maskName="BAD") 

202 self._defects = Defects.fromMask(mi, "BAD")._defects 

203 

204 @contextlib.contextmanager 

205 def bulk_update(self): 

206 """Temporarily suspend normalization of the defect list. 

207 """ 

208 self._bulk_update = True 

209 try: 

210 yield 

211 finally: 

212 self._bulk_update = False 

213 self._normalize() 

214 

215 def insert(self, index, value): 

216 self._defects.insert(index, self._check_value(value)) 

217 self._normalize() 

218 

219 def getMetadata(self): 

220 """Retrieve metadata associated with these `Defects`. 

221 

222 Returns 

223 ------- 

224 meta : `lsst.daf.base.PropertyList` 

225 Metadata. The returned `~lsst.daf.base.PropertyList` can be 

226 modified by the caller and the changes will be written to 

227 external files. 

228 """ 

229 return self._metadata 

230 

231 def setMetadata(self, metadata=None): 

232 """Store a copy of the supplied metadata with the defects. 

233 

234 Parameters 

235 ---------- 

236 metadata : `lsst.daf.base.PropertyList`, optional 

237 Metadata to associate with the defects. Will be copied and 

238 overwrite existing metadata. If not supplied the existing 

239 metadata will be reset. 

240 """ 

241 if metadata is None: 

242 self._metadata = PropertyList() 

243 else: 

244 self._metadata = copy.copy(metadata) 

245 

246 # Ensure that we have the obs type required by calibration ingest 

247 self._metadata["OBSTYPE"] = self._OBSTYPE 

248 

249 def copy(self): 

250 """Copy the defects to a new list, creating new defects from the 

251 bounding boxes. 

252 

253 Returns 

254 ------- 

255 new : `Defects` 

256 New list with new `Defect` entries. 

257 

258 Notes 

259 ----- 

260 This is not a shallow copy in that new `Defect` instances are 

261 created from the original bounding boxes. It's also not a deep 

262 copy since the bounding boxes are not recreated. 

263 """ 

264 return self.__class__(d.getBBox() for d in self) 

265 

266 def transpose(self): 

267 """Make a transposed copy of this defect list. 

268 

269 Returns 

270 ------- 

271 retDefectList : `Defects` 

272 Transposed list of defects. 

273 """ 

274 retDefectList = self.__class__() 

275 for defect in self: 

276 bbox = defect.getBBox() 

277 dimensions = bbox.getDimensions() 

278 nbbox = lsst.geom.Box2I(lsst.geom.Point2I(bbox.getMinY(), bbox.getMinX()), 

279 lsst.geom.Extent2I(dimensions[1], dimensions[0])) 

280 retDefectList.append(nbbox) 

281 return retDefectList 

282 

283 def maskPixels(self, maskedImage, maskName="BAD"): 

284 """Set mask plane based on these defects. 

285 

286 Parameters 

287 ---------- 

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

289 Image to process. Only the mask plane is updated. 

290 maskName : str, optional 

291 Mask plane name to use. 

292 """ 

293 # mask bad pixels 

294 mask = maskedImage.getMask() 

295 bitmask = mask.getPlaneBitMask(maskName) 

296 for defect in self: 

297 bbox = defect.getBBox() 

298 lsst.afw.geom.SpanSet(bbox).clippedTo(mask.getBBox()).setMask(mask, bitmask) 

299 

300 def toFitsRegionTable(self): 

301 """Convert defect list to `~lsst.afw.table.BaseCatalog` using the 

302 FITS region standard. 

303 

304 Returns 

305 ------- 

306 table : `lsst.afw.table.BaseCatalog` 

307 Defects in tabular form. 

308 

309 Notes 

310 ----- 

311 The table created uses the 

312 `FITS regions <https://fits.gsfc.nasa.gov/registry/region.html>`_ 

313 definition tabular format. The ``X`` and ``Y`` coordinates are 

314 converted to FITS Physical coordinates that have origin pixel (1, 1) 

315 rather than the (0, 0) used in LSST software. 

316 """ 

317 

318 nrows = len(self._defects) 

319 

320 schema = lsst.afw.table.Schema() 

321 x = schema.addField("X", type="D", units="pix", doc="X coordinate of center of shape") 

322 y = schema.addField("Y", type="D", units="pix", doc="Y coordinate of center of shape") 

323 shape = schema.addField("SHAPE", type="String", size=16, doc="Shape defined by these values") 

324 r = schema.addField("R", type="ArrayD", size=2, units="pix", doc="Extents") 

325 rotang = schema.addField("ROTANG", type="D", units="deg", doc="Rotation angle") 

326 component = schema.addField("COMPONENT", type="I", doc="Index of this region") 

327 table = lsst.afw.table.BaseCatalog(schema) 

328 table.resize(nrows) 

329 

330 if nrows: 330 ↛ 367line 330 didn't jump to line 367, because the condition on line 330 was never false

331 # Adding entire columns is more efficient than adding 

332 # each element separately 

333 xCol = [] 

334 yCol = [] 

335 rCol = [] 

336 

337 for i, defect in enumerate(self._defects): 

338 box = defect.getBBox() 

339 center = box.getCenter() 

340 # Correct for the FITS 1-based offset 

341 xCol.append(center.getX() + 1.0) 

342 yCol.append(center.getY() + 1.0) 

343 

344 width = box.width 

345 height = box.height 

346 

347 if width == 1 and height == 1: 

348 # Call this a point 

349 shapeType = "POINT" 

350 else: 

351 shapeType = "BOX" 

352 

353 # Strings have to be added per row 

354 table[i][shape] = shapeType 

355 

356 rCol.append(np.array([width, height], dtype=np.float64)) 

357 

358 # Assign the columns 

359 table[x] = np.array(xCol, dtype=np.float64) 

360 table[y] = np.array(yCol, dtype=np.float64) 

361 

362 table[r] = np.array(rCol) 

363 table[rotang] = np.zeros(nrows, dtype=np.float64) 

364 table[component] = np.arange(nrows) 

365 

366 # Set some metadata in the table (force OBSTYPE to exist) 

367 metadata = copy.copy(self.getMetadata()) 

368 metadata["OBSTYPE"] = self._OBSTYPE 

369 metadata[SCHEMA_NAME_KEY] = "FITS Region" 

370 metadata[SCHEMA_VERSION_KEY] = 1 

371 table.setMetadata(metadata) 

372 

373 return table 

374 

375 def writeFits(self, *args): 

376 """Write defect list to FITS. 

377 

378 Parameters 

379 ---------- 

380 *args 

381 Arguments to be forwarded to 

382 `lsst.afw.table.BaseCatalog.writeFits`. 

383 """ 

384 table = self.toFitsRegionTable() 

385 

386 # Add some additional headers useful for tracking purposes 

387 metadata = table.getMetadata() 

388 now = datetime.datetime.utcnow() 

389 metadata["DATE"] = now.isoformat() 

390 metadata["CALIB_CREATION_DATE"] = now.strftime("%Y-%m-%d") 

391 metadata["CALIB_CREATION_TIME"] = now.strftime("%T %Z").strip() 

392 

393 table.writeFits(*args) 

394 

395 def toSimpleTable(self): 

396 """Convert defects to a simple table form that we use to write 

397 to text files. 

398 

399 Returns 

400 ------- 

401 table : `lsst.afw.table.BaseCatalog` 

402 Defects in simple tabular form. 

403 

404 Notes 

405 ----- 

406 These defect tables are used as the human readable definitions 

407 of defects in calibration data definition repositories. The format 

408 is to use four columns defined as follows: 

409 

410 x0 : `int` 

411 X coordinate of bottom left corner of box. 

412 y0 : `int` 

413 Y coordinate of bottom left corner of box. 

414 width : `int` 

415 X extent of the box. 

416 height : `int` 

417 Y extent of the box. 

418 """ 

419 schema = lsst.afw.table.Schema() 

420 x = schema.addField("x0", type="I", units="pix", 

421 doc="X coordinate of bottom left corner of box") 

422 y = schema.addField("y0", type="I", units="pix", 

423 doc="Y coordinate of bottom left corner of box") 

424 width = schema.addField("width", type="I", units="pix", 

425 doc="X extent of box") 

426 height = schema.addField("height", type="I", units="pix", 

427 doc="Y extent of box") 

428 table = lsst.afw.table.BaseCatalog(schema) 

429 

430 nrows = len(self._defects) 

431 table.resize(nrows) 

432 

433 if nrows: 433 ↛ 453line 433 didn't jump to line 453, because the condition on line 433 was never false

434 

435 xCol = [] 

436 yCol = [] 

437 widthCol = [] 

438 heightCol = [] 

439 

440 for defect in self._defects: 

441 box = defect.getBBox() 

442 xCol.append(box.getBeginX()) 

443 yCol.append(box.getBeginY()) 

444 widthCol.append(box.getWidth()) 

445 heightCol.append(box.getHeight()) 

446 

447 table[x] = np.array(xCol, dtype=np.int64) 

448 table[y] = np.array(yCol, dtype=np.int64) 

449 table[width] = np.array(widthCol, dtype=np.int64) 

450 table[height] = np.array(heightCol, dtype=np.int64) 

451 

452 # Set some metadata in the table (force OBSTYPE to exist) 

453 metadata = copy.copy(self.getMetadata()) 

454 metadata["OBSTYPE"] = self._OBSTYPE 

455 metadata[SCHEMA_NAME_KEY] = "Simple" 

456 metadata[SCHEMA_VERSION_KEY] = 1 

457 table.setMetadata(metadata) 

458 

459 return table 

460 

461 def writeText(self, filename): 

462 """Write the defects out to a text file with the specified name. 

463 

464 Parameters 

465 ---------- 

466 filename : `str` 

467 Name of the file to write. The file extension ".ecsv" will 

468 always be used. 

469 

470 Returns 

471 ------- 

472 used : `str` 

473 The name of the file used to write the data (which may be 

474 different from the supplied name given the change to file 

475 extension). 

476 

477 Notes 

478 ----- 

479 The file is written to ECSV format and will include any metadata 

480 associated with the `Defects`. 

481 """ 

482 

483 # Using astropy table is the easiest way to serialize to ecsv 

484 afwTable = self.toSimpleTable() 

485 table = afwTable.asAstropy() 

486 

487 metadata = afwTable.getMetadata() 

488 now = datetime.datetime.utcnow() 

489 metadata["DATE"] = now.isoformat() 

490 metadata["CALIB_CREATION_DATE"] = now.strftime("%Y-%m-%d") 

491 metadata["CALIB_CREATION_TIME"] = now.strftime("%T %Z").strip() 

492 

493 table.meta = metadata.toDict() 

494 

495 # Force file extension to .ecsv 

496 path, ext = os.path.splitext(filename) 

497 filename = path + ".ecsv" 

498 table.write(filename, format="ascii.ecsv") 

499 return filename 

500 

501 @staticmethod 

502 def _get_values(values, n=1): 

503 """Retrieve N values from the supplied values. 

504 

505 Parameters 

506 ---------- 

507 values : `numbers.Number` or `list` or `np.array` 

508 Input values. 

509 n : `int` 

510 Number of values to retrieve. 

511 

512 Returns 

513 ------- 

514 vals : `list` or `np.array` or `numbers.Number` 

515 Single value from supplied list if ``n`` is 1, or `list` 

516 containing first ``n`` values from supplied values. 

517 

518 Notes 

519 ----- 

520 Some supplied tables have vectors in some columns that can also 

521 be scalars. This method can be used to get the first number as 

522 a scalar or the first N items from a vector as a vector. 

523 """ 

524 if n == 1: 

525 if isinstance(values, numbers.Number): 

526 return values 

527 else: 

528 return values[0] 

529 

530 return values[:n] 

531 

532 @classmethod 

533 def fromTable(cls, table): 

534 """Construct a `Defects` from the contents of a 

535 `~lsst.afw.table.BaseCatalog`. 

536 

537 Parameters 

538 ---------- 

539 table : `lsst.afw.table.BaseCatalog` 

540 Table with one row per defect. 

541 

542 Returns 

543 ------- 

544 defects : `Defects` 

545 A `Defects` list. 

546 

547 Notes 

548 ----- 

549 Two table formats are recognized. The first is the 

550 `FITS regions <https://fits.gsfc.nasa.gov/registry/region.html>`_ 

551 definition tabular format written by `toFitsRegionTable` where the 

552 pixel origin is corrected from FITS 1-based to a 0-based origin. 

553 The second is the legacy defects format using columns ``x0``, ``y0`` 

554 (bottom left hand pixel of box in 0-based coordinates), ``width`` 

555 and ``height``. 

556 

557 The FITS standard regions can only read BOX, POINT, or ROTBOX with 

558 a zero degree rotation. 

559 """ 

560 

561 defectList = [] 

562 

563 schema = table.getSchema() 

564 

565 # Check schema to see which definitions we have 

566 if "X" in schema and "Y" in schema and "R" in schema and "SHAPE" in schema: 

567 # This is a FITS region style table 

568 isFitsRegion = True 

569 

570 # Preselect the keys 

571 xKey = schema["X"].asKey() 

572 yKey = schema["Y"].asKey() 

573 shapeKey = schema["SHAPE"].asKey() 

574 rKey = schema["R"].asKey() 

575 rotangKey = schema["ROTANG"].asKey() 

576 

577 elif "x0" in schema and "y0" in schema and "width" in schema and "height" in schema: 577 ↛ 588line 577 didn't jump to line 588, because the condition on line 577 was never false

578 # This is a classic LSST-style defect table 

579 isFitsRegion = False 

580 

581 # Preselect the keys 

582 xKey = schema["x0"].asKey() 

583 yKey = schema["y0"].asKey() 

584 widthKey = schema["width"].asKey() 

585 heightKey = schema["height"].asKey() 

586 

587 else: 

588 raise ValueError("Unsupported schema for defects extraction") 

589 

590 for record in table: 

591 

592 if isFitsRegion: 

593 # Coordinates can be arrays (some shapes in the standard 

594 # require this) 

595 # Correct for FITS 1-based origin 

596 xcen = cls._get_values(record[xKey]) - 1.0 

597 ycen = cls._get_values(record[yKey]) - 1.0 

598 shape = record[shapeKey].upper() 

599 if shape == "BOX": 

600 box = lsst.geom.Box2I.makeCenteredBox(lsst.geom.Point2D(xcen, ycen), 

601 lsst.geom.Extent2I(cls._get_values(record[rKey], 

602 n=2))) 

603 elif shape == "POINT": 

604 # Handle the case where we have an externally created 

605 # FITS file. 

606 box = lsst.geom.Point2I(xcen, ycen) 

607 elif shape == "ROTBOX": 

608 # Astropy regions always writes ROTBOX 

609 rotang = cls._get_values(record[rotangKey]) 

610 # We can support 0 or 90 deg 

611 if math.isclose(rotang % 90.0, 0.0): 611 ↛ 613line 611 didn't jump to line 613, because the condition on line 611 was never true

612 # Two values required 

613 r = cls._get_values(record[rKey], n=2) 

614 if math.isclose(rotang % 180.0, 0.0): 

615 width = r[0] 

616 height = r[1] 

617 else: 

618 width = r[1] 

619 height = r[0] 

620 box = lsst.geom.Box2I.makeCenteredBox(lsst.geom.Point2D(xcen, ycen), 

621 lsst.geom.Extent2I(width, height)) 

622 else: 

623 log.warning("Defect can not be defined using ROTBOX with non-aligned rotation angle") 

624 continue 

625 else: 

626 log.warning("Defect lists can only be defined using BOX or POINT not %s", shape) 

627 continue 

628 

629 else: 

630 # This is a classic LSST-style defect table 

631 box = lsst.geom.Box2I(lsst.geom.Point2I(record[xKey], record[yKey]), 

632 lsst.geom.Extent2I(record[widthKey], record[heightKey])) 

633 

634 defectList.append(box) 

635 

636 defects = cls(defectList) 

637 defects.setMetadata(table.getMetadata()) 

638 

639 # Once read, the schema headers are irrelevant 

640 metadata = defects.getMetadata() 

641 for k in (SCHEMA_NAME_KEY, SCHEMA_VERSION_KEY): 

642 if k in metadata: 

643 del metadata[k] 

644 

645 return defects 

646 

647 @classmethod 

648 def readFits(cls, *args): 

649 """Read defect list from FITS table. 

650 

651 Parameters 

652 ---------- 

653 *args 

654 Arguments to be forwarded to 

655 `lsst.afw.table.BaseCatalog.writeFits`. 

656 

657 Returns 

658 ------- 

659 defects : `Defects` 

660 Defects read from a FITS table. 

661 """ 

662 table = lsst.afw.table.BaseCatalog.readFits(*args) 

663 return cls.fromTable(table) 

664 

665 @classmethod 

666 def readText(cls, filename): 

667 """Read defect list from standard format text table file. 

668 

669 Parameters 

670 ---------- 

671 filename : `str` 

672 Name of the file containing the defects definitions. 

673 

674 Returns 

675 ------- 

676 defects : `Defects` 

677 Defects read from a FITS table. 

678 """ 

679 with warnings.catch_warnings(): 

680 # Squash warnings due to astropy failure to close files; we think 

681 # this is a real problem, but the warnings are even worse. 

682 # https://github.com/astropy/astropy/issues/8673 

683 warnings.filterwarnings("ignore", category=ResourceWarning, module="astropy.io.ascii") 

684 table = astropy.table.Table.read(filename) 

685 

686 # Need to convert the Astropy table to afw table 

687 schema = lsst.afw.table.Schema() 

688 for colName in table.columns: 

689 schema.addField(colName, units=str(table[colName].unit), 

690 type=table[colName].dtype.type) 

691 

692 # Create AFW table that is required by fromTable() 

693 afwTable = lsst.afw.table.BaseCatalog(schema) 

694 

695 afwTable.resize(len(table)) 

696 for colName in table.columns: 

697 # String columns will fail -- currently we do not expect any 

698 afwTable[colName] = table[colName] 

699 

700 # Copy in the metadata from the astropy table 

701 metadata = PropertyList() 

702 for k, v in table.meta.items(): 

703 metadata[k] = v 

704 afwTable.setMetadata(metadata) 

705 

706 # Extract defect information from the table itself 

707 return cls.fromTable(afwTable) 

708 

709 @classmethod 

710 def readLsstDefectsFile(cls, filename): 

711 """Read defects information from a legacy LSST format text file. 

712 

713 Parameters 

714 ---------- 

715 filename : `str` 

716 Name of text file containing the defect information. 

717 

718 Returns 

719 ------- 

720 defects : `Defects` 

721 The defects. 

722 

723 Notes 

724 ----- 

725 These defect text files are used as the human readable definitions 

726 of defects in calibration data definition repositories. The format 

727 is to use four columns defined as follows: 

728 

729 x0 : `int` 

730 X coordinate of bottom left corner of box. 

731 y0 : `int` 

732 Y coordinate of bottom left corner of box. 

733 width : `int` 

734 X extent of the box. 

735 height : `int` 

736 Y extent of the box. 

737 

738 Files of this format were used historically to represent defects 

739 in simple text form. Use `Defects.readText` and `Defects.writeText` 

740 to use the more modern format. 

741 """ 

742 # Use loadtxt so that ValueError is thrown if the file contains a 

743 # non-integer value. genfromtxt converts bad values to -1. 

744 defect_array = np.loadtxt(filename, 

745 dtype=[("x0", "int"), ("y0", "int"), 

746 ("x_extent", "int"), ("y_extent", "int")]) 

747 

748 return cls(lsst.geom.Box2I(lsst.geom.Point2I(row["x0"], row["y0"]), 

749 lsst.geom.Extent2I(row["x_extent"], row["y_extent"])) 

750 for row in defect_array) 

751 

752 @classmethod 

753 def fromFootprintList(cls, fpList): 

754 """Compute a defect list from a footprint list, optionally growing 

755 the footprints. 

756 

757 Parameters 

758 ---------- 

759 fpList : `list` of `lsst.afw.detection.Footprint` 

760 Footprint list to process. 

761 

762 Returns 

763 ------- 

764 defects : `Defects` 

765 List of defects. 

766 """ 

767 # normalize_on_init is set to False to avoid recursively calling 

768 # fromMask/fromFootprintList in Defects.__init__. 

769 return cls(itertools.chain.from_iterable(lsst.afw.detection.footprintToBBoxList(fp) 

770 for fp in fpList), normalize_on_init=False) 

771 

772 @classmethod 

773 def fromMask(cls, maskedImage, maskName): 

774 """Compute a defect list from a specified mask plane. 

775 

776 Parameters 

777 ---------- 

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

779 Image to process. 

780 maskName : `str` or `list` 

781 Mask plane name, or list of names to convert. 

782 

783 Returns 

784 ------- 

785 defects : `Defects` 

786 Defect list constructed from masked pixels. 

787 """ 

788 mask = maskedImage.getMask() 

789 thresh = lsst.afw.detection.Threshold(mask.getPlaneBitMask(maskName), 

790 lsst.afw.detection.Threshold.BITMASK) 

791 fpList = lsst.afw.detection.FootprintSet(mask, thresh).getFootprints() 

792 return cls.fromFootprintList(fpList)