Skip to content

Commit db7b04d

Browse files
authored
Merge pull request #47 from yhuang43/master
add shear components to VOL_GEOM
2 parents f25a0c2 + eba77bb commit db7b04d

File tree

4 files changed

+120
-50
lines changed

4 files changed

+120
-50
lines changed

surfa/io/framed.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,16 @@ def load(self, filename, atype):
352352
arr.metadata['target-valid'] = valid
353353
arr.metadata['target-fname'] = fname
354354

355+
# gcamorph src & trg geoms (mgz warp)
356+
elif tag == fsio.tags.gcamorph_geom_plusshear:
357+
arr.source, valid, fname = read_geom(file, shearless=False)
358+
arr.metadata['source-valid'] = valid
359+
arr.metadata['source-fname'] = fname
360+
361+
arr.target, valid, fname = read_geom(file, shearless=False)
362+
arr.metadata['target-valid'] = valid
363+
arr.metadata['target-fname'] = fname
364+
355365
# gcamorph meta (mgz warp: int int float)
356366
elif tag == fsio.tags.gcamorph_meta:
357367
arr.format = read_bytes(file, dtype='>i4')
@@ -463,8 +473,10 @@ def save(self, arr, filename, intent=FramedArrayIntents.mri):
463473
write_bytes(file, arr.metadata.get('field-strength', 0.0), '>f4')
464474

465475
# gcamorph geom and gcamorph meta for mgz warp
476+
# output both fsio.tags.gcamorph_geom and fsio.tags.gcamorph_geom_plusshear
466477
if intent == FramedArrayIntents.warpmap:
467478
# gcamorph src & trg geoms (mgz warp)
479+
# fsio.tags.gcamorph_geom
468480
fsio.write_tag(file, fsio.tags.gcamorph_geom)
469481
write_geom(file,
470482
geom=arr.source,
@@ -475,6 +487,21 @@ def save(self, arr, filename, intent=FramedArrayIntents.mri):
475487
valid=arr.metadata.get('target-valid', True),
476488
fname=arr.metadata.get('target-fname', ''))
477489

490+
# fsio.tags.gcamorph_geom_plusshear
491+
# gcamorph_geom_plusshear has a length, datalength needs to be consistent with write_geom()
492+
datalength = 1200
493+
fsio.write_tag(file, fsio.tags.gcamorph_geom_plusshear, datalength)
494+
write_geom(file,
495+
geom=arr.source,
496+
valid=arr.metadata.get('source-valid', True),
497+
fname=arr.metadata.get('source-fname', ''),
498+
shearless=False)
499+
write_geom(file,
500+
geom=arr.target,
501+
valid=arr.metadata.get('target-valid', True),
502+
fname=arr.metadata.get('target-fname', ''),
503+
shearless=False)
504+
478505
# gcamorph meta (mgz warp: int int float)
479506
fsio.write_tag(file, fsio.tags.gcamorph_meta, 12)
480507
write_bytes(file, arr.format, dtype='>i4')

surfa/io/fsio.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class tags:
1919
gcamorph_labels = 12
2020
gcamorph_meta = 13 # introduced for mgz warpfield
2121
gcamorph_affine = 14 # introduced for mgz warpfield (m3z outputs the same information under xform)
22+
gcamorph_geom_plusshear = 15 # information output under gcamorph_geom + shear components
2223
old_surf_geom = 20
2324
surf_geom = 21
2425
surf_dataspace = 22 # surface tag - surface [x y z] space

