Coverage for tests/test_mask.py: 93%

307 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-16 15:24 -0700

1# This file is part of lsst-images. 

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# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14import dataclasses 

15import os 

16from typing import Any 

17 

18import astropy.io.fits 

19import numpy as np 

20import pytest 

21 

22import lsst.utils.tests 

23from lsst.images import ( 

24 Box, 

25 Mask, 

26 MaskPlane, 

27 MaskSchema, 

28 get_legacy_non_cell_coadd_mask_planes, 

29 get_legacy_visit_image_mask_planes, 

30) 

31from lsst.images._mask import _guess_legacy_plane_map 

32from lsst.images.tests import RoundtripFits, assert_masks_equal, compare_mask_to_legacy 

33 

34try: 

35 from lsst.afw.image import MaskedImageReader as LegacyMaskedImageReader 

36 

37except ImportError: 

38 type LegacyMaskedImageReader = Any # type: ignore[no-redef] 

39 

40EXTERNAL_DATA_DIR = os.environ.get("TESTDATA_IMAGES_DIR", None) 

41 

42 

43@dataclasses.dataclass 

44class _LegacyTestData: 

45 mask: Mask 

46 reader: LegacyMaskedImageReader 

47 plane_map: dict[str, MaskPlane] 

48 

49 

50@pytest.fixture(scope="session") 

51def legacy_test_data() -> _LegacyTestData: 

52 """Return a Mask read directly from the legacy test dataset and a legacy 

53 reader for that image. 

54 

55 Skips if TESTDATA_IMAGES_DIR is unset or lsst.afw.image is unavailable. 

56 """ 

57 if EXTERNAL_DATA_DIR is None: 57 ↛ 59line 57 didn't jump to line 59 because the condition on line 57 was always true

58 pytest.skip("TESTDATA_IMAGES_DIR is not in the environment.") 

59 try: 

60 from lsst.afw.image import MaskedImageFitsReader 

61 except ImportError: 

62 pytest.skip("'lsst.afw.image' could not be imported.") 

63 filename = os.path.join(EXTERNAL_DATA_DIR, "dp2", "legacy", "visit_image.fits") 

64 plane_map = get_legacy_visit_image_mask_planes() 

65 mask = Mask.read_legacy(filename, ext=2, plane_map=plane_map) 

66 reader = MaskedImageFitsReader(filename) 

67 return _LegacyTestData(mask=mask, reader=reader, plane_map=plane_map) 

68 

69 

70def make_mask_planes(rng: np.random.Generator, n_planes: int, n_placeholders: int) -> list[MaskPlane | None]: 

71 """Return a shuffled list of MaskPlane objects with placeholder Nones.""" 

72 planes: list[MaskPlane | None] = [] 

73 for i in range(n_planes): 

74 planes.append(MaskPlane(f"M{i}", f"D{i}")) 

75 planes.extend([None] * n_placeholders) 

76 rng.shuffle(planes) 

77 return planes 

78 

79 

80def test_schema() -> None: 

81 """Test MaskSchema construction, accessors, and basic operations.""" 

82 rng = np.random.default_rng(500) 

83 planes = make_mask_planes(rng, 17, 5) 

84 with pytest.raises(TypeError): 

85 MaskSchema.bits_per_element(np.float32) 

86 assert MaskSchema.bits_per_element(np.uint8) == 8 

87 schema = MaskSchema(planes, dtype=np.uint8) 

88 assert list(schema) == planes 

89 assert len(schema) == len(planes) 

90 assert schema[5] == planes[5] 

91 assert eval(repr(schema), {"dtype": np.dtype, "MaskSchema": MaskSchema, "MaskPlane": MaskPlane}) == schema 

92 string = str(schema) 

93 assert len(string.split("\n")) == 17 

94 bit5 = schema.bit("M5") 

95 assert f"M5 [{bit5.index}@{hex(bit5.mask)}]: D5" in string 

96 assert schema == MaskSchema(planes, np.uint8) 

97 assert schema != MaskSchema(planes, np.int16) 

98 assert schema != MaskSchema(planes[:-1], np.uint8) 

99 assert schema.dtype == np.dtype(np.uint8) 

100 assert schema.mask_size == 3 

