"""@package docstring
Iso2Mesh for Python - Mesh data queries and manipulations

Copyright (c) 2024-2025 Qianqian Fang <q.fang at neu.edu>
"""
__all__ = [
    "brain2mesh",
    "brain1020",
    "intriangulation",
    "label2tpm",
    "tpm2label",
]

##====================================================================================
## dependent libraries
##====================================================================================

import numpy as np
from iso2mesh.core import v2s, s2m
from iso2mesh.modify import (
    removeisolatednode,
    removedupelem,
    surfboolean,
    meshcheckrepair,
    sms,
    mergemesh,
    slicesurf3,
    slicesurf,
)
from iso2mesh.trait import (
    meshcentroid,
    elemvolume,
    volface,
    layersurf,
    faceneighbors,
    ray2surf,
)
from iso2mesh.geometry import meshabox
from iso2mesh.volume import volgrow, fillholes3d
from iso2mesh.line import (
    polylineinterp,
    polylinelen,
    polylinesimplify,
    closestnode,
    maxloop,
)
from iso2mesh.plot import plotmesh

from typing import Tuple, Dict, Union, Optional, List
import warnings
from collections import defaultdict

##====================================================================================
## implementations
##====================================================================================


def brain2mesh(seg, **cfg):
    """
    Brain2mesh: a one-liner for human brain 3D mesh generation

    Author: Qianqian Fang <q.fang at neu.edu>
    Other contributors: see AUTHORS.txt for details
    Version: 0.8 (Python port)
    URL: http://mcx.space/brain2mesh
    License: GPL version 3
    Reference:
      Anh Phong Tran, Shijie Yan and Qianqian Fang, "Improving model-based
      fNIRS analysis using mesh-based anatomical and light-transport models,"
      Neurophotonics, 7(1), 015008, URL: http://dx.doi.org/10.1117/1.NPh.7.1.015008

    Parameters:
    -----------
    seg : dict or ndarray
        Pre-segmented brain volume (supporting both probalistic tissue
        segmentation and labeled volume). Two formats are accepted:
        1. a dict with subfields (wm,gm,csf,skull,scalp)
           e.g.: seg['wm'], seg['gm'], seg['csf'] represents the white-matter,
           gray-matter and csf segmentations, respectively,
               or
        2. a 4D array for with tissues sorted in outer-to-inner order
           the 4th dimension of the array can 3-6, with the following assumptions
           size(seg,3) == 6 assumes 0-Scalp, 1-Skull, 2-CSF, 3-GM, 4-WM, 5-air pockets
           size(seg,3) == 5 assumes 0-Scalp, 1-Skull, 2-CSF, 3-GM, 4-WM
           size(seg,3) == 4 assumes 0-Scalp, 1-CSF, 2-GM, 3-WM
           size(seg,3) == 3 assumes 0-CSF, 1-GM, 2-WM

    cfg : dict, optional
        Configuration options for the resulting tetrahedral mesh
        cfg['radbound'] : dict with keys {wm,gm,csf,skull,scalp}
           Radius of the Delaunay sphere used in sampling the surfaces.
           Default values are 1.7, 1.7, 2, 2.5 and 3, respectively (reference values for 1x1x1mm^3)
           Scale proportionally for denser volumes. Lower values correspond to denser, higher
           fidelity surface extraction, but also results in denser meshes.
        cfg['maxnode'] : int, default=100000
           Maximum number of nodes extracted for a given surface.
        cfg['maxvol'] : float, default=100
           Maximum volumetric size of elements
        cfg['smooth'] : int, default=0
           Number of iterations to smooth each tissue surface
        cfg['ratio'] : float, default=1.414
           Radius-edge ratio. Lower values increase quality but results in denser meshes
        cfg['dorelabel'] : bool, default=False
           Removes layered meshing assumptions. Currently only works if all five tissue types are present.
        cfg['doairseg'] : bool, default=True
           Within skull layer, additional segmentations can be found.
        cfg['dotruncate'] : str or bool, default='-z' or True
           Truncate mesh in specified direction or disable with 0/False.
        cfg['marginsize'] : int, default=4
           Number of voxels below CSF mesh to truncate when dotruncate is set.
        cfg['imfill'] : str, default='fillholes3d'
           Function name for 3D image hole-filling function

    Returns:
    --------
    brain_n : ndarray
        Node coordinates of the tetrahedral mesh
    brain_el : ndarray
        Element list of the tetrahedral mesh / the last column denotes the boundary ID
    brain_f : ndarray
        Mesh surface element list of the tetrahedral mesh

    Tissue ID for the outputs are as follow:
    0-Air/background, 1-Scalp, 2-Skull, 3-CSF, 4-GM, 5-WM, 6-air pockets

    Reference:
    If you use Brain2Mesh in your publication, the authors of this toolbox
    appreciate if you can cite our Neurophotonics paper listed above.
    """

    # Handling the inputs
    if seg is None:
        print(__doc__)
        return

    # Default density and adaptiveness parameters
    density = defaultdict(
        lambda: 20, {"wm": 2, "gm": 2, "csf": 5, "skull": 4, "scalp": 8}
    )
    adaptiveness = defaultdict(lambda: 1)

    # Parse configuration
    radbound = cfg.get("radbound", density)
    distbound = cfg.get("distbound", adaptiveness)
    qratio = cfg.get("ratio", 1.414)
    maxvol = cfg.get("maxvol", 100)
    maxnode = cfg.get("maxnode", 100000)
    dotruncate = cfg.get("dotruncate", 1)
    dorelabel = cfg.get("dorelabel", 0)
    doairseg = cfg.get("doairseg", 1)
    threshold = cfg.get("threshold", 0.5)
    smooth = cfg.get("smooth", 0)
    surfonly = cfg.get("surfonly", 0)
    marginsize = cfg.get("marginsize", 4)

    segname = list(radbound.keys())

    # Convert seg to dict format if needed
    if isinstance(seg, dict):
        tpm = seg
    elif isinstance(seg, np.ndarray) and seg.ndim == 4:
        tpm = {}
        for i in range(seg.shape[3]):
            if i < len(segname):
                tpm[segname[i]] = seg[:, :, :, i]
    else:
        raise ValueError("This seg input is currently not supported")

    # Normalizing segmentation inputs to 0-1
    def normalizer(x):
        return np.array(x, dtype=np.float64) / float(np.max(x))

    for key in tpm:
        tpm[key] = normalizer(tpm[key])

    opt = []
    for i in range(len(tpm)):
        opt_i = {"maxnode": maxnode}
        if segname[i] in radbound:
            opt_i["radbound"] = radbound[segname[i]]
        opt_i["distbound"] = distbound[segname[i]]
        opt.append(opt_i)

    cube3 = np.ones((3, 3, 3), dtype=bool)

    # Pre-processing steps to create separations between the tissues in the volume space
    dim = tpm["wm"].shape
    tpm["wm"] = fillholes3d(tpm["wm"] > 0)
    p_wm = tpm["wm"].copy()
    p_pial = p_wm + tpm["gm"]
    p_pial = np.maximum(p_pial, volgrow(p_wm, 1, cube3))
    p_pial = fillholes3d(p_pial > 0)
    expandedGM = p_pial - tpm["wm"] - tpm["gm"]
    expandedGM = volgrow(expandedGM, 1, cube3)

    if "csf" in tpm:
        p_csf = p_pial + tpm["csf"]
        p_csf[p_csf > 1] = 1
        p_csf = np.maximum(p_csf, volgrow(p_pial, 1, cube3))
        expandedCSF = p_csf - tpm["wm"] - tpm["gm"] - tpm["csf"] - expandedGM
        expandedCSF = volgrow(expandedCSF, 1, cube3)

    if "skull" in tpm and "scalp" in tpm and "csf" in tpm:
        p_bone = p_csf + tpm["skull"]
        p_bone[p_bone > 1] = 1
        p_bone = np.maximum(p_bone, volgrow(p_csf, 1, cube3))
        p_skin = p_bone + tpm["scalp"]
        p_skin[p_skin > 1] = 1
        p_skin = np.maximum(p_skin, volgrow(p_bone, 1, cube3))
        expandedSkull = (
            p_bone
            - tpm["wm"]
            - tpm["gm"]
            - tpm["csf"]
            - tpm["skull"]
            - expandedCSF
            - expandedGM
        )
        expandedSkull = volgrow(expandedSkull, 1, cube3)
    elif "scalp" in tpm and "skull" not in tpm:
        p_skin = p_csf + tpm["scalp"]
        p_skin[p_skin > 1] = 1
        p_skin = np.maximum(p_skin, volgrow(p_csf, 1, cube3))
    elif "skull" in tpm and "scalp" not in tpm:
        p_bone = p_csf + tpm["skull"]
        p_bone[p_bone > 1] = 1
        p_bone = np.maximum(p_bone, volgrow(p_csf, 1, cube3))

    # Grayscale/Binary extractions of the surface meshes for the different tissues
    thresh = 0.5
    if not isinstance(threshold, dict):
        thresh = threshold

    wm_thresh = threshold.get("wm", thresh) if isinstance(threshold, dict) else thresh
    gm_thresh = threshold.get("gm", thresh) if isinstance(threshold, dict) else thresh

    wm_n, wm_f, _, _ = v2s(p_wm, wm_thresh, opt[0], "cgalsurf")
    pial_n, pial_f, _, _ = v2s(p_pial, gm_thresh, opt[1], "cgalsurf")
    wm_n, wm_f = meshcheckrepair(wm_n, wm_f[:, :3], "isolated")
    pial_n, pial_f = meshcheckrepair(pial_n, pial_f[:, :3], "isolated")

    if "csf" in tpm:
        csf_thresh = (
            threshold.get("csf", thresh) if isinstance(threshold, dict) else thresh
        )
        csf_n, csf_f, _, _ = v2s(p_csf, csf_thresh, opt[2], "cgalsurf")
        csf_n, csf_f = meshcheckrepair(csf_n, csf_f[:, :3], "isolated")

    if "skull" in tpm:
        optskull = {"radbound": radbound["skull"], "maxnode": maxnode}
        skull_thresh = (
            threshold.get("skull", thresh) if isinstance(threshold, dict) else thresh
        )
        bone_n, bone_f, _, _ = v2s(p_bone, skull_thresh, optskull, "cgalsurf")

        bone_node, el_bone, _ = s2m(
            bone_n, bone_f, 1.0, maxvol, "tetgen1.5", None, None, "-A"
        )
        unique_labels = np.unique(el_bone[:, 4])  # 5th column is index 4 (0-based)
        vol_bone = []
        for i, label in enumerate(unique_labels):
            vol_bone.append(
                np.sum(elemvolume(bone_node, el_bone[el_bone[:, 4] == label, :4]))
            )

        maxval = np.max(vol_bone)
        I = np.argmax(vol_bone)
        max_label = unique_labels[I]

        if len(unique_labels) > 1:
            no_air2 = bone_node
            el_air2 = el_bone[el_bone[:, 4] != max_label, :]
            no_air2, el_air2, _ = removeisolatednode(no_air2, el_air2)
            f_air2 = volface(el_air2[:, :4])[0]

        bone_n2 = bone_node
        bone_f2 = volface(el_bone[:, :4])[0]
        bone_f2 = removedupelem(bone_f2)
        bone_n2, bone_f2, _ = removeisolatednode(bone_n2, bone_f2)
        if doairseg == 0:
            bone_n = bone_n2
            bone_f = bone_f2

    if "scalp" in tpm:
        optscalp = {"radbound": radbound["scalp"], "maxnode": maxnode}
        scalp_thresh = (
            threshold.get("scalp", thresh) if isinstance(threshold, dict) else thresh
        )
        skin_n, skin_f, _, _ = v2s(p_skin, scalp_thresh, optscalp, "cgalsurf")

    # Smoothing step
    if isinstance(smooth, dict) or smooth > 0:
        scount = 0
        if not isinstance(smooth, dict):
            scount = smooth

        wm_smooth = smooth.get("wm", scount) if isinstance(smooth, dict) else scount
        if wm_smooth > 0:
            wm_n = sms(wm_n, wm_f[:, :3], wm_smooth, 0.5, "lowpass")
            wm_n, wm_f = meshcheckrepair(wm_n, wm_f[:, :3], "meshfix")

        gm_smooth = smooth.get("gm", scount) if isinstance(smooth, dict) else scount
        if gm_smooth > 0:
            pial_n = sms(pial_n, pial_f[:, :3], gm_smooth, 0.5, "lowpass")
            pial_n, pial_f = meshcheckrepair(pial_n, pial_f[:, :3], "meshfix")

        if "csf" in tpm:
            csf_smooth = (
                smooth.get("csf", scount) if isinstance(smooth, dict) else scount
            )
            if csf_smooth > 0:
                csf_n = sms(csf_n, csf_f[:, :3], csf_smooth, 0.5, "lowpass")
                csf_n, csf_f = meshcheckrepair(csf_n, csf_f[:, :3], "meshfix")

        if "skull" in tpm:
            skull_smooth = (
                smooth.get("skull", scount) if isinstance(smooth, dict) else scount
            )
            if skull_smooth > 0:
                bone_n = sms(bone_n, bone_f[:, :3], skull_smooth, 0.5, "lowpass")
                bone_n, bone_f = meshcheckrepair(bone_n, bone_f[:, :3], "meshfix")

        if "scalp" in tpm:
            scalp_smooth = (
                smooth.get("scalp", scount) if isinstance(smooth, dict) else scount
            )
            if scalp_smooth > 0:
                skin_n = sms(skin_n, skin_f[:, :3], scalp_smooth, 0.5, "lowpass")
                skin_n, skin_f = meshcheckrepair(skin_n, skin_f[:, :3], "meshfix")

    # Surface only mode
    if surfonly == 1:
        wm_f = np.column_stack([wm_f, np.ones(wm_f.shape[0])])
        pial_f = np.column_stack([pial_f, np.full(pial_f.shape[0], 2)])
        brain_n, brain_el = mergemesh(wm_n, wm_f, pial_n, pial_f)

        if "csf" in tpm:
            csf_f = np.column_stack([csf_f, np.full(csf_f.shape[0], 3)])
            brain_n, brain_el = mergemesh(brain_n, brain_el, csf_n, csf_f)
            if "skull" in tpm:
                bone_f = np.column_stack([bone_f, np.full(bone_f.shape[0], 4)])
                brain_n, brain_el = mergemesh(brain_n, brain_el, bone_n, bone_f)
                if "scalp" in tpm:
                    skin_f = np.column_stack([skin_f, np.full(skin_f.shape[0], 5)])
                    brain_n, brain_el = mergemesh(brain_n, brain_el, skin_n, skin_f)

        labels, ia, ib = np.unique(
            brain_el[:, 3], return_index=True, return_inverse=True
        )
        labels = np.arange(5, 5 - len(labels), -1)
        brain_el[:, 3] = labels[ib]
        brain_f = np.array([])
        return brain_n, brain_el, brain_f

    # Main loop for the meshing pipeline
    for loop in range(2):
        # If the first pass fails, a second pass is called using the decoupled function
        # to eliminate intersections between surface meshes
        if (loop == 1) and ("label_elem" in locals()):
            continue
        if (loop == 1) and ("label_elem" not in locals()):
            if "bone_n" in locals() and "skin_n" in locals():
                print("decoupling bone and skin surfaces")
                bone_n, bone_f = surfboolean(
                    bone_n[:, :3],
                    bone_f[:, :3],
                    "decouple",
                    skin_n[:, :3],
                    skin_f[:, :3],
                )
            if "bone_n" in locals() and "csf_n" in locals():
                print("decoupling csf and bone surfaces")
                csf_n, csf_f = surfboolean(
                    csf_n[:, :3], csf_f[:, :3], "decouple", bone_n[:, :3], bone_f[:, :3]
                )
            if "pial_n" in locals() and "csf_n" in locals():
                print("decoupling pial and csf surfaces")
                pial_n, pial_f = surfboolean(
                    pial_n[:, :3], pial_f[:, :3], "decouple", csf_n[:, :3], csf_f[:, :3]
                )
            if "pial_n" in locals() and "wm_n" in locals():
                print("decoupling wm and pial surfaces")
                wm_n, wm_f = surfboolean(
                    wm_n[:, :3], wm_f[:, :3], "decouple", pial_n[:, :3], pial_f[:, :3]
                )

        if "wm" in tpm and "gm" in tpm:
            surf_n, surf_f = surfboolean(
                wm_n[:, :3], wm_f[:, :3], "resolve", pial_n, pial_f
            )
        if "csf" in tpm:
            surf_n, surf_f = surfboolean(surf_n, surf_f, "resolve", csf_n, csf_f)
        if "skull" in tpm:
            surf_n, surf_f = surfboolean(surf_n, surf_f, "resolve", bone_n, bone_f)
        if "scalp" in tpm:
            surf_n, surf_f = surfboolean(surf_n, surf_f, "resolve", skin_n, skin_f)

        final_surf_n = surf_n
        final_surf_f = surf_f

        if surfonly == 2:
            brain_n = final_surf_n
            brain_el = final_surf_f
            brain_f = np.array([])
            return brain_n, brain_el, brain_f

        # If the whole head option is deactivated, the cut is made at the base of the brain using a box cutting
        if dotruncate == 1 or isinstance(dotruncate, str):
            dim_max = np.max(surf_n, axis=0)
            if "csf" in tpm:
                dim2 = np.min(csf_n, axis=0)
            else:
                dim2 = np.min(surf_n, axis=0)

            if dotruncate == 1 or dotruncate == "-z":
                nbox, fbox, ebox = meshabox(
                    [-1, -1, dim2[2] + marginsize],
                    [dim_max[0] + 1, dim_max[1] + 1, dim_max[2] + 1],
                    500,
                )
            elif dotruncate == "-y":
                nbox, fbox, ebox = meshabox(
                    [-1, dim2[1] + marginsize, -1],
                    [dim_max[0] + 1, dim_max[1] + 1, dim_max[2] + 1],
                    500,
                )
            elif dotruncate == "-x":
                nbox, fbox, ebox = meshabox(
                    [dim2[0] + marginsize, -1, -1],
                    [dim_max[0] + 1, dim_max[1] + 1, dim_max[2] + 1],
                    500,
                )
            elif dotruncate == "+z":
                nbox, fbox, ebox = meshabox(
                    [-1, -1, -1],
                    [dim_max[0] + 1, dim_max[1] + 1, dim2[2] - marginsize],
                    500,
                )
            elif dotruncate == "+y":
                nbox, fbox, ebox = meshabox(
                    [-1, -1, -1],
                    [dim_max[0] + 1, dim2[1] - marginsize, dim_max[2] + 1],
                    500,
                )
            elif dotruncate == "+x":
                nbox, fbox, ebox = meshabox(
                    [-1, -1, -1],
                    [dim2[0] - marginsize, dim_max[1] + 1, dim_max[2] + 1],
                    500,
                )

            fbox = volface(ebox)[0]
            nbox, fbox, _ = removeisolatednode(nbox, fbox)
            final_surf_n, final_surf_f = surfboolean(
                nbox, fbox, "first", surf_n, surf_f
            )

        if surfonly == 3:
            brain_n = final_surf_n
            brain_el = final_surf_f
            brain_f = np.array([])
            return brain_n, brain_el, brain_f

        # Generates a coarse tetrahedral mesh of the combined tissues
        final_surf_n, final_surf_f = meshcheckrepair(final_surf_n, final_surf_f, "dup")
        try:
            final_n, final_e, _ = s2m(
                final_surf_n,
                final_surf_f,
                1.0,
                maxvol,
                "tetgen1.5",
                None,
                None,
                "-YY -A",
            )
        except RuntimeError as e:
            print(
                f"volumetric mesh generation failed with error: {e}, returning the intermediate surface model only"
            )
            continue
            # brain_n = final_surf_n
            # brain_f = final_surf_f
            # brain_el = np.array([])
            # return brain_n, brain_el, brain_f

        # Removes the elements that are part of the box, but not the brain/head
        if dotruncate == 1 or isinstance(dotruncate, str):
            maxval_idx = np.argmax(final_n, axis=0)
            k = np.where(final_e[:, :4] == maxval_idx[2] + 1)[0]
            if len(k) > 0:
                k = k[0]
                exclude_label = final_e[k % len(final_e), 4]
                final_e = final_e[final_e[:, 4] != exclude_label, :]
                final_n, final_e, _ = removeisolatednode(final_n, final_e)

        # Here the labels created through the coarse mesh generated through Tetgen are saved
        # with the centroid of one of the elements for intriangulation testing later
        label, label_elem = np.unique(final_e[:, 4], return_index=True)
        label_centroid = meshcentroid(final_n, final_e[label_elem, :4])

        if "scalp" in tpm:
            no_skin, el_skin, _ = s2m(
                skin_n, skin_f, 1.0, maxvol, "tetgen1.5", None, None, "-A"
            )
            unique_skin_labels = np.unique(el_skin[:, 4])
            vol_skin = []
            for i, skin_label in enumerate(unique_skin_labels):
                vol_skin.append(
                    np.sum(
                        elemvolume(no_skin, el_skin[el_skin[:, 4] == skin_label, :4])
                    )
                )

            maxval = np.max(vol_skin)
            I = np.argmax(vol_skin)
            max_skin_label = unique_skin_labels[I]

            if len(unique_skin_labels) > 1:
                no_air = no_skin
                el_air = el_skin[el_skin[:, 4] != max_skin_label, :]
                no_air, el_air, _ = removeisolatednode(no_air, el_air)
                f_air = volface(el_air[:, :4])[0]
                f_air = removedupelem(f_air)

            el_skin = el_skin[el_skin[:, 4] == max_skin_label, :]
            no_skin, el_skin, _ = removeisolatednode(no_skin, el_skin)
            f_skin = volface(el_skin[:, :4])[0]
            f_skin = removedupelem(f_skin)

        # When the label_elem does not exist, it often indicates a failure at the generation of a coarse
        # tetrahedral mesh. The alternative meshing pathway using decoupling is then called to make a
        # second attempt at creating the combined tetrahedral mesh.
        if "label_elem" not in locals() and loop == 0:
            print(
                "Initial meshing procedure failed. The option parameter might need to be adjusted."
            )
            print("Activating alternative meshing pathway...")
            continue

        # The labels are given to each of the tissues
        # WM(1) - GM(2) - CSF(3) - Bone(4) - Scalp(5) - Air(6)
        newlabel = np.zeros(len(label_elem))
        if "bone_n" in locals() and "no_air2" in locals():
            # Need to subtract 1 when using as indices since faces/elements are 1-indexed
            newlabel = intriangulation(no_air2, f_air2[:, :3], label_centroid)
        if "no_skin" in locals() and "no_air" in locals():
            newlabel = newlabel | intriangulation(no_air, f_air[:, :3], label_centroid)

        newlabel = newlabel.astype(float)
        idx = np.where(newlabel == 0)[0]

        newtag = np.zeros(len(idx))
        newtag = intriangulation(wm_n, wm_f[:, :3], label_centroid[idx, :]) * 6
        newtag = np.maximum(
            newtag,
            intriangulation(pial_n, pial_f[:, :3], label_centroid[idx, :]) * 5,
        )
        if "csf_n" in locals():
            newtag = np.maximum(
                newtag,
                intriangulation(csf_n, csf_f[:, :3], label_centroid[idx, :]) * 4,
            )
        if "bone_n2" in locals():
            newtag = np.maximum(
                newtag,
                intriangulation(bone_n2, bone_f2[:, :3], label_centroid[idx, :]) * 3,
            )
        if "no_skin" in locals():
            newtag = np.maximum(
                newtag,
                intriangulation(no_skin, f_skin[:, :3], label_centroid[idx, :]) * 2,
            )

        newlabel[idx] = newtag
        newlabel = 7 - newlabel
        final_e[:, 4] = newlabel[final_e[:, 4] - 1]  # Subtract 1 for 0-based indexing

        # This step consolidates adjacent labels of the same tissue
        new_label = np.unique(final_e[:, 4])
        face = np.empty((0, 3), dtype=int)
        for i in new_label:
            face = np.vstack([face, volface(final_e[final_e[:, 4] == i, :4])[0]])
        face = np.sort(face, axis=1)
        face = np.unique(face, axis=0)
        node, face, _ = removeisolatednode(final_n, face)

        # The final mesh is generated here with the desired properties
        cmdopt = f"-A -pq{qratio}a{maxvol}"
        brain_n, brain_el, _ = s2m(
            node, face, 1.0, maxvol, "tetgen1.5", None, None, cmdopt
        )

        label2, label_brain_el = np.unique(brain_el[:, 4], return_index=True)
        label_centroid2 = meshcentroid(brain_n, brain_el[label_brain_el, :4])

        # The labeling process is repeated for the final mesh
        # WM(1) - GM(2) - CSF(3) - Bone(4) - Scalp(5) - Air(6)
        newlabel = np.zeros(len(label_brain_el))
        if "bone_n" in locals() and "no_air2" in locals():
            newlabel = intriangulation(no_air2, f_air2[:, :3], label_centroid2)
        if "no_skin" in locals() and "no_air" in locals():
            newlabel = newlabel | intriangulation(no_air, f_air[:, :3], label_centroid2)

        newlabel = newlabel.astype(float)
        idx = np.where(newlabel == 0)[0]

        newtag = np.zeros(len(idx))
        newtag = intriangulation(wm_n, wm_f[:, :3], label_centroid2[idx, :]) * 6
        newtag = np.maximum(
            newtag,
            intriangulation(pial_n, pial_f[:, :3], label_centroid2[idx, :]) * 5,
        )
        if "csf_n" in locals():
            newtag = np.maximum(
                newtag,
                intriangulation(csf_n, csf_f[:, :3], label_centroid2[idx, :]) * 4,
            )
        if "bone_n2" in locals():
            newtag = np.maximum(
                newtag,
                intriangulation(bone_n2, bone_f2[:, :3], label_centroid2[idx, :]) * 3,
            )
        if "no_skin" in locals():
            newtag = np.maximum(
                newtag,
                intriangulation(no_skin, f_skin[:, :3], label_centroid2[idx, :]) * 2,
            )

        newlabel[idx] = newtag
        newlabel = 7 - newlabel
        brain_el[:, 4] = newlabel[brain_el[:, 4] - 1]  # Subtract 1 for 0-based indexing

        break  # Exit the loop after successful completion

    # Relabeling step to remove layered assumptions
    if dorelabel == 1 and ("skull" in tpm and "scalp" in tpm):
        centroid = meshcentroid(brain_n[:, :3], brain_el[:, :4])
        centroid = np.ceil(centroid).astype(int)
        tag = np.zeros(len(brain_el))
        facenb = faceneighbors(brain_el[:, :4])

        for i in range(len(brain_el)):
            cx, cy, cz = centroid[i]
            if (expandedGM[cx, cy, cz] > 0.5) and (brain_el[i, 4] == 2):
                if tpm["scalp"][cx, cy, cz] > 0.5:
                    brain_el[i, 4] = 5
                elif tpm["skull"][cx, cy, cz] > 0.5:
                    brain_el[i, 4] = 4
                else:
                    brain_el[i, 4] = 3
                tag[i] = 1
                for j in range(4):
                    if facenb[i, j] > 0:
                        tag[facenb[i, j] - 1] = 1  # Subtract 1 for 0-based indexing
            elif (expandedCSF[cx, cy, cz] > 0.5) and (brain_el[i, 4] == 3):
                if tpm["scalp"][cx, cy, cz] > 0.5:
                    brain_el[i, 4] = 5
                else:
                    brain_el[i, 4] = 4
                tag[i] = 1
                for j in range(4):
                    if facenb[i, j] > 0:
                        tag[facenb[i, j] - 1] = 1  # Subtract 1 for 0-based indexing
            elif (expandedSkull[cx, cy, cz] > 0.5) and (brain_el[i, 4] == 4):
                brain_el[i, 4] = 5
                tag[i] = 1
                for j in range(4):
                    if facenb[i, j] > 0:
                        tag[facenb[i, j] - 1] = 1  # Subtract 1 for 0-based indexing

        labels = np.zeros((len(brain_el), 4))
        labels2 = np.zeros((len(brain_el), 6))
        for i in range(len(brain_el)):
            for j in range(4):
                if facenb[i, j] > 0:
                    neighbor_idx = facenb[i, j] - 1  # Subtract 1 for 0-based indexing
                    labels[i, j] = brain_el[neighbor_idx, 4]
                    labels2[i, int(labels[i, j])] += 1
                else:
                    labels[i, j] = 0

        max_labels = np.argmax(labels2, axis=1)
        max_counts = np.max(labels2, axis=1)

        for i in range(len(brain_el)):
            if tag[i] == 1:
                if (max_counts[i] > 2) and (brain_el[i, 4] != max_labels[i]):
                    brain_el[i, 4] = max_labels[i]

    brain_el[:, 4] = 6 - brain_el[:, 4]

    # Generate brain_f if requested
    brain_f = np.array([])
    if cfg.get("face", False):  # Check if brain_f is requested
        brain_f = layersurf(brain_el)[0]

    return brain_n, brain_el, brain_f