surfa/io/fsnifti1extension.py

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@ class FSNifti1Extension:
1515
This class handles Freesurfer Nifti1 header extension IO.
1616
1717
Class variables:
18+
_verbose: if it is True, output debug information
1819
_content: FSNifti1Extension.Content
1920
"""
2021

21-
def __init__(self):
22+
def __init__(self, verbose=False):
2223
"""
2324
FSNifti1Extension Constructor
2425
"""
2526

2627
# initialization
28+
self._verbose = verbose
2729
self._content = FSNifti1Extension.Content()
2830

2931

@@ -56,8 +58,9 @@ def read(self, fileobj, esize, offset=0):
5658
self.content.intent = int.from_bytes(fsexthdr[1:3], byteorder='big')
5759
self.content.version = fsexthdr[3]
5860

59-
print(f'[DEBUG] FSNifti1Extension.read(): esize = {esize:6d}')
60-
print(f'[DEBUG] FSNifti1Extension.read(): endian = \'{self.content.endian:c}\', intent = {self.content.intent:d}, version = {self.content.version:d}')
61+
if (self._verbose):
62+
print(f'[DEBUG] FSNifti1Extension.read(): esize = {esize:6d}')
63+
print(f'[DEBUG] FSNifti1Extension.read(): endian = \'{self.content.endian:c}\', intent = {self.content.intent:d}, version = {self.content.version:d}')
6164

6265
# process Freesurfer Nifti1 extension tag data
6366
tagdatalen = esize - 12 # exclude esize, ecode, fsexthdr
@@ -66,7 +69,8 @@ def read(self, fileobj, esize, offset=0):
6669
# read tagid (4 bytes), data-length (8 bytes)
6770
(tag, length) = FSNifti1Extension.read_tag(fileobj)
6871

69-
print(f'[DEBUG] FSNifti1Extension.read(): remaining taglen = {tagdatalen:6d} (tag = {tag:2d}, length = {length:5d})')
72+
if (self._verbose):
73+
print(f'[DEBUG] FSNifti1Extension.read(): remaining taglen = {tagdatalen:6d} (tag = {tag:2d}, length = {length:5d})')
7074

7175
if (tag == 0):
7276
break
@@ -88,20 +92,20 @@ def read(self, fileobj, esize, offset=0):
8892
dof = iou.read_int(fileobj, length)
8993
self.content.dof = dof
9094

91-
# ras_xform (TAG_RAS_XFORM = 8)
92-
elif (tag == FSNifti1Extension.Tags.ras_xform):
93-
self.content.ras_xform = dict(
94-
rotation = iou.read_bytes(fileobj, '>f4', 9).reshape((3, 3), order='F'),
95-
center = iou.read_bytes(fileobj, '>f4', 3)
96-
)
97-
9895
# gcamorph src & trg geoms (warp) (TAG_GCAMORPH_GEOM = 10)
9996
elif (tag == FSNifti1Extension.Tags.gcamorph_geom):
10097
if (not self.content.warpmeta):
10198
self.content.warpmeta = {}
10299

103100
(self.content.warpmeta['source-geom'], self.content.warpmeta['source-valid'], self.content.warpmeta['source-fname']) = iou.read_geom(fileobj, niftiheaderext=True)
104101
(self.content.warpmeta['target-geom'], self.content.warpmeta['target-valid'], self.content.warpmeta['target-fname']) = iou.read_geom(fileobj, niftiheaderext=True)
102+
# gcamorph src & trg geoms (warp) (TAG_GCAMORPH_GEOM_PLUSSHEAR = 15)
103+
elif (tag == FSNifti1Extension.Tags.gcamorph_geom_plusshear):
104+
if (not self.content.warpmeta):
105+
self.content.warpmeta = {}
106+
107+
(self.content.warpmeta['source-geom'], self.content.warpmeta['source-valid'], self.content.warpmeta['source-fname']) = iou.read_geom(fileobj, niftiheaderext=True, shearless=False)
108+
(self.content.warpmeta['target-geom'], self.content.warpmeta['target-valid'], self.content.warpmeta['target-fname']) = iou.read_geom(fileobj, niftiheaderext=True, shearless=False)
105109
# gcamorph meta (warp: int int float) (TAG_GCAMORPH_META = 13)
106110
elif (tag == FSNifti1Extension.Tags.gcamorph_meta):
107111
if (not self.content.warpmeta):
@@ -129,7 +133,8 @@ def read(self, fileobj, esize, offset=0):
129133
# check if we reach the end
130134
tagdatalen -= (len_tagheader + length)
131135
if (tagdatalen < len_tagheader):
132-
print(f'[DEBUG] FSNifti1Extension.read(): remaining taglen = {tagdatalen:6d}')
136+
if (self._verbose):
137+
print(f'[DEBUG] FSNifti1Extension.read(): remaining taglen = {tagdatalen:6d}')
133138
break;
134139

135140
return self.content
@@ -163,7 +168,8 @@ def write(self, fileobj, content, countbytesonly=False):
163168
iou.write_int(fileobj, content.version, size=1, byteorder='big')
164169

165170
num_bytes = 4
166-
print(f'[DEBUG] FSNifti1Extension.write(): dlen = {num_bytes:6d}')
171+
if (self._verbose):
172+
print(f'[DEBUG] FSNifti1Extension.write(): dlen = {num_bytes:6d}')
167173

168174
# tag data
169175
addtaglength = 12
@@ -175,7 +181,8 @@ def write(self, fileobj, content, countbytesonly=False):
175181
tag = FSNifti1Extension.Tags.gcamorph_geom
176182
length = FSNifti1Extension.getlen_gcamorph_geom(source_fname, target_fname)
177183
num_bytes += length + addtaglength
178-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
184+
if (self._verbose):
185+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
179186
if (not countbytesonly):
180187
FSNifti1Extension.write_tag(fileobj, tag, length)
181188
iou.write_geom(fileobj,
@@ -189,11 +196,32 @@ def write(self, fileobj, content, countbytesonly=False):
189196
fname=target_fname,
190197
niftiheaderext=True)
191198

199+
tag = FSNifti1Extension.Tags.gcamorph_geom_plusshear
200+
length = FSNifti1Extension.getlen_gcamorph_geom(source_fname, target_fname, shearless=False)
201+
num_bytes += length + addtaglength
202+
if (self._verbose):
203+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
204+
if (not countbytesonly):
205+
FSNifti1Extension.write_tag(fileobj, tag, length)
206+
iou.write_geom(fileobj,
207+
geom=content.warpmeta['source-geom'],
208+
valid=content.warpmeta.get('source-valid', True),
209+
fname=source_fname,
210+
niftiheaderext=True,
211+
shearless=False)
212+
iou.write_geom(fileobj,
213+
geom=content.warpmeta['target-geom'],
214+
valid=content.warpmeta.get('target-valid', True),
215+
fname=target_fname,
216+
niftiheaderext=True,
217+
shearless=False)
218+
192219
# gcamorph meta (warp: int int float) (TAG_GCAMORPH_META = 13)
193220
tag = FSNifti1Extension.Tags.gcamorph_meta
194221
length = 12
195222
num_bytes += length + addtaglength
196-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
223+
if (self._verbose):
224+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
197225
if (not countbytesonly):
198226
FSNifti1Extension.write_tag(fileobj, tag, length)
199227
iou.write_bytes(fileobj, content.warpmeta['format'], dtype='>i4')
@@ -206,7 +234,8 @@ def write(self, fileobj, content, countbytesonly=False):
206234
tag = FSNifti1Extension.Tags.end_data
207235
length = 1 # extra char '*'
208236
num_bytes += length + addtaglength
209-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
237+
if (self._verbose):
238+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
210239
if (not countbytesonly):
211240
FSNifti1Extension.write_tag(fileobj, tag, length)
212241
extrachar = '*'
@@ -220,7 +249,8 @@ def write(self, fileobj, content, countbytesonly=False):
220249
tag = FSNifti1Extension.Tags.old_colortable
221250
length = FSNifti1Extension.getlen_labels(content.labels)
222251
num_bytes += length + addtaglength
223-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
252+
if (self._verbose):
253+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
224254
if (not countbytesonly):
225255
FSNifti1Extension.write_tag(fileobj, tag, length)
226256
fsio.write_binary_lookup_table(fileobj, content.labels)
@@ -231,7 +261,8 @@ def write(self, fileobj, content, countbytesonly=False):
231261
tag = FSNifti1Extension.Tags.history
232262
length = len(hist)
233263
num_bytes += length + addtaglength
234-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
264+
if (self._verbose):
265+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
235266
if (not countbytesonly):
236267
FSNifti1Extension.write_tag(fileobj, tag, length)
237268
fileobj.write(hist.encode('utf-8'))
@@ -240,28 +271,19 @@ def write(self, fileobj, content, countbytesonly=False):
240271
tag = FSNifti1Extension.Tags.dof
241272
length = 4
242273
num_bytes += length + addtaglength
243-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
274+
if (self._verbose):
275+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
244276
if (not countbytesonly):
245277
FSNifti1Extension.write_tag(fileobj, tag, length)
246278
iou.write_int(fileobj, content.dof, size=4)
247279

248-
# ras_xform (TAG_RAS_XFORM = 8)
249-
if (content.ras_xform):
250-
tag = FSNifti1Extension.Tags.ras_xform
251-
length = 48
252-
num_bytes += length + addtaglength
253-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
254-
if (not countbytesonly):
255-
FSNifti1Extension.write_tag(fileobj, tag, length)
256-
iou.write_bytes(fileobj, np.ravel(content.ras_xform['rotation'], order='F'), '>f4')
257-
iou.write_bytes(fileobj, content.ras_xform['center'], '>f4')
258-
259280
# scan_parameters (TAG_SCAN_PARAMETERS = 45)
260281
if (content.scan_parameters):
261282
tag = FSNifti1Extension.Tags.scan_parameters
262283
length = 20 + len(content.scan_parameters['pedir'])
263284
num_bytes += length + addtaglength
264-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
285+
if (self._verbose):
286+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
265287
if (not countbytesonly):
266288
FSNifti1Extension.write_tag(fileobj, tag, length)
267289
iou.write_bytes(fileobj, content.scan_parameters['te'], '>f4')
@@ -284,7 +306,8 @@ def write(self, fileobj, content, countbytesonly=False):
284306
tag = FSNifti1Extension.Tags.end_data
285307
length = 1 # extra char '*'
286308
num_bytes += length + addtaglength
287-
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
309+
if (self._verbose):
310+
print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}')
288311
if (not countbytesonly):
289312
FSNifti1Extension.write_tag(fileobj, tag, length)
290313
extrachar = '*'
@@ -364,7 +387,7 @@ def getlen_labels(labels):
364387

365388

366389
@staticmethod
367-
def getlen_gcamorph_geom(fname_source, fname_target):
390+
def getlen_gcamorph_geom(fname_source, fname_target, shearless=True):
368391
"""
369392
Calculate total bytes that will be written for the labels.
370393
@@ -383,6 +406,8 @@ def getlen_gcamorph_geom(fname_source, fname_target):
383406