101 assert schema.names == {f"M{i}" for i in range(17)} 

102 assert schema.descriptions == {f"M{i}": f"D{i}" for i in range(17)} 

103 bit7 = schema.bit("M7") 

104 bitmask57 = schema.bitmask("M5", "M7") 

105 assert bitmask57[bit5.index] & bit5.mask 

106 assert bitmask57[bit7.index] & bit7.mask 

107 bitmask57[bit5.index] &= ~bit5.mask 

108 bitmask57[bit7.index] &= ~bit7.mask 

109 assert not bitmask57.any() 

110 splits = schema.split(np.int16) 

111 assert len(splits) == 2 

112 assert splits[0].mask_size == 1 

113 assert splits[1].mask_size == 1 

114 assert list(splits[0]) + list(splits[1]) == [p for p in planes if p is not None] 

115 assert len(splits[0]) == 15 

116 assert len(splits[1]) == 2 

117 

118 

119def test_schema_from_fits_header() -> None: 

120 """Verify MaskSchema.from_fits_header inverts update_header.""" 

121 planes = [ 

122 MaskPlane("NO_DATA", "No data was available for this pixel."), 

123 MaskPlane("COSMIC_RAY", "A cosmic ray affected this pixel."), 

124 MaskPlane("DETECTED", "Pixel was part of a detected source."), 

125 ] 

126 schema = MaskSchema(planes, dtype=np.uint8) 

127 header = astropy.io.fits.Header() 

128 schema.update_header(header) 

129 result = MaskSchema.from_fits_header(header) 

130 assert result.dtype == np.dtype(np.uint8) 

131 assert list(result) == planes 

132 assert result == schema 

133 

134 

135def test_schema_from_fits_header_preserves_gaps() -> None: 

136 """Verify None placeholders are reconstructed from gaps in MSKN card 

137 numbering. 

138 """ 

139 planes: list[MaskPlane | None] = [MaskPlane("A", "a"), None, MaskPlane("B", "b")] 

140 header = astropy.io.fits.Header() 

141 MaskSchema(planes, dtype=np.uint8).update_header(header) 

142 assert list(MaskSchema.from_fits_header(header)) == planes 

143 

144 

145def test_schema_from_fits_header_requires_cards() -> None: 

146 """Verify MaskSchema.from_fits_header raises ValueError on a header with 

147 no MSKN cards. 

148 """ 

149 with pytest.raises(ValueError): 

150 MaskSchema.from_fits_header(astropy.io.fits.Header()) 

151 

152 

153def test_interpret() -> None: 

154 """Verify interpret returns correct plane names across a multi-byte 

155 schema. 

156 """ 

157 # 3 named planes padded with Nones to exceed 8 bits; A and B land in 

158 # byte 0, C lands in byte 1. 

159 planes: list[MaskPlane | None] = [ 

160 MaskPlane("A", "a"), 

161 MaskPlane("B", "b"), 

162 None, 

163 None, 

164 None, 

165 None, 

166 None, 

167 None, 

168 MaskPlane("C", "c"), 

169 ] 

170 schema = MaskSchema(planes, dtype=np.uint8) 

171 assert schema.mask_size == 2 

172 

173 assert set(schema.interpret(schema.bitmask("A", "C"))) == {"A", "C"} 

174 assert set(schema.interpret(schema.bitmask("B"))) == {"B"} 

175 assert set(schema.interpret(np.zeros(schema.mask_size, dtype=schema.dtype))) == set() 

176 assert set(schema.interpret(schema.bitmask("A", "B", "C"))) == {"A", "B", "C"} 

177 

178 

179def test_basics() -> None: 

180 """Test basic Mask construction, string representation, and error 

181 conditions. 

182 """ 

183 rng = np.random.default_rng(500) 

184 planes = make_mask_planes(rng, 35, n_placeholders=5) 

185 schema = MaskSchema(planes, dtype=np.uint8) 

186 bbox = Box.factory[5:50, 6:60] 

187 mask = Mask( 

188 0, 

189 schema=schema, 

190 bbox=bbox, 

191 metadata={"four_and_a_half": 4.5}, 

192 ) 

193 

194 assert mask[...] is not mask 

195 assert mask.__eq__(42) == NotImplemented 

196 assert mask == mask 

