Skip to content

Commit e37cb5d

Browse files
Merge pull request #470 from matthew-brett/gifti-serialization-bug
MRG: fix in-memory GIFTI XML output, drop python 2.6 Fix errors writing XML output from GIFTI images created in-memory (rather than loaded). Also, drop Python 2.6 testing and compatibility.
2 parents 3859dfa + 5dad677 commit e37cb5d

9 files changed

+188
-11
lines changed

.travis.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ env:
2424
- PYDICOM=1
2525
- INSTALL_TYPE="setup"
2626
python:
27-
- 2.6
2827
- 3.3
2928
- 3.4
3029
- 3.5
@@ -38,6 +37,10 @@ matrix:
3837
env:
3938
- DEPENDS=numpy==1.5.1 PYDICOM=0
4039
# Absolute minimum dependencies plus oldest MPL
40+
# Check these against:
41+
# doc/source/installation.rst
42+
# requirements.txt
43+
# .travis.yml
4144
- python: 2.7
4245
env:
4346
- DEPENDS="numpy==1.5.1 matplotlib==1.3.1" PYDICOM=0

doc/source/installation.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ is for you.
8181
Requirements
8282
------------
8383

84-
* Python_ 2.6 or greater
84+
.. check these against:
85+
nibabel/info.py
86+
requirements.txt
87+
.travis.yml
88+
89+
* Python_ 2.7 or greater
8590
* NumPy_ 1.5 or greater
8691
* SciPy_ (optional, for full SPM-ANALYZE support)
8792
* PyDICOM_ 0.9.7 or greater (optional, for DICOM support)

doc/source/links_names.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
https://pip.readthedocs.org/en/latest/installing.html
109109
.. _twine: https://pypi.python.org/pypi/twine
110110
.. _datapkg: https://pythonhosted.org/datapkg/
111-
.. _python imaging library: http://pythonware.com/products/pil/
111+
.. _python imaging library: https://pypi.python.org/pypi/Pillow
112112

113113
.. Python imaging projects
114114
.. _PyMVPA: http://www.pymvpa.org

nibabel/gifti/gifti.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class GiftiNVPairs(object):
8181
name : str
8282
value : str
8383
"""
84-
def __init__(self, name='', value=''):
84+
def __init__(self, name=u'', value=u''):
8585
self.name = name
8686
self.value = value
8787

@@ -344,7 +344,7 @@ def __init__(self,
344344
coordsys=None,
345345
ordering="C",
346346
meta=None,
347-
ext_fname='',
347+
ext_fname=u'',
348348
ext_offset=0):
349349
"""
350350
Returns a shell object that cannot be saved.
@@ -436,6 +436,7 @@ def _to_xml_element(self):
436436
# fix endianness to machine endianness
437437
self.endian = gifti_endian_codes.code[sys.byteorder]
438438

439+
# All attribute values must be strings
439440
data_array = xml.Element('DataArray', attrib={
440441
'Intent': intent_codes.niistring[self.intent],
441442
'DataType': data_type_codes.niistring[self.datatype],
@@ -444,7 +445,7 @@ def _to_xml_element(self):
444445
'Encoding': gifti_encoding_codes.specs[self.encoding],
445446
'Endian': gifti_endian_codes.specs[self.endian],
446447
'ExternalFileName': self.ext_fname,
447-
'ExternalFileOffset': self.ext_offset})
448+
'ExternalFileOffset': str(self.ext_offset)})
448449
for di, dn in enumerate(self.dims):
449450
data_array.attrib['Dim%d' % di] = str(dn)
450451

@@ -517,7 +518,8 @@ def metadata(self):
517518

518519

519520
class GiftiImage(xml.XmlSerializable, FileBasedImage):
520-
"""
521+
""" GIFTI image object
522+
521523
The Gifti spec suggests using the following suffixes to your
522524
filename when saving each specific type of data:
523525
@@ -553,7 +555,7 @@ class GiftiImage(xml.XmlSerializable, FileBasedImage):
553555
parser = None
554556

555557
def __init__(self, header=None, extra=None, file_map=None, meta=None,
556-
labeltable=None, darrays=None, version="1.0"):
558+
labeltable=None, darrays=None, version=u"1.0"):
557559
super(GiftiImage, self).__init__(header=header, extra=extra,
558560
file_map=file_map)
559561
if darrays is None:

nibabel/gifti/parse_gifti_fast.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ def read_data_block(encoding, endian, ordering, datatype, shape, data):
7777
return newarr
7878

