midom 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- midom/__init__.py +0 -0
- midom/components.py +168 -0
- midom/constants.py +77 -0
- midom/identifiers.py +484 -0
- midom/subspaces.py +653 -0
- midom-0.1.0.dist-info/METADATA +66 -0
- midom-0.1.0.dist-info/RECORD +10 -0
- midom-0.1.0.dist-info/WHEEL +5 -0
- midom-0.1.0.dist-info/licenses/LICENSE +674 -0
- midom-0.1.0.dist-info/top_level.txt +1 -0
midom/__init__.py
ADDED
|
File without changes
|
midom/components.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Python definition of the parts"""
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
from dicomcriterion import Criterion
|
|
5
|
+
from dicomcriterion.exceptions import CriterionError
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, field_validator
|
|
7
|
+
from pydantic.functional_serializers import field_serializer
|
|
8
|
+
|
|
9
|
+
from midom.constants import ActionCode, ActionCodes
|
|
10
|
+
from midom.identifiers import (
|
|
11
|
+
PrivateBlockTagIdentifier,
|
|
12
|
+
TagIdentifier,
|
|
13
|
+
tag_identifier_from_string,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PixelArea(BaseModel):
|
|
18
|
+
area: tuple[int, int, int, int]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TagAction(BaseModel):
|
|
22
|
+
"""Describes the action to take for a single identifier (tag or tag group) and the
|
|
23
|
+
reason for this action.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
model_config = ConfigDict(
|
|
27
|
+
arbitrary_types_allowed=True
|
|
28
|
+
) # for TagIdentifier
|
|
29
|
+
|
|
30
|
+
identifier: TagIdentifier
|
|
31
|
+
action: ActionCode
|
|
32
|
+
justification: str
|
|
33
|
+
|
|
34
|
+
@field_serializer("identifier")
|
|
35
|
+
def serialize_identifier(self, value, _info):
|
|
36
|
+
return value.key()
|
|
37
|
+
|
|
38
|
+
@field_validator("identifier", mode="before")
|
|
39
|
+
@classmethod
|
|
40
|
+
def deserialize_identifier(cls, value):
|
|
41
|
+
if isinstance(value, TagIdentifier):
|
|
42
|
+
return value
|
|
43
|
+
elif isinstance(value, str):
|
|
44
|
+
return tag_identifier_from_string(value)
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f'Invalid input data for TagAction.identifier: "{value}"'
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@field_serializer("action")
|
|
51
|
+
def serialize_action(self, value, _info):
|
|
52
|
+
return value.key
|
|
53
|
+
|
|
54
|
+
@field_validator("action", mode="before")
|
|
55
|
+
@classmethod
|
|
56
|
+
def deserialize_action(cls, value):
|
|
57
|
+
if isinstance(value, ActionCode):
|
|
58
|
+
return value
|
|
59
|
+
elif isinstance(value, str):
|
|
60
|
+
return ActionCodes.from_string(value)
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f'Invalid input data for TagAction.identifier: "{value}"'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class CriterionString(BaseModel):
|
|
68
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
69
|
+
|
|
70
|
+
content: Criterion
|
|
71
|
+
|
|
72
|
+
@field_serializer("content")
|
|
73
|
+
def serialize_identifier(self, value, _info):
|
|
74
|
+
return value._expression
|
|
75
|
+
|
|
76
|
+
@field_validator("content", mode="before")
|
|
77
|
+
@classmethod
|
|
78
|
+
def deserialize_tag(cls, value):
|
|
79
|
+
if isinstance(value, Criterion):
|
|
80
|
+
return value
|
|
81
|
+
elif isinstance(value, str):
|
|
82
|
+
try:
|
|
83
|
+
return Criterion(value)
|
|
84
|
+
except CriterionError as e:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f'Could not parse "{value}" as criterion. Original error: {e}'
|
|
87
|
+
) from e
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Expected Criterion or str as input but "
|
|
91
|
+
f'found f"{type(value)}" for "{value}'
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Filter(BaseModel):
|
|
96
|
+
criterion: CriterionString
|
|
97
|
+
justification: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class PixelOperation(BaseModel):
|
|
101
|
+
description: str
|
|
102
|
+
criterion: CriterionString
|
|
103
|
+
areas: List[PixelArea]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PrivateAllowGroup(BaseModel):
|
|
107
|
+
# Allow arbitrary for PrivateBlockTagIdentifier
|
|
108
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
109
|
+
|
|
110
|
+
elements: List[PrivateBlockTagIdentifier]
|
|
111
|
+
justification: str
|
|
112
|
+
|
|
113
|
+
@field_serializer("elements")
|
|
114
|
+
def serialize_identifier(self, value, _info):
|
|
115
|
+
return [x.key() for x in value]
|
|
116
|
+
|
|
117
|
+
@field_validator("elements", mode="before")
|
|
118
|
+
@classmethod
|
|
119
|
+
def deserialize_tag(cls, values):
|
|
120
|
+
if not isinstance(values, list):
|
|
121
|
+
ValueError(f"elements should be a list, got {values}")
|
|
122
|
+
deserialized = []
|
|
123
|
+
for x in values:
|
|
124
|
+
if isinstance(x, TagIdentifier):
|
|
125
|
+
deserialized.append(x)
|
|
126
|
+
elif isinstance(x, str):
|
|
127
|
+
deserialized.append(tag_identifier_from_string(x))
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f'Invalid input data for TagAction.identifier: "{x}"'
|
|
131
|
+
)
|
|
132
|
+
return deserialized
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Protocol(BaseModel):
|
|
136
|
+
"""Defines how to handle the deidentification of any incoming dataset. It does
|
|
137
|
+
not say anything about implementation, it only prescribes what should be done
|
|
138
|
+
to each part of a dataset and under which circumstances to reject it outright.
|
|
139
|
+
|
|
140
|
+
Overview of elements:
|
|
141
|
+
tags: Dict[str, List[TagAction]]
|
|
142
|
+
SOPInstanceUID that can contain wildcards: List of what to do with DICOM tags
|
|
143
|
+
|
|
144
|
+
filters: List[Filter]
|
|
145
|
+
If any of these filters matches, reject the DICOM dataset
|
|
146
|
+
|
|
147
|
+
pixel: List[PixelOperation]
|
|
148
|
+
What to do with pixel data. Where to put black boxes if a rule matches
|
|
149
|
+
|
|
150
|
+
private: List[PrivateAllowGroup]
|
|
151
|
+
Which private tags to allow and why
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
tags: Dict[str, List[TagAction]]
|
|
155
|
+
filters: List[Filter]
|
|
156
|
+
pixel: List[PixelOperation]
|
|
157
|
+
private: List[PrivateAllowGroup]
|
|
158
|
+
|
|
159
|
+
def sort_tags(self):
|
|
160
|
+
"""Sort tag list for each SOPInstanceUID according to generality. The more
|
|
161
|
+
general, the lower in the list. This means you can take any DICOM tag and
|
|
162
|
+
try to match each element in the list until one hits.
|
|
163
|
+
"""
|
|
164
|
+
for key, action_list in self.tags.items():
|
|
165
|
+
self.tags[key] = sorted(
|
|
166
|
+
action_list,
|
|
167
|
+
key=lambda x: x.identifier.number_of_matchable_tags(),
|
|
168
|
+
)
|
midom/constants.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Implementations of some of the MIDOM constants and objects"""
|
|
2
|
+
from pydantic.dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DeltaCodes:
|
|
6
|
+
"""How has a DICOM element changed?"""
|
|
7
|
+
|
|
8
|
+
UNCHANGED = "UNCHANGED"
|
|
9
|
+
CHANGED = "CHANGED"
|
|
10
|
+
REMOVED = "REMOVED"
|
|
11
|
+
EMPTIED = "EMPTIED"
|
|
12
|
+
CREATED = "CREATED"
|
|
13
|
+
|
|
14
|
+
ALL = {UNCHANGED, CHANGED, REMOVED, EMPTIED, CREATED}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ActionCode:
|
|
19
|
+
key: str
|
|
20
|
+
var_name: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ActionCodes:
|
|
24
|
+
"""NEMA specifications from table E1-1. Indicates what to do with each tag"""
|
|
25
|
+
|
|
26
|
+
DUMMY = ActionCode("D", "DUMMY") # replace with dummy
|
|
27
|
+
EMPTY = ActionCode("Z", "EMPTY") # replace with zero length
|
|
28
|
+
REMOVE = ActionCode("X", "REMOVE") # remove
|
|
29
|
+
KEEP = ActionCode("K", "KEEP") # keep
|
|
30
|
+
CLEAN = ActionCode("C", "CLEAN") # clean
|
|
31
|
+
UID = ActionCode("U", "UID") # replace with consistent UID
|
|
32
|
+
EMPTY_OR_DUMMY = ActionCode("Z/D", "EMPTY_OR_DUMMY")
|
|
33
|
+
REPLACE_OR_DUMMY = ActionCode("X/Z", "REPLACE_OR_DUMMY") # X unless Z is
|
|
34
|
+
# required for consistency
|
|
35
|
+
REMOVE_OR_EMPTY = ActionCode("X/Z", "REMOVE_OR_EMPTY") # X unless Z is
|
|
36
|
+
# required for consistency
|
|
37
|
+
REMOVE_OR_DUMMY = ActionCode("X/D", "REMOVE_OR_DUMMY") # X unless D is
|
|
38
|
+
# required for consistency
|
|
39
|
+
REMOVE_OR_EMPTY_OR_DUMMY = ActionCode(
|
|
40
|
+
"X/Z/D", "REMOVE_OR_EMPTY_OR_DUMMY"
|
|
41
|
+
) # X unless Z or D is required
|
|
42
|
+
REMOVE_OR_EMPTY_OR_UID = ActionCode(
|
|
43
|
+
"X/Z/U*", "REMOVE_OR_EMPTY_OR_UID"
|
|
44
|
+
) # X unless Z or U is required
|
|
45
|
+
UNDEFINED = ActionCode(
|
|
46
|
+
"?", "Undefined"
|
|
47
|
+
) # not part of ALL below, special case
|
|
48
|
+
|
|
49
|
+
ALL = {
|
|
50
|
+
DUMMY,
|
|
51
|
+
EMPTY,
|
|
52
|
+
REMOVE,
|
|
53
|
+
KEEP,
|
|
54
|
+
CLEAN,
|
|
55
|
+
UID,
|
|
56
|
+
EMPTY_OR_DUMMY,
|
|
57
|
+
REMOVE_OR_EMPTY,
|
|
58
|
+
REMOVE_OR_DUMMY,
|
|
59
|
+
REMOVE_OR_EMPTY_OR_DUMMY,
|
|
60
|
+
REMOVE_OR_EMPTY_OR_UID,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
PER_KEY = {x.key: x for x in ALL}
|
|
64
|
+
PER_VAR_NAME = {x.var_name: x for x in ALL}
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_string(cls, key_or_var_name: str):
|
|
68
|
+
"""I've got a string with either a full name or a key. Which action
|
|
69
|
+
code is this?
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return (cls.PER_KEY | cls.PER_VAR_NAME)[key_or_var_name]
|
|
73
|
+
except KeyError as e:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Unknown action code '{key_or_var_name}'. I "
|
|
76
|
+
f"know {','.join([str(x) for x in cls.ALL])}"
|
|
77
|
+
) from e
|