Skip to content

Commit 9fb39b7

Browse files
committed
Merge pull request #138 from moloney/csa_slice_norm
BF: Only use the cross product (possibly negated) for the slice normal. The information for the slice normal can come from the crossproduct of the within-slice axes, or the private Siemens CSA header. These changes check that these are more or less the same apart from a sign flip, and then flips the cross-product vector if there is a sign flip in the CSA header slice normal.
2 parents dcd5fa3 + fcff0cd commit 9fb39b7

File tree

4 files changed

+52
-9
lines changed

4 files changed

+52
-9
lines changed

nibabel/nicom/dicomwrappers.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -407,14 +407,28 @@ def __init__(self, dcm_data=None, csa_header=None):
407407

408408
@one_time
409409
def slice_normal(self):
410-
slice_normal = csar.get_slice_normal(self.csa_header)
411-
if not slice_normal is None:
412-
return np.array(slice_normal)
413-
iop = self.image_orient_patient
414-
if iop is None:
410+
#The std_slice_normal comes from the cross product of the directions
411+
#in the ImageOrientationPatient
412+
std_slice_normal = super(SiemensWrapper, self).slice_normal
413+
csa_slice_normal = csar.get_slice_normal(self.csa_header)
414+
if std_slice_normal is None and csa_slice_normal is None:
415415
return None
416-
return np.cross(*iop.T[:])
417-
416+
elif std_slice_normal is None:
417+
return np.array(csa_slice_normal)
418+
elif csa_slice_normal is None:
419+
return std_slice_normal
420+
else:
421+
#Make sure the two normals are very close to parallel unit vectors
422+
dot_prod = np.dot(csa_slice_normal, std_slice_normal)
423+
assert np.allclose(np.fabs(dot_prod), 1.0, atol=1e-5)
424+
#Use the slice normal computed with the cross product as it will
425+
#always be the most orthogonal, but take the sign from the CSA
426+
#slice normal
427+
if dot_prod < 0:
428+
return -std_slice_normal
429+
else:
430+
return std_slice_normal
431+
418432
@one_time
419433
def series_signature(self):
420434
''' Add ICE dims from CSA header to signature '''
13.1 KB
Binary file not shown.

nibabel/nicom/tests/test_dicomreaders.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ def test_read_dwi():
2727

2828
@dicom_test
2929
def test_read_dwis():
30-
data, aff, bs, gs = didr.read_mosaic_dwi_dir(IO_DATA_PATH, '*.dcm.gz')
30+
data, aff, bs, gs = didr.read_mosaic_dwi_dir(IO_DATA_PATH,
31+
'siemens_dwi_*.dcm.gz')
3132
assert_equal(data.ndim, 4)
3233
assert_array_almost_equal(aff, EXPECTED_AFFINE)
3334
assert_array_almost_equal(bs, (0, EXPECTED_PARAMS[0]))

nibabel/nicom/tests/test_dicomwrappers.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
else:
3131
DATA = None
3232
DATA_FILE_B0 = pjoin(IO_DATA_PATH, 'siemens_dwi_0.dcm.gz')
33+
DATA_FILE_SLC_NORM = pjoin(IO_DATA_PATH, 'csa_slice_norm.dcm')
3334

3435
# This affine from our converted image was shown to match our image
3536
# spatially with an image from SPM DICOM conversion. We checked the
@@ -48,7 +49,6 @@
4849
0.99997450,
4950
-0.005023611)]
5051

51-
5252
@dicom_test
5353
def test_wrappers():
5454
# test direct wrapper calls
@@ -143,3 +143,31 @@ def test_slice_indicator():
143143
assert_equal(z, dw_1000.slice_indicator)
144144
dw_empty = didw.Wrapper()
145145
assert_true(dw_empty.slice_indicator is None)
146+
147+
148+
@dicom_test
149+
def test_orthogonal():
150+
#Test that the slice normal is sufficiently orthogonal
151+
dw = didw.wrapper_from_file(DATA_FILE_SLC_NORM)
152+
R = dw.rotation_matrix
153+
assert np.allclose(np.eye(3),
154+
np.dot(R, R.T),
155+
atol=1e-6)
156+
157+
@dicom_test
158+
def test_use_csa_sign():
159+
#Test that we get the same slice normal, even after swapping the iop
160+
#directions
161+
dw = didw.wrapper_from_file(DATA_FILE_SLC_NORM)
162+
iop = dw.image_orient_patient
163+
dw.image_orient_patient = np.c_[iop[:,1], iop[:,0]]
164+
dw2 = didw.wrapper_from_file(DATA_FILE_SLC_NORM)
165+
assert np.allclose(dw.slice_normal, dw2.slice_normal)
166+
167+
@dicom_test
168+
def test_assert_parallel():
169+
#Test that we get an AssertionError if the cross product and the CSA
170+
#slice normal are not parallel
171+
dw = didw.wrapper_from_file(DATA_FILE_SLC_NORM)
172+
dw.image_orient_patient = np.c_[[1., 0., 0.], [0., 1., 0.]]
173+
assert_raises(AssertionError, dw.__getattribute__, 'slice_normal')

0 commit comments

Comments
 (0)