Skip to content

Add fmri_surface_data object for CIFTI/GIFTI surface & grayordinate data#86

Open
torwager wants to merge 30 commits into
masterfrom
canlab_fmri_surface_object
Open

Add fmri_surface_data object for CIFTI/GIFTI surface & grayordinate data#86
torwager wants to merge 30 commits into
masterfrom
canlab_fmri_surface_object

Conversation

@torwager

@torwager torwager commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

New CANlab object fmri_surface_data (subclass of image_vector) — the surface/grayordinate analogue of fmri_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

  • Native I/O: CIFTI-2 + GIFTI readers/writers (CanlabCore/Surface_tools/canlab_*), bit-exact vs cifti_read/gifti; verified to work with those tools removed from the path.
  • Object model: brain_model replaces volInfo (populated for the subcortical sub-block); .dat always full — no empty-squeezing (remove_empty/replace_empty no-ops).
  • Interop: reconstruct_image, to_fmri_data, compare_space, write.
  • Volume↔surface: vol2surf/surf2vol via the vendored CBIG RF warps (vol→surf→vol r≈1.0), fully native.
  • Analysis: cat/horzcat, mean, apply_mask, threshold (+cluster-extent k), ttest, regress, predict, ica (predict/ttest/ica delegate to fmri_data + remap).
  • Rendering: surface (native + any addbrain/MNI surface via resampling), render_on_surface, plot; reparse_contiguous, apply_parcellation, surface_region.
  • Docs: docs/fmri_surface_data_methods.md (integrated into Object_methods.md/Workflows.md), docs/workflows/fmri_surface_data_howto.md (with figures), plus developer docs + runnable walkthrough under CanlabCore/docs/.
  • Sample data: one small HCP Open Access group myelin map in Sample_datasets/CIFTI_surface_examples/; other atlases resolve from Neuroimaging_Pattern_Masks.
  • Tests: 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 made ica error for all image_vector subclasses.
  • addbrain.m help: documented the fs_LR/fsaverage standard-mesh correspondence.
  • Deprecation pointers in the external-dependent Cifti_plotting readers.

Notes / deferred (see design plan)

  • vol2surf/surf2vol use a fixed group MNI152↔fsaverage correspondence (not a per-subject ribbon mapper); fsaverage↔fs_LR deformation is a planned enhancement.
  • Optional future polish: dedicated fmri_surface_statistic_image/fmri_surface_atlas subclasses; CC0 TemplateFlow meshes.

🤖 Generated with Claude Code

torwager and others added 3 commits July 9, 2026 00:39
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>
@torwager torwager force-pushed the canlab_fmri_surface_object branch from 32b80e6 to f178d11 Compare July 9, 2026 04:41
torwager and others added 26 commits July 9, 2026 00:52
…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>
…fusion mappers

State clearly, in the method help headers and the surface docs, that vol2surf and
surf2vol are native reimplementations of the CBIG Registration-Fusion (RF-ANTs)
MNI152<->fsaverage mappers (Wu et al. 2018, Human Brain Mapping): vol2surf is a
line-for-line equivalent of CBIG_RF_projectMNI2fsaverage using the identical
vendored warp, driven by SPM's volInfo.mat instead of FreeSurfer's MRIread, so no
FreeSurfer or Connectome Workbench is needed. surf2vol is the native inverse using
the same warp. Also note that CBIG's heavier fsaverage2Vol ribbon-fill script
requires FreeSurfer + the CBIG MARS toolbox + external mask geometry (not bundled)
and is intentionally not wired in. Adds the Wu et al. 2018 reference. Docs only; no
code changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…urface_data

New method resample_surface(obj, target_space) resamples cortical data between
surface spaces natively (no FreeSurfer / Connectome Workbench):

- fsaverage_164k <-> fs_LR_32k (HCP CIFTI) via the vendored HCP registration
  ("deformed") sphere that expresses fsaverage vertices in the fs_LR-aligned
  frame, so both meshes share one spherical frame; resampled with the bundled
  spherical barycentric / nearest-neighbor engine.
- fsaverage_164k <-> fsaverage6/5/4 as the exact nested icosahedral subset
  (verified: first 40962 verts form a valid ic6 mesh, 81920 faces).

Accuracy/speed: barycentric ("linear") is the default for continuous data
(round-trip corr ~0.9999 vs ~0.998 for nearest); interpolation weights depend
only on geometry, so they are computed once and applied to all maps as a sparse
matrix multiply -- a 50-map object costs about the same as one map (~10s one-time
weight build for fsaverage<->fs_LR). Binary masks and .dlabel images auto-select
nearest to preserve discrete values; 'interp' overrides. Subcortical (voxel)
models are carried through unchanged. resample_surface(obj,'list') prints the
available target spaces + aliases.

