Skip to content

Commit 78aee47

Browse files
authored
Merge pull request #56 from JuliaNeuroscience/orientation
Orientation method for getting dim-names from affine (#55)
2 parents 7af45a2 + 24b4aaf commit 78aee47

File tree

3 files changed

+263
-1
lines changed

3 files changed

+263
-1
lines changed

Diff for: Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "NIfTI"
22
uuid = "a3a9e032-41b5-5fc4-967a-a6b7a19844d3"
3-
version = "0.5.5"
3+
version = "0.5.6"
44

55
[deps]
66
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

Diff for: src/coordinates.jl

+249
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
11

2+
@inline function _det(r11, r12, r13, r21, r22, r23, r31, r32, r33)
3+
r11 * r22 * r33 -
4+
r11 * r32 * r23 -
5+
r21 * r12 * r33 +
6+
r21 * r32 * r13 +
7+
r31 * r12 * r23 -
8+
r31 * r22 * r13
9+
end
10+
11+
@inline function _mul_trace(
12+
x11, x12, x13, x21, x22, x23, x31, x32, x33,
13+
y11, y12, y13, y21, y22, y23, y31, y32, y33
14+
)
15+
16+
return (x11 * y11 + x12 * y21 + x13 * y31) + # z11
17+
(x21 * y12 + x22 * y22 + x23 * y32) + # z22
18+
(x31 * y13 + x32 * y23 + x33 * y33) # z33
19+
end
20+
221
get_sform(x::NIVolume) = get_sform(x.header)
322
function get_sform(hdr::NIfTI1Header)
423
if hdr.sform_code > 0
@@ -89,3 +108,233 @@ function setaffine(h::NIfTI1Header, affine::Array{T,2}) where {T}
89108
h
90109
end
91110

111+
"""
112+
orientation(img)::Tuple{Symbol,Symbol,Symbol}
113+
114+
Returns a tuple providing the orientation of a NIfTI image.
115+
"""
116+
orientation(x) = orientation(x.header)
117+
function orientation(hdr::NIfTI1Header)
118+
if hdr.sform_code > 0
119+
return @inbounds _dir2ori(
120+
hdr.srow_x[1], hdr.srow_x[2], hdr.srow_x[3],
121+
hdr.srow_y[1], hdr.srow_y[2], hdr.srow_y[3],
122+
hdr.srow_z[1], hdr.srow_z[2], hdr.srow_z[3]
123+
)
124+
elseif hdr.qform_code <= 0
125+
return @inbounds _dir2ori(
126+
hdr.pixdim[2], 0, 0,
127+
0, hdr.pixdim[3], 0,
128+
0, 0, hdr.pixdim[4]
129+
)
130+
else
131+
dx = hdr.pixdim[2]
132+
dy = hdr.pixdim[3]
133+
# aka qfac left handedness
134+
if hdr.pixdim[1] < 0
135+
dz = -hdr.pixdim[4]
136+
else
137+
dz = hdr.pixdim[4]
138+
end
139+
b = hdr.quatern_b
140+
c = hdr.quatern_c
141+
d = hdr.quatern_d
142+
b2 = b*b
143+
c2 = c*c
144+
d2 = d*d
145+
a = 1 - b2 - c2 - d2
146+
if a < 1.e-7
147+
a = 1 / sqrt(b2 + c2 + d2)
148+
b *= a
149+
c *= a
150+
d *= a # normalize (b,c,d) vector
151+
a = zero(a) # a = 0 ==> 180 degree rotation
152+
else
153+
a = sqrt(a) # angle = 2*arccos(a)
154+
end
155+
return _dir2ori(
156+
(a*a+b*b-c*c-d*d)*dx, (2*b*c-2*a*d)*dy, (2*b*d+2*a*c)*dz,
157+
(2*b*c+2*a*d)*dx, (a*a+c*c-b*b-d*d)*dy, (2*c*d-2*a*b)*dz,
158+
(2*b*d-2*a*c)*dx, (2*c*d+2*a*b)*dy, (a*a+d*d-c*c-b*b)*dz,
159+
)
160+
end
161+
end
162+
163+
_encoding_name(x) = _encoding_name(Int(x))
164+
@inline function _encoding_name(x::Int)
165+
if x === 1
166+
return :left
167+
elseif x === -1
168+
return :right
169+
elseif x === 2
170+
return :posterior
171+
elseif x === -2
172+
return :anterior
173+
elseif x === 3
174+
return :inferior
175+
elseif x === -3
176+
return :superior
177+
else
178+
error("$x does not map to a dimension name.")
179+
end
180+
end
181+
182+
function _dir2ori(xi, xj, xk, yi, yj, yk, zi, zj, zk)
183+
# Normalize column vectors to get unit vectors along each ijk-axis
184+
# normalize i axis
185+
val = sqrt(xi*xi + yi*yi + zi*zi)
186+
if val == 0
187+
error("Invalid rotation directions.")
188+
end
189+
xi /= val
190+
yi /= val
191+
zi /= val
192+
193+
# normalize j axis
194+
val = sqrt(xj*xj + yj*yj + zj*zj)
195+
if val == 0
196+
error("Invalid rotation directions.")
197+
end
198+
xj /= val
199+
yj /= val
200+
zj /= val
201+
202+
# orthogonalize j axis to i axis, if needed
203+
val = xi*xj + yi*yj + zi* zj # dot product between i and j
204+
if abs(val) > .0001
205+
xj -= val*xi
206+
yj -= val*yi
207+
zj -= val*zi
208+
209+
val = sqrt(xj*xj + yj*yj + zj*zj) # must renormalize
210+
if val == 0
211+
error("The first and second dimensions cannot be parallel.")
212+
end
213+
xj /= val
214+
yj /= val
215+
zj /= val
216+
end
217+
218+
# normalize k axis; if it is zero, make it the cross product i x j
219+
val = sqrt(xk*xk + yk*yk + zk*zk)
220+
if val == 0
221+
xk = yi*zj-zi*yj
222+
yk = zi*xj-zj*xi
223+
zk = xi*yj-yi*xj
224+
else
225+
xk = xk/val
226+
yk = yk/val
227+
zk = zk/val
228+
end
229+
230+
# orthogonalize k to i
231+
val = xi*xk + yi*yk + zi*zk # dot product between i and k
232+
if abs(val) > 0.0001
233+
xk -= val*xi
234+
yk -= val*yi
235+
zk -= val*zi
236+
237+
# must renormalize
238+
val = sqrt(xk*xk + yk*yk + zk*zk)
239+
if val == 0
240+
return 0 # I think this is suppose to be an error output
241+
end
242+
xk /= val
243+
yk /= val
244+
zk /= val
245+
end
246+
247+
# orthogonalize k to j */
248+
val = xj*xk + yj*yk + zj*zk # dot product between j and k
249+
if abs(val) > 0.0001
250+
xk -= val*xj
251+
yk -= val*yj
252+
zk -= val*zj
253+
254+
val = sqrt(xk*xk + yk*yk + zk*zk)
255+
if val == 0
256+
return 0 # bad
257+
end
258+
xk /= val
259+
yk /= val
260+
zk /= val
261+
end
262+
263+
# at this point Q is the rotation matrix from the (i,j,k) to (x,y,z) axes
264+
detQ = _det(xi, xj, xk, yi, yj, yk, zi, zj, zk)
265+
# if( detQ == 0.0 ) return ; /* shouldn't happen unless user is a DUFIS */
266+
267+
# Build and test all possible +1/-1 coordinate permutation matrices P;
268+
# then find the P such that the rotation matrix M=PQ is closest to the
269+
# identity, in the sense of M having the smallest total rotation angle.
270+
271+
# Despite the formidable looking 6 nested loops, there are
272+
# only 3*3*3*2*2*2 = 216 passes, which will run very quickly.
273+
vbest = -666
274+
ibest = pbest=qbest=rbest= 1
275+
jbest = 2
276+
kbest = 3
277+
for (i, j, k) in ((1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1))
278+
for p in (-1, 1) # p,q,r are -1 or +1
279+
for q in (-1, 1) # and go into rows 1,2,3
280+
for r in (-1, 1)
281+
p11, p12, p13 = _nval_other_zero(i, p)
282+
p21, p22, p23 = _nval_other_zero(j, q)
283+
p31, p32, p33 = _nval_other_zero(k, r)
284+
#=
285+
P[1,i] = p
286+
P[2,j] = q
287+
P[3,k] = r
288+
detP = det(P) # sign of permutation
289+
=#
290+
detP = _det(p11, p12, p13, p21, p22, p23, p31, p32, p33)
291+
# doesn't match sign of Q
292+
if detP * detQ >= 0.0
293+
# angle of M rotation = 2.0 * acos(0.5 * sqrt(1.0 + trace(M)))
294+
# we want largest trace(M) == smallest angle == M nearest to I
295+
val = _mul_trace(
296+
p11, p12, p13, p21, p22, p23, p31, p32, p33,
297+
xi, xj, xk, yi, yj, yk, zi, zj, zk
298+
)
299+
if val > vbest
300+
vbest = val
301+
ibest = i
302+
jbest = j
303+
kbest = k
304+
pbest = p
305+
qbest = q
306+
rbest = r
307+
end
308+
end
309+
end
310+
end
311+
end
312+
end
313+
# At this point ibest is 1 or 2 or 3; pbest is -1 or +1; etc.
314+
315+
# The matrix P that corresponds is the best permutation approximation
316+
# to Q-inverse; that is, P (approximately) takes (x,y,z) coordinates
317+
# to the (i,j,k) axes.
318+
319+
# For example, the first row of P (which contains pbest in column ibest)
320+
# determines the way the i axis points relative to the anatomical
321+
# (x,y,z) axes. If ibest is 2, then the i axis is along the y axis,
322+
# which is direction P2A (if pbest > 0) or A2P (if pbest < 0).
323+
324+
# So, using ibest and pbest, we can assign the output code for
325+
# the i axis. Mutatis mutandis for the j and k axes, of course.
326+
327+
return (_encoding_name(ibest*pbest), _encoding_name(jbest*qbest), _encoding_name(kbest*rbest))
328+
end
329+
330+
@inline function _nval_other_zero(n, val)
331+
if n === 1
332+
return val, 0, 0
333+
elseif n === 2
334+
return 0, val, 0
335+
else
336+
return 0, 0, val
337+
end
338+
end
339+
340+

Diff for: test/runtests.jl

+13
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ function image_tests(fname, mmap)
6060
6.30749f-27 0.323208 2.17108 -7.2488
6161
0.0 0.0 0.0 1.0
6262
]
63+
@test NIfTI.orientation(file) == (:right, :posterior, :inferior)
6364
end
6465

6566
image_tests(NII, false)
@@ -111,3 +112,15 @@ img = niread(joinpath(dirname(@__FILE__), "data/avg152T1_LR_nifti.nii.gz"))
111112

112113
GC.gc() # closes mmapped files
113114

115+
@test NIfTI._dir2ori(-1.0, 0.0, 0.0,
116+
0.0, 1.0, 0.0,
117+
0.0, 0.0, 1.0) == (:right, :posterior, :inferior)
118+
119+
@test NIfTI._dir2ori(1.0, 0.0, 0.0,
120+
0.0, -1.0, 0.0,
121+
0.0, 0.0, 1.0) == (:left, :anterior, :inferior)
122+
123+
124+
@test NIfTI._dir2ori(1.0, 0.0, 0.0,
125+
0.0, -1.0, 0.0,
126+
0.0, 0.0, -1.0) == (:left, :anterior, :superior)

0 commit comments

Comments
 (0)