Skip to content

About

Map MNI, Talairach or native-space brain coordinates to their nearest scalp entry point

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Latest commit

 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

brain2scalp logo

Map a brain target to its nearest scalp entry point

Python 3.10+ License: MIT


Table of Contents


Features

  • Exact nearest scalp point - the head mask is stripped of thin unattached sheets (caps, hair), closed with a rolling-ball morphological closing, hole-filled, and converted to a triangle mesh (marching cubes). The scalp entry is the mesh vertex with the minimum Euclidean distance to the brain target (KD-tree query), precise to about half a voxel.
  • MNI and Talairach input - Talairach converted to MNI152 with the Lancaster et al. (2007) affine (spm, fsl or other variant).
  • Multiple targets - batch coordinate list in one pass.
  • Multi-format output - plain text table, JSON, or CSV.
  • Optional visualization - 6-panel pipeline-steps view, 3-panel orthogonal slices, and interactive Plotly 3D viewer.

Installation

1. Install Python

Requires Python >= 3.10.

Check if you already have it:

python --version
# or
python3 --version

If not Python 3.10.x or higher, download from https://www.python.org/downloads/


2. Create and activate environment (recommended)

venv

python -m venv env_name

# Activate it
# Windows:
env_name\Scripts\activate
# macOS / Linux:
source env_name/bin/activate

conda

conda create -n env_name python=3.10

# Activate it
conda activate env_name

3. Install brain2scalp

From PyPI (recommended):

# Core + CLI (no visualization)
pip install brain2scalp

# With visualization (matplotlib + plotly)
pip install "brain2scalp[viz]"

# Development (adds pytest, ruff, mypy)
pip install "brain2scalp[dev]"

From GitHub:

You need Git installed (git-scm.com). Then:

git clone https://github.com/LeonardoPoliti/brain2scalp.git
cd brain2scalp

# Core + CLI
pip install .

# With visualization
pip install ".[viz]"

Verify the install:

brain2scalp --help

You should see the CLI help text.


Requirements

Dependency Minimum Notes
Python 3.10
nibabel 5.0 NIfTI I/O
numpy 1.24
scipy 1.10 KD-tree, morphology
scikit-image 0.21 Otsu threshold, marching cubes
matplotlib 3.7 [viz] only
plotly 5.18 [viz] only

All dependencies are installed automatically by pip.


Quick Start

Command line examples

# Single MNI target
brain2scalp --nii mni_icbm152_t1_tal_nlin_sym_09c.nii --target -46 20 32

# Talairach target
brain2scalp --nii mni_icbm152_t1_tal_nlin_sym_09c.nii --target -46 20 32 --space talairach

# Multiple targets on one line
brain2scalp --nii head.nii --target "-46 20 32, 10 -20 45"

# Multiple targets from a CSV file
brain2scalp --nii head.nii --target-file targets.csv

# JSON output to file (format inferred from extension)
brain2scalp --nii head.nii --target -46 20 32 --output result.json

# CSV output to file
brain2scalp --nii head.nii --target -46 20 32 --output result.csv

# 3-panel orthogonal slice view of the target
brain2scalp --nii head.nii --target -46 20 32 --vizTarget

# 6-panel pipeline steps figure
brain2scalp --nii head.nii --target -46 20 32 --vizSteps

# Interactive 3D viewer
brain2scalp --nii head.nii --target -46 20 32 --viz3d

# Save all figures without displaying
brain2scalp --nii head.nii --target -46 20 32 --vizTarget --vizSteps --save-fig ./figs --no-plot

Python API examples

from brain2scalp.core import run

# Single target (coordinates only)
result = run(
    nii_path="mni_icbm152_t1_tal_nlin_sym_09c.nii",
    targets=[-46.0, 20.0, 32.0],
)

print(result.scalp_entry_mni)    # [x, y, z] in MNI mm
print(result.distance_mm)        # brain-to-scalp distance (mm)
print(result.to_dict())          # JSON-serializable dict

# Single target with volumetric arrays (for visualization)
state = run(
    nii_path="head.nii",
    targets=[-46.0, 20.0, 32.0],
    full=True,
    closing_radius_mm=8.0,
    verbose=True,
)

# Multiple targets
results = run(
    nii_path="head.nii",
    targets=[[-46.0, 20.0, 32.0], [10.0, -20.0, 45.0]],
)

# Multiple targets with volumetric arrays
states = run(
    nii_path="head.nii",
    targets=[[-46.0, 20.0, 32.0], [10.0, -20.0, 45.0]],
    full=True,
    is_talairach=False,
    closing_radius_mm=8.0,
)

Atlas vs Personal Scans

Using an MNI Atlas

An MNI152 full-head atlas gives a reasonable approximation of scalp topology and is sufficient for group-level studies or when no individual scan is available.

Example atlas: mni_icbm152_t1_tal_nlin_sym_09c.nii
Download from: https://nist.mni.mcgill.ca/icbm-152-nonlinear-atlases-2009/

When to use:

  • You have MNI or Talairach coordinates from a literature study or group activation map.
  • No individual scan is available.
  • You need a quick estimate of scalp position for target planning.

Limitations:

  • Results reflect average anatomy, not the individual's head shape.
  • Accuracy degrades for targets far from group-mean anatomy (e.g. atypical skull shape, lesions).

Using a Personal Head Scan

Scan requirements:

  • Must be a full-head scan
  • .nii or .nii.gz format with valid qform/sform header.
  • 4D volumes are accepted; only the first frame is used.

What to avoid:

  • Brain-extracted images (FSL BET output, FreeSurfer brain.mgz) - detected and rejected.
  • Label/parcellation atlases - also detected and rejected.
  • Files with missing or corrupt affine - rejected at load time.

Option A - Native space

Use this when your target coordinates come from the same scan.

Option B - Register to MNI first

Use this when your target is in MNI space but you want subject-specific scalp topology. Register the full-head scan to the MNI152 template with a linear (12-DOF) registration, then pass the registered scan with --space mni.

Note: Use a full-head MNI template as the registration target to preserve scalp geometry in the output.


Coordinate Space Summary

