Skip to content

RF: Support "flat" ASCII-encoded GIFTI DataArrays #1298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nibabel/gifti/gifti.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ def agg_data(self, intent_code=None):
>>> triangles_2 = surf_img.agg_data('triangle')
>>> triangles_3 = surf_img.agg_data(1009) # Numeric code for pointset
>>> print(np.array2string(triangles))
[0 1 2]
[[0 1 2]]
>>> np.array_equal(triangles, triangles_2)
True
>>> np.array_equal(triangles, triangles_3)
Expand Down
66 changes: 29 additions & 37 deletions nibabel/gifti/parse_gifti_fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,21 @@ def read_data_block(darray, fname, data, mmap):
if mmap is True:
mmap = 'c'
enclabel = gifti_encoding_codes.label[darray.encoding]
dtype = data_type_codes.type[darray.datatype]

if enclabel not in ('ASCII', 'B64BIN', 'B64GZ', 'External'):
raise GiftiParseError(f'Unknown encoding {darray.encoding}')

# Encode the endianness in the dtype
byteorder = gifti_endian_codes.byteorder[darray.endian]
dtype = data_type_codes.dtype[darray.datatype].newbyteorder(byteorder)

shape = tuple(darray.dims)
order = array_index_order_codes.npcode[darray.ind_ord]

# GIFTI_ENCODING_ASCII
if enclabel == 'ASCII':
# GIFTI_ENCODING_ASCII
c = StringIO(data)
da = np.loadtxt(c, dtype=dtype)
return da # independent of the endianness
elif enclabel not in ('B64BIN', 'B64GZ', 'External'):
return 0

# GIFTI_ENCODING_EXTBIN
return np.loadtxt(StringIO(data), dtype=dtype, ndmin=1).reshape(shape, order=order)