7979

80+
def _str2int(in_str):
81+
# Convert string to integer, where empty string gives 0
82+
return int(in_str) if in_str else 0
83+
84+
8085
class GiftiImageParser(XmlParser):
8186

8287
def __init__(self, encoding=None, buffer_size=35000000, verbose=0):
@@ -187,7 +192,7 @@ def StartElementHandler(self, name, attrs):
187192
if "ExternalFileName" in attrs:
188193
self.da.ext_fname = attrs["ExternalFileName"]
189194
if "ExternalFileOffset" in attrs:
190-
self.da.ext_offset = attrs["ExternalFileOffset"]
195+
self.da.ext_offset = _str2int(attrs["ExternalFileOffset"])
191196
self.img.darrays.append(self.da)
192197
self.fsm_state.append('DataArray')
193198

nibabel/gifti/tests/test_gifti.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
import warnings
44
import sys
5+
from io import BytesIO
56

67
import numpy as np
78

@@ -12,6 +13,7 @@
1213
GiftiCoordSystem)
1314
from nibabel.gifti.gifti import data_tag
1415
from nibabel.nifti1 import data_type_codes
16+
from nibabel.fileholders import FileHolder
1517

1618
from numpy.testing import (assert_array_almost_equal,
1719
assert_array_equal)
@@ -275,3 +277,114 @@ def test_data_tag_deprecated():
275277
warnings.filterwarnings('once', category=DeprecationWarning)
276278
data_tag(np.array([]), 'ASCII', '%i', 1)
277279
assert_equal(len(w), 1)
280+
281+
282+
def test_gifti_round_trip():
283+
# From section 14.4 in GIFTI Surface Data Format Version 1.0
284+
# (with some adaptations)
285+
286+
test_data = b'''<?xml version="1.0" encoding="UTF-8"?>
287+
<!DOCTYPE GIFTI SYSTEM "http://www.nitrc.org/frs/download.php/1594/gifti.dtd">
288+
<GIFTI
289+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
290+
xsi:noNamespaceSchemaLocation="http://www.nitrc.org/frs/download.php/1303/GIFTI_Caret.xsd"
291+
Version="1.0"
292+
NumberOfDataArrays="2">
293+
<MetaData>
294+
<MD>
295+
<Name><![CDATA[date]]></Name>
296+
<Value><![CDATA[Thu Nov 15 09:05:22 2007]]></Value>
297+
</MD>
298+
</MetaData>
299+
<LabelTable/>
300+
<DataArray Intent="NIFTI_INTENT_POINTSET"
301+
DataType="NIFTI_TYPE_FLOAT32"
302+
ArrayIndexingOrder="RowMajorOrder"
303+
Dimensionality="2"
304+
Dim0="4"
305+
Dim1="3"
306+
Encoding="ASCII"
307+
Endian="LittleEndian"
308+
ExternalFileName=""
309+
ExternalFileOffset="">
310+
<CoordinateSystemTransformMatrix>
311+
<DataSpace><![CDATA[NIFTI_XFORM_TALAIRACH]]></DataSpace>
312+
<TransformedSpace><![CDATA[NIFTI_XFORM_TALAIRACH]]></TransformedSpace>
313+
<MatrixData>
314+
1.000000 0.000000 0.000000 0.000000
315+
0.000000 1.000000 0.000000 0.000000
316+
0.000000 0.000000 1.000000 0.000000
317+
0.000000 0.000000 0.000000 1.000000
318+
</MatrixData>
319+
</CoordinateSystemTransformMatrix>
320+
<Data>
321+
10.5 0 0
322+
0 20.5 0
323+
0 0 30.5
324+
0 0 0
325+
</Data>
326+
</DataArray>
327+
<DataArray Intent="NIFTI_INTENT_TRIANGLE"
328+
DataType="NIFTI_TYPE_INT32"
329+
ArrayIndexingOrder="RowMajorOrder"
330+
Dimensionality="2"
331+
Dim0="4"
332+
Dim1="3"
333+
Encoding="ASCII"
334+
Endian="LittleEndian"
335+
ExternalFileName="" ExternalFileOffset="">
336+
<Data>
337+
0 1 2
338+
1 2 3
339+
0 1 3
340+
0 2 3
341+
</Data>
342+
</DataArray>
343+
</GIFTI>'''
344+
345+
exp_verts = np.zeros((4, 3))
346+
exp_verts[0, 0] = 10.5
347+
exp_verts[1, 1] = 20.5
348+
exp_verts[2, 2] = 30.5
349+
exp_faces = np.asarray([[0, 1, 2], [1, 2, 3], [0, 1, 3], [0, 2, 3]],
350+
dtype=np.int32)
351+
352+
def _check_gifti(gio):
353+
vertices = gio.get_arrays_from_intent('NIFTI_INTENT_POINTSET')[0].data
354+
faces = gio.get_arrays_from_intent('NIFTI_INTENT_TRIANGLE')[0].data
355+
assert_array_equal(vertices, exp_verts)
356+
assert_array_equal(faces, exp_faces)
357+
358+
bio = BytesIO()
359+
fmap = dict(image=FileHolder(fileobj=bio))
360+
361+
bio.write(test_data)
362+
bio.seek(0)
363+
gio = GiftiImage.from_file_map(fmap)
364+
_check_gifti(gio)
365+
# Write and read again
366+
bio.seek(0)
367+
gio.to_file_map(fmap)
368+
bio.seek(0)
369+
gio2 = GiftiImage.from_file_map(fmap)
370+
_check_gifti(gio2)
371+
372+
373+
def test_data_array_round_trip():
374+
# Test valid XML generated from new in-memory array
375+
# See: https://github.com/nipy/nibabel/issues/469
376+
verts = np.zeros((4, 3), np.float32)
377+
verts[0, 0] = 10.5
378+
verts[1, 1] = 20.5
379+
verts[2, 2] = 30.5
380+
381+
vertices = GiftiDataArray(verts)
382+
img = GiftiImage()
383+
img.add_gifti_data_array(vertices)
384+
bio = BytesIO()
385+
fmap = dict(image=FileHolder(fileobj=bio))
386+
bio.write(img.to_xml())
387+
bio.seek(0)
388+
gio = GiftiImage.from_file_map(fmap)
389+
vertices = gio.darrays[0].data
390+
assert_array_equal(vertices, verts)

