Skip to content
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

ENH: CoordinateImage API #1090

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1b7df51
ENH: Start restoring triangular meshes
effigies Sep 19, 2023
5d25cef
TEST: Test basic TriangularMesh behavior
effigies Sep 20, 2023
e426afe
RF: Use order kwarg for array proxies
effigies Sep 20, 2023
1a46c70
TEST: Rework FreeSurferHemisphere and H5Geometry examples with mixin
effigies Sep 20, 2023
be05f09
TEST: Tag tests that require access to the data directory
effigies Sep 20, 2023
cbb91d1
RF: Allow coordinate names to be set on init
effigies Sep 22, 2023
107ead6
TEST: More fully test mesh and family APIs
effigies Sep 22, 2023
368c145
ENH: Avoid duplicating objects, note that coordinate family mappings …
effigies Sep 22, 2023
9d5361a
ENH: Add copy() method to ArrayProxy
effigies Sep 19, 2023
81b1033
ENH: Copy lock if filehandle is shared, add tests
effigies Sep 22, 2023
b70a4d9
Merge branches 'enh/copyarrayproxy', 'enh/xml-kwargs' and 'enh/triang…
effigies Sep 22, 2023
798f0c6
ENH: Add stubs from BIAP 9
effigies Feb 18, 2022
7a6d50c
ENH: Implement CoordinateAxis. and Parcel.__getitem__
effigies Feb 21, 2022
344bfd8
TEST: Add FreeSurferSubject geometry collection, test loading Cifti2 …
effigies Feb 21, 2022
1138a95
ENH: Add CaretSpecFile type for use with CIFTI-2
effigies Feb 22, 2022
c7ab610
TEST: Load CaretSpecFile as a GeometryCollection
effigies Feb 22, 2022
a458ec3
ENH: Hacky mixin to make surface CaretDataFiles implement TriangularMesh
effigies Feb 23, 2022
921173b
FIX: CoordinateAxis.__getitem__ fix
effigies Feb 24, 2022
d4d42a0
ENH: Implement CoordinateAxis.get_indices
effigies Feb 24, 2022
b51ec36
TEST: Add some assertions and smoke tests to exercise methods
effigies Feb 24, 2022
76b52f5
ENH: Add from_image/from_header methods to bring logic out of tests
effigies Jan 25, 2023
1392a06
TEST: Add fsLR.wb.spec file for interpreting fsLR data
effigies Feb 24, 2022
5edddd4
ENH: Add CoordinateImage slicing by parcel name
effigies Jan 25, 2023
05ca9fb
RF: Simplify CaretSpecParser slightly
effigies Feb 25, 2022
6c2407d
TEST: Check SurfaceDataFile retrieval
effigies Aug 20, 2022
20f71df
Merge branch 'master' into enh/coordimage_api
effigies Jan 16, 2025
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
217 changes: 217 additions & 0 deletions nibabel/cifti2/caretspec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
# See COPYING file distributed along with the NiBabel package for the
# copyright and license terms.
#
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Read / write access to CaretSpecFile format

The format of CaretSpecFiles does not seem to have any independent
documentation.

Code can be found here [0], and a DTD was worked out in this email thread [1].

[0]: https://github.com/Washington-University/workbench/tree/master/src/Files
[1]: https://groups.google.com/a/humanconnectome.org/g/hcp-users/c/EGuwdaTVFuw/m/tg7a_-7mAQAJ
"""
import xml.etree.ElementTree as et
from urllib.parse import urlparse

import nibabel as nb
from nibabel import pointset as ps
from nibabel import xmlutils as xml
from nibabel.caret import CaretMetaData


class CaretSpecDataFile(xml.XmlSerializable):
"""DataFile

* Attributes

* Structure - A string from the BrainStructure list to identify
what structure this element refers to (usually left cortex,
right cortex, or cerebellum).
* DataFileType - A string from the DataFileType list
* Selected - A boolean

* Child Elements: [NA]
* Text Content: A URI
* Parent Element - CaretSpecFile

