Coverage for tests/objects/test_image_feature.py: 100%

283 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-10-22 18:11 +0000

1import geojson 

2import math 

3import numpy as np 

4from qubalab.objects import ( 

5 ObjectType, 

6 ImageFeature, 

7 Classification, 

8 geojson_features_from_string, 

9) 

10 

11 

12def test_geometry(): 

13 expected_geometry = geojson.Point((-115.81, 37.24)) 

14 image_feature = ImageFeature(expected_geometry) 

15 

16 geometry = image_feature.geometry 

17 

18 assert geometry == expected_geometry 

19 

20 

21def test_json_serializable_with_geometry(): 

22 geometry = geojson.Point((-115.81, 37.24)) 

23 

24 image_feature = ImageFeature(geometry) 

25 

26 geojson.dumps(image_feature) # will throw an exception if not serializable 

27 

28 

29def test_id(): 

30 expected_id = 23 

31 image_feature = ImageFeature(None, id=expected_id) 

32 

33 id = image_feature.id 

34 

35 assert id == str(expected_id) 

36 

37 

38def test_json_serializable_with_id(): 

39 id = 23 

40 

41 image_feature = ImageFeature(None, id=id) 

42 

43 geojson.dumps(image_feature) # will throw an exception if not serializable 

44 

45 

46def test_classification(): 

47 expected_classification = Classification("name", (1, 1, 1)) 

48 image_feature = ImageFeature(None, classification=expected_classification) 

49 

50 classification = image_feature.classification 

51 

52 assert classification == expected_classification 

53 

54 

55def test_json_serializable_with_classification(): 

56 classification = Classification("name", (1, 1, 1)) 

57 

58 image_feature = ImageFeature(None, classification=classification) 

59 

60 geojson.dumps(image_feature) # will throw an exception if not serializable 

61 

62 

63def test_name(): 

64 expected_name = "name" 

65 image_feature = ImageFeature(None, name=expected_name) 

66 

67 name = image_feature.name 

68 

69 assert name == expected_name 

70 

71 

72def test_json_serializable_with_name(): 

73 name = "name" 

74 

75 image_feature = ImageFeature(None, name=name) 

76 

77 geojson.dumps(image_feature) # will throw an exception if not serializable 

78 

79 

80def test_measurements(): 

81 inital_measurements = {"some_value": 0.324, "nan_value": float("nan")} 

82 expected_measurements = { 

83 k: v for k, v in inital_measurements.items() if not math.isnan(v) 

84 } # NaN values are skipped 

85 image_feature = ImageFeature(None, measurements=inital_measurements) 

86 

87 measurements = image_feature.measurements 

88 

89 assert measurements == expected_measurements 

90 

91 

92def test_json_serializable_with_measurements(): 

93 measurements = {"some_value": 0.324, "nan_value": float("nan")} 

94 

95 image_feature = ImageFeature(None, measurements=measurements) 

96 

97 geojson.dumps(image_feature) # will throw an exception if not serializable 

98 

99 

100def test_object_type(): 

101 expected_object_type = ObjectType.DETECTION 

102 image_feature = ImageFeature(None, object_type=expected_object_type) 

103 

104 object_type = image_feature.object_type 

105 

106 assert object_type == expected_object_type 

107 

108 

109def test_json_serializable_with_object_type(): 

110 object_type = ObjectType.DETECTION 

111 

112 image_feature = ImageFeature(None, object_type=object_type) 

113 

114 geojson.dumps(image_feature) # will throw an exception if not serializable 

115 

116 

117def test_is_detection(): 

118 expected_object_type = ObjectType.DETECTION 

119 image_feature = ImageFeature(None, object_type=expected_object_type) 

120 

121 is_detection = image_feature.is_detection 

122 

123 assert is_detection 

124 

125 

126def test_is_not_detection(): 

127 expected_object_type = ObjectType.ANNOTATION 

128 image_feature = ImageFeature(None, object_type=expected_object_type) 

129 

