Skip to content

dicomtrolley.storage

Classes and functions for writing downloaded results to disk

DICOMDiskStorage

A place on disk that you can write datasets to.

Source code in dicomtrolley/storage.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DICOMDiskStorage:
    """A place on disk that you can write datasets to."""

    def save(self, dataset, path: Optional[str] = None) -> None:
        """Write dataset. Creates sub-folders if needed

        Parameters
        ----------
        dataset: Dataset
            Save this pydicom dataset
        path: str, optional
            Save to this path. Defaults to saving to default path

        """
        raise NotImplementedError()

save(dataset, path=None)

Write dataset. Creates sub-folders if needed

Parameters

dataset: Dataset Save this pydicom dataset path: str, optional Save to this path. Defaults to saving to default path

Source code in dicomtrolley/storage.py
17
18
19
20
21
22
23
24
25
26
27
28
def save(self, dataset, path: Optional[str] = None) -> None:
    """Write dataset. Creates sub-folders if needed

    Parameters
    ----------
    dataset: Dataset
        Save this pydicom dataset
    path: str, optional
        Save to this path. Defaults to saving to default path

    """
    raise NotImplementedError()

FlatStorageDir

Bases: StorageDir

Stores without sub-folders, only instanceid as filename

Source code in dicomtrolley/storage.py
75
76
77
78
79
class FlatStorageDir(StorageDir):
    """Stores without sub-folders, only instanceid as filename"""

    def generate_path(self, dataset):
        return Path(self.get_value(dataset, "SOPInstanceUID"))

StorageDir

Bases: DICOMDiskStorage

Saves in folder structure studyid/seriesid/instanceid

Source code in dicomtrolley/storage.py
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
class StorageDir(DICOMDiskStorage):
    """Saves in folder structure studyid/seriesid/instanceid"""

    def __init__(self, path: str):
        self.path = path

    def __str__(self):
        return f"StorageDir at {self.path}"

    def save(self, dataset, path: Optional[str] = None):
        """Write dataset. Creates subfolders if needed.

        Raises
        ------
        StorageError
            If writing to disk does not work for some reason.
        """
        if not path:
            path = self.path

        slice_path = Path(path) / self.generate_path(dataset)
        slice_path.parent.mkdir(parents=True, exist_ok=True)

        logger.debug(f'Saving to "{slice_path}"')
        try:
            dataset.save_as(slice_path)
        except ValueError as e:
            raise StorageError() from e

    def generate_path(self, dataset):
        """A path studyid/seriesid/instanceid to save a slice to."""

        stu_uid = self.get_value(dataset, "StudyInstanceUID")
        ser_uid = self.get_value(dataset, "SeriesInstanceUID")
        sop_uid = self.get_value(dataset, "SOPInstanceUID")
        return Path(stu_uid) / ser_uid / sop_uid

    @staticmethod
    def get_value(dataset, tag_name):
        """Extract value for use in path. If not found return 'unknown'"""
        default = "unknown"
        return str(dataset.get(tag_name, default)).replace(".", "_")

generate_path(dataset)

A path studyid/seriesid/instanceid to save a slice to.

Source code in dicomtrolley/storage.py
60
61
62
63
64
65
66
def generate_path(self, dataset):
    """A path studyid/seriesid/instanceid to save a slice to."""

    stu_uid = self.get_value(dataset, "StudyInstanceUID")
    ser_uid = self.get_value(dataset, "SeriesInstanceUID")
    sop_uid = self.get_value(dataset, "SOPInstanceUID")
    return Path(stu_uid) / ser_uid / sop_uid

get_value(dataset, tag_name) staticmethod

Extract value for use in path. If not found return 'unknown'

Source code in dicomtrolley/storage.py
68
69
70
71
72
@staticmethod
def get_value(dataset, tag_name):
    """Extract value for use in path. If not found return 'unknown'"""
    default = "unknown"
    return str(dataset.get(tag_name, default)).replace(".", "_")

save(dataset, path=None)

Write dataset. Creates subfolders if needed.

Raises

StorageError If writing to disk does not work for some reason.

Source code in dicomtrolley/storage.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def save(self, dataset, path: Optional[str] = None):
    """Write dataset. Creates subfolders if needed.

    Raises
    ------
    StorageError
        If writing to disk does not work for some reason.
    """
    if not path:
        path = self.path

    slice_path = Path(path) / self.generate_path(dataset)
    slice_path.parent.mkdir(parents=True, exist_ok=True)

    logger.debug(f'Saving to "{slice_path}"')
    try:
        dataset.save_as(slice_path)
    except ValueError as e:
        raise StorageError() from e

make_writable(ds)

Make alterations to a dataset to make it writable

Source code in dicomtrolley/storage.py
115
116
117
118
119
120
121
122
123
124
125
126
127
def make_writable(ds):
    """Make alterations to a dataset to make it writable"""
    file_meta = Dataset()
    file_meta.MediaStorageSOPClassUID = ds.SOPClassUID
    file_meta.TransferSyntaxUID = (
        "1.2.840.10008.1.2.1"  # Explicit VR Little Endian
    )
    ds.file_meta = file_meta
    ds.implicit_vr = False

    ds = remove_illegal_elements_for_writing(ds)

    return ds

remove_illegal_elements_for_writing(dataset)

Remove elements for which the contents do not match the Value Representation

These exist in certain private tags obtained via /metadata. It is unclear whether this is an issue in the WADO-RS server implementation or pydicom.

Removing them is a workaround until the underlying issue can be resolved.

This function is inside WadoRSMetadata because this is the only downloader for which the output has this problem.

Notes

Modifies input dataset in-place! Even if you do not assign this function's output to a variable, the input Dataset will still be modified.

Source code in dicomtrolley/storage.py
 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
def remove_illegal_elements_for_writing(dataset):
    """Remove elements for which the contents do not match the Value Representation

    These exist in certain private tags obtained via /metadata. It is unclear
    whether this is an issue in the WADO-RS server implementation or pydicom.

    Removing them is a workaround until the underlying issue can be resolved.

    This function is inside WadoRSMetadata because this is the only downloader
    for which the output has this problem.

    Notes
    -----
    Modifies input dataset in-place! Even if you do not assign this function's
    output to a variable, the input Dataset will still be modified.
    """

    def is_illegal(element_in):
        return element_in.VR == "UN" and type(element_in.value) not in (
            str,
            bytes,
        )

    illegal_elements = [x for x in dataset if is_illegal(x)]
    for element in illegal_elements:
        del dataset[element.tag]
        logger.debug(
            f"Removing illegal element {element}. US (Unknown bytes) VR "
            f"but content is not byte-like"
        )
    return dataset