Attributes
----------
structure : str
Name of brain structure
data_file_type : str
Type of data file
selected : bool
Used for workbench internals
uri : str
URI of data file
"""

def __init__(self, structure=None, data_file_type=None, selected=None, uri=None):
super().__init__()
self.structure = structure
self.data_file_type = data_file_type
self.selected = selected
self.uri = uri

if data_file_type == 'SURFACE':
self.__class__ = SurfaceDataFile

def _to_xml_element(self):
data_file = xml.Element('DataFile')
data_file.attrib['Structure'] = str(self.structure)
data_file.attrib['DataFileType'] = str(self.data_file_type)
data_file.attrib['Selected'] = 'true' if self.selected else 'false'
data_file.text = self.uri
return data_file

Check warning on line 71 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L66-L71

Added lines #L66 - L71 were not covered by tests

def __repr__(self):
return self.to_xml().decode()


class SurfaceDataFile(ps.TriangularMesh, CaretSpecDataFile):
_gifti = None
_coords = None
_triangles = None

def _get_gifti(self):
if self._gifti is None:
parts = urlparse(self.uri)

Check warning on line 84 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L84

Added line #L84 was not covered by tests
if parts.scheme == 'file':
self._gifti = nb.load(parts.path)

Check warning on line 86 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L86

Added line #L86 was not covered by tests
elif parts.scheme == '':
self._gifti = nb.load(self.uri)

Check warning on line 88 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L88

Added line #L88 was not covered by tests
else:
self._gifti = nb.GiftiImage.from_url(self.uri)
return self._gifti

Check warning on line 91 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L90-L91

Added lines #L90 - L91 were not covered by tests

def get_triangles(self, name=None):
if self._triangles is None:
gifti = self._get_gifti()
self._triangles = gifti.agg_data('triangle')
return self._triangles

Check warning on line 97 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L95-L97

Added lines #L95 - L97 were not covered by tests

def get_coords(self, name=None):
if self._coords is None:
gifti = self._get_gifti()
self._coords = gifti.agg_data('pointset')
return self._coords

Check warning on line 103 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L101-L103

Added lines #L101 - L103 were not covered by tests


class CaretSpecFile(xml.XmlSerializable):
"""Class for CaretSpecFile XML documents

These are used to identify related surfaces and volumes for use with CIFTI-2
data files.
"""

def __init__(self, metadata=None, data_files=(), version='1.0'):
super().__init__()
if metadata is not None:
metadata = CaretMetaData(metadata)

Check warning on line 116 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L116

Added line #L116 was not covered by tests
self.metadata = metadata
self.data_files = list(data_files)
self.version = version

def _to_xml_element(self):
caret_spec = xml.Element('CaretSpecFile')
caret_spec.attrib['Version'] = str(self.version)

Check warning on line 123 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L122-L123

Added lines #L122 - L123 were not covered by tests
if self.metadata is not None:
caret_spec.append(self.metadata._to_xml_element())

Check warning on line 125 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L125

Added line #L125 was not covered by tests
for data_file in self.data_files:
caret_spec.append(data_file._to_xml_element())
return caret_spec

Check warning on line 128 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L127-L128

Added lines #L127 - L128 were not covered by tests

def to_xml(self, enc='UTF-8', **kwargs):
ele = self._to_xml_element()
et.indent(ele, ' ')
return et.tostring(ele, enc, xml_declaration=True, short_empty_elements=False, **kwargs)

Check warning on line 133 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L131-L133

Added lines #L131 - L133 were not covered by tests

def __eq__(self, other):
return self.to_xml() == other.to_xml()

Check warning on line 136 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L136

Added line #L136 was not covered by tests

@classmethod
def from_filename(klass, fname, **kwargs):
parser = CaretSpecParser(**kwargs)
with open(fname, 'rb') as fobj:
parser.parse(fptr=fobj)
return parser.caret_spec


class CaretSpecParser(xml.XmlParser):
def __init__(self, encoding=None, buffer_size=3500000, verbose=0):
super().__init__(encoding=encoding, buffer_size=buffer_size, verbose=verbose)
self.struct_state = []

self.caret_spec = None

# where to write CDATA:
self.write_to = None

# Collecting char buffer fragments
self._char_blocks = []

def StartElementHandler(self, name, attrs):
self.flush_chardata()
if name == 'CaretSpecFile':
self.caret_spec = CaretSpecFile(version=attrs['Version'])
elif name == 'MetaData':
self.caret_spec.metadata = CaretMetaData()
elif name == 'MD':
self.struct_state.append({})

Check warning on line 166 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L166

Added line #L166 was not covered by tests
elif name in ('Name', 'Value'):
self.write_to = name

Check warning on line 168 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L168

Added line #L168 was not covered by tests
elif name == 'DataFile':
selected_map = {'true': True, 'false': False}
data_file = CaretSpecDataFile(
structure=attrs['Structure'],
data_file_type=attrs['DataFileType'],
selected=selected_map[attrs['Selected']],
)
self.caret_spec.data_files.append(data_file)
self.struct_state.append(data_file)
self.write_to = 'DataFile'

def EndElementHandler(self, name):
self.flush_chardata()
if name == 'MD':
MD = self.struct_state.pop()
self.caret_spec.metadata[MD['Name']] = MD['Value']

Check warning on line 184 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L183-L184

Added lines #L183 - L184 were not covered by tests
elif name in ('Name', 'Value'):
self.write_to = None

Check warning on line 186 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L186

Added line #L186 was not covered by tests
elif name == 'DataFile':
self.struct_state.pop()
self.write_to = None

def CharacterDataHandler(self, data):
"""Collect character data chunks pending collation