130 is_detection = image_feature.is_detection 

131 

132 assert not (is_detection) 

133 

134 

135def test_is_cell(): 

136 expected_object_type = ObjectType.CELL 

137 image_feature = ImageFeature(None, object_type=expected_object_type) 

138 

139 is_cell = image_feature.is_cell 

140 

141 assert is_cell 

142 

143 

144def test_is_not_cell(): 

145 expected_object_type = ObjectType.DETECTION 

146 image_feature = ImageFeature(None, object_type=expected_object_type) 

147 

148 is_cell = image_feature.is_cell 

149 

150 assert not (is_cell) 

151 

152 

153def test_is_tile(): 

154 expected_object_type = ObjectType.TILE 

155 image_feature = ImageFeature(None, object_type=expected_object_type) 

156 

157 is_tile = image_feature.is_tile 

158 

159 assert is_tile 

160 

161 

162def test_is_not_tile(): 

163 expected_object_type = ObjectType.DETECTION 

164 image_feature = ImageFeature(None, object_type=expected_object_type) 

165 

166 is_tile = image_feature.is_tile 

167 

168 assert not (is_tile) 

169 

170 

171def test_is_annotation(): 

172 expected_object_type = ObjectType.ANNOTATION 

173 image_feature = ImageFeature(None, object_type=expected_object_type) 

174 

175 is_annotation = image_feature.is_annotation 

176 

177 assert is_annotation 

178 

179 

180def test_is_not_annotation(): 

181 expected_object_type = ObjectType.DETECTION 

182 image_feature = ImageFeature(None, object_type=expected_object_type) 

183 

184 is_annotation = image_feature.is_annotation 

185 

186 assert not (is_annotation) 

187 

188 

189def test_color(): 

190 expected_color = (4, 5, 6) 

191 image_feature = ImageFeature(None, color=expected_color) 

192 

193 color = image_feature.color 

194 

195 assert color == expected_color 

196 

197 

198def test_json_serializable_with_color(): 

199 color = (4, 5, 6) 

200 

201 image_feature = ImageFeature(None, color=color) 

202 

203 geojson.dumps(image_feature) # will throw an exception if not serializable 

204 

205 

206def test_nucleus_geometry(): 

207 expected_nucleus_geometry = geojson.Point((-115.81, 37.24)) 

208 image_feature = ImageFeature( 

209 None, extra_geometries={"nucleus": expected_nucleus_geometry} 

210 ) 

211 

212 nucleus_geometry = image_feature.nucleus_geometry 

213 

214 assert nucleus_geometry == expected_nucleus_geometry 

215 

216 

217def test_json_serializable_with_nucleus_geometry(): 

218 nucleus_geometry = geojson.Point((-115.81, 37.24)) 

219 

220 image_feature = ImageFeature(None, extra_geometries={"nucleus": nucleus_geometry}) 

221 

222 geojson.dumps(image_feature) # will throw an exception if not serializable 

223 

224 

225def test_geometry_when_created_from_feature(): 

226 expected_geometry = geojson.Point((-115.81, 37.24)) 

227 feature = geojson.Feature(geometry=expected_geometry) 

228 image_feature = ImageFeature.create_from_feature(feature) 

229 

230 geometry = image_feature.geometry 

231 

232 assert geometry == expected_geometry 

233 

234 

235def test_id_when_created_from_feature(): 

236 expected_id = 23 

237 feature = geojson.Feature(id=expected_id) 

238 image_feature = ImageFeature.create_from_feature(feature) 

239 

240 id = image_feature.id 

241 

242 assert id == str(expected_id) 

243 

244 

245def test_classification_when_created_from_feature(): 

246 expected_classification = Classification("name", (1, 1, 1)) 

247 feature = geojson.Feature(properties={"classification": expected_classification}) 

248 image_feature = ImageFeature.create_from_feature(feature) 

249 

250 classification = image_feature.classification 

251 

252 assert classification == expected_classification 

253 

254 