# We assume that the external data file is raw uncompressed binary, with
# the data type/endianness/ordering specified by the other DataArray
# attributes
Expand All @@ -94,53 +98,41 @@ def read_data_block(darray, fname, data, mmap):
newarr = None
if mmap:
try:
newarr = np.memmap(
return np.memmap(
ext_fname,
dtype=dtype,
mode=mmap,
offset=darray.ext_offset,
shape=tuple(darray.dims),
shape=shape,
order=order,
)
# If the memmap fails, we ignore the error and load the data into
# memory below
except (AttributeError, TypeError, ValueError):
pass
# mmap=False or np.memmap failed
if newarr is None:
# We can replace this with a call to np.fromfile in numpy>=1.17,
# as an "offset" parameter was added in that version.
with open(ext_fname, 'rb') as f:
f.seek(darray.ext_offset)
nbytes = np.prod(darray.dims) * dtype().itemsize
buff = f.read(nbytes)
newarr = np.frombuffer(buff, dtype=dtype)
return np.fromfile(
ext_fname,
dtype=dtype,
count=np.prod(darray.dims),
offset=darray.ext_offset,
).reshape(shape, order=order)

# Numpy arrays created from bytes objects are read-only.
# Neither b64decode nor decompress will return bytearrays, and there
# are not equivalents to fobj.readinto to allow us to pass them, so
# there is not a simple way to avoid making copies.
# If this becomes a problem, we should write a decoding interface with
# a tunable chunk size.
dec = base64.b64decode(data.encode('ascii'))
if enclabel == 'B64BIN':
buff = bytearray(dec)
else:
dec = base64.b64decode(data.encode('ascii'))
if enclabel == 'B64BIN':
# GIFTI_ENCODING_B64BIN
buff = bytearray(dec)
else:
# GIFTI_ENCODING_B64GZ
buff = bytearray(zlib.decompress(dec))
del dec
newarr = np.frombuffer(buff, dtype=dtype)

sh = tuple(darray.dims)
if len(newarr.shape) != len(sh):
newarr = newarr.reshape(sh, order=array_index_order_codes.npcode[darray.ind_ord])

# check if we need to byteswap
required_byteorder = gifti_endian_codes.byteorder[darray.endian]
if required_byteorder in ('big', 'little') and required_byteorder != sys.byteorder:
newarr = newarr.byteswap()
return newarr
# GIFTI_ENCODING_B64GZ
buff = bytearray(zlib.decompress(dec))
del dec
return np.frombuffer(buff, dtype=dtype).reshape(shape, order=order)


def _str2int(in_str):
Expand Down
76 changes: 76 additions & 0 deletions nibabel/gifti/tests/data/ascii_flat_data.gii
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE GIFTI SYSTEM "http://www.nitrc.org/frs/download.php/115/gifti.dtd">
<GIFTI Version="1.0" NumberOfDataArrays="2">
<MetaData>
<MD>
<Name><![CDATA[Caret-Version]]></Name>
<Value><![CDATA[5.512]]></Value>
</MD>
<MD>
<Name><![CDATA[date]]></Name>
<Value><![CDATA[Thu Dec 27 14:27:43 2007]]></Value>
</MD>
<MD>
<Name><![CDATA[encoding]]></Name>
<Value><![CDATA[XML]]></Value>
</MD>
</MetaData>
<LabelTable/>
<DataArray Intent="NIFTI_INTENT_POINTSET"
DataType="NIFTI_TYPE_FLOAT32"
ArrayIndexingOrder="RowMajorOrder"
Dimensionality="2"
Dim0="10"
Dim1="3"
Encoding="ASCII"
Endian="LittleEndian"
ExternalFileName=""
ExternalFileOffset="">
<MetaData>
<MD>
<Name><![CDATA[AnatomicalStructurePrimary]]></Name>
<Value><![CDATA[CortexLeft]]></Value>
</MD>
<MD>
<Name><![CDATA[AnatomicalStructureSecondary]]></Name>
<Value><![CDATA[Pial]]></Value>
</MD>
<MD>
<Name><![CDATA[GeometricType]]></Name>
<Value><![CDATA[Anatomical]]></Value>
</MD>
<MD>
<Name><![CDATA[UniqueID]]></Name>
<Value><![CDATA[{70e032e9-4123-47ee-965d-5b29107cbd83}]]></Value>
</MD>
</MetaData>
<CoordinateSystemTransformMatrix>
<DataSpace><![CDATA[NIFTI_XFORM_TALAIRACH]]></DataSpace>
<TransformedSpace><![CDATA[NIFTI_XFORM_TALAIRACH]]></TransformedSpace>
<MatrixData>1.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 1.000000 0.000000 0.000000 0.000000 0.000000 1.000000</MatrixData>
</CoordinateSystemTransformMatrix>
<Data>155.17539978 135.58103943 98.30715179 140.33973694 190.0491333 73.24776459 157.3598938 196.97969055 83.65809631 171.46174622 137.43661499 78.4709549 148.54592896 97.06752777 65.96373749 123.45701599 111.46841431 66.3571167 135.30892944 202.28720093 36.38148499 178.28155518 162.59469604 37.75128937 178.11087036 115.28820038 57.17986679 142.81582642 82.82115173 31.02205276</Data>
</DataArray>
<DataArray Intent="NIFTI_INTENT_TRIANGLE"
DataType="NIFTI_TYPE_INT32"
ArrayIndexingOrder="RowMajorOrder"
Dimensionality="2"
Dim0="10"
Dim1="3"
Encoding="ASCII"
Endian="LittleEndian"
ExternalFileName=""
ExternalFileOffset="">
<MetaData>
<MD>
<Name><![CDATA[TopologicalType]]></Name>
<Value><![CDATA[CLOSED]]></Value>
</MD>
<MD>
<Name><![CDATA[UniqueID]]></Name>
<Value><![CDATA[{747d8015-455b-43ad-82ac-dcfb7606004a}]]></Value>
</MD>
</MetaData>
<Data>6402 17923 25602 14085 25602 17923 25602 14085 4483 17923 1602 14085 4483 25603 25602 25604 25602 25603 25602 25604 6402 25603 3525 25604 1123 17922 12168 25604 12168 17922 </Data>
</DataArray>
</GIFTI>
28 changes: 24 additions & 4 deletions nibabel/gifti/tests/test_parse_gifti_fast.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,19 @@
DATA_FILE5 = pjoin(IO_DATA_PATH, 'base64bin.gii')
DATA_FILE6 = pjoin(IO_DATA_PATH, 'rh.aparc.annot.gii')
DATA_FILE7 = pjoin(IO_DATA_PATH, 'external.gii')

datafiles = [DATA_FILE1, DATA_FILE2, DATA_FILE3, DATA_FILE4, DATA_FILE5, DATA_FILE6, DATA_FILE7]
numDA = [2, 1, 1, 1, 2, 1, 2]
DATA_FILE8 = pjoin(IO_DATA_PATH, 'ascii_flat_data.gii')

datafiles = [
DATA_FILE1,
DATA_FILE2,
DATA_FILE3,
DATA_FILE4,
DATA_FILE5,
DATA_FILE6,
DATA_FILE7,
DATA_FILE8,
]
numDA = [2, 1, 1, 1, 2, 1, 2, 2]

DATA_FILE1_darr1 = np.array(
[
Expand All @@ -50,7 +60,7 @@
[-17.614349, -65.401642, 21.071466],
]
)
DATA_FILE1_darr2 = np.array([0, 1, 2])
DATA_FILE1_darr2 = np.array([[0, 1, 2]])

DATA_FILE2_darr1 = np.array(
[
Expand Down Expand Up @@ -152,6 +162,10 @@
dtype=np.int32,
)

DATA_FILE8_darr1 = np.copy(DATA_FILE5_darr1)

DATA_FILE8_darr2 = np.copy(DATA_FILE5_darr2)


def assert_default_types(loaded):
default = loaded.__class__()
Expand Down Expand Up @@ -448,3 +462,9 @@ def test_load_compressed():
img7 = load(fn)
assert_array_almost_equal(img7.darrays[0].data, DATA_FILE7_darr1)
assert_array_almost_equal(img7.darrays[1].data, DATA_FILE7_darr2)


def test_load_flat_ascii_data():
img = load(DATA_FILE8)
assert_array_almost_equal(img.darrays[0].data, DATA_FILE8_darr1)
assert_array_almost_equal(img.darrays[1].data, DATA_FILE8_darr2)