Repository: airctic/icevision

Issue:
Tutorial on writing a custom RecordMixin, to show how to read in special file formats like 16 bit gray scale or Dicom
Is there a dataset in icedata that can be used for this example, or do we need to get data from somewhere?
I don't intend to include any example of training a NN in the tutorial, only data reading and augmentation.

---
**Don't remove**
Main issue for examples: #39



Discussion:
Currently we don't have any datasets using dicom or 16bit gray, would you like to create a tutorial on that?

After the tutorial is complete it would be super awesome to also add the dataset to icedata

Resolving patch:
diff --git a/icevision/core/record_components.py b/icevision/core/record_components.py
--- a/icevision/core/record_components.py
+++ b/icevision/core/record_components.py
@@ -8,6 +8,7 @@
     "BaseLabelsRecordComponent",
     "InstancesLabelsRecordComponent",
     "ClassificationLabelsRecordComponent",
+    "GrayScaleRecordComponent",
     "BBoxesRecordComponent",
     "MasksRecordComponent",
     "AreasRecordComponent",
@@ -17,6 +18,7 @@
     "LossesRecordComponent",
 ]
 
+from icevision.utils.imageio import open_gray_scale_image
 from icevision.imports import *
 from icevision.utils import *
 from icevision.core.components import *
@@ -49,9 +51,6 @@ def _num_annotations(self) -> Dict[str, int]:
     def _autofix(self) -> Dict[str, bool]:
         return {}
 
-    def _remove_annotation(self, i) -> None:
-        return
-
     def _aggregate_objects(self) -> Dict[str, List[dict]]:
         return {}
 
@@ -216,6 +215,14 @@ def _builder_template(self) -> List[str]:
         return ["record{task}set_img_size(<ImgSize>)"]
 
 
+class GrayScaleRecordComponent(FilepathRecordComponent):
+    """Overwrites the FilepathRecordComponent to load radiographic images like 16bit grayscale tiff images."""
+
+    def _load(self):
+        img = open_gray_scale_image(self.filepath)
+        self.set_img(img)
+
+
 ### Annotation parsers ###
 class BaseLabelsRecordComponent(ClassMapRecordComponent):
     def __init__(self, task=tasks.common):
diff --git a/icevision/core/record_defaults.py b/icevision/core/record_defaults.py
--- a/icevision/core/record_defaults.py
+++ b/icevision/core/record_defaults.py
@@ -1,4 +1,11 @@
-__all__ = ["ObjectDetectionRecord", "InstanceSegmentationRecord"]
+__all__ = [
+    "ObjectDetectionRecord",
+    "InstanceSegmentationRecord",
+    "KeypointsRecord",
+    "GrayScaleObjectDetectionRecord",
+    "GrayScaleInstanceSegmentationRecord",
+    "GrayScaleKeypointsRecord",
+]
 
 from icevision.core.record import *
 from icevision.core.record_components import *
@@ -34,3 +41,35 @@ def KeypointsRecord():
             KeyPointsRecordComponent(),
         )
     )
+
+
+def GrayScaleObjectDetectionRecord():
+    return BaseRecord(
+        (
+            GrayScaleRecordComponent(),
+            InstancesLabelsRecordComponent(),
+            BBoxesRecordComponent(),
+        )
+    )
+
+
+def GrayScaleInstanceSegmentationRecord():
+    return BaseRecord(
+        (
+            GrayScaleRecordComponent(),
+            InstancesLabelsRecordComponent(),
+            BBoxesRecordComponent(),
+            MasksRecordComponent(),
+        )
+    )
+
+
+def GrayScaleKeypointsRecord():
+    return BaseRecord(
+        (
+            GrayScaleRecordComponent(),
+            InstancesLabelsRecordComponent(),
+            BBoxesRecordComponent(),
+            KeyPointsRecordComponent(),
+        )
+    )
diff --git a/icevision/utils/imageio.py b/icevision/utils/imageio.py
--- a/icevision/utils/imageio.py
+++ b/icevision/utils/imageio.py
@@ -32,6 +32,14 @@ def open_img(fn, gray=False) -> PIL.Image.Image:
     return image
 
 
