dbdicom 0.3.0__py3-none-any.whl → 0.3.2__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.
Potentially problematic release.
This version of dbdicom might be problematic. Click here for more details.
- dbdicom/api.py +53 -10
- dbdicom/database.py +122 -0
- dbdicom/dataset.py +22 -71
- dbdicom/dbd.py +124 -182
- dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/register.py +315 -234
- dbdicom/sop_classes/mr_image.py +122 -130
- dbdicom/sop_classes/parametric_map.py +94 -20
- dbdicom/utils/image.py +7 -6
- {dbdicom-0.3.0.dist-info → dbdicom-0.3.2.dist-info}/METADATA +2 -4
- {dbdicom-0.3.0.dist-info → dbdicom-0.3.2.dist-info}/RECORD +16 -15
- {dbdicom-0.3.0.dist-info → dbdicom-0.3.2.dist-info}/WHEEL +1 -1
- {dbdicom-0.3.0.dist-info → dbdicom-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.3.0.dist-info → dbdicom-0.3.2.dist-info}/top_level.txt +0 -0
dbdicom/api.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
|
|
2
|
-
import numpy as np
|
|
3
2
|
import vreg
|
|
4
3
|
|
|
5
4
|
from dbdicom.dbd import DataBaseDicom
|
|
@@ -16,6 +15,15 @@ def open(path:str) -> DataBaseDicom:
|
|
|
16
15
|
"""
|
|
17
16
|
return DataBaseDicom(path)
|
|
18
17
|
|
|
18
|
+
def to_json(path):
|
|
19
|
+
"""Summarise the contents of the DICOM folder in a json file
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
path (str): path to the DICOM folder
|
|
23
|
+
"""
|
|
24
|
+
dbd = open(path)
|
|
25
|
+
dbd.close()
|
|
26
|
+
|
|
19
27
|
def print(path):
|
|
20
28
|
"""Print the contents of the DICOM folder
|
|
21
29
|
|
|
@@ -24,6 +32,7 @@ def print(path):
|
|
|
24
32
|
"""
|
|
25
33
|
dbd = open(path)
|
|
26
34
|
dbd.print()
|
|
35
|
+
dbd.close()
|
|
27
36
|
|
|
28
37
|
|
|
29
38
|
def summary(path) -> dict:
|
|
@@ -36,7 +45,24 @@ def summary(path) -> dict:
|
|
|
36
45
|
dict: Nested dictionary with summary information on the database.
|
|
37
46
|
"""
|
|
38
47
|
dbd = open(path)
|
|
39
|
-
|
|
48
|
+
s = dbd.summary()
|
|
49
|
+
dbd.close()
|
|
50
|
+
return s
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def tree(path) -> dict:
|
|
54
|
+
"""Return the structure of the database as a dictionary tree.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
path (str): path to the DICOM folder
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
dict: Nested dictionary with summary information on the database.
|
|
61
|
+
"""
|
|
62
|
+
dbd = open(path)
|
|
63
|
+
s = dbd.register
|
|
64
|
+
dbd.close()
|
|
65
|
+
return s
|
|
40
66
|
|
|
41
67
|
|
|
42
68
|
def patients(path, name:str=None, contains:str=None, isin:list=None)->list:
|
|
@@ -56,7 +82,9 @@ def patients(path, name:str=None, contains:str=None, isin:list=None)->list:
|
|
|
56
82
|
list: list of patients fulfilling the criteria.
|
|
57
83
|
"""
|
|
58
84
|
dbd = open(path)
|
|
59
|
-
|
|
85
|
+
p = dbd.patients(name, contains, isin)
|
|
86
|
+
dbd.close()
|
|
87
|
+
return p
|
|
60
88
|
|
|
61
89
|
|
|
62
90
|
def studies(entity:str | list, name:str=None, contains:str=None, isin:list=None)->list:
|
|
@@ -79,10 +107,14 @@ def studies(entity:str | list, name:str=None, contains:str=None, isin:list=None)
|
|
|
79
107
|
"""
|
|
80
108
|
if isinstance(entity, str): # path = folder
|
|
81
109
|
dbd = open(entity)
|
|
82
|
-
|
|
110
|
+
s = dbd.studies(entity, name, contains, isin)
|
|
111
|
+
dbd.close()
|
|
112
|
+
return s
|
|
83
113
|
elif len(entity)==2: # path = patient
|
|
84
114
|
dbd = open(entity[0])
|
|
85
|
-
|
|
115
|
+
s = dbd.studies(entity, name, contains, isin)
|
|
116
|
+
dbd.close()
|
|
117
|
+
return s
|
|
86
118
|
else:
|
|
87
119
|
raise ValueError(
|
|
88
120
|
"The path must be a folder or a 2-element list "
|
|
@@ -110,10 +142,14 @@ def series(entity:str | list, name:str=None, contains:str=None, isin:list=None)-
|
|
|
110
142
|
"""
|
|
111
143
|
if isinstance(entity, str): # path = folder
|
|
112
144
|
dbd = open(entity)
|
|
113
|
-
|
|
145
|
+
s = dbd.series(entity, name, contains, isin)
|
|
146
|
+
dbd.close()
|
|
147
|
+
return s
|
|
114
148
|
elif len(entity) in [2,3]:
|
|
115
149
|
dbd = open(entity[0])
|
|
116
|
-
|
|
150
|
+
s = dbd.series(entity, name, contains, isin)
|
|
151
|
+
dbd.close()
|
|
152
|
+
return s
|
|
117
153
|
else:
|
|
118
154
|
raise ValueError(
|
|
119
155
|
"To retrieve a series, the entity must be a database, patient or study."
|
|
@@ -168,7 +204,9 @@ def volume(series:list, dims:list=None, multislice=False) -> vreg.Volume3D:
|
|
|
168
204
|
vreg.Volume3D: vole read from the series.
|
|
169
205
|
"""
|
|
170
206
|
dbd = open(series[0])
|
|
171
|
-
|
|
207
|
+
vol = dbd.volume(series, dims, multislice)
|
|
208
|
+
dbd.close()
|
|
209
|
+
return vol
|
|
172
210
|
|
|
173
211
|
def write_volume(vol:vreg.Volume3D, series:list, ref:list=None,
|
|
174
212
|
multislice=False):
|
|
@@ -200,6 +238,7 @@ def to_nifti(series:list, file:str, dims:list=None, multislice=False):
|
|
|
200
238
|
"""
|
|
201
239
|
dbd = open(series[0])
|
|
202
240
|
dbd.to_nifti(series, file, dims, multislice)
|
|
241
|
+
dbd.close()
|
|
203
242
|
|
|
204
243
|
def from_nifti(file:str, series:list, ref:list=None, multislice=False):
|
|
205
244
|
"""Create a DICOM series from a nifti file.
|
|
@@ -232,7 +271,9 @@ def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
|
|
|
232
271
|
return value.
|
|
233
272
|
"""
|
|
234
273
|
dbd = open(series[0])
|
|
235
|
-
|
|
274
|
+
array = dbd.pixel_data(series, dims, include)
|
|
275
|
+
dbd.close()
|
|
276
|
+
return array
|
|
236
277
|
|
|
237
278
|
# write_pixel_data()
|
|
238
279
|
# values()
|
|
@@ -255,7 +296,9 @@ def unique(pars:list, entity:list) -> dict:
|
|
|
255
296
|
dict: dictionary with unique values for each attribute.
|
|
256
297
|
"""
|
|
257
298
|
dbd = open(entity[0])
|
|
258
|
-
|
|
299
|
+
u = dbd.unique(pars, entity)
|
|
300
|
+
dbd.close()
|
|
301
|
+
return u
|
|
259
302
|
|
|
260
303
|
|
|
261
304
|
|
dbdicom/database.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import pydicom
|
|
7
|
+
|
|
8
|
+
import dbdicom.utils.dcm4che as dcm4che
|
|
9
|
+
import dbdicom.utils.files as filetools
|
|
10
|
+
import dbdicom.dataset as dbdataset
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
COLUMNS = [
|
|
14
|
+
# Identifiers (unique)
|
|
15
|
+
'PatientID',
|
|
16
|
+
'StudyInstanceUID',
|
|
17
|
+
'SeriesInstanceUID',
|
|
18
|
+
'SOPInstanceUID',
|
|
19
|
+
# Human-readable identifiers (not unique)
|
|
20
|
+
'PatientName',
|
|
21
|
+
'StudyDescription',
|
|
22
|
+
'StudyDate',
|
|
23
|
+
'SeriesDescription',
|
|
24
|
+
'SeriesNumber',
|
|
25
|
+
'InstanceNumber',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
def read(path):
|
|
29
|
+
files = filetools.all_files(path)
|
|
30
|
+
tags = COLUMNS + ['NumberOfFrames'] # + ['SOPClassUID']
|
|
31
|
+
array = []
|
|
32
|
+
dicom_files = []
|
|
33
|
+
for i, file in tqdm(enumerate(files), total=len(files), desc='Reading DICOM folder'):
|
|
34
|
+
try:
|
|
35
|
+
ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
|
|
36
|
+
except:
|
|
37
|
+
pass
|
|
38
|
+
else:
|
|
39
|
+
if isinstance(ds, pydicom.dataset.FileDataset):
|
|
40
|
+
if 'TransferSyntaxUID' in ds.file_meta:
|
|
41
|
+
if not 'Rows' in ds: # Image only
|
|
42
|
+
continue
|
|
43
|
+
row = dbdataset.get_values(ds, tags)
|
|
44
|
+
array.append(row)
|
|
45
|
+
index = os.path.relpath(file, path)
|
|
46
|
+
dicom_files.append(index)
|
|
47
|
+
df = pd.DataFrame(array, index = dicom_files, columns = tags)
|
|
48
|
+
df = _multiframe_to_singleframe(path, df)
|
|
49
|
+
dbtree = _tree(df)
|
|
50
|
+
return dbtree
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _multiframe_to_singleframe(path, df):
|
|
54
|
+
"""Converts all multiframe files in the folder into single-frame files.
|
|
55
|
+
|
|
56
|
+
Reads all the multi-frame files in the folder,
|
|
57
|
+
converts them to singleframe files, and delete the original multiframe file.
|
|
58
|
+
"""
|
|
59
|
+
singleframe = df.NumberOfFrames.isnull()
|
|
60
|
+
multiframe = singleframe == False
|
|
61
|
+
nr_multiframe = multiframe.sum()
|
|
62
|
+
if nr_multiframe != 0:
|
|
63
|
+
for relpath in tqdm(df[multiframe].index.values, desc="Converting multiframe file " + relpath):
|
|
64
|
+
filepath = os.path.join(path, relpath)
|
|
65
|
+
singleframe_files = dcm4che.split_multiframe(filepath)
|
|
66
|
+
if singleframe_files != []:
|
|
67
|
+
# add the single frame files to the dataframe
|
|
68
|
+
dfnew = read(singleframe_files, df.columns, path)
|
|
69
|
+
df = pd.concat([df, dfnew])
|
|
70
|
+
# delete the original multiframe
|
|
71
|
+
os.remove(filepath)
|
|
72
|
+
# drop the file also if the conversion has failed
|
|
73
|
+
df.drop(index=relpath, inplace=True)
|
|
74
|
+
df.drop('NumberOfFrames', axis=1, inplace=True)
|
|
75
|
+
return df
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _tree(df):
|
|
79
|
+
# A human-readable summary tree
|
|
80
|
+
|
|
81
|
+
df.sort_values(['PatientID','StudyInstanceUID','SeriesNumber'], inplace=True)
|
|
82
|
+
df = df.fillna('None')
|
|
83
|
+
summary = []
|
|
84
|
+
|
|
85
|
+
for uid_patient in df.PatientID.unique():
|
|
86
|
+
df_patient = df[df.PatientID == uid_patient]
|
|
87
|
+
patient_name = df_patient.PatientName.values[0]
|
|
88
|
+
patient = {
|
|
89
|
+
'PatientName': patient_name,
|
|
90
|
+
'PatientID': uid_patient,
|
|
91
|
+
'studies': [],
|
|
92
|
+
}
|
|
93
|
+
summary.append(patient)
|
|
94
|
+
for uid_study in df_patient.StudyInstanceUID.unique():
|
|
95
|
+
df_study = df_patient[df_patient.StudyInstanceUID == uid_study]
|
|
96
|
+
study_desc = df_study.StudyDescription.values[0]
|
|
97
|
+
study_date = df_study.StudyDate.values[0]
|
|
98
|
+
study = {
|
|
99
|
+
'StudyDescription': study_desc,
|
|
100
|
+
'StudyDate': study_date,
|
|
101
|
+
'StudyInstanceUID': uid_study,
|
|
102
|
+
'series': [],
|
|
103
|
+
}
|
|
104
|
+
patient['studies'].append(study)
|
|
105
|
+
for uid_sery in df_study.SeriesInstanceUID.unique():
|
|
106
|
+
df_series = df_study[df_study.SeriesInstanceUID == uid_sery]
|
|
107
|
+
series_desc = df_series.SeriesDescription.values[0]
|
|
108
|
+
series_nr = int(df_series.SeriesNumber.values[0])
|
|
109
|
+
series = {
|
|
110
|
+
'SeriesNumber': series_nr,
|
|
111
|
+
'SeriesDescription': series_desc,
|
|
112
|
+
'SeriesInstanceUID': uid_sery,
|
|
113
|
+
'instances': {},
|
|
114
|
+
}
|
|
115
|
+
study['series'].append(series)
|
|
116
|
+
for uid_instance in df_series.SOPInstanceUID.unique():
|
|
117
|
+
df_instance = df_series[df_series.SOPInstanceUID == uid_instance]
|
|
118
|
+
instance_nr = int(df_instance.InstanceNumber.values[0])
|
|
119
|
+
relpath = df_instance.index[0]
|
|
120
|
+
series['instances'][instance_nr]=relpath
|
|
121
|
+
|
|
122
|
+
return summary
|
dbdicom/dataset.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
# Test data
|
|
2
|
+
# https://www.aliza-dicom-viewer.com/download/datasets
|
|
3
|
+
|
|
1
4
|
import os
|
|
2
5
|
from datetime import datetime
|
|
3
6
|
import struct
|
|
4
7
|
from tqdm import tqdm
|
|
5
8
|
|
|
6
9
|
import numpy as np
|
|
7
|
-
import pandas as pd
|
|
8
10
|
import pydicom
|
|
9
11
|
from pydicom.util.codify import code_file
|
|
10
12
|
import pydicom.config
|
|
11
|
-
from pydicom.dataset import Dataset
|
|
12
13
|
import vreg
|
|
13
14
|
|
|
15
|
+
|
|
14
16
|
import dbdicom.utils.image as image
|
|
15
17
|
import dbdicom.utils.variables as variables
|
|
16
18
|
from dbdicom.sop_classes import (
|
|
@@ -71,6 +73,8 @@ def new_dataset(sop_class):
|
|
|
71
73
|
return xray_angiographic_image.default()
|
|
72
74
|
if sop_class == 'UltrasoundMultiFrameImage':
|
|
73
75
|
return ultrasound_multiframe_image.default()
|
|
76
|
+
if sop_class == 'ParametricMap':
|
|
77
|
+
return parametric_map.default()
|
|
74
78
|
else:
|
|
75
79
|
raise ValueError(
|
|
76
80
|
f"DICOM class {sop_class} is not currently supported"
|
|
@@ -235,7 +239,7 @@ def codify(source_file, save_file, **kwargs):
|
|
|
235
239
|
file.close()
|
|
236
240
|
|
|
237
241
|
|
|
238
|
-
def read_data(files, tags, path=None, images_only=False):
|
|
242
|
+
def read_data(files, tags, path=None, images_only=False): # obsolete??
|
|
239
243
|
|
|
240
244
|
if np.isscalar(files):
|
|
241
245
|
files = [files]
|
|
@@ -263,34 +267,6 @@ def read_data(files, tags, path=None, images_only=False):
|
|
|
263
267
|
|
|
264
268
|
|
|
265
269
|
|
|
266
|
-
def read_dataframe(files, tags, path=None, images_only=False):
|
|
267
|
-
if np.isscalar(files):
|
|
268
|
-
files = [files]
|
|
269
|
-
if np.isscalar(tags):
|
|
270
|
-
tags = [tags]
|
|
271
|
-
array = []
|
|
272
|
-
dicom_files = []
|
|
273
|
-
for i, file in tqdm(enumerate(files), desc='Reading DICOM folder'):
|
|
274
|
-
try:
|
|
275
|
-
ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
|
|
276
|
-
except:
|
|
277
|
-
pass
|
|
278
|
-
else:
|
|
279
|
-
if isinstance(ds, pydicom.dataset.FileDataset):
|
|
280
|
-
if 'TransferSyntaxUID' in ds.file_meta:
|
|
281
|
-
if images_only:
|
|
282
|
-
if not 'Rows' in ds:
|
|
283
|
-
continue
|
|
284
|
-
row = get_values(ds, tags)
|
|
285
|
-
array.append(row)
|
|
286
|
-
if path is None:
|
|
287
|
-
index = file
|
|
288
|
-
else:
|
|
289
|
-
index = os.path.relpath(file, path)
|
|
290
|
-
dicom_files.append(index)
|
|
291
|
-
df = pd.DataFrame(array, index = dicom_files, columns = tags)
|
|
292
|
-
return df
|
|
293
|
-
|
|
294
270
|
|
|
295
271
|
def _add_new(ds, tag, value, VR='OW'):
|
|
296
272
|
if not isinstance(tag, pydicom.tag.BaseTag):
|
|
@@ -583,7 +559,7 @@ def pixel_data(ds):
|
|
|
583
559
|
try:
|
|
584
560
|
array = ds.pixel_array
|
|
585
561
|
except:
|
|
586
|
-
|
|
562
|
+
raise ValueError("Dataset has no pixel data.")
|
|
587
563
|
array = array.astype(np.float32)
|
|
588
564
|
slope = float(getattr(ds, 'RescaleSlope', 1))
|
|
589
565
|
intercept = float(getattr(ds, 'RescaleIntercept', 0))
|
|
@@ -592,7 +568,7 @@ def pixel_data(ds):
|
|
|
592
568
|
return np.transpose(array)
|
|
593
569
|
|
|
594
570
|
|
|
595
|
-
def set_pixel_data(ds, array
|
|
571
|
+
def set_pixel_data(ds, array):
|
|
596
572
|
if array is None:
|
|
597
573
|
raise ValueError('The pixel array cannot be set to an empty value.')
|
|
598
574
|
|
|
@@ -608,7 +584,7 @@ def set_pixel_data(ds, array, value_range=None):
|
|
|
608
584
|
# if array.ndim >= 3: # remove spurious dimensions of 1
|
|
609
585
|
# array = np.squeeze(array)
|
|
610
586
|
|
|
611
|
-
array = image.clip(array.astype(np.float32)
|
|
587
|
+
array = image.clip(array.astype(np.float32))
|
|
612
588
|
array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
|
|
613
589
|
array = np.transpose(array)
|
|
614
590
|
|
|
@@ -632,6 +608,15 @@ def volume(ds, multislice=False):
|
|
|
632
608
|
def set_volume(ds, volume:vreg.Volume3D, multislice=False):
|
|
633
609
|
if volume is None:
|
|
634
610
|
raise ValueError('The volume cannot be set to an empty value.')
|
|
611
|
+
try:
|
|
612
|
+
mod = SOPCLASSMODULE[ds.SOPClassUID]
|
|
613
|
+
except KeyError:
|
|
614
|
+
raise ValueError(
|
|
615
|
+
f"DICOM class {ds.SOPClassUID} is not currently supported."
|
|
616
|
+
)
|
|
617
|
+
if hasattr(mod, 'set_volume'):
|
|
618
|
+
return getattr(mod, 'set_volume')(ds, volume)
|
|
619
|
+
|
|
635
620
|
image = np.squeeze(volume.values)
|
|
636
621
|
if image.ndim != 2:
|
|
637
622
|
raise ValueError("Can only write 2D images to a dataset.")
|
|
@@ -712,41 +697,7 @@ def set_signal_type(ds, value):
|
|
|
712
697
|
|
|
713
698
|
|
|
714
699
|
|
|
700
|
+
if __name__=='__main__':
|
|
715
701
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
# # Date and Time of Creation
|
|
719
|
-
# dt = datetime.now()
|
|
720
|
-
# timeStr = dt.strftime('%H%M%S') # long format with micro seconds
|
|
721
|
-
|
|
722
|
-
# ds.ContentDate = dt.strftime('%Y%m%d')
|
|
723
|
-
# ds.ContentTime = timeStr
|
|
724
|
-
# ds.AcquisitionDate = dt.strftime('%Y%m%d')
|
|
725
|
-
# ds.AcquisitionTime = timeStr
|
|
726
|
-
# ds.SeriesDate = dt.strftime('%Y%m%d')
|
|
727
|
-
# ds.SeriesTime = timeStr
|
|
728
|
-
# ds.InstanceCreationDate = dt.strftime('%Y%m%d')
|
|
729
|
-
# ds.InstanceCreationTime = timeStr
|
|
730
|
-
|
|
731
|
-
# if UID is not None:
|
|
732
|
-
|
|
733
|
-
# # overwrite UIDs
|
|
734
|
-
# ds.PatientID = UID[0]
|
|
735
|
-
# ds.StudyInstanceUID = UID[1]
|
|
736
|
-
# ds.SeriesInstanceUID = UID[2]
|
|
737
|
-
# ds.SOPInstanceUID = UID[3]
|
|
738
|
-
|
|
739
|
-
# if ref is not None:
|
|
740
|
-
|
|
741
|
-
# # Series, Instance and Class for Reference
|
|
742
|
-
# refd_instance = Dataset()
|
|
743
|
-
# refd_instance.ReferencedSOPClassUID = ref.SOPClassUID
|
|
744
|
-
# refd_instance.ReferencedSOPInstanceUID = ref.SOPInstanceUID
|
|
745
|
-
|
|
746
|
-
# refd_series = Dataset()
|
|
747
|
-
# refd_series.ReferencedInstanceSequence = Sequence([refd_instance])
|
|
748
|
-
# refd_series.SeriesInstanceUID = ds.SeriesInstanceUID
|
|
749
|
-
|
|
750
|
-
# ds.ReferencedSeriesSequence = Sequence([refd_series])
|
|
751
|
-
|
|
752
|
-
# return ds
|
|
702
|
+
pass
|
|
703
|
+
#codify('C:\\Users\\md1spsx\\Documents\\f32bit.dcm', 'C:\\Users\\md1spsx\\Documents\\f32bit.py')
|