Skip to content

dicomtrolley.parsing

Models parsing things into study/series/instance structure

DICOMObjectTree

For querying, adding and modifying collections of DICOM objects

Serves a different purpose from parsing.TreeNode. parsing.TreeNode is meant to build internally coherent(with parents, children) DICOMObjects from raw input. DICOMObjectTree allows manipulating these objects when they have already been created

Notes

If series or instances are added to the store, parent study or series will be created and added automatically.

Source code in dicomtrolley/parsing.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
class DICOMObjectTree:
    """For querying, adding and modifying collections of DICOM objects

    Serves a different purpose from parsing.TreeNode. parsing.TreeNode is meant
    to build internally coherent(with parents, children) DICOMObjects from raw input.
    DICOMObjectTree allows manipulating these objects when they have already been
    created

    Notes
    -----
    If series or instances are added to the store, parent study or series
    will be created and added automatically.
    """

    def __init__(self, objects: Sequence[DICOMObject]):
        # make sure the tree is complete and all objects are based in studies
        studies = DICOMParseTree.init_from_objects(objects).as_studies()
        self._study_dict: Dict[str, Study] = {x.uid: x for x in studies}

    def __getitem__(self, series_uid) -> Study:
        return self._study_dict[series_uid]

    @property
    def studies(self):
        return list(self._study_dict.values())

    def add_study(self, study: Study):
        """Add given study to tree, overwriting exiting"""
        self._study_dict[study.uid] = study

    def retrieve(self, reference: DICOMObjectReference) -> DICOMObject:
        """Retrieve data for the given study, series or instance from tree

        Raises
        ------
        DICOMObjectNotFound
            If data for the given parameters does not exist in tree
        """
        try:
            if isinstance(reference, StudyReference):
                return self[reference.study_uid]
            elif isinstance(reference, SeriesReference):
                return self[reference.study_uid].get(reference.series_uid)
            elif isinstance(reference, InstanceReference):
                return (
                    self[reference.study_uid]
                    .get(reference.series_uid)
                    .get(reference.instance_uid)
                )
            else:
                raise DICOMTrolleyError(
                    f"Unknown reference type {type(reference)}"
                )
        except KeyError as e:
            raise DICOMObjectNotFound(
                f"Study with uid {reference.study_uid} not found in tree"
            ) from e

add_study(study)

Add given study to tree, overwriting exiting

Source code in dicomtrolley/parsing.py
215
216
217
def add_study(self, study: Study):
    """Add given study to tree, overwriting exiting"""
    self._study_dict[study.uid] = study

retrieve(reference)

Retrieve data for the given study, series or instance from tree

Raises

DICOMObjectNotFound If data for the given parameters does not exist in tree

Source code in dicomtrolley/parsing.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def retrieve(self, reference: DICOMObjectReference) -> DICOMObject:
    """Retrieve data for the given study, series or instance from tree

    Raises
    ------
    DICOMObjectNotFound
        If data for the given parameters does not exist in tree
    """
    try:
        if isinstance(reference, StudyReference):
            return self[reference.study_uid]
        elif isinstance(reference, SeriesReference):
            return self[reference.study_uid].get(reference.series_uid)
        elif isinstance(reference, InstanceReference):
            return (
                self[reference.study_uid]
                .get(reference.series_uid)
                .get(reference.instance_uid)
            )
        else:
            raise DICOMTrolleyError(
                f"Unknown reference type {type(reference)}"
            )
    except KeyError as e:
        raise DICOMObjectNotFound(
            f"Study with uid {reference.study_uid} not found in tree"
        ) from e

DICOMParseTree

Models study/series/instance as a tree. Allows arbitrary branch insertions

