[1]:
import _engine_plugin_guard

import os
import shutil as sh

import numpy as np

# delete test project if the notebook was already run
proj_path = os.path.join(os.path.abspath('..'), 'test_project')
if os.path.isdir(proj_path):
    sh.rmtree(proj_path)

# formatting helpers
def bold(string: str):
    return '\033[1m' + string + '\033[0m'

Tutorial on the Python bindings for ImFusion Labels

[2]:
# The labels module resides in the imfusion package
import imfusion
from imfusion import labels
Public beta build of ImFusion Python SDK. Not for commercial use.
[3]:
# A lot of information can already be found in the module's docstring:
labels?

Setting up an ImFusion Labels project

Project is the central entity of the labels module. It holds the definitions of all annotations (Tags, Labelmap Layers, Keypoint Layers and BoundingBox Layers) and provides access to the project’s database in the form of Descriptor instances (we will get to those in the next section). The Project class can either create a new Labels project or load an existing one. All interactions with the annotations and data go through an instance of that class. Let’s create a new project for now:

[4]:
# Create an empty project named 'TestProject' stored in a folder called 'test_project' in the notebooks parent directory
# Creating the project instance will also create the project folder on disk.
project = labels.Project(name='TestProject', project_path=os.path.join(os.path.abspath('..'), 'test_project'))

Annotation Layers

Labels has the concept of annotation layers for storing any type of annotations (Labelmaps, Landmarks, BoudingBoxes), meaning that each type of annotation can have multiple different data associated with it. An example would be having one labelmap layer per annotator in a multi-annotator dataset. The definitions for these layers can be added through the Project instance analagously to the tag definitions. The instances of these layers also have a very similar interface to the tags.

[5]:
# Let's add two labelmap layers, a landmark layer and a bounding box layer
project.add_labelmap_layer(name='Segmentation')
project.add_landmark_layer(name='Points of Interest')
project.add_boundingbox_layer(name='Region of Interest')

print(f'Here are the defined labelmap layers:', f'{project.labelmap_layers=}', '', sep='\n')
print(f'Here are the defined landmark layers:', f'{project.landmark_layers=}', '', sep='\n')
print(f'Here are the defined bounding box layers:', f'{project.boundingbox_layers=}', '', sep='\n')
Here are the defined labelmap layers:
project.labelmap_layers=LabelMapsAccessor(
        LabelMapLayer(id=p6mWrFYpzc8m, name='Segmentation')
)

Here are the defined landmark layers:
project.landmark_layers=LandmarkLayersAccessor(
        LandmarkLayer(id=dICO9aWsoPsO, name='Points of Interest')
)

Here are the defined bounding box layers:
project.boundingbox_layers=BoundingBoxLayersAccessor(
        BoundingBoxLayer(id=QZcOWd9ZeKSw, name='Region of Interest')
)

[6]:
# Each layer can store independent annotation definitions
segmentation_layer = project.labelmap_layers[0]
points_layer = project.landmark_layers[0]
roi_layer = project.boundingbox_layers[0]


#For now the layers are empty
print(bold('Background label is automatically added with a value of 0 when the layer is created.'))
print(f'{segmentation_layer.annotations=}')
print(f'{points_layer.annotations=}')
print(f'{roi_layer.annotations=}')
Background label is automatically added with a value of 0 when the layer is created.
segmentation_layer.annotations=LabelsAccessor(
        Label(name='Background', color=(0, 0, 0), value=0)
)
points_layer.annotations=LandmarksAccessor(

)
roi_layer.annotations=BoundingBoxAccessor(

)
[7]:
# We can add new definitions to a layer using the add_annotation method
segmentation_layer.add_annotation(name='Circle', value=1, color=(128, 0, 0))
segmentation_layer.add_annotation(name='Core', value=2, color=(128, 128, 0))
points_layer.add_annotation(name='Center', color=(0, 128, 0))
roi_layer.add_annotation(name='Containing Box', color=(0, 0, 128))

print(f'{segmentation_layer.annotations=}')
print(f'{points_layer.annotations=}')
print(f'{roi_layer.annotations=}')
segmentation_layer.annotations=LabelsAccessor(
        Label(name='Background', color=(0, 0, 0), value=0),
        Label(name='Circle', color=(128, 0, 0), value=1),
        Label(name='Core', color=(128, 128, 0), value=2)
)
points_layer.annotations=LandmarksAccessor(
        Landmark(name='Center', color=(0, 128, 0))
)
roi_layer.annotations=BoundingBoxAccessor(
        BoundingBox(name='Containing Box', color=(0, 0, 128))
)