Tests (canlab_test_surface_resample, 8/8 pass; core cases use a synthetic
fsaverage object from bundled assets, so no NPM needed): fsaverage->fs_LR, exact
nested downsample, round-trip accuracy, binary->nearest auto, interp override,
multi-map weight reuse, subcortex carried through.

Docs: a prominent "Surface spaces" section (fsaverage164k/6/5/4, fs_LR-32k, the
91k grayordinate model) with resolutions and references (Fischl 1999, Van Essen
2012, Glasser 2013); clarified that .geom holds ATTACHED CUSTOM meshes only and
standard meshes load on demand; and resample_surface method entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
onavg (Feilong et al. 2024, Nature Methods; CC0) is an equal-area cortical
surface template. TemplateFlow tpl-onavg ships onavg spheres registered to the
fs_LR frame (space-fsLR), which is the same common frame resample_surface already
uses -- so onavg resamples to fsaverage / fs_LR with the existing native spherical
engine, no FreeSurfer/Workbench.

- Vendor the space-fsLR onavg registration spheres (den-41k = 40962 verts/hemi,
  den-10k = 10242) as .mat (single verts / uint32 faces, ~2.35 MB total) under
  canlab_canonical_brains/Canonical_brains_surfaces/onavg/, with a provenance
  README (source, CC0 license, reference).
- resample_surface gains target/source spaces 'onavg' / 'onavg_41k' and
  'onavg_10k' (aliases onavg41k / onavg10k). onavg is a non-nested tessellation,
  so it always interpolates (no exact-subset fast path). Verified: fsaverage ->
  onavg -> fsaverage round-trip corr 0.9999; fs_LR -> onavg works.
- Test + docs: onavg round-trip test; onavg rows in the 'list' output and the
  Surface spaces table (with reference).

Only the registration spheres are vendored (enough to resample). onavg group
statistics (sulcal depth, curvature, vertex area) on GIN are not bundled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously, rendering an fmri_surface_data layer onto a surface in a different
space (e.g. fs_LR data on an fsaverage foursurfaces_freesurfer mesh) skipped the
mesh with a spacemismatch warning. Now the painter resamples the data onto that
mesh's space automatically and paints it.

- resample_surface now accepts an isosurface PATCH HANDLE or a mesh struct (with
  .vertices) as the target, resolving the space by per-hemisphere vertex count
  (32492 -> fs_LR, 163842 -> fsaverage_164k, 40962/10242/2562 -> fsaverage6/5/4).
  A non-standard vertex count errors clearly (cannot resample onto an arbitrary
  mesh; use surf2vol + render_on_surface).
- render_layer_surfaces/paint_surface_native_layer: a cortical patch whose vertex
  count differs from the data but is a recognized standard mesh triggers an
  on-the-fly resample_surface (nearest, for render speed), CACHED on the layer
  (obj.activation_maps{k}.resampled) so set_colormap / rethreshold / set_opacity
  re-renders are instant (verified 0.4s first paint, 0.02s cached re-render). Only
  truly non-standard meshes remain unpainted (spacemismatch warning, reworded).
  Threshold is applied per hemisphere via a shared helper so native and resampled
  meshes behave identically.

Tests: canlab_test_surface_fmridisplay's mismatch test now asserts auto-resample
+ paint (was: warn + not painted); a resample test covers patch/mesh-struct
targets and the non-standard-count error. Full surface suite 64 passed / 0 failed
(1 incomplete = ICA); volume fmridisplay_handle 47/47 unaffected.

Docs: document rendering auto-resample and the patch/mesh target of
resample_surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…olume projection

Auto-resampling handled cross-space *standard* meshes (fs_LR<->fsaverage), but
arbitrary MNI isosurfaces with no spherical registration (addbrain 'hires left',
'cutaway', pial meshes) still warned and went unpainted. Now they are painted by
projecting the data to a volume and sampling it at the mesh vertices.

- New public method to_display_volume(obj): project any surface/grayordinate object
  to an MNI fmri_data -- cortex resampled to fsaverage (resample_surface, auto
  nearest for labels) then surf2vol, MERGED with subcortex (to_fmri_data). The
  private obj_to_volume now delegates to it, so render_on_surface(any_surf, arbitrary
  patch) works for fs_LR data too (previously fsaverage-only) and includes subcortex.