Source code in dicomtrolley/parsing.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
class DICOMParseTree:
    """Models study/series/instance as a tree. Allows arbitrary branch insertions"""

    def __init__(self, root=None, allow_overwrite=False):
        """

        Parameters
        ----------
        root: TreeNode, optional
            Root node. Will contain all. Defaults to empty
        allow_overwrite: bool, optional
            If False, will raise exception when overwriting any TreeNode.data,
            Defaults to False
        """
        if not root:
            self.root = TreeNode(allow_overwrite=allow_overwrite)

    @classmethod
    def init_from_objects(cls, objects: Sequence[DICOMObject]):
        """Create a tree from given objects. Useful for augmenting query results

        Notes
        -----
        Parent objects for instances and series will be created to
        ensure all nodes are connected to the tree root. For example, running this
        method with only a single instance as input will result in a regular
        root->study->series->instance tree. This is possible because all DICOMObjects
        maintain links to their parent objects

        Returns
        -------
        DICOMParseTree
        """

        tree = cls(allow_overwrite=True)
        for item in objects:
            tree.update(item)

        return tree

    def update(self, other):
        """Add all nodes in other to this tree. Overwrite existing

        Parameters
        ----------
        other: DICOMObject
            Dicom object to add to this tree
        """
        for item in flatten(other):
            self.insert_dicom_object(item)

    def insert(self, data, study_uid, series_uid=None, instance_uid=None):
        """Insert data at the given level in tree. Will complain about missing
        branches

        Raises
        ------
        DICOMTrolleyError
            If inserting fails for any reason
        """
        if instance_uid and not series_uid:
            DICOMTrolleyError(
                f"Instance was given ({instance_uid}) but series was not. I can "
                f"not insert this into a study/series/instance tree"
            )
        try:
            if series_uid:
                if instance_uid:
                    self.root[study_uid][series_uid][instance_uid].data = data
                else:
                    self.root[study_uid][series_uid].data = data
            else:
                self.root[study_uid].data = data
        except ValueError as e:
            raise DICOMTrolleyError(
                f"Error inserting dataset into "
                f"{study_uid}/{series_uid}/{instance_uid}: {e}"
            ) from e

    def insert_dataset(self, ds: Dataset):
        self.insert(
            data=ds,
            study_uid=ds.get("StudyInstanceUID"),
            series_uid=ds.get("SeriesInstanceUID"),
            instance_uid=ds.get("SOPInstanceUID"),
        )

    def insert_dicom_object(self, dicom_object: DICOMObject):
        """Insert dicomtrolley dicom object"""

        if isinstance(dicom_object, Study):
            self.insert(data=dicom_object.data, study_uid=dicom_object.uid)
        elif isinstance(dicom_object, Series):
            self.insert(
                data=dicom_object.data,
                study_uid=dicom_object.parent.uid,
                series_uid=dicom_object.uid,
            )
        elif isinstance(dicom_object, Instance):
            self.insert(
                data=dicom_object.data,
                study_uid=dicom_object.parent.parent.uid,
                series_uid=dicom_object.parent.uid,
                instance_uid=dicom_object.uid,
            )
        else:
            raise DICOMTrolleyError(
                f"Unknown DICOM object {dicom_object}. I do "
                f"not know where to insert this in the DICOM "
                f"tree"
            )

    @staticmethod
    def as_study(study_node_in) -> Study:
        """Tree node at the study level into a Study containing Series, Instances"""

        def value_or_dataset(val):
            if val:
                return val
            else:
                return Dataset()

        study_instance_uid, study_node = study_node_in

        study = Study(
            uid=str(study_instance_uid),
            data=value_or_dataset(study_node.data),
            series=tuple(),
        )
        all_series = []
        for series_instance_uid, series_node in study_node.items():
            series = Series(
                uid=series_instance_uid,
                data=value_or_dataset(series_node.data),
                parent=study,
                instances=tuple(),
            )
            all_series.append(series)
            all_instances = []
            for sop_instance_uid, instance_node in series_node.items():
                all_instances.append(
                    Instance(
                        uid=sop_instance_uid,
                        data=value_or_dataset(instance_node.data),
                        parent=series,
                    )
                )
            series.instances = tuple(all_instances)
        study.series = tuple(all_series)
        return study

    def as_studies(self) -> List[Study]:
        """Tree as DICOMObject instances: Study containing Series, Instance"""

        return [self.as_study(node) for node in self.root.items()]

__init__(root=None, allow_overwrite=False)

Parameters

root: TreeNode, optional Root node. Will contain all. Defaults to empty allow_overwrite: bool, optional If False, will raise exception when overwriting any TreeNode.data, Defaults to False

Source code in dicomtrolley/parsing.py
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(self, root=None, allow_overwrite=False):
    """

    Parameters
    ----------
    root: TreeNode, optional
        Root node. Will contain all. Defaults to empty
    allow_overwrite: bool, optional
        If False, will raise exception when overwriting any TreeNode.data,
        Defaults to False
    """
    if not root:
        self.root = TreeNode(allow_overwrite=allow_overwrite)

as_studies()

Tree as DICOMObject instances: Study containing Series, Instance

Source code in dicomtrolley/parsing.py
179
180
181
182
def as_studies(self) -> List[Study]:
    """Tree as DICOMObject instances: Study containing Series, Instance"""

    return [self.as_study(node) for node in self.root.items()]

as_study(study_node_in) staticmethod

Tree node at the study level into a Study containing Series, Instances

