Skip to content

electrode_overlay

tit.tools.electrode_overlay

Create NIfTI electrode placement overlays from simulation configs.

create_electrode_overlay_nifti

create_electrode_overlay_nifti(config_path: str | Path, reference_nifti: str | Path, output_path: str | Path, montage_name: str | None = None, eeg_positions_dir: str | Path | None = None) -> str

Write a label-mask NIfTI showing XYZ electrode placements.

The saved simulation config.json provides electrode centers and dimensions. The reference NIfTI provides the output grid and affine, so the overlay can be opened alongside that image in Freeview or another NIfTI viewer. Because SimNIBS configs do not store electrode normals, the pad volume is an axis-aligned approximation around each center.

Source code in tit/tools/electrode_overlay.py
def create_electrode_overlay_nifti(
    config_path: str | Path,
    reference_nifti: str | Path,
    output_path: str | Path,
    montage_name: str | None = None,
    eeg_positions_dir: str | Path | None = None,
) -> str:
    """Write a label-mask NIfTI showing XYZ electrode placements.

    The saved simulation ``config.json`` provides electrode centers and
    dimensions. The reference NIfTI provides the output grid and affine, so
    the overlay can be opened alongside that image in Freeview or another
    NIfTI viewer. Because SimNIBS configs do not store electrode normals, the
    pad volume is an axis-aligned approximation around each center.
    """
    import nibabel as nib

    config_path = Path(config_path)
    reference_nifti = Path(reference_nifti)
    output_path = Path(output_path)

    with config_path.open() as f:
        config_data = json.load(f)

    position_pairs = _extract_position_pairs(
        config_data, montage_name=montage_name, eeg_positions_dir=eeg_positions_dir
    )
    shape, radii_mm = _electrode_geometry(config_data)

    reference_img = nib.load(str(reference_nifti))
    mask = np.zeros(reference_img.shape[:3], dtype=np.uint16)
    affine = np.asarray(reference_img.affine, dtype=float)

    for pair_idx, pair_positions in enumerate(position_pairs, start=1):
        for center_xyz in pair_positions:
            _draw_electrode(mask, affine, center_xyz, radii_mm, pair_idx, shape)

    output_path.parent.mkdir(parents=True, exist_ok=True)
    header = reference_img.header.copy()
    if hasattr(header, "set_data_dtype"):
        header.set_data_dtype(np.uint16)
    overlay_img = nib.Nifti1Image(mask, affine, header)
    nib.save(overlay_img, str(output_path))
    write_electrode_overlay_lut(
        electrode_overlay_lut_path(output_path), len(position_pairs)
    )
    return str(output_path)

simulation_config_has_xyz_electrodes

simulation_config_has_xyz_electrodes(config_path: str | Path, montage_name: str | None = None, eeg_positions_dir: str | Path | None = None) -> bool

Return whether config_path contains resolvable electrode coordinates.

Source code in tit/tools/electrode_overlay.py
def simulation_config_has_xyz_electrodes(
    config_path: str | Path,
    montage_name: str | None = None,
    eeg_positions_dir: str | Path | None = None,
) -> bool:
    """Return whether *config_path* contains resolvable electrode coordinates."""
    try:
        with Path(config_path).open() as f:
            config_data = json.load(f)
        _extract_xyz_positions(
            config_data,
            montage_name=montage_name,
            eeg_positions_dir=eeg_positions_dir,
        )
    except (OSError, ValueError, TypeError, json.JSONDecodeError):
        return False
    return True