def intriangulation(
    vertices: np.ndarray, faces: np.ndarray, testp: np.ndarray, heavytest: int = 0
) -> np.ndarray:
    """
    Test points in 3d whether inside or outside a (closed) triangulation
    Usage: in = intriangulation(vertices, faces, testp, heavytest)

    Arguments (input):
    vertices   - points in 3d as matrix with three columns
    faces      - description of triangles as matrix with three columns.
                Each row contains three indices into the matrix of vertices
                which gives the three cornerpoints of the triangle.
    testp      - points in 3d as matrix with three columns
    heavytest  - int n >= 0. Perform n additional randomized rotation tests.

    IMPORTANT: the set of vertices and faces has to form a watertight surface!

    Arguments (output):
    in - a vector of length size(testp,0), containing 0 and 1.
         in[nr] =  0: testp[nr,:] is outside the triangulation
         in[nr] =  1: testp[nr,:] is inside the triangulation
         in[nr] = -1: unable to decide for testp[nr,:]

    Thanks to Adam A for providing the FEX submission voxelise. The
    algorithms of voxelise form the algorithmic kernel of intriangulation.

    Thanks to Sven to discussions about speed and avoiding problems in
    special cases.

    Author: Johannes Korsawe, heavily based on voxelise from Adam A.
    E-mail: johannes.korsawe@volkswagen.de
    Python conversion: Preserves exact algorithm from MATLAB version
    Release: 1.3
    Release date: 25/09/2013
    """

    # Check number of inputs
    if vertices is None or faces is None or testp is None:
        print("??? Error using ==> intriangulation\nThree input matrices are needed.\n")
        return np.array([])

    if heavytest is None:
        heavytest = 0

    # Check size of inputs
    if vertices.shape[1] != 3 or faces.shape[1] != 3 or testp.shape[1] != 3:
        print(
            "??? Error using ==> intriangulation\nAll input matrices must have three columns.\n"
        )
        return np.array([])

    # Convert faces from 1-based to 0-based indexing when used as indices
    # Note: we keep faces array unchanged, but subtract 1 when indexing
    ipmax = np.max(faces)
    zerofound = np.any(faces == 0)
    if ipmax > vertices.shape[0] or zerofound:
        print(
            "??? Error using ==> intriangulation\nThe triangulation data is defect. use trisurf(faces,vertices[:,0],vertices[:,1],vertices[:,2]) for test of deficiency.\n"
        )
        return np.array([])

    # Loop for heavytest
    inreturn = np.zeros((testp.shape[0], 1))
    VER = vertices.copy()
    TESTP = testp.copy()

    for n in range(1, heavytest + 2):  # MATLAB: for n = 1:heavytest + 1
        # Randomize
        if n > 1:
            v = np.random.rand(1, 3)
            D = rotmatrix(v / np.linalg.norm(v), np.random.rand() * 180 / np.pi)
            vertices = VER @ D
            testp = TESTP @ D
        else:
            vertices = VER.copy()

        # Preprocessing data
        meshXYZ = np.zeros((faces.shape[0], 3, 3))
        for loop in range(3):
            # Subtract 1 when using faces as indices (convert from 1-based to 0-based)
            meshXYZ[:, :, loop] = vertices[faces[:, loop] - 1, :]

        # Basic idea (ingenious from FeX-submission voxelise):
        # If point is inside, it will cross the triangulation an uneven number of times in each direction (x, -x, y, -y, z, -z).

        # The function VOXELISEinternal is about 98% identical to its version inside voxelise.m.
        # This includes the elaborate comments. Thanks to Adam A!

        # z-direction:
        # initialization of results and correction list
        in_result, cl = VOXELISEinternal(testp[:, 0], testp[:, 1], testp[:, 2], meshXYZ)

        # x-direction:
        # has only to be done for those points, that were not determinable in the first step --> cl
        if len(cl) > 0:
            in2, cl2 = VOXELISEinternal(
                testp[cl, 1], testp[cl, 2], testp[cl, 0], meshXYZ[:, [1, 2, 0], :]
            )
            # Use results of x-direction that determined "inside"
            in_result[cl[in2 == 1]] = 1
            # remaining indices with unclear result
            cl = cl[cl2]

        # y-direction:
        # has only to be done for those points, that were not determinable in the first and second step --> cl
        if len(cl) > 0:
            in3, cl3 = VOXELISEinternal(
                testp[cl, 2], testp[cl, 0], testp[cl, 1], meshXYZ[:, [2, 0, 1], :]
            )

            # Use results of y-direction that determined "inside"
            in_result[cl[in3 == 1]] = 1
            # remaining indices with unclear result
            cl = cl[cl3]

        # Mark those indices, where all three tests have failed
        in_result[cl] = -1

        if n == 1:
            inreturn = in_result.copy()  # Starting guess
        else:
            # if ALWAYS inside, use as inside!
            # I = find(inreturn ~= in);
            # inreturn(I(in(I)==0)) = 0;

            # if AT LEAST ONCE inside, use as inside!
            I = np.where(inreturn.flatten() != in_result.flatten())[0]
            inreturn.flatten()[I[in_result.flatten()[I] == 1]] = 1

    return inreturn.flatten().astype(int)