- render_layer_surfaces/paint_surface_native_layer: a patch whose vertex count is
  not a recognized standard cortical mesh now projects via to_display_volume (cached
  on the layer as display_volume) and paints with image_vector.render_on_surface
  using the layer's truecolor map + opacity; the layer threshold is applied to the
  volume. Only a genuinely failing projection leaves a mesh unpainted. Verified on
  the Gordon atlas: 'hires left' and 'cutaway' render cortex + subcortex, no warning,
  ~1.7-3.6s first paint, cached re-renders fast.

Tests: to_display_volume returns a cortical MNI volume; rendering onto an addbrain
'hires left' mesh paints it (guarded). Full surface suite 64 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…for auto-selection

- New helper canlab_color_mode(dat) is the single source of truth for choosing a
  blob colour mode: 'solid' (<=2 distinct nonzero values, i.e. a binary mask),
  'unique' (an atlas / .dlabel parcellation, or a few integer labels), or
  'colormap' (continuous / many values). Cutoff 1000 for a generic integer map;
  atlas and .dlabel/.label objects are always 'unique' regardless of region count
  (Gordon-333, Julich-314, etc. exceed the old 300 orthviews cutoff).

- fmri_surface_data surface rendering (add_surface_blobs) honours an explicit
  'unique' or 'solid' flag and, when no colour spec is given, auto-selects the mode
  via canlab_color_mode. 'unique' -> one solid colour per region (scn_standard_colors
  as an indexed colormap); 'solid' -> a single colour for all in-data vertices.
  surface(o2, surf_obj) / surface(surf_obj) / addblobs route the flags through.
  Verified: surface(gordon_dlabel) now auto-renders a unique-colour parcellation
  (cortex + subcortex); surface(obj,'solid') and 'unique' work explicitly;
  continuous maps still default to a colormap.

Tests: canlab_color_mode classification + surface unique/solid/auto (14/14 in the
fmridisplay suite; full surface suite 66 passed / 0 failed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… -> light sea green

- render_blobs now treats 'unique' and 'solid' as recognized (ignored) options
  instead of emitting "Unknown input string option:unique". These flags are handled
  upstream (region/montage colortype, add_surface_blobs), so montage(atl,'unique')
  no longer prints the spurious warning. addblobs already suppresses its own
  unknown-option warning.
- fmridisplay controller window background changed from orange to light sea green
  ([0.125 0.698 0.667]).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… consistency)

Root cause of montage slices intermittently rendering in the wrong figure/axes: the
controller is a uifigure, and opening OR rebuilding it (auto-launch in the
constructor, update_controller after addblobs/set_colormap, or an explicit
controller() call) could leave it as gcf, so the next gca/gcf-based slice drawing
targeted the controller or a stale figure. The constructor already guarded its own
launch; this moves the guard INTO controller() via onCleanup so EVERY entry path
(including the early update-in-place return) restores the caller's current figure.

Timing-neutral (the controller still opens when it did before), so it does not
disturb figure-timing behavior -- verified the full fmridisplay_handle suite (47/47,
incl. test_foursurfaces_uses_dedicated_figure) and the surface suite (67 passed)
still pass. The bug is interactive (real-window focus) and does not reproduce
headless, but this addresses the mechanism directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wn item

The colormap dropdown listed 'indexed (atlas)' but on_colormap had no case for it
(a no-op). Renamed it to 'unique (per region)' and wired it up: selecting it applies
one solid colour per region via set_colormap('indexmap', scn_standard_colors(n)) on
the layer, and current_colormap_label reports 'unique (per region)' for an indexed
layer. The dropdown now offers colormap options, 'unique (per region)', and
'solid colour…'. Verified: option present, initial value mapping, and the underlying
set_colormap(indexmap) action; fmridisplay_handle (47/47) and surface_fmridisplay
(14/14) suites pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
surface() on a volumetric atlas (via image_vector/surface -> region/surface)
rendered the integer region indices through a CONTINUOUS colormap (blended at
borders), and 'unique' produced an "Unknown input string option" warning.

region/surface now:
- accepts 'unique'/'solid' flags (no warning);
- 'unique' -> one solid colour per region (scn_standard_colors as an indexed
  colormap; render_on_surface uses nearest automatically), matching region/montage;
- 'solid' -> a single colour;
- when no explicit colour spec is given, auto-selects the mode via the shared
  canlab_color_mode. region2imagevec stores integer region indices for an atlas
  (-> 'unique') but continuous .Z values for a statistic map (-> 'colormap'), so
  surface(atlas) now auto-renders unique colours while surface(t) keeps its
  colormap (verified: no regression on continuous maps).

Test added (guarded on the julich atlas); surface_data suite 9/9, full surface +
fmridisplay_handle suites 114 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant