Note
Go to the end to download the full example code.
Plot 3D Brain Views Headlessly (No Browser)¶
cortex.export.save_3d_views and cortex.export.plot_panels can render
and save multiple 3D screenshots of brain data without any manual browser
interaction by passing headless=True.
Under the hood this launches a headless Chromium browser via Playwright, which connects to the pycortex webviewer and renders the WebGL scene using software rasterisation. You can use these functions to display and save 3D views of brain data in scripts and notebooks.
Prerequisites¶
Install Playwright and download the bundled Chromium binary once:
pip install playwright
playwright install chromium
import os
import tempfile
import numpy as np
import matplotlib.pyplot as plt
import cortex
import cortex.export
np.random.seed(42)
volume = cortex.Volume.random(subject="S1", xfmname="fullhead")
# Choose which angles and surface states to render
# Each entry in ``list_angles`` is paired with the corresponding entry in
# ``list_surfaces``. Both lists must have the same length.
list_angles = [
"lateral_pivot",
"medial_pivot",
"left",
"right",
]
list_surfaces = ["inflated"] * len(list_angles)
Render and save using plot_panels¶
Build a list of panels (one panel per angle/surface) and render them into a single figure with cortex.export.plot_panels. This uses the same headless renderer as cortex.export.save_3d_views.
panels: list[cortex.export.PanelParams] = []
n = len(list_angles)
for i, (angle, surface) in enumerate(zip(list_angles, list_surfaces)):
panels.append(
{
"extent": (i / n, 0.0, 1.0 / n, 1.0),
"view": cortex.export.PanelView(angle=angle, surface=surface),
}
)
fig = cortex.export.plot_panels(
volume,
panels=panels,
figsize=(2 * n, 2),
windowsize=(1024 * 2, 768 * 2),
viewer_params=dict(labels_visible=[], overlays_visible=["rois"]),
headless=True,
)
plt.show()

Generating new ctm file...
wm
wm
inflated
inflated
Started server on port 39192
{'camera.azimuth': 270, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 0, 'surface.{subject}.shift': 0, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 180, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 0, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 90, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 0, 'surface.{subject}.shift': 0, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
Stopping server
Save individual views to files¶
cortex.export.save_3d_views saves each angle as a separate PNG file.
# sphinx_gallery_multi_image_block = "single"
TARGET_WIDTH = 10 # inches — consistent width for uniform title sizing
base_name = os.path.join(tempfile.mkdtemp(), "fig")
fnames = cortex.export.save_3d_views(
volume,
base_name=base_name,
list_angles=list_angles,
list_surfaces=list_surfaces,
viewer_params=dict(labels_visible=[], overlays_visible=["rois"]),
headless=True,
)
for fname, angle in zip(fnames, list_angles):
img = plt.imread(fname)
aspect = img.shape[0] / img.shape[1]
fig, ax = plt.subplots(figsize=(TARGET_WIDTH, TARGET_WIDTH * aspect))
ax.imshow(img)
ax.axis("off")
ax.set_title(angle, fontsize=14, fontweight="bold")
fig.subplots_adjust(left=0, right=1, top=0.88, bottom=0)
plt.show()
Started server on port 6691
{'camera.azimuth': 180, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 0, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 0
waiting for camera.altitude {} -> 90
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 10
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
{'camera.azimuth': 90, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 0, 'surface.{subject}.shift': 0, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 90
waiting for camera.altitude {} -> 90
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 0
waiting for surface.S1.shift {} -> 0
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
{'camera.azimuth': 270, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 0, 'surface.{subject}.shift': 0, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 270
waiting for camera.altitude {} -> 90
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 0
waiting for surface.S1.shift {} -> 0
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
Stopping server
Predefined panel layouts¶
cortex.export ships with several ready-made panel configurations.
Every public name matching params_* is a dict that can be passed
directly to cortex.export.plot_panels. The loop below discovers
them automatically, so this gallery stays up-to-date when new presets
are added.
# sphinx_gallery_multi_image_block = "single"
predefined = {
name: getattr(cortex.export, name)
for name in sorted(dir(cortex.export))
if name.startswith("params_")
}
for name, params in predefined.items():
fig = cortex.export.plot_panels(volume, headless=True, **params)
w, h = fig.get_size_inches()
# Rescale to a consistent width
new_w = TARGET_WIDTH
new_h = h * (TARGET_WIDTH / w) + 0.6
scale = (h * TARGET_WIDTH / w) / new_h
for ax in fig.get_axes():
pos = ax.get_position()
ax.set_position([pos.x0, pos.y0 * scale, pos.width, pos.height * scale])
fig.set_size_inches(new_w, new_h)
fig.suptitle(name, fontsize=14, fontweight="bold", y=1.0 - 0.2 / new_h)
plt.show()
Started server on port 60082
{'camera.azimuth': 180, 'camera.altitude': 180, 'camera.target': [0, -100, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.altitude 179.9 -> 180
{'camera.azimuth': 180, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 0, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 0
waiting for camera.altitude {} -> 90
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 10
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
{'camera.azimuth': 180, 'camera.altitude': 0, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 1, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 0, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 180
waiting for camera.altitude {} -> 0
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 1
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 0
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
Stopping server
Started server on port 13581
{'camera.azimuth': 180, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 0, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 0
waiting for camera.altitude {} -> 90
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 10
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
{'camera.azimuth': 180, 'camera.altitude': 0, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 1, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 0, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 180
waiting for camera.altitude {} -> 0
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 1
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 0
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
Stopping server
Started server on port 23598
{'camera.azimuth': 180, 'camera.altitude': 180, 'camera.target': [0, -100, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.altitude 179.9 -> 180
{'camera.azimuth': 180, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 0, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 0
waiting for camera.altitude {} -> 90
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 10
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
{'camera.azimuth': 180, 'camera.altitude': 0, 'camera.target': [0, -100, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 180
waiting for camera.altitude {} -> 0
waiting for camera.target {} -> [0, -100, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 10
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
Stopping server
Started server on port 21312
{'camera.azimuth': 0, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.25, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 180, 'camera.altitude': 180, 'camera.target': [0, -100, 0], 'surface.{subject}.unfold': 0.25, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.altitude 179.9 -> 180
{'camera.azimuth': 180, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.25, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
Stopping server
Started server on port 31735
{'camera.azimuth': 180, 'camera.altitude': 180, 'camera.target': [0, -100, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.altitude 179.9 -> 180
{'camera.azimuth': 180, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
{'camera.azimuth': 0, 'camera.altitude': 90, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 0.5, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 10, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 0
waiting for camera.altitude {} -> 90
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 0.5
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 10
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
{'camera.azimuth': 180, 'camera.altitude': 0, 'camera.target': [0, 0, 0], 'surface.{subject}.unfold': 1, 'surface.{subject}.pivot': 180, 'surface.{subject}.shift': 0, 'surface.{subject}.specularity': 0, 'surface.{subject}.sampler': 'nearest', 'surface.{subject}.layers': 1}
waiting for camera.azimuth {} -> 180
waiting for camera.altitude {} -> 0
waiting for camera.target {} -> [0, 0, 0]
waiting for surface.S1.unfold {} -> 1
waiting for surface.S1.pivot {} -> 180
waiting for surface.S1.shift {} -> 0
waiting for surface.S1.specularity {} -> 0
waiting for surface.S1.sampler {} -> nearest
waiting for surface.S1.layers {} -> 1
Stopping server
Total running time of the script: (6 minutes 33.686 seconds)








