Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: First attempt at multiframe support (3D only for now) #84

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b24d860
ENH: Handle slope / intercept scaling more efficiently
moloney Aug 31, 2023
0ff1c6a
BF: Leverage nicom data transforms for Mosaic/Multiframe
moloney Aug 31, 2023
cded103
ENH: First attempt at multiframe support (3D only for now)
moloney Feb 10, 2023
d0d75b9
Avoid dupe values from SharedFunctionalGroupSequence
moloney Feb 14, 2023
62046c9
BF: More 3D multiframe fixes
moloney Aug 31, 2023
2812ffe
BF: Fix typo in last commit
moloney Aug 31, 2023
b66925f
t/test_cli.py: port setup/teardown to pytest 8.
emollier May 29, 2024
3c6bc98
prefer newer unittest.mock from standard library
a-detiste May 30, 2024
3c7ee96
Merge pull request #87 from emollier/pytest-8
moloney Jun 5, 2024
615a914
Merge pull request #88 from a-detiste/master
moloney Jun 5, 2024
27856f1
BF+TST: Handle None values in dicom datasets
moloney Jun 5, 2024
a9b1430
ENH: Handle slope / intercept scaling more efficiently
moloney Aug 31, 2023
94a6675
BF: Leverage nicom data transforms for Mosaic/Multiframe
moloney Aug 31, 2023
139079f
ENH: First attempt at multiframe support (3D only for now)
moloney Feb 10, 2023
a053b51
Avoid dupe values from SharedFunctionalGroupSequence
moloney Feb 14, 2023
790f479
BF: More 3D multiframe fixes
moloney Aug 31, 2023
9c7441c
BF: Fix typo in last commit
moloney Aug 31, 2023
6183d48
WIP: Multi 3D multiframe files to 4D Nifti conversion seems to work
moloney Jun 17, 2024
7de2452
Merge branch 'enh-multiframe' of github.com:moloney/dcmstack into enh…
moloney Jun 17, 2024
b26a5ee
ENH: Add --pdb option in CLI for debugging
moloney Jun 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
import sys, os

#Mock unavailable packages for ReadTheDocs
import mock
try:
from unitttest import mock
except ImportError:
import mock

MOCK_MODULES = ['numpy',
'nibabel',
Expand Down
86 changes: 81 additions & 5 deletions src/dcmstack/dcmmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,21 @@ def patch_dcm_ds_is(dcm):
elem.value = [int(val) for val in elem.value]


def gen_simplified_sequences(meta_dict):
"""Get rid of useless nesting of meta data from multiframe DICOM"""
for k, v in meta_dict.items():
if isinstance(v, list):
if len(v) == 0:
continue
if len(v) == 1:
for sub_key, sub_val in v[0].items():
yield sub_key, sub_val
continue
yield k, v




class NiftiWrapper(object):
'''Wraps a Nifti1Image object containing a DcmMeta header extension.
Provides access to the meta data and the ability to split or merge the
Expand Down Expand Up @@ -1530,18 +1545,33 @@ def from_dicom_wrapper(klass, dcm_wrp, meta_dict=None):
the `extract` module for generating this dict.

'''
data = dcm_wrp.get_data()
# This is kinda hacky, but no great way to get unscaled data out of
# the dicom wrapper classes
orig_scale_data = dcm_wrp._scale_data
dcm_wrp._scale_data = lambda data: data
try:
data = dcm_wrp.get_data()
finally:
dcm_wrp._scale_data = orig_scale_data

#The Nifti patient space flips the x and y directions
affine = np.dot(np.diag([-1., -1., 1., 1.]), dcm_wrp.affine)

#Make 2D data 3D
if len(data.shape) == 2:
data = data.reshape(data.shape + (1,))
elif len(data.shape) > 3:
data = np.squeeze(data)
if len(data.shape) > 3:
raise ValueError("4D+ Multiframe not supported yet")

#Create the nifti image and set header data
nii_img = nb.nifti1.Nifti1Image(data, affine)
hdr = nii_img.header
slope = float(dcm_wrp.get('RescaleSlope', 1.0))
inter = float(dcm_wrp.get('RescaleIntercept', 0.0))
if (slope, inter) != (1.0, 0.0):
hdr.set_slope_inter(slope, inter)
hdr.set_xyzt_units('mm', 'sec')
dim_info = {'freq' : None,
'phase' : None,
Expand All @@ -1556,13 +1586,59 @@ def from_dicom_wrapper(klass, dcm_wrp, meta_dict=None):
dim_info['phase'] = 0
dim_info['freq'] = 1
hdr.set_dim_info(**dim_info)

#Embed the meta data extension
# Create result and embed any provided meta data
result = klass(nii_img, make_empty=True)

result.meta_ext.reorient_transform = np.eye(4)
if meta_dict:
result.meta_ext.get_class_dict(('global', 'const')).update(meta_dict)
if not dcm_wrp.is_multiframe:
result.meta_ext.get_class_dict(('global', 'const')).update(meta_dict)
else:
global_meta = meta_dict.copy()
del global_meta["SharedFunctionalGroupsSequence"]
del global_meta["PerFrameFunctionalGroupsSequence"]
assert len(meta_dict["SharedFunctionalGroupsSequence"]) == 1
for k, v in gen_simplified_sequences(
meta_dict["SharedFunctionalGroupsSequence"][0]
):
if k in global_meta:
if global_meta[k] == v:
continue
k = f"Shared.{k}"
global_meta[k] = v
fg_seqs = meta_dict.get("PerFrameFunctionalGroupsSequence")
dcm_wrp.image_shape
sorted_indices = np.lexsort(dcm_wrp._frame_indices.T)
slice_meta = {}
for slice_count, slice_idx in enumerate(sorted_indices):
for k, v in gen_simplified_sequences(fg_seqs[slice_idx]):
if k in global_meta:
k = f"PerFrame.{k}"
if k not in slice_meta:
if slice_count == 0:
slice_meta[k] = [v]
else:
slice_meta[k] = [None] * slice_count
slice_meta[k].append(v)
else:
n_vals = len(slice_meta[k])
if n_vals != slice_count:
slice_meta[k] += [None] * (slice_count - n_vals)
slice_meta[k].append(v)
moved = []
for k, vals in slice_meta.items():
if len(vals) != data.shape[-1]:
vals += [None] * (data.shape[-1] - len(vals))
if all(x == vals[0] for x in vals):
moved.append(k)
if isinstance(k, str) and k.startswith("PerFrame."):
if global_meta[k.split('.')[1]] == vals[0]:
continue
global_meta[k] = vals[0]
for k in moved:
del slice_meta[k]
result.meta_ext.get_class_dict(('global', 'const')).update(global_meta)
result.meta_ext.get_class_dict(('global', 'slices')).update(slice_meta)

return result

Expand Down
Loading