255def test_name_when_created_from_feature(): 

256 expected_name = "name" 

257 feature = geojson.Feature(properties={"name": expected_name}) 

258 image_feature = ImageFeature.create_from_feature(feature) 

259 

260 name = image_feature.name 

261 

262 assert name == expected_name 

263 

264 

265def test_measurements_when_created_from_feature(): 

266 inital_measurements = {"some_value": 0.324, "nan_value": float("nan")} 

267 expected_measurements = { 

268 k: v for k, v in inital_measurements.items() if not math.isnan(v) 

269 } # NaN values are skipped 

270 feature = geojson.Feature(properties={"measurements": inital_measurements}) 

271 image_feature = ImageFeature.create_from_feature(feature) 

272 

273 measurements = image_feature.measurements 

274 

275 assert measurements == expected_measurements 

276 

277 

278def test_object_type_when_created_from_feature(): 

279 expected_object_type = ObjectType.CELL 

280 feature = geojson.Feature(properties={"object_type": expected_object_type.name}) 

281 image_feature = ImageFeature.create_from_feature(feature) 

282 

283 object_type = image_feature.object_type 

284 

285 assert object_type == expected_object_type 

286 

287 

288def test_color_when_created_from_feature(): 

289 expected_color = (4, 5, 6) 

290 feature = geojson.Feature(properties={"color": expected_color}) 

291 image_feature = ImageFeature.create_from_feature(feature) 

292 

293 color = image_feature.color 

294 

295 assert color == expected_color 

296 

297 

298def test_nucleus_geometry_when_created_from_feature(): 

299 expected_nucleus_geometry = geojson.Point((-115.81, 37.24)) 

300 feature = geojson.Feature(properties={"nucleusGeometry": expected_nucleus_geometry}) 

301 image_feature = ImageFeature.create_from_feature(feature) 

302 

303 nucleus_geometry = image_feature.nucleus_geometry 

304 

305 assert nucleus_geometry == expected_nucleus_geometry 

306 

307 

308def test_number_of_features_when_created_from_label_image_without_scale(): 

309 label_image = np.array( 

310 [ 

311 [0, 1, 1, 0, 0], 

312 [0, 1, 1, 0, 0], 

313 [0, 0, 0, 0, 0], 

314 [0, 0, 0, 1, 0], 

315 [0, 0, 0, 1, 0], 

316 [0, 0, 0, 0, 0], 

317 [0, 0, 2, 2, 0], 

318 [0, 0, 0, 0, 0], 

319 [0, 0, 0, 0, 0], 

320 [0, 3, 0, 0, 0], 

321 ], 

322 dtype=np.uint8, 

323 ) 

324 expected_number_of_features = 3 

325 

326 features = ImageFeature.create_from_label_image(label_image) 

327 

328 assert len(features) == expected_number_of_features 

329 

330 

331def test_number_of_features_when_created_from_label_image_with_scale(): 

332 scale = 2 

333 label_image = np.array( 

334 [ 

335 [0, 1, 1, 0, 0], 

336 [0, 1, 1, 0, 0], 

337 [0, 0, 0, 0, 0], 

338 [0, 0, 0, 1, 0], 

339 [0, 0, 0, 1, 0], 

340 [0, 0, 0, 0, 0], 

341 [0, 0, 2, 2, 0], 

342 [0, 0, 0, 0, 0], 

343 [0, 0, 0, 0, 0], 

344 [0, 3, 0, 0, 0], 

345 ], 

346 dtype=np.uint8, 

347 ) 

348 expected_number_of_features = 3 

349 

350 features = ImageFeature.create_from_label_image(label_image, scale=scale) 

351 

352 assert len(features) == expected_number_of_features 

353 

354 

355def test_object_type_when_created_from_label_image(): 

356 expected_object_type = ObjectType.CELL 