197 assert ( 

198 str(mask) 

199 == "Mask([y=5:50, x=6:60], ['M34', 'M15', 'M29', 'M1', 'M20', 'M11', 'M13', 'M7', 'M17', 'M12', " 

200 "'M31', 'M16', 'M2', 'M3', 'M8', 'M26', 'M22', 'M5', 'M18', 'M19', 'M24', 'M21', 'M27', 'M6', " 

201 "'M28', 'M10', 'M4', 'M23', 'M0', 'M25', 'M9', 'M14', 'M33', 'M32', 'M30'])" 

202 ) 

203 assert repr(mask).startswith( 

204 "Mask(..., bbox=Box(y=Interval(start=5, stop=50), x=Interval(start=6, stop=60)), " 

205 "schema=MaskSchema([MaskPlane(name='M34', description='D34')" 

206 ), f"Repr: {mask!r}" 

207 

208 with pytest.raises(TypeError): 

209 # No bbox, size or array. 

210 Mask(0, schema=schema) 

211 

212 with pytest.raises(ValueError): 

213 # Box mismatch. 

214 Mask(mask.array, schema=schema, bbox=Box.factory[0:20, -5:45]) 

215 

216 with pytest.raises(ValueError): 

217 # Shape mismatch. 

218 Mask(mask.array, schema=schema, shape=(5, 10, 5)) 

219 

220 with pytest.raises(ValueError): 

221 # Cannot be 2-D. 

222 Mask(mask.array.reshape((2430, 5)), schema=schema, bbox=Box.factory[0:20, -5:45]) 

223 

224 

225def test_read_write() -> None: 

226 """Test explicit calls to Mask.read and Mask.write through FITS.""" 

227 rng = np.random.default_rng(500) 

228 planes = make_mask_planes(rng, 35, n_placeholders=5) 

229 schema = MaskSchema(planes, dtype=np.uint8) 

230 bbox = Box.factory[5:50, 6:60] 

231 mask = Mask( 

232 0, 

233 schema=schema, 

234 bbox=bbox, 

235 metadata={"four_and_a_half": 4.5}, 

236 ) 

237 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

238 mask.write(tmpFile) 

239 new = Mask.read(tmpFile) 

240 assert new == mask 

241 # __eq__ ignores metadata. 

242 assert new.metadata["four_and_a_half"] == 4.5 

243 assert new.metadata == mask.metadata 

244 

245 

246def test_serialize_multi() -> None: 

247 """Test serializing a mask with more than 31 mask planes (multiple 

248 HDUs). 

249 """ 

250 rng = np.random.default_rng(500) 

251 planes = make_mask_planes(rng, 35, n_placeholders=5) 

252 schema = MaskSchema(planes, dtype=np.uint8) 

253 bbox = Box.factory[5:50, 6:60] 

254 mask = Mask(0, schema=schema, bbox=bbox, metadata={"four_and_a_half": 4.5}) 

255 shape = bbox.shape 

256 for plane in schema: 

257 if plane is not None: 

258 mask.set(plane.name, rng.random(shape) > 0.5) 

259 with RoundtripFits(mask) as roundtrip: 

260 fits = roundtrip.inspect() 

261 assert fits[1].header["EXTNAME"] == "MASK" 

262 assert fits[1].header.get("EXTVER", 1) == 1 

263 assert fits[1].header["ZCMPTYPE"] == "GZIP_2" 

264 assert fits[2].header["EXTNAME"] == "MASK" 

265 assert fits[2].header["EXTVER"] == 2 

266 assert fits[2].header["ZCMPTYPE"] == "GZIP_2" 

267 n = 0 

268 for plane in planes: 

269 if plane is not None: 

270 hdu = fits[1] if n < 31 else fits[2] 

271 assert hdu.header[f"MSKN{(n % 31):04d}"] == plane.name 

272 assert hdu.header[f"MSKM{(n % 31):04d}"] == 1 << (n % 31) 

273 assert hdu.header[f"MSKD{(n % 31):04d}"] == plane.description 

274 n += 1 

275 assert_masks_equal(mask, roundtrip.result) 

276 

277 

278def test_add_plane_returns_new_mask() -> None: 

279 """Verify add_plane returns a new mask without modifying the original or 

280 its views. 

281 """ 

