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.
- {dbdicom-0.2.3/src/dbdicom.egg-info → dbdicom-0.2.4}/PKG-INFO +3 -2
- {dbdicom-0.2.3 → dbdicom-0.2.4}/pyproject.toml +2 -1
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/dataset.py +2 -1
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/__init__.py +0 -1
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/vreg.py +1 -1
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/instance.py +43 -22
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/series.py +284 -9
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/image.py +106 -29
- {dbdicom-0.2.3 → dbdicom-0.2.4/src/dbdicom.egg-info}/PKG-INFO +3 -2
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/SOURCES.txt +0 -1
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/requires.txt +1 -0
- dbdicom-0.2.3/src/dbdicom/utils/vreg.py +0 -2818
- {dbdicom-0.2.3 → dbdicom-0.2.4}/LICENSE +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/MANIFEST.in +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/README.md +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/setup.cfg +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/create.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/dro.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/create.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/dictionaries.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/ct_image.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/enhanced_mr_image.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/mr_image.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/parametric_map.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/ultrasound_multiframe_image.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/ds/types/xray_angiographic_image.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/dipy.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/elastix.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/matplotlib.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/numpy.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/scipy.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/skimage.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/extensions/sklearn.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/README.md +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/deidentify +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/deidentify.bat +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/emf2sf +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/bin/emf2sf.bat +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/etc/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/etc/emf2sf/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/etc/emf2sf/log4j.properties +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/__init__.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/commons-cli-1.4.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-core-5.23.1.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-emf-5.23.1.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/log4j-1.2.17.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/macosx-x86-64/libopencv_java.jnilib +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/slf4j-api-1.7.30.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/slf4j-log4j12-1.7.30.jar +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio.dll +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/manager.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/message.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/pipelines.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/record.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/database.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/patient.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/types/study.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/dcm4che.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/files.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom/utils/variables.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/dependency_links.txt +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/src/dbdicom.egg-info/top_level.txt +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_dataset.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_dcm4che.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_dro.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_extensions.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_manager.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_record.py +0 -0
- {dbdicom-0.2.3 → dbdicom-0.2.4}/tests/test_series.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: dbdicom
|
|
3
|
-
Version: 0.2.
|
|
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.
|
|
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'])
|
|
@@ -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
|
|
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['
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
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()
|