dbdicom 0.2.3__tar.gz → 0.2.4__tar.gz

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 (80) hide show
  1. {dbdicom-0.2.3/src/dbdicom.egg-info → dbdicom-0.2.4}/PKG-INFO +3 -2
  2. {dbdicom-0.2.3 → dbdicom-0.2.4}/pyproject.toml +2 -1
  3. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/dataset.py +2 -1
  4. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/__init__.py +0 -1
  5. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/vreg.py +1 -1
  6. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/instance.py +43 -22
  7. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/series.py +284 -9
  8. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/image.py +106 -29
  9. {dbdicom-0.2.3 → dbdicom-0.2.4/src/dbdicom.egg-info}/PKG-INFO +3 -2
  10. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/SOURCES.txt +0 -1
  11. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/requires.txt +1 -0
  12. dbdicom-0.2.3/src/dbdicom/utils/vreg.py +0 -2818
  13. {dbdicom-0.2.3 → dbdicom-0.2.4}/LICENSE +0 -0
  14. {dbdicom-0.2.3 → dbdicom-0.2.4}/MANIFEST.in +0 -0
  15. {dbdicom-0.2.3 → dbdicom-0.2.4}/README.md +0 -0
  16. {dbdicom-0.2.3 → dbdicom-0.2.4}/setup.cfg +0 -0
  17. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/__init__.py +0 -0
  18. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/create.py +0 -0
  19. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/dro.py +0 -0
  20. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/__init__.py +0 -0
  21. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/create.py +0 -0
  22. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/dictionaries.py +0 -0
  23. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/ct_image.py +0 -0
  24. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/enhanced_mr_image.py +0 -0
  25. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/mr_image.py +0 -0
  26. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/parametric_map.py +0 -0
  27. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/ultrasound_multiframe_image.py +0 -0
  28. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/xray_angiographic_image.py +0 -0
  29. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/dipy.py +0 -0
  30. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/elastix.py +0 -0
  31. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/matplotlib.py +0 -0
  32. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/numpy.py +0 -0
  33. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/scipy.py +0 -0
  34. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/skimage.py +0 -0
  35. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/sklearn.py +0 -0
  36. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/__init__.py +0 -0
  37. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/README.md +0 -0
  38. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/__init__.py +0 -0
  39. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/__init__.py +0 -0
  40. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/deidentify +0 -0
  41. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/deidentify.bat +0 -0
  42. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/emf2sf +0 -0
  43. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/emf2sf.bat +0 -0
  44. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/etc/__init__.py +0 -0
  45. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/etc/emf2sf/__init__.py +0 -0
  46. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/etc/emf2sf/log4j.properties +0 -0
  47. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/__init__.py +0 -0
  48. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/commons-cli-1.4.jar +0 -0
  49. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-core-5.23.1.jar +0 -0
  50. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-emf-5.23.1.jar +0 -0
  51. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar +0 -0
  52. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar +0 -0
  53. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/log4j-1.2.17.jar +0 -0
  54. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/macosx-x86-64/libopencv_java.jnilib +0 -0
  55. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/slf4j-api-1.7.30.jar +0 -0
  56. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/slf4j-log4j12-1.7.30.jar +0 -0
  57. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio.dll +0 -0
  58. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll +0 -0
  59. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll +0 -0
  60. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll +0 -0
  61. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll +0 -0
  62. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/manager.py +0 -0
  63. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/message.py +0 -0
  64. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/pipelines.py +0 -0
  65. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/record.py +0 -0
  66. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/database.py +0 -0
  67. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/patient.py +0 -0
  68. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/study.py +0 -0
  69. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/dcm4che.py +0 -0
  70. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/files.py +0 -0
  71. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/variables.py +0 -0
  72. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/dependency_links.txt +0 -0
  73. {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/top_level.txt +0 -0
  74. {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_dataset.py +0 -0
  75. {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_dcm4che.py +0 -0
  76. {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_dro.py +0 -0
  77. {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_extensions.py +0 -0
  78. {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_manager.py +0 -0
  79. {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_record.py +0 -0
  80. {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_series.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: dbdicom
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A pythonic interface for reading and writing DICOM databases
5
5
  Author-email: Steven Sourbron <s.sourbron@sheffield.ac.uk>, Ebony Gunwhy <e.gunwhy@sheffield.ac.uk>
6
6
  Project-URL: Homepage, https://qib-sheffield.github.io/dbdicom/
@@ -27,6 +27,7 @@ Requires-Dist: pylibjpeg-libjpeg
27
27
  Requires-Dist: importlib-resources
28
28
  Requires-Dist: scipy
29
29
  Requires-Dist: imageio
30
+ Requires-Dist: vreg
30
31
  Provides-Extra: docs
31
32
  Requires-Dist: sphinx; extra == "docs"
32
33
  Requires-Dist: pydata-sphinx-theme; extra == "docs"
@@ -7,7 +7,7 @@ requires = ['setuptools>=61.2']
7
7
 
8
8
  [project]
9
9
  name = "dbdicom"
10
- version = "0.2.3"
10
+ version = "0.2.4"
11
11
  dependencies = [
12
12
  "matplotlib",
13
13
  "nibabel",
@@ -19,6 +19,7 @@ dependencies = [
19
19
  "importlib-resources", #necessary?
20
20
  'scipy',
21
21
  'imageio',
22
+ 'vreg',
22
23
  ]
23
24
 
24
25
  # optional information
@@ -463,6 +463,7 @@ def new_uid(n=None):
463
463
  return [pydicom.uid.generate_uid() for _ in range(n)]
464
464
 
465
465
 
466
+ # Obsolete - replaced by instance.affine()
466
467
  def get_affine_matrix(ds):
467
468
  """Affine transformation matrix for a DICOM image"""
468
469
 
@@ -470,7 +471,6 @@ def get_affine_matrix(ds):
470
471
  # if slice_spacing is None:
471
472
  # slice_spacing = get_values(ds, 'SliceThickness')
472
473
  slice_spacing = get_values(ds, 'SliceThickness')
473
-
474
474
  return image.affine_matrix(
475
475
  get_values(ds, 'ImageOrientationPatient'),
476
476
  get_values(ds, 'ImagePositionPatient'),
@@ -478,6 +478,7 @@ def get_affine_matrix(ds):
478
478
  slice_spacing)
479
479
 
480
480
 
481
+ # Obsolete - replaced by instance.set_affine()
481
482
  def set_affine_matrix(ds, affine):
482
483
  v = image.dismantle_affine_matrix(affine)
483
484
  set_values(ds, 'PixelSpacing', v['PixelSpacing'])
@@ -5,6 +5,5 @@ from . import (
5
5
  scipy,
6
6
  skimage,
7
7
  sklearn,
8
- vreg,
9
8
  matplotlib,
10
9
  )
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import pandas as pd
3
3
  import scipy
4
4
  import dbdicom
5
- from dbdicom.utils import vreg
5
+ import vreg
6
6
  from dbdicom import Series
7
7
 
8
8
 
@@ -7,6 +7,7 @@ import numpy as np
7
7
  import nibabel as nib
8
8
  import pandas as pd
9
9
  import matplotlib.pyplot as plt
10
+ import vreg
10
11
 
11
12
 
12
13
  from dbdicom.record import Record
@@ -39,31 +40,12 @@ class Instance(Record):
39
40
  uid = self.manager.copy_instance_to_series(self.key(), series.keys(), series)
40
41
  return self.record('Instance', uid)
41
42
 
42
- def array(self):
43
+ def pixel_values(self):
43
44
  return self.get_pixel_array()
44
45
 
45
- def get_pixel_array(self):
46
- ds = self.get_dataset()
47
- return ds.get_pixel_array()
48
-
49
- def set_array(self, array): # obsolete
50
- self.set_pixel_array(array)
51
-
52
46
  def set_pixel_values(self, array):
53
47
  self.set_pixel_array(array)
54
48
 
55
- def set_pixel_array(self, array): # make private
56
- ds = self.get_dataset()
57
- if ds is None:
58
- ds = new_dataset('MRImage')
59
- ds.set_pixel_array(array)
60
- in_memory = self.key() in self.manager.dataset
61
- self.set_dataset(ds)
62
- # This bit added ad-hoc because set_dataset() places the datset in memory
63
- # So if the instance is not in memory, it needs to be written and removed again
64
- if not in_memory:
65
- self.clear()
66
-
67
49
  def set_dataset(self, dataset):
68
50
  self._key = self.manager.set_instance_dataset(self.uid, dataset, self.key())
69
51
 
@@ -141,11 +123,25 @@ class Instance(Record):
141
123
  center = self.WindowCenter,
142
124
  )
143
125
 
126
+ def volume(self):
127
+ return vreg.volume(self.pixel_values(),
128
+ self.affine())
129
+
130
+ def set_volume(self, volume:vreg.Volume3D):
131
+ self.set_pixel_values(np.squeeze(volume.values))
132
+ self.set_affine(volume.affine)
133
+
134
+ def affine(self):
135
+ return image.affine_matrix(self.ImageOrientationPatient,
136
+ self.ImagePositionPatient,
137
+ self.PixelSpacing,
138
+ self.SliceThickness)
139
+
144
140
  def set_affine(self, affine):
145
141
  p = image.dismantle_affine_matrix(affine)
146
142
  self.read()
147
- self.SpacingBetweenSlices = p['SpacingBetweenSlices']
148
- self.SliceThickness = p['SpacingBetweenSlices']
143
+ #self.SpacingBetweenSlices = p['SpacingBetweenSlices']
144
+ self.SliceThickness = p['SliceThickness']
149
145
  self.PixelSpacing = p['PixelSpacing']
150
146
  self.ImageOrientationPatient = p['ImageOrientationPatient']
151
147
  self.ImagePositionPatient = p['ImagePositionPatient']
@@ -153,6 +149,31 @@ class Instance(Record):
153
149
  self.clear()
154
150
 
155
151
 
152
+ # OBSOLETE API
153
+
154
+ def array(self): # obsolete replace by pixel_values
155
+ return self.get_pixel_array()
156
+
157
+ def get_pixel_array(self): # obsolete replace by pixel_values
158
+ ds = self.get_dataset()
159
+ return ds.get_pixel_array()
160
+
161
+ def set_array(self, array): # obsolete replace by set_pixel_values
162
+ self.set_pixel_array(array)
163
+
164
+ def set_pixel_array(self, array): # obsolete replace by set_pixel_values
165
+ ds = self.get_dataset()
166
+ if ds is None:
167
+ ds = new_dataset('MRImage')
168
+ ds.set_pixel_array(array)
169
+ in_memory = self.key() in self.manager.dataset
170
+ self.set_dataset(ds)
171
+ # This bit added ad-hoc because set_dataset() places the datset in memory
172
+ # So if the instance is not in memory, it needs to be written and removed again
173
+ if not in_memory:
174
+ self.clear()
175
+
176
+
156
177
  def map_to(source, target):
157
178
  """Map non-zero image pixels onto a target image.
158
179
 
@@ -6,8 +6,9 @@ import math
6
6
  from numbers import Number
7
7
 
8
8
  import numpy as np
9
- import pandas as pd
10
9
  import nibabel as nib
10
+ import vreg
11
+
11
12
 
12
13
  from dbdicom.record import Record, read_dataframe_from_instance_array
13
14
  from dbdicom.ds import MRImage
@@ -410,7 +411,13 @@ class Series(Record):
410
411
  return values
411
412
 
412
413
 
413
- def frames(self, dims=('InstanceNumber', ), return_coords=False, return_vals=(), mesh=True, slice={}, coords={}, exclude=False, **filters):
414
+
415
+
416
+
417
+ def frames(
418
+ self, dims=('InstanceNumber', ), return_coords=False,
419
+ return_vals=(), mesh=True, slice={}, coords={}, exclude=False,
420
+ **filters):
414
421
  """Return the frames of given coordinates in the correct order"""
415
422
 
416
423
  if np.isscalar(dims):
@@ -1187,8 +1194,207 @@ class Series(Record):
1187
1194
  for f, frame in enumerate(frames):
1188
1195
  self.progress(f+1, frames.size, 'Writing pixel values..')
1189
1196
  frame.set_pixel_array(values[:,:,f])
1197
+
1198
+ def volume(self):
1199
+ return self.volumes(stack=True)
1200
+
1201
+ def volumes(self, dims='SliceLocation', mesh=True, stack=False):
1202
+ """Return vreg volumes for each frame, or stacked"""
1203
+
1204
+ frames = self.frames(dims, mesh=mesh)
1205
+ vols = [f.volume() for f in frames.reshape(-1)]
1206
+ vols = np.asarray(vols).reshape(frames.shape)
1207
+ if not stack:
1208
+ return vols
1209
+ shape = vols.shape
1210
+ vols = vols.reshape((shape[0],-1))
1211
+ vols_stack = []
1212
+ for k in range(vols.shape[1]):
1213
+ vstack = vreg.concatenate(vols[:,k], prec=3)
1214
+ vols_stack.append(vstack)
1215
+ if len(shape) == 1:
1216
+ return vols_stack[0]
1217
+ else:
1218
+ return np.asarray(vols_stack).reshape(shape[1:])
1219
+
1220
+
1221
+ def set_volumes(self, volumes, dims='SliceLocation', mesh=True):
1222
+
1223
+ # Convert affines to arrays if needed
1224
+ if isinstance(volumes, list):
1225
+ volumes = np.array(volumes)
1226
+
1227
+ # Get frames
1228
+ frames = self.frames(dims, mesh=mesh)
1229
+
1230
+ # One affine for each frame
1231
+ if volumes.shape == frames.shape:
1232
+ volumes = volumes.reshape(-1)
1233
+ for i, f in enumerate(frames.reshape(-1)):
1234
+ self.progress(i, frames.size, 'Setting affines.. ')
1235
+ f.set_volume(volumes[i])
1236
+
1237
+ # Different number of affines and frames
1238
+ else:
1239
+ # A volumetric series
1240
+ if frames.ndim==1:
1241
+ volumes = volumes.reshape(-1)
1242
+ if volumes.size > 1:
1243
+ raise ValueError(
1244
+ "Cannot set volumes. A volume can only "
1245
+ "have one element.")
1246
+ volumes = volumes[0].split(frames.size)
1247
+ for z, f in enumerate(frames):
1248
+ self.progress(z+1, frames.size, 'Setting volumes.. ')
1249
+ f.set_volume(volumes[z])
1250
+
1251
+ # Multislice affine replicated across all times
1252
+ elif volumes.size == frames.shape[0]:
1253
+ frames = frames.reshape((frames.shape[0],-1))
1254
+ volumes = volumes.reshape(-1)
1255
+ nz, nt = frames.shape
1256
+ cnt=0
1257
+ for z in range(nz):
1258
+ for t in range(nt):
1259
+ cnt+=1
1260
+ self.progress(cnt, nt*nz, 'Setting volumes.. ')
1261
+ frames[z,t].set_volume(volumes[z])
1262
+
1263
+ # One volume replicated across all times
1264
+ elif volumes.size==1:
1265
+ frames = frames.reshape((frames.shape[0],-1))
1266
+ nz, nt = frames.shape
1267
+ volumes = volumes[0].split(nz)
1268
+ cnt=0
1269
+ for z in range(nz):
1270
+ for t in range(nt):
1271
+ cnt+=1
1272
+ self.progress(cnt, nt*nz, 'Setting volumes.. ')
1273
+ frames[z,t].set_volume(volumes[z])
1274
+
1275
+ # Volume for each time point
1276
+ elif volumes.shape == frames.shape[1:]:
1277
+ frames = frames.reshape((frames.shape[0],-1))
1278
+ volumes = volumes.reshape(-1)
1279
+ nz, nt = frames.shape
1280
+ cnt=0
1281
+ for t in range(nt):
1282
+ volumes_t = volumes[t].split(nz)
1283
+ for z, f in enumerate(frames[:,t]):
1284
+ cnt+=1
1285
+ self.progress(cnt, nt*nz, 'Setting volumes.. ')
1286
+ f.set_volume(volumes_t[z])
1287
+
1288
+ # Incompatible shapes
1289
+ else:
1290
+ raise ValueError(
1291
+ "Cannot set volumes. The volume array has an incompatible "
1292
+ "shape or size.")
1293
+ return self
1190
1294
 
1191
1295
 
1296
+ def affines(self, dims='SliceLocation', mesh=True, stack=False):
1297
+ """Return affines for each frame"""
1298
+
1299
+ frames = self.frames(dims, mesh=mesh)
1300
+ affines = [f.affine() for f in frames.reshape(-1)]
1301
+ affines = np.asarray(affines).reshape(frames.shape)
1302
+ if not stack:
1303
+ return affines
1304
+ shape = affines.shape
1305
+ affines = affines.reshape((shape[0],-1))
1306
+ nt = affines.shape[1]
1307
+ affines_stack = np.empty(nt, dtype=np.ndarray)
1308
+ for t in range(nt):
1309
+ affines_stack[t] = image_utils.stack_affines(affines[:,t])
1310
+ if len(shape)==1:
1311
+ return affines_stack[0]
1312
+ else:
1313
+ return affines_stack.reshape(shape[1:])
1314
+
1315
+ def set_affines(self, affines, dims='SliceLocation', mesh=True):
1316
+
1317
+ # Convert affines to arrays if needed
1318
+ if isinstance(affines, np.ndarray):
1319
+ aff = np.empty(1, dtype=np.ndarray)
1320
+ aff[0] = affines
1321
+ affines = aff
1322
+ elif isinstance(affines, list):
1323
+ aff = np.empty(len(affines), dtype=np.ndarray)
1324
+ for i, a in enumerate(affines):
1325
+ aff[i] = a
1326
+ affines = aff
1327
+
1328
+ # Get frames
1329
+ frames = self.frames(dims, mesh=mesh)
1330
+
1331
+ # One affine for each frame
1332
+ if affines.shape == frames.shape:
1333
+ affines = affines.reshape(-1)
1334
+ for i, f in enumerate(frames.reshape(-1)):
1335
+ self.progress(i, frames.size, 'Setting affines.. ')
1336
+ f.set_affine(affines[i])
1337
+
1338
+ # Different number of affines and frames
1339
+ else:
1340
+ # A volumetric series
1341
+ if frames.ndim==1:
1342
+ affines = affines.reshape(-1)
1343
+ if affines.size > 1:
1344
+ raise ValueError(
1345
+ "Cannot set affines. A volumetric affine can only "
1346
+ "have one element.")
1347
+ affines = image_utils.unstack_affine(affines[0], frames.shape[0])
1348
+ for z, f in enumerate(frames):
1349
+ self.progress(z+1, frames.size, 'Setting affines.. ')
1350
+ f.set_affine(affines[z])
1351
+
1352
+ # Multislice affine replicated across all times
1353
+ elif affines.size == frames.shape[0]:
1354
+ frames = frames.reshape((frames.shape[0],-1))
1355
+ affines = affines.reshape(-1)
1356
+ nz, nt = frames.shape
1357
+ cnt=0
1358
+ for z in range(nz):
1359
+ for t in range(nt):
1360
+ cnt+=1
1361
+ self.progress(cnt, nt*nz, 'Setting affines.. ')
1362
+ frames[z,t].set_affine(affines[z])
1363
+
1364
+ # One volume affine replicated across all times
1365
+ elif affines.size==1:
1366
+ frames = frames.reshape((frames.shape[0],-1))
1367
+ nz, nt = frames.shape
1368
+ affines = image_utils.unstack_affine(affines[0], nz)
1369
+ cnt=0
1370
+ for z in range(nz):
1371
+ for t in range(nt):
1372
+ cnt+=1
1373
+ self.progress(cnt, nt*nz, 'Setting affines.. ')
1374
+ frames[z,t].set_affine(affines[z])
1375
+
1376
+ # Volume affine for each time point
1377
+ elif affines.shape == frames.shape[1:]:
1378
+ frames = frames.reshape((frames.shape[0],-1))
1379
+ affines = affines.reshape(-1)
1380
+ nz, nt = frames.shape
1381
+ cnt=0
1382
+ for t in range(nt):
1383
+ affines_t = image_utils.unstack_affine(affines[t], nz)
1384
+ for z, f in enumerate(frames[:,t]):
1385
+ cnt+=1
1386
+ self.progress(cnt, nt*nz, 'Setting affines.. ')
1387
+ f.set_affine(affines_t[z])
1388
+
1389
+ # Incompatible shapes
1390
+ else:
1391
+ raise ValueError(
1392
+ "Cannot set affines. The affine array has an incompatible "
1393
+ "shape or size.")
1394
+ return self
1395
+
1396
+
1397
+ # TODO: make obsolete (ignores dimensions or multi-volume series)
1192
1398
  def affine(self, slice={}, coords={}, **filters) -> np.ndarray:
1193
1399
  """Return the affine of the Series.
1194
1400
 
@@ -1251,7 +1457,7 @@ class Series(Record):
1251
1457
 
1252
1458
  return image_utils.affine_matrix_multislice(orientation, pos, spacing)
1253
1459
 
1254
-
1460
+ # TODO: amke obsolete - does not handle dimensions or multislice vs volume
1255
1461
  def set_affine(self, affine:np.ndarray, dims=('InstanceNumber',), slice={}, coords={}, multislice=False, **filters):
1256
1462
  """Set the affine matrix of a series.
1257
1463
 
@@ -1532,7 +1738,7 @@ class Series(Record):
1532
1738
  msg += 'The data may be corrupted - please check'
1533
1739
  raise ValueError(msg)
1534
1740
  # Multiple slice groups in series - return list of affine matrices
1535
- if isinstance(image_orientation[0], list):
1741
+ if self.is_multislice():
1536
1742
  affine_matrices = []
1537
1743
  for dir in image_orientation:
1538
1744
  slice_group = self.instances(ImageOrientationPatient=dir)
@@ -1544,6 +1750,14 @@ class Series(Record):
1544
1750
  slice_group = self.instances()
1545
1751
  affine = _slice_group_affine_matrix(slice_group, image_orientation)
1546
1752
  return np.array([affine])
1753
+
1754
+ def is_multislice(self)->bool:
1755
+ """Check if the series is multislice
1756
+
1757
+ Returns:
1758
+ bool: True if the series is multislice.
1759
+ """
1760
+ return is_multislice(self)
1547
1761
 
1548
1762
 
1549
1763
  def islice(self, indices={}, **inds) -> Series:
@@ -2372,7 +2586,52 @@ def set_pixel_array(series, array, source=None, pixels_first=False, **kwargs):
2372
2586
  image.set_pixel_array(array[i,...])
2373
2587
  image.clear()
2374
2588
 
2375
-
2589
+ # TODO: make this obsolete - only used ion affine_matrix
2590
+ def is_multislice(series):
2591
+ orientation = series.ImageOrientationPatient
2592
+ # Series is multislice if there are multiple unique orientations
2593
+ if isinstance(orientation[0], list):
2594
+ return True
2595
+ #
2596
+ # NOTE: 08/01/25: Added below conditions to correctly deal with situations
2597
+ # where individual slices have been shifted but not rotated.
2598
+ # From here: a series is multislice as soon as slices are not part of a
2599
+ # uniformly spaced 3D volume.
2600
+ #
2601
+ pos = series.ImagePositionPatient
2602
+ # If there is only one slice location, the series is not multislice
2603
+ if not isinstance(pos[0], list):
2604
+ return False
2605
+ #
2606
+ # If there are multiple positions, check that they are all on the slice
2607
+ # vector. If at least one if them is not, the series is multislice.
2608
+ #
2609
+ # Get slice vector
2610
+ row_vec = np.array(orientation[:3])
2611
+ column_vec = np.array(orientation[3:])
2612
+ slice_vec = np.cross(row_vec, column_vec)
2613
+ for p in pos[1:]:
2614
+ # Position relative to first slice position
2615
+ prel = np.array(p)-np.array(pos[0])
2616
+ # Parallel means cross product has length zero
2617
+ norm = np.linalg.norm(np.cross(slice_vec, prel))
2618
+ # Round to micrometers to avoid numerical error
2619
+ if np.round(norm, 3) != 0:
2620
+ return True
2621
+ #
2622
+ # If they are all on the slice vector, check that they have the same
2623
+ # spacing. If more than one spacing is found, the series is multislice.
2624
+ #
2625
+ # Get slice locations
2626
+ loc = [np.dot(p, slice_vec) for p in pos]
2627
+ # Sort slice locations
2628
+ loc = np.sort(loc)
2629
+ # Get unique slice spacing (to micrometer precision)
2630
+ spacing = np.unique(np.around(loc[1:]-loc[:-1], 3))
2631
+ # If there is more than 1 slice spacing, the series is multislice
2632
+ return spacing.size != 1
2633
+
2634
+ # TODO: make this obsolete -replace by affines
2376
2635
  def affine_matrix(series):
2377
2636
  """Returns the affine matrix of a series.
2378
2637
 
@@ -2386,14 +2645,30 @@ def affine_matrix(series):
2386
2645
  msg = 'This is a required DICOM field \n'
2387
2646
  msg += 'The data may be corrupted - please check'
2388
2647
  raise ValueError(msg)
2648
+
2389
2649
  # Multiple slice groups in series - return list of affine matrices
2390
- if isinstance(image_orientation[0], list):
2650
+ if is_multislice(series):
2651
+ #
2652
+ # NOTE: 08/01/2025: Changed definition of slice groups from "frames with
2653
+ # the same orientation" to "frames with the same orientation and position"
2654
+ #
2655
+ # Get unique image positions
2656
+ image_position = series.ImagePositionPatient
2657
+ # Make sure orientations and positions are losts
2658
+ if not isinstance(image_orientation[0], list):
2659
+ image_orientation = [image_orientation]
2660
+ if not isinstance(image_position[0], list):
2661
+ image_position = [image_position]
2662
+ # Return one affine per slice group
2391
2663
  affine_matrices = []
2392
2664
  for dir in image_orientation:
2393
- slice_group = series.instances(ImageOrientationPatient=dir)
2394
- affine = _slice_group_affine_matrix(slice_group, dir)
2395
- affine_matrices.append((affine, slice_group))
2665
+ for pos in image_position:
2666
+ slice_group = series.instances(ImageOrientationPatient=dir, ImagePositionPatient=pos)
2667
+ if len(slice_group) > 0:
2668
+ affine = _slice_group_affine_matrix(slice_group, dir)
2669
+ affine_matrices.append((affine, slice_group))
2396
2670
  return affine_matrices
2671
+
2397
2672
  # Single slice group in series - return a single affine matrix
2398
2673
  else:
2399
2674
  slice_group = series.instances()