357 label_image = np.array( 

358 [ 

359 [0, 1, 1, 0, 0], 

360 [0, 1, 1, 0, 0], 

361 [0, 0, 0, 0, 0], 

362 [0, 0, 0, 1, 0], 

363 [0, 0, 0, 1, 0], 

364 [0, 0, 0, 0, 0], 

365 [0, 0, 2, 2, 0], 

366 [0, 0, 0, 0, 0], 

367 [0, 0, 0, 0, 0], 

368 [0, 3, 0, 0, 0], 

369 ], 

370 dtype=np.uint8, 

371 ) 

372 

373 features = ImageFeature.create_from_label_image( 

374 label_image, object_type=expected_object_type 

375 ) 

376 

377 assert all(feature.object_type == expected_object_type for feature in features) 

378 

379 

380def test_measurement_when_created_from_label_image(): 

381 expected_measurements = [{"Label": float(label)} for label in [1, 2, 3]] 

382 label_image = np.array( 

383 [ 

384 [0, 1, 1, 0, 0], 

385 [0, 1, 1, 0, 0], 

386 [0, 0, 0, 0, 0], 

387 [0, 0, 0, 1, 0], 

388 [0, 0, 0, 1, 0], 

389 [0, 0, 0, 0, 0], 

390 [0, 0, 2, 2, 0], 

391 [0, 0, 0, 0, 0], 

392 [0, 0, 0, 0, 0], 

393 [0, 3, 0, 0, 0], 

394 ], 

395 dtype=np.uint8, 

396 ) 

397 

398 features = ImageFeature.create_from_label_image(label_image, include_labels=True) 

399 

400 assert all(feature.measurements in expected_measurements for feature in features) 

401 

402 

403def test_classification_when_created_from_label_image_and_classification_name_provided(): 

404 expected_classification_name = "name" 

405 label_image = np.array( 

406 [ 

407 [0, 1, 1, 0, 0], 

408 [0, 1, 1, 0, 0], 

409 [0, 0, 0, 0, 0], 

410 [0, 0, 0, 1, 0], 

411 [0, 0, 0, 1, 0], 

412 [0, 0, 0, 0, 0], 

413 [0, 0, 2, 2, 0], 

414 [0, 0, 0, 0, 0], 

415 [0, 0, 0, 0, 0], 

416 [0, 3, 0, 0, 0], 

417 ], 

418 dtype=np.uint8, 

419 ) 

420 

421 features = ImageFeature.create_from_label_image( 

422 label_image, classification_names=expected_classification_name 

423 ) 

424 

425 assert all( 

426 feature.classification.names == (expected_classification_name,) 

427 for feature in features 

428 ) 

429 

430 

431def test_classification_when_created_from_label_image_and_classification_dict_provided(): 

432 classification_dict = { 

433 # no classification for label 1 

434 2: "name2", 

435 3: "name3", 

436 } 

437 expected_classification_names = classification_dict.values() 

438 label_image = np.array( 

439 [ 

440 [0, 1, 1, 0, 0], 

441 [0, 1, 1, 0, 0], 

442 [0, 0, 0, 0, 0], 

443 [0, 0, 0, 1, 0], 

444 [0, 0, 0, 1, 0], 

445 [0, 0, 0, 0, 0], 

446 [0, 0, 2, 2, 0], 

447 [0, 0, 0, 0, 0], 

448 [0, 0, 0, 0, 0], 

449 [0, 3, 0, 0, 0], 

450 ], 

451 dtype=np.uint8, 

452 ) 

453 

454 features = ImageFeature.create_from_label_image( 

455 label_image, classification_names=classification_dict 

456 ) 

457 

458 assert all( 

459 feature.classification is None 

460 or feature.classification.names[0] in expected_classification_names 

461 for feature in features 

462 ) 

463 

464 

465def test_number_of_features_when_created_from_binary_image_without_scale(): 

