dbdicom 0.3.1__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 +24 -1
- dbdicom/database.py +122 -0
- dbdicom/dataset.py +16 -34
- dbdicom/dbd.py +92 -167
- dbdicom/register.py +315 -234
- dbdicom/sop_classes/mr_image.py +122 -130
- dbdicom/sop_classes/parametric_map.py +93 -22
- dbdicom/utils/image.py +7 -6
- {dbdicom-0.3.1.dist-info → dbdicom-0.3.2.dist-info}/METADATA +2 -4
- {dbdicom-0.3.1.dist-info → dbdicom-0.3.2.dist-info}/RECORD +13 -12
- {dbdicom-0.3.1.dist-info → dbdicom-0.3.2.dist-info}/WHEEL +1 -1
- {dbdicom-0.3.1.dist-info → dbdicom-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.3.1.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
|
|
|
@@ -42,6 +50,21 @@ def summary(path) -> dict:
|
|
|
42
50
|
return s
|
|
43
51
|
|
|
44
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
|
|
66
|
+
|
|
67
|
+
|
|
45
68
|
def patients(path, name:str=None, contains:str=None, isin:list=None)->list:
|
|
46
69
|
"""Return a list of patients in the DICOM folder.
|
|
47
70
|
|
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
|
@@ -7,13 +7,12 @@ import struct
|
|
|
7
7
|
from tqdm import tqdm
|
|
8
8
|
|
|
9
9
|
import numpy as np
|
|
10
|
-
import pandas as pd
|
|
11
10
|
import pydicom
|
|
12
11
|
from pydicom.util.codify import code_file
|
|
13
12
|
import pydicom.config
|
|
14
|
-
from pydicom.dataset import Dataset
|
|
15
13
|
import vreg
|
|
16
14
|
|
|
15
|
+
|
|
17
16
|
import dbdicom.utils.image as image
|
|
18
17
|
import dbdicom.utils.variables as variables
|
|
19
18
|
from dbdicom.sop_classes import (
|
|
@@ -74,6 +73,8 @@ def new_dataset(sop_class):
|
|
|
74
73
|
return xray_angiographic_image.default()
|
|
75
74
|
if sop_class == 'UltrasoundMultiFrameImage':
|
|
76
75
|
return ultrasound_multiframe_image.default()
|
|
76
|
+
if sop_class == 'ParametricMap':
|
|
77
|
+
return parametric_map.default()
|
|
77
78
|
else:
|
|
78
79
|
raise ValueError(
|
|
79
80
|
f"DICOM class {sop_class} is not currently supported"
|
|
@@ -238,7 +239,7 @@ def codify(source_file, save_file, **kwargs):
|
|
|
238
239
|
file.close()
|
|
239
240
|
|
|
240
241
|
|
|
241
|
-
def read_data(files, tags, path=None, images_only=False):
|
|
242
|
+
def read_data(files, tags, path=None, images_only=False): # obsolete??
|
|
242
243
|
|
|
243
244
|
if np.isscalar(files):
|
|
244
245
|
files = [files]
|
|
@@ -266,34 +267,6 @@ def read_data(files, tags, path=None, images_only=False):
|
|
|
266
267
|
|
|
267
268
|
|
|
268
269
|
|
|
269
|
-
def read_dataframe(files, tags, path=None, images_only=False):
|
|
270
|
-
if np.isscalar(files):
|
|
271
|
-
files = [files]
|
|
272
|
-
if np.isscalar(tags):
|
|
273
|
-
tags = [tags]
|
|
274
|
-
array = []
|
|
275
|
-
dicom_files = []
|
|
276
|
-
for i, file in tqdm(enumerate(files), desc='Reading DICOM folder'):
|
|
277
|
-
try:
|
|
278
|
-
ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
|
|
279
|
-
except:
|
|
280
|
-
pass
|
|
281
|
-
else:
|
|
282
|
-
if isinstance(ds, pydicom.dataset.FileDataset):
|
|
283
|
-
if 'TransferSyntaxUID' in ds.file_meta:
|
|
284
|
-
if images_only:
|
|
285
|
-
if not 'Rows' in ds:
|
|
286
|
-
continue
|
|
287
|
-
row = get_values(ds, tags)
|
|
288
|
-
array.append(row)
|
|
289
|
-
if path is None:
|
|
290
|
-
index = file
|
|
291
|
-
else:
|
|
292
|
-
index = os.path.relpath(file, path)
|
|
293
|
-
dicom_files.append(index)
|
|
294
|
-
df = pd.DataFrame(array, index = dicom_files, columns = tags)
|
|
295
|
-
return df
|
|
296
|
-
|
|
297
270
|
|
|
298
271
|
def _add_new(ds, tag, value, VR='OW'):
|
|
299
272
|
if not isinstance(tag, pydicom.tag.BaseTag):
|
|
@@ -586,7 +559,7 @@ def pixel_data(ds):
|
|
|
586
559
|
try:
|
|
587
560
|
array = ds.pixel_array
|
|
588
561
|
except:
|
|
589
|
-
|
|
562
|
+
raise ValueError("Dataset has no pixel data.")
|
|
590
563
|
array = array.astype(np.float32)
|
|
591
564
|
slope = float(getattr(ds, 'RescaleSlope', 1))
|
|
592
565
|
intercept = float(getattr(ds, 'RescaleIntercept', 0))
|
|
@@ -595,7 +568,7 @@ def pixel_data(ds):
|
|
|
595
568
|
return np.transpose(array)
|
|
596
569
|
|
|
597
570
|
|
|
598
|
-
def set_pixel_data(ds, array
|
|
571
|
+
def set_pixel_data(ds, array):
|
|
599
572
|
if array is None:
|
|
600
573
|
raise ValueError('The pixel array cannot be set to an empty value.')
|
|
601
574
|
|
|
@@ -611,7 +584,7 @@ def set_pixel_data(ds, array, value_range=None):
|
|
|
611
584
|
# if array.ndim >= 3: # remove spurious dimensions of 1
|
|
612
585
|
# array = np.squeeze(array)
|
|
613
586
|
|
|
614
|
-
array = image.clip(array.astype(np.float32)
|
|
587
|
+
array = image.clip(array.astype(np.float32))
|
|
615
588
|
array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
|
|
616
589
|
array = np.transpose(array)
|
|
617
590
|
|
|
@@ -635,6 +608,15 @@ def volume(ds, multislice=False):
|
|
|
635
608
|
def set_volume(ds, volume:vreg.Volume3D, multislice=False):
|
|
636
609
|
if volume is None:
|
|
637
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
|
+
|
|
638
620
|
image = np.squeeze(volume.values)
|
|
639
621
|
if image.ndim != 2:
|
|
640
622
|
raise ValueError("Can only write 2D images to a dataset.")
|
dbdicom/dbd.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from datetime import datetime
|
|
3
|
+
import json
|
|
3
4
|
|
|
4
5
|
from tqdm import tqdm
|
|
5
6
|
import numpy as np
|
|
@@ -8,9 +9,9 @@ import vreg
|
|
|
8
9
|
from pydicom.dataset import Dataset
|
|
9
10
|
|
|
10
11
|
import dbdicom.utils.arrays
|
|
11
|
-
|
|
12
|
-
import dbdicom.utils.dcm4che as dcm4che
|
|
12
|
+
|
|
13
13
|
import dbdicom.dataset as dbdataset
|
|
14
|
+
import dbdicom.database as dbdatabase
|
|
14
15
|
import dbdicom.register as register
|
|
15
16
|
import dbdicom.const as const
|
|
16
17
|
|
|
@@ -32,7 +33,8 @@ class DataBaseDicom():
|
|
|
32
33
|
file = self._register_file()
|
|
33
34
|
if os.path.exists(file):
|
|
34
35
|
try:
|
|
35
|
-
|
|
36
|
+
with open(file, 'r') as f:
|
|
37
|
+
self.register = json.load(f)
|
|
36
38
|
except:
|
|
37
39
|
# If the file is corrupted, delete it and load again
|
|
38
40
|
os.remove(file)
|
|
@@ -44,75 +46,45 @@ class DataBaseDicom():
|
|
|
44
46
|
def read(self):
|
|
45
47
|
"""Read the DICOM folder again
|
|
46
48
|
"""
|
|
47
|
-
|
|
48
|
-
files = filetools.all_files(self.path)
|
|
49
|
-
self.register = dbdataset.read_dataframe(
|
|
50
|
-
files,
|
|
51
|
-
register.COLUMNS + ['NumberOfFrames','SOPClassUID'],
|
|
52
|
-
path=self.path,
|
|
53
|
-
images_only = True)
|
|
54
|
-
self.register['removed'] = False
|
|
55
|
-
self.register['created'] = False
|
|
56
|
-
# No support for multiframe data at the moment
|
|
57
|
-
self._multiframe_to_singleframe()
|
|
49
|
+
self.register = dbdatabase.read(self.path)
|
|
58
50
|
# For now ensure all series have just a single CIOD
|
|
59
|
-
|
|
51
|
+
# Leaving this out for now until the issue occurs again
|
|
52
|
+
# self._split_series()
|
|
60
53
|
return self
|
|
61
|
-
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
"""Close the DICOM folder
|
|
65
|
-
|
|
66
|
-
This also saves changes in the header file to disk.
|
|
67
|
-
"""
|
|
55
|
+
|
|
68
56
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
created = created[created].index
|
|
72
|
-
removed = removed[removed].index
|
|
57
|
+
def delete(self, entity):
|
|
58
|
+
"""Delete a DICOM entity from the database
|
|
73
59
|
|
|
60
|
+
Args:
|
|
61
|
+
entity (list): entity to delete
|
|
62
|
+
"""
|
|
63
|
+
removed = register.index(self.register, entity)
|
|
74
64
|
# delete datasets marked for removal
|
|
75
65
|
for index in removed.tolist():
|
|
76
66
|
file = os.path.join(self.path, index)
|
|
77
67
|
if os.path.exists(file):
|
|
78
68
|
os.remove(file)
|
|
79
69
|
# and drop then from the register
|
|
80
|
-
self.register.drop(
|
|
81
|
-
|
|
82
|
-
# for new or edited data, mark as saved.
|
|
83
|
-
self.register.loc[created, 'created'] = False
|
|
84
|
-
|
|
85
|
-
# save register
|
|
86
|
-
file = self._register_file()
|
|
87
|
-
self.register.to_pickle(file)
|
|
70
|
+
self.register = register.drop(removed)
|
|
88
71
|
return self
|
|
89
72
|
|
|
90
73
|
|
|
91
|
-
def
|
|
92
|
-
"""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
removed = removed[removed].index
|
|
98
|
-
|
|
99
|
-
# permanently delete newly created datasets
|
|
100
|
-
for index in created.tolist():
|
|
101
|
-
file = os.path.join(self.path, index)
|
|
102
|
-
if os.path.exists(file):
|
|
103
|
-
os.remove(file)
|
|
104
|
-
|
|
105
|
-
# and drop then from the register
|
|
106
|
-
self.register.drop(index=created, inplace=True)
|
|
107
|
-
|
|
108
|
-
# Restore those that were marked for removal
|
|
109
|
-
self.register.loc[removed, 'removed'] = False
|
|
110
|
-
|
|
111
|
-
# save register
|
|
74
|
+
def close(self):
|
|
75
|
+
"""Close the DICOM folder
|
|
76
|
+
|
|
77
|
+
This also saves changes in the header file to disk.
|
|
78
|
+
"""
|
|
79
|
+
# Save df as pkl
|
|
112
80
|
file = self._register_file()
|
|
113
|
-
|
|
114
|
-
|
|
81
|
+
with open(file, 'w') as f:
|
|
82
|
+
json.dump(self.register, f, indent=4)
|
|
83
|
+
return self
|
|
115
84
|
|
|
85
|
+
def _register_file(self):
|
|
86
|
+
return os.path.join(self.path, 'dbtree.json')
|
|
87
|
+
|
|
116
88
|
|
|
117
89
|
def summary(self):
|
|
118
90
|
"""Return a summary of the contents of the database.
|
|
@@ -122,6 +94,7 @@ class DataBaseDicom():
|
|
|
122
94
|
"""
|
|
123
95
|
return register.summary(self.register)
|
|
124
96
|
|
|
97
|
+
|
|
125
98
|
def print(self):
|
|
126
99
|
"""Print the contents of the DICOM folder
|
|
127
100
|
"""
|
|
@@ -277,10 +250,11 @@ class DataBaseDicom():
|
|
|
277
250
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
278
251
|
multislice (bool, optional): Whether the data are to be read
|
|
279
252
|
as multislice or not. In multislice data the voxel size
|
|
280
|
-
is taken from the slice gap rather
|
|
253
|
+
is taken from the slice gap rather than the slice thickness. Defaults to False.
|
|
281
254
|
"""
|
|
282
255
|
if ref is None:
|
|
283
256
|
ds = dbdataset.new_dataset('MRImage')
|
|
257
|
+
#ds = dbdataset.new_dataset('ParametricMap')
|
|
284
258
|
else:
|
|
285
259
|
if ref[0] == series[0]:
|
|
286
260
|
ref_mgr = self
|
|
@@ -293,12 +267,11 @@ class DataBaseDicom():
|
|
|
293
267
|
attr = self._attributes(series)
|
|
294
268
|
n = self._max_instance_number(attr['SeriesInstanceUID'])
|
|
295
269
|
|
|
296
|
-
new_instances = {}
|
|
297
270
|
if vol.ndim==3:
|
|
298
271
|
slices = vol.split()
|
|
299
272
|
for i, sl in tqdm(enumerate(slices), desc='Writing volume..'):
|
|
300
273
|
dbdataset.set_volume(ds, sl, multislice)
|
|
301
|
-
self._write_dataset(ds, attr, n + 1 + i
|
|
274
|
+
self._write_dataset(ds, attr, n + 1 + i)
|
|
302
275
|
else:
|
|
303
276
|
i=0
|
|
304
277
|
vols = vol.separate().reshape(-1)
|
|
@@ -306,10 +279,9 @@ class DataBaseDicom():
|
|
|
306
279
|
for sl in vt.split():
|
|
307
280
|
dbdataset.set_volume(ds, sl, multislice)
|
|
308
281
|
dbdataset.set_value(ds, sl.dims, sl.coords[:,...])
|
|
309
|
-
self._write_dataset(ds, attr, n + 1 + i
|
|
282
|
+
self._write_dataset(ds, attr, n + 1 + i)
|
|
310
283
|
i+=1
|
|
311
|
-
|
|
312
|
-
self._update_register(new_instances)
|
|
284
|
+
return self
|
|
313
285
|
|
|
314
286
|
|
|
315
287
|
def to_nifti(self, series:list, file:str, dims=None, multislice=False):
|
|
@@ -488,16 +460,6 @@ class DataBaseDicom():
|
|
|
488
460
|
f"Cannot copy {from_entity} to {to_entity}. "
|
|
489
461
|
)
|
|
490
462
|
|
|
491
|
-
def delete(self, entity):
|
|
492
|
-
"""Delete a DICOM entity from the database
|
|
493
|
-
|
|
494
|
-
Args:
|
|
495
|
-
entity (list): entity to delete
|
|
496
|
-
"""
|
|
497
|
-
index = register.index(self.register, entity)
|
|
498
|
-
self.register.loc[index,'removed'] = True
|
|
499
|
-
return self
|
|
500
|
-
|
|
501
463
|
def move(self, from_entity, to_entity):
|
|
502
464
|
"""Move a DICOM entity
|
|
503
465
|
|
|
@@ -510,15 +472,15 @@ class DataBaseDicom():
|
|
|
510
472
|
|
|
511
473
|
def _values(self, attributes:list, entity:list):
|
|
512
474
|
# Create a np array v with values for each instance and attribute
|
|
513
|
-
if set(attributes) <= set(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
else:
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
475
|
+
# if set(attributes) <= set(dbdatabase.COLUMNS):
|
|
476
|
+
# index = register.index(self.register, entity)
|
|
477
|
+
# v = self.register.loc[index, attributes].values
|
|
478
|
+
# else:
|
|
479
|
+
files = register.files(self.register, entity)
|
|
480
|
+
v = np.empty((len(files), len(attributes)), dtype=object)
|
|
481
|
+
for i, f in enumerate(files):
|
|
482
|
+
ds = dbdataset.read_dataset(f)
|
|
483
|
+
v[i,:] = dbdataset.get_values(ds, attributes)
|
|
522
484
|
return v
|
|
523
485
|
|
|
524
486
|
def _copy_patient(self, from_patient, to_patient):
|
|
@@ -562,30 +524,28 @@ class DataBaseDicom():
|
|
|
562
524
|
n = self._max_instance_number(attr['SeriesInstanceUID'])
|
|
563
525
|
|
|
564
526
|
# Copy the files to the new series
|
|
565
|
-
new_instances = {}
|
|
566
527
|
for i, f in tqdm(enumerate(files), total=len(files), desc=f'Copying series {to_series[1:]}'):
|
|
567
528
|
# Read dataset and assign new properties
|
|
568
529
|
ds = dbdataset.read_dataset(f)
|
|
569
|
-
self._write_dataset(ds, attr, n + 1 + i
|
|
570
|
-
self._update_register(new_instances)
|
|
530
|
+
self._write_dataset(ds, attr, n + 1 + i)
|
|
571
531
|
|
|
572
532
|
|
|
573
533
|
def _max_series_number(self, study_uid):
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return
|
|
534
|
+
for pt in self.register:
|
|
535
|
+
for st in pt['studies']:
|
|
536
|
+
if st['StudyInstanceUID'] == study_uid:
|
|
537
|
+
n = [sr['SeriesNumber'] for sr in st['studies']]
|
|
538
|
+
return np.amax(n)
|
|
539
|
+
return 0
|
|
580
540
|
|
|
581
541
|
def _max_instance_number(self, series_uid):
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
542
|
+
for pt in self.register:
|
|
543
|
+
for st in pt['studies']:
|
|
544
|
+
for sr in st['series']:
|
|
545
|
+
if sr['SeriesInstanceUID'] == series_uid:
|
|
546
|
+
n = list(sr['instances'].keys())
|
|
547
|
+
return np.amax([int(i) for i in n])
|
|
548
|
+
return 0
|
|
589
549
|
|
|
590
550
|
def _attributes(self, entity):
|
|
591
551
|
if len(entity)==4:
|
|
@@ -653,84 +613,49 @@ class DataBaseDicom():
|
|
|
653
613
|
return study_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
|
|
654
614
|
|
|
655
615
|
|
|
656
|
-
def _write_dataset(self, ds:Dataset, attr:dict, instance_nr:int
|
|
616
|
+
def _write_dataset(self, ds:Dataset, attr:dict, instance_nr:int):
|
|
657
617
|
# Set new attributes
|
|
658
618
|
attr['SOPInstanceUID'] = dbdataset.new_uid()
|
|
659
|
-
attr['InstanceNumber'] = instance_nr
|
|
619
|
+
attr['InstanceNumber'] = str(instance_nr)
|
|
660
620
|
dbdataset.set_values(ds, list(attr.keys()), list(attr.values()))
|
|
661
621
|
# Save results in a new file
|
|
662
622
|
rel_path = os.path.join('dbdicom', dbdataset.new_uid() + '.dcm')
|
|
663
623
|
dbdataset.write(ds, os.path.join(self.path, rel_path))
|
|
664
|
-
# Add
|
|
665
|
-
register
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
624
|
+
# Add an entry in the register
|
|
625
|
+
register.add_instance(self.register, attr, rel_path)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# def _split_series(self):
|
|
630
|
+
# """
|
|
631
|
+
# Split series with multiple SOP Classes.
|
|
632
|
+
|
|
633
|
+
# If a series contain instances from different SOP Classes,
|
|
634
|
+
# these are separated out into multiple series with identical SOP Classes.
|
|
635
|
+
# """
|
|
636
|
+
# # For each series, check if there are multiple
|
|
637
|
+
# # SOP Classes in the series and split them if yes.
|
|
638
|
+
# for series in self.series():
|
|
639
|
+
# series_index = register.index(self.register, series)
|
|
640
|
+
# df_series = self.register.loc[series_index]
|
|
641
|
+
# sop_classes = df_series.SOPClassUID.unique()
|
|
642
|
+
# if len(sop_classes) > 1:
|
|
643
|
+
# # For each sop_class, create a new series and move all
|
|
644
|
+
# # instances of that sop_class to the new series
|
|
645
|
+
# desc = series[-1] if isinstance(series, str) else series[0]
|
|
646
|
+
# for i, sop_class in tqdm(enumerate(sop_classes[1:]), desc='Splitting series with multiple SOP Classes.'):
|
|
647
|
+
# df_sop_class = df_series[df_series.SOPClassUID == sop_class]
|
|
648
|
+
# relpaths = df_sop_class.index.tolist()
|
|
649
|
+
# sop_class_files = [os.path.join(self.path, p) for p in relpaths]
|
|
650
|
+
# sop_class_series = series[:-1] + [desc + f' [{i+1}]']
|
|
651
|
+
# self._files_to_series(sop_class_files, sop_class_series)
|
|
652
|
+
# # Delete original files permanently
|
|
653
|
+
# self.register.drop(relpaths)
|
|
654
|
+
# for f in sop_class_files:
|
|
655
|
+
# os.remove(f)
|
|
656
|
+
# self.register.drop('SOPClassUID', axis=1, inplace=True)
|
|
674
657
|
|
|
675
658
|
|
|
676
|
-
def _register_file(self):
|
|
677
|
-
filename = os.path.basename(os.path.normpath(self.path)) + ".pkl"
|
|
678
|
-
return os.path.join(self.path, filename)
|
|
679
|
-
|
|
680
659
|
|
|
681
|
-
def _multiframe_to_singleframe(self):
|
|
682
|
-
"""Converts all multiframe files in the folder into single-frame files.
|
|
683
|
-
|
|
684
|
-
Reads all the multi-frame files in the folder,
|
|
685
|
-
converts them to singleframe files, and delete the original multiframe file.
|
|
686
|
-
"""
|
|
687
|
-
singleframe = self.register.NumberOfFrames.isnull()
|
|
688
|
-
multiframe = singleframe == False
|
|
689
|
-
nr_multiframe = multiframe.sum()
|
|
690
|
-
if nr_multiframe != 0:
|
|
691
|
-
for relpath in tqdm(self.register[multiframe].index.values, desc="Converting multiframe file " + relpath):
|
|
692
|
-
filepath = os.path.join(self.path, relpath)
|
|
693
|
-
singleframe_files = dcm4che.split_multiframe(filepath)
|
|
694
|
-
if singleframe_files != []:
|
|
695
|
-
# add the single frame files to the dataframe
|
|
696
|
-
df = dbdataset.read_dataframe(singleframe_files, self.register.columns, path=self.path)
|
|
697
|
-
df['removed'] = False
|
|
698
|
-
df['created'] = False
|
|
699
|
-
self.register = pd.concat([self.register, df])
|
|
700
|
-
# delete the original multiframe
|
|
701
|
-
os.remove(filepath)
|
|
702
|
-
# drop the file also if the conversion has failed
|
|
703
|
-
self.register.drop(index=relpath, inplace=True)
|
|
704
|
-
self.register.drop('NumberOfFrames', axis=1, inplace=True)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
def _split_series(self):
|
|
708
|
-
"""
|
|
709
|
-
Split series with multiple SOP Classes.
|
|
710
|
-
|
|
711
|
-
If a series contain instances from different SOP Classes,
|
|
712
|
-
these are separated out into multiple series with identical SOP Classes.
|
|
713
|
-
"""
|
|
714
|
-
# For each series, check if there are multiple
|
|
715
|
-
# SOP Classes in the series and split them if yes.
|
|
716
|
-
for series in self.series():
|
|
717
|
-
series_index = register.index(self.register, series)
|
|
718
|
-
df_series = self.register.loc[series_index]
|
|
719
|
-
sop_classes = df_series.SOPClassUID.unique()
|
|
720
|
-
if len(sop_classes) > 1:
|
|
721
|
-
# For each sop_class, create a new series and move all
|
|
722
|
-
# instances of that sop_class to the new series
|
|
723
|
-
desc = series[-1] if isinstance(series, str) else series[0]
|
|
724
|
-
for i, sop_class in tqdm(enumerate(sop_classes[1:]), desc='Splitting series with multiple SOP Classes.'):
|
|
725
|
-
df_sop_class = df_series[df_series.SOPClassUID == sop_class]
|
|
726
|
-
relpaths = df_sop_class.index.tolist()
|
|
727
|
-
sop_class_files = [os.path.join(self.path, p) for p in relpaths]
|
|
728
|
-
sop_class_series = series[:-1] + [desc + f' [{i+1}]']
|
|
729
|
-
self._files_to_series(sop_class_files, sop_class_series)
|
|
730
|
-
# Delete original files permanently
|
|
731
|
-
self.register.drop(relpaths)
|
|
732
|
-
for f in sop_class_files:
|
|
733
|
-
os.remove(f)
|
|
734
|
-
self.register.drop('SOPClassUID', axis=1, inplace=True)
|
|
735
660
|
|
|
736
661
|
|