- Features
- Installation
- Quick Start
- Atlas vs Personal Scans
- CLI Reference
- Python API
- Output Formats
- Visualization
- Package Layout
- Citation
- License
- 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,fslorothervariant). - 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.
Requires Python >= 3.10.
Check if you already have it:
python --version
# or
python3 --versionIf not Python 3.10.x or higher, download from https://www.python.org/downloads/
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_nameFrom 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 --helpYou should see the CLI help text.
| 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.
# 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-plotfrom 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,
)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).
Scan requirements:
- Must be a full-head scan
.niior.nii.gzformat 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.
--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) |
brain2scalp --nii PATH --target COORDS [options]
| 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.
| 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 |
| 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 |
| 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 |
from brain2scalp.core import 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)| 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 |
| 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 format is determined by the file extension passed to --output. Without --output, results are printed as text to stdout.
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
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
}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.5Plain CSV, one target per row. Header row is auto-detected and skipped.
x,y,z
-46,20,32
10,-20,45Requires
pip install "brain2scalp[viz]".
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.
3-panel sagittal / coronal / axial slices centered on the brain target, with the projected scalp entry point overlaid.
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.
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
- 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-radiusdoes 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.
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}
}MIT - see LICENSE.
Copyright 2026 Leonardo Politi.