Source code in dicomtrolley/parsing.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
@staticmethod
def as_study(study_node_in) -> Study:
    """Tree node at the study level into a Study containing Series, Instances"""

    def value_or_dataset(val):
        if val:
            return val
        else:
            return Dataset()

    study_instance_uid, study_node = study_node_in

    study = Study(
        uid=str(study_instance_uid),
        data=value_or_dataset(study_node.data),
        series=tuple(),
    )
    all_series = []
    for series_instance_uid, series_node in study_node.items():
        series = Series(
            uid=series_instance_uid,
            data=value_or_dataset(series_node.data),
            parent=study,
            instances=tuple(),
        )
        all_series.append(series)
        all_instances = []
        for sop_instance_uid, instance_node in series_node.items():
            all_instances.append(
                Instance(
                    uid=sop_instance_uid,
                    data=value_or_dataset(instance_node.data),
                    parent=series,
                )
            )
        series.instances = tuple(all_instances)
    study.series = tuple(all_series)
    return study

init_from_objects(objects) classmethod

Create a tree from given objects. Useful for augmenting query results

Notes

Parent objects for instances and series will be created to ensure all nodes are connected to the tree root. For example, running this method with only a single instance as input will result in a regular root->study->series->instance tree. This is possible because all DICOMObjects maintain links to their parent objects

Returns

DICOMParseTree

Source code in dicomtrolley/parsing.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@classmethod
def init_from_objects(cls, objects: Sequence[DICOMObject]):
    """Create a tree from given objects. Useful for augmenting query results

    Notes
    -----
    Parent objects for instances and series will be created to
    ensure all nodes are connected to the tree root. For example, running this
    method with only a single instance as input will result in a regular
    root->study->series->instance tree. This is possible because all DICOMObjects
    maintain links to their parent objects

    Returns
    -------
    DICOMParseTree
    """

    tree = cls(allow_overwrite=True)
    for item in objects:
        tree.update(item)

    return tree

insert(data, study_uid, series_uid=None, instance_uid=None)

Insert data at the given level in tree. Will complain about missing branches

Raises

DICOMTrolleyError If inserting fails for any reason

Source code in dicomtrolley/parsing.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def insert(self, data, study_uid, series_uid=None, instance_uid=None):
    """Insert data at the given level in tree. Will complain about missing
    branches

    Raises
    ------
    DICOMTrolleyError
        If inserting fails for any reason
    """
    if instance_uid and not series_uid:
        DICOMTrolleyError(
            f"Instance was given ({instance_uid}) but series was not. I can "
            f"not insert this into a study/series/instance tree"
        )
    try:
        if series_uid:
            if instance_uid:
                self.root[study_uid][series_uid][instance_uid].data = data
            else:
                self.root[study_uid][series_uid].data = data
        else:
            self.root[study_uid].data = data
    except ValueError as e:
        raise DICOMTrolleyError(
            f"Error inserting dataset into "
            f"{study_uid}/{series_uid}/{instance_uid}: {e}"
        ) from e

insert_dicom_object(dicom_object)

Insert dicomtrolley dicom object

Source code in dicomtrolley/parsing.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def insert_dicom_object(self, dicom_object: DICOMObject):
    """Insert dicomtrolley dicom object"""

    if isinstance(dicom_object, Study):
        self.insert(data=dicom_object.data, study_uid=dicom_object.uid)
    elif isinstance(dicom_object, Series):
        self.insert(
            data=dicom_object.data,
            study_uid=dicom_object.parent.uid,
            series_uid=dicom_object.uid,
        )
    elif isinstance(dicom_object, Instance):
        self.insert(
            data=dicom_object.data,
            study_uid=dicom_object.parent.parent.uid,
            series_uid=dicom_object.parent.uid,
            instance_uid=dicom_object.uid,
        )
    else:
        raise DICOMTrolleyError(
            f"Unknown DICOM object {dicom_object}. I do "
            f"not know where to insert this in the DICOM "
            f"tree"
        )

update(other)

Add all nodes in other to this tree. Overwrite existing

Parameters

other: DICOMObject Dicom object to add to this tree

Source code in dicomtrolley/parsing.py
68
69
70
71
72
73
74
75
76
77
def update(self, other):
    """Add all nodes in other to this tree. Overwrite existing

    Parameters
    ----------
    other: DICOMObject
        Dicom object to add to this tree
    """
    for item in flatten(other):
        self.insert_dicom_object(item)

flatten(dicom_object)

All nodes and as a flat list, study, series

Source code in dicomtrolley/parsing.py
20
21
22
23
24
25
def flatten(dicom_object) -> List[DICOMObject]:
    """All nodes and as a flat list, study, series"""
    nodes = [dicom_object]
    for child in dicom_object.children():
        nodes = nodes + flatten(child)
    return nodes