/
conceptcode.py
500 lines (389 loc) · 13.7 KB
/
conceptcode.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
183
184
185
186
187
188
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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
# Copyright 2021 SECTRA AB
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dataclasses import dataclass
from typing import Any, ClassVar, Dict, List, Optional, Type, TypeVar
from pydicom.dataset import Dataset
from pydicom.sequence import Sequence as DicomSequence
from pydicom.sr.codedict import codes
from pydicom.sr.coding import Code
ConceptCodeType = TypeVar("ConceptCodeType", bound="ConceptCode")
CidConceptCodeType = TypeVar("CidConceptCodeType", bound="CidConceptCode")
@dataclass(frozen=True)
class ConceptCode:
"""Help functions for handling SR codes.
Provides functions for converting between Code and dicom dataset.
"""
value: str
scheme_designator: str
meaning: str
scheme_version: Optional[str] = None
sequence_name: ClassVar[str]
def __hash__(self):
return hash(
(self.value, self.scheme_designator, self.meaning, self.scheme_version)
)
def __eq__(self, other: Any) -> bool:
if isinstance(other, Code):
return self.code == other
if isinstance(other, ConceptCode):
return self.code == other.code
return NotImplemented
@property
def code(self) -> Code:
return Code(
value=self.value,
scheme_designator=self.scheme_designator,
meaning=self.meaning,
scheme_version=self.scheme_version,
)
@classmethod
def from_code(cls: Type[ConceptCodeType], code: Code) -> ConceptCodeType:
return cls(
value=code.value,
scheme_designator=code.scheme_designator,
meaning=code.meaning,
scheme_version=code.scheme_version,
)
def to_ds(self) -> Dataset:
"""Codes code into DICOM dataset.
Returns
-------
Dataset
Dataset of code.
"""
ds = Dataset()
ds.CodeValue = self.value
ds.CodingSchemeDesignator = self.scheme_designator
ds.CodeMeaning = self.meaning
if self.scheme_version is not None:
ds.CodeSchemeVersion = self.scheme_version
return ds
def insert_into_ds(self, ds: Dataset) -> Dataset:
"""Codes and insert object into sequence in dataset.
Parameters
----------
ds: Dataset
Dataset to insert into.
Returns
-------
Dataset
Dataset with object inserted.
"""
# Append if sequence already set otherwise create
try:
sequence = getattr(ds, self.sequence_name)
sequence.append(self.to_ds())
except AttributeError:
setattr(ds, self.sequence_name, DicomSequence([self.to_ds()]))
return ds
@classmethod
def _from_ds(
cls: Type[ConceptCodeType],
ds: Dataset,
) -> Optional[List[ConceptCodeType]]:
"""Return list of ConceptCode from sequence in dataset.
Parameters
----------
ds: Dataset
Datasete containing code sequence.
Returns
-------
Optional[List[ConceptCodeType]
Codes created from sequence in dataset.
"""
if cls.sequence_name not in ds:
return None
return [
cls(
value=code_ds.CodeValue,
scheme_designator=code_ds.CodingSchemeDesignator,
meaning=code_ds.CodeMeaning,
scheme_version=getattr(code_ds, "CodeSchemeVersion", None),
)
for code_ds in getattr(ds, cls.sequence_name)
]
class SingleConceptCode(ConceptCode):
"""Code for concepts # type: ignore that only allow a single item"""
@classmethod
def from_ds(cls: Type[ConceptCodeType], ds: Dataset) -> Optional["ConceptCodeType"]:
"""Return measurement code for value. Value can be a code meaning (str)
or a DICOM dataset containing the code.
Parameters
----------
value: Dataset
The dataset for creating the code.
Returns
-------
Optional[ConceptCodeType]
Concept code created from value.
"""
codes = cls._from_ds(ds)
if codes is None:
return None
return codes[0]
class MultipleConceptCode(ConceptCode):
"""Code for concepts # type: ignore that allow multiple items"""
@classmethod
def from_ds(cls: Type[ConceptCodeType], ds: Dataset) -> List[ConceptCodeType]:
"""Return measurement code for value. Value can be a code meaning (str)
or a DICOM dataset containing the code.
Parameters
----------
value: Dataset
The dataset for creating the code.
Returns
-------
List[ConceptCodeType]
Concept codes created from dataset.
"""
codes = cls._from_ds(ds)
if codes is None:
return []
return codes
class CidConceptCode(ConceptCode):
"""Code for concepts # type: ignore defined in Context groups"""
cid: ClassVar[Dict[str, Code]]
def __init__(
self,
meaning: str,
value: Optional[str] = None,
scheme_designator: Optional[str] = None,
scheme_version: Optional[str] = None,
):
if value is None or scheme_designator is None:
code = self._from_meaning(meaning)
super().__init__(
value=code.value,
scheme_designator=code.scheme_designator,
meaning=code.meaning,
scheme_version=code.scheme_version,
)
else:
super().__init__(
value=value,
scheme_designator=scheme_designator,
meaning=meaning,
scheme_version=scheme_version,
)
@classmethod
def _from_meaning(cls: Type[CidConceptCodeType], meaning: str) -> Code:
try:
return next(
code
for code in cls.cid.values()
if code.meaning.lower() == meaning.lower()
)
except StopIteration:
raise ValueError(
f"There is no code with meaning '{meaning}' in {list(cls.cid.keys())}."
)
@classmethod
def list(cls) -> List[str]:
"""Return possible meanings for concept.
Returns
-------
List[str]
Possible meanings for concept.
"""
return [code.meaning for code in cls.cid.values()]
class UnitCode(SingleConceptCode):
"""Code for concepts representing units according to UCUM scheme"""
sequence_name = "MeasurementUnitsCodeSequence"
def __init__(
self,
meaning: str,
value: Optional[str] = None,
scheme_designator: Optional[str] = None,
scheme_version: Optional[str] = None,
):
if value is None or scheme_designator is None:
code = self._from_unit(meaning)
super().__init__(
value=code.value,
scheme_designator=code.scheme_designator,
meaning=code.meaning,
scheme_version=code.scheme_version,
)
else:
super().__init__(
value=value,
scheme_designator=scheme_designator,
meaning=meaning,
scheme_version=scheme_version,
)
@classmethod
def from_unit(cls, unit: str) -> "UnitCode":
"""Return UCUM scheme ConceptCode.
Parameters
----------
meaning: str
Code meaning.
Returns
-------
ConceptCode
Code created from meaning.
"""
code = cls._from_unit(unit)
return cls(
value=code.value,
scheme_designator=code.scheme_designator,
meaning=code.meaning,
scheme_version=code.scheme_version,
)
@classmethod
def _from_unit(cls, unit: str) -> Code:
"""Return UCUM scheme ConceptCode.
Parameters
----------
meaning: str
Code meaning.
Returns
----------
ConceptCode
Code created from meaning.
"""
if unit == "1":
meaning = "unary"
elif unit == "{ratio}":
meaning = "ratio"
else:
meaning = unit
return Code(value=unit, scheme_designator="UCUM", meaning=meaning)
@classmethod
def meanings(cls) -> List[str]:
"""Return possible meanings for concept.
Returns
-------
List[str]
Possible meanings for concept.
"""
return []
class MeasurementCode(CidConceptCode, SingleConceptCode):
"""
Concept code for measurement type.
Microscopy Measurement Types
"""
sequence_name = "ConceptNameCodeSequence"
cid = {"Area": Code("42798000", "SCT", "Area")}
class AnnotationTypeCode(CidConceptCode, SingleConceptCode):
"""
Concept code for annotation type.
Microscopy Annotation Property Types
"""
sequence_name = "AnnotationPropertyTypeCodeSequence"
cid = {
"Nucleus": Code("84640000", "SCT", "Nucleus"),
"EntireCell": Code("4421005", "SCT", "Cell"),
}
class AnnotationCategoryCode(CidConceptCode, SingleConceptCode):
"""
Concept code for annotation category.
From CID 7150 Segmentation Property Categories
"""
sequence_name = "AnnotationPropertyCategoryCodeSequence"
cid = codes.cid7150.concepts # type: ignore
class IlluminationCode(CidConceptCode, MultipleConceptCode):
"""
Concept code for illumination type.
From CID 8123 Microscopy Illumination Method
"""
sequence_name = "IlluminationTypeCodeSequence"
cid = codes.cid8123.concepts # type: ignore
class LenseCode(CidConceptCode, MultipleConceptCode):
"""
Concept code for lense.
From CID 8121 Microscopy Lens Type
"""
sequence_name = "LensesCodeSequence"
cid = codes.cid8121.concepts # type: ignore
class LightPathFilterCode(CidConceptCode, MultipleConceptCode):
"""
Concept code for light path filter.
From CID 8124 Microscopy Filter
"""
sequence_name = "LightPathFilterTypeStackCodeSequence"
cid = codes.cid8124.concepts # type: ignore
class ImagePathFilterCode(CidConceptCode, MultipleConceptCode):
"""
Concept code for image path filter.
From CID 8124 Microscopy Filter
"""
sequence_name = "ImagePathFilterTypeStackCodeSequence"
cid = codes.cid8124.concepts # type: ignore
class IlluminationColorCode(CidConceptCode, SingleConceptCode):
"""
Concept code for illumination color.
From CID 8122 Microscopy Illuminator and Sensor Color
"""
sequence_name = "IlluminationColorCodeSequence"
cid = codes.cid8122.concepts # type: ignore
class IlluminatorCode(CidConceptCode, SingleConceptCode):
"""
Concept code for illuminator type.
From CID 8125 Microscopy Illuminator Type
"""
sequence_name = "IlluminatorTypeCodeSequence"
cid = codes.cid8125.concepts # type: ignore
class ChannelDescriptionCode(CidConceptCode, MultipleConceptCode):
"""
Concept code for channel descriptor.
From CID 8122 Microscopy Illuminator and Sensor Color
"""
sequence_name = "ChannelDescriptionCodeSequence"
cid = codes.cid8122.concepts # type: ignore
class SpecimenCollectionProcedureCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8109.concepts # type: ignore
class SpecimenSamplingProcedureCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8110.concepts # type: ignore
class SpecimenPreparationProcedureCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8111.concepts # type: ignore
class SpecimenStainsCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8112.concepts # type: ignore
class SpecimenPreparationStepsCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8113.concepts # type: ignore
class SpecimenFixativesCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8114.concepts # type: ignore
class SpecimenEmbeddingMediaCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8115.concepts # type: ignore
class AnatomicPathologySpecimenTypesCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8103.concepts # type: ignore
class ContainerComponentTypeCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8102.concepts # type: ignore
class ContainerTypeCode(CidConceptCode, SingleConceptCode):
sequence_name = "ConceptCodeSequence"
cid = codes.cid8101.concepts # type: ignore
class ConceptNameCode(SingleConceptCode):
sequence_name = "ConceptNameCodeSequence"
@classmethod
def list(cls) -> List[str]:
return []
def dataset_to_code(dataset: Dataset) -> Code:
version = dataset.get("CodingSchemeVersion", None)
if version == "":
version = None
return Code(
dataset.CodeValue,
dataset.CodingSchemeDesignator,
dataset.CodeMeaning,
version,
)