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
« 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)
12def test_geometry():
13 expected_geometry = geojson.Point((-115.81, 37.24))
14 image_feature = ImageFeature(expected_geometry)
16 geometry = image_feature.geometry
18 assert geometry == expected_geometry
21def test_json_serializable_with_geometry():
22 geometry = geojson.Point((-115.81, 37.24))
24 image_feature = ImageFeature(geometry)
26 geojson.dumps(image_feature) # will throw an exception if not serializable
29def test_id():
30 expected_id = 23
31 image_feature = ImageFeature(None, id=expected_id)
33 id = image_feature.id
35 assert id == str(expected_id)
38def test_json_serializable_with_id():
39 id = 23
41 image_feature = ImageFeature(None, id=id)
43 geojson.dumps(image_feature) # will throw an exception if not serializable
46def test_classification():
47 expected_classification = Classification("name", (1, 1, 1))
48 image_feature = ImageFeature(None, classification=expected_classification)
50 classification = image_feature.classification
52 assert classification == expected_classification
55def test_json_serializable_with_classification():
56 classification = Classification("name", (1, 1, 1))
58 image_feature = ImageFeature(None, classification=classification)
60 geojson.dumps(image_feature) # will throw an exception if not serializable
63def test_name():
64 expected_name = "name"
65 image_feature = ImageFeature(None, name=expected_name)
67 name = image_feature.name
69 assert name == expected_name
72def test_json_serializable_with_name():
73 name = "name"
75 image_feature = ImageFeature(None, name=name)
77 geojson.dumps(image_feature) # will throw an exception if not serializable
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)
87 measurements = image_feature.measurements
89 assert measurements == expected_measurements
92def test_json_serializable_with_measurements():
93 measurements = {"some_value": 0.324, "nan_value": float("nan")}
95 image_feature = ImageFeature(None, measurements=measurements)
97 geojson.dumps(image_feature) # will throw an exception if not serializable
100def test_object_type():
101 expected_object_type = ObjectType.DETECTION
102 image_feature = ImageFeature(None, object_type=expected_object_type)
104 object_type = image_feature.object_type
106 assert object_type == expected_object_type
109def test_json_serializable_with_object_type():
110 object_type = ObjectType.DETECTION
112 image_feature = ImageFeature(None, object_type=object_type)
114 geojson.dumps(image_feature) # will throw an exception if not serializable
117def test_is_detection():
118 expected_object_type = ObjectType.DETECTION
119 image_feature = ImageFeature(None, object_type=expected_object_type)
121 is_detection = image_feature.is_detection
123 assert is_detection
126def test_is_not_detection():
127 expected_object_type = ObjectType.ANNOTATION
128 image_feature = ImageFeature(None, object_type=expected_object_type)
130 is_detection = image_feature.is_detection
132 assert not (is_detection)
135def test_is_cell():
136 expected_object_type = ObjectType.CELL
137 image_feature = ImageFeature(None, object_type=expected_object_type)
139 is_cell = image_feature.is_cell
141 assert is_cell
144def test_is_not_cell():
145 expected_object_type = ObjectType.DETECTION
146 image_feature = ImageFeature(None, object_type=expected_object_type)
148 is_cell = image_feature.is_cell
150 assert not (is_cell)
153def test_is_tile():
154 expected_object_type = ObjectType.TILE
155 image_feature = ImageFeature(None, object_type=expected_object_type)
157 is_tile = image_feature.is_tile
159 assert is_tile
162def test_is_not_tile():
163 expected_object_type = ObjectType.DETECTION
164 image_feature = ImageFeature(None, object_type=expected_object_type)
166 is_tile = image_feature.is_tile
168 assert not (is_tile)
171def test_is_annotation():
172 expected_object_type = ObjectType.ANNOTATION
173 image_feature = ImageFeature(None, object_type=expected_object_type)
175 is_annotation = image_feature.is_annotation
177 assert is_annotation
180def test_is_not_annotation():
181 expected_object_type = ObjectType.DETECTION
182 image_feature = ImageFeature(None, object_type=expected_object_type)
184 is_annotation = image_feature.is_annotation
186 assert not (is_annotation)
189def test_color():
190 expected_color = (4, 5, 6)
191 image_feature = ImageFeature(None, color=expected_color)
193 color = image_feature.color
195 assert color == expected_color
198def test_json_serializable_with_color():
199 color = (4, 5, 6)
201 image_feature = ImageFeature(None, color=color)
203 geojson.dumps(image_feature) # will throw an exception if not serializable
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 )
212 nucleus_geometry = image_feature.nucleus_geometry
214 assert nucleus_geometry == expected_nucleus_geometry
217def test_json_serializable_with_nucleus_geometry():
218 nucleus_geometry = geojson.Point((-115.81, 37.24))
220 image_feature = ImageFeature(None, extra_geometries={"nucleus": nucleus_geometry})
222 geojson.dumps(image_feature) # will throw an exception if not serializable
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)
230 geometry = image_feature.geometry
232 assert geometry == expected_geometry
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)
240 id = image_feature.id
242 assert id == str(expected_id)
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)
250 classification = image_feature.classification
252 assert classification == expected_classification
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)
260 name = image_feature.name
262 assert name == expected_name
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)
273 measurements = image_feature.measurements
275 assert measurements == expected_measurements
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)
283 object_type = image_feature.object_type
285 assert object_type == expected_object_type
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)
293 color = image_feature.color
295 assert color == expected_color
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)
303 nucleus_geometry = image_feature.nucleus_geometry
305 assert nucleus_geometry == expected_nucleus_geometry
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
326 features = ImageFeature.create_from_label_image(label_image)
328 assert len(features) == expected_number_of_features
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
350 features = ImageFeature.create_from_label_image(label_image, scale=scale)
352 assert len(features) == expected_number_of_features
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 )
373 features = ImageFeature.create_from_label_image(
374 label_image, object_type=expected_object_type
375 )
377 assert all(feature.object_type == expected_object_type for feature in features)
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 )
398 features = ImageFeature.create_from_label_image(label_image, include_labels=True)
400 assert all(feature.measurements in expected_measurements for feature in features)
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 )
421 features = ImageFeature.create_from_label_image(
422 label_image, classification_names=expected_classification_name
423 )
425 assert all(
426 feature.classification.names == (expected_classification_name,)
427 for feature in features
428 )
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 )
454 features = ImageFeature.create_from_label_image(
455 label_image, classification_names=classification_dict
456 )
458 assert all(
459 feature.classification is None
460 or feature.classification.names[0] in expected_classification_names
461 for feature in features
462 )
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
483 features = ImageFeature.create_from_label_image(binary_image)
485 assert len(features) == expected_number_of_features
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
507 features = ImageFeature.create_from_label_image(binary_image, scale=scale)
509 assert len(features) == expected_number_of_features
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 )
530 features = ImageFeature.create_from_label_image(
531 binary_image, object_type=expected_object_type
532 )
534 assert all(feature.object_type == expected_object_type for feature in features)
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 )
555 features = ImageFeature.create_from_label_image(binary_image, include_labels=True)
557 assert all(feature.measurements == expected_measurement for feature in features)
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 )
578 features = ImageFeature.create_from_label_image(
579 binary_image, classification_names=expected_classification_name
580 )
582 assert all(
583 feature.classification.names == (expected_classification_name,)
584 for feature in features
585 )
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 )
607 features = ImageFeature.create_from_label_image(
608 binary_image, classification_names=classification_dict
609 )
611 assert all(
612 feature.classification.names[0] in expected_classification_names
613 for feature in features
614 )
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 }
625 classification = image_feature.classification
627 assert classification == expected_classification
630def test_name_when_set_after_creation():
631 expected_name = "name"
632 image_feature = ImageFeature(None)
633 image_feature.names = expected_name
635 name = image_feature.names
637 assert name == expected_name
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
648 measurements = image_feature.measurements
650 assert measurements == expected_measurements
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
658 object_type = image_feature.object_type
660 assert object_type == expected_object_type
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
668 object_type = image_feature.object_type
670 assert object_type == expected_object_type
673def test_color_when_set_after_creation():
674 expected_color = (4, 5, 6)
675 image_feature = ImageFeature(None)
676 image_feature.color = expected_color
678 color = image_feature.color
680 assert color == expected_color
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
688 nucleus_geometry = image_feature.nucleus_geometry
690 assert nucleus_geometry == expected_nucleus_geometry
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 )
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 )