282 rng = np.random.default_rng(500) 

283 planes = make_mask_planes(rng, 3, n_placeholders=0) 

284 schema = MaskSchema(planes, dtype=np.uint8) 

285 bbox = Box.factory[5:50, 6:60] 

286 mask = Mask(0, schema=schema, bbox=bbox) 

287 m0 = rng.random(bbox.shape) > 0.5 

288 mask.set("M0", m0) 

289 view = mask[bbox] # shares the array and old schema with mask 

290 original_array = mask.array 

291 

292 new_mask = mask.add_plane("OUTSIDE_STENCIL", "Pixel lies outside the stencil.") 

293 

294 # The original mask and any views keep the old schema and array. 

295 assert "OUTSIDE_STENCIL" not in mask.schema.names 

296 assert "OUTSIDE_STENCIL" not in view.schema.names 

297 assert mask.array is original_array 

298 # The new mask reallocated a fresh array and carries the new plane. 

299 assert new_mask.array is not original_array 

300 assert "OUTSIDE_STENCIL" in new_mask.schema.names 

301 assert new_mask.schema.descriptions["OUTSIDE_STENCIL"] == "Pixel lies outside the stencil." 

302 # The new plane is the fourth (overall index 3) so it lives in byte 0. 

303 bit = new_mask.schema.bit("OUTSIDE_STENCIL") 

304 assert bit.index == 0 

305 assert bit.mask == 1 << 3 

306 assert new_mask.schema.mask_size == 1 

307 # Existing plane data is preserved and the new plane starts all-False. 

308 np.testing.assert_array_equal(new_mask.get("M0"), m0) 

309 assert not new_mask.get("OUTSIDE_STENCIL").any() 

310 

311 

312def test_add_plane_grows_byte() -> None: 

313 """Verify adding a ninth plane crosses the 8-plane boundary into a 

314 second byte. 

315 """ 

316 rng = np.random.default_rng(500) 

317 planes = make_mask_planes(rng, 8, n_placeholders=0) 

318 schema = MaskSchema(planes, dtype=np.uint8) 

319 bbox = Box.factory[5:50, 6:60] 

320 mask = Mask(0, schema=schema, bbox=bbox) 

321 set_planes = {} 

322 for plane in planes: 

323 assert plane is not None 

324 boolean_mask = rng.random(bbox.shape) > 0.5 

325 mask.set(plane.name, boolean_mask) 

326 set_planes[plane.name] = boolean_mask 

327 

328 new_mask = mask.add_plane("OUTSIDE_STENCIL", "Pixel lies outside the stencil.") 

329 

330 # The original is unchanged; the new mask spills into a second byte. 

331 assert mask.schema.mask_size == 1 

332 bit = new_mask.schema.bit("OUTSIDE_STENCIL") 

333 assert bit.index == 1 

334 assert bit.mask == 1 << 0 

335 assert new_mask.schema.mask_size == 2 

336 assert new_mask.array.shape == bbox.shape + (2,) 

337 assert not new_mask.get("OUTSIDE_STENCIL").any() 

338 # Every pre-existing plane keeps its data. 

339 for name, boolean_mask in set_planes.items(): 

340 np.testing.assert_array_equal(new_mask.get(name), boolean_mask) 

341 

342 

343def test_add_planes_multiple() -> None: 

344 """Verify add_planes adds several planes in a single call.""" 

345 rng = np.random.default_rng(500) 

346 planes = make_mask_planes(rng, 3, n_placeholders=0) 

347 bbox = Box.factory[0:4, 0:5] 

348 mask = Mask(0, schema=MaskSchema(planes, dtype=np.uint8), bbox=bbox) 

349 m0 = rng.random(bbox.shape) > 0.5 

350 mask.set("M0", m0) 

351 

352 new_mask = mask.add_planes([MaskPlane("A", "plane a"), MaskPlane("B", "plane b")]) 

353 

354 assert set(mask.schema.names) == {"M0", "M1", "M2"} # original unchanged 

355 assert set(new_mask.schema.names) == {"M0", "M1", "M2", "A", "B"} 

356 np.testing.assert_array_equal(new_mask.get("M0"), m0) 

357 assert not new_mask.get("A").any() 