384407
# See freesurfer/utils/fstagsio.cpp::getlen_gcamorph_geom().
385408
num_bytes = 2 * 80
409+
if (not shearless):
410+
num_bytes += 2 * 12 # 2 * 3 * 4 bytes (3 shear float numbers each)
386411
num_bytes += len(fname_source)
387412
num_bytes += len(fname_target)
388413

@@ -407,16 +432,20 @@ class Tags:
407432
408433
This class defines the tags recognized in surfa.
409434
It is a subset of tag IDs defined in freesurfer/include/tags.h
435+
436+
TAG_RAS_XFORM is also output by Freesurfer for FS nifti1 header extension only to store shearless rotation and center parameters.
437+
This is to prevent precision loss from sform/qform decompose and compose when nii is read and written in Freesurfer.
438+
The tag is not necessary for Surfa.
410439
"""
411440

412-
old_colortable = 1 # TAG_OLD_COLORTABLE
413-
history = 3 # TAG_CMDLINE
414-
dof = 7 # TAG_DOF
415-
ras_xform = 8 # TAG_RAS_XFORM
416-
gcamorph_geom = 10 # TAG_GCAMORPH_GEOM
417-
gcamorph_meta = 13 # TAG_GCAMORPH_META
418-
scan_parameters = 45 # TAG_SCAN_PARAMETERS
419-
end_data = -1 # TAG_END_NIIHDREXTENSION
441+
old_colortable = 1 # TAG_OLD_COLORTABLE
442+
history = 3 # TAG_CMDLINE
443+
dof = 7 # TAG_DOF
444+
gcamorph_geom = 10 # TAG_GCAMORPH_GEOM
445+
gcamorph_meta = 13 # TAG_GCAMORPH_META
446+
gcamorph_geom_plusshear = 15 # information output under gcamorph_geom + shear components
447+
scan_parameters = 45 # TAG_SCAN_PARAMETERS
448+
end_data = -1 # TAG_END_NIIHDREXTENSION
420449

421450

422451
class Content:

surfa/io/utils.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def write_bytes(file, value, dtype):
108108
file.write(np.asarray(value).astype(dtype, copy=False).tobytes())
109109

110110

111-
def read_geom(file, niftiheaderext=False):
111+
def read_geom(file, niftiheaderext=False, shearless=True):
112112
"""
113113
Read an image geometry from a binary file buffer. See VOL_GEOM.read() in mri.h.
114114
@@ -129,12 +129,21 @@ def read_geom(file, niftiheaderext=False):
129129
If True, write for nifti header extension.
130130
"""
131131
valid = bool(read_bytes(file, '>i4', 1))
132-
geom = ImageGeometry(
133-
shape=read_bytes(file, '>i4', 3).astype(int),
134-
voxsize=read_bytes(file, '>f4', 3),
135-
rotation=read_bytes(file, '>f4', 9).reshape((3, 3), order='F'),
136-
center=read_bytes(file, '>f4', 3),
137-
)
132+
if (shearless):
133+
geom = ImageGeometry(
134+
shape=read_bytes(file, '>i4', 3).astype(int),
135+
voxsize=read_bytes(file, '>f4', 3),
136+
rotation=read_bytes(file, '>f4', 9).reshape((3, 3), order='F'),
137+
center=read_bytes(file, '>f4', 3),
138+
)
139+
else:
140+
geom = ImageGeometry(
141+
shape=read_bytes(file, '>i4', 3).astype(int),
142+
voxsize=read_bytes(file, '>f4', 3),
143+
rotation=read_bytes(file, '>f4', 9).reshape((3, 3), order='F'),
144+
center=read_bytes(file, '>f4', 3),
145+
shear=read_bytes(file, '>f4', 3)
146+
)
138147

139148
len_fname_max = 512
140149
if (not niftiheaderext):
@@ -154,7 +163,7 @@ def read_geom(file, niftiheaderext=False):
154163
return geom, valid, fname
155164

156165

157-
def write_geom(file, geom, valid=True, fname='', niftiheaderext=False):
166+
def write_geom(file, geom, valid=True, fname='', niftiheaderext=False, shearless=True):
158167
"""
159168
Write an image geometry to a binary file buffer. See VOL_GEOM.write() in mri.h.
160169
@@ -173,11 +182,15 @@ def write_geom(file, geom, valid=True, fname='', niftiheaderext=False):
173182
"""
174183
write_bytes(file, valid, '>i4')
175184

176-
voxsize, rotation, center = geom.shearless_components()
185+
voxsize, rotation, center, shear = geom.voxsize, geom.rotation, geom.center, geom.shear
186+
if (shearless):
187+
voxsize, rotation, center = geom.shearless_components()
177188
write_bytes(file, geom.shape, '>i4')
178189
write_bytes(file, voxsize, '>f4')
179190
write_bytes(file, np.ravel(rotation, order='F'), '>f4')
180191
write_bytes(file, center, '>f4')
192+
if (not shearless):
193+
write_bytes(file, shear, '>f4')
181194

182195
len_fname_max = 512
183196
if (not niftiheaderext):

0 commit comments

Comments
 (0)