Tags

Tag definitions can be added using the add_tag method on a Project instance. This method will add the new tag to the project and return an instance of the Tag class that represents the newly created tag. This class bundles information about the tag definition with the actual tag value (if it is accessed through a Descriptor as we will see later). Tags, much like most of the other classes in the module that represent annotations in Labels, have a name, a color and a value. Additionally, they store a reference to their owning project and descriptor and know their index.

[8]:
# New tags can be added
project.add_tag(name='MyBool', kind=labels.TagKind.Bool)
project.add_tag(name='MyNumber', kind=labels.TagKind.Float, color=(10, 20, 30))
enum_tag = project.add_tag(name='MyChoices', kind=labels.TagKind.Enum, options=['A', 'B'])
enum_tag.add_option('C')  # We can also add options to enum tags afterwards
print(f'Here are the defined tags:\n{project.tags=}')
Here are the defined tags:
project.tags=TagsAccessor(
        Tag(name='MyBool', id='1v0gMQm8VH5w', kind=<TagKind.Bool: 0>, color=(255, 255, 255)),
        Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30)),
        Tag(name='MyChoices', id='byOPycbPqM9o', kind=<TagKind.Enum: 1>, color=(255, 255, 255), options=['default', 'A', 'B', 'C'])
)
[9]:
# You can retrieve tag definitions from the project using their name or index:
print(f'Using name:\t\t{project.tags["MyNumber"]=}\n'
      f'Using index:\t{project.tags[1]=}')
Using name:             project.tags["MyNumber"]=Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30))
Using index:    project.tags[1]=Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30))
[10]:
# Using a Tag instance you can access the information in the tag definition and the parents of the tag
print(f'{enum_tag.name=}',
      f'{enum_tag.color=}',
      f'{enum_tag.options=}',
      '',
      bold('These are the parents of this tag:'),
      f'{enum_tag.project=}',
      f'{enum_tag.descriptor=}',
      '',
      bold('Index in the project`s tag definitions:'),
      f'{enum_tag.index=}',
      '',
      bold('The tag has no value, since it is accessed through the project and not a descriptor:'),
      f'{enum_tag.value=}',
      sep='\n')
enum_tag.name='MyChoices'
enum_tag.color=(255, 255, 255)
enum_tag.options=['default', 'A', 'B', 'C']

These are the parents of this tag:
enum_tag.project=<imfusion.labels._bindings.Project object at 0x7f172216e270>
enum_tag.descriptor=None

