dbdicom 0.2.6__py3-none-any.whl → 0.3.1__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/__init__.py +1 -28
- dbdicom/api.py +287 -0
- dbdicom/const.py +144 -0
- dbdicom/dataset.py +721 -0
- dbdicom/dbd.py +736 -0
- 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 +527 -0
- dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
- dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +153 -26
- dbdicom/{ds/types → sop_classes}/mr_image.py +185 -140
- dbdicom/sop_classes/parametric_map.py +310 -0
- dbdicom/sop_classes/secondary_capture.py +140 -0
- dbdicom/sop_classes/segmentation.py +311 -0
- dbdicom/{ds/types → sop_classes}/ultrasound_multiframe_image.py +1 -15
- dbdicom/{ds/types → sop_classes}/xray_angiographic_image.py +2 -17
- dbdicom/utils/arrays.py +36 -0
- dbdicom/utils/files.py +0 -20
- dbdicom/utils/image.py +10 -629
- dbdicom-0.3.1.dist-info/METADATA +28 -0
- dbdicom-0.3.1.dist-info/RECORD +53 -0
- dbdicom/create.py +0 -457
- dbdicom/dro.py +0 -174
- dbdicom/ds/__init__.py +0 -10
- dbdicom/ds/create.py +0 -63
- dbdicom/ds/dataset.py +0 -869
- dbdicom/ds/dictionaries.py +0 -620
- dbdicom/ds/types/parametric_map.py +0 -226
- dbdicom/extensions/__init__.py +0 -9
- dbdicom/extensions/dipy.py +0 -448
- dbdicom/extensions/elastix.py +0 -503
- dbdicom/extensions/matplotlib.py +0 -107
- dbdicom/extensions/numpy.py +0 -271
- dbdicom/extensions/scipy.py +0 -1512
- dbdicom/extensions/skimage.py +0 -1030
- dbdicom/extensions/sklearn.py +0 -243
- dbdicom/extensions/vreg.py +0 -1390
- dbdicom/manager.py +0 -2132
- dbdicom/message.py +0 -119
- dbdicom/pipelines.py +0 -66
- dbdicom/record.py +0 -1893
- dbdicom/types/database.py +0 -107
- dbdicom/types/instance.py +0 -231
- dbdicom/types/patient.py +0 -40
- dbdicom/types/series.py +0 -2874
- dbdicom/types/study.py +0 -58
- dbdicom-0.2.6.dist-info/METADATA +0 -72
- dbdicom-0.2.6.dist-info/RECORD +0 -66
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/WHEEL +0 -0
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/top_level.txt +0 -0
dbdicom/record.py
DELETED
|
@@ -1,1893 +0,0 @@
|
|
|
1
|
-
# Importing annotations to handle or sign in import type hints
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
import datetime
|
|
6
|
-
|
|
7
|
-
# Import packages
|
|
8
|
-
import numpy as np
|
|
9
|
-
import pandas as pd
|
|
10
|
-
import dbdicom.ds.dataset as dbdataset
|
|
11
|
-
from dbdicom.ds import MRImage
|
|
12
|
-
from dbdicom.utils.files import export_path
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Record():
|
|
17
|
-
|
|
18
|
-
name = 'Record'
|
|
19
|
-
|
|
20
|
-
def __init__(self, create, manager, uid='Database', key=None, **kwargs):
|
|
21
|
-
|
|
22
|
-
self._logfile = None
|
|
23
|
-
self._key = key
|
|
24
|
-
self._mute = False
|
|
25
|
-
self.uid = uid
|
|
26
|
-
self.attributes = kwargs
|
|
27
|
-
self.manager = manager
|
|
28
|
-
self.new = create
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def __eq__(self, other):
|
|
32
|
-
if other is None:
|
|
33
|
-
return False
|
|
34
|
-
return self.uid == other.uid
|
|
35
|
-
|
|
36
|
-
def __getattr__(self, attribute):
|
|
37
|
-
return self.get_values(attribute)
|
|
38
|
-
|
|
39
|
-
def __getitem__(self, attributes):
|
|
40
|
-
return self.get_values(attributes)
|
|
41
|
-
|
|
42
|
-
def __setattr__(self, attribute, value):
|
|
43
|
-
if attribute in ['_key','_mute', 'uid', 'manager', 'attributes', 'new', '_logfile']:
|
|
44
|
-
self.__dict__[attribute] = value
|
|
45
|
-
else:
|
|
46
|
-
self._set_values([attribute], [value])
|
|
47
|
-
|
|
48
|
-
def __setitem__(self, attributes, values):
|
|
49
|
-
self._set_values(attributes, values)
|
|
50
|
-
|
|
51
|
-
def loc(self):
|
|
52
|
-
return self.manager._loc(self.name, self.uid)
|
|
53
|
-
# df = self.manager.register
|
|
54
|
-
# return (df.removed==False) & (df[self.name]==self.uid)
|
|
55
|
-
|
|
56
|
-
def keys(self):
|
|
57
|
-
loc = self.loc()
|
|
58
|
-
keys = self.manager._keys(loc)
|
|
59
|
-
# keys = self.manager.register.index[self.loc()]
|
|
60
|
-
if len(keys) == 0:
|
|
61
|
-
if self.name == 'Database':
|
|
62
|
-
return keys
|
|
63
|
-
else:
|
|
64
|
-
raise Exception("This record has no data")
|
|
65
|
-
else:
|
|
66
|
-
self._key = keys[0]
|
|
67
|
-
return keys
|
|
68
|
-
|
|
69
|
-
def _set_key(self):
|
|
70
|
-
loc = self.loc()
|
|
71
|
-
all_keys = self.manager._keys(loc)
|
|
72
|
-
if len(all_keys) == 0:
|
|
73
|
-
msg = 'This record has been removed from the database and can no longer be accessed.'
|
|
74
|
-
raise ValueError(msg)
|
|
75
|
-
self._key = all_keys[0]
|
|
76
|
-
|
|
77
|
-
def key(self):
|
|
78
|
-
try:
|
|
79
|
-
key_removed = self.manager._at(self._key, 'removed')
|
|
80
|
-
except:
|
|
81
|
-
self._set_key()
|
|
82
|
-
else:
|
|
83
|
-
if key_removed:
|
|
84
|
-
self._set_key()
|
|
85
|
-
return self._key
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def status(self):
|
|
89
|
-
return self.manager.status
|
|
90
|
-
|
|
91
|
-
@property
|
|
92
|
-
def dialog(self):
|
|
93
|
-
return self.manager.dialog
|
|
94
|
-
|
|
95
|
-
def set_log(self, filepath:str=None):
|
|
96
|
-
"""Set a new file for logging.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
filepath: full path to a log file. If not provided the current log file is removed. Alternatively the value 'Default' can be assigned, in which case a standard file at the same location of the database is automatically opened. Defaults to None.
|
|
100
|
-
|
|
101
|
-
Raises:
|
|
102
|
-
FileNotFoundError: if the log file cannot be written to.
|
|
103
|
-
|
|
104
|
-
See also:
|
|
105
|
-
`log`
|
|
106
|
-
|
|
107
|
-
Examples:
|
|
108
|
-
|
|
109
|
-
Set a new log file:
|
|
110
|
-
|
|
111
|
-
>>> record.set_log('path/to/logfile')
|
|
112
|
-
|
|
113
|
-
and start logging:
|
|
114
|
-
|
|
115
|
-
>>> record.log('Starting new calculation...)
|
|
116
|
-
|
|
117
|
-
Alternatively, start a new log at the default location:
|
|
118
|
-
|
|
119
|
-
>>> record.set_log('Default')
|
|
120
|
-
"""
|
|
121
|
-
if filepath is None:
|
|
122
|
-
self._logfile = None
|
|
123
|
-
return
|
|
124
|
-
if filepath == 'Default':
|
|
125
|
-
# Use default log name
|
|
126
|
-
self._logfile = os.path.join(self.manager.path, "activity_log.txt")
|
|
127
|
-
else:
|
|
128
|
-
self._logfile = filepath
|
|
129
|
-
try:
|
|
130
|
-
file = open(self._logfile, 'a')
|
|
131
|
-
file.write(str(datetime.datetime.now())[0:19] + "Starting a new log..")
|
|
132
|
-
file.close()
|
|
133
|
-
except:
|
|
134
|
-
msg = 'Cannot write to log ' + self._logfile
|
|
135
|
-
raise FileNotFoundError(msg)
|
|
136
|
-
|
|
137
|
-
def log(self, message:str):
|
|
138
|
-
"""Write an entry in the log file.
|
|
139
|
-
|
|
140
|
-
If no logfile is set, this function only writes a message in the terminal.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
message (str): text message to be written in the log file. The function automatically includes some timing information so this does not need to be included in the message.
|
|
144
|
-
|
|
145
|
-
Raises:
|
|
146
|
-
FileNotFoundError: if the log file cannot be written to.
|
|
147
|
-
|
|
148
|
-
See also:
|
|
149
|
-
`set_log`
|
|
150
|
-
|
|
151
|
-
Examples:
|
|
152
|
-
Set a default file for logging and write a first message:
|
|
153
|
-
|
|
154
|
-
>>> record.set_log('Default')
|
|
155
|
-
>>> record.log('Starting new calculation...)
|
|
156
|
-
"""
|
|
157
|
-
|
|
158
|
-
self.message(message)
|
|
159
|
-
if self._logfile is None:
|
|
160
|
-
return
|
|
161
|
-
try:
|
|
162
|
-
file = open(self._logfile, 'a')
|
|
163
|
-
file.write("\n"+str(datetime.datetime.now())[0:19] + ": " + message)
|
|
164
|
-
file.close()
|
|
165
|
-
except:
|
|
166
|
-
msg = 'Cannot write to log ' + self._logfile
|
|
167
|
-
raise FileNotFoundError(msg)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
# Properties
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def print(self):
|
|
174
|
-
"""Print a summary of the record and its contents.
|
|
175
|
-
|
|
176
|
-
See Also:
|
|
177
|
-
:func:`~path`
|
|
178
|
-
|
|
179
|
-
Example:
|
|
180
|
-
Print a summary of a database:
|
|
181
|
-
|
|
182
|
-
>>> database = db.dro.database_hollywood()
|
|
183
|
-
>>> database.print()
|
|
184
|
-
---------- DATABASE --------------
|
|
185
|
-
Location: In memory
|
|
186
|
-
Patient James Bond
|
|
187
|
-
Study MRI [19821201]
|
|
188
|
-
Series 001 [Localizer]
|
|
189
|
-
Nr of instances: 0
|
|
190
|
-
Series 002 [T2w]
|
|
191
|
-
Nr of instances: 0
|
|
192
|
-
Study Xray [19821205]
|
|
193
|
-
Series 001 [Chest]
|
|
194
|
-
Nr of instances: 0
|
|
195
|
-
Series 002 [Head]
|
|
196
|
-
Nr of instances: 0
|
|
197
|
-
Patient Scarface
|
|
198
|
-
Study MRI [19850105]
|
|
199
|
-
Series 001 [Localizer]
|
|
200
|
-
Nr of instances: 0
|
|
201
|
-
Series 002 [T2w]
|
|
202
|
-
Nr of instances: 0
|
|
203
|
-
Study Xray [19850106]
|
|
204
|
-
Series 001 [Chest]
|
|
205
|
-
Nr of instances: 0
|
|
206
|
-
Series 002 [Head]
|
|
207
|
-
Nr of instances: 0
|
|
208
|
-
----------------------------------
|
|
209
|
-
|
|
210
|
-
Or print a summary of any record in the hierarchy:
|
|
211
|
-
|
|
212
|
-
>>> patients = database.patients(PatientName='Scarface')
|
|
213
|
-
>>> patients[0].print()
|
|
214
|
-
---------- PATIENT -------------
|
|
215
|
-
Patient Scarface
|
|
216
|
-
Study MRI [19850105]
|
|
217
|
-
Series 001 [Localizer]
|
|
218
|
-
Nr of instances: 0
|
|
219
|
-
Series 002 [T2w]
|
|
220
|
-
Nr of instances: 0
|
|
221
|
-
Study Xray [19850106]
|
|
222
|
-
Series 001 [Chest]
|
|
223
|
-
Nr of instances: 0
|
|
224
|
-
Series 002 [Head]
|
|
225
|
-
Nr of instances: 0
|
|
226
|
-
--------------------------------
|
|
227
|
-
"""
|
|
228
|
-
self.manager.print(self.uid, self.name)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def path(self) -> str:
|
|
232
|
-
"""Directory of the DICOM database
|
|
233
|
-
|
|
234
|
-
Returns:
|
|
235
|
-
str: full path to the directory
|
|
236
|
-
|
|
237
|
-
See Also:
|
|
238
|
-
:func:`~print`
|
|
239
|
-
|
|
240
|
-
Example:
|
|
241
|
-
Create a new database in memory:
|
|
242
|
-
|
|
243
|
-
>>> database = db.database()
|
|
244
|
-
>>> print(database.path())
|
|
245
|
-
None
|
|
246
|
-
|
|
247
|
-
Open an existing DICOM database:
|
|
248
|
-
|
|
249
|
-
>>> database = db.database('path\\to\\DICOM\\database')
|
|
250
|
-
>>> print(database.path())
|
|
251
|
-
path\to\DICOM\database
|
|
252
|
-
"""
|
|
253
|
-
return self.manager.path
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def empty(self)->bool:
|
|
257
|
-
"""Check if the record has data.
|
|
258
|
-
|
|
259
|
-
Returns:
|
|
260
|
-
bool: False if the record has data, True if not
|
|
261
|
-
|
|
262
|
-
See Also:
|
|
263
|
-
:func:`~print`
|
|
264
|
-
:func:`~path`
|
|
265
|
-
|
|
266
|
-
Example:
|
|
267
|
-
|
|
268
|
-
Check if a database on disk is empty:
|
|
269
|
-
|
|
270
|
-
>>> database = db.database('path\\to\\database')
|
|
271
|
-
>>> print(database.empty)
|
|
272
|
-
False
|
|
273
|
-
|
|
274
|
-
Create a new database from scratch and verify that it is empty:
|
|
275
|
-
|
|
276
|
-
>>> database = db.database()
|
|
277
|
-
>>> print(database.empty())
|
|
278
|
-
True
|
|
279
|
-
|
|
280
|
-
Creating a new series in the database, and verify that it is no longer empty:
|
|
281
|
-
|
|
282
|
-
>>> series = database.new_series()
|
|
283
|
-
>>> print(database.empty())
|
|
284
|
-
False
|
|
285
|
-
|
|
286
|
-
Verify that the new series is empty:
|
|
287
|
-
|
|
288
|
-
>>> print(series.empty())
|
|
289
|
-
True
|
|
290
|
-
|
|
291
|
-
Populate the series with a numpy array and verify that it is now no longer empty:
|
|
292
|
-
|
|
293
|
-
>>> zeros = np.zeros((3, 2, 128, 128))
|
|
294
|
-
>>> series.set_pixel_values(zeros)
|
|
295
|
-
>>> print(series.empty())
|
|
296
|
-
False
|
|
297
|
-
"""
|
|
298
|
-
if self.manager.register.empty:
|
|
299
|
-
return True
|
|
300
|
-
return self.children() == []
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
def files(self) -> list:
|
|
304
|
-
"""Return a list of all DICOM files saved in the database
|
|
305
|
-
|
|
306
|
-
Returns:
|
|
307
|
-
list: A list of absolute filepaths to valid DICOM files
|
|
308
|
-
|
|
309
|
-
See Also:
|
|
310
|
-
:func:`~print`
|
|
311
|
-
:func:`~path`
|
|
312
|
-
:func:`~empty`
|
|
313
|
-
|
|
314
|
-
Example:
|
|
315
|
-
|
|
316
|
-
A new database in memory has no files on disk:
|
|
317
|
-
|
|
318
|
-
>>> database = db.database()
|
|
319
|
-
>>> print(database.files())
|
|
320
|
-
[]
|
|
321
|
-
|
|
322
|
-
If a series is created in memory, there are no files on disk:
|
|
323
|
-
|
|
324
|
-
>>> series = db.zeros((3,128,128))
|
|
325
|
-
>>> print(series.files())
|
|
326
|
-
[]
|
|
327
|
-
|
|
328
|
-
If a series is created in memory, then written to disk, there are files associated. Since the default format is single-frame MRImage, there are 3 files in this case:
|
|
329
|
-
|
|
330
|
-
>>> series.write('path\\to\\DICOM\\database')
|
|
331
|
-
>>> print(series.files())
|
|
332
|
-
['path\\to\\DICOM\\database\\dbdicom\\1.2.826.0.1.3680043.8.498.10200622747714198480020099226433338888.dcm', 'path\\to\\DICOM\\database\\dbdicom\\1.2.826.0.1.3680043.8.498.95074529334441498207488699470663781148.dcm', 'path\\to\\DICOM\\database\\dbdicom\\1.2.826.0.1.3680043.8.498.30452523525370800574103459899629273584.dcm']
|
|
333
|
-
"""
|
|
334
|
-
files = [self.manager.filepath(key) for key in self.keys()]
|
|
335
|
-
files = [f for f in files if f is not None] # Added 29/05/23 - check if this creates issues
|
|
336
|
-
return files
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def label(self)->str:
|
|
340
|
-
"""Return a human-readable label describing the record.
|
|
341
|
-
|
|
342
|
-
Returns:
|
|
343
|
-
str: label with descriptive information.
|
|
344
|
-
|
|
345
|
-
See Also:
|
|
346
|
-
:func:`~print`
|
|
347
|
-
|
|
348
|
-
Example:
|
|
349
|
-
Print the label of a default series:
|
|
350
|
-
|
|
351
|
-
>>> series = db.zeros((3,128,128), SeriesDescription='Empty demo')
|
|
352
|
-
>>> print(series.label())
|
|
353
|
-
Series 001 [Empty demo]
|
|
354
|
-
"""
|
|
355
|
-
return self.manager.label(self.uid, key=self.key(), type=self.__class__.__name__)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
# Navigating the tree
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def parent(self):
|
|
363
|
-
"""Return the parent of the record.
|
|
364
|
-
|
|
365
|
-
Returns:
|
|
366
|
-
Record: The parent object.
|
|
367
|
-
|
|
368
|
-
See Also:
|
|
369
|
-
:func:`~children`
|
|
370
|
-
:func:`~siblings`
|
|
371
|
-
:func:`~series`
|
|
372
|
-
:func:`~studies`
|
|
373
|
-
:func:`~patients`
|
|
374
|
-
:func:`~database`
|
|
375
|
-
|
|
376
|
-
Example:
|
|
377
|
-
Find the parent of a study:
|
|
378
|
-
|
|
379
|
-
>>> study = db.study()
|
|
380
|
-
>>> patient = study.parent()
|
|
381
|
-
>>> print(patient.PatientName)
|
|
382
|
-
New Patient
|
|
383
|
-
"""
|
|
384
|
-
# Note this function is reimplemented in all subclasses.
|
|
385
|
-
# It is included in the Record class only for documentation purposes.
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def children(self, **kwargs)->list:
|
|
390
|
-
"""Return all children of the record.
|
|
391
|
-
|
|
392
|
-
Args:
|
|
393
|
-
kwargs: Provide any number of valid DICOM (tag, value) pair as keywords to filter the list.
|
|
394
|
-
|
|
395
|
-
Returns:
|
|
396
|
-
list: A list of all children.
|
|
397
|
-
|
|
398
|
-
See Also:
|
|
399
|
-
:func:`~parent`
|
|
400
|
-
:func:`~siblings`
|
|
401
|
-
:func:`~series`
|
|
402
|
-
:func:`~studies`
|
|
403
|
-
:func:`~patients`
|
|
404
|
-
:func:`~database`
|
|
405
|
-
|
|
406
|
-
Example:
|
|
407
|
-
Find the patients of a given database:
|
|
408
|
-
|
|
409
|
-
>>> database = db.dro.database_hollywood()
|
|
410
|
-
>>> patients = database.children()
|
|
411
|
-
>>> print([p.PatientName for p in patients])
|
|
412
|
-
['James Bond', 'Scarface']
|
|
413
|
-
|
|
414
|
-
Find all patients with a given name:
|
|
415
|
-
|
|
416
|
-
>>> patients = database.children(PatientName='James Bond')
|
|
417
|
-
>>> print([p.PatientName for p in patients])
|
|
418
|
-
['James Bond']
|
|
419
|
-
|
|
420
|
-
Find the studies that have been performed on a given patient:
|
|
421
|
-
>>> studies = patients[0].children()
|
|
422
|
-
>>> print([s.StudyDescription for s in studies])
|
|
423
|
-
['MRI', 'Xray']
|
|
424
|
-
"""
|
|
425
|
-
# Note this function is reimplemented in all subclasses.
|
|
426
|
-
# It is included in the Record class for documentation purposes.
|
|
427
|
-
return []
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
def siblings(self, **kwargs)->list:
|
|
431
|
-
"""Return all siblings of the record.
|
|
432
|
-
|
|
433
|
-
Args:
|
|
434
|
-
kwargs: Provide any number of valid DICOM (tag, value) pair as keywords to filter the list.
|
|
435
|
-
|
|
436
|
-
Returns:
|
|
437
|
-
list: A list of all siblings.
|
|
438
|
-
|
|
439
|
-
See Also:
|
|
440
|
-
:func:`~parent`
|
|
441
|
-
:func:`~children`
|
|
442
|
-
:func:`~series`
|
|
443
|
-
:func:`~studies`
|
|
444
|
-
:func:`~patients`
|
|
445
|
-
:func:`~database`
|
|
446
|
-
|
|
447
|
-
Example:
|
|
448
|
-
Retrieve a study from a database, and find all other studies performed on the same patient:
|
|
449
|
-
|
|
450
|
-
>>> database = db.dro.database_hollywood()
|
|
451
|
-
>>> study = database.studies()[0]
|
|
452
|
-
>>> print([s.StudyDescription for s in study.siblings()])
|
|
453
|
-
['Xray']
|
|
454
|
-
"""
|
|
455
|
-
siblings = self.parent().children(**kwargs)
|
|
456
|
-
siblings.remove(self)
|
|
457
|
-
return siblings
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
def series(self, sort=True, sortby=['PatientName', 'StudyDescription', 'SeriesNumber'], **kwargs)->list:
|
|
461
|
-
"""Return a list of series under the record.
|
|
462
|
-
|
|
463
|
-
If the record is a study, this returns the record's children. If it is a patient, this returns a list the record's grand children.
|
|
464
|
-
|
|
465
|
-
Args:
|
|
466
|
-
sort (bool, optional): Set to False to return an unsorted list (faster). Defaults to True.
|
|
467
|
-
sortby (list, optional): list of DICOM keywords to sort the result. This argument is ignored if sort=False. Defaults to ['PatientName', 'StudyDescription', 'SeriesNumber'].
|
|
468
|
-
kwargs (keyword arguments, optional): Set any number of valid DICOM (tag, value) pairs as keywords to filer the list. The result will only contain series with the appropriate values
|
|
469
|
-
|
|
470
|
-
Returns:
|
|
471
|
-
list: A list of dbdicom Series objects.
|
|
472
|
-
|
|
473
|
-
See Also:
|
|
474
|
-
:func:`~parent`
|
|
475
|
-
:func:`~children`
|
|
476
|
-
:func:`~siblings`
|
|
477
|
-
:func:`~studies`
|
|
478
|
-
:func:`~patients`
|
|
479
|
-
:func:`~database`
|
|
480
|
-
|
|
481
|
-
Example:
|
|
482
|
-
Find all series in a database, and print their labels:
|
|
483
|
-
|
|
484
|
-
>>> database = db.dro.database_hollywood()
|
|
485
|
-
>>> series_list = database.series()
|
|
486
|
-
>>> print([s.label() for s in series_list])
|
|
487
|
-
['Series 001 [Localizer]', 'Series 002 [T2w]', 'Series 001 [Chest]', 'Series 002 [Head]', 'Series 001 [Localizer]', 'Series 002 [T2w]', 'Series 001 [Chest]', 'Series 002 [Head]']
|
|
488
|
-
|
|
489
|
-
Find all series with a given SeriesDescription:
|
|
490
|
-
|
|
491
|
-
>>> series_list = database.series(SeriesDescription='Chest')
|
|
492
|
-
>>> print([s.label() for s in series_list])
|
|
493
|
-
['Series 001 [Chest]', 'Series 001 [Chest]']
|
|
494
|
-
|
|
495
|
-
Find all series with a given SeriesDescription of a given Patient:
|
|
496
|
-
|
|
497
|
-
>>> series_list = database.series(SeriesDescription='Chest', PatientName='James Bond')
|
|
498
|
-
>>> print([s.label() for s in series_list])
|
|
499
|
-
['Series 001 [Chest]']
|
|
500
|
-
"""
|
|
501
|
-
series = self.manager.series(keys=self.keys(), sort=sort, sortby=sortby, **kwargs)
|
|
502
|
-
return [self.record('Series', uid) for uid in series]
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
def studies(self, sort=True, sortby=['PatientName', 'StudyDescription'], **kwargs)->list:
|
|
506
|
-
"""Return a list of studies under the record.
|
|
507
|
-
|
|
508
|
-
If the record is a patient, this returns the record's children. If it is a series, this returns the parent study.
|
|
509
|
-
|
|
510
|
-
Args:
|
|
511
|
-
sort (bool, optional): Set to False to return an unsorted list (faster). Defaults to True.
|
|
512
|
-
sortby (list, optional): list of DICOM keywords to sort the result. This argument is ignored if sort=False. Defaults to ['PatientName', 'StudyDescription'].
|
|
513
|
-
kwargs (keyword arguments, optional): Set any number of valid DICOM (tag, value) pairs as keywords to filer the list. The result will only contain studies with the appropriate values.
|
|
514
|
-
|
|
515
|
-
Returns:
|
|
516
|
-
list: A list of dbdicom Study objects.
|
|
517
|
-
|
|
518
|
-
See Also:
|
|
519
|
-
:func:`~parent`
|
|
520
|
-
:func:`~children`
|
|
521
|
-
:func:`~siblings`
|
|
522
|
-
:func:`~series`
|
|
523
|
-
:func:`~patients`
|
|
524
|
-
:func:`~database`
|
|
525
|
-
|
|
526
|
-
Example:
|
|
527
|
-
Find all studies in a database:
|
|
528
|
-
|
|
529
|
-
>>> database = db.dro.database_hollywood()
|
|
530
|
-
>>> studies_list = database.studies()
|
|
531
|
-
>>> print([s.label() for s in studies_list])
|
|
532
|
-
['Study MRI [19821201]', 'Study Xray [19821205]', 'Study MRI [19850105]', 'Study Xray [19850106]']
|
|
533
|
-
|
|
534
|
-
Find all studies of a given Patient:
|
|
535
|
-
|
|
536
|
-
>>> studies_list = database.studies(PatientName='James Bond')
|
|
537
|
-
>>> print([s.label() for s in studies_list])
|
|
538
|
-
['Study MRI [19821201]', 'Study Xray [19821205]']
|
|
539
|
-
"""
|
|
540
|
-
studies = self.manager.studies(keys=self.keys(), sort=sort, sortby=sortby, **kwargs)
|
|
541
|
-
return [self.record('Study', uid) for uid in studies]
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
def patients(self, sort=True, sortby=['PatientName'], **kwargs)->list:
|
|
545
|
-
"""Return a list of patients under the record.
|
|
546
|
-
|
|
547
|
-
If the record is a database, this returns the children. If it is a series or a study, this returns the parent patient.
|
|
548
|
-
|
|
549
|
-
Args:
|
|
550
|
-
sort (bool, optional): Set to False to return an unsorted list (faster). Defaults to True.
|
|
551
|
-
sortby (list, optional): list of DICOM keywords to sort the result. This argument is ignored if sort=False. Defaults to ['PatientName'].
|
|
552
|
-
kwargs (keyword arguments, optional): Set any number of valid DICOM (tag, value) pairs as keywords to filer the list. The result will only contain patients with the appropriate values.
|
|
553
|
-
|
|
554
|
-
Returns:
|
|
555
|
-
list: A list of dbdicom Patient objects.
|
|
556
|
-
|
|
557
|
-
See Also:
|
|
558
|
-
:func:`~parent`
|
|
559
|
-
:func:`~children`
|
|
560
|
-
:func:`~siblings`
|
|
561
|
-
:func:`~series`
|
|
562
|
-
:func:`~studies`
|
|
563
|
-
:func:`~database`
|
|
564
|
-
|
|
565
|
-
Example:
|
|
566
|
-
Find all patients in a database:
|
|
567
|
-
|
|
568
|
-
>>> database = db.dro.database_hollywood()
|
|
569
|
-
>>> patients_list = database.patients()
|
|
570
|
-
>>> print([s.label() for s in patients_list])
|
|
571
|
-
['Patient James Bond', 'Patient Scarface']
|
|
572
|
-
|
|
573
|
-
Find all patients with a given name:
|
|
574
|
-
|
|
575
|
-
>>> patients_list = database.patients(PatientName='James Bond')
|
|
576
|
-
>>> print([s.label() for s in patients_list])
|
|
577
|
-
['Patient James Bond']
|
|
578
|
-
"""
|
|
579
|
-
patients = self.manager.patients(keys=self.keys(), sort=sort, sortby=sortby, **kwargs)
|
|
580
|
-
return [self.record('Patient', uid) for uid in patients]
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
def database(self):
|
|
584
|
-
"""Return the database of the record.
|
|
585
|
-
|
|
586
|
-
Returns:
|
|
587
|
-
Database: Database of the record
|
|
588
|
-
|
|
589
|
-
See Also:
|
|
590
|
-
:func:`~parent`
|
|
591
|
-
:func:`~children`
|
|
592
|
-
:func:`~siblings`
|
|
593
|
-
:func:`~series`
|
|
594
|
-
:func:`~studies`
|
|
595
|
-
|
|
596
|
-
Example:
|
|
597
|
-
Get the database of a study:
|
|
598
|
-
|
|
599
|
-
>>> study = db.study()
|
|
600
|
-
>>> database = study.database()
|
|
601
|
-
>>> print(database.label())
|
|
602
|
-
Database [in memory]
|
|
603
|
-
"""
|
|
604
|
-
return self.record('Database')
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
# Edit a record
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
def new_patient(self, **kwargs):
|
|
611
|
-
"""Create a new patient.
|
|
612
|
-
|
|
613
|
-
Args:
|
|
614
|
-
kwargs (optional): Any valid DICOM (tag, value) pair can be assigned up front as properties of the new patient.
|
|
615
|
-
|
|
616
|
-
Returns:
|
|
617
|
-
Patient: instance of the new patient
|
|
618
|
-
|
|
619
|
-
See Also:
|
|
620
|
-
:func:`~new_study`
|
|
621
|
-
:func:`~new_series`
|
|
622
|
-
:func:`~new_pibling`
|
|
623
|
-
|
|
624
|
-
Example:
|
|
625
|
-
Create a new patient in a database:
|
|
626
|
-
|
|
627
|
-
>>> database = db.database()
|
|
628
|
-
>>> database.print()
|
|
629
|
-
---------- DATABASE --------------
|
|
630
|
-
Location: In memory
|
|
631
|
-
----------------------------------
|
|
632
|
-
|
|
633
|
-
>>> nemo = database.new_patient(PatientName='Nemo')
|
|
634
|
-
>>> dory = database.new_patient(PatientName='Dory')
|
|
635
|
-
>>> database.print()
|
|
636
|
-
---------- DATABASE --------------
|
|
637
|
-
Location: In memory
|
|
638
|
-
Patient Dory
|
|
639
|
-
Patient Nemo
|
|
640
|
-
----------------------------------
|
|
641
|
-
|
|
642
|
-
A lower-level record can also create a new patient. Create a new series and show its default database:
|
|
643
|
-
|
|
644
|
-
>>> series = db.series()
|
|
645
|
-
>>> series.database().print()
|
|
646
|
-
---------- DATABASE --------------
|
|
647
|
-
Location: In memory
|
|
648
|
-
Patient New Patient
|
|
649
|
-
Study New Study [None]
|
|
650
|
-
Series 001 [New Series]
|
|
651
|
-
Nr of instances: 0
|
|
652
|
-
----------------------------------
|
|
653
|
-
|
|
654
|
-
The series can create new patients in its database directly:
|
|
655
|
-
|
|
656
|
-
>>> dory = series.new_patient(PatientName='Dory')
|
|
657
|
-
>>> nemo = series.new_patient(PatientName='Nemo')
|
|
658
|
-
>>> series.print()
|
|
659
|
-
---------- DATABASE --------------
|
|
660
|
-
Location: In memory
|
|
661
|
-
Patient Dory
|
|
662
|
-
Patient Nemo
|
|
663
|
-
Patient New Patient
|
|
664
|
-
Study New Study [None]
|
|
665
|
-
Series 001 [New Series]
|
|
666
|
-
Nr of instances: 0
|
|
667
|
-
----------------------------------
|
|
668
|
-
"""
|
|
669
|
-
attr = {**kwargs, **self.attributes}
|
|
670
|
-
uid, key = self.manager.new_patient(parent=self.uid, **attr)
|
|
671
|
-
return self.record('Patient', uid, key, **attr)
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
def new_study(self, **kwargs):
|
|
675
|
-
"""Create a new study.
|
|
676
|
-
|
|
677
|
-
Args:
|
|
678
|
-
kwargs (optional): Any valid DICOM (tag, value) pair can be assigned up front as properties of the new study.
|
|
679
|
-
|
|
680
|
-
Returns:
|
|
681
|
-
Study: instance of the new study
|
|
682
|
-
|
|
683
|
-
See Also:
|
|
684
|
-
:func:`~new_patient`
|
|
685
|
-
:func:`~new_series`
|
|
686
|
-
:func:`~new_pibling`
|
|
687
|
-
|
|
688
|
-
Example:
|
|
689
|
-
Create a new study in a patient:
|
|
690
|
-
|
|
691
|
-
>>> dory = db.patient(PatientName='Dory')
|
|
692
|
-
>>> dory.print()
|
|
693
|
-
---------- PATIENT -------------
|
|
694
|
-
Patient Dory
|
|
695
|
-
--------------------------------
|
|
696
|
-
|
|
697
|
-
>>> fMRI = dory.new_study(StudyDescription='fMRI', StudyDate='20091001')
|
|
698
|
-
>>> CThead = dory.new_study(StudyDescription='CT head', StudyDate='20091002')
|
|
699
|
-
>>> dory.print()
|
|
700
|
-
---------- PATIENT -------------
|
|
701
|
-
Patient Dory
|
|
702
|
-
Study CT head [20091002]
|
|
703
|
-
Study fMRI [20091001]
|
|
704
|
-
--------------------------------
|
|
705
|
-
|
|
706
|
-
Any other record can also create a new study. Missing intermediate generations are created automatically:
|
|
707
|
-
|
|
708
|
-
>>> database = db.database()
|
|
709
|
-
>>> database.print()
|
|
710
|
-
---------- DATABASE --------------
|
|
711
|
-
Location: In memory
|
|
712
|
-
----------------------------------
|
|
713
|
-
|
|
714
|
-
>>> fMRI = database.new_study(StudyDescription='fMRI')
|
|
715
|
-
>>> database.print()
|
|
716
|
-
---------- DATABASE --------------
|
|
717
|
-
Location: In memory
|
|
718
|
-
Patient New Patient
|
|
719
|
-
Study fMRI [None]
|
|
720
|
-
----------------------------------
|
|
721
|
-
"""
|
|
722
|
-
attr = {**kwargs, **self.attributes}
|
|
723
|
-
uid, key = self.manager.new_study(parent=self.uid, key=self.key(),**attr)
|
|
724
|
-
return self.record('Study', uid, key, **attr)
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
def new_series(self, **kwargs):
|
|
728
|
-
"""Create a new series.
|
|
729
|
-
|
|
730
|
-
Args:
|
|
731
|
-
kwargs (optional): Any valid DICOM (tag, value) pair can be assigned up front as properties of the new series.
|
|
732
|
-
|
|
733
|
-
Returns:
|
|
734
|
-
Series: instance of the new series
|
|
735
|
-
|
|
736
|
-
See Also:
|
|
737
|
-
:func:`~new_patient`
|
|
738
|
-
:func:`~new_study`
|
|
739
|
-
:func:`~new_pibling`
|
|
740
|
-
|
|
741
|
-
Example:
|
|
742
|
-
Consider an empty study:
|
|
743
|
-
|
|
744
|
-
>>> fMRI = db.study(StudyDescription='fMRI', StudyDate='20230203')
|
|
745
|
-
>>> fMRI.print()
|
|
746
|
-
---------- STUDY ---------------
|
|
747
|
-
Study fMRI [20230203]
|
|
748
|
-
--------------------------------
|
|
749
|
-
|
|
750
|
-
Create two new series in the study:
|
|
751
|
-
|
|
752
|
-
>>> rstate = fMRI.new_series(SeriesDescription='Resting state')
|
|
753
|
-
>>> ftap = fMRI.new_series(SeriesDescription='Finger tap')
|
|
754
|
-
>>> fMRI.print()
|
|
755
|
-
---------- STUDY ---------------
|
|
756
|
-
Study fMRI [20230203]
|
|
757
|
-
Series 001 [Resting state]
|
|
758
|
-
Nr of instances: 0
|
|
759
|
-
Series 002 [Finger tap]
|
|
760
|
-
Nr of instances: 0
|
|
761
|
-
--------------------------------
|
|
762
|
-
|
|
763
|
-
Any other record can also create a new series. Missing intermediate generations are created automatically:
|
|
764
|
-
|
|
765
|
-
>>> database = db.database()
|
|
766
|
-
>>> database.print()
|
|
767
|
-
---------- DATABASE --------------
|
|
768
|
-
Location: In memory
|
|
769
|
-
----------------------------------
|
|
770
|
-
|
|
771
|
-
>>> rstate = database.new_series(SeriesDescription='Resting state')
|
|
772
|
-
>>> ftap = database.new_series(SeriesDescription='Finger tap')
|
|
773
|
-
>>> database.print()
|
|
774
|
-
---------- DATABASE --------------
|
|
775
|
-
Location: In memory
|
|
776
|
-
Patient New Patient
|
|
777
|
-
Study New Study [None]
|
|
778
|
-
Series 001 [Resting state]
|
|
779
|
-
Nr of instances: 0
|
|
780
|
-
Patient New Patient
|
|
781
|
-
Study New Study [None]
|
|
782
|
-
Series 001 [Finger tap]
|
|
783
|
-
Nr of instances: 0
|
|
784
|
-
----------------------------------
|
|
785
|
-
|
|
786
|
-
Note since any missing levels in the hierarchy are automatically created, these new series now end up in different patients.
|
|
787
|
-
|
|
788
|
-
"""
|
|
789
|
-
attr = {**kwargs, **self.attributes}
|
|
790
|
-
uid, key = self.manager.new_series(parent=self.uid, **attr)
|
|
791
|
-
return self.record('Series', uid, key, **attr)
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
def new_child(self, **kwargs):
|
|
795
|
-
"""Create a new child of the record.
|
|
796
|
-
|
|
797
|
-
Args:
|
|
798
|
-
kwargs: Any valid DICOM (tag, value) pair to assign to the new sibling.
|
|
799
|
-
|
|
800
|
-
See Also:
|
|
801
|
-
:func:`~new_patient`
|
|
802
|
-
:func:`~new_study`
|
|
803
|
-
:func:`~new_series`
|
|
804
|
-
:func:`~new_sibling`
|
|
805
|
-
:func:`~new_pibling`
|
|
806
|
-
|
|
807
|
-
Example:
|
|
808
|
-
Consider an empty study:
|
|
809
|
-
|
|
810
|
-
>>> fMRI = db.study(StudyDescription='fMRI', StudyDate='20230203')
|
|
811
|
-
>>> fMRI.print()
|
|
812
|
-
---------- STUDY ---------------
|
|
813
|
-
Study fMRI [20230203]
|
|
814
|
-
--------------------------------
|
|
815
|
-
|
|
816
|
-
Create two new series in the study:
|
|
817
|
-
|
|
818
|
-
>>> rstate = fMRI.new_child(SeriesDescription='Resting state')
|
|
819
|
-
>>> ftap = fMRI.new_child(SeriesDescription='Finger tap')
|
|
820
|
-
>>> fMRI.print()
|
|
821
|
-
---------- STUDY ---------------
|
|
822
|
-
Study fMRI [20230203]
|
|
823
|
-
Series 001 [Resting state]
|
|
824
|
-
Nr of instances: 0
|
|
825
|
-
Series 002 [Finger tap]
|
|
826
|
-
Nr of instances: 0
|
|
827
|
-
--------------------------------
|
|
828
|
-
|
|
829
|
-
Note the same result could also be obtained by calling :func:`~new_series` on the study.
|
|
830
|
-
"""
|
|
831
|
-
# Note this function is implemented in all subclasses - included here for documentation purposes.
|
|
832
|
-
pass
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
def new_sibling(self, suffix:str=None, **kwargs):
|
|
836
|
-
"""Create a new sibling of the record under the same parent.
|
|
837
|
-
|
|
838
|
-
Args:
|
|
839
|
-
kwargs: Any valid DICOM (tag, value) pair to assign to the new sibling.
|
|
840
|
-
|
|
841
|
-
Raises:
|
|
842
|
-
RuntimeError: when called on a Record of type Database. New records can only be created within an existing database.
|
|
843
|
-
|
|
844
|
-
See Also:
|
|
845
|
-
:func:`~new_patient`
|
|
846
|
-
:func:`~new_study`
|
|
847
|
-
:func:`~new_series`
|
|
848
|
-
:func:`~new_pibling`
|
|
849
|
-
|
|
850
|
-
Example:
|
|
851
|
-
Create a sibling series under the same study:
|
|
852
|
-
|
|
853
|
-
>>> rstate = db.series(SeriesDescription='Resting state')
|
|
854
|
-
>>> ftap = rstate.new_sibling(SeriesDescription='Finger tap')
|
|
855
|
-
>>> rstate.parent().print()
|
|
856
|
-
---------- STUDY ---------------
|
|
857
|
-
Study New Study [None]
|
|
858
|
-
Series 001 [Resting state]
|
|
859
|
-
Nr of instances: 0
|
|
860
|
-
Series 002 [Finger tap]
|
|
861
|
-
Nr of instances: 0
|
|
862
|
-
--------------------------------
|
|
863
|
-
"""
|
|
864
|
-
# Note this function is implemented in all subclasses - included here for documentation purposes.
|
|
865
|
-
# Note the suffix argument is deprecated and should not be used.
|
|
866
|
-
pass
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
def new_pibling(self, **kwargs):
|
|
870
|
-
"""Create a new sibling of the parent record (pibling).
|
|
871
|
-
|
|
872
|
-
Args:
|
|
873
|
-
kwargs (optional): Any valid DICOM (tag, value) pair can be assigned up front as properties of the new pibling.
|
|
874
|
-
|
|
875
|
-
Returns:
|
|
876
|
-
Record: instance of the new parent
|
|
877
|
-
|
|
878
|
-
See Also:
|
|
879
|
-
:func:`~new_patient`
|
|
880
|
-
:func:`~new_study`
|
|
881
|
-
:func:`~new_series`
|
|
882
|
-
|
|
883
|
-
Example:
|
|
884
|
-
Use a series to create a new study directly. A use case is where image processing results derived from a series should be saved in a separate study under the same patient.
|
|
885
|
-
|
|
886
|
-
>>> fMRI = db.study(StudyDescription='fMRI', StudyDate='202305010')
|
|
887
|
-
>>> rstate = fMRI.new_series(SeriesDescription='Resting state')
|
|
888
|
-
>>> rstate_results = rstate.new_pibling(StudyDescription='fMRI resting state analysis', StudyDate='20230603')
|
|
889
|
-
>>> rstate.patient().print()
|
|
890
|
-
---------- PATIENT -------------
|
|
891
|
-
Patient New Patient
|
|
892
|
-
Study New Study [None]
|
|
893
|
-
Series 001 [Resting state]
|
|
894
|
-
Nr of instances: 0
|
|
895
|
-
Study fMRI resting state analysis [20230603]
|
|
896
|
-
--------------------------------
|
|
897
|
-
"""
|
|
898
|
-
type = self.__class__.__name__
|
|
899
|
-
if type == 'Database':
|
|
900
|
-
return None
|
|
901
|
-
if type == 'Patient':
|
|
902
|
-
return None
|
|
903
|
-
return self.parent().new_sibling(**kwargs)
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
def remove(self):
|
|
907
|
-
"""Remove a record from the database.
|
|
908
|
-
|
|
909
|
-
See Also:
|
|
910
|
-
:func:`~copy`
|
|
911
|
-
:func:`~copy_to`
|
|
912
|
-
:func:`~move_to`
|
|
913
|
-
|
|
914
|
-
Example:
|
|
915
|
-
Create a new study in an empty database, then remove it again:
|
|
916
|
-
|
|
917
|
-
>>> database = db.database()
|
|
918
|
-
>>> study = database.new_study(StudyDescription='Demo Study')
|
|
919
|
-
>>> database.print()
|
|
920
|
-
---------- DATABASE --------------
|
|
921
|
-
Location: In memory
|
|
922
|
-
Patient New Patient
|
|
923
|
-
Study Demo Study [None]
|
|
924
|
-
----------------------------------
|
|
925
|
-
|
|
926
|
-
>>> study.remove()
|
|
927
|
-
>>> database.print()
|
|
928
|
-
---------- DATABASE --------------
|
|
929
|
-
Location: In memory
|
|
930
|
-
Patient New Patient
|
|
931
|
-
----------------------------------
|
|
932
|
-
|
|
933
|
-
A record that has been removed from the database can no longer be accessed. Any attempt to do so will raise an error:
|
|
934
|
-
|
|
935
|
-
>>> print(study.label())
|
|
936
|
-
ValueError: This record has been removed from the database and can no longer be accessed.
|
|
937
|
-
|
|
938
|
-
Note:
|
|
939
|
-
Removing a record will also remove all of its children, and this will be permanent after saving the record with :func:`~save`.
|
|
940
|
-
|
|
941
|
-
If a record has been removed accidentally in an interactive session, use :func:`~restore` to revert back to the last saved state.
|
|
942
|
-
"""
|
|
943
|
-
self.manager.delete(self.uid, keys=self.keys())
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
def move_to(self, parent):
|
|
947
|
-
"""Move the record to another parent.
|
|
948
|
-
|
|
949
|
-
Args:
|
|
950
|
-
parent: parent where the record will be moved to.
|
|
951
|
-
|
|
952
|
-
See Also:
|
|
953
|
-
:func:`~remove`
|
|
954
|
-
:func:`~copy`
|
|
955
|
-
:func:`~copy_to`
|
|
956
|
-
|
|
957
|
-
Example:
|
|
958
|
-
Create a database with two studies and a single series in one:
|
|
959
|
-
|
|
960
|
-
>>> demo = db.series(SeriesDescription='!!WATCH ME MOVE!!')
|
|
961
|
-
>>> test = demo.new_pibling(StudyDescription='Test')
|
|
962
|
-
>>> series.patient().print()
|
|
963
|
-
---------- PATIENT -------------
|
|
964
|
-
Patient New Patient
|
|
965
|
-
Study New Study [None]
|
|
966
|
-
Series 001 [!!WATCH ME MOVE!!]
|
|
967
|
-
Nr of instances: 0
|
|
968
|
-
Study Test [None]
|
|
969
|
-
--------------------------------
|
|
970
|
-
|
|
971
|
-
Now move the series to the other study:
|
|
972
|
-
|
|
973
|
-
>>> series.move_to(study)
|
|
974
|
-
>>> series.patient().print()
|
|
975
|
-
---------- PATIENT -------------
|
|
976
|
-
Patient New Patient
|
|
977
|
-
Study New Study [None]
|
|
978
|
-
Study Test [None]
|
|
979
|
-
Series 001 [Demo]
|
|
980
|
-
Nr of instances: 0
|
|
981
|
-
--------------------------------
|
|
982
|
-
|
|
983
|
-
"""
|
|
984
|
-
move_to(self, parent)
|
|
985
|
-
return self
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
def copy_to(self, parent, **kwargs):
|
|
989
|
-
"""Return a copy of the record under another parent.
|
|
990
|
-
|
|
991
|
-
Args:
|
|
992
|
-
parent: parent where the copy will be placed.
|
|
993
|
-
kwargs (optional): Any valid DICOM (tag, value) pair can be assigned up front as properties of the copy.
|
|
994
|
-
|
|
995
|
-
Returns:
|
|
996
|
-
Record: copy of the same type.
|
|
997
|
-
|
|
998
|
-
See Also:
|
|
999
|
-
:func:`~remove`
|
|
1000
|
-
:func:`~copy`
|
|
1001
|
-
:func:`~move_to`
|
|
1002
|
-
|
|
1003
|
-
Example:
|
|
1004
|
-
Create a database with a single patient/study/series:
|
|
1005
|
-
|
|
1006
|
-
>>> series = db.series(SeriesDescription='Demo')
|
|
1007
|
-
>>> series.patient().print()
|
|
1008
|
-
---------- PATIENT -------------
|
|
1009
|
-
Patient New Patient
|
|
1010
|
-
Study New Study [None]
|
|
1011
|
-
Series 001 [Demo]
|
|
1012
|
-
Nr of instances: 0
|
|
1013
|
-
--------------------------------
|
|
1014
|
-
|
|
1015
|
-
Create a new study *Copies* under the same patient, and copy the the *Demo* series into it:
|
|
1016
|
-
|
|
1017
|
-
>>> study = series.new_pibling(StudyDescription='Copies')
|
|
1018
|
-
>>> copy = series.copy_to(study, SeriesDescription='Copy of Demo')
|
|
1019
|
-
|
|
1020
|
-
The same patient now has two studies, each with a single series:
|
|
1021
|
-
|
|
1022
|
-
>>> series.patient().print()
|
|
1023
|
-
---------- PATIENT -------------
|
|
1024
|
-
Patient New Patient
|
|
1025
|
-
Study Copies [None]
|
|
1026
|
-
Series 001 [Copy of Demo]
|
|
1027
|
-
Nr of instances: 0
|
|
1028
|
-
Study New Study [None]
|
|
1029
|
-
Series 001 [Demo]
|
|
1030
|
-
Nr of instances: 0
|
|
1031
|
-
--------------------------------
|
|
1032
|
-
"""
|
|
1033
|
-
return parent._copy_from(self, **kwargs)
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
def copy(self, **kwargs):
|
|
1037
|
-
"""Return a copy of the record under the same parent.
|
|
1038
|
-
|
|
1039
|
-
Args:
|
|
1040
|
-
kwargs (optional): Any valid DICOM (tag, value) pair can be assigned up front as properties of the copy.
|
|
1041
|
-
|
|
1042
|
-
Returns:
|
|
1043
|
-
Record: copy of the same type.
|
|
1044
|
-
|
|
1045
|
-
See Also:
|
|
1046
|
-
:func:`~remove`
|
|
1047
|
-
:func:`~copy_to`
|
|
1048
|
-
:func:`~move_to`
|
|
1049
|
-
|
|
1050
|
-
Example:
|
|
1051
|
-
Create a new DICOM study and build two copies in the same patient, assigning a new study description on the fly:
|
|
1052
|
-
|
|
1053
|
-
>>> study = db.study(StudyDescription='Original', StudyDate='20001231')
|
|
1054
|
-
>>> copy1 = study.copy(StudyDescription='Copy 1')
|
|
1055
|
-
>>> copy2 = study.copy(StudyDescription='Copy 2')
|
|
1056
|
-
>>> study.parent().print()
|
|
1057
|
-
---------- PATIENT -------------
|
|
1058
|
-
Patient New Patient
|
|
1059
|
-
Study Copy 1 [20001231]
|
|
1060
|
-
Study Copy 2 [20001231]
|
|
1061
|
-
Study Original [20001231]
|
|
1062
|
-
--------------------------------
|
|
1063
|
-
"""
|
|
1064
|
-
return self.copy_to(self.parent(), **kwargs)
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
# Load and save
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
def restore(self):
|
|
1071
|
-
"""Restore the record to the last changed state.
|
|
1072
|
-
|
|
1073
|
-
.. warning::
|
|
1074
|
-
|
|
1075
|
-
Restoring is irreversible! Any edits made to the record since the last time it was saved will be lost.
|
|
1076
|
-
|
|
1077
|
-
See Also:
|
|
1078
|
-
:func:`~save`
|
|
1079
|
-
|
|
1080
|
-
Create a new patient and change the name:
|
|
1081
|
-
|
|
1082
|
-
>>> patient = db.patient(PatientName='James Bond')
|
|
1083
|
-
>>> patient.PatientName = 'Scarface'
|
|
1084
|
-
>>> print(patient.PatientName)
|
|
1085
|
-
Scarface
|
|
1086
|
-
|
|
1087
|
-
Calling restore will undo the changes:
|
|
1088
|
-
|
|
1089
|
-
>>> patient.restore()
|
|
1090
|
-
>>> print(patient.PatientName)
|
|
1091
|
-
James Bond
|
|
1092
|
-
"""
|
|
1093
|
-
rows = self.manager._extract_record(self.name, self.uid)
|
|
1094
|
-
self.manager.restore(rows)
|
|
1095
|
-
self.write()
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
def save(self, path=None):
|
|
1099
|
-
"""Save any changes made to the record.
|
|
1100
|
-
|
|
1101
|
-
.. warning::
|
|
1102
|
-
|
|
1103
|
-
Saving is irreversible! Any edits made to the record before saving cannot be undone.
|
|
1104
|
-
|
|
1105
|
-
See Also:
|
|
1106
|
-
:func:`~restore`
|
|
1107
|
-
|
|
1108
|
-
Example:
|
|
1109
|
-
Create a new patient, change the name, and save:
|
|
1110
|
-
|
|
1111
|
-
>>> patient = db.patient(PatientName='James Bond')
|
|
1112
|
-
>>> patient.PatientName = 'Scarface'
|
|
1113
|
-
>>> patient.save()
|
|
1114
|
-
|
|
1115
|
-
At this point the original information can no longer be restored. Calling restore does not revert back to the original:
|
|
1116
|
-
|
|
1117
|
-
>>> patient.restore()
|
|
1118
|
-
>>> print(patient.PatientName)
|
|
1119
|
-
Scarface
|
|
1120
|
-
"""
|
|
1121
|
-
rows = self.manager._extract_record(self.name, self.uid)
|
|
1122
|
-
self.manager.save(rows)
|
|
1123
|
-
self.write(path)
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
def load(self):
|
|
1127
|
-
"""Load the record into memory.
|
|
1128
|
-
|
|
1129
|
-
After loading the record into memory, all subsequent changes will be made in memory only. Call clear() to write any changes to disk and remove it from memory.
|
|
1130
|
-
|
|
1131
|
-
Note:
|
|
1132
|
-
If the record already exists in memory, read() does nothing. This is to avoid that any changes made after reading are overwritten.
|
|
1133
|
-
|
|
1134
|
-
See Also:
|
|
1135
|
-
:func:`~clear`
|
|
1136
|
-
|
|
1137
|
-
Example:
|
|
1138
|
-
|
|
1139
|
-
As an example, we can verify that editing data in memory is faster than on disk. We'll need the time package and a large series on disk:
|
|
1140
|
-
|
|
1141
|
-
>>> from time import time
|
|
1142
|
-
>>> path = 'path\\to\\empty\\folder'
|
|
1143
|
-
>>> series = db.zeros((20,20,256,256), in_database=db.database(path))
|
|
1144
|
-
|
|
1145
|
-
Now measure the time it takes to set the slice locations to a constant value:
|
|
1146
|
-
|
|
1147
|
-
>>> t=time(); series.SliceLocation=1; print(time()-t)
|
|
1148
|
-
17.664631605148315
|
|
1149
|
-
|
|
1150
|
-
Since the series was created on disk, this is editing on disk. Now load the series into memory and perform the same steps:
|
|
1151
|
-
|
|
1152
|
-
>>> series.load()
|
|
1153
|
-
>>> t=time(); series.SliceLocation=1; print(time()-t)
|
|
1154
|
-
2.3518126010894775
|
|
1155
|
-
|
|
1156
|
-
On the machine where this was executed, the same computation runs more than 10 times faster in memory.
|
|
1157
|
-
"""
|
|
1158
|
-
self.manager.read(self.uid, keys=self.keys())
|
|
1159
|
-
return self
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
def clear(self):
|
|
1163
|
-
"""Clear the record from memory.
|
|
1164
|
-
|
|
1165
|
-
This will write the record to disk and clear it from memory. After this step, subsequent calculations will be performed from disk.
|
|
1166
|
-
|
|
1167
|
-
Note:
|
|
1168
|
-
If the record does not exist in memory, or if its database does not have a path on disk associated, read() does nothing.
|
|
1169
|
-
|
|
1170
|
-
See Also:
|
|
1171
|
-
:func:`~read`
|
|
1172
|
-
|
|
1173
|
-
Example:
|
|
1174
|
-
|
|
1175
|
-
As an example, we can verify that editing data in memory is faster than on disk. We'll need the time package and a large series in memory. We also provide a path to a directory for writing data:
|
|
1176
|
-
|
|
1177
|
-
>>> from time import time
|
|
1178
|
-
>>> series = db.zeros((20,20,256,256))
|
|
1179
|
-
>>> series.database().set_path(path)
|
|
1180
|
-
|
|
1181
|
-
Now measure the time it takes to set the slice locations to a constant value:
|
|
1182
|
-
|
|
1183
|
-
>>> t=time(); series.SliceLocation=1; print(time()-t)
|
|
1184
|
-
1.9060208797454834
|
|
1185
|
-
|
|
1186
|
-
Since the series was created in memory, this is editing in memory. Now we clear the series from memory and perform the same computation:
|
|
1187
|
-
|
|
1188
|
-
>>> series.clear()
|
|
1189
|
-
>>> t=time(); series.SliceLocation=1; print(time()-t)
|
|
1190
|
-
17.933974981307983
|
|
1191
|
-
|
|
1192
|
-
The computation is now run from disk and is 10 times slower because of the need to read and write the files.
|
|
1193
|
-
"""
|
|
1194
|
-
self.manager.clear(self.uid, keys=self.keys())
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
def export_as_dicom(self, path:str):
|
|
1198
|
-
"""Export record in DICOM format to an external directory.
|
|
1199
|
-
|
|
1200
|
-
Note since this is exporting outside of the current database this will assign new identifiers to the exported data.
|
|
1201
|
-
|
|
1202
|
-
Args:
|
|
1203
|
-
path (str): path to export directory.
|
|
1204
|
-
|
|
1205
|
-
See Also:
|
|
1206
|
-
:func:`~export_as_png`
|
|
1207
|
-
:func:`~export_as_nifti`
|
|
1208
|
-
:func:`~export_as_npy`
|
|
1209
|
-
:func:`~export_as_csv`
|
|
1210
|
-
|
|
1211
|
-
Example:
|
|
1212
|
-
|
|
1213
|
-
Create a 4D series and export as DICOM
|
|
1214
|
-
|
|
1215
|
-
>>> series = db.ones((128, 128, 10, 5))
|
|
1216
|
-
>>> path = 'path\\to\\empty\\folder'
|
|
1217
|
-
>>> series.export_as_dicom(path)
|
|
1218
|
-
|
|
1219
|
-
This should create a single folder in the directory, populated with 50 DICOM files.
|
|
1220
|
-
"""
|
|
1221
|
-
if self.name == 'Database':
|
|
1222
|
-
folder = 'Database'
|
|
1223
|
-
else:
|
|
1224
|
-
folder = self.label()
|
|
1225
|
-
path = export_path(path, folder)
|
|
1226
|
-
for child in self.children():
|
|
1227
|
-
child.export_as_dicom(path)
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
def export_as_png(self, path:str, center:float=None, width:float=None, colormap:str=None):
|
|
1231
|
-
"""Export record in PNG format.
|
|
1232
|
-
|
|
1233
|
-
Args:
|
|
1234
|
-
path (str): path to export directory.
|
|
1235
|
-
center (float, optional): center of the color window. Defaults to None, in which case the center is taken from the DICOM header.
|
|
1236
|
-
width (float, optional): width of the color window. Defaults to None, in which case the width is taken from the DICOM header.
|
|
1237
|
-
colormap (str, optional): color map to use as lookup table. Any valid matplotlib colormap can be entered here. Please the `matplotlib colormap reference <https://matplotlib.org/stable/gallery/color/colormap_reference.html>`_ for a complete list. Defaults to None, in which case the colormap is taken from the DICOM header.
|
|
1238
|
-
|
|
1239
|
-
See Also:
|
|
1240
|
-
:func:`~export_as_dicom`
|
|
1241
|
-
:func:`~export_as_nifti`
|
|
1242
|
-
:func:`~export_as_npy`
|
|
1243
|
-
:func:`~export_as_csv`
|
|
1244
|
-
|
|
1245
|
-
Example:
|
|
1246
|
-
|
|
1247
|
-
Create a 4D series and export as PNG, using the colormap plasma:
|
|
1248
|
-
|
|
1249
|
-
>>> series = db.ones((128, 128, 10, 5))
|
|
1250
|
-
>>> path = 'path\\to\\empty\\folder'
|
|
1251
|
-
>>> series.export_as_png(path, center=1, width=0.5, colormap='plasma')
|
|
1252
|
-
|
|
1253
|
-
This should create a single folder in the directory, populated with 50 PNG files.
|
|
1254
|
-
"""
|
|
1255
|
-
if self.name == 'Database':
|
|
1256
|
-
folder = 'Database'
|
|
1257
|
-
else:
|
|
1258
|
-
folder = self.label()
|
|
1259
|
-
path = export_path(path, folder)
|
|
1260
|
-
for child in self.children():
|
|
1261
|
-
child.export_as_png(path, center=center, width=width, colormap=colormap)
|
|
1262
|
-
|
|
1263
|
-
def export_as_csv(self, path:str):
|
|
1264
|
-
"""Export record in CSV format to an external directory.
|
|
1265
|
-
|
|
1266
|
-
Args:
|
|
1267
|
-
path (str): path to export directory.
|
|
1268
|
-
|
|
1269
|
-
See Also:
|
|
1270
|
-
:func:`~export_as_png`
|
|
1271
|
-
:func:`~export_as_nifti`
|
|
1272
|
-
:func:`~export_as_npy`
|
|
1273
|
-
:func:`~export_as_dicom`
|
|
1274
|
-
|
|
1275
|
-
Example:
|
|
1276
|
-
|
|
1277
|
-
Create a 4D series and export as CSV:
|
|
1278
|
-
|
|
1279
|
-
>>> series = db.ones((128, 128, 10, 5))
|
|
1280
|
-
>>> path = 'path\\to\\empty\\folder'
|
|
1281
|
-
>>> series.export_as_csv(path)
|
|
1282
|
-
|
|
1283
|
-
This should create a single folder in the directory, populated with 50 CSV files.
|
|
1284
|
-
"""
|
|
1285
|
-
if self.name == 'Database':
|
|
1286
|
-
folder = 'Database'
|
|
1287
|
-
else:
|
|
1288
|
-
folder = self.label()
|
|
1289
|
-
path = export_path(path, folder)
|
|
1290
|
-
for child in self.children():
|
|
1291
|
-
child.export_as_csv(path)
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
def export_as_nifti(self, path:str, dims:tuple=None):
|
|
1295
|
-
"""Export record in NIFTI format to an external directory.
|
|
1296
|
-
|
|
1297
|
-
Args:
|
|
1298
|
-
path (str): path to export directory.
|
|
1299
|
-
dims (tuple, optional): when set, volumes are extracted along the given dimensions and exported in single files. If dims is not set, each image will be exported in its own file.
|
|
1300
|
-
|
|
1301
|
-
See Also:
|
|
1302
|
-
:func:`~export_as_png`
|
|
1303
|
-
:func:`~export_as_dicom`
|
|
1304
|
-
:func:`~export_as_npy`
|
|
1305
|
-
:func:`~export_as_csv`
|
|
1306
|
-
|
|
1307
|
-
Example:
|
|
1308
|
-
|
|
1309
|
-
Create a 4D series and export as NIFTI:
|
|
1310
|
-
|
|
1311
|
-
>>> series = db.ones((128, 128, 10, 5))
|
|
1312
|
-
>>> path = 'path\\to\\empty\\folder'
|
|
1313
|
-
>>> series.export_as_nifti(path)
|
|
1314
|
-
|
|
1315
|
-
This should create a single folder in the directory, populated with 50 NIFTI files.
|
|
1316
|
-
|
|
1317
|
-
In order to export the entire series in a single volume, provide the dimensions along which the volume is to be taken:
|
|
1318
|
-
|
|
1319
|
-
>>> dims = ('SliceLocation', 'AcquisitionTime')
|
|
1320
|
-
>>> series.export_as_nifti(path, dims=dims)
|
|
1321
|
-
|
|
1322
|
-
This will now create a single nifti file.
|
|
1323
|
-
|
|
1324
|
-
Note: in this case the dimensions must be specified as slice location and acquisition time because these are the default dimensions used by series creation functions like :func:`~ones`.
|
|
1325
|
-
"""
|
|
1326
|
-
if self.name == 'Database':
|
|
1327
|
-
folder = 'Database'
|
|
1328
|
-
else:
|
|
1329
|
-
folder = self.label()
|
|
1330
|
-
path = export_path(path, folder)
|
|
1331
|
-
for child in self.children():
|
|
1332
|
-
child.export_as_nifti(path, dims=dims)
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
def export_as_npy(self, path:str, dims:tuple=None):
|
|
1336
|
-
"""Export record in numpy's NPY format to an external directory.
|
|
1337
|
-
|
|
1338
|
-
Args:
|
|
1339
|
-
path (str): path to export directory.
|
|
1340
|
-
dims (tuple, optional): when set, volumes are extracted along the given dimensions and exported in single files. If dims is not set (None), each image will be exported in its own file. Defaults to None.
|
|
1341
|
-
|
|
1342
|
-
See Also:
|
|
1343
|
-
:func:`~export_as_png`
|
|
1344
|
-
:func:`~export_as_nifti`
|
|
1345
|
-
:func:`~export_as_dicom`
|
|
1346
|
-
:func:`~export_as_csv`
|
|
1347
|
-
|
|
1348
|
-
Example:
|
|
1349
|
-
|
|
1350
|
-
Create a 4D series:
|
|
1351
|
-
|
|
1352
|
-
>>> series = db.ones((128, 128, 10, 5))
|
|
1353
|
-
|
|
1354
|
-
Export the series as npy, with each slice in a separate file:
|
|
1355
|
-
|
|
1356
|
-
>>> path = 'path\\to\\empty\\folder'
|
|
1357
|
-
>>> series.export_as_npy(path)
|
|
1358
|
-
|
|
1359
|
-
This will create 50 npy files in the folder, one for each image. To save the entire volume in a single file, specify the dimensions of the volume:
|
|
1360
|
-
|
|
1361
|
-
>>> dims = ('SliceLocation', 'AcquisitionTime')
|
|
1362
|
-
>>> series.export_as_npy(path, dims)
|
|
1363
|
-
|
|
1364
|
-
This will create a single npy file.
|
|
1365
|
-
|
|
1366
|
-
Note: in this case the dimensions must be specified as slice location and acquisition time because these are the default dimensions used by series creation functions like :func:`~ones`.
|
|
1367
|
-
"""
|
|
1368
|
-
if self.name == 'Database':
|
|
1369
|
-
folder = 'Database'
|
|
1370
|
-
else:
|
|
1371
|
-
folder = self.label()
|
|
1372
|
-
path = export_path(path, folder)
|
|
1373
|
-
for child in self.children():
|
|
1374
|
-
child.export_as_npy(path, dims=dims)
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
def progress(self, value: float, maximum: float, message: str=None):
|
|
1379
|
-
"""Print progress message to the terminal..
|
|
1380
|
-
|
|
1381
|
-
Args:
|
|
1382
|
-
value (float): current status
|
|
1383
|
-
maximum (float): maximal value
|
|
1384
|
-
message (str, optional): Message to include in the update. Defaults to None.
|
|
1385
|
-
|
|
1386
|
-
Note:
|
|
1387
|
-
When working through a terminal this could easily be replicated with a print statement. The advantage of using the progress interface is that the code does not need to be changed when the computation is run through a graphical user interface (assuming this uses a compatible API).
|
|
1388
|
-
|
|
1389
|
-
Another advantage is that messaging can be muted/unmuted using .mute() and .unmute(), for instance when the object is passed to a subroutine.
|
|
1390
|
-
|
|
1391
|
-
See Also:
|
|
1392
|
-
:func:`~message`
|
|
1393
|
-
:func:`~mute`
|
|
1394
|
-
:func:`~unmute`
|
|
1395
|
-
|
|
1396
|
-
Example:
|
|
1397
|
-
>>> nr_of_slices = 3
|
|
1398
|
-
>>> series = db.zeros((nr_of_slices,128,128))
|
|
1399
|
-
>>> for slice in range(nr_of_slices):
|
|
1400
|
-
series.progress(1+slice, nr_of_slices, 'Looping over slices')
|
|
1401
|
-
Looping over slices [33 %]
|
|
1402
|
-
Looping over slices [67 %]
|
|
1403
|
-
Looping over slices [100 %]
|
|
1404
|
-
"""
|
|
1405
|
-
if not self._mute:
|
|
1406
|
-
self.manager.status.progress(value, maximum, message=message)
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
def message(self, message: str):
|
|
1410
|
-
"""Print a message to the user.
|
|
1411
|
-
|
|
1412
|
-
Args:
|
|
1413
|
-
message (str): Message to be printed.
|
|
1414
|
-
|
|
1415
|
-
Note:
|
|
1416
|
-
When working through a terminal a print statement would have exactly the same effect. The advantage of using the message interface is that the code does not need to be changed when the computation is run through a graphical user interface (assuming this uses a compatible API).
|
|
1417
|
-
|
|
1418
|
-
Another advantage is that messaging can be muted/unmuted using .mute() and .unmute() for instance when the object is passed to a subroutine.
|
|
1419
|
-
|
|
1420
|
-
See Also:
|
|
1421
|
-
:func:`~progress`
|
|
1422
|
-
:func:`~mute`
|
|
1423
|
-
:func:`~unmute`
|
|
1424
|
-
|
|
1425
|
-
Example:
|
|
1426
|
-
|
|
1427
|
-
>>> series.message('Starting computation..')
|
|
1428
|
-
Starting computation..
|
|
1429
|
-
|
|
1430
|
-
After muting the same statment does not send a message:
|
|
1431
|
-
|
|
1432
|
-
>>> series.mute()
|
|
1433
|
-
>>> series.message('Starting computation..')
|
|
1434
|
-
|
|
1435
|
-
Unmute to reactivate sending messages:
|
|
1436
|
-
|
|
1437
|
-
>>> series.unmute()
|
|
1438
|
-
>>> series.message('Starting computation..')
|
|
1439
|
-
Starting computation..
|
|
1440
|
-
"""
|
|
1441
|
-
if not self._mute:
|
|
1442
|
-
self.manager.status.message(message)
|
|
1443
|
-
|
|
1444
|
-
def mute(self):
|
|
1445
|
-
"""Prevent the object from sending status updates to the user
|
|
1446
|
-
|
|
1447
|
-
See Also:
|
|
1448
|
-
:func:`~unmute`
|
|
1449
|
-
:func:`~message`
|
|
1450
|
-
:func:`~progress`
|
|
1451
|
-
|
|
1452
|
-
Example:
|
|
1453
|
-
>>> series = db.zeros((3,128,128))
|
|
1454
|
-
>>> print('My message: ')
|
|
1455
|
-
>>> series.message('Hello World')
|
|
1456
|
-
>>> series.mute()
|
|
1457
|
-
>>> print('My message: ')
|
|
1458
|
-
>>> series.message('Hello World')
|
|
1459
|
-
|
|
1460
|
-
My message:
|
|
1461
|
-
Hello World
|
|
1462
|
-
My message:
|
|
1463
|
-
"""
|
|
1464
|
-
self._mute = True
|
|
1465
|
-
self.status.muted = True
|
|
1466
|
-
|
|
1467
|
-
def unmute(self):
|
|
1468
|
-
"""Allow the object from sending status updates to the user
|
|
1469
|
-
|
|
1470
|
-
Note:
|
|
1471
|
-
Records are unmuted by default, so unmuting is only necessary after a previouse call to mute(). Unmuting has no effect when the record is already unmuted.
|
|
1472
|
-
|
|
1473
|
-
See Also:
|
|
1474
|
-
:func:`~mute`
|
|
1475
|
-
:func:`~message`
|
|
1476
|
-
:func:`~progress`
|
|
1477
|
-
|
|
1478
|
-
Example:
|
|
1479
|
-
>>> series = db.zeros((3,128,128))
|
|
1480
|
-
>>> print('My message: ')
|
|
1481
|
-
>>> series.message('Hello World')
|
|
1482
|
-
>>> series.mute()
|
|
1483
|
-
>>> print('My message: ')
|
|
1484
|
-
>>> series.message('Hello World')
|
|
1485
|
-
>>> series.unmute()
|
|
1486
|
-
>>> print('My message: ')
|
|
1487
|
-
>>> series.message('Hello World')
|
|
1488
|
-
|
|
1489
|
-
My message:
|
|
1490
|
-
Hello World
|
|
1491
|
-
My message:
|
|
1492
|
-
My message:
|
|
1493
|
-
Hello World
|
|
1494
|
-
"""
|
|
1495
|
-
self._mute = False
|
|
1496
|
-
self.status.muted = False
|
|
1497
|
-
|
|
1498
|
-
def type(self):
|
|
1499
|
-
return self.__class__.__name__
|
|
1500
|
-
|
|
1501
|
-
def exists(self):
|
|
1502
|
-
#if self.manager.register is None:
|
|
1503
|
-
if not self.manager.is_open():
|
|
1504
|
-
return False
|
|
1505
|
-
try:
|
|
1506
|
-
keys = self.keys().tolist()
|
|
1507
|
-
except:
|
|
1508
|
-
return False
|
|
1509
|
-
return keys != []
|
|
1510
|
-
|
|
1511
|
-
def record(self, type, uid='Database', key=None, **kwargs):
|
|
1512
|
-
return self.new(self.manager, uid, type, key=key, **kwargs)
|
|
1513
|
-
|
|
1514
|
-
def register(self):
|
|
1515
|
-
return self.manager._extract(self.keys())
|
|
1516
|
-
#return self.manager.register.loc[self.keys(),:]
|
|
1517
|
-
|
|
1518
|
-
def instances(self, sort=True, sortby=None, select={}, **kwargs):
|
|
1519
|
-
inst = self.manager.instances(keys=self.keys(), sort=sort, sortby=sortby, select=select, **kwargs)
|
|
1520
|
-
return [self.record('Instance', uid, key) for key, uid in inst.items()]
|
|
1521
|
-
|
|
1522
|
-
def images(self, sort=True, sortby=None, **kwargs):
|
|
1523
|
-
inst = self.manager.instances(keys=self.keys(), sort=sort, sortby=sortby, images=True, **kwargs)
|
|
1524
|
-
return [self.record('Instance', uid, key) for key, uid in inst.items()]
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
# This needs a test whether the instance is an image - else move to the next
|
|
1529
|
-
def image(self, **kwargs):
|
|
1530
|
-
return self.instance(**kwargs)
|
|
1531
|
-
|
|
1532
|
-
# Needs a unit test
|
|
1533
|
-
def instance(self, uid=None, key=None):
|
|
1534
|
-
if key is not None:
|
|
1535
|
-
#uid = self.manager.register.at[key, 'SOPInstanceUID']
|
|
1536
|
-
uid = self.manager._at(key, 'SOPInstanceUID')
|
|
1537
|
-
if uid is None:
|
|
1538
|
-
return
|
|
1539
|
-
return self.record('Instance', uid, key=key)
|
|
1540
|
-
if uid is not None:
|
|
1541
|
-
return self.record('Instance', uid)
|
|
1542
|
-
key = self.key()
|
|
1543
|
-
#uid = self.manager.register.at[key, 'SOPInstanceUID']
|
|
1544
|
-
uid = self.manager._at(key, 'SOPInstanceUID')
|
|
1545
|
-
return self.record('Instance', uid, key=key)
|
|
1546
|
-
|
|
1547
|
-
# Needs a unit test
|
|
1548
|
-
def sery(self, uid=None, key=None):
|
|
1549
|
-
if key is not None:
|
|
1550
|
-
#uid = self.manager.register.at[key, 'SeriesInstanceUID']
|
|
1551
|
-
uid = self.manager._at(key, 'SeriesInstanceUID')
|
|
1552
|
-
if uid is None:
|
|
1553
|
-
return
|
|
1554
|
-
return self.record('Series', uid, key=key)
|
|
1555
|
-
if uid is not None:
|
|
1556
|
-
return self.record('Series', uid)
|
|
1557
|
-
key = self.key()
|
|
1558
|
-
#uid = self.manager.register.at[key, 'SeriesInstanceUID']
|
|
1559
|
-
uid = self.manager._at(key, 'SeriesInstanceUID')
|
|
1560
|
-
return self.record('Series', uid, key=key)
|
|
1561
|
-
|
|
1562
|
-
# Needs a unit test
|
|
1563
|
-
def study(self, uid=None, key=None):
|
|
1564
|
-
if key is not None:
|
|
1565
|
-
#uid = self.manager.register.at[key, 'StudyInstanceUID']
|
|
1566
|
-
uid = self.manager._at(key, 'StudyInstanceUID')
|
|
1567
|
-
if uid is None:
|
|
1568
|
-
return
|
|
1569
|
-
return self.record('Study', uid, key=key)
|
|
1570
|
-
if uid is not None:
|
|
1571
|
-
return self.record('Study', uid)
|
|
1572
|
-
key = self.key()
|
|
1573
|
-
#uid = self.manager.register.at[key, 'StudyInstanceUID']
|
|
1574
|
-
uid = self.manager._at(key, 'StudyInstanceUID')
|
|
1575
|
-
return self.record('Study', uid, key=key)
|
|
1576
|
-
|
|
1577
|
-
# Needs a unit test
|
|
1578
|
-
def patient(self, uid=None, key=None):
|
|
1579
|
-
if key is not None:
|
|
1580
|
-
#uid = self.manager.register.at[key, 'PatientID']
|
|
1581
|
-
uid = self.manager._at(key, 'PatientID')
|
|
1582
|
-
if uid is None:
|
|
1583
|
-
return
|
|
1584
|
-
return self.record('Patient', uid, key=key)
|
|
1585
|
-
if uid is not None:
|
|
1586
|
-
return self.record('Patient', uid)
|
|
1587
|
-
key = self.key()
|
|
1588
|
-
#uid = self.manager.register.at[key, 'PatientID']
|
|
1589
|
-
uid = self.manager._at(key, 'PatientID')
|
|
1590
|
-
return self.record('Patient', uid, key=key)
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
def read(self): # Obsolete - replace by load()
|
|
1596
|
-
return self.load()
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
def write(self, path=None):
|
|
1600
|
-
if path is not None:
|
|
1601
|
-
self.manager.path = path
|
|
1602
|
-
try:
|
|
1603
|
-
keys = self.keys()
|
|
1604
|
-
except: # empty database
|
|
1605
|
-
pass
|
|
1606
|
-
else:
|
|
1607
|
-
self.manager.write(self.uid, keys=keys)
|
|
1608
|
-
self.manager._write_df()
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
def new_instance(self, dataset=None, **kwargs):
|
|
1612
|
-
attr = {**kwargs, **self.attributes}
|
|
1613
|
-
uid, key = self.manager.new_instance(parent=self.uid, dataset=dataset, **attr)
|
|
1614
|
-
return self.record('Instance', uid, key, **attr)
|
|
1615
|
-
|
|
1616
|
-
def _set_values(self, attributes, values):
|
|
1617
|
-
keys = self.keys()
|
|
1618
|
-
self._key = self.manager.set_values(attributes, values, keys)
|
|
1619
|
-
|
|
1620
|
-
def get_values(self, attributes):
|
|
1621
|
-
return self.manager.get_values(attributes, self.keys())
|
|
1622
|
-
|
|
1623
|
-
def init_dataset(self, dtype='mri'):
|
|
1624
|
-
if dtype=='mri':
|
|
1625
|
-
ds = MRImage()
|
|
1626
|
-
else: # dummy option for now
|
|
1627
|
-
ds = MRImage()
|
|
1628
|
-
for a in self.attributes:
|
|
1629
|
-
ds.set_values(a, self.attributes[a])
|
|
1630
|
-
return ds
|
|
1631
|
-
|
|
1632
|
-
def get_dataset(self):
|
|
1633
|
-
ds = self.manager.get_dataset(self.uid, self.keys())
|
|
1634
|
-
return ds
|
|
1635
|
-
|
|
1636
|
-
def set_dataset(self, dataset):
|
|
1637
|
-
self.manager.set_dataset(self.uid, dataset, self.keys())
|
|
1638
|
-
|
|
1639
|
-
def read_dataframe(*args, **kwargs):
|
|
1640
|
-
return read_dataframe(*args, **kwargs)
|
|
1641
|
-
|
|
1642
|
-
def series_data(self):
|
|
1643
|
-
attr = dbdataset.module_series()
|
|
1644
|
-
vals = self[attr]
|
|
1645
|
-
return attr, vals
|
|
1646
|
-
|
|
1647
|
-
def study_data(self):
|
|
1648
|
-
attr = dbdataset.module_study()
|
|
1649
|
-
vals = self[attr]
|
|
1650
|
-
return attr, vals
|
|
1651
|
-
|
|
1652
|
-
def patient_data(self):
|
|
1653
|
-
attr = dbdataset.module_patient()
|
|
1654
|
-
vals = self[attr]
|
|
1655
|
-
return attr, vals
|
|
1656
|
-
|
|
1657
|
-
# def tree(*args, **kwargs):
|
|
1658
|
-
# return tree(*args, **kwargs)
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
#
|
|
1663
|
-
# Functions on a list of records of the same database
|
|
1664
|
-
#
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
def copy_to(records:list, parent:Record):
|
|
1668
|
-
"""Copy a list of records to a new parent.
|
|
1669
|
-
|
|
1670
|
-
Args:
|
|
1671
|
-
records (list): list of Records of the same type
|
|
1672
|
-
parent (Record): location for the copies.
|
|
1673
|
-
|
|
1674
|
-
See also:
|
|
1675
|
-
`copy`
|
|
1676
|
-
`move_to`
|
|
1677
|
-
|
|
1678
|
-
Example:
|
|
1679
|
-
|
|
1680
|
-
Consider the hollywood demo database:
|
|
1681
|
-
|
|
1682
|
-
>>> database = db.dro.database_hollywood()
|
|
1683
|
-
|
|
1684
|
-
There are currently two MRI studies in the database:
|
|
1685
|
-
|
|
1686
|
-
>>> MRIs = database.studies(StudyDescription='MRI)
|
|
1687
|
-
>>> len(MRIs)
|
|
1688
|
-
2
|
|
1689
|
-
|
|
1690
|
-
Create a new patient and copy the MRI studies there:
|
|
1691
|
-
|
|
1692
|
-
>>> tarantino = database.new_patient(PatientName='Tarantino')
|
|
1693
|
-
>>> db.copy_to(MRIs, tarantino)
|
|
1694
|
-
>>> tarantino_MRIs = tarantino.studies()
|
|
1695
|
-
>>> len(tarantino_MRIs)
|
|
1696
|
-
2
|
|
1697
|
-
|
|
1698
|
-
Note that all header information is automatically updated:
|
|
1699
|
-
|
|
1700
|
-
>>> tarantino_MRIs[0].PatientName
|
|
1701
|
-
Tarantino
|
|
1702
|
-
|
|
1703
|
-
Since the studies were copied, the originals remained and the total number of studies in the database has increased:
|
|
1704
|
-
|
|
1705
|
-
>>> MRIs = database.studies(StudyDescription='MRI)
|
|
1706
|
-
>>> len(MRIs)
|
|
1707
|
-
4
|
|
1708
|
-
"""
|
|
1709
|
-
if not isinstance(records, list):
|
|
1710
|
-
return records.copy_to(parent)
|
|
1711
|
-
copy = []
|
|
1712
|
-
desc = parent.label()
|
|
1713
|
-
for r, record in enumerate(records):
|
|
1714
|
-
record.progress(r+1, len(records), 'Copying ' + desc)
|
|
1715
|
-
copy_record = record.copy_to(parent)
|
|
1716
|
-
if isinstance(copy_record, list):
|
|
1717
|
-
copy += copy_record
|
|
1718
|
-
else:
|
|
1719
|
-
copy.append(copy_record)
|
|
1720
|
-
record.status.hide()
|
|
1721
|
-
return copy
|
|
1722
|
-
|
|
1723
|
-
def move_to(records:list, target:Record):
|
|
1724
|
-
"""Move a list of records to a new parent.
|
|
1725
|
-
|
|
1726
|
-
Args:
|
|
1727
|
-
records (list): list of Records of the same type
|
|
1728
|
-
parent (Record): location for the copies.
|
|
1729
|
-
|
|
1730
|
-
See also:
|
|
1731
|
-
`copy`
|
|
1732
|
-
`copy_to`
|
|
1733
|
-
|
|
1734
|
-
Example:
|
|
1735
|
-
|
|
1736
|
-
Consider the hollywood demo database:
|
|
1737
|
-
|
|
1738
|
-
>>> database = db.dro.database_hollywood()
|
|
1739
|
-
|
|
1740
|
-
There are currently two MRI studies in the database:
|
|
1741
|
-
|
|
1742
|
-
>>> MRIs = database.studies(StudyDescription='MRI)
|
|
1743
|
-
>>> len(MRIs)
|
|
1744
|
-
2
|
|
1745
|
-
|
|
1746
|
-
Create a new patient and move the MRI studies there:
|
|
1747
|
-
|
|
1748
|
-
>>> tarantino = database.new_patient(PatientName='Tarantino')
|
|
1749
|
-
>>> db.copy_to(MRIs, tarantino)
|
|
1750
|
-
>>> tarantino_MRIs = tarantino.studies()
|
|
1751
|
-
>>> len(tarantino_MRIs)
|
|
1752
|
-
2
|
|
1753
|
-
|
|
1754
|
-
Note that all header information is automatically updated:
|
|
1755
|
-
|
|
1756
|
-
>>> tarantino_MRIs[0].PatientName
|
|
1757
|
-
Tarantino
|
|
1758
|
-
|
|
1759
|
-
Since the studies were moved, the total number of studies in the database has stayed the same:
|
|
1760
|
-
|
|
1761
|
-
>>> MRIs = database.studies(StudyDescription='MRI)
|
|
1762
|
-
>>> len(MRIs)
|
|
1763
|
-
2
|
|
1764
|
-
|
|
1765
|
-
And the original patients do not have any MRI studies left:
|
|
1766
|
-
|
|
1767
|
-
>>> jb = database.patients(PatientName = 'James Bond')
|
|
1768
|
-
>>> MRIs = jb[0].studies(StudyDescription='MRI')
|
|
1769
|
-
>>> len(MRIs)
|
|
1770
|
-
0
|
|
1771
|
-
"""
|
|
1772
|
-
if not isinstance(records, list):
|
|
1773
|
-
records = [records]
|
|
1774
|
-
mgr = records[0].manager
|
|
1775
|
-
uids = [rec.uid for rec in records]
|
|
1776
|
-
mgr.move_to(uids, target.uid, **target.attributes)
|
|
1777
|
-
return records
|
|
1778
|
-
|
|
1779
|
-
def group(records:list, into:Record=None, inplace=False)->Record:
|
|
1780
|
-
if not isinstance(records, list):
|
|
1781
|
-
records = [records]
|
|
1782
|
-
if into is None:
|
|
1783
|
-
into = records[0].new_pibling()
|
|
1784
|
-
if inplace:
|
|
1785
|
-
move_to(records, into)
|
|
1786
|
-
else:
|
|
1787
|
-
copy_to(records, into)
|
|
1788
|
-
return into
|
|
1789
|
-
|
|
1790
|
-
def merge(records:list, into:Record=None, inplace=False)->Record:
|
|
1791
|
-
"""Merge a list of records into a single new record.
|
|
1792
|
-
|
|
1793
|
-
Args:
|
|
1794
|
-
records (list): list of Records of the same type
|
|
1795
|
-
into (Record, optional): location for the merged series. If None is provided, the merged series is created in the parent of the first record in the list. Defaults to None.
|
|
1796
|
-
inplace (bool, optional): If set to True, the original series will be removed and only the merged series retain. If set to False the original series will contine to exist. Default is False.
|
|
1797
|
-
|
|
1798
|
-
Returns:
|
|
1799
|
-
new_record (Record): the merged record.
|
|
1800
|
-
|
|
1801
|
-
See also:
|
|
1802
|
-
`copy`
|
|
1803
|
-
`copy_to`
|
|
1804
|
-
|
|
1805
|
-
Example:
|
|
1806
|
-
|
|
1807
|
-
The first patient in the hollywood demo database currently has two studies
|
|
1808
|
-
|
|
1809
|
-
>>> database = db.dro.database_hollywood()
|
|
1810
|
-
>>> jb = database.patients(PatientName = 'James Bond')[0]
|
|
1811
|
-
>>> len(jb.studies())
|
|
1812
|
-
2
|
|
1813
|
-
|
|
1814
|
-
If we merge them together, the patient now has three studies, the original MRI and Xray studies, and the new merged study:
|
|
1815
|
-
|
|
1816
|
-
>>> new_study = db.merge(jb.studies())
|
|
1817
|
-
>>> len(jb.studies())
|
|
1818
|
-
3
|
|
1819
|
-
>>> jb.StudyDescription
|
|
1820
|
-
['MRI', 'New Study', 'Xray']
|
|
1821
|
-
|
|
1822
|
-
Since the original MRI and Xray studies had two series each, the new study now has 2+2=4 series:
|
|
1823
|
-
|
|
1824
|
-
>>> len(new_study.series())
|
|
1825
|
-
4
|
|
1826
|
-
|
|
1827
|
-
We have used here the default setting of ``inplace=False``, so the original series are preserved. To see what happens with ``inplace=True``, lets merge all 3 studies of the patient:
|
|
1828
|
-
|
|
1829
|
-
>>> single_jb_study = db.merge(jb.studies(), inplace=True)
|
|
1830
|
-
|
|
1831
|
-
Since we have merged in place, the original 3 studies have been removed and there is now only one study left.
|
|
1832
|
-
|
|
1833
|
-
>>> len(jb.studies())
|
|
1834
|
-
1
|
|
1835
|
-
|
|
1836
|
-
The new study now groups the 8 series that were in the original 3 studies:
|
|
1837
|
-
|
|
1838
|
-
>>> len(single_jb_study.series())
|
|
1839
|
-
8
|
|
1840
|
-
"""
|
|
1841
|
-
if not isinstance(records, list):
|
|
1842
|
-
records = [records]
|
|
1843
|
-
children = []
|
|
1844
|
-
for record in records:
|
|
1845
|
-
children += record.children()
|
|
1846
|
-
new_record = group(children, into=into, inplace=inplace)
|
|
1847
|
-
if inplace:
|
|
1848
|
-
for record in records:
|
|
1849
|
-
record.remove()
|
|
1850
|
-
return new_record
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
#
|
|
1856
|
-
# Read and write
|
|
1857
|
-
#
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
def read_dataframe(record, tags, select={}, **filters):
|
|
1862
|
-
if set(tags) <= set(record.manager.columns):
|
|
1863
|
-
df = record.register()[tags]
|
|
1864
|
-
filters = {**select, **filters}
|
|
1865
|
-
for f in filters:
|
|
1866
|
-
if f in df:
|
|
1867
|
-
if isinstance(filters[f], np.ndarray):
|
|
1868
|
-
df = df[df[f].isin(filters[f])]
|
|
1869
|
-
else:
|
|
1870
|
-
df = df[df[f] == filters[f]]
|
|
1871
|
-
return df
|
|
1872
|
-
instances = record.instances(select=select, **filters)
|
|
1873
|
-
return _read_dataframe_from_instance_array_values(instances, tags)
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
def read_dataframe_from_instance_array(instances, tags):
|
|
1877
|
-
mgr = instances[0].manager
|
|
1878
|
-
if set(tags) <= set(mgr.columns):
|
|
1879
|
-
keys = [i.key() for _, i in np.ndenumerate(instances)]
|
|
1880
|
-
return mgr._extract(keys)[tags]
|
|
1881
|
-
return _read_dataframe_from_instance_array_values(instances, tags)
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
def _read_dataframe_from_instance_array_values(instances, tags):
|
|
1885
|
-
indices = []
|
|
1886
|
-
data = []
|
|
1887
|
-
for i, instance in enumerate(instances):
|
|
1888
|
-
index = instance.key()
|
|
1889
|
-
values = instance.get_values(tags)
|
|
1890
|
-
indices.append(index)
|
|
1891
|
-
data.append(values)
|
|
1892
|
-
instance.progress(i+1, len(instances), 'Reading dataframe..')
|
|
1893
|
-
return pd.DataFrame(data, index=indices, columns=tags)
|