Add fmri_surface_data object for CIFTI/GIFTI surface & grayordinate data#86
Open
torwager wants to merge 20 commits into
Open
Add fmri_surface_data object for CIFTI/GIFTI surface & grayordinate data#86torwager wants to merge 20 commits into
torwager wants to merge 20 commits into
Conversation
New CANlab object `fmri_surface_data` (subclass of image_vector), the surface/
grayordinate analogue of fmri_data. Wraps the HCP CIFTI grayordinate standard as
a true CANlab object with full fmri_data-style method parity, running natively in
MATLAB with no external toolbox (sole exception: ica, which needs GIFT/icatb like
the base class).
Highlights:
- Native CIFTI-2 + GIFTI readers/writers (Surface_tools/canlab_read|write_cifti|
gifti), bit-exact vs cifti_read/gifti oracles; proven to work with those tools
removed from the path.
- Object: construction from CIFTI/GIFTI/struct/key-value; brain_model replaces
volInfo (volInfo populated for the subcortical sub-block); no empty-squeezing
(.dat always full; remove_empty/replace_empty are no-ops).
- Interop: reconstruct_image, to_fmri_data, compare_space (0/1/2/3), write.
- Volume<->surface: vol2surf / surf2vol via vendored CBIG RF warps (vol->surf->
vol r~1.0), fully native.
- Data/analysis: cat/horzcat, mean, apply_mask, threshold (+cluster-extent 'k'),
ttest, regress, predict, ica (predict/ttest/ica delegate to fmri_data via a
proxy and remap results).
- Rendering: surface (native 4-panel + on any addbrain/MNI surface via volume
resampling), render_on_surface, plot; reparse_contiguous (mesh graph),
apply_parcellation, surface_region.
- Docs: docs/fmri_surface_data_{design_plan,methods}.md + runnable walkthrough.m.
- Tests: Unit_tests/surface_data/ (7 files, 41 tests passing).
Also: fix a stray non-functional debug line in @image_vector/ica.m that errored
ica() for all image_vector subclasses; document the fs_LR/fsaverage standard-mesh
correspondence in addbrain.m; add deprecation pointers in the external-dependent
Cifti_plotting readers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- docs/fmri_surface_data_methods.md: per-class methods page (properties + methods grouped by area), integrated into docs/Object_methods.md (class hierarchy + object-classes table) and docs/Workflows.md. - docs/workflows/fmri_surface_data_howto.md: intro workflow — load surface data and render it natively; map volumetric data to the surface (vol2surf) and back (surf2vol / to_fmri_data); parcellation, thresholding, and group analysis. Includes three rendered example figures. - Methods page includes a summarized inventory of surface atlases / maps available in the companion Neuroimaging_Pattern_Masks repository. - Sample data: bundle one small HCP Open Access group myelin map (Sample_datasets/CIFTI_surface_examples/) with a provenance README; additional surface atlases are resolved from Neuroimaging_Pattern_Masks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Verified the whole object end-to-end (41 tests pass; walkthrough runs both with and without Neuroimaging_Pattern_Masks). Fixes from the review: - apply_mask: now falls back to the object's .mask property when no mask arg is passed (matching the documented behavior), with a clear error if neither is set. Added a unit test. - Walkthrough (CanlabCore/docs/fmri_surface_data_walkthrough.m): use the bundled HCP myelin map as the primary object so it runs with only CanlabCore; guard the subcortex / atlas / parcellation sections on NPM availability; fix the parcellation demo (previously skipped because spaces never matched). - How-to (docs/workflows/fmri_surface_data_howto.md): fix Section E/F snippets that referenced undefined variables (mydata, subj1..3, scores) and called regress before setting the design; they now match the runnable walkthrough. - Docs accuracy: regress is native OLS (not "delegated"); cluster-extent threshold is implemented (not "planned"); ica is a class override (not inherited); canlab_surface_vertexcolors signature includes graycolor. - Method help examples (vol2surf, surface, surf2vol, canlab_read_cifti): use the bundled emotionreg / myelin sample data instead of a private-repo image. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
32b80e6 to
f178d11
Compare
…ge/slices; doc links
- Volume-only image_vector methods (flip, isosurface, interpolate, resample_space,
extract_gray_white_csf, searchlight, slice_movie, trim_mask, read_from_file,
extract_roi_averages, ...) are overridden in a methods(Hidden) block: they no
longer clutter methods(obj)/tab-completion, and calling one raises a clear
fmri_surface_data:unsupportedMethod error pointing to the surface equivalent.
The class stays a full image_vector subclass.
- orthviews / montage / slices now route the subcortical grayordinates through
to_fmri_data and call the fmri_data method (meaningful subcortex display);
cortex-only objects get a clear "use surface(obj)" error.
- Added test_volume_only_methods_hidden; documented in the methods page
("Volume-only methods (redirected or masked)").
- Made the walkthrough / design-plan references in the docs into links.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ow a MATLAB table - Object property `intent` renamed to `imagetype` (holds dscalar/dtseries/dlabel/func/shape/label). The CIFTI/GIFTI reader & writer structs keep the faithful `intent`/`intents` field names at the I/O boundary; the constructor/write map between `obj.imagetype` and the CIFTI intent. - `label_table` property is now a MATLAB `table` (variables key, name, rgba) instead of a struct array — nicer to view/query. Boundary converters (private struct2labeltable / labeltable2struct) translate to/from the native readers' struct arrays; apply_parcellation accepts either format. - Updated tests (assert imagetype + istable(label_table)) and the methods docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ization pipeline surface() / render_on_surface() now accept the same colour vocabulary as the master visualization pipeline (addblobs / set_colormap): clim / cmaprange, colormap / colormapname (single sequential map), pos_colormap / neg_colormap, splitcolor, maxcolor / mincolor, and color (solid). surface() forwards any colour option straight through to render_on_surface, and the via-volume path forwards the compatible options to image_vector/render_on_surface. So the same options colour surface data and volume blobs. Added test_harmonized_color_options; updated surface() help and the methods page. (Level 1 of the visualization harmonization; the stateful fmridisplay-layer integration is the next step.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tive layers) Level 2 of the visualization harmonization: an fmri_surface_data can now be added to a managed fmridisplay and driven by the same controls as volume data. - @fmridisplay/addblobs: detects an fmri_surface_data and dispatches to a new @fmridisplay/add_surface_blobs, which registers it as a SURFACE-NATIVE layer (source_surface) rather than converting to a volume region. - @fmridisplay/render_layer_surfaces: paints surface-native layers by colouring the per-vertex data DIRECTLY on any registered mesh whose vertex count matches the object's space (fs_LR-32k / fsaverage-164k), at full fidelity, using the same central canlab_colormap value->colour map as montages (so colours match). set_colormap / set_opacity / removeblobs / refresh / the controller act on the layer like a volume layer. Non-matching meshes are skipped; a surface layer has no montage representation (use to_fmri_data / surf2vol for that). - Fix @fmri_surface_data/isempty: image_vector/isempty treats an empty volInfo as empty, but a cortex-only surface object legitimately has an empty volInfo, so a fully-populated cortical map wrongly reported empty. Now empty iff .dat has no rows. - Added canlab_test_surface_fmridisplay (addblobs paints, set_colormap/removeblobs propagate, non-matching/no-surface handled). Full surface suite: 47 passing. - Documented the managed-display integration in the methods page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r the controller Redesign the fmri_surface_data surface() UX so the stateful display is the single, self-consistent entry point: - fmri_surface_data/surface(obj) native mode now returns a managed fmridisplay (not a raw-handle struct): it adds the surface set that MATCHES the object's space (fsLR_32k -> foursurfaces_hcp, fsaverage_164k -> foursurfaces_freesurfer) and paints the data as a managed surface-native layer. This is what makes the colormap changeable after rendering (set_colormap / removeblobs / refresh now act on the surfaces). Non-default surftype (midthickness/sphere) is honored by swapping the managed patches' geometry. - fmridisplay/surface(o2, obj[, keyword]) detects an fmri_surface_data argument, adds its native surfaces (matching set by default, or an explicit keyword) and paints it via add_surface_blobs, all under controller management -- so "o2 = surface(o2, obj)" actually renders instead of silently dropping obj into the addbrain pass-through. - Fix a hemisphere-assignment bug: addbrain relabels the foursurfaces_* patches with the group keyword, erasing their left/right tags, which made the native layer paint LEFT data onto BOTH hemispheres. render_layer_surfaces now resolves hemisphere by tag, falling back to vertex x-centroid sign (left < 0 < right). - Cross-space request (e.g. fsaverage meshes for fs_LR data) now emits a clear fmridisplay:render_layer_surfaces:spacemismatch warning instead of rendering something wrong -- a surface object has no volume to resample onto a foreign mesh, so the fix is to add the matching surfaces (surface(obj) does so). - add_surface_blobs normalizes color aliases (clim -> cmaprange, colormapname -> colormap, named colormap string -> numeric LUT) so a managed surface layer honors the same color vocabulary as the volume path. - Clarify the .geom property doc: it holds ATTACHED custom mesh geometry from a .surf.gii and is empty for a data-only CIFTI (that is expected) -- standard meshes are loaded on demand by surface_space, not cached in .geom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mismatch warning - canlab_test_surface_fmridisplay: add tests that surface(obj) returns a managed fmridisplay and paints each hemisphere with ITS OWN data (regression for the foursurfaces tag/x-centroid hemisphere fix); that surface(o2, obj) adds+paints on an existing display; and that an explicit cross-space request warns with fmridisplay:render_layer_surfaces:spacemismatch and does not mispaint. - canlab_test_surface_render: update the native-render tests to the managed fmridisplay return (via a cortex_patches helper) instead of the old struct. All surface_data tests pass (50 passed / 0 failed; 1 incomplete = ICA toolbox absent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-mismatch behavior Update the method reference (dev + readthedocs copies), the runnable walkthrough, and the how-to to show that surface(obj) returns a managed fmridisplay, how to recolor live with set_colormap, how surface(o2, obj) adds+paints on an existing display, and the spacemismatch warning for cross-space surfaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r, opacity, rethreshold, colormap) The surface-native layer was bolted onto the stateful display but did not integrate with the controller-driven machinery. Fixes so it behaves like a volume layer: - Controller sync: add_surface_blobs now calls update_controller (like addblobs), so the auto-launched controller reflects the new surface layer instead of keeping its empty state until the user reopens it by hand. - Opacity: paint_surface_native_layer now saves the anatomy gray to the patch UserData on first paint, and composite_surfaces resets to gray when ANY patch in a view carries saved anatomy. Previously the layer blended against its OWN colour (base == painted colour), so set_opacity was a no-op. Now opacity blends toward gray and restores fully at 1. - Rethreshold: add a surface-native branch that stores the threshold as a per-vertex magnitude cutoff (applied at paint time) instead of building a region from the object -- a cortical surface object has no matching volInfo, so the old image_vector path produced "Illegal size for mask.dat" / "Mask has multiple images" warnings and did not actually threshold the surface. - Colormap keeps the threshold: canlab_colormap.map preserved NaN for the split map but not for 'single' (maxcolor/mincolor) or 'solid' (color) -- clamp01(NaN) became 0, painting uncoloured/thresholded vertices with the min endpoint. Now both preserve NaN, so set_colormap no longer resets a rethreshold and the medial wall stays gray under a single/solid map. - refresh skips the montage render_blobs path for surface-native layers (they have no volume mapdata); the surfaces are redrawn by composite_surfaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ace + montage) A mixed grayordinate object (cortex + subcortical voxels, e.g. a 91k dscalar) previously rendered only its cortex; the subcortical data was dropped. - surface(obj) / surface(o2, obj): when the object has a subcortical sub-block, add its voxels as a second managed VOLUME layer (via to_fmri_data), so the subcortical anatomical meshes the foursurfaces set draws (thalamus, brainstem, cerebellum, ...) are painted. The layer is confined to the subcortical meshes via skip_cortex_nv (in render_layer_surfaces) so it never bleeds onto the cortical surfaces / medial wall, which the surface-native cortex layer owns. Cortex and subcortex share one colour range unless the caller passes clim/cmaprange, and both respond to set_colormap/rethreshold/set_opacity/the controller together. - montage(o2, obj): detect an fmri_surface_data argument, build the montage, and render the subcortical grayordinates on the slices (instead of ignoring obj). Cortex-only objects warn and point to surface(obj). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ering - Tests: opacity blends toward gray and restores; rethreshold applies a per-vertex magnitude cutoff with no volInfo warning; set_colormap preserves the threshold; a mixed object renders cortex + subcortex without bleeding onto the medial wall; montage(o2, obj) draws the subcortical grayordinates. Update the render tests for the cortex+subcortex two-layer result. - Docs: document mixed-object subcortical rendering and the surface rethreshold (magnitude cutoff) in the method reference. All surface_data tests pass (54 passed / 0 failed; 1 incomplete = ICA toolbox absent); canlab_colormap (19) and fmridisplay_handle (47) suites pass unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oller in sync when adding surfaces - addblobs consumed 'wh_surface'/'wh_montage' (and variants) but still forwarded them verbatim to render_blobs, which warned "Unknown input string option: wh_surface" whenever a surface layer was added to a display that also has montages (e.g. surface(o2, obj, 'foursurfaces_hcp') after montage(o2, obj), or the subcortical volume layer surface() adds for a mixed object). Strip these view-selectors from varargin after parsing, so they never reach render_blobs nor persist in render_args (where refresh would forward them again). - @fmridisplay/surface now calls update_controller after adding a surface view (both the single-surface and foursurfaces_* multi paths), so an open controller stays bound to the same object and reflects the current views. No new controller is ever created (update_controller is a no-op when none is open; fmridisplay is a handle class so surface(o2, ...) mutates o2 in place). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e/continuous maps Recolouring a split-range layer to a single (maxcolor/mincolor) or continuous (numeric 'colormap', e.g. hot) map reused the layer's 4-element split cmaprange verbatim, so the map took [lo hi] = range(1:2) -- the NEGATIVE arm only. Every value above the negative arm's top then clamped to the max colour: with hot that is white, so a mixed grayordinate object's subcortical surfaces (whose values sit in the positive arm) rendered ~96% white after set_colormap(o, 'colormap', hot). canlab_colormap.single / .continuous now collapse a 4-element range to its full span [range(1) range(4)] (mirroring how .split calls expand_split_range). Verified visually across surface + volume, cortex + subcortex, with hot / parula / maxcolor-mincolor / solid: no white artifacts, and the standard volume path is unchanged (set_colormap hot on a volume montage+surface stays correct). Adds a regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New loadable keywords (files in Neuroimaging_Pattern_Masks/spatial_basis_functions):
- hcp_ica15 / hcp_ica25 / hcp_ica50 (aliases hcp15/hcp25/hcp50): HCP resting-state
group-ICA components, returned as fmri_surface_data (91k grayordinates).
- spectral_bases (alias hcp_bases): 200 spectral (Laplacian eigenmap) basis
functions -> fmri_surface_data.
- mito_maps (alias mito): mitochondrial energetic-capacity maps (CI, CII, CIV,
MitoD, MRC, TRC) -> fmri_data (6 maps), with names.
CIFTI/grayordinate sets return fmri_surface_data; volumetric sets return fmri_data.
load_image_set('list') now prints a series of CATEGORIZED tables, each under a
descriptive title -- multivariate signatures, person-level datasets, network/ICA/
topic maps, surface (CIFTI) map sets, gradients & basis maps, and meta-analysis/
receptor maps -- and RETURNS a struct whose fields are those tables (previously it
returned a single table). The signatures table is unchanged and is the .signatures
field. Help header documents 'list' prominently and the new keywords.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ized 'list'
- Register the Tian 3T subcortical atlas at explicit granularity scales:
tian_s1 / tian_s2 / tian_s3 / tian_s4 (16 / 32 / 50 / 54 regions), each with an
_fsl6 (MNI152NLin6Asym) variant; default is fmriprep20. The existing tian_3t
keyword already loads the finest scale (S4). Removes a stale commented-out
schaefer400 block (that file is a 32-region network-level parcellation, already
covered by yeo17networks, not the 400-parcel Schaefer).
- load_atlas('list') prints categorized tables of atlas keywords (combined, cortex,
subcortical, thalamus/hypothalamus, brainstem/cerebellum, networks) and returns a
struct of those tables, mirroring load_image_set('list'). Help header documents it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New workflow page docs/workflows/load_image_set_howto.md: discovering sets with
load_image_set('list'), loading datasets, applying signatures (apply_mask), image
similarity to networks/topics (image_similarity_plot), dual regression
(dual_regression), and surface (CIFTI) map sets. Linked from docs/Workflows.md.
- canlab_test_load_registries.m: verifies the new surface/volumetric keywords return
the right object types and map counts, that both 'list' options return a struct of
tables, and that the Tian S1-S4 atlases load with the expected region counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This registry test needs the full Neuroimaging_Pattern_Masks data (large CIFTI / .mat binaries). It is headless-safe and its per-test assumeFail guards already prevent CI failures, but by request it is now kept out of the CI unit tier: setupOnce assumeFail's the whole file when GITHUB_ACTIONS is set, so it is skipped (Incomplete) in CI and runs normally when executed locally. teardownOnce is guarded so the early skip does not error on the unset figvis field (which would otherwise flip filtered tests to failed). Local: 6 passed / 0 failed. Simulated CI: 0 failed / 6 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The required data files (HCP-ICA / spectral CIFTI, mito_maps.mat, and the Tian S1-S4 atlas objects) are all committed to the public Neuroimaging_Pattern_Masks repo that CI checks out, and the test is headless-safe, so it runs in the CI unit tier. Removes the GITHUB_ACTIONS skip added earlier; the per-test assumeFail guards remain so a missing file is skipped (Incomplete) rather than failing CI, and the teardownOnce guard stays as defensive cleanup. Verified with GITHUB_ACTIONS set: 6 passed / 0 failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
New CANlab object
fmri_surface_data(subclass ofimage_vector) — the surface/grayordinate analogue offmri_data, wrapping the HCP CIFTI grayordinate standard as a true CANlab object. Runs natively in MATLAB with no external toolbox (sole exception:ica, which needs GIFT/icatb like the base class).What's included
CanlabCore/Surface_tools/canlab_*), bit-exact vscifti_read/gifti; verified to work with those tools removed from the path.brain_modelreplacesvolInfo(populated for the subcortical sub-block);.datalways full — no empty-squeezing (remove_empty/replace_emptyno-ops).reconstruct_image,to_fmri_data,compare_space,write.vol2surf/surf2volvia the vendored CBIG RF warps (vol→surf→vol r≈1.0), fully native.cat/horzcat,mean,apply_mask,threshold(+cluster-extentk),ttest,regress,predict,ica(predict/ttest/ica delegate tofmri_data+ remap).surface(native + any addbrain/MNI surface via resampling),render_on_surface,plot;reparse_contiguous,apply_parcellation,surface_region.docs/fmri_surface_data_methods.md(integrated intoObject_methods.md/Workflows.md),docs/workflows/fmri_surface_data_howto.md(with figures), plus developer docs + runnable walkthrough underCanlabCore/docs/.Sample_datasets/CIFTI_surface_examples/; other atlases resolve fromNeuroimaging_Pattern_Masks.CanlabCore/Unit_tests/surface_data/— 7 files, 41 tests passing (ica skipped when its toolbox is absent).Incidental fixes
@image_vector/ica.m: removed a stray non-functional debug line that madeicaerror for allimage_vectorsubclasses.addbrain.mhelp: documented the fs_LR/fsaverage standard-mesh correspondence.Cifti_plottingreaders.Notes / deferred (see design plan)
vol2surf/surf2voluse a fixed group MNI152↔fsaverage correspondence (not a per-subject ribbon mapper); fsaverage↔fs_LR deformation is a planned enhancement.fmri_surface_statistic_image/fmri_surface_atlassubclasses; CC0 TemplateFlow meshes.🤖 Generated with Claude Code