Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ Two orthogonal mixin axes: **braindata** (the array plus subject/transform ident
Flow: dataviews → JSON + PNG mosaics (`data.py:Package`) + compressed CTM surface packs (`cortex/brainctm.py`, cached per subject) → Tornado template (`template.html`, extended by `static.html` etc., resolved via `FallbackLoader` so user template dirs can override) → Three.js app in `resources/js/`.

- `view.py`: `show` (live Tornado server from `serve.py`, returns a `JSProxy` websocket RPC handle for driving JS from Python) and `make_static` (self-contained directory; `htmlembed.py` can inline everything into a single file).
- `serve.WebApp` binds its listening socket in `__init__`, on the loopback interface (`serve.LOOPBACK`) unless a caller passes another `address`; `view.show` opens up to every interface only when the `domain_name` config option names a domain to reach the viewer under.
- The bundled Three.js is **r69** — very old; the shader pipeline depends on its conventions, do not casually upgrade.
- All GLSL lives in `resources/js/shaderlib.js` as arrays of string lines assembled per-configuration — there are no `.glsl` files.
- CTM packs reorder vertices: `cortex.utils.get_ctmmap` / `get_ctm2webgl_map` translate between CTM/WebGL ordering and the original surface ordering. Indexing viewer data with original vertex indices without remapping is a classic bug.
- `cortex/export/headless.py` (`headless_viewer`) runs the viewer in headless Chromium via Playwright for screenshots/tests.
- `cortex/webgl/aligner.py` + `resources/js/aligner.js` (`aligner.html`, `resources/css/aligner.css`) is the browser-based manual aligner behind `cortex.align.webgl_manual`. It reuses the CTM pack, the mosaic PNG loader (`dataset.VolumeData`) and the `Shaders.aligner_*` builders in `shaderlib.js`, and works in a world frame that is the reference volume's voxel grid in mm permuted to RAS (`reference_frame`); the edited transform is the pycortex `coord` transform. Its `display` control picks one of three displays; the data view draws the surfaces in the anatomy's own frame with the morph targets of the CTM pack (`Shaders.aligner_volume({morphs: n})`), so they inflate and flatten there while the alignment only moves where the volume is sampled. `/save` writes to the filestore, so it takes only posts carrying the per-page `save_token` from the page config, and `JSAligner.save` polls the page's save state rather than returning while the request is still out.
- Some JS resources use CRLF line endings (e.g. `dataset.js`); keep the existing endings when editing.
- `setup.py` explicitly enumerates `cortex.webgl` `package_data` patterns — new resource subdirectories must be added there or they won't ship in wheels.

Expand Down
196 changes: 121 additions & 75 deletions cortex/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,90 +14,136 @@
from .xfm import Transform


def mayavi_manual(subject, xfmname, reference=None, **kwargs):
"""Open GUI for manually aligning a functional volume to the cortical surface for `subject`. This
creates a new transform called `xfm`. The name of a nibabel-readable file (e.g. nii) should be
supplied as `reference`. This image will be copied into the database.

To modify an existing functional-anatomical transform, `reference` can be left blank, and the
previously used reference will be loaded.

<<ADD DETAILS ABOUT TRANSFORMATION MATRIX FORMAT HERE>>

When the GUI is closed, the transform will be saved into the pycortex database. The GUI requires
Mayavi support.
def webgl_manual(
subject: str,
xfmname: str,
reference: Optional[str] = None,
view_only: bool = False,
cmap: Optional[str] = None,
mesh_color: Optional[str] = None,
mesh_opacity: Optional[float] = None,
open_browser: Optional[bool] = None,
autoclose: Optional[bool] = None,
port: Optional[int] = None,
recache: bool = False,
types: tuple[str, ...] = ("inflated",),
title: Optional[str] = None,
display_url: bool = True,
template: str = "aligner.html",
):
"""Open the browser-based aligner for manually aligning a functional volume
to the cortical surface of `subject`.

The functional reference volume stays on its own voxel grid, so its slices
are shown without resampling, and the pial and white matter surfaces are
moved into its space; in each slice view the surfaces are cut off at the
displayed slice, so that their outline can be compared with the anatomy
in the image. Only rotations and translations are possible.

The page shows the coronal, axial and sagittal slices and a 3D view of
the three slices. In a slice view, a left drag moves the cursor, which
sets the slices of the other views and is the pivot of rotations; the
wheel (or ``[`` and ``]``) changes the slice, ctrl + wheel zooms, and a
middle (or shift + left) drag pans. A right drag, the WASD keys or the
arrow keys translate the surfaces in the plane of the view under the
mouse, ctrl + right drag or ``q`` / ``e`` rotate them about the cursor;
shift makes the keyboard steps ten times smaller and ctrl + z undoes. The panel on
the right holds the Save button, the view mode (surface outlines on the
slices, or the volume painted on the surface, also toggled with ``m``),
the colormap with its range, brightness, contrast, gamma and flip, the
color and opacity of the surfaces, the slices and the keyboard steps.

The ``transform`` field above the Save button holds the name the
alignment is saved under. It starts as `xfmname`; editing it saves the
alignment as a new transform and leaves the one it was loaded from
alone. An asterisk on the Save button and in the window title marks an
alignment that differs from the one last saved.

When Save is pressed the transform is stored into the pycortex database,
as a 'coord' transform. A new transform requires `reference`, which is
copied into the database; an existing transform is loaded together with
its stored reference.

Saving also deletes the masks cached for the transform it writes, since
they were cut out of the reference volume through the alignment being
replaced. The page warns about this when it opens a transform that has
masks, and the save message names the ones it deleted. Data already
masked with them has to be masked again from the volumes. Pass
``view_only=True`` to inspect an alignment without saving.

Parameters
----------
subject : str
Subject identifier.
xfmname : str
String identifying the transform to be created or loaded.
Name of the transform to create or modify.
reference : str, optional
Path to a nibabel-readable image that will be used as the reference for this transform.
If given the default value of None, this function will attempt to load an existing reference
image from the database.
kwargs : dict
Passed to mayavi_aligner.get_aligner.
Path to a nibabel-readable functional volume, required for a new
transform. Must be None for an existing transform.
view_only : bool, optional
Open the aligner without the possibility to save, to inspect an
alignment. Default False.
cmap : str, optional
Initial colormap for the reference volume, one of the 1D pycortex
colormaps. Defaults to the `colormap` option of the `webgl_aligner`
section of the config file.
mesh_color : str, optional
Initial color of the surface outlines, as a matplotlib color.
Defaults to the `mesh_color` config option.
mesh_opacity : float, optional
Initial opacity of the whole surfaces in the 3D view (0 shows only
their outlines on the slices). Defaults to the `mesh_opacity` config
option.
open_browser : bool, optional
Open the aligner in the default browser. Defaults to the
`open_browser` option of the `webshow` config section.
autoclose : bool, optional
Stop the server when the last browser window disconnects. Defaults
to the `autoclose` option of the `webshow` config section.
port : int, optional
Port of the server; a free port is picked when None.
recache : bool, optional
Regenerate the cached surface (CTM) files. Default False.
types : tuple of str, optional
Surface types included in the CTM pack, to share the cache with the
viewer. Default ("inflated",).
title : str, optional
Title of the browser window. Defaults to the subject and the
transform.
display_url : bool, optional
When `open_browser` is False, display an IPython link to the
aligner. Default True.
template : str, optional
Name of the tornado template of the page. Default 'aligner.html'.

Returns
-------
m : 2D ndarray, shape (4, 4)
Transformation matrix.
handle : cortex.webgl.aligner.JSAligner or cortex.webgl.serve.WebApp
A handle to the running aligner: ``handle.get_xfm()`` returns the
current transform, ``handle.save()`` saves it. When the aligner is
started with ``open_browser=False`` the tornado server is returned
instead; its ``get_client()`` returns the handle once a browser has
connected.
"""

warnings.warn("This is the old cortex.align.manual(), and has been "
"deprecated. Please use the new cortex.align.manual() "
"(previously cortex.align.fs_manual()), which uses "
"the `freeview` program in the freesurfer suite, to "
"perform manual alignment.", DeprecationWarning
)
from .database import db
from .mayavi_aligner import get_aligner
def save_callback(aligner):
db.save_xfm(subject, xfmname, aligner.get_xfm("magnet"), xfmtype='magnet', reference=reference)
print("saved xfm")

def view_callback(aligner):
print('view-only mode! ignoring changes')

# Check whether transform w/ this xfmname already exists
view_only_mode = False
try:
db.get_xfm(subject, xfmname)
# Transform exists, make sure that reference is None
if reference is not None:
raise ValueError('Refusing to overwrite reference for existing transform %s, use reference=None to load stored reference' % xfmname)

# if masks have been cached, quit! user must remove them by hand
from glob import glob
if len(glob(db.get_paths(subject)['masks'].format(xfmname=xfmname, type='*'))):
print('Refusing to overwrite existing transform %s because there are cached masks. Delete the masks manually if you want to modify the transform.' % xfmname)
checked = False
while not checked:
resp = input("Do you want to continue in view-only mode? (Y/N) ").lower().strip()
if resp in ["y", "yes", "n", "no"]:
checked = True
if resp in ["y", "yes"]:
view_only_mode = True
print("Continuing in view-only mode...")
else:
raise ValueError("Exiting...")
else:
print("Didn't get that, please try again..")
except IOError:
# Transform does not exist, make sure that reference exists
if reference is None or not os.path.exists(reference):
raise ValueError('Reference image file (%s) does not exist' % reference)




m = get_aligner(subject, xfmname, epifile=reference, **kwargs)
m.save_callback = view_callback if view_only_mode else save_callback
m.configure_traits()

return m
from .webgl import aligner

return aligner.show(
subject,
xfmname,
reference=reference,
view_only=view_only,
cmap=cmap,
mesh_color=mesh_color,
mesh_opacity=mesh_opacity,
open_browser=open_browser,
autoclose=autoclose,
port=port,
recache=recache,
types=types,
title=title,
display_url=display_url,
template=template,
)


def fs_manual(subject, xfmname, **kwargs):
Expand Down Expand Up @@ -136,8 +182,8 @@ def manual(
ALSO: all the freesurfer environment stuff shouldn't be necessary, except that
I don't know what vox2ras-tkr is doing.

Renamed from fs_manual() to manual(), since old manual() function was no longer
supported (or functional) for a while due to changes in mayavi.
Renamed from fs_manual() to manual(), since the old manual() function had
been unsupported for a while.


Parameters
Expand Down
16 changes: 9 additions & 7 deletions cortex/defaults.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,14 @@ blender = blender
slim = None
meshlab = None

[mayavi_aligner]
line_width = 1
point_size = 2
outline_color = white
outline_rep = wireframe
opacity = 0
[webgl_aligner]
# Initial settings of the browser-based aligner (cortex.align.webgl_manual):
# the colormap of the reference volume (a 1D pycortex colormap), the color of
# the surface outlines (a matplotlib color) and the opacity of the whole
# surfaces in the 3D view (0 shows only their outlines on the slices).
colormap = gray
mesh_color = white
mesh_opacity = 0

[paths_default]
stroke = white
Expand Down Expand Up @@ -172,7 +173,8 @@ webgl_smooth = 0.0
[webgl]
layers = rois,
# When creating webgl viewers, this domain name will be appended to your computer name. Default is blank (no extra domain name)
domain_name =
# Blank also keeps the viewer on the loopback interface, where only this computer can reach it; setting it serves the viewer on every interface, under that name.
domain_name =
[webshow]
autoclose = true
open_browser = true
Loading
Loading