The parser breaks the data up into chunks of size depending on the
buffer_size of the parser. A large bit of character data, with standard
parser buffer_size (such as 8K) can easily span many calls to this
function. We thus collect the chunks and process them when we hit start
or end tags.
"""
if self._char_blocks is None:
self._char_blocks = []
self._char_blocks.append(data)

def flush_chardata(self):
"""Collate and process collected character data"""
if self._char_blocks is None:
return

Check warning on line 207 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L207

Added line #L207 was not covered by tests

data = ''.join(self._char_blocks).strip()
# Reset the char collector
self._char_blocks = None
# Process data
if self.write_to in ('Name', 'Value'):
self.struct_state[-1][self.write_to] = data

Check warning on line 214 in nibabel/cifti2/caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/caretspec.py#L214

Added line #L214 was not covered by tests

elif self.write_to == 'DataFile':
self.struct_state[-1].uri = data
34 changes: 34 additions & 0 deletions nibabel/cifti2/tests/test_caretspec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import unittest
from pathlib import Path

from nibabel.cifti2.caretspec import *
from nibabel.optpkg import optional_package
from nibabel.testing import data_path

requests, has_requests, _ = optional_package('requests')


def test_CaretSpecFile():
fsLR = CaretSpecFile.from_filename(Path(data_path) / 'fsLR.wb.spec')

assert fsLR.metadata == {}
assert fsLR.version == '1.0'
assert len(fsLR.data_files) == 5

for df in fsLR.data_files:
assert isinstance(df, CaretSpecDataFile)
if df.data_file_type == 'SURFACE':
assert isinstance(df, SurfaceDataFile)


@unittest.skipUnless(has_requests, reason='Test fetches from URL')
def test_SurfaceDataFile():
fsLR = CaretSpecFile.from_filename(Path(data_path) / 'fsLR.wb.spec')
df = fsLR.data_files[0]
assert df.data_file_type == 'SURFACE'
try:
coords, triangles = df.get_mesh()
except IOError:
raise unittest.SkipTest(reason='Broken URL')
assert coords.shape == (32492, 3)
assert triangles.shape == (64980, 3)

Check warning on line 34 in nibabel/cifti2/tests/test_caretspec.py

View check run for this annotation

Codecov / codecov/patch

nibabel/cifti2/tests/test_caretspec.py#L26-L34

Added lines #L26 - L34 were not covered by tests
Loading
Loading