From 62d1e529818aed6da710422638a27cf957fba177 Mon Sep 17 00:00:00 2001 From: Ben Cipollini Date: Sat, 13 Jun 2015 09:06:19 -1000 Subject: [PATCH 1/8] Add a write_morph_data function. --- nibabel/freesurfer/__init__.py | 2 +- nibabel/freesurfer/io.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/__init__.py b/nibabel/freesurfer/__init__.py index a48793f4ed..a588fb06e5 100644 --- a/nibabel/freesurfer/__init__.py +++ b/nibabel/freesurfer/__init__.py @@ -1,6 +1,6 @@ """Reading functions for freesurfer files """ -from .io import read_geometry, read_morph_data, \ +from .io import read_geometry, read_morph_data, write_morph_data, \ read_annot, read_label, write_geometry, write_annot from .mghformat import load, save, MGHImage diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index f397abf44e..c5589fe0a0 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -160,6 +160,25 @@ def read_morph_data(filepath): return curv +def write_morph_data(filename, values): + ''' + ''' + with open(filename, 'wb') as f: + + # magic number + np.array([255], dtype='>u1').tofile(f) + np.array([255], dtype='>u1').tofile(f) + np.array([255], dtype='>u1').tofile(f) + + # vertices number and two un-used int4 + np.array([len(values)], dtype='>i4').tofile(f) + np.array([0], dtype='>i4').tofile(f) + np.array([1], dtype='>i4').tofile(f) + + # now the data + np.array(values, dtype='>f4').tofile(f) + + def read_annot(filepath, orig_ids=False): """Read in a Freesurfer annotation from a .annot file. From 61345e1c6fcea5880bfd39d2011278cb129c24b4 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 29 Feb 2016 21:22:44 -0500 Subject: [PATCH 2/8] DOC: Update write_morph_data docstring STY: Conform to conventions of other freesurfer.io functions --- nibabel/freesurfer/io.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index c5589fe0a0..aa8d5abddc 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -160,23 +160,27 @@ def read_morph_data(filepath): return curv -def write_morph_data(filename, values): - ''' - ''' - with open(filename, 'wb') as f: - - # magic number - np.array([255], dtype='>u1').tofile(f) - np.array([255], dtype='>u1').tofile(f) - np.array([255], dtype='>u1').tofile(f) - - # vertices number and two un-used int4 - np.array([len(values)], dtype='>i4').tofile(f) - np.array([0], dtype='>i4').tofile(f) - np.array([1], dtype='>i4').tofile(f) - - # now the data - np.array(values, dtype='>f4').tofile(f) +def write_morph_data(filepath, values): + """Write out a Freesurfer morphometry data file. + + See: + http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm#CurvNew + + Parameters + ---------- + filepath : str + Path to annotation file to be written + values : ndarray, shape (n_vertices,) + Surface morphometry values + """ + magic_bytes = np.array([255, 255, 255], dtype=np.uint8) + with open(filepath, 'wb') as fobj: + magic_bytes.tofile(fobj) + + # vertex count, face count (unused), vals per vertex (only 1 supported) + np.array([len(values), 0, 1], dtype='>i4').tofile(fobj) + + np.array(values, dtype='>f4').tofile(fobj) def read_annot(filepath, orig_ids=False): From df0b9bba8d5550e49828d141c52a7fc709253b99 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Mon, 29 Feb 2016 21:41:12 -0500 Subject: [PATCH 3/8] TST: Test morph_data round trip --- nibabel/freesurfer/tests/test_io.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/nibabel/freesurfer/tests/test_io.py b/nibabel/freesurfer/tests/test_io.py index b596f98fd7..b8526d4d2e 100644 --- a/nibabel/freesurfer/tests/test_io.py +++ b/nibabel/freesurfer/tests/test_io.py @@ -13,7 +13,7 @@ from numpy.testing import assert_equal, dec from .. import (read_geometry, read_morph_data, read_annot, read_label, - write_geometry, write_annot) + write_geometry, write_morph_data, write_annot) from ...tests.nibabel_data import get_nibabel_data @@ -92,6 +92,11 @@ def test_morph_data(): curv = read_morph_data(curv_path) assert_true(-1.0 < curv.min() < 0) assert_true(0 < curv.max() < 1.0) + with InTemporaryDirectory(): + new_path = 'test' + write_morph_data(new_path, curv) + curv2 = read_morph_data(new_path) + assert_equal(curv2, curv) @freesurfer_test From 2a3a0b2a7dc2aecd50a40432a4ce0688f334698f Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 1 Mar 2016 14:15:44 -0500 Subject: [PATCH 4/8] DOC: Update docstring Check bounds on values to be written Add fnum parameter to match write_curv.m --- nibabel/freesurfer/io.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index aa8d5abddc..5c6c5820e4 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -160,27 +160,46 @@ def read_morph_data(filepath): return curv -def write_morph_data(filepath, values): - """Write out a Freesurfer morphometry data file. +def write_morph_data(filepath, values, fnum=0): + """Write Freesurfer morphometry data `values` to file `filepath` - See: + Equivalent to FreeSurfer's `write_curv.m`_ + + See also: http://www.grahamwideman.com/gw/brain/fs/surfacefileformats.htm#CurvNew + .. _write_curv.m: \ + https://github.com/neurodebian/freesurfer/blob/debian-sloppy/matlab/write_curv.m + Parameters ---------- filepath : str Path to annotation file to be written - values : ndarray, shape (n_vertices,) + values : array-like Surface morphometry values + fnum : int, optional + Number of faces in the associated surface """ magic_bytes = np.array([255, 255, 255], dtype=np.uint8) + + i4info = np.iinfo('i4') + if len(values) > i4info.max: + raise ValueError("Too many values for morphometry file") + if not i4info.min <= fnum <= i4info.max: + raise ValueError("Argument fnum must be between {0} and {1}".format( + i4info.min, i4info.max)) + + array = np.asarray(values).astype('>f4') + if len(array.shape) > 1: + raise ValueError("Multi-dimensional values not supported") + with open(filepath, 'wb') as fobj: magic_bytes.tofile(fobj) # vertex count, face count (unused), vals per vertex (only 1 supported) - np.array([len(values), 0, 1], dtype='>i4').tofile(fobj) + np.array([len(values), fnum, 1], dtype='>i4').tofile(fobj) - np.array(values, dtype='>f4').tofile(fobj) + array.tofile(fobj) def read_annot(filepath, orig_ids=False): From d684b01a052e1b5f7ab31e6ac8f0298632811be8 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 1 Mar 2016 14:36:44 -0500 Subject: [PATCH 5/8] RF: Enable writing morphometry data to file-like objects --- nibabel/freesurfer/io.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index 5c6c5820e4..b635a0b6e1 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -6,6 +6,7 @@ from .. externals.six.moves import xrange +from ..openers import Opener def _fread3(fobj): @@ -160,8 +161,8 @@ def read_morph_data(filepath): return curv -def write_morph_data(filepath, values, fnum=0): - """Write Freesurfer morphometry data `values` to file `filepath` +def write_morph_data(file_like, values, fnum=0): + """Write Freesurfer morphometry data `values` to file-like `file_like` Equivalent to FreeSurfer's `write_curv.m`_ @@ -173,8 +174,9 @@ def write_morph_data(filepath, values, fnum=0): Parameters ---------- - filepath : str - Path to annotation file to be written + file_like : file-like + String containing path of file to be written, or file-like object, open + in binary write (`'wb'` mode, implementing the `write` method) values : array-like Surface morphometry values fnum : int, optional @@ -193,13 +195,13 @@ def write_morph_data(filepath, values, fnum=0): if len(array.shape) > 1: raise ValueError("Multi-dimensional values not supported") - with open(filepath, 'wb') as fobj: - magic_bytes.tofile(fobj) + with Opener(file_like, 'wb') as fobj: + fobj.write(magic_bytes) # vertex count, face count (unused), vals per vertex (only 1 supported) - np.array([len(values), fnum, 1], dtype='>i4').tofile(fobj) + fobj.write(np.array([len(values), fnum, 1], dtype='>i4')) - array.tofile(fobj) + fobj.write(array) def read_annot(filepath, orig_ids=False): From 1566d9bf49a1940e5096c7ed876fe3c524d90e4f Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 1 Mar 2016 17:04:09 -0500 Subject: [PATCH 6/8] TST: Test dimension, size, and fnum constraints Squeeze array to simplify dimension test --- nibabel/freesurfer/io.py | 12 ++++++------ nibabel/freesurfer/tests/test_io.py | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index b635a0b6e1..904a6105c8 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -184,22 +184,22 @@ def write_morph_data(file_like, values, fnum=0): """ magic_bytes = np.array([255, 255, 255], dtype=np.uint8) + array = np.asarray(values).astype('>f4').squeeze() + if len(array.shape) > 1: + raise ValueError("Multi-dimensional values not supported") + i4info = np.iinfo('i4') - if len(values) > i4info.max: + if len(array) > i4info.max: raise ValueError("Too many values for morphometry file") if not i4info.min <= fnum <= i4info.max: raise ValueError("Argument fnum must be between {0} and {1}".format( i4info.min, i4info.max)) - array = np.asarray(values).astype('>f4') - if len(array.shape) > 1: - raise ValueError("Multi-dimensional values not supported") - with Opener(file_like, 'wb') as fobj: fobj.write(magic_bytes) # vertex count, face count (unused), vals per vertex (only 1 supported) - fobj.write(np.array([len(values), fnum, 1], dtype='>i4')) + fobj.write(np.array([len(array), fnum, 1], dtype='>i4')) fobj.write(array) diff --git a/nibabel/freesurfer/tests/test_io.py b/nibabel/freesurfer/tests/test_io.py index b8526d4d2e..c398c97fa9 100644 --- a/nibabel/freesurfer/tests/test_io.py +++ b/nibabel/freesurfer/tests/test_io.py @@ -10,12 +10,13 @@ from nose.tools import assert_true import numpy as np -from numpy.testing import assert_equal, dec +from numpy.testing import assert_equal, assert_raises, dec from .. import (read_geometry, read_morph_data, read_annot, read_label, write_geometry, write_morph_data, write_annot) from ...tests.nibabel_data import get_nibabel_data +from ...fileslice import strided_scalar DATA_SDIR = 'fsaverage' @@ -99,6 +100,25 @@ def test_morph_data(): assert_equal(curv2, curv) +def test_write_morph_data(): + """Test write_morph_data edge cases""" + values = np.arange(20, dtype='>f4') + okay_shapes = [(20,), (20, 1), (20, 1, 1), (1, 20)] + bad_shape = (10, 2) + big_num = np.iinfo('i4').max + 1 + with InTemporaryDirectory(): + for shape in okay_shapes: + write_morph_data('test.curv', values.reshape(shape)) + # Check ordering is preserved, regardless of shape + assert_equal(values, read_morph_data('test.curv')) + assert_raises(ValueError, write_morph_data, 'test.curv', + np.zeros(shape), big_num) + assert_raises(ValueError, write_morph_data, 'test.curv', + values.reshape(bad_shape)) + assert_raises(ValueError, write_morph_data, 'test.curv', + strided_scalar((big_num,))) + + @freesurfer_test def test_annot(): """Test IO of .annot""" From 02eec3b46a7f18c03a0eaa00c74828fb339e2602 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 1 Mar 2016 17:58:55 -0500 Subject: [PATCH 7/8] TST: Restrict accepted shapes to plausible vectors Coercing vector to float before checking size could cause memory blow-up --- nibabel/freesurfer/io.py | 15 +++++++++------ nibabel/freesurfer/tests/test_io.py | 7 ++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/nibabel/freesurfer/io.py b/nibabel/freesurfer/io.py index 904a6105c8..6ccdef5f12 100644 --- a/nibabel/freesurfer/io.py +++ b/nibabel/freesurfer/io.py @@ -179,17 +179,20 @@ def write_morph_data(file_like, values, fnum=0): in binary write (`'wb'` mode, implementing the `write` method) values : array-like Surface morphometry values + + Shape must be (N,), (N, 1), (1, N) or (N, 1, 1) fnum : int, optional Number of faces in the associated surface """ magic_bytes = np.array([255, 255, 255], dtype=np.uint8) - array = np.asarray(values).astype('>f4').squeeze() - if len(array.shape) > 1: - raise ValueError("Multi-dimensional values not supported") + vector = np.asarray(values) + vnum = np.prod(vector.shape) + if vector.shape not in ((vnum,), (vnum, 1), (1, vnum), (vnum, 1, 1)): + raise ValueError("Invalid shape: argument values must be a vector") i4info = np.iinfo('i4') - if len(array) > i4info.max: + if vnum > i4info.max: raise ValueError("Too many values for morphometry file") if not i4info.min <= fnum <= i4info.max: raise ValueError("Argument fnum must be between {0} and {1}".format( @@ -199,9 +202,9 @@ def write_morph_data(file_like, values, fnum=0): fobj.write(magic_bytes) # vertex count, face count (unused), vals per vertex (only 1 supported) - fobj.write(np.array([len(array), fnum, 1], dtype='>i4')) + fobj.write(np.array([vnum, fnum, 1], dtype='>i4')) - fobj.write(array) + fobj.write(vector.astype('>f4')) def read_annot(filepath, orig_ids=False): diff --git a/nibabel/freesurfer/tests/test_io.py b/nibabel/freesurfer/tests/test_io.py index c398c97fa9..db2d136ec9 100644 --- a/nibabel/freesurfer/tests/test_io.py +++ b/nibabel/freesurfer/tests/test_io.py @@ -104,7 +104,7 @@ def test_write_morph_data(): """Test write_morph_data edge cases""" values = np.arange(20, dtype='>f4') okay_shapes = [(20,), (20, 1), (20, 1, 1), (1, 20)] - bad_shape = (10, 2) + bad_shapes = [(10, 2), (1, 1, 20, 1, 1)] big_num = np.iinfo('i4').max + 1 with InTemporaryDirectory(): for shape in okay_shapes: @@ -113,10 +113,11 @@ def test_write_morph_data(): assert_equal(values, read_morph_data('test.curv')) assert_raises(ValueError, write_morph_data, 'test.curv', np.zeros(shape), big_num) - assert_raises(ValueError, write_morph_data, 'test.curv', - values.reshape(bad_shape)) assert_raises(ValueError, write_morph_data, 'test.curv', strided_scalar((big_num,))) + for shape in bad_shapes: + assert_raises(ValueError, write_morph_data, 'test.curv', + values.reshape(shape)) @freesurfer_test From 949768a1125d208bf2ea32c1da8b0fa057e6870a Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Tue, 1 Mar 2016 21:47:15 -0500 Subject: [PATCH 8/8] TST: Skip big array check on 32-bit systems --- nibabel/freesurfer/tests/test_io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nibabel/freesurfer/tests/test_io.py b/nibabel/freesurfer/tests/test_io.py index db2d136ec9..69728f57a4 100644 --- a/nibabel/freesurfer/tests/test_io.py +++ b/nibabel/freesurfer/tests/test_io.py @@ -113,8 +113,10 @@ def test_write_morph_data(): assert_equal(values, read_morph_data('test.curv')) assert_raises(ValueError, write_morph_data, 'test.curv', np.zeros(shape), big_num) - assert_raises(ValueError, write_morph_data, 'test.curv', - strided_scalar((big_num,))) + # Windows 32-bit overflows Python int + if np.dtype(np.int) != np.dtype(np.int32): + assert_raises(ValueError, write_morph_data, 'test.curv', + strided_scalar((big_num,))) for shape in bad_shapes: assert_raises(ValueError, write_morph_data, 'test.curv', values.reshape(shape))