nibabel/gifti/tests/test_parse_gifti_fast.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,45 @@
9999
DATA_FILE6_darr1 = np.array([9182740, 9182740, 9182740], dtype=np.float32)
100100

101101

102+
def assert_default_types(loaded):
103+
default = loaded.__class__()
104+
for attr in dir(default):
105+
defaulttype = type(getattr(default, attr))
106+
# Optional elements may have default of None
107+
if defaulttype is type(None):
108+
continue
109+
loadedtype = type(getattr(loaded, attr))
110+
assert_equal(loadedtype, defaulttype,
111+
"Type mismatch for attribute: {} ({!s} != {!s})".format(
112+
attr, loadedtype, defaulttype))
113+
114+
115+
def test_default_types():
116+
# Test that variable types are same in loaded and default instances
117+
for fname in datafiles:
118+
img = load(fname)
119+
# GiftiImage
120+
assert_default_types(img)
121+
# GiftiMetaData
122+
assert_default_types(img.meta)
123+
# GiftiNVPairs
124+
for nvpair in img.meta.data:
125+
assert_default_types(nvpair)
126+
# GiftiLabelTable
127+
assert_default_types(img.labeltable)
128+
# GiftiLabel elements can be None or float; skip
129+
# GiftiDataArray
130+
for darray in img.darrays:
131+
assert_default_types(darray)
132+
# GiftiCoordSystem
133+
assert_default_types(darray.coordsys)
134+
# GiftiMetaData
135+
assert_default_types(darray.meta)
136+
# GiftiNVPairs
137+
for nvpair in darray.meta.data:
138+
assert_default_types(nvpair)
139+
140+
102141
def test_read_ordering():
103142
# DATA_FILE1 has an expected darray[0].data shape of (3,3). However if we
104143
# read another image first (DATA_FILE2) then the shape is wrong

nibabel/info.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,11 @@
102102
nibabel distribution.
103103
"""
104104

105-
# versions for dependencies
106-
NUMPY_MIN_VERSION = '1.5'
105+
# versions for dependencies. Check these against:
106+
# doc/source/installation.rst
107+
# requirements.txt
108+
# .travis.yml
109+
NUMPY_MIN_VERSION = '1.5.1'
107110
PYDICOM_MIN_VERSION = '0.9.7'
108111

109112
# Main setup parameters

requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1+
# Minumum requirements
2+
#
3+
# Check these against
4+
# nibabel/info.py
5+
# .travis.yml
6+
# doc/source/installation.rst
7+
18
numpy>=1.5.1

0 commit comments

Comments
 (0)