dbdicom 0.2.5__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dbdicom might be problematic. Click here for more details.

Files changed (52) hide show
  1. dbdicom/__init__.py +1 -28
  2. dbdicom/api.py +267 -0
  3. dbdicom/const.py +144 -0
  4. dbdicom/dataset.py +752 -0
  5. dbdicom/dbd.py +719 -0
  6. dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
  7. dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
  8. dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
  9. dbdicom/register.py +527 -0
  10. dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
  11. dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +153 -26
  12. dbdicom/{ds/types → sop_classes}/mr_image.py +185 -140
  13. dbdicom/sop_classes/parametric_map.py +307 -0
  14. dbdicom/sop_classes/secondary_capture.py +140 -0
  15. dbdicom/sop_classes/segmentation.py +311 -0
  16. dbdicom/{ds/types → sop_classes}/ultrasound_multiframe_image.py +1 -15
  17. dbdicom/{ds/types → sop_classes}/xray_angiographic_image.py +2 -17
  18. dbdicom/utils/arrays.py +36 -0
  19. dbdicom/utils/files.py +0 -20
  20. dbdicom/utils/image.py +10 -629
  21. dbdicom-0.3.0.dist-info/METADATA +28 -0
  22. dbdicom-0.3.0.dist-info/RECORD +53 -0
  23. {dbdicom-0.2.5.dist-info → dbdicom-0.3.0.dist-info}/WHEEL +1 -1
  24. dbdicom/create.py +0 -457
  25. dbdicom/dro.py +0 -174
  26. dbdicom/ds/__init__.py +0 -10
  27. dbdicom/ds/create.py +0 -63
  28. dbdicom/ds/dataset.py +0 -869
  29. dbdicom/ds/dictionaries.py +0 -620
  30. dbdicom/ds/types/parametric_map.py +0 -226
  31. dbdicom/extensions/__init__.py +0 -9
  32. dbdicom/extensions/dipy.py +0 -448
  33. dbdicom/extensions/elastix.py +0 -503
  34. dbdicom/extensions/matplotlib.py +0 -107
  35. dbdicom/extensions/numpy.py +0 -271
  36. dbdicom/extensions/scipy.py +0 -1512
  37. dbdicom/extensions/skimage.py +0 -1030
  38. dbdicom/extensions/sklearn.py +0 -243
  39. dbdicom/extensions/vreg.py +0 -1390
  40. dbdicom/manager.py +0 -2132
  41. dbdicom/message.py +0 -119
  42. dbdicom/pipelines.py +0 -66
  43. dbdicom/record.py +0 -1893
  44. dbdicom/types/database.py +0 -107
  45. dbdicom/types/instance.py +0 -231
  46. dbdicom/types/patient.py +0 -40
  47. dbdicom/types/series.py +0 -2874
  48. dbdicom/types/study.py +0 -58
  49. dbdicom-0.2.5.dist-info/METADATA +0 -71
  50. dbdicom-0.2.5.dist-info/RECORD +0 -66
  51. {dbdicom-0.2.5.dist-info → dbdicom-0.3.0.dist-info/licenses}/LICENSE +0 -0
  52. {dbdicom-0.2.5.dist-info → dbdicom-0.3.0.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)