466 binary_image = np.array( 

467 [ 

468 [False, True, True, False, False], 

469 [False, True, True, False, False], 

470 [False, False, False, False, False], 

471 [False, False, False, True, False], 

472 [False, False, False, True, False], 

473 [False, False, False, False, False], 

474 [False, False, True, True, False], 

475 [False, False, False, False, False], 

476 [False, False, False, False, False], 

477 [False, True, False, False, False], 

478 ], 

479 dtype=bool, 

480 ) 

481 expected_number_of_features = 1 

482 

483 features = ImageFeature.create_from_label_image(binary_image) 

484 

485 assert len(features) == expected_number_of_features 

486 

487 

488def test_number_of_features_when_created_from_binary_image_with_scale(): 

489 scale = 2 

490 binary_image = np.array( 

491 [ 

492 [False, True, True, False, False], 

493 [False, True, True, False, False], 

494 [False, False, False, False, False], 

495 [False, False, False, True, False], 

496 [False, False, False, True, False], 

497 [False, False, False, False, False], 

498 [False, False, True, True, False], 

499 [False, False, False, False, False], 

500 [False, False, False, False, False], 

501 [False, True, False, False, False], 

502 ], 

503 dtype=bool, 

504 ) 

505 expected_number_of_features = 1 

506 

507 features = ImageFeature.create_from_label_image(binary_image, scale=scale) 

508 

509 assert len(features) == expected_number_of_features 

510 

511 

512def test_object_type_when_created_from_binary_image(): 

513 expected_object_type = ObjectType.TILE 

514 binary_image = np.array( 

515 [ 

516 [False, True, True, False, False], 

517 [False, True, True, False, False], 

518 [False, False, False, False, False], 

519 [False, False, False, True, False], 

520 [False, False, False, True, False], 

521 [False, False, False, False, False], 

522 [False, False, True, True, False], 

523 [False, False, False, False, False], 

524 [False, False, False, False, False], 

525 [False, True, False, False, False], 

526 ], 

527 dtype=bool, 

528 ) 

529 

530 features = ImageFeature.create_from_label_image( 

531 binary_image, object_type=expected_object_type 

532 ) 

533 

534 assert all(feature.object_type == expected_object_type for feature in features) 

535 

536 

537def test_measurement_when_created_from_binary_image(): 

538 expected_measurement = {"Label": 1.0} 

539 binary_image = np.array( 

540 [ 

541 [False, True, True, False, False], 

542 [False, True, True, False, False], 

543 [False, False, False, False, False], 

544 [False, False, False, True, False], 

545 [False, False, False, True, False], 

546 [False, False, False, False, False], 

547 [False, False, True, True, False], 

548 [False, False, False, False, False], 

549 [False, False, False, False, False], 

550 [False, True, False, False, False], 

551 ], 

552 dtype=bool, 

553 ) 

554 

555 features = ImageFeature.create_from_label_image(binary_image, include_labels=True) 

556 

557 assert all(feature.measurements == expected_measurement for feature in features) 

558 

559 

560def test_classification_when_created_from_binary_image_and_classification_name_provided(): 

561 expected_classification_name = "name" 

562 binary_image = np.array( 

563 [ 

564 [False, True, True, False, False], 

565 [False, True, True, False, False], 

566 [False, False, False, False, False], 

567 [False, False, False, True, False], 

568 [False, False, False, True, False], 

569 [False, False, False, False, False], 

570 [False, False, True, True, False], 

571 [False, False, False, False, False], 

572 [False, False, False, False, False], 

573 [False, True, False, False, False], 

574 ], 

575 dtype=bool, 

576 ) 

577 

578 features = ImageFeature.create_from_label_image( 

579 binary_image, classification_names=expected_classification_name 

580 ) 

581 

582 assert all( 

583 feature.classification.names == (expected_classification_name,) 

584 for feature in features 

585 ) 

586 

587 

588def test_classification_when_created_from_binary_image_and_classification_dict_provided(): 

589 classification_dict = {1: "name1"} 

590 expected_classification_names = classification_dict.values() 