358 assert not new_mask.get("B").any() 

359 

360 

361def test_add_planes_drop_reassigns_bits() -> None: 

362 """Verify dropping a plane compacts the schema and repacks pixel values.""" 

363 rng = np.random.default_rng(500) 

364 bbox = Box.factory[0:4, 0:5] 

365 schema = MaskSchema([MaskPlane("A", "a"), MaskPlane("B", "b"), MaskPlane("C", "c")], dtype=np.uint8) 

366 mask = Mask(0, schema=schema, bbox=bbox) 

367 a = rng.random(bbox.shape) > 0.5 

368 c = rng.random(bbox.shape) > 0.5 

369 mask.set("A", a) 

370 mask.set("B", rng.random(bbox.shape) > 0.5) 

371 mask.set("C", c) 

372 

373 new_mask = mask.add_planes([MaskPlane("D", "d")], drop=["B"]) 

374 

375 # B is gone; D is appended after the retained planes. 

376 assert list(new_mask.schema.names) == ["A", "C", "D"] 

377 assert "B" not in new_mask.schema.names 

378 # C moved down from bit 2 to bit 1; D takes bit 2. 

379 assert new_mask.schema.bit("A").mask == 1 << 0 

380 assert new_mask.schema.bit("C").mask == 1 << 1 

381 assert new_mask.schema.bit("D").mask == 1 << 2 

382 # Retained pixel values follow their planes; the new plane is cleared. 

383 np.testing.assert_array_equal(new_mask.get("A"), a) 

384 np.testing.assert_array_equal(new_mask.get("C"), c) 

385 assert not new_mask.get("D").any() 

386 

387 

388def test_add_planes_with_placeholder() -> None: 

389 """Verify None placeholders reserve bits and survive add_planes and a 

390 FITS round-trip. 

391 """ 

392 rng = np.random.default_rng(500) 

393 bbox = Box.factory[0:4, 0:5] 

394 # Schema with a pre-existing placeholder reserving bit 1. 

395 schema = MaskSchema([MaskPlane("A", "a"), None, MaskPlane("B", "b")], dtype=np.uint8) 

396 mask = Mask(0, schema=schema, bbox=bbox) 

397 a = rng.random(bbox.shape) > 0.5 

398 b = rng.random(bbox.shape) > 0.5 

399 mask.set("A", a) 

400 mask.set("B", b) 

401 

402 # Append a block that itself contains an interior placeholder. 

403 new_mask = mask.add_planes([MaskPlane("C", "c"), None, MaskPlane("D", "d")]) 

404 

405 # The pre-existing placeholder stays at bit 1; the added placeholder 

406 # stays between C and D (bit 4), not at the end. 

407 assert list(new_mask.schema) == [ 

408 MaskPlane("A", "a"), 

409 None, 

410 MaskPlane("B", "b"), 

411 MaskPlane("C", "c"), 

412 None, 

413 MaskPlane("D", "d"), 

414 ] 

415 assert new_mask.schema.bit("A").mask == 1 << 0 

416 assert new_mask.schema.bit("B").mask == 1 << 2 

417 assert new_mask.schema.bit("C").mask == 1 << 3 

418 assert new_mask.schema.bit("D").mask == 1 << 5 

419 # Retained pixel values follow their planes; new planes start cleared. 

420 np.testing.assert_array_equal(new_mask.get("A"), a) 

421 np.testing.assert_array_equal(new_mask.get("B"), b) 

422 assert not new_mask.get("C").any() 

423 assert not new_mask.get("D").any() 

424 

425 with RoundtripFits(new_mask) as roundtrip: 

426 assert_masks_equal(new_mask, roundtrip.result) 

427 

428 

429def test_add_planes_drop_unknown_raises() -> None: 

430 """Verify dropping a non-existent plane raises ValueError.""" 

431 mask = Mask(0, schema=MaskSchema([MaskPlane("A", "a")], dtype=np.uint8), bbox=Box.factory[0:2, 0:2]) 

432 with pytest.raises(ValueError): 

433 mask.add_planes([], drop=["NOPE"]) 

434 

435 

436def test_add_plane_duplicate_raises() -> None: 

437 """Verify adding a plane whose name already exists raises ValueError.""" 

