dbdicom 0.2.6__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/create.py +0 -457
  24. dbdicom/dro.py +0 -174
  25. dbdicom/ds/__init__.py +0 -10
  26. dbdicom/ds/create.py +0 -63
  27. dbdicom/ds/dataset.py +0 -869
  28. dbdicom/ds/dictionaries.py +0 -620
  29. dbdicom/ds/types/parametric_map.py +0 -226
  30. dbdicom/extensions/__init__.py +0 -9
  31. dbdicom/extensions/dipy.py +0 -448
  32. dbdicom/extensions/elastix.py +0 -503
  33. dbdicom/extensions/matplotlib.py +0 -107
  34. dbdicom/extensions/numpy.py +0 -271
  35. dbdicom/extensions/scipy.py +0 -1512
  36. dbdicom/extensions/skimage.py +0 -1030
  37. dbdicom/extensions/sklearn.py +0 -243
  38. dbdicom/extensions/vreg.py +0 -1390
  39. dbdicom/manager.py +0 -2132
  40. dbdicom/message.py +0 -119
  41. dbdicom/pipelines.py +0 -66
  42. dbdicom/record.py +0 -1893
  43. dbdicom/types/database.py +0 -107
  44. dbdicom/types/instance.py +0 -231
  45. dbdicom/types/patient.py +0 -40
  46. dbdicom/types/series.py +0 -2874
  47. dbdicom/types/study.py +0 -58
  48. dbdicom-0.2.6.dist-info/METADATA +0 -72
  49. dbdicom-0.2.6.dist-info/RECORD +0 -66
  50. {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/WHEEL +0 -0
  51. {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/licenses/LICENSE +0 -0
  52. {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/top_level.txt +0 -0
@@ -1,1030 +0,0 @@
1
- import numpy as np
2
- import pandas as pd
3
- import scipy.ndimage as ndi
4
- import skimage
5
-
6
- from dbdicom.extensions import scipy
7
- from dbdicom.utils.image import interpolate3d_isotropic
8
-
9
-
10
- def volume_features(series):
11
-
12
- if isinstance(series, list):
13
- df = None
14
- for sery in series:
15
- df_sery = volume_features(sery)
16
- if df is None:
17
- df = df_sery
18
- else:
19
- df = pd.concat([df, df_sery], ignore_index=True)
20
- return df
21
-
22
- n_steps = 8
23
- step = 0
24
-
25
- step+=1
26
- series.status.progress(step, n_steps, 'Reading affine matrix..')
27
-
28
- affine = series.affine_matrix()
29
- if isinstance(affine, list):
30
- series.dialog.information('This series contains multiple orientations')
31
- return
32
- else:
33
- affine = affine[0]
34
-
35
- step+=1
36
- series.status.progress(step, n_steps, 'Reading array..')
37
-
38
- # Get array sorted by slice location
39
- arr, _ = series.array('SliceLocation', pixels_first=True)
40
-
41
- # If there are multiple volumes, show only the first one
42
- arr = arr[...,0]
43
-
44
- series_props = _volume_features(arr, affine=affine, show_progress=series.status.progress)
45
-
46
- instance = series.instance()
47
- columns = ['PatientID', 'StudyDescription', 'SeriesDescription', 'Parameter', 'Value', 'Unit']
48
- ids = [instance.PatientID, instance.StudyDescription, instance.SeriesDescription]
49
- data = []
50
- for par, val in series_props.items():
51
- row = ids + [par, val[0], val[1]]
52
- data.append(row)
53
- return pd.DataFrame(data, columns = columns)
54
-
55
-
56
- def _volume_features(arr, spacing=[1,1,1], affine=None, show_progress=print):
57
- """Calculate shape features from a mask array
58
-
59
- This function calculates various shape features from a given mask array.
60
-
61
- Arguments:
62
- - arr (numpy.ndarray): The input mask array - 3D but does not have to be binary
63
- - spacing (list, optional): The voxel dimensions in mm. Default is [1, 1, 1].
64
- - affine (numpy.ndarray, optional): The affine transformation matrix. Default is None. If the affine is
65
- provided then the spacing argument is ignored and spacing is derived from the affine.
66
- - show_progress (function, optional): A function to display progress messages. Default is print.
67
-
68
- Returns:
69
- - dict: A dictionary containing the calculated shape features with their corresponding units.
70
- """
71
-
72
- show_progress(1, 6, 'Preprocessing mask...')
73
-
74
- # Scale array in the range [0,1] so it can be treated as mask
75
- # Motivation: the function is intended for mask arrays but this will make
76
- # sure the results are meaningful even if non-binary arrays are provided.
77
- max = np.amax(arr)
78
- min = np.amin(arr)
79
- arr -= min
80
- arr /= max-min
81
- # Add zeropadding at the boundary slices for masks that extend to the edge
82
- # Motivation: this could have some effect if surfaces are extracted - could create issues
83
- # if the values extend right up to the boundary of the slab.
84
- shape = list(arr.shape)
85
- shape[-1] = shape[-1] + 2*4
86
- array = np.zeros(shape)
87
- array[:,:,4:-4] = arr
88
-
89
- show_progress(2, 6, 'Extracting surface...')
90
-
91
- # Get voxel dimensions from the affine
92
- # We are assuming here the voxel dimensions are in mm.
93
- # If not the units provided with the return values are incorrect.
94
- if affine is not None:
95
- column_spacing = np.linalg.norm(affine[:3, 0])
96
- row_spacing = np.linalg.norm(affine[:3, 1])
97
- slice_spacing = np.linalg.norm(affine[:3, 2])
98
- spacing = (column_spacing, row_spacing, slice_spacing)
99
- voxel_volume = spacing[0]*spacing[1]*spacing[2]
100
- nr_of_voxels = np.count_nonzero(array > 0.5)
101
- volume = nr_of_voxels * voxel_volume
102
- # Surface properties - for now only extracting surface area
103
- # Note: this is smoothing the surface first - not tested in depth whether this is necessary or helpful.
104
- # It does appear to make a big difference on surface area so should be looked at more carefully.
105
- smooth_array = ndi.gaussian_filter(array, 1.0)
106
- verts, faces, _, _ = skimage.measure.marching_cubes(smooth_array, spacing=spacing, level=0.5, step_size=1.0)
107
- surface_area = skimage.measure.mesh_surface_area(verts, faces)
108
-
109
- show_progress(3, 6, 'Interpolating to isotropic...')
110
-
111
- # Interpolate to isotropic for non-isotropic voxels
112
- # Motivation: this is required by the region_props function
113
- spacing = np.array(spacing)
114
- if np.amin(spacing) != np.amax(spacing):
115
- array, isotropic_spacing = interpolate3d_isotropic(array, spacing)
116
- isotropic_voxel_volume = isotropic_spacing**3
117
- else:
118
- isotropic_spacing = np.mean(spacing)
119
- isotropic_voxel_volume = voxel_volume
120
-
121
- show_progress(4, 6, 'Extracting volume properties...')
122
-
123
- # Get volume properties - mostly from region_props, except for compactness and depth
124
- array = np.round(array).astype(np.int16)
125
- region_props_3D = skimage.measure.regionprops(array)[0]
126
- # Calculate 'compactness' (our definition) - define as volume to surface ratio
127
- # expressed as a percentage of the volume-to-surface ration of an equivalent sphere.
128
- # The sphere is the most compact of all shapes, i.e. it has the largest volume to surface area ratio,
129
- # so this is guaranteed to be between 0 and 100%
130
- radius = region_props_3D['equivalent_diameter_area']*isotropic_spacing/2 # mm
131
- v2s = volume/surface_area # mm
132
- v2s_equivalent_sphere = radius/3 # mm
133
- compactness = 100 * v2s/v2s_equivalent_sphere # %
134
- # Fractional anisotropy - in analogy with FA in diffusion
135
- m0 = region_props_3D['inertia_tensor_eigvals'][0]
136
- m1 = region_props_3D['inertia_tensor_eigvals'][1]
137
- m2 = region_props_3D['inertia_tensor_eigvals'][2]
138
- m = (m0 + m1 + m2)/3 # average moment of inertia (trace of the inertia tensor)
139
- FA = np.sqrt(3/2) * np.sqrt((m0-m)**2 + (m1-m)**2 + (m2-m)**2) / np.sqrt(m0**2 + m1**2 + m2**2)
140
-
141
- show_progress(5, 6, 'Calculating depth...')
142
-
143
- # Measure maximum depth (our definition)
144
- distance = ndi.distance_transform_edt(array)
145
- max_depth = np.amax(distance)
146
-
147
- show_progress(6, 6, 'Creating output...')
148
-
149
- # Summarise all values with human-readable names and proper units in a dictionary with values and units.
150
- # Some of the definitions are rephrased or tweaked for more intuitive interpretation.
151
- # The volume can be calculated independently from regionprops - included as sanity check.
152
- series_props = {
153
- 'Surface area': (surface_area/100, 'cm^2'),
154
- 'Volume': (volume/1000, 'mL'),
155
- 'Bounding box volume': (region_props_3D['area_bbox']*isotropic_voxel_volume/1000, 'mL'),
156
- 'Convex hull volume': (region_props_3D['area_convex']*isotropic_voxel_volume/1000, 'mL'),
157
- 'Volume of holes': ((region_props_3D['area_filled']-region_props_3D['area'])*isotropic_voxel_volume/1000, 'mL'),
158
- 'Extent': (region_props_3D['extent']*100, '%'), # Percentage of bounding box filled
159
- 'Solidity': (region_props_3D['solidity']*100, '%'), # Percentage of convex hull filled
160
- 'Compactness': (compactness, '%'),
161
- 'Long axis length': (region_props_3D['axis_major_length']*isotropic_spacing/10, 'cm'),
162
- 'Short axis length': (region_props_3D['axis_minor_length']*isotropic_spacing/10, 'cm'),
163
- 'Equivalent diameter': (region_props_3D['equivalent_diameter_area']*isotropic_spacing/10, 'cm'),
164
- 'Longest caliper diameter': (region_props_3D['feret_diameter_max']*isotropic_spacing/10, 'cm'),
165
- 'Maximum depth': (max_depth*isotropic_spacing/10, 'cm'),
166
- 'Primary moment of inertia': (region_props_3D['inertia_tensor_eigvals'][0]*isotropic_spacing**2/100, 'cm^2'),
167
- 'Second moment of inertia': (region_props_3D['inertia_tensor_eigvals'][1]*isotropic_spacing**2/100, 'cm^2'),
168
- 'Third moment of inertia': (region_props_3D['inertia_tensor_eigvals'][2]*isotropic_spacing**2/100, 'cm^2'),
169
- 'Mean moment of inertia': (m*isotropic_spacing**2/100, 'cm^2'),
170
- 'Fractional anisotropy of inertia': (100*FA, '%'),
171
- 'QC - Volume check': (region_props_3D['area']*isotropic_voxel_volume/1000, 'mL'),
172
- # From eigenvectors of inertia tensor:
173
- # Include orientation info with respect to LPH coordinate system (tilt, roll)
174
- }
175
-
176
- return series_props
177
-
178
-
179
-
180
- def area_opening_2d(input, **kwargs):
181
- """
182
- Return grayscale area opening of an image.
183
-
184
- Wrapper for skimage.morphology.area_opening.
185
-
186
- Parameters
187
- ----------
188
- input: dbdicom series
189
-
190
- Returns
191
- -------
192
- output : dbdicom series
193
- """
194
- desc = input.instance().SeriesDescription + ' [area opening 2D]'
195
- result = input.copy(SeriesDescription = desc)
196
- images = result.images()
197
- for i, image in enumerate(images):
198
- input.status.progress(i+1, len(images), 'Calculating ' + desc)
199
- image.read()
200
- array = image.array()
201
- array = skimage.morphology.area_opening(array, **kwargs)
202
- image.set_array(array)
203
- _reset_window(image, array)
204
- image.clear()
205
- input.status.hide()
206
- return result
207
-
208
-
209
- def area_opening_3d(input, **kwargs):
210
- """
211
- Return grayscale area opening of an image.
212
-
213
- Wrapper for skimage.morphology.area_opening.
214
-
215
- Parameters
216
- ----------
217
- input: dbdicom series
218
-
219
- Returns
220
- -------
221
- output : dbdicom series
222
- """
223
- array, headers = input.array('SliceLocation', pixels_first=True)
224
- if array is None:
225
- return input
226
- desc = input.instance().SeriesDescription + ' [area opening 3D]'
227
- result = input.new_sibling(SeriesDescription = desc)
228
- for t in range(array.shape[3]):
229
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
230
- array[...,t] = skimage.morphology.area_opening(array[...,t], **kwargs)
231
- result.set_array(array[...,t], headers[:,t], pixels_first=True)
232
- _reset_window(result, array)
233
- input.status.hide()
234
- return result
235
-
236
-
237
- def area_closing_2d(input, **kwargs):
238
- """
239
- Return grayscale area closing of an image.
240
-
241
- Wrapper for skimage.morphology.area_closing.
242
-
243
- Parameters
244
- ----------
245
- input: dbdicom series
246
-
247
- Returns
248
- -------
249
- output : dbdicom series
250
- """
251
- desc = input.instance().SeriesDescription + ' [area closing 2D]'
252
- result = input.copy(SeriesDescription = desc)
253
- images = result.images()
254
- for i, image in enumerate(images):
255
- input.status.progress(i+1, len(images), 'Calculating ' + desc)
256
- image.read()
257
- array = image.array()
258
- array = skimage.morphology.area_closing(array, **kwargs)
259
- image.set_array(array)
260
- _reset_window(image, array)
261
- image.clear()
262
- input.status.hide()
263
- return result
264
-
265
-
266
- def area_closing_3d(input, **kwargs):
267
- """
268
- Return grayscale area closing of an image.
269
-
270
- Wrapper for skimage.morphology.area_closing.
271
-
272
- Parameters
273
- ----------
274
- input: dbdicom series
275
-
276
- Returns
277
- -------
278
- output : dbdicom series
279
- """
280
- array, headers = input.array('SliceLocation', pixels_first=True)
281
- if array is None:
282
- return input
283
- desc = input.instance().SeriesDescription + ' [area closing 3D]'
284
- result = input.new_sibling(SeriesDescription = desc)
285
- for t in range(array.shape[3]):
286
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
287
- array[...,t] = skimage.morphology.area_closing(array[...,t], **kwargs)
288
- result.set_array(array[...,t], headers[:,t], pixels_first=True)
289
- _reset_window(result, array)
290
- input.status.hide()
291
- return result
292
-
293
-
294
- def opening_2d(input, **kwargs):
295
- """
296
- Return grayscale morphological opening of an image.
297
-
298
- Wrapper for skimage.morphology.opening.
299
-
300
- Parameters
301
- ----------
302
- input: dbdicom series
303
-
304
- Returns
305
- -------
306
- output : dbdicom series
307
- """
308
- desc = input.instance().SeriesDescription + ' [opening 2D]'
309
- result = input.copy(SeriesDescription = desc)
310
- images = result.images()
311
- for i, image in enumerate(images):
312
- input.status.progress(i+1, len(images), 'Calculating ' + desc)
313
- image.read()
314
- array = image.array()
315
- array = skimage.morphology.opening(array, **kwargs)
316
- image.set_array(array)
317
- _reset_window(image, array)
318
- image.clear()
319
- input.status.hide()
320
- return result
321
-
322
-
323
- def opening_3d(input, **kwargs):
324
- """
325
- Return grayscale morphological opening of an image.
326
-
327
- Wrapper for skimage.morphology.opening.
328
-
329
- Parameters
330
- ----------
331
- input: dbdicom series
332
-
333
- Returns
334
- -------
335
- output : dbdicom series
336
- """
337
- array, headers = input.array('SliceLocation', pixels_first=True)
338
- if array is None:
339
- return input
340
- desc = input.instance().SeriesDescription + ' [opening 3D]'
341
- result = input.new_sibling(SeriesDescription = desc)
342
- for t in range(array.shape[3]):
343
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
344
- array[...,t] = skimage.morphology.opening(array[...,t], **kwargs)
345
- result.set_array(array[...,t], headers[:,t], pixels_first=True)
346
- _reset_window(result, array)
347
- input.status.hide()
348
- return result
349
-
350
-
351
- def closing_2d(input, **kwargs):
352
- """
353
- Return grayscale morphological closing of an image.
354
-
355
- Wrapper for skimage.morphology.closing.
356
-
357
- Parameters
358
- ----------
359
- input: dbdicom series
360
-
361
- Returns
362
- -------
363
- output : dbdicom series
364
- """
365
- desc = input.instance().SeriesDescription + ' [closing 2D]'
366
- result = input.copy(SeriesDescription = desc)
367
- images = result.images()
368
- for i, image in enumerate(images):
369
- input.status.progress(i+1, len(images), 'Calculating ' + desc)
370
- image.read()
371
- array = image.array()
372
- array = skimage.morphology.closing(array, **kwargs)
373
- image.set_array(array)
374
- _reset_window(image, array)
375
- image.clear()
376
- input.status.hide()
377
- return result
378
-
379
-
380
- def closing_3d(input, **kwargs):
381
- """
382
- Return grayscale morphological closing of an image.
383
-
384
- Wrapper for skimage.morphology.closing.
385
-
386
- Parameters
387
- ----------
388
- input: dbdicom series
389
-
390
- Returns
391
- -------
392
- output : dbdicom series
393
- """
394
- array, headers = input.array('SliceLocation', pixels_first=True)
395
- if array is None:
396
- return input
397
- desc = input.instance().SeriesDescription + ' [closing 3D]'
398
- result = input.new_sibling(SeriesDescription = desc)
399
- for t in range(array.shape[3]):
400
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
401
- array[...,t] = skimage.morphology.closing(array[...,t], **kwargs)
402
- result.set_array(array[...,t], headers[:,t], pixels_first=True)
403
- _reset_window(result, array)
404
- input.status.hide()
405
- return result
406
-
407
-
408
- def remove_small_holes_2d(input, **kwargs):
409
- """
410
- Remove contiguous holes smaller than the specified size.
411
-
412
- Wrapper for skimage.morphology.remove_small_holes.
413
-
414
- Parameters
415
- ----------
416
- input: dbdicom series
417
-
418
- Returns
419
- -------
420
- output : dbdicom series
421
- """
422
- desc = input.instance().SeriesDescription + ' [remove small holes 2D]'
423
- result = input.copy(SeriesDescription = desc)
424
- images = result.images()
425
- for i, image in enumerate(images):
426
- input.status.progress(i+1, len(images), 'Calculating ' + desc)
427
- image.read()
428
- array = image.array().astype(np.int16)
429
- array = skimage.morphology.remove_small_holes(array, **kwargs)
430
- image.set_array(array)
431
- _reset_window(image, array)
432
- image.clear()
433
- input.status.hide()
434
- return result
435
-
436
-
437
- def remove_small_holes_3d(input, **kwargs):
438
- """
439
- Remove contiguous holes smaller than the specified size.
440
-
441
- Wrapper for skimage.morphology.remove_small_holes.
442
-
443
- Parameters
444
- ----------
445
- input: dbdicom series
446
-
447
- Returns
448
- -------
449
- output : dbdicom series
450
- """
451
- array, headers = input.array('SliceLocation', pixels_first=True)
452
- if array is None:
453
- return input
454
- else:
455
- array = array.astype(np.int16)
456
- desc = input.instance().SeriesDescription + ' [remove holes 3D]'
457
- result = input.new_sibling(SeriesDescription = desc)
458
- for t in range(array.shape[3]):
459
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
460
- array[...,t] = skimage.morphology.remove_small_holes(array[...,t], **kwargs)
461
- result.set_array(array[...,t], headers[:,t], pixels_first=True)
462
- _reset_window(result, array)
463
- input.status.hide()
464
- return result
465
-
466
-
467
- def watershed_2d(input, markers=None, mask=None, **kwargs):
468
- """
469
- Labels structures in an image
470
-
471
- Wrapper for skimage.segmentation.watershed function.
472
-
473
- Parameters
474
- ----------
475
- input: dbdicom series
476
- markers: dbdicom series of the same dimensions as series
477
-
478
- Returns
479
- -------
480
- filtered : dbdicom series
481
- """
482
- desc = input.instance().SeriesDescription + ' [watershed 2D]'
483
- result = input.copy(SeriesDescription = desc)
484
- sortby = ['SliceLocation', 'AcquisitionTime']
485
- images = result.images(sortby=sortby)
486
- if markers is not None:
487
- markers = scipy.map_to(markers, input, label=True)
488
- markers = markers.images(sortby=sortby)
489
- if mask is not None:
490
- mask = scipy.map_to(mask, input, mask=True)
491
- mask = mask.images(sortby=sortby)
492
- for i, image in enumerate(images):
493
- input.status.progress(i+1, len(images), 'Calculating ' + desc)
494
- image.read()
495
- array = image.array()
496
- if markers is None:
497
- mrk = None
498
- else:
499
- mrk = markers[i].array().astype(np.uint)
500
- if mask is None:
501
- msk = None
502
- else:
503
- msk = mask[i].array().astype(np.bool8)
504
- array = skimage.segmentation.watershed(array, markers=mrk, mask=msk, **kwargs)
505
- array.astype(np.float32) # unnecessary - test
506
- image.set_array(array)
507
- _reset_window(image, array)
508
- image.clear()
509
- input.status.hide()
510
- return result
511
-
512
-
513
- def watershed_3d(input, markers=None, mask=None, **kwargs):
514
- """
515
- Determine watershed in 3D
516
-
517
- Wrapper for skimage.segmentation.watershed function.
518
-
519
- Parameters
520
- ----------
521
- input: dbdicom series
522
-
523
- Returns
524
- -------
525
- filtered : dbdicom series
526
- """
527
- array, headers = input.array('SliceLocation', pixels_first=True)
528
- if array is None:
529
- return input
530
- if markers is not None:
531
- markers = scipy.map_to(markers, input)
532
- markers, _ = markers.array('SliceLocation', pixels_first=True)
533
- markers = markers.astype(np.uint)
534
- if mask is not None:
535
- mask = scipy.map_to(mask, input)
536
- mask, _ = mask.array('SliceLocation', pixels_first=True)
537
- mask = mask.astype(np.bool8)
538
- desc = input.instance().SeriesDescription + ' [watershed 3D]'
539
- result = input.new_sibling(SeriesDescription = desc)
540
- for t in range(array.shape[3]):
541
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
542
- array[...,t] = skimage.segmentation.watershed(
543
- array[...,t],
544
- markers = None if markers is None else markers[...,t],
545
- mask = None if mask is None else mask[...,t],
546
- **kwargs)
547
- result.set_array(array[...,t], headers[:,t], pixels_first=True)
548
- _reset_window(result, array)
549
- input.status.hide()
550
- return result
551
-
552
-
553
- def skeletonize(input, **kwargs):
554
- """
555
- Labels structures in an image
556
-
557
- Wrapper for skimage.segmentation.watershed function.
558
-
559
- Parameters
560
- ----------
561
- input: dbdicom series
562
- markers: dbdicom series of the same dimensions as series
563
-
564
- Returns
565
- -------
566
- filtered : dbdicom series
567
- """
568
- desc = input.instance().SeriesDescription + ' [2d skeleton]'
569
- filtered = input.copy(SeriesDescription = desc)
570
- #images = filtered.instances() #sort=False should be faster - check
571
- images = filtered.images()
572
- for i, image in enumerate(images):
573
- input.status.progress(i+1, len(images), 'Calculating ' + desc)
574
- image.read()
575
- array = image.array()
576
- array = skimage.morphology.skeletonize(array, **kwargs)
577
- #array.astype(np.float32)
578
- image.set_array(array)
579
- _reset_window(image, array)
580
- image.clear()
581
- input.status.hide()
582
- return filtered
583
-
584
-
585
- def skeletonize_3d(input, **kwargs):
586
- """
587
- Labels structures in an image
588
-
589
- Wrapper for skimage.segmentation.watershed function.
590
-
591
- Parameters
592
- ----------
593
- input: dbdicom series
594
- markers: dbdicom series of the same dimensions as series
595
-
596
- Returns
597
- -------
598
- filtered : dbdicom series
599
- """
600
- desc = input.instance().SeriesDescription + ' [skeleton 3D]'
601
- filtered = input.copy(SeriesDescription = desc)
602
- array, headers = filtered.array('SliceLocation', pixels_first=True)
603
- if array is None:
604
- return filtered
605
- for t in range(array.shape[3]):
606
- if array.shape[3] > 1:
607
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
608
- else:
609
- input.status.message('Calculating ' + desc + '. Please bear with me..')
610
- array[:,:,:,t] = skimage.morphology.skeletonize_3d(array[:,:,:,t], **kwargs)
611
- filtered.set_array(array, headers[:,t], pixels_first=True)
612
- _reset_window(filtered, array)
613
- input.status.hide()
614
- return filtered
615
-
616
-
617
- def peak_local_max_3d(input, labels=None, **kwargs):
618
- """
619
- Determine local maxima
620
-
621
- Wrapper for skimage.feature.peak_local_max function.
622
- # https://scikit-image.org/docs/stable/api/skimage.feature.html#skimage.feature.peak_local_max
623
-
624
- Parameters
625
- ----------
626
- input: dbdicom series
627
-
628
- Returns
629
- -------
630
- filtered : dbdicom series
631
- """
632
- array, headers = input.array('SliceLocation', pixels_first=True)
633
- if array is None:
634
- return input
635
- if labels is not None:
636
- labels = scipy.map_to(labels, input)
637
- labels_array, _ = labels.array('SliceLocation', pixels_first=True)
638
- desc = input.instance().SeriesDescription + ' [peak local max 3D]'
639
- filtered = input.new_sibling(SeriesDescription = desc)
640
- for t in range(array.shape[3]):
641
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
642
- if labels is None:
643
- labels_t = None
644
- else:
645
- labels_t = labels_array[:,:,:,t].astype(np.int16)
646
- coords = skimage.feature.peak_local_max(array[:,:,:,t], labels=labels_t, **kwargs)
647
- mask = np.zeros(array.shape[:3], dtype=bool)
648
- mask[tuple(coords.T)] = True
649
- filtered.set_array(mask, headers[:,t], pixels_first=True)
650
- _reset_window(filtered, array)
651
- input.status.hide()
652
- return filtered
653
-
654
-
655
- def canny(input, sigma=1.0, **kwargs):
656
- """
657
- wrapper for skimage.feature.canny
658
- # https://scikit-image.org/docs/dev/api/skimage.feature.html#skimage.feature.canny
659
-
660
- Parameters
661
- ----------
662
- input: dbdicom series
663
-
664
- Returns
665
- -------
666
- filtered : dbdicom series
667
- """
668
- suffix = ' [Canny filter x ' + str(sigma) + ' ]'
669
- desc = input.instance().SeriesDescription
670
- filtered = input.copy(SeriesDescription = desc+suffix)
671
- #images = filtered.instances()
672
- images = filtered.images()
673
- for i, image in enumerate(images):
674
- input.status.progress(i+1, len(images), 'Filtering ' + desc)
675
- image.read()
676
- array = image.array()
677
- array = skimage.feature.canny(array, sigma=sigma, **kwargs)
678
- image.set_array(array)
679
- array = array.astype(np.ubyte)
680
- _reset_window(image, array)
681
- image.clear()
682
- input.status.hide()
683
- return filtered
684
-
685
-
686
- def convex_hull_image(series, **kwargs):
687
- """
688
- wrapper for skimage.morphology.convex_hull_image
689
-
690
- Parameters
691
- ----------
692
- input: dbdicom series
693
-
694
- Returns
695
- -------
696
- filtered : dbdicom series
697
- """
698
- suffix = ' [Convex hull 2D]'
699
- desc = series.instance().SeriesDescription
700
- chull = series.copy(SeriesDescription = desc+suffix)
701
- images = chull.images()
702
- for i, image in enumerate(images):
703
- series.status.progress(i+1, len(images), 'Calculating convex hull for ' + desc)
704
- image.read()
705
- array = np.around(image.array())
706
- array = skimage.morphology.convex_hull_image(array, **kwargs)
707
- image.set_array(array)
708
- array = array.astype(np.ubyte)
709
- _reset_window(image, array)
710
- image.clear()
711
- series.status.hide()
712
- return chull
713
-
714
-
715
- def convex_hull_image_3d(input, **kwargs):
716
- """
717
- wrapper for skimage.morphology.convex_hull_image
718
-
719
- Parameters
720
- ----------
721
- input: dbdicom series
722
-
723
- Returns
724
- -------
725
- filtered : dbdicom series
726
- """
727
- array, headers = input.array('SliceLocation', pixels_first=True)
728
- if array is None:
729
- return input
730
- desc = input.instance().SeriesDescription + ' [Convex hull 3D]'
731
- result = input.new_sibling(SeriesDescription = desc)
732
- for t in range(array.shape[3]):
733
- input.status.progress(t, array.shape[3], 'Calculating ' + desc)
734
- volume = np.around(array[:,:,:,t])
735
- hull = skimage.morphology.convex_hull_image(volume, **kwargs)
736
- result.set_array(hull, headers[:,t], pixels_first=True)
737
- _reset_window(result, hull)
738
- input.status.hide()
739
- return result
740
-
741
-
742
- def coregister_2d_to_2d(moving, fixed, return_array=False, attachment=1):
743
- # https://scikit-image.org/docs/stable/api/skimage.registration.html#skimage.registration.optical_flow_tvl1
744
-
745
- #fixed = fixed.map_to(moving)
746
- fixed = scipy.map_to(fixed, moving)
747
-
748
- # Get arrays for fixed and moving series
749
- array_fixed, _ = fixed.array('SliceLocation', pixels_first=True)
750
- array_moving, headers = moving.array('SliceLocation', pixels_first=True)
751
- if array_fixed is None or array_moving is None:
752
- return fixed
753
- array_moving, headers, array_fixed = array_moving[...,0], headers[...,0], array_fixed[...,0]
754
-
755
- # Coregister fixed and moving slice-by-slice
756
- row_coords, col_coords = np.meshgrid(
757
- np.arange(array_moving.shape[0]),
758
- np.arange(array_moving.shape[1]),
759
- indexing='ij')
760
- deformation = np.empty(array_moving.shape + (2,))
761
- for z in range(array_moving.shape[2]):
762
- moving.status.progress(z+1, array_moving.shape[2], 'Performing coregistration..')
763
- image0 = array_fixed[:,:,z]
764
- image1 = array_moving[:,:,z]
765
- v, u = skimage.registration.optical_flow_tvl1(image0, image1, attachment=attachment)
766
- new_coords = np.array([row_coords + v, col_coords + u])
767
- array_moving[:,:,z] = skimage.transform.warp(image1, new_coords, mode='edge')
768
- deformation[:,:,z,:] = np.stack([v, u], axis=-1)
769
-
770
- # Return array or new series
771
- if return_array:
772
- moving.status.message('Finished coregistration..')
773
- return array_moving, deformation, headers
774
- else:
775
- moving.status.message('Writing coregistered series to database..')
776
- # Create new dicom series
777
- desc = moving.instance().SeriesDescription
778
- coreg = moving.new_sibling(SeriesDescription = desc + ' [coregistered]')
779
- deform = moving.new_sibling(SeriesDescription = desc + ' [deformation field]')
780
- # Set arrays of new series
781
- coreg.set_array(array_moving, headers, pixels_first=True)
782
- for dim in range(deformation.shape[-1]):
783
- deform.set_array(deformation[...,dim], headers, pixels_first=True)
784
- moving.status.message('Finished coregistration..')
785
- return coreg, deform
786
-
787
-
788
- def coregister_3d_to_3d(moving, fixed, return_array=False, attachment=1):
789
- # https://scikit-image.org/docs/stable/api/skimage.registration.html#skimage.registration.optical_flow_tvl1
790
-
791
- fixed = scipy.map_to(fixed, moving)
792
-
793
- # Get arrays for fixed and moving series
794
- array_fixed, _ = fixed.array('SliceLocation', pixels_first=True)
795
- array_moving, headers = moving.array('SliceLocation', pixels_first=True)
796
- if array_fixed is None or array_moving is None:
797
- return fixed
798
- array_moving, headers, array_fixed = array_moving[...,0], headers[...,0], array_fixed[...,0]
799
-
800
- moving.status.message('Performing coregistration. Please be patient. Its hard work and I need to concentrate..')
801
- # Coregister fixed and moving slice-by-slice
802
- row_coords, col_coords, slice_coords = np.meshgrid(
803
- np.arange(array_moving.shape[0]),
804
- np.arange(array_moving.shape[1]),
805
- np.arange(array_moving.shape[2]),
806
- indexing='ij')
807
- v, u, w = skimage.registration.optical_flow_tvl1(array_fixed, array_moving, attachment=attachment)
808
- new_coords = np.array([row_coords + v, col_coords + u, slice_coords + w])
809
- array_moving = skimage.transform.warp(array_moving, new_coords, mode='edge')
810
- deformation = np.stack([v, u, w], axis=-1)
811
-
812
- # Return array or new series
813
- if return_array:
814
- moving.status.message('Finished coregistration..')
815
- return array_moving, deformation, headers
816
- else:
817
- moving.status.message('Writing coregistered series to database..')
818
- # Create new dicom series
819
- desc = moving.instance().SeriesDescription
820
- coreg = moving.new_sibling(SeriesDescription = desc + ' [coregistered]')
821
- deform = moving.new_sibling(SeriesDescription = desc + ' [deformation field]')
822
- # Set arrays of new series
823
- coreg.set_array(array_moving, headers, pixels_first=True)
824
- for dim in range(deformation.shape[-1]):
825
- deform.set_array(deformation[...,dim], headers, pixels_first=True)
826
- moving.status.message('Finished coregistration..')
827
- return coreg, deform
828
-
829
-
830
- def warp(image, deformation_field):
831
-
832
- # Get arrays for fixed and moving series
833
- array, headers = image.array('SliceLocation', pixels_first=True, first_volume=True)
834
- array_deform, _ = deformation_field.array('SliceLocation', pixels_first=True, first_volume=True)
835
-
836
- # For the deformation field the last dimension is the components
837
- # For the image use only the first time point
838
- # array, headers = array[...,0], headers[...,0]
839
-
840
- # For this function, image and deformation field must be aligned
841
- if array.shape != array_deform.shape[:-1]:
842
- msg = 'The dimensions of image and deformation field are not matching up. \n'
843
- msg += 'Please select two series with matching dimensions.'
844
- raise ValueError(msg)
845
-
846
- # Warp the arrays
847
- if array_deform.shape[-1] == 3:
848
- x, y, z = np.meshgrid(
849
- np.arange(array.shape[0]),
850
- np.arange(array.shape[1]),
851
- np.arange(array.shape[2]),
852
- indexing='ij')
853
- v, u, w = array_deform[...,0], array_deform[...,1], array_deform[...,2]
854
- new_coords = np.array([x+v, y+u, z+w])
855
- array = skimage.transform.warp(array, new_coords, mode='edge')
856
- elif array_deform.shape[-1] == 2:
857
- x, y = np.meshgrid(
858
- np.arange(array.shape[0]),
859
- np.arange(array.shape[1]),
860
- indexing='ij')
861
- for z in range(array.shape[2]):
862
- image.status.progress(z+1, array.shape[2], 'Deforming slices..')
863
- v, u = array_deform[...,z,0], array_deform[...,z,1]
864
- new_coords = np.array([x+v, y+u])
865
- array[...,z] = skimage.transform.warp(array[...,z], new_coords, mode='edge')
866
- else:
867
- msg = 'The deformation field does not have the correct dimensions. \n'
868
- msg += 'This needs to have the either 2 or 3 images for each slice location.'
869
- raise ValueError(msg)
870
-
871
- # Create new dicom series
872
- warped = image.new_sibling(suffix='warped')
873
- warped.set_array(array, headers, pixels_first=True)
874
-
875
- return warped
876
-
877
-
878
-
879
- def mdreg_constant_3d(series, attachment=1, max_improvement=1, max_iter=5):
880
-
881
- # Get arrays for fixed and moving series
882
- array, headers = series.array(['SliceLocation','AcquisitionTime'], pixels_first=True)
883
- if array is None:
884
- return
885
- array, headers = array[...,0], headers[...,0]
886
-
887
- # Coregister fixed and moving slice-by-slice
888
- row_coords, col_coords, slice_coords = np.meshgrid(
889
- np.arange(array.shape[0]),
890
- np.arange(array.shape[1]),
891
- np.arange(array.shape[2]),
892
- indexing='ij')
893
- v, u, w = np.zeros(array.shape), np.zeros(array.shape), np.zeros(array.shape)
894
- coreg = array.copy()
895
- for it in range(max_iter):
896
- target = np.mean(coreg, axis=3) # constant model
897
- cnt=0
898
- improvement = 0 # pixel sizes
899
- for t in range(array.shape[3]):
900
- cnt+=1
901
- msg = 'Performing iteration ' + str(it) + ' < ' + str(max_iter)
902
- msg += ' (best improvement so far = ' + str(round(improvement,2)) + ' pixels)'
903
- series.status.progress(cnt, array.shape[3], msg)
904
- v_t, u_t, w_t = skimage.registration.optical_flow_tvl1(
905
- target,
906
- array[:,:,:,t],
907
- attachment=attachment)
908
- coreg[:,:,:,t] = skimage.transform.warp(
909
- array[:,:,:,t],
910
- np.array([row_coords + v_t, col_coords + u_t, slice_coords + w_t]),
911
- mode='edge')
912
- improvement_t = np.amax(np.sqrt(np.square(v_t-v[:,:,:,t]) + np.square(u_t-u[:,:,:,t]) + np.square(w_t-w[:,:,:,t])))
913
- if improvement_t > improvement:
914
- improvement = improvement_t
915
- v[:,:,:,t], u[:,:,:,t], w[:,:,:,t] = v_t, u_t, w_t
916
- if improvement < max_improvement:
917
- break
918
-
919
- series.status.message('Writing coregistered series to database..')
920
- desc = series.instance().SeriesDescription + ' [coregistered]'
921
- registered_series = series.new_sibling(SeriesDescription=desc)
922
- registered_series.set_array(coreg, headers, pixels_first=True)
923
- series.status.message('Finished coregistration..')
924
- return registered_series
925
-
926
-
927
-
928
- def coregister_series_2d_to_2d(series, attachment=1):
929
-
930
- # Get arrays for fixed and moving series
931
- array, headers = series.array(['SliceLocation','AcquisitionTime'], pixels_first=True)
932
- if array is None:
933
- return
934
- array, headers = array[:,:,:,:,0], headers[:,:,0]
935
-
936
-
937
- # Coregister fixed and moving slice-by-slice
938
- row_coords, col_coords = np.meshgrid(
939
- np.arange(array.shape[0]),
940
- np.arange(array.shape[1]),
941
- indexing='ij')
942
- target = np.mean(array, axis=3)
943
- cnt=0
944
- for t in range(array.shape[3]):
945
- for z in range(array.shape[2]):
946
- cnt+=1
947
- series.status.progress(cnt, array.shape[2]*array.shape[3], 'Performing coregistration..')
948
- fixed = target[:,:,z]
949
- moving = array[:,:,z,t]
950
- v, u = skimage.registration.optical_flow_tvl1(fixed, moving, attachment=attachment)
951
- array[:,:,z,t] = skimage.transform.warp(
952
- moving,
953
- np.array([row_coords + v, col_coords + u]),
954
- mode='edge')
955
-
956
- # Return array or new series
957
- series.status.message('Writing coregistered series to database..')
958
- desc = series.instance().SeriesDescription + ' [coregistered]'
959
- registered_series = series.new_sibling(SeriesDescription=desc)
960
- registered_series.set_array(array, headers, pixels_first=True)
961
- series.status.message('Finished coregistration..')
962
- return registered_series
963
-
964
-
965
- def mdreg_constant_2d(series, attachment=1, max_improvement=1, max_iter=5):
966
-
967
- # Get arrays for fixed and moving series
968
- array, headers = series.array(['SliceLocation','AcquisitionTime'], pixels_first=True)
969
- if array is None:
970
- return
971
- array, headers = array[:,:,:,:,0], headers[:,:,0]
972
-
973
-
974
- # Coregister fixed and moving slice-by-slice
975
- row_coords, col_coords = np.meshgrid(
976
- np.arange(array.shape[0]),
977
- np.arange(array.shape[1]),
978
- indexing='ij')
979
- v, u = np.zeros(array.shape), np.zeros(array.shape)
980
- coreg = array.copy()
981
- for it in range(max_iter):
982
- target = np.mean(coreg, axis=3) # constant model
983
- cnt=0
984
- improvement = 0 # pixel sizes
985
- for t in range(array.shape[3]):
986
- for z in range(array.shape[2]):
987
- cnt+=1
988
- msg = 'Performing iteration ' + str(it) + ' < ' + str(max_iter)
989
- msg += ' (best improvement so far = ' + str(round(improvement,2)) + ' pixels)'
990
- series.status.progress(cnt, array.shape[2]*array.shape[3], msg)
991
- v_zt, u_zt = skimage.registration.optical_flow_tvl1(
992
- target[:,:,z],
993
- array[:,:,z,t],
994
- attachment=attachment)
995
- coreg[:,:,z,t] = skimage.transform.warp(
996
- array[:,:,z,t],
997
- np.array([row_coords + v_zt, col_coords + u_zt]),
998
- mode='edge')
999
- improvement_zt = np.amax(np.sqrt(np.square(v_zt-v[:,:,z,t]) + np.square(u_zt-u[:,:,z,t])))
1000
- if improvement_zt > improvement:
1001
- improvement = improvement_zt
1002
- v[:,:,z,t], u[:,:,z,t] = v_zt, u_zt
1003
- if improvement < max_improvement:
1004
- break
1005
-
1006
- series.status.message('Writing coregistered series to database..')
1007
- desc = series.instance().SeriesDescription + ' [coregistered]'
1008
- registered_series = series.new_sibling(SeriesDescription=desc)
1009
- registered_series.set_array(coreg, headers, pixels_first=True)
1010
- series.status.message('Finished coregistration..')
1011
- return registered_series
1012
-
1013
-
1014
-
1015
-
1016
-
1017
- # Helper functions
1018
-
1019
- def _reset_window(image, array):
1020
- arr = array.astype(np.float32)
1021
- min = np.amin(arr)
1022
- max = np.amax(arr)
1023
- image.WindowCenter= (max+min)/2
1024
- if min==max:
1025
- if min == 0:
1026
- image.WindowWidth = 1
1027
- else:
1028
- image.WindowWidth = min
1029
- else:
1030
- image.WindowWidth = 0.9*(max-min)