def VOXELISEinternal(
    testx: np.ndarray, testy: np.ndarray, testz: np.ndarray, meshXYZ: np.ndarray
) -> Tuple[np.ndarray, np.ndarray]:
    """
    Internal voxelization function - exact port from MATLAB voxelise

    This function is about 98% identical to its version inside voxelise.m.
    This includes the elaborate comments. Thanks to Adam A!
    """

    # Prepare logical array to hold the logical data:
    OUTPUT = np.zeros((testx.shape[0], 1))

    # Identify the min and max x,y coordinates of the mesh:
    meshZmin = np.min(meshXYZ[:, 2, :])
    meshZmax = np.max(meshXYZ[:, 2, :])

    # Identify the min and max x,y,z coordinates of each facet:
    meshXYZmin = np.min(meshXYZ, axis=2)
    meshXYZmax = np.max(meshXYZ, axis=2)

    # ======================================================
    # TURN OFF DIVIDE-BY-ZERO WARNINGS
    # ======================================================
    # This prevents the Y1predicted, Y2predicted, Y3predicted and YRpredicted
    # calculations creating divide-by-zero warnings. Suppressing these warnings
    # doesn't affect the code, because only the sign of the result is important.
    # That is, 'Inf' and '-Inf' results are ok.
    # The warning will be returned to its original state at the end of the code.
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", RuntimeWarning)

        # ======================================================
        # START COMPUTATION
        # ======================================================

        correctionLIST = (
            []
        )  # Prepare to record all rays that fail the voxelisation. This array is built on-the-fly, but since
        # it ought to be relatively small should not incur too much of a speed penalty.

        # Loop through each testpoint.
        # The testpoint-array will be tested by passing rays in the z-direction through
        # each x,y coordinate of the testpoints, and finding the locations where the rays cross the mesh.
        facetCROSSLIST = np.zeros(1000, dtype=int)  # uses countindex: nf
        nm = meshXYZmin.shape[0]

        for loop in range(len(OUTPUT)):
            nf = 0
            # - 1a - Find which mesh facets could possibly be crossed by the ray:
            # possibleCROSSLISTy = find( meshXYZmin(:,2)<=testy(loop) & meshXYZmax(:,2)>=testy(loop) );

            # - 1b - Find which mesh facets could possibly be crossed by the ray:
            # possibleCROSSLIST = possibleCROSSLISTy( meshXYZmin(possibleCROSSLISTy,1)<=testx(loop) & meshXYZmax(possibleCROSSLISTy,1)>=testx(loop) );

            # Do - 1a - and - 1b - faster
            possibleCROSSLISTy = np.where(
                (testy[loop] - meshXYZmin[:, 1]) * (meshXYZmax[:, 1] - testy[loop]) > 0
            )[0]
            possibleCROSSLISTx = (testx[loop] - meshXYZmin[possibleCROSSLISTy, 0]) * (
                meshXYZmax[possibleCROSSLISTy, 0] - testx[loop]
            ) > 0
            possibleCROSSLIST = possibleCROSSLISTy[possibleCROSSLISTx]

            if (
                len(possibleCROSSLIST) != 0
            ):  # Only continue the analysis if some nearby facets were actually identified
                # - 2 - For each facet, check if the ray really does cross the facet rather than just passing it close-by:

                # GENERAL METHOD:
                # 1. Take each edge of the facet in turn.
                # 2. Find the position of the opposing vertex to that edge.
                # 3. Find the position of the ray relative to that edge.
                # 4. Check if ray is on the same side of the edge as the opposing vertex.
                # 5. If this is true for all three edges, then the ray definitely passes through the facet.
                #
                # NOTES:
                # 1. If the ray crosses exactly on an edge, this is counted as crossing the facet.
                # 2. If a ray crosses exactly on a vertex, this is also taken into account.

                for loopCHECKFACET in possibleCROSSLIST:
                    # Check if ray crosses the facet. This method is much (>>10 times) faster than using the built-in function 'inpolygon'.
                    # Taking each edge of the facet in turn, check if the ray is on the same side as the opposing vertex. If so, let testVn=1

                    Y1predicted = meshXYZ[loopCHECKFACET, 1, 1] - (
                        (meshXYZ[loopCHECKFACET, 1, 1] - meshXYZ[loopCHECKFACET, 1, 2])
                        * (
                            meshXYZ[loopCHECKFACET, 0, 1]
                            - meshXYZ[loopCHECKFACET, 0, 0]
                        )
                        / (
                            meshXYZ[loopCHECKFACET, 0, 1]
                            - meshXYZ[loopCHECKFACET, 0, 2]
                        )
                    )
                    YRpredicted = meshXYZ[loopCHECKFACET, 1, 1] - (
                        (meshXYZ[loopCHECKFACET, 1, 1] - meshXYZ[loopCHECKFACET, 1, 2])
                        * (meshXYZ[loopCHECKFACET, 0, 1] - testx[loop])
                        / (
                            meshXYZ[loopCHECKFACET, 0, 1]
                            - meshXYZ[loopCHECKFACET, 0, 2]
                        )
                    )

                    if (
                        (
                            Y1predicted > meshXYZ[loopCHECKFACET, 1, 0]
                            and YRpredicted > testy[loop]
                        )
                        or (
                            Y1predicted < meshXYZ[loopCHECKFACET, 1, 0]
                            and YRpredicted < testy[loop]
                        )
                        or (
                            meshXYZ[loopCHECKFACET, 1, 1]
                            - meshXYZ[loopCHECKFACET, 1, 2]
                        )
                        * (meshXYZ[loopCHECKFACET, 0, 1] - testx[loop])
                        == 0
                    ):
                        # testV1 = 1;   # The ray is on the same side of the 2-3 edge as the 1st vertex.
                        pass
                    else:
                        # testV1 = 0;   # The ray is on the opposite side of the 2-3 edge to the 1st vertex.
                        # As the check is for ALL three checks to be true, we can continue here, if only one check fails
                        continue

                    Y2predicted = meshXYZ[loopCHECKFACET, 1, 2] - (
                        (meshXYZ[loopCHECKFACET, 1, 2] - meshXYZ[loopCHECKFACET, 1, 0])
                        * (
                            meshXYZ[loopCHECKFACET, 0, 2]
                            - meshXYZ[loopCHECKFACET, 0, 1]
                        )
                        / (
                            meshXYZ[loopCHECKFACET, 0, 2]
                            - meshXYZ[loopCHECKFACET, 0, 0]
                        )
                    )
                    YRpredicted = meshXYZ[loopCHECKFACET, 1, 2] - (
                        (meshXYZ[loopCHECKFACET, 1, 2] - meshXYZ[loopCHECKFACET, 1, 0])
                        * (meshXYZ[loopCHECKFACET, 0, 2] - testx[loop])
                        / (
                            meshXYZ[loopCHECKFACET, 0, 2]
                            - meshXYZ[loopCHECKFACET, 0, 0]
                        )
                    )

                    if (
                        (
                            Y2predicted > meshXYZ[loopCHECKFACET, 1, 1]
                            and YRpredicted > testy[loop]
                        )
                        or (
                            Y2predicted < meshXYZ[loopCHECKFACET, 1, 1]
                            and YRpredicted < testy[loop]
                        )
                        or (
                            meshXYZ[loopCHECKFACET, 1, 2]
                            - meshXYZ[loopCHECKFACET, 1, 0]
                        )
                        * (meshXYZ[loopCHECKFACET, 0, 2] - testx[loop])
                        == 0
                    ):
                        # testV2 = 1;   # The ray is on the same side of the 3-1 edge as the 2nd vertex.
                        pass
                    else:
                        # testV2 = 0;   # The ray is on the opposite side of the 3-1 edge to the 2nd vertex.
                        # As the check is for ALL three checks to be true, we can continue here, if only one check fails
                        continue

                    Y3predicted = meshXYZ[loopCHECKFACET, 1, 0] - (
                        (meshXYZ[loopCHECKFACET, 1, 0] - meshXYZ[loopCHECKFACET, 1, 1])
                        * (
                            meshXYZ[loopCHECKFACET, 0, 0]
                            - meshXYZ[loopCHECKFACET, 0, 2]
                        )
                        / (
                            meshXYZ[loopCHECKFACET, 0, 0]
                            - meshXYZ[loopCHECKFACET, 0, 1]
                        )
                    )
                    YRpredicted = meshXYZ[loopCHECKFACET, 1, 0] - (
                        (meshXYZ[loopCHECKFACET, 1, 0] - meshXYZ[loopCHECKFACET, 1, 1])
                        * (meshXYZ[loopCHECKFACET, 0, 0] - testx[loop])
                        / (
                            meshXYZ[loopCHECKFACET, 0, 0]
                            - meshXYZ[loopCHECKFACET, 0, 1]
                        )
                    )

                    if (
                        (
                            Y3predicted > meshXYZ[loopCHECKFACET, 1, 2]
                            and YRpredicted > testy[loop]
                        )
                        or (
                            Y3predicted < meshXYZ[loopCHECKFACET, 1, 2]
                            and YRpredicted < testy[loop]
                        )
                        or (
                            meshXYZ[loopCHECKFACET, 1, 0]
                            - meshXYZ[loopCHECKFACET, 1, 1]
                        )
                        * (meshXYZ[loopCHECKFACET, 0, 0] - testx[loop])
                        == 0
                    ):
                        # testV3 = 1;   # The ray is on the same side of the 1-2 edge as the 3rd vertex.
                        pass
                    else:
                        # testV3 = 0;   # The ray is on the opposite side of the 1-2 edge to the 3rd vertex.
                        # As the check is for ALL three checks to be true, we can continue here, if only one check fails
                        continue

                    nf = nf + 1
                    if nf <= len(facetCROSSLIST):
                        facetCROSSLIST[nf - 1] = loopCHECKFACET
                    else:
                        # Expand array if needed
                        facetCROSSLIST = np.concatenate(
                            [facetCROSSLIST, np.zeros(1000, dtype=int)]
                        )
                        facetCROSSLIST[nf - 1] = loopCHECKFACET

                # Use only values ~=0
                facetCROSSLIST = facetCROSSLIST[:nf]

                # - 3 - Find the z coordinate of the locations where the ray crosses each facet:
                gridCOzCROSS = np.zeros(nf)
                for i, loopFINDZ in enumerate(facetCROSSLIST):
                    # METHOD:
                    # 1. Define the equation describing the plane of the facet. For a
                    # more detailed outline of the maths, see:
                    # http://local.wasp.uwa.edu.au/~pbourke/geometry/planeeq/
                    #    Ax + By + Cz + D = 0
                    #    where  A = y1 (z2 - z3) + y2 (z3 - z1) + y3 (z1 - z2)
                    #           B = z1 (x2 - x3) + z2 (x3 - x1) + z3 (x1 - x2)
                    #           C = x1 (y2 - y3) + x2 (y3 - y1) + x3 (y1 - y2)
                    #           D = - x1 (y2 z3 - y3 z2) - x2 (y3 z1 - y1 z3) - x3 (y1 z2 - y2 z1)
                    # 2. For the x and y coordinates of the ray, solve these equations to find the z coordinate in this plane.

                    planecoA = (
                        meshXYZ[loopFINDZ, 1, 0]
                        * (meshXYZ[loopFINDZ, 2, 1] - meshXYZ[loopFINDZ, 2, 2])
                        + meshXYZ[loopFINDZ, 1, 1]
                        * (meshXYZ[loopFINDZ, 2, 2] - meshXYZ[loopFINDZ, 2, 0])
                        + meshXYZ[loopFINDZ, 1, 2]
                        * (meshXYZ[loopFINDZ, 2, 0] - meshXYZ[loopFINDZ, 2, 1])
                    )

                    planecoB = (
                        meshXYZ[loopFINDZ, 2, 0]
                        * (meshXYZ[loopFINDZ, 0, 1] - meshXYZ[loopFINDZ, 0, 2])
                        + meshXYZ[loopFINDZ, 2, 1]
                        * (meshXYZ[loopFINDZ, 0, 2] - meshXYZ[loopFINDZ, 0, 0])
                        + meshXYZ[loopFINDZ, 2, 2]
                        * (meshXYZ[loopFINDZ, 0, 0] - meshXYZ[loopFINDZ, 0, 1])
                    )

                    planecoC = (
                        meshXYZ[loopFINDZ, 0, 0]
                        * (meshXYZ[loopFINDZ, 1, 1] - meshXYZ[loopFINDZ, 1, 2])
                        + meshXYZ[loopFINDZ, 0, 1]
                        * (meshXYZ[loopFINDZ, 1, 2] - meshXYZ[loopFINDZ, 1, 0])
                        + meshXYZ[loopFINDZ, 0, 2]
                        * (meshXYZ[loopFINDZ, 1, 0] - meshXYZ[loopFINDZ, 1, 1])
                    )

                    planecoD = (
                        -meshXYZ[loopFINDZ, 0, 0]
                        * (
                            meshXYZ[loopFINDZ, 1, 1] * meshXYZ[loopFINDZ, 2, 2]
                            - meshXYZ[loopFINDZ, 1, 2] * meshXYZ[loopFINDZ, 2, 1]
                        )
                        - meshXYZ[loopFINDZ, 0, 1]
                        * (
                            meshXYZ[loopFINDZ, 1, 2] * meshXYZ[loopFINDZ, 2, 0]
                            - meshXYZ[loopFINDZ, 1, 0] * meshXYZ[loopFINDZ, 2, 2]
                        )
                        - meshXYZ[loopFINDZ, 0, 2]
                        * (
                            meshXYZ[loopFINDZ, 1, 0] * meshXYZ[loopFINDZ, 2, 1]
                            - meshXYZ[loopFINDZ, 1, 1] * meshXYZ[loopFINDZ, 2, 0]
                        )
                    )

                    if abs(planecoC) < 1e-14:
                        planecoC = 0

                    gridCOzCROSS[i] = (
                        -planecoD - planecoA * testx[loop] - planecoB * testy[loop]
                    ) / planecoC

                if len(gridCOzCROSS) == 0:
                    continue

                # Remove values of gridCOzCROSS which are outside of the mesh limits (including a 1e-12 margin for error).
                gridCOzCROSS = gridCOzCROSS[
                    (gridCOzCROSS >= meshZmin - 1e-12)
                    & (gridCOzCROSS <= meshZmax + 1e-12)
                ]

                # Round gridCOzCROSS to remove any rounding errors, and take only the unique values:
                gridCOzCROSS = np.round(gridCOzCROSS * 1e10) / 1e10

                # Replacement of the call to unique (gridCOzCROSS = unique(gridCOzCROSS);) by the following line:
                tmp = np.sort(gridCOzCROSS)
                if len(tmp) > 1:
                    I = np.concatenate(([True], tmp[1:] - tmp[:-1] != 0))
                    gridCOzCROSS = tmp[I]
                else:
                    gridCOzCROSS = tmp

                # - 4 - Label as being inside the mesh all the voxels that the ray passes through after crossing one facet before crossing another facet:

                if (
                    len(gridCOzCROSS) % 2 == 0
                ):  # Only rays which cross an even number of facets are voxelised
                    for loopASSIGN in range(1, len(gridCOzCROSS) // 2 + 1):
                        voxelsINSIDE = (
                            testz[loop] > gridCOzCROSS[2 * loopASSIGN - 2]
                            and testz[loop] < gridCOzCROSS[2 * loopASSIGN - 1]
                        )
                        OUTPUT[loop] = int(voxelsINSIDE)
                        if voxelsINSIDE:
                            break

                elif (
                    len(gridCOzCROSS) != 0
                ):  # Remaining rays which meet the mesh in some way are not voxelised, but are labelled for correction later.
                    correctionLIST.append(loop)

    # ======================================================
    # RESTORE DIVIDE-BY-ZERO WARNINGS TO THE ORIGINAL STATE
    # ======================================================
    # (handled automatically by context manager)

    # J.Korsawe: A correction is not possible as the testpoints need not to be
    #            ordered in any way.
    #            voxelise contains a correction algorithm which is appended here
    #            without changes in syntax.

    return OUTPUT.flatten().astype(int), np.array(correctionLIST, dtype=int)


def rotmatrix(v: np.ndarray, deg: float) -> np.ndarray:
    """
    Calculate the rotation matrix about v by deg degrees
    """

    deg = deg / 180 * np.pi
    if deg != 0:
        v = v / np.linalg.norm(v)
        v1 = v[0, 0] if v.ndim > 1 else v[0]
        v2 = v[0, 1] if v.ndim > 1 else v[1]
        v3 = v[0, 2] if v.ndim > 1 else v[2]
        ca = np.cos(deg)
        sa = np.sin(deg)
        D = np.array(
            [
                [
                    ca + v1 * v1 * (1 - ca),
                    v1 * v2 * (1 - ca) - v3 * sa,
                    v1 * v3 * (1 - ca) + v2 * sa,
                ],
                [
                    v2 * v1 * (1 - ca) + v3 * sa,
                    ca + v2 * v2 * (1 - ca),
                    v2 * v3 * (1 - ca) - v1 * sa,
                ],
                [
                    v3 * v1 * (1 - ca) - v2 * sa,
                    v3 * v2 * (1 - ca) + v1 * sa,
                    ca + v3 * v3 * (1 - ca),
                ],
            ]
        )
    else:
        D = np.eye(3)

    return D


def brain1020(
    node: np.ndarray,
    face: np.ndarray,
    initpoints: Optional[Union[Dict, np.ndarray]] = None,
    perc1: int = 10,
    perc2: int = 20,
    **kwargs,
) -> Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray], np.ndarray]:
    """
    Compute 10-20-like scalp landmarks with user-specified density on a head mesh

    Author: Qianqian Fang (q.fang at neu.edu)
    Python conversion: Preserves exact algorithm from MATLAB version

    Parameters:
    -----------
    node : ndarray
        Full head mesh node list
    face : ndarray
        Full head mesh element list- a 3-column array defines face list
        for the exterior (scalp) surface; a 4-column array defines the
        tetrahedral mesh of the full head.
    initpoints : dict or ndarray, optional
        One can provide the 3-D coordinates of the below
        5 landmarks: nz, iz, lpa, rpa, cz0 (cz0 is the initial guess of cz)
        initpoints can be a dict with the above landmark names
        as keys, or a 5x3 array defining these points in the above
        mentioned order (one can use the output landmarks as initpoints)
    perc1 : int, optional
        The percentage of geodesic distance towards the rim of
        the landmarks; this is the first number of the 10-20 or 10-10 or
        10-5 systems, in this case, it is 10 (for 10%). Default is 10.
    perc2 : int, optional
        The percentage of geodesic distance towards the center
        of the landmarks; this is the 2nd number of the 10-20 or 10-10 or
        10-5 systems, which are 20, 10, 5, respectively, default is 20
    **kwargs : dict
        Additional options:
        'display' : bool, default=True
            If True, plot landmarks and curves
        'cztol' : float, default=1e-6
            The tolerance for searching cz that bisects saggital and coronal reference curves
        'maxcziter' : int, default=10
            The maximum number of iterations to update cz to bisect both cm and sm curves
        'baseplane' : bool, default=True
            If True, create the reference curves along the primary control points (nz,iz,lpa,rpa)
        'minangle' : float, default=0
            If set to a positive number, this specifies the minimum angle (radian) between
            adjacent segments in the reference curves to avoid sharp turns

    Returns:
    --------
    landmarks : dict
        A dictionary storing all computed landmarks. The keys include two sections:
        1) 'nz','iz','lpa','rpa','cz': individual 3D positions defining
           the 5 principle reference points: nasion (nz), inion (iz),
           left-pre-auricular-point (lpa), right-pre-auricular-point
           (rpa) and vertex (cz) - cz is updated from initpoints to bisect
           the saggital and coronal ref. curves.
        2) landmarks along specific cross-sections, each cross section
           may contain more than 1 position.
    curves : dict
        A dictionary storing all computed cross-section curves. The
        keys are named similarly to landmarks, except that
        landmarks stores the 10-? points, and curves stores the
        detailed cross-sectional curves
    initpoints : ndarray
        A 5x3 array storing the principle reference points in the
        orders of 'nz','iz','lpa','rpa','cz'

    Notes:
    ------
    This function is part of brain2mesh toolbox (http://mcx.space/brain2mesh)
    License: GPL v3 or later, see LICENSE.txt for details

    This function requires a pre-installed Iso2Mesh Toolbox
    Download URL: http://github.com/fangq/iso2mesh
    Website: http://iso2mesh.sf.net
    """

    if node is None or face is None:
        raise ValueError("one must provide a head-mesh to call this function")

    if node.size == 0 or face.size == 0 or face.shape[1] <= 2 or node.shape[1] < 3:
        raise ValueError(
            "input node must have 3 columns, face must have at least 3 columns"
        )

    # Parse user options
    showplot = kwargs.get("display", False)
    baseplane = kwargs.get("baseplane", True)
    tol = kwargs.get("cztol", 1e-6)
    dosimplify = kwargs.get("minangle", 0)
    maxcziter = kwargs.get("maxcziter", 10)

    if isinstance(initpoints, list):
        initpoints = np.array(initpoints)

    # Handle case where initpoints has only 3 landmarks (nz, lpa, rpa)
    if initpoints is not None and (
        (isinstance(initpoints, dict) and "iz" not in initpoints)
        or (isinstance(initpoints, np.ndarray) and initpoints.shape[0] == 3)
    ):
        if isinstance(initpoints, dict):
            nz = np.array(initpoints["nz"]).flatten()
            lpa = np.array(initpoints["lpa"]).flatten()
            rpa = np.array(initpoints["rpa"]).flatten()
        else:
            nz = initpoints[0, :]
            lpa = initpoints[1, :]
            rpa = initpoints[2, :]

        # This assumes nz, lpa, rpa, iz are on the same plane to find iz on the head surface
        pa_mid = np.mean([lpa, rpa], axis=0)
        v0 = pa_mid - nz
        iz, e0 = ray2surf(node, face, nz, v0, ">")

        # To find cz, we assume that the vector from iz nz midpoint to cz is perpendicular
        # to the plane defined by nz, lpa, and rpa.
        iznz_mid = (nz + iz) * 0.5
        v0 = np.cross(nz - rpa, lpa - rpa)
        cz, e0 = ray2surf(node, face, iznz_mid, v0, ">")

        if isinstance(initpoints, dict):
            initpoints["iz"] = iz
            initpoints["cz"] = cz
        else:
            initpoints = np.vstack(
                [
                    initpoints[0:1, :],
                    iz.reshape(1, -1),
                    initpoints[1:3, :],
                    cz.reshape(1, -1),
                ]
            )

    # Convert initpoints input to a 5x3 array
    if isinstance(initpoints, dict):
        landmarks = {
            "nz": np.array(initpoints["nz"]).flatten(),
            "iz": np.array(initpoints["iz"]).flatten(),
            "lpa": np.array(initpoints["lpa"]).flatten(),
            "rpa": np.array(initpoints["rpa"]).flatten(),
            "cz": np.array(initpoints["cz"]).flatten(),
        }
        initpoints = np.vstack(
            [
                landmarks["nz"],
                landmarks["iz"],
                landmarks["lpa"],
                landmarks["rpa"],
                landmarks["cz"],
            ]
        )
    else:
        landmarks = {
            "nz": initpoints[0, :],
            "iz": initpoints[1, :],
            "lpa": initpoints[2, :],
            "rpa": initpoints[3, :],
            "cz": initpoints[4, :],
        }
    # Convert tetrahedral mesh into a surface mesh
    if face.shape[1] >= 4:
        face = volface(face[:, :4])[0]  # Use first 4 columns for tetrahedral faces

    if kwargs.get("clean", 1):
        # Find the bounding box of the top part of the head, and remove all other triangles
        p0 = landmarks.copy()

        v_ni = p0["nz"] - p0["iz"]
        v_lr = p0["lpa"] - p0["rpa"]
        v_cz0 = np.cross(v_ni, v_lr)
        v_cz0 = v_cz0 / np.linalg.norm(v_cz0)
        v_cn = p0["cz"] - p0["nz"]
        d_czlpa = np.dot(p0["cz"] - p0["lpa"], v_cz0)
        d_cznz = np.dot(v_cn, v_cz0)

        if abs(d_czlpa) > abs(d_cznz):  # if lpa is further away from cz than nz
            # Move nz to the same level as lpa, can also add rpa
            p0["nz"] = p0["nz"] - v_cz0 * (abs(d_czlpa) - abs(d_cznz))
            # Move iz to the same level as lpa, can also add rpa
            p0["iz"] = p0["iz"] - v_cz0 * (abs(d_czlpa) - abs(d_cznz))

        v_cz = d_cznz * v_cz0
        bbx0 = p0["nz"] - 0.6 * v_lr - 0.1 * v_cz + 0.1 * v_ni

        # Calculate mesh centroids
        c0 = meshcentroid(node, face)

        # Calculate distance from bounding box plane
        v_cz0_rep = np.tile(
            v_cz0, (face.shape[0], 1)
        )  # repmat(v_cz0, size(face, 1), 1)
        bbx0_rep = np.tile(bbx0, (face.shape[0], 1))  # repmat(bbx0, size(face, 1), 1)
        dz = np.sum(v_cz0_rep * (c0 - bbx0_rep), axis=1)

        # Filter faces - keep only those with dz > 0
        face = face[dz > 0, :]

        del p0, bbx0, c0, dz

    # Remove nodes not located in the surface
    node, face, _ = removeisolatednode(node, face)

    # If initpoints is not sufficient, we need interactive selection
    # For now, we'll require full initpoints or raise an error
    if initpoints is None or initpoints.shape[0] < 5:
        raise ValueError(
            "initpoints must contain 5 landmarks: nz, iz, lpa, rpa, cz. "
            + "Interactive selection not implemented in Python version."
        )

    if showplot:
        print("Initial points:")
        print(initpoints)

    # At this point, initpoints contains {nz, iz, lpa, rpa, cz0}
    # Plot the head mesh
    if showplot:
        hh = plotmesh(
            node, face, alpha=0.5, color="wheat", edgecolor="k", linewidth=0.1
        )
        ax = hh["ax"][0]
        ax.set_xlabel("X")
        ax.set_ylabel("Y")
        ax.set_zlabel("Z")
        ax.set_title("Brain 10-20 System Landmarks")

    lastcz = np.array([1, 1, 1]) * np.inf
    cziter = 0
    curves = {}

    # Find cz that bisects cm and sm curves within a tolerance, using UI 10-10 approach
    while np.linalg.norm(initpoints[4, :] - lastcz) > tol and cziter < maxcziter:
        # Step 1: nz, iz and cz0 to determine saggital reference curve
        nsagg, curveloop, _ = slicesurf(node, face, initpoints[[0, 1, 4], :], full=True)
        nsagg = nsagg[maxloop(curveloop) - 1, :]

        # Step 1.1: get cz1 as the mid-point between iz and nz
        slen, nsagg, _ = polylinelen(
            nsagg, initpoints[0, :], initpoints[1, :], initpoints[4, :]
        )
        if dosimplify:
            nsagg, slen = polylinesimplify(nsagg, dosimplify)
        idx, weight, cz = polylineinterp(slen, np.sum(slen) * 0.5, nsagg)
        initpoints[4, :] = cz[0, :]

        # Step 1.2: lpa, rpa and cz1 to determine coronal reference curve, update cz1
        curves["cm"], curveloop, _ = slicesurf(
            node, face, initpoints[[2, 3, 4], :], full=True
        )
        curves["cm"] = curves["cm"][maxloop(curveloop) - 1, :]

        len_cm, curves["cm"], _ = polylinelen(
            curves["cm"], initpoints[2, :], initpoints[3, :], initpoints[4, :]
        )
        if dosimplify:
            curves["cm"], len_cm = polylinesimplify(curves["cm"], dosimplify)
        idx, weight, coro = polylineinterp(len_cm, np.sum(len_cm) * 0.5, curves["cm"])
        lastcz = initpoints[4, :].copy()
        initpoints[4, :] = coro[0, :]
        cziter += 1
        if showplot:
            print(
                f"cz iteration {cziter} error {np.linalg.norm(initpoints[4, :] - lastcz):.2e}"
            )

    # Set the finalized cz to output
    landmarks["cz"] = initpoints[4, :].copy()

    if showplot:
        print("Finalized points:")
        print(initpoints)

    # Step 2: subdivide saggital (sm) and coronal (cm) ref curves
    perc_range = np.arange(perc1, 100 - perc1 + perc2, perc2) * 0.01
    idx, weight, coro = polylineinterp(
        len_cm, np.sum(len_cm) * perc_range, curves["cm"]
    )
    landmarks["cm"] = coro  # t7, c3, cz, c4, t8

    curves["sm"], curveloop, _ = slicesurf(
        node, face, initpoints[[0, 1, 4], :], full=True
    )
    curves["sm"] = curves["sm"][maxloop(curveloop) - 1, :]

    slen, curves["sm"], _ = polylinelen(
        curves["sm"], initpoints[0, :], initpoints[1, :], initpoints[4, :]
    )
    if dosimplify:
        curves["sm"], slen = polylinesimplify(curves["sm"], dosimplify)
    idx, weight, sagg = polylineinterp(slen, np.sum(slen) * perc_range, curves["sm"])
    landmarks["sm"] = sagg  # fpz, fz, cz, pz, oz

    # Step 3: fpz, t7 and oz to determine left 10% axial reference curve
    landmarks["aal"], curves["aal"], landmarks["apl"], curves["apl"] = slicesurf3(
        node,
        face,
        landmarks["sm"][0, :],
        landmarks["cm"][0, :],
        landmarks["sm"][-1, :],
        perc2 * 2,
        dosimplify,
        maxloop=1,
    )

    # Step 4: fpz, t8 and oz to determine right 10% axial reference curve
    landmarks["aar"], curves["aar"], landmarks["apr"], curves["apr"] = slicesurf3(
        node,
        face,
        landmarks["sm"][0, :],
        landmarks["cm"][-1, :],
        landmarks["sm"][-1, :],
        perc2 * 2,
        dosimplify,
        maxloop=1,
    )

    # Show plots of the landmarks
    if showplot:
        ax.plot(
            curves["sm"][:, 0],
            curves["sm"][:, 1],
            curves["sm"][:, 2],
            "r-",
            linewidth=2,
            label="Sagittal",
        )
        ax.plot(
            curves["cm"][:, 0],
            curves["cm"][:, 1],
            curves["cm"][:, 2],
            "g-",
            linewidth=2,
            label="Coronal",
        )
        ax.plot(
            curves["aal"][:, 0],
            curves["aal"][:, 1],
            curves["aal"][:, 2],
            "k-",
            linewidth=1,
        )
        ax.plot(
            curves["aar"][:, 0],
            curves["aar"][:, 1],
            curves["aar"][:, 2],
            "k-",
            linewidth=1,
        )
        ax.plot(
            curves["apl"][:, 0],
            curves["apl"][:, 1],
            curves["apl"][:, 2],
            "b-",
            linewidth=1,
        )
        ax.plot(
            curves["apr"][:, 0],
            curves["apr"][:, 1],
            curves["apr"][:, 2],
            "b-",
            linewidth=1,
        )

        ax.scatter(
            landmarks["sm"][:, 0],
            landmarks["sm"][:, 1],
            landmarks["sm"][:, 2],
            c="red",
            s=50,
            marker="o",
            label="Sagittal landmarks",
        )
        ax.scatter(
            landmarks["cm"][:, 0],
            landmarks["cm"][:, 1],
            landmarks["cm"][:, 2],
            c="green",
            s=50,
            marker="o",
            label="Coronal landmarks",
        )
        ax.scatter(
            landmarks["aal"][:, 0],
            landmarks["aal"][:, 1],
            landmarks["aal"][:, 2],
            c="black",
            s=50,
            marker="o",
        )
        ax.scatter(
            landmarks["aar"][:, 0],
            landmarks["aar"][:, 1],
            landmarks["aar"][:, 2],
            c="magenta",
            s=50,
            marker="o",
        )
        ax.scatter(
            landmarks["apl"][:, 0],
            landmarks["apl"][:, 1],
            landmarks["apl"][:, 2],
            c="black",
            s=50,
            marker="o",
        )
        ax.scatter(
            landmarks["apr"][:, 0],
            landmarks["apr"][:, 1],
            landmarks["apr"][:, 2],
            c="magenta",
            s=50,
            marker="o",
        )

    # Step 5: computing all anterior coronal cuts, moving away from the medial cut (cm) toward frontal
    idxcz = closestnode(landmarks["sm"], landmarks["cz"])[0]
    skipcount = int(np.floor(10 / perc2))

    for i in range(
        1, landmarks["aal"].shape[0] - skipcount + 1
    ):  # MATLAB: for i = 1:size(landmarks.aal, 1) - skipcount
        step = (
            (perc2 * 25)
            * 0.1
            * (
                1
                + (
                    (perc2 < 20 + perc2 < 10)
                    and i == landmarks["aal"].shape[0] - skipcount
                )
            )
        )
        # Subtract 1 from idxcz when using as index (convert from 1-based to 0-based)
        cal_landmarks, leftpart, car_landmarks, rightpart = slicesurf3(
            node,
            face,
            landmarks["aal"][i - 1, :],
            landmarks["sm"][idxcz - 1 - i, :],
            landmarks["aar"][i - 1, :],
            step,
            dosimplify,
            maxloop=1,
        )

        landmarks[f"cal_{i}"] = cal_landmarks
        landmarks[f"car_{i}"] = car_landmarks

        if showplot:
            ax.plot(leftpart[:, 0], leftpart[:, 1], leftpart[:, 2], "k-", linewidth=1)
            ax.plot(
                rightpart[:, 0], rightpart[:, 1], rightpart[:, 2], "k-", linewidth=1
            )
            ax.scatter(
                cal_landmarks[:, 0],
                cal_landmarks[:, 1],
                cal_landmarks[:, 2],
                c="yellow",
                s=30,
                marker="o",
            )
            ax.scatter(
                car_landmarks[:, 0],
                car_landmarks[:, 1],
                car_landmarks[:, 2],
                c="cyan",
                s=30,
                marker="o",
            )

    # Step 6: computing all posterior coronal cuts, moving away from the medial cut (cm) toward occipital
    for i in range(
        1, landmarks["apl"].shape[0] - skipcount + 1
    ):  # MATLAB: for i = 1:size(landmarks.apl, 1) - skipcount
        step = (
            (perc2 * 25)
            * 0.1
            * (
                1
                + (
                    (perc2 < 20 + perc2 < 10)
                    and i == landmarks["apl"].shape[0] - skipcount
                )
            )
        )
        # Subtract 1 from idxcz when using as index (convert from 1-based to 0-based)
        cpl_landmarks, leftpart, cpr_landmarks, rightpart = slicesurf3(
            node,
            face,
            landmarks["apl"][i - 1, :],
            landmarks["sm"][idxcz - 1 + i, :],
            landmarks["apr"][i - 1, :],
            step,
            dosimplify,
            maxloop=1,
        )

        landmarks[f"cpl_{i}"] = cpl_landmarks
        landmarks[f"cpr_{i}"] = cpr_landmarks

        if showplot:
            ax.plot(leftpart[:, 0], leftpart[:, 1], leftpart[:, 2], "k-", linewidth=1)
            ax.plot(
                rightpart[:, 0], rightpart[:, 1], rightpart[:, 2], "k-", linewidth=1
            )
            ax.scatter(
                cpl_landmarks[:, 0],
                cpl_landmarks[:, 1],
                cpl_landmarks[:, 2],
                c="yellow",
                s=30,
                marker="o",
            )
            ax.scatter(
                cpr_landmarks[:, 0],
                cpr_landmarks[:, 1],
                cpr_landmarks[:, 2],
                c="cyan",
                s=30,
                marker="o",
            )

    # Step 7: create the axial cuts across principle ref. points: left: nz, lpa, iz, right: nz, rpa, iz
    if baseplane and perc2 <= 10:
        (
            landmarks["paal"],
            curves["paal"],
            landmarks["papl"],
            curves["papl"],
        ) = slicesurf3(
            node,
            face,
            landmarks["nz"],
            landmarks["lpa"],
            landmarks["iz"],
            perc2 * 2,
            dosimplify,
            maxloop=1,
        )
        (
            landmarks["paar"],
            curves["paar"],
            landmarks["papr"],
            curves["papr"],
        ) = slicesurf3(
            node,
            face,
            landmarks["nz"],
            landmarks["rpa"],
            landmarks["iz"],
            perc2 * 2,
            dosimplify,
            maxloop=1,
        )

        if showplot:
            ax.plot(
                curves["paal"][:, 0],
                curves["paal"][:, 1],
                curves["paal"][:, 2],
                "k-",
                linewidth=1,
            )
            ax.plot(
                curves["paar"][:, 0],
                curves["paar"][:, 1],
                curves["paar"][:, 2],
                "k-",
                linewidth=1,
            )
            ax.plot(
                curves["papl"][:, 0],
                curves["papl"][:, 1],
                curves["papl"][:, 2],
                "k-",
                linewidth=1,
            )
            ax.plot(
                curves["papr"][:, 0],
                curves["papr"][:, 1],
                curves["papr"][:, 2],
                "k-",
                linewidth=1,
            )

            ax.scatter(
                landmarks["paal"][:, 0],
                landmarks["paal"][:, 1],
                landmarks["paal"][:, 2],
                c="yellow",
                s=30,
                marker="o",
            )
            ax.scatter(
                landmarks["papl"][:, 0],
                landmarks["papl"][:, 1],
                landmarks["papl"][:, 2],
                c="cyan",
                s=30,
                marker="o",
            )
            ax.scatter(
                landmarks["paar"][:, 0],
                landmarks["paar"][:, 1],
                landmarks["paar"][:, 2],
                c="yellow",
                s=30,
                marker="o",
            )
            ax.scatter(
                landmarks["papr"][:, 0],
                landmarks["papr"][:, 1],
                landmarks["papr"][:, 2],
                c="cyan",
                s=30,
                marker="o",
            )

    return landmarks, curves, initpoints