Index in the project`s tag definitions:
enum_tag.index=2

The tag has no value, since it is accessed through the project and not a descriptor:
enum_tag.value=None

Adding data

Data in Labels is handled as instances of the Descriptor class. It stores the meta information about a data samples, lets you load the underlying image and access the annotations on that particular sample. New data can be added to the project using the add_descriptor method on the Project class. It expects a SharedImageSet as the basis for creating a new descriptor. Optionally, the descriptor can be named using the name parameter. If name is not supplied it will use the name of the SharedImageSet. Additionally, we can tell Labels whether it should store a copy of the data in the project folder or not using the own_copy parameter. If the SharedImageSet was created on the fly (it will not have a DataSourceComponent) then this flag is ignored and Labels always saves a copy to the project folder.

[11]:
# Let's create a circle and use that as a data sample
side_length = 128
radius = side_length // 4
circle = np.zeros((side_length,side_length))
Y, X = np.ogrid[:side_length, :side_length]
dist_from_center = np.sqrt((X - side_length//2)**2 + (Y-side_length//2)**2)
circle[dist_from_center <= radius] = 1

# Now we create a SharedImageSet with the circle and add it to the project
circle_sis = imfusion.SharedImageSet(circle[None, ..., None])  # We have to create a batch and a channel dimension
descriptor = project.add_descriptor(circle_sis, name='Circle')

# We can also load data from disk and add it to our project
# To demonstrate this, we will first save our circle to disk and then load it back in:
save_path = os.path.join(project.path, 'circle.imf')
imfusion.save([circle_sis], save_path)
circle_sis2, *_ = imfusion.load(save_path)
circle_sis2.name = 'Circle-from-disk'  # If we give the SIS a name we won't have to specify it when adding it as a descriptor
descriptor2 = project.add_descriptor(circle_sis2)

# Note that the two descriptors have different identifiers, since they are treated as separate entities
# Also note that for the descriptor which we added from a numpy array `own_copy` was automatically set to True, meaning that labels saved a copy of the data in the project folder
print(descriptor)
print(descriptor2)
[GUI.Animations] No global backend set: installing a dummy one.
Descriptor(
        boundingbox_layers=BoundingBoxLayersAccessor(
                BoundingBoxLayer(id=QZcOWd9ZeKSw, name='Region of Interest', value=None)
        )
        byte_size=65536
        comments=''
        grouping=[]
        has_data=False
        height=128
        identifier='oWsRLnHFRuXn'
        import_time=1726731256011
        is_locked=False
        labelmap_layers=LabelMapsAccessor(
                LabelMapLayer(id=p6mWrFYpzc8m, name='Segmentation', value=None)
        )
        landmark_layers=LandmarkLayersAccessor(
                LandmarkLayer(id=dICO9aWsoPsO, name='Points of Interest', value=None)
        )
        latest_edit_time=1726731256011
        load_path=('/data/imfusion/github/public-python-demos/test_project/data/oWsRLnHFRuXn.imf', '')
        modality=<Modality.NA: 0>
        n_channels=1
        n_images=1
        n_slices=1
        name='Circle'
        original_data_path=''
        own_copy=True
        patient_name=''
        project=<imfusion.labels._bindings.Project object at 0x7f172216e270>
        region_of_interest=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        roi=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        scale=1.0
        series_instance_uid=''
        shift=0.0
        spacing=array([1., 1., 1.])
        sub_file_id=''
        tags=TagsAccessor(
                Tag(name='MyBool', id='1v0gMQm8VH5w', kind=<TagKind.Bool: 0>, color=(255, 255, 255), value=False),
                Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30), value=0.0),
                Tag(name='MyChoices', id='byOPycbPqM9o', kind=<TagKind.Enum: 1>, color=(255, 255, 255), options=['default', 'A', 'B', 'C'], value='default')
        )
        thumbnail_path='/data/imfusion/github/public-python-demos/test_project/thumbnails/data/oWsRLnHFRuXn.png'
        top_down=True
        type=<PixelType.FLOAT: 5126>
        width=128)
Descriptor(
        boundingbox_layers=BoundingBoxLayersAccessor(
                BoundingBoxLayer(id=QZcOWd9ZeKSw, name='Region of Interest', value=None)
        )
        byte_size=65536
        comments=''
        grouping=[]
        has_data=False
        height=128
        identifier='ayEssZSFPlal'
        import_time=1726731256036
        is_locked=False
        labelmap_layers=LabelMapsAccessor(
                LabelMapLayer(id=p6mWrFYpzc8m, name='Segmentation', value=None)
        )
        landmark_layers=LandmarkLayersAccessor(
                LandmarkLayer(id=dICO9aWsoPsO, name='Points of Interest', value=None)
        )
        latest_edit_time=1726731256036
        load_path=('circle.imf', '')
        modality=<Modality.NA: 0>
        n_channels=1
        n_images=1
        n_slices=1
        name='Circle-from-disk'
        original_data_path='circle.imf'
        own_copy=False
        patient_name=''
        project=<imfusion.labels._bindings.Project object at 0x7f172216e270>
        region_of_interest=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        roi=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        scale=1.0
        series_instance_uid=''
        shift=0.0
        spacing=array([1., 1., 1.])
        sub_file_id=''
        tags=TagsAccessor(
                Tag(name='MyBool', id='1v0gMQm8VH5w', kind=<TagKind.Bool: 0>, color=(255, 255, 255), value=False),
                Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30), value=0.0),
                Tag(name='MyChoices', id='byOPycbPqM9o', kind=<TagKind.Enum: 1>, color=(255, 255, 255), options=['default', 'A', 'B', 'C'], value='default')
        )
        thumbnail_path='/data/imfusion/github/public-python-demos/test_project/thumbnails/data/ayEssZSFPlal.png'
        top_down=True
        type=<PixelType.FLOAT: 5126>
        width=128)
[12]:
# We can now find the descriptors in the project using the `descriptor` attribute
print(project.descriptors)
[Descriptor(
        boundingbox_layers=BoundingBoxLayersAccessor(
                BoundingBoxLayer(id=QZcOWd9ZeKSw, name='Region of Interest', value=None)
        )
        byte_size=65536
        comments=''
        grouping=[]
        has_data=False
        height=128
        identifier='oWsRLnHFRuXn'
        import_time=1726731256011
        is_locked=False
        labelmap_layers=LabelMapsAccessor(
                LabelMapLayer(id=p6mWrFYpzc8m, name='Segmentation', value=None)
        )
        landmark_layers=LandmarkLayersAccessor(
                LandmarkLayer(id=dICO9aWsoPsO, name='Points of Interest', value=None)
        )
        latest_edit_time=1726731256011
        load_path=('/data/imfusion/github/public-python-demos/test_project/data/oWsRLnHFRuXn.imf', '')
        modality=<Modality.NA: 0>
        n_channels=1
        n_images=1
        n_slices=1
        name='Circle'
        original_data_path=''
        own_copy=True
        patient_name=''
        project=<imfusion.labels._bindings.Project object at 0x7f172216e270>
        region_of_interest=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        roi=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        scale=1.0
        series_instance_uid=''
        shift=0.0
        spacing=array([1., 1., 1.])
        sub_file_id=''
        tags=TagsAccessor(
                Tag(name='MyBool', id='1v0gMQm8VH5w', kind=<TagKind.Bool: 0>, color=(255, 255, 255), value=False),
                Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30), value=0.0),
                Tag(name='MyChoices', id='byOPycbPqM9o', kind=<TagKind.Enum: 1>, color=(255, 255, 255), options=['default', 'A', 'B', 'C'], value='default')
        )
        thumbnail_path='/data/imfusion/github/public-python-demos/test_project/thumbnails/data/oWsRLnHFRuXn.png'
        top_down=True
        type=<PixelType.FLOAT: 5126>
        width=128), Descriptor(
        boundingbox_layers=BoundingBoxLayersAccessor(
                BoundingBoxLayer(id=QZcOWd9ZeKSw, name='Region of Interest', value=None)
        )
        byte_size=65536
        comments=''
        grouping=[]
        has_data=False
        height=128
        identifier='ayEssZSFPlal'
        import_time=1726731256036
        is_locked=False
        labelmap_layers=LabelMapsAccessor(
                LabelMapLayer(id=p6mWrFYpzc8m, name='Segmentation', value=None)
        )
        landmark_layers=LandmarkLayersAccessor(
                LandmarkLayer(id=dICO9aWsoPsO, name='Points of Interest', value=None)
        )
        latest_edit_time=1726731256036
        load_path=('circle.imf', '')
        modality=<Modality.NA: 0>
        n_channels=1
        n_images=1
        n_slices=1
        name='Circle-from-disk'
        original_data_path='circle.imf'
        own_copy=False
        patient_name=''
        project=<imfusion.labels._bindings.Project object at 0x7f172216e270>
        region_of_interest=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        roi=(array([0, 0, 0], dtype=int32), array([0, 0, 0], dtype=int32))
        scale=1.0
        series_instance_uid=''
        shift=0.0
        spacing=array([1., 1., 1.])
        sub_file_id=''
        tags=TagsAccessor(
                Tag(name='MyBool', id='1v0gMQm8VH5w', kind=<TagKind.Bool: 0>, color=(255, 255, 255), value=False),
                Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30), value=0.0),
                Tag(name='MyChoices', id='byOPycbPqM9o', kind=<TagKind.Enum: 1>, color=(255, 255, 255), options=['default', 'A', 'B', 'C'], value='default')
        )
        thumbnail_path='/data/imfusion/github/public-python-demos/test_project/thumbnails/data/ayEssZSFPlal.png'
        top_down=True
        type=<PixelType.FLOAT: 5126>
        width=128)]
[13]:
# We can also retrieve the image from the descriptor
circle_from_descriptor = descriptor.load_image(crop_to_roi=False).numpy()  # We don't have a roi on this descriptor but this argument can be used to either get the full or the cropped image
assert np.allclose(circle, circle_from_descriptor.squeeze())

Adding annotations to data

While the project holds the definitions of tags and annotations, their values can only be changed when accessing them through a Descriptor instance.

Tags

[14]:
# Given our descriptor, we can set the values of its tags
descriptor.tags['MyBool'] = True
descriptor.tags['MyNumber'] = 9001
descriptor.tags['MyChoices'] = 'B'

# Since the tags now have non-default values they implicitly evaluate to True
for tag in descriptor.tags:
    print(tag)
    assert tag

# We can reset the tags by setting them to the default values of their respective tag type
descriptor.tags['MyBool'] = False
descriptor.tags['MyNumber'] = 0
descriptor.tags['MyChoices'] = 'default'

# Now the tags will evaluate to False again
for tag in descriptor.tags:
    print(tag)
    assert not tag
Tag(name='MyBool', id='1v0gMQm8VH5w', kind=<TagKind.Bool: 0>, color=(255, 255, 255), value=True)
Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30), value=9001.0)
Tag(name='MyChoices', id='byOPycbPqM9o', kind=<TagKind.Enum: 1>, color=(255, 255, 255), options=['default', 'A', 'B', 'C'], value='B')
Tag(name='MyBool', id='1v0gMQm8VH5w', kind=<TagKind.Bool: 0>, color=(255, 255, 255), value=False)
Tag(name='MyNumber', id='Yk4jntz9YmwX', kind=<TagKind.Float: 2>, color=(10, 20, 30), value=0.0)
Tag(name='MyChoices', id='byOPycbPqM9o', kind=<TagKind.Enum: 1>, color=(255, 255, 255), options=['default', 'A', 'B', 'C'], value='default')

Annotations

[15]:
# Now we can add some annotation to our circle

# First let's add a segmentation map of the pixels that belong to the circle
# Note that this labelmap will be saved to disk once we call `save_new_data` (that's just how Labels currently works)
labelmap = imfusion.SharedImageSet(circle.astype(np.uint8)[None, ..., None])
descriptor.labelmap_layers['Segmentation'].save_new_data(labelmap)

# And we can verify that it is indeed stored for this descriptor
print(descriptor.labelmap_layers['Segmentation'].load())
imfusion.SharedImageSet(size: 1, [imfusion.SharedImage(UBYTE width: 128 height: 128)])
[16]:
# We can also add a landmark at the center of the circle
point_set = labels.LandmarkSet.from_descriptor(descriptor=descriptor, layer_name='Points of Interest')
point_set.add(type='Center', frame=0, world=(0, 0, 0))

descriptor.landmark_layers['Points of Interest'].save_new_data(point_set)

print(descriptor.landmark_layers['Points of Interest'].load().asdict())
{'Center': {0: [(0.0, 0.0, 0.0)]}}
[17]:
# Finally let's add a box around the circle
# It works analogous to adding a landmark
box_set = labels.BoxSet.from_descriptor(descriptor=descriptor, layer_name='Region of Interest')
box_set.add(type='Containing Box', frame=0, top_left=(-32, -32, 1), lower_right=(32, 32, 1))

descriptor.boundingbox_layers['Region of Interest'].save_new_data(box_set)

print(descriptor.boundingbox_layers['Region of Interest'].load().asdict())
{'Containing Box': {0: [((-32.0, -32.0, 1.0), (32.0, 32.0, 1.0))]}}

Saving the project

Almost all actions we perform on the Project instance are held in memory. The only exception is when we add data to the annotation layers of a descriptor as it is written to disk immediately. To save the modifications on the project we have to call its save method

[18]:
# Here we save all the changes we made to the project to disk
project.save()

# You can now open the project through the Labels GUI and verify that everything is as you expect it to be

Loading the project

[19]:
# Let's load the project back in and verify that everything is still there
loaded_project = labels.Project.load(project.path)

for loaded, expected in zip(loaded_project.descriptors, project.descriptors):
    assert loaded.name == expected.name
    assert loaded.identifier == expected.identifier

for loaded, expected in zip(loaded_project.tags, project.tags):
    assert loaded.name == expected.name
    assert loaded.kind == expected.kind
[20]:
# We can modify the loaded project and then save it back.
# For example let's add a new label and modify the labelmap of one of the circle descriptors

# Instead of calling `Project.save()` at the end, you can also use a context manager to automatically save the project when exiting the context
with loaded_project:

    # We can also modify the annotation definition from the descriptor
    descriptor = loaded_project.descriptors[0]
    labelmap_layer = descriptor.labelmap_layers['Segmentation']
    labelmap_layer.add_annotation(name='Core', value=2)

    # Let's load the label map and modify it
    labelmap = labelmap_layer.load().numpy().squeeze()
    labelmap[dist_from_center < radius//2] = 2

    # Now we can write the labelmap back to disk
    labelmap_layer.save_new_data(imfusion.SharedImageSet(labelmap[None, ..., None]))