From a2f7cc1459ff4d78765ee9742ba3c6b648c1cc8d Mon Sep 17 00:00:00 2001 From: Malte Hoffmann Date: Fri, 21 Jun 2024 12:50:40 -0400 Subject: [PATCH 1/4] Save warps in NIfTI format. --- surfa/io/fsnifti1extension.py | 12 ++++++------ surfa/transform/warp.py | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/surfa/io/fsnifti1extension.py b/surfa/io/fsnifti1extension.py index e217feb..fafbc07 100644 --- a/surfa/io/fsnifti1extension.py +++ b/surfa/io/fsnifti1extension.py @@ -553,17 +553,17 @@ def _from_framedimage(self, framedimage): # gcamorph src & trg geoms (mgz warp) self.warpmeta['source-geom'] = framedimage.source - self.warpmeta['source-valid'] = framedimage.metadata['source-valid'] - self.warpmeta['source-fname'] = framedimage.metadata['source-fname'] + self.warpmeta['source-valid'] = framedimage.metadata.get('source-valid', True) + self.warpmeta['source-fname'] = framedimage.metadata.get('source-fname', '') self.warpmeta['target-geom'] = framedimage.target - self.warpmeta['target-valid'] = framedimage.metadata['target-valid'] - self.warpmeta['target-fname'] = framedimage.metadata['target-fname'] + self.warpmeta['target-valid'] = framedimage.metadata.get('target-valid', True) + self.warpmeta['target-fname'] = framedimage.metadata.get('target-fname', '') # gcamorph meta (mgz warp: int int float) self.warpmeta['format'] = framedimage.format - self.warpmeta['spacing'] = framedimage.metadata['spacing'] - self.warpmeta['exp_k'] = framedimage.metadata['exp_k'] + self.warpmeta['spacing'] = framedimage.metadata.get('spacing', 1) + self.warpmeta['exp_k'] = framedimage.metadata.get('exp_k', 0.0) return diff --git a/surfa/transform/warp.py b/surfa/transform/warp.py index 459a89d..5edfbd6 100644 --- a/surfa/transform/warp.py +++ b/surfa/transform/warp.py @@ -58,6 +58,7 @@ def __init__(self, data, source=None, target=None, format=Format.disp_crs, **kwa raise ValueError(f'invalid shape {data.shape} for {basedim}D warp') super().__init__(basedim, data, geometry=target, **kwargs) + self.metadata['intent'] = sf.core.framed.FramedArrayIntents.warpmap def __call__(self, *args, **kwargs): """ From 045d9e89e3e7e08d2727a776b79f37efa58c24ad Mon Sep 17 00:00:00 2001 From: Malte Hoffmann Date: Fri, 21 Jun 2024 12:56:25 -0400 Subject: [PATCH 2/4] Clean up whitespace. --- surfa/io/fsnifti1extension.py | 102 ++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 48 deletions(-) diff --git a/surfa/io/fsnifti1extension.py b/surfa/io/fsnifti1extension.py index fafbc07..958a08f 100644 --- a/surfa/io/fsnifti1extension.py +++ b/surfa/io/fsnifti1extension.py @@ -9,7 +9,7 @@ from surfa.core.labels import LabelLookup from surfa.core.framed import FramedArrayIntents - + class FSNifti1Extension: """ This class handles Freesurfer Nifti1 header extension IO. @@ -17,7 +17,7 @@ class FSNifti1Extension: Class variables: _content: FSNifti1Extension.Content """ - + def __init__(self): """ FSNifti1Extension Constructor @@ -36,7 +36,7 @@ def read(self, fileobj, esize, offset=0): fileobj : file-like object opened file-like object esize : int - nifti1 header extension size including sizes of esize and ecode + nifti1 header extension size including sizes of esize and ecode offset : int offset to Freesurfer Nifti1 Header Extension @@ -58,7 +58,7 @@ def read(self, fileobj, esize, offset=0): print(f'[DEBUG] FSNifti1Extension.read(): esize = {esize:6d}') print(f'[DEBUG] FSNifti1Extension.read(): endian = \'{self.content.endian:c}\', intent = {self.content.intent:d}, version = {self.content.version:d}') - + # process Freesurfer Nifti1 extension tag data tagdatalen = esize - 12 # exclude esize, ecode, fsexthdr len_tagheader = 12 # tagid (4 bytes), data-length (8 bytes) @@ -67,7 +67,7 @@ def read(self, fileobj, esize, offset=0): (tag, length) = FSNifti1Extension.read_tag(fileobj) print(f'[DEBUG] FSNifti1Extension.read(): remaining taglen = {tagdatalen:6d} (tag = {tag:2d}, length = {length:5d})') - + if (tag == 0): break @@ -82,7 +82,7 @@ def read(self, fileobj, esize, offset=0): self.content.history.append(history) else: self.content.history = [history] - + # dof (TAG_DOF = 7) elif (tag == FSNifti1Extension.Tags.dof): dof = iou.read_int(fileobj, length) @@ -94,7 +94,7 @@ def read(self, fileobj, esize, offset=0): rotation = iou.read_bytes(fileobj, '>f4', 9).reshape((3, 3), order='F'), center = iou.read_bytes(fileobj, '>f4', 3) ) - + # gcamorph src & trg geoms (warp) (TAG_GCAMORPH_GEOM = 10) elif (tag == FSNifti1Extension.Tags.gcamorph_geom): if (not self.content.warpmeta): @@ -121,7 +121,7 @@ def read(self, fileobj, esize, offset=0): len_pedir = length - 20 self.content.scan_parameters['pedir'] = fileobj.read(len_pedir).decode('utf-8').rstrip('\x00') - + # skip everything else else: fileobj.read(length) @@ -131,10 +131,10 @@ def read(self, fileobj, esize, offset=0): if (tagdatalen < len_tagheader): print(f'[DEBUG] FSNifti1Extension.read(): remaining taglen = {tagdatalen:6d}') break; - + return self.content - - + + def write(self, fileobj, content, countbytesonly=False): """ Write Freesurfer Nifti1 header extension data saved to the file-like object. @@ -171,7 +171,7 @@ def write(self, fileobj, content, countbytesonly=False): # gcamorph src & trg geoms (warp) (TAG_GCAMORPH_GEOM = 10) source_fname = content.warpmeta.get('source-fname', '') target_fname = content.warpmeta.get('target-fname', '') - + tag = FSNifti1Extension.Tags.gcamorph_geom length = FSNifti1Extension.getlen_gcamorph_geom(source_fname, target_fname) num_bytes += length + addtaglength @@ -187,15 +187,15 @@ def write(self, fileobj, content, countbytesonly=False): geom=content.warpmeta['target-geom'], valid=content.warpmeta.get('target-valid', True), fname=target_fname, - niftiheaderext=True) - + niftiheaderext=True) + # gcamorph meta (warp: int int float) (TAG_GCAMORPH_META = 13) tag = FSNifti1Extension.Tags.gcamorph_meta length = 12 num_bytes += length + addtaglength print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}') if (not countbytesonly): - FSNifti1Extension.write_tag(fileobj, tag, length) + FSNifti1Extension.write_tag(fileobj, tag, length) iou.write_bytes(fileobj, content.warpmeta['format'], dtype='>i4') iou.write_bytes(fileobj, content.warpmeta.get('spacing', 1), dtype='>i4') iou.write_bytes(fileobj, content.warpmeta.get('exp_k', 0.0), dtype='>f4') @@ -211,10 +211,10 @@ def write(self, fileobj, content, countbytesonly=False): FSNifti1Extension.write_tag(fileobj, tag, length) extrachar = '*' fileobj.write(extrachar.encode('utf-8')) - + return num_bytes - - + + # lookup table (TAG_OLD_COLORTABLE = 1) if (content.labels): tag = FSNifti1Extension.Tags.old_colortable @@ -222,7 +222,7 @@ def write(self, fileobj, content, countbytesonly=False): num_bytes += length + addtaglength print(f'[DEBUG] FSNifti1Extension.write(): +{length:5d}, +{addtaglength:d}, dlen = {num_bytes:6d}, TAG = {tag:2d}') if (not countbytesonly): - FSNifti1Extension.write_tag(fileobj, tag, length) + FSNifti1Extension.write_tag(fileobj, tag, length) fsio.write_binary_lookup_table(fileobj, content.labels) # history (TAG_CMDLINE = 3) @@ -235,7 +235,7 @@ def write(self, fileobj, content, countbytesonly=False): if (not countbytesonly): FSNifti1Extension.write_tag(fileobj, tag, length) fileobj.write(hist.encode('utf-8')) - + # dof (TAG_DOF = 7) tag = FSNifti1Extension.Tags.dof length = 4 @@ -289,7 +289,7 @@ def write(self, fileobj, content, countbytesonly=False): FSNifti1Extension.write_tag(fileobj, tag, length) extrachar = '*' fileobj.write(extrachar.encode('utf-8')) - + return num_bytes @@ -310,13 +310,13 @@ def read_tag(fileobj): length : long data length of tagged data """ - + tag = iou.read_int(fileobj) length = iou.read_int(fileobj, size=8) return (tag, length) - - + + @staticmethod def write_tag(fileobj, tag, length): """ @@ -335,7 +335,7 @@ def write_tag(fileobj, tag, length): iou.write_int(fileobj, tag) iou.write_int(fileobj, length, size=8) - + @staticmethod def getlen_labels(labels): """ @@ -359,7 +359,7 @@ def getlen_labels(labels): num_bytes += 8 # structure id, len(structure-name)+1 num_bytes += len(element.name) + 1 # structure name num_bytes += 16 # ri, gi, bi, t-ai - + return num_bytes @@ -385,7 +385,7 @@ def getlen_gcamorph_geom(fname_source, fname_target): num_bytes = 2 * 80 num_bytes += len(fname_source) num_bytes += len(fname_target) - + return num_bytes @@ -405,15 +405,15 @@ class Tags: tagid-2 data-length-2 tag-data-2 ... - This class defines the tags recognized in surfa. + This class defines the tags recognized in surfa. It is a subset of tag IDs defined in freesurfer/include/tags.h """ old_colortable = 1 # TAG_OLD_COLORTABLE history = 3 # TAG_CMDLINE dof = 7 # TAG_DOF - ras_xform = 8 # TAG_RAS_XFORM - gcamorph_geom = 10 # TAG_GCAMORPH_GEOM + ras_xform = 8 # TAG_RAS_XFORM + gcamorph_geom = 10 # TAG_GCAMORPH_GEOM gcamorph_meta = 13 # TAG_GCAMORPH_META scan_parameters = 45 # TAG_SCAN_PARAMETERS end_data = -1 # TAG_END_NIIHDREXTENSION @@ -463,7 +463,7 @@ def __init__(self, framedimage=None): self._endian = '>' self._intent = FramedArrayIntents.mri self._version = 1 - + self._dof = 1 self._scan_parameters = None self._ras_xform = None @@ -487,7 +487,7 @@ def update_framedimage(self, framedimage): # update input framedimage metadata framedimage.metadata['intent'] = self.intent - + if (self.intent == FramedArrayIntents.warpmap): # gcamorph src & trg geoms (mgz warp) framedimage.source = self.warpmeta['source-geom'] @@ -521,13 +521,13 @@ def update_framedimage(self, framedimage): scan_params['te'] = self.scan_parameters['te'] scan_params['ti'] = self.scan_parameters['ti'] framedimage.metadata.update(scan_params) - + if (self.history): framedimage.metadata['history'] = self.history - + if (self.labels): framedimage.labels = self.labels - + def _from_framedimage(self, framedimage): """ @@ -544,13 +544,13 @@ def _from_framedimage(self, framedimage): self.intent = framedimage.metadata.get('intent', FramedArrayIntents.mri) if (isinstance(self.intent, np.int_)): self.intent = self.intent.item() # convert numpy int to python int - + self.dof = 1 - + if (self.intent == FramedArrayIntents.warpmap): if (not self.warpmeta): self.warpmeta = {} - + # gcamorph src & trg geoms (mgz warp) self.warpmeta['source-geom'] = framedimage.source self.warpmeta['source-valid'] = framedimage.metadata.get('source-valid', True) @@ -566,12 +566,12 @@ def _from_framedimage(self, framedimage): self.warpmeta['exp_k'] = framedimage.metadata.get('exp_k', 0.0) return - + # update ras_xform self.ras_xform = dict( rotation = framedimage.geom.rotation, - center = framedimage.geom.center - ) + center = framedimage.geom.center, + ) # update scan_parameters self.scan_parameters = dict( @@ -579,19 +579,20 @@ def _from_framedimage(self, framedimage): field_strength = framedimage.metadata.get('field-strength'), flip_angle = framedimage.metadata.get('fa', 0), te = framedimage.metadata.get('te', 0), - ti = framedimage.metadata.get('ti', 0) + ti = framedimage.metadata.get('ti', 0) ) - + if (framedimage.metadata.get('history')): self.history = framedimage.metadata['history'] - + if (framedimage.labels): self.labels = framedimage.labels - + @property def endian(self): return self._endian + @endian.setter def endian(self, endian): self._endian = endian @@ -599,6 +600,7 @@ def endian(self, endian): @property def intent(self): return self._intent + @intent.setter def intent(self, intent): self._intent = intent @@ -606,6 +608,7 @@ def intent(self, intent): @property def version(self): return self._version + @version.setter def version(self, version): self._version = version @@ -613,6 +616,7 @@ def version(self, version): @property def dof(self): return self._dof + @dof.setter def dof(self, dof): self._dof = dof @@ -620,6 +624,7 @@ def dof(self, dof): @property def scan_parameters(self): return self._scan_parameters + @scan_parameters.setter def scan_parameters(self, scan_parameters): self._scan_parameters = scan_parameters @@ -627,6 +632,7 @@ def scan_parameters(self, scan_parameters): @property def ras_xform(self): return self._ras_xform + @ras_xform.setter def ras_xform(self, ras_xform): self._ras_xform = ras_xform @@ -634,6 +640,7 @@ def ras_xform(self, ras_xform): @property def warpmeta(self): return self._warpmeta + @warpmeta.setter def warpmeta(self, warpmeta): self._warpmeta = warpmeta @@ -641,6 +648,7 @@ def warpmeta(self, warpmeta): @property def history(self): return self._history + @history.setter def history(self, history): self._history = history @@ -648,9 +656,7 @@ def history(self, history): @property def labels(self): return self._labels + @labels.setter def labels(self, labels): self._labels = labels - - - From fa7c4b1f8dd260649b1f4759aaefef43a154538e Mon Sep 17 00:00:00 2001 From: Malte Hoffmann Date: Fri, 21 Jun 2024 13:11:15 -0400 Subject: [PATCH 3/4] Fix typo. --- surfa/transform/warp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/surfa/transform/warp.py b/surfa/transform/warp.py index 5edfbd6..8b8a7bf 100644 --- a/surfa/transform/warp.py +++ b/surfa/transform/warp.py @@ -215,7 +215,7 @@ def transform(self, image, method='linear', fill=0): Returns ------- Volume - Transfomred image. + Transformed image. """ if not isinstance(image, sf.Volume): raise ValueError('Warp.transform() - input is not a Volume') From eda352dd4fb05495f2f2666c3892f22e099bcf8b Mon Sep 17 00:00:00 2001 From: Malte Hoffmann Date: Fri, 21 Jun 2024 13:11:56 -0400 Subject: [PATCH 4/4] Improve conciseness. --- surfa/core/framed.py | 1 - surfa/io/fsnifti1extension.py | 63 +++++++++++++++++------------------ 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/surfa/core/framed.py b/surfa/core/framed.py index 40796b4..474b427 100644 --- a/surfa/core/framed.py +++ b/surfa/core/framed.py @@ -276,7 +276,6 @@ def _shape_changed(self): """ pass - # optional parameter to specify FramedArray intent, default is MRI data def save(self, filename, fmt=None, intent=FramedArrayIntents.mri): """ Write array to file. diff --git a/surfa/io/fsnifti1extension.py b/surfa/io/fsnifti1extension.py index 958a08f..df26b28 100644 --- a/surfa/io/fsnifti1extension.py +++ b/surfa/io/fsnifti1extension.py @@ -529,64 +529,63 @@ def update_framedimage(self, framedimage): framedimage.labels = self.labels - def _from_framedimage(self, framedimage): + def _from_framedimage(self, image): """ - Create FSNifti1Extension.Content object from input FramedImage metadata + Create FSNifti1Extension.Content object from FramedImage instance. Parameters ---------- - framedimage : FramedImage - input FramedImage - """ + image : FramedImage + Input Image. + """ self.endian = '>' self.version = 1 - self.intent = framedimage.metadata.get('intent', FramedArrayIntents.mri) - if (isinstance(self.intent, np.int_)): - self.intent = self.intent.item() # convert numpy int to python int - + self.intent = image.metadata.get('intent', FramedArrayIntents.mri) self.dof = 1 + if isinstance(self.intent, np.int_): + self.intent = self.intent.item() # convert numpy int to python int - if (self.intent == FramedArrayIntents.warpmap): - if (not self.warpmeta): + if self.intent == FramedArrayIntents.warpmap: + if not self.warpmeta: self.warpmeta = {} # gcamorph src & trg geoms (mgz warp) - self.warpmeta['source-geom'] = framedimage.source - self.warpmeta['source-valid'] = framedimage.metadata.get('source-valid', True) - self.warpmeta['source-fname'] = framedimage.metadata.get('source-fname', '') + self.warpmeta['source-geom'] = image.source + self.warpmeta['source-valid'] = image.metadata.get('source-valid', True) + self.warpmeta['source-fname'] = image.metadata.get('source-fname', '') - self.warpmeta['target-geom'] = framedimage.target - self.warpmeta['target-valid'] = framedimage.metadata.get('target-valid', True) - self.warpmeta['target-fname'] = framedimage.metadata.get('target-fname', '') + self.warpmeta['target-geom'] = image.target + self.warpmeta['target-valid'] = image.metadata.get('target-valid', True) + self.warpmeta['target-fname'] = image.metadata.get('target-fname', '') # gcamorph meta (mgz warp: int int float) - self.warpmeta['format'] = framedimage.format - self.warpmeta['spacing'] = framedimage.metadata.get('spacing', 1) - self.warpmeta['exp_k'] = framedimage.metadata.get('exp_k', 0.0) + self.warpmeta['format'] = image.format + self.warpmeta['spacing'] = image.metadata.get('spacing', 1) + self.warpmeta['exp_k'] = image.metadata.get('exp_k', 0.0) return # update ras_xform self.ras_xform = dict( - rotation = framedimage.geom.rotation, - center = framedimage.geom.center, + rotation = image.geom.rotation, + center = image.geom.center, ) # update scan_parameters self.scan_parameters = dict( - pedir = framedimage.metadata.get('phase-encode-direction', 'UNKNOWN'), - field_strength = framedimage.metadata.get('field-strength'), - flip_angle = framedimage.metadata.get('fa', 0), - te = framedimage.metadata.get('te', 0), - ti = framedimage.metadata.get('ti', 0) - ) + pedir = image.metadata.get('phase-encode-direction', 'UNKNOWN'), + field_strength = image.metadata.get('field-strength'), + flip_angle = image.metadata.get('fa', 0), + te = image.metadata.get('te', 0), + ti = image.metadata.get('ti', 0), + ) - if (framedimage.metadata.get('history')): - self.history = framedimage.metadata['history'] + if image.metadata.get('history'): + self.history = image.metadata['history'] - if (framedimage.labels): - self.labels = framedimage.labels + if image.labels: + self.labels = image.labels @property