def label2tpm(
    vol: np.ndarray, names: Optional[Union[List[str], Dict[int, str]]] = None, **kwargs
) -> Dict[str, np.ndarray]:
    """
    Converting a multi-label volume to binary tissue probabilistic maps (TPMs)

    Author: Qianqian Fang (q.fang at neu.edu)
    Python conversion: Preserves exact algorithm from MATLAB version

    Parameters:
    -----------
    vol : ndarray
        A 2-D or 3-D array of integer values.
    names : list or dict, optional
        A list of strings defining the names of each label,
        alternatively, a dictionary with label (integer) as
        the key and name as the value. The names can be a subset of all
        labels.

    Returns:
    --------
    seg : dict
        A dictionary with subfields of 3D or 4D uint8 array; the subfield
        names are defined via the optional names input; the unnamed
        labels will be named as 'label_#'.

    Notes:
    ------
    This function is part of brain2mesh toolbox (http://mcx.space/brain2mesh)
    License: GPL v3 or later, see LICENSE.txt for details
    """

    if not np.issubdtype(vol.dtype, np.number):
        raise TypeError("input must be a numerical array")

    # Get unique values excluding 0, equivalent to setdiff(sort(unique(vol(:))), 0)
    vol_flat = vol.flatten(order="F")  # Flatten using Fortran order as requested
    unique_vals = np.unique(vol_flat)
    val = np.setdiff1d(unique_vals, 0)  # Remove 0 from unique values
    val = np.sort(val)  # Ensure sorted order

    # Check if input should be converted to labels first
    if len(val) > vol.size * (0.1**vol.ndim):
        raise ValueError("please convert the input to labels first")

    seg = {}
    for i in range(
        len(val)
    ):  # MATLAB: for i = 1:length(val), Python: for i in range(len(val))
        nm = f"label_{val[i]}"  # sprintf('label_%d', val(i)) equivalent

        if names is not None:
            if isinstance(names, list):
                # MATLAB: if (i <= length(names)), Python: if (i < len(names))
                if i < len(names):
                    nm = names[i]
            elif isinstance(names, dict):
                # MATLAB: elseif (isa(names, 'containers.Map') && isKey(names, val(i)))
                if val[i] in names:
                    nm = names[val[i]]

        # MATLAB: seg.(nm) = uint8(vol == val(i))
        seg[nm] = (vol == val[i]).astype(np.uint8)

    if kwargs.get("sigma", 0) > 0:
        from scipy.ndimage import gaussian_filter

        segsum = np.zeros_like(seg[next(iter(seg))], dtype=np.float32)
        for key in seg:
            seg[key] = gaussian_filter(seg[key].astype(np.float32), sigma=1)
            segsum += seg[key]

        for key in seg:
            np.divide(seg[key], segsum, out=seg[key], where=segsum != 0)

    return seg


