dbdicom 0.2.6__py3-none-any.whl → 0.3.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (52) hide show
  1. dbdicom/__init__.py +1 -28
  2. dbdicom/api.py +287 -0
  3. dbdicom/const.py +144 -0
  4. dbdicom/dataset.py +721 -0
  5. dbdicom/dbd.py +736 -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 +310 -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.1.dist-info/METADATA +28 -0
  22. dbdicom-0.3.1.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.1.dist-info}/WHEEL +0 -0
  51. {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/licenses/LICENSE +0 -0
  52. {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/top_level.txt +0 -0
dbdicom/utils/image.py CHANGED
@@ -1,415 +1,6 @@
1
- import math
2
- import numpy as np
3
- from scipy.interpolate import interpn
4
- from scipy.ndimage import affine_transform
5
-
6
-
7
- def as_mosaic(array, rows=None):
8
- """Reformat a 3D array (x,y,z) into a 2D mosaic"""
9
-
10
- nz = array.shape[2]
11
- if rows is None:
12
- rows = math.ceil(math.sqrt(nz))
13
- cols = math.ceil(nz/rows)
14
- mosaic = np.zeros((array.shape[0]*cols, array.shape[1]*rows))
15
- for k in range(nz):
16
- j = math.floor(k/cols)
17
- i = k-j*cols
18
- mosaic[
19
- i*array.shape[0]:(i+1)*array.shape[0],
20
- j*array.shape[1]:(j+1)*array.shape[1],
21
- ] = array[:,:,k]
22
- return mosaic
23
-
24
-
25
-
26
-
27
- def ellipsoid(a, b, c, spacing=(1., 1., 1.), levelset=False):
28
- """
29
- Generates ellipsoid with semimajor axes aligned with grid dimensions
30
- on grid with specified `spacing`.
31
-
32
- Parameters
33
- ----------
34
- a : float
35
- Length of semimajor axis aligned with x-axis.
36
- b : float
37
- Length of semimajor axis aligned with y-axis.
38
- c : float
39
- Length of semimajor axis aligned with z-axis.
40
- spacing : tuple of floats, length 3
41
- Spacing in (x, y, z) spatial dimensions.
42
- levelset : bool
43
- If True, returns the level set for this ellipsoid (signed level
44
- set about zero, with positive denoting interior) as np.float64.
45
- False returns a binarized version of said level set.
46
-
47
- Returns
48
- -------
49
- ellip : (N, M, P) array
50
- Ellipsoid centered in a correctly sized array for given `spacing`.
51
- Boolean dtype unless `levelset=True`, in which case a float array is
52
- returned with the level set above 0.0 representing the ellipsoid.
53
-
54
- Note
55
- ----
56
- This function is copy-pasted directly from skimage source code without modification - this to avoid bringing in skimage as an essential dependency.
57
-
58
- """
59
- if (a <= 0) or (b <= 0) or (c <= 0):
60
- raise ValueError('Parameters a, b, and c must all be > 0')
61
-
62
- offset = np.r_[1, 1, 1] * np.r_[spacing]
63
-
64
- # Calculate limits, and ensure output volume is odd & symmetric
65
- low = np.ceil(- np.r_[a, b, c] - offset)
66
- high = np.floor(np.r_[a, b, c] + offset + 1)
67
-
68
- for dim in range(3):
69
- if (high[dim] - low[dim]) % 2 == 0:
70
- low[dim] -= 1
71
- num = np.arange(low[dim], high[dim], spacing[dim])
72
- if 0 not in num:
73
- low[dim] -= np.max(num[num < 0])
74
-
75
- # Generate (anisotropic) spatial grid
76
- x, y, z = np.mgrid[low[0]:high[0]:spacing[0],
77
- low[1]:high[1]:spacing[1],
78
- low[2]:high[2]:spacing[2]]
79
-
80
- if not levelset:
81
- arr = ((x / float(a)) ** 2 +
82
- (y / float(b)) ** 2 +
83
- (z / float(c)) ** 2) <= 1
84
- else:
85
- arr = ((x / float(a)) ** 2 +
86
- (y / float(b)) ** 2 +
87
- (z / float(c)) ** 2) - 1
88
-
89
- return arr
90
-
91
-
92
- def multislice_affine_transform(array_source, affine_source, output_affine, slice_thickness=None, **kwargs):
93
- """Generalization of scipy's affine transform.
94
-
95
- This version also works when the source array is 2D and when it is multislice 2D (ie. slice thickness < slice spacing).
96
- In these scenarios each slice is first reshaped into a volume with provided slice thickness and mapped separately.
97
- """
98
-
99
- slice_spacing = np.linalg.norm(affine_source[:3,2])
100
-
101
- # Single-slice 2D sequence
102
- if array_source.shape[2] == 1:
103
- return _map_multislice_array(array_source, affine_source, output_affine, **kwargs)
104
-
105
- # Multi-slice 2D sequence
106
- elif slice_spacing != slice_thickness:
107
- return _map_multislice_array(array_source, affine_source, output_affine, slice_thickness=slice_thickness, **kwargs)
108
-
109
- # 3D volume sequence
110
- else:
111
- return _map_volume_array(array_source, affine_source, output_affine, **kwargs)
112
-
113
-
114
-
115
- def _map_multislice_array(array, affine, output_affine, output_shape=None, slice_thickness=None, mask=False, label=False, cval=0):
116
-
117
- # Turn each slice into a volume and map as volume.
118
- array_mapped = None
119
- for z in range(array.shape[2]):
120
- array_z, affine_z = slice_to_volume(array, affine, z, slice_thickness=slice_thickness)
121
- array_mapped_z = _map_volume_array(array_z, affine_z, output_affine, output_shape=output_shape, cval=cval)
122
- if array_mapped is None:
123
- array_mapped = array_mapped_z
124
- else:
125
- array_mapped += array_mapped_z
126
-
127
- # If source is a mask array, set values to [0,1].
128
- if mask:
129
- array_mapped[array_mapped > 0.5] = 1
130
- array_mapped[array_mapped <= 0.5] = 0
131
- elif label:
132
- array_mapped = np.around(array_mapped)
133
-
134
- return array_mapped
135
-
136
-
137
- def slice_to_volume(array, affine, z=0, slice_thickness=None):
138
-
139
- # Reshape array to 4D (x,y,z + remainder)
140
- shape = array.shape
141
- if len(shape) > 3:
142
- nk = np.prod(shape[3:])
143
- else:
144
- nk = 1
145
- array = array.reshape(shape[:3] + (nk,))
146
-
147
- # Extract a 2D array
148
- array_z = array[:,:,z,:]
149
- array_z = array_z[:,:,np.newaxis,:]
150
-
151
- # Duplicate the array in the z-direction to create 2 slices.
152
- nz = 2
153
- array_z = np.repeat(array_z, nz, axis=2)
154
-
155
- # Reshape to original nr of dimensions
156
- if len(shape) > 3:
157
- dim = shape[:2] + (nz,) + shape[3:]
158
- else:
159
- dim = shape[:2] + (nz,)
160
- array_z = array_z.reshape(dim)
161
-
162
- # Offset the slice position accordingly
163
- affine_z = affine.copy()
164
- affine_z[:3,3] += z*affine_z[:3,2]
165
-
166
- # Set the slice spacing to equal the slice thickness
167
- if slice_thickness is not None:
168
- slice_spacing = np.linalg.norm(affine_z[:3,2])
169
- affine_z[:3,2] *= slice_thickness/slice_spacing
170
-
171
- # Offset the slice position by half of the slice thickness.
172
- affine_z[:3,3] -= affine_z[:3,2]/2
173
-
174
- return array_z, affine_z
175
-
176
-
177
- def _map_volume_array(array, affine, output_affine, output_shape=None, mask=False, label=False, cval=0):
178
-
179
- shape = array.shape
180
- if shape[2] == 1:
181
- msg = 'This function only works for an array with at least 2 slices'
182
- raise ValueError(msg)
183
-
184
- # Get transformation matrix
185
- source_to_target = np.linalg.inv(affine).dot(output_affine)
186
- source_to_target = np.around(source_to_target, 3) # remove round-off errors in the inversion
187
-
188
- # Reshape array to 4D (x,y,z + remainder)
189
- if output_shape is None:
190
- output_shape = shape[:3]
191
- nk = np.prod(shape[3:])
192
- output = np.empty(output_shape + (nk,))
193
- array = array.reshape(shape[:3] + (nk,))
194
-
195
- #Perform transformation
196
- for k in range(nk):
197
- output[:,:,:,k] = affine_transform(
198
- array[:,:,:,k],
199
- matrix = source_to_target[:3,:3],
200
- offset = source_to_target[:3,3],
201
- output_shape = output_shape,
202
- cval = cval,
203
- order = 0 if mask else 3,
204
- )
205
-
206
- # If source is a mask array, set values to [0,1]
207
- if mask:
208
- output[output > 0.5] = 1
209
- output[output <= 0.5] = 0
210
-
211
- # If source is a label array, round to integers
212
- elif label:
213
- output = np.around(output)
214
-
215
- return output.reshape(output_shape + shape[3:])
216
-
217
-
218
-
219
- # https://discovery.ucl.ac.uk/id/eprint/10146893/1/geometry_medim.pdf
220
-
221
- def interpolate3d_scale(array, scale=2):
222
-
223
- array, _ = interpolate3d_isotropic(array, [1,1,1], isotropic_spacing=1/scale)
224
- return array
225
-
226
-
227
- def interpolate3d_isotropic(array, spacing, isotropic_spacing=None):
228
-
229
- if isotropic_spacing is None:
230
- isotropic_spacing = np.amin(spacing)
231
-
232
- # Get x, y, z coordinates for array
233
- nx = array.shape[0]
234
- ny = array.shape[1]
235
- nz = array.shape[2]
236
- Lx = (nx-1)*spacing[0]
237
- Ly = (ny-1)*spacing[1]
238
- Lz = (nz-1)*spacing[2]
239
- x = np.linspace(0, Lx, nx)
240
- y = np.linspace(0, Ly, ny)
241
- z = np.linspace(0, Lz, nz)
242
-
243
- # Get x, y, z coordinates for isotropic array
244
- nxi = 1 + np.floor(Lx/isotropic_spacing)
245
- nyi = 1 + np.floor(Ly/isotropic_spacing)
246
- nzi = 1 + np.floor(Lz/isotropic_spacing)
247
- Lxi = (nxi-1)*isotropic_spacing
248
- Lyi = (nyi-1)*isotropic_spacing
249
- Lzi = (nzi-1)*isotropic_spacing
250
- xi = np.linspace(0, Lxi, nxi.astype(int))
251
- yi = np.linspace(0, Lyi, nyi.astype(int))
252
- zi = np.linspace(0, Lzi, nzi.astype(int))
253
-
254
- # Interpolate to isotropic
255
- ri = np.meshgrid(xi,yi,zi, indexing='ij')
256
- array = interpn((x,y,z), array, np.stack(ri, axis=-1))
257
- return array, isotropic_spacing
258
-
259
-
260
- def bounding_box(
261
- image_orientation, # ImageOrientationPatient (assume same for all slices)
262
- image_positions, # ImagePositionPatient for all slices
263
- pixel_spacing, # PixelSpacing (assume same for all slices)
264
- rows, # Number of rows
265
- columns): # Number of columns
266
-
267
- """
268
- Calculate the bounding box of an 3D image stored in slices in the DICOM file format.
269
-
270
- Parameters:
271
- image_orientation (list):
272
- a list of 6 elements representing the ImageOrientationPatient DICOM tag for the image.
273
- This specifies the orientation of the image slices in 3D space.
274
- image_positions (list):
275
- a list of 3-element lists representing the ImagePositionPatient DICOM tag for each slice in the image.
276
- This specifies the position of each slice in 3D space.
277
- pixel_spacing (list):
278
- a list of 2 elements representing the PixelSpacing DICOM tag for the image.
279
- This specifies the spacing between pixels in the rows and columns of each slice.
280
- rows (int):
281
- an integer representing the number of rows in each slice.
282
- columns (int):
283
- an integer representing the number of columns in each slice.
284
-
285
- Returns:
286
- dict: a dictionary with keys 'RPF', 'LPF', 'LPH', 'RPH', 'RAF', 'LAF', 'LAH', and 'RAH',
287
- representing the Right Posterior Foot, Left Posterior Foot, Left Posterior Head,
288
- Right Posterior Head, Right Anterior Foot, Left Anterior Foot,
289
- Left Anterior Head, and Right Anterior Head, respectively.
290
- Each key maps to a list of 3 elements representing the x, y, and z coordinates
291
- of the corresponding corner of the bounding box.
292
-
293
- """
294
-
295
- row_spacing = pixel_spacing[0]
296
- column_spacing = pixel_spacing[1]
297
-
298
- row_cosine = np.array(image_orientation[:3])
299
- column_cosine = np.array(image_orientation[3:])
300
- slice_cosine = np.cross(row_cosine, column_cosine)
301
-
302
- number_of_slices = len(image_positions)
303
- image_locations = [np.dot(np.array(pos), slice_cosine) for pos in image_positions]
304
- slab_thickness = max(image_locations) - min(image_locations)
305
- slice_spacing = slab_thickness / (number_of_slices - 1)
306
- image_position_first_slice = image_positions[image_locations.index(min(image_locations))]
307
-
308
- # ul = Upper Left corner of a slice
309
- # ur = Upper Right corner of a slice
310
- # bl = Bottom Left corner of a slice
311
- # br = Bottom Right corner of a slice
312
-
313
- # Initialize with the first slice
314
- ul = image_position_first_slice
315
- ur = ul + row_cosine * (columns-1) * column_spacing
316
- br = ur + column_cosine * (rows-1) * row_spacing
317
- bl = ul + column_cosine * (rows-1) * row_spacing
318
- corners = np.array([ul, ur, br, bl])
319
- amin = np.amax(corners, axis=0)
320
- amax = np.amax(corners, axis=0)
321
- box = {
322
- 'RPF': [amin[0],amax[1],amin[2]], # Right Posterior Foot
323
- 'LPF': [amax[0],amax[1],amin[2]], # Left Posterior Foot
324
- 'LPH': [amax[0],amax[1],amax[2]], # Left Posterior Head
325
- 'RPH': [amin[0],amax[1],amax[2]], # Right Posterior Head
326
- 'RAF': [amin[0],amin[1],amin[2]], # Right Anterior Foot
327
- 'LAF': [amax[0],amin[1],amin[2]], # Left Anterior Foot
328
- 'LAH': [amax[0],amin[1],amax[2]], # Left Anterior Head
329
- 'RAH': [amin[0],amin[1],amax[2]], # Right Anterior Head
330
- }
331
-
332
- # Update with all other slices
333
- # PROBABLY SUFFICIENT TO USE ONLY THE OUTER SLICES!!
334
- for _ in range(1, number_of_slices):
335
-
336
- ul += slice_cosine * slice_spacing
337
- ur = ul + row_cosine * (columns-1) * column_spacing
338
- br = ur + column_cosine * (rows-1) * row_spacing
339
- bl = ul + column_cosine * (rows-1) * row_spacing
340
-
341
- corners = np.array([ul, ur, br, bl])
342
- amin = np.amin(corners, axis=0)
343
- amax = np.amax(corners, axis=0)
344
-
345
- box['RPF'][0] = min([box['RPF'][0], amin[0]])
346
- box['RPF'][1] = max([box['RPF'][1], amax[1]])
347
- box['RPF'][2] = min([box['RPF'][2], amin[2]])
348
-
349
- box['LPF'][0] = max([box['LPF'][0], amax[0]])
350
- box['LPF'][1] = max([box['LPF'][1], amax[1]])
351
- box['LPF'][2] = min([box['LPF'][2], amin[2]])
352
-
353
- box['LPH'][0] = max([box['LPH'][0], amax[0]])
354
- box['LPH'][1] = max([box['LPH'][1], amax[1]])
355
- box['LPH'][2] = max([box['LPH'][2], amax[2]])
356
-
357
- box['RPH'][0] = min([box['RPH'][0], amin[0]])
358
- box['RPH'][1] = max([box['RPH'][1], amax[1]])
359
- box['RPH'][2] = max([box['RPH'][2], amax[2]])
360
1
 
361
- box['RAF'][0] = min([box['RAF'][0], amin[0]])
362
- box['RAF'][1] = min([box['RAF'][1], amin[1]])
363
- box['RAF'][2] = min([box['RAF'][2], amin[2]])
364
-
365
- box['LAF'][0] = max([box['LAF'][0], amax[0]])
366
- box['LAF'][1] = min([box['LAF'][1], amin[1]])
367
- box['LAF'][2] = min([box['LAF'][2], amin[2]])
368
-
369
- box['LAH'][0] = max([box['LAH'][0], amax[0]])
370
- box['LAH'][1] = min([box['LAH'][1], amin[1]])
371
- box['LAH'][2] = max([box['LAH'][2], amax[2]])
372
-
373
- box['RAH'][0] = min([box['RAH'][0], amin[0]])
374
- box['RAH'][1] = min([box['RAH'][1], amin[1]])
375
- box['RAH'][2] = max([box['RAH'][2], amax[2]])
376
-
377
- return box
378
-
379
-
380
-
381
- def standard_affine_matrix(
382
- bounding_box,
383
- pixel_spacing,
384
- slice_spacing,
385
- orientation = 'axial'):
386
-
387
- row_spacing = pixel_spacing[0]
388
- column_spacing = pixel_spacing[1]
389
-
390
- if orientation == 'axial':
391
- image_position = bounding_box['RAF']
392
- row_cosine = np.array([1,0,0])
393
- column_cosine = np.array([0,1,0])
394
- slice_cosine = np.array([0,0,1])
395
- elif orientation == 'coronal':
396
- image_position = bounding_box['RAH']
397
- row_cosine = np.array([1,0,0])
398
- column_cosine = np.array([0,0,-1])
399
- slice_cosine = np.array([0,1,0])
400
- elif orientation == 'sagittal':
401
- image_position = bounding_box['LAH']
402
- row_cosine = np.array([0,1,0])
403
- column_cosine = np.array([0,0,-1])
404
- slice_cosine = np.array([-1,0,0])
2
+ import numpy as np
405
3
 
406
- affine = np.identity(4, dtype=np.float32)
407
- affine[:3, 0] = row_cosine * column_spacing
408
- affine[:3, 1] = column_cosine * row_spacing
409
- affine[:3, 2] = slice_cosine * slice_spacing
410
- affine[:3, 3] = image_position
411
-
412
- return affine
413
4
 
414
5
 
415
6
  def affine_matrix( # single slice function
@@ -425,6 +16,10 @@ def affine_matrix( # single slice function
425
16
  column_cosine = np.array(image_orientation[3:])
426
17
  slice_cosine = np.cross(row_cosine, column_cosine)
427
18
 
19
+ # The coronal orientation has a left-handed reference frame
20
+ if np.array_equal(np.around(image_orientation, 3), [1,0,0,0,0,-1]):
21
+ slice_cosine = -slice_cosine
22
+
428
23
  affine = np.identity(4, dtype=np.float32)
429
24
  affine[:3, 0] = row_cosine * column_spacing
430
25
  affine[:3, 1] = column_cosine * row_spacing
@@ -444,61 +39,11 @@ def slice_location(
444
39
  column_cosine = np.array(image_orientation[3:])
445
40
  slice_cosine = np.cross(row_cosine, column_cosine)
446
41
 
447
- return np.dot(np.array(image_position), slice_cosine)
448
-
449
-
450
- def image_position_from_slice_location(slice_location:float, affine=np.eye(4))->list:
451
- v = dismantle_affine_matrix(affine)
452
- return list(affine[:3, 3] + slice_location * np.array(v['slice_cosine']))
453
-
454
-
455
- def image_position_patient(affine, number_of_slices):
456
- slab = dismantle_affine_matrix(affine)
457
- image_positions = []
458
- image_locations = []
459
- for s in range(number_of_slices):
460
- pos = [
461
- slab['ImagePositionPatient'][i]
462
- + s*slab['SpacingBetweenSlices']*slab['slice_cosine'][i]
463
- for i in range(3)
464
- ]
465
- loc = np.dot(np.array(pos), np.array(slab['slice_cosine']))
466
- image_positions.append(pos)
467
- image_locations.append(loc)
468
- return image_positions, image_locations
469
-
470
-
471
- def affine_matrix_multislice(
472
- image_orientation, # ImageOrientationPatient (assume same for all slices)
473
- image_positions, # ImagePositionPatient for all slices
474
- pixel_spacing): # PixelSpacing (assume same for all slices)
475
-
476
- row_spacing = pixel_spacing[0]
477
- column_spacing = pixel_spacing[1]
478
-
479
- row_cosine = np.array(image_orientation[:3])
480
- column_cosine = np.array(image_orientation[3:])
481
- slice_cosine = np.cross(row_cosine, column_cosine)
482
-
483
- image_locations = [np.dot(np.array(pos), slice_cosine) for pos in image_positions]
484
- #number_of_slices = len(image_positions)
485
- number_of_slices = np.unique(image_locations).size
486
- if number_of_slices == 1:
487
- msg = 'Cannot calculate affine matrix for the slice group. \n'
488
- msg += 'All slices have the same location. \n'
489
- msg += 'Use the single-slice affine formula instead.'
490
- raise ValueError(msg)
491
- slab_thickness = np.amax(image_locations) - np.amin(image_locations)
492
- slice_spacing = slab_thickness / (number_of_slices - 1)
493
- image_position_first_slice = image_positions[image_locations.index(np.amin(image_locations))]
494
-
495
- affine = np.identity(4, dtype=np.float32)
496
- affine[:3, 0] = row_cosine * column_spacing
497
- affine[:3, 1] = column_cosine * row_spacing
498
- affine[:3, 2] = slice_cosine * slice_spacing
499
- affine[:3, 3] = image_position_first_slice
42
+ # The coronal orientation has a left-handed reference frame
43
+ if np.array_equal(np.around(image_orientation, 3), [1,0,0,0,0,-1]):
44
+ slice_cosine = -slice_cosine
500
45
 
501
- return affine
46
+ return np.dot(np.array(image_position), slice_cosine)
502
47
 
503
48
 
504
49
  def dismantle_affine_matrix(affine):
@@ -514,116 +59,13 @@ def dismantle_affine_matrix(affine):
514
59
  slice_cosine = affine[:3, 2] / slice_thickness
515
60
  return {
516
61
  'PixelSpacing': [row_spacing, column_spacing],
517
- 'SpacingBetweenSlices': slice_thickness, # Obsolete
62
+ # 'SpacingBetweenSlices': slice_thickness, # Obsolete
518
63
  'SliceThickness': slice_thickness,
519
64
  'ImageOrientationPatient': row_cosine.tolist() + column_cosine.tolist(),
520
65
  'ImagePositionPatient': affine[:3, 3].tolist(), # first slice for a volume
521
66
  'slice_cosine': slice_cosine.tolist()}
522
67
 
523
68
 
524
- def unstack_affine(affine, nz):
525
-
526
- pos0 = affine[:3, 3]
527
- slice_vec = affine[:3, 2]
528
-
529
- affines = []
530
- for z in range(nz):
531
- affine_z = affine.copy()
532
- affine_z[:3, 3] = pos0 + z*slice_vec
533
- affines.append(affine_z)
534
-
535
- return affines
536
-
537
-
538
- def stack_affines(affines):
539
-
540
- aff = [dismantle_affine_matrix(a) for a in affines]
541
-
542
- # Check that all affines have the same orientation
543
- orient = [a['ImageOrientationPatient'] for a in aff]
544
- orient = [x for i, x in enumerate(orient) if i==orient.index(x)]
545
- if len(orient) > 1:
546
- raise ValueError(
547
- "Slices have different orientations and cannot be stacked")
548
- orient = orient[0]
549
-
550
- # Check that all affines have the same slice_cosine
551
- slice_cosine = [a['slice_cosine'] for a in aff]
552
- slice_cosine = [x for i, x in enumerate(slice_cosine) if i==slice_cosine.index(x)]
553
- if len(slice_cosine) > 1:
554
- raise ValueError(
555
- "Slices have different slice cosines and cannot be stacked")
556
- slice_cosine = slice_cosine[0]
557
-
558
- # Check all slices have the same thickness
559
- thick = [a['SpacingBetweenSlices'] for a in aff] # note incorrectly named
560
- thick = np.unique(thick)
561
- if len(thick)>1:
562
- raise ValueError(
563
- "Slices have different slice thickness and cannot be stacked")
564
- thick = thick[0]
565
-
566
- # Check all slices have the same pixel spacing
567
- pix_space = [a['PixelSpacing'] for a in aff]
568
- pix_space = [x for i, x in enumerate(pix_space) if i==pix_space.index(x)]
569
- if len(pix_space)>1:
570
- raise ValueError(
571
- "Slices have different pixel sizes and cannot be stacked. ")
572
- pix_space = pix_space[0]
573
-
574
- # Get orientations (orthogonal assumed here)
575
- row_vec = np.array(orient[:3])
576
- column_vec = np.array(orient[3:])
577
- slice_vec = np.array(slice_cosine)
578
-
579
- # Check that all slice spacings are equal
580
- pos = [a['ImagePositionPatient'] for a in aff]
581
- loc = np.array([np.dot(p, slice_vec) for p in pos])
582
- # Get unique slice spacing (to micrometer precision)
583
- slice_spacing = np.unique(np.around(loc[1:]-loc[:-1], 3))
584
- # If there is more than 1 slice spacing, the series is multislice
585
- if slice_spacing.size != 1:
586
- raise ValueError(
587
- "There are different spacings between consecutive slices. "
588
- "The slices cannot be stacked.")
589
- slice_spacing = slice_spacing[0]
590
-
591
- # Check the slice spacing is equal to the slice thickness
592
- if np.around(thick-slice_spacing, 3) != 0:
593
- raise ValueError(
594
- "This is a multi-slice sequence, i.e. the slice spacing is "
595
- "different from the slice thickness. If you want to stack the "
596
- "slices, set the slice thickness equal to the slice spacing "
597
- "first (" + str(slice_spacing) + " mm).")
598
-
599
- # Check that all positions are on the slice vector
600
- for p in pos[1:]:
601
- # Position relative to first slice position
602
- prel = np.array(p)-np.array(pos[0])
603
- # Parallel means cross product has length zero
604
- norm = np.linalg.norm(np.cross(slice_vec, prel))
605
- # Round to micrometers to avoid numerical error
606
- if np.round(norm, 3) != 0:
607
- raise ValueError(
608
- "Slices are not aligned and cannot be stacked")
609
-
610
- # Build affine for the stack
611
- affine = np.identity(4, dtype=np.float32)
612
- affine[:3, 0] = row_vec * pix_space[1]
613
- affine[:3, 1] = column_vec * pix_space[0]
614
- affine[:3, 2] = slice_vec * slice_spacing
615
- affine[:3, 3] = pos[0]
616
-
617
- return affine
618
-
619
-
620
-
621
- def affine_to_RAH(affine):
622
- """Convert to the coordinate system used in NifTi"""
623
-
624
- rot_180 = np.identity(4, dtype=np.float32)
625
- rot_180[:2,:2] = [[-1,0],[0,-1]]
626
- return np.matmul(rot_180, affine)
627
69
 
628
70
 
629
71
  def clip(array, value_range = None):
@@ -673,64 +115,3 @@ def scale_to_range(array, bits_allocated, signed=False):
673
115
  else:
674
116
  return array.astype(np.uint64), slope, intercept
675
117
 
676
-
677
- def _scale_to_range(array, bits_allocated):
678
- # Obsolete - generalized as above
679
-
680
- range = 2.0**bits_allocated - 1
681
- maximum = np.amax(array)
682
- minimum = np.amin(array)
683
- if maximum == minimum:
684
- slope = 1
685
- else:
686
- slope = range / (maximum - minimum)
687
- intercept = -slope * minimum
688
- array *= slope
689
- array += intercept
690
-
691
- if bits_allocated == 8:
692
- return array.astype(np.uint8), slope, intercept
693
- if bits_allocated == 16:
694
- return array.astype(np.uint16), slope, intercept
695
- if bits_allocated == 32:
696
- return array.astype(np.uint32), slope, intercept
697
- if bits_allocated == 64:
698
- return array.astype(np.uint64), slope, intercept
699
-
700
-
701
- def BGRA(array, RGBlut=None, width=None, center=None):
702
-
703
- if (width is None) or (center is None):
704
- max = np.amax(array)
705
- min = np.amin(array)
706
- else:
707
- max = center+width/2
708
- min = center-width/2
709
-
710
- # Scale pixel array into byte range
711
- array = np.clip(array, min, max)
712
- array -= min
713
- if max > min:
714
- array *= 255/(max-min)
715
- array = array.astype(np.ubyte)
716
-
717
- BGRA = np.empty(array.shape[:2]+(4,), dtype=np.ubyte)
718
- BGRA[:,:,3] = 255 # Alpha channel
719
-
720
- if RGBlut is None:
721
- # Greyscale image
722
- for c in range(3):
723
- BGRA[:,:,c] = array
724
- else:
725
- # Scale LUT into byte range
726
- RGBlut *= 255
727
- RGBlut = RGBlut.astype(np.ubyte)
728
- # Create RGB array by indexing LUT with pixel array
729
- for c in range(3):
730
- BGRA[:,:,c] = RGBlut[array,2-c]
731
-
732
- return BGRA
733
-
734
-
735
-
736
-
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbdicom
3
+ Version: 0.3.1
4
+ Summary: A pythonic interface for reading and writing DICOM databases
5
+ Author-email: Steven Sourbron <s.sourbron@sheffield.ac.uk>, Ebony Gunwhy <e.gunwhy@sheffield.ac.uk>
6
+ Project-URL: Homepage, https://openmiblab.github.io/dbdicom/
7
+ Keywords: python,medical imaging,DICOM
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Topic :: Scientific/Engineering
12
+ Classifier: Environment :: Console
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3
17
+ Requires-Python: >=3.6
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: tqdm
21
+ Requires-Dist: importlib-resources
22
+ Requires-Dist: numpy
23
+ Requires-Dist: pandas
24
+ Requires-Dist: vreg
25
+ Requires-Dist: pydicom
26
+ Requires-Dist: python-gdcm
27
+ Requires-Dist: pylibjpeg-libjpeg
28
+ Dynamic: license-file