Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dff5b80
Add test cases for contour rendering feature (TDD)
mvdoc Mar 23, 2026
d41ff8e
Add get_contour_vertices() and quickflat contour functions
mvdoc Mar 23, 2026
ac1cf5b
Wire with_contours parameter into quickflat.make_figure()
mvdoc Mar 23, 2026
18028cc
Add contour rendering to WebGL surface_vertex shader
mvdoc Mar 23, 2026
6d3768b
Add WebGL contour UI controls and overlay selection
mvdoc Mar 23, 2026
25934ec
Fix contour overlay UI crashing Vertex data in WebGL viewer
mvdoc Mar 23, 2026
d3bb461
Expose contour parameters to programmatic WebGL API
mvdoc Mar 23, 2026
e91ed16
WIP: Fix overlay mode contour data binding
mvdoc Mar 23, 2026
ab67217
Fix WebGL contour overlay data binding and uniform types
mvdoc Mar 23, 2026
eb140a4
Fix contourMode dropdown value parsing
mvdoc Mar 24, 2026
6de3e41
Fix WebGL contour overlay data binding
mvdoc Mar 24, 2026
bff2fd0
Add quickflat contour rendering example
mvdoc Mar 24, 2026
af46532
Reorganize WebGL contour UI into dedicated folder
mvdoc Mar 24, 2026
46abcf3
Add colored contour mode using overlay colormap
mvdoc Mar 24, 2026
2c1dfd0
Use overlay's own colormap for colored contours; reorder UI
mvdoc Mar 24, 2026
bb76580
Add headless WebGL contour rendering example
mvdoc Mar 24, 2026
b1ee62d
Add contour_overlay/contour_mode params to save_3d_views and plot_panels
mvdoc Mar 24, 2026
26dc76c
Accept Vertex directly as contour_overlay in save_3d_views
mvdoc Mar 24, 2026
35e123d
Use string contour_mode names; default to 'contours+fill' (black bord…
mvdoc Mar 24, 2026
59e446e
Address PR review issues: critical fixes and documentation
mvdoc Mar 24, 2026
7047957
Fix pre-existing typo: Unkown -> Unknown
mvdoc Mar 24, 2026
02877c6
Add contour support to surface_pixel shader for Volume+Vertex overlay
mvdoc Mar 24, 2026
293b4d2
Address Copilot review comments
mvdoc Mar 24, 2026
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
13 changes: 13 additions & 0 deletions cortex/export/panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def plot_panels(
interpolation: str = "nearest",
layers: int = 1,
headless: bool = False,
contour_overlay: Optional[Union[str, Dataview]] = None,
contour_mode: str = "contours+fill",
) -> Figure:
"""Plot on the same figure a number of views, as defined by a list of panel
specifications.
Expand Down Expand Up @@ -110,6 +112,15 @@ def plot_panels(
Software WebGL (SwiftShader) is used, so no GPU or display server is
needed. (Default: False)

contour_overlay : Dataview, str, or None
Parcellation data whose borders will be drawn as contour lines.
Can be a Vertex/Dataview (automatically bundled into a Dataset),
or a string naming a view within an existing Dataset. (Default: None)

contour_mode : str
Contour rendering mode. Options: "contours", "contours+fill",
"colored", "colored+fill". (Default: "contours+fill")

Returns
-------
fig : matplotlib.Figure
Expand Down Expand Up @@ -150,6 +161,8 @@ def plot_panels(
interpolation=interpolation,
layers=layers,
headless=headless,
contour_overlay=contour_overlay,
contour_mode=contour_mode,
)

fig = plt.figure(figsize=figsize)
Expand Down
98 changes: 81 additions & 17 deletions cortex/export/save_views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import contextlib
import os
import time
from typing import Any, Mapping, Sequence, TypedDict, Union
from typing import Any, Mapping, Optional, Sequence, TypedDict, Union

import cortex

Expand Down Expand Up @@ -38,6 +38,8 @@ def save_3d_views(
trim: bool = True,
sleep: float = 10,
headless: bool = False,
contour_overlay: Optional[Union[str, Dataview]] = None,
contour_mode: str = "contours+fill",
) -> list[str]:
"""Saves 3D views of `volume` under multiple specifications.

Expand All @@ -47,47 +49,47 @@ def save_3d_views(

Parameters
----------
volume: pycortex.Volume or pycortex.Vertex object
volume : pycortex.Volume, pycortex.Vertex, or pycortex.Dataset
Data to be displayed.

base_name: str
base_name : str
Base name for images.

list_angles: list of (str or dict)
list_angles : list of (str or dict)
Views to be used. Should be of length one, or of the same length as
`list_surfaces`. Choices are:
'left', 'right', 'front', 'back', 'top', 'bottom', 'flatmap',
'medial_pivot', 'lateral_pivot', 'bottom_pivot',
or tuple of (view_name, custom dictionary of parameters).
See `angle_view_params` in this file for parameter dict examples.

list_surfaces: list of (str or dict)
list_surfaces : list of (str or dict)
Surfaces to be used. Should be of length one, or of the same length as
`list_angles`. Choices are:
'inflated', 'flatmap', 'fiducial', 'inflated_cut',
or a custom dictionary of parameters.

viewer_params: dict
viewer_params : dict
Parameters passed to the viewer.

interpolation: str
interpolation : str
Interpolation used to visualize the data. Possible choices are "nearest",
"trilinear". (Default: "nearest").

layers: int
layers : int
Number of layers between the white and pial surfaces to average prior to
plotting the data. (Default: 1).

size: tuple of int
size : tuple of int
Size of produced image (before trimming).

trim: bool
trim : bool
Whether to trim the white borders of the image.

sleep: float > 0
sleep : float > 0
Time in seconds, to let the viewer open.

headless: bool
headless : bool
If True, render using a headless Chromium browser via Playwright instead
of requiring the user to manually open a browser window. This allows
the function to run fully autonomously without any user interaction.
Expand All @@ -96,14 +98,39 @@ def save_3d_views(
Software WebGL (SwiftShader) is used, so no GPU or display server is
needed. (Default: False)

contour_overlay : Dataview, str, or None
Parcellation data whose borders will be drawn as contour lines.
Can be a Vertex/Dataview object (automatically bundled into a Dataset
with ``volume``), or a string naming a view within an existing Dataset
passed as ``volume``. (Default: None)

contour_mode : str
Contour rendering mode when ``contour_overlay`` is set.
Options: "contours", "contours+fill", "colored", "colored+fill".
(Default: "contours+fill")

Returns
-------
file_names: list of str
file_names : list of str
Image paths.
"""
msg = "list_angles and list_surfaces should have the same length."
assert len(list_angles) == len(list_surfaces), msg

# If contour_overlay is a Dataview, bundle volume + overlay into a Dataset.
# Preserve the original volume reference for isinstance checks below.
_contour_overlay_name = None
_original_volume = volume
if contour_overlay is not None:
if isinstance(contour_overlay, str):
_contour_overlay_name = contour_overlay
else:
# contour_overlay is a Dataview — wrap into Dataset
_contour_overlay_name = "__contour_overlay__"
volume = cortex.Dataset(
data=volume, **{_contour_overlay_name: contour_overlay}
)

# Create viewer — use a proper context manager so that cleanup always
# runs, even if an exception occurs during rendering.
if headless:
Expand All @@ -117,16 +144,53 @@ def save_3d_views(
# Wait for the viewer to be loaded
time.sleep(sleep)

# Add interpolation and layers params only if we have a volume
if isinstance(volume, (cortex.Volume, cortex.Volume2D, cortex.VolumeRGB)):
# Set up contour overlay if requested
if _contour_overlay_name is not None:
_contour_mode_map = {
"contours": 1,
"contours+fill": 2,
"colored": 3,
"colored+fill": 4,
}
if contour_mode not in _contour_mode_map:
raise ValueError(
f"Unknown contour_mode {contour_mode!r}. "
f"Valid options: {list(_contour_mode_map.keys())}"
)
_contour_mode_int = _contour_mode_map[contour_mode]
handle._set_view(
**{
"surface.{subject}.contours.overlay": _contour_overlay_name,
}
)
# Wait for overlay data to load
time.sleep(sleep)
handle._set_view(
**{
"surface.{subject}.contours.mode": _contour_mode_int,
}
)
time.sleep(1)

# Add interpolation and layers params only if the primary data is a volume.
# Use _original_volume (before Dataset wrapping) for the type check.
if isinstance(
_original_volume, (cortex.Volume, cortex.Volume2D, cortex.VolumeRGB)
):
interpolation_params = {
"surface.{subject}.sampler": interpolation,
"surface.{subject}.layers": layers,
}
else:
interpolation_params = dict()

has_flatmap = hasattr(getattr(cortex.db, volume.subject).surfaces, "flat")
# Get subject name — handle both Dataview and Dataset
if hasattr(_original_volume, "subject"):
_subject = _original_volume.subject
else:
# Dataset: get subject from first view
_subject = next(iter(volume))[1].subject
has_flatmap = hasattr(getattr(cortex.db, _subject).surfaces, "flat")
file_names: list[str] = []
for view, surface in zip(list_angles, list_surfaces):
if isinstance(view, str):
Expand Down Expand Up @@ -163,7 +227,7 @@ def save_3d_views(
# wait for the view to have changed
for _ in range(100):
for k, v in this_view_params.items():
k = k.format(subject=volume.subject) if "{subject}" in k else k
k = k.format(subject=_subject) if "{subject}" in k else k
if handle.ui.get(k)[0] != v:
print("waiting for", k, handle.ui.get(k)[0], "->", v)
time.sleep(0.1)
Expand Down
Loading