591 binary_image = np.array( 

592 [ 

593 [False, True, True, False, False], 

594 [False, True, True, False, False], 

595 [False, False, False, False, False], 

596 [False, False, False, True, False], 

597 [False, False, False, True, False], 

598 [False, False, False, False, False], 

599 [False, False, True, True, False], 

600 [False, False, False, False, False], 

601 [False, False, False, False, False], 

602 [False, True, False, False, False], 

603 ], 

604 dtype=bool, 

605 ) 

606 

607 features = ImageFeature.create_from_label_image( 

608 binary_image, classification_names=classification_dict 

609 ) 

610 

611 assert all( 

612 feature.classification.names[0] in expected_classification_names 

613 for feature in features 

614 ) 

615 

616 

617def test_classification_when_set_after_creation(): 

618 expected_classification = Classification("name", (1, 1, 1)) 

619 image_feature = ImageFeature(None) 

620 image_feature.classification = { 

621 "names": expected_classification.names, 

622 "color": expected_classification.color, 

623 } 

624 

625 classification = image_feature.classification 

626 

627 assert classification == expected_classification 

628 

629 

630def test_name_when_set_after_creation(): 

631 expected_name = "name" 

632 image_feature = ImageFeature(None) 

633 image_feature.names = expected_name 

634 

635 name = image_feature.names 

636 

637 assert name == expected_name 

638 

639 

640def test_measurements_when_set_after_creation(): 

641 inital_measurements = {"some_value": 0.324, "nan_value": float("nan")} 

642 expected_measurements = { 

643 k: v for k, v in inital_measurements.items() if not math.isnan(v) 

644 } # NaN values are skipped 

645 image_feature = ImageFeature(None) 

646 image_feature.measurements = inital_measurements 

647 

648 measurements = image_feature.measurements 

649 

650 assert measurements == expected_measurements 

651 

652 

653def test_object_type_when_set_after_creation(): 

654 expected_object_type = ObjectType.CELL 

655 image_feature = ImageFeature(None) 

656 image_feature.object_type = expected_object_type 

657 

658 object_type = image_feature.object_type 

659 

660 assert object_type == expected_object_type 

661 

662 

663def test_object_type_name_when_set_after_creation(): 

664 expected_object_type = ObjectType.TILE 

665 image_feature = ImageFeature(None) 

666 image_feature.object_type = expected_object_type.name 

667 

668 object_type = image_feature.object_type 

669 

670 assert object_type == expected_object_type 

671 

672 

673def test_color_when_set_after_creation(): 

674 expected_color = (4, 5, 6) 

675 image_feature = ImageFeature(None) 

676 image_feature.color = expected_color 

677 

678 color = image_feature.color 

679 

680 assert color == expected_color 

681 

682 

683def test_nucleus_geometry_when_set_after_creation(): 

684 expected_nucleus_geometry = geojson.Point((-115.81, 37.24)) 

685 image_feature = ImageFeature(None) 

686 image_feature.nucleus_geometry = expected_nucleus_geometry 

687 

688 nucleus_geometry = image_feature.nucleus_geometry 

689 

690 assert nucleus_geometry == expected_nucleus_geometry 

691 

692 

693def test_imagefeature_handles_classification_names(): 

694 string = """ 

695 {"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{"objectType":"annotation","classification":{"names":["a","b"]}}} 

696 """ 

697 feature = geojson_features_from_string(string) 

698 ifeature = ImageFeature.create_from_feature(feature) 

699 assert ifeature.classification.names == tuple( 

700 feature["properties"]["classification"]["names"] 

701 ) 

702 

703 

704def test_imagefeature_handles_classification_name(): 

705 string = """ 

706 {"type":"Feature","geometry":{"type":"Point","coordinates":[0,0]},"properties":{"objectType":"annotation","classification":{"names":["a","b"]}}} 

707 """ 

708 feature = geojson_features_from_string(string) 

709 ifeature = ImageFeature.create_from_feature(feature) 

710 assert ifeature.classification.names == tuple( 

711 feature["properties"]["classification"]["names"] 

712 )