def tpm2label(
    seg: Union[Dict[str, np.ndarray], List[np.ndarray], np.ndarray],
    segorder: Optional[List[str]] = None,
) -> Union[np.ndarray, Tuple[np.ndarray, List[str]]]:
    """
    Converting tissue probabilistic maps (TPMs) to a multi-label volume

    Author: Qianqian Fang (q.fang at neu.edu)
    Python conversion: Preserves exact algorithm from MATLAB version

    Parameters:
    -----------
    seg : dict, list, or ndarray
        A dictionary, a list, or a 3D or 4D array; if seg is a list
        or a dictionary, their elements must be 2D/3D arrays of the same
        sizes;
    segorder : list, optional
        If seg is a dictionary, segorder allows one to assign output
        labels using customized order instead of the creation order

    Returns:
    --------
    vol : ndarray
        A 2-D or 3-D array of the same type/size of the input arrays. The
        label for each voxel is determined by the index to the highest
        value in TPM of the same voxel. If a voxel is a background voxel
        - i.e. zeros for all TPMs, it stays 0
    names : list, optional
        A list storing the names of the labels (if input is a
        dictionary), the first string is the name for label 1, and so on
        (only returned if input is a dictionary)

    Notes:
    ------
    This function is part of brain2mesh toolbox (http://mcx.space/brain2mesh)
    License: GPL v3 or later, see LICENSE.txt for details
    """

    mask = seg
    names = []

    # Handle dictionary input (equivalent to isstruct(seg) in MATLAB)
    if isinstance(seg, dict):
        if segorder is not None:
            # Reorder fields according to segorder (equivalent to orderfields(seg, segorder))
            seg = orderfields_python(seg, segorder)

        # Get field names (equivalent to fieldnames(seg))
        names = list(seg.keys())

        # Extract values using cellfun equivalent
        # MATLAB: mask = cellfun(@(x) seg.(x), names, 'UniformOutput', false);
        mask = [seg[x] for x in names]

    # Handle list input (equivalent to iscell(mask) in MATLAB)
    if isinstance(mask, list):
        # Concatenate along new dimension (equivalent to cat(ndims(mask{1}) + 1, mask{:}))
        if len(mask) > 0:
            # Get number of dimensions of first element
            ndims_first = mask[0].ndim
            # Concatenate along the next dimension
            mask = np.stack(mask, axis=ndims_first)
        else:
            mask = np.array([])

    # Check if input is numeric (equivalent to isnumeric(mask) in MATLAB)
    if not np.issubdtype(mask.dtype, np.number):
        raise TypeError(
            "input must be a list/dict array with numeric elements of matching dimensions"
        )

    # Find maximum values and indices along last dimension
    # MATLAB: [newmask, vol] = max(mask, [], ndims(mask));
    last_dim = (
        mask.ndim - 1
    )  # ndims(mask) in MATLAB is mask.ndim in Python, but for axis we need ndim-1
    newmask = np.max(mask, axis=last_dim)
    vol = (
        np.argmax(mask, axis=last_dim) + 1
    )  # +1 to convert from 0-based to 1-based indexing

    # Set background voxels to 0 (equivalent to vol .* (sum(mask, ndims(mask)) > 0))
    background_mask = np.sum(mask, axis=last_dim) > 0
    vol = vol * background_mask.astype(vol.dtype)

    # Return results based on input type
    if isinstance(seg, dict):
        return vol, names
    else:
        return vol
