Attaching Metadata to Data#

The DataComponent class allows attaching arbitrary metadata to any instance of Data. The example below demonstrates this by attaching an ImageInfoDataComponent, which can hold information such as patient ID, patient name, etc., to Data , e.g. a SharedImageSet.

>>> import imfusion
>>> sis = imfusion.SharedImageSet()
>>> sis.components.add(imfusion.data.components.ImageInfoDataComponent())
ImageInfoDataComponent(
    patient_id="",
    patient_name="",
    ...)
>>> sis.components.image_info.patient_id = "my_patient_id"

There is a variety of built-in DataComponent available in the ImFusion SDK. The full list of DataComponent can be obtained with imfusion.data.components.list_available().

Note

The imfusion.data.components.list_available() function returns all components that are implemented in the ImFusion C++ SDK, including those that are not yet available in the Python bindings and hence do not have a corresponding specialized class in the imfusion.data.components Python module.

Regardless of whether Python bindings exist, imfusion.data.components.create() enables creating every DataComponent in Python:

>>> prop = imfusion.Properties({"patientId": "my_patient_id"})
>>> image_info_data_comp = imfusion.data.components.create("ImageInfoDataComponent", prop)
>>> image_info_data_comp
ImageInfoDataComponent(
    patient_id="my_patient_id",
    patient_name="",
    ...)

As with any DataComponent, the actual data fields of ImageInfoDataComponent are accessible through Python attributes:

>>> image_info_data_comp.patient_id
'my_patient_id'

Note

There is a difference in naming conventions between the DataComponent’s Python attributes (e.g., patient_id) and the corresponding Properties keys (e.g., patientId). The former are listed in the documentation of the specific class, e.g., ImageInfoDataComponent. To view the keys used in Properties, you can instantiate the respective data component with its default constructor and inspect the parameters of its Properties, as in the example below:

>>> image_info_data_comp = imfusion.data.components.create("ImageInfoDataComponent")
>>> prop: imfusion.Properties = image_info_data_comp.configuration()
>>> prop.params()
['patientId', 'patientName', ...]

Properties may also be used to modify the data component after it has been created:

>>> image_info_data_comp = imfusion.data.components.create("ImageInfoDataComponent")
>>> prop = imfusion.Properties({"patientId": "my_patient_id"})
>>> image_info_data_comp.configure(prop)

Provided the data component is available in our Python bindings, you can of course also instantiate it directly:

>>> image_info_data_comp = imfusion.data.components.ImageInfoDataComponent()

Custom Python Data Components#

You can also define custom data components directly in Python using the register() decorator together with field():

>>> @imfusion.data.components.register(name="my_component")
... class MyComponent:
...     custom_info: str = imfusion.data.components.field()
...
...     def __init__(self, custom_info: str = ""):
...         self.custom_info = custom_info

The registered name serves as both the factory ID (see create()) and the named attribute on DataComponentList:

>>> sis = imfusion.SharedImageSet()
>>> sis.components.add(MyComponent(custom_info="my_custom_info"))
MyComponent(custom_info='my_custom_info')
>>> assert sis.components.my_component.custom_info == "my_custom_info"

register() automatically injects configure, configuration, id, and name into the class, so the component fully conforms to the imfusion data component interface. If __eq__ and __repr__ are not defined, field-based implementations are generated automatically. Generated equality compares only attributes declared with field(). Other instance attributes are ignored. Define __eq__ explicitly if those attributes must participate in equality.

Warning

Defining configure, configuration, id, or name in the class body raises a TypeError at registration time, since these names are reserved for the injected implementations.

For static type-checker support, the class may optionally inherit from imfusion.data.components.typing.PyDataComponent, which declares the injected methods. This is not required and has no effect at runtime.

Advanced usage: custom serialization#

field() accepts optional serializer and deserializer callbacks for types that Properties does not natively support, such as dict. The serializer must return a str, and serializer and deserializer must always be provided together:

>>> import json
>>> @imfusion.data.components.register(name="my_component_advanced")
... class MyComponentAdvanced():
...     metadata: dict = imfusion.data.components.field(
...         serializer=json.dumps,
...         deserializer=json.loads,
...     )
...
...     def __init__(self, metadata: dict | None = None):
...         self.metadata = metadata or {}
>>> sis = imfusion.SharedImageSet()
>>> comp = MyComponentAdvanced(metadata={"label": "tumor", "confidence": 0.95})
>>> sis.components.add(comp)
MyComponentAdvanced(metadata={'label': 'tumor', 'confidence': 0.95})
>>> assert sis.components.my_component_advanced.metadata["label"] == "tumor"

Field values are always stored as native Python objects on the component instance, regardless of whether a serializer is provided. The serializer is called on assignment to validate that it returns a str (an early check), and again by configuration() to produce the serialized form for Properties. The deserializer is called by configure() to restore the Python value when the component is loaded, for example from an .imf file.