Skip to content

Commit d700939

Browse files
authored
Update shell-sampling determination (#241)
* Fix DTI protocol (#228) * Update reslicing to use new `mrgrid` command This also works with older `mrreslice` * Fix bad indent in tensorReorder() * Update documentation * Add bval scaling (#230) * Fix SNR plotting issues (#233) * Add try/except loop to SNR plot * Fix eddy path when `--noqc` is parsef * Fix a bad indent * Update CHANGELOG * Init tractography module (#234) * Init tractography module with DSI Studio * Update main to use tractography modules * Update version and MANIFEST * Update docs * Fiber Tracking: FBI tractography (#235) (#237) * Fix DTI protocol (#228) * Update reslicing to use new `mrgrid` command This also works with older `mrreslice` * Fix bad indent in tensorReorder() * Update documentation * Add bval scaling (#230) * Fix SNR plotting issues (#233) * Add try/except loop to SNR plot * Fix eddy path when `--noqc` is parsef * Fix a bad indent * Update CHANGELOG * Init tractography module (#234) * Init tractography module with DSI Studio * Update main to use tractography modules * Update version and MANIFEST * Update docs * Change dki_ak fname (#238) * Update shell sampling determination (#240) * Add new functions to determine DWI shell sampling * Remove references to old functions * Change how shell sampling is determined * Update documentation
1 parent 48b7af5 commit d700939

5 files changed

Lines changed: 177 additions & 83 deletions

File tree

CHANGELOG.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ Changelog
44
All notable changes to this project will be documented in this file or
55
page
66

7+
`v1.0_RC6`_
8+
-----------
9+
10+
Dec 22, 2020
11+
12+
**Added**:
13+
14+
* None
15+
16+
**Changed**
17+
18+
* Replaced ``preprocessing.util.bvec_is_fullsphere()`` and
19+
``preprocessing.util.vecs_are_fullsphere()`` with
20+
``preprocessing.mrinfoutil.is_fullsphere()``. Even though datasets
21+
may be half-shelled, it is inaccurate to label them as such because
22+
distortion relative to b-value is not linear. As such, the
23+
``slm=linear`` makes no sense. This new method performs the proper
24+
checks required before labelling a DWI as fully-shelled. A DWI is
25+
half-shelled iff max B-value is less than 3000 AND the norm of the
26+
mean direction vector is more than 0.3.
27+
28+
**Removed**
29+
30+
* See above
31+
732
`v1.0_RC5`_
833
-----------
934

@@ -247,6 +272,7 @@ Initial port of MATLAB code to Python. 200,000,000,000 BCE
247272

248273
.. Links
249274
275+
.. _v1.0_RC6: https://github.com/m-ama/PyDesigner/releases/tag/v1.0_RC6
250276
.. _v1.0_RC5: https://github.com/m-ama/PyDesigner/releases/tag/v1.0_RC5
251277
.. _v1.0_RC4: https://github.com/m-ama/PyDesigner/releases/tag/v1.0_RC4
252278
.. _v1.0_RC3: https://github.com/m-ama/PyDesigner/releases/tag/v1.0_RC3

designer/info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
)
1212
__packagename__ = 'PyDesigner'
13-
__version__='v1.0-RC5'
13+
__version__='v1.0-RC6'
1414
__author__ = 'PyDesigner developers'
1515
__copyright__ = 'Copyright 2020, PyDesigner developers, MUSC Advanced Image Analysis (MAMA)'
1616
__credits__ = [

designer/preprocessing/mrinfoutil.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,152 @@ def pescheme(path):
353353
nums.append(float(num))
354354
pe_scheme.append(nums)
355355
return pe_scheme
356+
357+
def num_shells(path):
358+
"""
359+
Returns the number of b-value shells detected in input file
360+
361+
Parameters
362+
----------
363+
path : str
364+
Path to input image or directory
365+
366+
Returns
367+
-------
368+
int
369+
Number of shells
370+
"""
371+
if not op.exists(path):
372+
raise OSError('Input path does not exist. Please ensure that the '
373+
'folder or file specified exists.')
374+
ftype = format(path)
375+
if ftype != 'MRtrix':
376+
raise IOError('This function only works with MRtrix (.mif) '
377+
'formatted filetypes. Please ensure that the input '
378+
'filetype meets this requirement')
379+
arg = ['mrinfo', '-shell_bvalues']
380+
arg.append(path)
381+
completion = subprocess.run(arg, stdout=subprocess.PIPE)
382+
if completion.returncode != 0:
383+
raise IOError('Input {} is not currently supported by '
384+
'PyDesigner.'.format(path))
385+
# Remove new line delimiter
386+
console = str(completion.stdout).split('\\n')
387+
# Remove 'b'
388+
console[0] = console[0][1:]
389+
# Remove quotes
390+
console = [s.replace("'", "") for s in console]
391+
# Condense empty strings
392+
console = [s.replace('"', '') for s in console]
393+
# Remove empty strings form list
394+
console.remove('')
395+
# Split spaces
396+
console = [s.split(' ') for s in console]
397+
console = [item for sublist in console for item in sublist]
398+
console = list(filter(None, console))
399+
return len(console)
400+
401+
def max_shell(path):
402+
"""
403+
Returns the maximum b-value shell in DWI
404+
405+
Parameters
406+
----------
407+
path : str
408+
Path to input image or directory
409+
410+
Returns
411+
-------
412+
int
413+
Max b-value
414+
"""
415+
if not op.exists(path):
416+
raise OSError('Input path does not exist. Please ensure that the '
417+
'folder or file specified exists.')
418+
ftype = format(path)
419+
if ftype != 'MRtrix':
420+
raise IOError('This function only works with MRtrix (.mif) '
421+
'formatted filetypes. Please ensure that the input '
422+
'filetype meets this requirement')
423+
arg = ['mrinfo', '-shell_bvalues']
424+
arg.append(path)
425+
completion = subprocess.run(arg, stdout=subprocess.PIPE)
426+
if completion.returncode != 0:
427+
raise IOError('Input {} is not currently supported by '
428+
'PyDesigner.'.format(path))
429+
# Remove new line delimiter
430+
console = str(completion.stdout).split('\\n')
431+
# Remove 'b'
432+
console[0] = console[0][1:]
433+
# Remove quotes
434+
console = [s.replace("'", "") for s in console]
435+
# Condense empty strings
436+
console = [s.replace('"', '') for s in console]
437+
# Remove empty strings form list
438+
console.remove('')
439+
# Split spaces
440+
console = [s.split(' ') for s in console]
441+
console = [item for sublist in console for item in sublist]
442+
console = list(filter(None, console))
443+
console = [round(int(s)) for s in console]
444+
print(console)
445+
return max(console)
446+
447+
def is_fullsphere(path):
448+
"""
449+
Returns boolean value indicating whether input file has full
450+
spherical sampling
451+
452+
Parameters
453+
----------
454+
path : str
455+
Path to input image or directory
456+
457+
Returns
458+
-------
459+
bool
460+
True if full spherical sampling
461+
False if half-spherical sampling
462+
"""
463+
if not op.exists(path):
464+
raise OSError('Input path does not exist. Please ensure that the '
465+
'folder or file specified exists.')
466+
ftype = format(path)
467+
if ftype != 'MRtrix':
468+
raise IOError('This function only works with MRtrix (.mif) '
469+
'formatted filetypes. Please ensure that the input '
470+
'filetype meets this requirement')
471+
arg = ['dirstat', path, '-output', 'ASYM']
472+
completion = subprocess.run(arg, stdout=subprocess.PIPE)
473+
if completion.returncode != 0:
474+
raise IOError('Input {} is not currently supported by '
475+
'PyDesigner.'.format(path))
476+
# Remove new line delimiter
477+
console = str(completion.stdout).split('\\n')
478+
# Remove 'b'
479+
console[0] = console[0][1:]
480+
# Remove quotes
481+
console = [s.replace("'", "") for s in console]
482+
# Condense empty strings
483+
console = [s.replace('"', '') for s in console]
484+
# Remove empty strings form list
485+
console.remove('')
486+
# Convert strings to list
487+
console = [float(s) for s in console]
488+
# Find mean of all b-shells and round to nearest one decimal place
489+
mean_dir = round(sum(console) / len(console), 1)
490+
# Having too many b-value shells for protocols such as FBI or
491+
# HARDI, eddy correction can create a lot of trouble. If that's
492+
# the case, we just assume that data is fully shelled. This
493+
# disables heuristics testing performed by eddy correction. A DWI
494+
# will return half-shelled if and only if the norm of mean
495+
# direction vector is greater that 0.3 AND has max b-value less
496+
# than or equal to 3000. The linear model breaks down for high
497+
# b-values.
498+
if mean_dir < 0.3:
499+
return True
500+
else:
501+
if max_shell(path) > 3000:
502+
return True
503+
else:
504+
return False

designer/preprocessing/mrpreproc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def undistort(input, output, rpe='rpe_header', epib0=1,
376376
arg.extend(['-nthreads', str(nthreads)])
377377
# Determine whether half or full sphere sampling
378378
repol_string = '--repol '
379-
if util.bvec_is_fullsphere(op.join(outdir, 'dwiec.bvec')):
379+
if mrinfoutil.is_fullsphere(input):
380380
# is full, add appropriate dwifslpreproc option
381381
repol_string += '--data_is_shelled'
382382
else:

designer/preprocessing/util.py

Lines changed: 0 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -13,87 +13,6 @@
1313
import warnings
1414
from designer.preprocessing import mrinfoutil
1515

16-
def bvec_is_fullsphere(bvec):
17-
"""
18-
Determines if .bvec file is full or half-sphere
19-
20-
Parameters
21-
----------
22-
bvec : str
23-
The filename of the bvec
24-
25-
Returns
26-
-------
27-
bool
28-
True if full-sphere, False if half
29-
"""
30-
31-
# Check for existence
32-
if not op.exists(bvec):
33-
raise Exception('.bvec file '+bvec+' does not exist.')
34-
# Attempt to load file
35-
try:
36-
data = np.loadtxt(bvec)
37-
except:
38-
raise Exception('.bvec file '+bvec+' cannot be read.')
39-
40-
# Transpose data so 2nd dimension is the axis i,j,k (x,y,z)
41-
data = np.transpose(data)
42-
# Check that data has correct dimensions
43-
size_dim0 = np.size(data, 0)
44-
if np.size(data, 1) != 3:
45-
raise Exception('.bvec file '+bvec+' is not 3D; is '+str(size_dim0))
46-
47-
return vecs_are_fullsphere(data)
48-
49-
def vecs_are_fullsphere(bvecs):
50-
"""
51-
Determines if input vectors are full or half-sphere
52-
53-
Parameters
54-
----------
55-
bvecs : ndarray
56-
Aray of size [n_vectors x 3]
57-
58-
Returns
59-
-------
60-
bool
61-
True if full-sphere, False if half
62-
63-
Notes
64-
-----
65-
Adapted from Chris Rorden's nii_preprocess as seen here:
66-
https://github.com/neurolabusc/nii_preprocess/blob/dd1c84f23f8828923dd5fc493a22156b7006a3d4/nii_preprocess.m#L1786-L1824
67-
and reproduced from the original designer pipeline here:
68-
https://github.com/m-ama/PyDesigner/blob/7a39ec4cb9679f1c3e9ead68baa8f8c111b0618a/designer/designer.py#L347-L368
69-
"""
70-
# TODO: figure out why this math works
71-
# Check dimensions
72-
if np.size(bvecs, 1) != 3:
73-
raise Exception('bvecs are not 3-dimensional.')
74-
# Remove NaNs
75-
bvecs[~np.isnan(bvecs.any(axis=1))]
76-
# Remove any 0-vectors
77-
bvecs[~np.all(bvecs == 0, axis=1)]
78-
# Assume half-sphere if no vectors remaining
79-
if not bvecs.any():
80-
raise Exception('bvecs do not point anywhere.')
81-
# Get mean length
82-
mean = np.mean(bvecs, 0)
83-
# Get x-component of direction
84-
x_component = np.sqrt(np.sum(np.power(mean, 2)))
85-
# Create a matrix to divide by this length
86-
Dx = np.repeat(x_component, 3)
87-
# Get mean unit length
88-
mean_ulength = np.divide(mean, Dx)
89-
# Scale by mean unit length
90-
mean = np.ones([np.size(bvecs, 0), 3])* mean_ulength
91-
# UNKNOWN
92-
minV = min(np.sum(bvecs.conj() * mean, axis=1))
93-
# Get angle in degrees
94-
theta = math.degrees(math.acos(minV))
95-
return (theta >= 110)
96-
9716
def find_valid_ext(pathname):
9817
"""
9918
Finds valid extensions for dwifile, helper function

0 commit comments

Comments
 (0)