438 rng = np.random.default_rng(500) 

439 planes = make_mask_planes(rng, 3, n_placeholders=0) 

440 schema = MaskSchema(planes, dtype=np.uint8) 

441 mask = Mask(0, schema=schema, bbox=Box.factory[0:4, 0:4]) 

442 with pytest.raises(ValueError): 

443 mask.add_plane("M0", "Duplicate of an existing plane.") 

444 

445 

446def test_add_plane_roundtrip() -> None: 

447 """Verify a runtime-added plane and its data survive a FITS round-trip.""" 

448 rng = np.random.default_rng(500) 

449 planes = make_mask_planes(rng, 8, n_placeholders=0) 

450 schema = MaskSchema(planes, dtype=np.uint8) 

451 bbox = Box.factory[5:50, 6:60] 

452 mask = Mask(0, schema=schema, bbox=bbox) 

453 mask = mask.add_plane("OUTSIDE_STENCIL", "Pixel lies outside the stencil.") 

454 mask.set("OUTSIDE_STENCIL", rng.random(bbox.shape) > 0.5) 

455 with lsst.utils.tests.getTempFilePath(".fits") as tmpFile: 

456 mask.write(tmpFile) 

457 new = Mask.read(tmpFile) 

458 assert new == mask 

459 assert new.schema.descriptions["OUTSIDE_STENCIL"] == "Pixel lies outside the stencil." 

460 assert_masks_equal(new, mask) 

461 

462 

463def test_legacy_non_cell_coadd_plane_map() -> None: 

464 """Verify the non-cell coadd map defines a distinct SENSOR_EDGE plane.""" 

465 plane_map = get_legacy_non_cell_coadd_mask_planes() 

466 assert "SENSOR_EDGE" in plane_map 

467 assert plane_map["SENSOR_EDGE"].name == "SENSOR_EDGE" 

468 

469 

470def test_guess_legacy_plane_map_coadd_discriminator() -> None: 

471 """Verify INEXACT_PSF routes to a coadd map and SENSOR_EDGE discriminates 

472 non-cell from cell. 

473 """ 

474 non_cell = _guess_legacy_plane_map({"INEXACT_PSF": 11, "SENSOR_EDGE": 14}) 

475 assert "SENSOR_EDGE" in non_cell 

476 cell = _guess_legacy_plane_map({"INEXACT_PSF": 11}) 

477 assert "SENSOR_EDGE" not in cell 

478 

479 

480def test_legacy(legacy_test_data: _LegacyTestData) -> None: 

481 """Test Mask.read_legacy, Mask.to_legacy, and Mask.from_legacy.""" 

482 assert legacy_test_data.mask.schema.names == {p.name for p in legacy_test_data.plane_map.values()} 

483 assert legacy_test_data.mask.bbox == Box.from_legacy(legacy_test_data.reader.readBBox()) 

484 legacy_mask = legacy_test_data.reader.readMask() 

485 compare_mask_to_legacy(legacy_test_data.mask, legacy_mask, legacy_test_data.plane_map) 

486 compare_mask_to_legacy( 

487 legacy_test_data.mask, 

488 legacy_test_data.mask.to_legacy(legacy_test_data.plane_map), 

489 legacy_test_data.plane_map, 

490 ) 

491 assert_masks_equal( 

492 legacy_test_data.mask, Mask.from_legacy(legacy_mask, plane_map=legacy_test_data.plane_map) 

493 ) 

494 # Write the mask out in the new format, and test that we can read it back. 

495 with RoundtripFits(legacy_test_data.mask, storage_class="MaskV2") as roundtrip: 

496 pass 

497 assert_masks_equal(roundtrip.result, legacy_test_data.mask) 

498 

499 

500def test_legacy_butler_read(legacy_test_data: _LegacyTestData) -> None: 

501 """Test that a round-tripped MaskV2 can be read back as a legacy afw 

502 Mask via Butler. 

503 """ 

504 with RoundtripFits(legacy_test_data.mask, storage_class="MaskV2") as roundtrip: 

505 legacy_mask = roundtrip.get(storageClass="Mask") 

506 assert isinstance(legacy_mask, lsst.afw.image.Mask) 

507 compare_mask_to_legacy(legacy_test_data.mask, legacy_mask)