+def open_gray_scale_image(fn):
+    "Opens an radiographic/gray scale image, stacks the channel to represent a RGB image and returns is as a 32bit float array."
+    img = np.array(PIL.Image.open(fn))
+    img = np.dstack([img, img, img])
+    img = img.astype(np.float32)
+    return img
+
+
 # TODO: Deprecated
 def get_image_size(filepath: Union[str, Path]) -> Tuple[int, int]:
     """


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -144,6 +144,22 @@ def coco_keypoints_parser(coco_dir):
     )
 
 
+@pytest.fixture
+def object_detection_record(samples_source):
+    record = ObjectDetectionRecord()
+
+    record.set_record_id(1)
+    record.set_filepath(samples_source / "voc/JPEGImages/2007_000063.jpg")
+    record.set_img_size(ImgSize(width=500, height=375))
+    record.detection.set_class_map(ClassMap(["a", "b"]))
+    record.detection.add_labels_by_id([1, 2])
+    record.detection.add_bboxes(
+        [BBox.from_xyxy(1, 2, 3, 4), BBox.from_xyxy(10, 20, 30, 40)]
+    )
+
+    return record
+
+
 @pytest.fixture(scope="module")
 def coco_mask_records(coco_mask_parser):
     return coco_mask_parser.parse(data_splitter=SingleSplitSplitter())[0]
@@ -364,6 +380,22 @@ def object_detection_record(samples_source):
     return record
 
 
+@pytest.fixture
+def gray_scale_object_detection_record(samples_source):
+    record = ObjectDetectionRecord()
+
+    record.set_record_id(1)
+    record.set_filepath(samples_source / "gray_scale/gray_scale_h_50_w_50_image.tiff")
+    record.set_img_size(ImgSize(width=50, height=50))
+    record.detection.set_class_map(ClassMap(["a", "b"]))
+    record.detection.add_labels_by_id([1, 2])
+    record.detection.add_bboxes(
+        [BBox.from_xyxy(1, 2, 3, 4), BBox.from_xyxy(10, 20, 30, 40)]
+    )
+
+    return record
+
+
 @pytest.fixture
 def instance_segmentation_record(object_detection_record):
     record = object_detection_record
@@ -374,6 +406,16 @@ def instance_segmentation_record(object_detection_record):
     return record
 
 
+@pytest.fixture
+def gray_scale_instance_segmentation_record(gray_scale_object_detection_record):
+    record = object_detection_record
+    record.add_component(MasksRecordComponent())
+
+    record.detection.add_masks([MaskArray(np.ones((2, 4, 4), dtype=np.uint8))])
+
+    return record
+
+
 @pytest.fixture
 def empty_annotations_record():
     record = BaseRecord(
diff --git a/tests/core/test_record.py b/tests/core/test_record.py
--- a/tests/core/test_record.py
+++ b/tests/core/test_record.py
@@ -27,6 +27,31 @@ def record(samples_source):
     return record
 
 
+@pytest.fixture()
+def gray_scale_record(samples_source):
+    record = BaseRecord(
+        (
+            BBoxesRecordComponent(),
+            InstancesLabelsRecordComponent(),
+            MasksRecordComponent(),
+            GrayScaleRecordComponent(),
+        )
+    )
+
+    record.set_record_id(1)
+    record.set_image_size(3, 3)
+    record.set_filepath(samples_source / "gray_scale/gray_scale_h_50_w_50_image.tiff")
+    record.detection.set_class_map(ClassMap(["a", "b"]))
+    record.detection.add_labels(["a", "b"])
+    record.detection.add_bboxes(
+        [BBox.from_xyxy(1, 2, 4, 4), BBox.from_xyxy(1, 2, 1, 3)]
+    )
+    mask_filepath = samples_source / "voc/SegmentationObject/2007_000063.png"
+    record.detection.add_masks([VocMaskFile(mask_filepath)])
+
+    return record
+
+
 @pytest.fixture
 def record_empty_annotations():
     record = BaseRecord((BBoxesRecordComponent(), InstancesLabelsRecordComponent()))
@@ -83,6 +108,22 @@ def test_record_load(record):
     assert isinstance(record_loaded.detection.masks, EncodedRLEs)
 
 
+def test_gray_scale_record_load(gray_scale_record):
+    record_loaded = gray_scale_record.load()
+
+    assert isinstance(record_loaded.img, np.ndarray)
+    assert isinstance(record_loaded.detection.masks, MaskArray)
+
+    # test original record is not modified
+    assert gray_scale_record.img == None
+    assert isinstance(gray_scale_record.detection.masks, EncodedRLEs)
+
+    # test unload
+    record_loaded.unload()
+    assert record_loaded.img == None
+    assert isinstance(record_loaded.detection.masks, EncodedRLEs)
+
+
 class TestKeypointsMetadata(KeypointsMetadata):
     labels = ("nose", "ankle")
 
diff --git a/tests/utils/test_imageio.py b/tests/utils/test_imageio.py
--- a/tests/utils/test_imageio.py
+++ b/tests/utils/test_imageio.py
@@ -30,3 +30,15 @@ def test_open_img(samples_source, fn, expected):
 def test_get_image_size(samples_source, fn, expected):
     size = get_image_size(samples_source / fn)
     assert size == (expected)
+
+
+@pytest.mark.parametrize(
+    "fn,expected",
+    [
+        ("gray_scale/gray_scale_h_10_w_10_image.tiff", (10, 10)),
+        ("gray_scale/gray_scale_h_50_w_50_image.tiff", (50, 50)),
+    ],
+)
+def test_open_gray_scale_image(samples_source, fn, expected):
+    # When returning np arrays
+    assert np.array(open_img(samples_source / fn)).shape[:-1] == expected