--space Input type Conversion applied
mni (default) MNI152 mm coordinates None
talairach Talairach mm coordinates Lancaster 2007 affine → MNI (`--tal-method spm
native mm in the scan's own space None (use with unregistered personal scans)

CLI Reference

brain2scalp --nii PATH --target COORDS [options]

Input

Flag Default Description
--nii PATH required Full-head .nii or .nii.gz file
--target COORDS required* Target coordinates in mm. Single: '-46 20 32'. Multiple (comma-separated): '-46 20 32, 10 -20 45'
--target-file PATH - CSV file with one target per row (x,y,z). Optional header auto-detected. Combined with --target; duplicates silently dropped.
--space {mni,talairach,native} mni Coordinate space of the input target
--tal-method {spm,fsl,other} spm Lancaster 2007 variant used with --space talairach. Pick the one matching how your MNI space was produced

* Either --target or --target-file is required.

Processing

Flag Default Description
--threshold FLOAT auto (Otsu) Override head-mask intensity threshold
--closing-radius MM 8 Rolling-ball closing radius for the head mask. Seals concavities narrower than ~2× this value (ear canals, nostrils, patchy scalp). See Limitations

Output

Flag Default Description
--output PATH stdout (text) Save result to file. Format is inferred from extension: .txt, .json, or .csv
--quiet off Suppress progress messages

Visualization (requires [viz])

Flag Default Description
--vizSteps [IDX ...] off 6-panel pipeline steps figure. Optionally pass target indices to limit which are shown, e.g. --vizSteps 0 2
--step-axis {0,1,2} auto Slice axis for --vizSteps: 0=sagittal, 1=coronal, 2=axial
--vizTarget [IDX ...] off 3-panel orthogonal slice view centered on target + entry
--viz3d [IDX ...] off Interactive Plotly 3D viewer
--volume-stride N 3 Subsample every Nth voxel for the 3D volume render. Lower = denser but slower
--save-fig DIR display Save all figures to this directory (created if missing)
--no-plot off Suppress display. Combine with --save-fig to save without showing

Python API

from brain2scalp.core import run

run()

Parameter Type Default Description
nii_path str | Path required Full-head .nii or .nii.gz
targets list[float] or list[list[float]] required Single [x, y, z] or list of triplets in mm
full bool False If True, return PipelineState(s) with volumetric arrays
threshold float | None None (auto) Override head-mask threshold
is_talairach bool False Convert input from Talairach to MNI
is_native bool False Treat target as native scan mm space
closing_radius_mm float 8.0 Rolling-ball closing radius for the head mask
tal_method str "spm" Lancaster 2007 variant ("spm", "fsl", "other") when is_talairach=True
verbose bool False Print progress to stdout

Return type mirrors input shape:

targets full Return type
[x, y, z] False ScalpResult
[x, y, z] True PipelineState
[[x,y,z], ...] False list[ScalpResult]
[[x,y,z], ...] True list[PipelineState]
# Single target → ScalpResult
result = run(
    nii_path="mni_icbm152_t1_tal_nlin_sym_09c.nii",
    targets=[-46.0, 20.0, 32.0],
    verbose=True,
)

# Multiple targets → list[ScalpResult] 
results = run(
    nii_path="head.nii",
    targets=[[-46.0, 20.0, 32.0], [10.0, -20.0, 45.0]],
)

# With volumetric arrays for visualization
state = run("head.nii", [-46.0, 20.0, 32.0], full=True)
states = run("head.nii", [[-46.0, 20.0, 32.0], [10.0, -20.0, 45.0]], full=True)

ScalpResult attributes

Attribute Type Description
brain_target_mni list[float] Input target in MNI space
scalp_entry_mni list[float] Scalp entry point in MNI space
distance_mm float Euclidean distance brain→scalp
coordinate_space str 'mni', 'talairach', or 'native'
is_talairach bool Whether input was Talairach
tal_target list[float] | None Original Talairach coords (if applicable)
tal_method str | None Lancaster variant used (if applicable)
nii_path str Path to the NIfTI file used
atlas_shape tuple NIfTI volume shape
voxel_size_mm tuple Voxel dimensions in mm
threshold_used float Intensity threshold applied
closing_radius_mm float Closing radius used
to_dict() dict JSON-serializable representation

PipelineState - additional attributes (full=True)

Attribute Description
data Raw NIfTI volume (float64)
affine NIfTI affine matrix (4×4)
mask Binary head mask (uint8)
filled Closed and hole-filled mask (uint8)
scalp_surface_mni (N, 3) float64 — scalp mesh vertices in MNI mm

Output Formats

Output format is determined by the file extension passed to --output. Without --output, results are printed as text to stdout.

Text (.txt)

When Talairach input is used, tal_x/y/z columns are added automatically.

  target  mni_x   mni_y   mni_z   entry_x  entry_y  entry_z  dist_mm
       0  -46.0    20.0    32.0    -62.3     35.7     47.5     32.5
       1   10.0   -20.0    45.0     14.8    -28.1     58.3     19.2

JSON (.json)

A single result is returned as an object; multiple results as an array. When Talairach input is used, a tal_original field is added.

{
  "target": 0,
  "mni":  { "x": -46.0, "y": 20.0,  "z": 32.0 },
  "entry": { "x": -62.3, "y": 35.7,  "z": 47.5 },
  "distance_mm": 32.5
}

CSV (.csv)

Same columns as text. tal_x/y/z columns included when Talairach input was used.

target,mni_x,mni_y,mni_z,entry_x,entry_y,entry_z,dist_mm
0,-46.0,20.0,32.0,-62.3,35.7,47.5,32.5

Target file format (--target-file)

Plain CSV, one target per row. Header row is auto-detected and skipped.

x,y,z
-46,20,32
10,-20,45

Visualization

Requires pip install "brain2scalp[viz]".

Pipeline steps (--vizSteps)

6-panel figure showing each stage of the head-mask pipeline: raw data → binary mask → closed + filled mask → sealed regions → scalp surface contour → overlay with target and entry point.

Pipeline steps


Orthogonal slice view (--vizTarget)

3-panel sagittal / coronal / axial slices centered on the brain target, with the projected scalp entry point overlaid.

Orthogonal slice view


Interactive 3D viewer (--viz3d)

Plotly HTML viewer with isosurface rendering of the head, a toggleable scalp surface mesh, and brain target / scalp entry markers. Includes threshold and opacity sliders. Supports up to 8 targets. Stride (settable via --volume-stride) controls voxel subsampling for the isosurface: lower values give finer geometry but increase file size and browser render time; higher values are faster and lighter.

Interactive 3D viewer


Package Layout

brain2scalp/
├── brain2scalp/
│   ├── core/
│   │   ├── models.py        ScalpResult, PipelineState
│   │   ├── transform.py     Talairach ↔ MNI (Lancaster 2007)
│   │   ├── atlas.py         Load, reorient, validate NIfTI
│   │   ├── mask.py          Threshold → strip thin sheets → closing → fill
│   │   ├── projection.py    Marching-cubes surface + nearest vertex
│   │   └── pipeline.py      run() 
│   ├── cli/
│   │   ├── main.py          argparse entry point
│   │   └── formatters.py    text / JSON / CSV output
│   └── viz/
│       ├── slice2d_view.py  Pipeline steps + orthogonal slice figures
│       └── view_3d.py       Plotly interactive 3D viewer
├── tests/
│   ├── test_atlas.py
│   ├── test_transform.py
│   ├── test_mask.py
│   ├── test_projection.py
│   ├── test_pipeline.py
│   ├── test_cli_helpers.py
│   └── test_formatters.py
└── assets/
    └── b2s_logo.png

Limitations

  • Caps, EEG nets, hair. Thin sheets of signal that are not attached to the head (a cap a few mm above the skin, hair) are stripped from the mask automatically (1 mm morphological opening; thin voxels survive only within 2 mm of thick tissue). Where a cap touches the skin, or the air gap under it is ≤ 2 voxels, cap and skin are inseparable at scan resolution and the entry may land on the cap (up to ~5 mm too far out). Lowering --closing-radius does not help — the gap is narrower than any radius that keeps ear canals and nostrils sealed; below ~6 mm the tool warns when an entry lands on an internal cavity wall. For convexity targets on a scan with a cap, a higher --threshold (cap material is darker than scalp fat) removes it, at the cost of darker inferior tissue.
  • Heads touching the field of view. Volume faces the head touches (typically the neck cut) are left open in the surface mesh, so an entry is never placed on the cut plane; a target very close to the cut snaps to the skin at its rim.
  • Precision. Entry points are mesh vertices, spaced about one voxel apart: the true nearest skin point lies within ~0.5 voxel of the reported one.
  • Native scans. The full-head check that requires signal below z = −50 mm is MNI-specific and is skipped with --space native; the lateral-border check still runs.

Warning

Research use only. This tool has not been validated for clinical use and must not be used to guide patient treatment, clinical diagnosis or medical decision-making. For any clinical application, always rely on a qualified clinician and a certified neuronavigation system.


Citation

If you use brain2scalp in your research, please cite:

APA: Politi, L. (2026). brain2scalp (v0.3.0). Zenodo. https://doi.org/10.5281/zenodo.20708694

@software{politi_brain2scalp_2026,
  author    = {Politi, Leonardo},
  title     = {brain2scalp},
  year      = {2026},
  publisher = {Zenodo},
  doi       = {10.5281/zenodo.20708694},
  url       = {https://doi.org/10.5281/zenodo.20708694}
}

License

MIT - see LICENSE.

Copyright 2026 Leonardo Politi.

About

Map MNI, Talairach or native-space brain coordinates to their nearest scalp entry point

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages